Skip to content

Commit 0d00a75

Browse files
authored
Merge pull request #696 from realpython/python_mcp
completed mcp code
2 parents 4d2d923 + dd4a2f0 commit 0d00a75

File tree

6 files changed

+647
-0
lines changed

6 files changed

+647
-0
lines changed

python-mcp/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Python MCP: Connect Your LLM With the World
2+
3+
This folder contains the source code for [Python MCP: Connect Your LLM With the World](https://realpython.com/python-mcp-connect-your-llm-with-world/).
4+
5+
## Setup
6+
7+
Create a new virtual environment, and run the following command to install Python MCP and the additional requirements for this project:
8+
9+
```console
10+
(venv) $ python -m pip install "mcp[cli]"
11+
```
12+
13+
You'll use [`pytest-asyncio`](https://realpython.com/pytest-python-testing/) to test your MCP server. With that, you've installed all the Python dependencies you'll need for this tutorial, and you're ready to dive into MCP!
14+
15+
## Usage
16+
17+
Once your environment is set up, you can run the MCP server with the following command:
18+
19+
```console
20+
(venv) $ python main.py
21+
```
22+
23+
In `test/test_server.py`, you'll also want to change `SERVER_PATH` to the local path to your `main.py` file. See the tutorial for all the details on what's going on here, along with how to connect your server to Cursor's MCP client.

python-mcp/main.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import asyncio
2+
3+
from mcp.server.fastmcp import FastMCP
4+
from transactional_db import CUSTOMERS_TABLE, ORDERS_TABLE, PRODUCTS_TABLE
5+
6+
mcp = FastMCP("ecommerce_tools")
7+
8+
9+
@mcp.tool()
10+
async def get_customer_info(customer_id: str) -> str:
11+
"""Search for a customer using their unique identifier"""
12+
await asyncio.sleep(1)
13+
customer_info = CUSTOMERS_TABLE.get(customer_id)
14+
15+
if not customer_info:
16+
return "Customer not found"
17+
18+
return str(customer_info)
19+
20+
21+
@mcp.tool()
22+
async def get_order_details(order_id: str) -> str:
23+
"""Get details about a specific order."""
24+
await asyncio.sleep(1)
25+
order = ORDERS_TABLE.get(order_id)
26+
if not order:
27+
return f"No order found with ID {order_id}."
28+
29+
items = [
30+
PRODUCTS_TABLE[sku]["name"]
31+
for sku in order["items"]
32+
if sku in PRODUCTS_TABLE
33+
]
34+
return (
35+
f"Order ID: {order_id}\n"
36+
f"Customer ID: {order['customer_id']}\n"
37+
f"Date: {order['date']}\n"
38+
f"Status: {order['status']}\n"
39+
f"Total: ${order['total']:.2f}\n"
40+
f"Items: {', '.join(items)}"
41+
)
42+
43+
44+
@mcp.tool()
45+
async def check_inventory(product_name: str) -> str:
46+
"""Search inventory for a product by product name."""
47+
await asyncio.sleep(1)
48+
matches = []
49+
for sku, product in PRODUCTS_TABLE.items():
50+
if product_name.lower() in product["name"].lower():
51+
matches.append(
52+
f"{product['name']} (SKU: {sku}) — Stock: {product['stock']}"
53+
)
54+
return "\n".join(matches) if matches else "No matching products found."
55+
56+
57+
@mcp.tool()
58+
async def get_customer_ids_by_name(customer_name: str) -> list[str]:
59+
"""Get customer IDs by using a customer's full name"""
60+
await asyncio.sleep(1)
61+
return [
62+
cust_id
63+
for cust_id, info in CUSTOMERS_TABLE.items()
64+
if info.get("name") == customer_name
65+
]
66+
67+
68+
@mcp.tool()
69+
async def get_orders_by_customer_id(
70+
customer_id: str,
71+
) -> dict[str, dict[str, str]]:
72+
"""Get orders by customer ID"""
73+
await asyncio.sleep(1)
74+
return {
75+
order_id: order
76+
for order_id, order in ORDERS_TABLE.items()
77+
if order.get("customer_id") == customer_id
78+
}
79+
80+
81+
if __name__ == "__main__":
82+
mcp.run(transport="stdio")

python-mcp/pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[project]
2+
name = "python-mcp"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
dependencies = [
8+
"httpx>=0.28.1",
9+
"mcp[cli]>=1.7.1",
10+
"pypdf2>=3.0.1",
11+
"pytest-asyncio>=0.26.0",
12+
]

python-mcp/tests/test_server.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from contextlib import AsyncExitStack
2+
3+
import pytest
4+
from mcp import ClientSession, StdioServerParameters
5+
from mcp.client.stdio import stdio_client
6+
7+
SERVER_PATH = "/path/to/your/python-mcp/main.py"
8+
EXPECTED_TOOLS = [
9+
"get_customer_info",
10+
"get_order_details",
11+
"check_inventory",
12+
"get_customer_ids_by_name",
13+
"get_orders_by_customer_id",
14+
]
15+
16+
17+
@pytest.mark.asyncio
18+
async def test_mcp_server_connection():
19+
"""Connect to an MCP server and verify the tools"""
20+
21+
exit_stack = AsyncExitStack()
22+
23+
server_params = StdioServerParameters(
24+
command="python", args=[SERVER_PATH], env=None
25+
)
26+
27+
stdio_transport = await exit_stack.enter_async_context(
28+
stdio_client(server_params)
29+
)
30+
stdio, write = stdio_transport
31+
session = await exit_stack.enter_async_context(ClientSession(stdio, write))
32+
33+
await session.initialize()
34+
35+
response = await session.list_tools()
36+
tools = response.tools
37+
tool_names = [tool.name for tool in tools]
38+
tool_descriptions = [tool.description for tool in tools]
39+
40+
print("\nYour server has the follow tools:")
41+
for tool_name, tool_description in zip(tool_names, tool_descriptions):
42+
print(f"{tool_name}: {tool_description}")
43+
44+
assert sorted(EXPECTED_TOOLS) == sorted(tool_names)
45+
46+
await exit_stack.aclose()

python-mcp/transactional_db.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
CUSTOMERS_TABLE = {
2+
"CUST123": {
3+
"name": "Alice Johnson",
4+
"email": "[email protected]",
5+
"phone": "555-1234",
6+
},
7+
"CUST456": {
8+
"name": "Bob Smith",
9+
"email": "[email protected]",
10+
"phone": "555-5678",
11+
},
12+
}
13+
14+
ORDERS_TABLE = {
15+
"ORD1001": {
16+
"customer_id": "CUST123",
17+
"date": "2024-04-01",
18+
"status": "Shipped",
19+
"total": 89.99,
20+
"items": ["SKU100", "SKU200"],
21+
},
22+
"ORD1015": {
23+
"customer_id": "CUST123",
24+
"date": "2024-05-17",
25+
"status": "Processing",
26+
"total": 45.50,
27+
"items": ["SKU300"],
28+
},
29+
"ORD1022": {
30+
"customer_id": "CUST456",
31+
"date": "2024-06-04",
32+
"status": "Delivered",
33+
"total": 120.00,
34+
"items": ["SKU100", "SKU100"],
35+
},
36+
}
37+
38+
PRODUCTS_TABLE = {
39+
"SKU100": {"name": "Wireless Mouse", "price": 29.99, "stock": 42},
40+
"SKU200": {"name": "Keyboard", "price": 59.99, "stock": 18},
41+
"SKU300": {"name": "USB-C Cable", "price": 15.50, "stock": 77},
42+
}

0 commit comments

Comments
 (0)