Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · mcp

Build an MCP client changeddocs/2026-07-28/develop/build-client

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

Recorded here
Lines+2,518added
Lines−0removed
From line — no hunk to open at
First seen 14 Aug 2026 this site's first read of the page
Recorded edits2to this page, all time

# Build an MCP client ## Next steps

The whole hunk

2518 lines, first recorded
/
lines

The first capture of this source. The page was already there, and this is what it said.

# 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/2026-07-28/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.

Feedback