Skip to content

feat(http): HTTP transport + OAuth 2.1 + PKCE for remote MCP hosts (0.2.0)#2

Merged
govindkavaturi-art merged 3 commits intomainfrom
feat/http-transport-remote-connector
Apr 18, 2026
Merged

feat(http): HTTP transport + OAuth 2.1 + PKCE for remote MCP hosts (0.2.0)#2
govindkavaturi-art merged 3 commits intomainfrom
feat/http-transport-remote-connector

Conversation

@govindkavaturi-art
Copy link
Copy Markdown
Member

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/cueapi calls POST /v1/auth/mcp-exchange to mint the per-connection CueAPI api_key.

What lands

Dual-transport entry (backward compat preserved). src/index.ts routes to stdio-entry.ts (default, same as 0.1.x) or http-entry.ts (new) based on --transport flag or MCP_TRANSPORT env var. Existing Claude Desktop / Code / Cursor / Zed configs keep working with zero changes.

HTTP server with six endpoints:

Endpoint Purpose
GET /health Liveness
GET /.well-known/oauth-authorization-server RFC 8414 metadata
GET /authorize OAuth entry — PKCE mandatory, S256 only
GET /callback/cueapi Post magic-link — calls /v1/auth/mcp-exchange, mints 60s auth_code
POST /token authorization_code + refresh_token grants, PKCE verified, auth_code single-use
POST /mcp MCP protocol via StreamableHTTPServerTransport, bearer-auth'd

Token store — abstract TokenStore interface; SQLiteTokenStore default backend. Three tables: auth_codes (60s TTL), access_tokens (24h), refresh_tokens (30d). CueAPI api_key encrypted at rest with AES-256-GCM, key derived from OAUTH_SIGNING_SECRET via scrypt.

State-signing helperssignState / verifyState HMAC-SHA256 a JSON envelope so the PKCE challenge + Anthropic's state round-trip through cueapi.ai's magic-link flow tamper-proof.

Client refactorCueAPIClient constructor apiKey is 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)

  • PKCE 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
  • api_key encrypted at rest via AES-256-GCM — asserted by raw-DB-file scan confirming the plaintext is NOT present on disk
  • OAUTH_SIGNING_SECRET minimum 32 chars enforced at SQLiteTokenStore construction

Config (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 Anthropic client_ids
CUEAPI_MCP_CLIENT_ID             this deployment's registered 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

Stdio mode (existing): CUEAPI_API_KEY + optional CUEAPI_BASE_URL. Unchanged.

Tests

npm test45 passed, 0 failures across 4 files:

  • tests/pkce.test.ts (8) — S256 RFC vector, tamper rejection, length bounds, base64url round-trip
  • tests/tools.test.ts (9) — existing, untouched
  • tests/token-store.test.ts (15) — encryption + IV randomization + GCM tamper + wrong-secret + state signing + SQLite CRUD + TTL + cleanup + at-rest scan
  • tests/oauth-flow.test.ts (13) — supertest end-to-end with mocked fetchApiKeyFromSession. Happy path /authorize/callback/cueapi/tokenPOST /mcp bearer-check, plus PKCE rejection, auth-code replay rejection, tampered state rejection

What's NOT in this PR (deferred)

  • RedisTokenStore — SQLite default works for single-instance Railway deploy. Interface is factored so a Redis backend can drop in later with no call-site changes.
  • Dockerfile + railway.json — Phase 7 of the sprint is a deployment doc for mcp.cueapi.ai setup; the Dockerfile lands alongside it.
  • HTTP transport client-SDK smoke tests — the three security tests in oauth-flow.test.ts cover the main paths; exhaustive coverage is a follow-up.

Version bump

package.json0.2.0 (new capability, minor bump). Not yet published — publish after #275 (Stage B) lands in prod.

Commits

  • e6ccdca refactor(transport): dual-transport router + stdio extracted + per-request apiKey
  • cdb0aba feat(http): HTTP transport with OAuth 2.1 + PKCE + SQLite token store
  • d75604c test(http): 36 new tests for pkce, token-store, OAuth flow end-to-end

Test plan

  • npm run build clean (confirmed locally — zero TS errors)
  • npm test → 45/45 green (confirmed locally)
  • Stdio mode still works — smoke test a local Claude Desktop pointing at this branch
  • After merge: publish 0.2.0 to npm, then start Stage D deploy

Gk and others added 3 commits April 18, 2026 08:35
…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>
Copy link
Copy Markdown

@argus-qa-ai argus-qa-ai left a comment

Choose a reason for hiding this comment

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

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.

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.

2 participants