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
1211import json
13- import sys
1412from http .server import BaseHTTPRequestHandler , HTTPServer
1513from typing import Any , Optional
1614
1715# Standard OAuth-style challenge so the client can drive an OAuth flow
1816WWW_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
2154class 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
119216if __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