---
title: Building MCP Clients
description: "While many users interact with **Model Context Protocol (MCP)** through the Claude Desktop, the real power lies in building your own custom clients. This allows you to integrate MCP servers directly into your own applications, chatbots, or specialized GUIs. By…"
url: "https://csestudy247.com/docs/building-mcp-clients/"
updated: "2026-07-31"
category: MCP
---

# Building MCP Clients

While many users interact with **Model Context Protocol (MCP)** through the Claude Desktop, the real power lies in building your own custom clients. This allows you to integrate MCP servers directly into your own applications, chatbots, or specialized GUIs. By using **LangChain** and **Python**, you can create a seamless connection between Large Language Models (LLMs) and various tools, whether they are hosted locally or remotely.

## What is an MCP Client?

An MCP (Model Context Protocol) Client is a component that enables Al applications (like chatbots) to communicate with MCP servers. While platforms like Claude Desktop come with built-in MCP clients, building your own client allows you to:

1. Create custom chatbots with MCP integration
2. Connect to multiple MCP servers simultaneously
3. Build specialized Al applications with tool access
4. Integrate MCP capabilities into existing applications

## Why Build Your Own MCP Client?

![](https://csestudy247.com/wp-content/uploads/2026/07/WhyBuildYourOwnMCPClient.jpeg)

## MCP Client Development Approaches

When building an **MCP (Model Context Protocol)** client in Python, developers have several options depending on their project's requirements. Some approaches provide complete control over the MCP protocol, while others focus on simplifying development or integrating MCP with AI frameworks. The three most common approaches are:

- **Official MCP Python SDK**
- **FastMCP**
- **LangChain MCP Adapters**

Let's explore each approach in detail.

---

## 2.1 Official MCP Python SDK

The **Official MCP Python SDK** is the reference implementation for building MCP clients and servers in Python. It provides direct access to the Model Context Protocol, allowing developers to establish connections, initialize sessions, discover available tools, invoke tools, and communicate with MCP servers using the official protocol.

Since this SDK exposes the protocol at a lower level, it offers maximum flexibility and complete control over MCP communication. Although it requires more code and a deeper understanding of the protocol, it is the best choice for developers building custom integrations or enterprise-grade applications.

### Example

```
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

server_params = StdioServerParameters(
    command="python",
    args=["server.py"]
)

async with stdio_client(server_params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()

        tools = await session.list_tools()

        print(tools)
```

### How It Works

1. Configure the MCP server using `StdioServerParameters`.
2. Connect to the server using the `stdio_client`.
3. Create a `ClientSession`.
4. Initialize the session.
5. Retrieve the list of available tools from the server.

| **Advantages** | **Limitations** | **Best For** |
| --- | --- | --- |
| Full control over the MCP protocol | More verbose code | Custom MCP clients |
| Access to all official MCP features | Steeper learning curve | Enterprise applications |
| Highly customizable | Requires understanding of MCP concepts | Advanced integrations |
| Suitable for production applications | More boilerplate compared to higher-level libraries | Developers who want full protocol control |

---

# 2.2 FastMCP

**FastMCP** is a high-level Python framework that simplifies both MCP server and client development. Instead of dealing with low-level protocol details, developers can use a clean and intuitive API to connect to MCP servers, discover tools, and invoke them with minimal code.

FastMCP significantly reduces development time and is an excellent choice for rapid prototyping, proof-of-concept projects, and developers who want a more developer-friendly experience.

## Example

```
from fastmcp import Client

async with Client("server.py") as client:

    tools = await client.list_tools()

    result = await client.call_tool(
        "add",
        {
            "a": 5,
            "b": 3
        }
    )

    print(result)
```

### How It Works

1. Create a FastMCP client.
2. Connect to the MCP server.
3. Retrieve all available tools.
4. Invoke a tool by passing its name and parameters.
5. Receive the tool's response.

| **Advantages** | **Limitations** | **Best For** |
| --- | --- | --- |
| Very simple API | Less protocol-level control than the Official MCP SDK | Beginners |
| Less boilerplate code | Abstracts many implementation details | Rapid application development |
| Faster development | Some advanced protocol features may require the Official MCP SDK | Prototype applications |
| Supports local and remote MCP servers | — | General-purpose MCP clients |
| Excellent for rapid prototyping | — | — |

---

# 2.3 LangChain MCP Adapters (Recommended for AI Applications)

The **LangChain MCP Adapters** library provides a bridge between MCP servers and the LangChain ecosystem. It automatically converts MCP tools into LangChain-compatible tools, allowing them to be used directly by LLMs, AI agents, and LangGraph workflows.

The library also supports connecting to multiple MCP servers simultaneously, making it easy to build intelligent applications that can access tools from different servers through a single client.

### Example

```
from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient(servers_config)

tools = await client.get_tools()

print(tools)
```

### How It Works

1. Configure one or more MCP servers.
2. Create a `MultiServerMCPClient`.
3. Connect to all configured servers.
4. Load and convert MCP tools into LangChain tools.
5. Bind the tools to an LLM or AI agent.

| **Advantages** | **Limitations** | **Best For** |
| --- | --- | --- |
| Seamless LangChain integration | Requires knowledge of LangChain | AI agents |
| Works with LangGraph | Additional dependencies | LangChain applications |
| Built-in multi-server support | Less control over low-level MCP protocol | LangGraph workflows |
| Native tool binding with LLMs | — | Chatbots |
| Ideal for AI agents and chatbots | — | RAG systems |
| — | — | Multi-server MCP applications |

---

## Comparison Table

| Feature | Official MCP Python SDK | FastMCP | LangChain MCP Adapters |
| --- | --- | --- | --- |
| **Ease of Use** | ⭐⭐☆☆☆ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐☆ |
| **Multi-Server Support** | Manual | Basic | Built-in |
| **LLM Integration** | Manual | Manual | Native |
| **Protocol Control** | Full | High | Limited |
| **Boilerplate Code** | High | Low | Low |
| **Learning Curve** | Steep | Moderate | Gentle |
| **Best For** | Custom implementations | Rapid development | AI agents & chatbot applications |

---

# Which Approach Should You Choose?

Choosing the right MCP client development approach depends on your project requirements.

- **Choose the Official MCP Python SDK** if you need complete control over the MCP protocol or are building highly customized applications.
- **Choose FastMCP** if you want a simple, developer-friendly API for quickly building MCP clients and servers with minimal code.
- **Choose LangChain MCP Adapters** if you are developing AI agents, chatbots, or RAG applications using LangChain or LangGraph and need seamless integration with MCP tools.

## System Architecture Diagram

![](https://csestudy247.com/wp-content/uploads/2026/07/deepseek_mermaid_20260731_5d8c99-scaled.png)

## Building an MCP Client with LangChain MCP Adapters (Step-by-Step)

In this section, we'll build a simple MCP client using **LangChain MCP Adapters**. The implementation is divided into multiple phases so you can understand the purpose of each component before combining them into a complete application.

---

### Phase 1: Create the Basic Client Skeleton

The first step is to create the basic project structure. Since MCP communication is asynchronous, we'll use Python's `asyncio` module. We'll also load environment variables using `python-dotenv`.

### Code

```
import asyncio
from dotenv import load_dotenv
from langchain_mcp_adapters.client import MultiServerMCPClient

load_dotenv()

async def main():
    """Main async function."""
    pass

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

### Explanation

- Import the required libraries.
- Load environment variables using `load_dotenv()`.
- Create an asynchronous `main()` function.
- Start the application using `asyncio.run()`.

### Key Points

| Feature | Description |
| --- | --- |
| `async/await` | Required because MCP operations are asynchronous. |
| `asyncio.run()` | Starts and executes the async event loop. |
| `load_dotenv()` | Loads API keys and environment variables before creating the client. |

---

## Phase 2: Configure MCP Servers

Next, define the MCP servers that your client will connect to. Each server configuration includes the transport type, executable command, and startup arguments.

### Code

```
SERVERS = {
    "math": {
        "transport": "stdio",
        "command": "/path/to/uv",
        "args": [
            "run",
            "fastmcp",
            "run",
            "/path/to/mcp-math-server/main.py"
        ]
    }
}
```

### Explanation

This configuration launches a local FastMCP server using the **stdio** transport. You can configure multiple servers by adding additional entries to the `SERVERS` dictionary.

### Configuration Parameters

| Parameter | Purpose |
| --- | --- |
| `transport` | Communication method (`stdio`, HTTP, etc.). |
| `command` | Executable used to start the server. |
| `args` | Command-line arguments passed to the server. |

---

## Phase 3: Connect to the Server and Fetch Tools

Create the MCP client, connect to the configured server, and retrieve the available tools.

### Code

```
client = MultiServerMCPClient(SERVERS)

tools = await client.get_tools()

named_tools = {
    tool.name: tool
    for tool in tools
}

print("Available Tools:", list(named_tools.keys()))
```

### Explanation

The client establishes a connection with the configured MCP server and automatically retrieves all available tools. The tools are stored in a dictionary for quick lookup.

### Example Output

```
Available Tools:
['add', 'subtract', 'multiply', 'divide', 'power', 'modulus']
```

---

## Phase 4: Bind MCP Tools to the LLM

Now create the language model and bind the retrieved MCP tools.

### Code

```
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="gpt-4"
)

llm_with_tools = llm.bind_tools(tools)
```

### Explanation

The `bind_tools()` method allows the language model to understand which tools are available and automatically decide when they should be called.

### Key Points

| Feature | Description |
| --- | --- |
| `ChatOpenAI` | Creates the LLM instance. |
| `bind_tools()` | Makes MCP tools available to the model. |

---

## Phase 5: Send a Prompt and Detect Tool Calls

Send the user's question to the LLM and check whether it wants to call an MCP tool.

### Code

```
prompt = "What is the product of 12 and 15 using the math tool?"

response = await llm_with_tools.ainvoke(prompt)

if not response.tool_calls:
    print(response.content)
    return

for tool_call in response.tool_calls:

    print(tool_call["name"])
    print(tool_call["args"])
```

### Explanation

Instead of answering immediately, the LLM may decide that a tool is needed. If so, it returns one or more tool calls.

### Tool Call Example

```
{
    "id": "call_abc123",
    "name": "multiply",
    "args": {
        "a": 12,
        "b": 15
    }
}
```

---

## Phase 6: Execute the Tool

Execute every requested tool and store the results as `ToolMessage` objects.

### Code

```
from langchain_core.messages import ToolMessage
import json

tool_messages = []

for tool_call in response.tool_calls:

    result = await named_tools[
        tool_call["name"]
    ].ainvoke(tool_call["args"])

    tool_messages.append(
        ToolMessage(
            tool_call_id=tool_call["id"],
            content=json.dumps(result)
        )
    )
```

### Explanation

Each MCP tool is executed using its arguments. The result is converted into a `ToolMessage` so that the language model can understand the output.

### Workflow

```
LLM
   │
Requests Tool
   │
   ▼
MCP Client
   │
Executes Tool
   │
   ▼
Returns Result
```

---

## Phase 7: Generate the Final Response

Finally, send the original prompt, the LLM's tool request, and the tool results back to the model so it can generate a natural-language response.

### Code

```
final_response = await llm_with_tools.ainvoke([
    prompt,
    response,
    *tool_messages
])

print(final_response.content)
```

### Explanation

The model now has everything it needs:

- The original user question
- The tool call it generated
- The results returned by the MCP server

Using this complete conversation history, the LLM produces the final answer.

### Complete Workflow

```
User Prompt
      │
      ▼
LLM
      │
Tool Required?
      │
      ▼
MCP Client
      │
Execute Tool
      │
      ▼
Tool Result
      │
      ▼
LLM
      │
      ▼
Final Answer
```

## Connecting to Local MCP Servers

```
SERVERS = {
    "math": {
        "transport": "stdio",
        "command": "uv",   # Or provide the full path to uv.exe
        "args": [
            "run",
            "fastmcp",
            "run",
            "/path/to/math-server/main.py"
        ]
    }
}
```

### Example (Windows)

```
SERVERS = {
    "math": {
        "transport": "stdio",
        "command": "C:/Users/sanjit/AppData/Local/Programs/Python/Python310/Scripts/uv.exe",
        "args": [
            "run",
            "fastmcp",
            "run",
            "D:/Coding/mcp-math-server/main.py"
        ]
    }
}
```

### Example (If `uv` is in your PATH)

```
SERVERS = {    "math": {        "transport": "stdio",        "command": "uv",        "args": [            "run",            "fastmcp",            "run",            "main.py"        ]    }}
```

---

## **Full Code**

```
import asyncio
import json
import sys
from dotenv import load_dotenv

from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage, ToolMessage

# Fix Windows console encoding
sys.stdout.reconfigure(encoding="utf-8")

# Load environment variables
load_dotenv()

# =====================================================
# MCP SERVERS
# =====================================================

SERVERS = {
    # Remote MCP Server (Render)
    "math_remote": {
        "transport": "streamable_http",
        "url": "https://vague-harlequin-bat.fastmcp.app/mcp"
    },

    # Local Manim MCP Server
    "manim-server": {
        "transport": "stdio",
        "command": r"C:\Users\sanji\OneDrive\Desktop\mcp_project\venv\Scripts\python.exe",
        "args": [
            r"C:\Users\sanji\OneDrive\Desktop\mcp_project\manim-mcp-server\src\manim_server.py"
        ],
        "env": {
            "MANIM_EXECUTABLE": r"C:\Users\sanji\OneDrive\Desktop\mcp_project\venv\Scripts\manim.exe"
        }
    }
}

async def main():

    print("=" * 60)
    print("Connecting to MCP Servers...")
    print("=" * 60)

    # Connect to all MCP servers
    try:
        client = MultiServerMCPClient(SERVERS)
        tools = await client.get_tools()

    except Exception as e:
        print("\nFailed to connect to MCP servers.")
        print(e)
        return

    if not tools:
        print("No tools were loaded.")
        return

    # Tool lookup dictionary
    named_tools = {tool.name: tool for tool in tools}

    print("\n========== AVAILABLE TOOLS ==========")

    for i, tool in enumerate(tools, start=1):
        print(f"{i}. {tool.name}")

    print(f"\nTotal Tools Loaded: {len(tools)}")

    # =====================================================
    # Gemini
    # =====================================================

    llm = ChatGoogleGenerativeAI(
        model="gemini-2.5-flash",
        temperature=0,
    )

    llm_with_tools = llm.bind_tools(tools)

    # =====================================================
    # Prompt
    # =====================================================

    messages = [
        HumanMessage(
            content="""
Use the available MCP tools.

1. Use add_numbers to calculate 25 + 75.
2. Roll 5 dice using roll_dice.
3. Create a rotating triangle animation using execute_manim_code.

Do not calculate anything yourself.
Always use the available MCP tools.
"""
        )
    ]

    # =====================================================
    # First LLM Response
    # =====================================================

    response = await llm_with_tools.ainvoke(messages)

    print("\n========== MODEL RESPONSE ==========")
    print(response.content)

    print("\n========== TOOL CALLS ==========")

    if not response.tool_calls:
        print("No tool calls generated.")
        return

    for tool_call in response.tool_calls:
        print(json.dumps(tool_call, indent=4))

    # =====================================================
    # Execute Tools
    # =====================================================

    tool_messages = []

    for tool_call in response.tool_calls:

        tool_name = tool_call["name"]
        tool_args = tool_call["args"]

        print("\n" + "-" * 60)
        print(f"Calling Tool : {tool_name}")
        print(f"Arguments    : {tool_args}")

        tool = named_tools.get(tool_name)

        if tool is None:
            print(f"Tool '{tool_name}' not found.")
            continue

        try:

            result = await tool.ainvoke(tool_args)

            print("\nTool Result:")
            print(json.dumps(result, indent=4, default=str))

            tool_messages.append(
                ToolMessage(
                    tool_call_id=tool_call["id"],
                    content=json.dumps(result, default=str)
                )
            )

        except Exception as e:

            print("\nTool Error:")
            print(e)

            tool_messages.append(
                ToolMessage(
                    tool_call_id=tool_call["id"],
                    content=str(e)
                )
            )

    # =====================================================
    # Final LLM Response
    # =====================================================

    final_response = await llm_with_tools.ainvoke(
        messages + [response] + tool_messages
    )

    print("\n" + "=" * 60)
    print("FINAL RESPONSE")
    print("=" * 60)

    print(final_response.content)

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

Output

============================================================

# Connecting to MCP Servers…

========== AVAILABLE TOOLS ==========

1. add_numbers
2. roll_dice
3. execute_manim_code
4. cleanup_manim_temp_dir

Total Tools Loaded: 4

========== TOOL CALLS ==========
Calling Tool : add_numbers
Arguments : {'a': 25, 'b': 75}

Tool Result:
100

---

Calling Tool : roll_dice
Arguments : {'n_dice': 5}

Tool Result:
\[2, 6, 1, 4, 3\]

---

Calling Tool : execute_manim_code
Arguments : {…}

Tool Result:
{
…
}

============================================================

# FINAL RESPONSE

The sum of 25 and 75 is 100.

The five dice rolled: 2, 6, 1, 4, 3.

A rotating triangle animation has been created successfully.

## Key Advantages of Custom Clients

- **Multi-Server Integration:** Connect your chatbot to a Math server, an Expense Tracker, and a Manim visualization server all at once.
- **Local & Remote Flexibility:** Mix local tools on your machine with remote APIs.
- **Custom Logic:** You can handle normal conversation and complex tool-assisted queries in a single flow by checking if the LLM response contains `tool_calls`

## Building MCP Clients Quiz

Q.1 When building an MCP client that communicates with multiple MCP servers simultaneously, which class from the langchain_mcp_adapters library is recommended?

- SingleServerClient.
- MCPTransportManager.
- LangChainMCPWrapper.
- MultiServerMCPClient.

    Its name directly indicates support for multiple servers.    [Discuss it](#)

Explanation

MultiServerMCPClient allows a single MCP client to connect to and manage multiple MCP servers simultaneously, making it easy to access tools from different servers within one application.

Q.2 Which transport method is commonly used for communication between a local MCP client and a local MCP server?

- WebSocket.
- STDIO.
- gRPC.
- HTTP POST.

    Think about how local command-line programs exchange data.    [Discuss it](#)

Explanation

STDIO (Standard Input/Output) is commonly used for local MCP servers because both the client and server run on the same machine and communicate through input and output streams.

Q.3 Why does the tutorial use the langchain-mcp-adapters library instead of the official MCP Python library?

- It is the only Python library available.
- It provides a lightweight wrapper that integrates naturally with LangChain and LangGraph.
- The official MCP library cannot connect to remote servers.
- It executes significantly faster.

    Think about compatibility with the LangChain ecosystem.    [Discuss it](#)

Explanation

The langchain-mcp-adapters library provides seamless integration with LangChain and LangGraph, allowing developers to use MCP tools directly inside existing AI workflows.

Q.4 Why must the command and args fields be provided when configuring a local MCP server?

- To allow the client to launch the MCP server process automatically when needed.
- To authenticate the user.
- To download the server from the internet.
- To define mathematical formulas.

    The server may not already be running.    [Discuss it](#)

Explanation

For local servers, the client needs to know which executable to start and what arguments to pass so it can automatically launch the MCP server before communicating with it.

Q.5 After the LLM decides to call a tool, what happens before the final answer is generated?

- The client executes the tool and sends the result back to the LLM inside a ToolMessage.
- The client restarts the MCP server.
- The client manually verifies the result.
- The user must approve the tool call in the terminal.

    The LLM must receive the tool's output before completing its response.    [Discuss it](#)

Explanation

The MCP client invokes the requested tool, packages the result inside a ToolMessage, and sends it back to the LLM so it can generate the final response using the retrieved information.

Q.6 Why was the implementation changed from accessing only the first tool call to looping through all tool calls?

- To avoid crashes when no tools exist.
- To connect to more servers.
- To improve calculation speed.
- Because the LLM may request multiple tool executions for a single prompt.

    A user may request several actions in one question.    [Discuss it](#)

Explanation

Modern LLMs can generate multiple tool calls in one response. Looping through all tool_calls ensures every requested tool is executed before producing the final answer.

Q.7 Which transport protocol is used for the Remote Expense Tracker MCP server demonstrated in the tutorial?

- SSE (Server-Sent Events).
- UDP.
- FTP.
- STDIO.

    It supports streaming communication over HTTP.    [Discuss it](#)

Explanation

The remote Expense Tracker server communicates using Server-Sent Events (SSE), allowing streamed communication between the remote MCP server and the client.

Q.8 What is the purpose of the tool_call_id when creating a ToolMessage?

- It identifies the user.
- It acts as a password.
- It selects the tool version.
- It allows the LLM to associate the returned result with the correct tool request.

    Think about matching each response with its corresponding request.    [Discuss it](#)

Explanation

The tool_call_id uniquely identifies the original tool request so the LLM knows which returned result belongs to which tool invocation, especially when multiple tools are called.

Q.9 What is the purpose of the bind_tools() method?

- It merges server code into the client.
- It encrypts communication.
- It generates a Streamlit interface.
- It informs the LLM about available tools and their schemas so it can decide when to use them.

    The LLM cannot call tools it doesn't know about.    [Discuss it](#)

Explanation

bind_tools() registers the available tools with the LLM, including their names, descriptions, and input schemas, enabling intelligent tool selection during inference.

Q.10 If a user asks 'What is the capital of India?' how does the MCP client avoid unnecessary tool execution?

- It forces the math server to answer.
- It checks whether the LLM returned any tool_calls, and if none exist, it simply returns the generated text response.
- It switches to another LLM.
- It automatically uses the Manim server.

    Not every question requires a tool.    [Discuss it](#)

Explanation

The implementation first checks whether the LLM response contains any tool_calls. If none are present, the client directly returns the LLM's textual answer without invoking any MCP tools.

[How to build MCP Clients | MCP Trilogy | CampusX](https://www.youtube.com/embed/o4ajsc-tSBc?list=PLKnIA16_Rmva_oZ9F4ayUu9qcWgF7Fyc0)
