Transit MCP 6.0 is an anonymous, read-only service at https://transitmcp.com/mcp. The previous https://metro-mcp.anuragd.me/mcp endpoint remains an allowlisted alias of the same Worker. No login is required. No client credentials, bearer token, or OAuth flow is required, and Transit MCP does not collect client credentials. Stale Authorization headers are ignored before the request reaches the MCP handler.
Any client can call the public endpoint; there is no caller identity. Cloudflare applies roughly 300 requests per 60-second window per source-IP key per Cloudflare location. Shared egress IPs group clients, and enforcement is eventually consistent. This is abuse protection, not a guarantee of a dedicated user quota.
Mcp-Method and Mcp-Name are untrusted request headers at the edge. An edge rule must validate and allowlist any secondary dimension itself, key primarily on trusted Cloudflare identity or source IP, and never treat a header as identity.
The Worker derives trust from configured MCP_PUBLIC_ORIGIN and hostname allowlists, never from an incoming Host header. MCP requests must arrive at the canonical origin or at an HTTPS default-port URL whose hostname is in MCP_ALLOWED_HOSTNAMES. Undeclared or malformed Host and browser Origin values return 403; origin-less desktop/server clients remain valid. Exact POST/OPTIONS /sse requests are rewritten to /mcp. GET and DELETE, slash variants, and legacy session-message URLs return 405.
MCP 2026 clients send request metadata on every operation without an initialization handshake. Header/body version, method, and name mismatches are rejected. The server is stateless and does not advertise protocol sessions, resumability, or server push.
Modern ambiguous-station selection uses signed request state with a five-minute TTL. MCP_REQUEST_STATE_KEY is a dedicated stable secret of at least 32 bytes and must differ by environment. There is no caller identity: state binds the MCP method plus validated operation payload semantics (tool, city, query, and offered candidate IDs) and rejects expiration, tampering, a changed query, and selections outside the offered candidates. State is signed rather than encrypted, so it contains no secrets or unnecessary personal data.
The active SDK v2 registrations define a JSON Schema through Zod for every tool. JSON Schema and Zod enforce the documented types, requiredness, object shape, and enums where present. For example, city fields use the supported-city enum, while optional numeric coordinates remain optional numbers. A description such as "7-digit regional bus stop ID" is documentation unless the corresponding schema also declares that constraint.
Validation beyond the wire schema is field-specific:
- Station searches normalize case and whitespace where needed, then resolve names and identifiers against transit-domain data. Resource and transfer lookups reject unknown cities or identifiers.
- Thrown WMATA failures and other uncaught non-cancellation adapter errors are mapped to operational tool errors by the shared tool-error boundary. MTA prediction-feed failures are skipped, so predictions may be partial or empty. MTA incident-feed failures return empty incidents. Abort failures rethrow in both MTA paths, and MCP protocol errors keep their own semantics.
- Path and query values are encoded only where the active transit adapter does so. The optional WMATA bus-route filter uses URL encoding; other fields rely on domain lookup, numeric types, or their adapter contract. This is not a universal character or path constraint.
- Successful tool results are returned as structured JSON plus a JSON-serialized text representation. That structured JSON and text are not rendered as trusted HTML by the Worker.
Tool inputs are not universally sanitized. Many strings intentionally have no generic regex or maximum-length constraint, including station names, station searches, line/route identifiers, and bus stop IDs. The server does not execute tool input as SQL or shell commands. Any new field constraint must be added to the active Zod schema and covered by a wire-level test before it is documented as enforced.
Cloudflare serves matching static assets before invoking the Worker. The checked-in public/_headers policy therefore applies the static response headers at the asset-serving boundary. Responses generated by Worker code receive response-type-aware headers from the outer Worker, which preserves route-owned values and fills only headers that are absent.
- MCP JSON responses receive a deny-by-default CSP with scripts, styles, images, connections, frames, base URIs, and form actions disabled.
- Event streams receive a deny-by-default CSP that allows only same-origin connections.
- Privacy, terms, and support pages receive a scriptless CSP with
script-src 'none', plusX-Frame-Options,X-Content-Type-Options,Referrer-Policy, andPermissions-Policyfrom the static-assets policy. - The landing and docs pages have a separate path-specific CSP for their existing inline interactions and first-party/Google Fonts resources.
- Other responses receive the shared
X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy, and disabled legacy XSS-filter headers when a route has not already supplied a stricter value.
Static pages contain no untrusted HTML.
1. Keep schemas and downstream boundaries explicit
inputSchema: z.object({
city: z.enum(['dc', 'nyc']),
query: z.string(),
})Schema descriptions explain values to clients but do not create regex or length constraints. Encode URL components in the transit adapter that builds the relevant upstream request, and render user-influenced HTML only through the dedicated escaping path.
2. Use TypeScript Strictly
// Enable in tsconfig.json
{
"strict": true,
"noUncheckedIndexedAccess": true
}3. Handle Errors and Telemetry Safely
Preserve abort and MCP protocol errors at their existing boundaries, and use the shared tool-error mapping for uncaught operational failures. Send Metro-owned diagnostics through serializeTelemetry; do not add raw error objects, secrets, or user payloads to Metro-owned log entries.
4. Use Prepared Statements (if using SQL)
// We don't use SQL, but if we did:
db.query('SELECT * FROM stations WHERE id = ?', [stationId]);
// NOT: db.query(`SELECT * FROM stations WHERE id = '${stationId}'`);5. Review Exact Dependency Pins
bun install --frozen-lockfile
bun audit6. Review Code for Security
- Check each input has an accurate SDK schema and field-specific downstream boundary
- Verify that authentication is absent by design and no caller identity is inferred from request headers
- Verify any Cloudflare edge policy keys on trusted Cloudflare identity or source IP
- Confirm security headers are set
7. Test Security Features
- Exercise the registered SDK wire schema rather than a duplicate validator.
- Test every documented enum, regex, range, or length limit at that wire boundary.
- Verify outbound URL construction and HTML escaping at their actual adapter/rendering boundaries.
1. Use Strong Secrets
# Store each value interactively; do not put it in shell history or config
bunx wrangler secret put MCP_REQUEST_STATE_KEY
bunx wrangler secret put WMATA_API_KEYProduction and preview require distinct WMATA API keys, MRTR state keys, origins, allowlists, and rate-limit bindings.
2. Monitor Edge Rate Policy
- Review Cloudflare edge analytics separately from application telemetry
- Check for unusually high edge rejection rates
- Adjust configured edge rules if legitimate users are blocked
- Investigate patterns of abuse
3. Review Logs Regularly
Metro application telemetry is serialized through serializeTelemetry into these allowlisted fields only: era, protocolVersion, mcpMethod, mcpName, clientName, alias, upstream, durationMs, and statusClass. String dimensions use finite server-owned categories. Correlation and session identifiers are never emitted. Unknown fields and unsupported categorical values are dropped; unknown client names become unclassified. The only retained client software names are ChatGPT, openai-mcp, Claude Desktop, claude-ai, and the explicit synthetic client transit-prompt-evals. These are unverified self-reports from legacy initialize clientInfo or modern io.modelcontextprotocol/clientInfo metadata, not evidence of identity or human usage. Request arguments, coordinates, queries, references, itineraries, personal aliases, IPs and API keys never enter serialized telemetry.
recordMcpUsage additionally writes one Workers Analytics Engine data point (ANALYTICS binding, mcp_usage dataset) per legacy initialize compatibility event and per tools/call. The physical layout stays five blobs (method, tool, client category, environment, HTTP status class), one double (duration), and an always-empty index. Allowed environments are production, preview, development, and test; anything else becomes unclassified. Initialization events are reported separately and never added to tool-call totals. HTTP 2xx cannot prove a tool-level success. The write is synchronous, cannot throw into the request, and is skipped when the binding is absent. Analytics Engine retains data for three months; the public privacy page discloses this store. Historical identifiers are not queried by the aggregate report; this change does not delete historical records.
See anonymous usage and prompt evaluation tooling for dry-run commands, explicit filtering, and evidence limits.
Worker log and tail access remain sensitive. Restrict access, limit retention, and redact downstream before exporting or sharing logs.
4. Keep exact runtime pins reviewed
bun install --frozen-lockfile
bun audit5. Use Environment Variables Correctly
6. Preserve rollback boundaries
- Keep the inactive
MetroMcpAgentexport and originalv1migration. - Do not add a Durable Object deletion migration during the 6.0 stabilization and rollback window.
- Roll back by restoring the prior Worker version and its prior binding configuration.
- Treat production deployment, DNS, secret changes, and old namespace deletion as separately approved operations.
Metro application telemetry reports only the response status class, not a raw response or error. Metro-owned code must not add raw error objects, secrets, request bodies, user payloads, or MRTR responses. The direct conformance runner uses only its target URL and does not add an authentication layer.
If Security Issue Discovered:
- Assess severity: What data/systems are affected?
- Contain: Can it be mitigated quickly?
- Fix: Deploy patch
- Notify: Inform affected users if needed
- Review: How did it happen? How to prevent?
If Abuse Detected:
- Identify: Which IPs/users?
- Block: Add or update an approved Cloudflare edge rule if needed
- Investigate: Automated or targeted attack?
- Adjust: Update rate limits or validation rules
- Every tool input has an accurate SDK schema and documented downstream boundary
- Any edge rate policy primarily keys on trusted Cloudflare identity or source IP and independently allowlists secondary request-header dimensions
- Anonymous MCP access is documented accurately
- Production and preview origins use HTTPS; only loopback development may use HTTP
- Security headers are applied
- Secrets are stored in environment variables
- Exact dependency pins are reviewed and the frozen install passes
- Security tests pass
- Worker log/tail access is restricted, retention is limited, and downstream exports are redacted
- Public errors and structured telemetry are reviewed for sensitive-data disclosure
- Production and preview origins, rate-limit bindings, and MRTR keys are distinct
- Rollback assets and the original
v1migration remain intact