Skip to content

Commit a78277a

Browse files
committed
enhance(e2e): expanded mock_mcp_server
- added functional tools to mock_mcp - added mock_mcp that does not require auth
1 parent fb0e2bf commit a78277a

4 files changed

Lines changed: 170 additions & 18 deletions

File tree

docker-compose-library.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ services:
1111
depends_on:
1212
mock-mcp:
1313
condition: service_healthy
14+
mock-mcp-no-auth:
15+
condition: service_healthy
1416
networks:
1517
- lightspeednet
1618
volumes:
@@ -90,6 +92,7 @@ services:
9092
context: ./tests/e2e/mock_mcp_server
9193
dockerfile: Dockerfile
9294
container_name: mock-mcp
95+
command: ["--port", "3000"]
9396
ports:
9497
- "3000:3000"
9598
networks:
@@ -101,6 +104,23 @@ services:
101104
retries: 3
102105
start_period: 2s
103106

107+
mock-mcp-no-auth:
108+
build:
109+
context: ./tests/e2e/mock_mcp_server
110+
dockerfile: Dockerfile
111+
container_name: mock-mcp-no-auth
112+
command: ["--port", "3001", "--no-auth"]
113+
ports:
114+
- "3001:3001"
115+
networks:
116+
- lightspeednet
117+
healthcheck:
118+
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3001/health')"]
119+
interval: 5s
120+
timeout: 3s
121+
retries: 3
122+
start_period: 2s
123+
104124

105125
networks:
106126
lightspeednet:

docker-compose.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ services:
106106
condition: service_healthy
107107
mock-mcp:
108108
condition: service_healthy
109+
mock-mcp-no-auth:
110+
condition: service_healthy
109111
networks:
110112
- lightspeednet
111113
healthcheck:
@@ -137,6 +139,7 @@ services:
137139
context: ./tests/e2e/mock_mcp_server
138140
dockerfile: Dockerfile
139141
container_name: mock-mcp
142+
command: ["--port", "3000"]
140143
ports:
141144
- "3000:3000"
142145
networks:
@@ -148,6 +151,23 @@ services:
148151
retries: 3
149152
start_period: 2s
150153

154+
mock-mcp-no-auth:
155+
build:
156+
context: ./tests/e2e/mock_mcp_server
157+
dockerfile: Dockerfile
158+
container_name: mock-mcp-no-auth
159+
command: ["--port", "3001", "--no-auth"]
160+
ports:
161+
- "3001:3001"
162+
networks:
163+
- lightspeednet
164+
healthcheck:
165+
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3001/health')"]
166+
interval: 5s
167+
timeout: 3s
168+
retries: 3
169+
start_period: 2s
170+
151171
# Mock TLS inference server for TLS E2E tests
152172
mock-tls-inference:
153173
build:
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
FROM python:3.12-slim
22
WORKDIR /app
33
COPY server.py .
4-
EXPOSE 3000
5-
CMD ["python", "server.py"]
4+
EXPOSE 3000 3001
5+
ENTRYPOINT ["python", "server.py"]

tests/e2e/mock_mcp_server/server.py

Lines changed: 128 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,60 @@
11
#!/usr/bin/env python3
2-
"""Minimal mock MCP server for E2E tests with OAuth support.
2+
"""Minimal mock MCP server for E2E tests with optional OAuth support.
33
4-
Responds to GET (OAuth probe) with 401 and WWW-Authenticate. Accepts POST
5-
(MCP JSON-RPC) when Authorization: Bearer <token> is present; otherwise 401.
6-
Uses only Python stdlib.
4+
By default, requires Bearer authentication on every request (except /health).
5+
Pass ``--no-auth`` to disable authentication and serve all requests openly.
76
8-
Run as ``python server.py [port]``; default port is 3001 (Docker ``mock-mcp``).
9-
OpenShift e2e passes 3000 to match the pod's containerPort.
7+
Run as ``python server.py [--port PORT] [--no-auth]``; default port is 3000.
108
"""
119

10+
import argparse
1211
import json
13-
import sys
1412
from http.server import BaseHTTPRequestHandler, HTTPServer
1513
from typing import Any, Optional
1614

1715
# Standard OAuth-style challenge so the client can drive an OAuth flow
1816
WWW_AUTHENTICATE = 'Bearer realm="mock-mcp", error="invalid_token"'
1917

18+
_TWO_NUMBER_SCHEMA: dict = {
19+
"type": "object",
20+
"properties": {
21+
"a": {"type": "number", "description": "First operand"},
22+
"b": {"type": "number", "description": "Second operand"},
23+
},
24+
"required": ["a", "b"],
25+
}
26+
27+
28+
def _math_tool_schemas() -> list[dict]:
29+
"""Return MCP tool descriptors for the four arithmetic operations."""
30+
return [
31+
{
32+
"name": "add",
33+
"description": "Add two numbers",
34+
"inputSchema": _TWO_NUMBER_SCHEMA,
35+
},
36+
{
37+
"name": "subtract",
38+
"description": "Subtract two numbers (a - b)",
39+
"inputSchema": _TWO_NUMBER_SCHEMA,
40+
},
41+
{
42+
"name": "multiply",
43+
"description": "Multiply two numbers",
44+
"inputSchema": _TWO_NUMBER_SCHEMA,
45+
},
46+
{
47+
"name": "divide",
48+
"description": "Divide two numbers (a / b)",
49+
"inputSchema": _TWO_NUMBER_SCHEMA,
50+
},
51+
]
52+
2053

2154
class Handler(BaseHTTPRequestHandler):
22-
"""HTTP handler: GET/POST without valid Bearer → 401; POST with Bearer → MCP."""
55+
"""HTTP handler for MCP JSON-RPC with optional Bearer auth."""
56+
57+
require_auth: bool = True
2358

2459
def _require_oauth(self) -> None:
2560
"""Send 401 with WWW-Authenticate."""
@@ -32,7 +67,13 @@ def _require_oauth(self) -> None:
3267
self.wfile.write(body)
3368

3469
def _parse_auth(self) -> Optional[str]:
35-
"""Return Bearer token if present, else None."""
70+
"""Return Bearer token if present, else None.
71+
72+
When ``require_auth`` is False, always returns a sentinel value so
73+
every request is treated as authenticated.
74+
"""
75+
if not self.require_auth:
76+
return "no-auth"
3677
auth = self.headers.get("Authorization")
3778
if auth and auth.startswith("Bearer ") and "invalid" not in auth:
3879
return auth[7:].strip()
@@ -67,11 +108,10 @@ def do_POST(self) -> None: # pylint: disable=invalid-name
67108
raw = self.rfile.read(length) if length else b"{}"
68109
try:
69110
req = json.loads(raw.decode("utf-8"))
70-
req_id = req.get("id", 1)
71-
method = req.get("method", "")
72111
except (json.JSONDecodeError, UnicodeDecodeError):
73-
req_id = 1
74-
method = ""
112+
req = {}
113+
req_id = req.get("id", 1)
114+
method = req.get("method", "")
75115

76116
if method == "initialize":
77117
self._json_response(
@@ -104,20 +144,92 @@ def do_POST(self) -> None: # pylint: disable=invalid-name
104144
}
105145
},
106146
},
107-
}
147+
},
148+
*_math_tool_schemas(),
108149
],
109150
},
110151
}
111152
)
153+
elif method == "tools/call":
154+
self._handle_tool_call(req, req_id)
112155
else:
113156
self._json_response({"jsonrpc": "2.0", "id": req_id, "result": {}})
114157

158+
def _handle_tool_call(self, req: dict, req_id: int) -> None:
159+
"""Dispatch tools/call requests to the appropriate handler."""
160+
params = req.get("params", {})
161+
tool_name = params.get("name", "")
162+
arguments = params.get("arguments", {})
163+
164+
ops = {
165+
"add": lambda a, b: a + b,
166+
"subtract": lambda a, b: a - b,
167+
"multiply": lambda a, b: a * b,
168+
"divide": lambda a, b: a / b,
169+
}
170+
171+
if tool_name in ops:
172+
a = arguments.get("a", 0)
173+
b = arguments.get("b", 0)
174+
if tool_name == "divide" and b == 0:
175+
self._json_response(
176+
{
177+
"jsonrpc": "2.0",
178+
"id": req_id,
179+
"result": {
180+
"content": [
181+
{"type": "text", "text": "Error: division by zero"}
182+
],
183+
"isError": True,
184+
},
185+
}
186+
)
187+
return
188+
result = ops[tool_name](a, b)
189+
self._json_response(
190+
{
191+
"jsonrpc": "2.0",
192+
"id": req_id,
193+
"result": {
194+
"content": [{"type": "text", "text": str(result)}],
195+
},
196+
}
197+
)
198+
else:
199+
self._json_response(
200+
{
201+
"jsonrpc": "2.0",
202+
"id": req_id,
203+
"result": {
204+
"content": [
205+
{"type": "text", "text": f"Unknown tool: {tool_name}"}
206+
],
207+
"isError": True,
208+
},
209+
}
210+
)
211+
115212
def log_message(self, format: str, *args: Any) -> None:
116213
"""Suppress request logging for minimal output."""
117214

118215

119216
if __name__ == "__main__":
120-
port = int(sys.argv[1]) if len(sys.argv) > 1 else 3000
217+
parser = argparse.ArgumentParser(description="Mock MCP server for E2E tests")
218+
parser.add_argument("--port", type=int, default=3000, help="Port to listen on")
219+
parser.add_argument(
220+
"--no-auth",
221+
action="store_true",
222+
default=False,
223+
help="Disable Bearer authentication",
224+
)
225+
# Legacy positional port for backward compatibility
226+
parser.add_argument("legacy_port", nargs="?", type=int, default=None)
227+
args = parser.parse_args()
228+
229+
port = args.legacy_port if args.legacy_port is not None else args.port
230+
Handler.require_auth = not args.no_auth
231+
232+
mode = "open (no auth)" if args.no_auth else "auth required"
121233
server = HTTPServer(("0.0.0.0", port), Handler)
122-
print(f"Mock MCP server on :{port}")
234+
print(f"Mock MCP server on :{port} [{mode}]")
123235
server.serve_forever()

0 commit comments

Comments
 (0)