WEIR

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.


Published · Updated · 7 min read

Cover image: Remote MCP from plain Python

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:

  1. Install the SDKpip install mcp in a virtual environment.
  2. Connect with your keyAn httpx2 client carries the Authorization header.
  3. List the toolsclient.list_tools() returns names and input schemas.
  4. Call a toolclient.call_tool() returns the tool's JSON.
  5. 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 MCPError inside two exception groups.
  • Set the timeout yourself. A bare httpx2.AsyncClient gives up after 5 seconds, and a search can take longer.
  • The SDK picks the protocol. Client tries 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:

40tools that read X (Twitter), Reddit or your account
4monitor reads, from the monitors:read scope
0credits for get_usage, get_limits and get_identity

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 likeCount is 0, so the loop reads retweetedPost.
  • meta.nextCursor means more pages. Send it back as cursor with the same arguments until it is null.
  • content repeats the JSON as text, for clients that ignore structured_content.

Each call costs 1 credit plus 1 per post. limit runs from 1 to 50 and defaults to 10:

Credits per search_x call
3 posts4 credits
10 posts (the default)11 credits
50 posts (the maximum)51 credits

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: