MCP tool calling under the hood

When you ask an agent to create a Jira ticket, and a few seconds later the ticket exists, we usually say that the model called a tool. That's useful shorthand, and I use it too, but it can make the process sound more direct than it really is. The model almost certainly didn't interact with Jira's services directly. What it most likely did was generate a structured request asking the harness, which is the application wrapped around it, to do those things on its behalf. I want to walk through what actually happens during a tool call, using a simple example prompt:

Create a Jira ticket for the login bug we just discussed.

We'll get into this, but FYI there's a video version of this article here if you prefer that medium.

Before the model is ever invoked, the application has to build a request for it. That request bundles together everything the model needs in order to act sensibly: the user's message, whatever conversation history the model needs to understand what "the login bug" refers to, any relevant system instructions, and descriptions of the tools that are available to it. A Jira tool might be described with a name like create_jira_ticket, a short explanation of what it does, and a schema for its arguments, maybe a title, a description, and a project key. There usually isn't a separate search step here. The conversation and the available tool definitions travel together in the same inference request, and the model chooses among the tools it's been given based on their names, descriptions, and schemas. Those definitions might be cached by the provider to reduce cost or latency, but logically they're still part of what the model sees when it decides what to do next.

Here's what that tool definition might actually look like on the wire:

{
"name": "create_jira_ticket",
"description": "Create a new Jira issue",
"parameters": {
"type": "object",
"properties": {
"title": { "type": "string" },
"description": { "type": "string" },
"project": { "type": "string" }
},
"required": ["title", "project"]
}
}

Notice everything that's missing from it: an API client, credentials, retry logic, any code that actually creates a ticket. The implementation is abstracted away from the model. It only needs to know that this capability exists and what a valid request for it looks like, which is a lot like a function signature with no body behind it.

declare function create_jira_ticket(args: {
title: string;
description: string;
project: string;
}): { key: string; status: string };

That's the right frame for the whole exchange. It's like a function or RPC call. The model is invoked with the conversation, the instructions, and that signature, and during inference it decides that the Jira tool is appropriate and produces a tool-use response. Conceptually, that response might look something like this:

{
"name": "create_jira_ticket",
"arguments": {
"title": "Login fails after session expiration",
"description": "Users are redirected to an error page after their session expires.",
"project": "WEB"
}
}

Depending on the model API, the arguments may be constrained to the tool's schema while they're being generated, which means the model can be prevented from omitting required arguments, using the wrong types, or producing malformed JSON. But that only tells us the request has the correct shape. Knowing WEB is a real Jira project, and that the current user has permission to create a ticket there, and whether the application should allow the operation at all are all out of scope for the model. Those decisions still belong to the application.

At this point, the model has not created anything. It has produced a structured request indicating which action it wants the application to take, and it's the harness that receives that request and decides whether to execute it. It might check the user's permissions. It might apply an organizational policy. It might require human confirmation before proceeding. Or it might reject the request entirely. This distinction matters more than it might seem to on first read, because a model being able to request an action is not the same thing as a model having the authority to perform it.

If the request is allowed, the harness invokes the actual tool implementation, and this part is mostly ordinary software. Some code authenticates with Jira. It constructs an API request. It handles network failures, rate limits, retries, logging, and whatever else the integration requires, none of which the model has any visibility into.

def create_jira_ticket(title, description, project):
token = jira_auth.get_token()
resp = http.post(f"{JIRA_URL}/issue",
headers={"Authorization": token},
json={"title": title, "description": description, "project": project})
return with_retries(resp)

The model generally doesn't know or care whether the tool is implemented as a local function, an HTTP call, a shell command, an MCP server, or something else entirely. All of those are the same opaque box from where the model sits.

Once the ticket has been created, Jira returns a result, maybe something like this:

{
"key": "WEB-1427",
"status": "created"
}

That result goes back to the harness, but the interaction still has a few steps ahead of it. The tool usually doesn't formulate the final response to the user. It just returns data. The harness puts that result back into the model's context and invokes the model again, and only now can the model see that the request succeeded and that the new ticket is WEB-1427. It uses that information to produce the response the user actually sees, something like "Done. I created WEB-1427 for the login issue." So what looked like one interaction to the user was actually at least two model invocations with real application code running in between. The first inference selected a tool and generated its arguments. The application executed the tool. The second inference interpreted the result and generated a response.

None of this is limited to a single round trip, either. The loop can continue for as long as it's useful. The model might search Jira for duplicate tickets first, then look up the correct project, then create the ticket, and each of those can be its own tool request followed by another inference.

This is also where MCP enters the conversation, though it doesn't fundamentally change this loop. It's "just" a standard for how the application discovers tools, reads their descriptions, invokes them, and receives their results.

The model still chooses a capability during inference. The harness still mediates the request. Some external implementation still performs the operation. And the result still has to come back into the model's context before the model can do anything with it.

So when we say that an AI called a tool, what usually happened is a little more specific than that phrase suggests. The harness told the model which tools were available, the model generated a structured request, the harness decided whether to allow it, and normal application code performed the operation. The resulting tokens were added to context, then the model was invoked again to respond to the user.

If any of this used unfamiliar terms, like inference or harness, you might be interested in this video that explains some commonly used but underdefined AI jargon.