Source Intelligence
Sweep 28 Aug 2026 ยท 00:00Z Build v2.1.250 478 read Stable v2.1.236 Latest v2.1.250 Next v2.1.250 Feeds RSS JSON llms.txt

DisclaimerUnofficial, and not affiliated with Anthropic. Nearly all of this is read straight out of what ships: npm bundles, captured prompts, published docs. Anthropic's own notes go in verbatim, marked as theirs. The rest is my reading, and every entry carries the strings behind it. If one looks wrong, vote it down and say why.

One change

Build an MCP client

docs/draft/develop/build-client

The page's own history The capture it came from

Nearest release: v2.1.246, published 8 hours before this site recorded the change. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

docs/draft/develop/build-client New page · 2518 lines, new page

# Build an MCP client ## Next steps

A whole new page. There's nothing to diff it against, so here is what it says.

# Build an MCP client

> Get started building your own client that can integrate with all MCP servers.

In this tutorial, you'll learn how to build an LLM-powered chatbot client that connects to MCP servers.

Before you begin, it helps to have gone through our [Build an MCP Server](/docs/draft/develop/build-server) tutorial so you can understand how clients and servers communicate.

<Tabs>
  <Tab title="Python">
    [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-python)

    ## System Requirements

    Before starting, ensure your system meets these requirements:

    * Mac or Windows computer
    * Latest Python version installed
    * Latest version of `uv` installed
    * You must use the Python MCP SDK 2.0.0 or higher

    ## Setting Up Your Environment

    First, create a new Python project with `uv`:

    <CodeGroup>
      ```bash macOS/Linux theme={null}
      # Create project directory
      uv init mcp-client
      cd mcp-client

      # Create virtual environment
      uv venv

      # Activate virtual environment
      source .venv/bin/activate

      # Install required packages
      uv add mcp anthropic python-dotenv

      # Remove boilerplate files
      rm main.py

      # Create our main file
      touch client.py
      ```

      ```powershell Windows theme={null}
      # Create project directory
      uv init mcp-client
      cd mcp-client

      # Create virtual environment
      uv venv

      # Activate virtual environment
      .venv\Scripts\activate

      # Install required packages
      uv add mcp anthropic python-dotenv

      # Remove boilerplate files
      del main.py

      # Create our main file
      new-item client.py
      ```
    </CodeGroup>

    ## Setting Up Your API Key

    You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).

    Create a `.env` file to store it:

    ```bash theme={null}
    echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
    ```

    Add `.env` to your `.gitignore`:

    ```bash theme={null}
    echo ".env" >> .gitignore
    ```

    <Warning>
      Make sure you keep your `ANTHROPIC_API_KEY` secure!
    </Warning>

    ## Creating the Client

    ### Imports and Setup

    First, let's set up our imports and the pieces the rest of the file shares:

    ```python theme={null}
    import asyncio
    import sys

    from mcp import Client, StdioServerParameters
    from mcp.client.stdio import stdio_client
    from mcp_types import TextContent

    from anthropic import Anthropic
    from dotenv import load_dotenv

    load_dotenv()  # load environment variables from .env

    MODEL = "claude-opus-5"
    anthropic = Anthropic()
    ```

    `Client` is the single object your program talks to the server through. Listing the tools, calling one, reading a resource: each of those is a method on it.

    ### Server Connection Management

    Next, we'll work out which process to launch for a given server script:

    ```python theme={null}
    def server_params(server_script_path: str) -> StdioServerParameters:
        """Describe the subprocess that runs an MCP server

        Args:
            server_script_path: Path to the server script (.py or .js)
        """
        if server_script_path.endswith(".py"):
            command = "python"
        elif server_script_path.endswith(".js"):
            command = "node"
        else:
            raise ValueError("Server script must be a .py or .js file")

        return StdioServerParameters(command=command, args=[server_script_path])
    ```

    `StdioServerParameters` is configuration, not a connection. `stdio_client()` turns it into a stdio transport, and `Client` opens that transport when you enter its `async with` block. We'll do both in `main()`.

    ### Query Processing Logic

    Now let's add the core functionality for processing queries and handling tool calls:

    ```python theme={null}
    async def process_query(client: Client, query: str) -> str:
        """Process a query using Claude and available tools"""
        messages = [
            {
                "role": "user",
                "content": query
            }
        ]

        tool_list = await client.list_tools()
        available_tools = [{
            "name": tool.name,
            "description": tool.description,
            "input_schema": tool.input_schema
        } for tool in tool_list.tools]

        # Initial Claude API call
        response = anthropic.messages.create(
            model=MODEL,
            max_tokens=1000,
            messages=messages,
            tools=available_tools
        )

        # Process response and handle tool calls
        final_text = []
        tool_results = []

        for content in response.content:
            if content.type == 'text':
                final_text.append(content.text)
            elif content.type == 'tool_use':
                tool_name = content.name
                tool_args = content.input

                # Execute tool call
                result = await client.call_tool(tool_name, tool_args)
                final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")

                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": content.id,
                    "content": "\n".join(
                        block.text
                        for block in result.content
                        if isinstance(block, TextContent)
                    ),
                    "is_error": result.is_error
                })

        if tool_results:
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

            # Get next response from Claude
            response = anthropic.messages.create(
                model=MODEL,
                max_tokens=1000,
                messages=messages,
                tools=available_tools
            )

            for content in response.content:
                if content.type == 'text':
                    final_text.append(content.text)

        return "\n".join(final_text)
    ```

    `call_tool` returns a `CallToolResult`. Its `content` is a list of blocks, which is why we narrow to `TextContent` before reading `.text`. A tool that raises does not raise here: it answers with `is_error` set, and passing that flag on lets Claude read the message and try something else.

    ### Interactive Chat Interface

    Now we'll add the chat loop:

    ```python theme={null}
    async def chat_loop(client: Client) -> None:
        """Run an interactive chat loop"""
        print("\nMCP Client Started!")
        print("Type your queries or 'quit' to exit.")

        while True:
            try:
                query = (await asyncio.to_thread(input, "\nQuery: ")).strip()
            except EOFError:
                break

            if query.lower() == 'quit':
                break

            try:
                response = await process_query(client, query)
                print("\n" + response)
            except Exception as e:
                print(f"\nError: {e}")
    ```

    `input()` blocks, so it runs on a worker thread. That keeps the event loop free to service the connection while you type.

    ### Main Entry Point

    Finally, we'll add the main execution logic:

    ```python theme={null}
    async def main() -> None:
        if len(sys.argv) < 2:
            print("Usage: python client.py <path_to_server_script>")
            sys.exit(1)

        async with Client(stdio_client(server_params(sys.argv[1]))) as client:
            tool_list = await client.list_tools()
            tool_names = [tool.name for tool in tool_list.tools]
            print("\nConnected to server with tools:", tool_names)

            await chat_loop(client)


    if __name__ == "__main__":
        asyncio.run(main())
    ```

    That `async with` is the entire connection lifecycle. Entering it launches the server and agrees a protocol version with it; leaving it disconnects and shuts the subprocess down. There is nothing to close by hand.

    You can find the complete `client.py` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-python/client.py).

    ## Key Components Explained

    ### 1. Client Initialization

    * A single `Client` carries the connection, and `async with` is its whole lifecycle
    * There is no connect/close pair to call and nothing to clean up afterwards
    * Configures the Anthropic client for Claude interactions

    ### 2. Server Connection

    * Supports both Python and Node.js servers
    * Validates server script type
    * Launches the server as a subprocess and speaks stdio to it
    * Lists the available tools once the connection is open

    ### 3. Query Processing

    * Maintains conversation context
    * Handles Claude's responses and tool calls
    * Manages the message flow between Claude and tools
    * Combines results into a coherent response

    ### 4. Interactive Interface

    * Provides a simple command-line interface
    * Handles user input and displays responses
    * Includes basic error handling
    * Allows graceful exit

    ### 5. Resource Management

    * Leaving the `async with` block disconnects and shuts the server subprocess down
    * A failing query is reported without ending the session

Cut at 300 lines. The page has the rest.