M5: rmcp stdio client and tool adapters
Adds an rmcp-based stdio MCP client in harness-mcp plus adapters exposing a configured server's tools as namespaced harness-tools, with an echo-server fixture and integration test, and wiring through harness-app.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal MCP stdio server fixture for harness-mcp integration tests.
|
||||
|
||||
Speaks newline-delimited JSON-RPC (the framing rmcp's child-process transport uses) and
|
||||
implements just enough of the protocol to be discovered and called: `initialize`,
|
||||
`notifications/initialized`, `tools/list`, and `tools/call`. Exposes one tool, `echo`,
|
||||
which returns its `text` argument, plus `boom`, which returns an error result.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
PROTOCOL_VERSION = "2024-11-05"
|
||||
|
||||
TOOLS = [
|
||||
{
|
||||
"name": "echo",
|
||||
"description": "Returns the text it is given.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {"text": {"type": "string"}},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "boom",
|
||||
"description": "Always fails.",
|
||||
"inputSchema": {"type": "object", "properties": {}},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def reply(msg_id, result):
|
||||
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": result}) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main():
|
||||
# readline() rather than `for line in sys.stdin`: the latter's read-ahead buffer blocks
|
||||
# until it fills, which would stall the JSON-RPC handshake line-by-line.
|
||||
while True:
|
||||
line = sys.stdin.readline()
|
||||
if line == "": # EOF: parent closed stdin
|
||||
break
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
msg = json.loads(line)
|
||||
method = msg.get("method")
|
||||
msg_id = msg.get("id")
|
||||
|
||||
if method == "initialize":
|
||||
reply(msg_id, {
|
||||
"protocolVersion": PROTOCOL_VERSION,
|
||||
"capabilities": {"tools": {}},
|
||||
"serverInfo": {"name": "echo-fixture", "version": "0.1.0"},
|
||||
})
|
||||
elif method == "notifications/initialized":
|
||||
pass # notification: no response
|
||||
elif method == "tools/list":
|
||||
reply(msg_id, {"tools": TOOLS})
|
||||
elif method == "tools/call":
|
||||
params = msg.get("params") or {}
|
||||
name = params.get("name")
|
||||
args = params.get("arguments") or {}
|
||||
if name == "echo":
|
||||
reply(msg_id, {
|
||||
"content": [{"type": "text", "text": args.get("text", "")}],
|
||||
"isError": False,
|
||||
})
|
||||
elif name == "boom":
|
||||
reply(msg_id, {
|
||||
"content": [{"type": "text", "text": "kaboom"}],
|
||||
"isError": True,
|
||||
})
|
||||
else:
|
||||
reply(msg_id, {
|
||||
"content": [{"type": "text", "text": f"unknown tool {name}"}],
|
||||
"isError": True,
|
||||
})
|
||||
elif msg_id is not None:
|
||||
# Unknown request: empty result keeps the client happy.
|
||||
reply(msg_id, {})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user