How to Build a Travel Planning Agent with Geoapify MCP Tools

API or service
Geoapify MCP Server
Task
Travel request → MCP tool calls → walking day plan
Examples
MCPAI
Difficulty
Intermediate
Time
15 min

Turn a travel request into a walking day plan

A traveler asks an AI agent:

Plan a walking day in San Francisco. Start and end at the Ferry Building.
Morning: a museum. Then a viewpoint. After that, Italian lunch in North Beach.
Keep each walk under 25 minutes. Use the Geoapify tools for places and walking times.

A language model can name famous places from memory, but it cannot reliably tell which museum is open in the morning, which lunch spot is a short walk from the viewpoint, or how long each walk takes. The Geoapify MCP Server gives the agent tools for these facts. The agent chains five of them:

  1. geocode_address turns "Ferry Building, San Francisco" into coordinates.
  2. list_place_categories maps "museum", "viewpoint", and "Italian lunch" to supported category IDs.
  3. search_places finds real candidates for each stop.
  4. calculate_route_matrix compares walking times from one stop to several candidates.
  5. calculate_route returns the walking distance and time of every leg of the final day.

Task flow: travel request → geocode_address → list_place_categories → search_places per stop → calculate_route_matrix to choose among candidates → calculate_route for the full day → day plan built only from tool results.

This guide shows the real tool calls and results of that workflow, then the agent instructions that keep it grounded. It assumes the Geoapify MCP Server is already connected to your agent. To set it up, see Connect Geoapify MCP to an AI client or Add Geoapify MCP tools to OpenClaw and other agents.

Map the travel request to MCP tools

Split the request into facts the agent must look up and decisions it can make itself:

Part of the request Tool Key arguments Result used next
"Start and end at the Ferry Building" geocode_address query, limit: 1 lat, lon of the start
"museum", "viewpoint", "Italian lunch" list_place_categories none entertainment.museum, tourism.attraction.viewpoint, catering.restaurant.italian
One candidate list per stop search_places category, lat, lon, radius_meters, limit Names, coordinates, opening_hours, distance
"Keep each walk under 25 minutes" while choosing a stop calculate_route_matrix sources, targets, mode: "walk" Walking time and distance per pair
The final day calculate_route waypoints, mode: "walk", geometry_scope Total and per-leg walking time

Search for each stop around the right center. The museum and viewpoint searches use the Ferry Building because the day starts there. The lunch search uses the chosen viewpoint, because lunch follows it.

The distance field in search_places results is a straight-line distance from the search center. Use calculate_route_matrix or calculate_route for walking time. On hilly or irregular street grids, the walking time can be much longer than the straight-line distance suggests.

Choose the tool arguments to keep the context small. Five candidates per stop are enough for a day plan, and geometry_scope: "none" skips route geometry when the agent only needs times. Request geometry only when the application draws the route on a map.

Credits for this workflow: geocode_address 1, list_place_categories 0, three search_places calls with up to 20 results each 3, a 1 × 5 walking matrix 5, and a route with four legs 4, about 13 credits in total. tools/list and initialize are free. See MCP pricing.

Find the start and candidate stops

Geocode the start

{
  "name": "geocode_address",
  "arguments": {
    "query": "Ferry Building, San Francisco, CA",
    "limit": 1,
    "lang": "en"
  }
}

Reduced result:

{
  "count": 1,
  "results": [
    {
      "formatted": "Ferry Building, Harry Bridges Plaza, San Francisco, CA 94111, United States of America",
      "lat": 37.7947815,
      "lon": -122.393678,
      "result_type": "amenity",
      "rank": {"confidence": 1, "match_type": "full_match"}
    }
  ]
}

Continue only with one confident result. If the result is empty or ambiguous, ask the traveler to clarify.

Choose category IDs

list_place_categories takes an empty argument object and returns 833 supported category IDs. The agent selects entertainment.museum, tourism.attraction.viewpoint, and catering.restaurant.italian. The list rarely changes, so an agent can reuse it during a session.

Find a morning museum

{
  "name": "search_places",
  "arguments": {
    "category": "entertainment.museum",
    "lat": 37.7947815,
    "lon": -122.393678,
    "radius_meters": 2000,
    "limit": 5,
    "lang": "en"
  }
}

Reduced result:

Name Distance opening_hours
San Francisco Railway Museum 109 m Tu-Sa 12:00-17:00
Pier 24 787 m not returned
Exploratorium 802 m Tu-Su 10:00-17:00; Jun 19 - Sep 4 Mo-Su 10:00-17:00, Fr 10:00-21:00
Institute of Contemporary Art San Francisco 860 m We, Fr-Su 11:00-17:00; Th 11:00-19:00
San Francisco Historical Society Museum 877 m Tu-Sa 10:00-16:00

The nearest museum opens at noon, which does not fit "morning". The agent picks the Exploratorium (lat 37.8008982, lon -122.3985255), which opens at 10:00 from Tuesday to Sunday. Opening hours come from OpenStreetMap and can be missing or out of date, so the plan should tell the traveler to confirm them.

Find the viewpoint

The same search with "category": "tourism.attraction.viewpoint" returns the Bay Bridge and Treasure Island Viewpoint (346 m), Bay Lights Viewpoint (836 m), two unnamed viewpoints, and Coit Tower (1,362 m). Coit Tower is the only named result in the North Beach area ("suburb": "North Beach"), where the traveler wants lunch, so the agent picks it (lat 37.8023791, lon -122.4058344). Skip unnamed results in a plan that people will read.

Find lunch near the viewpoint

{
  "name": "search_places",
  "arguments": {
    "category": "catering.restaurant.italian",
    "lat": 37.8023791,
    "lon": -122.4058344,
    "radius_meters": 800,
    "limit": 5,
    "lang": "en"
  }
}

Reduced result:

Name Address Distance opening_hours
Bocce Cafe 478 Green Street 297 m not returned
Sodini's 510 Green Street 331 m Mo-Fr 17:00-00:00; Sa-Su16:00-00:00
Caffé Sport 574 Green Street 388 m not returned
Flour + Water Pizzeria 532 Columbus Avenue 419 m Mo-Su 11:30-22:00
Mona Lisa Mare e Monti 414 Columbus Avenue 427 m not returned

Sodini's opens in the evening, so it is not a lunch option. The next step compares the walking times of the candidates.

Compare walks and build the day route

Compare lunch candidates by walking time

Use calculate_route_matrix with the viewpoint as the only source and the five lunch candidates as targets:

{
  "name": "calculate_route_matrix",
  "arguments": {
    "sources": [{"lat": 37.8023791, "lon": -122.4058344}],
    "targets": [
      {"lat": 37.7998993, "lon": -122.4070903},
      {"lat": 37.7997834, "lon": -122.4076847},
      {"lat": 37.7996767, "lon": -122.4086205},
      {"lat": 37.8000523, "lon": -122.4095795},
      {"lat": 37.7989554, "lon": -122.4080374}
    ],
    "mode": "walk"
  }
}

Reduced result:

{
  "sources_to_targets": [[
    {"distance": 449, "time": 426, "source_index": 0, "target_index": 0},
    {"distance": 542, "time": 473, "source_index": 0, "target_index": 1},
    {"distance": 630, "time": 548, "source_index": 0, "target_index": 2},
    {"distance": 680, "time": 598, "source_index": 0, "target_index": 3},
    {"distance": 708, "time": 611, "source_index": 0, "target_index": 4}
  ]],
  "distance_units": "meters",
  "time_units": "seconds"
}

Match cells to places by target_index, not by name. Bocce Cafe (index 0) is the shortest walk, 449 m and about 7 minutes. Its opening hours were not returned, so the agent offers it with a note and names the closest place with confirmed lunch hours, Flour + Water Pizzeria (index 3, about 10 minutes, opens 11:30), as the alternative.

The matrix also shows why the agent needs walking times. Bocce Cafe is 297 m from Coit Tower in a straight line but 449 m on foot.

Calculate the full walking day

Send the stops in the order the traveler asked for, and return to the start:

{
  "name": "calculate_route",
  "arguments": {
    "waypoints": [
      {"lat": 37.7947815, "lon": -122.393678},
      {"lat": 37.8008982, "lon": -122.3985255},
      {"lat": 37.8023791, "lon": -122.4058344},
      {"lat": 37.7998993, "lon": -122.4070903},
      {"lat": 37.7947815, "lon": -122.393678}
    ],
    "mode": "walk",
    "geometry_scope": "none"
  }
}

Result:

{
  "distance": 4037,
  "units": "metric",
  "distance_units": "meters",
  "time": 3870.437,
  "time_units": "seconds",
  "legs": [
    {"distance": 843, "time": 744.579},
    {"distance": 1113, "time": 1292.075},
    {"distance": 449, "time": 426.141},
    {"distance": 1631, "time": 1407.64}
  ]
}

Every leg is under 25 minutes, so the plan meets the traveler's limit. The day has 4.0 km and about 65 minutes of walking. The second leg covers 1,113 m in about 22 minutes, slower per meter than the others. Always use the returned time rather than a fixed walking speed.

The traveler fixed the order of the stops, so this call keeps optimize_stops at its default, false. When the order is flexible, set "optimize_stops": true to let the tool reorder the middle stops while it keeps the first and last waypoint. For this day, the optimized call returned the same order and the same 4,037 m.

Present the day plan

The agent adds visit lengths of its own, labels them as assumptions, and converts leg times into a schedule:

Time Stop Walk to the next stop
10:00 Start at the Ferry Building 843 m, 12 min
10:12–12:12 Exploratorium (opens 10:00, Tue–Sun; 2 hours assumed) 1,113 m, 22 min
12:34–13:04 Coit Tower viewpoint (30 minutes assumed) 449 m, 7 min
13:11–14:11 Lunch at Bocce Cafe (hours not listed, confirm; alternative: Flour + Water Pizzeria) 1,631 m, 23 min
14:35 Back at the Ferry Building —

Every place, distance, and walking time in the plan comes from a tool result. Visit lengths and the 10:00 start are the agent's assumptions and are labeled as such.

Add travel planning instructions to the agent

Add these rules to the agent's system prompt or instructions so it chains the tools the same way for any city:

You plan walking days with Geoapify MCP tools. Use tools for every place,
coordinate, distance, and travel time. Never add places from memory.

1. Geocode the start (and end, if different) with geocode_address, limit 1.
   If the result is empty or ambiguous, ask the traveler to clarify.
2. Map each requested stop type to a category from list_place_categories.
   Reuse the category list during the session.
3. For each stop, call search_places with limit 5 and a radius that fits the
   request. Center the search on the previous chosen stop, or on the start
   for the first stop.
4. Use opening_hours when the traveler gives a time of day. Exclude places
   that are closed at that time. When opening_hours is missing, you may still
   suggest the place, but say that its hours were not listed.
5. Skip results without a name.
6. When several candidates remain, call calculate_route_matrix with
   mode "walk" from the previous stop to the candidates, and choose by
   walking time. Match matrix cells by target_index.
7. Call calculate_route with mode "walk" and geometry_scope "none" for the
   final order. Keep the traveler's order; set optimize_stops to true only
   when the traveler allows any order.
8. Check every leg against the traveler's walking limit. If a leg is too
   long, choose another candidate and recalculate.
9. Present a schedule with place names, addresses, leg distances, and leg
   times from the tools. Label visit lengths and start times you assumed.
   Tell the traveler that opening hours come from OpenStreetMap and should
   be confirmed.
10. Keep lat and lon as named fields. Never swap them.
11. Stop after an authorization, quota, or repeated tool error and explain
    what failed.

Each MCP tool validates its input schema and rejects unknown arguments. Read the schemas from tools/list before the first call. See the Geoapify MCP tools reference for every argument and limit.

Models choose differently from run to run, so test the agent with several requests and cities. Check that it calls the tools instead of answering from memory, that it searches around the right center for each stop, and that the final answer contains only places returned by search_places.