Tool Calling and MCP: How Agents Touch the World
Tools give a text model hands. MCP makes every tool speak the same language.
Tools are functions an agent can ask its runtime to run. The model emits a tool name and arguments, the runtime runs the function outside the model, and the result comes back as an observation. MCP (Model Context Protocol) standardizes how applications discover and call tools that live in separate servers, so one server works with any application that speaks MCP.
The part of a tool that decides whether it gets used well is not its code. It is the plain-language description the model 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 picks it at the right moment far more often.
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 acts on 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, then emits a structured request for a specific tool with specific inputs. The runtime executes that tool, and the result, an observation, goes 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 plain-language description, and an input schema, usually written in JSON (JavaScript Object Notation) Schema. The description is what the model reads. It tells the model when to use the tool, what it does, what inputs it needs, and what it returns. A good description also says when not to use it.
Here is a bad description for Mailmind's get_email_info:
Here is a good one:
The difference is large. Anthropic's guide to defining tools calls detailed descriptions "by far the most important factor in tool performance." A model that reads the good description calls the right tool at the right time far more often. A model that reads the bad one has to guess.
Here is the whole tool as the model receives it, in the shape Anthropic's Messages API uses (MCP calls the schema field inputSchema; other providers use their own names for the same three parts):
When the model wants it, its reply contains a tool call block, and the runtime answers with a tool result that quotes the same ID:
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 so much?
The description is the model's only window into what the tool means. The model cannot read the implementation or learn from side effects. It reasons entirely from the prompt and the observations so far. If the description is vague, the model will invent uses for the tool. If it says "gets email", the model might try it to search, to delete, or to fetch a whole thread, and each of those fails in a confusing way.
The description is also the cheapest place to prevent misuse. You can state rules like "never use this for bulk operations" or "this tool is slow, prefer cached results." Models usually follow rules stated clearly in a description, but usually is not a guarantee. Anything that must never happen, such as a bulk delete or an email sent without approval, has to be enforced in the tool's code or the runtime: a permission check, a rate limit, an approval step. The MCP specification makes the same point for tools in general: applications should keep a human in the loop who can deny a tool call. Guardrails: Input, Process, Output shows how to build those checks later in this series.
The input schema is the second piece. It defines the shape of the arguments. But the schema alone does not tell the model when to use the tool. The description does. That is why the description deserves as much care as the code.
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 server error. The input can be malformed. The agent must handle these failures gracefully, or a single passing error can push the whole run off course.
The runtime sorts 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 model is told the error and asked to reason about another approach. Partial results (a search that returns some items plus a warning) are wrapped in an observation that flags what is missing, so the model can decide whether to go ahead or try another strategy.
A dead external service trips a circuit breaker. After a set number of consecutive failures, the circuit opens and the agent stops calling that tool. It is told the tool is unavailable and given a fallback if one exists. In Mailmind, if the send_confirmation tool keeps failing, the agent might add 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 retries blindly. It always gets a reason, and it uses that reason to decide what to do next.
How do you keep tool output from overwhelming the context window?
Every token a tool returns lands in the conversation, and the runtime sends it again on every later call. A search that returns 200 full emails costs money and time on every remaining step, dilutes the model's attention, and if it pushes the request past the context window, the call fails outright.
The first line of defence is to design tools that return only what the agent needs. Mailmind's search_inbox does not return full email bodies. It returns a list of IDs plus a one-line summary per result, and the agent calls get_email_info only for the messages it actually wants to read. Anthropic's guide Writing effective tools for agents (September 2025) suggests the same family of moves: some combination of pagination, range selection, filtering and truncation, with sensible defaults, for any tool response that could use up a lot of context.
- 200 emails, roughly 100,000 tokens
- Every later call re-sends them
- Relevant details buried
- Only ID, subject, sender, date
- 200 emails, roughly 6,000 tokens
- Agent opens only the emails it needs
The second line is pagination. The tool returns a page of results and a cursor, and the agent can page further if it needs to. The description nudges it to stop early: "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. When a tool returns a large result, a post-processing step can summarize it before it enters the agent's context. This is risky, because the summary might drop the one detail that mattered. Use it only when the output is genuinely too large and the agent does not need every word.
The fourth line is caching, used carefully. Cache only read-only tools, and only for the length of one run, so that asking for the same email twice does not fetch it twice. Never cache a tool that changes anything: a cached send_confirmation would skip a real send. Be wary with reads whose answer changes quickly, too.
What happens when a tool call fails across multiple systems?
This is the problem many 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.
Rolling back across separate systems is hard and often fails. The archive wrote to the mail server and the task update wrote to the task database; undoing them takes two more operations, which can fail too. There are two common patterns. Compensating actions, known as the saga pattern, undo completed steps with new operations when a later step fails. The outbox pattern avoids the half-done state in the first place.
With an outbox, the agent does not call the three systems one by one. It writes all three intended actions to a local outbox table in a single transaction: "archive thread T", "add task X", "send confirmation to user Y with content Z". That write either happens completely or not at all. The agent then tells the user the actions are queued, and the user sees each one's status update as it completes. A background worker reads the outbox and carries out each action against the real system, retrying each one until it succeeds. If an action still fails after many retries, it moves to a dead-letter queue, an alert fires, and a human can step in. The transactional outbox is a well-known pattern in distributed systems; the agent is just another client.
One catch matters. The worker may carry out an action more than once, for example if it crashes after sending but before marking the row done. So every action needs an idempotency key, a unique ID that lets the receiving side recognise a repeat and ignore it, so a retry can never send the confirmation twice.
The outbox turns the multi-system operation into one local write plus work that finishes later. The agent's context never sees the passing failures, and the user sees honest status instead of a false "done".
How does MCP standardize tool integration?
Before MCP, every agent integration was custom glue code. You wrote a function to call the Gmail API, wrapped it in your agent framework's format, and did the same again for the calendar, for Notion, for Slack. Each tool had its own discovery, its own error handling, its own authentication. The engineering cost was high and the result was brittle.
MCP is an open standard that fixes this, often compared to a USB-C port: one plug instead of a drawer of adapters. You may have met it in MCP in Plain English: AI's USB-C Moment; here is the part an agent builder needs, and the MCP in Practice series covers the whole protocol. MCP has three roles, as its architecture describes. The host is the AI application, such as a chat app or an IDE (integrated development environment). The host runs one MCP client for each server it connects to. A server exposes capabilities of three kinds, which the spec's server features overview sorts by who is in charge: tools are model-controlled (functions the model can choose to call), resources are application-controlled (data the app attaches, such as files), and prompts are user-controlled (templates the user picks, such as slash commands). The model never talks to a server directly. The host gives the model the combined tool list from all its servers, and when the model asks for a tool, the host routes the call through the matching client.
MCP defines two standard transports. stdio is for local servers: the client launches the server as a child process and they exchange newline-delimited messages over its standard input and output. Streamable HTTP is for remote servers: each message is an HTTP POST to one endpoint, and the reply comes back as a JSON object or as a stream of Server-Sent Events for that request. The older HTTP+SSE transport, which Streamable HTTP replaced in the 2025-03-26 revision, is deprecated. Other channels, such as WebSockets, are possible only as custom transports, not as a standard.
In Mailmind, you would run an MCP server for the mail provider, one for the task manager and one for the calendar. The host connects to all three, builds one tool list, and the model sees a single set of tools with no custom code per integration.
How does MCP actually work under the hood?
Messages are JSON-RPC 2.0: small JSON objects with a method name, parameters, and an ID that ties each response to its request. Under the current revision of the spec, dated 2026-07-28 and the latest as of October 2026, MCP is stateless. There is no opening handshake. Every request carries the protocol version and the client's capabilities in a _meta field, and every server must answer server/discover, which a client may call first to learn the server's supported versions and capabilities. The revisions from 2024-11-05 to 2025-11-25 opened each connection with an initialize handshake instead, so you will still meet it in older servers and tutorials. The changelog lists what changed.
Two methods carry the tool work, as the spec's page on tools defines them. tools/list returns each tool's name, description and inputSchema, plus optional extras such as an outputSchema and annotations that hint at how the tool behaves. tools/call runs one tool. Here is Mailmind's client asking the mail server to search:
And the server's answer:
A result can also carry structuredContent, machine-readable data that matches the tool's outputSchema. If the tool itself fails, the server still returns a result, with isError: true and an explanation in content, so the model can read the error and try something else. Problems with the request itself, such as an unknown tool name, come back as JSON-RPC protocol errors instead.
The protocol is deliberately small. The real complexity lives in the servers. One warning before you plug servers in: treat what a server returns as untrusted data. The spec tells clients to treat tool annotations as untrusted unless they come from a trusted server, and a tool's output can carry instructions planted by whoever wrote the email or web page it read. MCP Security: The Attack Surface You Just Installed covers those risks.
Quick Reference
| Property | Value |
|---|---|
| Tool anatomy | name, description, input schema |
| Critical tool metadata | description (guides the model's choice) |
| Must-never rules | enforced in code or the runtime, not only in the description |
| Transient failure handling | exponential backoff, retry |
| Permanent failure handling | do not retry; the model reasons about an alternative |
| Circuit breaker | opens after N consecutive failures; fallback used |
| Token optimization | paginate, filter, return only needed fields, summarize with care |
| Caching | read-only tools only, within one run |
| Outbox pattern | one local write of intended actions; a worker executes and retries, with idempotency keys |
| Dead-letter queue | holds actions that fail after all retries; triggers an alert |
| MCP roles | host (the AI app), one client per server, servers |
| MCP server primitives | tools (model-controlled), resources (application-controlled), prompts (user-controlled) |
| MCP transports | stdio (local) and Streamable HTTP (remote); HTTP+SSE deprecated |
| MCP wire format | JSON-RPC 2.0, stateless since the 2026-07-28 revision (no initialize handshake) |
| MCP request flow | optional server/discover, then tools/list, then tools/call, which returns content and isError |
| MCP authorization | optional; for HTTP servers that use it, based on OAuth 2.1; stdio servers read credentials from their environment |
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 defines how the model asks for a tool and how you hand back the result. You still write, host and wire up the code behind every tool, separately for every application. MCP standardizes that layer: a tool server written once works with any application that speaks MCP, and the application discovers its tools at run time. The model's API is the last step. MCP handles the connection to the tools.
Q: How do I test a tool that interacts with third-party APIs?
Test the tool's integration with the external system separately. Then 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 arguments. 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, you do not need it. The moment it writes to two or more independent systems, you need a plan for the half-finished case, and an outbox or compensating actions is the usual answer. A lost confirmation hardly matters in a demo. In a production system that people rely on, it does.
Q: How does MCP handle authentication?
Authorization is optional in MCP: a server may require none. When a remote server does protect its tools, the spec's authorization section defines how, for HTTP transports, based on OAuth 2.1. The MCP server acts as an OAuth resource server. A request without a valid token gets an HTTP 401 response that points the client to the right authorization server. The client gets an access token there (the authorization server may ask the user to sign in and approve) and retries with the token. Servers that run locally over stdio should not use this flow; they take credentials from their environment, such as environment variables. The 2026-07-28 revision also tightens the flow, for example by having clients check the authorization server's iss value, and it deprecates Dynamic Client Registration in favour of Client ID Metadata Documents.
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, and the agent sees only the final result. Loops where a tool triggers another agent call are possible but dangerous. The system must enforce a maximum call depth and a token budget, or 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 acceptance with delivery. A 2xx response from the email provider means the request was accepted, not that the email arrived. The provider might queue the message and later drop it silently, so the tool's success signal comes too early. The fix is to capture the provider's message ID and confirm delivery afterwards. The send_confirmation tool writes an outbox record with the message ID and an idempotency key. A background process checks the delivery status through the provider's events API. If delivery is not confirmed within, say, 30 minutes, the record moves to a dead-letter queue and an alert fires. The agent can then try another channel or tell the user. Meanwhile the user sees the confirmation as queued, not as done. The key is to treat the action as in flight until delivery is confirmed, not when the API says "OK."
Next: Prompt Engineering for Agents, Production Grade. It covers how to write the system prompt that tells an agent what to do with its tools: roles, steps, output formats and constraints.
If you want this kind of breakdown every week, how real systems actually work under the hood, subscribe to Internals Decoded at internalsdecoded.com.
Sources
- Define tools
- Writing effective tools for agents
- Pattern: Saga
- Pattern: Transactional outbox
- MCP specification (2026-07-28): Key Changes
- MCP specification: Architecture
- MCP specification: Server features
- MCP specification: Transports
- MCP specification: Discovery
- MCP specification: Tools
- MCP specification: Authorization