docs/quickstart.mdx
2024-12-07 18:45:23 -05:00

614 lines
No EOL
20 KiB
Text

---
title: "Quickstart"
description: "Get started with building your first MCP server and client in just a few minutes."
---
In this quickstart, you'll learn the ins and outs of MCP by building your first client and server. We'll start with a basic setup, and then progress to more complex use cases.
You can think of MCP as a bridge between your LLMs and your data and tools. Model Context Protocol was developed by Anthropic (the makers of [Claude](https://claude.ai)), but MCP can work with any Large Language Model provider that supports tool calling.
### What we'll be building
Many LLMs (including Claude) do not currently have the ability to know the current time and date. Let's use MCP to fix that!
In this quickstart, we'll start by building a Claude CLI chatbot which connects to a simple server that exposes two tools: `get_current_time` and `convert_time`.
This will allow us to ask Claude questions like:
"What time is it in NYC?"
"When it's 4 PM in New York, what time is it in London?"
### Prerequisite knowledge
This quickstart assumes you have familiarity with:
- Python
- LLMs like Claude
It also helps to be familiar with [tool use (aka function calling)](https://docs.anthropic.com/en/docs/build-with-claude/tool-use), but it's not required.
### System requirements
Before we begin, make sure your system meets the following requirements for best results:
- You're on a Mac or Windows computer
- You have the latest Python version installed
### Set up your environment
First, let's create and activate a virtual environment in a new Python project.
```bash
# Install virtualenv if you don't have it
pip install virtualenv
# Create a new directory for our project
mkdir mcp-quickstart
cd mcp-quickstart
# Create virtual environment
python -m virtualenv venv
# Activate virtual environment
# On Windows:
venv\Scripts\activate
# On Unix or MacOS:
source venv/bin/activate
# Install mcp, anthropic, and python-dotenv
pip install mcp anthropic python-dotenv
```
### Set up Anthropic API key
You'll need an Anthropic API key, which you can get from the [Anthropic Console](https://console.anthropic.com/settings/keys).
After you've gotten your key, create a `.env` file to store it securely.
```bash
# Creates the .env file
touch .env
```
```bash .env
ANTHROPIC_API_KEY=<your key here>
```
```bash .gitignore
# Make sure to add your .env key to your .gitignore to exclude it from source control!
.env
```
### Creating the client (client.py)
Next, let's create a Python file called `client.py` in your `mcp-quickstart` directory.
```bash
touch client.py
```
Let's start by importing our libraries at the top of the file:
```python client.py
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.
```
Next, we'll create a `class` called `ClaudeCLIChatbot` with a single `__init__` method for now:
```python client.py
# continued from above
class ClaudeCLIChatbot:
def __init__(self):
# This is where we'll keep the MCP client session
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
# Initialize Anthropic client
self.anthropic = Anthropic()
# rest of the class methods will go here
```
Now, lets add a `connect_to_time_server` method. We'll call this method when we first start the main process to actually initialize the connection between our client and our time server.
```python client.py
class ClaudeCLIChatbot:
# continued from above
async def connect_to_time_server(self, server_script_path: str):
"""Connect to the MCP time 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()
response = await self.session.list_tools()
tools = response.tools
print("\nConnected to time server with tools:", [tool.name for tool in tools])
# more methods will go here
```
Let's add a `process_query` method. This is where the following happens:
- It sends the user's query to Claude
- Claude decides which tools to use based on the query
- The client executes the tools and returns the results to Claude
- Claude provides a natural language response
```python client.py
class ClaudeCLIChatbot:
# continued from above
async def process_query(self, query: str) -> str:
"""Use Claude to understand the query and call appropriate 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]
# Make the initial call to the Anthropic API
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=available_tools
)
# Process each content block in the response
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
# Call the tool and get result
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 combined output
return "\n".join(final_text)
# more methods will go here
```
Now we'll add the last two methods to our class: `chat_loop` and `cleanup`. These methods are responsible for the continuous running of our chatbot and the cleanup afterwards.
```python client.py
class ClaudeCLIChatbot:
# continued from above
async def chat_loop(self):
"""Main chat loop"""
print("\nWelcome to the Time Assistant!")
print("Ask me anything about times and timezones. For example:")
print("- What time is it now?")
print("- What time is it in Tokyo?")
print("- When it's 4 PM in New York, what time is it in London?")
print("- Convert 9:30 AM Tokyo time to New York time")
print("\nType 'quit' to exit.")
while True:
try:
query = input("\nWhat would you like to know? ").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):
"""Cleanup resources"""
await self.exit_stack.aclose()
```
The final step for our `client.py` file is to add and call the `main` function that will actually initialize our connection to the MCP time server (which we'll create in the next step) and begin the chat loop.
```python client.py
class ClaudeCLIChatbot:
# methods from above
async def main():
if len(sys.argv) < 2:
print("Usage: python chatbot.py <path_to_server_script>")
sys.exit(1)
chatbot = ClaudeCLIChatbot()
try:
await chatbot.connect_to_time_server(sys.argv[1])
await chatbot.chat_loop()
finally:
await chatbot.cleanup()
if __name__ == "__main__":
import sys
asyncio.run(main())
```
Putting it all together, the `client.py` file should look like this:
```python client.py
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 ClaudeCLIChatbot:
def __init__(self):
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
# Initialize Anthropic client
self.anthropic = Anthropic()
async def connect_to_time_server(self, server_script_path: str):
"""Connect to the MCP time 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()
response = await self.session.list_tools()
tools = response.tools
print("\nConnected to time server with tools:", [tool.name for tool in tools])
async def process_query(self, query: str) -> str:
"""Use Claude to understand the query and call appropriate 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]
# Make the initial call to the Anthropic API
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=available_tools
)
# Process each content block in the response
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
# Call the tool and get result
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 combined output
return "\n".join(final_text)
async def chat_loop(self):
"""Main chat loop"""
print("\nWelcome to the Time Assistant!")
print("Ask me anything about times and timezones. For example:")
print("- What time is it now?")
print("- What time is it in Tokyo?")
print("- When it's 4 PM in New York, what time is it in London?")
print("- Convert 9:30 AM Tokyo time to New York time")
print("\nType 'quit' to exit.")
while True:
try:
query = input("\nWhat would you like to know? ").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):
"""Cleanup resources"""
await self.exit_stack.aclose()
async def main():
if len(sys.argv) < 2:
print("Usage: python chatbot.py <path_to_server_script>")
sys.exit(1)
chatbot = ClaudeCLIChatbot()
try:
await chatbot.connect_to_time_server(sys.argv[1])
await chatbot.chat_loop()
finally:
await chatbot.cleanup()
if __name__ == "__main__":
import sys
asyncio.run(main())
```
### Creating the server (server.py)
Next, let's create our server file that will provide time-related tools to our client. The original source code for the MCP time server can be [found here](https://github.com/modelcontextprotocol/servers/tree/main/src/time).
Create a new file called `server.py` in your project directory.
```python
touch server.py
```
The server will expose two tools:
- `get_current_time`: Gets the current time in a specified timezone
- `convert_time`: Converts times between different timezones
Here's the complete server code with explanations:
```python
from datetime import datetime
from zoneinfo import ZoneInfo
import json
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
async def run_server():
# Create server instance
server = Server("time-server")
# Define our tools
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="get_current_time",
description="Get current time in a specific timezone",
inputSchema={
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "IANA timezone name (e.g., 'UTC', 'America/New_York')"
}
},
"required": ["timezone"]
}
),
Tool(
name="convert_time",
description="Convert time between timezones",
inputSchema={
"type": "object",
"properties": {
"source_timezone": {
"type": "string",
"description": "Source timezone"
},
"time": {
"type": "string",
"description": "Time in HH:MM format"
},
"target_timezone": {
"type": "string",
"description": "Target timezone"
}
},
"required": ["source_timezone", "time", "target_timezone"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
try:
if name == "get_current_time":
timezone = ZoneInfo(arguments["timezone"])
current_time = datetime.now(timezone)
result = {
"time": current_time.isoformat(),
"timezone": str(timezone)
}
elif name == "convert_time":
source_tz = ZoneInfo(arguments["source_timezone"])
target_tz = ZoneInfo(arguments["target_timezone"])
# Parse time
hour, minute = map(int, arguments["time"].split(":"))
source_time = datetime.now(source_tz).replace(
hour=hour,
minute=minute
)
# Convert to target timezone
target_time = source_time.astimezone(target_tz)
result = {
"source_time": source_time.isoformat(),
"target_time": target_time.isoformat(),
"source_timezone": str(source_tz),
"target_timezone": str(target_tz)
}
else:
raise ValueError(f"Unknown tool: {name}")
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
except Exception as e:
raise ValueError(f"Error processing request: {str(e)}")
# Initialize and run the server
options = server.create_initialization_options()
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, options)
if __name__ == "__main__":
import asyncio
asyncio.run(run_server())
```
### Understanding the server code
The server code consists of three main parts:
1. **Tool Definitions**: The `list_tools()` function defines what tools are available and their input schemas. Each tool has:
- A name
- A description
- An input schema that defines what parameters it accepts
2. **Tool Implementation**: The `call_tool()` function contains the actual logic for each tool:
- `get_current_time`: Returns the current time in a specified timezone
- `convert_time`: Converts a time from one timezone to another
3. **Server Setup**: The main server setup code that initializes the MCP server and starts listening for requests
### Running the application
1. Make sure both files are in your project directory:
```
mcp-quickstart/
├── /venv
├── .env
├── client.py
└── server.py
```
2. Run the client, passing the server script as an argument:
```bash
python client.py server.py
```
3. You can now ask questions like:
```
What time is it in New York?
What time was it an hour ago in Sydney?
Convert 3:30 PM PST to EST
When it's 9 AM in London, what time is it in Sydney?
```
<Frame>
<img src="/images/time-server-quickstart.png" />
</Frame>
### What's happening under the hood
When you ask a question:
1. The client sends your question to Claude
2. Claude analyzes the available tools and decides which one(s) to use
3. The client executes the chosen tool(s) through the MCP server
4. The results are sent back to Claude
5. Claude formulates a natural language response
6. The response is displayed to you!
### Next steps
Now that you have a basic MCP application working, you can:
1. Add more tools to the server
2. Enhance the error handling
3. Add support for more time formats
4. Expand the functionality to handle more complex queries
This example demonstrates the power of MCP in connecting LLMs with external tools and data sources. You can use this pattern to build all sorts of applications that combine Claude's natural language understanding with custom tools and services.