MCP Client Guide: What It Is, Top Clients and How to Build One
What an MCP client is, how it differs from the host, which MCP clients support which transports and features, and how to build one in TypeScript or Python.
An MCP client is the connector inside an AI application that holds a connection to exactly one MCP server. It sends that server's requests (list tools, call a tool, read a resource), and it passes results back to the app. The application that runs the model and the UI is called the host. A host such as Claude Code, Cursor or VS Code creates one client for every server you configure. People often say "MCP client" when they mean the whole app, and that's fine in everyday speech, but the difference matters once you build one.
This guide covers what a client does on the wire, which clients support what in October 2026, and how to write a minimal client with the official TypeScript and Python SDKs. If you need the protocol basics first, read What is MCP?. For the message-by-message flow, see How does MCP work?.
Host vs client vs server
The MCP architecture spec names three roles:
| Role | What it is | Example |
|---|---|---|
| Host | The AI application that the user talks to. It runs the model, enforces consent and decides which context to share | Claude Code, Cursor, VS Code, ChatGPT |
| Client | A connector the host creates, with a 1:1 link to one server | The object VS Code instantiates when you add the Sentry server |
| Server | A program that exposes tools, resources and prompts | GitHub, Filesystem, Playwright |
So if you connect three servers to Cursor, you have one host and three clients. Keeping one client per server is part of the security model: a server never sees the full conversation, and it can't see into other servers. The host decides what crosses those boundaries. (For the server side of the picture, see What is an MCP server?.)
What an MCP client actually does
Under the current spec revision, 2026-07-28, MCP is stateless. That changes the client's job compared with older tutorials, which open with an initialize handshake. A modern client:
- Attaches metadata to every request. Each JSON-RPC request carries
_metawithio.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilities(both required), and usuallyio.modelcontextprotocol/clientInfo. On Streamable HTTP, the client also sends a matchingMCP-Protocol-Versionheader (spec). - Optionally discovers the server.
server/discoverreturns supported versions, capabilities and server instructions in one call. Calling it is optional, but it is the recommended probe on stdio when the server might be an older, handshake-based one. - Lists and calls primitives. It sends
tools/list,resources/listandprompts/list, hands the tool definitions to the model, and sendstools/callwhen the model picks a tool. - Answers input requests. When a server needs something from the user, it no longer sends a request of its own. It returns an
InputRequiredResult(resultType: "input_required"), for example an elicitation form. The client shows the form, then retries the original request withinputResponses. This pattern is called Multi Round-Trip Requests. - Listens for changes. To learn when a tool list changes or a resource updates, the client opens a long-lived
subscriptions/listenrequest and opts in to specific notification types. - Handles auth. For remote HTTP servers, the client runs the OAuth flow in the authorization spec. Stdio servers take credentials from environment variables instead. Our MCP authentication guide covers the details.
Roots, sampling and logging still work, but the 2026-07-28 revision marks them deprecated, so a new client doesn't need to add them.
Major MCP clients compared
Every cell below comes from the vendor's current docs. "Not documented" means we could not find it in the official docs, not that the feature is missing.
| Client | Local (stdio) | Remote transports | Where servers are configured | Primitives / extras documented |
|---|---|---|---|---|
| Claude Code | Yes | Streamable HTTP (recommended), SSE (deprecated) | claude mcp add, ~/.claude.json, project .mcp.json | Tools, resources, prompts, elicitation, list-changed refresh, OAuth |
| Claude Desktop / claude.ai | Yes (Desktop) | Remote servers as Custom Connectors | claude_desktop_config.json; Settings → Connectors | Tools, resources, prompts |
| Cursor | Yes | SSE, Streamable HTTP | .cursor/mcp.json, ~/.cursor/mcp.json | Tools, prompts, resources, roots, elicitation, MCP Apps, OAuth |
| VS Code (Copilot) | Yes | HTTP, SSE | Workspace .mcp.json, user mcp.json | Tools, resources, prompts, MCP Apps |
| ChatGPT | No (needs a tunnel) | SSE, streaming HTTP | Plugins → Add custom MCP server (web) | Tools (read and write), OAuth |
| Codex | Yes | Streamable HTTP | codex mcp add, ~/.codex/config.toml | Tools, OAuth login |
Some notes from the docs:
- Claude Code stores servers in three scopes (local, project, user). It asks for
prompts/listandresources/listduring discovery, and it refreshes lists when servers send list-changed notifications (docs). Setup is in our Claude Code MCP guide. - Claude Desktop reads local servers from
~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS and%APPDATA%\Claude\claude_desktop_config.jsonon Windows. Remote servers are added as Custom Connectors under Settings → Connectors (docs). See Claude Desktop MCP setup. - Cursor has one of the broadest feature lists, with roots, elicitation and MCP Apps all marked supported (docs). See Cursor MCP setup.
- VS Code now prefers a workspace
.mcp.jsonwith amcpServerskey. The older.vscode/mcp.json, which uses aserverskey, is deprecated for new servers. Prompts appear as/<server>.<prompt>(docs). See VS Code MCP setup. - ChatGPT connects only to remote servers, through Plugins on the web (docs). See ChatGPT MCP setup.
- Codex uses one
config.tomlacross the CLI, the IDE extension and Codex in the ChatGPT desktop app. See Codex MCP setup.
For the full list, including Windsurf, Zed, Cline, Goose and others, browse the MCP.so clients directory.
Build a minimal MCP client
You only need to write a client if you're building your own host, such as an agent, a chatbot, a test harness or a gateway. Both Tier 1 SDKs moved to v2 in 2026 and support the stateless revision.
TypeScript (@modelcontextprotocol/client v2)
npm install @modelcontextprotocol/client
import {
Client,
StreamableHTTPClientTransport,
} from '@modelcontextprotocol/client';
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{ versionNegotiation: { mode: 'auto' } }
);
await client.connect(
new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'))
);
console.log(client.getProtocolEra()); // 'modern' or 'legacy'
const { tools } = await client.listTools();
console.log(tools.map((t) => t.name));
const result = await client.callTool({
name: 'greet',
arguments: { name: 'Ada' },
});
if (result.isError) {
console.error('Tool failed:', result.content);
} else {
console.log(result.content);
}
await client.close();
Three details from the SDK docs are easy to miss:
versionNegotiation. Without it,connect()runs the legacy 2025initializehandshake.mode: 'auto'probes withserver/discoverand falls back to the handshake if the server is older.mode: { pin: '2026-07-28' }refuses to fall back.- Tool errors come back as results. A tool that fails returns
isError: trueinstead of throwing, so check it before you trustcontent. Only protocol failures, such as an unknown tool or a timeout, throw. listTools()follows every page. It walks pagination for you, up tolistMaxPages, which defaults to 64.
To launch a local server instead, change only the transport:
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
await client.connect(
new StdioClientTransport({ command: 'node', args: ['server.js'] })
);
To accept elicitation, declare the capability in the constructor (capabilities: { elicitation: { form: {} } }) and register a handler with client.setRequestHandler('elicitation/create', ...). The SDK takes care of the MRTR retry.
Python (mcp v2)
uv add "mcp[cli]"
import asyncio
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
tools = await client.list_tools()
print([t.name for t in tools.tools])
result = await client.call_tool("add", {"a": 1, "b": 2})
print(result.structured_content)
asyncio.run(main())
When you pass a URL, Client uses Streamable HTTP. For a local server, pass a stdio transport instead: Client(stdio_client(StdioServerParameters(command="python", args=["server.py"]))), imported from mcp and mcp.client.stdio (Python SDK). The async with block covers the whole lifecycle. Entering it connects and agrees on a protocol version, and leaving it shuts everything down.
Wire it to a model
A useful client turns MCP tool definitions into your LLM's tool format. You map name, description and inputSchema, send the model's tool calls through callTool, and add the results to the conversation. The official build-a-client tutorial shows the full loop with the Anthropic SDK. Before you point your client at real servers, test it with the MCP Inspector.
How to choose an MCP client
- Coding agent in the terminal: Claude Code or Codex. Both have CLI commands for adding servers and OAuth login.
- IDE: Cursor or VS Code. Cursor documents more of the optional features (roots, elicitation, Apps). VS Code fits teams that are already on Copilot.
- Chat for non-developers: claude.ai or ChatGPT, with remote servers only. Pick servers that offer a hosted HTTPS endpoint, such as Notion or Sentry.
- Your own product: use an SDK client, turn on
versionNegotiation: 'auto'(or the Python v2Client) so you work with both modern and legacy servers, and follow MCP security best practices. In particular, show tool calls to the user and ask before running anything with side effects.
Feature support changes from release to release. Before you rely on resources, prompts or elicitation, confirm that your target client documents them. Otherwise, expose the same capability as a tool, because every client supports tools.
Next steps
Once you've picked a client, give it something to connect to. Browse thousands of servers in the MCP.so server directory, compare apps in the clients directory, or start with our list of the best MCP servers.
Want to run everything on your own hardware? See how to use MCP tools with local Ollama models.
Frequently asked questions
What is an MCP client?
- An MCP client is the connector inside an AI application that holds a one-to-one connection with a single MCP server. It sends requests such as tools/list and tools/call on behalf of the host application and returns the results to it.
What is the difference between an MCP host and an MCP client?
- The host is the AI application the user interacts with, such as Claude Code, Cursor or VS Code. The host creates one MCP client per configured server, and each client talks to exactly one server.
Which apps are MCP clients?
- Popular MCP-capable apps include Claude Code, Claude Desktop, Cursor, VS Code with Copilot, ChatGPT and Codex. They differ in transports and features, for example ChatGPT only connects to remote servers while Cursor also documents roots, elicitation and MCP Apps.
How do I build my own MCP client?
- Use an official SDK such as @modelcontextprotocol/client for TypeScript or the mcp package for Python. Create a Client, connect it over Streamable HTTP or stdio, then call listTools and callTool and pass the tools to your LLM.