@cyanheads/wsdot-mcp-server
Query WA highway conditions, ferry schedules, vessel locations, toll rates, border waits, and alerts via MCP. STDIO or Streamable HTTP.
12 Tools • 1 Skill Resource
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/wsdot-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/wsdot-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
[](https://github.com/cyanheads/wsdot-mcp-server/releases/latest/download/wsdot-mcp-server.mcpb) [](https://cursor.com/en/install-mcp?name=wsdot-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvd3Nkb3QtbWNwLXNlcnZlciJdLCJlbnYiOnsiV1NET1RfQUNDRVNTX0NPREUiOiJ5b3VyLWFjY2Vzcy1jb2RlIn19) [](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22wsdot-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Fwsdot-mcp-server%22%5D%2C%22env%22%3A%7B%22WSDOT_ACCESS_CODE%22%3A%22your-access-code%22%7D%7D)
[](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
**Public Hosted Server:** [https://wsdot.caseyjhand.com/mcp](https://wsdot.caseyjhand.com/mcp)
---
## Overview
Washington State roads and ferries from WSDOT. Check mountain passes, travel times, tolls, border waits, alerts, and traffic cameras, and track ferry schedules, vessel locations, and terminal space.
### 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. |
### Resources
| Resource | Description |
|:---|:---|
| `skill://wsdot-guide/SKILL.md` | Ferry trip guidance for combining schedules, alerts, space, and vessel positions. |
## Capability reference
### `skill://wsdot-guide/SKILL.md` resource
- Ferry trip guidance for combining schedules, service alerts, live vehicle space, and vessel positions. Also included in plugin installs as `wsdot-guide`.
### `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), and `startMilepost` / `endMilepost` — an alert matches when its extent overlaps the range; omit all for every statewide alert. Ordered by `alertId`, paged (default 20, max 500)
- An unknown region fails as `invalid_region` and a start above the end as `invalid_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 `stateRoute` matches 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 → endLocationName` segment. `stateRoute` is the feed's bare zero-padded number (`"099"`) and `travelDirection` the 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
- `crossingName` is a route code (`I5`, `SR543Trucks`) and `location.description` the readable name; a lane with no current data omits `waitTimeInMinutes`
---
### `wsdot_search_cameras` tool
- Optional `stateRoute`, `region` code (`NW`, `SW`, `OL`, `ER`, `SC`, `NC`, `OS` for the Oregon TripCheck cameras, `WA` for airport cameras), `startMilepost` / `endMilepost`, and `titleContains` (every word, any order). Ordered by `cameraId`, paged (default 50, max 500)
- Returns metadata and image URLs, not image bytes (camera images are copyright WSDOT). An unknown region fails as `invalid_region` and a reversed range as `invalid_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_schedule` and `wsdot_get_terminal_space` take
---
### `wsdot_get_ferry_routes` tool
- Optional `tripDate` (`YYYY-MM-DD`, default today); a date outside the range WSF has published fails as `invalid_date` stating that range
- Each route carries its ID (the `impactedRouteIds` of `wsdot_get_ferry_alerts`), abbreviation, description, and `terminalPairs` — the departing → arriving pairs `wsdot_get_ferry_schedule` accepts that day
---
### `wsdot_get_ferry_schedule` tool
- Requires `departingTerminalId` and `arrivingTerminalId` (a pair from `terminalPairs` on `wsdot_get_ferry_routes`); optional `tripDate` (default today) and `remainingOnly` (future departures only, today only)
- Each sailing carries `departureTime` / `arrivalTime` in ISO 8601 **UTC** (`tripDate` is the Pacific service day), `vesselId`, `loadingRule`, and indexes into the pair's `annotations`. There is no cancellation flag — WSF drops a cancelled sailing, so check `wsdot_get_ferry_alerts`
- A pair with no schedule that day fails as `invalid_terminal_pair` and a date with none as `invalid_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 `opRouteAbbrev` as `none reported`
---
### `wsdot_get_terminal_space` tool
- Optional `departingTerminalId` (from `wsdot_get_ferry_terminals`), else every terminal; paged by terminal (default 5, max 20), with every sailing of a returned terminal included
- `driveUpSpaceCount` is the key field — zero means the drive-up lane is full, and an oversubscribed sailing's negative upstream count floors to zero. `arrivingTerminalIds` chains into `wsdot_get_ferry_schedule`
---
### `wsdot_get_ferry_alerts` tool
- No input — active WSF disruptions, delays, and bulletins, each with `alertTitle`, a one-line `alertDescription`, and the full `bulletinText` (detail such as a replacement sailing appears only in the body)
- `impactedRouteIds` map to `wsdot_get_ferry_routes`; `affectsAllRoutes: true` marks a fleet-wide alert, whose empty route list then means every route
## Features
Built on [`@cyanheads/mcp-ts-core`](https://github.com/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_routes` for 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_code` and `api_unavailable` errors carry an explicit recovery hint distinguishing configuration faults from transient upstream ones
- `driveUpSpaceCount: 0` and 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`/`undefined` rather than synthetic defaults (e.g. an omitted `waitTimeInMinutes`, an absent `arrivalTime`)
- `content[]` and `structuredContent` carry the same values, not just the same fields — a `false` flag, 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
Connect to the hosted instance, no install:
```sh
claude mcp add --transport http wsdot-mcp-server https://wsdot.caseyjhand.com/mcp
```
Or run it locally:
```sh
claude mcp add wsdot-mcp-server -e WSDOT_ACCESS_CODE=your-access-code -- npx -y @cyanheads/wsdot-mcp-server
```
For other clients, add this to the MCP config file:
```json
{
"mcpServers": {
"wsdot-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/wsdot-mcp-server"],
"env": {
"WSDOT_ACCESS_CODE": "your-access-code"
}
}
}
}
```
Get a free access code at [wsdot.wa.gov/Traffic/api/](https://wsdot.wa.gov/Traffic/api/). For Docker or a source build, see [Running the server](#running-the-server).
## 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/](https://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). | `/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`](./.env.example) for the full list of optional overrides.
## Running the server
### Local development
Requires [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
- **Clone and install:**
```sh
git clone https://github.com/cyanheads/wsdot-mcp-server.git
cd wsdot-mcp-server && bun install
cp .env.example .env # set WSDOT_ACCESS_CODE
```
- **Build and run:**
```sh
# One-time build
bun run rebuild
# Run the built server
bun run start:stdio
# or
bun run start:http
```
- **Run checks and tests:**
```sh
bun run devcheck # Lint, format, typecheck, security
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against spec
```
### Docker
```sh
docker run --rm -e WSDOT_ACCESS_CODE=your-access-code -p 3010:3010 ghcr.io/cyanheads/wsdot-mcp-server:latest
```
The image defaults to HTTP transport, stateless session mode, and logs to `/var/log/wsdot-mcp-server`. For stdio, use `docker run -i --rm -e MCP_TRANSPORT_TYPE=stdio -e WSDOT_ACCESS_CODE=your-access-code ghcr.io/cyanheads/wsdot-mcp-server:latest`. To build locally, run `docker build -t 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). |
| `skills/` | Published ferry trip guide, also served as an MCP skill resource. |
| `tests/` | Unit and integration tests, mirroring the `src/` structure. |
## Development guide
See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no `try/catch` in tool logic
- Use `ctx.log` for request-scoped logging, `ctx.state` for tenant-scoped storage
- Register new tools in the `createApp()` arrays in `src/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:
```sh
bun run devcheck
bun run test
```
## License
Apache-2.0 — see [LICENSE](./LICENSE) for details.