Skip to content

feat(mcp): expose the Company Brain as MCP servers - #23

Merged
shibadityadeb merged 2 commits into
mainfrom
feat/mcp-servers
Jul 31, 2026
Merged

shibadityadeb merged 2 commits into
mainfrom
feat/mcp-servers

Conversation

@shibadityadeb

@shibadityadeb shibadityadeb commented Jul 31, 2026

Copy link
Copy Markdown
Owner

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

  • Tenant = Organization (the spec's "workspace"). McpServer hangs off the org.
  • Transport = Streamable HTTP via @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.
  • Reuses retrieval + graph — tools call the existing ScopedRetrievalService and GraphService, so "live knowledge" is automatic (always reads current graph/embeddings, no rebuild).

Changes

  • Schema: McpServer, McpApiKey (hashed, mirrors APIKey), McpConnection + migration.
  • Retrieval (packages/retrieval): fail-closed KnowledgeScopeFilter in the scoped sources + resolveScopeFilter/parseScopeConfig. Workspace mode = unrestricted; scoped mode is confined to a provable slice (via Document.projectId/ownerIdKnowledgeObject.sourceDocumentId, meetings by member email), excluding anything unattributable.
  • API (apps/api/src/modules/mcp): management REST (/api/v1/mcp-servers CRUD + 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.
  • Web (/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

  • Unit tests for the fail-closed scope resolver (packages/retrieval/src/scope.test.ts), retrieval/api/web typecheck + lint clean.
  • Exercised the live protocol endpoint: initialize, tools/list, tools/call search_knowledge (real scoped results from the DB), and 401 on bad/missing key.

v1 boundary (deferred to follow-ups)

  • Scoped mode exposes the knowledge graph + selected meetings; memories and raw synced resources are excluded fail-closed (no provable project FK). Workspace mode is full.
  • Create form's scoped inputs take raw IDs (no project/member picker — those list endpoints don't exist yet).
  • Deferred: usage analytics, activity feed, private/shared/public visibility tiers, first-class Team model.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added MCP server management, including creation, configuration, status, visibility, and deletion.
    • Added scoped, read-only tools for knowledge search, object retrieval, graph exploration, projects, and recent meetings.
    • Added secure API key creation, rotation, revocation, expiration, and connection tracking.
    • Added MCP server pages for listing, setup, details, client configuration downloads, and key management.
    • Added configurable workspace or ID-based knowledge access controls.
  • Bug Fixes
    • Retrieval now respects configured knowledge scopes and safely fails closed for invalid or empty scopes.

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>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

MCP platform

Layer / File(s) Summary
Persistence and input contracts
apps/api/package.json, apps/api/prisma/..., apps/api/src/modules/mcp/mcp.schemas.ts, apps/api/src/modules/mcp/mcp.keys.ts
Adds MCP server, key, and connection models with lifecycle fields, indexes, cascades, validation schemas, and hashed key generation.
Scoped retrieval resolution
packages/retrieval/src/*
Adds workspace and scoped retrieval configuration, resolves organization-owned allowlists, propagates filters through retrieval, and fails closed for empty scopes.
MCP service and protocol flow
apps/api/src/app.ts, apps/api/src/modules/mcp/*
Adds organization-scoped server and key management, five scoped MCP tools, connection tracking, bearer authentication, and Streamable HTTP handling.
Web management experience
apps/web/src/app/(app)/mcp/*, apps/web/src/lib/api.ts, apps/web/src/lib/nav.ts
Adds MCP listing, creation, detail, key-management, connected-client, configuration, and navigation experiences.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: exposing the Company Brain through MCP servers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-servers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (5)
apps/web/src/app/(app)/mcp/page.tsx (1)

16-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Shared helper lives in an App Router page module. scopeSummary is defined and exported from a route page.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: move scopeSummary to a shared module such as apps/web/src/lib/mcp.ts and import it here.
  • apps/web/src/app/(app)/mcp/[id]/page.tsx#L17-L17: replace import { 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 win

Make clientName non-null so the unique constraint deduplicates.

clientName is nullable and takes part in @@unique([mcpServerId, clientName]). PostgreSQL treats NULLs as distinct, so rows with a NULL clientName can duplicate for the same server and inflate connectionCount. McpService.recordConnection already 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 value

Index the catalog by name instead of by position.

Each registerTool call reads its description through a positional index, for example TOOL_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 | 🔵 Trivial

Consider the per-request cost of rebuilding the scoped server.

buildScopedMcpServer (and its resolveScopeFilter call chain) runs on every protocol POST, not only on initialize. 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 win

Consider parallelizing the independent lookups.

resolveScopeFilter is resolved on every MCP protocol request (per buildScopedMcpServer in apps/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 with Promise.all reduces 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3f0fcef and 15c0135.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • apps/api/package.json
  • apps/api/prisma/migrations/20260731095218_mcp_servers/migration.sql
  • apps/api/prisma/schema.prisma
  • apps/api/src/app.ts
  • apps/api/src/modules/mcp/mcp.keys.ts
  • apps/api/src/modules/mcp/mcp.routes.ts
  • apps/api/src/modules/mcp/mcp.schemas.ts
  • apps/api/src/modules/mcp/mcp.server.ts
  • apps/api/src/modules/mcp/mcp.service.ts
  • apps/api/src/modules/mcp/mcp.tools.ts
  • apps/web/src/app/(app)/mcp/[id]/page.tsx
  • apps/web/src/app/(app)/mcp/new/page.tsx
  • apps/web/src/app/(app)/mcp/page.tsx
  • apps/web/src/lib/api.ts
  • apps/web/src/lib/nav.ts
  • packages/retrieval/src/index.ts
  • packages/retrieval/src/scope.test.ts
  • packages/retrieval/src/scope.ts
  • packages/retrieval/src/scoped-retrieval.ts
  • packages/retrieval/src/sources.ts
  • packages/retrieval/src/types.ts

Comment on lines +201 to +207
if (body?.method === 'initialize') {
void service.recordConnection(
resolved.server.id,
body.params?.clientInfo?.name ?? null,
body.params?.clientInfo?.version ?? null,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
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.

Comment on lines +209 to +224
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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:


🌐 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:


🌐 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:


🏁 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 || true

Repository: 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:


🌐 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:


🏁 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"
fi

Repository: 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:


🌐 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:


🌐 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:


🌐 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:


🌐 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:


🌐 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:


🏁 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
fi

Repository: 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
done

Repository: 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
fi

Repository: 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
done

Repository: 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.

Comment on lines +22 to +23
/** Enabled tool names; empty/omitted ⇒ the full catalog. */
tools: z.array(z.string()).optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'ALL_TOOL_NAMES' -B2 -A5 apps/api/src/modules/mcp

Repository: 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)
PY

Repository: 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)
PY

Repository: 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.

Comment on lines +33 to +46
/** 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 }));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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.ts

Repository: 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(),)]))
PY

Repository: 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.

Comment on lines +166 to +180
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(),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.expiresAt verbatim. If the old key already expired, the new key is expired at creation. authenticateKey rejects 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.

Suggested change
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.

Comment on lines +53 to +54
export function registerTools(server: McpServer, ctx: ToolContext): void {
const on = (name: string) => ctx.enabledTools.size === 0 || ctx.enabledTools.has(name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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.

Comment on lines +124 to +149
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
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.

Comment on lines +50 to +72
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),
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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.

Comment thread apps/web/src/lib/api.ts
Comment on lines +318 to +327
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) });
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

Suggested change
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.

Comment on lines +96 to +101
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'model User' -A25 apps/api/prisma/schema.prisma

Repository: 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 4

Repository: 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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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 win

Stop rendering the loading spinner after a failed fetch.

When api.listMcpServers() rejects, servers remains null. The loading branch then renders indefinitely while the error is shown. Track loading separately, clear it in finally, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 15c0135 and 7807948.

📒 Files selected for processing (3)
  • apps/web/src/app/(app)/mcp/[id]/page.tsx
  • apps/web/src/app/(app)/mcp/page.tsx
  • apps/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

Comment on lines +7 to +10
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)`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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.

@shibadityadeb
shibadityadeb merged commit afa3f6c into main Jul 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant