A tools bundle speaks MCP over stdio: the loop in TypeScript, and skeleton SDKs for Python, Go, Rust and C (hq ADR 0187)
The node's runtime launches a tools bundle as a process and speaks MCP over stdio to it, so a tool may be written in any language and the transport stays in the runtime (ADR 0039's refusal, kept). Each SDK is the loop and the tool type and nothing else: `@novox/mesh-sdk/stdio` (serveStdio, and serveRegisteredOverStdio for a bundle that already registers the in-process way), python/, go/, rust/ and c/ beside it, each with an example answering `greet` and the seat verb `node-lamp.on`. Skeletons, by the operator's direction: the bare minimum one bundle per language needs to be built and answer, proven here by each example answering tools/list and tools/call on stdio.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""The Novox Mesh SDK for Python — the skeleton (novox/hq ADR 0187).
|
||||
|
||||
A tools bundle is a process the node's runtime launches and speaks MCP over stdio to: `initialize`,
|
||||
`tools/list` once, `tools/call` per call, newline-framed JSON-RPC on stdin and stdout. This module is
|
||||
the loop, so a bundle writes its tools and nothing else. A tool named `<seat>.<verb>` is the
|
||||
module's implementation of that seat's verb; any other name is the module's own.
|
||||
|
||||
No transport here: the bus, the subjects and the memberships are the runtime's (ADR 0039, 0175).
|
||||
stdout is the protocol; log to stderr.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
PROTOCOL = "2025-03-26"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tool:
|
||||
name: str
|
||||
description: str
|
||||
run: Callable[[Dict[str, Any]], Any]
|
||||
# The JSON schema of the arguments, or the bare property map the catalogue's modules write.
|
||||
input: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
def as_schema(given: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not given:
|
||||
return {"type": "object", "properties": {}}
|
||||
if given.get("type") == "object" or "properties" in given:
|
||||
return given
|
||||
return {"type": "object", "properties": given}
|
||||
|
||||
|
||||
def serve(name: str, tools: List[Tool]) -> None:
|
||||
"""Serve these tools to the runtime over stdio until stdin closes."""
|
||||
by_name = {t.name: t for t in tools}
|
||||
out = sys.stdout
|
||||
|
||||
def say(message: Dict[str, Any]) -> None:
|
||||
out.write(json.dumps(message) + "\n")
|
||||
out.flush()
|
||||
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
request = json.loads(line)
|
||||
except ValueError:
|
||||
continue
|
||||
rid = request.get("id")
|
||||
method = request.get("method")
|
||||
params = request.get("params") or {}
|
||||
if method == "initialize":
|
||||
say({"jsonrpc": "2.0", "id": rid, "result": {"protocolVersion": PROTOCOL, "capabilities": {"tools": {}}, "serverInfo": {"name": name, "version": "1"}}})
|
||||
elif method == "notifications/initialized":
|
||||
pass
|
||||
elif method == "ping":
|
||||
if rid is not None:
|
||||
say({"jsonrpc": "2.0", "id": rid, "result": {}})
|
||||
elif method == "tools/list":
|
||||
say({"jsonrpc": "2.0", "id": rid, "result": {"tools": [
|
||||
{"name": t.name, "description": t.description, "inputSchema": as_schema(t.input)} for t in tools]}})
|
||||
elif method == "tools/call":
|
||||
tool = by_name.get(str(params.get("name", "")))
|
||||
if tool is None:
|
||||
say({"jsonrpc": "2.0", "id": rid, "error": {"code": -32602, "message": f"{name} has no tool {params.get('name')}"}})
|
||||
continue
|
||||
try:
|
||||
result = tool.run(params.get("arguments") or {})
|
||||
say({"jsonrpc": "2.0", "id": rid, "result": {"content": [{"type": "text", "text": json.dumps(result)}]}})
|
||||
except Exception as err: # the tool failed: a tool error, not a protocol one
|
||||
say({"jsonrpc": "2.0", "id": rid, "result": {"content": [{"type": "text", "text": str(err)}], "isError": True}})
|
||||
elif rid is not None:
|
||||
say({"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": f"no {method}"}})
|
||||
Reference in New Issue
Block a user