-
Notifications
You must be signed in to change notification settings - Fork 4.7k
writing an mcp client in python
Most people meet MCP as a user of a client: Claude Code, an editor, a chat app. The word makes the thing sound bigger than it is. A client is a process that speaks JSON-RPC to a server and decides what to do with the answers. There is no model in that sentence, and there does not have to be one in your client either.
That is the useful realisation, because a client with no model is the fastest way to find out whether a server works.
import asyncio
import os
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(
command=sys.executable,
args=["-m", "aihawk"],
env={**os.environ, "PYTHONPATH": "src"},
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
init = await session.initialize()
print("server:", init.serverInfo.name, init.serverInfo.version)
tools = (await session.list_tools()).tools
print("tools:", len(tools))
for t in tools[:3]:
print(" ", t.name)
out = await session.call_tool("browser_list", {})
print("browser_list ->", out.content[0].text[:200])
asyncio.run(main())Run against this project's server on 2026-09-13, that prints:
server: stealth 1.28.0
tools: 16
browser_open
browser_close
browser_list
browser_list -> {"focus": "main", "limit": 2, "browsers": [{"id": "main",
"running": false, "focused": true, "url": "", "urls": []}], "note": "1 of 2
browsers. Commands that name none go to main."}
Four things are worth naming in those thirty lines.
stdio_client launches the server. You are not connecting to something
already running; you are giving the SDK a command line and it spawns the
process, hands you its pipes, and kills it when the block exits. That is what
makes a stdio server easy to develop against and is the same mechanism every
desktop client uses when you paste a config block.
initialize is a handshake, not a formality. Nothing else is legal before
it. Its answer is where you learn the protocol version and what the server
declares it can do.
list_tools is the same call the model's host makes. What comes back is
exactly what would be spent on context every turn, so a client is also how you
measure a server's cost honestly.
How many MCP tools is too many does that
arithmetic on this one.
call_tool takes the name and a dict, and gives you content back. For a
browser server that content might be text, a snapshot, or an image. Read the
text rather than the shape of the object: the answer above is JSON in a string,
which is a choice this server makes and not a rule.
init.serverInfo.version reported 1.28.0. That is the version of the
installed mcp SDK, not of the server package, which is on a completely
different number. It is not a bug, but it is a trap: do not use serverInfo
to decide which version of a server you are talking to unless that server
explicitly sets it. If you need to branch on a server's version, ask the server,
through a tool that answers it.
This is the kind of thing a thirty-line client finds in thirty seconds and a week of reading documentation does not.
Deciding whether the problem is the server or the client. When tools are not showing up in your editor, this script answers the only question that matters: does the server work at all? If it lists tools here and not there, the registration is wrong, not the server. A browser MCP server in GitHub Copilot covers the registration side.
Testing your own server without a model. A model is a bad test harness: it works around a broken tool rather than reporting it, so a misleading reply becomes a wrong action instead of a red test. How to build an MCP server has more on that failure mode.
Scripting a server you did not write. Nothing says a client has to be interactive. If a server exposes a tool you want in a nightly job, this is the whole integration.
Adding one is not a different program. You call list_tools, convert each tool
to whatever shape your model provider expects, pass the model's chosen call
through call_tool, and feed the result back as a message. That loop is the
entire "agent" part, and writing it once is the clearest way to see that MCP is
a discovery and transport convention rather than an intelligence layer.
Whether you want that loop at all is a real question with a real answer: MCP alternatives is about when the protocol is the wrong shape for what you are doing.
What is an MCP client? The side that connects to a server, lists what it offers and calls it. Your editor is one. A thirty-line script is one too.
Do I need an LLM to write a client? No, and leaving it out is the point for testing.
What is the difference between an MCP client and an MCP server? The client starts the connection and calls tools; the server offers them. Over stdio the client also launches the server process.
How do I connect to a remote MCP server instead? Swap stdio_client for the
streamable HTTP client and give it a URL. Everything after the handshake is
identical, by specification. Local or remote
explains what else changes.
Why do the tools not show up? Run the script above. If they appear here, your client's config is the problem, not the server.
See also: How to build an MCP server, tools, resources and prompts, and the MCP server for the server this was run against.
- The MCP transports overview, retrieved 2026-09-13, for stdio framing and the identical-semantics guarantee across bindings.
- The
mcpPython SDK, version 1.28.0 as installed here on 2026-09-13, forClientSession,stdio_clientandStdioServerParameters. - The script above was executed against this project's server on 2026-09-13; the output block is its real output, wrapped to fit.
Written while maintaining a server, which is why the client is pointed at ours.
It is thirty lines against any server, and the paragraph that cost us something
is the one about serverInfo.
- OpenAI Operator alternatives
- Open-source Operator-style agents
- Is OpenAI Operator still available?
- OpenAI Operator vs Claude computer use
- browser-use alternatives
- Choosing an AI browser agent
- Open-source AI browser agents
- Open-source computer-use agents
- What is an AI web agent?
- AI browser agents vs traditional scraping
- Cloud browser infrastructure for AI agents, explained
- Browserbase alternatives
- Firecrawl vs an AI browser agent
- Skyvern alternatives
- Stagehand vs browser-use
- Project Mariner is gone: what replaced it
- Manus alternatives
- Gemini computer use vs Claude computer use
- AIHawk, reviewed honestly by its own wiki
- AI browser vs AI browser agent: which one do you want?
- AI browser agent vs RPA: which one fits the job
- AI browser agent vs n8n, Zapier and Make
- Vercel agent-browser alternatives, compared honestly
- What is an agentic browser? Definition and the two kinds
- Open-source agentic browsers: the three layers, compared
- Choosing an MCP server for browser automation: four axes
- Stealth MCP servers compared: Camoufox, nodriver, Patchright
- Playwright MCP alternatives, and the three you don't need
- Autonomous browser agents: the four rungs of autonomy
- What is actually free in the AI browser agent stack
- browser-use on GitHub: what the repo actually gives you
- Playwright MCP vs Chrome DevTools MCP: different jobs
- How to choose among MCP servers: a map by category
- Which MCP servers are worth adding to Claude Code
- MCP on GitHub: finding servers and judging them fast
- MCP vs an API: the decision, and what the wrapper costs
- MCP alternatives: when the protocol is the wrong shape
- Why does my AI agent get blocked?
- The timing signal AI agents give off
- Agent retry loops trip rate limits, not fingerprints
- Claude computer use detected as a bot
- browser-use getting blocked: what you can and cannot change
- Playwright MCP session blocked: four causes, four fixes
- Playwright MCP and captchas: what actually gets you past
- Cloudflare and a browser MCP server: what is being read
- Can an AI agent solve a captcha? The honest answer
- Getting an AI agent to fill out forms
- Which model to use with AIHawk
- Browser problem or model problem?
- Running AIHawk's browser from Claude Code
- Extracting data to a CSV with an AI agent
- Monitoring a page for changes with an AI agent
- Running AIHawk's browser from Claude Desktop
- Running AIHawk's browser from Cursor
- Using an AI agent to hunt for apartments
- Getting website data into Google Sheets with an AI agent
- Using an AI agent to download invoices from portals
- AI agents for web research
- Using an AI agent to test your own website
- Running AIHawk's browser from Cline
- Posting to social media with an AI agent
- Posting to Facebook with an AI agent
- Posting to Instagram with an AI agent
- Posting to X with an AI agent
- Automating LinkedIn posts: read this first
- Appointment bots: what they are and what an agent can legitimately do
- Track prices across sites with an AI agent
- Build a lead list with an AI browser agent
- Run an AI browser agent on a schedule
- AI browser agent with a local LLM: what changes
- Should you log your AI agent into your accounts?
- How to write a task an AI browser agent can follow
- Move data between two web apps with an AI agent
- The MCP server
- How the tools are shaped, and why
- Playwright MCP vs the Playwright CLI: which fits when
- Playwright MCP: browser is already in use, and the fix
- Playwright MCP best practices: four decisions that matter
- Playwright MCP with a proxy, and the three leaks it leaves
- A browser MCP server in GitHub Copilot: setup and limits
- Using a browser MCP server for web scraping: the pattern
- Which LLM for browser automation: the four properties
- How to build a browser agent, and what to take instead
- Getting an AI agent to log into a website: three routes
- MCP tools, resources and prompts: who controls each
- How many MCP tools is too many? The context arithmetic
- How to build an MCP server: the decisions, not the scaffold
- Local or remote MCP server: what changes, and what does not
- Writing an MCP client in Python: the thirty-line version