Skip to content

feat(mcp): remote MCP server for workspaces (v1, read-only)#590

Open
syednahm wants to merge 32 commits into
ThinkEx-OSS:mainfrom
syednahm:remote-mcp-server
Open

feat(mcp): remote MCP server for workspaces (v1, read-only)#590
syednahm wants to merge 32 commits into
ThinkEx-OSS:mainfrom
syednahm:remote-mcp-server

Conversation

@syednahm

@syednahm syednahm commented Jul 4, 2026

Copy link
Copy Markdown

Summary

Adds a remote, read-only Model Context Protocol (MCP) server so external AI clients can authorize with ThinkEx and read workspace structure and content over Streamable HTTP at /mcp. Implements the v1 scope from #571 as a thin protocol adapter over the existing workspace capability layer, protected by OAuth 2.1 with a workspace:read scope.

What's included

Endpoint & transport

  • Stateless Streamable HTTP handler at /mcp via Cloudflare Agents createMcpHandler() (no McpAgent/session state), wired into the existing Worker routing without touching AI/kernel/auth routes.

Auth & authorization

  • OAuth 2.1 via the Better Auth OAuth Provider plugin: dynamic client registration, consent screen, and public clients.
  • Discovery through /.well-known/oauth-protected-resource/mcp (RFC 9728) plus authorization-server metadata; unauthenticated /mcp requests return 401 with a WWW-Authenticate header.
  • Every /mcp request is re-authorized before any tool runs, in layers:
    1. Token — JWT verified on each request: audience bound to the RFC 8707 resource (<origin>/mcp) and workspace:read scope required; opaque tokens rejected.
    2. Connection — the token's (clientId, userId) is re-checked against live OAuth consent in D1, so a disconnected/revoked client is rejected immediately even while its token is otherwise unexpired.
    3. Workspace membership — enforced per tool call; scopes can only narrow, never exceed, the user's workspace role.
  • Access tokens are capped to a short TTL (20m); disconnecting an app deletes its consent and purges refresh tokens, so revocation takes effect on the next call.

Tools (explicit, read-only)

  • thinkex_list_workspaces, thinkex_workspace_list_items, thinkex_workspace_read_items.
  • All route through the shared list/read capability operations — never WorkspaceKernel, DocumentSession, R2, or D1 directly.
  • Paginated reads (PDF pages / 1000-line document pages) with a 100k-char response cap.
  • Domain failures (path not found, folder read, unsupported type, out-of-range pages, access denied) return as structured failed entries; protocol errors are reserved for auth/schema/unknown-tool cases.

Supporting changes

  • Connections settings UI: client setup with generated MCP config, OAuth consent screen, and an authorized-applications list with one-click revoke.
  • DB migration for the OAuth tables (drizzle/0002_mcp_oauth.sql).
  • Structured audit hook (recordMcpToolCall) capturing actor fields per call (not yet wired to telemetry).
  • Developer setup and v1 limitations documented in docs/MCP.md.

Testing

  • Unit tests (Vitest): coverage for token/bearer verification, per-request connection authorization, tool access denial, read-content cap, routing, and tool behavior — 63 MCP tests, 77 in the full suite, all passing. pnpm check (format/lint/types) and the production build also pass.
  • MCP Inspector: manually verified the issue's cases — unauthenticated request rejected; a revoked/disconnected client rejected on its next call; user lists only own workspaces; access to a non-member workspace denied; list root and nested folders; read a document page; read PDF pages 1-3 when ready; unsupported/pending files return structured status instead of leaking internals; invalid path and out-of-range page ranges return structured failures.

v1 limitations

Read-only (no write tools), text/Markdown projections only (no images), no MCP resources/subscriptions, and no per-call rate limiting yet. Full list in docs/MCP.md.

Closes #571


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added MCP Connections settings page with editor setup guidance, generated MCP config, and an authorized applications list (with OAuth revoke).
    • Implemented MCP request routing at /mcp, scope-based MCP authentication, and MCP workspace tools (list workspaces/items and read items).
    • Added OAuth endpoints for authorization + consent and protected-resource discovery metadata.
  • Bug Fixes
    • Improved MCP read-content truncation: clearer truncated indicators and safer UTF-16/surrogate handling.
  • Documentation
    • Added MCP protocol documentation covering endpoints, auth flow, client setup, and read-only limitations.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a remote MCP server at /mcp with OAuth discovery, bearer-token auth, read-only workspace tools, consent handling, and settings UI. It also adds schema/migration support, route wiring, and MCP setup documentation.

Changes

Remote MCP Server and OAuth Integration

Layer / File(s) Summary
OAuth schema and migration
drizzle/0002_mcp_oauth.sql, drizzle/meta/0002_snapshot.json, drizzle/meta/_journal.json, src/db/schema.ts, package.json, pnpm-workspace.yaml, .gitignore
Adds OAuth persistence, schema relations, migration metadata, and dependency/config updates.
Auth server and client wiring
src/lib/auth.server.ts, src/lib/auth-client.ts, src/lib/app-origin.ts, src/features/workspaces/mcp/mcp-scopes.ts, src/features/account/connections/mcp-setup.functions.ts
Configures JWT/OAuth provider plugins, sets MCP-related issuers/audiences and scopes, updates the client plugin list, and adds MCP URL helpers.
MCP bearer auth and audit
src/features/workspaces/mcp/mcp-auth.ts, src/features/workspaces/mcp/mcp-auth.test.ts, src/features/workspaces/mcp/mcp-audit.ts
Implements bearer-token verification, MCP actor/error types, access-context builders, and audit record helpers with tests.
MCP routing, tool access, and execution
src/features/workspaces/agent-routes.ts, src/features/workspaces/mcp/mcp-route.ts, src/features/workspaces/mcp/mcp-route.test.ts, src/features/workspaces/mcp/mcp-tool-access.ts, src/features/workspaces/mcp/mcp-tool-access.test.ts, src/features/workspaces/mcp/mcp-schemas.ts, src/features/workspaces/operations/workspace-page-range-schema.ts, src/features/workspaces/operations/workspace-tool-schemas.ts, src/features/workspaces/mcp/mcp-tools.ts, src/features/workspaces/mcp/mcp-tools.test.ts, src/features/workspaces/mcp/mcp-read-content-cap.ts, src/features/workspaces/mcp/mcp-read-content-cap.test.ts, src/server.ts
Adds MCP request routing, structured access-denied responses, tool schemas, tool registration, content capping, and server entrypoint wiring, with tests.
OAuth well-known discovery
src/routes/[.]well-known/oauth-authorization-server.ts, src/routes/[.]well-known/oauth-protected-resource/mcp.ts, src/routeTree.gen.ts
Adds OAuth discovery routes and regenerates route metadata for the new paths.
OAuth consent flow
src/features/account/connections/oauth-api.ts, src/features/account/connections/oauth-scope-labels.ts, src/routes/oauth/consent.tsx, src/routeTree.gen.ts
Adds consent/client API helpers, scope label formatting, and the consent page for allow/deny flows.
Settings navigation and Connections page
src/features/account/components/SettingsNav.tsx, src/features/account/components/SettingsPage.tsx, src/features/account/connections/mcp-setup.ts, src/features/account/connections/ConnectionsSettingsPage.tsx, src/routes/_protected/settings.tsx, src/routes/_protected/settings/index.tsx, src/routes/_protected/settings/connections.tsx, src/routeTree.gen.ts
Adds settings navigation, nests settings routes, and adds the Connections page for MCP setup and OAuth authorization management.
MCP documentation
docs/MCP.md
Documents the /mcp endpoint, client setup, tool sequence, local development, and v1 limitations.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested labels: capy

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes appear to satisfy #571: protected /mcp routing, OAuth checks, read-only tools, structured failures, pagination, and local docs are all present.
Out of Scope Changes check ✅ Passed No clear out-of-scope code changes stand out; the extra UI, auth, schema, migration, and docs work all supports the MCP/OAuth feature set.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a new remote, read-only MCP server for workspaces.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@syednahm syednahm marked this pull request as ready for review July 4, 2026 18:42

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

10 issues found across 42 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="drizzle/0002_mcp_oauth.sql">

<violation number="1" location="drizzle/0002_mcp_oauth.sql:4">
P1: This migration stores JWT signing private keys and bearer tokens in plain `text` columns. A database leak would expose the `jwks.private_key`, allowing attackers to forge JWTs and impersonate users, and would expose every active refresh/access token for session hijacking. Consider encrypting the signing key at rest (e.g., envelope encryption) and hashing refresh tokens before storage per RFC 9700.</violation>

<violation number="2" location="drizzle/0002_mcp_oauth.sql:18">
P2: The `oauth_consent` table currently allows multiple rows for the same `(client_id, user_id)` pair because it only has separate non-unique indexes on each column. Duplicate consent records can lead to inconsistent scope resolution (e.g., which row wins when querying consent). Adding a composite unique index on `(client_id, user_id)` would prevent this. If anonymous consent is not intended, also consider making `user_id` non-nullable.</violation>

<violation number="3" location="drizzle/0002_mcp_oauth.sql:45">
P2: The `oauth_access_token` and `oauth_refresh_token` tables include `expires_at` (and `revoked` on refresh tokens) but don't have indexes on these lifecycle columns. As token volume grows, cleanup and revocation queries will scan the entire table. Consider adding indexes on `expires_at` for both tables and on `revoked` for `oauth_refresh_token` to avoid future performance degradation.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

CREATE TABLE `jwks` (
`id` text PRIMARY KEY NOT NULL,
`public_key` text NOT NULL,
`private_key` text NOT NULL,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: This migration stores JWT signing private keys and bearer tokens in plain text columns. A database leak would expose the jwks.private_key, allowing attackers to forge JWTs and impersonate users, and would expose every active refresh/access token for session hijacking. Consider encrypting the signing key at rest (e.g., envelope encryption) and hashing refresh tokens before storage per RFC 9700.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At drizzle/0002_mcp_oauth.sql, line 4:

<comment>This migration stores JWT signing private keys and bearer tokens in plain `text` columns. A database leak would expose the `jwks.private_key`, allowing attackers to forge JWTs and impersonate users, and would expose every active refresh/access token for session hijacking. Consider encrypting the signing key at rest (e.g., envelope encryption) and hashing refresh tokens before storage per RFC 9700.</comment>

<file context>
@@ -0,0 +1,101 @@
+CREATE TABLE `jwks` (
+	`id` text PRIMARY KEY NOT NULL,
+	`public_key` text NOT NULL,
+	`private_key` text NOT NULL,
+	`created_at` integer NOT NULL,
+	`expires_at` integer
</file context>

Comment thread src/features/workspaces/mcp/mcp-auth.test.ts Outdated
Comment thread src/features/account/connections/oauth-scope-labels.ts Outdated
Comment thread src/routes/[.]well-known/oauth-protected-resource/mcp.ts Outdated
Comment thread src/features/account/connections/ConnectionsSettingsPage.tsx Outdated
Comment thread src/features/workspaces/mcp/mcp-auth.ts Outdated
Comment thread src/features/account/connections/oauth-api.ts Outdated
Comment thread src/features/account/connections/oauth-api.ts Outdated
@@ -0,0 +1,101 @@
CREATE TABLE `jwks` (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The oauth_access_token and oauth_refresh_token tables include expires_at (and revoked on refresh tokens) but don't have indexes on these lifecycle columns. As token volume grows, cleanup and revocation queries will scan the entire table. Consider adding indexes on expires_at for both tables and on revoked for oauth_refresh_token to avoid future performance degradation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At drizzle/0002_mcp_oauth.sql, line 45:

<comment>The `oauth_access_token` and `oauth_refresh_token` tables include `expires_at` (and `revoked` on refresh tokens) but don't have indexes on these lifecycle columns. As token volume grows, cleanup and revocation queries will scan the entire table. Consider adding indexes on `expires_at` for both tables and on `revoked` for `oauth_refresh_token` to avoid future performance degradation.</comment>

<file context>
@@ -0,0 +1,101 @@
+--> statement-breakpoint
+CREATE UNIQUE INDEX `oauth_client_client_id_unique` ON `oauth_client` (`client_id`);--> statement-breakpoint
+CREATE INDEX `oauthClient_userId_idx` ON `oauth_client` (`user_id`);--> statement-breakpoint
+CREATE TABLE `oauth_refresh_token` (
+	`id` text PRIMARY KEY NOT NULL,
+	`token` text NOT NULL,
</file context>

`enable_end_session` integer,
`subject_type` text,
`scopes` text,
`user_id` text,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The oauth_consent table currently allows multiple rows for the same (client_id, user_id) pair because it only has separate non-unique indexes on each column. Duplicate consent records can lead to inconsistent scope resolution (e.g., which row wins when querying consent). Adding a composite unique index on (client_id, user_id) would prevent this. If anonymous consent is not intended, also consider making user_id non-nullable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At drizzle/0002_mcp_oauth.sql, line 18:

<comment>The `oauth_consent` table currently allows multiple rows for the same `(client_id, user_id)` pair because it only has separate non-unique indexes on each column. Duplicate consent records can lead to inconsistent scope resolution (e.g., which row wins when querying consent). Adding a composite unique index on `(client_id, user_id)` would prevent this. If anonymous consent is not intended, also consider making `user_id` non-nullable.</comment>

<file context>
@@ -0,0 +1,101 @@
+	`enable_end_session` integer,
+	`subject_type` text,
+	`scopes` text,
+	`user_id` text,
+	`created_at` integer,
+	`updated_at` integer,
</file context>

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a read-only remote MCP server for workspace access. The main changes are:

  • OAuth provider setup, discovery metadata, and /mcp bearer-token verification.
  • Read-only MCP tools for listing workspaces, listing items, and reading items.
  • OAuth consent and connections settings UI.
  • Database tables and docs for MCP OAuth state.

Confidence Score: 4/5

The MCP connection flow can fail before clients receive a usable bearer token.

  • The token exchange path can be unavailable after consent.
  • Protected-resource discovery can point clients at the wrong authorization server in configured split-origin deployments.
  • The workspace tool path still uses the existing read scope and membership checks.

src/lib/auth.server.ts, src/routes/[.]well-known/oauth-protected-resource/mcp.ts, src/features/account/connections/oauth-api.ts

Important Files Changed

Filename Overview
src/lib/auth.server.ts Adds OAuth provider configuration for MCP, but the token endpoint setting can block the authorization-code exchange.
src/features/workspaces/mcp/mcp-auth.ts Adds MCP bearer-token verification and maps workspace:read into read-only workspace contexts.
src/features/workspaces/mcp/mcp-tools.ts Registers read-only MCP tools and delegates workspace list/read calls to existing access-controlled operations.
src/features/workspaces/mcp/mcp-read-content-cap.ts Adds a total content cap for MCP read responses and marks truncated items.
src/features/account/connections/oauth-api.ts Adds browser helpers for OAuth consent and settings APIs, with response parsing and response-shape risks.
src/routes/[.]well-known/oauth-protected-resource/mcp.ts Adds protected-resource metadata for MCP, but always advertises the app origin as the authorization server.
src/routes/oauth/consent.tsx Adds the OAuth consent screen and submits signed query data through the new API helper.
src/db/schema.ts Adds OAuth and JWKS tables and relations matching the new migration.

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." | Re-trigger Greptile

Comment thread src/lib/auth.server.ts
const baseURL = getAuthBaseURL();

return betterAuth({
disabledPaths: ["/token"],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 OAuth Code Exchange Disabled

The MCP OAuth flow redirects a client through consent and then requires an authorization-code exchange before /mcp receives a bearer token. With /token disabled here, discovery-based MCP clients can complete consent but fail when they exchange the returned code, so they never obtain the token that verifyMcpBearerToken requires.

return Response.json(
{
resource: getMcpResourceUrl(appOrigin),
authorization_servers: [appOrigin],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Wrong Authorization Server Advertised

This metadata always points clients at appOrigin, but the auth server base can be configured separately through getAuthBaseURL(). In split-origin or reverse-proxy deployments, MCP clients discover the wrong issuer and build authorization/token URLs against the app host, while token verification expects the configured auth issuer/JWKS path, so the OAuth connection flow fails.

Comment on lines +58 to +59
async function readAuthJson<T>(response: Response): Promise<T> {
const payload = (await response.json()) as T & { message?: string };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Empty Auth Responses Throw

readAuthJson parses every auth response as JSON before checking status. If /api/auth/oauth2/delete-consent returns a successful empty response, the revoke mutation throws during response.json(), the UI reports failure, and the consent list is not refreshed even though the server may have deleted the consent.

Comment on lines +101 to +103
export async function getOAuthConsents(): Promise<OAuthConsentRecord[]> {
return authGet<OAuthConsentRecord[]>("/api/auth/oauth2/get-consents");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Consent Fields May Not Match

The settings UI types get-consents as camelCase fields like clientId and createdAt, while the OAuth tables and Better Auth-facing client metadata use snake_case names like client_id and created_at. If the provider returns raw OAuth records, consent.clientId is undefined, client lookups request client_id=undefined, and authorized apps render with missing or wrong metadata.

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

🧹 Nitpick comments (6)
package.json (1)

43-43: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Dependency versions verified; note a known transitive-vuln audit gap for the MCP SDK.

@better-auth/oauth-provider and @modelcontextprotocol/sdk versions are valid and current. However, @modelcontextprotocol/sdk@1.29.0 pulls in transitive deps (ajv/fast-uri, hono, express-rate-limit) with published advisories that can fail npm audit --audit-level=high in CI, even though the SDK's own semver ranges already allow the fixed versions.

If your CI enforces audit gating, consider adding pnpm overrides for these transitive packages until the SDK's lockfile is regenerated upstream.

Also applies to: 72-72

🤖 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 `@package.json` at line 43, The dependency entry for `@modelcontextprotocol/sdk`
is fine, but the current lockfile can still fail high-severity audit checks
because it pulls vulnerable transitive packages. Update package.json to add pnpm
overrides for the affected transitive dependencies (ajv/fast-uri, hono,
express-rate-limit) so CI audit gating passes until the SDK’s lockfile is
refreshed upstream; keep the change anchored near the existing dependency
declarations alongside `@better-auth/oauth-provider` and
`@modelcontextprotocol/sdk`.
src/lib/auth.server.ts (1)

224-226: 🔒 Security & Privacy | 🔵 Trivial

Open dynamic client registration needs an abuse guard.

allowDynamicClientRegistration + allowUnauthenticatedClientRegistration: true exposes an unauthenticated registration endpoint that writes to oauth_client. Without rate limiting or a registration cap this is an unbounded write/DoS and DB-bloat vector. The PR notes per-call rate limiting is not implemented yet — consider gating the registration path specifically (IP-based rate limit, per-origin cap, or short-lived registration tokens) before enabling this in production.

🤖 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 `@src/lib/auth.server.ts` around lines 224 - 226, The dynamic client
registration setup in auth.server needs an abuse guard before keeping
allowDynamicClientRegistration with allowUnauthenticatedClientRegistration
enabled. Add a registration-specific control in the auth registration path, such
as IP-based rate limiting, per-origin caps, or short-lived registration tokens,
and ensure the logic is enforced where the OAuth client is created so
unauthenticated writes to oauth_client cannot be abused.
src/features/workspaces/mcp/mcp-tools.ts (1)

22-31: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Tool results skip outputSchema/structuredContent; consider aligning with MCP's schema-validated output pattern.

All three tools return only a JSON-stringified text blob via mcpTextResult, with no outputSchema declared on any registerTool call. Per current MCP TypeScript SDK guidance, tools that want schema-validated output should declare outputSchema and return structuredContent matching it (in addition to the content text block for backward compatibility) — this is also explicitly one of this PR's stated goals ("Return compact, schema-validated outputs...").

Also consider adding annotations: { readOnlyHint: true } to each of these tools, since v1 is explicitly read-only.

♻️ Example for one tool
 server.registerTool(
 	"thinkex_list_workspaces",
 	{
 		description: "List workspaces accessible to the authenticated user.",
+		outputSchema: { workspaces: z.array(z.object({ id: z.string(), name: z.string(), description: z.string().nullable(), role: z.string() })) },
+		annotations: { readOnlyHint: true },
 	},
 	async () =>
 		runMcpTool({
 			actor,
 			deniedResultForCode: mcpListWorkspacesAccessDeniedResult,
 			toolName: "thinkex_list_workspaces",
 			run: async () => { /* ... */ },
 		}),
 );

mcpTextResult would then also need to attach structuredContent: result alongside content.

Also applies to: 67-144

🤖 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 `@src/features/workspaces/mcp/mcp-tools.ts` around lines 22 - 31, The MCP tools
currently return only JSON text through mcpTextResult, so they are not using
schema-validated outputs. Update the three registerTool definitions in
mcp-tools.ts to declare an outputSchema and return structuredContent alongside
the existing content text, keeping mcpTextResult aligned with that pattern for
backward compatibility. Also mark these read-only v1 tools with annotations: {
readOnlyHint: true } so the tool metadata matches the intended behavior.
src/features/workspaces/mcp/mcp-route.test.ts (1)

1-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the authenticated success path.

Current tests only cover the null-path and missing-token cases. Consider mocking verifyMcpBearerToken to succeed and asserting createMcpHandler/registerMcpTools receive the expected actor data (userId, clientId, scopes mapped into authContext.props), to guard the prop-mapping wiring against regressions.

src/features/workspaces/mcp/mcp-route.ts (1)

21-61: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider CORS/Origin handling on the /mcp handler.

createMcpHandler accepts a corsOptions (and the underlying transport expects Origin validation) but neither is configured here. The MCP Streamable HTTP spec states servers "MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks," and browser-based MCP clients will fail CORS preflight without explicit headers.

Given this endpoint is bearer-token protected, DNS-rebinding risk is reduced (a rebound origin can't forge a valid Authorization header), but if any browser-based MCP client is a target, this gap will break connectivity silently.

🌐 Suggested addition
 	const handler = createMcpHandler(buildMcpServer(actor), {
 		authContext: {
 			props: {
 				userId: actor.userId,
 				clientId: actor.clientId,
 				scopes: [...actor.grantedScopes],
 			},
 		},
+		corsOptions: {
+			allowedOrigins: [/* trusted origins, if browser-based clients are supported */],
+		},
 	});
🤖 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 `@src/features/workspaces/mcp/mcp-route.ts` around lines 21 - 61, The /mcp
route currently creates the MCP handler without any Origin/CORS configuration,
so browser-based clients may fail preflight and the underlying transport won’t
perform the expected Origin validation. Update routeMcpRequest to pass explicit
corsOptions and any required origin-check settings into createMcpHandler, using
the existing buildMcpServer(actor) and authContext setup unchanged, so the MCP
endpoint can accept legitimate browser clients while still validating incoming
Origins.
src/routes/oauth/consent.tsx (1)

101-145: 🔒 Security & Privacy | 🔵 Trivial

Consent-phishing risk with dynamically registered/public clients.

Since the OAuth provider layer supports dynamic and public client registration, any third party can register a client and set an arbitrary client_name/logo_uri/client_uri shown here. This is an inherent OAuth consent-screen risk (not specific to this file) worth keeping in mind operationally — e.g. surfacing the actual redirect/callback domain prominently, or rate-limiting/moderating DCR registrations — since users may over-trust a convincing display name/logo.

🤖 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 `@src/routes/oauth/consent.tsx` around lines 101 - 145, The consent screen in
consent.tsx currently trusts client-provided metadata from dynamic/public
registrations, which can be misleading. Update the consent UI rendering in the
application details section to prominently show the actual redirect/callback
domain or verified origin alongside clientName/client_uri/logo_uri, and ensure
the consent flow does not rely on those fields alone for user trust. Also
consider adding a moderation/rate-limit gate for DCR registrations in the OAuth
provider layer if that is where client registration is handled.
🤖 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 `@src/features/account/connections/oauth-api.ts`:
- Around line 58-70: The readAuthJson helper currently parses the body with
response.json() before verifying response.ok, which causes raw SyntaxError
failures for non-JSON or empty responses instead of the intended fallback.
Update readAuthJson in oauth-api.ts to safely handle invalid/empty bodies by
checking the response status and wrapping JSON parsing in error handling, then
fall back to a generic “Request failed” message when parsing fails or no message
is available; keep the existing message extraction logic for valid JSON
responses.

In `@src/features/workspaces/mcp/mcp-read-content-cap.ts`:
- Around line 20-56: The truncation logic in capMcpReadItemsContent can cut a
UTF-16 surrogate pair when slicing item.content at remainingBudget, which can
produce malformed truncated text for emoji or other astral characters. Update
the truncation path to compute a safe cut position that never splits a surrogate
pair before building truncatedContent, then append
MCP_READ_CONTENT_TRUNCATION_NOTICE as before and keep the truncated flag
behavior unchanged.

In `@src/routes/`[.]well-known/oauth-authorization-server.ts:
- Around line 1-11: The oauth-authorization-server metadata route currently
returns via Route and GET without CORS headers, which can block cross-origin
discovery clients. Update the handler in
createFileRoute("/.well-known/oauth-authorization-server") to attach the same
Access-Control-Allow-Origin behavior used by the protected-resource metadata
route, ideally within the GET handler returned by withAuth/auth.handler so
browser-based access works consistently.

In `@src/routes/oauth/consent.tsx`:
- Around line 95-99: The consent screen in consent.tsx traps the user when
client_id is missing because the error message is shown but both the Deny and
Allow paths are blocked. Update the consent action handling in the consent page
component so the user always has an escape route, either by removing the
client_id guard from the Deny button in the consent actions or by adding another
navigation/back-out option when search.client_id is absent. Make the change in
the consent form/button logic that uses search.client_id and isPending so the
page remains dismissible even for invalid requests.

---

Nitpick comments:
In `@package.json`:
- Line 43: The dependency entry for `@modelcontextprotocol/sdk` is fine, but the
current lockfile can still fail high-severity audit checks because it pulls
vulnerable transitive packages. Update package.json to add pnpm overrides for
the affected transitive dependencies (ajv/fast-uri, hono, express-rate-limit) so
CI audit gating passes until the SDK’s lockfile is refreshed upstream; keep the
change anchored near the existing dependency declarations alongside
`@better-auth/oauth-provider` and `@modelcontextprotocol/sdk`.

In `@src/features/workspaces/mcp/mcp-route.ts`:
- Around line 21-61: The /mcp route currently creates the MCP handler without
any Origin/CORS configuration, so browser-based clients may fail preflight and
the underlying transport won’t perform the expected Origin validation. Update
routeMcpRequest to pass explicit corsOptions and any required origin-check
settings into createMcpHandler, using the existing buildMcpServer(actor) and
authContext setup unchanged, so the MCP endpoint can accept legitimate browser
clients while still validating incoming Origins.

In `@src/features/workspaces/mcp/mcp-tools.ts`:
- Around line 22-31: The MCP tools currently return only JSON text through
mcpTextResult, so they are not using schema-validated outputs. Update the three
registerTool definitions in mcp-tools.ts to declare an outputSchema and return
structuredContent alongside the existing content text, keeping mcpTextResult
aligned with that pattern for backward compatibility. Also mark these read-only
v1 tools with annotations: { readOnlyHint: true } so the tool metadata matches
the intended behavior.

In `@src/lib/auth.server.ts`:
- Around line 224-226: The dynamic client registration setup in auth.server
needs an abuse guard before keeping allowDynamicClientRegistration with
allowUnauthenticatedClientRegistration enabled. Add a registration-specific
control in the auth registration path, such as IP-based rate limiting,
per-origin caps, or short-lived registration tokens, and ensure the logic is
enforced where the OAuth client is created so unauthenticated writes to
oauth_client cannot be abused.

In `@src/routes/oauth/consent.tsx`:
- Around line 101-145: The consent screen in consent.tsx currently trusts
client-provided metadata from dynamic/public registrations, which can be
misleading. Update the consent UI rendering in the application details section
to prominently show the actual redirect/callback domain or verified origin
alongside clientName/client_uri/logo_uri, and ensure the consent flow does not
rely on those fields alone for user trust. Also consider adding a
moderation/rate-limit gate for DCR registrations in the OAuth provider layer if
that is where client registration is handled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e11be5f-3222-461d-a58d-af9885ac3f6d

📥 Commits

Reviewing files that changed from the base of the PR and between e30f263 and 50c6659.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (41)
  • .gitignore
  • docs/MCP.md
  • drizzle/0002_mcp_oauth.sql
  • drizzle/meta/0002_snapshot.json
  • drizzle/meta/_journal.json
  • package.json
  • pnpm-workspace.yaml
  • src/db/schema.ts
  • src/features/account/components/SettingsNav.tsx
  • src/features/account/components/SettingsPage.tsx
  • src/features/account/connections/ConnectionsSettingsPage.tsx
  • src/features/account/connections/mcp-setup.functions.ts
  • src/features/account/connections/mcp-setup.ts
  • src/features/account/connections/oauth-api.ts
  • src/features/account/connections/oauth-scope-labels.ts
  • src/features/workspaces/agent-routes.ts
  • src/features/workspaces/mcp/mcp-audit.ts
  • src/features/workspaces/mcp/mcp-auth.test.ts
  • src/features/workspaces/mcp/mcp-auth.ts
  • src/features/workspaces/mcp/mcp-read-content-cap.test.ts
  • src/features/workspaces/mcp/mcp-read-content-cap.ts
  • src/features/workspaces/mcp/mcp-route.test.ts
  • src/features/workspaces/mcp/mcp-route.ts
  • src/features/workspaces/mcp/mcp-schemas.ts
  • src/features/workspaces/mcp/mcp-tool-access.test.ts
  • src/features/workspaces/mcp/mcp-tool-access.ts
  • src/features/workspaces/mcp/mcp-tools.test.ts
  • src/features/workspaces/mcp/mcp-tools.ts
  • src/features/workspaces/operations/workspace-page-range-schema.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
  • src/lib/app-origin.ts
  • src/lib/auth-client.ts
  • src/lib/auth.server.ts
  • src/routeTree.gen.ts
  • src/routes/[.]well-known/oauth-authorization-server.ts
  • src/routes/[.]well-known/oauth-protected-resource/mcp.ts
  • src/routes/_protected/settings.tsx
  • src/routes/_protected/settings/connections.tsx
  • src/routes/_protected/settings/index.tsx
  • src/routes/oauth/consent.tsx
  • src/server.ts

Comment thread src/features/account/connections/oauth-api.ts
Comment thread src/features/workspaces/mcp/mcp-read-content-cap.ts
Comment thread src/routes/[.]well-known/oauth-authorization-server.ts
Comment thread src/routes/oauth/consent.tsx

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 8 files (changes from recent commits).

Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.

Re-trigger cubic

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Added 1 comment

Comment thread pnpm-lock.yaml

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 7 files (changes from recent commits).

Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.

Re-trigger cubic

@capy-ai

capy-ai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="drizzle/0002_mcp_oauth.sql">

<violation number="1" location="drizzle/0002_mcp_oauth.sql:4">
P1: This migration stores JWT signing private keys and bearer tokens in plain `text` columns. A database leak would expose the `jwks.private_key`, allowing attackers to forge JWTs and impersonate users, and would expose every active refresh/access token for session hijacking. Consider encrypting the signing key at rest (e.g., envelope encryption) and hashing refresh tokens before storage per RFC 9700.</violation>

<violation number="2" location="drizzle/0002_mcp_oauth.sql:18">
P2: The `oauth_consent` table currently allows multiple rows for the same `(client_id, user_id)` pair because it only has separate non-unique indexes on each column. Duplicate consent records can lead to inconsistent scope resolution (e.g., which row wins when querying consent). Adding a composite unique index on `(client_id, user_id)` would prevent this. If anonymous consent is not intended, also consider making `user_id` non-nullable.</violation>

<violation number="3" location="drizzle/0002_mcp_oauth.sql:45">
P2: The `oauth_access_token` and `oauth_refresh_token` tables include `expires_at` (and `revoked` on refresh tokens) but don't have indexes on these lifecycle columns. As token volume grows, cleanup and revocation queries will scan the entire table. Consider adding indexes on `expires_at` for both tables and on `revoked` for `oauth_refresh_token` to avoid future performance degradation.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/routes/oauth/consent.tsx

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/routes/oauth/consent.tsx">

<violation number="1" location="src/routes/oauth/consent.tsx:69">
P2: When `result.url` exists but `window.location.assign()` throws an error, the catch block handles it correctly with a redirect-failure toast but then falls through to the unconditional toast below (`"Authorization completed, but no redirect was returned."`). This is misleading because the redirect URL was returned — it just failed to execute. The user sees two conflicting messages. Add a `return;` statement at the end of the catch block to prevent this incorrect fallthrough.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment on lines +69 to +75
try {
window.location.assign(result.url);
return;
} catch (error) {
setCompletedAction(null);
toast.error(
getErrorMessage(error, "Unable to redirect after authorization. Please try again."),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When result.url exists but window.location.assign() throws an error, the catch block handles it correctly with a redirect-failure toast but then falls through to the unconditional toast below ("Authorization completed, but no redirect was returned."). This is misleading because the redirect URL was returned — it just failed to execute. The user sees two conflicting messages. Add a return; statement at the end of the catch block to prevent this incorrect fallthrough.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/oauth/consent.tsx, line 69:

<comment>When `result.url` exists but `window.location.assign()` throws an error, the catch block handles it correctly with a redirect-failure toast but then falls through to the unconditional toast below (`"Authorization completed, but no redirect was returned."`). This is misleading because the redirect URL was returned — it just failed to execute. The user sees two conflicting messages. Add a `return;` statement at the end of the catch block to prevent this incorrect fallthrough.</comment>

<file context>
@@ -66,8 +66,15 @@ function OAuthConsentPage() {
 				setCompletedAction(accept ? "allow" : "deny");
-				window.location.assign(result.url);
-				return;
+				try {
+					window.location.assign(result.url);
+					return;
</file context>
Suggested change
try {
window.location.assign(result.url);
return;
} catch (error) {
setCompletedAction(null);
toast.error(
getErrorMessage(error, "Unable to redirect after authorization. Please try again."),
try {
window.location.assign(result.url);
return;
} catch (error) {
setCompletedAction(null);
toast.error(
getErrorMessage(error, "Unable to redirect after authorization. Please try again."),
);
return;
}

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/mcp/mcp-authorization.test.ts">

<violation number="1" location="src/features/workspaces/mcp/mcp-authorization.test.ts:1">
P2: The `assertMcpConnectionAuthorized` tests don't verify that the DB query actually filters by `userId` and `clientId`. The mock `where` function accepts any arguments and always returns the same `{ limit }` chain, so a bug that drops the where clause (or one of its conditions) would still pass. Consider asserting that `where` was called with the expected `and(eq(...), eq(...))` conditions after the pass/fail tests to actually validate the authorization filtering.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@@ -0,0 +1,92 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The assertMcpConnectionAuthorized tests don't verify that the DB query actually filters by userId and clientId. The mock where function accepts any arguments and always returns the same { limit } chain, so a bug that drops the where clause (or one of its conditions) would still pass. Consider asserting that where was called with the expected and(eq(...), eq(...)) conditions after the pass/fail tests to actually validate the authorization filtering.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/mcp/mcp-authorization.test.ts:

<comment>The `assertMcpConnectionAuthorized` tests don't verify that the DB query actually filters by `userId` and `clientId`. The mock `where` function accepts any arguments and always returns the same `{ limit }` chain, so a bug that drops the where clause (or one of its conditions) would still pass. Consider asserting that `where` was called with the expected `and(eq(...), eq(...))` conditions after the pass/fail tests to actually validate the authorization filtering.</comment>

<file context>
@@ -0,0 +1,92 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+vi.mock("#/db/server", () => ({
+	createDbContext: vi.fn(),
+}));
+
+vi.mock("better-auth/oauth2", () => ({
+	verifyAccessToken: vi.fn(),
+}));
</file context>

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

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

Build a remote MCP server for ThinkEx workspaces

1 participant