Files
mesh-sdk/go/stdio.go
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

109 lines
3.7 KiB
Go

// Package stdio is the Novox Mesh SDK for Go — 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 package 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.
package stdio
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
)
// Protocol is the MCP version this speaks; the runtime says the same.
const Protocol = "2025-03-26"
// Tool is one tool a bundle serves.
type Tool struct {
Name string
Description string
// Input is the JSON schema of the arguments; nil is a tool that takes nothing.
Input map[string]any
Run func(args map[string]any) (any, error)
}
type request struct {
ID *json.RawMessage `json:"id"`
Method string `json:"method"`
Params struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments"`
} `json:"params"`
}
// Serve answers the runtime on stdin and stdout until stdin closes.
func Serve(name string, tools []Tool) error {
return ServeOn(name, tools, os.Stdin, os.Stdout)
}
// ServeOn is Serve over any pair of streams, for a test.
func ServeOn(name string, tools []Tool, in io.Reader, out io.Writer) error {
byName := map[string]Tool{}
for _, t := range tools {
byName[t.Name] = t
}
enc := json.NewEncoder(out)
say := func(m map[string]any) { m["jsonrpc"] = "2.0"; _ = enc.Encode(m) }
scanner := bufio.NewScanner(in)
scanner.Buffer(make([]byte, 1<<20), 1<<24)
for scanner.Scan() {
var req request
if err := json.Unmarshal(scanner.Bytes(), &req); err != nil {
continue
}
id := any(nil)
if req.ID != nil {
id = *req.ID
}
switch req.Method {
case "initialize":
say(map[string]any{"id": id, "result": map[string]any{"protocolVersion": Protocol,
"capabilities": map[string]any{"tools": map[string]any{}},
"serverInfo": map[string]any{"name": name, "version": "1"}}})
case "notifications/initialized":
case "ping":
if id != nil {
say(map[string]any{"id": id, "result": map[string]any{}})
}
case "tools/list":
listed := make([]map[string]any, 0, len(tools))
for _, t := range tools {
schema := t.Input
if schema == nil {
schema = map[string]any{"type": "object", "properties": map[string]any{}}
} else if _, isSchema := schema["type"]; !isSchema {
schema = map[string]any{"type": "object", "properties": schema}
}
listed = append(listed, map[string]any{"name": t.Name, "description": t.Description, "inputSchema": schema})
}
say(map[string]any{"id": id, "result": map[string]any{"tools": listed}})
case "tools/call":
t, ok := byName[req.Params.Name]
if !ok {
say(map[string]any{"id": id, "error": map[string]any{"code": -32602, "message": fmt.Sprintf("%s has no tool %s", name, req.Params.Name)}})
continue
}
result, err := t.Run(req.Params.Arguments)
if err != nil {
say(map[string]any{"id": id, "result": map[string]any{"content": []map[string]any{{"type": "text", "text": err.Error()}}, "isError": true}})
continue
}
text, _ := json.Marshal(result)
say(map[string]any{"id": id, "result": map[string]any{"content": []map[string]any{{"type": "text", "text": string(text)}}}})
default:
if id != nil {
say(map[string]any{"id": id, "error": map[string]any{"code": -32601, "message": "no " + req.Method}})
}
}
}
return scanner.Err()
}