Skip to content

Commit a15767b

Browse files
committed
📡 feat: redirect / to /mcp or /sse by Accept header & load settings from TOML
- Added root route redirecting to MCP or SSE endpoints based on Accept header (fixed MCP Clients usage). - Centralized pyproject.toml value loading for VERSION, and description. ✅ Local Tests: all 116 tests passed successfully.
1 parent 03d9fde commit a15767b

File tree

6 files changed

+102
-20
lines changed

6 files changed

+102
-20
lines changed

‎config.yaml‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
project_name: MCP Selenium Server
2-
version: 0.1.0
1+
project_name: MCP Selenium Grid
32
deployment_mode: docker # one of: docker, kubernetes (DeploymentMode enum values)
43
api_v1_str: /api/v1
54
api_token: CHANGE_ME

‎pyproject.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
[project]
1515
name = "mcp-selenium-grid"
16-
version = "0.1.0.dev2"
17-
description = "MCP Server for managing Selenium Grid instances"
16+
version = "0.1.0.dev3"
17+
description = "MCP Server for managing Selenium Grid"
1818
readme = "README.md"
1919
license = { file = "LICENSE" }
2020
requires-python = ">=3.12"
@@ -25,7 +25,7 @@ dependencies = [
2525
"fastapi[standard]>=0.115.14", # Web framework
2626
"fastapi-cli[standard-no-fastapi-cloud-cli]>=0.0.8",
2727
"fastapi-mcp>=0.3.4", # MCP integration for FastAPI
28-
"pydantic[email]>=2.11.7", # Data validation, email support
28+
"pydantic>=2.11.7", # Data validation
2929
"pydantic-settings>=2.10.1", # Settings management (latest is 2.2.1)
3030
"docker>=7.1.0", # Docker API client
3131
"kubernetes>=33.1.0", # Kubernetes API client3

‎src/app/common/toml.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from os import getcwd
2+
from pathlib import Path
3+
from tomllib import load
4+
from typing import Any
5+
6+
ROOT_DIR = Path(getcwd()).resolve()
7+
8+
9+
def load_value_from_toml(
10+
keys: list[str], file_path: Path = ROOT_DIR / "pyproject.toml", default: Any = None
11+
) -> Any:
12+
"""
13+
Load a nested value from a TOML file.
14+
15+
Args:
16+
keys: List of nested keys to traverse.
17+
file_path: Path to the TOML file.
18+
default: Value to return if keys not found.
19+
20+
Returns:
21+
The value from the TOML file.
22+
23+
Raises:
24+
FileNotFoundError: If the file doesn't exist and no default is provided.
25+
ValueError: If the keys are missing and no default is provided.
26+
"""
27+
if not file_path.exists():
28+
if default is not None:
29+
return default
30+
raise FileNotFoundError(f"{file_path} not found")
31+
32+
try:
33+
with file_path.open("rb") as f:
34+
data = load(f)
35+
for key in keys:
36+
data = data[key]
37+
return data
38+
except KeyError:
39+
if default is not None:
40+
return default
41+
raise ValueError(f"Keys {'.'.join(keys)} not found in {file_path}")
42+
except Exception as e:
43+
raise ValueError(f"Error reading {file_path}: {e}") from e

‎src/app/core/settings.py‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
"""Core settings for MCP Server."""
22

3-
from pydantic import Field, SecretStr
3+
from pydantic import Field, SecretStr, field_validator
44

5+
from app.common.toml import load_value_from_toml
56
from app.services.selenium_hub.models.general_settings import SeleniumHubGeneralSettings
67

78

89
class Settings(SeleniumHubGeneralSettings):
910
"""MCP Server settings."""
1011

1112
# API Settings
12-
PROJECT_NAME: str = Field(default="MCP Selenium Server")
13-
VERSION: str = Field(default="0.1.0")
14-
API_V1_STR: str = Field(default="/api/v1")
13+
PROJECT_NAME: str = "MCP Selenium Grid"
14+
VERSION: str = ""
15+
16+
@field_validator("VERSION", mode="before")
17+
@classmethod
18+
def load_version_from_pyproject(cls, v: str) -> str:
19+
return v or load_value_from_toml(["project", "version"])
20+
21+
API_V1_STR: str = "/api/v1"
1522

1623
# API Token
17-
API_TOKEN: SecretStr = Field(default=SecretStr("CHANGE_ME"))
24+
API_TOKEN: SecretStr = SecretStr("CHANGE_ME")
1825

1926
# Security Settings
2027
BACKEND_CORS_ORIGINS: list[str] = Field(

‎src/app/main.py‎

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
"""MCP Server for managing Selenium Grid instances."""
1+
"""MCP Server for managing Selenium Grid."""
22

33
import asyncio
44
from contextlib import asynccontextmanager
55
from typing import Any, AsyncGenerator
66

77
from fastapi import Depends, FastAPI, HTTPException, Request, status
88
from fastapi.middleware.cors import CORSMiddleware
9+
from fastapi.responses import JSONResponse, RedirectResponse
910
from fastapi.security import HTTPAuthorizationCredentials
1011
from fastapi_mcp import FastApiMCP
1112
from prometheus_client import generate_latest
1213
from prometheus_fastapi_instrumentator import Instrumentator
1314
from starlette.responses import Response
1415

16+
from app.common.toml import load_value_from_toml
1517
from app.dependencies import get_settings, verify_token
18+
from app.logger import logger
1619
from app.models import HealthCheckResponse, HealthStatus, HubStatusResponse
1720
from app.routers.browsers import router as browsers_router
1821
from app.routers.selenium_proxy import router as selenium_proxy_router
@@ -23,6 +26,7 @@ def create_application() -> FastAPI:
2326
"""Create FastAPI application for MCP."""
2427
# Initialize settings once at the start
2528
settings = get_settings()
29+
DESCRIPTION = load_value_from_toml(["project", "description"])
2630

2731
@asynccontextmanager
2832
async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
@@ -52,14 +56,12 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
5256
yield
5357

5458
# --- Server shutdown: remove Selenium Hub resources (Docker or Kubernetes) ---
55-
# manager = SeleniumHubManager(settings)
56-
# manager.cleanup()
5759
hub.cleanup()
5860

5961
app = FastAPI(
6062
title=settings.PROJECT_NAME,
6163
version=settings.VERSION,
62-
description="MCP Server for managing Selenium Grid instances",
64+
description=DESCRIPTION,
6365
lifespan=lifespan,
6466
)
6567

@@ -77,7 +79,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
7779

7880
# Prometheus metrics endpoint
7981
@app.get("/metrics")
80-
def metrics(credentials: HTTPAuthorizationCredentials = Depends(verify_token)) -> Response:
82+
async def metrics(
83+
credentials: HTTPAuthorizationCredentials = Depends(verify_token),
84+
) -> Response:
8185
return Response(generate_latest(), media_type="text/plain")
8286

8387
# Health check endpoint
@@ -127,8 +131,37 @@ async def get_hub_stats(
127131
app.include_router(selenium_proxy_router)
128132

129133
# --- MCP Integration ---
130-
mcp = FastApiMCP(app)
131-
mcp.mount() # Mounts at /mcp by default
134+
mcp = FastApiMCP(
135+
app,
136+
name="MCP Selenium Grid",
137+
description=DESCRIPTION,
138+
describe_full_response_schema=True,
139+
describe_all_responses=True,
140+
)
141+
MCP_HTTP_PATH = "/mcp"
142+
MCP_SSE_PATH = "/sse"
143+
mcp.mount_http(mount_path=MCP_HTTP_PATH)
144+
mcp.mount_sse(mount_path=MCP_SSE_PATH)
145+
146+
@app.api_route("/", methods=["GET", "POST"], include_in_schema=False)
147+
async def root_redirect(request: Request) -> Response:
148+
accept: str = request.headers.get("accept", "").lower()
149+
method: str = request.method.upper()
150+
151+
logger.info(f"Received {method=} with Accept: {accept}")
152+
153+
if "text/event-stream" in accept:
154+
# MCP allows POST or GET here
155+
logger.info(f"Redirecting to SSE endpoint /sse (method={method})")
156+
return RedirectResponse(url="/sse")
157+
elif "application/json" in accept:
158+
# JSON RPC endpoint (usually POST)
159+
logger.info(f"Redirecting to HTTP JSON RPC endpoint /mcp (method={method})")
160+
return RedirectResponse(url="/mcp")
161+
else:
162+
logger.warning(f"Unsupported Accept header or method: method={method}, accept={accept}")
163+
return JSONResponse({"detail": "Unsupported Accept header or method"}, status_code=405)
164+
132165
# ----------------------
133166

134167
return app

‎uv.lock‎

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

0 commit comments

Comments
 (0)