|
23 | 23 | from http.server import HTTPServer, BaseHTTPRequestHandler |
24 | 24 | from datetime import datetime |
25 | 25 | from pathlib import Path |
26 | | -from typing import Dict |
| 26 | +from typing import Any |
27 | 27 |
|
28 | 28 |
|
29 | 29 | # Global storage for captured headers (last request) |
30 | | -last_headers: Dict[str, str] = {} |
| 30 | +last_headers: dict[str, str] = {} |
31 | 31 | request_log: list = [] |
32 | 32 |
|
33 | 33 |
|
34 | 34 | class MCPMockHandler(BaseHTTPRequestHandler): |
35 | 35 | """HTTP request handler for mock MCP server.""" |
36 | 36 |
|
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 |
39 | 39 | timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
40 | 40 | print(f"[{timestamp}] {format % args}") |
41 | 41 |
|
@@ -79,19 +79,22 @@ def do_POST(self) -> None: # pylint: disable=invalid-name |
79 | 79 |
|
80 | 80 | # Determine tool name based on authorization header to avoid collisions |
81 | 81 | 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" |
95 | 98 |
|
96 | 99 | # Handle MCP protocol methods |
97 | 100 | if method == "initialize": |
@@ -150,51 +153,53 @@ def do_POST(self) -> None: # pylint: disable=invalid-name |
150 | 153 |
|
151 | 154 | def do_GET(self) -> None: # pylint: disable=invalid-name |
152 | 155 | """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()) |
198 | 203 |
|
199 | 204 |
|
200 | 205 | def generate_self_signed_cert(cert_dir: Path) -> tuple[Path, Path]: |
@@ -263,7 +268,7 @@ def run_https_server(port: int, httpd: HTTPServer) -> None: |
263 | 268 | print(f"HTTPS server error: {e}") |
264 | 269 |
|
265 | 270 |
|
266 | | -def main(): |
| 271 | +def main() -> None: |
267 | 272 | """Start the mock MCP server with both HTTP and HTTPS.""" |
268 | 273 | http_port = int(sys.argv[1]) if len(sys.argv) > 1 else 3000 |
269 | 274 | https_port = http_port + 1 |
@@ -294,7 +299,7 @@ def main(): |
294 | 299 | print(" • /debug/headers - View captured headers") |
295 | 300 | print(" • /debug/requests - View request log") |
296 | 301 | print("MCP endpoint:") |
297 | | - print(" • POST /mcp/v1/list_tools") |
| 302 | + print(" • POST to any path (e.g., / or /mcp/v1/list_tools)") |
298 | 303 | print("=" * 70) |
299 | 304 | print("Note: HTTPS uses a self-signed certificate (for testing only)") |
300 | 305 | print("Press Ctrl+C to stop") |
|
0 commit comments