Module 5

Model Context Protocol (MCP)

MCP is an open standard for connecting AI applications to external tools and data. Build a connector once as an MCP server, and any MCP-compatible application can use it.

About 35 minutes2 Python examplesIntermediate

What MCP is

The Model Context Protocol was introduced by Anthropic in November 2024 as an open protocol. It defines how an AI application asks an external program what it can do (its tools, data, and prompt templates) and how it uses those capabilities. Messages follow JSON-RPC 2.0, a simple request and response format. The official documentation compares MCP to a USB-C port: one standard connector instead of a custom cable for every device.

Why it is useful

Build once, reuse everywhere

Without a standard, every app needs its own integration for every system. With MCP, one server for your database works in any MCP-compatible host.

Clear boundaries

The server holds credentials and business logic. The model only sees tool descriptions and results, and the host can ask the user before each call.

Shared ecosystem

Open-source and vendor servers exist for common systems such as file systems, Git hosting, databases, and browsers, so you rarely start from zero.

Hosts, clients, and servers

Host

The AI application the user works in, such as a chat app, an IDE, or your own agent. It runs the model and manages permissions.

Client

A connector inside the host. Each client keeps a one-to-one connection with one server.

Server

A program that exposes capabilities. It can run locally (started by the host, talking over stdio) or remotely (over Streamable HTTP).

Tools, resources, and prompts

The three server primitives in MCP
PrimitiveWhat it isWho decides to use itExampleMethods
ToolsFunctions that do something or compute somethingThe model (usually with user approval)get_course_schedule(module)tools/list, tools/call
ResourcesRead-only data identified by a URIThe application or user attaches them as contextcourse://syllabusresources/list, resources/read
PromptsReusable prompt templates with argumentsThe user picks them, often as slash commandssummarize_module(module)prompts/list, prompts/get

Clients can offer features to servers too, such as sampling (a server asks the host's model to generate text). See the specification for the full list.

Interactive MCP workflow

Follow one question from the user, through the host and an MCP server, to an external system and back. Press Play or use Next.

How MCP connects agents to external systems

The model never talks to the database or API directly. It only requests a tool call. The host decides whether to allow it, the client sends it to the right server, and the server, which you control, performs the real operation with its own credentials and validation. This separation is what makes MCP integrations reusable and easier to secure.

Try it: MCP messages and a real server

Example 1 simulates both sides in one program so you can read the actual JSON-RPC messages. Input: five client requests. Output: each request and the server's reply. It is a simplification for learning and skips transport and parts of the handshake.

"""MCP concepts: a client and server exchanging JSON-RPC messages

The Model Context Protocol (MCP) uses JSON-RPC 2.0 messages between a client
(inside the AI application) and a server (which exposes capabilities).
A server can offer three kinds of capabilities:
  * Tools: functions the model can call (tools/list, tools/call)
  * Resources: data the application can read (resources/list, resources/read)
  * Prompts: reusable prompt templates (prompts/list, prompts/get)

Illustrative simulation: this runs both sides in one process and skips the
transport (stdio or HTTP) and parts of the initialization handshake. Message
shapes are simplified. See the next example for the official Python SDK.
"""
import json


class TinyMCPServer:
    """A toy server exposing one tool, one resource, and one prompt."""

    def __init__(self):
        self.tools = {
            "get_course_schedule": {
                "description": "Return the schedule for a course module",
                "inputSchema": {"type": "object", "properties": {"module": {"type": "integer"}}, "required": ["module"]},
            }
        }
        self.resources = {"course://syllabus": "Module 1: Evolution of AI ... Module 10: Governance"}
        self.prompts = {"summarize_module": "Summarize module {module} for a beginner in 3 bullet points."}

    def handle(self, request: dict) -> dict:
        method, params = request["method"], request.get("params", {})
        if method == "initialize":
            result = {"serverInfo": {"name": "course-server"}, "capabilities": {"tools": {}, "resources": {}, "prompts": {}}}
        elif method == "tools/list":
            result = {"tools": [{"name": n, **spec} for n, spec in self.tools.items()]}
        elif method == "tools/call":
            module = params["arguments"]["module"]
            result = {"content": [{"type": "text", "text": f"Module {module} runs on Sunday at 4 pm"}]}
        elif method == "resources/list":
            result = {"resources": [{"uri": u} for u in self.resources]}
        elif method == "resources/read":
            uri = params["uri"]
            result = {"contents": [{"uri": uri, "text": self.resources[uri]}]}
        elif method == "prompts/get":
            text = self.prompts[params["name"]].format(**params["arguments"])
            result = {"messages": [{"role": "user", "content": {"type": "text", "text": text}}]}
        else:
            return {"jsonrpc": "2.0", "id": request["id"], "error": {"code": -32601, "message": "Method not found"}}
        return {"jsonrpc": "2.0", "id": request["id"], "result": result}


class TinyMCPClient:
    def __init__(self, server):
        self.server, self.next_id = server, 1

    def request(self, method: str, params: dict | None = None) -> dict:
        msg = {"jsonrpc": "2.0", "id": self.next_id, "method": method}
        if params:
            msg["params"] = params
        self.next_id += 1
        print("client ->", json.dumps(msg))
        reply = self.server.handle(msg)
        print("server <-", json.dumps(reply), "\n")
        return reply


client = TinyMCPClient(TinyMCPServer())
client.request("initialize")
client.request("tools/list")
client.request("tools/call", {"name": "get_course_schedule", "arguments": {"module": 5}})
client.request("resources/read", {"uri": "course://syllabus"})
client.request("prompts/get", {"name": "summarize_module", "arguments": {"module": 5}})

Example 2 uses the official Python SDK (version 2). The MCPServer class (named FastMCP in version 1 of the SDK) turns decorated Python functions into a tool, a resource, and a prompt. A client then starts the server as a subprocess and calls it. It needs the mcp package, so run it in Colab or locally; the notebook writes the server file and runs the client for you.

"""A real MCP server and client with the official Python SDK

This uses the official `mcp` Python SDK (https://github.com/modelcontextprotocol/python-sdk),
version 2 or later. In version 2 the high-level server class is MCPServer
(it was called FastMCP in version 1).
Part 1 writes a small server with one tool, one resource, and one prompt.
Part 2 starts the server as a subprocess over stdio and calls it from a client.

Run it in Google Colab or locally (Python 3.10+). In a plain script, run the
server and client as two separate files; the notebook does both for you.
"""
# %%
# !pip -q install "mcp[cli]>=2"

# %%
# %%writefile course_server.py
from mcp.server.mcpserver import MCPServer

mcp = MCPServer("Course Helper")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b


@mcp.resource("course://modules/{number}")
def module_title(number: int) -> str:
    """Title of a course module."""
    titles = {1: "Evolution of AI", 2: "Components of AI Agents", 5: "Model Context Protocol"}
    return titles.get(int(number), "Unknown module")


@mcp.prompt()
def explain(topic: str) -> str:
    """A reusable prompt template."""
    return f"Explain {topic} to a beginner using one everyday analogy."


if __name__ == "__main__":
    mcp.run()          # stdio transport by default

# %%
# Part 2: a client that launches the server and talks to it.
import asyncio
import sys

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


async def main():
    params = StdioServerParameters(command=sys.executable, args=["course_server.py"])
    # Server log messages go to a file (notebooks do not always expose a real stderr).
    with open("server.log", "w") as log:
        async with stdio_client(params, errlog=log) as (read, write):
            async with ClientSession(read, write) as session:
                await session.initialize()

                tools = await session.list_tools()
                print("Tools:", [t.name for t in tools.tools])

                result = await session.call_tool("add", arguments={"a": 2, "b": 3})
                print("add(2, 3) ->", result.content[0].text)

                resource = await session.read_resource("course://modules/5")
                print("Resource:", resource.contents[0].text)

                prompt = await session.get_prompt("explain", arguments={"topic": "MCP"})
                print("Prompt:", prompt.messages[0].content.text)


asyncio.run(main())
Test your server visually

The official MCP Inspector is a browser-based tool for connecting to a server, listing its capabilities, and calling tools by hand.

Using MCP safely

  • Trust: only install servers from sources you trust. A server runs code on your machine or sees your data.
  • Least privilege: give each server the minimum access it needs, for example a read-only database user.
  • Consent: keep user approval on for tools that change data, send messages, or spend money.
  • Untrusted content: tool results and descriptions can contain hidden instructions (prompt injection). Treat them as data.

Official documentation

Check your understanding

A course syllabus that the app can attach to the conversation is best exposed as a...