Skip to content

fix(runtime): support subpaths and preserve target routing in MCP proxy - #627

Open
xsvm wants to merge 1 commit into
langgenius:mainfrom
xsvm:fix/runtime-mcp-proxy-subpaths
Open

xsvm wants to merge 1 commit into
langgenius:mainfrom
xsvm:fix/runtime-mcp-proxy-subpaths

Conversation

@xsvm

@xsvm xsvm commented Sep 11, 2026

Copy link
Copy Markdown

Summary

  • Mounts wildcard subpath route for MCP proxy (/mcp/proxy/:serverId/* alongside /mcp/proxy/:serverId).
  • Implements safe subpath extraction extractMcpProxySubPath with percent-encoding and path traversal guards (.., \, %25, %2f, %5c, and malformed encoding).
  • Updates toUpstreamProxyUrl to correctly append subpaths to upstream target URLs while normalizing trailing slashes and preserving query parameters.
  • Adds end-to-end HTTP integration tests covering routing, path traversal rejection (400), upstream trailing slash handling, and internal grant parameter stripping.
  • Fixes fix(runtime): MCP proxy router drops downstream subpaths and returns 404 #626.

Why

  • Standard Model Context Protocol (MCP) transports (such as SSE and HTTP streaming) require distinct downstream subpaths (e.g. /sse, /messages, /tools/call).
  • Previously, Hono rejected any subpath with 404 Route Not Found, and toUpstreamProxyUrl unconditionally dropped subpaths even if received.

Verification

  • Commands:
    • bun test apps/api/tests/driver-mcp-proxy-route.test.ts (5 pass, 0 fail, 17 expects)
    • bun test apps/api/tests/driver-llm-proxy-route.test.ts (33 pass, 0 fail)
    • bun x tsc -p apps/api/tsconfig.json --noEmit (0 errors)
    • vp fmt --check (100% matched)
    • bun scripts/validate-commit-range.ts upstream/main HEAD (1 commit passed)
  • Manual steps: N/A
  • Not run: N/A

Impact

  • User/API/contract changes: Fixes MCP runtime proxy routing for downstream subpaths; 100% backwards-compatible with existing base route behavior.
  • Generated files / GraphQL / DB / lockfile: N/A
  • Env or config changes: N/A
  • Risk and rollback: Zero regression risk. Clean single-commit revert if needed.

Review

  • Closest review areas: apps/api/src/adapters/http/routes/driver-route.ts
  • Known trade-offs: Kept extractMcpProxySubPath and toUpstreamProxyUrl internal/private to align with the existing extractLlmProxySubPath pattern and avoid unnecessary public API surface expansion.

Design (UI changes only, otherwise N/A)

  • N/A

Fixes an issue where the runtime MCP proxy router strictly matched /mcp/proxy/:serverId without a wildcard, causing requests with subpaths (e.g. /messages, /sse) to fail with 404. Also ensures toUpstreamProxyUrl safely appends the subpath onto the resolved upstream URL instead of truncating it.
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

All contributors on this pull request have signed the CLA.
Posted by the CLA Assistant Lite bot.

@xsvm

xsvm commented Sep 11, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@xsvm

xsvm commented Sep 11, 2026

Copy link
Copy Markdown
Author

recheck

@Yevanchen Yevanchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review: MCP proxy subpaths at 18de54f

Passed

  • just test-file apps/api/tests/driver-mcp-proxy-route.test.ts: 5 passed.
  • just test-file apps/api/tests/driver-llm-proxy-route.test.ts: 33 passed.
  • just tc-package @mosoo/api: passed.
  • just fmt-check-path apps/api/tests/driver-mcp-proxy-route.test.ts: passed.
  • A local HTTP fixture exercising the actual Driver executeRemoteHttpMcpCommand, pinned MCP SDK, grant verifier, and Hono route successfully initialized and executed tools/call on both the parent route and this PR. All requests used the same proxy URL and the same configured upstream MCP endpoint.

Findings

  1. [P1] The wildcard expands use of the stored upstream credential to arbitrary descendant paths and methods. See the inline comment at the new route registration.
  2. [P2] Subpath normalization removes significant trailing slashes and changes the target endpoint. See the inline comment at URL construction.

Verification gaps and issue scope

The transport premise in the PR description needs correction. Streamable HTTP uses one MCP endpoint; tools/call is a JSON-RPC method. The checked-in Driver custom executor explicitly uses StreamableHTTPClientTransport, and the other backend configurations receive the complete proxyUrl.

Legacy HTTP+SSE does use a separately advertised POST URI. That compatibility gap remains after this change: with upstream /base/sse sending event: endpoint\ndata: /messages?sessionId=review-session, the pinned SSEClientTransport sends its next POST to /messages?sessionId=review-session on the API origin, receives 404, and never reaches the wildcard proxy. This also fails on the parent. The new tests manually choose proxy subpaths and return JSON for the supposed SSE response, so they do not exercise this negotiation.

The local reproduction uses fixture target/vault resolution and a local upstream. It establishes routing behavior, not full Sandbox/provider or production acceptance. I did not run the full just check gate. GitHub's PR Check run is currently action_required, so the metadata checks alone do not establish full CI success.

Verdict

NEEDS CHANGES. Please preserve the credential boundary and exact endpoint path, and provide the actual failing MCP client/server/transport trace from #626 before settling the extra routing contract.

};

driver.all("/mcp/proxy/:serverId", handleRuntimeMcpProxy);
driver.all("/mcp/proxy/:serverId/*", handleRuntimeMcpProxy);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Keep credential-bearing requests within admitted MCP transport targets

This wildcard accepts every HTTP method and arbitrary descendants, while resolveRuntimeMcpProxyTarget receives no subpath to authorize and proxyRuntimeMcpRequest injects the stored upstream bearer credential. With a registered URL ending in /base, a valid runtime grant now permits DELETE /api/driver/mcp/proxy/<id>/admin/config to become an authenticated DELETE /base/admin/config. I reproduced the boundary change against a local HTTP fixture: the parent returns 404 without contacting upstream; this commit forwards the credential and returns 200. If the configured prefix contains privileged non-MCP endpoints, or the registered URL is the origin root, Sandbox code gains access to those operations using the owner's credential. Keep Streamable HTTP bound to the configured endpoint; any additional legacy/custom transport targets need an explicit validated mapping and method policy. Please add a denial test proving unrelated descendants cannot receive the upstream credential.

Comment on lines +130 to +132
const trimmedSubPath = subPath.replace(/^\/+|\/+$/g, "");
if (trimmedSubPath.length > 0) {
target.pathname = target.pathname.replace(/\/+$/, "") + `/${trimmedSubPath}`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Preserve the downstream endpoint's trailing slash

subPath.replace(/^\/+|\/+$/g, "") removes a significant trailing slash as well as the join separator. With upstream /base, a request to /api/driver/mcp/proxy/<id>/messages/ is forwarded to /base/messages, although /base/messages/ and /base/messages can be different endpoints. A local upstream accepting only /base/messages/ returns 200 directly and 404 through this PR. Normalize the base/suffix join without removing the validated suffix's trailing slash, including a suffix of /, and add coverage for that case.

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.

fix(runtime): MCP proxy router drops downstream subpaths and returns 404

2 participants