fabro/lib/crates/fabro-mcp/tests/test_mcp_server.py
Bryan Helmkamp a19f6dd03a
feat(cli): add Fabro MCP server (#236)
## Summary

Adds a stdio-based Fabro MCP server so MCP clients can manage Fabro
workflow runs through the authenticated `fabro` CLI, without a separate
MCP auth flow.

## What Changed

- Adds `fabro mcp start`, `fabro mcp config`, and `fabro mcp init
<agent>` for launching and configuring the MCP server.
- Introduces a new `fabro-mcp-server` crate with run-management tools:
  - `fabro_run_create`
  - `fabro_run_search`
  - `fabro_run_interact`
  - `fabro_run_gather`
  - `fabro_run_events`
- Reuses the CLI's authenticated server connection behavior, including
OAuth refresh, dev-token/local-server handling, explicit server targets,
proxy behavior, and stdio env/cwd isolation.
- Moves shared run-manifest construction into `fabro-manifest` so CLI
runs and MCP-created runs use the same override semantics.
- Extends MCP client stdio support with configured cwd and exact
environment handling for reliable spawned-server tests.

---------

Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com>
Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-11 18:20:50 -04:00

106 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""Minimal MCP server for integration testing over stdio.
Speaks JSON-RPC 2.0 over stdin/stdout per the MCP specification.
Exposes a single tool: echo(message) -> message.
"""
import json
import os
import sys
SERVER_INFO = {
"name": "test-echo-server",
"version": "0.1.0",
}
TOOL = {
"name": "echo",
"description": "Echo back the message",
"inputSchema": {
"type": "object",
"properties": {
"message": {"type": "string", "description": "Message to echo"}
},
"required": ["message"],
},
}
def handle_request(req):
method = req.get("method")
req_id = req.get("id")
params = req.get("params", {})
if method == "initialize":
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {"tools": {}},
"serverInfo": SERVER_INFO,
},
}
if method == "tools/list":
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {"tools": [TOOL]},
}
if method == "tools/call":
tool_name = params.get("name")
arguments = params.get("arguments", {})
if tool_name == "echo":
msg = arguments.get("message", "")
if msg == "__cwd__":
msg = os.getcwd()
elif msg.startswith("__env:") and msg.endswith("__"):
key = msg[len("__env:") : -len("__")]
msg = os.environ.get(key, "")
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": msg}],
},
}
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": f"unknown tool: {tool_name}"}],
"isError": True,
},
}
# Notifications (no id) — just ignore
if req_id is None:
return None
return {
"jsonrpc": "2.0",
"id": req_id,
"error": {"code": -32601, "message": f"Method not found: {method}"},
}
def main():
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
req = json.loads(line)
except json.JSONDecodeError:
continue
resp = handle_request(req)
if resp is not None:
sys.stdout.write(json.dumps(resp) + "\n")
sys.stdout.flush()
if __name__ == "__main__":
main()