docs/tutorials/building-a-client.mdx
2024-12-09 22:45:23 -05:00

289 lines
No EOL
8.1 KiB
Text

---
title: "Building a MCP client"
description: "Learn how to build your first client in MCP"
---
In this tutorial, you'll learn how to build a client application that connects to MCP servers.
## Prerequisite Knowledge
This tutorial assumes familiarity with:
- Python
- LLMs like Claude
It helps to understand [tool use (aka function calling)](https://docs.anthropic.com/en/docs/build-with-claude/tool-use), but it's not required.
## System Requirements
Before starting, ensure your system meets these requirements:
- Mac or Windows computer
- Latest Python version installed
## Setting Up Your Environment
First, create and activate a virtual environment:
```bash
# Install virtualenv if needed
pip install virtualenv
# Create project directory
mkdir mcp-client
cd mcp-client
# Create virtual environment
python -m virtualenv venv
# Activate virtual environment
# On Windows:
venv\Scripts\activate
# On Unix or MacOS:
source venv/bin/activate
# Install required packages
pip install mcp anthropic python-dotenv
```
## 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
# Create .env file
touch .env
```
Add your key to the `.env` file:
```bash
ANTHROPIC_API_KEY=<your key here>
```
Add `.env` to your `.gitignore`:
```bash
echo ".env" >> .gitignore
```
## Creating the Client
Let's create a Python client that can connect to any MCP server. We'll call it `client.py`:
```python
import asyncio
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv() # load environment variables from .env
class MCPClient:
def __init__(self):
# Initialize session and client objects
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.anthropic = Anthropic()
async def connect_to_server(self, server_script_path: str):
"""Connect to an MCP server"""
server_params = StdioServerParameters(
command="python",
args=[server_script_path],
env=None
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()
# List available tools
response = await self.session.list_tools()
tools = response.tools
print("\nConnected to server with tools:", [tool.name for tool in tools])
async def process_query(self, query: str) -> str:
"""Process a query using Claude and available tools"""
messages = [
{
"role": "user",
"content": query
}
]
response = await self.session.list_tools()
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
} for tool in response.tools]
# Initial Claude API call
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=available_tools
)
# Process response and handle tool calls
tool_results = []
final_text = []
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 self.session.call_tool(tool_name, tool_args)
tool_results.append({"call": tool_name, "result": result})
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
# Continue conversation with tool results
if hasattr(content, 'text') and content.text:
messages.append({
"role": "assistant",
"content": content.text
})
messages.append({
"role": "user",
"content": result.content
})
# Get next response from Claude
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
)
final_text.append(response.content[0].text)
return "\n".join(final_text)
async def chat_loop(self):
"""Run an interactive chat loop"""
print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.")
while True:
try:
query = input("\nQuery: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query)
print("\n" + response)
except Exception as e:
print(f"\nError: {str(e)}")
async def cleanup(self):
"""Clean up resources"""
await self.exit_stack.aclose()
async def main():
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)
client = MCPClient()
try:
await client.connect_to_server(sys.argv[1])
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
import sys
asyncio.run(main())
```
## Understanding the Client Code
Let's break down the key components of our MCP client:
1. **MCPClient Class**
- Manages the connection to the MCP server
- Handles communication between Claude and the server's tools
- Maintains the chat session
2. **Key Methods**
- `connect_to_server()`: Establishes connection with an MCP server
- `process_query()`: Handles user queries by:
- Getting available tools from the server
- Sending the query to Claude with tool information
- Executing any tool calls Claude requests
- Processing Claude's responses
- `chat_loop()`: Runs the interactive chat interface
- `cleanup()`: Properly closes connections and resources
## Running the Client
To run your client with any MCP server:
```bash
python client.py path/to/server.py
```
The client will:
1. Connect to the specified server
2. List available tools
3. Start an interactive chat session where you can:
- Enter queries
- See tool executions
- Get responses from Claude
## How It Works
When you submit a query:
1. The client gets the list of available tools from the server
2. Your query is sent to Claude along with tool descriptions
3. Claude decides which tools (if any) to use
4. The client executes any requested tool calls through the server
5. Results are sent back to Claude
6. Claude provides a natural language response
7. The response is displayed to you
## Best Practices
1. **Error Handling**
- Always wrap tool calls in try-catch blocks
- Provide meaningful error messages
- Gracefully handle connection issues
2. **Resource Management**
- Use `AsyncExitStack` for proper cleanup
- Close connections when done
- Handle server disconnections
3. **Security**
- Store API keys securely in `.env`
- Validate server responses
- Be cautious with tool permissions
## Next Steps
Now that you have a basic MCP client, you can:
1. Add support for different server types
2. Enhance error handling and recovery
3. Implement more sophisticated conversation handling
4. Add authentication and security features
5. Create a better user interface
## Resources
- [MCP Documentation](https://modelcontextprotocol.github.io/)
- [Claude API Documentation](https://docs.anthropic.com/claude/reference)
- [Tool Use Guide](https://docs.anthropic.com/claude/docs/function-calling)