Skip to content

Commit d9b9b71

Browse files
committed
🔓 feat: simplify local usage with SERVER_AUTH_DISABLED mode — all tests pass
1 parent 86a0fd6 commit d9b9b71

File tree

9 files changed

+167
-21
lines changed

9 files changed

+167
-21
lines changed

README.md

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ For Docker-based deployment, ensure Docker is running and use the Docker configu
4949
"--port", "8000"
5050
],
5151
"env": {
52-
"API_TOKEN": "CHANGE_ME",
52+
"API_TOKEN": "SERVER_AUTH_DISABLED",
5353
"ALLOWED_ORIGINS": "[\"http://127.0.0.1:8000\"]",
5454
"DEPLOYMENT_MODE": "docker",
5555
"SELENIUM_GRID__USERNAME": "USER",
@@ -136,7 +136,7 @@ uvx mcp-selenium-grid helm uninstall --delete-namespace
136136
"--port", "8000"
137137
],
138138
"env": {
139-
"API_TOKEN": "CHANGE_ME",
139+
"API_TOKEN": "SERVER_AUTH_DISABLED",
140140
"ALLOWED_ORIGINS": "[\"http://127.0.0.1:8000\"]",
141141
"DEPLOYMENT_MODE": "kubernetes",
142142
"SELENIUM_GRID__USERNAME": "USER",
@@ -166,7 +166,7 @@ uvx mcp-selenium-grid helm uninstall --delete-namespace
166166
"--rm",
167167
"--init",
168168
"-p", "8000:80",
169-
"-e", "API_TOKEN=CHANGE_ME",
169+
"-e", "API_TOKEN=SERVER_AUTH_DISABLED",
170170
"-e", "ALLOWED_ORIGINS=[\"http://127.0.0.1:8000\"]",
171171
"-e", "DEPLOYMENT_MODE=kubernetes", // required for docker
172172
"-e", "SELENIUM_GRID__USERNAME=USER",
@@ -189,6 +189,91 @@ uvx mcp-selenium-grid helm uninstall --delete-namespace
189189

190190
> The server will be available at `http://localhost:8000` with interactive API documentation at `http://localhost:8000/docs`.
191191
192+
### Server with auth enabled
193+
194+
#### UVX
195+
196+
Using default args
197+
198+
```bash
199+
uvx mcp-selenium-grid server run
200+
```
201+
202+
Custom args
203+
204+
```bash
205+
API_TOKEN=CHANGE_ME uvx mcp-selenium-grid server run --host 127.0.0.1 --port 8000
206+
```
207+
208+
#### Docker
209+
210+
Default args
211+
212+
```bash
213+
docker run -i --rm --init -p 8000:80 ghcr.io/falamarcao/mcp-selenium-grid:latest
214+
```
215+
216+
Custom args
217+
218+
```bash
219+
docker run -i --rm --init -p 8000:80 \
220+
-e API_TOKEN=CHANGE_ME \
221+
-e ALLOWED_ORIGINS='["http://127.0.0.1:8000"]' \
222+
-e DEPLOYMENT_MODE=kubernetes \
223+
-e SELENIUM_GRID__USERNAME=USER \
224+
-e SELENIUM_GRID__PASSWORD=CHANGE_ME \
225+
-e SELENIUM_GRID__VNC_PASSWORD=CHANGE_ME \
226+
-e SELENIUM_GRID__VNC_VIEW_ONLY=false \
227+
-e SELENIUM_GRID__MAX_BROWSER_INSTANCES=4 \
228+
-e SELENIUM_GRID__SE_NODE_MAX_SESSIONS=1 \
229+
-e KUBERNETES__KUBECONFIG=/kube/config-local-k3s \
230+
-e KUBERNETES__CONTEXT=k3s-selenium-grid \
231+
-e KUBERNETES__NAMESPACE=selenium-grid-dev \
232+
-e KUBERNETES__SELENIUM_GRID_SERVICE_NAME=selenium-grid \
233+
ghcr.io/falamarcao/mcp-selenium-grid:latest
234+
```
235+
236+
#### MCP Server configuration (mcp.json)
237+
238+
```json
239+
{
240+
"mcpServers": {
241+
"mcp-selenium-grid": {
242+
"url": "http://localhost:8000",
243+
"headers": {
244+
"Authorization": "Bearer CHANGE_ME"
245+
}
246+
}
247+
}
248+
}
249+
```
250+
251+
```json
252+
{
253+
"mcpServers": {
254+
"mcp-selenium-grid": {
255+
"url": "http://localhost:8000/mcp",
256+
"headers": {
257+
"Authorization": "Bearer CHANGE_ME"
258+
}
259+
}
260+
}
261+
}
262+
```
263+
264+
```json
265+
{
266+
"mcpServers": {
267+
"mcp-selenium-grid": {
268+
"url": "http://localhost:8000/sse",
269+
"headers": {
270+
"Authorization": "Bearer CHANGE_ME"
271+
}
272+
}
273+
}
274+
}
275+
```
276+
192277
## 🤝 Contributing
193278

194279
For development setup, testing, and contribution guidelines, please see [CONTRIBUTING.md](CONTRIBUTING.md).

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
[project]
1515
name = "mcp-selenium-grid"
16-
version = "0.1.0.dev6"
16+
version = "0.1.0.dev7"
1717
description = "MCP Server for managing Selenium Grid"
1818
readme = "README.md"
1919
license = { file = "LICENSE" }

src/app/dependencies.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
HTTPBearer,
1212
)
1313

14+
from app.common.logger import logger
1415
from app.core.settings import Settings
1516

1617

@@ -21,7 +22,7 @@ def get_settings() -> Settings:
2122

2223

2324
# HTTP Bearer token setup
24-
security = HTTPBearer()
25+
security = HTTPBearer(auto_error=False)
2526
basic_auth_scheme = HTTPBasic(auto_error=True)
2627

2728

@@ -46,6 +47,20 @@ async def verify_token(
4647
HTTPException: 401 if the token is invalid or missing.
4748
"""
4849

50+
# If API_TOKEN is empty, skip auth (allow access)
51+
if settings.API_TOKEN.get_secret_value() == "SERVER_AUTH_DISABLED":
52+
logger.critical(
53+
"API_TOKEN is disabled — skipping token verification, access granted as anonymous".upper()
54+
)
55+
return {"sub": "anonymous"}
56+
57+
# Check if header exists (auto_error=False)
58+
if not credentials:
59+
raise HTTPException(
60+
status_code=status.HTTP_403_FORBIDDEN,
61+
detail="Not authenticated",
62+
)
63+
4964
if not compare_digest(settings.API_TOKEN.get_secret_value(), credentials.credentials):
5065
raise HTTPException(
5166
status_code=status.HTTP_401_UNAUTHORIZED,

src/app/main.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from app.routers.selenium_proxy import router as selenium_proxy_router
2222
from app.services.selenium_hub import SeleniumHub
2323

24-
SETTINGS = get_settings()
24+
settings = get_settings()
2525
MCP_HTTP_PATH = "/mcp"
2626
MCP_SSE_PATH = "/sse"
2727

@@ -37,7 +37,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
3737
app.state.browsers_instances_lock = asyncio.Lock()
3838

3939
# Initialize Selenium Hub singleton
40-
hub = SeleniumHub(SETTINGS) # This will create or return the singleton instance
40+
hub = SeleniumHub(settings) # This will create or return the singleton instance
4141

4242
# Ensure hub is running and healthy before starting the application
4343
try:
@@ -61,19 +61,19 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
6161
hub.cleanup()
6262

6363
app = FastAPI(
64-
title=SETTINGS.PROJECT_NAME,
65-
version=SETTINGS.VERSION,
66-
description=SETTINGS.DESCRIPTION,
64+
title=settings.PROJECT_NAME,
65+
version=settings.VERSION,
66+
description=settings.DESCRIPTION,
6767
lifespan=lifespan,
6868
)
6969

7070
Instrumentator().instrument(app)
7171

7272
# CORS middleware
73-
if SETTINGS.BACKEND_CORS_ORIGINS:
73+
if settings.BACKEND_CORS_ORIGINS:
7474
app.add_middleware(
7575
CORSMiddleware,
76-
allow_origins=[str(origin) for origin in SETTINGS.BACKEND_CORS_ORIGINS],
76+
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
7777
allow_credentials=True,
7878
allow_methods=["*"],
7979
allow_headers=["*"],
@@ -96,7 +96,7 @@ async def health_check(
9696
is_healthy = await hub.check_hub_health()
9797
return HealthCheckResponse(
9898
status=HealthStatus.HEALTHY if is_healthy else HealthStatus.UNHEALTHY,
99-
deployment_mode=SETTINGS.DEPLOYMENT_MODE,
99+
deployment_mode=settings.DEPLOYMENT_MODE,
100100
)
101101

102102
# Stats endpoint
@@ -120,22 +120,22 @@ async def get_hub_stats(
120120
return HubStatusResponse(
121121
hub_running=is_running,
122122
hub_healthy=is_healthy,
123-
deployment_mode=SETTINGS.DEPLOYMENT_MODE,
124-
max_instances=SETTINGS.selenium_grid.MAX_BROWSER_INSTANCES,
123+
deployment_mode=settings.DEPLOYMENT_MODE,
124+
max_instances=settings.selenium_grid.MAX_BROWSER_INSTANCES,
125125
browsers=app_state.browsers_instances,
126126
webdriver_remote_url=hub.WEBDRIVER_REMOTE_URL,
127127
)
128128

129129
# Include browser management endpoints
130-
app.include_router(browsers_router, prefix=SETTINGS.API_V1_STR)
130+
app.include_router(browsers_router, prefix=settings.API_V1_STR)
131131
# Include Selenium Hub proxy endpoints
132132
app.include_router(selenium_proxy_router)
133133

134134
# --- MCP Integration ---
135135
mcp = FastApiMCP(
136136
app,
137-
name=SETTINGS.PROJECT_NAME,
138-
description=SETTINGS.DESCRIPTION,
137+
name=settings.PROJECT_NAME,
138+
description=settings.DESCRIPTION,
139139
describe_full_response_schema=True,
140140
describe_all_responses=True,
141141
auth_config=AuthConfig(

src/tests/conftest.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,31 @@ def client(request: FixtureRequest) -> Generator[TestClient, None, None]:
285285

286286
with TestClient(app) as test_client:
287287
yield test_client
288-
app.dependency_overrides = {}
288+
289+
app.dependency_overrides.clear()
290+
291+
292+
# Client fixture with API_TOKEN patched to empty string
293+
@pytest.fixture(scope="function", params=[DeploymentMode.DOCKER, DeploymentMode.KUBERNETES])
294+
def client_disabled_auth(request: FixtureRequest) -> Generator[TestClient, None, None]:
295+
from app.main import create_application # noqa: PLC0415
296+
from fastapi.testclient import TestClient # noqa: PLC0415
297+
298+
app = create_application()
299+
300+
# Override settings based on deployment mode
301+
settings = get_settings()
302+
settings.DEPLOYMENT_MODE = request.param
303+
304+
# Disable Auth
305+
settings.API_TOKEN = SecretStr("SERVER_AUTH_DISABLED")
306+
307+
app.dependency_overrides[get_settings] = lambda: settings
308+
309+
with TestClient(app) as test_client:
310+
yield test_client
311+
312+
app.dependency_overrides.clear()
289313

290314

291315
def reset_selenium_hub_singleton() -> None:

src/tests/integration/test_health_and_stats.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from app.models import HealthStatus
55
from fastapi import status
66
from fastapi.testclient import TestClient
7-
from pytest import FixtureRequest
7+
from pytest import FixtureRequest, MonkeyPatch
88

99

1010
@pytest.mark.integration
@@ -34,6 +34,13 @@ def test_health_check_requires_auth(client: TestClient) -> None:
3434
assert response.status_code == status.HTTP_403_FORBIDDEN
3535

3636

37+
@pytest.mark.integration
38+
def test_disabled_auth(client_disabled_auth: TestClient, monkeypatch: MonkeyPatch) -> None:
39+
"""Test health check endpoint without authentication when API_TOKEN is empty."""
40+
response = client_disabled_auth.get("/health")
41+
assert response.status_code == status.HTTP_200_OK
42+
43+
3744
@pytest.mark.integration
3845
def test_hub_stats_endpoint(
3946
client: TestClient, auth_headers: dict[str, str], request: FixtureRequest

src/tests/unit/test_dependencies.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ def get_settings_override_missing() -> Settings:
3636
async def verify_token_override(
3737
credentials: HTTPAuthorizationCredentials = Depends(security),
3838
) -> dict[str, str]:
39+
# Check if header exists (auto_error=False)
40+
if not credentials:
41+
raise HTTPException(
42+
status_code=status.HTTP_403_FORBIDDEN,
43+
detail="Not authenticated",
44+
)
45+
3946
if credentials.credentials != "valid_token":
4047
raise HTTPException(
4148
status_code=status.HTTP_401_UNAUTHORIZED,

src/tests/unit/test_settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ def test_deployment_mode_override_by_env(monkeypatch: pytest.MonkeyPatch) -> Non
4040
monkeypatch.delenv("DEPLOYMENT_MODE", raising=False)
4141

4242

43+
@pytest.mark.unit
44+
def test_api_token_override_by_env(monkeypatch: pytest.MonkeyPatch) -> None:
45+
monkeypatch.setenv("API_TOKEN", "")
46+
settings = Settings()
47+
assert settings.API_TOKEN.get_secret_value() == ""
48+
monkeypatch.delenv("API_TOKEN", raising=False)
49+
50+
4351
# --- YAML Loading and Special Behaviors ---
4452
@pytest.mark.unit
4553
def test_settings_loads_from_yaml(tmp_path: Path) -> None:

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)