Skip to content

Commit 81e7f59

Browse files
committed
fix(smithery): add HTTP mode support and enhance configuration schema
1 parent cb85154 commit 81e7f59

File tree

6 files changed

+189
-25
lines changed

6 files changed

+189
-25
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ RUN --mount=type=cache,target=/root/.cache/uv \
2424
RUN adduser -D -u 1000 mcpuser && chown -R mcpuser:mcpuser /app
2525
USER mcpuser
2626

27-
# Default command
28-
CMD ["uv", "run", "python", "main.py", "--no-setup"]
27+
# Default command - detect if HTTP mode is needed
28+
CMD ["sh", "-c", "if [ \"$HTTP_MODE\" = \"true\" ]; then uv run python http_main.py; else uv run python main.py --no-setup; fi"]

http_main.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env python3
2+
"""
3+
HTTP entry point for Smithery deployment.
4+
5+
This module provides the HTTP server entry point for running the LinkedIn MCP server
6+
in Smithery's container environment.
7+
"""
8+
9+
import os
10+
import logging
11+
import uvicorn
12+
from linkedin_mcp_server.http_server import create_http_server
13+
14+
15+
def main():
16+
"""Main entry point for HTTP server."""
17+
# Get port from environment variable (required by Smithery)
18+
port = int(os.environ.get("PORT", 8000))
19+
host = os.environ.get("HOST", "0.0.0.0")
20+
21+
# Configure logging
22+
log_level = os.environ.get("LOG_LEVEL", "info").lower()
23+
logging.basicConfig(
24+
level=getattr(logging, log_level.upper()),
25+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
26+
)
27+
28+
print("🔗 LinkedIn MCP Server HTTP Mode 🔗")
29+
print("=" * 40)
30+
print(f"🚀 Starting HTTP server on {host}:{port}")
31+
print("📡 MCP endpoint available at /mcp")
32+
print("💓 Health check available at /health")
33+
34+
# Create and run the HTTP server
35+
app = create_http_server()
36+
37+
uvicorn.run(
38+
app,
39+
host=host,
40+
port=port,
41+
log_level=log_level,
42+
access_log=True,
43+
)
44+
45+
46+
if __name__ == "__main__":
47+
main()

linkedin_mcp_server/http_server.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""
2+
HTTP server for Smithery deployment compatibility.
3+
4+
This module provides the HTTP interface required by Smithery for MCP server deployment.
5+
"""
6+
7+
import os
8+
import logging
9+
import urllib.parse
10+
from typing import Dict, Any
11+
from linkedin_mcp_server.server import create_mcp_server
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
def parse_config_from_query(query_string: str) -> Dict[str, Any]:
17+
"""Parse configuration from query parameters using dot-notation."""
18+
config = {}
19+
params = urllib.parse.parse_qs(query_string)
20+
21+
for key, values in params.items():
22+
if not values:
23+
continue
24+
25+
value = values[0] # Take first value
26+
27+
# Handle dot notation (e.g., "server.host" -> {"server": {"host": "value"}})
28+
keys = key.split(".")
29+
current = config
30+
31+
for k in keys[:-1]:
32+
if k not in current:
33+
current[k] = {}
34+
current = current[k]
35+
36+
current[keys[-1]] = value
37+
38+
return config
39+
40+
41+
def apply_config_to_environment(config: Dict[str, Any]) -> None:
42+
"""Apply configuration to environment variables."""
43+
if "linkedin" in config:
44+
linkedin_config = config["linkedin"]
45+
if "email" in linkedin_config:
46+
os.environ["LINKEDIN_EMAIL"] = linkedin_config["email"]
47+
if "password" in linkedin_config:
48+
os.environ["LINKEDIN_PASSWORD"] = linkedin_config["password"]
49+
50+
if "chromedriver" in config:
51+
chromedriver_config = config["chromedriver"]
52+
if "path" in chromedriver_config:
53+
os.environ["CHROMEDRIVER_PATH"] = chromedriver_config["path"]
54+
if "headless" in chromedriver_config:
55+
os.environ["CHROME_HEADLESS"] = str(chromedriver_config["headless"]).lower()
56+
57+
if "server" in config:
58+
server_config = config["server"]
59+
if "debug" in server_config:
60+
os.environ["SERVER_DEBUG"] = str(server_config["debug"]).lower()
61+
if "lazy_init" in server_config:
62+
os.environ["SERVER_LAZY_INIT"] = str(server_config["lazy_init"]).lower()
63+
64+
65+
def create_http_server():
66+
"""Create and configure the HTTP server for Smithery deployment."""
67+
# Create the FastMCP server
68+
mcp = create_mcp_server()
69+
70+
# Create the HTTP app using FastMCP's SSE support (compatible with HTTP)
71+
app = mcp.sse_app()
72+
73+
# Add a hook to handle query parameters for configuration
74+
@app.middleware("http")
75+
async def config_middleware(request, call_next):
76+
# Parse configuration from query parameters
77+
if request.url.query:
78+
config = parse_config_from_query(request.url.query)
79+
apply_config_to_environment(config)
80+
81+
response = await call_next(request)
82+
return response
83+
84+
return app

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies = [
1111
"linkedin-scraper",
1212
"mcp[cli]>=1.6.0",
1313
"pyperclip>=1.9.0",
14+
"uvicorn>=0.24.0",
1415
]
1516

1617
[tool.setuptools.package-data]

smithery.yaml

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,54 @@
1+
runtime: "container"
2+
build:
3+
dockerfile: "Dockerfile"
4+
dockerBuildPath: "."
15
startCommand:
2-
type: stdio
3-
configSchema:
4-
type: object
5-
required:
6-
- linkedinEmail
7-
- linkedinPassword
8-
properties:
9-
linkedinEmail:
10-
type: string
11-
description: "LinkedIn email address for authentication"
12-
linkedinPassword:
13-
type: string
14-
description: "LinkedIn password for authentication"
15-
commandFunction: |
16-
(config) => ({
17-
command: 'docker',
18-
args: [
19-
'run', '-i', '--rm',
20-
'-e', `LINKEDIN_EMAIL=${config.linkedinEmail}`,
21-
'-e', `LINKEDIN_PASSWORD=${config.linkedinPassword}`,
22-
'stickerdaniel/linkedin-mcp-server'
23-
]
24-
})
6+
type: "http"
7+
configSchema:
8+
type: "object"
9+
properties:
10+
linkedin:
11+
type: "object"
12+
properties:
13+
email:
14+
type: "string"
15+
description: "Your LinkedIn email address"
16+
password:
17+
type: "string"
18+
description: "Your LinkedIn password"
19+
format: "password"
20+
required: ["email", "password"]
21+
description: "LinkedIn account credentials for authentication"
22+
chromedriver:
23+
type: "object"
24+
properties:
25+
path:
26+
type: "string"
27+
description: "Path to ChromeDriver executable (optional, auto-detected in container)"
28+
headless:
29+
type: "boolean"
30+
description: "Run browser in headless mode"
31+
default: true
32+
description: "ChromeDriver configuration options"
33+
server:
34+
type: "object"
35+
properties:
36+
debug:
37+
type: "boolean"
38+
description: "Enable debug logging"
39+
default: false
40+
lazy_init:
41+
type: "boolean"
42+
description: "Initialize browser session lazily on first tool use"
43+
default: true
44+
description: "Server configuration options"
45+
required: ["linkedin"]
46+
exampleConfig:
47+
linkedin:
48+
email: "your-email@example.com"
49+
password: "your-password"
50+
chromedriver:
51+
headless: true
52+
server:
53+
debug: false
54+
lazy_init: true

uv.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)