Commit Graph

3 Commits

Author SHA1 Message Date
Elie Habib
14a31c4283 feat(mcp): OAuth 2.0 Authorization Server for claude.ai connector (#2418)
* feat(mcp): add OAuth 2.0 Authorization Server for claude.ai connector

Implements spec-compliant MCP authentication so claude.ai's remote connector
(which requires OAuth Client ID + Secret, no custom headers) can authenticate.

- public/.well-known/oauth-authorization-server: RFC 8414 discovery document
- api/oauth/token.js: client_credentials grant, issues UUID Bearer token in Redis TTL 3600s
- api/_oauth-token.js: resolveApiKeyFromBearer() looks up token in Redis
- api/mcp.ts: 3-tier auth (Bearer OAuth first, then ?key=, then X-WorldMonitor-Key);
  switch to getPublicCorsHeaders; surface error messages in catch
- vercel.json: rewrite /oauth/token, exclude oauth from SPA, CORS headers
- tests: update SPA no-cache pattern

Supersedes PR #2417. Usage: URL=worldmonitor.app/mcp, Client ID=worldmonitor, Client Secret=<API key>

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

* docs: fix markdown lint in OAuth plan (blank lines around lists)

* fix(oauth): address all P1+P2 code review findings for MCP OAuth endpoint

- Add per-IP rate limiting (10 req/min) to /oauth/token via Upstash slidingWindow
- Return HTTP 401 + WWW-Authenticate header when Bearer token is invalid/expired
- Add Cache-Control: no-store + Pragma: no-cache to token response (RFC 6749 §5.1)
- Simplify _oauth-token.js to delegate to readJsonFromUpstash (removes duplicated Redis boilerplate)
- Remove dead code from token.js: parseBasicAuth, JSON body path, clientId/issuedAt fields
- Add Content-Type: application/json header for /.well-known/oauth-authorization-server
- Remove response_types_supported (only applies to authorization endpoint, not client_credentials)

Closes: todos 075, 076, 077, 078, 079

🤖 Generated with claude-sonnet-4-6 via Claude Code (https://claude.ai/claude-code) + Compound Engineering v2.40.0

Co-Authored-By: claude-sonnet-4-6 (200K context) <noreply@anthropic.com>

* chore(review): fresh review findings — todos 081-086, mark 075/077/078/079 complete

* fix(mcp): remove ?key= URL param auth + mask internal errors

- Remove ?key= query param auth path — API keys in URLs appear in
  Vercel/CF access logs, browser history, Referer headers. OAuth
  client_credentials (same PR) already covers clients that cannot
  set custom headers. Only two auth paths remain: Bearer OAuth and
  X-WorldMonitor-Key header.

- Revert err.message disclosure: catch block was accidentally exposing
  internal service URLs/IPs via err.message. Restore original hardcoded
  string, add console.error for server-side visibility.

Resolves: todos 081, 082

* fix(oauth): resolve all P2/P3 review findings (todos 076, 080, 083-086)

- 076: no-credentials path in mcp.ts now returns HTTP 401 + WWW-Authenticate instead of rpcError (200)
- 080: store key fingerprint (sha256 first 16 hex chars) in Redis, not plaintext key
- 083: replace Array.includes() with timingSafeIncludes() (constant-time HMAC comparison) in token.js and mcp.ts
- 084: resolveApiKeyFromBearer uses direct fetch that throws on Redis errors (500 not 401 on infra failure)
- 085: token.js imports getClientIp, getPublicCorsHeaders, jsonResponse from shared helpers; removes local duplicates
- 086: mcp.ts auth chain restructured to check Bearer header first, passes token string to resolveApiKeyFromBearer (eliminates double header read + unconditional await)

* test(mcp): update auth test to expect HTTP 401 for missing credentials

Align with todo 076 fix: no-credentials path now returns 401 + WWW-Authenticate
instead of JSON-RPC 200 response. Also asserts WWW-Authenticate header presence.

* chore: mark todos 076, 080, 083-086 complete

* fix(mcp): harden OAuth error paths and fix rate limit cross-user collision

- Wrap resolveApiKeyFromBearer() in try/catch in mcp.ts; Redis/network
  errors now return 503 + Retry-After: 5 instead of crashing the handler
- Wrap storeToken() fetch in try/catch in oauth/token.js; network errors
  return false so the existing if (!stored) path returns 500 cleanly
- Re-key token endpoint rate limit by sha256(clientSecret).slice(0,8)
  instead of IP; prevents cross-user 429s when callers share Anthropic's
  shared outbound IPs (Claude remote MCP connector)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 14:53:32 +04:00
Elie Habib
7100b56fdc feat(mcp): add 5 AI inference tools to MCP server (#2387)
* chore: redeploy to pick up WORLDMONITOR_VALID_KEYS fix

* feat(mcp): add 5 AI inference tools — WorldBrief, CountryBrief, DeductionPanel, Forecasts, Social

Add RpcToolDef discriminated union alongside CacheToolDef so the MCP can
support both Redis cache reads and live LLM inference calls.

New tools (22 total, up from 17):
- get_social_velocity: Reddit geopolitical signals (cache read, intelligence:social:reddit:v1)
- get_world_brief: fetches live headlines via list-feed-digest then calls
  summarize-article LLM; optional geo_context to focus the brief
- get_country_brief: calls get-country-intel-brief LLM endpoint; supports
  analytical framework injection (PR #2380 compatible)
- analyze_situation: calls deduct-situation for open-ended AI geopolitical
  reasoning with query + optional context
- generate_forecasts: calls get-forecasts for fresh AI forecast generation
  (distinct from get_forecast_predictions which reads pre-computed cache)

The _execute path makes internal fetch calls using new URL(req.url).origin
and forwards the PRO API key header.

* fix(mcp): address Greptile review — timeouts, User-Agent, test coverage

- generate_forecasts: reduce AbortSignal timeout 60s → 25s (Vercel Edge hard ceiling ~30s)
- get_world_brief: reduce timeouts 10s+30s → 8s+20s (stays under 30s)
- Add User-Agent header to all _execute internal RPC fetches
- Move get_social_velocity out of AI inference comment block (it's a cache tool)
- Assert _execute not leaked in tools/list alongside existing _cacheKeys check
2026-03-28 00:38:28 +04:00
Elie Habib
bca939c423 feat(mcp): PRO MCP server — WorldMonitor data via Model Context Protocol (#2382)
* feat(mcp): PRO MCP server — WorldMonitor data via Model Context Protocol

Adds api/mcp.ts: a Vercel edge function implementing MCP Streamable HTTP
transport (protocol 2025-03-26) for PRO API key holders.

PRO users point Claude Desktop (or any MCP client) at
https://api.worldmonitor.app/mcp with their X-WorldMonitor-Key header
and get 17 tools covering all major WorldMonitor data domains.

- Auth: validateApiKey(forceKey:true) — returns JSON-RPC -32001 on failure
- Rate limit: 60 calls/min per API key via Upstash sliding window (rl:mcp prefix)
- Tools read from existing Redis bootstrap cache (no upstream API calls)
- Each response includes cached_at (ISO timestamp) and stale (boolean)
- tools/list served from in-memory registry (<500ms, no Redis)
- Tool calls with warm cache respond in <800ms
- 11 tests covering auth, rate limit, protocol, tools/list, tools/call

* fix(mcp): handle Redis fetch throws + fix confusing label derivation

P1: wrap executeTool call in tools/call handler with try-catch so
TimeoutError/TypeError from AbortSignal.timeout in readJsonFromUpstash
returns JSON-RPC -32603 instead of an unhandled rejection → 500.
Mirrors the same guard already on the rate-limiter call above it.

P2: walk backwards through cache key segments, skipping version tags
(v\d+), bare numbers, 'stale', and 'sebuf' suffixes to find the first
meaningful label. Fixes 'economic:fred:v1:FEDFUNDS:0' → 'FEDFUNDS'
and 'risk:scores:sebuf:stale:v1' / 'theater_posture:sebuf:stale:v1'
both resolving to 'stale' (which collides with the top-level stale flag).

Adds test for P1 scenario (12 tests total, all pass).
2026-03-27 23:59:17 +04:00