AI Engineering: Build AI-Powered Apps
Hands-onIntermediate 40 min· Agents · 12 of 19

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

RoleWhat it isExample
HostThe AI application the user talks toClaude Desktop, an IDE, your agent
ClientThe connector inside the host, one per serverCreated by the host automatically
ServerA program that exposes capabilitiesThe Brew & Bean server you will write

What a server can offer

CapabilityControlled byUse for
ToolsThe model decides when to callActions and lookups: get_price, create_order, search_files
ResourcesThe application decides what to loadRead-only data: a file, a menu, a database row
PromptsThe user picks themReusable 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

  1. 1Create a folder brew-mcp and a virtual environment: python -m venv .venv, then activate it.
  2. 2Install the SDK: pip install "mcp<2".
  3. 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

brew_server.py
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 default

A 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.

client_demo.py
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.

Terminal
python client_demo.py
You may also see log lines such as 'Processing request of type ListToolsRequest' from the server
tool: 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:

Terminal
npx @modelcontextprotocol/inspector python brew_server.py

Opens 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 Code (macOS/Linux)
claude mcp add brew-and-bean -- /full/path/to/brew-mcp/.venv/bin/python /full/path/to/brew-mcp/brew_server.py

Registers the server. On Windows use the paths to .venv\Scripts\python.exe and brew_server.py. Run claude mcp list to confirm.

claude_desktop_config.json (Claude Desktop)
{
  "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.

Prompt · Test the connection
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

  1. Does the server only expose what it needs? Fewer tools, narrower arguments.
  2. Are tool arguments validated (ids, ranges, lengths) before use?
  3. Are secrets read from environment variables, never hard-coded or returned in results?
  4. Do tools that write, spend or delete require user approval?
  5. Could a tool result contain untrusted text (web pages, emails)? Then treat it as data, not instructions.

MCP or plain function calling?

SituationChoice
One app, a few tools you controlPlain function calling is simpler
The same tools needed in several apps or by other teamsMCP server
You want to use ready-made integrations (files, GitHub, databases)Use existing MCP servers from trusted publishers
Remote, multi-user service with loginMCP over HTTP with proper authentication

How this comes up in interviews

Be ready to answer

  1. Explain MCP in two sentences: an open standard to connect AI apps to tools and data, so each integration is written once.
  2. Name the three capabilities: tools, resources, prompts, and who controls each.
  3. Explain why stdio servers must not print to stdout.
  4. 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

  1. MCP is an open standard: write a server once, use it from any MCP client (Claude Desktop, Claude Code, IDEs, your own agents).
  2. A server offers tools (model-controlled), resources (read-only data) and prompts (user-selected templates).
  3. With the Python SDK, type hints become the tool schema and the docstring becomes the description, so write them for the model.
  4. In stdio servers never print to stdout. Test with a client script or the Inspector before connecting an app.
  5. 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.

Start free