Spoke Dispatch MCP server
The Spoke Dispatch MCP server lets MCP clients query and (when writes are enabled) manage delivery plans, stops, routes, drivers, depots, and optimization operations.
List and retrieve plans, stops, routes, drivers, depots, and plan-optimization operations with filtering and token pagination.
Search all stops (assigned and unassigned) using full-text keywords plus Spoke's SQL-like filter language.
View stop details including type, route, position, delivery state/outcome, ETA, time window, and packages.
Control privacy: recipient and driver contact details, full addresses, proof-of-delivery links, signee names, and tracking links are withheld or redacted by default;
include_contact_detailsreturns them.With
SPOKE_ALLOW_WRITES=true, create plans, batch-import stops, start plan optimization, and distribute optimized routes to drivers (distribution is destructive and notifies real drivers).Read-only by default; updating, deleting, live-plan endpoints, and member management are not covered.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Spoke Dispatch MCP server How is today's Bristol plan going, and which stops have failed?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Spoke Dispatch MCP server
An MCP server that lets Claude, ChatGPT and other MCP clients work with a Spoke Dispatch team (delivery route planning and driver dispatch, formerly Circuit for Teams): plans, stops, routes, drivers, depots and plan-optimization operations, and (when enabled) creating plans, importing stops, optimizing and distributing plans. It is built from Spoke's public API documentation and its published OpenAPI 3.1 spec (https://developer.dispatch.spoke.com/openapi-v1.json, "Spoke API" v1).
Once it's connected, a dispatcher can ask things like:
"How is today's Bristol plan going? Which stops have failed, and what is the ETA for the rest?"
"Where is Sam's route up to, and has it been started?"
"Find every stop for order A-1005, and the ones in Bath that were not attempted last week."
"Which drivers are paused, and which depot are they on?"
With writes enabled: "Create tomorrow's plan for Sam and Priya, import these 40 stops, optimize it, and when the operation is done, distribute it."
Tools
Tool | What it does | API calls |
| Plans (one per working day) with start date, depot, optimization and distribution state, driver and route IDs and route overrides. Filters by exact title and a start-date range; token-paginated. |
|
| One plan. |
|
| Every stop of a plan: type (start/stop/end), route, position, delivery state and outcome, ETA, time window, packages, order references, service and payment-on-delivery data. Filters by the recipient's external ID. |
|
| Full-text keyword search plus Spoke's SQL-like filter language across all stops, assigned and unassigned, with the documented sort fields. |
|
| Routes (one driver's run) with stop count, driver, plan and state (distributed, started, completed, recipients notified, with timestamps). |
|
| One route. |
|
| The stops of one route, same shape as |
|
| Drivers with active status, depots and route overrides (working hours, vehicle, max stops). Filters by active status. |
|
| One driver. |
|
| Depots with their default route settings (start time, start and end address, round trip, time at stop, max stops, vehicle). |
|
| Plan-optimization operations: done or not, who started them, target plan, result (stops optimized, stops skipped with reasons) or error. Filters by done and type. |
|
| One operation, to poll after |
|
| Creates an empty plan for a date with optional drivers, depot and route overrides. Writes only. |
|
| Batch-imports 1 to 100 stops into a writable plan using the API's own field names; reports the IDs created and the stops Spoke rejected with its reason, each identified by the external ID and first address line sent in that same call. Writes only. |
|
| Starts optimizing a plan into routes; returns the operation to poll. Writes only. |
|
| Sends an optimized plan's routes to the drivers' apps. Marked destructive: it notifies real drivers and the API has no way to undo it. Writes only. |
|
Not covered on purpose: updating and deleting plans, stops, drivers, depots and members; creating stops one at a time; the live-plan endpoints (:liveCreate, :liveUpdate, :liveImport, :liveDelete, :reoptimize, :redistribute, :save); unassigned stops other than through search; custom stop property definitions; cancelling operations; members.
Related MCP server: delivery-mcp-server
Setup
Requires Node 18 or later.
npm install
npm run buildYou need an API key for your Spoke Dispatch team, generated under Settings > Integrations > API. The API authenticates with HTTP Basic: the key is the username and the password is empty, and that is what this server sends (the spec also accepts the key as a Bearer token).
Claude Desktop: add this to claude_desktop_config.json:
{
"mcpServers": {
"spoke": {
"command": "node",
"args": ["/absolute/path/to/spoke-mcp/dist/index.js"],
"env": { "SPOKE_API_KEY": "your-key" }
}
}
}Claude Code:
claude mcp add spoke -e SPOKE_API_KEY=your-key -- node /absolute/path/to/spoke-mcp/dist/index.jsVariable | Required | Meaning |
| yes | Your API key, sent as the HTTP Basic username with an empty password. |
| no |
|
| no | Defaults to |
Safety defaults
Read-only unless
SPOKE_ALLOW_WRITES=true. Read tools carry the MCPreadOnlyHintannotation;distribute_planis marked destructive because it pushes routes to real drivers and cannot be undone through the API.Recipients are third parties. By default a stop's recipient block only says which fields exist (
name,email,phone,external_id), the address is reduced to its locality (the part after the street line, with postal codes cut to their area part or dropped: UKBS7 8QHbecomesBS7, an Irish EircodeD02 X285becomesD02, a CanadianK1A 0A2becomesK1A, a Dutch1012 JSand any all-digit code such as a US ZIP20500,20500-0003, or a German, French or Australian code are dropped, so2 Melbourne Road, Bristol, BS7 8QH, UKbecomesBristol, BS7, UKand1600 Pennsylvania Ave NW, Washington, DC 20500, USAbecomesWashington, DC, USA; coordinates and the Google place ID are left out), and the signee name, the location where the delivery was attempted, the proof-of-delivery photo and signature links and the recipient tracking link are withheld. The same applies to unassigned stops returned bysearch_stops. Everything is returned when the assistant explicitly asks (include_contact_details). Photo and signature URLs are passed through as links; nothing is ever downloaded.Drivers are employees. Their email, phone number and their own start/end addresses are only returned with
include_contact_details; names and display names are returned (minus any email or phone typed into them).Plan and depot addresses (where routes start and end) are the team's own operating locations and are returned as stored.
In free text (plan, route and depot titles and names, driver display names, stop notes, driver and recipient notes and custom property values; seller names, products, package labels and Spoke's own error messages take the same code path but are not exercised by the tests) email addresses are replaced with
[email redacted]and phone-number-like sequences with[phone redacted]by default. The phone match is a heuristic pattern covering UK national numbers (07700 900123,020 7946 0958,(020) 7946 0958),+- and00-prefixed international numbers (+447700900123,+1 415 555 0123,0044 20 7946 0958) and North-American 10-digit national numbers ((415) 555-0123,415-555-0123,415.555.0123,4155550123), each with spaces, dots or hyphens between the groups; the tests exercise all of these forms. A 7-digit local number without an area code, and formats of other countries written without a leading0or+, are not matched. Other digit strings that happen to look like one of those shapes (a 9-11 digit string starting with0, a 10-digit string starting with2-9) are redacted too, while Spoke IDs, barcodes, dates, epoch timestamps and order references such asA-1002are left alone; the raw text is available withinclude_contact_details.The address summary is a heuristic: it takes
addressLineTwo(or the full address after its first comma) as the locality, so what it contains depends on how Spoke split the address into lines. When Spoke stores the street inaddressLineTwo(for instanceFlat 3in line one and2 Melbourne Road, Bristol, BS7 8QH, UKin line two) the summary contains the house number and street; the tests show this case. Postal codes in formats other than the ones listed above are only removed when they are all digits.IDs are checked before any call is made: the spec's path parameters are
[a-zA-Z0-9---_]{1,50}(letters, digits,-and_), and every tool accepts either that bare form or the prefixed form the API uses in bodies and responses (plans/<id>). Dates must beYYYY-MM-DDand a real calendar date (2026-02-30and2026-13-01are refused locally);create_planadditionally requires the year the request schema documents (2000 to 2100). Times must beHH:MM. Both are converted to the spec's{year, month, day}and{hour, minute}objects.import_stopsvalidates every stop locally against the documented request shape (unknown fields, an empty address and more than 100 stops are refused before any call) and convertsHH:MMwindows and bare driver IDs to the documented forms. Its failure list has noinclude_contact_detailsswitch: for each stop Spoke rejected it returns Spoke's reason plus the external ID and first address line that the caller sent in that same call (the API echoes the whole stop back rather than its position in the array), with the free-text redaction applied to the reason and the address line; the echoed recipient name, email and phone are not returned.Rate limits, as Spoke documents them ("On Rate-Limiting" in the spec): read endpoints 10 requests per second; write endpoints (POST, PATCH, DELETE) 5 per second, driver creation 1 per second; the stop batch imports 100 per 10 minutes with at most 30 per minute; the driver batch import 2 per minute; plan optimization and re-optimization 100 per 10 minutes with at most 30 per minute; bursts are tolerated briefly, sustained high rates are rejected, and a client that keeps retrying rejected requests keeps being rejected. Spoke recommends exponential backoff with a random delay and documents no
Retry-Afterheader. This server spaces reads 120 ms apart, writes 250 ms apart and imports and optimizations 2.1 s apart. A 429 is retried at most twice for any method (a rejected request was not processed), waiting forRetry-Afterwhen one is sent (whole or fractional seconds, or an HTTP-date) and otherwise 1 s then 2 s plus up to 300 ms of jitter. Each wait is capped at 10 seconds, so one request waits at most 20 seconds in all; a tool call that makes several requests (pages, lookups) can still run past the MCP client's default 60-second request timeout. Past the cap: if Spoke asks for a longer wait the call gives up at once and the message says how long to wait.502, 503 and 504 are retried the same way for
GETonly; when all three attempts fail the error says the service may be unavailable and to try again in a few minutes, without the gateway's HTML. The four write tools are never retried after a gateway error, because the request may already have been processed and a retry could create a plan, import stops, or notify drivers twice; the error names the read tool to check with first (list_plans,list_plan_stops,list_operations,get_plan).Spoke documents no idempotency keys, so none are sent.
A 200 whose body is not JSON (a proxy or a login page in the way) is reported as an error naming
SPOKE_BASE_URL, never as an empty list.A rejected API key produces a message that says which variable to fix and where keys come from. The documented 403 for a plan or route older than the subscription's delivery-history period (
plan_inaccessible,route_inaccessible) is explained; 404s carry Spoke's message (Plan not found,Route not found,Driver not found,The operation was not found.); 409s pass on Spoke's message and code (plan_not_writable,plan_already_optimized,plan_optimization_in_progress,plan_not_optimized,plan_already_distributedare exercised;no_drivers_availabletakes the same path); 400 and 412 pass on the message, code and offending parameter, and 410 and 422 take the same path without being exercised.
Tests
npm testThe test suite:
Validates every fixture record with Ajv (JSON Schema 2020-12, as the spec is OpenAPI 3.1) against the component schemas in Spoke's published spec (
planSchema,stopSchema,unassignedStopSchema,routeSchema,driverSchema,depotSchema,operationSchema), all of which setadditionalProperties: falseand mark nearly every field required, with negative controls. The spec is downloaded fromdeveloper.dispatch.spoke.com/openapi-v1.jsontospec.jsonon the first run.Starts a local mock of the API under
/public/v1that serves those fixtures with the documentedpageToken/nextPageToken/maxPageSizepagination (including each endpoint's maximum page size and the search'sexclusiveMaximumof 20), the documentedfilter.*parameters, Basic-auth 401s, the documented 403, 404 and 409 bodies, a subset of the stop-search filter language, and answers the firstGET /depotswith a 429. The mock's list, detail, search, create, import, optimize and distribute responses and its error responses are validated against the spec's response schemas. (One quirk: the search's documented 400 is aoneOfwhose generic first branch also matches every specific branch, so nothing can satisfy it; the mock's answer is checked against the "Invalid filter string" branch.)Starts the built server and drives it over stdio with the official MCP client: 25 checks covering every tool, tool annotations, token pagination across pages to the null token with whole pages and a continuation that repeats the original filters, every documented filter passed through exactly (
filter.title,filter.startsGte,filter.startsLte,filter.externalId,filter.active,filter.done,filter.type, and the search'skeyword,filter,sortField,sortOrder), a page size of 19 on the search and of 2 (its documented minimum) when one result is asked for, recipient, address, proof-of-delivery, tracking-link and free-text redaction by default and their return on request, the address summary on UK, Irish, US, Canadian, Dutch, German and Australian postal codes and on a street stored inaddressLineTwo, the phone pattern on UK, international and North-American forms with negative controls, thePOST /plansandPOST .../stops:importbodies validated against the spec's request schemas, local refusal of dates that are not real calendar dates or outside the documented 2000-2100 plan years, local refusal of an empty address, an unknown stop field and more than 100 stops, a partially failed import reported with the redacted reason, external ID and address line, the documented 409s, ID validation before any call and the documented 404 and 403 messages, the 429 retry waiting for aRetry-Afterof 2 s (a wait the no-header fallback cannot produce) and in the fractional and HTTP-date forms, exponential backoff without the header, giving up after three attempts and at once above the cap, a 429 on a write retried once, a 502 retried forGETonly and never for any of the four writes, aGETfailing three times reported without the gateway HTML, a non-JSON 200 reported as an error, the write gate with the variable unset and set tofalse, and that every request usedBasic base64(key:),Content-Type: application/jsonon writes, and a documented method and path.
The suite makes 77 requests against the mock and takes about 40 seconds, most of it deliberate waits in the retry checks.
Status
This is a working prototype. It has not yet been run against the live API, because it was built without a Spoke Dispatch account. Everything below is taken from the published spec and should be confirmed on a real account:
Whether API keys are available during the free trial, and whether a Basic header with an empty password (
base64("<key>:")) is accepted exactly as the spec describes; the Bearer form is the documented fallback.The real
nextPageTokenvalues and whether a continuation must repeatmaxPageSizeunchanged (the server repeats every query parameter, as the spec says to). What the API answers for a stale or foreign page token (the spec documents a 400 withparam: pageTokenfor the search only).GET /stops:search: the spec documentsmaxPageSizewithexclusiveMaximum: 20and a default of 20, which contradict each other; the server sends 19. Whetherkeywordandfiltermay be combined, the exact filter grammar the live API accepts (the mock implements a subset), how fresh the search index is, and whethersortFieldmust be one of the ten documented fields.The filter parameter spelling: the spec documents both
?filter[title]=and?filter.title=; the server sends the dot form.How addresses are split into
address,addressLineOneandaddressLineTwoon real stops, which decides what the default locality summary contains. Real coordinates are also left out by default.Whether
stopSchema.routeis really the full embedded route object on list responses (as the schema says) and whetherdeliveryInfoisnullor anunattemptedrecord before an attempt.POST /plans: the accepted date range forstarts("does not accept dates that are too far in the future or past"; the server only enforces the schema's 2000-2100), whetherrouteOverrides.optimizationSettings.objectiveaccepts anything other than the single documented valuedistribute_services, and whether drivers must already belong to the plan's depot (the migration guide says so).POST /plans/{planId}/stops:import: the wording of the per-stop failure messages (the mock's "Address could not be geocoded" is a placeholder), whether the API geocodes from text fields alone, whethercustomPropertieskeys are property IDs as documented, and what a partially failed import returns beyondsuccessandfailed.POST .../:optimizeand:distribute: whether the 409 bodies match the spec's enums exactly, how long optimization takes, and whether distribution notifies drivers immediately.The 401 body (the spec gives it only a description; the mock answers
{"message": "Unauthorized"}) and whether any error message ever echoes request data such as a recipient's email. The server applies the free-text redaction to every error message; the tests exercise it on a per-stop import failure message, not on the message of an HTTP error response.How addresses outside the UK, Ireland, Canada and the Netherlands are formatted on real stops; a postal code in another format that is not all digits stays in the default summary.
Whether Spoke sends
Retry-Afteron 429 (nothing is documented), and whether a rate-limited write is indeed never processed; the server assumes so and retries a 429 on a write once.How many requests per second the API really tolerates from one key; the spacing here follows the documented limits with a margin.
Going to production
This version runs locally over stdio, with the dispatcher's own API key. For teams to connect from claude.ai or ChatGPT without handling keys, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by Spoke, and then a listing in the Claude and ChatGPT connector directories.
Licence
MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.
Available Tools
12 toolsget_driverGet a driverARead-only
One driver: name, active status, depots and route overrides. Email, phone and start/end address only with include_contact_details.
Inputs: driver_id (required), include_contact_details.
get_operationGet an operationARead-only
One operation, to poll a plan optimization started with optimize_plan: done, timestamps, and the result or error once finished.
Inputs: operation_id (required).
get_planGet a planBRead-only
One plan: start date, depot, optimization and distribution state, driver IDs, route IDs and route overrides.
Inputs: plan_id (required), include_contact_details.
get_routeGet a routeARead-only
One route with its stop count, driver, plan and state. Use list_route_stops for the stops.
Inputs: route_id (required), include_contact_details.
list_depotsList depotsBRead-only
Depots (the team's operating bases) with their default route settings: start time, start and end address, round trip, time at stop, max stops, vehicle type.
Inputs: page_token, max_results, include_contact_details.
list_driversList driversARead-only
Drivers on the team with their active status, depots and route overrides (working hours, vehicle, max stops). Email, phone and a driver's own start/end address are only returned with include_contact_details.
Inputs: active, page_token, max_results, include_contact_details.
list_operationsList operationsBRead-only
Long-running operations (currently only plan_optimization) with whether they are done, who started them, the target plan and the result (stops optimized, stops skipped with reasons, or an error).
Inputs: done, type, page_token, max_results.
list_plansList plansARead-only
Plans (one per working day, each with its drivers and routes) in the order the API returns them. Filter by an exact title and/or a start-date range. Plan titles are free text: email addresses and phone numbers in them are redacted unless include_contact_details is true.
Inputs: title, starts_to, page_token, max_results, +2 more.
list_plan_stopsStops of a planARead-only
Every stop of one plan with its type (start/stop/end), route, position, delivery state and outcome, ETA, time window and packages. By default recipients are withheld, addresses are summarised to the locality, and proof-of-delivery links, signee names and recipient tracking links are left out; set include_contact_details to get them.
Inputs: plan_id (required), page_token, external_id, max_results, +1 more.
list_routesList routesBRead-only
Routes (one driver's run within a plan) with stop count, driver, plan and state (distributed, started, completed, recipients notified, with timestamps).
Inputs: page_token, max_results, include_contact_details.
list_route_stopsStops of a routeARead-only
The stops of one route (one driver's run) in the order the API returns them, with delivery state and outcome, ETA, time window and packages. By default recipients are withheld, addresses are summarised to the locality, and proof-of-delivery links, signee names and recipient tracking links are left out; set include_contact_details to get them.
Inputs: route_id (required), page_token, external_id, max_results, +1 more.
search_stopsSearch stopsARead-only
Full-text search across all stops (assigned and unassigned) within the team's data retention, by keyword and/or the API's filter language, e.g. deliveryInfo.state = "failed_not_home" and createdAt >= "2026-09-01T00:00:00Z" or address.address ~= "Bristol". Filterable fields include planId, routeId, type, activity, notes, address., recipient.name/email/phone/externalId, deliveryInfo.state/attempted/succeeded/attemptedAt, orderInfo., barcodes, packageLabel, createdAt, arrivalAt and customProperties.. String comparisons with >, >=, <, <= work on ISO-8601 dates only. The search index lags the live data slightly, so a stop created seconds ago may not be found yet.
Inputs: filter, keyword, page_token, sort_field, +3 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 12 tools
Each tool has a distinct scope: get_* retrieves a single entity, list_* retrieves collections, list_plan_stops and list_route_stops differ by parent scope, and search_stops provides cross-cutting full-text search. Overlaps between get_route and list_routes (or get_plan and list_plans) are cardinality differences, not ambiguities.
All tool names use consistent snake_case with a clear verb_noun pattern: get_route, list_plans, get_plan, list_plan_stops, search_stops, etc. No mixed conventions or confusing verb choices.
The 12 tools are well-scoped: each covers a distinct resource and action (plans, routes, stops, drivers, depots, operations) without obvious filler or duplication. This is an appropriate size for the dispatch domain.
The surface covers read access to plans, routes, stops, drivers, depots, and operations, but has no mutation or lifecycle tools (create/update/delete for core entities). Crucially, get_operation references optimize_plan as the source of operations, yet no optimize_plan tool is provided, creating a dead end for a key workflow.
Maintenance
Related MCP Connectors
Governed data discovery, exact queries, decisions, simulations, and runtime utilities over MCP.
Streamable HTTP MCP server exposing planner flows, tasks, and squads.
Discover, plan and run tools across your connected business systems through one OAuth MCP endpoint
Query, browse, and automate OmegaAI workspaces from any MCP client. Streamable HTTP with OAuth 2.0.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables LLM clients to access scheduling data, KPIs, routes, and trigger optimization algorithms via MCP.9-
- FlicenseAqualityCmaintenancePlans and optimizes last-mile delivery routes with capacity and time-window constraints, offering tools for managing deliveries, vehicles, routes, and what-if scenarios.10-
- FlicenseNot gradedqualityCmaintenanceEnables logistics route optimization by exposing tools for traffic factors, holiday and circulation restrictions, and fuel price references over a remote MCP HTTPS endpoint.-
- FlicenseNot gradedqualityBmaintenanceEnables authenticated MCP tools for planning commercial routes with fail-closed economic boundaries, plus marketplace discovery, matching, and agent interaction capabilities.-