Index Ai Academy
Phase 9 · Model Context Protocol · Clients and hosts

Hosts: connecting servers to a model

Many servers, one agent: namespacing, routing and approvals.

About 40 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

The host is where MCP meets the model and the user. Claude Code, Claude's apps and IDE assistants are all hosts, and so is any agent you build that loads MCP servers. Everything from the agent loop in Phase 1 comes together here: tool definitions, tool_use blocks, tool results and approvals, now fed by any number of servers. Getting the host right is what makes plugging in a new server safe.

What you'll be able to do

  • Describe a host's job: many clients, one model, one user to protect
  • Merge tools from several servers without name clashes, within the model API's naming rules
  • Route a model's tool_use to the right server and turn MCP results into tool_result blocks
  • Decide when to ask the user for approval, using annotations only from trusted servers

What it covers

  1. The host's job
  2. From MCP tool to model tool, and back
  3. Approvals
  4. Keeping context under control
  5. Converting one result
  6. Where people go wrong with hosts

The host's job

Picture an executive assistant with a phone line to every department. The boss says "get me last month's sales". The assistant knows who to call, checks that the request is fine, makes the call and brings back the answer. The boss never dials anyone.

A host works the same way, with the model as the boss. It holds one MCP client per server, and one conversation with a model. Each turn it:

  1. Gathers tools from every connected server, and adds the servers' instructions to the system prompt.
  2. Sends the conversation and the merged tool list to the model.
  3. When the model replies with tool_use blocks, finds the server for each tool, checks whether to ask the user first, calls tools/call, and turns each MCP result into a tool_result.
  4. Repeats until the model stops asking for tools.

The model never learns which server a tool came from. To the model, they're all tools.

Names: clashes and rules

Two people called John in one office become "John in sales" and "John in accounting". Two servers can both offer search too. The host must give each tool a unique name, usually by prefixing the server's name: crm__search and wiki__search. It keeps a route table from that name back to the server and the tool's original name.

The model API has its own naming rules, too. Claude's tool names must match ^[a-zA-Z0-9_-]{1,64}$, while MCP allows dots and up to 128 characters. So the host also cleans names, swapping disallowed characters for _ and shortening long ones, and refuses to load two tools whose cleaned names collide rather than guessing.

import re

def clean(server, tool):
    name = re.sub(r"[^a-zA-Z0-9_-]", "_", f"{server}__{tool}")
    return name[:64]

for server, tool in [("crm", "search"), ("wiki", "search"), ("files", "fs.read_file"), ("big", "x" * 80)]:
    print(f"{server}/{tool[:20]:20} -> {clean(server, tool)}")
It runs right here in your browser, with nothing to install.
Quick check
What does this print?
tools = [("shop", "search"), ("docs", "search")]
names = [f"{s}__{t}" for s, t in tools]
print(len(set(names)))
  1. 2
  2. 1
  3. 4
  4. 0
Show the answer

2

The names become shop__search and docs__search, which differ, so the set holds 2. A server prefix keeps the two search tools apart.

Quick check
Why does a host prefix tool names with the server's name?
  1. Two servers can offer tools with the same name, and the host needs to route each call to the right server
  2. It makes the tools run faster
  3. MCP servers reject calls without it
  4. Models require a prefix
Show the answer

Two servers can offer tools with the same name, and the host needs to route each call to the right server

The prefix makes names unique across servers, and the route table maps them back to the server and original name.

From MCP tool to model tool, and back

The host also acts as a translator, because MCP and the model API describe the same things in slightly different words. Going out, an MCP tool becomes a Claude tool: the cleaned name, the description, and inputSchema as input_schema. Coming back, a tools/call result becomes a tool_result block for the next user turn:

MCP resultClaude tool_result
content text blockscontent: the text
structuredContent with no textcontent: the JSON as text
isError: trueis_error: true, so the model knows to correct itself
image blocksimage content blocks, so the model can see them
the call itself failed (server down, protocol error)is_error: true with a short explanation

Every tool_use must get a tool_result with the same tool_use_id, even when the call failed or the user said no. Otherwise the next model request is rejected.

In plain words: every question the model asks gets an answer, even if the answer is "that failed" or "the user said no".

Try it now

The host stands between the model and every server, and it's where safety lives.

Approvals

The host stands between the model and the world. A good assistant signs for the small things and checks with you before writing a large check. Before running a tool, the host decides whether to ask the user. A reasonable policy:

  • A tool from a trusted server marked readOnlyHint: true may run without asking.
  • Anything else asks first, and the host shows the arguments, because data leaks out through arguments (an email address, a document's contents).
  • If the user declines, return a tool_result with is_error: true saying so. The model can then explain, or try something else.

Annotations from an untrusted server don't relax anything. A malicious server can label its "exfiltrate everything" tool read-only.

Quick check
The user declines a tool call the model asked for. What should the host send back to the model?
  1. A new user message saying "never mind"
  2. A tool_result for that tool_use_id, with is_error true and a note that the user declined
  3. Nothing; skip the tool_use
  4. A fake successful result
Show the answer

A tool_result for that tool_use_id, with is_error true and a note that the user declined

Every tool_use needs a matching tool_result. Saying the user declined lets the model respond sensibly.

Keeping context under control

Every tool definition goes into the model's context on every turn. Five servers with twenty tools each is a hundred definitions, maybe tens of thousands of tokens, before the user has said anything. Too many tools also make the model choose worse, the way a 300-dish menu slows everyone down.

Hosts fight this by letting users turn servers and tools on and off, and by loading tool definitions on demand: the model first sees short names or a search tool, and the full schema is added when it's needed. Phase 10 builds this.

Some model APIs can also act as the MCP client themselves: you pass a list of remote server URLs in the request, and the provider calls the servers during generation. That's convenient, but your code then gives up the routing, approval and logging this lesson builds, so check what control you still have.

Quick check
A host connects 12 servers that offer 300 tools in total. What keeps the model's context manageable?
  1. Drop tool descriptions to save tokens
  2. Send all 300 definitions on every call
  3. Merge all tools into one big tool
  4. Load only the servers and tools a task needs, defer the rest, and keep results short
Show the answer

Load only the servers and tools a task needs, defer the rest, and keep results short

Every definition costs tokens on every call, and long lists make choosing harder. Fewer, relevant tools work better.

Worked example

Converting one result

Three MCP results, converted into the tool_result blocks the model sees.

import json

def to_tool_result(tool_use_id, result):
    texts = [b["text"] for b in result.get("content", []) if b.get("type") == "text"]
    if not texts and "structuredContent" in result:
        texts = [json.dumps(result["structuredContent"])]
    return {"type": "tool_result", "tool_use_id": tool_use_id,
            "content": "\n".join(texts), "is_error": bool(result.get("isError"))}

ok = {"resultType": "complete", "content": [{"type": "text", "text": "Retargeting: 3,940 clicks"}]}
data_only = {"resultType": "complete", "content": [], "structuredContent": {"stock": 12}}
failed = {"resultType": "complete", "content": [{"type": "text", "text": "No campaign cmp_999."}], "isError": True}
for i, r in enumerate([ok, data_only, failed]):
    print(to_tool_result(f"toolu_{i}", r))
In the lesson you can change this and run it.

Text passes through, structured-only results become JSON text, and isError becomes is_error. A full host would also pass images through as image blocks. The model reads these on its next turn exactly as it would the results of your own Python tools.

Where people go wrong with hosts

The model never talks to a server. People often picture the model calling MCP directly. It can't. It only ever sees tool definitions and asks for calls. The host decides what to do, makes the call and passes back the result. That's why the host is the right place for every safety rule.

Names can clash. Two servers can both offer search. Without a prefix, one silently hides the other, and the model may send private data to the wrong one. Add the server's name to each tool, and keep a table that maps back to the real name.

A declined call still needs an answer. If a person says no, the conversation still contains a tool_use with no result, so the next API call fails. Send a tool_result that says the user declined. The model reads that and can take another approach.

Annotations come from the server. A tool that claims to be read-only might not be. Only skip approval for servers you trust. This is also a place to be strict: asking one extra time costs a click, while wrongly skipping can cost a lot more.

Practice what you just read

  • 2 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

In MCP, what is the host?

  1. The app with the model and the user, holding one client per connected server
  2. The machine the server runs on, whether local or in the cloud
  3. The company that publishes the server and keeps it maintained
  4. The model itself, which decides what to connect to and when to call it

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson