Topic 21

Connecting an Agent to a Server

Integration

Connecting is three things: discovery, a tool list, and a mapping from the server's tools into the same dispatcher that already runs your local ones. None of it is hard. What is interesting is what the agent is now holding — tool descriptions written by another team, arriving at runtime, priced into every turn, and free to change between one ticket and the next.

The failure surface is new as well. A local function cannot be unreachable. A server can be down, slow, or answering with a tool list that is not the one you tested against, and each of those needs a defined behaviour decided before the first ticket meets it. The alternative is a stack trace where a buyer with a cracked shelving unit was expecting a reply.

Four steps at start-up, and a fifth cost that never stops
Start or dial2-second deadline each
Discoverwhat each side supports
Fetch the tool listnames · prose · schemas
Registerprefixed, same dispatcher
Billed every turn~1,500 tokens of definitions

The Connection Sequence

Four steps, in order: start or dial the server, discover what it supports, fetch the tool list, and register each tool with the dispatcher under the name the model will see. At Sundry this runs once at process start, across three servers, with a deadline on each — because a start-up that blocks on a hung subprocess is an outage that looks like a deploy problem.

Start-up: connect each server, register what it offers, survive the ones that fail
for spec in SERVERS:                          # orders, policy, carrier
    try:
        client = connect(spec, timeout=2.0)      # start or dial, then discover
        listing = client.list_tools()             # names, descriptions, schemas
        for tool in listing.tools:
            dispatcher.register(
                name=f"{spec.prefix}_{tool.name}",  # orders_get_order
                schema=tool.input_schema,
                call=client.caller(tool.name),
                server=spec.name,
                list_hash=listing.digest,         # recorded with every run
            )
    except ConnectError as err:
        degraded.add(spec.name)                   # run without it, and say so
        log.warning("server unavailable", server=spec.name, err=err)

In words: for each configured server, connect with a deadline, ask what it offers, and register every tool under a prefixed name together with the metadata you will want at three in the morning — which server it came from and a digest of the exact list you received. If the connection fails, that server goes on a degraded list and the agent starts anyway. A dead carrier server has no business preventing a refund the agent could otherwise process from the order record alone.

Namespacing and Collisions

Sundry's policy server publishes a tool called search. So does the knowledge-base server a different team installed. Both are reasonable names in isolation and together they are a defect, because the model chooses between tools by reading names and descriptions and now sees two entries that look like the same thing. Prefixing is an accuracy fix rather than housekeeping, and the cheapest one available — applied at the registration line, before either name reaches the model.

Prefix for the domain rather than the deployment: policy_search and kb_search tell the model something, server2_search tells it nothing it can use. On the 120-ticket eval set, the two search tools were confused in eleven runs before prefixing and in one after — and that last one needed the descriptions fixed too, because a name only disambiguates as far as the sentence beside it agrees with it. Chapter 3's rule holds across the boundary: the description is the interface.

One Dispatcher for Both Kinds

Past the registration line, nothing downstream should be able to tell a local function from a remote tool. The same dispatcher validates arguments, checks authorization, starts a timer, executes, shapes the result and writes the audit record. The model already cannot tell the difference — it receives a name, a description and a schema either way — and your code has no reason to.

The failure mode when a second path appears is precise and ugly. Somebody adds an "MCP tools" branch to keep the transport details tidy, and that branch skips argument validation because the server validates, skips the timer because the client already has one, and skips the audit write because the tool is read-only today. The tools you control least become the only ones with no record. State it as a rule: if a tool call does not land in the same log table as issue_refund, it did not happen as far as Chapter 13 is concerned.

The dispatcher is also where result handling gets normalized. A local function returns an object your code built; a remote tool returns whatever the server chose to send, which may be 40 KB of JSON with a stack trace in the middle of it. Neither reaches the model in that state. Both go through the same shaping, the same size cap and the same explicit truncation marker, so what a tool result costs the ticket is a number you set in one place rather than a number the server picked.

Failure Modes of the Boundary

Five things go wrong at a server boundary, and every one of them needs an answer written down before it happens rather than chosen during an incident. The column that matters is the last one: what the loop does next, in a way the model can act on and the buyer can be told about.

FailureWhat the agent seesWhat the loop does
Server unreachable at start-upIts tools are absent from the listRun degraded, and state in the reply what cannot be checked
Call times outA structured timeout resultRetry once if read-only, otherwise escalate — never blind-retry a side effect
Server errors or crashes mid-runA structured error resultOne retry, then drop the server for the rest of this run
Tool list changed since yesterdayDifferent names, schemas or proseProceed, record the diff against the stored digest, flag it for an eval run
Model asks for a tool that is goneA name the dispatcher cannot resolveReturn a structured "no such tool" result, never raise into the loop

The first row is the one teams get wrong, because the degraded answer feels like a worse product than an error page. It is not. "I can see the order and the duplicate delivery charge; the carrier's tracking service is not responding, so I cannot confirm the delivery scan yet" resolves a good share of tickets on its own and tells the buyer something true. A 500 resolves none of them. The last row matters for a different reason: a model that asks for a tool that has been withdrawn is working from the list it was given at turn one rather than misbehaving, and the loop should hand it a result it can reason about rather than an exception.

Cost and Latency Accounting

Two meters run whenever a server is connected. The first is tokens: Sundry's nine local schemas cost about 680 tokens per request, the four order tools add roughly 520, and the policy and carrier servers about 300 between them — close to 1,500 tokens of tool definitions on every turn from the three servers in the start-up config, before a single word of the ticket, and the knowledge-base server another team installed is not in that number. That is a fifth of a modest context spent describing capabilities, most of which any given ticket will not use, and the rate is worth reading twice: the imported schemas run about 130 tokens each against the 75 Chapter 3 trimmed the local ones to, because nobody on the other side of the boundary is paying for them.

The second is wall clock. A stdio server that takes 400 ms to spawn puts 400 ms in front of the first call of the ticket, against a p95 target of 9 seconds to the first useful message. The boundary itself adds only about 15 ms per call, but the service behind it adds whatever it adds — the carrier's own API is the slowest thing Sundry calls. Put server time in the same trace as model time, split out per server. Otherwise "the agent got slow this week" is a conversation with no evidence in it, and Chapter 13 spends a page explaining why that conversation never converges.

Common Mistakes
  • Letting a failed connection kill the run — an agent that says "I cannot reach the carrier right now, here is what the order record shows" resolves tickets, and a 500 resolves none of them while looking more correct in the code.
  • Registering remote tools outside the dispatcher to keep transport handling tidy — validation, authorization, timing and audit then silently do not apply to exactly the tools whose behaviour you control least.
  • Ignoring a changed tool list because nothing broke — behaviour shifts under you between two tickets, and with no digest recorded per run there is nothing to diff when resolution drops and everyone swears they deployed nothing.
  • Assuming the server validates arguments — a malformed order id that the server accepts and the API rejects is your incident, on your pager, in your trace, and the fix is a schema check on your side too.
Best Practices
  • Record the server's identity, its tool list and a digest of that list with every run, so a behaviour change has a receipt rather than a theory.
  • Prefix tool names by domain at registration — policy_search, not server2_search — so no two entries the model sees are confusable.
  • Give every server a connect timeout, a per-call timeout and a written degraded behaviour, including what the buyer is told when it fires.
  • Route every tool call through the one dispatcher without exceptions, so authorization, timing and audit apply to remote tools exactly as they do to local ones.
Comparable toolsConsul service discovery for ordinary clientsEnvoy timeouts and retries at the boundaryKubernetes Services endpoints that move under youOpenAPI generators the same registration, at build time

Knowledge Check

What is the correct order of the connection sequence when an agent starts up against an MCP server?

  • Start or dial the server, discover what it supports, fetch the tool list, then register each tool with your own dispatcher
  • Dial the server, register its tools with the model provider, then fetch descriptions on the first call that needs one
  • Fetch the tool list on every turn of the loop, discover capabilities once, then dial the server when a call is needed
  • Send the model the server address, let it discover capabilities, then execute whatever tool call it decides to make

Two connected servers each publish a tool named search. Why does prefixing them count as an accuracy fix rather than housekeeping?

  • The model picks tools by reading names and descriptions, so two indistinguishable entries produce wrong calls
  • The protocol rejects a duplicate tool name at registration, so one of the two servers would fail to connect at all
  • Prefixed names compress better in the tool list, which reduces the schema tokens billed on every turn of a ticket
  • The dispatcher cannot route a call without a prefix, since tool names are the only routing key it has available

The carrier server is unreachable when a ticket about a possibly-undelivered parcel arrives. What should the loop do?

  • Run without that capability and say plainly which check could not be made, using what the order record does show
  • Fail the ticket with an error so the queue retries it later, once the carrier server is confirmed healthy again
  • Retry the connection on every turn until it succeeds, since the tracking scan is what decides the delivery question
  • Let the model answer from what it knows about typical carrier timelines and note the estimate as approximate

Resolution drops four points overnight. Nobody on the agent team deployed anything. What recorded artefact makes this diagnosable?

  • The tool list each run received, with a digest, so an upstream description edit can be diffed against yesterday's
  • The server's uptime and error-rate dashboard, which shows whether it was healthy across the window in question
  • The upstream repository's commit history, which records every change the owning team made to the server code
  • The per-server latency percentiles for the same period, which reveal whether the boundary got slower overnight

You got correct