MCP Python Client: Connect to a Remote MCP Server (2026)
Build an MCP Python client with the official mcp 2.3 SDK: send a bearer key to a remote server, list tools, call one, and handle 401s and tool errors.
Tarun Khatri
Published · Updated · 7 min read
To build an MCP Python client for a remote server, install the official mcp SDK and send your key in an Authorization header.
This guide is for developers who call an MCP server from a script, a test or their own agent. You get tested code, real output and the error cases. We run Weir, the server in the examples.
Set WEIR_API_KEY, then run this to list a remote server's tools:
import asyncio, os, httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
async def main():
key = {"Authorization": f"Bearer {os.environ['WEIR_API_KEY']}"}
async with httpx2.AsyncClient(headers=key, timeout=httpx2.Timeout(30, read=300)) as http:
async with Client(streamable_http_client("https://api.weirapp.io/mcp", http_client=http)) as client:
print([tool.name for tool in (await client.list_tools()).tools])
asyncio.run(main())
You need Python 3.11 or newer and an API key for the server. Here is the map:
- Install the SDKpip install mcp in a virtual environment.
- Connect with your keyAn httpx2 client carries the Authorization header.
- List the toolsclient.list_tools() returns names and input schemas.
- Call a toolclient.call_tool() returns the tool's JSON.
- Handle errorsRead is_error, and retry only what is retryable.
Step 1: Install the MCP Python SDK
The official SDK is the mcp package on PyPI, at version 2.3.0 as of October 2026. Install it in a virtual environment:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "mcp>=2.3,<3"
It pulls in httpx2, the HTTP library the SDK now uses in place of httpx. The SDK runs on Python 3.10, but the except* in Step 2 needs 3.11.
Skip the cli extra: the README says it only adds the mcp command-line tool.
Step 2: Connect the MCP Python client to a remote server
Give streamable_http_client the server URL and your own httpx2.AsyncClient. Headers, timeouts and auth all live on that one HTTP client.
import asyncio, os, re
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
URL = "https://api.weirapp.io/mcp"
HEADERS = {"Authorization": f"Bearer {os.environ['WEIR_API_KEY']}"}
async def fail_on_auth(response):
if response.status_code in (401, 403):
raise PermissionError(f"{response.status_code} {response.headers.get('www-authenticate')}")
Then open the connection, and keep it open while you work:
async def main():
hooks = {"response": [fail_on_auth]}
timeout = httpx2.Timeout(30, read=300)
async with httpx2.AsyncClient(headers=HEADERS, timeout=timeout, event_hooks=hooks) as http:
async with Client(streamable_http_client(URL, http_client=http)) as client:
await list_tools(client) # Step 3
await search(client) # Step 4
try:
asyncio.run(main())
except* PermissionError as group: # Python 3.11+
print("Check WEIR_API_KEY:", group.exceptions[0])
Three choices here matter:
- The hook keeps the 401. Without it, a bad key surfaces as a vague
MCPErrorinside two exception groups. - Set the timeout yourself. A bare
httpx2.AsyncClientgives up after 5 seconds, and a search can take longer. - The SDK picks the protocol.
Clienttries the 2026-07-28 version first, then falls back to the older handshake.
We ran this against Weir's live server on 22 October 2026 with a wrong key. The hook caught the 401 on the first request:
Check WEIR_API_KEY: 401 Bearer error="invalid_token", error_description="The credential is not a Weir access token or API key.", ...
Get a real key in the Weir dashboard. Weir's pricing starts with 5,000 free credits a month, no card.
Step 3: List the server's tools
list_tools() returns each tool's name, description and input schema:
async def list_tools(client):
listing = await client.list_tools()
print(len(listing.tools), "tools")
for tool in listing.tools[:5]:
print(tool.name, "-", (tool.description or "").split(".")[0])
Attributes are snake_case in version 2, so read tool.input_schema, not inputSchema.
Your tool count depends on what the credential may do. On Weir, a signed-in connection with the default scopes lists 44 tools, and a key lists what its scopes allow:
We counted those 44 on our own signed-in connection on 22 October. Each name is an API operation in snake_case, and the Weir MCP docs list every tool with its price.
Step 4: Call search_x and read the result
call_tool() takes the tool name and a dict of arguments. Weir's tools return the same JSON as its REST API, in structured_content:
async def search(client):
result = await client.call_tool("search_x", {"fromUsers": ["XDevelopers"], "limit": 3})
if result.is_error:
raise RuntimeError(result.content[0].text)
page = result.structured_content
for post in page["data"]:
original = post["retweetedPost"] or post # a repost keeps its counts on the original
print(post["createdAt"][:10], original["likeCount"], post["url"])
print(page["meta"]["creditsCharged"], "credits")
We made this exact call on 22 October 2026 through Weir's MCP server, from our own signed-in connection. It returned three posts for 4 credits. Trimmed:
{
"data": [
{ "id": "2113062427566969325", "likeCount": 0, "retweetedPost": { "likeCount": 133 } },
{ "id": "2111520482914877569", "likeCount": 0, "retweetedPost": { "likeCount": 81 } },
{ "id": "2111172163776708859", "likeCount": 120, "retweetedPost": null }
],
"meta": { "nextCursor": "v2.8pRN...", "creditsCharged": 4, "creditsRemaining": 882 }
}
Fed that response, the loop prints:
2026-10-22 133 https://x.com/XDevelopers/status/2113062427566969325
2026-10-17 81 https://x.com/XDevelopers/status/2111520482914877569
2026-10-16 120 https://x.com/XDevelopers/status/2111172163776708859
4 credits
What to notice:
- Two of the three are reposts. A repost's own
likeCountis 0, so the loop readsretweetedPost. meta.nextCursormeans more pages. Send it back ascursorwith the same arguments until it is null.contentrepeats the JSON as text, for clients that ignorestructured_content.
Each call costs 1 credit plus 1 per post. limit runs from 1 to 50 and defaults to 10:
So 5,000 free credits cover about 450 ten-post searches. Price a bigger job with the X API cost calculator.
Step 5: Handle 402, 429 and other tool errors
After you connect, Weir reports most problems inside the tool result, not as exceptions:
| Problem | Where it shows up | What to do |
|---|---|---|
| Bad or missing key | HTTP 401 at connect | Fix the key |
| Key lacks a scope | is_error result |
New key with the scope |
| Out of credits (402) | is_error result |
Stop and add credits |
| Rate limited (429) | is_error result |
Wait, then retry |
| Bad arguments | is_error result |
Fix the arguments |
| Unknown tool name | MCPError is raised |
Check list_tools() |
We tested the first row live. The rest come from Weir's server code and tests.
Weir's error JSON carries retryable, and its errors docs say to branch on it. This helper retries a 429 and stops on everything else:
async def call(client, name, arguments, tries=3):
for _ in range(tries):
result = await client.call_tool(name, arguments)
if not result.is_error:
return result.structured_content
message = result.content[0].text
error = (result.structured_content or {}).get("error", {})
if not error.get("retryable"):
raise RuntimeError(message) # 402, 403, 404, bad arguments
wait = re.search(r"after (\d+) seconds", message)
await asyncio.sleep(int(wait.group(1)) if wait else 10)
raise RuntimeError(f"{name} kept failing: {message}")
In search(), replace the first four lines with page = await call(client, "search_x", {...}).
Why an older MCP client example in Python breaks
Version 2 of the SDK renamed and moved things, so code written for version 1 fails. We hit each of these on 2.3.0:
| Old code | Error on 2.3.0 | Use instead |
|---|---|---|
streamablehttp_client(url) |
ImportError |
streamable_http_client(url) |
streamable_http_client(url, headers=) |
TypeError |
httpx2.AsyncClient(headers=...) |
as (read, write, _) |
ValueError |
as (read, write), or Client |
result.structuredContent |
AttributeError |
result.structured_content |
import httpx |
Not installed | import httpx2 |
| No timeout on the HTTP client | Gives up at 5 seconds | httpx2.Timeout(30, read=300) |
One more trap: Client raises a RuntimeError when you call it outside both async with blocks.
When not to use Weir or a raw client
- You want an agent, not a script. Frameworks wrap this same connection. See our LangChain guide.
- You only need the data. Weir's REST API is plain HTTP with no protocol layer, as in getting tweets with Python.
- You need to post or act on X. Weir is read-only. Our X official MCP server guide covers what X's own tools can write.
- A contract with X is required. Weir reads public data and is not licensed by X, so access can change.
FAQ
How do I connect to a remote MCP server in Python?
Put your auth header on an httpx2.AsyncClient and pass it to streamable_http_client(url, http_client=...). Wrap that in Client(...) inside async with, then call list_tools() and call_tool().
What is the difference between Client and ClientSession?
ClientSession speaks the protocol over a pair of streams. Client wraps it with the transport and the version handshake, so it is the shorter route. client.session still reaches the session for lower-level calls.
Can I use FastMCP as an MCP Python client?
Yes. FastMCP 4.1.3's Client takes a URL and auth="<token>", according to its bearer-auth docs. We didn't install it for this guide, so test it yourself. The official mcp package needs a few more lines.
Why does my MCP client get a 401 from the server?
The key is missing, mistyped, revoked or expired. Weir answers 401 before any tool runs, and the WWW-Authenticate header says why. The hook from Step 2 prints it. Retrying with the same key never helps.
Sources
All checked on 22 October 2026:
- mcp on PyPI (2.3.0, 2 October 2026): https://pypi.org/project/mcp/
- MCP Python SDK docs: https://py.sdk.modelcontextprotocol.io/client/, https://py.sdk.modelcontextprotocol.io/client/transports/ and https://py.sdk.modelcontextprotocol.io/migration/
- FastMCP on PyPI (4.1.3, 20 October 2026) and its bearer auth docs: https://pypi.org/project/fastmcp/ and https://gofastmcp.com/clients/auth/bearer
- Python 3.11, except*: https://docs.python.org/3/whatsnew/3.11.html
- Weir docs: https://docs.weirapp.io/mcp, https://docs.weirapp.io/authentication and https://docs.weirapp.io/errors
- Our runs with mcp 2.3.0 against https://api.weirapp.io/mcp and a local stub, 22 October 2026