feat(mcp): expose the Company Brain as MCP servers - #23
Conversation
Adds Model Context Protocol support so an organization can expose its Company Brain to external AI tools (Claude Desktop, Cursor, VS Code, Continue, Cline, OpenAI Agents) as a permission-aware, always-live context provider. - Schema: McpServer, McpApiKey (hashed, mirrors APIKey), McpConnection. - Retrieval: fail-closed KnowledgeScopeFilter in the scoped sources + resolveScopeFilter/parseScopeConfig, so a scoped server is confined to a provable knowledge slice (workspace mode stays unrestricted). - API: management REST (/api/v1/mcp-servers CRUD + key create/rotate/ revoke, JWT-guarded) and a stateless Streamable HTTP protocol endpoint (/api/v1/mcp/:id) authed by a hashed key, exposing read-only scoped tools (search_knowledge, get_knowledge_object, query_graph, list_projects, list_recent_meetings) backed by the existing retrieval + knowledge graph. - Web: MCP Servers section (registry, create flow, detail page with server URL, key management, and copy/download connect configs) + nav. Tools read live retrieval/graph, so the server always reflects the latest Brain with no rebuild. Verified end-to-end against the running protocol endpoint (initialize, tools/list, scoped tools/call, 401 on bad key) plus unit tests for the fail-closed scope resolver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds MCP server persistence, scoped retrieval, API-key authentication, Streamable HTTP tools, and web management pages. The implementation includes server CRUD, key lifecycle operations, connection tracking, five MCP tools, and client configuration downloads. ChangesMCP platform
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (5)
apps/web/src/app/(app)/mcp/page.tsx (1)
16-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShared helper lives in an App Router page module.
scopeSummaryis defined and exported from a routepage.tsx, and the detail page imports it from that route module. This couples two route bundles and relies on a non-standard export from a page file, which Route Export Validation can reject.
apps/web/src/app/(app)/mcp/page.tsx#L16-L24: movescopeSummaryto a shared module such asapps/web/src/lib/mcp.tsand import it here.apps/web/src/app/(app)/mcp/[id]/page.tsx#L17-L17: replaceimport { scopeSummary } from '../page'with an import from the new shared module.🤖 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/web/src/app/`(app)/mcp/page.tsx around lines 16 - 24, Move the exported scopeSummary helper out of the App Router page module into a shared module such as apps/web/src/lib/mcp.ts, preserving its behavior; update apps/web/src/app/(app)/mcp/page.tsx#L16-L24 to import it from there, and replace the import in apps/web/src/app/(app)/mcp/[id]/page.tsx#L17 with the shared-module import.apps/api/prisma/schema.prisma (1)
407-414: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winMake
clientNamenon-null so the unique constraint deduplicates.
clientNameis nullable and takes part in@@unique([mcpServerId, clientName]). PostgreSQL treats NULLs as distinct, so rows with a NULLclientNamecan duplicate for the same server and inflateconnectionCount.McpService.recordConnectionalready coerces null to'unknown', so the model can declare the column non-null.♻️ Proposed schema change
- clientName String? + clientName String `@default`("unknown") clientVersion String?Add the matching migration statements:
UPDATE "mcp_connections" SET "clientName" = 'unknown' WHERE "clientName" IS NULL; ALTER TABLE "mcp_connections" ALTER COLUMN "clientName" SET DEFAULT 'unknown'; ALTER TABLE "mcp_connections" ALTER COLUMN "clientName" SET NOT NULL;🤖 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/api/prisma/schema.prisma` around lines 407 - 414, Update the mcp_connections model so clientName is required with a default of 'unknown' instead of nullable, preserving the @@unique([mcpServerId, clientName]) constraint. Add a migration that backfills existing NULL clientName values to 'unknown', sets the column default, and then enforces NOT NULL; keep McpService.recordConnection’s existing null coercion behavior.apps/api/src/modules/mcp/mcp.tools.ts (1)
15-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueIndex the catalog by name instead of by position.
Each
registerToolcall reads its description through a positional index, for exampleTOOL_CATALOG[0].description. If someone reorders or inserts an entry, the descriptions silently attach to the wrong tools. A name-keyed lookup removes the coupling.♻️ Proposed refactor
export const ALL_TOOL_NAMES: string[] = TOOL_CATALOG.map((t) => t.name); + +const DESCRIPTIONS: Record<string, string> = Object.fromEntries( + TOOL_CATALOG.map((t) => [t.name, t.description]), +);Then use
DESCRIPTIONS.search_knowledge,DESCRIPTIONS.get_knowledge_object, and so on.🤖 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/api/src/modules/mcp/mcp.tools.ts` around lines 15 - 30, Replace the positional TOOL_CATALOG structure with a name-keyed DESCRIPTIONS lookup, preserving each tool’s existing description. Update every registerTool call to read the description via its tool-name key, such as DESCRIPTIONS.search_knowledge, and retain ALL_TOOL_NAMES derived from the catalog or equivalent name collection without relying on entry positions.apps/api/src/modules/mcp/mcp.routes.ts (1)
30-41: 🚀 Performance & Scalability | 🔵 TrivialConsider the per-request cost of rebuilding the scoped server.
buildScopedMcpServer(and itsresolveScopeFiltercall chain) runs on every protocol POST, not only oninitialize. This is an intentional stateless design per the code comments, trading per-request DB work for horizontal scalability without session pinning. Under sustained tool-call traffic, consider a short-lived cache of the resolved scope filter keyed by server id (invalidated on server update) to reduce DB round trips while still remaining close to real time.Also applies to: 209-214
🤖 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/api/src/modules/mcp/mcp.routes.ts` around lines 30 - 41, Cache the resolved scope filter used by buildScopedMcpServer and its resolveScopeFilter call chain, keyed by server id, with a short TTL to reduce repeated database lookups during protocol POST requests. Invalidate the cached entry whenever the corresponding server is updated, while preserving the stateless request handling and near-real-time scope behavior.packages/retrieval/src/scope.ts (1)
58-122: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider parallelizing the independent lookups.
resolveScopeFilteris resolved on every MCP protocol request (perbuildScopedMcpServerinapps/api/src/modules/mcp/mcp.server.ts:18-44). The document lookup (lines 75-80) and the member-email lookup (lines 97-100) don't depend on each other and currently run sequentially. Running them withPromise.allreduces the per-request latency on this hot path.🤖 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/retrieval/src/scope.ts` around lines 58 - 122, Parallelize the independent document and member-email lookups in resolveScopeFilter using Promise.all, while preserving the existing conditional queries and result handling. Ensure document IDs are available before the dependent knowledgeObject lookup, and continue adding matched meeting IDs to meetingIds after the parallel results resolve.
🤖 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/api/src/modules/mcp/mcp.routes.ts`:
- Around line 201-207: Add a rejection handler to the fire-and-forget
service.recordConnection call in the initialize branch, logging the asynchronous
failure through the route’s existing logger or error-reporting mechanism so it
remains contained rather than becoming an unhandled promise rejection.
- Around line 209-224: Update the MCP route handler around reply.hijack(),
mcp.connect(), and transport.handleRequest() to wrap the post-hijack async
operations in try/catch. On rejection, send a valid error response directly
through the raw response before the stream can hang, while preserving the
existing close cleanup and successful transport handling.
In `@apps/api/src/modules/mcp/mcp.schemas.ts`:
- Around line 22-23: Update the tools field in both create and update schemas to
validate entries against the known catalog using ALL_TOOL_NAMES or TOOL_CATALOG,
rejecting unknown tool names instead of allowing them to be silently discarded
by persistence.
In `@apps/api/src/modules/mcp/mcp.service.ts`:
- Around line 166-180: Update rotateKey to reject the existing key when
existing.revokedAt is set, before revoking or issuing a replacement. When
constructing the issueKey options, only pass expiresAt if existing.expiresAt is
still in the future; otherwise omit it so the rotated key receives the normal
non-expiring/default expiry behavior.
- Around line 33-46: Update resolveOrganization and the MCP route handlers to
enforce authorization: require admin or owner membership for MCP server
creation, updates, and deletion; require admin membership for key creation,
rotation, revocation, and server-detail responses that include key or connection
lists. Preserve authentication and reject unauthorized memberships before
performing management operations.
In `@apps/api/src/modules/mcp/mcp.tools.ts`:
- Around line 53-54: Make the persisted tool configuration authoritative by
removing the empty-set bypass from registerTools’s on helper, so only names
present in ctx.enabledTools are registered. Update McpService.update to reject
an empty tools array or normalize it to ALL_TOOL_NAMES, matching
McpService.create and ensuring persisted servers always retain a non-empty tool
set.
In `@apps/web/src/app/`(app)/mcp/[id]/page.tsx:
- Around line 124-149: Update rotateKey, revokeKey, and deleteServer to catch
rejected API requests and surface action-specific errors inline without
replacing the whole page. Follow the existing createKey error-handling pattern,
while preserving the finally-based busy-state cleanup in rotateKey and revokeKey
and only navigating in deleteServer after a successful deletion.
In `@apps/web/src/app/`(app)/mcp/new/page.tsx:
- Around line 50-72: Update submit to validate scoped mode before creating the
request: when mode is "scoped", require at least one non-empty value across
projectIds, memberIds, documentIds, or meetingIds. Set the existing error state
and return without submitting when all four dimensions are empty, while
preserving workspace behavior and valid scoped submissions.
In `@apps/web/src/lib/api.ts`:
- Around line 318-327: Update the declared return types of createMcpServer and
updateMcpServer to match their actual API payloads: the POST response should
omit keys, connections, connectionCount, and keyCount while retaining url, and
the PATCH response should omit url, connectionCount, and keyCount while
retaining the raw server fields. Reuse existing compatible type utilities or
define narrowly scoped response types rather than declaring McpServerDetail or
McpServerSummary.
In `@packages/retrieval/src/scope.ts`:
- Around line 96-101: Update the member email lookup in the surrounding scope
function to filter through active Membership records using the current
organizationId and memberIds, then fetch emails only for the resulting user IDs.
Replace the direct User.findMany filter with this membership-scoped flow while
preserving the existing email mapping and filtering behavior.
---
Nitpick comments:
In `@apps/api/prisma/schema.prisma`:
- Around line 407-414: Update the mcp_connections model so clientName is
required with a default of 'unknown' instead of nullable, preserving the
@@unique([mcpServerId, clientName]) constraint. Add a migration that backfills
existing NULL clientName values to 'unknown', sets the column default, and then
enforces NOT NULL; keep McpService.recordConnection’s existing null coercion
behavior.
In `@apps/api/src/modules/mcp/mcp.routes.ts`:
- Around line 30-41: Cache the resolved scope filter used by
buildScopedMcpServer and its resolveScopeFilter call chain, keyed by server id,
with a short TTL to reduce repeated database lookups during protocol POST
requests. Invalidate the cached entry whenever the corresponding server is
updated, while preserving the stateless request handling and near-real-time
scope behavior.
In `@apps/api/src/modules/mcp/mcp.tools.ts`:
- Around line 15-30: Replace the positional TOOL_CATALOG structure with a
name-keyed DESCRIPTIONS lookup, preserving each tool’s existing description.
Update every registerTool call to read the description via its tool-name key,
such as DESCRIPTIONS.search_knowledge, and retain ALL_TOOL_NAMES derived from
the catalog or equivalent name collection without relying on entry positions.
In `@apps/web/src/app/`(app)/mcp/page.tsx:
- Around line 16-24: Move the exported scopeSummary helper out of the App Router
page module into a shared module such as apps/web/src/lib/mcp.ts, preserving its
behavior; update apps/web/src/app/(app)/mcp/page.tsx#L16-L24 to import it from
there, and replace the import in apps/web/src/app/(app)/mcp/[id]/page.tsx#L17
with the shared-module import.
In `@packages/retrieval/src/scope.ts`:
- Around line 58-122: Parallelize the independent document and member-email
lookups in resolveScopeFilter using Promise.all, while preserving the existing
conditional queries and result handling. Ensure document IDs are available
before the dependent knowledgeObject lookup, and continue adding matched meeting
IDs to meetingIds after the parallel results resolve.
🪄 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 Plus
Run ID: 11fae07a-ed7c-4e08-a889-0a730096e1fc
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (21)
apps/api/package.jsonapps/api/prisma/migrations/20260731095218_mcp_servers/migration.sqlapps/api/prisma/schema.prismaapps/api/src/app.tsapps/api/src/modules/mcp/mcp.keys.tsapps/api/src/modules/mcp/mcp.routes.tsapps/api/src/modules/mcp/mcp.schemas.tsapps/api/src/modules/mcp/mcp.server.tsapps/api/src/modules/mcp/mcp.service.tsapps/api/src/modules/mcp/mcp.tools.tsapps/web/src/app/(app)/mcp/[id]/page.tsxapps/web/src/app/(app)/mcp/new/page.tsxapps/web/src/app/(app)/mcp/page.tsxapps/web/src/lib/api.tsapps/web/src/lib/nav.tspackages/retrieval/src/index.tspackages/retrieval/src/scope.test.tspackages/retrieval/src/scope.tspackages/retrieval/src/scoped-retrieval.tspackages/retrieval/src/sources.tspackages/retrieval/src/types.ts
| if (body?.method === 'initialize') { | ||
| void service.recordConnection( | ||
| resolved.server.id, | ||
| body.params?.clientInfo?.name ?? null, | ||
| body.params?.clientInfo?.version ?? null, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Add error handling to the fire-and-forget recordConnection call.
void service.recordConnection(...) has no .catch(). If the write fails asynchronously, it becomes an unhandled promise rejection instead of a logged, contained failure.
🛠️ Proposed fix
if (body?.method === 'initialize') {
- void service.recordConnection(
+ service.recordConnection(
resolved.server.id,
body.params?.clientInfo?.name ?? null,
body.params?.clientInfo?.version ?? null,
- );
+ ).catch((err) => request.log.error(err, 'Failed to record MCP connection'));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (body?.method === 'initialize') { | |
| void service.recordConnection( | |
| resolved.server.id, | |
| body.params?.clientInfo?.name ?? null, | |
| body.params?.clientInfo?.version ?? null, | |
| ); | |
| } | |
| if (body?.method === 'initialize') { | |
| service.recordConnection( | |
| resolved.server.id, | |
| body.params?.clientInfo?.name ?? null, | |
| body.params?.clientInfo?.version ?? null, | |
| ).catch((err) => request.log.error(err, 'Failed to record MCP connection')); | |
| } |
🤖 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/api/src/modules/mcp/mcp.routes.ts` around lines 201 - 207, Add a
rejection handler to the fire-and-forget service.recordConnection call in the
initialize branch, logging the asynchronous failure through the route’s existing
logger or error-reporting mechanism so it remains contained rather than becoming
an unhandled promise rejection.
| const mcp = await buildScopedMcpServer({ | ||
| prisma: app.prisma, | ||
| retrieval, | ||
| graph, | ||
| record: resolved.server, | ||
| }); | ||
| const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); | ||
| reply.raw.on('close', () => { | ||
| void transport.close(); | ||
| void mcp.close(); | ||
| }); | ||
| // Hand the raw Node req/res to the transport; Fastify must not also respond. | ||
| reply.hijack(); | ||
| await mcp.connect(transport); | ||
| await transport.handleRequest(request.raw, reply.raw, request.body); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does @modelcontextprotocol/sdk 1.30.0 StreamableHTTPServerTransport.handleRequest catch and respond to internal errors itself, or does it propagate them to the caller?
💡 Result:
In @modelcontextprotocol/sdk 1.30.0, the StreamableHTTPServerTransport.handleRequest method (and its underlying implementation) both catches internal errors and reports them to the caller through an onerror callback [1][2][3]. When the transport encounters an error, it performs two distinct actions: 1. Responding to the HTTP Client: It attempts to return a JSON-RPC error response (with appropriate HTTP status codes, such as 400 or 500) to the caller [3][4]. 2. Reporting via Callback: It invokes the optional onerror callback (if provided) with an Error object, allowing the server implementation to log the error, track it for observability, or perform custom cleanup [2][5][3]. The SDK includes specific logic to catch various transport-level errors—including request parsing failures, validation errors, and protocol issues—to ensure they are consistently surfaced through both the HTTP response and the onerror reporting mechanism [1][2]. The implementation does not propagate exceptions up the promise chain to the caller of handleRequest in a way that would crash the server; instead, it encapsulates these as handled operational errors [3][4].
Citations:
- 1: fix(server): surface parse/validation transport errors via onerror in streamable HTTP modelcontextprotocol/typescript-sdk#1684
- 2: fix(server): call onerror callback for all transport errors modelcontextprotocol/typescript-sdk#1433
- 3: https://github.com/modelcontextprotocol/typescript-sdk/blob/e74a358728991216391995e8daa5d0573614abc5/src/server/streamableHttp.ts
- 4: https://github.com/modelcontextprotocol/servers/blob/main/src/everything/transports/streamableHttp.ts
- 5: https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/server/server/streamableHttp.html
🌐 Web query:
@modelcontextprotocol/sdk 1.30.0 StreamableHTTPServerTransport handleRequest source await mcpServer handleRequest
💡 Result:
In the Model Context Protocol (MCP) TypeScript SDK, the StreamableHTTPServerTransport is designed to bridge incoming HTTP requests to an MCP server [1][2]. The handleRequest method is the primary entry point for processing these requests [1][2]. When using an McpServer instance, you typically call its handleRequest method directly with the parsed request body, which then interacts with the transport layer [3][4][5]. However, if you are working directly with the transport layer (e.g., in a custom integration or framework-specific route handler), the StreamableHTTPServerTransport.handleRequest method handles the full request-response lifecycle, including HTTP header validation, session management, and SSE streaming [1][2]. Key details regarding the interaction include: 1. Request Handling: The transport's handleRequest method signature typically accepts an incoming HTTP request (e.g., IncomingMessage in Node.js or a Web Standard Request) and a server response object [1][6][2]. It processes the request, manages the session (if stateful), and ensures the response is correctly formatted as JSON-RPC or an SSE stream [1][6][2]. 2. Stateless vs. Stateful: By configuring the sessionIdGenerator to undefined, the transport operates in a stateless mode [6][7]. This is often preferred for serverless environments (e.g., Cloudflare Workers, AWS Lambda) where holding in-memory session state across different function invocations is not feasible [8]. 3. Implementation Pattern: Common implementations involve an HTTP route handler (e.g., in Hono, Express, or Next.js) that passes the request and response to the transport's handleRequest method [5][9]. If using the McpServer class, it manages the connection to the transport, and incoming messages are often processed via mcpServer.handleRequest(body) [3][4]. For detailed usage, the official documentation highlights that the Streamable HTTP transport is the recommended standard for remote servers and modern MCP integrations [10][11]. If you encounter specific issues—such as requests returning 500 or 400 errors in stateless mode—ensure your SDK version is up to date, as several regressions related to transport reuse and session handling in serverless environments were addressed in versions following 1.25.0 [12][8].
Citations:
- 1: https://cdn.jsdelivr.net/npm/@modelcontextprotocol/sdk@1.20.2/dist/esm/server/streamableHttp.d.ts
- 2: https://ts.sdk.modelcontextprotocol.io/v2/classes/_modelcontextprotocol_node.streamableHttp.NodeStreamableHTTPServerTransport.html
- 3: https://raw.githubusercontent.com/NeverSight/skills_feed/refs/heads/main/data/skills-md/jezweb/claude-skills/openai-apps-mcp/SKILL.md
- 4: https://www.skillavatars.com/skills/openai-apps-mcp
- 5: https://agentpedia.codes/agent-skills/ai-tools/openai-apps-mcp
- 6: https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/server/server/streamableHttp.html
- 7: https://github.com/modelcontextprotocol/typescript-sdk/blob/2c0c481cb9dbfd15c8613f765c940a5f5bace94d/packages/server/src/server/streamableHttp.ts
- 8: https://ai.codefather.cn/skills/2014279370171428868
- 9: https://zenn.dev/sbk0716/articles/ac111eca8ec263
- 10: https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.29.0/docs/server.md
- 11: https://ts.sdk.modelcontextprotocol.io/documents/server.html
- 12: Stateless StreamableHTTPServerTransport: non-initialize requests return 500 with empty body when transport is reused (regression vs 1.24.3) modelcontextprotocol/typescript-sdk#1994
🌐 Web query:
github @modelcontextprotocol/typescript-sdk v1.30.0 packages/server/src/server/streamableHttp.ts handleRequest response.end try catch
💡 Result:
In the Model Context Protocol (MCP) TypeScript SDK, the NodeStreamableHTTPServerTransport (which wraps WebStandardStreamableHTTPServerTransport) handles incoming requests via the handleRequest method [1][2][3]. Regarding your specific interest in response.end and try...catch patterns within handleRequest for streamableHttp.ts: 1. Architecture: The WebStandardStreamableHTTPServerTransport is designed to be environment-agnostic, using Web Standard APIs (Request, Response, ReadableStream) [2][3]. The NodeStreamableHTTPServerTransport provides the specific glue for Node.js IncomingMessage and ServerResponse objects [1]. 2. Implementation Pattern: In standard implementations of this transport, handleRequest generally does not wrap the entire request lifecycle in a single try...catch block that terminates with response.end. Instead, the logic typically delegates to specific handlers (handlePostRequest, handleGetRequest, handleDeleteRequest) based on the HTTP method [4][5]. 3. Recent Updates: Version 1.30.0 of the MCP TypeScript SDK introduced enhancements for SSE (Server-Sent Events) keepalive functionality [6][7]. The keepalive implementation does utilize try...catch blocks within the setInterval logic responsible for enqueuing : keepalive comment frames to the ReadableStreamDefaultController to prevent errors from crashing the timer if the stream is already closed or cancelled [6]. 4. Error Handling: In the context of MCP transports, errors during request handling are often managed by emitting JSON-RPC error responses or delegating to the underlying transport's error handling mechanisms rather than relying on a global try...catch at the handleRequest level that calls response.end directly [4][8]. If you are seeing a specific try...catch implementation involving response.end in your local version or a custom wrapper, note that common patterns for Node.js servers typically require response.end() to be called within a finally block or specific error-handling branches to ensure resources are released, especially if an error occurs after headers have been sent [9][8]. For the most accurate assessment of the source code in v1.30.0, you can review the repository's packages/server/src/server/ directory, specifically looking at how the NodeStreamableHTTPServerTransport class (if used) or the underlying WebStandardStreamableHTTPServerTransport delegates stream responses [10][7][3].
Citations:
- 1: https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.29.0/src/server/streamableHttp.ts
- 2: https://github.com/modelcontextprotocol/typescript-sdk/blob/2c0c481cb9dbfd15c8613f765c940a5f5bace94d/packages/server/src/server/streamableHttp.ts
- 3: https://github.com/modelcontextprotocol/typescript-sdk/blob/7d7e62cc/packages/server/src/server/streamableHttp.ts
- 4: https://github.com/modelcontextprotocol/typescript-sdk/blob/621ccea997bf318ee99c7f64ce19609e838615fa/src/server/streamableHttp.ts
- 5: Subclassing McpServer causes tool invocation issues modelcontextprotocol/typescript-sdk#451
- 6: modelcontextprotocol/typescript-sdk@d618283
- 7: modelcontextprotocol/typescript-sdk@v1.29.0...1.30.0
- 8: https://github.com/maddada/Ghostex/blob/main/gxserver/src/server.ts
- 9: https://github.com/lukey662/agentsandskills/blob/main/src/studio/studio-server.ts
- 10: https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/server/server/streamableHttp.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Find MCP route/service files"
fd -a 'mcp\.routes\.ts|mcp\.service\.ts|mcp\.(routes|service)\.ts$' . | sed 's#^\./##'
echo
echo "Relevant route file excerpts"
file=$(fd 'mcp\.routes\.ts$' apps/api/src/modules/mcp | head -n 1)
if [ -n "${file:-}" ]; then
wc -l "$file"
sed -n '160,240p' "$file" | cat -n
fi
echo
echo "Search for MCP server connect/handleRequest usage"
rg -n "StreamableHTTPServerTransport|mcp\.connect|handleRequest|recordConnection|server\.status" apps/api/src/modules/mcp || trueRepository: shibadityadeb/Context-Brain
Length of output: 4389
🌐 Web query:
@modelcontextprotocol/sdk v1.30.0 McpServer connect transport handleRequest source
💡 Result:
In the Model Context Protocol (MCP) TypeScript SDK, the interaction between McpServer, transports, and request handling is designed to decouple the server logic from the underlying communication medium [1][2]. McpServer.connect(transport) The McpServer.connect(transport) method is used to attach a transport implementation to your MCP server instance [3][2]. By calling this method, the server assumes ownership of the transport, initiates the connection, and begins listening for incoming JSON-RPC messages [3][4]. Transport.handleRequest The transport.handleRequest method is specific to HTTP-based transports (such as Streamable HTTP) [5][6]. It is not a general method for all transport types; for example, stdio transports operate over stdin/stdout and do not expose a handleRequest method [1][2]. When using Streamable HTTP transports (e.g., WebStandardStreamableHTTPServerTransport or NodeStreamableHTTPServerTransport), handleRequest serves as the entry point for incoming HTTP requests (GET, POST, or DELETE) [5][6]. It validates the request, manages session state (if applicable), and routes the request to the MCP server's internal logic [7][8]. Typical usage patterns: 1. Stdio (Local): You instantiate the server and transport, then connect them [2][9]. The SDK handles all message passing internally via standard streams [1]. const server = new McpServer({ name: 'my-server', version: '1.0.0' }); const transport = new StdioServerTransport; await server.connect(transport); 2. Streamable HTTP (Remote): You connect the transport to the server, and then delegate incoming HTTP requests from your web framework or runtime to transport.handleRequest [5][6]. const server = new McpServer({ name: 'my-server', version: '1.0.0' }); const transport = new WebStandardStreamableHTTPServerTransport({... }); await server.connect(transport); // Example in a request handler app.post('/mcp', async (req, res) => { await transport.handleRequest(req, res, req.body); }); For more advanced or automated setups, the SDK also provides higher-level abstractions like createMcpHandler, which automates the wiring of transport connections and request handling for common frameworks [10].
Citations:
- 1: https://ts.sdk.modelcontextprotocol.io/server
- 2: https://github.com/modelcontextprotocol/typescript-sdk/blob/HEAD/docs/server.md
- 3: https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/server/server/mcp.html
- 4: https://ts.sdk.modelcontextprotocol.io/classes/server.Server.html
- 5: https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/server/server/streamableHttp.html
- 6: https://ts.sdk.modelcontextprotocol.io/v2/classes/_modelcontextprotocol_server.server_streamableHttp.WebStandardStreamableHTTPServerTransport.html
- 7: https://ts.sdk.modelcontextprotocol.io/v2/serving/sessions-state-scaling.html
- 8: https://github.com/modelcontextprotocol/typescript-sdk/blob/2c0c481cb9dbfd15c8613f765c940a5f5bace94d/packages/server/src/server/streamableHttp.ts
- 9: https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.29.0/docs/server.md
- 10: https://ts.sdk.modelcontextprotocol.io/v2/serving/http.html
🌐 Web query:
@modelcontextprotocol/sdk v1.30.0 types source McpServer connect onmessage transport sendMessage
💡 Result:
In the @modelcontextprotocol/sdk (TypeScript), the McpServer class provides a high-level API for building MCP servers [1][2]. The connect method is used to bind an McpServer instance to a specific transport [2][3]. McpServer.connect(transport) The connect(transport) method performs the following actions: 1. Attaches the server to the provided Transport instance [2][3]. 2. Starts the transport and begins listening for incoming messages [2][3]. 3. Assumes ownership of the Transport, replacing any previously set callbacks and expecting to be the exclusive user of that instance [2][3]. It is defined as an asynchronous method returning a Promise [2][3]. Transport Mechanisms The SDK supports several transport types, which handle the underlying communication (such as onmessage and sendMessage logic) [1][4]: - StdioServerTransport: For local, process-spawned integrations, communicating over stdin/stdout [1][4]. - StreamableHTTPServerTransport: Recommended for remote, multi-node deployments [1][5]. - HTTP + SSE: Deprecated and intended only for backwards compatibility [1][6]. Internal Logic (onmessage/sendMessage) The Transport interface itself abstracts the message-passing details. When McpServer calls connect(transport), it typically sets up the transport's internal handlers: - onmessage: The server registers a callback on the transport to process incoming JSON-RPC messages [2]. - sendMessage: The McpServer uses the transport's sendMessage method (or equivalent interface) to dispatch responses, notifications, or logging messages back to the client [1][7][2]. For advanced operations, such as manual control over message handling or sending custom notifications, you can access the underlying low-level Server instance via the McpServer.server property [2][3].
Citations:
- 1: https://ts.sdk.modelcontextprotocol.io/documents/server.html
- 2: https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/server/server/mcp.html
- 3: https://github.com/modelcontextprotocol/typescript-sdk/blob/01954e621afe525cc3c1bbe8d781e44734cf81c2/packages/server/src/server/mcp.ts
- 4: https://ts.sdk.modelcontextprotocol.io/server
- 5: https://github.com/modelcontextprotocol/typescript-sdk/blob/HEAD/docs/server.md
- 6: https://ts.sdk.modelcontextprotocol.io/index.html
- 7: https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.29.0/docs/server.md
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repository lock/package files for MCP SDK version"
fd -a '(^package\.json$|^pnpm-lock\.yaml$|^package-lock\.json$|^yarn\.lock$)' . | sed 's#^\./##' | rg 'package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$' || true
echo
for f in $(fd '(^package\.json$|^pnpm-lock\.yaml$|^package-lock\.json$|^yarn\.lock$)' . | rg 'package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$' | head -n 20); do
echo "--- $f"
rg -n '"`@modelcontextprotocol/sdk`"|modelcontextprotocol/sdk|`@modelcontextprotocol/sdk`' "$f" || true
done
echo
if [ -d node_modules/@modelcontextprotocol ]; then
echo "--- local SDK version and streamableHttp source excerpt"
sed -n '1,80p' node_modules/@modelcontextprotocol/sdk/package.json 2>/dev/null || true
if [ -f node_modules/@modelcontextprotocol/sdk/dist/esm/server/streamableHttp.mjs ]; then
sed -n '1,220p' node_modules/@modelcontextprotocol/sdk/dist/esm/server/streamableHttp.mjs
fi
if [ -f node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.mjs ]; then
rg -n "connect\\(|handleRequest\\(|MessageEvent|sendMessage|onmessage" node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.mjs node_modules/@modelcontextprotocol/sdk/dist/esm/server/streamableHttp.mjs || true
fi
else
echo "node_modules/@modelcontextprotocol not present"
fiRepository: shibadityadeb/Context-Brain
Length of output: 2364
🌐 Web query:
@modelcontextprotocol/sdk v1.30.0 packages/server/src/server/streamableHttp.ts try catch body JSON.parse response.writeHead 500
💡 Result:
In the @modelcontextprotocol/sdk, specifically within the streamable HTTP server implementation (such as in packages/server/src/server/streamableHttp.ts), logic surrounding JSON.parse is wrapped in try-catch blocks to handle potential malformed request bodies [1]. When a parsing error occurs, the transport typically catches the exception and returns a JSON-RPC error response with a 400 status code (Parse error: Invalid JSON or Invalid JSON-RPC message) rather than a 500 error [1]. Historically, some implementations or related middleware have used response.writeHead(500) or other error-handling paths if exceptions occurred outside of anticipated parsing stages, or when transports encountered lifecycle issues like unauthorized reuse of stateless instances [2]. Recent improvements in the SDK have focused on surfacing these errors more cleanly, often by ensuring they conform to the standard JSON-RPC error shape and explicitly calling onerror handlers, preventing them from becoming opaque 500 errors in the underlying framework (such as Hono) [2]. If you are encountering 500 errors, it may be due to unhandled exceptions or specific lifecycle failures that have been addressed in more recent versions of the SDK (e.g., via better re-entry guards or improved cleanup logic) [2][3].
Citations:
- 1: https://github.com/modelcontextprotocol/typescript-sdk/blob/95d28cbae98e57df1b987b656a555777de976545/packages/server/src/server/streamableHttp.ts
- 2: fix(server): surface stateless transport reuse errors modelcontextprotocol/typescript-sdk#1995
- 3: RangeError: Maximum call stack size exceeded in webStandardStreamableHttp.js:639 when multiple transports close simultaneously modelcontextprotocol/typescript-sdk#1699
🌐 Web query:
@modelcontextprotocol/sdk v1.30.0 Transport sendMessage try catch
💡 Result:
In the Model Context Protocol (MCP) TypeScript SDK (v1.30.0), the sendMessage() method is typically used within the context of UI-based extensions (e.g., MCP Apps) [1][2] or internal transport-level communication [3]. It is not a standard, high-level method on the general Client or Server classes for generic JSON-RPC messages; rather, it is specific to interfaces like ui/message [1][2] or internal transport implementations [3]. When using sendMessage(), or performing any transport-level communication in MCP, you should handle errors using both try/catch blocks and the transport's onerror callback [4][5][1]. 1. Method-Level try/catch: Methods like sendMessage() that return a Promise [1][6] should be wrapped in try/catch to handle synchronous errors or Promise rejections [1]. const result = await app.sendMessage({ role: "user", content: [{ type: "text", text: "Hello" }], }); if (result.isError) { console.error("Host rejected the message"); } 2. Transport-Level onerror: Because transports may experience asynchronous or stream-level failures (e.g., EPIPE, connection drops, or parsing errors), simply wrapping send() or sendMessage() calls may not catch all failures [4][5][7]. You must set up an onerror handler on your transport instance to ensure visibility into background or fatal transport errors [5][7]. transport.onerror = (error) => { console.error("Transport error detected:", error); }; Context on specific issues: - Stdio Transport: Users have reported that stdout.write() failures (like EPIPE) can lead to crashes if not wrapped in try/catch and paired with an 'error' event listener on the stream [4]. - Streamable HTTP Transport: Recent versions have focused on ensuring that errors are propagated correctly to onerror and returned as valid JSON-RPC error responses rather than being silently swallowed or returning opaque 500 status codes [5][7]. Always check the official documentation for the specific transport type (StdioClientTransport, StreamableHTTPClientTransport, etc.) you are using, as error lifecycle management varies by implementation [8].
Citations:
- 1: https://deepwiki.com/modelcontextprotocol/ext-apps/4.6-sending-messages-and-logs
- 2: https://deepwiki.com/modelcontextprotocol/ext-apps/4-building-ui-applications
- 3: https://app.unpkg.com/@modelcontextprotocol/sdk@1.25.2/files/dist/esm/server/streamableHttp.d.ts
- 4: Unhandled 'EPIPE' in 'StdioServerTransport' causes fatal process crash on client disconnect modelcontextprotocol/typescript-sdk#1564
- 5: fix: call onerror for silently swallowed transport errors modelcontextprotocol/typescript-sdk#1580
- 6: https://sunpeak.ai/docs/mcp-apps/app/requests/send-message
- 7: Stateless StreamableHTTPServerTransport: non-initialize requests return 500 with empty body when transport is reused (regression vs 1.24.3) modelcontextprotocol/typescript-sdk#1994
- 8: https://ts.sdk.modelcontextprotocol.io/documents/client.html
🌐 Web query:
@modelcontextprotocol/sdk v1.30.0 packages/server/src/server/streamableHttp.ts throw
💡 Result:
In the @modelcontextprotocol/sdk, streamable HTTP transport implementations—found in files such as streamableHttp.ts—contain several scenarios where the code explicitly throws errors. The specific behavior depends on whether you are using the client-side (StreamableHTTPClientTransport) or server-side implementation. Client-Side (StreamableHTTPClientTransport) 1. Transport Lifecycle: The start method throws an Error if the transport has already been started (i.e., if _abortController is already defined), preventing multiple concurrent connections [1]. 2. Authentication: - If an authProvider is missing when authentication is required, it may throw an UnauthorizedError [2]. - If a step-up authorization challenge is returned (403 Forbidden) and the transport is configured with onInsufficientScope: 'throw', it throws an InsufficientScopeError [3]. - If step-up retries exceed the configured limit (maxStepUpRetries), it throws an SdkHttpError [4]. 3. HTTP Requests: The send method may throw errors for non-OK HTTP responses. Historically, these were plain Errors [5], though updates have moved toward using more specific error classes like StreamableHTTPError to allow programmatic access to status codes [5]. 4. SSE Stream Errors: By default, the client may throw an SdkError with code ClientHttpFailedToOpenStream if it fails to connect to the SSE endpoint [6]. Note that logic has been updated in some versions to gracefully handle certain status codes (like 405, and in some contexts 404/406) as fallbacks rather than throwing errors [6]. Server-Side (StreamableHTTPServerTransport) 1. Transport Reuse: Stateless transport instances enforce a lifecycle rule that prevents reuse. Attempting to reuse a stateless transport instance will trigger a JSON-RPC error response [7]. 2. Header Validation: Requests that do not meet required criteria (such as missing mandatory Accept headers) may trigger a 406 Not Acceptable status response [8], which effectively acts as a rejection/error for the incoming request. Because the SDK is under active development, if you are encountering a specific "throw" in streamableHttp.ts, it is recommended to check the specific version's source code on GitHub (e.g., in the modelcontextprotocol/typescript-sdk repository) to confirm the exact condition [9]. If you are experiencing unexpected behavior, searching the repository's issues for "streamableHttp.ts" often reveals confirmed bugs or proposed fixes related to these error paths [1][6][8][5].
Citations:
- 1: StreamableHTTPClientTransport cannot be restarted after close() — breaks OAuth re-authentication modelcontextprotocol/typescript-sdk#1641
- 2: https://github.com/modelcontextprotocol/typescript-sdk/blob/e0de0829019a4eab7af29c05f9a7ec13364f121e/src/client/streamableHttp.ts
- 3: https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/client/client/streamableHttp.html
- 4: https://github.com/modelcontextprotocol/typescript-sdk/blob/95d28cbae98e57df1b987b656a555777de976545/packages/client/src/client/streamableHttp.ts
- 5: send() should use StreamableHTTPError instead of plain Error for HTTP errors modelcontextprotocol/typescript-sdk#1176
- 6: StreamableHTTPClientTransport should handle 404 and 406 gracefully for GET SSE stream modelcontextprotocol/typescript-sdk#1635
- 7: fix(server): surface stateless transport reuse errors modelcontextprotocol/typescript-sdk#1995
- 8: StreamableHTTPServerTransport rejects JSON-only Accept with 406 even when enableJsonResponse: true modelcontextprotocol/typescript-sdk#1944
- 9: modelcontextprotocol/typescript-sdk@1.17.2...v1.26.0
🌐 Web query:
@modelcontextprotocol/sdk v1.30.0 packages/server/src/server/streamableHttp.ts "throw new Error" handlePostRequest "await this.handlePostRequest"
💡 Result:
In the @modelcontextprotocol/sdk, particularly within the Streamable HTTP transport implementation for the server, the handlePostRequest method is designed to process incoming JSON-RPC messages via POST requests [1][2]. The code you referenced, which involves throwing a new Error when handling POST requests, is typically part of the request validation logic [1]. For example, the server validates that the client includes the correct headers, such as an Accept header containing both application/json and text/event-stream, and a Content-Type header of application/json [1]. If these validations fail, the server often uses this.onerror to log the error and subsequently returns a standardized JSON-RPC error response (such as a 406 Not Acceptable or 415 Unsupported Media Type) to the client rather than allowing the application to crash [1]. It is important to note the following context regarding the evolution of this SDK: 1. Architectural Changes: The @modelcontextprotocol/sdk (v1.x) has been superseded by a modularized v2 structure [3]. In v2, the single SDK package was split into dedicated packages like @modelcontextprotocol/server and @modelcontextprotocol/client [3]. 2. Error Handling Improvements: Development discussions within the repository have specifically highlighted the importance of using more descriptive error classes (e.g., StreamableHTTPError) instead of generic Error objects in HTTP-related transport logic to allow for better programmatic handling of status codes [4][5]. 3. Current Versioning: As of July 31, 2026, the ecosystem has moved toward newer protocol revisions, such as the 2026-07-28 era protocol, which involves significant updates to server and client initialization and transport handling compared to the 2025-era implementations [6]. If you are encountering a specific error related to handlePostRequest, ensure your client is sending the required headers (Accept and Content-Type) and that you are using the appropriate version of the SDK for your project requirements [1]. If you are still on v1.x, consider migrating to v2 to benefit from improved error handling and architectural updates [3].
Citations:
- 1: https://github.com/modelcontextprotocol/typescript-sdk/blob/ab552c30d5499b5e0aaa1ab47ee2481028910c3d/packages/server/src/server/streamableHttp.ts
- 2: Subclassing McpServer causes tool invocation issues modelcontextprotocol/typescript-sdk#451
- 3: https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2.html
- 4: send() should use StreamableHTTPError instead of plain Error for HTTP errors modelcontextprotocol/typescript-sdk#1176
- 5: https://github.com/modelcontextprotocol/typescript-sdk/blob/e0de0829019a4eab7af29c05f9a7ec13364f121e/src/client/streamableHttp.ts
- 6: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/migration/support-2026-07-28.md
🌐 Web query:
"packages/server/src/server/streamableHttp.ts" "async handleRequest" "@modelcontextprotocol/sdk"
💡 Result:
The handleRequest method in the Model Context Protocol (MCP) TypeScript SDK's streamableHttp.ts serves as the primary entry point for processing incoming HTTP requests within an MCP server [1][2]. Depending on the specific package and environment, there are two distinct implementations of this method [1][3]. 1. Web Standard Implementation (in @modelcontextprotocol/sdk) In the core server package, handleRequest operates on Web Standard Request objects [4][1]. Signature: async handleRequest(req: Request, options?: HandleRequestOptions): Promise [1][5] Functionality: It performs security validations, such as checking origin headers for DNS rebinding protection, and then routes the request based on the HTTP method (GET, POST, or DELETE) to the appropriate handler (handleGetRequest, handlePostRequest, or handleDeleteRequest) [4][5][6]. The HandleRequestOptions parameter allows for passing pre-parsed bodies (avoiding re-parsing req.json) and authentication information [4][5]. 2. Node.js Implementation (in @modelcontextprotocol/node) In the Node.js-specific transport, handleRequest facilitates integration with Node.js HTTP servers by bridging the gap between Node.js and Web Standard objects [2][3][7]. Signature: async handleRequest(req: IncomingMessage & { auth?: AuthInfo }, res: ServerResponse, parsedBody?: unknown): Promise [2][3][7] Functionality: This method converts Node.js IncomingMessage and ServerResponse objects into Web Standard formats and manages the underlying transport delegation [8][2][9]. It allows for passing a pre-parsed body (e.g., from body-parser middleware) directly into the processing flow [2][3][7]. Both versions are designed to support the MCP Streamable HTTP transport specification, enabling features like SSE (Server-Sent Events) streaming and efficient request handling [1][7].
Citations:
- 1: https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/server/server/streamableHttp.html
- 2: https://ts.sdk.modelcontextprotocol.io/v2/api/@modelcontextprotocol/node/streamableHttp.html
- 3: https://ts.sdk.modelcontextprotocol.io/v2/classes/_modelcontextprotocol_node.streamableHttp.NodeStreamableHTTPServerTransport.html
- 4: https://github.com/modelcontextprotocol/typescript-sdk/blob/ab552c30d5499b5e0aaa1ab47ee2481028910c3d/packages/server/src/server/streamableHttp.ts
- 5: https://github.com/modelcontextprotocol/typescript-sdk/blob/7d7e62cc/packages/server/src/server/streamableHttp.ts
- 6: https://github.com/modelcontextprotocol/typescript-sdk/blob/2c0c481cb9dbfd15c8613f765c940a5f5bace94d/packages/server/src/server/streamableHttp.ts
- 7: https://cdn.jsdelivr.net/npm/@modelcontextprotocol/sdk@1.20.2/dist/esm/server/streamableHttp.d.ts
- 8: https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.29.0/src/server/streamableHttp.ts
- 9: https://github.com/modelcontextprotocol/typescript-sdk/blob/e4227d13/packages/middleware/node/src/streamableHttp.ts
🌐 Web query:
@modelcontextprotocol/sdk 1.30.0 handlePostRequest catch response.end headersSent
💡 Result:
In the @modelcontextprotocol/sdk (TypeScript SDK), the handlePostRequest method is a internal component of the StreamableHTTPServerTransport [1][2]. This method handles incoming HTTP POST requests containing JSON-RPC messages [3][1]. Regarding response.end() and headersSent: 1. Error Handling Pattern: The SDK follows a standard pattern for handling errors during POST requests to ensure that responses are only sent if they have not already been initiated by other processes or middleware [4][5]. When an error occurs within handlePostRequest, the code typically includes a check if (!res.headersSent) before attempting to send a JSON-RPC error response (e.g., a 500 Internal Server Error) [4][5]. 2. Header and Response Conflicts: Issues related to calling response.end() after headers have already been sent often arise when integrating with Node.js frameworks like Express, especially when third-party middleware (e.g., logging, compression) modifies the response object or fails to correctly chain/return the res object during methods like writeHead() [6]. 3. Development Recommendations: - To avoid "headers already sent" errors, ensure that your MCP endpoint is registered correctly in your server stack [7]. Placing the MCP route early in the middleware chain can prevent other middleware from prematurely ending the response or writing headers [7]. - If you encounter these errors, check your middleware stack for components that might be overriding res.writeHead() or res.end() without maintaining proper compatibility with the Node.js http.ServerResponse interface [6]. - In some custom implementations or when using older versions of the SDK, developers have worked around these issues by manually checking res.headersSent or ensuring that res.status() and res.end() calls are not chained if the underlying middleware implementation is unreliable [6][4]. For the latest versions of the SDK (including 1.30.0), refer to the official repository for the most recent patches regarding HTTP connection lifecycle and response handling [8][9].
Citations:
- 1: https://cdn.jsdelivr.net/npm/@modelcontextprotocol/sdk@1.20.2/dist/esm/server/streamableHttp.d.ts
- 2: https://cdn.jsdelivr.net/npm/@modelcontextprotocol/sdk@1.12.0/dist/cjs/server/streamableHttp.d.ts
- 3: https://github.com/modelcontextprotocol/typescript-sdk/blob/621ccea997bf318ee99c7f64ce19609e838615fa/src/server/streamableHttp.ts
- 4: https://github.com/modelcontextprotocol/servers/blob/main/src/everything/transports/streamableHttp.ts
- 5: https://npmx.dev/package/@modelcontextprotocol/sdk/v/1.23.0
- 6: SSEServerTransport error while ending the response modelcontextprotocol/typescript-sdk#325
- 7: handlePostMessage fails when reqBody is not passed modelcontextprotocol/typescript-sdk#223
- 8: https://npmx.dev/package-changelog/@modelcontextprotocol/sdk/v/1.25.3
- 9: fix: release HTTP connections after POST responses modelcontextprotocol/typescript-sdk#1214
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
cd "$tmp"
echo "Fetch MCP SDK package metadata/source candidates"
npm view `@modelcontextprotocol/sdk`@1.30.0 dist.tarball --json
tarball="$(npm view `@modelcontextprotocol/sdk`@1.30.0 dist.tarball)"
curl -fsSL "$tarball" -o sdk.tgz
tar -tzf sdk.tgz | rg 'package/(packages/(server|node)/src/server/streamableHttp.ts|package.json|dist/)' | sed -n '1,120p'
echo
echo "Extract relevant source"
tar -xzf sdk.tgz
echo "--- package metadata"
jq '.version, .files, .exports // empty' package/package.json || true
echo "--- streamableHttp.ts excerpts"
for f in package/packages/server/src/server/streamableHttp.ts package/packages/node/src/streamableHttp.ts; do
if [ -f "$f" ]; then
echo "+++ $f"
rg -n "async handleRequest|handlePostRequest|handleGetRequest|headersSent|writeHead|end\\(|try|catch|throw|onerror" "$f" || true
fi
done
echo
echo "--- exact relevant excerpts"
if [ -f package/packages/server/src/server/streamableHttp.ts ]; then
awk '
/async handleRequest\(request: Request, opts/ { s=1 }
s && /^ \}$/ { print; s=0 }
s { print }
' package/packages/server/src/server/streamableHttp.ts | sed -n '1,220p' | cat -n
fi
if [ -f package/packages/node/src/streamableHttp.ts ]; then
awk '
/async handleRequest\(req: IncomingMessage/,/return void this \._handleRequest/ { s=1 }
s && /^ \}$/ { print; s=0 }
s { print }
' package/packages/node/src/streamableHttp.ts | sed -n '1,220p' | cat -n
fiRepository: shibadityadeb/Context-Brain
Length of output: 8340
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
cd "$tmp"
echo "Fetch MCP SDK built dist tarball contents"
tarball="$(npm view `@modelcontextprotocol/sdk`@1.30.0 dist.tarball 2>/dev/null)"
curl -fsSL "$tarball" -o sdk.tgz
tar -tzf sdk.tgz | rg 'dist/(esm|cjs)/.*streamableHttp' | sed -n '1,40p'
echo
echo "Extract relevant built dist"
tar -xzf sdk.tgz
echo "--- dist streamableHttp files"
for f in $(fd 'streamableHttp' dist -x sh -c 'echo "+++ $1"; rg -n "async handleRequest|handlePostRequest|headersSent|writeHead|response\.end|catch|throw|onerror" "$1" | tail -n 60' sh {}); do
echo
echo "+++ excerpt for $f"
sed -n '140,280p' "$f" | cat -n
doneRepository: shibadityadeb/Context-Brain
Length of output: 1678
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
cd "$tmp"
tarball="$(npm view `@modelcontextprotocol/sdk`@1.30.0 dist.tarball 2>/dev/null)"
curl -fsSL "$tarball" -o sdk.tgz
tar -xzf sdk.tgz
echo "CJS handleRequest and related excerpts"
awk '
/async function handleRequest\(request, opts\) {\}/ { s=1 }
s { print NR":"$0 }
s && /^ }\}/ { exit }
' package/dist/cjs/server/streamableHttp.js | sed -n '1,260p'
echo
echo "ESM handleRequest and related excerpts"
awk '
/async function handleRequest\(request, opts\) {\}/ { s=1 }
s { print NR":"$0 }
s && /^ }\}/ { exit }
' package/dist/esm/server/streamableHttp.js | sed -n '1,260p'
echo
echo "Node handleRequest excerpt"
if [ -f package/dist/cjs/streamableHttp.js ]; then
rg -n "NodeStreamableHTTPServerTransport|async handleRequest|_handleRequest|headersSent|writeHead|end\\(" package/dist/cjs/streamableHttp.js || true
sed -n '1,240p' package/dist/cjs/streamableHttp.js | cat -n
fi
if [ -f package/dist/esm/streamableHttp.js ]; then
rg -n "NodeStreamableHTTPServerTransport|async handleRequest|_handleRequest|headersSent|writeHead|end\\(" package/dist/esm/streamableHttp.js || true
sed -n '1,240p' package/dist/esm/streamableHttp.js | cat -n
fiRepository: shibadityadeb/Context-Brain
Length of output: 272
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
cd "$tmp"
tarball="$(npm view `@modelcontextprotocol/sdk`@1.30.0 dist.tarball 2>/dev/null)"
curl -fsSL "$tarball" -o sdk.tgz
tar -xzf sdk.tgz
echo "Files matching StreamableHTTPServerTransport/handle"
find package -type f \( -name '*streamableHttp*' \) | sort | sed -n '1,80p'
echo
for f in package/dist/cjs/server/streamableHttp.js package/dist/esm/server/streamableHttp.js; do
if [ -f "$f" ]; then
echo "+++ $f length"
wc -l "$f"
echo "+++ occurrences"
grep -n "handlePostRequest\\|handleGetRequest\\|handleDeleteRequest\\|async handleRequest\\|headersSent\\|writeHead\\|end\\(\\)" "$f" || true
echo "+++ lines 1-220"
sed -n '1,220p' "$f" | cat -n
fi
done
echo
for f in package/dist/cjs/node.js package/dist/esm/node.js package/dist/cjs/streamableHttp.js package/dist/esm/streamableHttp.js; do
if [ -f "$f" ]; then
echo "+++ $f"
wc -l "$f"
grep -n "NodeStreamableHTTPServerTransport\\|handleRequest\\|headersSent\\|writeHead\\|end\\(" "$f" || true
fi
doneRepository: shibadityadeb/Context-Brain
Length of output: 18216
Don’t rely on MCP SDK error handling after hijacking Fastify responses.
reply.hijack() removes this request from Fastify’s response lifecycle, and the SDK version is ^1.30.0; if this route upgrades to or resolves to @modelcontextprotocol/node@1.30.0, transport.handleRequest(...) can defer error responses to Hono’s Node response wrapper. Add a route-level async handler with reply.hijack() and a try/catch so any rejection sends a valid error response before the response stream can hang.
🤖 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/api/src/modules/mcp/mcp.routes.ts` around lines 209 - 224, Update the
MCP route handler around reply.hijack(), mcp.connect(), and
transport.handleRequest() to wrap the post-hijack async operations in try/catch.
On rejection, send a valid error response directly through the raw response
before the stream can hang, while preserving the existing close cleanup and
successful transport handling.
| /** Enabled tool names; empty/omitted ⇒ the full catalog. */ | ||
| tools: z.array(z.string()).optional(), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n 'ALL_TOOL_NAMES' -B2 -A5 apps/api/src/modules/mcpRepository: shibadityadeb/Context-Brain
Length of output: 2871
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- mcp.schemas.ts ---\n'
sed -n '1,80p' apps/api/src/modules/mcp/mcp.schemas.ts
printf '\n--- mcp.service.ts relevant sections ---\n'
sed -n '1,30p' apps/api/src/modules/mcp/mcp.service.ts
sed -n '100,145p' apps/api/src/modules/mcp/mcp.service.ts
sed -n '180,225p' apps/api/src/modules/mcp/mcp.service.ts
printf '\n--- mcp.tools.ts relevant sections ---\n'
sed -n '1,60p' apps/api/src/modules/mcp/mcp.tools.ts
printf '\n--- TypeScript/TS config availability ---\n'
for f in apps/api/tsconfig.json package.json apps/api/package.json; do
if [ -f "$f" ]; then
echo "### $f"
sed -n '1,220p' "$f" | rg -n '"compilerOptions"|"types"|"include"|"files"|"strict"|"typeRoots"|all|package|npm' -C 2 || true
fi
done
printf '\n--- programmatic shape check ---\n'
python3 - <<'PY'
from pathlib import Path
schema = Path('apps/api/src/modules/mcp/mcp.schemas.ts').read_text()
service = Path('apps/api/src/modules/mcp/mcp.service.ts').read_text()
print('mcp.schemas.ts has tools array optional:', 'tools: z.array(z.string()).optional()' in schema)
print('create imports ALL_TOOL_NAMES:', "import { ALL_TOOL_NAMES, TOOL_CATALOG } from './mcp.tools.js';" in service)
print('create filters tools:', 'body.tools.filter((t) => ALL_TOOL_NAMES.includes(t))' in service[service.find('async create'):service.find('async update', service.find('async create'))])
print('update filters tools for persist:', 'data.tools = body.tools.filter((t) => ALL_TOOL_NAMES.includes(t));' in service)
PYRepository: shibadityadeb/Context-Brain
Length of output: 256
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- mcp.schemas.ts ---'
sed -n '1,80p' apps/api/src/modules/mcp/mcp.schemas.ts
printf '%s\n' ''
printf '%s\n' '--- mcp.service.ts relevant sections ---'
sed -n '1,30p' apps/api/src/modules/mcp/mcp.service.ts
sed -n '100,145p' apps/api/src/modules/mcp/mcp.service.ts
printf '%s\n' ''
printf '%s\n' '--- mcp.tools.ts relevant sections ---'
sed -n '1,60p' apps/api/src/modules/mcp/mcp.tools.ts
printf '%s\n' ''
printf '%s\n' '--- programmatic shape check ---'
python3 - <<'PY'
from pathlib import Path
schema = Path('apps/api/src/modules/mcp/mcp.schemas.ts').read_text()
service = Path('apps/api/src/modules/mcp/mcp.service.ts').read_text()
create_start = service.find('async create')
update_start = service.find('async update')
create_section = service[create_start:update_start] if create_start != -1 and update_start != -1 else service[create_start:create_start + 400]
print('mcp.schemas.ts has tools array optional:', 'tools: z.array(z.string()).optional()' in schema)
print('create imports ALL_TOOL_NAMES:', "import { ALL_TOOL_NAMES, TOOL_CATALOG } from './mcp.tools.js';" in service)
print('create filters unknown tools:', 'body.tools.filter((t) => ALL_TOOL_NAMES.includes(t))' in create_section)
print('update persists filtered tools:', 'data.tools = body.tools.filter((t) => ALL_TOOL_NAMES.includes(t));' in service)
print('service filters unknown tools in create:', 'body.tools.filter((t) => ALL_TOOL_NAMES.includes(t))' in create_section)
PYRepository: shibadityadeb/Context-Brain
Length of output: 7433
Validate tools against the known catalog instead of silently dropping unknown names.
tools accepts any string, but create and update routes persist only ALL_TOOL_NAMES entries. A request such as tools: ['unknownTool'] will create or update the server with an empty enabled-tools list and no validation error. Restrict both create/update schemas to a z.enum or .refine() built from TOOL_CATALOG/ALL_TOOL_NAMES.
🤖 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/api/src/modules/mcp/mcp.schemas.ts` around lines 22 - 23, Update the
tools field in both create and update schemas to validate entries against the
known catalog using ALL_TOOL_NAMES or TOOL_CATALOG, rejecting unknown tool names
instead of allowing them to be silently discarded by persistence.
| /** The org the acting user belongs to (first membership). */ | ||
| async resolveOrganization(userId: string): Promise<string> { | ||
| const membership = await this.deps.prisma.membership.findFirst({ | ||
| where: { userId, deletedAt: null }, | ||
| orderBy: { createdAt: 'asc' }, | ||
| }); | ||
| if (!membership) | ||
| throw new ForbiddenError('You must belong to an organization to manage MCP servers'); | ||
| return membership.organizationId; | ||
| } | ||
|
|
||
| toolCatalog() { | ||
| return TOOL_CATALOG.map((t) => ({ name: t.name, description: t.description })); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the existing role/permission helpers and how other sensitive modules gate routes.
rg -nP --type=ts -C3 '\b(requireRole|authorize|requireAdmin|hasRole|MembershipRole|checkPermission)\b' apps/api/src | head -80
# Show the preHandler chains used by MCP routes.
rg -nP --type=ts -C2 'preHandler' apps/api/src/modules/mcp/mcp.routes.tsRepository: shibadityadeb/Context-Brain
Length of output: 8721
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== MCP routes outline =="
ast-grep outline apps/api/src/modules/mcp/mcp.routes.ts --view expanded || true
sed -n '1,220p' apps/api/src/modules/mcp/mcp.routes.ts | cat -n
echo "== MCP service relevant sections =="
wc -l apps/api/src/modules/mcp/mcp.service.ts
sed -n '1,260p' apps/api/src/modules/mcp/mcp.service.ts | cat -n
echo "== Auth/authorize middleware =="
sed -n '1,220p' apps/api/src/middleware/authorize.ts 2>/dev/null | cat -n || true
rg -n --type=ts -C2 'requireRole|requireAdmin|requirePermissions|hasRole|MembershipRole|role' apps/api/src/middleware apps/api/src/modules | head -140
echo "== Programmatic route role coverage =="
python3 - <<'PY'
from pathlib import Path
routes = Path('apps/api/src/modules/mcp/mcp.routes.ts').read_text()
methods = []
for i,line in enumerate(routes.splitlines(),1):
if 'app.' in line:
m=line.split()[1] if line.split() else ''
methods.append((i,m, line))
for i,m,line in methods:
print(f"{i}: {m:5} {line.strip()}")
for m,name in [('get','/mcp-servers'),('get','/mcp-servers/tool-catalog'),('post','/mcp-servers'),
('patch','/mcp-servers/:id'),('del','/mcp-servers/:id'),
('post','/mcp-servers/:id/keys'),
('post','/mcp-servers/:id/keys/:keyId/rotate'),
('delete','/mcp-servers/:id/keys/:keyId'),
('get','/mcp-servers/:id/connections')]:
idx=[r for r in methods if r[2].find(name) != -1][:1]
if idx:
print(name, "preHandler includes role gate?", any('require' in line or 'authorize' in line or 'hasRole' in line or 'checkPermission' in line for l in lines[idx[0][0]:idx[0][0]+8] for lines in [(routes.splitlines(),)]))
PYRepository: shibadityadeb/Context-Brain
Length of output: 31942
Add role checks before MCP server and key management.
resolveOrganization() only requires any active membership, while the /mcp-servers routes use preHandler: [authenticate] without an admin/owner gate. Limit server creation, update, and deletion to admins or owners, and add an admin-only check for key creation, rotation, revocation, and server details with key/connections lists.
🤖 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/api/src/modules/mcp/mcp.service.ts` around lines 33 - 46, Update
resolveOrganization and the MCP route handlers to enforce authorization: require
admin or owner membership for MCP server creation, updates, and deletion;
require admin membership for key creation, rotation, revocation, and
server-detail responses that include key or connection lists. Preserve
authentication and reject unauthorized memberships before performing management
operations.
| async rotateKey(organizationId: string, id: string, keyId: string, userId: string) { | ||
| await this.getOrThrow(organizationId, id); | ||
| const existing = await this.deps.prisma.mcpApiKey.findFirst({ | ||
| where: { id: keyId, mcpServerId: id }, | ||
| }); | ||
| if (!existing) throw new NotFoundError('Key not found'); | ||
| await this.deps.prisma.mcpApiKey.update({ | ||
| where: { id: keyId }, | ||
| data: { revokedAt: new Date() }, | ||
| }); | ||
| return this.issueKey(organizationId, id, userId, { | ||
| name: `${existing.name} (rotated)`, | ||
| expiresAt: existing.expiresAt?.toISOString(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reject rotation of revoked keys and drop expired expiry values.
rotateKey has two defects:
- It does not check
existing.revokedAt. Rotating a revoked key issues a new valid key and restores access that an operator removed. - It copies
existing.expiresAtverbatim. If the old key already expired, the new key is expired at creation.authenticateKeyrejects it, so the user receives a secret that never authenticates.
🐛 Proposed fix
async rotateKey(organizationId: string, id: string, keyId: string, userId: string) {
await this.getOrThrow(organizationId, id);
const existing = await this.deps.prisma.mcpApiKey.findFirst({
where: { id: keyId, mcpServerId: id },
});
if (!existing) throw new NotFoundError('Key not found');
+ if (existing.revokedAt) throw new ForbiddenError('Cannot rotate a revoked key');
+ const expiresAt =
+ existing.expiresAt && existing.expiresAt.getTime() > Date.now() ? existing.expiresAt : null;
await this.deps.prisma.mcpApiKey.update({
where: { id: keyId },
data: { revokedAt: new Date() },
});
return this.issueKey(organizationId, id, userId, {
name: `${existing.name} (rotated)`,
- expiresAt: existing.expiresAt?.toISOString(),
+ expiresAt: expiresAt?.toISOString(),
});
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async rotateKey(organizationId: string, id: string, keyId: string, userId: string) { | |
| await this.getOrThrow(organizationId, id); | |
| const existing = await this.deps.prisma.mcpApiKey.findFirst({ | |
| where: { id: keyId, mcpServerId: id }, | |
| }); | |
| if (!existing) throw new NotFoundError('Key not found'); | |
| await this.deps.prisma.mcpApiKey.update({ | |
| where: { id: keyId }, | |
| data: { revokedAt: new Date() }, | |
| }); | |
| return this.issueKey(organizationId, id, userId, { | |
| name: `${existing.name} (rotated)`, | |
| expiresAt: existing.expiresAt?.toISOString(), | |
| }); | |
| } | |
| async rotateKey(organizationId: string, id: string, keyId: string, userId: string) { | |
| await this.getOrThrow(organizationId, id); | |
| const existing = await this.deps.prisma.mcpApiKey.findFirst({ | |
| where: { id: keyId, mcpServerId: id }, | |
| }); | |
| if (!existing) throw new NotFoundError('Key not found'); | |
| if (existing.revokedAt) throw new ForbiddenError('Cannot rotate a revoked key'); | |
| const expiresAt = | |
| existing.expiresAt && existing.expiresAt.getTime() > Date.now() ? existing.expiresAt : null; | |
| await this.deps.prisma.mcpApiKey.update({ | |
| where: { id: keyId }, | |
| data: { revokedAt: new Date() }, | |
| }); | |
| return this.issueKey(organizationId, id, userId, { | |
| name: `${existing.name} (rotated)`, | |
| expiresAt: expiresAt?.toISOString(), | |
| }); | |
| } |
🤖 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/api/src/modules/mcp/mcp.service.ts` around lines 166 - 180, Update
rotateKey to reject the existing key when existing.revokedAt is set, before
revoking or issuing a replacement. When constructing the issueKey options, only
pass expiresAt if existing.expiresAt is still in the future; otherwise omit it
so the rotated key receives the normal non-expiring/default expiry behavior.
| export function registerTools(server: McpServer, ctx: ToolContext): void { | ||
| const on = (name: string) => ctx.enabledTools.size === 0 || ctx.enabledTools.has(name); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
An empty tools list exposes every tool.
on() treats enabledTools.size === 0 as "no restriction". McpService.update persists body.tools.filter(...) verbatim, and the web page submits tools: [] when the user unchecks every tool. The server then registers the full catalog, which is the opposite of the stored configuration.
Resolve the ambiguity at the boundary. McpService.create already substitutes ALL_TOOL_NAMES for an empty list, so registration can treat the persisted set as authoritative.
🐛 Proposed fix
export function registerTools(server: McpServer, ctx: ToolContext): void {
- const on = (name: string) => ctx.enabledTools.size === 0 || ctx.enabledTools.has(name);
+ const on = (name: string) => ctx.enabledTools.has(name);Then make McpService.update reject an empty tools array, or normalize it to the full catalog, so existing servers keep a non-empty list.
🤖 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/api/src/modules/mcp/mcp.tools.ts` around lines 53 - 54, Make the
persisted tool configuration authoritative by removing the empty-set bypass from
registerTools’s on helper, so only names present in ctx.enabledTools are
registered. Update McpService.update to reject an empty tools array or normalize
it to ALL_TOOL_NAMES, matching McpService.create and ensuring persisted servers
always retain a non-empty tool set.
| async function rotateKey(keyId: string) { | ||
| setBusy(true); | ||
| try { | ||
| const { secret } = await api.rotateMcpKey(id, keyId); | ||
| setFreshSecret(secret); | ||
| await load(); | ||
| } finally { | ||
| setBusy(false); | ||
| } | ||
| } | ||
|
|
||
| async function revokeKey(keyId: string) { | ||
| setBusy(true); | ||
| try { | ||
| await api.revokeMcpKey(id, keyId); | ||
| await load(); | ||
| } finally { | ||
| setBusy(false); | ||
| } | ||
| } | ||
|
|
||
| async function deleteServer() { | ||
| if (!confirm('Delete this MCP server? Connected clients will stop working.')) return; | ||
| await api.deleteMcpServer(id); | ||
| router.push('/mcp'); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Handle failures in rotateKey, revokeKey, and deleteServer.
These three handlers have no catch. A rejected request produces an unhandled promise rejection, shows no message, and leaves the view unchanged. deleteServer also navigates only on success, so a failed delete leaves the user on the page with no feedback. createKey already uses the correct pattern.
🐛 Proposed fix
async function rotateKey(keyId: string) {
setBusy(true);
try {
const { secret } = await api.rotateMcpKey(id, keyId);
setFreshSecret(secret);
await load();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to rotate key');
} finally {
setBusy(false);
}
}
async function revokeKey(keyId: string) {
setBusy(true);
try {
await api.revokeMcpKey(id, keyId);
await load();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to revoke key');
} finally {
setBusy(false);
}
}
async function deleteServer() {
if (!confirm('Delete this MCP server? Connected clients will stop working.')) return;
- await api.deleteMcpServer(id);
- router.push('/mcp');
+ try {
+ await api.deleteMcpServer(id);
+ router.push('/mcp');
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to delete server');
+ }
}Note that error currently replaces the whole page (lines 151-154). Consider rendering these action errors inline instead.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function rotateKey(keyId: string) { | |
| setBusy(true); | |
| try { | |
| const { secret } = await api.rotateMcpKey(id, keyId); | |
| setFreshSecret(secret); | |
| await load(); | |
| } finally { | |
| setBusy(false); | |
| } | |
| } | |
| async function revokeKey(keyId: string) { | |
| setBusy(true); | |
| try { | |
| await api.revokeMcpKey(id, keyId); | |
| await load(); | |
| } finally { | |
| setBusy(false); | |
| } | |
| } | |
| async function deleteServer() { | |
| if (!confirm('Delete this MCP server? Connected clients will stop working.')) return; | |
| await api.deleteMcpServer(id); | |
| router.push('/mcp'); | |
| } | |
| async function rotateKey(keyId: string) { | |
| setBusy(true); | |
| try { | |
| const { secret } = await api.rotateMcpKey(id, keyId); | |
| setFreshSecret(secret); | |
| await load(); | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : 'Failed to rotate key'); | |
| } finally { | |
| setBusy(false); | |
| } | |
| } | |
| async function revokeKey(keyId: string) { | |
| setBusy(true); | |
| try { | |
| await api.revokeMcpKey(id, keyId); | |
| await load(); | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : 'Failed to revoke key'); | |
| } finally { | |
| setBusy(false); | |
| } | |
| } | |
| async function deleteServer() { | |
| if (!confirm('Delete this MCP server? Connected clients will stop working.')) return; | |
| try { | |
| await api.deleteMcpServer(id); | |
| router.push('/mcp'); | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : 'Failed to delete server'); | |
| } | |
| } |
🤖 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/web/src/app/`(app)/mcp/[id]/page.tsx around lines 124 - 149, Update
rotateKey, revokeKey, and deleteServer to catch rejected API requests and
surface action-specific errors inline without replacing the whole page. Follow
the existing createKey error-handling pattern, while preserving the
finally-based busy-state cleanup in rotateKey and revokeKey and only navigating
in deleteServer after a successful deletion.
| async function submit() { | ||
| if (!name.trim()) { | ||
| setError('Name is required'); | ||
| return; | ||
| } | ||
| setSubmitting(true); | ||
| setError(null); | ||
| const body: CreateMcpServerInput = { | ||
| name: name.trim(), | ||
| description: description.trim() || undefined, | ||
| prompt: prompt.trim() || undefined, | ||
| tools: [...enabled], | ||
| scopeConfig: | ||
| mode === 'workspace' | ||
| ? { mode: 'workspace' } | ||
| : { | ||
| mode: 'scoped', | ||
| projectIds: parseIds(projectIds), | ||
| memberIds: parseIds(memberIds), | ||
| documentIds: parseIds(documentIds), | ||
| meetingIds: parseIds(meetingIds), | ||
| }, | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Enforce the "at least one dimension" rule for scoped servers.
The help text on lines 155-158 states that a scoped server needs at least one dimension. submit does not enforce it. If the user selects scoped and leaves all four fields empty, the page creates a server whose tools always answer "No … in scope." The failure is silent.
🐛 Proposed fix
setSubmitting(true);
setError(null);
+ const scoped = {
+ projectIds: parseIds(projectIds),
+ memberIds: parseIds(memberIds),
+ documentIds: parseIds(documentIds),
+ meetingIds: parseIds(meetingIds),
+ };
+ if (mode === 'scoped' && !Object.values(scoped).some((ids) => ids.length > 0)) {
+ setError('Add at least one project, member, document or meeting id for a scoped server');
+ setSubmitting(false);
+ return;
+ }
const body: CreateMcpServerInput = {
name: name.trim(),
description: description.trim() || undefined,
prompt: prompt.trim() || undefined,
tools: [...enabled],
- scopeConfig:
- mode === 'workspace'
- ? { mode: 'workspace' }
- : {
- mode: 'scoped',
- projectIds: parseIds(projectIds),
- memberIds: parseIds(memberIds),
- documentIds: parseIds(documentIds),
- meetingIds: parseIds(meetingIds),
- },
+ scopeConfig: mode === 'workspace' ? { mode: 'workspace' } : { mode: 'scoped', ...scoped },
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function submit() { | |
| if (!name.trim()) { | |
| setError('Name is required'); | |
| return; | |
| } | |
| setSubmitting(true); | |
| setError(null); | |
| const body: CreateMcpServerInput = { | |
| name: name.trim(), | |
| description: description.trim() || undefined, | |
| prompt: prompt.trim() || undefined, | |
| tools: [...enabled], | |
| scopeConfig: | |
| mode === 'workspace' | |
| ? { mode: 'workspace' } | |
| : { | |
| mode: 'scoped', | |
| projectIds: parseIds(projectIds), | |
| memberIds: parseIds(memberIds), | |
| documentIds: parseIds(documentIds), | |
| meetingIds: parseIds(meetingIds), | |
| }, | |
| }; | |
| async function submit() { | |
| if (!name.trim()) { | |
| setError('Name is required'); | |
| return; | |
| } | |
| setSubmitting(true); | |
| setError(null); | |
| const scoped = { | |
| projectIds: parseIds(projectIds), | |
| memberIds: parseIds(memberIds), | |
| documentIds: parseIds(documentIds), | |
| meetingIds: parseIds(meetingIds), | |
| }; | |
| if (mode === 'scoped' && !Object.values(scoped).some((ids) => ids.length > 0)) { | |
| setError('Add at least one project, member, document or meeting id for a scoped server'); | |
| setSubmitting(false); | |
| return; | |
| } | |
| const body: CreateMcpServerInput = { | |
| name: name.trim(), | |
| description: description.trim() || undefined, | |
| prompt: prompt.trim() || undefined, | |
| tools: [...enabled], | |
| scopeConfig: mode === 'workspace' ? { mode: 'workspace' } : { mode: 'scoped', ...scoped }, | |
| }; |
🤖 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/web/src/app/`(app)/mcp/new/page.tsx around lines 50 - 72, Update submit
to validate scoped mode before creating the request: when mode is "scoped",
require at least one non-empty value across projectIds, memberIds, documentIds,
or meetingIds. Set the existing error state and return without submitting when
all four dimensions are empty, while preserving workspace behavior and valid
scoped submissions.
| createMcpServer(body: CreateMcpServerInput): Promise<McpServerDetail> { | ||
| return request('/api/v1/mcp-servers', { method: 'POST', body: JSON.stringify(body) }); | ||
| }, | ||
|
|
||
| updateMcpServer( | ||
| id: string, | ||
| body: Partial<CreateMcpServerInput> & { status?: 'ACTIVE' | 'DISABLED' }, | ||
| ): Promise<McpServerSummary> { | ||
| return request(`/api/v1/mcp-servers/${id}`, { method: 'PATCH', body: JSON.stringify(body) }); | ||
| }, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Narrow the declared response types for create and update.
The POST route returns { ...server, url } built from McpService.create, so keys, connections, connectionCount, and keyCount are absent. createMcpServer still declares McpServerDetail. The PATCH route returns the raw Prisma record, so url, connectionCount, and keyCount are absent, yet updateMcpServer declares McpServerSummary. Both declarations let field access compile and return undefined at runtime.
🐛 Proposed fix
+export type McpServerRecord = Omit<
+ McpServerSummary,
+ 'connectionCount' | 'keyCount' | 'url'
+> & { prompt: string | null };
+- createMcpServer(body: CreateMcpServerInput): Promise<McpServerDetail> {
+ createMcpServer(body: CreateMcpServerInput): Promise<McpServerRecord & { url: string }> {
return request('/api/v1/mcp-servers', { method: 'POST', body: JSON.stringify(body) });
},
updateMcpServer(
id: string,
body: Partial<CreateMcpServerInput> & { status?: 'ACTIVE' | 'DISABLED' },
- ): Promise<McpServerSummary> {
+ ): Promise<McpServerRecord> {
return request(`/api/v1/mcp-servers/${id}`, { method: 'PATCH', body: JSON.stringify(body) });
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| createMcpServer(body: CreateMcpServerInput): Promise<McpServerDetail> { | |
| return request('/api/v1/mcp-servers', { method: 'POST', body: JSON.stringify(body) }); | |
| }, | |
| updateMcpServer( | |
| id: string, | |
| body: Partial<CreateMcpServerInput> & { status?: 'ACTIVE' | 'DISABLED' }, | |
| ): Promise<McpServerSummary> { | |
| return request(`/api/v1/mcp-servers/${id}`, { method: 'PATCH', body: JSON.stringify(body) }); | |
| }, | |
| export type McpServerRecord = Omit< | |
| McpServerSummary, | |
| 'connectionCount' | 'keyCount' | 'url' | |
| > & { prompt: string | null }; | |
| createMcpServer(body: CreateMcpServerInput): Promise<McpServerRecord & { url: string }> { | |
| return request('/api/v1/mcp-servers', { method: 'POST', body: JSON.stringify(body) }); | |
| }, | |
| updateMcpServer( | |
| id: string, | |
| body: Partial<CreateMcpServerInput> & { status?: 'ACTIVE' | 'DISABLED' }, | |
| ): Promise<McpServerRecord> { | |
| return request(`/api/v1/mcp-servers/${id}`, { method: 'PATCH', body: JSON.stringify(body) }); | |
| }, |
🤖 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/web/src/lib/api.ts` around lines 318 - 327, Update the declared return
types of createMcpServer and updateMcpServer to match their actual API payloads:
the POST response should omit keys, connections, connectionCount, and keyCount
while retaining url, and the PATCH response should omit url, connectionCount,
and keyCount while retaining the raw server fields. Reuse existing compatible
type utilities or define narrowly scoped response types rather than declaring
McpServerDetail or McpServerSummary.
| if (memberIds.length) { | ||
| const members = await prisma.user.findMany({ | ||
| where: { id: { in: memberIds } }, | ||
| select: { email: true }, | ||
| }); | ||
| const emails = members.map((m) => m.email).filter((e): e is string => !!e); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n 'model User' -A25 apps/api/prisma/schema.prismaRepository: shibadityadeb/Context-Brain
Length of output: 1130
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files =="
git ls-files | rg '(^|/)scope\.ts$|(^|/)mcp\.server\.ts$|schema\.prisma$|package\.json$' | head -n 120
echo
echo "== packages/retrieval/src/scope.ts outline =="
ast-grep outline packages/retrieval/src/scope.ts || true
echo
echo "== packages/retrieval/src/scope.ts relevant lines =="
sed -n '1,150p' packages/retrieval/src/scope.ts
echo
echo "== Membership model and organization references in schema =="
rg -n 'model Membership|membership|organizationId|MeetingParticipant|participant|memberIds|Member' apps/api/prisma/schema.prisma -C 4Repository: shibadityadeb/Context-Brain
Length of output: 43482
Scope the member email lookup through memberships.
User does not have a direct organizationId, so this query can read emails for any user id in memberIds, including ids outside the current organization. Join through Membership instead: look up active memberships for { organizationId, userId: { in: memberIds } }, then fetch only those users.
🤖 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/retrieval/src/scope.ts` around lines 96 - 101, Update the member
email lookup in the surrounding scope function to filter through active
Membership records using the current organizationId and memberIds, then fetch
emails only for the resulting user IDs. Replace the direct User.findMany filter
with this membership-scoped flow while preserving the existing email mapping and
filtering behavior.
Next.js App Router only allows a page module to export the default component (+ reserved fields); exporting `scopeSummary` from mcp/page.tsx failed `next build` type validation (tsc alone doesn't enforce this). Move it to mcp/scope-summary.ts and import it in both the list and detail pages. Verified with a production `next build`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/app/(app)/mcp/page.tsx (1)
24-26: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winStop rendering the loading spinner after a failed fetch.
When
api.listMcpServers()rejects,serversremainsnull. The loading branch then renders indefinitely while the error is shown. Track loading separately, clear it infinally, and provide a retry action.Proposed loading-state fix
const [servers, setServers] = useState<McpServerSummary[] | null>(null); const [error, setError] = useState<string | null>(null); + const [isLoading, setIsLoading] = useState(true); const load = useCallback(async () => { + setIsLoading(true); + setError(null); try { setServers(await api.listMcpServers()); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load MCP servers'); + } finally { + setIsLoading(false); } }, []); ... - {!servers && ( + {isLoading && (Also applies to: 49-55
🤖 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/web/src/app/`(app)/mcp/page.tsx around lines 24 - 26, Update the MCP server loading flow around api.listMcpServers() to track loading independently from the nullable servers value, and clear that loading state in finally so failures stop rendering the spinner while preserving the error message. Add a retry action that re-invokes the existing fetch logic after failure, and update the loading/error rendering branches accordingly.
🤖 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/web/src/app/`(app)/mcp/scope-summary.ts:
- Around line 7-10: Update the scope summary count formatting for projectIds,
memberIds, documentIds, and meetingIds so a count of one uses the singular label
and all other counts use the plural label. Preserve the existing conditional
inclusion and summary construction behavior.
---
Outside diff comments:
In `@apps/web/src/app/`(app)/mcp/page.tsx:
- Around line 24-26: Update the MCP server loading flow around
api.listMcpServers() to track loading independently from the nullable servers
value, and clear that loading state in finally so failures stop rendering the
spinner while preserving the error message. Add a retry action that re-invokes
the existing fetch logic after failure, and update the loading/error rendering
branches accordingly.
🪄 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 Plus
Run ID: c567b985-17e3-47e9-8c70-e68264a74115
📒 Files selected for processing (3)
apps/web/src/app/(app)/mcp/[id]/page.tsxapps/web/src/app/(app)/mcp/page.tsxapps/web/src/app/(app)/mcp/scope-summary.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/src/app/(app)/mcp/[id]/page.tsx
| if (scope.projectIds?.length) parts.push(`${scope.projectIds.length} project(s)`); | ||
| if (scope.memberIds?.length) parts.push(`${scope.memberIds.length} member(s)`); | ||
| if (scope.documentIds?.length) parts.push(`${scope.documentIds.length} document(s)`); | ||
| if (scope.meetingIds?.length) parts.push(`${scope.meetingIds.length} meeting(s)`); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use singular labels for count one.
The helper returns text such as 1 project(s) and 1 meeting(s). Use singular and plural forms based on the count because this text appears in both the list and detail pages.
Proposed pluralization fix
const parts: string[] = [];
- if (scope.projectIds?.length) parts.push(`${scope.projectIds.length} project(s)`);
- if (scope.memberIds?.length) parts.push(`${scope.memberIds.length} member(s)`);
- if (scope.documentIds?.length) parts.push(`${scope.documentIds.length} document(s)`);
- if (scope.meetingIds?.length) parts.push(`${scope.meetingIds.length} meeting(s)`);
+ const formatCount = (count: number, singular: string) =>
+ `${count} ${singular}${count === 1 ? '' : 's'}`;
+ if (scope.projectIds?.length) parts.push(formatCount(scope.projectIds.length, 'project'));
+ if (scope.memberIds?.length) parts.push(formatCount(scope.memberIds.length, 'member'));
+ if (scope.documentIds?.length) parts.push(formatCount(scope.documentIds.length, 'document'));
+ if (scope.meetingIds?.length) parts.push(formatCount(scope.meetingIds.length, 'meeting'));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (scope.projectIds?.length) parts.push(`${scope.projectIds.length} project(s)`); | |
| if (scope.memberIds?.length) parts.push(`${scope.memberIds.length} member(s)`); | |
| if (scope.documentIds?.length) parts.push(`${scope.documentIds.length} document(s)`); | |
| if (scope.meetingIds?.length) parts.push(`${scope.meetingIds.length} meeting(s)`); | |
| const formatCount = (count: number, singular: string) => | |
| `${count} ${singular}${count === 1 ? '' : 's'}`; | |
| if (scope.projectIds?.length) parts.push(formatCount(scope.projectIds.length, 'project')); | |
| if (scope.memberIds?.length) parts.push(formatCount(scope.memberIds.length, 'member')); | |
| if (scope.documentIds?.length) parts.push(formatCount(scope.documentIds.length, 'document')); | |
| if (scope.meetingIds?.length) parts.push(formatCount(scope.meetingIds.length, 'meeting')); |
🤖 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/web/src/app/`(app)/mcp/scope-summary.ts around lines 7 - 10, Update the
scope summary count formatting for projectIds, memberIds, documentIds, and
meetingIds so a count of one uses the singular label and all other counts use
the plural label. Preserve the existing conditional inclusion and summary
construction behavior.
What
Adds Model Context Protocol (MCP) support so an organization can expose its Company Brain to external AI tools (Claude Desktop, Cursor, VS Code, Continue, Cline, OpenAI Agents, any MCP client) as a permission-aware, always-live context provider. Each org can own multiple MCP servers, each exposing a configurable slice of collective knowledge.
How it fits the codebase
Organization(the spec's "workspace").McpServerhangs off the org.@modelcontextprotocol/sdk, mounted in the existing Fastify API at/api/v1/mcp/:id, authed by a hashed MCP API key (Authorization: Bearer). Stateless — fresh server + transport per request.ScopedRetrievalServiceandGraphService, so "live knowledge" is automatic (always reads current graph/embeddings, no rebuild).Changes
McpServer,McpApiKey(hashed, mirrorsAPIKey),McpConnection+ migration.packages/retrieval): fail-closedKnowledgeScopeFilterin the scoped sources +resolveScopeFilter/parseScopeConfig. Workspace mode = unrestricted; scoped mode is confined to a provable slice (viaDocument.projectId/ownerId→KnowledgeObject.sourceDocumentId, meetings by member email), excluding anything unattributable.apps/api/src/modules/mcp): management REST (/api/v1/mcp-serversCRUD + key create/rotate/revoke, JWT-guarded) and the protocol endpoint with read-only scoped tools:search_knowledge,get_knowledge_object,query_graph,list_projects,list_recent_meetings./mcp): registry list, create flow (scope picker + tool toggles + prompt), detail page (server URL, key management with one-time secret reveal, copy/download connect configs for Claude Desktop/Cursor/VS Code/Continue/Cline) + sidebar nav item.Verification
packages/retrieval/src/scope.test.ts), retrieval/api/web typecheck + lint clean.initialize,tools/list,tools/call search_knowledge(real scoped results from the DB), and401on bad/missing key.v1 boundary (deferred to follow-ups)
🤖 Generated with Claude Code
Summary by CodeRabbit