Model Context Protocol (MCP): connecting AI to tools
MCP is an open standard that lets any AI app use your tools and data. Build a Brew & Bean MCP server in Python with tools, a resource and a prompt, test it with a client, and connect it to an AI app safely.
What you'll build: an MCP server for the Brew & Bean menu, with two tools, one resource and one prompt. You will test it with a small Python client, with the MCP Inspector, and then connect it to an AI app such as Claude Code or Claude Desktop so the AI can answer menu questions using your code.
Prerequisites: Python 3.10+, comfort with functions and type hints, and the idea of tool calling from *Function calling: letting the model use tools*. Node.js is only needed for the optional Inspector step.
The problem MCP solves
In *Function calling*, you wrote tool definitions inside your own app. If you want the same tools in a chat app, a code editor and your own agent, you would write the glue three times, and every AI app would need a custom integration for every service. That is an M x N problem.
Model Context Protocol (MCP) is an open standard that fixes it. A service exposes its capabilities once as an MCP server. Any MCP client (an AI app such as Claude Desktop, Claude Code, or your own agent) can connect and use them. Think of USB: one standard plug, so any device works with any computer.
The three roles
| Role | What it is | Example |
|---|---|---|
| Host | The AI application the user talks to | Claude Desktop, an IDE, your agent |
| Client | The connector inside the host, one per server | Created by the host automatically |
| Server | A program that exposes capabilities | The Brew & Bean server you will write |
What a server can offer
| Capability | Controlled by | Use for |
|---|---|---|
| Tools | The model decides when to call | Actions and lookups: get_price, create_order, search_files |
| Resources | The application decides what to load | Read-only data: a file, a menu, a database row |
| Prompts | The user picks them | Reusable prompt templates, like slash commands |
Tools are the part you will use most. Messages travel as JSON-RPC, over stdio (the client starts your server as a subprocess and talks through its input and output, best for local tools) or over HTTP (for remote servers shared by many users). We use stdio.
Step 1: Install
- 1Create a folder
brew-mcpand a virtual environment:python -m venv .venv, then activate it. - 2Install the SDK:
pip install "mcp<2". - 3Create the two files in the next steps.
SDK versions
The Python MCP SDK had a major version 2 release that renamed FastMCP to MCPServer (import from mcp.server.mcpserver import MCPServer) and changed other APIs. This lesson pins mcp<2 so the code runs as written. If you use version 2, follow its migration guide and rename the class; the decorators work the same way.
Step 2: Write the server
import json
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("brew-and-bean")
MENU = {
"espresso": {"name": "Espresso", "category": "coffee", "price": 120},
"cappuccino": {"name": "Cappuccino", "category": "coffee", "price": 180},
"cold-brew": {"name": "Cold Brew", "category": "coffee", "price": 220},
"filter-coffee": {"name": "Filter Coffee", "category": "coffee", "price": 80},
"masala-chai": {"name": "Masala Chai", "category": "tea", "price": 90},
"croissant": {"name": "Butter Croissant", "category": "bakery", "price": 150},
"banana-bread": {"name": "Banana Bread", "category": "bakery", "price": 130},
"veg-sandwich": {"name": "Veg Grilled Sandwich", "category": "food", "price": 160},
}
@mcp.tool()
def get_price(item_id: str) -> str:
"""Get the price in rupees of one menu item, given its id such as 'espresso'."""
item = MENU.get(item_id)
if item is None:
return f"No item with id '{item_id}'. Valid ids: {', '.join(MENU)}"
return f"{item['name']} costs Rs {item['price']}"
@mcp.tool()
def order_total(item_ids: list[str], quantities: list[int]) -> str:
"""Compute the total in rupees for an order. item_ids and quantities are parallel lists."""
if len(item_ids) != len(quantities):
return "item_ids and quantities must have the same length"
total = 0
for item_id, qty in zip(item_ids, quantities):
if item_id not in MENU:
return f"Unknown item id '{item_id}'. Valid ids: {', '.join(MENU)}"
if qty < 1:
return "Quantities must be at least 1"
total += MENU[item_id]["price"] * qty
return f"Total: Rs {total}"
@mcp.resource("menu://all")
def full_menu() -> str:
"""The complete menu as JSON."""
return json.dumps(MENU, indent=2)
@mcp.prompt()
def recommend(taste: str) -> str:
"""A reusable prompt that asks for a menu recommendation."""
return (
f"A customer likes: {taste}. Using the menu resource menu://all, "
"recommend up to 2 items and give each price. Do not invent items."
)
if __name__ == "__main__":
mcp.run() # speaks MCP over stdio by defaultA complete MCP server over stdio.
What each part does:
- FastMCP("brew-and-bean") creates the server and gives it a name.
- @mcp.tool() registers a function as a tool. The SDK builds the tool's JSON schema from the type hints and uses the docstring as the description the model reads. So write clear docstrings, as you did in *Function calling*.
- The tools return short strings, including helpful errors like the list of valid ids, so the model can correct itself.
- order_total computes money in code. The model should never do arithmetic on prices.
- @mcp.resource("menu://all") exposes read-only data under a URI. @mcp.prompt() exposes a reusable prompt template.
- mcp.run() starts the server on stdio.
Do not print to stdout
With stdio, standard output is the protocol channel. Never use print() in a stdio server: it corrupts the messages and the client disconnects. For debugging, write to standard error: print("debug", file=sys.stderr) (remember import sys), or use the logging module.
Step 3: Test with a Python client
Before connecting a real AI app, test the server with code. This client starts your server, lists its tools, calls one, and reads the resource.
import asyncio
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
# sys.executable = the Python you are running now, so the server uses the same environment
params = StdioServerParameters(command=sys.executable, args=["brew_server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
for tool in tools.tools:
print("tool:", tool.name, "-", tool.description)
result = await session.call_tool(
"order_total",
{"item_ids": ["cappuccino", "masala-chai"], "quantities": [2, 1]},
)
print("result:", result.content[0].text)
menu = await session.read_resource("menu://all")
print("resource starts with:", menu.contents[0].text[:40].replace("\n", " "))
asyncio.run(main())Run it in the same folder as brew_server.py.
python client_demo.pytool: get_price - Get the price in rupees of one menu item, given its id such as 'espresso'.
tool: order_total - Compute the total in rupees for an order. item_ids and quantities are parallel lists.
result: Total: Rs 450
resource starts with: { "espresso": { "name": "Espresso"Check the arithmetic: 2 cappuccinos at Rs 180 is 360, plus one masala chai at Rs 90, makes Rs 450. This is exactly what an AI app does when it connects: initialise, list tools, call tools.
Step 4: Try the Inspector (optional)
The MCP Inspector is a browser tool for testing any server by hand. With Node.js installed:
npx @modelcontextprotocol/inspector python brew_server.pyOpens a local web page. Use the Tools tab to call get_price with item_id set to cold-brew. On Windows, run it from the activated environment so that python points to your venv.
Step 5: Connect it to an AI app
Any MCP-capable app can now use the server. Two common ways, both of which start your script as a subprocess. Use the full path to the Python in your virtual environment, so the app finds the installed mcp package.
claude mcp add brew-and-bean -- /full/path/to/brew-mcp/.venv/bin/python /full/path/to/brew-mcp/brew_server.pyRegisters the server. On Windows use the paths to .venv\Scripts\python.exe and brew_server.py. Run claude mcp list to confirm.
{
"mcpServers": {
"brew-and-bean": {
"command": "/full/path/to/brew-mcp/.venv/bin/python",
"args": ["/full/path/to/brew-mcp/brew_server.py"]
}
}
}Add this in Claude Desktop's settings under Developer, Edit Config, then restart the app. On Windows, write backslashes as double backslashes in JSON strings.
Using the Brew & Bean tools, tell me the total for 2 cold brews and 3 croissants, and show how you got it. Then tell me which of those two items is cheaper.What to expect: The AI should call order_total (or get_price twice) and answer Rs 890: 2 x 220 = 440, plus 3 x 150 = 450. Apps usually ask your permission before running a tool. If it answers without asking, it may have done the sum itself, so check whether the tool was used.
Security: treat MCP servers like installed software
Trust and permissions
An MCP server runs code on your machine with your permissions, and what its tools return goes into the model's context. Only install servers from sources you trust, read what they do, and give them the minimum access (a read-only database user, one folder, not your whole disk). A malicious tool description or tool result can try to steer the model: this is prompt injection, covered in *Guardrails, safety and prompt-injection defence*. Keep the approval prompt on for tools that change things.
Server safety checklist
- Does the server only expose what it needs? Fewer tools, narrower arguments.
- Are tool arguments validated (ids, ranges, lengths) before use?
- Are secrets read from environment variables, never hard-coded or returned in results?
- Do tools that write, spend or delete require user approval?
- Could a tool result contain untrusted text (web pages, emails)? Then treat it as data, not instructions.
MCP or plain function calling?
| Situation | Choice |
|---|---|
| One app, a few tools you control | Plain function calling is simpler |
| The same tools needed in several apps or by other teams | MCP server |
| You want to use ready-made integrations (files, GitHub, databases) | Use existing MCP servers from trusted publishers |
| Remote, multi-user service with login | MCP over HTTP with proper authentication |
How this comes up in interviews
Be ready to answer
- Explain MCP in two sentences: an open standard to connect AI apps to tools and data, so each integration is written once.
- Name the three capabilities: tools, resources, prompts, and who controls each.
- Explain why stdio servers must not print to stdout.
- Discuss the risks: tool poisoning, prompt injection, over-broad permissions, and how you reduce them.
Try it: Add a tool and a resource
Add a tool items_in_category(category: str) that lists the names and prices in a category (coffee, tea, bakery, food). Add a resource menu://category/{category} that returns the same as JSON. Test both with the client.
Try it: Use your own server from an agent
Write a script that starts your server from the client code, converts the listed tools into Gemini function declarations, and runs the agent loop from *AI agents: tools, loops and planning* with session.call_tool as the executor.
Key takeaways
- MCP is an open standard: write a server once, use it from any MCP client (Claude Desktop, Claude Code, IDEs, your own agents).
- A server offers tools (model-controlled), resources (read-only data) and prompts (user-selected templates).
- With the Python SDK, type hints become the tool schema and the docstring becomes the description, so write them for the model.
- In stdio servers never print to stdout. Test with a client script or the Inspector before connecting an app.
- An MCP server runs with your permissions: install only trusted servers, limit access, validate arguments and keep approval on for risky tools.
Check yourself
Finishing the quiz marks the lesson as done.
1. What is the main benefit of MCP?
2. Which MCP capability is chosen by the model during a conversation?
3. Your stdio MCP server connects, then the client disconnects with a parse error after you add print('starting'). Why?
4. In the Python SDK, what becomes the description of a tool that the model reads?
5. Which is the best safety practice for an MCP server that queries a database?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.