// 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 // `.` 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() }