---
title: "Building MCP clients and wiring them into your own agent"
description: "Why build a client? Hosts cover chat and IDE use. You need your own client when MCP tools must power your product or automation: a reporting agent, a…"
url: https://optimizeall.com/learn/model-context-protocol-mcp/building-mcp-clients-and-agent-integration
updated: 2026-10-05
---

Model Context Protocol (MCP): Connect AI to Your Tools and Data · Connecting hosts and building clients · lesson 10 of 18 · 16 min

# Building MCP clients and wiring them into your own agent

## Why build a client?

Hosts cover chat and IDE use. You need your own client when MCP tools must power **your** product or automation: a reporting agent, a support backend, a batch pipeline. A client connects to servers, lists capabilities, calls tools, reads resources and fetches prompts, and bridges them to whichever model API you use.

## Client responsibilities

1. **Connection management**: stdio subprocesses or HTTP endpoints; timeouts; reconnects.
2. **Discovery**: list tools (and resources/prompts); cache lists using `ttlMs` hints where available; refresh on change notifications.
3. **Translation**: convert MCP tool definitions into your model provider's tool format, and MCP results back into tool results.
4. **Policy**: allowlist which servers and tools each agent may use; require approval for writes; validate arguments.
5. **Naming**: namespace tools per server to avoid collisions (`crm__find_contacts`).
6. **Security**: treat tool results as untrusted input; never forward tokens between servers.
7. **Observability**: trace every call with server, tool, duration and errors; propagate trace context in `_meta` where supported.

## The Python v2 client

```python
from mcp import Client, StdioServerParameters

# Remote (Streamable HTTP): pass a URL. Local (stdio): pass StdioServerParameters.
async with Client("https://mcp.example.com/mcp") as crm, \
           Client(StdioServerParameters(command="python", args=["analytics_server.py"])) as analytics:
    tools = (await crm.list_tools()).tools
    result = await crm.call_tool("crm_find_contacts", {"query": "Acme"})
    print(result.structured_content, result.is_error)
```

For authenticated HTTP, build the transport with your own HTTP client and headers (the SDK provides `streamable_http_client(url, http_client=...)`), or use the SDK's OAuth client support for interactive flows. TypeScript offers the equivalent with `Client`, `StreamableHTTPClientTransport` and `StdioClientTransport` from `@modelcontextprotocol/client`.

## Bridging MCP tools to an LLM: a complete example

The pattern: gather tools from all connected servers, convert them to your provider's schema with namespaced names, run the agent loop, and dispatch each call to the right server.

```python
import asyncio, json, os
import anthropic
from mcp import Client, StdioServerParameters

llm = anthropic.AsyncAnthropic()
MODEL = os.environ.get("MODEL", "claude-sonnet-5")
ALLOWED = {"crm": {"crm_find_contacts", "crm_contact_brief"},          # policy: read-only tools
           "analytics": {"find_campaigns", "get_performance"}}

async def run(goal: str):
    servers = {
        "crm": Client("https://mcp.example.com/mcp"),
        "analytics": Client(StdioServerParameters(command="python", args=["analytics_server.py"])),
    }
    async with servers["crm"] as crm, servers["analytics"] as analytics:
        clients = {"crm": crm, "analytics": analytics}
        tools, route = [], {}
        for sname, c in clients.items():
            for t in (await c.list_tools()).tools:
                if t.name not in ALLOWED[sname]:
                    continue
                qualified = f"{sname}__{t.name}"
                route[qualified] = (sname, t.name)
                tools.append({"name": qualified, "description": t.description or "",
                              "input_schema": t.input_schema})
        messages = [{"role": "user", "content": goal}]
        for _ in range(10):
            resp = await llm.messages.create(model=MODEL, max_tokens=4000, tools=tools, messages=messages)
            messages.append({"role": "assistant", "content": resp.content})
            if resp.stop_reason != "tool_use":
                return "".join(b.text for b in resp.content if b.type == "text")
            results = []
            for b in resp.content:
                if b.type != "tool_use":
                    continue
                sname, tname = route[b.name]
                r = await clients[sname].call_tool(tname, b.input)
                text = json.dumps(r.structured_content) if r.structured_content is not None else \
                       "\n".join(getattr(x, "text", "") for x in r.content)
                results.append({"type": "tool_result", "tool_use_id": b.id,
                                "content": text[:8000], "is_error": bool(r.is_error)})
            messages.append({"role": "user", "content": results})
        return "Stopped: step budget exhausted"

print(asyncio.run(run("Brief me on Acme and how their UAE campaigns performed last week.")))
```

Key choices: an explicit **allowlist** per server (policy lives in your client, not in the server's annotations), **namespaced** names, **capped** result sizes, and errors passed through as `is_error`. The same bridge works for OpenAI or Gemini by changing the tool-format conversion (lesson 5 of the platform-integration course compares formats).

## Alternatives to writing the bridge

- **Claude API MCP connector** for remote servers (the API calls the server for you).
- **OpenAI Agents SDK** `MCPServerStdio` / `MCPServerStreamableHttp` with `require_approval`.
- **Claude Agent SDK** `mcp_servers` option (stdio, HTTP, or in-process SDK servers).
- **Anthropic SDK MCP helpers** (`anthropic[mcp]`) that convert MCP tools for the tool runner; check compatibility with your installed MCP SDK major version.

## Worked example: a nightly client-report pipeline in Riyadh

An agency generates Arabic and English weekly reports for 25 clients. A Python worker connects to the analytics and CRM servers, runs the bridged agent per client with read-only tools, writes the report to storage, and posts a link to Slack. Because the tools come from MCP servers, the same servers also power analysts' ad-hoc questions in Claude and developers' IDE workflows.

## Pitfalls

- Exposing every server tool to the model without an allowlist.
- Name collisions across servers (two `search` tools).
- Forwarding an access token received for one server to another (forbidden token passthrough; module 5).
- Ignoring `is_error` and treating failures as data.

## Measuring success

Per-server call success rate, latency per tool, tokens contributed by tool definitions, and agent task success on your eval set.

## Video lecture: Building MCP clients and wiring them into your own agent

Lecture coming soon · 15 chapters · about 9 minutes. Read the full transcript below.

1. Building MCP clients
2. Why build a client?
3. Seven client jobs
4. Python v2 Client
5. Bridge step 1: gather tools
6. Simple example: morning digest
7. Bridge step 2: the loop
8. Alternatives
9. Example: nightly report pipeline
10. Pitfalls and metrics
11. Client performance
12. Deeper: Sunday-night reports (illustrative)
13. Watch me do it: the bridge
14. Try this now
15. Recap

## Lecture transcript

### Building MCP clients

Hosts like Claude and IDEs cover chat and coding. But when MCP tools need to power your own product, like a reporting agent or a support backend, you need your own client. In this lesson you'll learn what a client is responsible for, use the Python SDK's client, and build a complete bridge that lets a model use tools from several MCP servers.

### Why build a client?

Why build your own client when so many hosts exist? Because hosts are designed for people chatting. Your business processes, like nightly reports, ticket triage or onboarding pipelines, run without anyone typing. Think of the difference between using a taxi app and running a delivery fleet. The app is perfect for one person getting a ride. A fleet needs its own dispatch system, rules about which drivers take which jobs, and tracking. A custom MCP client is your dispatch system.

### Seven client jobs

A client has seven jobs. Manage connections, local or remote, with timeouts. Discover tools, resources and prompts, caching lists sensibly. Translate MCP tool definitions into your model provider's format and results back again. Enforce policy: which servers and tools each agent may use, and approvals for writes. Namespace tools per server to avoid collisions. Treat results as untrusted and never forward tokens between servers. And trace every call.

### Python v2 Client

The Python SDK version two makes connecting easy. Pass a URL to the client and it speaks Streamable HTTP. Pass standard I O server parameters, a command and arguments, and it launches a local server. Then list tools and call them, reading structured content and the error flag from each result. For authenticated HTTP, build the transport with your own HTTP client and headers, or use the SDK's OAuth support. TypeScript has the same concepts in its client package.

### Bridge step 1: gather tools

Now the bridge. First, connect to every server and list its tools. Keep only the tools on your allowlist for that server, because policy belongs in your code, not in the server's annotations. Rename each tool with a server prefix, like crm double underscore find contacts, and remember which server it came from. Convert each definition into the model's tool format, which for Claude is a name, a description and an input schema.

### Simple example: morning digest

A simple example. A small agency wants a morning digest: yesterday's new leads from the CRM server and yesterday's ad spend from the analytics server, summarized in three bullets and posted to their team chat. The client connects to both servers, exposes just two read tools to the model, runs one short loop, and posts the result. No chat window, no human typing, and the same two servers keep working in Claude for ad hoc questions during the day.

### Bridge step 2: the loop

Then run the familiar agent loop. Send the goal and the combined tool list to the model. When it asks for a tool, look up which server owns it, call that server with the original tool name, and turn the result into a tool result: structured content as JSON if present, otherwise the text, capped in size, with the error flag preserved. Append all results in one message and loop, with a step budget.

### Alternatives

The same bridge works with OpenAI or Gemini; only the tool format conversion changes. And you don't always need to write it. The Claude API's MCP connector calls remote servers for you. The OpenAI Agents SDK has MCP server classes with approval policies. The Claude Agent SDK takes MCP servers in its options, including in process ones. And Anthropic's SDK has helpers that convert MCP tools for its tool runner, though you should check compatibility with your MCP SDK version.

### Example: nightly report pipeline

A worked example. An agency in Riyadh produces weekly Arabic and English reports for twenty five clients. A Python worker connects to its analytics and CRM servers, runs the bridged agent for each client with read only tools, stores each report, and posts a link to Slack. The same MCP servers also power analysts' ad hoc questions in Claude and developers' IDE workflows. Build once, use everywhere.

### Pitfalls and metrics

Avoid four pitfalls. Exposing every tool without an allowlist. Name collisions across servers. Forwarding a token you received for one server to another, which the spec forbids. And ignoring the error flag, so failures get treated as data. Measure per server success rates, latency per tool, how many tokens tool definitions add, and task success on your eval set.

### Client performance

What about performance when your client connects to several servers? List tools once and cache them, respecting the cache hints servers send. Run independent tool calls in parallel. Set timeouts per server, so one slow server doesn't stall the whole agent. And if a server is down, remove its tools from the list for that run and tell the model, rather than letting every call fail. Your agent stays useful even when one integration has a bad day.

### Deeper: Sunday-night reports (illustrative)

Let's deepen the Riyadh reporting pipeline with illustrative numbers. Twenty five clients, Arabic and English reports every Sunday night. The worker runs the bridged agent once per client with four read only tools. The first version sent every tool from both servers to the model and averaged many more calls per report; with the allowlist, calls dropped and the reports became more consistent. When the CRM server was down for maintenance one night, the client removed its tools and the reports went out with a note, CRM data unavailable, instead of failing entirely. Account managers preferred a slightly incomplete report on time over no report at all.

### Watch me do it: the bridge

Watch me do it. Let's step through the bridge. The allowed dictionary lists the read only tools I permit from each server. In run, I create two clients: one from the CRM URL, one launching the analytics server as a subprocess, and open both. For each server I list tools, skip anything not in the allowlist, build a qualified name with a double underscore, record the route back to the server and tool, and convert the definition into Claude's format with name, description and input schema. Then the loop: call the model with the combined tools. If it doesn't want a tool, return the text. For each tool use block, look up the route, call the owning server with the original tool name, and turn the result into text: structured content as JSON if present, otherwise the text parts, capped at eight thousand characters, keeping the error flag. All results go back in one user message. The loop stops after ten steps.

### Try this now

Try this now. Take the bridge example from the lesson and connect it to one of your own servers. Then extend it in one direction. Either add a second model provider by writing the conversion for its tool format, or add an approval step for one write tool that pauses and asks you in the terminal before calling the server. Add a log line for every call with the server name, tool name, duration and error flag. You now have the core of a production client.

### Recap

To recap: build a client when MCP must power your own software. Handle connections, discovery, translation, policy, namespacing, security and tracing, and use the bridge pattern to give any model tools from many servers. Your next step: extend the bridge to a second model provider, or add an approval step for one write tool, and trace each call with server and tool names.

## Key takeaways

- Build your own client when MCP tools must power your product or automation.
- Clients handle connections, discovery, translation to model tool formats, policy, namespacing, security and tracing.
- The Python v2 Client accepts a URL for HTTP servers or StdioServerParameters for local ones.
- Enforce an allowlist in the client and namespace tools per server.
- Hosted options exist: Claude API MCP connector, OpenAI Agents SDK, Claude Agent SDK.

## Try it

Extend the bridge example to a second model provider or add an approval step for one write tool, and trace each call with server and tool names.

- [Previous: Connecting MCP servers to Claude, ChatGPT, IDEs and agents](https://optimizeall.com/learn/model-context-protocol-mcp/connecting-servers-to-ai-hosts)
- [Next: Authorization for remote MCP servers: OAuth done right](https://optimizeall.com/learn/model-context-protocol-mcp/authorization-oauth-for-mcp)
- [All lessons of Model Context Protocol (MCP): Connect AI to Your Tools and Data](https://optimizeall.com/learn/model-context-protocol-mcp)
