Skip to content

fix(mcp): preserve bridged input schemas#167

Merged
leeroybrun merged 4 commits into
happier-dev:devfrom
jr200:fix/mcp-bridge-preserve-input-schema
May 11, 2026
Merged

fix(mcp): preserve bridged input schemas#167
leeroybrun merged 4 commits into
happier-dev:devfrom
jr200:fix/mcp-bridge-preserve-input-schema

Conversation

@jr200
Copy link
Copy Markdown
Contributor

@jr200 jr200 commented May 6, 2026

Summary

  • preserve remote MCP input schema details in the stdio bridge instead of flattening every bridged tool to z.any()
  • convert remote JSON tool schemas into equivalent Zod schemas so downstream clients still see required arguments like content
  • add an integration regression that verifies a bridged tool with required and optional nullable fields keeps required: [\"content\"] over both HTTP and SSE transports

Problem

When Happier bridged remote MCP tools through remoteMcpStdioBridge, it registered every tool with inputSchema: z.any(). Tool calls still worked, but listTools() lost required-field information. In practice this broke Hindsight-backed tools in Happier-managed OpenCode sessions because required arguments such as content disappeared from the surfaced schema.

Verification

  • ran yarn --cwd "/Users/primary/code/jr200/happier/apps/cli" build
  • ran yarn --cwd "/Users/primary/code/jr200/happier/apps/cli" vitest run --config vitest.integration.config.ts src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts

View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Summary by CodeRabbit

  • New Features

    • Added a new "remember" tool to available capabilities (supports progress updates).
  • Improvements

    • Tool listings now preserve explicit input schemas and forward request metadata and progress callbacks.
    • Bridge/server lifecycle and forwarding are more robust, with cleaner client/server shutdown behavior.
  • Tests

    • Integration tests extended to verify "remember" appears in listings, its schema is preserved, progress is emitted, and invocation returns expected content.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ef36171e-8ffd-4cfe-a7e7-9a10c7f38cf6

📥 Commits

Reviewing files that changed from the base of the PR and between b5a19bd and 3a31a5d.

📒 Files selected for processing (4)
  • apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
  • apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts
  • apps/cli/src/mcp/mcpToolCallRequestOptions.test.ts
  • apps/cli/src/mcp/mcpToolCallRequestOptions.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/cli/src/mcp/mcpToolCallRequestOptions.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
  • apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts

Walkthrough

The STDIO bridge was refactored to use an MCP SDK Server that proxies ListTools and CallTool to a connected remote MCP Client; integration tests add a typed remember tool, assert schema preservation, verify remember invocation, and check progress forwarding over HTTP and SSE transports.

Changes

Remote MCP bridge request-proxy + remember tests

Layer / File(s) Summary
Test imports & MCP types
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
Updated test imports to use MCP SDK Server, Client, and typed request schemas.
remember JSON Schema & registry
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
Adds rememberInputSchema and adds remember to the shared remoteTools registry alongside echo.
Remote test MCP server handlers
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
Create shared remote MCP Server and register listTools and callTool handlers implementing echo, remember, and unknown-tool error responses.
Schema & content helpers
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
Adds expectRememberSchemaPreserved and readFirstTextContent helpers for assertions on schema preservation and extracting first text content from results.
HTTP test server setup
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
Refactors Streamable HTTP test server to connect to the shared remote MCP server via StreamableHTTPServerTransport.
SSE test server setup
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
Refactors SSE test server to create per-session remote MCP server instances and route /message POSTs through the session SSEServerTransport.
HTTP integration assertions
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
HTTP integration test: assert remember in listTools, validate inputSchema preservation, call echo and remember, assert returned first-text content and progress event; adjust client lifecycle (close in finally).
SSE integration assertions
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
SSE integration test: same remember presence, schema validation, and callTool assertions for echo and remember; client closed in finally (no progress assertion).
Bridge: SDK imports
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts
Update bridge imports to use MCP SDK Server, typed request schemas, and Client types.
Bridge: remove arg parser
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts
Remove local parseArgsValue helper previously used to register remote tools individually.
Bridge: Server and handlers
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts
Construct an MCP Server for the STDIO bridge and register ListToolsRequestSchema and CallToolRequestSchema handlers that proxy to the connected remote Client via callMcpToolWithResolvedTimeout, forwarding _meta and progress notifications when present.
Bridge: lifecycle & shutdown
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts
Guarded remote-client shutdown on STDIO transport close and close local server on stdin end.
callMcpToolWithResolvedTimeout tests
apps/cli/src/mcp/mcpToolCallRequestOptions.test.ts
Adds test ensuring requestMetadata is put into _meta and onprogress is forwarded into the resolved SDK request options.
callMcpToolWithResolvedTimeout impl
apps/cli/src/mcp/mcpToolCallRequestOptions.ts
Extend function signature to accept optional requestMetadata and onprogress; include _meta when provided and merge onprogress into the call options passed to client.callTool.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant BridgeServer
  participant RemoteMcpClient
  participant RemoteServer
  Client->>BridgeServer: ListToolsRequest
  BridgeServer->>RemoteMcpClient: forward listTools()
  RemoteMcpClient->>RemoteServer: transport -> remote MCP server
  RemoteServer-->>RemoteMcpClient: tools list
  RemoteMcpClient-->>BridgeServer: tools list
  BridgeServer-->>Client: tools list
  Client->>BridgeServer: CallToolRequest(name,args,_meta)
  BridgeServer->>RemoteMcpClient: callTool(name,args,_meta) via callMcpToolWithResolvedTimeout
  RemoteMcpClient->>RemoteServer: transport -> remote callTool
  RemoteServer-->>RemoteMcpClient: tool response (content / progress)
  RemoteMcpClient-->>BridgeServer: response / progress notifications
  BridgeServer-->>Client: tool response (content) / notifications/progress
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: preserving input schemas in the MCP bridge instead of flattening them to z.any().
Description check ✅ Passed The PR description covers all major template sections: Summary (what changed), Why (problem solved), and includes verification steps and notes, though 'How to test' is embedded in the verification section rather than explicitly formatted.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 6, 2026

Greptile Summary

This PR refactors remoteMcpStdioBridge to use the low-level Server class (instead of the high-level McpServer) and wires up raw ListToolsRequestSchema/CallToolRequestSchema handlers that forward directly to the remote client. This completely sidesteps the previous Zod-conversion layer (z.any() or any JSON-schema-to-Zod mapping), meaning the remote server's JSON schema is passed through verbatim to downstream clients.

  • Schema transparency: listTools responses are now proxied without modification, preserving required, anyOf, allOf, $ref, nested objects, and all other JSON Schema constructs exactly as the remote advertises them.
  • Cleaner call forwarding: callTool is forwarded via callMcpToolWithResolvedTimeout using request.params.name and request.params.arguments, which retains correct timeout resolution per tool.
  • Test improvements: Integration tests now use the low-level Server on the test-server side too, add the remember tool with a complex schema (nullable fields, nested objects), and assert full schema preservation over both HTTP and SSE transports.

Confidence Score: 5/5

This PR is safe to merge; the transparent-proxy approach eliminates the Zod conversion layer entirely and the integration tests confirm schema fidelity over both transports.

The change replaces a lossy schema conversion with a direct passthrough — removing the main source of information loss. The callTool path correctly delegates argument normalisation and timeout resolution to callMcpToolWithResolvedTimeout. Integration tests exercise both HTTP and SSE paths and assert the exact schema shape round-trips, including required fields, nullable anyOf, and nested objects. No behavioural regressions were identified.

No files require special attention.

Important Files Changed

Filename Overview
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts Replaces per-tool McpServer registration (with z.any() schemas) with raw Server request handlers that transparently proxy listTools and callTool to the remote client; logic is simpler and more correct.
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts Upgrades test harness to low-level Server, adds the 'remember' tool with nullable anyOf fields and nested object schema, asserts schema round-trip fidelity over HTTP and SSE, and fixes client cleanup in finally blocks.

Sequence Diagram

sequenceDiagram
    participant STDIO Client
    participant Bridge (Server / StdioServerTransport)
    participant Remote MCP Server (HTTP or SSE)

    STDIO Client->>Bridge (Server / StdioServerTransport): listTools (cursor?)
    Bridge (Server / StdioServerTransport)->>Remote MCP Server (HTTP or SSE): listTools(request.params)
    Remote MCP Server (HTTP or SSE)-->>Bridge (Server / StdioServerTransport): tools[] with raw JSON schemas
    Bridge (Server / StdioServerTransport)-->>STDIO Client: tools[] (verbatim, no Zod conversion)

    STDIO Client->>Bridge (Server / StdioServerTransport): callTool(name, arguments)
    Bridge (Server / StdioServerTransport)->>Remote MCP Server (HTTP or SSE): callMcpToolWithResolvedTimeout(name, arguments)
    Remote MCP Server (HTTP or SSE)-->>Bridge (Server / StdioServerTransport): tool result
    Bridge (Server / StdioServerTransport)-->>STDIO Client: tool result (verbatim)
Loading

Reviews (2): Last reviewed commit: "test(mcp): address remote bridge review ..." | Re-trigger Greptile

Comment on lines +57 to +67
const anyOf = Array.isArray(record.anyOf) ? record.anyOf.map((entry) => jsonSchemaToZod(entry)) : null;
if (anyOf && anyOf.length > 0) {
const [first, ...rest] = anyOf;
return rest.reduce<z.ZodTypeAny>((current, entry) => z.union([current, entry]), first);
}

const oneOf = Array.isArray(record.oneOf) ? record.oneOf.map((entry) => jsonSchemaToZod(entry)) : null;
if (oneOf && oneOf.length > 0) {
const [first, ...rest] = oneOf;
return rest.reduce<z.ZodTypeAny>((current, entry) => z.union([current, entry]), first);
}
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.

P2 Nested unions for 3+ anyOf/oneOf alternatives

The pairwise reduce builds z.union([z.union([A, B]), C]) instead of a flat z.union([A, B, C]). When the bridge's McpServer re-serializes this Zod schema for the STDIO client, some Zod-to-JSON-Schema adapters emit a nested {"anyOf": [{"anyOf": [A_schema, B_schema]}, C_schema]} rather than the flat {"anyOf": [A, B, C]} the remote originally advertised. The PR's goal is to faithfully preserve the remote schema; a 3-alternative union (e.g., string | number | null) would arrive at the downstream client in a non-standard nested form. A flat approach — collecting all entries and passing them directly to z.union(entries as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]) — avoids the nesting entirely.

Comment on lines +53 to +116
function jsonSchemaToZod(schema: unknown): z.ZodTypeAny {
const record = asRecord(schema);
if (!record) return z.any();

const anyOf = Array.isArray(record.anyOf) ? record.anyOf.map((entry) => jsonSchemaToZod(entry)) : null;
if (anyOf && anyOf.length > 0) {
const [first, ...rest] = anyOf;
return rest.reduce<z.ZodTypeAny>((current, entry) => z.union([current, entry]), first);
}

const oneOf = Array.isArray(record.oneOf) ? record.oneOf.map((entry) => jsonSchemaToZod(entry)) : null;
if (oneOf && oneOf.length > 0) {
const [first, ...rest] = oneOf;
return rest.reduce<z.ZodTypeAny>((current, entry) => z.union([current, entry]), first);
}

if (Array.isArray(record.enum) && record.enum.length > 0) {
const literals = record.enum.map((entry) => z.literal(entry));
const [first, ...rest] = literals;
return rest.reduce<z.ZodTypeAny>((current, entry) => z.union([current, entry]), first);
}

const typeValue = record.type;
if (Array.isArray(typeValue) && typeValue.length > 0) {
const entries = typeValue.map((entry) => jsonSchemaToZod({ ...record, type: entry }));
const [first, ...rest] = entries;
return rest.reduce<z.ZodTypeAny>((current, entry) => z.union([current, entry]), first);
}

switch (typeValue) {
case 'string':
return z.string();
case 'number':
return z.number();
case 'integer':
return z.number().int();
case 'boolean':
return z.boolean();
case 'null':
return z.null();
case 'array': {
return z.array(jsonSchemaToZod(record.items));
}
case 'object': {
const properties = asRecord(record.properties) ?? {};
const required = new Set(Array.isArray(record.required) ? record.required.filter((entry): entry is string => typeof entry === 'string') : []);
const shape: Record<string, z.ZodTypeAny> = {};
for (const [key, value] of Object.entries(properties)) {
const propertySchema = jsonSchemaToZod(value);
shape[key] = required.has(key) ? propertySchema : propertySchema.optional();
}

let objectSchema = z.object(shape);
if (record.additionalProperties === false) return objectSchema.strict();

const additionalProperties = asRecord(record.additionalProperties);
if (additionalProperties) return objectSchema.catchall(jsonSchemaToZod(additionalProperties));

return objectSchema.passthrough();
}
default:
return z.any();
}
}
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.

P2 allOf and $ref silently degrade to z.any()

jsonSchemaToZod handles anyOf, oneOf, enum, and scalar primitive types but has no branch for allOf or $ref. If a remote MCP server returns a schema that uses either construct (e.g., an intersection type or a shared definition), the resulting Zod schema is z.any(), stripping all field-level constraints. This is the same loss of information the PR is meant to fix. A comment or // TODO in the fallback default case would at least surface the gap; adding a best-effort allOfz.intersection path (or z.and) would close it.

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)
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts (1)

52-68: ⚡ Quick win

Optional: consider strengthening the assertion to also verify context/tags are surfaced as nullable.

The current assertion only pins required: ['content']. Since the translator now also has to round-trip nullable+optional fields (z.union([z.string(), z.null()]).optional()), asserting that context and tags appear in properties with the expected nullable shape would lock in the broader translator behavior, not just the required-field fix. Skip if you'd rather keep the test minimal.

🧪 Example assertion extension
       expect(rememberTool?.inputSchema).toMatchObject({
         type: 'object',
         required: ['content'],
         properties: {
           content: { type: 'string' },
+          context: expect.any(Object),
+          tags: expect.any(Object),
         },
       });
+      expect((rememberTool?.inputSchema as any)?.required).not.toContain('context');
+      expect((rememberTool?.inputSchema as any)?.required).not.toContain('tags');

Also applies to: 110-126

🤖 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 `@apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts` around
lines 52 - 68, Extend the test assertion for the 'remember' tool registration
(mcp.registerTool with inputSchema) to also verify that the generated JSON
Schema includes nullable+optional shapes for context and tags: assert that
properties.context is a schema allowing string or null and optional, and
properties.tags is a schema allowing array-of-strings or null and optional, in
addition to the existing required:['content'] check; update the parallel
assertion in the second block (the other registration around lines 110-126)
similarly so both round-trip nullable+optional fields are validated.
🤖 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.

Nitpick comments:
In `@apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts`:
- Around line 52-68: Extend the test assertion for the 'remember' tool
registration (mcp.registerTool with inputSchema) to also verify that the
generated JSON Schema includes nullable+optional shapes for context and tags:
assert that properties.context is a schema allowing string or null and optional,
and properties.tags is a schema allowing array-of-strings or null and optional,
in addition to the existing required:['content'] check; update the parallel
assertion in the second block (the other registration around lines 110-126)
similarly so both round-trip nullable+optional fields are validated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3f6bcad3-b6c0-49b4-bd63-3a6d8eb20c94

📥 Commits

Reviewing files that changed from the base of the PR and between 8445043 and ad086fe.

📒 Files selected for processing (2)
  • apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
  • apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 6, 2026
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts (1)

236-255: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close the stdio client in finally, not only on success.

If any assertion or tool call fails before Lines 255/304, the bridge subprocess launched by StdioClientTransport never gets closed. That leaks processes across tests and can introduce flake. Hoist client out of the try block and close it in each finally.

🔧 Apply the same cleanup pattern to both tests
-    try {
+    let client: Client | null = null;
+    try {
       ...
-      const client = new Client({ name: 'bridge-test', version: '1.0.0' }, { capabilities: {} });
+      client = new Client({ name: 'bridge-test', version: '1.0.0' }, { capabilities: {} });
       await client.connect(transport);
       ...
-      await client.close();
     } finally {
+      await client?.close().catch(() => {});
       httpServer.stop();
       await rm(tmp, { recursive: true, force: true });
     }

As per coding guidelines, "Tests should be deterministic and isolated (no shared global state, no ordering reliance)".

Also applies to: 289-304

🤖 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 `@apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts` around
lines 236 - 255, The test currently constructs and connects a Client inside a
try block but only calls client.close() on the success path, leaking the
StdioClientTransport subprocess on failures; hoist the Client variable (new
Client(...)) and its connect call out so the client reference is available in
the outer scope, then ensure you call client.close() in a finally block (or use
a try/finally) after the test's tool calls and assertions; apply this cleanup
pattern to both tests that create a Client (the blocks invoking client.connect,
listTools(), callTool() and readFirstTextContent) so the subprocess is always
closed even on assertion/tool-call failures.
🤖 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 `@apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts`:
- Around line 27-37: The metadata schema in the test uses allOf with the source
property nested inside it while additionalProperties: false sits at the root,
which invalidates fixtures like { source: 'test' }; update the schema in
remoteMcpStdioBridge.integration.test.ts by removing the allOf wrapper and
declaring properties: { source: { type: 'string' } }, required: ['source'], and
additionalProperties: false directly on the metadata object so the validator
recognizes the source property (i.e., flatten the schema used in the test).

In `@apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts`:
- Around line 86-89: The Server is advertising capabilities.tools.listChanged =
true but the bridge (remoteMcpStdioBridge) does not forward remote
"notifications/tools/list_changed" events, violating MCP; either remove the
false advertisement or implement forwarding: update the Server constructor to
use { capabilities: { tools: {} } } to stop advertising listChanged, or
subscribe the remote client for "notifications/tools/list_changed" and forward
those events over the stdio transport by emitting the same notification from the
Server when the remote event arrives (ensure forwarding logic ties into the
existing ListTools/CallTool proxy paths and the stdio client used by this
bridge).

---

Outside diff comments:
In `@apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts`:
- Around line 236-255: The test currently constructs and connects a Client
inside a try block but only calls client.close() on the success path, leaking
the StdioClientTransport subprocess on failures; hoist the Client variable (new
Client(...)) and its connect call out so the client reference is available in
the outer scope, then ensure you call client.close() in a finally block (or use
a try/finally) after the test's tool calls and assertions; apply this cleanup
pattern to both tests that create a Client (the blocks invoking client.connect,
listTools(), callTool() and readFirstTextContent) so the subprocess is always
closed even on assertion/tool-call failures.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0839d9e1-f6c6-4c63-b67d-c2835ae47388

📥 Commits

Reviewing files that changed from the base of the PR and between ad086fe and d9acff5.

📒 Files selected for processing (2)
  • apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
  • apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts

Comment thread apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts
Comment thread apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 11, 2026
@leeroybrun
Copy link
Copy Markdown
Collaborator

Thank you @jr200 !

I have actually just changed the approach slightly: instead of converting the remote JSON Schema into Zod and registering each remote tool locally, the bridge now uses the lower-level MCP Server API and directly proxies:

  • listTools to the remote MCP server
  • callTool to the remote MCP server

That way, the remote inputSchema is preserved verbatim, including complex JSON Schema constructs, without us maintaining a partial JSON-Schema-to-Zod converter in the bridge.

@CodeRabbit @greptile please review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me read the current state of the files before reviewing.


🏁 Script executed:

#!/bin/bash
# Read the main bridge file and the integration test file
cat apps/cli/src/mcp/bridges/remoteMcpStdioBridge.ts

echo "---FILE_SEPARATOR---"

cat apps/cli/src/mcp/bridges/remoteMcpStdioBridge.integration.test.ts

echo "---FILE_SEPARATOR---"

# Look for related utilities (callMcpToolWithResolvedTimeout, etc.)
rg -n "callMcpToolWithResolvedTimeout" --type ts -C3

echo "---FILE_SEPARATOR---"

# Check if there are any other bridge files for context
fd "remoteMcp" --type f

Length of output: 22833


@leeroybrun This is a clean and well-reasoned approach — directly proxying at the protocol level is clearly superior to maintaining a partial JSON-Schema-to-Zod converter, and the integration tests with expectRememberSchemaPreserved give solid confidence that complex schemas (nested objects, anyOf, required, additionalProperties) are passed through verbatim.

A couple of things worth considering:


1. Remote client not closed on stdio disconnect (potential process hang)

When the stdio transport closes (e.g., the parent process exits or drops the connection), remoteClient is never explicitly closed. This could prevent the bridge process from exiting cleanly, especially when the remote uses a long-lived HTTP/SSE connection.

// After server.connect(stdio), add:
stdio.onclose = async () => {
  await remoteClient.close().catch(() => {});
};

Or equivalently via process.on('exit', ...) / process.on('SIGTERM', ...) for belt-and-suspenders cleanup.


2. Progress token (_meta) not forwarded for callTool

callMcpToolWithResolvedTimeout doesn't forward request.params._meta to the remote, so progress notifications from the local caller won't reach the upstream server. This is a pre-existing limitation of callMcpToolWithResolvedTimeout and may be acceptable for now, but worth noting as a known gap if the remote tools emit progress updates.


3. Pagination cursor is correctly forwarded for listTools

Passing request.params directly to remoteClient.listTools() means any cursor for paginated tool lists is transparently proxied — good catch.


4. Integration test structure ✅

The per-request createRemoteTestMcpServer('test-mcp') in startTestMcpHttpServer is intentional for session isolation (stateless HTTP). The finally blocks properly close the client and clean up temp dirs in both transport tests.


Overall the implementation is solid. The remote client lifecycle (#1) is the only item I'd consider addressing before merging; the _meta forwarding (#2) can be tracked as a follow-up.

@leeroybrun leeroybrun merged commit 7992407 into happier-dev:dev May 11, 2026
1 check 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.

2 participants