Skip to content

Commit 89f443e

Browse files
committed
addressed review comments
1 parent 607063d commit 89f443e

File tree

12 files changed

+259
-165
lines changed

12 files changed

+259
-165
lines changed

Makefile

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

3030

3131
check-types: ## Checks type hints in sources
32-
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/
32+
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/
3333

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

3737
format: ## Format the code into unified format
3838
uv run black .
@@ -82,13 +82,13 @@ black: ## Check source code using Black code formatter
8282
uv run black --check .
8383

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

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

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

9393
ruff: ## Check source code using Ruff linter
9494
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
@@ -302,21 +302,21 @@ MCP (Model Context Protocol) servers provide tools and capabilities to the AI ag
302302

303303
**Basic Configuration Structure:**
304304

305-
Each MCP server requires three fields:
305+
Each MCP server requires two fields:
306306
- `name`: Unique identifier for the MCP server
307-
- `provider_id`: MCP provider identification (typically `"model-context-protocol"`)
308307
- `url`: The endpoint where the MCP server is running
309308

309+
And one optional field:
310+
- `provider_id`: MCP provider identification (defaults to `"model-context-protocol"`)
311+
310312
**Minimal Example:**
311313

312314
```yaml
313315
mcp_servers:
314316
- name: "filesystem-tools"
315-
provider_id: "model-context-protocol"
316-
url: "http://localhost:3000"
317+
url: "http://localhost:9000"
317318
- name: "git-tools"
318-
provider_id: "model-context-protocol"
319-
url: "http://localhost:3001"
319+
url: "http://localhost:9001"
320320
```
321321

322322
In addition to the basic configuration above, you can configure authentication headers for your MCP servers to securely communicate with services that require credentials.
@@ -332,7 +332,6 @@ Store authentication tokens in secret files and reference them in your configura
332332
```yaml
333333
mcp_servers:
334334
- name: "api-service"
335-
provider_id: "model-context-protocol"
336335
url: "http://api-service:8080"
337336
authorization_headers:
338337
Authorization: "/var/secrets/api-token" # Path to file containing token
@@ -356,7 +355,6 @@ Use the special `"kubernetes"` keyword to automatically use the authenticated us
356355
```yaml
357356
mcp_servers:
358357
- name: "k8s-internal-service"
359-
provider_id: "model-context-protocol"
360358
url: "http://internal-mcp.default.svc.cluster.local:8080"
361359
authorization_headers:
362360
Authorization: "kubernetes" # Uses user's k8s token from request auth
@@ -371,7 +369,6 @@ Use the special `"client"` keyword to allow clients to provide custom tokens per
371369
```yaml
372370
mcp_servers:
373371
- name: "user-specific-service"
374-
provider_id: "model-context-protocol"
375372
url: "http://user-service:8080"
376373
authorization_headers:
377374
Authorization: "client" # Token provided via MCP-HEADERS
@@ -387,7 +384,9 @@ curl -X POST "http://localhost:8080/v1/query" \
387384
-d '{"query": "Get my data"}'
388385
```
389386

390-
**Note**: The `MCP-HEADERS` dictionary is keyed by **server name** (not URL), matching the `name` field in your MCP server configuration.
387+
**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.
388+
389+
**Structure**: `MCP-HEADERS: {"<server-name>": {"<header-name>": "<header-value>", ...}, ...}`
391390

392391
##### Combining Authentication Methods
393392

@@ -397,21 +396,18 @@ You can mix and match authentication methods across different MCP servers, and e
397396
mcp_servers:
398397
# Static credentials for public API
399398
- name: "weather-api"
400-
provider_id: "model-context-protocol"
401399
url: "http://weather-api:8080"
402400
authorization_headers:
403401
X-API-Key: "/var/secrets/weather-api-key"
404402
405403
# Kubernetes auth for internal services
406404
- name: "internal-db"
407-
provider_id: "model-context-protocol"
408405
url: "http://db-mcp.cluster.local:8080"
409406
authorization_headers:
410407
Authorization: "kubernetes"
411408
412409
# Mixed: static API key + per-user token
413410
- name: "multi-tenant-service"
414-
provider_id: "model-context-protocol"
415411
url: "http://multi-tenant:8080"
416412
authorization_headers:
417413
X-Service-Key: "/var/secrets/service-key" # Static service credential
@@ -1118,7 +1114,7 @@ python dev-tools/mcp-mock-server/server.py
11181114
# Add to lightspeed-stack.yaml:
11191115
mcp_servers:
11201116
- name: "mock-test"
1121-
url: "http://localhost:3000"
1117+
url: "http://localhost:9000"
11221118
authorization_headers:
11231119
Authorization: "/tmp/test-token"
11241120
```

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: 2 additions & 0 deletions
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)

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)