Skip to main content
Glama

plan_day

Turn an itinerary into one navigable multi-stop route. Provide a start and stops (each a location {lat, lon} or a free-text name to geocode, plus optional dwell_minutes time at the stop), optional depart_at (RFC 3339) for absolute ETAs, optimise: true to reorder stops for the shortest day (VROOM solver), and return_to_start. Costing "auto", "truck" (with a truck profile the whole day respects dimensional/ADR restrictions), "bicycle", "pedestrian" or "motor_scooter". Returns the stops in visit order with per-leg duration/distance and arrival/departure times, totals, and the full route geometry (polyline6). Geocoded names carry a resolution, when ambiguous is true, check alternatives and re-run with an explicit location rather than trusting the guess. Returns view_url, a mapmap.ai link that draws the answer on a map (no key needed); the link carries the stop coordinates and names.

Input Schema

NameRequiredDescriptionDefault
startYesWhere the day starts (name or location; `dwell_minutes` ignored).
stopsYesThe stops to visit (1–20). Visited in the given order unless `optimise` is true.
truckNoTruck profile (dimensions + ADR declaration). Requires costing "truck".
costingNoCosting model: "auto" (default), "truck", "bicycle", "pedestrian" or "motor_scooter".auto
optimiseNoReorder the stops for the shortest day (VROOM solver; requires the optimisation sidecar). Default false: visit in the given order.
depart_atNoDeparture time as RFC 3339 (e.g. "2026-07-18T09:00:00Z"); when given, every ETA is also returned as an absolute timestamp.
return_to_startNoEnd the day back at the start (default false).

Output Schema

NameRequiredDescriptionDefault
startYesThe day's starting point.
stopsYesThe stops in visit order, each with leg, ETAs and any geocoding resolution to double-check.
costingYesThe costing the plan was routed with.
summaryYesOne-line human-readable summary of the day.
view_urlNoA mapmap.ai link that draws this day plan on a map: open it or pass it on as the last line of the answer. Nothing is stored server-side and opening it needs no key; the link itself carries the stop coordinates and stop names (plus any arrival clocks, dwell times and totals) and nothing else, so anyone holding it can read those stops. Absent when the plan cannot be linked (see `view_url_note`) or the server runs with links turned off (`SN_MCP_SITE_URL` empty).
depart_atNoThe departure time echoed back, when one was given.
finish_atNoAbsolute end of the day (RFC 3339), when `depart_at` was given.
optimisedYesWhether the stop order was optimised (VROOM) or kept as given.
return_legNoThe leg back to the start, when `return_to_start` was set.
total_dwell_sYesTotal time at stops, seconds.
view_url_noteNoWhy there is no `view_url` (too many vehicles or stops for one link, or over the 6,000 character cap), or what the link left out to fit under the cap (arrival times, dwell times, then stop names). Relay it with the link.
finish_offset_sYesEnd of the day (last arrival + dwell), seconds after departure.
total_distance_mYesTotal travel distance, metres.
geometry_polyline6YesFull multi-stop route geometry (polyline6), hand it to the map SDK or the `route` tool consumers directly.
total_travel_duration_sYesTotal driving/travel time, seconds.

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed7 schema fields changed
    • changedInput schema / $defs / TruckSpec / properties / tunnel_code / description
      Previous value: -"ADR 8.6.4 tunnel restriction code of the load, e.g. \"B\", \"C5000D\",\n\"B/D\", or \"(—)\"/\"none\" for explicitly unrestricted. Leave unset if\nunknown: a hazmat load without a code is conservatively treated as\ncode B (allowed only through category-A tunnels)."New value: +"ADR 8.6.4 tunnel restriction code of the load, e.g. \"B\", \"C5000D\",\n\"B/D\", or \"(-)\"/\"none\" for explicitly unrestricted. Leave unset if\nunknown: a hazmat load without a code is conservatively treated as\ncode B (allowed only through category-A tunnels)."
    • changedOutput schema / $defs / GeocodeHit / properties / match / description
      Previous value: -"How far this hit can be trusted to be the place that was asked\nfor — see [`GeocodeMatch`]. Present whenever the MapMap gateway\nanswered; absent on a deployment falling back to the direct Photon\ngeocoder, and absent on the gateway's own fast paths (a pasted\ncoordinate pair, a bare UK outward code, a category browse), which\nanswer without a ranking to report on."New value: +"How far this hit can be trusted to be the place that was asked\nfor, see [`GeocodeMatch`]. Present whenever the MapMap gateway\nanswered; absent on a deployment falling back to the direct Photon\ngeocoder, and absent on the gateway's own fast paths (a pasted\ncoordinate pair, a bare UK outward code, a category browse), which\nanswer without a ranking to report on."
    • changedOutput schema / $defs / GeocodeMatch / description
      Previous value: -"How well one geocoding result answers what was actually asked.\n\nGeocoding's real failure mode is not \"no answer\" but a confident answer\nto a different question: a plausible row on the wrong street, with\nnothing in the response to say so. This object is that missing say-so,\nand an agent should read it before acting on an address.\n\nHow to read it:\n\n* Any component `unmatched` or `inferred` on the TOP hit means the\n  answer does not carry the address that was asked for — an `unmatched`\n  postcode means the result has no postcode at all, `inferred` means it\n  has a different one. Neither is a match. Say so rather than presenting\n  the hit as the address, and reach for `verify_places` when the address\n  came from a model or a user and needs checking rather than using.\n* A small `score_gap` means the ranking barely chose between this hit\n  and the runner-up, which is exactly when to show the alternatives\n  instead of picking one for the user."New value: +"How well one geocoding result answers what was actually asked.\n\nGeocoding's real failure mode is not \"no answer\" but a confident answer\nto a different question: a plausible row on the wrong street, with\nnothing in the response to say so. This object is that missing say-so,\nand an agent should read it before acting on an address.\n\nHow to read it:\n\n* Any component `unmatched` or `inferred` on the TOP hit means the\n  answer does not carry the address that was asked for, an `unmatched`\n  postcode means the result has no postcode at all, `inferred` means it\n  has a different one. Neither is a match. Say so rather than presenting\n  the hit as the address, and reach for `verify_places` when the address\n  came from a model or a user and needs checking rather than using.\n* A small `score_gap` means the ranking barely chose between this hit\n  and the runner-up, which is exactly when to show the alternatives\n  instead of picking one for the user."
    • changedOutput schema / $defs / GeocodeMatch / properties / score_gap / description
      Previous value: -"The top result's score minus the runner-up's, rounded to 3 decimal\nplaces. `0` for a single result, and `0` from the `photon` source,\nwhich publishes no per-result score — so a `0` is \"no signal\", not\n\"a tie\"."New value: +"The top result's score minus the runner-up's, rounded to 3 decimal\nplaces. `0` for a single result, and `0` from the `photon` source,\nwhich publishes no per-result score, so a `0` is \"no signal\", not\n\"a tie\"."
    • changedOutput schema / $defs / GeocodeMatchComponents / description
      Previous value: -"Per-component verdicts inside a [`GeocodeMatch`]. Each is one of\n\"matched\", \"inferred\" or \"unmatched\"; a component that was not supplied\nis absent entirely.\n\n* \"matched\" — the result's own field carries the value asked for (case-\n  and accent-insensitive, and by containment, so `city: \"London\"`\n  matches \"City of London\").\n* \"inferred\" — the result carries a value for that component, but not\n  the one asked for. It reached the page through ranking, as when a\n  street is found by its transliterated name and displayed under its\n  canonical one.\n* \"unmatched\" — the result carries no value for that component at all."New value: +"Per-component verdicts inside a [`GeocodeMatch`]. Each is one of\n\"matched\", \"inferred\" or \"unmatched\"; a component that was not supplied\nis absent entirely.\n\n* \"matched\", the result's own field carries the value asked for (case-\n  and accent-insensitive, and by containment, so `city: \"London\"`\n  matches \"City of London\").\n* \"inferred\", the result carries a value for that component, but not\n  the one asked for. It reached the page through ranking, as when a\n  street is found by its transliterated name and displayed under its\n  canonical one.\n* \"unmatched\", the result carries no value for that component at all."
    • changedOutput schema / $defs / StopResolution / properties / ambiguous / description
      Previous value: -"True when other plausible matches exist somewhere else — check\n`alternatives` and re-run with an explicit `location` if the\nchosen one is wrong."New value: +"True when other plausible matches exist somewhere else, check\n`alternatives` and re-run with an explicit `location` if the\nchosen one is wrong."
    • changedOutput schema / properties / geometry_polyline6 / description
      Previous value: -"Full multi-stop route geometry (polyline6) — hand it to the map SDK\nor the `route` tool consumers directly."New value: +"Full multi-stop route geometry (polyline6), hand it to the map SDK\nor the `route` tool consumers directly."
  2. Changed2 schema fields changed
    • addedOutput schema / properties / view_url
      Added value: +{
      +  "description": "A mapmap.ai link that draws this day plan on a map: open it or pass it\non as the last line of the answer. Nothing is stored server-side and\nopening it needs no key; the link itself carries the stop coordinates\nand stop names (plus any arrival clocks, dwell times and totals) and\nnothing else, so anyone holding it can read those stops. Absent when\nthe plan cannot be linked (see `view_url_note`) or the server runs\nwith links turned off (`SN_MCP_SITE_URL` empty).",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / view_url_note
      Added value: +{
      +  "description": "Why there is no `view_url` (too many vehicles or stops for one link,\nor over the 6,000 character cap), or what the link left out to fit\nunder the cap (arrival times, dwell times, then stop names). Relay it\nwith the link.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
  3. Changed10 schema fields changed
    • addedInput schema / $defs / LatLon / anyOf
      Added value: +[
      +  {
      +    "properties": {
      +      "lat": {
      +        "description": "Latitude in decimal degrees (−90 to 90).",
      +        "format": "double",
      +        "type": "number"
      +      },
      +      "lon": {
      +        "description": "Longitude in decimal degrees (−180 to 180).",
      +        "format": "double",
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "lat",
      +      "lon"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "description": "GeoJSON position [lon, lat]: longitude FIRST.",
      +    "items": {
      +      "format": "double",
      +      "type": "number"
      +    },
      +    "maxItems": 2,
      +    "minItems": 2,
      +    "type": "array"
      +  }
      +]
    • changedInput schema / $defs / LatLon / description
      Previous value: -"A WGS84 coordinate pair in decimal degrees."New value: +"A WGS84 coordinate in decimal degrees: a {lat, lon} object (preferred), or a GeoJSON [lon, lat] array with LONGITUDE FIRST, the same order as every polygon field on this server."
    • removedInput schema / $defs / LatLon / properties
      Removed value: -{
      -  "lat": {
      -    "description": "Latitude in decimal degrees (−90 to 90).",
      -    "format": "double",
      -    "type": "number"
      -  },
      -  "lon": {
      -    "description": "Longitude in decimal degrees (−180 to 180).",
      -    "format": "double",
      -    "type": "number"
      -  }
      -}
    • removedInput schema / $defs / LatLon / required
      Removed value: -[
      -  "lat",
      -  "lon"
      -]
    • removedInput schema / $defs / LatLon / type
      Removed value: -"object"
    • addedOutput schema / $defs / LatLon / anyOf
      Added value: +[
      +  {
      +    "properties": {
      +      "lat": {
      +        "description": "Latitude in decimal degrees (−90 to 90).",
      +        "format": "double",
      +        "type": "number"
      +      },
      +      "lon": {
      +        "description": "Longitude in decimal degrees (−180 to 180).",
      +        "format": "double",
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "lat",
      +      "lon"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "description": "GeoJSON position [lon, lat]: longitude FIRST.",
      +    "items": {
      +      "format": "double",
      +      "type": "number"
      +    },
      +    "maxItems": 2,
      +    "minItems": 2,
      +    "type": "array"
      +  }
      +]
    • changedOutput schema / $defs / LatLon / description
      Previous value: -"A WGS84 coordinate pair in decimal degrees."New value: +"A WGS84 coordinate in decimal degrees: a {lat, lon} object (preferred), or a GeoJSON [lon, lat] array with LONGITUDE FIRST, the same order as every polygon field on this server."
    • removedOutput schema / $defs / LatLon / properties
      Removed value: -{
      -  "lat": {
      -    "description": "Latitude in decimal degrees (−90 to 90).",
      -    "format": "double",
      -    "type": "number"
      -  },
      -  "lon": {
      -    "description": "Longitude in decimal degrees (−180 to 180).",
      -    "format": "double",
      -    "type": "number"
      -  }
      -}
    • removedOutput schema / $defs / LatLon / required
      Removed value: -[
      -  "lat",
      -  "lon"
      -]
    • removedOutput schema / $defs / LatLon / type
      Removed value: -"object"
  4. Changed3 schema fields changed
    • addedOutput schema / $defs / GeocodeHit / properties / match
      Added value: +{
      +  "anyOf": [
      +    {
      +      "$ref": "#/$defs/GeocodeMatch"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "description": "How far this hit can be trusted to be the place that was asked\nfor — see [`GeocodeMatch`]. Present whenever the MapMap gateway\nanswered; absent on a deployment falling back to the direct Photon\ngeocoder, and absent on the gateway's own fast paths (a pasted\ncoordinate pair, a bare UK outward code, a category browse), which\nanswer without a ranking to report on."
      +}
    • addedOutput schema / $defs / GeocodeMatch
      Added value: +{
      +  "description": "How well one geocoding result answers what was actually asked.\n\nGeocoding's real failure mode is not \"no answer\" but a confident answer\nto a different question: a plausible row on the wrong street, with\nnothing in the response to say so. This object is that missing say-so,\nand an agent should read it before acting on an address.\n\nHow to read it:\n\n* Any component `unmatched` or `inferred` on the TOP hit means the\n  answer does not carry the address that was asked for — an `unmatched`\n  postcode means the result has no postcode at all, `inferred` means it\n  has a different one. Neither is a match. Say so rather than presenting\n  the hit as the address, and reach for `verify_places` when the address\n  came from a model or a user and needs checking rather than using.\n* A small `score_gap` means the ranking barely chose between this hit\n  and the runner-up, which is exactly when to show the alternatives\n  instead of picking one for the user.",
      +  "properties": {
      +    "components": {
      +      "$ref": "#/$defs/GeocodeMatchComponents",
      +      "description": "Per-component verdict on this hit: one entry for each structured\ncomponent supplied, and empty when the query was free text only."
      +    },
      +    "score_gap": {
      +      "description": "The top result's score minus the runner-up's, rounded to 3 decimal\nplaces. `0` for a single result, and `0` from the `photon` source,\nwhich publishes no per-result score — so a `0` is \"no signal\", not\n\"a tie\".",
      +      "format": "double",
      +      "type": "number"
      +    },
      +    "source": {
      +      "description": "Which backend answered: \"mapmap-index\" (the first-party index) or\n\"photon\".",
      +      "type": "string"
      +    }
      +  },
      +  "required": [
      +    "components",
      +    "score_gap",
      +    "source"
      +  ],
      +  "type": "object"
      +}
    • addedOutput schema / $defs / GeocodeMatchComponents
      Added value: +{
      +  "description": "Per-component verdicts inside a [`GeocodeMatch`]. Each is one of\n\"matched\", \"inferred\" or \"unmatched\"; a component that was not supplied\nis absent entirely.\n\n* \"matched\" — the result's own field carries the value asked for (case-\n  and accent-insensitive, and by containment, so `city: \"London\"`\n  matches \"City of London\").\n* \"inferred\" — the result carries a value for that component, but not\n  the one asked for. It reached the page through ranking, as when a\n  street is found by its transliterated name and displayed under its\n  canonical one.\n* \"unmatched\" — the result carries no value for that component at all.",
      +  "properties": {
      +    "city": {
      +      "description": "Verdict on the supplied `city`.",
      +      "type": [
      +        "string",
      +        "null"
      +      ]
      +    },
      +    "country": {
      +      "description": "Verdict on the supplied `country`.",
      +      "type": [
      +        "string",
      +        "null"
      +      ]
      +    },
      +    "housenumber": {
      +      "description": "Verdict on the supplied `housenumber`.",
      +      "type": [
      +        "string",
      +        "null"
      +      ]
      +    },
      +    "postcode": {
      +      "description": "Verdict on the supplied `postcode`.",
      +      "type": [
      +        "string",
      +        "null"
      +      ]
      +    },
      +    "street": {
      +      "description": "Verdict on the supplied `street`.",
      +      "type": [
      +        "string",
      +        "null"
      +      ]
      +    }
      +  },
      +  "type": "object"
      +}
  5. First observed

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does reasonably well: it discloses that `optimise` depends on the VROOM solver ("requires the optimisation sidecar"), that geocoded names return a `resolution` and should be re-run with explicit coordinates when `ambiguous`, and that `view_url` encodes stop coordinates/names with no key required. It does not mention rate limits, quota, or error behavior, keeping it short of a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the purpose, then walks through inputs and return shape in a logical order, and every clause carries information. It is dense and somewhat long, with some redundancy against the schema (re-listing costing values and truck restrictions), which keeps it from a 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 7-parameter planning tool with an output schema, the description is comprehensive: inputs, ordering behavior, geocoding ambiguity handling and the map link are all covered, and return values are explained (though the output schema already covers them). With no annotations, a brief note on permissions/cost or failure modes would be the only remaining gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The description restates the same parameter facts already documented in the schema (costing list, truck profile, optimise/VROOM, depart_at semantics) rather than adding new syntax or default guidance, so it earns no more than the baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource+scope: "Turn an itinerary into one navigable multi-stop route," which is immediately distinguishable from siblings like `route` (single route), `order_stops`/`optimise_routes` (pure ordering) and `plan_errands`. The description also enumerates the distinguishing capabilities (dwell time, ETAs, optimise via VROOM) so an agent can tell what makes this tool different without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied rather than stated: the feature set (multi-stop itinerary, dwell_minutes, depart_at, return_to_start) makes the intended scenario inferable, but there is no explicit "use this when / use X instead when" routing against close siblings such as `plan_errands`, `order_stops`, or `route`. No exclusions or prerequisites are given.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources