3fcfaee02d
CI / helm (push) Successful in 38s
CI / build (push) Successful in 1m3s
CI / push-validation (push) Successful in 30s
CI / lint (push) Successful in 1m22s
CI / quality (push) Successful in 1m31s
CI / security (push) Successful in 1m46s
CI / typecheck (push) Successful in 1m53s
CI / integration_tests (push) Successful in 3m41s
CI / e2e_tests (push) Failing after 4m5s
CI / unit_tests (push) Successful in 4m58s
CI / docker (push) Successful in 1m30s
CI / benchmark-regression (push) Failing after 45s
CI / coverage (push) Successful in 11m32s
CI / status-check (push) Successful in 5s
CI / benchmark-publish (push) Successful in 1h20m26s
Implements concrete StdioMCPTransport class that: - Spawns MCP server as subprocess and communicates via JSON-RPC 2.0 over stdio - Performs MCP handshake (initialize + notifications/initialized) - Supports tools/list and tools/call methods - Uses RLock for thread-safe concurrent access - Auto-selected when transport='stdio' in MCPServerConfig Adds BDD tests for stdio transport covering: - Connection lifecycle and error handling - Tool discovery and invocation - MCPToolAdapter integration ISSUES CLOSED: #4918
182 lines
4.8 KiB
Python
Executable File
182 lines
4.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""MCP stub server for integration testing of StdioMCPTransport.
|
|
|
|
Implements the MCP JSON-RPC 2.0 stdio protocol for deterministic testing.
|
|
This server speaks MCP 2024-11-05 protocol version.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import threading
|
|
import time
|
|
from typing import Any
|
|
|
|
MCP_PROTOCOL_VERSION = "2024-11-05"
|
|
|
|
TOOLS = [
|
|
{
|
|
"name": "test/echo",
|
|
"description": "Echoes back the input arguments",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"message": {"type": "string", "description": "Message to echo"},
|
|
},
|
|
"required": ["message"],
|
|
},
|
|
},
|
|
{
|
|
"name": "test/multiply",
|
|
"description": "Multiplies two numbers",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"a": {"type": "number", "description": "First number"},
|
|
"b": {"type": "number", "description": "Second number"},
|
|
},
|
|
"required": ["a", "b"],
|
|
},
|
|
},
|
|
{
|
|
"name": "test/slow",
|
|
"description": "A slow tool that takes time to respond",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"delay": {"type": "number", "description": "Delay in seconds"},
|
|
},
|
|
},
|
|
},
|
|
]
|
|
|
|
request_id = 0
|
|
capabilities: dict[str, Any] = {}
|
|
initialized = False
|
|
slow_tool_delay = 0.0
|
|
lock = threading.Lock()
|
|
|
|
|
|
def send_response(resp_id: int | str | None, result: Any) -> None:
|
|
response = {
|
|
"jsonrpc": "2.0",
|
|
"id": resp_id,
|
|
"result": result,
|
|
}
|
|
sys.stdout.write(json.dumps(response) + "\n")
|
|
sys.stdout.flush()
|
|
|
|
|
|
def send_error(resp_id: int | str | None, code: int, message: str) -> None:
|
|
response = {
|
|
"jsonrpc": "2.0",
|
|
"id": resp_id,
|
|
"error": {"code": code, "message": message},
|
|
}
|
|
sys.stdout.write(json.dumps(response) + "\n")
|
|
sys.stdout.flush()
|
|
|
|
|
|
def handle_initialize(params: dict[str, Any]) -> dict[str, Any]:
|
|
global capabilities, initialized
|
|
capabilities = params.get("capabilities", {})
|
|
initialized = True
|
|
return {
|
|
"protocolVersion": MCP_PROTOCOL_VERSION,
|
|
"capabilities": {"tools": True, "resources": {}},
|
|
"serverInfo": {
|
|
"name": "test-mcp-stdio-server",
|
|
"version": "1.0.0",
|
|
},
|
|
}
|
|
|
|
|
|
def handle_tools_list(params: dict[str, Any]) -> dict[str, Any]:
|
|
return {"tools": TOOLS}
|
|
|
|
|
|
def handle_tools_call(req_id: int | str | None, params: dict[str, Any]) -> None:
|
|
global slow_tool_delay
|
|
|
|
tool_name = params.get("name", "")
|
|
arguments = params.get("arguments", {})
|
|
|
|
if tool_name == "test/echo":
|
|
message = arguments.get("message", "")
|
|
send_response(
|
|
req_id,
|
|
{"content": [{"type": "text", "text": json.dumps({"echoed": message})}]},
|
|
)
|
|
return
|
|
|
|
if tool_name == "test/multiply":
|
|
a = arguments.get("a", 0)
|
|
b = arguments.get("b", 0)
|
|
send_response(
|
|
req_id,
|
|
{"content": [{"type": "text", "text": json.dumps({"result": a * b})}]},
|
|
)
|
|
return
|
|
|
|
if tool_name == "test/slow":
|
|
delay = arguments.get("delay", 0.1)
|
|
if delay > slow_tool_delay:
|
|
time.sleep(delay)
|
|
send_response(
|
|
req_id,
|
|
{"content": [{"type": "text", "text": json.dumps({"delayed": delay})}]},
|
|
)
|
|
return
|
|
|
|
send_error(req_id, -32602, f"Unknown tool: {tool_name}")
|
|
|
|
|
|
def handle_request(request: dict[str, Any]) -> None:
|
|
global request_id, slow_tool_delay
|
|
|
|
method = request.get("method", "")
|
|
params = request.get("params", {})
|
|
req_id = request.get("id")
|
|
|
|
if method == "initialize":
|
|
result = handle_initialize(params)
|
|
send_response(req_id, result)
|
|
elif method == "notifications/initialized":
|
|
pass
|
|
elif method == "tools/list":
|
|
if not initialized:
|
|
send_error(req_id, -32602, "Not initialized")
|
|
return
|
|
result = handle_tools_list(params)
|
|
send_response(req_id, result)
|
|
elif method == "tools/call":
|
|
if not initialized:
|
|
send_error(req_id, -32602, "Not initialized")
|
|
return
|
|
handle_tools_call(req_id, params)
|
|
elif method == "test/set_slow_delay":
|
|
slow_tool_delay = params.get("delay", 0.0)
|
|
send_response(req_id, {"ok": True})
|
|
else:
|
|
send_error(req_id, -32601, f"Unknown method: {method}")
|
|
|
|
|
|
def main() -> None:
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
|
|
try:
|
|
request = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
|
|
if isinstance(request, dict) and request.get("jsonrpc") == "2.0":
|
|
handle_request(request)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|