# Applied AI — Advanced Level
# Session 5: MCP in depth

**Program:** Applied AI — Professional training in artificial intelligence
**Instructor:** Yann Isola
**Level:** Advanced — Solutions Architects preparing for certification *Claude Certified Architect*
**Recommended duration:** 3.5 hours (2 hours of lecture + 1.5 hours of practical exercises)
**Prerequisites:** Sessions 1 to 4 of the advanced level (orchestration, tools/function calling, context management, multi-agent architecture)

---

## Educational objectives

At the end of this session, participants will be able to:

1. **Describe** the complete architecture of the Model Context Protocol (MCP): Host ↔ Client ↔ Server, and the exact role of each component.
2. **Choose and justify** a transport layer (local stdio vs remote HTTP Streamable) according to architectural, security and deployment criteria.
3. **Distinguish** the three MCP primitives — Tools, Resources, Prompts — and select the correct primitive depending on **who controls the invocation** (the model, the application or the user). This is a central examination point for certification.
4. **Build** a complete MCP server in Python (official SDK) exposing tools, resources and prompts, with integrated guardrails.
5. **Build** an MCP client: session management, tool discovery, capacity negotiation.
6. **Apply** advanced patterns: dynamic tool registration, resource subscription with notifications, prompt chaining, normalization of heterogeneous data.

---

## Session plan

| Block | Duration | Content |
|------|-------|---------|
| 1 | 30 mins | MCP Architecture: Host, Client, Server, JSON-RPC |
| 2 | 25 mins | Transport layer: stdio vs Streamable HTTP |
| 3 | 35 mins | The three primitives: Tools, Resources, Prompts |
| 4 | 30 mins | Build an MCP Server and Client (Python SDK) |
| 5 | 20 mins | Security, ecosystem and advanced patterns |
| 6 | 90 mins | Practical exercises + interactive demonstration (web page) |
| 7 | 10 mins | Validation quiz and certification summary |

---

# Block 1 — MCP Architecture: Host, Client, Server

## 1.1 The problem that MCP solves

Before MCP, each AI application editor had to write a specific connector for each external tool: N applications × M tools = N×M integrations. This is the classic “M×N” problem that standards solve (like USB-C did for connectivity, or LSP — *Language Server Protocol* — for code editors).

**MCP transforms M×N into M+N**: each tool exposes a single standard interface (an MCP server), each application implements a single standard interface (an MCP client). Any server works with any compatible host.

> **Educational point:** the USB-C analogy is officially used by Anthropic. Participants will find it in the certification documentation. Have a participant rephrase it: “MCP is to AI what USB-C is to hardware: a universal port. »

## 1.2 The three components```
┌─────────────────────────── HÔTE ───────────────────────────┐
│  (IDE, application de chat, agent autonome)                 │
│                                                             │
│   ┌──────────┐      ┌──────────┐      ┌──────────┐          │
│   │ Client 1 │      │ Client 2 │      │ Client 3 │          │
│   └────┬─────┘      └────┬─────┘      └────┬─────┘          │
└────────┼─────────────────┼─────────────────┼────────────────┘
         │ JSON-RPC        │ JSON-RPC        │ JSON-RPC
    ┌────▼─────┐      ┌────▼─────┐      ┌────▼─────┐
    │ Serveur  │      │ Serveur  │      │ Serveur  │
    │filesystem│      │  GitHub  │      │PostgreSQL│
    └──────────┘      └──────────┘      └──────────┘
```| Component | Role | Examples |
|---------------|------|----------|
| **Host** (*Host*) | The application that embeds the model and controls the user experience. Decides which servers to connect, enforces security and consent policies. | Claude Desktop, an IDE (Cursor, VS Code), a business application |
| **Customer** (*Customer*) | Component managed by the SDK, **one client per server** (strict 1:1 relationship). Maintains session, performs capacity negotiation, routes messages. | Instantiated by the MCP SDK in the host |
| **Server** (*Server*) | Capability Provider: Exposes Tools, Resources, and Prompts via the protocol. Independent process. | Filesystem Server, GitHub Server, Home Server |

**Rule to be hammered out for certification:** the Client:Server relationship is **1:1**. A host connecting to 3 servers instantiates 3 clients. This isolation is a deliberate architectural choice (see Block 5: security).

## 1.3 JSON-RPC 2.0: the common language

All MCP messages are **JSON-RPC 2.0** (*Remote Procedure Call*) messages. Three types of messages:

1. **Request** (*Request*) — waits for a response, carries a `id`:```json
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "rechercher_commande",
    "arguments": { "numero": "CMD-2026-0193" }
  }
}
```2. **Response** (*Response*) — has the same `id`, contains `result` or `error`:```json
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [
      { "type": "text", "text": "{\"statut\": \"expédiée\", \"montant\": 129.90}" }
    ]
  }
}
```3. **Notification** (*Notification*) — no `id`, no response expected:```json
{
  "jsonrpc": "2.0",
  "method": "notifications/resources/updated",
  "params": { "uri": "db://commandes/CMD-2026-0193" }
}
```## 1.4 The life cycle of a session

1. **`initialize`**: the client sends its protocol version and its **capabilities** (what it knows how to manage: sampling, notifications, etc.). The server responds with its own capabilities (tools, resources, prompts, subscriptions...).
2. **`notifications/initialized`**: Client confirms — session is open.
3. **Discovery**: `tools/list`, `resources/list`, `prompts/list`.
4. **Operations**: `tools/call`, `resources/read`, `prompts/get`, subscriptions...
5. **Closing**: proper termination of the transport.

> **Certification point — capacity negotiation:** the client declares what he supports at the *handshake* (initial handshake). A server should never send a subscription notification to a client that has not declared the corresponding capacity. During the exam, you will be asked why negotiation exists: **to allow the evolution of the protocol without breaking compatibility** — an old client and a recent server can cooperate on the intersection of their capabilities.

---

# Block 2 — Transport layer: stdio vs Streamable HTTP

## 2.1 stdio: local transport

The server is started as a **subprocess** of the host. JSON-RPC messages flow to **stdin/stdout** (standard input/output), one JSON message per line.

**Features:**
- Minimum latency (no network).
- Host-related lifecycle: when the host shuts down, the server dies.
- Security by construction: no open network port, the server inherits the permissions of the local user.
- `stderr` remains available for logs (never write logs to stdout — this will corrupt the JSON-RPC stream; classic beginner's mistake, and exam trap question).

**Typical configuration (Claude Desktop):**```json
{
  "mcpServers": {
    "commandes": {
      "command": "python",
      "args": ["/opt/mcp/serveur_commandes.py"],
      "env": { "DB_URL": "postgresql://localhost/boutique" }
    }
  }
}
```## 2.2 Streamable HTTP: remote transport

For remote servers (shared, multi-user, cloud), MCP uses **Streamable HTTP**, which **replaces the old deprecated SSE** (*Server-Sent Events* — events sent by the server) transport.

**Operation:**
- The client sends its JSON-RPC messages in `POST` to a single endpoint (e.g. `/mcp`).
- The server responds either with a simple JSON response, or by opening a **flow** on the same connection to push multiple messages (progressive results, notifications).
- Session support via `Mcp-Session-Id` header, flow resumption possible after interruption.

**Features:**
- Shared server: a deployment serves thousands of clients.
- Web standard authentication: OAuth 2.1, *bearer* tokens, HTTP headers.
- Passes firewalls and corporate infrastructures (proxies, load balancers).

## 2.3 Decision matrix (to know for the exam)

| Criterion | studio | HTTP Streamable |
|---|---|---|
| Location | Same machine as host | Remote/cloud machine |
| Number of users | 1 (the local user) | N (shared) |
| Authentication | Legacy from the operating system | OAuth 2.1 / tokens |
| Latency | Minimal | Network (variable) |
| Deployment | Distributed with host application | Operated as a web service |
| Access to local resources (files, devices) | Direct | Impossible (or via tunnel) |
| Typical use case | Filesystem server, local development tools | Enterprise SaaS server, shared business API |

> **Certification pocket rule:** *“Local and single-user files → stdio. Shared, authenticated, scalable service → Streamable HTTP. SSE only → false answer (deprecated). »*

---

# Block 3 — The three primitives: Tools, Resources, Prompts

This is **the heart of the session and the certification**. The question is not "what does the primitive do?" » but “**who decides on its invocation?**”.

## 3.1 Control table (to be memorized)

| Primitive | Who controls? | Trigger | Analogy |
|---|---|---|---|
| **Tool** (tool) | **The model** (*model-controlled*) | The LLM decides to call the tool during its reasoning | Officer's hands |
| **Resource** (resource) | **The application** (*application-controlled*) | The host decides what data to inject into the context | The Agent's Eyes — read-only |
| **Prompt** (prompt) | **The user** (*user-controlled*) | Human explicitly chooses a prompt template (menu, slash command) | A pre-filled form |

## 3.2 Tools: invoked by the model

- Input described by a **JSON Schema** (JSON schema — data structure description format): the model knows exactly what arguments to provide.
- **Structured** output: list of content blocks (`text`, `image`, embedded resource) + possible typed `structuredContent`.
- Authorized side effects (write, send, reimburse, etc.) — hence the requirement for **user consent** (Block 5).```json
{
  "name": "rembourser_commande",
  "description": "Rembourse une commande. Refusé au-delà de 500 $ sans validation humaine.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "numero": { "type": "string", "description": "Numéro de commande, ex. CMD-2026-0193" },
      "montant": { "type": "number", "description": "Montant en dollars US" }
    },
    "required": ["numero", "montant"]
  }
}
```## 3.3 Resources: exposed by the application

- Identified by **URI** (*Uniform Resource Identifier*): `file:///rapports/q2.pdf`, `db://clients/12345`, `api://meteo/paris`.
- **Read only**: a resource never modifies state. If the action modifies something, it is a Tool.
- **Subscriptions**: a client can subscribe (`resources/subscribe`) and receive `notifications/resources/updated` when the resource changes — the host can then reread and refresh the context.
- **Resource templates**: Parameterized URIs (`db://commandes/{numero}`) to expose entire families of resources.

**Why “application-controlled”?** Because it is the host — not the model — that decides which resources to load in the context. This protects the context window (the host filters) and privacy (the model does not "snoop" freely).

## 3.4 Prompts: triggered by the user

- **Parameterized** prompt templates exposed by the server: name, description, argument list.
- The host presents them to the user (slash command, menu). The user chooses, fills in the arguments, and the prompt generates one or more messages injected into the conversation.
- Use cases: complex and repeatable workflows — “PR analysis”, “incident report”, “contract review” — where we want to guarantee the structure of the request.```json
{
  "name": "rapport_incident",
  "description": "Génère un rapport d'incident structuré",
  "arguments": [
    { "name": "severite", "description": "P1 à P4", "required": true },
    { "name": "systeme", "description": "Système affecté", "required": true }
  ]
}
```## 3.5 Decision tree (reproduced in the interactive web page)

1. **The action modifies a state or triggers a side effect?** → **Tool**.
2. **It is data to be read, and it is the application which must decide when to inject it?** → **Resource**.
3. **This is a workflow that the human explicitly triggers with parameters ?** → **Prompt**.
4. **The model must decide alone, during the reasoning, to fetch the data?** → then even a reading can be a **Tool** (e.g. `rechercher_client`). The Resource/Tool boundary is through **who controls**, not read/write only.

> **Exam trap #1:** “a search in a database is a read, therefore it is a Resource” — **false** if it is the model which must decide to trigger it dynamically. A search invoked by the model is a Tool. Data pre-selected by the application is a Resource.

---

# Block 4 — Build an MCP server and client (Python SDK)

## 4.1 Complete server with guardrails

The official Python SDK (`mcp`, with API `FastMCP`) provides three decorators. Complete example — order management server with **refund safeguards** and **date normalization**:```python
# serveur_commandes.py
# ⚠ Vérifiez la version du SDK : l'API évolue rapidement.
from mcp.server.fastmcp import FastMCP
from datetime import datetime

mcp = FastMCP("commandes")

MAX_REMBOURSEMENT = 500.0  # Garde-fou métier : au-delà, validation humaine

def normaliser_date(valeur: str) -> str:
    """Normalise les dates hétérogènes des systèmes sources vers ISO 8601.
    Les serveurs MCP tiers renvoient des formats variés : c'est au point
    d'intégration de normaliser, jamais au modèle de deviner."""
    formats = ("%Y-%m-%d", "%d/%m/%Y", "%m-%d-%Y", "%d %b %Y", "%Y-%m-%dT%H:%M:%S")
    for fmt in formats:
        try:
            return datetime.strptime(valeur.strip(), fmt).date().isoformat()
        except ValueError:
            continue
    raise ValueError(f"Format de date non reconnu : {valeur!r}")

# ── TOOL : invoqué par le modèle, effets de bord, garde-fou ──
@mcp.tool()
def rembourser_commande(numero: str, montant: float) -> dict:
    """Rembourse une commande. Bloqué au-delà de 500 $ (validation humaine requise)."""
    if montant > MAX_REMBOURSEMENT:
        # Le garde-fou vit CÔTÉ SERVEUR : il s'applique même si le
        # prompt du modèle est manipulé (injection). Défense en profondeur.
        return {
            "statut": "refuse",
            "raison": f"Montant {montant} $ > plafond {MAX_REMBOURSEMENT} $. "
                      "Escalade vers un opérateur humain requise.",
            "escalade": True,
        }
    return {"statut": "effectue", "numero": numero, "montant": montant}

# ── TOOL de lecture contrôlée par le modèle ──
@mcp.tool()
def rechercher_commande(numero: str) -> dict:
    """Recherche une commande par numéro (le modèle décide quand chercher)."""
    brut = {"numero": numero, "date_livraison": "15/03/2026", "statut": "expédiée"}
    brut["date_livraison"] = normaliser_date(brut["date_livraison"])  # → 2026-03-15
    return brut

# ── RESOURCE : donnée en lecture, contrôlée par l'application ──
@mcp.resource("db://commandes/{numero}")
def ressource_commande(numero: str) -> str:
    """Fiche commande complète, exposée à l'hôte via URI paramétrée."""
    return f'{{"numero": "{numero}", "historique": [...], "client": "..."}}'

# ── PROMPT : modèle d'invite déclenché par l'utilisateur ──
@mcp.prompt()
def analyse_litige(numero: str, motif: str) -> str:
    """Flux structuré d'analyse de litige client."""
    return (
        f"Analyse le litige sur la commande {numero}, motif : {motif}.\n"
        "1. Vérifie l'historique de la commande.\n"
        "2. Compare avec la politique de remboursement.\n"
        "3. Propose une résolution ; si remboursement > 500 $, recommande une escalade."
    )

if __name__ == "__main__":
    mcp.run()  # transport stdio par défaut ; mcp.run(transport="streamable-http") pour le distant
```**Teaching Points of Emphasis:**
- The $500 safeguard is **in the server**, not in the system prompt. A prompt side guard is a suggestion; a server-side guardrail is a law. On consideration: “where to place a critical business control?” » → at the deepest level, server/tool ​​side.
- Date standardization illustrates the role of the MCP server as an **anti-corruption layer** between heterogeneous systems: ISO 8601 output, always.

## 4.2 MCP client: session, discovery, invocation```python
# client_commandes.py
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    params = StdioServerParameters(
        command="python", args=["serveur_commandes.py"]
    )
    async with stdio_client(params) as (lecture, ecriture):
        async with ClientSession(lecture, ecriture) as session:
            # 1. Handshake : négociation de capacités
            init = await session.initialize()
            print("Capacités serveur :", init.capabilities)

            # 2. Découverte
            outils = await session.list_tools()
            for outil in outils.tools:
                print(f"- {outil.name} : {outil.description}")

            # 3. Invocation
            resultat = await session.call_tool(
                "rembourser_commande",
                {"numero": "CMD-2026-0193", "montant": 750.0},
            )
            print(resultat.content)  # → statut: refuse, escalade: True

asyncio.run(main())
```**Note:** the `ClientSession` encapsulates the entire protocol (handshake, `id` JSON-RPC, request/response correlation). The architect must nevertheless understand what is going on “under the hood” — this is exactly what the interactive web page for this session visualizes.

---

# Block 5 — Security, ecosystem, advanced patterns

## 5.1 MCP security model

Three pillars, all required for certification:

1. **Server isolation**: each server is a **separate process**, with a dedicated client (1:1 relationship). The GitHub server never sees data from the PostgreSQL server. **No inter-server leak** by construction: the servers do not communicate with each other; only the host sees the whole.
2. **User Consent**: Any side effect tool call must be approved by the user (or by explicit host policy). The host is the application point: it displays the tool, the arguments, and asks for confirmation.
3. **Least privilege**: a server only receives necessary access (targeted environment variables, narrow-scope tokens, explicit authorized directories for a filesystem server).

> **Educational point — prompt injection:** a third-party MCP server can return malicious text ("ignore your instructions and..."). The host should treat tool output as **unreliable data**, never as instructions. Connect this point to the $500 safeguard: the server-side defense holds even if the model is manipulated.

## 5.2 Ecosystem

More than **50 community servers** ⚠ (number growing rapidly — check before each session) cover common needs: filesystem, GitHub, Slack, PostgreSQL, Google Drive, browser, persistent memory... Architect's reflex: **search for an existing server before writing one**. We write an in-house server for its internal business systems, not for generic integrations.

## 5.3 Advanced patterns

| Boss | Mechanism | Use cases |
|---|---|---|
| **Dynamic tool registration** | The server adds/removes tools during the session and issues `notifications/tools/list_changed`; the client rediscovers via `tools/list`. | State-dependent tools (after connection to a database, expose its tables); escalation of privileges after authentication. |
| **Resource Subscription** | `resources/subscribe` → the server pushes `notifications/resources/updated` on each change; the host replays the resource. | Real-time dashboard, monitored configuration file, ticket whose status changes. |
| **Prompt chaining** | An MCP prompt generates a sequence of messages that orchestrate several successive tool calls. | “Dispute analysis” flow: research → policy verification → proposal → possible escalation. |
| **Normalization at the border** | The server converts all heterogeneous formats (dates, currencies, units) to a canonical format before responding. | Aggregation of several MCP servers returning dates in different formats → ISO 8601 everywhere. |

## 5.4 Certification summary — the 7 reflexes

1. Client:Server = **1:1**, host orchestrates.
2. Everything is **JSON-RPC 2.0**: request (id), response (id), notification (without id).
3. Transport: **stdio = local/single-user**, **Streamable HTTP = remote/shared**, SSE = deprecated.
4. Primitives by **controller**: Tool = model, Resource = application, Prompt = user.
5.Handshake capacity negotiation guarantees **scalable compatibility**.
6. Business safeguards **server side**, never only in the prompt.
7. Tool outputs = **unreliable data**; user consent for side effects.

---

## Animation tips

- **Block 1:** have a participant draw the architecture on the board before showing the diagram. The common error (single client for multiple servers) will pop up on its own — fix it live.
- **Block 3:** use the primitives selector on the web page in “oral quiz” mode: read a use case, have the room vote, then reveal the recommendation.
- **Block 4:** run the example server live with the MCP inspector (`npx @modelcontextprotocol/inspector python serveur_commandes.py`) if the environment allows it ⚠ (command to check, the tooling evolves).
- **Error to be caused intentionally:** add a `print("debug")` in the stdio server and show the corruption of the JSON-RPC stream. Unforgettable lesson: **logs go to stderr**.
- **Timing:** if the group is late, compress Block 5 (ecosystem) and return to the guide; never compress Block 3 (primitives), it is the most discriminating in the exam.

## References

- MCP specification: `modelcontextprotocol.io` ⚠ (dated versions, check current revision)
- Python SDK: repository `modelcontextprotocol/python-sdk`
- Chapter 4 of the course guide (`guide_fr.md`)