Skip to content

feat(mcp): @orpc/mcp — serve one oRPC router as an MCP server (tools/resources/prompts)#1604

Open
mi3lix9 wants to merge 13 commits into
middleapi:mainfrom
mi3lix9:feat/mcp-integration
Open

feat(mcp): @orpc/mcp — serve one oRPC router as an MCP server (tools/resources/prompts)#1604
mi3lix9 wants to merge 13 commits into
middleapi:mainfrom
mi3lix9:feat/mcp-integration

Conversation

@mi3lix9

@mi3lix9 mi3lix9 commented Jun 27, 2026

Copy link
Copy Markdown

Summary

@orpc/mcp exposes an oRPC router as a Model Context Protocol server — the same procedures you serve over RPC and OpenAPI become MCP tools / resources / prompts, with the same types, validation, and middleware. Opt-in via mcp.tool / mcp.resource / mcp.prompt.

Built on the standard handler (per review)

Reworked from the original standalone dispatcher to use oRPC's StandardHandler (@dinwwwh's feedback):

  • MCPHandlerCodec resolves tools/call / resources/read / prompts/get and runs them through the normal procedure pipeline (middleware, validation, context, plugins).
  • MCPHandlerPlugin (auto-registered) is a routing interceptor that early-responds for the protocol routes (initialize, ping, the list methods, completion) and frames the JSON-RPC envelope.
  • fetch/node extend FetchHandler/NodeHttpHandler; stdio drives the same handler via a synthesized request. One code path, all transports — and any StandardHandler plugin (CORS, body-limit, OTel) composes.

Review items addressed

  • Security (Gemini, HIGH): the hand-rolled readBody is gone — body reading/limits now come from the adapter + BodyLimitHandlerPlugin (fixes the DoS + multi-byte UTF-8 corruption). Added optional Origin/DNS-rebinding validation. Both bot threads resolved.
  • API: a single namespaced meta API — mcp.tool/resource/prompt (no callable mcp()); mcp.resource is typed to require uri | uriTemplate.
  • Catalog pagination on every list method (opaque cursor, configurable pageSize, invalid cursor → -32602).
  • Dropped JSON-RPC batching (incompatible with the one-request/one-procedure flow; deprecated in the spec direction).

Conformance — MCP 2025-11-25

Implemented: lifecycle + capability negotiation, ping, tools/resources/prompts (list/call/read/get), pagination, typed errors (in-band tool errors vs JSON-RPC), structuredContent/outputSchema.

Deliberately not implemented because they are being removed/replaced in the next revision (checked against the draft changelog): sessions (Mcp-Session-Id), the GET SSE channel, listChanged/subscribe. The stateless POST→response design is intentional and forward-aligned.

Non-blocking follow-ups (SHOULD/MAY): honor notifications/cancelled (currently a no-op), _meta/progress, binary resource contents, real completion/complete.

Authorization

By design, authentication/authorization is the application's job via oRPC context + middleware (documented) — the package stays unopinionated about tokens, scopes, or OAuth.

Testing

Repo-wide CI gates pass with these changes: type:check, lint, and 1961 tests — including an end-to-end suite driven by the official @modelcontextprotocol/sdk client over real HTTP, plus unit/adapter/pagination coverage. Wired into the Next playground (/mcp) and documented at docs/integrations/mcp.

One decision for the maintainer

Positioning: keep as @orpc/mcp, or ship as an experimental package while the MCP spec is mid-revision (the next revision is a large one)? Happy to rename either way.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Introduced @orpc/mcp with MCP server handlers for fetch/web, Node.js, and stdio.
    • Opt-in router procedures are exposed as MCP tools, resources, and prompts (including structured results and URI-template resources).
    • Added full MCP JSON-RPC support for initialization, method dispatch, and catalog pagination.
  • Security & Robustness
    • Added JSON-RPC hardening: method/verb validation, parse/batch/ID handling, and Origin/Host allowlisting protection.
  • Documentation
    • Added an MCP integration guide and updated site navigation/README entries.
  • Tests
    • Added adapter, registry, pagination, content encoding, protocol/error, hardening, and SDK compliance test coverage.

mi3lix9 and others added 6 commits June 27, 2026 20:29
Adds the mcp() meta plugin (mirrors openapi()) plus a native MCPHandler that
exposes opted-in procedures as MCP tools, resources, and prompts over stdio and
Streamable HTTP (fetch/node). Reuses @orpc/json-schema converters for
inputSchema/outputSchema and the procedure client for dispatch.

- meta.ts: mcp(), mcp.tool/resource/prompt, getMCPMeta
- registry.ts: opt-in router walk -> tools/resources/prompts definitions
- content/error/uri-template: encoding + URI templates + error planes
- adapters: standard dispatcher + fetch/node/stdio handlers
- integration test: 15 passing; type:check + lint clean

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
Covers meta merge, content/error encoding, URI templates, registry
classification, and fetch/node/stdio adapter round-trips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
- playground: annotate planet procedures with mcp.tool() and mount
  MCPHandler at /mcp over the same router (one API -> RPC + OpenAPI + MCP)
- docs: add integrations/mcp.md + sidebar entry; README package list

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
…, json body typing)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
…l/sdk client

Drives the node MCPHandler over real HTTP with the canonical MCP SDK client
(v1.29.0): initialize handshake, tools list/call (incl. in-band typed error),
static + templated resources, and prompts. The SDK validates every response
against its own schemas, proving real protocol compliance (not just
self-consistency). Added @modelcontextprotocol/sdk as a dev dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
…idation)

A live run with the official SDK client surfaced that returning an error payload
in `structuredContent` makes clients reject the result (-32602) when the tool
declares an `outputSchema` — the client validates structuredContent against the
success schema. Errors now return only `content` + `isError: true`.

Adds an SDK e2e regression: a schema-typed tool that errors must not violate its
outputSchema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
@vercel

vercel Bot commented Jun 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orpc Ready Ready Preview, Comment Jun 29, 2026 4:44pm

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b345b649-cfad-4fbb-947a-05dd9b966579

📥 Commits

Reviewing files that changed from the base of the PR and between b6c283c and 6a654af.

📒 Files selected for processing (1)
  • packages/mcp/src/adapters/standard/utils.ts
💤 Files with no reviewable changes (1)
  • packages/mcp/src/adapters/standard/utils.ts

📝 Walkthrough

Walkthrough

Adds a new @orpc/mcp package for exposing oRPC routers as MCP servers over fetch, Node HTTP, and stdio. It introduces MCP metadata, registry building, content encoding, JSON-RPC handling, transport adapters, tests, docs, and playground wiring.

Changes

@orpc/mcp package

Layer / File(s) Summary
Wire types, constants, metadata, and URI templates
packages/mcp/src/types.ts, packages/mcp/src/constants.ts, packages/mcp/src/meta.ts, packages/mcp/src/uri-template.ts, packages/mcp/src/uri-template.test.ts, packages/mcp/src/meta.test.ts
Defines MCP and JSON-RPC wire types, protocol constants, MCP metadata helpers, and URI template compilation and tests.
Content encoders and JSON-RPC errors
packages/mcp/src/content.ts, packages/mcp/src/content.test.ts, packages/mcp/src/error.ts, packages/mcp/src/error.test.ts
Implements MCP output encoders and JSON-RPC error conversion for tool, resource, and prompt responses.
Registry builder and router extraction
packages/mcp/src/registry.ts, packages/mcp/src/registry.test.ts
Builds the MCP registry from annotated router procedures, compiles schemas, and memoizes the resulting provider.
Standard handler utils, codec, plugin, and pagination
packages/mcp/src/adapters/standard/utils.ts, packages/mcp/src/adapters/standard/mcp-handler-codec.ts, packages/mcp/src/adapters/standard/mcp-handler-plugin.ts, packages/mcp/src/adapters/standard/index.ts, packages/mcp/src/adapters/standard/mcp-pagination.test.ts, packages/mcp/src/adapters/standard/mcp-hardening.test.ts
Validates MCP JSON-RPC envelopes, resolves procedures through the standard handler pipeline, routes protocol methods, paginates list responses, and enforces HTTP security rules.
Fetch and Node adapters with transport tests
packages/mcp/src/adapters/fetch/mcp-handler.ts, packages/mcp/src/adapters/fetch/index.ts, packages/mcp/src/adapters/fetch/mcp-handler.test.ts, packages/mcp/src/adapters/node/mcp-handler.ts, packages/mcp/src/adapters/node/index.ts, packages/mcp/src/adapters/node/mcp-handler.test.ts, packages/mcp/src/sdk-compliance.test.ts
Wraps the standard handler into fetch and Node HTTP adapters and verifies initialize, list, call, parsing, batch, notification, and security behavior.
stdio transport handler
packages/mcp/src/adapters/stdio/mcp-handler.ts, packages/mcp/src/adapters/stdio/index.ts, packages/mcp/src/adapters/stdio/mcp-handler.test.ts
Implements newline-delimited JSON-RPC handling over stdin/stdout, including message size enforcement and request synthesis.
Package entry points, playground wiring, and docs
packages/mcp/src/index.ts, packages/mcp/package.json, packages/mcp/tsconfig.json, playgrounds/next/package.json, playgrounds/next/src/app/mcp/[[...rest]]/route.ts, playgrounds/next/src/routers/planet.ts, apps/content/docs/integrations/mcp.md, apps/content/.vitepress/config.ts, README.md
Exports the package surface, adds package and playground wiring, updates the playground router for MCP metadata, and adds MCP documentation and navigation.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I nibbled the bytes and hopped along,
Through tools and prompts where routers belong.
Fetch and stdio now sing the tune,
MCP at dawn and MCP at noon,
A tiny hare cheers this protocol song.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.19% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Clearly summarizes the new @orpc/mcp package and its purpose of serving an oRPC router over MCP tools, resources, and prompts.
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 unit tests (beta)
  • Create PR with unit tests

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces the @orpc/mcp package, which allows developers to expose oRPC routers as Model Context Protocol (MCP) servers supporting tools, resources, and prompts over Fetch, Node.js, and stdio transports. The changes include core registry and mapping logic, transport adapters, comprehensive test suites, and documentation. Feedback on the implementation highlights a high-severity security vulnerability in the Node.js adapter's readBody function, which lacks request size limits (exposing the server to memory exhaustion DoS) and can corrupt multi-byte UTF-8 characters. Additionally, a performance improvement is recommended to process JSON-RPC batch requests concurrently using Promise.all instead of sequentially.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/mcp/src/adapters/node/mcp-handler.ts Outdated
Comment thread packages/mcp/src/adapters/standard/mcp-handler.ts Outdated
@dinwwwh

dinwwwh commented Jun 28, 2026

Copy link
Copy Markdown
Member

I don't like this approach. We should utilize the existing standard handler and standard request/response flow. For MCP-specific routes like initialize, list, and spec, we should use a plugin that automatically registers them when creating the MCP handler

…ed plugin

Addresses PR middleapi#1604 review (@dinwwwh): MCP now uses oRPC's standard handler and
request/response flow instead of a bespoke JSON-RPC dispatcher.

- MCPHandlerCodec (StandardHandlerCodec): resolves tools/call, resources/read,
  prompts/get to a procedure and runs it through the standard pipeline.
- MCPHandlerPlugin (StandardHandlerPlugin): auto-registered routing interceptor
  that early-responds for protocol routes (initialize, ping, list, completion)
  and frames the JSON-RPC envelope for procedure results.
- fetch/node adapters extend FetchHandler/NodeHttpHandler; stdio drives the same
  StandardHandler via a synthesized request. One code path, all transports.

Security & robustness (PR review + MCP SDK study):
- Removes hand-rolled node readBody -> body reading/limits come from the adapter
  + BodyLimitHandlerPlugin (fixes DoS + multi-byte UTF-8 corruption).
- Adds Origin/Host (DNS-rebinding) validation (enableDnsRebindingProtection +
  allowedOrigins/allowedHosts; missing Origin passes; reject -> 403).
- stdio enforces a max message length.
- Fixes a bug found via the official-SDK e2e: the body was read twice
  (resolveBody consumes the stream) -> memoize the parsed envelope per request.

Drops JSON-RPC batching (incompatible with one-request/one-procedure; deprecated
in the spec direction) -> rejected with -32600.

66 tests pass incl. the official @modelcontextprotocol/sdk e2e; type:check + lint
clean. Docs + Next playground updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
mi3lix9 added a commit to mi3lix9/orpc that referenced this pull request Jun 28, 2026
…ed plugin

Addresses PR middleapi#1604 review (@dinwwwh): MCP now uses oRPC's standard handler and
request/response flow instead of a bespoke JSON-RPC dispatcher.

- MCPHandlerCodec (StandardHandlerCodec): resolves tools/call, resources/read,
  prompts/get to a procedure and runs it through the standard pipeline.
- MCPHandlerPlugin (StandardHandlerPlugin): auto-registered routing interceptor
  that early-responds for protocol routes (initialize, ping, list, completion)
  and frames the JSON-RPC envelope for procedure results.
- fetch/node adapters extend FetchHandler/NodeHttpHandler; stdio drives the same
  StandardHandler via a synthesized request. One code path, all transports.

Security & robustness (PR review + MCP SDK study):
- Removes hand-rolled node readBody -> body reading/limits come from the adapter
  + BodyLimitHandlerPlugin (fixes DoS + multi-byte UTF-8 corruption).
- Adds Origin/Host (DNS-rebinding) validation (enableDnsRebindingProtection +
  allowedOrigins/allowedHosts; missing Origin passes; reject -> 403).
- stdio enforces a max message length.
- Fixes a bug found via the official-SDK e2e: the body was read twice
  (resolveBody consumes the stream) -> memoize the parsed envelope per request.

Drops JSON-RPC batching (incompatible with one-request/one-procedure; deprecated
in the spec direction) -> rejected with -32600.

66 tests pass incl. the official @modelcontextprotocol/sdk e2e; type:check + lint
clean. Docs + Next playground updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
@mi3lix9

mi3lix9 commented Jun 28, 2026

Copy link
Copy Markdown
Author

Thanks for the steer @dinwwwh — reworked it around the standard handler as requested. Pushed in bf9665a.

Architecture (per your feedback)

MCPHandler is no longer a bespoke JSON-RPC dispatcher. It now builds a StandardHandler and wraps it with the existing FetchHandler/NodeHttpHandler adapters, so MCP rides the standard request/response flow (middleware, validation, context, interceptors, and any handler plugin — CORS/body-limit/OTel):

  • MCPHandlerCodec (StandardHandlerCodec) — resolves tools/call / resources/read / prompts/get to a procedure and runs it through the normal pipeline.
  • MCPHandlerPlugin (StandardHandlerPlugin), auto-registered when constructing the handler — a routing interceptor that early-responds for the MCP-specific routes (initialize, ping, the list methods, completion/complete, notifications) and frames the JSON-RPC envelope for procedure results. This is exactly the "plugin that registers them when creating the MCP handler" pattern (same shape as the OpenAPI spec plugin).

stdio drives the same StandardHandler via a synthesized request, so there's one code path for every transport.

Review findings addressed

  • readBody DoS + multi-byte UTF-8 (Gemini, HIGH): gone — there's no hand-rolled body reader anymore. Body reading/limits come from the node adapter + BodyLimitHandlerPlugin.
  • Origin / DNS-rebinding: added enableDnsRebindingProtection + allowedOrigins/allowedHosts (a missing Origin still passes for non-browser clients; a disallowed one → 403).
  • Batch processing (Gemini, perf): I went the other way and dropped JSON-RPC batching — it's incompatible with the standard one-request/one-procedure flow and is deprecated in the MCP spec direction. Batches now get a clear -32600. Happy to revisit if you'd rather keep it.

Bonus: a real bug the e2e caught

I added an end-to-end test that drives the node handler with the official @modelcontextprotocol/sdk client. During the rewrite it caught a genuine bug: the request body was read twice (resolveBody consumes the stream), so every procedure call fell through to "unknown target". Fixed by memoizing the parsed envelope per request — and the e2e now guards against regressions.

Status: 66 tests pass (incl. the official-SDK e2e), type:check + lint clean. Still draft. Deferred (happy to take direction on priority): sessions (Mcp-Session-Id), server→client GET SSE, pagination cursors, full completion/complete, and OAuth/RFC-9728 auth (the new architecture makes adding an auth interceptor straightforward).

…only

Removes the callable mcp({ ... }) form; the primitive is now always chosen via
mcp.tool() / mcp.resource() / mcp.prompt() (per PR feedback — one obvious way,
better DX). mcp.resource() is typed to require exactly one of uri | uriTemplate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
…on docs

- Add opaque-cursor pagination to tools/list, resources/list,
  resources/templates/list, prompts/list via a configurable `pageSize`
  (default 100, so small catalogs are unchanged). Invalid cursor -> -32602.
- Docs: Authorization is the app's job (context + middleware example);
  result pagination is the developer's job (tool input/output example);
  catalog pagination is built in. Reframe limitations (sessions/GET-SSE are
  being removed in the next MCP revision, so statelessness is intentional).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
@mi3lix9 mi3lix9 marked this pull request as ready for review June 28, 2026 16:18
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement New feature or request javascript Pull requests that update javascript code labels Jun 28, 2026
Prose-led structure cloned from the other integration pages (Installation
code-group, Setup, per-primitive sections, Serving, Authorization, Security,
Limitations) with :::warning/tip callouts. Drops the pagination section
(internal detail) and the meta-options/how-it-maps reference tables in favor of
inline explanations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (1)
packages/mcp/src/adapters/stdio/mcp-handler.test.ts (1)

43-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression test for maxMessageLength.

The suite never exercises the over-limit branch, so transport-level size-limit regressions—especially whitespace-padded inputs—can slip through unnoticed.

🤖 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 `@packages/mcp/src/adapters/stdio/mcp-handler.test.ts` around lines 43 - 101,
Add a regression test in mCPHandler (stdio) that exercises the maxMessageLength
over-limit path. Extend the existing drive/createHandler test setup with an
input whose payload exceeds the limit, including a whitespace-padded case, and
assert the handler emits the expected transport error instead of processing the
message. Use the existing helpers and response assertions in mcp-handler.test.ts
to keep the test aligned with the current initialize/tools/list coverage.
🤖 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/content/docs/integrations/mcp.md`:
- Around line 167-172: The multi-surface example is incomplete because
`MCPHandler` is instantiated without the schema converters that are required
when the router uses Zod-backed schemas. Update the `handlers` example to pass
the same converter setup used elsewhere on the page into `new
MCPHandler(router)` so the MCP surface matches the documented requirement.

In `@packages/mcp/src/adapters/standard/mcp-handler-plugin.ts`:
- Line 101: The page size handling in MCPHandlerPlugin currently accepts
fractional values, which later breaks pagination between the `slice`,
`nextCursor`, and `decodeCursor` flow. Update the `MCPHandlerPlugin` option
validation so `pageSize` is only accepted when it is a positive integer, and
fall back to `DEFAULT_LIST_PAGE_SIZE` otherwise; make the same validation
consistent anywhere `pageSize` is read or used in the affected pagination paths.
- Around line 297-313: The security check in checkSecurity currently fails open
when enableDnsRebindingProtection is on but allowedOrigins or allowedHosts is
undefined, because present Origin/Host headers are skipped instead of rejected.
Update checkSecurity in mcp-handler-plugin so that when DNS-rebinding protection
is enabled, any present Origin or Host value must be explicitly allowlisted, and
if the corresponding allowlist is undefined or the value is not included, return
false. Keep the existing behavior that returns true only when no protected
header is present or it matches the configured allowlist.
- Around line 143-157: The incoming JSON-RPC validation in mcp-handler-plugin.ts
is too permissive for payload.id, allowing non-primitive ids like objects or
booleans to reach response framing. Tighten the validation path in
isValidIncoming and the request handling around the id extraction so only valid
JSON-RPC id types are treated as requests; otherwise return Invalid Request
before calling frameProcedure or any response builder. If null ids are meant to
be supported, update frameProcedure and any related request types to accept that
explicitly and keep the validation consistent with the JSON-RPC rules.

In `@packages/mcp/src/adapters/stdio/mcp-handler.ts`:
- Around line 68-73: The DoS guard in mcp-handler’s line handling is checking
the trimmed content instead of the original buffered line, so oversized requests
padded with whitespace can bypass maxMessageLength. In the logic around the
line-processing block, validate the raw line length before calling trim(), and
use the existing INVALID_REQUEST response path to reject anything over the limit
while still allowing blank lines to be skipped after the length check.

In `@packages/mcp/src/content.ts`:
- Around line 4-6: `isPlainRecord` is too broad because it treats `Date` and
class instances as records, which can leak non-plain objects into
`structuredContent` when `hasOutputSchema` is used. Tighten `isPlainRecord` in
`content.ts` to accept only real plain objects (for example, by checking the
prototype is `Object.prototype` or `null`) and make sure the `hasOutputSchema`
path that builds `structuredContent` uses this stricter guard so only
object-shaped JSON-compatible values are passed through.
- Around line 23-25: The content serialization in content.ts is treating empty
content-block arrays as plain text because the Array.isArray guard requires
length > 0. Update the logic in the content conversion path so that any array
consisting entirely of valid content blocks, including [], is returned as {
content: output } rather than being stringified, and keep the existing
isContentBlock check in the same branch.

In `@packages/mcp/src/registry.ts`:
- Around line 101-107: The MCP schema conversion path is dropping composed
schemas from convertSchemas when inputSchemas or outputSchemas contain multiple
entries, because the current object-only checks reject the returned allOf
wrapper. Update registry.ts handling in the tool input/output and prompt
argument paths to preserve composed schemas by accepting allOf via
asObjectJsonSchema instead of only plain object schemas, and merge object
members from allOf when building prompt arguments so converted metadata is not
reduced to a generic object or emptied.
- Around line 86-112: The registry currently overwrites existing MCP entries
when names or URIs collide, which can silently expose the wrong
tool/prompt/resource. Add a uniqueness guard in the registration path around
registry.tools.set, registry.prompts.set, and registry.resources.set (for
example via a shared helper like setUnique) so duplicate meta.name,
defaultName(path), or fixed uri values throw an error instead of replacing the
earlier entry. Keep the existing ToolDefinition/PromptDefinition/resource
construction flow in registry.ts, but perform the duplicate check immediately
before inserting each entry.

In `@packages/mcp/src/sdk-compliance.test.ts`:
- Around line 82-86: The test setup in sdk-compliance.test.ts is binding the
server and client to different loopback families, which causes the connection to
fail. Update the server startup in the test so the `server.listen` call and the
`StreamableHTTPClientTransport` URL use the same host family, either both on
`127.0.0.1` or both on `::1`, and keep the `Client` connection logic aligned
with that choice.

In `@packages/mcp/src/types.ts`:
- Around line 11-46: `JSONRPCId` is narrower than the actual incoming envelope
shape, so `isValidIncoming()` can accept a request with a null `id` even though
`JSONRPCRequest` and `JSONRPCIncoming` do not allow it. Update the wire types in
`types.ts` to either include `null` in `JSONRPCId`/`JSONRPCRequest` or, if null
IDs should not be supported, tighten the `isValidIncoming()` guard used before
`frameProcedure()` to reject them so the runtime behavior matches the declared
contract.

In `@playgrounds/next/package.json`:
- Line 22: Move `@orpc/mcp` from devDependencies to dependencies in the package
manifest because runtime code in the Next playground imports it from route.ts
and planet.ts. Update the package.json entry so production installs include it,
and keep the change aligned with the existing package declaration structure.

In `@playgrounds/next/src/app/mcp/`[[...rest]]/route.ts:
- Around line 6-9: Enable the DNS-rebinding guard on the MCP App Router endpoint
by wiring the new Origin/Host validation into the route’s MCPHandler setup.
Update the handler initialization in the route that mounts MCPHandler so the
browser-facing endpoint enforces the guard for requests before they reach router
handling, using the same validation mechanism added elsewhere in this PR. Refer
to the MCPHandler construction and the route handler export in this file to
apply the check consistently.

---

Nitpick comments:
In `@packages/mcp/src/adapters/stdio/mcp-handler.test.ts`:
- Around line 43-101: Add a regression test in mCPHandler (stdio) that exercises
the maxMessageLength over-limit path. Extend the existing drive/createHandler
test setup with an input whose payload exceeds the limit, including a
whitespace-padded case, and assert the handler emits the expected transport
error instead of processing the message. Use the existing helpers and response
assertions in mcp-handler.test.ts to keep the test aligned with the current
initialize/tools/list coverage.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 35a2800c-cb75-4064-9d0f-1fb6786edb6f

📥 Commits

Reviewing files that changed from the base of the PR and between 25fa31f and c4e931c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (36)
  • README.md
  • apps/content/.vitepress/config.ts
  • apps/content/docs/integrations/mcp.md
  • packages/mcp/package.json
  • packages/mcp/src/adapters/fetch/index.ts
  • packages/mcp/src/adapters/fetch/mcp-handler.test.ts
  • packages/mcp/src/adapters/fetch/mcp-handler.ts
  • packages/mcp/src/adapters/node/index.ts
  • packages/mcp/src/adapters/node/mcp-handler.test.ts
  • packages/mcp/src/adapters/node/mcp-handler.ts
  • packages/mcp/src/adapters/standard/index.ts
  • packages/mcp/src/adapters/standard/mcp-handler-codec.ts
  • packages/mcp/src/adapters/standard/mcp-handler-plugin.ts
  • packages/mcp/src/adapters/standard/mcp-pagination.test.ts
  • packages/mcp/src/adapters/standard/utils.ts
  • packages/mcp/src/adapters/stdio/index.ts
  • packages/mcp/src/adapters/stdio/mcp-handler.test.ts
  • packages/mcp/src/adapters/stdio/mcp-handler.ts
  • packages/mcp/src/constants.ts
  • packages/mcp/src/content.test.ts
  • packages/mcp/src/content.ts
  • packages/mcp/src/error.test.ts
  • packages/mcp/src/error.ts
  • packages/mcp/src/index.ts
  • packages/mcp/src/meta.test.ts
  • packages/mcp/src/meta.ts
  • packages/mcp/src/registry.test.ts
  • packages/mcp/src/registry.ts
  • packages/mcp/src/sdk-compliance.test.ts
  • packages/mcp/src/types.ts
  • packages/mcp/src/uri-template.test.ts
  • packages/mcp/src/uri-template.ts
  • packages/mcp/tsconfig.json
  • playgrounds/next/package.json
  • playgrounds/next/src/app/mcp/[[...rest]]/route.ts
  • playgrounds/next/src/routers/planet.ts

Comment thread apps/content/docs/integrations/mcp.md Outdated
Comment thread packages/mcp/src/adapters/standard/mcp-handler-plugin.ts Outdated
Comment thread packages/mcp/src/adapters/standard/mcp-handler-plugin.ts
Comment thread packages/mcp/src/adapters/standard/mcp-handler-plugin.ts
Comment thread packages/mcp/src/adapters/stdio/mcp-handler.ts Outdated
Comment thread packages/mcp/src/registry.ts
Comment thread packages/mcp/src/sdk-compliance.test.ts
Comment thread packages/mcp/src/types.ts
Comment thread playgrounds/next/package.json
Comment thread playgrounds/next/src/app/mcp/[[...rest]]/route.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

@orpc/mcp is a strong foundation, but several spec-compliance and security gaps should be closed before this lands on main: Origin validation is off by default, the MCP lifecycle is not enforced, and a handful of wire-protocol details (MCP-Protocol-Version, the completion/complete capability, JSON-RPC id handling, error payloads) deviate from MCP 2025-11-25.

Reviewed changes — adds a new MCP package that serves an oRPC router as a 2025-11-25 MCP server over fetch/node HTTP and stdio, with opt-in mcp.tool / mcp.resource / mcp.prompt meta annotations.

  • Adds packages/mcp with StandardHandler-based MCPHandlerPlugin / MCPHandlerCodec.
  • Implements tools/resources/prompts list/call/read/get, lifecycle negotiation, pagination, and error mapping.
  • Provides fetch, node, and stdio transport adapters and a Next.js playground route.
  • Adds docs, unit tests, and an e2e SDK compliance test driven by the official @modelcontextprotocol/sdk client.

⚠️ Lifecycle is not enforced

route() dispatches every method unconditionally, so a client can call tools/list, tools/call, etc. before initialize. The MCP lifecycle spec states that initialization MUST be the first interaction. The handler needs per-session handshake state, and non-lifecycle methods should be rejected until the handshake completes.

⚠️ DNS-rebinding protection is off by default

Streamable HTTP requires servers to validate the Origin header by default, yet enableDnsRebindingProtection defaults to false. Even when enabled, both allowedOrigins and allowedHosts are optional, so leaving either unset silently disables that half of the check. The inline comment on the default has more context.

⚠️ The MCP-Protocol-Version header is ignored

route() reads the JSON-RPC body but never inspects the MCP-Protocol-Version HTTP header. The spec requires validating it on subsequent HTTP requests (falling back to 2025-03-26 when absent, and returning 400 for unsupported values).

⚠️ Several protocol details don't match MCP 2025-11-25

  • completion/complete is answered, but the completions capability is never advertised.
  • resources/read with a missing or non-string uri returns -32002 instead of -32602.
  • Out-of-bounds catalog cursors silently return an empty page instead of -32602.
  • JSON-RPC id: null / object / array values flow through instead of being rejected or treated as notifications.

⚠️ Error responses can leak typed-error payloads

For resources and prompts, orpcError.toJSON() is placed verbatim in error.data, so any application data passed to ORPCError(..., { data }) is exposed to MCP clients. The inline comment points to the affected line.

⚠️ Registry can silently overwrite or drop definitions

registry.tools.set(name, ...), registry.resources.set(meta.uri, ...), and registry.prompts.set(name, ...) never detect collisions. Two procedures with the same explicit name (or default names that collide after defaultName truncation) hide each other. Separately, multi-schema outputs are merged into { allOf }, which isObjectJsonSchema rejects, so the tool outputSchema is silently dropped. Inline comments mark both sites.

⚠️ stdio transport lacks DoS and error boundaries

The stdio loop has no per-byte limit before readline buffers a line, no message-rate limit, no JSON parse-depth limit, and handler.handle rejections are unhandled. A malformed or hostile client can crash the reader or exhaust process memory. Inline comments point to the affected code.

ℹ️ Nitpicks

  • packages/mcp/src/adapters/standard/utils.ts:13-15 exports getMessageId, but it is unused; either use it to normalize JSON-RPC ids or remove it.
  • packages/mcp/src/adapters/fetch/mcp-handler.ts:39-43 and node/mcp-handler.ts:34-38 spread options (including converters) into both StandardHandler and the underlying transport handler, which leaks an internal option into the base handler contract.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

Comment thread packages/mcp/src/adapters/standard/mcp-handler-plugin.ts
Comment thread packages/mcp/src/adapters/standard/mcp-handler-plugin.ts
Comment thread packages/mcp/src/adapters/standard/mcp-handler-plugin.ts
Comment thread packages/mcp/src/adapters/standard/mcp-handler-plugin.ts
Comment thread packages/mcp/src/adapters/standard/mcp-handler-plugin.ts Outdated
Comment thread packages/mcp/src/registry.ts
Comment thread packages/mcp/src/error.ts
Comment thread packages/mcp/src/adapters/stdio/mcp-handler.ts Outdated
Comment thread packages/mcp/src/adapters/stdio/mcp-handler.ts
Comment thread packages/mcp/src/adapters/fetch/mcp-handler.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ The docs rewrite is cleaner and more consistent with the rest of the site. Two small gaps worth a look.

Reviewed changes since last review — rewrites apps/content/docs/integrations/mcp.md in the standard docs style.

  • Replaces the ad-hoc structure with Tools/Resources/Prompts/Serving/Authorization/Security sections.
  • Updates install command tag from @latest to @beta.
  • Tightens examples and removes the previous Pagination and Notes & limitations sections.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

Comment thread apps/content/docs/integrations/mcp.md
Comment thread apps/content/docs/integrations/mcp.md
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

…ness)

Verified each CodeRabbit/pullfrog finding against the code; fixed the real ones:

- security: checkSecurity no longer fails open — enabling DNS-rebinding
  protection without an allowlist now throws (was a silent no-op).
- registry: throw on duplicate tool/prompt names and resource URIs/templates
  instead of silently overwriting.
- registry: preserve composed (allOf) schemas from multiple .input()/.output()
  so tool inputSchema and prompt arguments are not dropped.
- pagination: reject non-integer pageSize; reject stale/out-of-range cursors
  with -32602.
- protocol: validate the JSON-RPC id type (reject object/boolean/null);
  resources/read with a missing/non-string uri returns -32602 (was -32002);
  removed the completion/complete stub (the capability was never advertised).
- stdio: check raw line length before trimming (whitespace-padding bypass);
  wrap dispatch in try/catch so one failure cannot tear down the reader.
- docs: the multi-surface example now passes converters.

Adds 14 regression tests. Skipped findings (false positives / opinion / by
design) are explained on the PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

dec3d6e closes most of the prior review feedback, but the DNS-rebinding guard still does not match its documented behavior and asObjectJsonSchema silently drops JSON Schema keywords when merging allOf.

Reviewed changes — the dec3d6e follow-up commit that hardens the MCP handler against prior review feedback.

  • Fixed JSON-RPC id validationisValidIncoming now rejects null/object/array ids.
  • Hardened catalog paginationpageSize must be a positive integer; stale/out-of-range cursors return -32602.
  • Removed unadvertised completion/complete — the stub route was deleted.
  • Added duplicate-name guards — registry build now throws on colliding tool/resource/prompt names.
  • Preserved composed schemasasObjectJsonSchema merges allOf object members.
  • Improved stdio resilience — raw line length is checked before trimming; handler.handle errors no longer crash the reader.
  • Fixed resources/read error codes — missing/non-string uri returns -32602.
  • Added mcp-hardening.test.ts — covers id validation, pagination, DNS rebinding, error codes, and registry integrity.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

Comment thread packages/mcp/src/adapters/standard/mcp-handler-plugin.ts
Comment thread packages/mcp/src/adapters/standard/mcp-handler-plugin.ts
Comment thread packages/mcp/src/registry.ts
Comment thread packages/mcp/src/adapters/standard/utils.ts Outdated
Resolve conflicts:
- README.md: keep the @orpc/mcp entry plus main's corrected @orpc/next line.
- pnpm-lock.yaml: take main's lockfile (already includes the @orpc/mcp importer);
  frozen-lockfile install passes.

Note: the repo-wide `lint_and_typecheck` is red because of a pre-existing
undici-types/FormData type error on main (origin/main e9cc1ee also fails it);
@orpc/mcp itself type-checks clean, and lint + the full test suite pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwXhtSxKx3fpdk3b9mEttL
@mi3lix9

mi3lix9 commented Jun 29, 2026

Copy link
Copy Markdown
Author

@dinwwwh please review the changes and tell me if we need any extra change, I have tested this on my machine with Claude Code and it works fine!

@dinwwwh

dinwwwh commented Jun 30, 2026

Copy link
Copy Markdown
Member

I'm still getting familiar with MCP, so this PR is taking me a bit longer.

@mi3lix9

mi3lix9 commented Jun 30, 2026

Copy link
Copy Markdown
Author

I'm still getting familiar with MCP, so this PR is taking me a bit longer.

Take your time bro, for me I used the official MCP website as a reference, and test it in Claude Code.

return jsonRpc(403, null, { error: { code: FORBIDDEN_ERROR, message: 'Origin not allowed' } })
}

// 2. MCP uses HTTP POST. (GET SSE / DELETE sessions are not implemented yet.)

@dinwwwh dinwwwh Jun 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can check the batch plugin to see how to handle batching or long-lived sessions. (In short, you can call next multiple times) for simple just ignore it

Also, in oRPC, SSE is an async iterator (EventStreamBody) from Standard Server, so it works for both request and response bodies: https://github.com/middleapi/standardserver/#event-stream-body

Comment thread packages/mcp/src/meta.ts
uri?: `${string}://${string}` | undefined

/** Resource-only: RFC 6570-style URI template (e.g. `planet://{id}`); vars map to input. */
uriTemplate?: string | undefined

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why not merge uri and uriTemplate into one?

return jsonRpc(200, id, { result })
}
catch (error) {
const jsonRpcError = error instanceof JSONRPCError

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  • Why not handle errors in mcp-handler-codec instead of try/catch here?
  • Instead of relying on JSONRPCError, why not just use ORPCError?

* Read + parse the JSON-RPC body of an MCP request (once per request).
* The returned promise rejects if the body is not valid JSON.
*/
export function readMCPPayload(request: StandardLazyRequest): Promise<unknown> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

WARNING: anti pattern, request instance can be changed during lifecycle

Comment thread packages/mcp/package.json
@@ -0,0 +1,75 @@
{
"name": "@orpc/mcp",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should use the name @orpc/experimental-mcp. There's no way this will be stable anytime soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request javascript Pull requests that update javascript code size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants