Building an MCP Server
Wrap your shop in a protocol.
Velo Supply's tools are locked inside Juno. If the marketing team wants stock numbers in their assistant, they'd have to copy that code. Wrap those tools in an MCP server, and every AI tool in the company can use them instantly.

- Tools locked inside Juno
- Marketing wants them too
- Wrap them in a server
A server in twenty lines
The official Python library makes this easy. We create a server, with a name. We add our existing tools, with no changes at all. Their type hints and descriptions become the schema. The decorator adds a resource, with an address. And a prompt template. The last line starts the server. That's a complete MCP server, in about twenty lines.
from mcp.server.mcpserver import MCPServer
from shop import check_stock, get_order
server = MCPServer("Velo Supply", log_level="ERROR")
server.add_tool(check_stock)
server.add_tool(get_order)
@server.resource("policy://returns")
def returns_policy() -> str:
"""Velo Supply's returns policy."""
return "Returns are free within thirty days if the item is unused."
@server.prompt()
def polite_reply(issue: str) -> str:
"""A template for a polite support reply."""
return f"Write a calm, two-sentence reply about: {issue}"
if __name__ == "__main__":
server.run()Talking to the server
Now a client. The official client can connect straight to a server object, which is perfect for testing. It lists the tools, which is how any app finds out what a server offers. It calls one. And it reads the resource. Run it. Our tools were discovered automatically, with their descriptions. Then a call worked, through the protocol. No custom integration code at all.
import asyncio
from mcp import Client
from server import server
async def main():
async with Client(server) as client:
tools = await client.list_tools()
for t in tools.tools:
print("Tool:", t.name, "-", t.description)
result = await client.call_tool("check_stock", {"item": "Chain"})
print("Result:", result.content[0].text.replace("\n", " "))
policy = await client.read_resource("policy://returns")
print("Resource:", policy.contents[0].text)
asyncio.run(main())Tool: check_stock - Return how many of an item are in stock.
Tool: get_order - Look up one order by its ID, such as A17.
Result: { "item": "Chain", "in_stock": 14 }
Resource: Returns are free within thirty days if the item is unused.Also uses shop.py
ORDERS = {
"A17": {"item": "Carbon wheelset", "price": 640, "status": "delivered", "days_ago": 12},
"B42": {"item": "Chain", "price": 28, "status": "shipped", "days_ago": 2},
"C09": {"item": "Helmet", "price": 95, "status": "delivered", "days_ago": 45},
}
STOCK = {"Carbon wheelset": 3, "Chain": 14, "Helmet": 0}
REFUNDS = []
def get_order(order_id: str) -> dict:
"""Look up one order by its ID, such as A17."""
return ORDERS.get(order_id, {"error": f"No order {order_id}"})
def check_stock(item: str) -> dict:
"""Return how many of an item are in stock."""
return {"item": item, "in_stock": STOCK.get(item, 0)}
def refund(order_id: str, amount: float) -> dict:
"""Refund money to the customer for an order."""
REFUNDS.append((order_id, amount))
return {"refunded": amount, "order_id": order_id}Also uses server.py
from mcp.server.mcpserver import MCPServer
from shop import check_stock, get_order
server = MCPServer("Velo Supply", log_level="ERROR")
server.add_tool(check_stock)
server.add_tool(get_order)
@server.resource("policy://returns")
def returns_policy() -> str:
"""Velo Supply's returns policy."""
return "Returns are free within thirty days if the item is unused."
@server.prompt()
def polite_reply(issue: str) -> str:
"""A template for a polite support reply."""
return f"Write a calm, two-sentence reply about: {issue}"
if __name__ == "__main__":
server.run()A real model, through MCP
Now we connect a real model to our server. We list the server's tools, and we convert them into declarations for Gemini. The model chooses one, and we send the call through MCP. Run it. The model picked the right tool, and the server replied. Notice that the model and the server have never met. MCP sits in between, and that's what makes everything swappable.
import asyncio, json
from google import genai
from google.genai import types
from mcp import Client
from server import server
async def main():
async with Client(server) as mcp_client:
listed = await mcp_client.list_tools()
tools = [types.FunctionDeclaration(name=t.name, description=t.description,
parameters_json_schema=t.input_schema)
for t in listed.tools]
config = types.GenerateContentConfig(tools=[types.Tool(function_declarations=tools)])
ai = genai.Client()
r = ai.models.generate_content(model="gemini-flash-latest", config=config,
contents="How many of the item Chain do you have in stock?")
call = r.function_calls[0]
print("The model chose:", call.name, dict(call.args))
result = await mcp_client.call_tool(call.name, dict(call.args))
print("The server said:", json.loads(result.content[0].text))
asyncio.run(main())The model chose: check_stock {'item': 'Chain'}
The server said: {'item': 'Chain', 'in_stock': 14}Also uses shop.py
ORDERS = {
"A17": {"item": "Carbon wheelset", "price": 640, "status": "delivered", "days_ago": 12},
"B42": {"item": "Chain", "price": 28, "status": "shipped", "days_ago": 2},
"C09": {"item": "Helmet", "price": 95, "status": "delivered", "days_ago": 45},
}
STOCK = {"Carbon wheelset": 3, "Chain": 14, "Helmet": 0}
REFUNDS = []
def get_order(order_id: str) -> dict:
"""Look up one order by its ID, such as A17."""
return ORDERS.get(order_id, {"error": f"No order {order_id}"})
def check_stock(item: str) -> dict:
"""Return how many of an item are in stock."""
return {"item": item, "in_stock": STOCK.get(item, 0)}
def refund(order_id: str, amount: float) -> dict:
"""Refund money to the customer for an order."""
REFUNDS.append((order_id, amount))
return {"refunded": amount, "order_id": order_id}Also uses server.py
from mcp.server.mcpserver import MCPServer
from shop import check_stock, get_order
server = MCPServer("Velo Supply", log_level="ERROR")
server.add_tool(check_stock)
server.add_tool(get_order)
@server.resource("policy://returns")
def returns_policy() -> str:
"""Velo Supply's returns policy."""
return "Returns are free within thirty days if the item is unused."
@server.prompt()
def polite_reply(issue: str) -> str:
"""A template for a polite support reply."""
return f"Write a calm, two-sentence reply about: {issue}"
if __name__ == "__main__":
server.run()Summary
You've built a server, and connected a client and a model. Next, how real applications connect to servers, and what changed in the protocol in twenty twenty six.
Key takeaways- Tools become servers with little code
- Clients discover tools
- Models stay swappable