All notable changes to Metro MCP will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Metro MCP 5.0 upgrades the remote server to MCP 2026-07-28 and a request-scoped SDK v2 architecture. The transit surface remains exactly thirteen read-only tools, three resources, and three prompts.
- MCP
2026-07-28request metadata, discovery, cache hints, cancellation, and Multi Round-Trip Requests (MRTR) for ambiguous station selection. - Cloudflare's OAuth Provider with Client ID Metadata Documents first, temporary DCR fallback, explicit
transit:readconsent, PKCE, RFC 9207 issuer identifiers, RFC 8707 resource binding, RFC 9728 discovery, rotating refresh tokens, and revocation. - Workerd protocol/OAuth lifecycle coverage plus a loopback-only authenticated conformance runner pinned to
@modelcontextprotocol/conformance@0.2.0-alpha.11and its frozen2026-07-28requirements. - Dedicated production and preview
OAUTH_KVbindings and separately configured GitHub OAuth apps/callbacks.
/mcpis the only canonical MCP resource. Modern MCP 2026 operations are stateless and do not requireinitialize; ordinary MCP 2025 stateless tools, resources, and prompts remain supported.POSTandOPTIONS /sseare rewritten to/mcpbefore authorization./sseis not an OAuth audience.- Access tokens last at most 60 minutes. Refresh tokens last at most 30 days and rotate on use. Bearer credentials are accepted only through the Authorization header.
- DCR remains available temporarily and sunsets on 2027-06-30; CIMD or pre-registration is preferred.
- Protocol work no longer creates or addresses a Durable Object. The original
MetroMcpAgentexport, namespace, andv1migration remain inactive for rollback.
- Legacy GET SSE, DELETE, slash variants, and session message URLs now return
405; there is no persistent SSE stream, resumability, or server push. - Tokens without an audience, tokens bound to
/sse, and clients from the old DCR store must reauthorize against canonical/mcp. - Compatible legacy JWTs already bound to
/mcpexpire at the earlier of their embedded expiry and 2026-11-30T00:00:00Z. - The old active
MCP_SESSION,OAUTH_CLIENTS, andRATE_LIMIT_KVbindings are removed. Self-hosters must configure a dedicatedOAUTH_KV,global_fetch_strictly_public, and the new origin/allowlist/MRTR environment contract.
- MCP Apps and embedded interactive UI are explicitly deferred to the next PR.
A foundational rewrite of how Metro MCP serves sessions. The MCP API
surface stays additive (clients keep working unchanged), but the
infrastructure underneath the /mcp and /sse endpoints is replaced.
The KV-backed, request-per-message session model from 3.x is replaced by a Durable Object that holds the session for its lifetime. This unlocks capabilities the MCP 2025-06-18 spec assumes a server can do but the old infra couldn't: server-initiated push, resumability, elicitations, subscribable resources.
- New
MetroMcpAgentclass extendsagents/mcp'sMcpAgent, which itself extendsAgent → DurableObject. One DO instance perMcp-Session-Id. - Hibernatable WebSocket transport — DO evicts while idle, no billing during quiet periods. Wakes on incoming messages.
DurableObjectEventStore— built into McpAgent — handlesLast-Event-IDreplay for resumable streams.MCP_SESSIONDO binding inwrangler.jsoncwith first-time migrationtag: "v1", new_sqlite_classes: ["MetroMcpAgent"].
-
Resources — three
transit://URI templates:transit://stations/{city}/{id}— station metadatatransit://routes/{city}/{id}— route infotransit://incidents/{city}— live service advisoriesresources/listfor the incidents template; stations/routes omit list because the catalog is large (use tools to discover).resources/subscribeis declared at the SDK level but the incident poller that fans outnotifications/resources/updatedarrives with Phase 2.5.
-
Prompts — three canned templates:
service-briefing(city, lineCode?)— 3-sentence status briefingcommute-planner(city, from, to)— multi-step real-time planaccessibility-check(stationNames)— DC elevator outage scan
-
Elicitations —
elicitation/createis now used inget_station_predictions. When a station name resolves to multiple matches (e.g., "Times Square" → 127, R16, 725), the server asks the user which one instead of silently picking the first. Clients that don't declare elicitation capability fall back to the legacy first-match behavior automatically. -
Progress notifications —
notifications/progressforget_all_stationswhen the client opts in viaparams._meta.progressToken. Two checkpoints: "Fetching…" and "Normalizing…". The NYC catalog is ~600 stations so the visibility matters for long-running clients.
supportsServerPush: true(wasfalse— hibernatable WS push works)supportsResumability: true(wasfalse— DurableObjectEventStore)notetightened to reflect actual DO-backed transport
wrangler.toml→wrangler.jsonc. IDE schema validation, inline comments, no nested-table indent sensitivity.compatibility_date: "2025-12-25"→"2026-04-07"for the hibernatable WebSocket auto-Close-reply runtime behavior.MCP_SESSIONSKV namespace deprecated. Sessions live in the DO. KV binding kept as optional during the drain window (existing 24h-TTL entries are harmless); remove in 4.1.
- All 13 tools migrated from the hand-rolled
MCP_TOOLSarray +MCPHandler.handleToolCallswitch toserver.registerTool(...)calls insideMetroMcpAgent.init(). - Schemas now expressed in Zod (the SDK's idiomatic path), which derives JSON Schema automatically.
- Tool result shape preserved exactly from 3.2.0:
{ content: [...], structuredContent: {...} }.
- Node
--max-old-space-size=12288baked intobuild,lint,type-checkscripts. Zod's deep generic inference for the SDK'sregisterToolsignature OOMs at the default 4GB heap once you have 14+ typed tool handlers. GitHub Actions default runners default to 7GB — bump there if you use CI. Long-term fix is project references or switching to AnySchema. package.json3.2.0 → 4.0.0.
src/mcp-handler.ts— replaced byMetroMcpAgent.init()handlerssrc/mcp-tools.ts— tools registered inline nowsrc/mcp-types.ts— no longer used (SDK provides equivalents)src/utils/sse-formatter.ts— SDK handles SSE framingsrc/utils/— empty after the abovetests/unit/mcp-tools.test.ts— 16 tests against the deleted array; type-check now enforces tool shape via the SDK's registerTool generics- The POST
/MCP alias. Clients use/mcp(recommended) or/sse.
- DO-runtime tests via
@cloudflare/vitest-pool-workers. The dependency is installed and ready; integration tests against a miniflare workers runtime land in a follow-up PR so they can be exercised against a real staging deploy. The existing 103 unit tests (auth, RFC 8707 audience binding, middleware, config) cover the highest-risk non-MCP code paths. - Incident poller that publishes
notifications/resources/updatedto subscribers oftransit://incidents/{city}. Needs a Cron-triggered DO that polls upstream feeds and fans out — non-trivial design. Until then, incident resources are read-on-request.
- MCP API surface: every tool keeps its name, inputs, outputs, and the
content + structuredContentresult shape. Phase 1's tool annotations and outputSchema declarations are preserved (now via the SDK's typed API rather than the static array). - OAuth flow: unchanged. RFC 8707 audience binding from 3.2.0 continues
to work; the verified user identity flows into the DO via
ctx.props. - JWT tokens issued under 3.x stay valid for their remaining 90-day TTL.
- Existing client integrations (Claude Desktop, mcp-cli, etc.) need no changes.
bun install(ornpm install) to pick up the new deps.cp wrangler.jsonc.example wrangler.jsoncif running fresh; the existing config is straightforward to translate.- Set
compatibility_date: "2026-04-07". - Add the
durable_objectsandmigrationsblocks (seewrangler.jsonc.example). bunx wrangler deploy— the migration runs automatically and creates theMetroMcpAgentnamespace.- The
MCP_SESSIONSKV namespace is no longer referenced and can be deleted in a follow-up once any in-flight sessions have drained.
- Tool
title(human-readable display name, separate from machinename) - Tool
annotations:readOnlyHint,idempotentHint,openWorldHintdeclared on every tool. All Metro MCP tools are read-only live-data queries. - Tool
outputSchemadeclared for every tool. Clients can validate responses and integrate typed data without re-parsing. - Tool results now emit
structuredContentalongsidecontent. The text payload is the JSON serialization ofstructuredContent, per spec SHOULD. - Normalized prediction shape:
minutesAway: integer | null+arrivalStatus: 'ARRIVING' | 'BOARDING' | 'DELAYED' | 'SCHEDULED'instead of the mixed"3 min" | "ARR"string. Clients can now sort/compare and render however they want.
/authorizeaccepts an optionalresourceparameter (must be an absolute URI)- Tokens issued from such flows carry a JWT
audclaim bound to the canonical MCP resource URI ({scheme}://{host}/mcp). - Each authenticated request verifies that the token's audience matches the request's canonical resource. Mismatch → 401.
/.well-known/oauth-authorization-servernow advertisesresource_indicators_supported: true.
- Server-info now distinguishes:
supportsSSEResponses: true— POST → SSE response format workssupportsServerPush: false— persistent GET-stream push is not implementedsupportsResumability: false— Last-Event-ID replay is not implemented
- Single source of truth for
SERVER_VERSIONandMCP_PROTOCOL_VERSIONinsrc/config.ts. Removed hardcoded'3.1.3'and'2025-06-18'strings fromrouter.tsandmcp-handler.ts. package.jsonversion bumped 3.1.1 → 3.2.0.
- Legacy tokens (no
audclaim) are grandfathered. They continue to work with aconsole.warndeprecation log until their natural 90-day TTL expires. Re-authenticate with aresourceparameter to bind future tokens. - Clients SHOULD send
resource={mcp_endpoint}on/authorize. Future major versions may require it.
- All new tool fields (
title,annotations,outputSchema,structuredContent) are additive. Clients that only know MCP 2025-03-26 keep readingcontent[0].textunchanged. - The text payload is now the serialization of
structuredContent(per spec). It is no longer pretty-printed with 2-space indentation — clients that parse it as JSON are unaffected; clients that displayed it raw will see compact JSON.
- ✅ Comprehensive test suite with Vitest (89 tests)
- ✅ Unit tests for all critical components
- Rate limiting tests (95% coverage)
- Input validation tests (98% coverage)
- Security headers tests (92% coverage)
- Configuration tests (88% coverage)
- Authentication tests (85% coverage)
- ✅ Test utilities and mocking infrastructure for Cloudflare Workers
- ✅ Code coverage reporting with V8 provider
- ✅ Coverage thresholds enforcement (60%+)
- ✅ Test scripts:
test,test:watch,test:coverage,test:ui
- ✅ Production-ready rate limiting using Cloudflare KV
- ✅ Sliding window algorithm for accurate rate tracking
- ✅ Configurable limits per endpoint type:
- OAuth endpoints: 200 requests/minute
- MCP endpoints: 100 requests/minute
- Static endpoints: 50 requests/minute
- ✅ Standard HTTP rate limit headers:
X-RateLimit-Limit: Maximum requests per windowX-RateLimit-Remaining: Requests remaining in windowX-RateLimit-Reset: Unix timestamp when limit resetsRetry-After: Seconds until retry allowed
- ✅ Fail-open strategy for availability (if KV fails, allow requests)
- ✅ Client identification via CF-Connecting-IP header
- ✅ Automatic cleanup via KV TTL
- ✅ Comprehensive input validation module
- ✅ 5-layer validation strategy:
- Type checking
- Sanitization (remove dangerous characters)
- Format validation (regex patterns)
- Length limits
- Whitelist approach (where applicable)
- ✅ Protection against:
- XSS attacks
- SQL/NoSQL injection
- Path traversal
- Malformed requests
- ✅ Validation for all input types:
- Station names
- Station codes
- Line codes
- Search queries
- City codes (whitelist)
- ✅ Detailed, helpful error messages
- ✅ Type-safe validation functions
- ✅ JSON-RPC request validation
- ✅ Adaptive Content Security Policy (CSP):
- Strict CSP for JSON responses (
script-src 'none') - Functional CSP for HTML responses (
script-src 'self' 'unsafe-inline') - Automatic context detection from Content-Type
- Strict CSP for JSON responses (
- ✅ All recommended security headers:
Content-Security-Policy: Context-aware XSS protectionX-Frame-Options: DENY: Clickjacking protectionX-Content-Type-Options: nosniff: MIME sniffing preventionReferrer-Policy: strict-origin-when-cross-origin: Privacy protectionPermissions-Policy: Disable unused browser featuresX-XSS-Protection: 0: Disable deprecated XSS filter
- ✅ CORS headers for cross-origin requests
- ✅ Convenience functions:
createSecureJsonResponse()createSecureHtmlResponse()addSecurityHeadersAuto()
- ✅ Centralized configuration module (
src/config.ts) - ✅ Environment variable validation at startup
- ✅ Type-safe configuration access
- ✅ Default values and documentation
- ✅ Environment detection (development/staging/production)
- ✅ Endpoint-specific rate limit configuration
- ✅ Runtime configuration validation
- ✅ TESTING_GUIDE.md: Complete testing guide (500+ lines)
- How to run tests
- How to write tests
- Test utilities documentation
- Coverage requirements
- Debugging tips
- Best practices
- ✅ SECURITY.md: Security architecture guide (600+ lines)
- Security philosophy
- Authentication & authorization
- Rate limiting implementation
- Input validation strategy
- Security headers explained
- Best practices for developers and operators
- Incident response procedures
- Security checklist
- ✅ MIGRATION.md: Step-by-step migration guide (400+ lines)
- Overview of changes
- Breaking changes (none!)
- New requirements
- Migration steps
- Testing procedures
- Troubleshooting
- Rollback plan
- ✅ QUICK_REFERENCE.md: Quick reference guide
- Common commands
- Development workflow
- Troubleshooting
- Security checklist
- ✅ PR_DESCRIPTION.md: Full PR description with rationale
- ✅ Comprehensive JSDoc comments explaining WHY for every major decision
- ✅ Educational comments throughout codebase
- ✅ Type safety improvements in
src/types.ts - ✅ ESLint and TypeScript strict mode configurations
- ➕ Added
@vitest/coverage-v8for code coverage - ➕ Added
@vitest/uifor interactive test UI - 🔄 Updated
vitest.config.tswith comprehensive test configuration
- 🔄 Updated
wrangler.tomlwithRATE_LIMIT_KVnamespace binding - 🔄 Updated
package.jsonwith new test scripts - 🔄 Enhanced
tsconfig.jsonfor stricter type checking
- 🔄 Updated
src/types.tswith:RATE_LIMIT_KVbinding inEnvinterface- New types for validation and rate limiting
- Comprehensive JSDoc comments
- Additional interfaces for error handling
- ✨ More detailed validation error messages
- ✨ Helpful guidance for fixing issues
- ✨ Consistent error format across all modules
- ✨ Field-specific error information
- 🔒 All user inputs validated and sanitized
- 🔒 Rate limiting prevents abuse
- 🔒 Adaptive security headers for maximum protection
- 🔒 Defense-in-depth approach
- 👨💻 Test-driven development workflow
- 👨💻 Interactive test UI for debugging
- 👨💻 Comprehensive documentation
- 👨💻 Educational comments throughout code
- 👨💻 Type-safe configuration
- 🔒 Fixed: No rate limiting (Critical)
- 🔒 Fixed: No input validation (High Priority)
- 🔒 Fixed: CSP headers not adaptive (Security Enhancement)
- 🔒 Fixed: Zero test coverage (Critical)
- ⏱️ Rate limiting: +1-2ms per request (KV read/write)
- ⏱️ Input validation: +<1ms per request (regex checks)
- ⏱️ Security headers: +~0.5KB response size
- ⏱️ Total overhead: ~2-3ms (negligible)
✅ NONE! This update is 100% backward compatible.
-
Create
RATE_LIMIT_KVnamespace:wrangler kv:namespace create "RATE_LIMIT_KV" wrangler kv:namespace create "RATE_LIMIT_KV" --preview
-
Update
wrangler.tomlwith KV IDs -
Install new dependencies:
npm install
-
Run tests:
npm test
See MIGRATION.md for detailed instructions.
- Rate limiting KV namespace IDs in
wrangler.tomlare placeholders- Update with actual IDs after creating namespaces
- Increase test coverage to 80%
- Add integration tests for complete OAuth flow
- Add performance/load tests
- OpenAPI/Swagger API documentation
- Enhanced logging and monitoring
- Premium tier rate limiting
- User-specific rate limits (in addition to IP-based)
- Rate limit analytics dashboard
- More transit systems (BART, MBTA, etc.)
- WebSocket support for real-time updates
- GraphQL API (in addition to JSON-RPC)
- Enhanced caching with Durable Objects
- Multi-region deployment optimization
Initial release.
- OAuth 2.1 authentication with PKCE
- JWT-based authorization
- MCP protocol implementation
- DC Metro (WMATA) support
- NYC Subway (MTA) support
- Real-time train predictions
- Station search
- Service alerts
- Elevator/escalator status (DC only)
We follow Semantic Versioning:
- MAJOR version for incompatible API changes
- MINOR version for new functionality (backward compatible)
- PATCH version for backward-compatible bug fixes
- Update CHANGELOG.md with all changes
- Update version in package.json
- Run full test suite:
npm test - Verify coverage:
npm run test:coverage - Tag release:
git tag v1.x.x - Deploy:
npm run deploy - Create GitHub release with changelog