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.

Endpoint:https://tridenttaxis.online/mcp

Transport

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. 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. 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. 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. 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. 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. 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. 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 no client_id and 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

  1. Extract Authorization: Bearer <token>. Missing โ†’ 401 with a WWW-Authenticate header pointing at the protected-resource metadata.
  2. Fetch the issuer's OpenID discovery document and JWKS (cached). Verify the JWT signature against the matching kid (ES256 / RS256).
  3. Enforce claim requirements below. Any failure โ‡’ 401 with error="invalid_token".
  4. On success, the tool handler receives a ctx with isAuthenticated(), getUserId(), getUserEmail(), getClientId(), getClaims(), and getToken() (forwarded to Supabase so RLS runs as the caller).

Required claims

ClaimRequirement
issMust equal https://<project-ref>.supabase.co/auth/v1 exactly. Proxy hosts (e.g. .lovable.cloud) are rejected under RFC 8414 issuer matching.
audMust include "authenticated". Tokens issued for other audiences (service_role, anon) are rejected.
expMust be in the future. Expired tokens return 401 โ€” refresh via the authorization server, do not retry the same token.
subNon-empty Supabase user UUID. Surfaced to tools as ctx.getUserId().
client_idRequired โ€” proves the token came from the OAuth flow. Copied app-session JWTs (from signInWithPassword) lack this claim and are rejected.
scopeNo 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/json

Response 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-resource

curl โ€” 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
  • pickupstringrequired

    Pickup address or place name, e.g. 'Helensburgh Central Station'.

    length 3โ€“200

  • dropoffstringrequired

    Dropoff 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-timeoptional

    ISO 8601 planned departure time (UTC). Used to pick day/night/holiday tariff.

  • meetAndGreetbooleanoptional

    Whether 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-only

Estimate 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

FieldTypeDescription
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.
departAtstring (ISO 8601, UTC)Planned departure โ€” used to pick day/night/holiday tariff.
meetAndGreetbooleanWhether an airport meet & greet is requested (+ยฃ25).

Structured output

FieldTypeDescription
fareGBPnumberFixed-price fare in GBP.
distanceMilesnumberDriving distance in miles.
durationSecondsnumberDriving duration in seconds.
rate"day" | "night" | "holiday"Tariff band applied.
airportSurchargeGBPnumberAirport pickup/drop fee already included in fareGBP.
tariffVersionstringVersion 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-only

Get 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

FieldTypeDescription
brandstringOperator brand.
areastringService coverage area.
vehiclesArray<{ id, maxPassengers, note? }>Fleet with passenger capacity.
surchargesobjectPre-booking, airport pickup, meet & greet, 8-seater local multiplier.
tariffsstring[]Tariff bands (T1 day, T2 night/evening, T3 Christmas & New Year).
bookingUrlstringCanonical 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 required

Start 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

FieldTypeDescription
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).
departAtstring (ISO 8601, UTC)Planned departure time.
meetAndGreetbooleanAirport meet & greet (+ยฃ25).
passengersinteger (1โ€“8)Used to validate against vehicle capacity.

Structured output

FieldTypeDescription
bookingUrlstringPrefilled URL for the passenger to open.
confirmedbooleanAlways false โ€” no booking is created server-side.
requiresPassengerActionbooleanAlways true โ€” passenger must submit the site form.
quoteobjectLive fare quote (fareGBP, distanceMiles, durationSeconds, rate, vehicleType, tariffVersion).
prefilledobjectEcho 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 fare
Success ยท HTTP 200Tool result with content[] + structuredContent. JSON-RPC id echoes request.

Request

{
  "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"
      ]
    }
  }
}
Tool error ยท HTTP 200result.isError: trueGeocode failure โ€” tool-level error: result.isError = true, content[0].text explains, no error field at top level.

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 info
Success ยท HTTP 200No arguments required. structuredContent carries the full operator overview.

Request

{
  "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"
    }
  }
}
Transport error ยท HTTP 200error.code: -32601Transport-level failure โ€” misspelled tool name returns JSON-RPC error object with code -32601 (Method not found). No result field.

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 booking
Success ยท HTTP 200Prefilled URL โ€” confirmed: false, requiresPassengerAction: true. Passenger completes name/phone/payment on the site.

Request

{
  "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
      }
    }
  }
}
Tool error ยท HTTP 200result.isError: trueCapacity exceeded โ€” validated before geocoding, so it fails fast. Tool-level error (result.isError: 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.

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

estimate_fareTool-level errorestimate_fare.error.json
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"
}
get_service_infoTransport errorget_service_info.error.json
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}`);
start_bookingTool-level errorstart_booking.error.json
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.

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 Authorization header when the operator has set MCP_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.

CodeSurfaceApplies toMeaning & remedy
INVALID_ARGUMENTSJSON-RPC -32602estimate_fare, start_bookingArguments failed Zod validation (missing required field, string too short/long, enum mismatch, non-integer passenger count, malformed ISO 8601 departAt).
GEOCODE_FAILEDisError: trueestimate_fare, start_bookingThe pickup or dropoff address could not be resolved to coordinates. Retry with a postcode, full street address, or well-known place name.
ROUTING_UNAVAILABLEisError: trueestimate_fare, start_bookingUpstream routing/distance service is temporarily unavailable. Safe to retry with backoff.
CAPACITY_EXCEEDEDisError: truestart_bookingRequested 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.
UNAUTHORIZEDHTTP 401estimate_fare, get_service_info, start_bookingOAuth 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_FOUNDJSON-RPC -32601estimate_fare, get_service_info, start_bookingUnknown tool name in params.name, or an unsupported JSON-RPC method. Check the tool list on this page.
NOT_ACCEPTABLEHTTP 406estimate_fare, get_service_info, start_bookingRequest 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_booking never 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 with 401 and cannot reach any tool.
  • Origin protection โ€” Accept-header enforcement (406) blocks browsers making naive cross-origin application/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.

ToolSuggested capWhy
estimate_fareโ‰ค 30 req/min, โ‰ค 2 concurrentEach call triggers geocoding + routing lookups against upstream providers with their own quotas.
start_bookingโ‰ค 10 req/min per end userSame upstreams as estimate_fare, plus the returned URL is only meaningful once per journey.
get_service_infoCache locally for โ‰ฅ 24hResponse is static reference data. Fetch once per session, not per tool call.

Backoff strategy

  1. On ROUTING_UNAVAILABLE or HTTP 5xx: retry with full jitter exponential backoff โ€” 1 s, 2 s, 4 s โ€” and stop after 3 attempts.
  2. On INVALID_ARGUMENTS, GEOCODE_FAILED, or CAPACITY_EXCEEDED: do not retry โ€” the input is deterministic. Prompt for corrected values instead.
  3. On 401 UNAUTHORIZED (OAuth mode): run the OAuth flow from the WWW-Authenticate resource metadata; do not resend the failing token.
  4. If a future response ever includes Retry-After or HTTP 429, 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.