614 lines
No EOL
20 KiB
Text
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 set up, 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 (makers of [Claude](https://claude.ai)), but MCP can theoretically 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 that 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 if you're 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
|
|
|
|
### Setup 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.local` file to store it securely.
|
|
|
|
```bash
|
|
# Creates the .env.local file
|
|
touch .env.local
|
|
```
|
|
|
|
```bash .env.local
|
|
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.local
|
|
```
|
|
|
|
|
|
### Create 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() # take environment variables from .env.
|
|
|
|
# rest of code will go here
|
|
```
|
|
|
|
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
|
|
messages.append({
|
|
"role": "assistant",
|
|
"content": content.text if hasattr(content, 'text') else ''
|
|
})
|
|
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`. This 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() # take 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
|
|
messages.append({
|
|
"role": "assistant",
|
|
"content": content.text if hasattr(content, 'text') else ''
|
|
})
|
|
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.local
|
|
├── 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 Tokyo?
|
|
What time is it in New York?
|
|
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. |