fabro/test/mcp/echo_server.py
Bryan Helmkamp b1d95faa57
Hand Petri the server's environment and MCP catalogs
Petri's Fabro frontend refused a bundle naming an environment it did not
declare and every MCP catalog reference, so the fixtures declared
`[environments.local]` and the server's catalogs never reached Petri.

Pin Petri at c874b86, where the frontend reads `[environments.<id>]` and
`[run.environment]` from every settings layer (bundle over project over the
host's layer, key by key), takes the environment a launch selected over the
layers, and resolves `[run.agent.mcps.<name>] id = "..."` against a catalog
the host binds. The server hands Petri its environment catalog as
`[environments.<id>]` tables of the settings layer it already passes, the
intent's environment as the launch's selection (`Launch::environment`, as
the intent overrides the bundle in Fabro's own resolution), and its MCP
catalog as `RuntimeSpec::mcp_catalog_toml`, one inline entry per definition
keyed by id. Offline validation hands Petri the seeded catalog the same
way, so `fabro validate` accepts `[run.environment] id = "local"`.

The fixtures drop the `[environments.local]` tables they carried for this;
the secrets test keeps its own, on purpose. Scenario tests cover a bundle
naming a catalog environment (its image lowered, and run on Docker when the
plugin and a daemon are there), a bundle's own table winning key by key,
the server refusing an unknown environment before Petri, and a catalog MCP
reference whose tool the agent session lists (an echo server under
`test/mcp/`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 07:08:08 -04:00

80 lines
2.3 KiB
Python
Executable file

#!/usr/bin/env python3
"""A one-tool MCP server over stdio for the server's scenario tests.
Speaks JSON-RPC 2.0, one message per line on stdin and stdout, as the MCP
specification describes for the stdio transport. It exposes ``echo(message)``,
which answers the message, so a test can see the server's tool reach an
agent session's tool list. Dependency-free.
"""
import json
import sys
SERVER_INFO = {"name": "fabro-test-echo", "version": "1.0.0"}
PROTOCOL_VERSION = "2025-03-26"
TOOLS = [
{
"name": "echo",
"description": "Echo back the message",
"inputSchema": {
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
}
]
def handle(request):
"""Answer one request, or ``None`` for a notification."""
method = request.get("method")
request_id = request.get("id")
params = request.get("params") or {}
if method == "initialize":
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"protocolVersion": params.get("protocolVersion", PROTOCOL_VERSION),
"capabilities": {"tools": {}},
"serverInfo": SERVER_INFO,
},
}
if method == "ping":
return {"jsonrpc": "2.0", "id": request_id, "result": {}}
if method == "tools/list":
return {"jsonrpc": "2.0", "id": request_id, "result": {"tools": TOOLS}}
if method == "tools/call":
message = (params.get("arguments") or {}).get("message", "")
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {"content": [{"type": "text", "text": message}]},
}
if request_id is None:
return None
return {
"jsonrpc": "2.0",
"id": request_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:
request = json.loads(line)
except json.JSONDecodeError:
continue
response = handle(request)
if response is not None:
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()
if __name__ == "__main__":
main()