Skip to content

feat(api): remote MCP server at /api/mcp + keyless key-management routes#224

Merged
duyet merged 2 commits into
mainfrom
u1-remote-mcp-v2
Jun 18, 2026
Merged

feat(api): remote MCP server at /api/mcp + keyless key-management routes#224
duyet merged 2 commits into
mainfrom
u1-remote-mcp-v2

Conversation

@duyet

@duyet duyet commented Jun 18, 2026

Copy link
Copy Markdown
Owner

What

Adds a remote MCP server at POST /api/mcp (MCP spec 2025-06-18, stateless Streamable HTTP) so agents can connect over the network with an AgentState API key or an OAuth/capability access token — no local stdio bridge required. Also adds keyless API-key management REST routes so non-dashboard clients (SDK / stdio MCP) can manage keys with just their API key.

This is unit U1 of the larger remote-MCP + OAuth + scoped-keys feature. It composes with U2 (OAuth): the 401 challenge points at /.well-known/oauth-protected-resource, which U2's discovery router serves.

Changes

  • middleware/mcp-auth.ts — accepts as_live_ API keys and as_cap_ capability tokens (honoring expiry/revocation). Sets projectId + capabilityScopes. On missing/invalid token returns 401 with WWW-Authenticate: Bearer resource_metadata="<origin>/.well-known/oauth-protected-resource". Keeps the 300ms constant-time failure delay. Normalizes legacy capability scopes (lease:write/claim:write) to API form (leases:write/claims:write) so capability tokens can satisfy the lease/claim tools they were minted for.
  • routes/mcp/index.ts — JSON-RPC dispatcher: initialize (with protocol-version negotiation), tools/list, tools/call, ping, notifications→202, unknown method→-32601, GET405. Origin check for DNS-rebinding protection. Tool failures returned as isError results; unexpected errors logged server-side + generic message returned (no internal leak).
  • routes/mcp/tools.ts — 15-tool registry calling the conversation / state / lease / claim / key services directly (project from auth context). Reuses the stdio MCP's tool names, descriptions, and zod input shapes; zod-to-json-schema generates the JSON Schemas for tools/list. New tools: create_api_key / list_api_keys / revoke_api_key with the same subset-delegation rule as the dashboard key routes.
  • routes/v1-keys.ts — keyless POST / GET / DELETE /api/v1/keys (project from auth context, never another project's keys).
  • index.ts — mounts both routers.
  • Adds zod-to-json-schema dependency.

Tests

test/mcp.test.ts (12 tests) covers: initialize serverInfo + version negotiation, tools/list, tools/call (list + store→recall round-trip), scope enforcement (scoped key out-of-scope → isError), capability-token lease delegation (singular→plural scope normalization), 401 + WWW-Authenticate, GET405. Full API suite: 302 passing. Typecheck + biome clean. Verified end-to-end against wrangler dev.

Review

Ran /code-review (xhigh). Fixed the one high-severity finding it surfaced: capability tokens carry singular scope forms but lease/claim tools required the plural API forms, so capability-token delegation for leases/claims would always 403 — fixed by normalizing at the auth boundary (now covered by a test). Also addressed protocol-version negotiation and internal-error logging/leak.

Follow-up (not in this PR): the child-scope delegation block is now duplicated across routes/keys.ts, routes/v1-keys.ts, and routes/mcp/tools.ts. Consider extracting a shared resolveChildScopes() helper — kept consistent-with-existing rather than refactoring the foundation here.

Co-Authored-By: Duyet Le [email protected]
Co-Authored-By: duyetbot [email protected]

Add a stateless Streamable HTTP MCP server (spec 2025-06-18) at POST /api/mcp
so agents can connect over the network with an AgentState API key or an
OAuth/capability access token, instead of running the local stdio bridge.

- middleware/mcp-auth.ts: accepts `as_live_` keys and `as_cap_` capability
  tokens (honoring expiry/revocation), sets projectId + capabilityScopes, and
  returns 401 with `WWW-Authenticate: Bearer resource_metadata="..."` pointing
  at the OAuth protected-resource document. Normalizes legacy capability scopes
  (lease:write/claim:write) to their API form (leases:write/claims:write) so a
  capability token can satisfy the lease/claim tools it was minted for.
- routes/mcp: JSON-RPC dispatcher (initialize / tools/list / tools/call / ping)
  + a 15-tool registry calling the conversation/state/lease/claim/key services
  directly. Per-tool scope enforcement; tool failures returned as isError
  results, protocol errors as JSON-RPC errors. Tools reuse the stdio MCP's tool
  names, descriptions, and zod input shapes (zod-to-json-schema for tools/list).
  New tools: create_api_key / list_api_keys / revoke_api_key with the same
  subset-delegation rule as the dashboard key routes.
- routes/v1-keys.ts: keyless POST/GET/DELETE /api/v1/keys so SDK/stdio clients
  can manage keys using only their API key (project from auth context).
- Mount both routers in index.ts.

Tests: test/mcp.test.ts covers initialize, tools/list, tools/call, scope
enforcement, capability-token lease delegation, protocol-version negotiation,
401 challenge, and 405 on GET. Full suite: 302 passing.

Co-Authored-By: Duyet Le <[email protected]>
Co-Authored-By: duyetbot <[email protected]>

@sourcery-ai sourcery-ai 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.

Sorry @duyet, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@duyet, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 56 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: af91a82c-c5c2-4ad4-9699-adb094b2026d

📥 Commits

Reviewing files that changed from the base of the PR and between 5517c4b and 18f7c93.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • packages/api/package.json
  • packages/api/src/index.ts
  • packages/api/src/middleware/mcp-auth.ts
  • packages/api/src/routes/mcp/index.ts
  • packages/api/src/routes/mcp/tools.ts
  • packages/api/src/routes/v1-keys.ts
  • packages/api/test/mcp.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch u1-remote-mcp-v2

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.

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

Code Review

This pull request introduces a remote Model Context Protocol (MCP) server over a stateless Streamable HTTP transport at /api/mcp, along with keyless API-key management at /api/v1/keys for non-dashboard clients. It adds custom authentication middleware (mcpAuth) to support both regular API keys and capability tokens, registers a comprehensive suite of tools (for conversations, state, leases, claims, and keys), and includes extensive integration tests. The review feedback highlights critical improvement opportunities in the tool handlers: specifically, the create_claim and mint_capability_token handlers should check for domain errors and throw a ToolError rather than returning service results directly, and the revoke_api_key handler should use the shared invalidateAuthCacheEntries helper to keep cache invalidation consistent and DRY.

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 on lines +445 to +453
return claimsService.createClaim(c.get("db"), {
projectId: c.get("projectId"),
subjectType: args.subject_type,
subjectId: args.subject_id,
statement: args.statement,
evidence: args.evidence.map(mapClaimEvidence),
});
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The create_claim tool handler returns the result of claimsService.createClaim directly without checking for a domain error. In this codebase, services typically return an object containing an optional error property on failure. If createClaim fails and returns { error }, the MCP client will receive a successful JSON-RPC response containing the error object instead of a proper JSON-RPC error with isError: true.\n\nWe should check result.error and throw a ToolError to ensure correct error propagation, matching the pattern used in other tool handlers (e.g., store_conversation, upsert_state).

    handler: async (c, args: z.infer<typeof createClaimSchema>) => {\n      const result = await claimsService.createClaim(c.get('db'), {\n        projectId: c.get('projectId'),\n        subjectType: args.subject_type,\n        subjectId: args.subject_id,\n        statement: args.statement,\n        evidence: args.evidence.map(mapClaimEvidence),\n      });\n      if (result.error) throw new ToolError(result.error.code, result.error.message);\n      return result;\n    },

Comment on lines +487 to +491
return capabilityTokensService.createCapabilityToken(c.get("db"), c.get("projectId"), {
name: args.name,
scopes: args.scopes,
expires_at: args.expires_at,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Similar to create_claim, the mint_capability_token tool handler returns the result of capabilityTokensService.createCapabilityToken directly without checking for a domain error. If the service fails and returns { error }, the tool will return a successful JSON-RPC response containing the error object instead of throwing a ToolError with isError: true. We should check result.error and throw a ToolError to ensure correct error propagation.

      const result = await capabilityTokensService.createCapabilityToken(c.get('db'), c.get('projectId'), {\n        name: args.name,\n        scopes: args.scopes,\n        expires_at: args.expires_at,\n      });\n      if (result.error) throw new ToolError(result.error.code, result.error.message);\n      return result;

Comment on lines +570 to +573
const cache = c.env.AUTH_CACHE;
if (cache) {
c.executionCtx.waitUntil(cache.delete(`auth:hash:${revokedHash}`));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To keep cache invalidation consistent and DRY, use the shared invalidateAuthCacheEntries helper instead of manually deleting the cache key. This ensures that if the cache key structure or invalidation logic changes in the future, it is updated in one place and prevents potential authorization bypasses due to stale cache.\n\nNote: You will need to import invalidateAuthCacheEntries from ../../lib/helpers at the top of the file.

Suggested change
const cache = c.env.AUTH_CACHE;
if (cache) {
c.executionCtx.waitUntil(cache.delete(`auth:hash:${revokedHash}`));
}
invalidateAuthCacheEntries(c, [revokedHash]);

# Conflicts:
#	packages/api/src/index.ts
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​zod-to-json-schema@​3.25.210010010083100

View full report

@duyet duyet merged commit 42a2323 into main Jun 18, 2026
6 checks passed
@duyet duyet deleted the u1-remote-mcp-v2 branch June 18, 2026 11:46
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