Skip to content

feat: HTTP Stream transport with per-request credential injection#30

Open
fjprobos wants to merge 3 commits intoCognitionAI:mainfrom
madeofclay:feat/http-stream-transport-with-per-request-auth
Open

feat: HTTP Stream transport with per-request credential injection#30
fjprobos wants to merge 3 commits intoCognitionAI:mainfrom
madeofclay:feat/http-stream-transport-with-per-request-auth

Conversation

@fjprobos
Copy link
Copy Markdown

@fjprobos fjprobos commented Mar 13, 2026

Closes #29

Why this matters

The classic stdio transport requires one server process per user with credentials baked in at startup. This makes it incompatible with modern MCP clients like Claude Code CLI, which connect to HTTP endpoints and pass credentials per-request. This PR adds HTTP Stream transport support so a single deployed instance can serve many users — each with their own Metabase credentials.

stdio (classic) HTTP Stream (this PR)
Deployment One process per user One shared process
Credentials Env vars at startup Per-request headers
Claude Code CLI ❌ Not supported ✅ Supported
Cursor, Windsurf ✅ Supported ✅ Supported

Changes

Core feature

  • src/auth.ts (new): createAuthenticateHandler() + createClientResolver() — the per-request credential injection logic extracted into a testable module
  • src/server.ts: detects MCP_TRANSPORT=http, wires FastMCP authenticate() hook to extract x-metabase-url, x-metabase-api-key (or x-metabase-username/x-metabase-password) from request headers; falls back to stdio mode with shared client when env var is absent
  • All tool files: signature changed from a MetabaseClient instance to a getClient(ctx) resolver so each execute() call picks up the session's client
  • sse-server.js: legacy SSE wrapper for clients that only support the older SSE transport

Tests (31 passing, 0 pre-existing)

  • tests/auth.test.tscreateClientResolver and createAuthenticateHandler unit tests
  • tests/metabase-client.test.ts — constructor with api-key, username/password, invalid-credentials
  • tests/tool-filters.test.ts — flag parsing and default behaviour
  • tests/config.test.tsloadConfig / validateConfig env var permutations

Docs

  • README.md: new "HTTP Stream Transport" section with Claude Code CLI setup instructions, header reference table, and npm test command

Usage — Claude Code CLI

# Add to current project
claude mcp add --transport http metabase "https://your-server.example.com/mcp" \
  --header "x-metabase-url: https://your-metabase.com" \
  --header "x-metabase-api-key: your_api_key"

# Or globally (available in all projects)
claude mcp add --transport http --scope user metabase "https://your-server.example.com/mcp" \
  --header "x-metabase-url: https://your-metabase.com" \
  --header "x-metabase-api-key: your_api_key"

Test plan

  • MCP_TRANSPORT=http node dist/server.js starts HTTP server on port 8011
  • Connect via Claude Code CLI with x-metabase-url + x-metabase-api-key headers — tools work
  • Connect without headers — server returns 401 with descriptive message
  • Without MCP_TRANSPORT=http, server starts in stdio mode (no regression)
  • npm test — 31 tests pass

🤖 Generated with Claude Code

fjprobos and others added 3 commits March 13, 2026 14:12
Adds support for running the server in HTTP Stream mode (MCP_TRANSPORT=http),
enabling a single deployed instance to serve multiple users with different
Metabase credentials passed via request headers.

- server.ts: detect MCP_TRANSPORT=http, configure FastMCP authenticate()
  to extract x-metabase-url, x-metabase-api-key (or username/password) from
  headers and attach a per-session MetabaseClient; falls back to stdio mode
  with shared client when env var is absent
- All tool files: change signature from MetabaseClient instance to
  getClient(ctx) resolver so each execute() call uses the session's client
- sse-server.js: add legacy SSE wrapper that spawns stdio processes (for
  clients that only support the older SSE transport)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- src/auth.ts: extract createAuthenticateHandler + createClientResolver
  so the logic can be imported and unit-tested independently of server startup
- src/server.ts: use the new auth module (no behaviour change)
- tests/auth.test.ts: 11 tests covering getClient resolver (session client,
  defaultClient, throws when neither) and authenticate handler (api-key,
  username/password, env fallback, 401 on missing URL, 401 on missing creds,
  session isolation)
- tests/metabase-client.test.ts: constructor tests for api-key, username/
  password, and invalid-credentials paths
- tests/tool-filters.test.ts: parseToolFilterOptions for all flag combinations
  and default-to-essential behaviour
- tests/config.test.ts: loadConfig and validateConfig with env var permutations
- vitest.config.ts: Vitest config for ESM/TypeScript
- package.json: add "test" and "test:watch" scripts
- README.md: document HTTP Stream transport mode, Claude Code CLI integration,
  header reference table, and npm test command

31 tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… endpoint

Use SSE_SERVER_URL env var (falls back to localhost:8010).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fjprobos fjprobos force-pushed the feat/http-stream-transport-with-per-request-auth branch from 0dd7cdd to ee52da5 Compare March 13, 2026 14:12
fjprobos pushed a commit to madeofclay/metabase-mcp-server that referenced this pull request Mar 13, 2026
Adds an OAuth 2.0 Authorization Code + PKCE gateway so that web-based
MCP clients (Claude.ai, Claude Code web) can connect without pre-sharing
credentials via request headers.

## Why this is needed

The HTTP Stream transport (PR CognitionAI#30) requires clients to pass Metabase
credentials as request headers. Web clients like Claude.ai do not support
custom headers — they require a standard OAuth 2.0 flow. This gateway
bridges that gap.

## How it works

1. Client discovers the gateway via `/.well-known/oauth-authorization-server`
2. User is redirected to `/oauth/authorize` — an HTML form asking for
   Metabase URL + API key (or username/password)
3. On submit, server stores credentials under a short-lived auth code
   and redirects back to the client
4. Client exchanges the code at `/oauth/token` for a signed JWT
5. All `/mcp` requests carry `Authorization: Bearer <JWT>`
6. Gateway validates the JWT, injects `x-metabase-*` headers, and
   proxies to the upstream HTTP Stream server

## Security

- Credentials are never logged (only session prefix + status)
- Client registration details logged only at LOG_LEVEL=debug
- Auth codes are single-use with a 10-minute TTL
- PKCE (S256) support for public clients
- JWT signed with configurable JWT_SECRET

## Tests (28 passing)

- OAuth discovery endpoints
- Dynamic client registration (RFC 7591)
- Authorization endpoint: form rendering, XSS escaping, validation
- Token endpoint: api-key flow, username/password flow, single-use codes
- PKCE: correct verifier, wrong verifier, missing verifier
- MCP proxy: 401 without token, 401 with invalid token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

Feature request: HTTP Stream transport with per-request credential injection

1 participant