Skip to content

Add WorkOS MCP auth gateway - #240

Merged
dodeja merged 19 commits into
mainfrom
codex/workos-mcp-auth
Jun 21, 2026
Merged

Add WorkOS MCP auth gateway#240
dodeja merged 19 commits into
mainfrom
codex/workos-mcp-auth

Conversation

@dodeja

@dodeja dodeja commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add hosted MCP OAuth protected-resource metadata and authorization-server proxy endpoints
  • Resolve WorkOS MCP bearer tokens through Terminal49 before creating the MCP server
  • Pass resolved Terminal49 bearer credentials plus account context into the SDK
  • Preserve existing Token auth behavior for API-key MCP clients

Verification

  • npm test --workspace @terminal49/mcp -- tests/api-handler.test.ts
  • npm test --workspace @terminal49/sdk -- src/client.test.ts
  • npm run build --workspace @terminal49/sdk
  • npm run type-check --workspace @terminal49/mcp
  • npm run type-check --workspace @terminal49/sdk

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-resource and .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 optional accountId and emit an x-account-id header on all requests.

  • OAuth discovery (api/oauth-protected-resource.ts, api/oauth-authorization-server.ts): new .well-known handlers proxy WorkOS metadata and advertise the MCP resource URL; both are wired into vercel.json.
  • WorkOS token resolution (api/mcp.ts): when T49_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.
  • SDK account context (sdks/typescript-sdk): Terminal49ClientConfig, Transport, and AuthInterceptor now accept accountId and pass it as x-account-id on 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

  • Internal configuration detail exposure (api/mcp.ts line ~408): when T49_MCP_RESOLVE_SECRET is 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.
  • No secrets are hard-coded; all credentials are read from environment variables.
  • The authorization-server proxy only fetches from a URL controlled by WORKOS_AUTHORIZATION_SERVER_URL / WORKOS_ISSUER env vars, so SSRF is not a concern.
  • The X-T49-MCP-Resolve-Secret shared secret is sent over HTTPS to the internal Terminal49 API; no concerns about transmission.

Important Files Changed

Filename Overview
api/mcp.ts Adds WorkOS MCP bearer token resolution path; leaks internal error messages to clients and uses a non-standard RFC 6750 error code in the WWW-Authenticate challenge.
api/oauth-authorization-server.ts New proxy handler for WorkOS authorization-server metadata; missing try/catch around upstream fetch and JSON parse means network failures produce unhandled exceptions.
api/oauth-protected-resource.ts New OAuth protected-resource metadata endpoint; fallback resource URL construction produces 'https://undefined/mcp' when Host header is absent.
packages/mcp/src/server.ts Threads optional accountId through to Terminal49Client construction; straightforward and correct.
sdks/typescript-sdk/src/client/interceptors.ts AuthInterceptor now accepts Bearer-prefixed tokens and optionally sets x-account-id header; logic is clean and covered by tests.
vercel.json Adds two new function entries and rewrites for the .well-known OAuth discovery endpoints; looks correct.

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 response
Loading

Fix All in Codex

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
api/mcp.ts:403-413
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;
      }
```

### Issue 2 of 4
api/oauth-authorization-server.ts:37-40
No error handling around the upstream proxy fetch. If the WorkOS authorization server is unreachable (DNS failure, timeout, network partition), the `fetch` call throws and the unhandled rejection propagates to Vercel, which returns a generic 500 with an unexpected body. Likewise, if the upstream response is not valid JSON, `response.json()` throws. Both should be caught and mapped to a controlled error response.

```suggestion
  let response: Response;
  try {
    response = await fetch(`${authorizationServer.replace(/\/+$/, '')}/.well-known/oauth-authorization-server`);
  } catch {
    res.status(502).json({ error: 'Failed to reach authorization server.' });
    return;
  }

  let payload: unknown;
  try {
    payload = await response.json();
  } catch {
    res.status(502).json({ error: 'Invalid response from authorization server.' });
    return;
  }

  res.status(response.status).json(payload);
```

### Issue 3 of 4
api/oauth-protected-resource.ts:20-22
When neither `T49_MCP_RESOURCE_URL` nor `WORKOS_MCP_RESOURCE` are set and the `Host` header is absent or `undefined`, `resourceUrl` returns `"https://undefined/mcp"`, which is an invalid resource identifier. OAuth clients that validate or cache by resource URL would receive malformed metadata.

```suggestion
  const host = req.headers.host;
  if (!host) {
    return '/mcp';
  }
  const protocol = host.startsWith('localhost') || host.startsWith('127.0.0.1') ? 'http' : 'https';
  return `${protocol}://${host}/mcp`;
```

### Issue 4 of 4
api/mcp.ts:106-111
`error="unauthorized"` is not a valid RFC 6750 Bearer challenge error code. The spec defines only `invalid_request`, `invalid_token`, and `insufficient_scope`. Standards-compliant MCP clients that inspect the error field may fail to trigger the re-authentication flow correctly. For the token-resolution failure case `invalid_token` is the appropriate value; for the missing-token challenge the error field should be omitted entirely.

```suggestion
function wwwAuthenticateHeader(): string {
  const metadataUrl = oauthProtectedResourceMetadataUrl();
  const parts = [
    'Bearer error="invalid_token"',
    'error_description="Authorization needed"',
  ];
```

Reviews (1): Last reviewed commit: "Add WorkOS MCP auth gateway" | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview, Comment Jun 19, 2026 10:10pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread api/mcp.ts Outdated
Comment thread api/mcp.ts
Comment on lines +403 to +413
} 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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security 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.

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

Fix in Codex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread api/oauth-authorization-server.ts Outdated
Comment thread api/oauth-protected-resource.ts Outdated
Comment thread api/mcp.ts Outdated
@dodeja
dodeja force-pushed the codex/workos-mcp-auth branch from c53e6a7 to af83ade Compare June 19, 2026 17:59
@mintlify

mintlify Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
terminal49 🟢 Ready View Preview Jun 19, 2026, 6:06 PM

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread api/mcp.ts Outdated
Comment thread api/mcp.ts Outdated
dodeja and others added 3 commits June 19, 2026 12:53
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>
@datadog-official

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>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread api/mcp.ts
authSource: resolvedAuth.source ?? 'authorization',
};

if (authKitMcpEnabled() && resolvedAuth.scheme === 'Bearer') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

dodeja and others added 2 commits June 19, 2026 13:22
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>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread packages/mcp/scripts/oauth-test-client.mjs Outdated
dodeja and others added 2 commits June 19, 2026 13:53
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>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread packages/mcp/WORKOS_MCP_SETUP.md Outdated
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>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread api/mcp.ts

if (!callerToken) {
setCorsHeaders(res);
setUnauthorizedChallenge(res, req, 'missing_credentials');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The OAuth challenge (resource_metadata) is now gated — initially on WorkOS being configured (78273f3), then refined to also require AuthKit enabled (f48ccff). Token/client-secret deployments no longer emit an OAuth discovery pointer.

dodeja and others added 2 commits June 19, 2026 14:49
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>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread api/mcp.ts Outdated
Comment on lines +106 to +107
if (oauthConfigured()) {
parts.push(`resource_metadata="${protectedResourceMetadataUrl(req)}"`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment on lines +47 to +50
res.status(200).json({
resource: resolveMcpResource(req),
authorization_servers: [authorizationServer.replace(/\/+$/, '')],
bearer_methods_supported: ['header'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@dodeja
dodeja merged commit 265b3db into main Jun 21, 2026
25 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