Every Claude Desktop and Claude Code is an MCP client. Now learn to build your own: web dashboards, CLI tools, custom agents, and monitoring systems that speak the MCP protocol directly — using Python SDK and TypeScript SDK.
An MCP client connects to one or more MCP servers, discovers available tools and resources, and invokes them. The client is responsible for the conversation loop — deciding which tools to call, passing results back, and handling errors. Claude Desktop and Claude Code do this automatically; when you build a custom client, you control this loop.
initialize request, receive server capabilitiestools/list and resources/list to enumerate available tools and their schemastools/call with tool name and arguments, receive result, handle errorsThe fastmcp.Client is a high-level async Python MCP client. It handles connection lifecycle, JSON-RPC framing, and result parsing. Use it in scripts, Lambda functions, or any Python service that needs to call MCP tools programmatically.
from fastmcp import Client from fastmcp.client.transports import SSETransport, StdioTransport import asyncio, json async def run_mcp_workflow(): # Option 1: Connect to a remote SSE server transport = SSETransport( "https://api.example.com/mcp", headers={"Authorization": "Bearer YOUR_KEY"} ) # Option 2: Connect to a local stdio server # transport = StdioTransport("python", ["-m", "my_mcp_server"]) async with Client(transport) as client: # Discover what's available tools = await client.list_tools() print(f"Available tools: {[t.name for t in tools]}") # Call a tool result = await client.call_tool( "search_documents", {"query": "AWS security best practices", "max_results": 5} ) print(f"Results: {result}") # Read a resource resource = await client.read_resource("schema://database/users") print(f"Schema: {resource}") asyncio.run(run_mcp_workflow())
The official @modelcontextprotocol/sdk TypeScript package is the reference client implementation. Use it for Node.js servers, Lambda functions, web backends (Next.js API routes), and browser-compatible clients.
import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" async function mcpWorkflow() { const transport = new SSEClientTransport( new URL("https://api.example.com/mcp"), { headers: { Authorization: `Bearer ${process.env.MCP_KEY}` } } ) const client = new Client( { name: "my-client", version: "1.0.0" }, { capabilities: {} } ) await client.connect(transport) // List tools const { tools } = await client.listTools() console.log("Tools:", tools.map(t => t.name)) // Call a tool const result = await client.callTool({ name: "search_documents", arguments: { query: "IAM policies", maxResults: 5 } }) console.log("Result:", result.content[0].text) await client.close() } mcpWorkflow().catch(console.error)
An agentic MCP client uses an LLM to decide which tools to call and in what order. The loop: (1) describe the goal to the LLM, (2) LLM picks a tool and args, (3) client calls the tool, (4) result goes back to LLM, (5) repeat until done. This is exactly what Claude Code does internally.
from fastmcp import Client import anthropic, json async def agentic_mcp_client(goal: str, mcp_server_url: str): claude = anthropic.Anthropic() async with Client(SSETransport(mcp_server_url)) as mcp: # Convert MCP tools to Anthropic tool format mcp_tools = await mcp.list_tools() anthropic_tools = [{ "name": t.name, "description": t.description, "input_schema": t.inputSchema } for t in mcp_tools] messages = [{"role": "user", "content": goal}] while True: response = claude.messages.create( model="claude-sonnet-4-5", max_tokens=4096, tools=anthropic_tools, messages=messages ) if response.stop_reason == "end_turn": return response.content[0].text # Done! # Execute tool calls tool_results = [] for block in response.content: if block.type == "tool_use": result = await mcp.call_tool(block.name, block.input) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": str(result) }) messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results})
Claude Code runs this exact agentic loop — connecting to your MCP servers, converting tools to Anthropic format, feeding tool results back into the conversation until the task is complete. Building it yourself gives you full control over the loop logic, logging, and error recovery.
Build a React UI that lets non-technical users invoke MCP tools through a friendly interface. The React app communicates with a thin Node.js proxy that handles the actual MCP connection — browsers can't connect to stdio servers directly.
// Next.js API route — acts as MCP proxy for the React frontend // pages/api/mcp-tool.ts import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import type { NextApiRequest, NextApiResponse } from "next" // Reuse connection across requests (connection pooling) let _client: Client | null = null async function getClient() { if (!_client) { _client = new Client({ name: "dashboard", version: "1.0" }, { capabilities: {} }) const transport = new SSEClientTransport(new URL(process.env.MCP_SERVER_URL!)) await _client.connect(transport) } return _client } export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { toolName, args } = req.body const client = await getClient() const result = await client.callTool({ name: toolName, arguments: args }) res.json({ result: result.content[0].text }) }
#!/usr/bin/env python3 # mcp-cli.py — invoke any MCP tool from the command line # Usage: python mcp-cli.py search_documents '{"query": "AWS IAM", "max_results": 3}' import sys, asyncio, json from fastmcp import Client from fastmcp.client.transports import SSETransport async def main(): tool_name = sys.argv[1] tool_args = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {} server_url = os.environ.get("MCP_SERVER", "http://localhost:8080") async with Client(SSETransport(server_url)) as client: result = await client.call_tool(tool_name, tool_args) print(json.dumps(result, indent=2)) asyncio.run(main())
4 questions on building MCP clients