Model Context Protocol
Trident Taxis MCP tools
A public MCP endpoint that lets AI assistants (Claude, ChatGPT, Cursor, Codex, and any other MCP-compatible client) price taxi journeys and hand passengers a prefilled booking link. This page documents the tools, their inputs and outputs, and how to authenticate when the operator has enabled OAuth mode.
https://tridenttaxis.online/mcpTransport
MCP Streamable HTTP
Protocol version
2025-06-18
Server
trident-taxis-mcp v0.1.0
Tools
3 public
Authentication
The MCP endpoint runs in one of two modes.
Default
Public โ no login
Any MCP client can connect anonymously and call every tool. All three tools operate on public reference data or return a link the passenger completes themselves โ none read or write user-specific data. No credentials, headers, or API keys are required.
Optional
OAuth 2.1 (operator-enabled)
When the operator sets MCP_REQUIRE_OAUTH=true, every tool call must present a valid access token issued by the site's authorization server. Compatible clients (Claude, ChatGPT, Cursor, Codex) self-register via dynamic client registration and complete the consent flow at /.lovable/oauth/consent. Sign-in uses the standard staff/driver account at /auth.
The mode is server-wide โ the endpoint can't be simultaneously public for some tools and OAuth-protected for others. Check the response of a probe request to determine the active mode.
OAuth 2.1 end-to-end flow
How to obtain an access token, refresh it, and format the Authorization header. Applies when the operator has set MCP_REQUIRE_OAUTH=true; in public mode this section is not required.
Flow at a glance
- 1
Discover the resource
GET /.well-known/oauth-protected-resource on the MCP host. The response's authorization_servers[0] is the issuer used for every step below.
- 2
Fetch issuer metadata
GET <issuer>/.well-known/oauth-authorization-server (RFC 8414). Read authorization_endpoint, token_endpoint, registration_endpoint, and code_challenge_methods_supported (must include S256).
- 3
Register a client (DCR)
POST <issuer>/oauth/register with redirect_uris and grant_types [authorization_code, refresh_token]. Public clients use token_endpoint_auth_method: none. Persist client_id โ it does not expire.
- 4
Authorize with PKCE
Redirect the user to <issuer>/oauth/authorize with response_type=code, PKCE S256, state, and resource=https://tridenttaxis.online/mcp (RFC 8707). The user signs in at /auth and approves at /.lovable/oauth/consent.
- 5
Exchange code for tokens
POST <issuer>/oauth/token with grant_type=authorization_code, code, code_verifier, redirect_uri, client_id, resource. Response gives access_token (JWT, ~1h), refresh_token, expires_in.
- 6
Call the MCP endpoint
Send Authorization: Bearer <access_token> to https://tridenttaxis.online/mcp on every JSON-RPC call. Also send Accept: application/json, text/event-stream.
- 7
Refresh before expiry (or on 401)
POST <issuer>/oauth/token with grant_type=refresh_token. Supabase rotates the refresh token on every use โ always store the new one and discard the old. Reusing a rotated refresh_token revokes the whole family and forces re-consent.
Steps 1โ2 ยท Discovery
The MCP host advertises its authorization server; the issuer advertises its endpoints. Cache both for the lifetime of the client.
Discovery (curl)
# 1. Discover the authorization server from the MCP resource.
curl -sS https://tridenttaxis.online/.well-known/oauth-protected-resource | jq
# โ { "resource": "https://tridenttaxis.online/mcp",
# "authorization_servers": ["https://<project-ref>.supabase.co/auth/v1"],
# "bearer_methods_supported": ["header"] }
# 2. Fetch the authorization server metadata (RFC 8414).
ISSUER="https://<project-ref>.supabase.co/auth/v1"
curl -sS "$ISSUER/.well-known/oauth-authorization-server" | jq \
'{issuer, authorization_endpoint, token_endpoint, registration_endpoint,
grant_types_supported, code_challenge_methods_supported}'Step 3 ยท Dynamic Client Registration
Compatible MCP clients register automatically. For custom clients, POST once and persist the returned client_id.
POST /oauth/register
# 3. Dynamic Client Registration (RFC 7591). Compatible clients
# (Claude, ChatGPT, Cursor, Codex) do this automatically on first connect.
curl -sS -X POST "$ISSUER/oauth/register" \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP Client",
"redirect_uris": ["https://my-client.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'
# โ { "client_id": "abc123...", "client_id_issued_at": 1737000000, ... }Step 4 ยท Authorize with PKCE (browser)
Public clients MUST use PKCE with S256. Include resource=https://tridenttaxis.online/mcp (RFC 8707) so the token is audience-bound to this MCP endpoint.
Build authorize URL (JavaScript)
// 4. Build the authorization URL with PKCE (S256).
const codeVerifier = crypto.randomUUID() + crypto.randomUUID();
const codeChallenge = base64url(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier))
);
sessionStorage.setItem("pkce_verifier", codeVerifier);
const authUrl = new URL(`${ISSUER}/oauth/authorize`);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", CLIENT_ID);
authUrl.searchParams.set("redirect_uri", "https://my-client.example.com/oauth/callback");
authUrl.searchParams.set("scope", "openid email profile");
authUrl.searchParams.set("state", crypto.randomUUID());
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");
authUrl.searchParams.set("resource", "https://tridenttaxis.online/mcp"); // RFC 8707
window.location.href = authUrl.toString();
// User signs in at /auth, then approves at /.lovable/oauth/consent,
// then the browser is redirected to redirect_uri?code=...&state=...
function base64url(buf) {
return btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}Step 5 ยท Token exchange
POST /oauth/token (authorization_code)
# 5. Exchange the authorization code for an access token + refresh token.
curl -sS -X POST "$ISSUER/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=$AUTH_CODE" \
-d "redirect_uri=https://my-client.example.com/oauth/callback" \
-d "client_id=$CLIENT_ID" \
-d "code_verifier=$CODE_VERIFIER" \
-d "resource=https://tridenttaxis.online/mcp"
# โ {
# "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...",
# "token_type": "Bearer",
# "expires_in": 3600,
# "refresh_token": "v1.M2Yx...",
# "scope": "openid email profile"
# }Step 6 ยท Authorization header format
Exactly Authorization: Bearer <access_token> per RFC 6750. No JWT , Token , or access_token= query parameter โ the resource server only accepts bearer_methods_supported: ["header"].
Authenticated tool call (curl)
# 7. Call the MCP endpoint. The header format is exactly:
# Authorization: Bearer <access_token>
# Case-insensitive scheme, single space, raw JWT. No "JWT ", no "Token ",
# no query-string tokens, no cookies.
curl -sS -X POST https://tridenttaxis.online/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "name": "get_service_info", "arguments": {} }
}'Step 7 ยท Refresh (rotating tokens)
Access tokens live ~1 hour. Refresh proactively before expires_at, or reactively on a single 401. Refresh tokens rotate on every use โ persist the new one atomically and discard the old. A reused refresh token revokes the family; the user must re-consent.
POST /oauth/token (refresh_token)
# 6. Refresh the access token before it expires (or after a 401).
curl -sS -X POST "$ISSUER/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=$REFRESH_TOKEN" \
-d "client_id=$CLIENT_ID"
# โ { "access_token": "<new>", "token_type": "Bearer",
# "expires_in": 3600, "refresh_token": "<rotated>" }
#
# Supabase rotates refresh tokens on every use. Persist the NEW refresh_token
# and discard the previous one โ reusing an old refresh_token revokes the family.Full refresh-on-401 wrapper (JavaScript)
// End-to-end wrapper: automatic refresh on 401, one retry only.
export async function callMcp(method, params) {
let accessToken = await tokenStore.getAccess();
const doCall = (token) => fetch("https://tridenttaxis.online/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({ jsonrpc: "2.0", id: crypto.randomUUID(), method, params }),
});
let res = await doCall(accessToken);
if (res.status === 401) {
// WWW-Authenticate carries resource_metadata for full re-discovery if needed.
accessToken = await refreshAccessToken(); // POST /oauth/token grant_type=refresh_token
if (!accessToken) throw new Error("Re-consent required at /.lovable/oauth/consent");
res = await doCall(accessToken); // single retry with fresh token
}
return res;
}
async function refreshAccessToken() {
const refresh = await tokenStore.getRefresh();
const r = await fetch(`${ISSUER}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refresh,
client_id: CLIENT_ID,
}),
});
if (!r.ok) return null; // refresh revoked โ re-run authorize step
const { access_token, refresh_token, expires_in } = await r.json();
await tokenStore.save({ access_token, refresh_token, expires_at: Date.now() + expires_in * 1000 });
return access_token;
}Common mistakes
- Pasting a session JWT from
supabase.auth.signInWithPasswordโ it has noclient_idand fails verification. Use the OAuth flow above. - Sending the token as
?access_token=โฆโ query-string bearers are rejected. - Keeping the old refresh token after a successful refresh โ the next call revokes both.
- Skipping
resource=โฆon authorize/token โ the resulting token is not bound to this MCP endpoint and validation fails. - Retrying the same 401 without refreshing โ 401 is not a transport error; refresh first, then one retry.
OAuth token validation
Applies only when the operator has enabled MCP_REQUIRE_OAUTH=true. In public mode no verification runs and no Authorization header is expected.
How each request is verified
- Extract
Authorization: Bearer <token>. Missing โ 401 with aWWW-Authenticateheader pointing at the protected-resource metadata. - Fetch the issuer's OpenID discovery document and JWKS (cached). Verify the JWT signature against the matching
kid(ES256 / RS256). - Enforce claim requirements below. Any failure โ 401 with
error="invalid_token". - On success, the tool handler receives a
ctxwithisAuthenticated(),getUserId(),getUserEmail(),getClientId(),getClaims(), andgetToken()(forwarded to Supabase so RLS runs as the caller).
Required claims
| Claim | Requirement |
|---|---|
| iss | Must equal https://<project-ref>.supabase.co/auth/v1 exactly. Proxy hosts (e.g. .lovable.cloud) are rejected under RFC 8414 issuer matching. |
| aud | Must include "authenticated". Tokens issued for other audiences (service_role, anon) are rejected. |
| exp | Must be in the future. Expired tokens return 401 โ refresh via the authorization server, do not retry the same token. |
| sub | Non-empty Supabase user UUID. Surfaced to tools as ctx.getUserId(). |
| client_id | Required โ proves the token came from the OAuth flow. Copied app-session JWTs (from signInWithPassword) lack this claim and are rejected. |
| scope | No scopes are checked. Supabase-issued OAuth tokens carry no OAuth scope string; authorization is per-tool (see below), not scope-based. |
Scopes: the resource server does not enforce OAuth scopes. Every tool is available to any successfully authenticated caller; per-tool authorization (for example, restricting start_booking to specific roles) is done inside the handler using ctx.getClaims() and the app's user_roles table under RLS.
Error payload on auth failure
All auth failures (missing header, malformed JWT, bad signature, wrong issuer, wrong audience, expired token, missing client_id) return the same shape: HTTP 401, a WWW-Authenticate header per RFC 6750, and a JSON-RPC error body with code -32001.
Response headers
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
error="invalid_token",
error_description="Missing or invalid access token",
resource_metadata="https://tridenttaxis.online/.well-known/oauth-protected-resource"
Content-Type: application/jsonResponse body
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32001,
"message": "Unauthorized: bearer token is missing, expired, or failed verification"
}
}Clients discover the authorization server by fetching the resource_metadata URL from the WWW-Authenticate header:
GET /.well-known/oauth-protected-resource
{
"resource": "https://tridenttaxis.online/mcp",
"authorization_servers": [
"https://<project-ref>.supabase.co/auth/v1"
],
"bearer_methods_supported": ["header"],
"resource_documentation": "https://tridenttaxis.online/mcp-docs"
}A 401 means the token itself did not verify. It is not a retryable transport error โ do not resend the same token. Run the OAuth flow again (refresh or re-consent) and retry with the new access token. Session JWTs pasted from supabase.auth.signInWithPassword will never pass verification because they omit client_id.
Request examples
Exact headers and payload shape for both modes. Every MCP Streamable HTTP call must send Accept: application/json, text/event-stream โ omitting it returns HTTP 406.
Public mode (default)
No Authorization header. Any client can call any tool.
curl
curl -X POST https://tridenttaxis.online/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "estimate_fare",
"arguments": {
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "saloon"
}
}
}'JavaScript (fetch)
// Public mode โ no Authorization header required.
const res = await fetch("https://tridenttaxis.online/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
// MCP Streamable HTTP requires BOTH media types on Accept, or the
// server rejects the call with 406 Not Acceptable.
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "estimate_fare",
arguments: {
pickup: "Helensburgh Central Station",
dropoff: "Glasgow Airport",
vehicleType: "saloon",
},
},
}),
});
const data = await res.json();OAuth mode (MCP_REQUIRE_OAUTH=true)
Same headers as public mode, plus Authorization: Bearer <access_token>. Unauthenticated calls receive 401 with a WWW-Authenticate header pointing at the protected-resource metadata.
curl โ discovery
# 1. Discover the authorization server (returned as WWW-Authenticate on 401,
# or fetched directly at the well-known URL).
curl https://tridenttaxis.online/.well-known/oauth-protected-resourcecurl โ tool call
# 2. Call a tool with the access token obtained from the OAuth 2.1 flow.
curl -X POST https://tridenttaxis.online/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "estimate_fare",
"arguments": {
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "saloon"
}
}
}'JavaScript (fetch)
// OAuth mode โ accessToken comes from the standard OAuth 2.1 flow
// (dynamic client registration + authorization code + PKCE against the
// Supabase-hosted authorization server discovered from the resource metadata).
const res = await fetch("https://tridenttaxis.online/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
"Authorization": `Bearer ${accessToken}`,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "estimate_fare",
arguments: {
pickup: "Helensburgh Central Station",
dropoff: "Glasgow Airport",
vehicleType: "saloon",
},
},
}),
});
// A missing or expired token yields:
// HTTP/1.1 401 Unauthorized
// WWW-Authenticate: Bearer resource_metadata="https://tridenttaxis.online/.well-known/oauth-protected-resource"
if (res.status === 401) {
// Re-run the OAuth flow using the resource_metadata URL above.
}
const data = await res.json();Try it
Pick a tool, tweak the JSON arguments, and run a live tools/call against this site's MCP endpoint from your browser.
Schema ยท 2 required, 3 optional
pickupstringrequiredPickup address or place name, e.g. 'Helensburgh Central Station'.
length 3โ200
dropoffstringrequiredDropoff address or place name, e.g. 'Glasgow Airport'.
length 3โ200
vehicleTypestringoptional"saloon" | "estate" | "executive" | "6seater" | "mpv" | "wav"Vehicle class. Defaults to saloon. 'mpv' is the 8-seater; 'wav' is the wheelchair-accessible TX4.
departAtstring ยท date-timeoptionalISO 8601 planned departure time (UTC). Used to pick day/night/holiday tariff.
meetAndGreetbooleanoptionalWhether an airport meet & greet is requested (+ยฃ25).
Sends a real tools/call JSON-RPC request to /mcp from your browser. When the server responds with text/event-stream, each SSE frame appears in the live log as it arrives; the final frame with the matching request id becomes the "Final response".
Tools
Each tool follows the standard MCP JSON-RPC tools/call shape. All are safe for autonomous use except start_booking, which requires the passenger to finish the flow on the site.
estimate_fare
Read-onlyEstimate a taxi fare
Live GBP quote between a UK pickup and dropoff using the Argyll & Bute council tariff, vehicle rate mapping, and surcharges (airport, meet & greet, 8-seater local, pre-booking).
Input
| Field | Type | Description |
|---|---|---|
| pickup* | string (3โ200) | Pickup address or place name. |
| dropoff* | string (3โ200) | Dropoff address or place name. |
| vehicleType | "saloon" | "estate" | "executive" | "6seater" | "mpv" | "wav" | Vehicle class. Defaults to saloon. mpv = 8-seater; wav = wheelchair-accessible TX4. |
| departAt | string (ISO 8601, UTC) | Planned departure โ used to pick day/night/holiday tariff. |
| meetAndGreet | boolean | Whether an airport meet & greet is requested (+ยฃ25). |
Structured output
| Field | Type | Description |
|---|---|---|
| fareGBP | number | Fixed-price fare in GBP. |
| distanceMiles | number | Driving distance in miles. |
| durationSeconds | number | Driving duration in seconds. |
| rate | "day" | "night" | "holiday" | Tariff band applied. |
| airportSurchargeGBP | number | Airport pickup/drop fee already included in fareGBP. |
| tariffVersion | string | Version tag of the council tariff used. |
Example request
{
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "executive",
"meetAndGreet": false
}Example response
{
"fareGBP": 62.4,
"distanceMiles": 22,
"durationSeconds": 1980,
"rate": "day",
"airportSurchargeGBP": 10,
"tariffVersion": "argyll-2024-04"
}Call this tool
JSON-RPC tools/call POST to https://tridenttaxis.online/mcp. Both application/json and text/event-stream must appear in Accept or the server returns HTTP 406.
curl (public mode)
curl -sS https://tridenttaxis.online/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"estimate_fare","arguments":{"pickup":"Helensburgh Central Station","dropoff":"Glasgow Airport","vehicleType":"executive","meetAndGreet":false}}}'fetch (public mode)
const res = await fetch("https://tridenttaxis.online/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "estimate_fare",
arguments: {
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "executive",
"meetAndGreet": false
},
},
}),
});
const ct = res.headers.get("content-type") ?? "";
if (ct.includes("text/event-stream")) {
// SSE: parse each `data: {...}` line as a JSON-RPC frame.
const text = await res.text();
const frames = text
.split("\n")
.filter((l) => l.startsWith("data: "))
.map((l) => JSON.parse(l.slice(6)));
console.log(frames.at(-1)?.result);
} else {
const json = await res.json();
console.log(json.result);
}curl (OAuth mode)
curl -sS https://tridenttaxis.online/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"estimate_fare","arguments":{"pickup":"Helensburgh Central Station","dropoff":"Glasgow Airport","vehicleType":"executive","meetAndGreet":false}}}'fetch (OAuth mode)
const res = await fetch("https://tridenttaxis.online/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
"Authorization": `Bearer ${accessToken}`,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "estimate_fare",
arguments: {
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "executive",
"meetAndGreet": false
},
},
}),
});
const ct = res.headers.get("content-type") ?? "";
if (ct.includes("text/event-stream")) {
// SSE: parse each `data: {...}` line as a JSON-RPC frame.
const text = await res.text();
const frames = text
.split("\n")
.filter((l) => l.startsWith("data: "))
.map((l) => JSON.parse(l.slice(6)));
console.log(frames.at(-1)?.result);
} else {
const json = await res.json();
console.log(json.result);
}Response arrives as application/json for a single reply or text/event-stream when the server streams โ branch on response.headers.get("content-type") and parse SSE data: lines as JSON-RPC frames.
get_service_info
Read-onlyGet taxi service info
Overview of the operator: coverage area, vehicle fleet with passenger capacity, tariff bands, surcharge summary, and booking channels. No input required.
Input
No parameters.
Structured output
| Field | Type | Description |
|---|---|---|
| brand | string | Operator brand. |
| area | string | Service coverage area. |
| vehicles | Array<{ id, maxPassengers, note? }> | Fleet with passenger capacity. |
| surcharges | object | Pre-booking, airport pickup, meet & greet, 8-seater local multiplier. |
| tariffs | string[] | Tariff bands (T1 day, T2 night/evening, T3 Christmas & New Year). |
| bookingUrl | string | Canonical booking URL. |
Example request
{}Example response
{
"brand": "Trident Taxis",
"area": "Helensburgh, Lomond, Argyll & Bute (UK)",
"vehicles": [
{
"id": "saloon",
"maxPassengers": 4
},
{
"id": "mpv",
"maxPassengers": 7,
"note": "8-seater; +100% on local hires"
}
],
"surcharges": {
"preBookingGBP": 0.4,
"airportPickupOrDropGBP": 10,
"airportMeetAndGreetGBP": 25,
"eightSeaterLocalMultiplier": 2
},
"tariffs": [
"T1 day",
"T2 night/evening",
"T3 Christmas & New Year"
],
"bookingUrl": "https://tridenttaxis.online"
}Call this tool
JSON-RPC tools/call POST to https://tridenttaxis.online/mcp. Both application/json and text/event-stream must appear in Accept or the server returns HTTP 406.
curl (public mode)
curl -sS https://tridenttaxis.online/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_service_info","arguments":{}}}'fetch (public mode)
const res = await fetch("https://tridenttaxis.online/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "get_service_info",
arguments: {},
},
}),
});
const ct = res.headers.get("content-type") ?? "";
if (ct.includes("text/event-stream")) {
// SSE: parse each `data: {...}` line as a JSON-RPC frame.
const text = await res.text();
const frames = text
.split("\n")
.filter((l) => l.startsWith("data: "))
.map((l) => JSON.parse(l.slice(6)));
console.log(frames.at(-1)?.result);
} else {
const json = await res.json();
console.log(json.result);
}curl (OAuth mode)
curl -sS https://tridenttaxis.online/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_service_info","arguments":{}}}'fetch (OAuth mode)
const res = await fetch("https://tridenttaxis.online/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
"Authorization": `Bearer ${accessToken}`,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "get_service_info",
arguments: {},
},
}),
});
const ct = res.headers.get("content-type") ?? "";
if (ct.includes("text/event-stream")) {
// SSE: parse each `data: {...}` line as a JSON-RPC frame.
const text = await res.text();
const frames = text
.split("\n")
.filter((l) => l.startsWith("data: "))
.map((l) => JSON.parse(l.slice(6)));
console.log(frames.at(-1)?.result);
} else {
const json = await res.json();
console.log(json.result);
}Response arrives as application/json for a single reply or text/event-stream when the server streams โ branch on response.headers.get("content-type") and parse SSE data: lines as JSON-RPC frames.
start_booking
Passenger action requiredStart a booking
Builds a prefilled booking URL with a live fare quote embedded in the query string. Does NOT confirm a booking โ the passenger completes name, phone, date and payment on the site.
Input
| Field | Type | Description |
|---|---|---|
| pickup* | string (3โ200) | Pickup address or place name. |
| dropoff* | string (3โ200) | Dropoff address or place name. |
| vehicleType* | "saloon" | "mpv" | "wav" | Vehicle class the booking form accepts (saloon โค4, mpv = 8-seater โค7, wav = TX4). |
| departAt | string (ISO 8601, UTC) | Planned departure time. |
| meetAndGreet | boolean | Airport meet & greet (+ยฃ25). |
| passengers | integer (1โ8) | Used to validate against vehicle capacity. |
Structured output
| Field | Type | Description |
|---|---|---|
| bookingUrl | string | Prefilled URL for the passenger to open. |
| confirmed | boolean | Always false โ no booking is created server-side. |
| requiresPassengerAction | boolean | Always true โ passenger must submit the site form. |
| quote | object | Live fare quote (fareGBP, distanceMiles, durationSeconds, rate, vehicleType, tariffVersion). |
| prefilled | object | Echo of the inputs written into the URL. |
Example request
{
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "mpv",
"passengers": 6,
"meetAndGreet": true
}Example response
{
"bookingUrl": "https://tridenttaxis.online/book?pickup=Helensburgh+Central+Station&dropoff=Glasgow+Airport&vehicleType=mpv&fare=118.00&distance=22&duration=1980&meetAndGreet=1",
"confirmed": false,
"requiresPassengerAction": true,
"quote": {
"fareGBP": 118,
"distanceMiles": 22,
"durationSeconds": 1980,
"rate": "day",
"vehicleType": "mpv",
"tariffVersion": "argyll-2024-04"
}
}Call this tool
JSON-RPC tools/call POST to https://tridenttaxis.online/mcp. Both application/json and text/event-stream must appear in Accept or the server returns HTTP 406.
curl (public mode)
curl -sS https://tridenttaxis.online/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"start_booking","arguments":{"pickup":"Helensburgh Central Station","dropoff":"Glasgow Airport","vehicleType":"mpv","passengers":6,"meetAndGreet":true}}}'fetch (public mode)
const res = await fetch("https://tridenttaxis.online/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "start_booking",
arguments: {
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "mpv",
"passengers": 6,
"meetAndGreet": true
},
},
}),
});
const ct = res.headers.get("content-type") ?? "";
if (ct.includes("text/event-stream")) {
// SSE: parse each `data: {...}` line as a JSON-RPC frame.
const text = await res.text();
const frames = text
.split("\n")
.filter((l) => l.startsWith("data: "))
.map((l) => JSON.parse(l.slice(6)));
console.log(frames.at(-1)?.result);
} else {
const json = await res.json();
console.log(json.result);
}curl (OAuth mode)
curl -sS https://tridenttaxis.online/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"start_booking","arguments":{"pickup":"Helensburgh Central Station","dropoff":"Glasgow Airport","vehicleType":"mpv","passengers":6,"meetAndGreet":true}}}'fetch (OAuth mode)
const res = await fetch("https://tridenttaxis.online/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
"Authorization": `Bearer ${accessToken}`,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "start_booking",
arguments: {
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "mpv",
"passengers": 6,
"meetAndGreet": true
},
},
}),
});
const ct = res.headers.get("content-type") ?? "";
if (ct.includes("text/event-stream")) {
// SSE: parse each `data: {...}` line as a JSON-RPC frame.
const text = await res.text();
const frames = text
.split("\n")
.filter((l) => l.startsWith("data: "))
.map((l) => JSON.parse(l.slice(6)));
console.log(frames.at(-1)?.result);
} else {
const json = await res.json();
console.log(json.result);
}Response arrives as application/json for a single reply or text/event-stream when the server streams โ branch on response.headers.get("content-type") and parse SSE data: lines as JSON-RPC frames.
Streaming start_booking (SSE)
End-to-end JavaScript for the streaming response path: proper SSE framing (blank-line events, multi-line data: fields), JSON-RPC progress notifications vs the final result, and a retry policy that only backs off on transient failures.
callStartBooking.ts
// Stream a start_booking call and assemble the final JSON-RPC result.
// Handles: chunked SSE parsing, multi-line data fields, JSON-RPC progress
// notifications, transient network / 5xx errors with jittered backoff, and
// deterministic 4xx / tool errors (no retry).
//
// Requires: fetch + ReadableStream (Node 18+, all modern browsers).
const ENDPOINT = "https://tridenttaxis.online/mcp";
const MAX_ATTEMPTS = 3;
const RETRYABLE_HTTP = new Set([408, 425, 429, 500, 502, 503, 504]);
async function callStartBooking(args, { accessToken, signal } = {}) {
const body = JSON.stringify({
jsonrpc: "2.0",
id: crypto.randomUUID(),
method: "tools/call",
params: { name: "start_booking", arguments: args },
});
let attempt = 0;
while (true) {
attempt++;
try {
const res = await fetch(ENDPOINT, {
method: "POST",
signal,
headers: {
"Content-Type": "application/json",
// Required โ omitting text/event-stream returns HTTP 406.
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
body,
});
// Transport-level failures โ retry only the transient ones.
if (!res.ok) {
if (RETRYABLE_HTTP.has(res.status) && attempt < MAX_ATTEMPTS) {
await backoff(attempt, res.headers.get("retry-after"));
continue;
}
// 401 (bad token), 400/404/422 (bad request), or exhausted retries.
throw new McpTransportError(res.status, await safeText(res));
}
const ct = res.headers.get("content-type") ?? "";
const frame = ct.includes("text/event-stream")
? await consumeSseStream(res.body, res)
: await res.json();
// JSON-RPC transport error (e.g. -32601 unknown tool). Not retryable.
if (frame.error) throw new McpRpcError(frame.error);
// MCP tool-level error โ the RPC succeeded but the tool refused.
// These are deterministic (capacity exceeded, geocode failure) โ
// do not retry, surface to the caller so they can correct inputs.
if (frame.result?.isError) {
const msg = frame.result.content?.[0]?.text ?? "Tool returned isError";
throw new McpToolError(msg, frame.result);
}
return frame.result; // { content, structuredContent }
} catch (err) {
// Network-level failures (DNS, TCP reset, abort during read) โ retry.
const isNetwork =
err instanceof TypeError || err?.name === "FetchError";
if (isNetwork && attempt < MAX_ATTEMPTS) {
await backoff(attempt);
continue;
}
throw err;
}
}
}
// ---------------------- SSE assembler ------------------------------------
// The MCP server streams a sequence of SSE events. Each 'message' event's
// data field is a JSON-RPC frame โ usually one 'notifications/progress'
// per intermediate step, then one final response with matching id + result.
// A single logical event can span multiple data: lines (SSE spec ยง9.2:
// the fields are concatenated with '\n' before dispatch).
async function consumeSseStream(body, res) {
if (!body) throw new Error("SSE response has no body");
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let dataLines = [];
let final = null;
const flush = () => {
if (dataLines.length === 0) return;
const raw = dataLines.join("\n");
dataLines = [];
if (raw === "" || raw === "[DONE]") return;
let frame;
try {
frame = JSON.parse(raw);
} catch {
// Malformed event โ ignore, keep reading.
return;
}
// JSON-RPC notifications have no id; skip them but let a UI observe.
if (frame.method === "notifications/progress") {
onProgress?.(frame.params);
return;
}
// The response frame matches the request id โ it's the final result.
if (frame.id !== undefined) final = frame;
};
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Events are separated by a blank line (\n\n). Split on that, keep
// the last partial fragment in the buffer for the next chunk.
let sep;
while ((sep = buffer.indexOf("\n\n")) !== -1) {
const rawEvent = buffer.slice(0, sep);
buffer = buffer.slice(sep + 2);
for (const line of rawEvent.split("\n")) {
if (line.startsWith(":") || line === "") continue; // comment / blank
if (line.startsWith("data:")) {
// Strip exactly one leading space per SSE spec.
dataLines.push(line.slice(5).replace(/^ /, ""));
}
// 'event:', 'id:', 'retry:' fields โ ignored for JSON-RPC framing.
}
flush();
}
}
} finally {
reader.releaseLock();
}
if (!final) {
// Server closed the stream without sending a response frame โ treat as
// transient so the outer loop retries.
const status = res.status;
throw Object.assign(new Error("SSE stream ended before final frame"), {
name: "FetchError",
status,
});
}
return final;
}
// Optional progress hook โ replace with your UI.
function onProgress(params) {
// e.g. { progressToken, progress: 0.4, message: "Geocoding pickup..." }
console.debug("progress", params);
}
// ---------------------- Backoff & error types ----------------------------
async function backoff(attempt, retryAfterHeader) {
const explicit = Number(retryAfterHeader);
const base = Number.isFinite(explicit) && explicit >= 0
? explicit * 1000
: Math.min(1000 * 2 ** (attempt - 1), 8000);
const jitter = Math.random() * base;
await new Promise((r) => setTimeout(r, base + jitter));
}
async function safeText(res) {
try { return await res.text(); } catch { return ""; }
}
class McpTransportError extends Error {
constructor(status, body) {
super(`MCP transport ${status}: ${body.slice(0, 200)}`);
this.status = status;
}
}
class McpRpcError extends Error {
constructor(err) {
super(`JSON-RPC ${err.code}: ${err.message}`);
this.code = err.code;
this.data = err.data;
}
}
class McpToolError extends Error {
constructor(msg, result) {
super(msg);
this.result = result;
}
}
// ---------------------- Usage --------------------------------------------
try {
const result = await callStartBooking({
pickup: "Helensburgh Central Station",
dropoff: "Glasgow Airport",
vehicleType: "mpv",
passengers: 6,
meetAndGreet: true,
});
console.log("Booking URL:", result.structuredContent.bookingUrl);
console.log("Fare ยฃ:", result.structuredContent.quote.fareGBP);
} catch (err) {
if (err instanceof McpToolError) {
// Deterministic โ show err.message to the user, do not retry.
} else if (err instanceof McpTransportError && err.status === 401) {
// Re-run OAuth flow, then retry with fresh token.
} else {
// Network / stream drop / 5xx after retries exhausted.
}
throw err;
}Retry
- HTTP 408 / 425 / 429 / 5xx (honors
Retry-After) - Network errors: DNS, TCP reset, aborted read
- SSE stream closed before the final JSON-RPC frame
Do NOT retry
result.isError(capacity, geocode) โ fix the input- JSON-RPC
-32601 / -32602โ unknown tool or bad args - HTTP 401 โ refresh the OAuth token first, then a single retry
Downloadable schemas & types
JSON Schema (Draft 2020-12) files for every tool's input and structured output, plus a TypeScript .d.ts declaration for drop-in typing in your own client.
Bundle index
Manifest of all tool schemas with $ref pointers.
estimate_fare
Input/output JSON Schema for estimate_fare.
get_service_info
Input/output JSON Schema for get_service_info.
start_booking
Input/output JSON Schema for start_booking.
Schemas follow JSON Schema Draft 2020-12. Use them to validate arguments before calling tools/call, or generate typed clients with tools like json-schema-to-typescript or quicktype.
Success vs error โ side by side
For each tool: the exact tools/call request beside the exact response, with a happy path and a representative failure. Field names, error codes, and payload shapes match what the server actually returns.
Tool-level errors
JSON-RPC succeeds (has result). result.isError: true and a human-readable content[].text. Do not retry โ the input is wrong.
Transport-level errors
Top-level error object with a JSON-RPC code (-32700, -32600, -32601, -32602, -32603). No result field.
estimate_fare
Estimate fareRequest
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "estimate_fare",
"arguments": {
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "executive",
"meetAndGreet": false
}
}
}Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "ยฃ62.40 ยท 22 miles ยท 33 min ยท day tariff"
}
],
"structuredContent": {
"fareGBP": 62.4,
"distanceMiles": 22,
"durationSeconds": 1980,
"rate": "day",
"airportSurchargeGBP": 10,
"tariffVersion": "argyll-2024-04",
"appliedRules": [
"Executive rate applied",
"Airport pickup/drop fee +ยฃ10"
]
}
}
}Request
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "estimate_fare",
"arguments": {
"pickup": "xxx",
"dropoff": "Glasgow Airport",
"vehicleType": "saloon"
}
}
}Response
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"isError": true,
"content": [
{
"type": "text",
"text": "Could not estimate fare โ address could not be geocoded or the routing service is unavailable."
}
]
}
}get_service_info
Get service infoRequest
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_service_info",
"arguments": {}
}
}Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "Trident Taxis ยท Helensburgh, Lomond, Argyll & Bute (UK)"
}
],
"structuredContent": {
"brand": "Trident Taxis",
"area": "Helensburgh, Lomond, Argyll & Bute (UK)",
"vehicles": [
{
"id": "saloon",
"maxPassengers": 4
},
{
"id": "mpv",
"maxPassengers": 7,
"note": "8-seater; +100% on local hires"
}
],
"surcharges": {
"preBookingGBP": 0.4,
"airportPickupOrDropGBP": 10,
"airportMeetAndGreetGBP": 25,
"eightSeaterLocalMultiplier": 2
},
"tariffs": [
"T1 day",
"T2 night/evening",
"T3 Christmas & New Year"
],
"bookingUrl": "https://tridenttaxis.online"
}
}
}Request
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_service_infoo",
"arguments": {}
}
}Response
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32601,
"message": "Method not found: no tool named 'get_service_infoo'"
}
}start_booking
Start bookingRequest
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "start_booking",
"arguments": {
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "mpv",
"passengers": 6,
"meetAndGreet": true
}
}
}Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "Booking session ready โ fare ยฃ118.00 ยท 22 miles ยท 33 min.\n\nSend the passenger this link to finish the booking (name, phone, date, payment):\nhttps://tridenttaxis.online/book?pickup=Helensburgh+Central+Station&dropoff=Glasgow+Airport&vehicleType=mpv&fare=118.00&distance=22&duration=1980&meetAndGreet=1"
}
],
"structuredContent": {
"bookingUrl": "https://tridenttaxis.online/book?pickup=Helensburgh+Central+Station&dropoff=Glasgow+Airport&vehicleType=mpv&fare=118.00&distance=22&duration=1980&meetAndGreet=1",
"confirmed": false,
"requiresPassengerAction": true,
"quote": {
"fareGBP": 118,
"distanceMiles": 22,
"durationSeconds": 1980,
"rate": "day",
"vehicleType": "mpv",
"tariffVersion": "argyll-2024-04"
},
"prefilled": {
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "mpv",
"departAt": null,
"meetAndGreet": true
}
}
}
}Request
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "start_booking",
"arguments": {
"pickup": "Helensburgh Central Station",
"dropoff": "Glasgow Airport",
"vehicleType": "saloon",
"passengers": 6
}
}
}Response
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"isError": true,
"content": [
{
"type": "text",
"text": "Vehicle 'saloon' seats up to 4 passengers, but 6 were requested. Choose 'mpv' for groups over 4."
}
]
}
}Full downloadable copies live under Sample payloads. Complete error code reference: Error codes & retries.
Sample payloads
Copy-ready JSON-RPC tools/call request/response pairs for every tool โ one success path, one representative failure. Each file contains both the request and the exact response the server returns.
estimate_fare
Estimate fareget_service_info
Get service infostart_booking
Start bookingAll samples
Manifest with links to every sample file.Tool-level failures (bad address, capacity exceeded) return result.isError: true โ the JSON-RPC call itself still succeeds. Transport failures (unknown tool, malformed JSON) return a top-level error object with a JSON-RPC code such as -32601. See Error codes.
Troubleshooting โ error samples โ fix
Each downloadable error sample mapped to its most likely cause and the exact client-side change to make. Match on the error signal in the second column, not the human-readable text โ that wording can change.
Showing 3 of 3 entries
- Error signal
- result.isError === true โ "address could not be geocoded"
- Likely cause
- Pickup or dropoff is a placeholder / typo / non-UK string, so the geocoder returns no match. The transport call itself succeeded.
- Client-side fix
- Send full postal addresses or well-known landmarks with a UK context. Trim whitespace, drop internal quotes, and prefer 'Street, Town, Postcode' order. Retry only after the input changes โ retrying the same string will fail identically.
Fix
// โ arguments: { pickup: "xxx", dropoff: "Glasgow Airport" }
// โ
{
"pickup": "Helensburgh Central Station, Helensburgh G84 7HL",
"dropoff": "Glasgow Airport (GLA), Paisley PA3 2SW",
"vehicleType": "saloon"
}- Error signal
- top-level error.code === -32601 (Method not found)
- Likely cause
- params.name does not match a registered tool โ usually a typo ('get_service_infoo') or calling a tool that was removed. This is a JSON-RPC transport error, so result is absent.
- Client-side fix
- Fetch the live tool catalog from /.mcp/list-tools (or tools/list) at startup and use those exact names. Never hard-code a name you have not just validated against the catalog.
Fix
const list = await fetch("https://tridenttaxis.online/.mcp/list-tools").then(r => r.json());
const names = new Set(list.tools.map((t: { name: string }) => t.name));
if (!names.has(toolName)) throw new Error(`Unknown MCP tool: ${toolName}`);- Error signal
- result.isError === true โ "seats up to N passengers, but M were requested"
- Likely cause
- vehicleType capacity is lower than passengers. Capacity is validated before geocoding, so this fails immediately regardless of address quality.
- Client-side fix
- Pick vehicleType from passenger count before calling: 1โ4 โ 'saloon', 5โ6 โ 'mpv', wheelchair user โ 'wav'. Never send passengers > 8.
Fix
function pickVehicle(passengers: number, wheelchair = false) {
if (wheelchair) return "wav";
if (passengers <= 4) return "saloon";
if (passengers <= 6) return "mpv";
throw new Error("Groups over 6 require a pre-booked 8-seater โ call the office.");
}Rule of thumb: result.isError === true means the request reached the tool and the tool refused it โ fix the arguments. A top-level error.code means the request never reached the tool โ fix the method name, headers, or auth.
One-click API client collections
Pre-built import files with one request per MCP tool โ exact headers (Accept: application/json, text/event-stream, MCP-Protocol-Version), the JSON-RPC tools/call body, and an optional Authorization header for OAuth mode.
Postman collection
Postman Collection v2.1.0
Postman โ File โ Import โ drop this file. Edit the accessToken variable and enable the Authorization header for OAuth mode.
Insomnia workspace
Insomnia v4 export
Insomnia โ Preferences โ Data โ Import โ From File. Edit the Base environment (accessToken) for OAuth mode.
Collection variables
- mcpEndpoint
- Defaults to
https://tridenttaxis.online/mcp. Override to point at a preview or self-hosted origin. - accessToken
- Blank by default (public mode). Paste an OAuth 2.1 access token and enable the
Authorizationheader when the operator has setMCP_REQUIRE_OAUTH=true.
Manifest with both files: /mcp-schemas/collections/index.json. The Postman collection also ships a test script that asserts a JSON-RPC result is present for JSON responses and that an SSE stream ends with a framed response.
Error codes
The MCP endpoint surfaces two error shapes: transport-level JSON-RPC errors (invalid arguments, unknown method) and tool-level errors returned as isError: true inside the result. Both are documented below with the same code names your client can pattern-match on.
| Code | Surface | Applies to | Meaning & remedy |
|---|---|---|---|
| INVALID_ARGUMENTS | JSON-RPC -32602 | estimate_fare, start_booking | Arguments failed Zod validation (missing required field, string too short/long, enum mismatch, non-integer passenger count, malformed ISO 8601 departAt). |
| GEOCODE_FAILED | isError: true | estimate_fare, start_booking | The pickup or dropoff address could not be resolved to coordinates. Retry with a postcode, full street address, or well-known place name. |
| ROUTING_UNAVAILABLE | isError: true | estimate_fare, start_booking | Upstream routing/distance service is temporarily unavailable. Safe to retry with backoff. |
| CAPACITY_EXCEEDED | isError: true | start_booking | Requested passenger count exceeds the vehicle's seating capacity (saloon โค4, mpv โค7, wav โค4 as saloon / โค7 as mini-bus). Choose a larger vehicle type. |
| UNAUTHORIZED | HTTP 401 | estimate_fare, get_service_info, start_booking | OAuth mode is enabled and the request had no bearer token, or the token was expired/invalid. The response includes a WWW-Authenticate header with resource_metadata to restart the OAuth flow. |
| METHOD_NOT_FOUND | JSON-RPC -32601 | estimate_fare, get_service_info, start_booking | Unknown tool name in params.name, or an unsupported JSON-RPC method. Check the tool list on this page. |
| NOT_ACCEPTABLE | HTTP 406 | estimate_fare, get_service_info, start_booking | Request omitted 'Accept: application/json, text/event-stream'. Add both media types on every MCP Streamable HTTP call. |
INVALID_ARGUMENTS โ Zod validation failed
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params: pickup: String must contain at least 3 character(s)"
}
}GEOCODE_FAILED / ROUTING_UNAVAILABLE
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"isError": true,
"content": [
{
"type": "text",
"text": "Could not estimate fare โ address could not be geocoded or the routing service is unavailable."
}
]
}
}CAPACITY_EXCEEDED โ start_booking
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"isError": true,
"content": [
{
"type": "text",
"text": "Vehicle 'saloon' seats up to 4 passengers, but 6 were requested. Choose 'mpv' for groups over 4."
}
]
}
}UNAUTHORIZED โ OAuth mode, missing/expired token
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://tridenttaxis.online/.well-known/oauth-protected-resource"
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32001,
"message": "Unauthorized: missing or invalid bearer token"
}
}Retry policy: ROUTING_UNAVAILABLE is transient โ retry with exponential backoff (2โ3 attempts).GEOCODE_FAILED, INVALID_ARGUMENTS, and CAPACITY_EXCEEDED are deterministic โ fix the input before retrying.
Rate limits & abuse protection
An honest summary of what the /mcp endpoint enforces today, and how well-behaved clients should throttle themselves.
No per-client rate limit is applied at the transport layer today. The endpoint does not currently return 429 Too Many Requests, and no X-RateLimit-* or Retry-After headers are emitted. Treat the guidance below as a client-side contract, not a server-enforced quota.
Enforced today
What the server actively blocks
- Strict input validation โ Zod rejects oversized or malformed arguments before any geocoding or routing work runs (see
INVALID_ARGUMENTS). - No anonymous writes โ
start_bookingnever creates a booking record; it only returns a prefilled URL the passenger must complete on the site. - Capacity guard โ passenger-count checks short-circuit before geocoding, so malformed capacity requests can't burn upstream quota.
- Optional OAuth mode โ when
MCP_REQUIRE_OAUTH=true, anonymous callers are rejected with401and cannot reach any tool. - Origin protection โ
Accept-header enforcement (406) blocks browsers making naive cross-originapplication/json-only requests.
Not enforced
What is not rate-limited
- Requests per IP, per session, or per OAuth client.
- Requests per tool name (all three tools share no quota).
- Concurrent in-flight requests.
- Total daily volume.
A shared rate-limit primitive lands with the next backend platform release; this section will be updated with concrete thresholds and Retry-After semantics when it ships.
Recommended client behavior
Because there is no server-side limit yet, well-behaved MCP clients should self-throttle. These numbers are a courtesy contract โ exceed them and you may be blocked at the edge without notice.
| Tool | Suggested cap | Why |
|---|---|---|
| estimate_fare | โค 30 req/min, โค 2 concurrent | Each call triggers geocoding + routing lookups against upstream providers with their own quotas. |
| start_booking | โค 10 req/min per end user | Same upstreams as estimate_fare, plus the returned URL is only meaningful once per journey. |
| get_service_info | Cache locally for โฅ 24h | Response is static reference data. Fetch once per session, not per tool call. |
Backoff strategy
- On
ROUTING_UNAVAILABLEor HTTP5xx: retry with full jitter exponential backoff โ 1 s, 2 s, 4 s โ and stop after 3 attempts. - On
INVALID_ARGUMENTS,GEOCODE_FAILED, orCAPACITY_EXCEEDED: do not retry โ the input is deterministic. Prompt for corrected values instead. - On
401 UNAUTHORIZED(OAuth mode): run the OAuth flow from theWWW-Authenticateresource metadata; do not resend the failing token. - If a future response ever includes
Retry-Afteror HTTP429, honor it verbatim โ that will be the signal that a real limiter has shipped.
MCP specification: modelcontextprotocol.io . Report inaccurate quotes or tool errors via the contact details on the About page.