?

Wire format

rousseau mcp starts an MCP server that speaks JSON-RPC 2.0 over stdio, per the Model Context Protocol specification revision 2024-11-05 (declared in ProtocolVersion in internal/mcp/protocol.go).

  • One request per line on stdin (bufio.Scanner reads up to 8 MiB per line).
  • One response per line on stdout (json.NewEncoder emits newline-delimited JSON).
  • The server blocks until stdin closes or ctx is cancelled.

JSON-RPC 2.0 envelope

Every request, notification, and response uses this envelope (from internal/mcp/protocol.go line 38):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}

Fields present depend on the envelope kind:

Field Request Notification Response
jsonrpc always "2.0" always "2.0" always "2.0"
id required absent echoed from the request
method required required absent
params optional optional absent
result absent absent success only
error absent absent failure only

Notifications carry no id and receive no response. rousseau only receives one notification (notifications/initialized), which is silently accepted.

Method reference

Rousseau's Server.dispatch (internal/mcp/server.go line 112) routes these methods:

Method Purpose Response
initialize Handshake. Client declares protocol version and capabilities. InitializeResult
notifications/initialized Client confirms it is ready. (notification, no response)
ping Liveness probe. {}
tools/list Enumerate registered tools. ToolsListResult
tools/call Invoke a tool. ToolsCallResult
resources/list Placeholder. Returns { "resources": [] } today. {"resources": []}
prompts/list Placeholder. Returns { "prompts": [] }. {"prompts": []}
shutdown Client-initiated shutdown. {}

Request/response examples

1. initialize

Client sends:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"claude-desktop","version":"0.7.0"}}}

Server replies:

{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","serverInfo":{"name":"rousseau","version":"0.6.0"},"capabilities":{"tools":{"listChanged":false}}}}

listChanged: false because rousseau's tool set is static at process start — no runtime add/remove.

2. tools/list

{"jsonrpc":"2.0","id":2,"method":"tools/list"}

Server replies with the registered tools in insertion order:

{"jsonrpc":"2.0","id":2,"result":{"tools":[
  {"name":"read","description":"Read a file...","inputSchema":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}},
  {"name":"grep","description":"Search for a regex...","inputSchema":{"type":"object","properties":{"pattern":{"type":"string"},"path":{"type":"string"}},"required":["pattern"]}},
  {"name":"bash","description":"Execute a shell command...","inputSchema":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}
]}}

3. tools/call

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"read","arguments":{"path":"/etc/hostname"}}}

Success:

{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"my-host.example.com\n"}]}}

Handler-level failure (surfaced as content, not as a JSON-RPC error — this is MCP convention):

{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"read: open /nope: no such file or directory"}],"isError":true}}

4. ping

{"jsonrpc":"2.0","id":4,"method":"ping"}
{"jsonrpc":"2.0","id":4,"result":{}}

Error codes

Rousseau uses the standard JSON-RPC 2.0 error range plus one MCP extension:

Code Constant Meaning When emitted
-32700 CodeParseError Invalid JSON in the envelope. Envelope failed json.Unmarshal.
-32600 CodeInvalidRequest Envelope shape is wrong. jsonrpc field is not "2.0".
-32601 CodeMethodNotFound Method not implemented. Dispatch fell through to the default case.
-32602 CodeInvalidParams Params failed to decode. params did not unmarshal into the expected shape.
-32603 CodeInternalError Something went wrong marshalling the response. Rare — indicates a bug.
-32000 CodeToolNotFound Tool name is not registered. tools/call with an unknown name.

What is exposed

Two surfaces:

  • Tools. Every mcp.ToolSpec registered before Serve is advertised in tools/list and callable via tools/call. rousseau wires the same tool implementations the local agent loop uses: read, write, edit, grep, bash.
  • Sessions. rousseau's SQLite session store is exposed so an MCP host can enumerate and read past conversations. resources/list returns one entry per session.

Tool failures are surfaced through the content channel with isError=true, not the JSON-RPC error channel. This is the MCP convention.

Client configuration — Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or the platform equivalent:

{
  "mcpServers": {
    "rousseau": {
      "command": "rousseau",
      "args": ["mcp"],
      "env": {
        "HOME": "/Users/you"
      }
    }
  }
}

Restart Claude Desktop. rousseau will appear in the tools palette; every registered tool is invocable.

For a rousseau built into a Podman image, the entry becomes:

{
  "mcpServers": {
    "rousseau": {
      "command": "podman",
      "args": [
        "run", "--rm", "-i",
        "-v", "/Users/you/.local/share/rousseau:/home/rousseau/.local/share/rousseau:rw,Z",
        "localhost/rousseau-agent:local",
        "mcp"
      ]
    }
  }
}

Bind-mount the state directory so the MCP host sees the same sessions as the daemon.

Registering a custom tool

Embedding the MCP server in your own binary:

srv := mcp.NewServer("rousseau", "0.1.0", logger)

srv.MustRegister(mcp.ToolSpec{
    Name:        "count_files",
    Description: "Count files under a path.",
    InputSchema: json.RawMessage(`{
      "type": "object",
      "properties": {"path": {"type": "string"}},
      "required": ["path"]
    }`),
    Handler: func(ctx context.Context, args json.RawMessage) ([]mcp.Content, error) {
        var in struct{ Path string }
        if err := json.Unmarshal(args, &in); err != nil {
            return nil, fmt.Errorf("bad input: %w", err)
        }
        // ... count files ...
        return []mcp.Content{{Type: "text", Text: fmt.Sprintf("%d", n)}}, nil
    },
})

_ = srv.Serve(ctx, os.Stdin, os.Stdout)

Duplicate registrations return an error; MustRegister panics on duplicate (reserved for wire-up in main).

Concurrency

Serve may be called concurrently on independent transports (stdin/stdout for the MCP host, plus a control channel if you want one). The server's tool map is protected by an RWMutex; handler execution is not serialised — implementations must be safe for concurrent use.

Debugging

Every request/response envelope is logged at debug level by default. Enable with:

log:
  level: debug
  format: text

Or:

ROUSSEAU_LOG_LEVEL=debug rousseau mcp 2>/tmp/mcp.log

The MCP host consumes stdout; keep the log stream on stderr.

Troubleshooting

Claude Desktop / Cursor never shows the rousseau tools

Almost always a wiring error, not a rousseau problem. Check: (1) the command and args in the host config invoke rousseau mcp (not rousseau chat); (2) the config file was saved and the host restarted; (3) rousseau mcp </dev/null from a shell doesn't crash — if it does, fix that first.

parse error on the very first message

The host is not sending line-delimited JSON. Some early MCP implementations send framed messages (Content-Length: N\r\n\r\n<body>); rousseau expects \n-delimited. Update the host to a build that uses stdio framing (all current major hosts do).

method not found: <foo>

The host is calling a method rousseau does not implement. Empty resources/list and prompts/list are provided as no-ops for the common probes; anything else returns -32601. Check internal/mcp/server.go dispatch() for the full method list.

Tool calls succeed but the host reports them as errors

The tool handler returned an error the wrong way. Handlers should return []Content{{Type: "text", Text: err.Error()}}, err != nil — rousseau catches the error and wraps it into isError: true. Do not return the error via the JSON-RPC error channel unless it is a protocol-level failure.

Container-based MCP fails with permission denied on state directory

The podman run invocation from Claude Desktop must include a -v for the state directory with the right SELinux label. Use :Z (private) unless the container is shared with other Podman workloads. Also verify the host UID inside the container matches the file ownership.

Related pages

Further reading

  • internal/mcp/protocol.go — envelope, method names, error codes.
  • internal/mcp/server.goServe, dispatch, tool registry.
  • internal/mcp/tools.go — helpers for registering rousseau's built-in tools.
  • internal/cli/mcp.go — the rousseau mcp command wiring.
  • Model Context Protocol spec — external reference.

Type to search 150+ pages. Ranking: BM25 with title/description boost.