Files
mesh-sdk/python/mesh_sdk/__init__.py
T
jochen 78817c949e 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.
2026-10-02 20:34:19 +02:00

78 lines
3.2 KiB
Python

"""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}"}})