Skip to content

feat: add mcp tool provider - #71

Merged
bobrykov merged 2 commits into
masterfrom
feat/mcp-tool-provider
Aug 8, 2026
Merged

feat: add mcp tool provider#71
bobrykov merged 2 commits into
masterfrom
feat/mcp-tool-provider

Conversation

@bobrykov

@bobrykov bobrykov commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@dch-labs dch-labs deleted a comment from coderabbitai Bot Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b2516b92-a856-4136-bf6d-c8d70c0de1bd

📥 Commits

Reviewing files that changed from the base of the PR and between c9415b2 and 90fd77b.

📒 Files selected for processing (4)
  • README.md
  • examples/mcp-adapter.rs
  • src/engine/bare/tests.rs
  • src/mcp.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • README.md
  • examples/mcp-adapter.rs
  • src/engine/bare/tests.rs
  • src/mcp.rs

📝 Walkthrough

Walkthrough

Adds a feature-gated MCP adapter with in-process connections, tool discovery, registration, invocation, schema and annotation preservation, result conversion, and error handling. Adds an example, integration tests, documentation, and Tokio runtime setup for two existing tests.

Changes

MCP adapter

Layer / File(s) Summary
MCP feature and public surface
Cargo.toml, src/lib.rs, README.md, CHANGELOG.md, src/mcp.rs
Adds the optional rmcp dependency, the mcp feature, the public module, and adapter documentation.
Client connection and provider discovery
src/mcp.rs
Adds in-process MCP connections, tool discovery, name prefixing, refresh, collision handling, and registry registration.
Tool calls and result conversion
src/mcp.rs
Adds McpTool, MCP-to-loopctl calls, schema and annotation mapping, content conversion, and MCP error handling.
End-to-end MCP validation
examples/mcp-adapter.rs, tests/mcp_tool_provider.rs
Adds an in-memory server example and tests for invocation, metadata, errors, multipart content, schemas, refresh, collisions, shutdown, and annotations.

Tokio test context

Layer / File(s) Summary
Tokio context for panic tests
src/engine/bare/tests.rs
Enters a Tokio runtime before polling two asynchronous panic-test paths.

Sequence Diagram(s)

sequenceDiagram
  participant MCPServer
  participant McpClient
  participant McpToolProvider
  participant ToolRegistry
  participant McpTool
  MCPServer->>McpClient: Serve tools over duplex transport
  McpToolProvider->>McpClient: Discover tools
  McpClient-->>McpToolProvider: Return tool definitions
  McpToolProvider->>ToolRegistry: Register adapted tools
  ToolRegistry->>McpTool: Invoke exposed tool
  McpTool->>McpClient: Forward tools/call
  McpClient->>MCPServer: Call server tool
  MCPServer-->>McpClient: Return MCP content and status
  McpClient-->>McpTool: Convert result to ToolOutput
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: adding an MCP tool provider and adapter.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 50.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-tool-provider

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (5)
src/engine/bare/tests.rs (1)

4461-4468: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the runtime setup with the comments.

tokio::runtime::Runtime::new() enters a multi-thread runtime, not a current-thread runtime. If current-thread scheduling is required, build with Runtime::new_current_thread().enable_all().build() and drive agent.run() with rt.block_on(...). Otherwise, update the comments to describe an entered Tokio runtime.

Also applies to: 4555-4562.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare/tests.rs` around lines 4461 - 4468, Update the runtime setup
around agent.run in both affected test blocks to match the stated scheduling
model: either construct a current-thread runtime with enable_all and drive the
future via rt.block_on, or revise the comments to accurately describe the
multi-thread runtime created by Runtime::new. Apply the same correction to both
occurrences.
src/mcp.rs (4)

388-389: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider manual Debug impls for the public types.

McpClient, McpToolProvider, and McpTool are public and have no Debug. Consumers that derive Debug on a struct holding one of these fail to compile. RunningService may not implement Debug, so write the impls manually and print the adapter-level fields only.

♻️ Sketch
impl std::fmt::Debug for McpTool {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("McpTool")
            .field("server_name", &self.server_name)
            .field("exposed_name", &self.exposed_name)
            .field("read_only_hint", &self.read_only_hint)
            .field("destructive_hint", &self.destructive_hint)
            .finish_non_exhaustive()
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 388 - 389, Implement manual std::fmt::Debug
implementations for the public McpClient, McpToolProvider, and McpTool types,
exposing only their adapter-level fields and avoiding non-Debug internals such
as RunningService. Use debug_struct with finish_non_exhaustive where
appropriate, preserving existing behavior while allowing consumers to derive
Debug for containing types.

123-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the spawned server's initialization failure.

At Line 124 a failed server.serve(server_end) is discarded with no record. The caller then sees only McpError::Handshake from the client side, which reports the EOF and not the real cause. Add a tracing::warn! on the error branch. The module already uses tracing in bridge_tool_list.

♻️ Proposed refactor
         tokio::spawn(async move {
-            if let Ok(running) = server.serve(server_end).await {
-                let _ = running.waiting().await.ok();
+            match server.serve(server_end).await {
+                Ok(running) => {
+                    let _ = running.waiting().await;
+                }
+                Err(e) => {
+                    tracing::warn!(error = %e, "in-process MCP server failed to initialize");
+                }
             }
         });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 123 - 128, Update the spawned task around
server.serve in the MCP initialization flow to handle its error branch
explicitly and emit a tracing::warn! containing the initialization error;
preserve the existing waiting behavior for successful server startup and the
client handshake flow.

170-173: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Reject non-object MCP arguments before sending the call.

input is only attached when it is a JSON object, so strings, numbers, arrays, or booleans are sent as a tools/call without arguments. Accept only serde_json::Value::Object and serde_json::Value::Null; return ToolInput::InvalidInput(...) for other values.

♻️ Proposed refactor
         let mut params = rmcp::model::CallToolRequestParams::new(server_name.to_string());
-        if let serde_json::Value::Object(map) = input {
-            params = params.with_arguments(map);
+        match input {
+            serde_json::Value::Object(map) => params = params.with_arguments(map),
+            serde_json::Value::Null => {}
+            other => {
+                return Err(ToolError::InvalidInput(format!(
+                    "MCP tool '{server_name}' expects object arguments, got {other}"
+                )));
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 170 - 173, Update the MCP tool-call argument
handling around CallToolRequestParams::new: accept serde_json::Value::Object by
attaching its map and serde_json::Value::Null as no arguments, but return
ToolInput::InvalidInput(...) for strings, numbers, arrays, and booleans before
sending the request.

769-777: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Surface the readable fields in unsupported-content notes.

ContentBlock::Resource(_) drops embedded text content, and ContentBlock::ResourceLink(_) drops the uri. For ResourceContents::TextResourceContents, include uri/text so the model sees usable payload. For binary resources, include only the resource type rather than replacing it with a fixed unsupported-note string.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 769 - 777, Update the ContentBlock::Resource and
ContentBlock::ResourceLink handling to inspect their payloads instead of
discarding fields: include uri and text for TextResourceContents, and for binary
resources expose only the resource type. Preserve the existing unsupported-note
behavior for other content types and avoid using the fixed generic message for
these resource variants.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Cargo.toml`:
- Line 36: Update the Tokio dependency features used by the MCP module to
include io-util, ensuring tokio::io::duplex in src/mcp.rs compiles when the mcp
feature is enabled while preserving the existing feature set.

In `@examples/mcp-adapter.rs`:
- Line 61: Update the await handling around running.waiting() in the example to
explicitly consume the Option result, matching the existing let _ pattern used
by mcp.rs, so no unused_must_use warning is emitted.

In `@README.md`:
- Line 161: Update the mcp feature row in the README feature table to state that
the adapter currently supports only in-process MCP servers, and clarify that
stdio and HTTP/SSE transports are not yet available. Replace the broad “any MCP
server” wording while preserving the existing adapter and Tool implementation
references.

In `@src/mcp.rs`:
- Around line 32-43: Update the hidden async run function declaration in the
McpClient documentation example to return a Result compatible with the ?
operators and the final Ok::<(), loopctl::mcp::McpError>(()) expression. Keep
the example’s existing client and registry flow unchanged.
- Around line 174-178: Add a configurable optional per-call timeout field to
McpClient and apply it around the service.call_tool(params) future in the
tool-call method. Preserve unlimited waiting when the timeout is unset, and map
elapsed tokio::time::timeout results to ToolError::Execution with the configured
duration; retain the existing service-error mapping.
- Around line 255-259: Update the documentation for connect and the
corresponding doc text around the earlier connect declaration to remove the
claim that connect runs the MCP initialize handshake. Describe connect as
performing the tools/list snapshot, and retain the statement that handshake
failures occur during McpClient construction via in_process or from_service.
- Around line 608-620: Update the server-tool loop around bridge_tool so the
None/empty-name skip emits a tracing warning before continuing, matching the
documented behavior. Revise the bridge_tool documentation to accurately state
that callers drop empty-name tools with a warning, while preserving the existing
duplicate-name warning and first-tool retention.
- Around line 602-606: Cap the tool collection in the list-all-tools flow before
entries are appended to out, using the existing configured discovery/result
limit if available. Ensure pagination stops once the cap is reached while
preserving normal tools/list cursor handling and the McpError mapping in the
caller.

In `@tests/mcp_tool_provider.rs`:
- Around line 632-663: Correct the documentation comment above AnnotatedServer
to remove the inaccurate claims about #[tool_handler] generating or skipping
list_tools, call_tool, and get_info. Describe only the behavior actually
implemented by the #[tool_router] impl and manual ServerHandler::list_tools
override.
- Around line 44-55: Update tests/mcp_tool_provider.rs lines 44-55 by retaining
connect_in_process for drop_provider_cancels_background_server and adding a
separate test that uses McpClient::in_process to discover tools. Update
examples/mcp-adapter.rs lines 53-67 by replacing the manual duplex, server
spawn, and from_service setup with
McpClient::in_process(GreetServer::new()).await.expect("client initialize").
- Around line 473-507: Update intra_batch_name_collision_keeps_one_no_panic to
use an AnnotatedServer that returns two tool entries with the same name from
separate sources, rather than registering both status_a and status_b in
CollisionServer’s single ToolRouter. Ensure the test reaches bridge_tool_list’s
seen-based deduplication path and still asserts exactly one git__status entry
without panicking.

---

Nitpick comments:
In `@src/engine/bare/tests.rs`:
- Around line 4461-4468: Update the runtime setup around agent.run in both
affected test blocks to match the stated scheduling model: either construct a
current-thread runtime with enable_all and drive the future via rt.block_on, or
revise the comments to accurately describe the multi-thread runtime created by
Runtime::new. Apply the same correction to both occurrences.

In `@src/mcp.rs`:
- Around line 388-389: Implement manual std::fmt::Debug implementations for the
public McpClient, McpToolProvider, and McpTool types, exposing only their
adapter-level fields and avoiding non-Debug internals such as RunningService.
Use debug_struct with finish_non_exhaustive where appropriate, preserving
existing behavior while allowing consumers to derive Debug for containing types.
- Around line 123-128: Update the spawned task around server.serve in the MCP
initialization flow to handle its error branch explicitly and emit a
tracing::warn! containing the initialization error; preserve the existing
waiting behavior for successful server startup and the client handshake flow.
- Around line 170-173: Update the MCP tool-call argument handling around
CallToolRequestParams::new: accept serde_json::Value::Object by attaching its
map and serde_json::Value::Null as no arguments, but return
ToolInput::InvalidInput(...) for strings, numbers, arrays, and booleans before
sending the request.
- Around line 769-777: Update the ContentBlock::Resource and
ContentBlock::ResourceLink handling to inspect their payloads instead of
discarding fields: include uri and text for TextResourceContents, and for binary
resources expose only the resource type. Preserve the existing unsupported-note
behavior for other content types and avoid using the fixed generic message for
these resource variants.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: be7a6066-b39b-4ba4-83cb-d6d70cd44689

📥 Commits

Reviewing files that changed from the base of the PR and between 53adf58 and c9415b2.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • Cargo.toml
  • README.md
  • examples/mcp-adapter.rs
  • src/engine/bare/tests.rs
  • src/lib.rs
  • src/mcp.rs
  • tests/mcp_tool_provider.rs

Comment thread Cargo.toml
Comment thread examples/mcp-adapter.rs Outdated
Comment thread README.md Outdated
Comment thread src/mcp.rs
Comment thread src/mcp.rs
Comment thread src/mcp.rs
Comment thread src/mcp.rs
Comment on lines +44 to +55
async fn connect_in_process<S>(server: S) -> (McpClient, tokio::task::JoinHandle<()>)
where
S: ServerHandler + Clone + Send + 'static,
{
let (server_end, client_end) = tokio::io::duplex(DUPLEX_BUFFER);
let server_handle = tokio::spawn(async move {
let running = server.serve(server_end).await.expect("server serve");
let _ = running.waiting().await.ok();
});
let client = ().serve(client_end).await.map(McpClient::from_service).expect("client serve");
(client, server_handle)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

McpClient::in_process is bypassed by both the test suite and the example. Both sites hand-roll the duplex creation, the server spawn, and from_service, which is exactly the sequence src/mcp.rs Lines 118-132 already provides. The public constructor therefore ships with no test coverage and no demonstration, even though src/mcp.rs Lines 12-14 present it as the primary in-process entry point.

  • tests/mcp_tool_provider.rs#L44-L55: keep connect_in_process for drop_provider_cancels_background_server, which needs the JoinHandle, and add one test that builds the client with McpClient::in_process and discovers tools.
  • examples/mcp-adapter.rs#L53-L67: replace the manual duplex, spawn, and from_service block with McpClient::in_process(GreetServer::new()).await.expect("client initialize").
📍 Affects 2 files
  • tests/mcp_tool_provider.rs#L44-L55 (this comment)
  • examples/mcp-adapter.rs#L53-L67
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/mcp_tool_provider.rs` around lines 44 - 55, Update
tests/mcp_tool_provider.rs lines 44-55 by retaining connect_in_process for
drop_provider_cancels_background_server and adding a separate test that uses
McpClient::in_process to discover tools. Update examples/mcp-adapter.rs lines
53-67 by replacing the manual duplex, server spawn, and from_service setup with
McpClient::in_process(GreetServer::new()).await.expect("client initialize").

Comment thread tests/mcp_tool_provider.rs
Comment thread tests/mcp_tool_provider.rs
@bobrykov
bobrykov merged commit f722c58 into master Aug 8, 2026
8 checks passed
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.

1 participant