Files
cleveragents-core/features/mocks/mcp_stub_server_stdio.py
CoreRasurae 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
feat(mcp): implement StdioMCPTransport for JSON-RPC 2.0 stdio communication
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
2026-05-13 07:13:34 +00:00

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()