Skip to content

Commit 1e034a4

Browse files
committed
addressed review comments
1 parent 26f3dde commit 1e034a4

13 files changed

Lines changed: 282 additions & 188 deletions

File tree

Makefile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ test-e2e-local: ## Run end to end tests for the service
3131

3232

3333
check-types: ## Checks type hints in sources
34-
uv run mypy --explicit-package-bases --disallow-untyped-calls --disallow-untyped-defs --disallow-incomplete-defs --ignore-missing-imports --disable-error-code attr-defined src/ tests/unit tests/integration tests/e2e/
34+
uv run mypy --explicit-package-bases --disallow-untyped-calls --disallow-untyped-defs --disallow-incomplete-defs --ignore-missing-imports --disable-error-code attr-defined src/ tests/unit tests/integration tests/e2e/ dev-tools/
3535

3636
security-check: ## Check the project for security issues
37-
bandit -c pyproject.toml -r src tests
37+
uv run bandit -c pyproject.toml -r src tests dev-tools
3838

3939
format: ## Format the code into unified format
4040
uv run black .
@@ -84,13 +84,13 @@ black: ## Check source code using Black code formatter
8484
uv run black --check .
8585

8686
pylint: ## Check source code using Pylint static code analyser
87-
uv run pylint src tests
87+
uv run pylint src tests dev-tools
8888

8989
pyright: ## Check source code using Pyright static type checker
90-
uv run pyright src
90+
uv run pyright src dev-tools
9191

9292
docstyle: ## Check the docstring style using Docstyle checker
93-
uv run pydocstyle -v src
93+
uv run pydocstyle -v src dev-tools
9494

9595
ruff: ## Check source code using Ruff linter
9696
uv run ruff check . --per-file-ignores=tests/*:S101 --per-file-ignores=scripts/*:S101

README.md

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -305,21 +305,21 @@ MCP (Model Context Protocol) servers provide tools and capabilities to the AI ag
305305

306306
**Basic Configuration Structure:**
307307

308-
Each MCP server requires three fields:
308+
Each MCP server requires two fields:
309309
- `name`: Unique identifier for the MCP server
310-
- `provider_id`: MCP provider identification (typically `"model-context-protocol"`)
311310
- `url`: The endpoint where the MCP server is running
312311

312+
And one optional field:
313+
- `provider_id`: MCP provider identification (defaults to `"model-context-protocol"`)
314+
313315
**Minimal Example:**
314316

315317
```yaml
316318
mcp_servers:
317319
- name: "filesystem-tools"
318-
provider_id: "model-context-protocol"
319-
url: "http://localhost:3000"
320+
url: "http://localhost:9000"
320321
- name: "git-tools"
321-
provider_id: "model-context-protocol"
322-
url: "http://localhost:3001"
322+
url: "http://localhost:9001"
323323
```
324324

325325
In addition to the basic configuration above, you can configure authentication headers for your MCP servers to securely communicate with services that require credentials.
@@ -335,7 +335,6 @@ Store authentication tokens in secret files and reference them in your configura
335335
```yaml
336336
mcp_servers:
337337
- name: "api-service"
338-
provider_id: "model-context-protocol"
339338
url: "http://api-service:8080"
340339
authorization_headers:
341340
Authorization: "/var/secrets/api-token" # Path to file containing token
@@ -359,7 +358,6 @@ Use the special `"kubernetes"` keyword to automatically use the authenticated us
359358
```yaml
360359
mcp_servers:
361360
- name: "k8s-internal-service"
362-
provider_id: "model-context-protocol"
363361
url: "http://internal-mcp.default.svc.cluster.local:8080"
364362
authorization_headers:
365363
Authorization: "kubernetes" # Uses user's k8s token from request auth
@@ -374,7 +372,6 @@ Use the special `"client"` keyword to allow clients to provide custom tokens per
374372
```yaml
375373
mcp_servers:
376374
- name: "user-specific-service"
377-
provider_id: "model-context-protocol"
378375
url: "http://user-service:8080"
379376
authorization_headers:
380377
Authorization: "client" # Token provided via MCP-HEADERS
@@ -390,7 +387,9 @@ curl -X POST "http://localhost:8080/v1/query" \
390387
-d '{"query": "Get my data"}'
391388
```
392389

393-
**Note**: The `MCP-HEADERS` dictionary is keyed by **server name** (not URL), matching the `name` field in your MCP server configuration.
390+
**Note**: `MCP-HEADERS` is an **HTTP request header** containing a JSON-encoded dictionary. The dictionary is keyed by **server name** (not URL), matching the `name` field in your MCP server configuration. Each server name maps to another dictionary containing the HTTP headers to forward to that specific MCP server.
391+
392+
**Structure**: `MCP-HEADERS: {"<server-name>": {"<header-name>": "<header-value>", ...}, ...}`
394393

395394
##### Combining Authentication Methods
396395

@@ -400,21 +399,18 @@ You can mix and match authentication methods across different MCP servers, and e
400399
mcp_servers:
401400
# Static credentials for public API
402401
- name: "weather-api"
403-
provider_id: "model-context-protocol"
404402
url: "http://weather-api:8080"
405403
authorization_headers:
406404
X-API-Key: "/var/secrets/weather-api-key"
407405
408406
# Kubernetes auth for internal services
409407
- name: "internal-db"
410-
provider_id: "model-context-protocol"
411408
url: "http://db-mcp.cluster.local:8080"
412409
authorization_headers:
413410
Authorization: "kubernetes"
414411
415412
# Mixed: static API key + per-user token
416413
- name: "multi-tenant-service"
417-
provider_id: "model-context-protocol"
418414
url: "http://multi-tenant:8080"
419415
authorization_headers:
420416
X-Service-Key: "/var/secrets/service-key" # Static service credential
@@ -1125,7 +1121,7 @@ python dev-tools/mcp-mock-server/server.py
11251121
# Add to lightspeed-stack.yaml:
11261122
mcp_servers:
11271123
- name: "mock-test"
1128-
url: "http://localhost:3000"
1124+
url: "http://localhost:9000"
11291125
authorization_headers:
11301126
Authorization: "/tmp/test-token"
11311127
```

dev-tools/MANUAL_TESTING.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,30 @@ curl -X POST http://localhost:8080/v1/streaming_query \
5959
-d '{"query": "Test all MCP auth types"}'
6060
```
6161

62+
<details>
63+
<summary><b>Optional: Using Real Tokens (for production testing)</b></summary>
64+
65+
If you want to test with actual tokens instead of mock values:
66+
67+
```bash
68+
# Extract real Kubernetes token (requires oc or kubectl)
69+
K8S_TOKEN=$(oc whoami -t 2>/dev/null || kubectl get secret -o jsonpath='{.data.token}' | base64 -d)
70+
71+
# Set your client token
72+
CLIENT_TOKEN="your-actual-client-token"
73+
74+
# Make request with real tokens
75+
curl -X POST http://localhost:8080/v1/streaming_query \
76+
-H "Content-Type: application/json" \
77+
-H "Authorization: Bearer ${K8S_TOKEN}" \
78+
-H "MCP-HEADERS: {\"mock-client-auth\": {\"Authorization\": \"Bearer ${CLIENT_TOKEN}\"}}" \
79+
-d '{"query": "Test with real tokens"}'
80+
```
81+
82+
**Note:** The mock MCP server doesn't validate tokens, so the simple example above is sufficient for local testing.
83+
84+
</details>
85+
6286
**What This Tests:**
6387
- **`mock-file-auth`**: Uses static token from `/tmp/lightspeed-mcp-test-token`
6488
- **`mock-k8s-auth`**: Forwards the Kubernetes token from your `Authorization` header
@@ -91,7 +115,7 @@ The mock server should return unique tool names for each auth type:
91115
- `mock_tool_client` - from `mock-client-auth`
92116

93117
Check the Lightspeed Core logs, you should see:
94-
```
118+
```text
95119
DEBUG Configured 3 MCP tools: ['mock-file-auth', 'mock-k8s-auth', 'mock-client-auth']
96120
```
97121

dev-tools/mcp-mock-server/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ This mock server helps developers:
1111
- Develop and test MCP-related features
1212
- Test both HTTP and HTTPS connections
1313

14+
**⚠️ Testing Only:** This server is single-threaded and handles requests sequentially. It is designed purely for development and testing purposes, not for production or high-load scenarios.
15+
1416
## Features
1517

1618
-**Pure Python** - No external dependencies (uses stdlib only)
@@ -34,7 +36,7 @@ python dev-tools/mcp-mock-server/server.py 8080
3436
```
3537

3638
You should see:
37-
```
39+
```text
3840
======================================================================
3941
MCP Mock Server starting with HTTP and HTTPS
4042
======================================================================

dev-tools/mcp-mock-server/server.py

Lines changed: 69 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@
2323
from http.server import HTTPServer, BaseHTTPRequestHandler
2424
from datetime import datetime
2525
from pathlib import Path
26-
from typing import Dict
26+
from typing import Any
2727

2828

2929
# Global storage for captured headers (last request)
30-
last_headers: Dict[str, str] = {}
30+
last_headers: dict[str, str] = {}
3131
request_log: list = []
3232

3333

3434
class MCPMockHandler(BaseHTTPRequestHandler):
3535
"""HTTP request handler for mock MCP server."""
3636

37-
def log_message(self, format, *args) -> None: # pylint: disable=redefined-builtin
38-
"""Log requests with timestamp."""
37+
def log_message(self, format: str, *args: Any) -> None:
38+
"""Log requests with timestamp.""" # pylint: disable=redefined-builtin
3939
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
4040
print(f"[{timestamp}] {format % args}")
4141

@@ -79,19 +79,22 @@ def do_POST(self) -> None: # pylint: disable=invalid-name
7979

8080
# Determine tool name based on authorization header to avoid collisions
8181
auth_header = self.headers.get("Authorization", "")
82-
if "test-secret-token" in auth_header:
83-
tool_name = "mock_tool_file"
84-
tool_desc = "Mock tool with file-based auth"
85-
elif "my-k8s-token" in auth_header:
86-
tool_name = "mock_tool_k8s"
87-
tool_desc = "Mock tool with Kubernetes token"
88-
elif "my-client-token" in auth_header:
89-
tool_name = "mock_tool_client"
90-
tool_desc = "Mock tool with client-provided token"
91-
else:
92-
# No auth header or unrecognized token
93-
tool_name = "mock_tool_no_auth"
94-
tool_desc = "Mock tool with no authorization"
82+
83+
# Match based on token content
84+
match auth_header:
85+
case _ if "test-secret-token" in auth_header:
86+
tool_name = "mock_tool_file"
87+
tool_desc = "Mock tool with file-based auth"
88+
case _ if "my-k8s-token" in auth_header:
89+
tool_name = "mock_tool_k8s"
90+
tool_desc = "Mock tool with Kubernetes token"
91+
case _ if "my-client-token" in auth_header:
92+
tool_name = "mock_tool_client"
93+
tool_desc = "Mock tool with client-provided token"
94+
case _:
95+
# No auth header or unrecognized token
96+
tool_name = "mock_tool_no_auth"
97+
tool_desc = "Mock tool with no authorization"
9598

9699
# Handle MCP protocol methods
97100
if method == "initialize":
@@ -150,51 +153,53 @@ def do_POST(self) -> None: # pylint: disable=invalid-name
150153

151154
def do_GET(self) -> None: # pylint: disable=invalid-name
152155
"""Handle GET requests (debug endpoints)."""
153-
# Debug endpoint to view captured headers
154-
if self.path == "/debug/headers":
155-
self.send_response(200)
156-
self.send_header("Content-Type", "application/json")
157-
self.end_headers()
158-
response = {
159-
"last_headers": last_headers,
160-
"request_count": len(request_log),
161-
}
162-
self.wfile.write(json.dumps(response, indent=2).encode())
163-
164-
# Debug endpoint to view request log
165-
elif self.path == "/debug/requests":
166-
self.send_response(200)
167-
self.send_header("Content-Type", "application/json")
168-
self.end_headers()
169-
self.wfile.write(json.dumps(request_log, indent=2).encode())
170-
171-
# Root endpoint - show help
172-
elif self.path == "/":
173-
self.send_response(200)
174-
self.send_header("Content-Type", "text/html")
175-
self.end_headers()
176-
help_html = """
177-
<html>
178-
<head><title>MCP Mock Server</title></head>
179-
<body>
180-
<h1>MCP Mock Server</h1>
181-
<p>This is a development mock server for testing MCP integrations.</p>
182-
<h2>Debug Endpoints:</h2>
183-
<ul>
184-
<li><a href="/debug/headers">/debug/headers</a> - View last captured headers</li>
185-
<li><a href="/debug/requests">/debug/requests</a> - View recent request log</li>
186-
</ul>
187-
<h2>MCP Endpoints:</h2>
188-
<ul>
189-
<li>POST /mcp/v1/list_tools - Mock MCP tools endpoint</li>
190-
</ul>
191-
</body>
192-
</html>
193-
"""
194-
self.wfile.write(help_html.encode())
195-
else:
196-
self.send_response(404)
197-
self.end_headers()
156+
# Handle different GET endpoints
157+
match self.path:
158+
case "/debug/headers":
159+
self._send_json_response(
160+
{"last_headers": last_headers, "request_count": len(request_log)}
161+
)
162+
case "/debug/requests":
163+
self._send_json_response(request_log)
164+
case "/":
165+
self._send_help_page()
166+
case _:
167+
self.send_response(404)
168+
self.end_headers()
169+
170+
def _send_json_response(self, data: dict | list) -> None:
171+
"""Send a JSON response."""
172+
self.send_response(200)
173+
self.send_header("Content-Type", "application/json")
174+
self.end_headers()
175+
self.wfile.write(json.dumps(data, indent=2).encode())
176+
177+
def _send_help_page(self) -> None:
178+
"""Send HTML help page for root endpoint."""
179+
self.send_response(200)
180+
self.send_header("Content-Type", "text/html")
181+
self.end_headers()
182+
help_html = """<!DOCTYPE html>
183+
<html>
184+
<head><title>MCP Mock Server</title></head>
185+
<body>
186+
<h1>MCP Mock Server</h1>
187+
<p>Development mock server for testing MCP integrations.</p>
188+
<h2>Debug Endpoints:</h2>
189+
<ul>
190+
<li><a href="/debug/headers">/debug/headers</a> - View captured headers</li>
191+
<li><a href="/debug/requests">/debug/requests</a> - View request log</li>
192+
</ul>
193+
<h2>MCP Protocol:</h2>
194+
<p>POST requests to any path with JSON-RPC format:</p>
195+
<ul>
196+
<li><code>{"jsonrpc": "2.0", "id": 1, "method": "initialize"}</code></li>
197+
<li><code>{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}</code></li>
198+
</ul>
199+
</body>
200+
</html>
201+
"""
202+
self.wfile.write(help_html.encode())
198203

199204

200205
def generate_self_signed_cert(cert_dir: Path) -> tuple[Path, Path]:
@@ -263,7 +268,7 @@ def run_https_server(port: int, httpd: HTTPServer) -> None:
263268
print(f"HTTPS server error: {e}")
264269

265270

266-
def main():
271+
def main() -> None:
267272
"""Start the mock MCP server with both HTTP and HTTPS."""
268273
http_port = int(sys.argv[1]) if len(sys.argv) > 1 else 3000
269274
https_port = http_port + 1
@@ -294,7 +299,7 @@ def main():
294299
print(" • /debug/headers - View captured headers")
295300
print(" • /debug/requests - View request log")
296301
print("MCP endpoint:")
297-
print(" • POST /mcp/v1/list_tools")
302+
print(" • POST to any path (e.g., / or /mcp/v1/list_tools)")
298303
print("=" * 70)
299304
print("Note: HTTPS uses a self-signed certificate (for testing only)")
300305
print("Press Ctrl+C to stop")

0 commit comments

Comments
 (0)