Files
mesh-sdk/go/stdio.go
T

109 lines
3.7 KiB
Go

// Package stdio is the Novox Mesh SDK for Go — the skeleton (novox/hq ADR 0188).
//
// 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()
}