Model Context Protocol (MCP): Connect AI to Your Tools and DataConnecting hosts and building clients · Lesson 10 of 18

Building MCP clients and wiring them into your own agent

Article · 16 min · 9 min lecture

Video lecture

Building MCP clients and wiring them into your own agent

15 chapters · about 9 min · full transcript

Coming soon

Chapter 1 of 15

Building MCP clients

  • Client responsibilities
  • The Python v2 Client
  • Bridging MCP tools to an LLM
  • Hosted alternatives

The narrated lecture is in production

Every chapter is scripted and ready. Browse the chapters and read the full transcript now — the video will appear here when it’s published.

Chapters

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

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.

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.

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.

Check your understanding

Quick questions to lock in the lesson. They don’t count towards your certificate.

  1. Two connected servers each expose a tool named search. What should your client do?
  2. Where should the policy about which tools an agent may use be enforced?
  3. A tool call returns is_error true. How should the bridge pass it to the model?

Put it into practice

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.

Enrol for free to save your progress

Reading is always free. Enrol to keep your place, take the final assessment and earn a verifiable certificate.