mcp server
wsdot-mcp-server
Query WA highway conditions, ferry schedules, vessel locations, toll rates, border waits, and alerts via MCP. STDIO or Streamable HTTP.
Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- Node.js 20 or newer
{
"mcpServers": {
"wsdot-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/wsdot-mcp-server"
],
"env": {
"WSDOT_ACCESS_CODE": "<your WSDOT_ACCESS_CODE>"
}
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 14, 2026Public Hosted Server: https://wsdot.caseyjhand.com/mcp
Overview
Washington State transportation data from the WSDOT Traveler API and the WSF Ferry API. Query mountain pass and highway conditions, search alerts and cameras, and track ferry schedules, vessel locations, and terminal space from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
Tools
| Tool | Description |
|---|---|
wsdot_get_mountain_passes | Current conditions for all WA mountain passes: status, road condition, traction laws, temperature, elevation. |
wsdot_search_alerts | Active highway alerts — incidents, construction, closures — filterable by state route, WSDOT region, and milepost range. |
wsdot_get_travel_times | Current vs. average travel times for named WA highway corridors (I-5, I-90, SR 520, etc.) with congestion delay. |
wsdot_get_toll_rates | Dynamic toll rates for WA express lanes and tolled facilities — SR 99, SR 167 HOT, I-405 Express, SR 509, SR 520 — filterable by route. |
wsdot_get_border_waits | Current vehicle wait times at all WA/Canada land border crossings. |
wsdot_search_cameras | Highway camera metadata and image URLs, filterable by state route, region, milepost range, and title words. |
wsdot_get_ferry_terminals | All WSF ferry terminals with numeric IDs needed for schedule and space lookups, plus coordinates. |
wsdot_get_ferry_routes | WSF routes operating on a given date — route ID, abbreviation, description, and the terminal pairs each serves, for route discovery, schedule lookups, and ferry-alert cross-reference. |
wsdot_get_ferry_schedule | Departure times for a specific WSF route — today-remaining or full-day future mode — with each sailing's vessel, loading rule, and notes. |
wsdot_get_vessel_locations | Real-time AIS positions, speed, heading, ETA, and dock status for all active WSF vessels. |
wsdot_get_terminal_space | Drive-up and reservable vehicle space available at WSF terminals for upcoming sailings. |
wsdot_get_ferry_alerts | Active WSF service disruptions and bulletins with impacted route IDs. |
Capability reference
wsdot_get_mountain_passes tool
- No input — current conditions for all 16 WA mountain passes (Snoqualmie, Stevens, White, Blewett, Cayuse, and others) in one call
- Each pass carries road condition, weather, temperature, elevation, and up to two directional traction/travel restrictions
wsdot_search_alerts tool
- Optional
stateRoute,region(Northwest, Olympic, Southwest, South Central, North Central, Eastern), andstartMilepost/endMilepost— an alert matches when its extent overlaps the range; omit all for every statewide alert. Ordered byalertId, paged (default 20, max 500) - An unknown region fails as
invalid_regionand a start above the end asinvalid_milepost_range, each with a recovery hint
wsdot_get_travel_times tool
- Optional
route— a route ("I-5","SR 520") returns every corridor measured on it, any other text matches corridor names ("Everett"); paged (default 50, max 500) - Each corridor reports current and average minutes — current above average means congestion, and the delta is the delay. A reversible express-lane corridor closed in the queried direction omits its figures rather than reporting zero
wsdot_get_toll_rates tool
- Covers SR 99 (WSDOT Tunnel), SR 167 HOT Lanes, I-405 Express Lanes, the SR 509 tolled segment, and the SR 520 Bridge. Optional
stateRoutematches the posted designation ("I-405","SR 520"); a route with no tolled facility returns an empty page whose notice names the tolled routes. Paged (default 50, max 500) - Each row leads with its
startLocationName → endLocationNamesegment.stateRouteis the feed's bare zero-padded number ("099") andtravelDirectionthe feed's code, which SR 99, SR 509, and SR 520 fix per facility — read direction from the segment's ends
wsdot_get_border_waits tool
- No input — eleven lane entries in
crossings[]across I-5 (Peace Arch), SR 543 (Pacific Highway), SR 539 (Lynden), and SR 9 (Sumas): a general-purpose and a Nexus lane at each, plus truck lanes at SR 539 and SR 543 crossingNameis a route code (I5,SR543Trucks) andlocation.descriptionthe readable name; a lane with no current data omitswaitTimeInMinutes
wsdot_search_cameras tool
- Optional
stateRoute,regioncode (NW,SW,OL,ER,SC,NC,OSfor the Oregon TripCheck cameras,WAfor airport cameras),startMilepost/endMilepost, andtitleContains(every word, any order). Ordered bycameraId, paged (default 50, max 500) - Returns metadata and image URLs, not image bytes (camera images are copyright WSDOT). An unknown region fails as
invalid_regionand a reversed range asinvalid_milepost_range
wsdot_get_ferry_terminals tool
- No input — all 20 WSF terminals, each with its numeric ID, abbreviation, and latitude/longitude
- Resolves names ("Bainbridge Island", "Seattle") to the IDs
wsdot_get_ferry_scheduleandwsdot_get_terminal_spacetake
wsdot_get_ferry_routes tool
- Optional
tripDate(YYYY-MM-DD, default today); a date outside the range WSF has published fails asinvalid_datestating that range - Each route carries its ID (the
impactedRouteIdsofwsdot_get_ferry_alerts), abbreviation, description, andterminalPairs— the departing → arriving pairswsdot_get_ferry_scheduleaccepts that day
wsdot_get_ferry_schedule tool
- Requires
departingTerminalIdandarrivingTerminalId(a pair fromterminalPairsonwsdot_get_ferry_routes); optionaltripDate(default today) andremainingOnly(future departures only, today only) - Each sailing carries
departureTime/arrivalTimein ISO 8601 UTC (tripDateis the Pacific service day),vesselId,loadingRule, and indexes into the pair'sannotations. There is no cancellation flag — WSF drops a cancelled sailing, so checkwsdot_get_ferry_alerts - A pair with no schedule that day fails as
invalid_terminal_pairand a date with none asinvalid_date, never as an empty schedule
wsdot_get_vessel_locations tool
- No input — position, speed, heading, ETA, and dock status for every active WSF vessel
- Positions may lag 30–60 seconds; many fields are absent for a vessel not in service, and one between assignments reports
opRouteAbbrevasnone reported
wsdot_get_terminal_space tool
- Optional
departingTerminalId(fromwsdot_get_ferry_terminals), else every terminal; paged by terminal (default 5, max 20), with every sailing of a returned terminal included driveUpSpaceCountis the key field — zero means the drive-up lane is full, and an oversubscribed sailing's negative upstream count floors to zero.arrivingTerminalIdschains intowsdot_get_ferry_schedule
wsdot_get_ferry_alerts tool
- No input — active WSF disruptions, delays, and bulletins, each with
alertTitle, a one-linealertDescription, and the fullbulletinText(detail such as a replacement sailing appears only in the body) impactedRouteIdsmap towsdot_get_ferry_routes;affectsAllRoutes: truemarks a fleet-wide alert, whose empty route list then means every route
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
WSDOT-specific:
- Dual API integration — WSDOT Traffic API and WSF Ferry API share a single
WSDOT_ACCESS_CODE - Cross-tool linking built into tool descriptions — ferry tools point to
wsdot_get_ferry_terminals/wsdot_get_ferry_routesfor ID resolution before a lookup - Normalized response shapes across both APIs — sparse upstream fields surface as optional rather than omitted or defaulted
- Stable pagination — the alert and camera feeds return the same set in more than one row order upstream, so results are sorted by ID to keep a given offset reproducible
- Paged list tools take
offset/limit, and a page also ends early at a 24,000-byte response budget, with the notice naming the next offset - Route filters take the forms people write —
"I-90","90","090","SR 520"— and every text filter accepts at most 200 characters
Agent-friendly output:
- Typed failure —
invalid_access_codeandapi_unavailableerrors carry an explicit recovery hint distinguishing configuration faults from transient upstream ones driveUpSpaceCount: 0and congestion delta fields (delayInMinutes) give agents actionable signal without string parsing- Plain text from HTML — alert descriptions, sailing notes, and bulletins arrive from upstream as HTML and are returned as text, with links inline as
link text (url) - Partial data preserved — sparse upstream payloads surface
null/undefinedrather than synthetic defaults (e.g. an omittedwaitTimeInMinutes, an absentarrivalTime) content[]andstructuredContentcarry the same values, not just the same fields — afalseflag, an empty list, and one populated half of a coordinate pair all render rather than dropping out of the markdown surface that some clients read; a blank or whitespace-only upstream string is absent from both, and a populated one arrives trimmed
Getting started
Public Hosted Instance
A public instance is available at https://wsdot.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"wsdot-mcp-server": {
"type": "streamable-http",
"url": "https://wsdot.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file. You'll need a WSDOT Traveler API access code — register at wsdot.wa.gov/Traffic/api/.
{
"mcpServers": {
"wsdot-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/wsdot-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"WSDOT_ACCESS_CODE": "your-access-code"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"wsdot-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/wsdot-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"WSDOT_ACCESS_CODE": "your-access-code"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"wsdot-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"-e", "WSDOT_ACCESS_CODE=your-access-code",
"ghcr.io/cyanheads/wsdot-mcp-server:latest"
]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 WSDOT_ACCESS_CODE=your-access-code bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- A WSDOT Traveler API access code. Register at wsdot.wa.gov/Traffic/api/ — registration is free.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/wsdot-mcp-server.git
- Navigate into the directory:
cd wsdot-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# edit .env and set WSDOT_ACCESS_CODE
Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
| Variable | Description | Default |
|---|---|---|
WSDOT_ACCESS_CODE | Required. WSDOT Traveler API access code. Register at wsdot.wa.gov/Traffic/api/. | — |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_HTTP_HOST | HTTP server hostname. | 127.0.0.1 |
MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path. | /mcp |
MCP_PUBLIC_URL | Public origin for TLS-terminating reverse-proxy deployments. | — |
MCP_SESSION_MODE | Session handling: auto, stateful, or stateless. The schema default auto resolves to stateful; this server sets stateless explicitly. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, notice, warning, error). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1. | in-memory |
LOG_TOOL_FAILURE_PAYLOADS | Log each failed tool call's arguments and result (key-name redaction only). | false |
OTEL_ENABLED | Enable OpenTelemetry instrumentation (spans, metrics, completion logs). | false |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP base URL; traces go to /v1/traces, metrics to /v1/metrics. | — |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | Opt-in OTLP log export; the base endpoint never enables it. | — |
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:http -
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t wsdot-mcp-server .
docker run --rm -e WSDOT_ACCESS_CODE=your-access-code -p 3010:3010 wsdot-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/wsdot-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers all 12 tools and initializes services. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts) — 6 traffic tools, 6 ferry tools. |
src/services/traffic | WSDOT Traffic API service (mountain passes, alerts, travel times, toll rates, border waits, cameras). |
src/services/ferry | WSF Ferry API service (terminals, routes, schedule, vessel locations, space, alerts). |
tests/ | Unit and integration tests, mirroring the src/ structure. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools in the
createApp()arrays insrc/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.