mcp-client-development
verifiedaf3b8e89-8a86-4a7f-a7fc-6998b4869e0a
Build an MCP client — connect to stdio and streamable-HTTP servers, run the initialize handshake, list tools/resources, call tools, handle errors, and wire tools into an LLM agent loop.
Metadata
Skill file
# Building an MCP Client
Use when you want *your* program or LLM agent to connect to an **MCP server** and
use its tools/resources. Server-side development is covered by the
`fastmcp-mcp-server` and `mcp-server-development` skills — this one is the flip
side: how to connect, discover, and invoke.
## MCP client fundamentals
The MCP protocol is JSON-RPC 2.0 over a transport. The client:
1. **Connects** over a transport (stdio child-process for local, or
streamable-HTTP for remote).
2. **Initializes** — an `initialize` handshake where client and server exchange
protocol versions and capabilities, then the client sends `initialized`.
3. **Discovers** — `tools/list`, `resources/list`, `prompts/list`.
4. **Invokes** — `tools/call` with a name and arguments; returns content blocks.
5. Keeps a session for the lifetime of the connection.
The canonical request/response:
```json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_weather","arguments":{"location":"New York"}}}
```
### Bring your own tools into the loop
- Once you know the schema (OpenAI-style `input_schema`), you map each MCP tool to
that API, letting the agent "call" MCP tools exactly like native tools.
- When the model emits a tool call, forward it to the MCP server with
`session.call_tool(name=..., arguments=...)`, read `result.content`, and feed
the returned text back to the model as the tool result. Cache the content:
most clients keep earlier tool results and provide them to the model on
subsequent turns instead of re-reading from disk.
## Handling tool call cycles and errors
- **Error handling:** wrap the tool-call execution and surface structured errors
to the model so it can react or retry; don't let a malformed tool blow up the
whole loop.
- **Cancellation/timeouts:** if a model wants to stop a long-running call, send a
`notifications/cancelled` notification for that request id. Set your own
client-side timeout too; `CallToolResult` supports `isError` to mark a call
that "succeeded" as an error.
- A common pattern is treating embedded content and text content as separate
result subtypes. Check the result's `content` list for `TextContent` /
`ImageContent` blocks and render/handle each appropriately.
## Building a minimal client in Python
The `mcp` Python SDK's `ClientSession` + `stdio_client` handles the protocol
mechanics for you:
```python
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(command="python", args=["server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools() # discover
print([t.name for t in tools.tools])
result = await session.call_tool("add_todo", {"title": "hi"}) # invoke
print(result.content)
asyncio.run(main())
```
For **streamable-HTTP** remote servers use `mcp.client.streamable_http` /
`streamable_http_client` with the endpoint URL, plus an authorization header when
the server requires it.
## Capability-aware design
- Handle the case where a server declares a subset of capabilities (tools only,
no resources) — don't assume all are present.
- Respect **pagination**: `tools/list` supports a `cursor` for large lists; loop
until `nextCursor` is absent.
- Some servers support tool **result caching** / resource caching — take
advantage of it to cut round-trips.
- Not all servers support prompts; feature-detect before offering.
## Pitfalls
- **Skipping initialize** — you must complete the handshake before `tools/list`;
calling early gets protocol errors.
- **Ignoring the session lifecycle** — reuse one session for the connection
instead of re-initializing per call.
- **Not mapping `inputSchema` correctly** when bridging to OpenAI-style tool
params (rename key, keep `type`, `properties`, `required`).
- **Treating every server as supporting every capability.**
- **No timeout / no cancellation** on long tool calls, so a hung tool stalls the
whole agent loop.
- **Trusting server output blindly** — validate arguments before sending and
treat returned content as untrusted data (see `mcp-server-security`).
## Verify
- Connect to a known-good server and confirm you can list and call at least one
tool end-to-end.
- Test a tool that errors (bad arguments) and confirm you get a structured,
non-crashing result back that your agent can act on.
- Confirm the initialize handshake works over both stdio and streamable-HTTP with
the servers you'll actually use.
- Test pagination/caching if the server returns a large tool list.