Add WorkOS MCP auth gateway - #240
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c53e6a7552
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } catch (error) { | ||
| const err = error as Error; | ||
| setCorsHeaders(res); | ||
| setUnauthorizedChallenge(res); | ||
| res.status(401).json({ | ||
| error: 'Unauthorized', | ||
| message: err.message, | ||
| }); | ||
| logLifecycle('mcp.request.complete', requestId, { reason: 'mcp_connection_resolve_failed' }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Internal configuration details returned to external clients. When
T49_MCP_RESOLVE_SECRET is not set, resolveWorkosMcpToken throws with the message 'T49_MCP_RESOLVE_SECRET must be set when AuthKit MCP auth is enabled.', which is passed verbatim into the 401 response body. This leaks internal env-var names to unauthenticated callers. The same path also exposes 'Terminal49 MCP connection resolve response is missing access_token or account_id.' when the upstream API returns an unexpected shape. The internal error should be logged and a generic message returned to the client.
| } catch (error) { | |
| const err = error as Error; | |
| setCorsHeaders(res); | |
| setUnauthorizedChallenge(res); | |
| res.status(401).json({ | |
| error: 'Unauthorized', | |
| message: err.message, | |
| }); | |
| logLifecycle('mcp.request.complete', requestId, { reason: 'mcp_connection_resolve_failed' }); | |
| return; | |
| } | |
| } catch (error) { | |
| const err = error as Error; | |
| logLifecycle('mcp.request.complete', requestId, { reason: 'mcp_connection_resolve_failed', error: err.message }); | |
| setCorsHeaders(res); | |
| setUnauthorizedChallenge(res); | |
| res.status(401).json({ | |
| error: 'Unauthorized', | |
| message: 'Authorization failed. Please re-authenticate.', | |
| }); | |
| return; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: api/mcp.ts
Line: 403-413
Comment:
Internal configuration details returned to external clients. When `T49_MCP_RESOLVE_SECRET` is not set, `resolveWorkosMcpToken` throws with the message `'T49_MCP_RESOLVE_SECRET must be set when AuthKit MCP auth is enabled.'`, which is passed verbatim into the 401 response body. This leaks internal env-var names to unauthenticated callers. The same path also exposes `'Terminal49 MCP connection resolve response is missing access_token or account_id.'` when the upstream API returns an unexpected shape. The internal error should be logged and a generic message returned to the client.
```suggestion
} catch (error) {
const err = error as Error;
logLifecycle('mcp.request.complete', requestId, { reason: 'mcp_connection_resolve_failed', error: err.message });
setCorsHeaders(res);
setUnauthorizedChallenge(res);
res.status(401).json({
error: 'Unauthorized',
message: 'Authorization failed. Please re-authenticate.',
});
return;
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Already resolved — this is a stale re-flag. It references resolveWorkosMcpToken and verbatim error strings from the original branch; the current code uses resolveConnectedClientToken, and the catch block (api/mcp.ts:436-468) returns only generic messages to the client: Invalid or expired token. (401), Authorization is not configured correctly. (500), and Authorization service is temporarily unavailable. Please retry. (502). The detailed err.message is written only to the server log via logLifecycle (correlated by request_id). No env-var names or internal detail reach the client.
c53e6a7 to
af83ade
Compare
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d978a1afeb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Unify the OAuth resource identifier behind one resolver (packages/mcp/src/resource.ts) so the Protected Resource Metadata `resource` and the WWW-Authenticate `resource_metadata` URL can never diverge across dev/preview/staging/prod (RFC 9728). Drop the authorization-server metadata proxy, which re-served WorkOS's issuer from the resource origin in violation of RFC 8414 section 3.3. Clients discover the AS directly via the PRM's authorization_servers. Use registered RFC 6750 error codes: a bare Bearer challenge when no credentials are presented, error="invalid_token" when a token is rejected. Keep client-facing auth errors generic; detail stays in logs. - api/oauth-protected-resource.ts, api/mcp.ts: resolve via shared module - remove api/oauth-authorization-server.ts and its vercel.json wiring - add resolver + PRM endpoint tests (resource.test.ts, oauth-metadata.test.ts) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidate the toolchain across @terminal49/mcp and @terminal49/sdk: replace Biome (and leftover dead ESLint configs) with oxlint for linting and oxfmt for formatting. Preserve each package's prior CI gate — MCP is lint-only, SDK is lint + format-check. Formatter config is migrated from Biome so single-quote style is preserved and the SDK's generated code stays excluded. Regenerate the root and SDK lockfiles (Biome out, oxlint/oxfmt in) and drop packages/mcp/package-lock.json: it is referenced by nothing (CI and Vercel install from the root workspace lockfile), was already stale, and cannot be regenerated standalone because @terminal49/sdk is a workspace package. Fix the two no-useless-fallback-in-spread warnings oxlint surfaced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AGENTS.md (and its CLAUDE.md/claude.md symlinks) described this as a docs-only repo, but it also hosts the deployed MCP server + OAuth gateway (api/, packages/mcp/) and the TypeScript SDK (sdks/typescript-sdk/). Document the real project structure, the npm-workspaces build/test/lint commands (oxlint + oxfmt, vitest, tsc), the generated files not to hand-edit, and MCP auth-gateway guidance (delegated audience validation, the single resource resolver, don't weaken the discovery endpoints). Point agent.md at AGENTS.md for code work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
oxlint and oxfmt ship native bindings that require Node 20+, so the `Lint SDK` step failed on the Node 18 matrix leg with a missing-binding error. Linting is static and runtime-independent, so gate it to Node 24 (matching how the docs-generation steps are gated). Build and test still run across the full 18/20/22/24 matrix for consumer compatibility. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a2c8a9ca5a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| authSource: resolvedAuth.source ?? 'authorization', | ||
| }; | ||
|
|
||
| if (authKitMcpEnabled() && resolvedAuth.scheme === 'Bearer') { |
There was a problem hiding this comment.
Preserve Bearer API-key pass-through with AuthKit
When T49_MCP_AUTHKIT_ENABLED=true, this branch treats every Authorization: Bearer ... value as a WorkOS token and calls /connected-clients/resolve. Existing MCP/API-key clients are still documented to send Terminal49 API keys with the Bearer scheme (docs/mcp/home.mdx:86), so those requests now fail with a 401 before reaching the pass-through path; only clients that switch to Token keep working. Distinguish WorkOS tokens or fall back to the existing API-token path on resolver 401 to avoid breaking current Bearer API-key users during the OAuth rollout.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Intentional, per the rollout decision — not a regression. When AuthKit is enabled, Bearer is reserved for WorkOS OAuth access tokens; API keys authenticate with the Token scheme (which still passes through). The docs were migrated to Token (docs/mcp/home.mdx), the code documents the intent at the branch (api/mcp.ts, comment above the AuthKit check), and packages/mcp/WORKOS_MCP_SETUP.md carries the rollout note: migrate Bearer API-key clients to Token before enabling AuthKit. Keeping this behavior by design.
When AuthKit is enabled, every failure from the connected-clients resolve endpoint was returned to the client as 401, so a Terminal49/API outage (5xx, 429, network error) told MCP clients their token was invalid — making them discard a valid token and loop through re-authentication instead of retrying a transient failure. Categorize resolve failures: only a token the resolver actively rejects (401/403) is a 401 invalid_token; a missing resolve secret is a 500 misconfiguration; everything else (upstream 5xx/429, network, malformed response) is a 502 the client can retry. Detail stays in the server log; client messages remain generic. (Addresses Codex review P2 on PR #240.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MCP quickstart told clients to send API keys as `Authorization: Bearer <T49_API_TOKEN>`. Once WorkOS OAuth is enabled the gateway treats every Bearer value as a WorkOS access token, so Bearer API-key clients would break. Switch the examples and the auth note to the `Token` scheme for API keys and document that `Bearer` is reserved for WorkOS OAuth. (Addresses Codex review P1 on PR #240.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 58dd5d0b0e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Re-add /.well-known/oauth-authorization-server as a 302 redirect to the WorkOS issuer's metadata (not the verbatim proxy that was previously removed). The client follows the redirect and fetches the document from the issuer's own origin, so its `issuer` matches the fetch origin per RFC 8414 section 3.3 — avoiding the mismatch a verbatim proxy creates. This is a compatibility shim for clients that probe the resource origin instead of following RFC 9728 PRM discovery; ChatGPT and Claude use PRM and never hit it. Wire the route in vercel.json and add redirect/CORS/method tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… runbook The quickstart told clients to connect to https://mcp.terminal49.com/mcp, but the OAuth resource identifier is the bare origin. A client that derives its resource indicator from the /mcp path would send a resource that does not match the PRM resource or the WorkOS-registered indicator, getting its token rejected. Standardize all connector config on the root origin https://mcp.terminal49.com (the server still responds at /mcp and /api/mcp). Add WORKOS_MCP_SETUP.md: production setup checklist (DCR, Resource Indicator, env vars, smoke tests) plus ChatGPT/Claude client specifics, so the WorkOS dashboard config is verifiable rather than tribal. Cross-link it from OAUTH_TEST_CLIENT.md and AGENTS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 50008a1327
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Document the two ways to test the MCP server locally with Claude Desktop: (1) direct stdio — Claude spawns the server, which talks straight to a local/prod t49 (simplest, tests the tools); (2) full local stack — Claude reaches the `vercel dev` gateway through `mcp-remote` (stdio↔HTTP bridge) in Token passthrough mode, exercising gateway auth + tools end to end. Claude Desktop can't reach localhost over an HTTP connector (its connectors run server-side), which is why both paths run a process on the machine. Add .env.local.example for the gateway and note the full WorkOS OAuth path needs a tunnel (not required for tool testing). The local stdio server was verified to boot and advertise all 10 tools. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flesh out the WorkOS OAuth path: expose the local gateway over an HTTPS tunnel (Resource Indicators must be HTTPS and a native Claude connector can't reach localhost), point WorkOS + the gateway env at that URL, and drive the flow three ways — Claude Desktop native custom connector (most realistic), mcp-remote as a local OAuth client (no token, it runs the flow), or the bundled oauth-test-client. Notes the aud == resource == indicator three-way match that gates the token exchange. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 92059f3ee9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| if (!callerToken) { | ||
| setCorsHeaders(res); | ||
| setUnauthorizedChallenge(res, req, 'missing_credentials'); |
There was a problem hiding this comment.
Gate OAuth challenges on configured AuthKit
When AuthKit/WorkOS is not enabled (for example the documented Token pass-through or client-secret modes), this still adds a Bearer resource_metadata challenge to 401 responses. In those deployments api/oauth-protected-resource.ts returns 500 unless WORKOS_AUTHORIZATION_SERVER_URL/WORKOS_ISSUER is set, so OAuth-aware MCP clients that follow the challenge are directed into a broken discovery flow instead of treating the API-key/client-secret auth failure normally. Only emit this OAuth challenge when the OAuth metadata endpoint is actually configured.
Useful? React with 👍 / 👎.
The 401 WWW-Authenticate challenge always included a `resource_metadata` pointer, even in Token/client-secret deployments where WorkOS is unset and the PRM endpoint 500s. An OAuth-aware client following that pointer would land in a broken discovery flow instead of treating it as an API-key auth failure. Gate `resource_metadata` on WORKOS_AUTHORIZATION_SERVER_URL / WORKOS_ISSUER being set; emit a bare Bearer challenge otherwise. Add gate tests and clear the WORKOS_* env between handler tests. (Addresses Codex review P2 on PR #240.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…a POST The OAuth test client defaulted its MCP endpoint to the resource origin, so post-OAuth calls hit `/` (works only via the root rewrite); default it to `<resource>/mcp` while keeping the resource/audience as the bare origin. Also fix the WORKOS_MCP_SETUP.md 401-challenge smoke test: a GET to /mcp returns 405 before the auth check, so use an unauthenticated POST. (Addresses two Codex review P2s on PR #240.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 78273f3b8a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (oauthConfigured()) { | ||
| parts.push(`resource_metadata="${protectedResourceMetadataUrl(req)}"`); |
There was a problem hiding this comment.
Gate OAuth discovery on enabled AuthKit
In this revision the challenge is gated only on WORKOS_* via oauthConfigured(). If WORKOS_AUTHORIZATION_SERVER_URL is present but T49_MCP_AUTHKIT_ENABLED is omitted or false, the 401 still sends resource_metadata, so OAuth-aware clients complete WorkOS and retry with a Bearer token; however the request handler skips the resolver when AuthKit is disabled and treats that token as a client secret/API token, causing the connector to fail or loop after a successful OAuth flow. Only advertise resource_metadata when the Bearer resolver path is actually enabled.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in f48ccff. resource_metadata is now gated on authKitMcpEnabled() && oauthConfigured(), so the OAuth challenge is only advertised when the Bearer resolver path is actually active. If WORKOS_* is set but AuthKit is off, no resource_metadata is emitted. Regression test added: omits resource_metadata when WorkOS is configured but AuthKit is disabled.
The transports table still listed `/mcp`; the root already serves the MCP handler (vercel.json routes `/`, `/mcp`, and `/api/mcp` to it), and the connector URL elsewhere is the root origin. State the root as canonical and note `/mcp` / `/api/mcp` remain as aliases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the public MCP docs clean — present `https://mcp.terminal49.com` as the single connector URL without listing the `/mcp` and `/api/mcp` aliases. The aliases still route in vercel.json; they're just not documented. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…figured The 401 challenge advertised resource_metadata whenever WORKOS_* was set, even with T49_MCP_AUTHKIT_ENABLED off. In that state a Bearer token is not resolved (it's treated as a passthrough key), so an OAuth-aware client would complete the WorkOS flow and then loop with a token the handler never honors. Require authKitMcpEnabled() && oauthConfigured() before emitting the OAuth challenge. Add the AuthKit-disabled gate test and document that Bearer is intentionally WorkOS-only under AuthKit (API keys use the Token scheme). (Addresses Codex review P2 on PR #240.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f48ccff3d4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| res.status(200).json({ | ||
| resource: resolveMcpResource(req), | ||
| authorization_servers: [authorizationServer.replace(/\/+$/, '')], | ||
| bearer_methods_supported: ['header'], |
There was a problem hiding this comment.
Gate well-known OAuth metadata on AuthKit
Fresh evidence beyond the gated WWW-Authenticate path: this well-known handler still returns a 200 PRM document whenever WORKOS_* is set, even if T49_MCP_AUTHKIT_ENABLED is false. In deployments that preconfigure WorkOS but leave AuthKit off, clients that discover OAuth by fetching /.well-known/oauth-protected-resource directly can complete WorkOS and retry with a Bearer token, but api/mcp.ts skips the resolver in that mode and treats the token as pass-through, causing failed connections/re-auth loops; gate this endpoint (and the AS-metadata shim) on the same AuthKit flag.
Useful? React with 👍 / 👎.
Summary
Verification
Greptile Summary
This PR adds a WorkOS/AuthKit OAuth gateway for the Terminal49 MCP server: two new Vercel functions expose the standard OAuth discovery endpoints (
.well-known/oauth-protected-resourceand.well-known/oauth-authorization-server), and the existing MCP handler gains a bearer-token resolution path that exchanges a WorkOS token for a Terminal49 API token and account ID before creating the MCP server. The TypeScript SDK is updated throughout to carry an optionalaccountIdand emit anx-account-idheader on all requests.api/oauth-protected-resource.ts,api/oauth-authorization-server.ts): new.well-knownhandlers proxy WorkOS metadata and advertise the MCP resource URL; both are wired intovercel.json.api/mcp.ts): whenT49_MCP_AUTHKIT_ENABLED=true, Bearer tokens are exchanged via an internal resolve endpoint;Token-scheme (API-key) clients continue through the existing client-secret path.sdks/typescript-sdk):Terminal49ClientConfig,Transport, andAuthInterceptornow acceptaccountIdand pass it asx-account-idon every API request.Confidence Score: 3/5
The new WorkOS token-resolution path and OAuth discovery handlers introduce three defects that should be addressed before shipping to production.
The MCP handler returns raw exception messages (including internal env-var names) to unauthenticated callers on every misconfigured or rejected resolve attempt. The authorization-server proxy has no error handling around the upstream fetch, so network failures produce unhandled rejections. The protected-resource handler constructs an invalid resource URL when no Host header is present. The SDK-level changes (accountId, Bearer header pass-through) and the existing Token-auth path are clean and well-tested.
api/mcp.ts, api/oauth-authorization-server.ts, and api/oauth-protected-resource.ts all need fixes before this goes to production.
Security Review
api/mcp.tsline ~408): whenT49_MCP_RESOLVE_SECRETis not set or the upstream resolve call returns an unexpected shape, the raw exception message (which names the env var) is returned to the unauthenticated caller in the 401 response body. A generic message should be returned and the internal detail logged server-side.WORKOS_AUTHORIZATION_SERVER_URL/WORKOS_ISSUERenv vars, so SSRF is not a concern.X-T49-MCP-Resolve-Secretshared secret is sent over HTTPS to the internal Terminal49 API; no concerns about transmission.Important Files Changed
Sequence Diagram
sequenceDiagram participant Client as MCP Client participant Meta as /.well-known/oauth-protected-resource participant AuthSrv as /.well-known/oauth-authorization-server participant MCP as /api/mcp participant Resolve as T49 API /auth/mcp/connections/resolve participant T49 as Terminal49 API Client->>Meta: GET /.well-known/oauth-protected-resource Meta-->>Client: "{ resource, authorization_servers }" Client->>AuthSrv: GET /.well-known/oauth-authorization-server AuthSrv->>WorkOS: proxy GET .well-known/oauth-authorization-server WorkOS-->>AuthSrv: WorkOS metadata AuthSrv-->>Client: WorkOS metadata Client->>MCP: POST /mcp (Authorization: Bearer workos-token) MCP->>Resolve: POST /auth/mcp/connections/resolve Resolve-->>MCP: "{ access_token, account_id }" MCP->>T49: API calls (Authorization: Bearer t49-token, x-account-id) T49-->>MCP: API response MCP-->>Client: MCP response Note over Client,MCP: Token auth path (T49_MCP_AUTHKIT_ENABLED != true) Client->>MCP: POST /mcp (Authorization: Token client-secret) MCP->>T49: API calls (Authorization: Token env-api-token) T49-->>MCP: API response MCP-->>Client: MCP responsePrompt To Fix All With AI
Reviews (1): Last reviewed commit: "Add WorkOS MCP auth gateway" | Re-trigger Greptile