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.
109 lines
3.7 KiB
Go
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()
|
|
}
|