Important Network Notice: We are a fully independent nationwide transport operator based in, Scotland. We are not related to any closed, London-only booking systems or alternative .co.uk platforms. Our live cross-border driver app framework is powered by Trident Taxis.

Text +44 7728 282341

Calling from abroad? Dial +44 333 038 9011 or +44 333 004 7179 — from the US, +1 888 718 5429 is free.

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).
export const codeVerifier = crypto.randomUUID() + crypto.randomUUID();
export const codeChallenge = base64url(
  await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier))
);
sessionStorage.setItem("pkce_verifier", codeVerifier);

export 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=...

export 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;
}

export 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.
export 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",
      },
    },
  }),
});
export 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).
export 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.
}
export 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" | "luxury" | "5seater" | "6seater" | "7seater" | "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
    },
    },
  }),
});

export 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
    },
    },
  }),
});

export 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: {},
    },
  }),
});

export 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: {},
    },
  }),
});

export 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
    },
    },
  }),
});

export 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
    },
    },
  }),
});

export 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).

export const ENDPOINT = "https://tridenttaxis.online/mcp";
export const MAX_ATTEMPTS = 3;
export const RETRYABLE_HTTP = new Set([408, 425, 429, 500, 502, 503, 504]);

export 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).
export 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.
export function onProgress(params) {
  // e.g. { progressToken, progress: 0.4, message: "Geocoding pickup..." }
  console.debug("progress", params);
}

// ---------------------- Backoff & error types ----------------------------
export 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));
}

export async function safeText(res) {
  try { return await res.text(); } catch { return ""; }
}

export class McpTransportError extends Error {
  constructor(status, body) {
    super(`MCP transport ${status}: ${body.slice(0, 200)}`);
    this.status = status;
  }
}
export class McpRpcError extends Error {
  constructor(err) {
    super(`JSON-RPC ${err.code}: ${err.message}`);
    this.code = err.code;
    this.data = err.data;
  }
}
export 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());
export 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.

Instant quote

Enter your pickup and destination for a live price on the published council tariff.

Drivers wanted in the UK and every country worldwide

We receive taxi and private hire bookings in the UK and every country worldwide and need more licensed local drivers and operators to cover them. Join the network, set your own rates, and get jobs sent straight to your phone.