feat(http): HTTP transport + OAuth 2.1 + PKCE for remote MCP hosts (0.2.0)#2
Merged
govindkavaturi-art merged 3 commits intomainfrom Apr 18, 2026
Merged
Conversation
…quest apiKey Preserves 0.1.x stdio behavior exactly (same env-var contract, same MCP server wiring, same error shapes) and opens the door for an HTTP transport alongside it. - src/index.ts is now a router that chooses between stdio and http based on --transport flag or MCP_TRANSPORT env var. Default is stdio, so existing Claude Desktop / Code / Cursor / Zed configs keep working on upgrade without any changes. - src/stdio-entry.ts extracts the 0.1.x logic verbatim into a stand-alone runStdio() export. Same Server setup, same tool wiring, same error formatting. - src/client.ts: apiKey constructor arg is now optional, and request() accepts an optional trailing apiKey override. Stdio mode continues to use the env-var key once at boot; HTTP mode (upcoming commit) creates a per-request CueAPIClient scoped to the OAuth access-token's underlying CueAPI key. - package.json: bumped to 0.2.0. Added express + better-sqlite3 as direct deps for the HTTP transport; types + supertest for tests. No behavior change for stdio users. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements Stage C of the multi-key scoping sprint — the remote MCP
transport that lets Claude.ai Custom Connectors (and any other HTTP-
speaking MCP host) authenticate via OAuth and talk MCP over HTTP.
## New files
- src/pkce.ts — RFC 7636 S256 verifier helpers. Rejects plain method
even if advertised; rejects verifiers outside the 43-128 char
range. Constant-time comparison. Pure Node crypto, no deps.
- src/token-store.ts — abstract TokenStore interface plus a
SQLiteTokenStore backend. Three tables: auth_codes (60s TTL),
access_tokens (24h), refresh_tokens (30d). CueAPI api_key stored
encrypted at rest via AES-256-GCM with a key derived from
OAUTH_SIGNING_SECRET (32-char minimum enforced). State-signing
helpers (signState / verifyState) HMAC-SHA256 a JSON envelope so
the PKCE challenge + Anthropic's state round-trip through
cueapi.ai's magic-link flow tamper-proof.
- src/http-entry.ts — Express app with six endpoints:
- GET /health
- GET /.well-known/oauth-authorization-server (RFC 8414)
- GET /authorize (PKCE mandatory, S256 only)
- GET /callback/cueapi (verifies signed state, calls Stage B's
/v1/auth/mcp-exchange to mint a scoped api_key, redirects
to Anthropic's redirect_uri with a 60s auth_code)
- POST /token (authorization_code + refresh_token grants; PKCE
verification on exchange; auth_code deleted single-use
on consumption)
- POST /mcp (StreamableHTTPServerTransport, Bearer access_token
auth; per-request CueAPIClient scoped to the stored key)
A 5-minute interval task drains expired rows. Graceful shutdown on
SIGINT/SIGTERM closes the SQLite handle.
## Security properties
- PKCE is mandatory on /authorize (rejects absent code_challenge,
rejects plain method)
- State envelope signed HMAC-SHA256; tampered states rejected at
/callback/cueapi with no side effects
- Auth codes single-use; replay after exchange returns
invalid_grant
- Access tokens never expose the underlying CueAPI api_key (looked
up server-side on /mcp, never echoed back)
- api_key encrypted at rest via AES-256-GCM; SQLite file
exfiltration without the signing secret yields no usable keys
- Token store enforces 32-char minimum on OAUTH_SIGNING_SECRET
## Config (env vars for HTTP mode)
Required:
MCP_PUBLIC_URL e.g. https://mcp.cueapi.ai
OAUTH_SIGNING_SECRET >=32 chars, random
ALLOWED_CLAUDE_AI_CLIENT_IDS comma-separated OAuth client_ids
CUEAPI_MCP_CLIENT_ID this deployment's client_id on cueapi.ai
Optional:
MCP_PORT=3000
CUEAPI_BASE_URL=https://api.cueapi.ai
CUEAPI_MCP_EXCHANGE_ENDPOINT=<override>
SQLITE_PATH=./mcp-tokens.db
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tests/pkce.test.ts (8 tests) — RFC 7636 S256 vector, determinism, tampered verifier rejection, plain-method rejection, length-bound rejection, base64url round-trip. tests/token-store.test.ts (15 tests) — encryption round-trip, IV randomization, GCM tamper detection, wrong-secret rejection; state signing + verification, tampered body/sig/secret rejection; SQLite CRUD for all three token kinds, expiry behavior on read, cleanup drops only expired rows, raw-DB-file scan confirms plaintext api_key is NOT present (encryption at rest is real). tests/oauth-flow.test.ts (13 tests) — uses supertest against buildApp() with a mock fetchApiKeyFromSession so /callback/cueapi doesn't need a live cueapi.ai. Exercises /health, /.well-known/..., /authorize happy path + 4 rejection cases, full flow /authorize → /callback/cueapi → /token → POST /mcp bearer check, plus three security cases: - /token rejects wrong PKCE verifier (invalid_grant) - /token rejects used auth code (single-use) - /callback rejects tampered signed state Full suite: 45 passed (4 files, zero failures). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
argus-qa-ai
approved these changes
Apr 18, 2026
argus-qa-ai
left a comment
There was a problem hiding this comment.
Argus review — LGTM. Security design is sound: PKCE mandatory (S256 only), HMAC-SHA256 state signing with timing-safe comparison, AES-256-GCM at-rest encryption with scrypt-derived key + randomized IV, auth codes single-use, appropriate TTLs (60s/24h/30d), 32-char secret minimum enforced at construction. Stateless StreamableHTTPServerTransport is the right pattern. One non-blocking flag: userId from mcp-exchange is discarded in /callback/cueapi — access_token and refresh_token store userId:"". Not a security issue, but worth wiring up for future features. 45/45 tests pass. Stage B (#275) is live. Approved.
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage C of the multi-key-per-user sprint. Ships the remote MCP transport that lets Claude.ai Custom Connectors (and any other HTTP-speaking MCP host) authenticate via OAuth and talk MCP over HTTP.
Depends on Stage B (cueapi #275) being live, since
/callback/cueapicallsPOST /v1/auth/mcp-exchangeto mint the per-connection CueAPI api_key.What lands
Dual-transport entry (backward compat preserved).
src/index.tsroutes tostdio-entry.ts(default, same as 0.1.x) orhttp-entry.ts(new) based on--transportflag orMCP_TRANSPORTenv var. Existing Claude Desktop / Code / Cursor / Zed configs keep working with zero changes.HTTP server with six endpoints:
GET /healthGET /.well-known/oauth-authorization-serverGET /authorizeGET /callback/cueapi/v1/auth/mcp-exchange, mints 60s auth_codePOST /tokenauthorization_code+refresh_tokengrants, PKCE verified, auth_code single-usePOST /mcpStreamableHTTPServerTransport, bearer-auth'dToken store — abstract
TokenStoreinterface;SQLiteTokenStoredefault backend. Three tables:auth_codes(60s TTL),access_tokens(24h),refresh_tokens(30d). CueAPIapi_keyencrypted at rest with AES-256-GCM, key derived fromOAUTH_SIGNING_SECRETvia scrypt.State-signing helpers —
signState/verifyStateHMAC-SHA256 a JSON envelope so the PKCE challenge + Anthropic's state round-trip through cueapi.ai's magic-link flow tamper-proof.Client refactor —
CueAPIClientconstructorapiKeyis now optional;request()accepts a trailing override. Stdio mode continues to use the env-var key once at boot; HTTP mode creates a per-request client scoped to the OAuth access_token's stored api_key.Security properties (asserted by tests)
/authorize— rejects absentcode_challenge, rejectsplainmethod/callback/cueapiwith no side effectsinvalid_grantapi_keyencrypted at rest via AES-256-GCM — asserted by raw-DB-file scan confirming the plaintext is NOT present on diskOAUTH_SIGNING_SECRETminimum 32 chars enforced atSQLiteTokenStoreconstructionConfig (HTTP mode)
Required:
Optional:
Stdio mode (existing):
CUEAPI_API_KEY+ optionalCUEAPI_BASE_URL. Unchanged.Tests
npm test→ 45 passed, 0 failures across 4 files:tests/pkce.test.ts(8) — S256 RFC vector, tamper rejection, length bounds, base64url round-triptests/tools.test.ts(9) — existing, untouchedtests/token-store.test.ts(15) — encryption + IV randomization + GCM tamper + wrong-secret + state signing + SQLite CRUD + TTL + cleanup + at-rest scantests/oauth-flow.test.ts(13) — supertest end-to-end with mockedfetchApiKeyFromSession. Happy path/authorize→/callback/cueapi→/token→POST /mcpbearer-check, plus PKCE rejection, auth-code replay rejection, tampered state rejectionWhat's NOT in this PR (deferred)
mcp.cueapi.aisetup; the Dockerfile lands alongside it.oauth-flow.test.tscover the main paths; exhaustive coverage is a follow-up.Version bump
package.json→0.2.0(new capability, minor bump). Not yet published — publish after #275 (Stage B) lands in prod.Commits
e6ccdcarefactor(transport): dual-transport router + stdio extracted + per-request apiKeycdb0abafeat(http): HTTP transport with OAuth 2.1 + PKCE + SQLite token stored75604ctest(http): 36 new tests for pkce, token-store, OAuth flow end-to-endTest plan
npm run buildclean (confirmed locally — zero TS errors)npm test→ 45/45 green (confirmed locally)