Tool Calling and MCP: How Agents Touch the World
Tools give a text model hands. MCP makes every tool speak the same language.
An LLM (large language model) agent sends structured function calls to external services. Those calls are tools. The agent emits a name and a set of parameters. The tool runs outside the LLM and returns a text result as an observation. MCP (Model Context Protocol) standardizes this. It lets any agent use any tool server without custom integration code.
The most important part of a tool is not its code. It is the natural-language description the agent reads to decide when and how to use it. Get that description wrong, and the tool is dead weight. Get it right, and the agent will use it at exactly the right moment.
In Part 3 you saw how memory gives an agent a persistent sense of the past. Now we turn to the present: how an agent manipulates the world outside its own context window. We will use Mailmind, the inbox-based AI assistant, as the running example throughout.
How does tool calling work inside an LLM agent?
The agent’s reasoning loop cycles through thought, action, and observation. An action is a tool call. The agent thinks about what it needs. It then emits a structured request for a specific tool with specific inputs. The system executes that tool. The result, an observation, is fed back into the agent’s context for the next thought.
This is the same ReAct loop you know from Part 1. The new part here is the tool itself. Every tool has three pieces: a name, a natural-language description, and an input schema (usually JSON (JavaScript Object Notation) Schema). The description is what the agent reads. It tells the model when to use the tool, what the tool does, what inputs it needs, and what it returns. A good description also says when not to use the tool.
Here is a bad description for Mailmind’s get_email_info:
Here is a good one:
The difference is enormous. The agent that reads the good description will call the right tool at the right time. The agent that reads the bad one will guess and often get it wrong.
The flow in Mailmind looks like this:
The agent never sees the real email server. It only sees the schema and the text that comes back.
Why does the tool description matter more than the code?
The description is the agent’s only interface to the tool’s semantics. The model has no access to the actual implementation. It cannot inspect the code. It cannot learn from side effects. It reasons entirely from the prompt and the history of observations. If the description is vague, the agent will invent uses for the tool that do not exist. If the description says “gets email,” the agent might try to use it to search, to delete, or to fetch a thread. All of those will fail in confusing ways.
The description is also the highest-leverage place to prevent misuse. You can encode rules like “never use this for bulk operations” or “this tool is slow, prefer cached results.” The agent will follow those rules when they are stated clearly in the prompt. This is not a matter of hoping the model behaves. It is a matter of giving it the right instruction.
The input schema is the second piece. It defines the shape of the parameters. But the schema alone does not tell the agent when to use the tool. The description does. That is why senior engineers spend more time on the description than on the function body.
How do agents handle tool failures without derailing a conversation?
A tool call can fail for many reasons. The network can drop. The API (application programming interface) can return a 500. The input can be malformed. The agent must handle these failures gracefully. Otherwise a single transient error can poison the entire context window.
The system classifies failures into three buckets. Transient failures (timeouts, rate limits, temporary server errors) are retried with exponential backoff. Permanent failures (not found, invalid parameters, authentication errors) are not retried. The agent is told the error and asked to reason about an alternative approach. Partial results (a search that returns some items but also a warning) are wrapped in a structured observation that flags the incompleteness. The agent can then decide whether to proceed with what it has or to try a different strategy.
A dead external service triggers a circuit breaker. After a configurable number of consecutive failures, the circuit opens. The agent stops calling that tool. It is informed that the tool is unavailable and given a fallback if one exists. In Mailmind, if the send_confirmation tool fails persistently, the agent might fall back to appending a note to the user’s dashboard instead of sending an email. The user sees a warning, not a silent failure.
The key is that the agent never blindly retries. It always gets a reason. It uses that reason to decide what to do next.
How do you keep tool output from overwhelming the context window?
Every token that comes back from a tool eats into the limited attention budget of the model. A search that returns 200 email summaries can blow the context window instantly. The agent then forgets the earlier steps of the conversation. That is a catastrophic failure.
The first line of defense is to design tools that return only what the agent needs. The search_inbox tool in Mailmind does not return full email bodies. It returns a list of IDs plus a short snippet per result. The agent can then call get_email_info only for the messages it actually wants to read.
- Full email bodies returned
- 200 emails = 25,000 tokens
- Agent hits context limit quickly
- Relevant details buried
- Only ID, subject, sender, date
- 200 emails = 800 tokens
- Plenty of room for reasoning
- Agent sees only what it needs
The second line is pagination. The tool returns a page of results and a cursor. The agent can page through results if it needs to. But it is encouraged to stop early. The description says: “Returns up to 10 results. If you need more, use the cursor, but consider whether you really need all of them.”
The third line is summarization. After a tool returns a large result, a post-processing step can summarize the output before it enters the agent’s context. This is risky. The summary might drop important details. Use it only when the output is genuinely too large and the agent does not need every word.
The fourth line is caching. Within a single agent run, the same tool call with the same parameters should return the same cached result. This avoids duplicate work and saves tokens. The cache is scoped to the run. It is cleared when the conversation ends.
What happens when a tool call fails across multiple systems?
This is the problem most engineers overlook. Mailmind is supposed to archive a thread, update the user’s task list, and send a confirmation. The first two calls succeed. The third fails silently. The user sees a task list update but no confirmation. The system is now in an inconsistent state.
Never roll back completed updates in separate systems. The archive operation wrote to the email server. The task update wrote to the task database. Both are external. A rollback is a second operation that can also fail. You quickly create a pile of compensating transactions that themselves need retries. The system becomes a mess.
Instead, use the outbox pattern. The agent writes the intended actions to a local outbox table. Each action is a row: “send confirmation email to user X with content Y.” The agent’s tool call to the outbox is synchronous and fast. The agent then reports success to the user. A background worker reads the outbox and executes the actions against the real external systems. It retries each action independently until it succeeds. If an action fails after many retries, it moves to a dead-letter queue. An alert fires. A human can intervene.
The outbox pattern turns the agent’s multi-system operation into a single atomic write. The rest is eventual consistency. The agent’s context never sees the transient failures. The user eventually gets the confirmation. The system heals itself.
This is the same pattern used by every reliable distributed system. The agent is just another client.
How does MCP standardize tool integration?
Before MCP, every agent integration was a custom piece of glue code. You wrote a Python function to call the Gmail API. You wrapped it in a specific format for your agent framework. You did the same for Calendar, for Notion, for Slack. Each tool had its own discovery mechanism, its own error handling, its own authentication. The engineering cost was high. The result was brittle.
MCP (Model Context Protocol) is the USB-C for AI tools. It defines a standard way for an agent to discover and call tools. The agent is the client. It connects to one or more MCP servers. Each server exposes a set of tools. The agent asks the server for a list of available tools. The server responds with the name, description, and input schema for each tool. The agent then calls a tool by sending a tools/call request with the tool name and parameters. The server executes the tool and returns the result.
The protocol is transport-agnostic. It can run over stdio for local tools, over HTTP with Server-Sent Events for remote tools, or over WebSockets. The same server can be used by any MCP client. The same client can use any MCP server. The integration work is done once, per external system, by the server author. The agent developer simply points the client at the server.
In Mailmind, you would have an MCP server for Gmail, one for the task manager, and one for the calendar. The agent connects to all three. The tool list is built automatically. The agent sees a unified set of tools without any custom code.
How does MCP actually work under the hood?
The client initiates a connection to the server. The two negotiate a protocol version. The server sends a capabilities response. It lists the tools it supports, as well as any resources (structured data) and prompts (templated messages) it can provide. The client can then call tools/list to get the full tool metadata. The server returns a JSON array of tool definitions. The client uses this to build the agent’s prompt.
When the agent emits a tool call, the client sends a tools/call request. The request includes the tool name and a JSON object of arguments. The server validates the arguments against the schema. If they are valid, the server executes the tool. It returns the result as a JSON response with a content field. The client wraps that in an observation for the agent.
The flow looks like this:
The protocol is deliberately simple. The real complexity lives in the server implementations. The client is a thin layer that translates between the agent’s internal representation and the MCP wire format. This separation means that improvements to the server (better error handling, authentication, retries) are invisible to the agent. The agent sees a stable tool interface.
Quick Reference
| Property | Value |
|---|---|
| Tool anatomy | name, description, input schema |
| Critical tool metadata | description (guides agent’s decision) |
| Transient failure handling | exponential backoff, retry |
| Permanent failure handling | do not retry; agent reasons about alternative |
| Circuit breaker | opens after N consecutive failures; fallback used |
| Token optimization | paginate, return only needed fields, summarize, cache |
| Outbox pattern | atomic write of intended actions; background worker executes and retries |
| Dead-letter queue | destination for actions that fail after all retries; triggers alert |
| MCP transport options | stdio, HTTP with SSE, WebSocket |
| MCP request flow | client connects → server advertises tools → client calls tools/call → server returns result |
Frequently Asked Questions
Q: Can’t I just use the LLM’s built-in function calling API? Why do I need MCP?
Built-in function calling APIs define the shape of the call but not the discovery or transport. You still need to write the code that connects the call to the real system. MCP standardizes that connection. It gives you a reusable server that any agent can consume. The LLM’s API is the last mile. MCP handles the rest.
Q: How do I test a tool that interacts with third-party APIs?
You test the tool’s integration with the external system separately. Then you test the agent’s use of the tool with a mock server that returns controlled responses. The agent’s reasoning is tested by giving it scenarios and checking that it calls the right tool with the right parameters. The tool’s correctness is tested by pointing it at a sandbox API.
Q: Isn’t the outbox pattern overkill for a simple agent?
If the agent touches only one system, the outbox is unnecessary. The moment it writes to two or more independent systems, the outbox is the only safe pattern. The cost of a lost confirmation is low for a demo. For a production system that users rely on, it is a hard requirement.
Q: How does MCP handle authentication?
MCP does not define an authentication standard. The server and client negotiate authentication out of band. Typical setups use environment variables, OAuth tokens, or a sidecar proxy. The protocol itself is unauthenticated. The security boundary is the transport layer and the server’s access controls.
Q: Can a tool call another tool? What about recursive loops?
A tool can call another tool internally. That is just function composition inside the server. The agent sees only the final result. Recursive loops where the agent calls a tool that triggers another agent call are possible but dangerous. The system must enforce a maximum call depth and a token budget. Without those, the agent can spin forever.
Test yourself
Mailmind’s send_confirmation tool always returns a success status. But users report that confirmations are sometimes missing. The tool code calls the third-party email API and checks the HTTP status code. It treats any 2xx response as success. What is the likely root cause? How would you fix it?
Answer: The tool confuses API acceptance with actual delivery. A 2xx response from the email provider means the request was accepted, not that the email was delivered. The provider might queue the message and later drop it silently. The tool’s success signal is too early. The fix is to capture the provider’s message ID and verify delivery asynchronously. The send_confirmation tool writes an outbox record with the message ID. A background process polls the delivery status using the provider’s events API. If delivery is not confirmed within 30 minutes, the record moves to a dead-letter queue and an alert fires. The agent can then fall back to a different channel or inform the user. The key is to treat the action as in-flight until delivery is confirmed, not when the API says “OK.”
If you want to design agents that actually do things reliably, not just talk about them, subscribe to Internals Decoded at internalsdecoded.com. Next up: how agents plan sequences of tool calls to achieve complex, multi-step goals.