A phase-by-phase walkthrough of every log line from a real agent run — mapped to the exact component that produced it. Two MCP stdio servers, LangChain tools (flight boards + BigQuery catalog), one local LLM, zero cloud inference for the model.
Tools: BigQuery catalog + static hints tool + four flight-board tools. Results return as JSON and are appended to the chat for the next model call.
Resources: reference://demo-airport-hints is registered on the BigQuery server (resources/list, resources/read). The same copy is available as get_demo_airport_hints() with no arguments so small models are not forced to synthesize a URI (mcp-use maps resource reads to LangChain tools that validate uri).
Prompts: airport-summary and compare-airports are server-side workflow templates. main.py can call prompts/get after connect and pass the returned text to agent.run when AGENT_USE_MCP_PROMPTS=1.
Transport: agent ↔ MCP is stdio JSON-RPC; only the flight server calls AeroAPI over the public internet.
Diagram: solid green/cyan lines show tools/call reaching both subprocesses (mcp-use picks the session by tool); purple and orange dashed lines show resources/read and prompts/get on the bigquery session only.
Every session runs initialize, then tools/list, resources/list, and prompts/list. Your log may show “0 resources / 0 prompts” for an older build; current bigquery_server.py advertises one resource URI and two prompts on that server.
🔌 Found 0 existing sessions → creating new ones... Connecting to MCP implementation: uv ← flight_server.py Starting StdioConnectionManager task StdioConnectionManager connected successfully → initialize (16.826s) ← cold uv dep resolution + subprocess startup → tools/list (0.008s) ← 4 tools returned → resources/list, prompts/list ← flight: none in this demo MCP session initialized with 4 tools, 0 resources, 0 prompts --- same sequence for bigquery_server.py (cold init varies) --- → resources/list ← e.g. reference://demo-airport-hints → prompts/list ← airport-summary, compare-airports MCP session initialized with 3 tools, 1 resource, 2 prompts ✓ Created 2 new sessions — mcp-use then wraps MCP tools + resources + prompts for LangChain
MCPClient.from_dict() reads mcp_config.json and spawns each server as a local subprocess via uv run. Communication is over stdio pipes — not HTTP. This is the MCP “stdio transport” mode. The protocol then does an MCP handshake: initialize → tools/list → resources/list → prompts/list.
The 16–21 second init times are cold uv dependency resolution. Subsequent runs are much faster once uv’s cache is warm. All of this happens before you type any query.
// mcp_config.json — the file that drives all of this { "flight": { "command": "uv", "args": ["run", "--directory", "C:\...\aero-ref", "src/aero_ref/flight_server.py"] }, "bigquery": { "command": "uv", "args": ["run", "--directory", "C:\...\aero-ref", "src/aero_ref/bigquery_server.py"] } }
bigquery_server.py + flight_server.py):Loaded 4 new tools for connector (flight server): get_airport_flights ← full board: arrivals + departures + scheduled get_airport_arrivals ← recent landings, ordered by actual_on desc get_airport_departures ← recent takeoffs, ordered by actual_off desc get_airport_flight_counts ← summary counts only — fastest snapshot Loaded 3 new tools for connector (bigquery server): list_demo_airports ← catalog scan with limit get_demo_airport ← lookup by ICAO / IATA / LID get_demo_airport_hints ← tiny static ICAO hint string (no args) Plus from bigquery (same connector), as LangChain-callable wrappers: 1 resource tool ← read by URI (same payload as hints tool) 2 prompt tools ← airport-summary, compare-airports 🛠 Created LangChain tools from client: 7 tools, 1 resources, 2 prompts 🧰 Found N tools across all connectors 🧠 Agent ready (names include flight + bigquery + prompt wrappers)
mcp-use runs tools/list, resources/list, and prompts/list during init. It converts MCP tools into LangChain StructuredTools, and typically exposes resources and prompts as additional callable tools the model can invoke.
main.py can also call prompts/get directly on the bigquery session to build the user message before agent.run — that path does not rely on the LLM to pick a prompt tool.
The LLM still chooses among whatever names land in the system prompt; it matches by text, not by “understanding” MCP.
System: You are a helpful AI assistant. You have access to the following tools: - get_airport_flights: All recent and upcoming flights at an airport... - get_airport_arrivals: Flights that have recently arrived... - get_airport_departures: Flights that have recently departed... - get_airport_flight_counts: Summary counts by status... - list_demo_airports: List rows from the demo airports table... - get_demo_airport: Look up one airport by code... - get_demo_airport_hints: Static ICAO hints (no args)... ... plus resource-read + prompt wrappers from bigquery (names vary) You can call tools on two MCP servers: 1) bigquery — catalog 2) flight — live AeroAPI Typical flow: airport metadata from BigQuery when useful, live boards from flight tools. Say which source facts came from. Human: "At Houston Bush (KIAH), use our airport reference for the official name and city. Then summarize live activity: flight counts (departed, enroute, scheduled arrivals and departures), plus whether recent arrivals or departures look unusually delayed."
tool_calls: [{
"name": "list_demo_airports", ← LLM picked the broadest catalog tool first
"args": {}, ← no arguments needed, returns all airports
"type": "tool_call"
}]
content: "" ← empty. LLM returned ONLY a tool call. No text answer yet.
input_tokens: 1,219
output_tokens: 18 ← just the JSON tool call
total_tokens: 1,237
langchain-ollama (ChatOllama) sent this prompt to localhost:11434. The LLM read the tool descriptions in the system prompt (more than in the original six-tool log, once resources and prompts are wrapped) and matched “airport reference” against list_demo_airports. Pure text matching — no deeper reasoning.
The 357-second time is dominated by model cold load: load_duration: 33.4 seconds + prompt_eval_duration: 316 seconds to process 1,219 input tokens at CPU speed. Only 18 tokens generated.
LangGraph sees tool_calls is not empty → does not route to END → routes to the tools node. The loop continues.
list_demo_airports (returns all airports) instead of get_demo_airport (direct lookup by code). It already knew the ICAO code was KIAH from the user message. A smarter model would call get_demo_airport(airport_code="KIAH") directly and save a round trip.MCP tool "list_demo_airports" received input: {'limit': 50} → stdio:bigquery_server.py → tools/call (2.293s) ← stdio:bigquery_server.py ← tools/call Tool result (structured): { "table": "project-b4252fcd.aero_reference.airports", "count": 2, "rows": [ { "airport_code": "KIAH", "name": "Houston Bush Intctl", "city": "Houston", "state": "TX", "code_iata": "IAH", "elevation": 97, "timezone": "America/Chicago", ... }, { "airport_code": "KSFO", "name": "San Francisco Intl", "city": "San Francisco", "state": "CA", ... } ] }
LangGraph’s tools node passes the tool call to mcp-use, which sends a tools/call MCP message to bigquery_server.py over stdio. The server executes the BigQuery SQL and returns 2 rows — your demo catalog.
mcp-use wraps this result in a ToolMessage and appends it to the conversation state. LangGraph routes back to the model node for another LLM call. The prompt is now longer.
content: "To proceed, I need to find the official name and city for Houston Bush (KIAH). Let's use the get_demo_airport function with airport code 'KIAH'..." tool_calls: [{ "name": "get_demo_airport", "args": { "airport_code": "KIAH" } }] input_tokens: 1,537 output_tokens: 69 ← context grew by +318 tokens (the tool result)
list_demo_airports result. Calling get_demo_airport(KIAH) again returned identical data. This is a 3b model limitation — it didn’t synthesize the existing context and instead re-fetched. A larger model would recognise the data was already present.content: "The official name for Houston Bush (KIAH) is Houston Bush Intercity..." [fills placeholder text, no real answer yet] tool_calls: [{ "name": "get_airport_flight_counts", "args": { "airport_id": "KIAH" } }] input_tokens: 1,765 output_tokens: 170 ← context now at 1,765 tokens
The LLM is making forward progress but inefficiently. By Call #3 it finally reasons: “I have the catalog data, now I need the live counts.” It correctly picks get_airport_flight_counts — the most efficient tool for summary counts, no pagination, ICAO-preferred.
Note it also wrote some content alongside the tool call (the placeholder text). LangGraph sees tool_calls is still non-empty and routes to the tools node ignoring the partial text.
MCP tool "get_airport_flight_counts" received input: {'airport_id': 'KIAH'} → stdio:flight_server.py → tools/call (1.808s) ← stdio:flight_server.py ← tools/call Tool result: { "airport_id": "KIAH", "departed": 91, "enroute": 254, "scheduled_arrivals": 1521, "scheduled_departures":1547 }
flight_server.py wraps the AeroAPI (FlightAware) GET /airports/KIAH/flights/counts endpoint. The 1.8-second response time is the real HTTP call to FlightAware’s servers. This is the only actual external network call in the entire agent run — everything else is local.
The result is compact (4 numbers) because this is the counts endpoint. If you’d asked for delay analysis, the agent would need to call get_airport_arrivals or get_airport_departures to inspect individual flight timings.
content: "The summary of live activity at Houston Bush (KIAH) is as follows: Flight Counts: Departed: 91 flights that have already departed. Enroute: 254 flights currently in transit. Scheduled Arrivals: 1,521 flights scheduled to arrive soon. Scheduled Departures: 1,547 flights scheduled to depart soon. Recent Activity: Recent arrivals and departures do not show any delays based on the live data. However, this information is up-to-date as of now..." tool_calls: [] ← EMPTY. LLM decided it is done. input_tokens: 1,960 output_tokens: 152 total_tokens: 2,112
tool_calls=[] → conditional edge routes to END → loop exits. Final answer returned. The agent used 4 model calls and 3 tool calls to answer one question.The final answer is reasonable but incomplete — it says “no delays based on live data” but the agent never actually called get_airport_arrivals or get_airport_departures to check individual flight times. It only had summary counts. This is a hallucinated conclusion from a 3b model that treated counts as delay evidence.
A well-prompted agent or a larger model would have called the arrivals/departures endpoints to compute actual delay statistics before drawing that conclusion.
LangGraph chain start │ ├─ ModelCallLimitMiddleware.before_model ← checks: under 24 step limit? yes. │ ├─ model node [LLM Call #1] ← ChatOllama → qwen2.5:3b (357s cold) │ output: tool_calls=[list_demo_airports(limit=50)] │ ├─ ModelCallLimitMiddleware.after_model ← call_count=1 │ ├─ tools node ← mcp-use → bigquery_server.py (2.3s) │ output: ToolMessage [KIAH: Houston Bush Intctl, KSFO: San Francisco Intl] │ ├─ model node [LLM Call #2] ← context: 1,537 tokens (153s) │ output: tool_calls=[get_demo_airport(KIAH)] ← redundant re-fetch │ ├─ tools node ← bigquery_server.py again (1.2s) │ output: ToolMessage [KIAH confirmed: Houston Bush Intctl] │ ├─ model node [LLM Call #3] ← context: 1,765 tokens (176s) │ output: partial text + tool_calls=[get_airport_flight_counts(KIAH)] │ ├─ tools node ← flight_server.py → AeroAPI (1.8s) │ output: ToolMessage {departed:91, enroute:254, arr:1521, dep:1547} │ ├─ model node [LLM Call #4] ← context: 1,960 tokens (123s) │ output: final text answer, tool_calls=[] │ └─ ModelCallLimitMiddleware.after_model ← call_count=4 (of 24 max) END ← tool_calls=[] → conditional edge routes to END Total wall time: 816 seconds LLM calls: 4 Tool calls: 3 (2x BigQuery + 1x AeroAPI) Tokens used: 2,112 (1,960 in + 152 out)
A token is a chunk of text — roughly 0.75 words on average. The LLM does not see letters or words, it sees token IDs (numbers). Every prompt you send and every response you get is measured in tokens.
"At Houston Bush (KIAH), use our airport reference..." At | Houston | Bush | (| KI | AH | ), | use | our | airport | reference... tok1 tok2 tok3 tok4 tok5 tok6 tok7 tok8 tok9 tok10 tok11... Rule of thumb: 100 tokens ≈ 75 words ≈ half a page 1000 tokens ≈ 750 words ≈ a short article Your full Turn 1 used 2,112 tokens ≈ 1,580 words
MODEL CONTEXT WINDOW ROUGHLY EQUIVALENT TO ———————————————————————————————————————————————————— qwen2.5:3b (yours) 32,768 tokens ~25,000 words / ~50 pages Llama3.2:9b 128,000 tokens ~96,000 words / ~200 pages Claude Sonnet 4 200,000 tokens ~150,000 words / ~300 pages Turn 1 used 2,112 tokens — only 6.4% of qwen2.5:3b's 32K limit. Turn 2 accumulated to ~2,813 tokens. With memory_enabled=False (your setting), each new agent.run() starts fresh.
One step = one model call. ModelCallLimitMiddleware increments a counter before and after every LLM call. When the counter hits max_steps, a StopIteration is raised and the loop is forced to exit — whether or not the LLM is finished.
Your aero-ref used max_steps=24 (seen in log: "Created agent with max_steps=24") Turn 1 used: 4 model calls of 24 — well within the limit. Step 1: LLM Call #1 → list_demo_airports tool → result Step 2: LLM Call #2 → get_demo_airport tool → result Step 3: LLM Call #3 → get_airport_flight_counts → result Step 4: LLM Call #4 → final answer, tool_calls=[] → DONE
1. Tool keeps failing → LLM keeps retrying get_airport_flight_counts → API timeout → retry → timeout → retry... 2. LLM gets confused in a loop calls list_demo_airports → gets result → calls list_demo_airports again! You saw a mild version of this: LLM called get_demo_airport after list_demo_airports had already returned the same data. 3. Task genuinely needs more steps than you allowed PA agent processing 5 requests with max_steps=10 → runs out before finishing all requests 4. LLM over-plans, breaks simple tasks into too many tool calls 3b models are especially prone to this with complex multi-part questions
max_steps=15. A full PA workflow (eligibility check + guidelines lookup + PA DB update + notification) needs ~7–9 steps. 15 gives headroom without letting a stuck agent burn tokens forever.Human: "Compare Houston Hobby (KHOU) and Bush (KIAH): official names and cities
from our catalog, then contrast live flight counts..."
Step 1: get_demo_airport(airport_code="KHOU") Result: {"count": 0, "rows": []} ← KHOU not in BigQuery demo table LLM response: "It appears there is no entry in the demo airport catalog for 'KHOU'. Let's proceed with fetching information for 'KIAH' and compare it to a real airport code, such as 'DFW' (Dallas/Fort Worth)..." Step 2: get_demo_airport(airport_code="KIAH") ← fetched AGAIN (already in context!) Result: KIAH data (identical to Turn 1) Step 3: LLM writes partial answer about KIAH and DFW, tool_calls=[] ← NEVER fetched KHOU live data, NEVER fetched DFW data, just made up a comparison without any evidence.
flight_booking_demo.user_travel.
qwen2.5:9b or llama3.2:9b would improve this significantly. Better catalog coverage is the other fix.| Component | Role | What it did in this run |
|---|---|---|
| MCPClient.from_dict() | Config reader + session manager | Read mcp_config.json, spawned 2 server subprocesses via stdio, ran MCP handshake on each |
| flight_server.py (FastMCP) | Flight MCP server | Exposed 4 tools wrapping AeroAPI endpoints. Handled 1 real HTTP call to FlightAware (→ 1.8s) |
| bigquery_server.py (FastMCP) | Catalog MCP server | Exposed 2 tools querying BigQuery aero_reference.airports. Handled 3 SQL queries across both turns |
| mcp-use MCPAgent | MCP-to-LangChain bridge + loop | Auto-wrapped 6 MCP tool schemas as LangChain StructuredTools; executed all tool calls via stdio; enforced max_steps=24 |
| LangGraph (inside mcp-use) | Orchestration loop + routing | Ran model→tools→model loop with conditional edges. Exited when tool_calls=[]. Ran ModelCallLimitMiddleware between every node. |
| ChatOllama (langchain-ollama) | LLM bridge | Translated LangChain message format to Ollama REST API at localhost:11434; streamed back AIMessages |
| qwen2.5:3b (Ollama) | LLM / reasoning engine | Made 4 LLM calls in Turn 1. Read tool schemas from system prompt, chose tools by text matching, wrote final summary. Showed 3b limitations: redundant tool calls, hallucinated delay conclusion, off-rails pivot in Turn 2. |
| BigQuery (GCP) | Airport reference catalog | Returned 2-row demo dataset (KIAH + KSFO). KHOU lookup returned 0 rows — catalog gap exposed in Turn 2. |
| AeroAPI (FlightAware) | Live flight data | Returned real-time counts for KIAH: 91 departed, 254 enroute, 1521 scheduled arrivals, 1547 scheduled departures. |