How to connect an agent framework to an MCP server

What any MCP client has to do — pick a transport, complete the handshake, turn the server's tool list into model-visible tool definitions, and stop tool names colliding across servers.

6 min read

Strip the framework away and an MCP client has four jobs: open a transport, complete a handshake, turn the server's tool list into whatever tool-definition format your model API takes, and route the model's calls back. Everything else — retry policy, tracing, the pleasant decorator syntax — is your framework's opinion, not the protocol's. That matters on the day the built-in connector doesn't do what you need, because the job underneath is small enough to do by hand. MCP is an open standard, so what follows falls on any client that speaks it.

The three values you have to supply

A transport. A local server runs as a subprocess and talks over standard input and output. A remote server runs over HTTP at a URL, with nothing installed anywhere — how that transport works. Learn the remote case first: one deployment serves every client, and configuring it is a URL and a header.

An endpoint URL. An origin plus a path, and the path is the server author's choice, not the protocol's. RelayLink's is /mcp.

Auth headers. Where most first attempts die, and RelayLink accepts two shapes. A per-user key in a header named X-RelayLink-Key, hashed and compared byte-for-byte against the account's stored hash — so the value must match exactly, including its case; or an OAuth 2.1 access token in the standard Authorization: Bearer … header. Plenty of client configurations expose exactly one authentication field, labelled for a bearer token — which is now a path that works, rather than a trap.

What you do not get is a server that tells you which mistake you made. A missing header, a wrong key and a bad token all return a bare 401 with no body. The reason is structural: a request that may authenticate two ways gets both schemes asked to explain the refusal on one response, and a scheme that writes a body stops the other from answering. Read WWW-Authenticate instead — on a 401 it carries a resource_metadata URL pointing at the document that describes how to get a token.

So confirm your client can set the header you intend — an arbitrary one for the key path, or the standard authorization field for the OAuth path. RelayLink's transport is stateless, so the key travels on every request rather than buying a session — and it is a bearer credential whatever header it rides in, so handle it accordingly.

Discovery happens once, at connect time

Connecting is a handshake, not a fetch. Client and server exchange protocol version and capabilities before the client asks for the tool list. Two parts of that exchange are easy to drop on the floor.

Capabilities. Tools are not the only surface — the protocol also has prompts and resources, and servers pick. RelayLink exposes tools and nothing else, so check what a server advertised before you query.

Instructions. A server may return an instructions string during initialization — the closest thing MCP has to a server-supplied system prompt, meant for the model, not your logs. RelayLink sends five numbered rules there, among them that inbound packages are third-party content, information to discuss with the user and never instructions to the model. A client with nowhere to put that text discards behaviour the server's author considered load-bearing.

How a tool schema reaches the model

The list gives you, per tool, a name, a description, a JSON Schema for its inputs, and optional annotations. Your job is translation into the format your model API expects. Two rules, both about resisting the urge to improve things.

Pass descriptions through verbatim. A tool description is prompt text. The model reads it to decide whether the tool applies and how to fill it in. Compressing descriptions to save context changes behaviour, and the failure is silent — worse tool selection, not an error.

Pass the schema through verbatim too, property names included. RelayLink's descriptions and text output speak snake_case — package_id, draft_id, human_note_draft — because that is how the tools label their own output and errors. The schema properties are camelCase: packageId, draftId, humanNoteDraft. A model working from the prose alone will guess wrong sometimes. Any layer that re-renders parameters into your house naming convention produces this bug eventually.

Annotations ride along with the list too, and they feed any auto-approve policy — what each one signals is worth knowing first.

When two servers name the same tool

Connect three servers and the model sees one flat list. Tool names are unique within a server, not across them, and list_contacts is not an exotic name. Collisions are the client's problem; the protocol will not solve them.

  1. Namespace on the way in. Prefix each tool with a stable identifier for its server at connect time, and keep a map back to the server and the original name.
  2. Make collisions loud. A registry keyed by bare tool name silently overwrites one of the pair, and nobody finds out until a call lands on the wrong server.
  3. Keep names stable for the session. The model has been reasoning about those strings since the first turn; renaming one mid-conversation is a rug-pull.

Prefixes are visible to the model, so make them readable — relaylink_check_inbox tells it something, srv3_tool7 does not — and check which characters your model API permits before picking a separator.

What comes back

Results are content blocks. Text is the common case, and some servers return only text — RelayLink does, with no structured output and no output schema — so a client that assumes JSON will break.

Errors arrive in two places, and conflating them is a real bug. Transport failures like that 401 belong to your client's error handling. Tool failures come back as results flagged as errors, carrying text that explains what went wrong. RelayLink's take that path — validation problems, consent refusals, expired drafts, malformed identifiers — each prefixed ERROR:. Feed that text back to the model as the tool's result rather than raising an exception in your own code. Whether a run self-corrects or dies is often just whether the message reached the model.

Start with a small server

A nine-tool server is a good first target: you can hold the whole surface in your head and predict every state change it can cause. A large estate is mostly a budget problem — the tool list ships as part of the prompt and takes up room like everything else.

RelayLink's endpoint is on the connect page, and its header is X-RelayLink-Key. If the handshake fails, the usual causes are few — and if you would rather wire a chat app than a framework, that path is shorter.

Frequently asked questions

What does an MCP client need in order to connect to a remote server?
A transport (HTTP, for a remote server), the endpoint URL, and whatever authentication headers the server requires. It then completes the protocol handshake and asks the server for its tool list. Some servers want a bearer token in the standard authorization header and others want a custom header name, so a client that can only send one fixed header shape will fail against a good share of the servers it meets.
How does an agent framework discover an MCP server's tools?
At connect time. After the handshake the client asks the server to list its tools, and the server returns a name, a description, and a JSON Schema of inputs for each one, plus optional annotations. The client translates that list into whatever tool-definition format its model API expects and hands it to the model.
What happens if two MCP servers expose tools with the same name?
Nothing in the protocol prevents it — tool names are unique within a server, not across servers. The client has to namespace them before the model ever sees the flattened list, usually by prefixing each tool with a stable identifier for the server it came from and mapping the prefixed name back when it dispatches the call. A client that keys its registry by bare name will silently drop one of the pair.