Back to Directory/Developer Tools

io.github.cyanheads/wsdot-mcp-server

WA highway conditions, ferry schedules, vessel locations, toll rates, and border waits via MCP.

Developer ToolsTypeScriptv0.4.0

@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

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework

Public 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

ToolDescription
wsdot_get_mountain_passesCurrent conditions for all WA mountain passes: status, road condition, traction laws, temperature, elevation.
wsdot_search_alertsActive highway alerts — incidents, construction, closures — filterable by state route, WSDOT region, and milepost range.
wsdot_get_travel_timesCurrent vs. average travel times for named WA highway corridors (I-5, I-90, SR 520, etc.) with congestion delay.
wsdot_get_toll_ratesDynamic toll rates for WA express lanes and tolled facilities: SR 99, SR 167 HOT, I-405 Express, SR 509, SR 520.
wsdot_get_border_waitsCurrent vehicle wait times at all WA/Canada land border crossings.
wsdot_search_camerasHighway camera metadata and image URLs, filterable by state route, region, and milepost range.
wsdot_get_ferry_terminalsAll WSF ferry terminals with numeric IDs needed for schedule and space lookups.
wsdot_get_ferry_routesWSF routes operating on a given date — route ID, abbreviation, and description for each, for route discovery and ferry-alert cross-reference.
wsdot_get_ferry_scheduleDeparture times for a specific WSF route — today-remaining or full-day future mode.
wsdot_get_vessel_locationsReal-time AIS positions, speed, heading, ETA, and dock status for all active WSF vessels.
wsdot_get_terminal_spaceDrive-up and reservable vehicle space available at WSF terminals for upcoming sailings.
wsdot_get_ferry_alertsActive WSF service disruptions and bulletins with impacted route IDs.

Capability reference

wsdot_get_mountain_passes tool

  • No input parameters — returns current conditions for all 16 WA mountain passes (Snoqualmie, Stevens, White, Blewett, Cayuse, and others) in one call
  • Fields include road condition, weather, temperature, elevation, and up to two directional traction/travel restrictions
  • Use for "is the pass open?", traction-law checks, or winter driving planning

wsdot_search_alerts tool

  • Filter by state route — natural forms all work: "I-90", "90", "090", or "SR 520" / "520"
  • Filter by WSDOT region: Northwest, Olympic, Southwest, South Central, North Central, Eastern
  • Filter by milepost range to scope to a corridor — an alert matches when its extent overlaps the range, so a closure that spans the boundary is returned
  • Omit all filters to return all current statewide alerts
  • Descriptions are normalized to plain text; a link renders inline as link text (url)
  • Results ordered by alertId and paged (default 50, max 500) — pass offset/limit; the notice reports the next offset

wsdot_get_travel_times tool

  • Covers I-5, I-90, SR 520, SR 99, I-405, SR 167, and others
  • Filter by route ("I-5", "5", "SR 520") to get every corridor measured on it, or by any text to match corridor names ("Everett")
  • When current time exceeds average, the corridor is congested; the delta is the delay
  • Reversible express-lane corridors report no travel time while closed in the queried direction — those figures are omitted rather than reported as zero minutes
  • Results are paged (default 50, max 500) — pass offset/limit; the notice reports the next offset

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
  • Rates are time-banded and change dynamically based on traffic conditions
  • stateRoute is a bare, zero-padded route number ("099", "405") with no route type; the rendered text resolves the posted designation, so I-405 reads as I-405 rather than SR 405
  • Each entry leads with its readable startLocationName → endLocationName segment; the opaque upstream trip key stays available as tripName
  • Results are paged (default 50, max 500) — pass offset/limit; the notice reports the next offset

wsdot_get_border_waits tool

  • No input parameters — covers I-5 (Peace Arch, Blaine), SR 543 (Pacific Highway, Blaine), SR 539 (Lynden), and SR 9 (Sumas)
  • Each crossing reports a general-purpose lane and a Nexus lane; SR 539 adds a truck lane and SR 543 adds truck and FAST truck lanes — eleven entries in crossings[], one per lane
  • crossingName is a route code (e.g. I5, SR543Trucks); location.description holds the readable name
  • Wait times in minutes; updateTime is ISO 8601. A crossing reporting no current data is still returned — only waitTimeInMinutes is omitted, and the rendered text reads Not available

wsdot_search_cameras tool

  • Filter by state route ("I-90", "90", "SR 520", or "520" all work), WSDOT region, or milepost range
  • Camera road names carry a route-type prefix, so "SR 26" excludes US 26 and "US 97" excludes US 97A; a bare "26" returns both
  • Returns metadata and image URLs — camera images are copyright WSDOT, not fetched as bytes
  • Results are ordered by cameraId and paged (default 50, max 500) — pass offset/limit; the notice reports the next offset

wsdot_get_ferry_terminals tool

  • No input parameters — returns all 20 WSF ferry terminals; the list rarely changes
  • Call this first to resolve human-readable names (e.g. "Bainbridge Island", "Seattle", "Kingston") to the numeric IDs required by wsdot_get_ferry_schedule and wsdot_get_terminal_space
  • Each terminal also carries an abbreviation and coordinates when reported

wsdot_get_ferry_routes tool

  • Optional tripDate (ISO 8601 YYYY-MM-DD); defaults to today
  • Returns each route's ID, abbreviation, and description
  • Route IDs correspond to impactedRouteIds in wsdot_get_ferry_alerts — use this tool to resolve alert route IDs to route names
  • Use to discover which routes are running; for the numeric terminal IDs that schedule and space lookups need, call wsdot_get_ferry_terminals

wsdot_get_ferry_schedule tool

  • Requires numeric departingTerminalId and arrivingTerminalId — use wsdot_get_ferry_terminals first
  • Optional tripDate (defaults to today) and remainingOnly: true (only future departures for today; ignored for future dates)
  • departureTime and arrivalTime are ISO 8601 UTC, while tripDate is the Pacific service day — an evening sailing therefore carries the following UTC calendar date and will not match tripDate. Convert to America/Los_Angeles before quoting a clock time
  • arrivalTime is populated on some routes and absent on others
  • No cancellation status — WSF drops a cancelled sailing from the schedule rather than flagging it, so a listed sailing is not confirmation it will run; check wsdot_get_ferry_alerts, which reports disruptions at route level
  • An invalid or non-through terminal pair returns a typed invalid_terminal_pair error rather than an empty schedule

wsdot_get_vessel_locations tool

  • No input parameters — fields include position, speed, heading, ETA, and dock status for every active WSF vessel
  • Use for "where is the ferry now?" or checking if a specific vessel is in service
  • Position data may lag 30–60 seconds; many fields are null for vessels not currently operating
  • Coordinates render at full upstream AIS precision — no rounding, so both response surfaces report the same position
  • A vessel between assignments reports an empty opRouteAbbrev, rendered as none reported rather than omitted

wsdot_get_terminal_space tool

  • Filter to a specific terminal by ID (from wsdot_get_ferry_terminals); omit for all terminals
  • driveUpSpaceCount is the key field — zero means the drive-up lane is full. Oversubscribed sailings report a negative count upstream; it is floored to zero so the value never reads as available space
  • arrivingTerminalIds lists the terminals a sailing serves and chains straight into wsdot_get_ferry_schedule; itineraryLabel is a display string that may name several stops, not a single destination
  • Results are paged by terminal (default 5, max 20) — offset/limit select whole terminals and totalCount counts matching terminals, not sailings; every sailing of a returned terminal is included, so page size varies with how many departures each terminal carries

wsdot_get_ferry_alerts tool

  • No input parameters — active WSF ferry service disruptions, delays, and bulletins
  • Each alert carries the bulletin's alertTitle, its one-line alertDescription, and the full bulletinText — detail such as a replacement sailing appears only in the body
  • bulletinText is plain text: upstream authors it as HTML, and a link is rendered inline as link text (url)
  • Each alert includes impactedRouteIds — cross-reference with wsdot_get_ferry_routes to map route IDs to names
  • affectsAllRoutes: true marks a fleet-wide alert, which need not enumerate routes — an empty impactedRouteIds then means every route rather than none

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_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

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
  • 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

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

Installation

  1. Clone the repository:
git clone https://github.com/cyanheads/wsdot-mcp-server.git
  1. Navigate into the directory:
cd wsdot-mcp-server
  1. Install dependencies:
bun install
  1. 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:

VariableDescriptionDefault
WSDOT_ACCESS_CODERequired. WSDOT Traveler API access code. Register at wsdot.wa.gov/Traffic/api/.—
MCP_TRANSPORT_TYPETransport: stdio or http.stdio
MCP_HTTP_PORTHTTP server port.3010
MCP_HTTP_HOSTHTTP server hostname.127.0.0.1
MCP_HTTP_ENDPOINT_PATHHTTP endpoint path./mcp
MCP_PUBLIC_URLPublic origin for TLS-terminating reverse-proxy deployments.—
MCP_SESSION_MODESession handling: auto, stateful, or stateless. The schema default auto resolves to stateful; this server sets stateless explicitly.stateless
MCP_AUTH_MODEAuthentication: none, jwt, or oauth.none
MCP_LOG_LEVELLog level (debug, info, notice, warning, error).info
LOGS_DIRDirectory for log files (Node.js only).<project-root>/logs
STORAGE_PROVIDER_TYPEStorage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1.in-memory
OTEL_ENABLEDEnable OpenTelemetry instrumentation (spans, metrics, completion logs).false

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

DirectoryPurpose
src/index.tscreateApp() entry point — registers all 12 tools and initializes services.
src/configServer-specific environment variable parsing and validation with Zod.
src/mcp-server/toolsTool definitions (*.tool.ts) — 6 traffic tools, 6 ferry tools.
src/services/trafficWSDOT Traffic API service (mountain passes, alerts, travel times, toll rates, border waits, cameras).
src/services/ferryWSF 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/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:

bun run devcheck
bun run test

License

Apache-2.0 — see LICENSE for details.

Installation

Source-derived launch command. Check the maintainer’s required arguments and credentials before running:

bash
npx -y @cyanheads/wsdot-mcp-server

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "io-github-cyanheads-wsdot-mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "@cyanheads/wsdot-mcp-server"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

@cyanheads/wsdot-mcp-servernpm

Compatible MCP Clients

io.github.cyanheads/wsdot-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More