Skip to content

feat(mcp): add MCP server mode (#217)#233

Merged
qhkm merged 3 commits intomainfrom
feat/mcp-server
Mar 3, 2026
Merged

feat(mcp): add MCP server mode (#217)#233
qhkm merged 3 commits intomainfrom
feat/mcp-server

Conversation

@qhkm
Copy link
Copy Markdown
Owner

@qhkm qhkm commented Mar 3, 2026

Summary

  • Add zeptoclaw mcp-server command exposing all registered tools via JSON-RPC 2.0
  • Stdio transport (default): line-delimited JSON-RPC over stdin/stdout for Claude Desktop, VS Code, Cursor
  • HTTP transport: --http :3000 POST endpoint (requires panel feature for axum dependency)
  • Reuses existing MCP protocol types from src/tools/mcp/protocol.rs
  • 32 new tests across handler, stdio, and mod modules

Closes #217

Test plan

  • cargo test --lib passes (2848 tests, including 32 new MCP server tests)
  • cargo clippy -- -D warnings passes
  • cargo fmt -- --check passes
  • echo '{"jsonrpc":"2.0","id":1,"method":"initialize"}' | cargo run -- mcp-server returns server info
  • MCP HTTP mode starts when --http :3000 is passed (requires panel feature)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Adds an MCP server command to the CLI with stdio by default and optional HTTP transport.
    • Exposes MCP endpoints to list and invoke available tools.
    • Preserves original JSON-RPC id types (number/string/null) in responses.
  • Bug Fixes

    • Returns a clear error when HTTP transport is requested but not enabled, with guidance to use stdio or enable HTTP.
  • Documentation

    • Adds user-facing help/docs for the MCP server command.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

Adds an MCP server mode: a new mcp-server CLI, a public mcp_server module with stdio (default) and optional HTTP transport, a JSON-RPC 2.0 request handler, and wiring to list and execute existing tools via the ZeptoKernel.

Changes

Cohort / File(s) Summary
CLI Routing
src/cli/mod.rs
Adds McpServer { http: Option<String> } command variant, cmd_mcp_server(http) async handler, and dispatch in the main run flow to boot config, kernel, and start chosen transport.
Library Export
src/lib.rs
Exports new public module mcp_server.
Request Handler
src/mcp_server/handler.rs
New JSON-RPC 2.0 dispatcher handle_request(...) supporting initialize, notifications/initialized, tools/list, and tools/call; validates params, maps tools, delegates execution to kernel, and returns MCP-formatted responses and errors (with tests).
Server Core
src/mcp_server/mod.rs
Adds McpServer wrapping Arc<ZeptoKernel> with start_stdio() and feature-gated start_http(addr) (axum + background processor), request validation, and per-request response plumbing.
Stdio Transport
src/mcp_server/stdio.rs
Implements run_stdio(kernel) reading line-delimited JSON-RPC from stdin, parsing/validating requests, delegating to handler, and writing JSON responses to stdout; includes ID extraction, error helpers, and tests.
Protocol Types
src/tools/mcp/protocol.rs
Changes McpResponse.id from Option<u64> to Option<serde_json::Value> to preserve original JSON-RPC id types (number, string, null); updates docs/tests accordingly.

Sequence Diagrams

sequenceDiagram
    participant Client as MCP Client
    participant Stdio as Stdio Transport
    participant Handler as Request Handler
    participant Kernel as ZeptoKernel

    Client->>Stdio: send line-delimited JSON-RPC
    activate Stdio
    Stdio->>Stdio: parse & validate jsonrpc/id/method
    alt invalid
        Stdio-->>Client: JSON-RPC error response
    else valid
        Stdio->>Handler: handle_request(method, params, id)
        activate Handler
        alt initialize
            Handler-->>Stdio: initialization response
        else tools/list
            Handler->>Kernel: get_tools()
            activate Kernel
            Kernel-->>Handler: tool list
            deactivate Kernel
            Handler-->>Stdio: tools/list response
        else tools/call
            Handler->>Kernel: execute_tool(name, args)
            activate Kernel
            Kernel-->>Handler: execution result
            deactivate Kernel
            Handler-->>Stdio: tools/call response
        end
        deactivate Handler
        Stdio-->>Client: JSON-RPC response + newline
    end
    deactivate Stdio
Loading
sequenceDiagram
    participant Client as HTTP Client
    participant Axum as HTTP Endpoint
    participant Channel as Tokio Channel
    participant Processor as Background Processor
    participant Handler as Request Handler
    participant Kernel as ZeptoKernel

    Client->>Axum: POST JSON-RPC
    activate Axum
    Axum->>Axum: validate request
    alt invalid
        Axum-->>Client: error response
    else valid
        Axum->>Channel: enqueue request
        Axum->>Processor: await oneshot response
        activate Processor
        Processor->>Handler: handle_request(...)
        activate Handler
        Handler->>Kernel: execute/list/etc
        activate Kernel
        Kernel-->>Handler: result
        deactivate Kernel
        Handler-->>Processor: McpResponse
        deactivate Handler
        Processor->>Axum: return response via channel
        deactivate Processor
        Axum-->>Client: JSON-RPC response
    end
    deactivate Axum
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I nibble bytes at break of dawn,
I map the tools and carry on,
Stdio whispers, HTTP rings,
JSON-RPC flutters its wings,
A rabbit cheers — the server springs! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(mcp): add MCP server mode (#217)' directly and clearly summarizes the main change: adding an MCP server mode feature.
Linked Issues check ✅ Passed The PR implements all core coding requirements from #217: JSON-RPC 2.0 handlers (initialize, tools/list, tools/call), stdio transport, Tool trait mapping to MCP definitions, CLI command, and MCP protocol version 2024-11-05.
Out of Scope Changes check ✅ Passed All changes align with #217 scope: MCP server implementation, JSON-RPC handlers, stdio/HTTP transports, CLI integration, and protocol type updates. No unrelated or extraneous changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/mcp-server

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/mod.rs`:
- Around line 716-717: Config::load() performs blocking filesystem I/O but is
called directly inside the async cmd_mcp_server function, which can block Tokio
executor threads; fix this by calling Config::load inside
tokio::task::spawn_blocking (or tokio::spawn_blocking) and await the JoinHandle,
mapping any error into the same anyhow::Error shape as before so the resulting
let config = ... line remains equivalent; locate the call site in cmd_mcp_server
and replace the direct Config::load() invocation with a spawn_blocking wrapper
around Config::load (preserving the map_err(...) behavior).

In `@src/mcp_server/handler.rs`:
- Around line 29-34: handle_request currently takes id: Option<u64>, which drops
non-numeric JSON-RPC IDs; change the signature to accept and propagate the raw
JSON id (e.g., id: Option<serde_json::Value> or Option<Value>) so string,
number, or null IDs are preserved and echoed in McpResponse; update any internal
uses that rely on numeric-only behavior (remove .as_u64() conversions and adjust
logic in McpResponse construction and any call sites) so the response's id field
mirrors the incoming Value exactly.

In `@src/mcp_server/mod.rs`:
- Around line 64-69: The handler currently defaults missing/non-string "method"
to an empty string which yields a -32601 later; instead mirror the stdio
transport by validating "method" immediately: change the extraction of method
(the body.get("method") / .and_then(|v| v.as_str()) logic) to return an Invalid
Request (-32600) response when "method" is absent or not a string (do not
default to ""), i.e. check method presence and type up-front using the same
error construction used for jsonrpc validation and short-circuit the handler on
failure so the HTTP transport matches stdio behavior.

In `@src/mcp_server/stdio.rs`:
- Line 31: The debug call is logging the full MCP stdin payload via debug!(line
= %line, "MCP stdin: received"); change it to log only safe metadata: compute
the payload size (e.g., line.len()) and, if needed, extract only safe fields
like "method" and "id" from the parsed JSON (using serde_json to read Value and
.get("method")/.get("id") without logging the rest), then call debug! with those
fields (e.g., payload_size, method, id) instead of the raw line; ensure no full
payload or nested argument content is ever included in the log and update the
code around the debug! invocation in stdio.rs accordingly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a56c15d and 7a6b965.

📒 Files selected for processing (5)
  • src/cli/mod.rs
  • src/lib.rs
  • src/mcp_server/handler.rs
  • src/mcp_server/mod.rs
  • src/mcp_server/stdio.rs

Comment thread src/cli/mod.rs Outdated
Comment thread src/mcp_server/handler.rs
Comment thread src/mcp_server/mod.rs Outdated
Comment thread src/mcp_server/stdio.rs Outdated
@qhkm
Copy link
Copy Markdown
Owner Author

qhkm commented Mar 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/mcp_server/stdio.rs (1)

45-48: Prefer if let over is_none() + unwrap() pattern.

While safe here, the current pattern could be more idiomatic. This also aligns with the coding guideline to avoid unwrap() in production paths.

♻️ Suggested refactor
-        // JSON-RPC 2.0 spec: notifications (no id) MUST NOT receive a
-        // response.  Detect via the method name extracted from the raw line.
-        if resp.is_none() {
-            continue;
-        }
-        let resp = resp.unwrap();
+        // JSON-RPC 2.0 spec: notifications (no id) MUST NOT receive a
+        // response.  Detect via the method name extracted from the raw line.
+        let Some(resp) = resp else {
+            continue;
+        };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_server/stdio.rs` around lines 45 - 48, Replace the is_none() +
unwrap() pattern around the resp variable with an idiomatic if let binding:
instead of checking if resp.is_none() then calling resp.unwrap(), use if let
Some(resp) = resp { /* existing body that used resp */ } else { continue; } so
the code in src/mcp_server/stdio.rs uses pattern matching and avoids unwrap().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/mcp_server/stdio.rs`:
- Around line 45-48: Replace the is_none() + unwrap() pattern around the resp
variable with an idiomatic if let binding: instead of checking if resp.is_none()
then calling resp.unwrap(), use if let Some(resp) = resp { /* existing body that
used resp */ } else { continue; } so the code in src/mcp_server/stdio.rs uses
pattern matching and avoids unwrap().

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dba9e35 and a5e7bdf.

📒 Files selected for processing (3)
  • src/mcp_server/handler.rs
  • src/mcp_server/mod.rs
  • src/mcp_server/stdio.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/mcp_server/mod.rs

qhkm and others added 3 commits March 3, 2026 23:05
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change McpResponse.id from Option<u64> to Option<serde_json::Value> so
the server preserves whatever id type the client sends (number, string,
or null) as required by JSON-RPC 2.0.

Also:
- Safe stdio logging (metadata only, no payloads)
- Proper Invalid Request when method is missing in HTTP transport
- spawn_blocking for Config::load() in CLI handler
- New tests for string id preservation and log metadata

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…plies

Issue 5: HTTP transport now accepts raw String body instead of Axum's
Json extractor, manually parses JSON, and returns a proper JSON-RPC
-32700 parse error response for malformed input. Previously, Axum
would reject bad JSON with its own HTTP 422 before the handler ran.

Issue 6: Notifications (methods starting with "notifications/") no
longer produce a response per JSON-RPC 2.0 spec. The stdio transport
skips writing to stdout, and the HTTP transport returns 204 No Content.

- Add is_notification() helper in handler.rs
- Change process_line() to return Option<McpResponse>
- Update all existing tests for Option return type
- Add tests: notification suppression, malformed JSON parse error,
  is_notification true/false cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@qhkm qhkm force-pushed the feat/mcp-server branch from a5e7bdf to fbdea59 Compare March 3, 2026 15:07
@qhkm qhkm merged commit ec41da0 into main Mar 3, 2026
5 checks passed
@qhkm qhkm deleted the feat/mcp-server branch March 3, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: MCP server mode — expose tools to Claude Desktop, VS Code, Cursor

1 participant