From a78277ae44340ee9e017fd7c9f310649813290fd Mon Sep 17 00:00:00 2001 From: JR Boos Date: Tue, 7 Jul 2026 16:06:30 -0400 Subject: [PATCH] enhance(e2e): expanded mock_mcp_server - added functional tools to mock_mcp - added mock_mcp that does not require auth --- docker-compose-library.yaml | 20 ++++ docker-compose.yaml | 20 ++++ tests/e2e/mock_mcp_server/Dockerfile | 4 +- tests/e2e/mock_mcp_server/server.py | 144 ++++++++++++++++++++++++--- 4 files changed, 170 insertions(+), 18 deletions(-) diff --git a/docker-compose-library.yaml b/docker-compose-library.yaml index 2e3d948ce..cc720d5ee 100755 --- a/docker-compose-library.yaml +++ b/docker-compose-library.yaml @@ -11,6 +11,8 @@ services: depends_on: mock-mcp: condition: service_healthy + mock-mcp-no-auth: + condition: service_healthy networks: - lightspeednet volumes: @@ -90,6 +92,7 @@ services: context: ./tests/e2e/mock_mcp_server dockerfile: Dockerfile container_name: mock-mcp + command: ["--port", "3000"] ports: - "3000:3000" networks: @@ -101,6 +104,23 @@ services: retries: 3 start_period: 2s + mock-mcp-no-auth: + build: + context: ./tests/e2e/mock_mcp_server + dockerfile: Dockerfile + container_name: mock-mcp-no-auth + command: ["--port", "3001", "--no-auth"] + ports: + - "3001:3001" + networks: + - lightspeednet + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3001/health')"] + interval: 5s + timeout: 3s + retries: 3 + start_period: 2s + networks: lightspeednet: diff --git a/docker-compose.yaml b/docker-compose.yaml index aa4631ad6..3bd0b4931 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -106,6 +106,8 @@ services: condition: service_healthy mock-mcp: condition: service_healthy + mock-mcp-no-auth: + condition: service_healthy networks: - lightspeednet healthcheck: @@ -137,6 +139,7 @@ services: context: ./tests/e2e/mock_mcp_server dockerfile: Dockerfile container_name: mock-mcp + command: ["--port", "3000"] ports: - "3000:3000" networks: @@ -148,6 +151,23 @@ services: retries: 3 start_period: 2s + mock-mcp-no-auth: + build: + context: ./tests/e2e/mock_mcp_server + dockerfile: Dockerfile + container_name: mock-mcp-no-auth + command: ["--port", "3001", "--no-auth"] + ports: + - "3001:3001" + networks: + - lightspeednet + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3001/health')"] + interval: 5s + timeout: 3s + retries: 3 + start_period: 2s + # Mock TLS inference server for TLS E2E tests mock-tls-inference: build: diff --git a/tests/e2e/mock_mcp_server/Dockerfile b/tests/e2e/mock_mcp_server/Dockerfile index c7b23d5fc..2ca6a7311 100644 --- a/tests/e2e/mock_mcp_server/Dockerfile +++ b/tests/e2e/mock_mcp_server/Dockerfile @@ -1,5 +1,5 @@ FROM python:3.12-slim WORKDIR /app COPY server.py . -EXPOSE 3000 -CMD ["python", "server.py"] +EXPOSE 3000 3001 +ENTRYPOINT ["python", "server.py"] diff --git a/tests/e2e/mock_mcp_server/server.py b/tests/e2e/mock_mcp_server/server.py index 3fa5a2df8..2abc5f767 100644 --- a/tests/e2e/mock_mcp_server/server.py +++ b/tests/e2e/mock_mcp_server/server.py @@ -1,25 +1,60 @@ #!/usr/bin/env python3 -"""Minimal mock MCP server for E2E tests with OAuth support. +"""Minimal mock MCP server for E2E tests with optional OAuth support. -Responds to GET (OAuth probe) with 401 and WWW-Authenticate. Accepts POST -(MCP JSON-RPC) when Authorization: Bearer is present; otherwise 401. -Uses only Python stdlib. +By default, requires Bearer authentication on every request (except /health). +Pass ``--no-auth`` to disable authentication and serve all requests openly. -Run as ``python server.py [port]``; default port is 3001 (Docker ``mock-mcp``). -OpenShift e2e passes 3000 to match the pod's containerPort. +Run as ``python server.py [--port PORT] [--no-auth]``; default port is 3000. """ +import argparse import json -import sys from http.server import BaseHTTPRequestHandler, HTTPServer from typing import Any, Optional # Standard OAuth-style challenge so the client can drive an OAuth flow WWW_AUTHENTICATE = 'Bearer realm="mock-mcp", error="invalid_token"' +_TWO_NUMBER_SCHEMA: dict = { + "type": "object", + "properties": { + "a": {"type": "number", "description": "First operand"}, + "b": {"type": "number", "description": "Second operand"}, + }, + "required": ["a", "b"], +} + + +def _math_tool_schemas() -> list[dict]: + """Return MCP tool descriptors for the four arithmetic operations.""" + return [ + { + "name": "add", + "description": "Add two numbers", + "inputSchema": _TWO_NUMBER_SCHEMA, + }, + { + "name": "subtract", + "description": "Subtract two numbers (a - b)", + "inputSchema": _TWO_NUMBER_SCHEMA, + }, + { + "name": "multiply", + "description": "Multiply two numbers", + "inputSchema": _TWO_NUMBER_SCHEMA, + }, + { + "name": "divide", + "description": "Divide two numbers (a / b)", + "inputSchema": _TWO_NUMBER_SCHEMA, + }, + ] + class Handler(BaseHTTPRequestHandler): - """HTTP handler: GET/POST without valid Bearer → 401; POST with Bearer → MCP.""" + """HTTP handler for MCP JSON-RPC with optional Bearer auth.""" + + require_auth: bool = True def _require_oauth(self) -> None: """Send 401 with WWW-Authenticate.""" @@ -32,7 +67,13 @@ def _require_oauth(self) -> None: self.wfile.write(body) def _parse_auth(self) -> Optional[str]: - """Return Bearer token if present, else None.""" + """Return Bearer token if present, else None. + + When ``require_auth`` is False, always returns a sentinel value so + every request is treated as authenticated. + """ + if not self.require_auth: + return "no-auth" auth = self.headers.get("Authorization") if auth and auth.startswith("Bearer ") and "invalid" not in auth: return auth[7:].strip() @@ -67,11 +108,10 @@ def do_POST(self) -> None: # pylint: disable=invalid-name raw = self.rfile.read(length) if length else b"{}" try: req = json.loads(raw.decode("utf-8")) - req_id = req.get("id", 1) - method = req.get("method", "") except (json.JSONDecodeError, UnicodeDecodeError): - req_id = 1 - method = "" + req = {} + req_id = req.get("id", 1) + method = req.get("method", "") if method == "initialize": self._json_response( @@ -104,20 +144,92 @@ def do_POST(self) -> None: # pylint: disable=invalid-name } }, }, - } + }, + *_math_tool_schemas(), ], }, } ) + elif method == "tools/call": + self._handle_tool_call(req, req_id) else: self._json_response({"jsonrpc": "2.0", "id": req_id, "result": {}}) + def _handle_tool_call(self, req: dict, req_id: int) -> None: + """Dispatch tools/call requests to the appropriate handler.""" + params = req.get("params", {}) + tool_name = params.get("name", "") + arguments = params.get("arguments", {}) + + ops = { + "add": lambda a, b: a + b, + "subtract": lambda a, b: a - b, + "multiply": lambda a, b: a * b, + "divide": lambda a, b: a / b, + } + + if tool_name in ops: + a = arguments.get("a", 0) + b = arguments.get("b", 0) + if tool_name == "divide" and b == 0: + self._json_response( + { + "jsonrpc": "2.0", + "id": req_id, + "result": { + "content": [ + {"type": "text", "text": "Error: division by zero"} + ], + "isError": True, + }, + } + ) + return + result = ops[tool_name](a, b) + self._json_response( + { + "jsonrpc": "2.0", + "id": req_id, + "result": { + "content": [{"type": "text", "text": str(result)}], + }, + } + ) + else: + self._json_response( + { + "jsonrpc": "2.0", + "id": req_id, + "result": { + "content": [ + {"type": "text", "text": f"Unknown tool: {tool_name}"} + ], + "isError": True, + }, + } + ) + def log_message(self, format: str, *args: Any) -> None: """Suppress request logging for minimal output.""" if __name__ == "__main__": - port = int(sys.argv[1]) if len(sys.argv) > 1 else 3000 + parser = argparse.ArgumentParser(description="Mock MCP server for E2E tests") + parser.add_argument("--port", type=int, default=3000, help="Port to listen on") + parser.add_argument( + "--no-auth", + action="store_true", + default=False, + help="Disable Bearer authentication", + ) + # Legacy positional port for backward compatibility + parser.add_argument("legacy_port", nargs="?", type=int, default=None) + args = parser.parse_args() + + port = args.legacy_port if args.legacy_port is not None else args.port + Handler.require_auth = not args.no_auth + + mode = "open (no auth)" if args.no_auth else "auth required" server = HTTPServer(("0.0.0.0", port), Handler) - print(f"Mock MCP server on :{port}") + print(f"Mock MCP server on :{port} [{mode}]") server.serve_forever()