# Les 16 — MCP (Model Context Protocol)
## Docenttekst (Klas A — 3 uur, fysiek, demo-driven)

**Les:** 16 van 18
**Onderwerp:** MCP — eigen server bouwen voor AI-clients (Cursor, Claude Desktop)
**Duur:** 180 minuten
**Demo-app:** `mcp-polderfest` — TypeScript MCP server tegen Polderfest Supabase

---

## VÓÓR DE LES — Setup (45 min)

1. Test je werkende `mcp-polderfest` op laptop — alle 4 tools + 1 resource werken
2. Cursor + Claude Desktop beide klaar met config voor demo-server
3. Open MCP Inspector in tab als backup
4. Browser tabs: modelcontextprotocol.io, docs.cursor.com/context/model-context-protocol, github.com/punkpeye/awesome-mcp-servers
5. Backup screenshots: Inspector UI, Cursor MCP-config, Claude Desktop MCP-icon
6. Test Supabase env vars werken via stdio (server kan crashen als env niet doorgegeven)

---

# HET SCRIPT

## BLOK 1 — Welkom + Terugblik + Waarom MCP (15 min)

`[SLIDE 1 — Title]`

**Vertel:** "Welkom bij les 16. Vandaag een hot topic — Model Context Protocol. MCP. We bouwen onze eigen MCP server."

`[SLIDE 2 — Terugblik]`

**Vertel:** "We hebben al heel veel gedaan. AI SDK, tool calling, agents, RAG, deployen naar Vercel. Allemaal in onze eigen Next.js app.

Maar denk eens na. Die handige tools die je hebt gebouwd — `searchBands`, `getStats`, RAG search — die zitten **vast aan jouw app**. Wil je dezelfde tool in Cursor? Apart bouwen. In Claude Desktop? Nog een keer. In een script dat 's nachts draait? Drie keer.

MCP lost dat op. Eén protocol, alle AI-clients begrijpen het. Bouw je tool één keer, gebruik 'm overal."

`[SLIDE 3 — Planning]`

**Vertel:** "Drie uur. 50 min theorie, vier demo's, lesopdracht en huiswerk."

---

## BLOK 2 — Theorie MCP (35 min)

`[SLIDE 4 — Wat is MCP]`

**Vertel:** "Model Context Protocol. Open standaard, gelanceerd door Anthropic in november 2024. Wat is het idee?

AI-clients — dat zijn programma's zoals Cursor, Claude Desktop, ChatGPT. Die noemen we **MCP clients**.

Tools en data-bronnen — JOUW code — zijn **MCP servers**.

Het protocol regelt hoe ze met elkaar praten. Als je het eenmaal volgt, werkt jouw tool in **elke** MCP-client. Cursor leest 'm, Claude Desktop leest 'm, jouw eigen custom client leest 'm.

De vergelijking die overal gemaakt wordt: USB-C voor AI tools. Eén standaard connector, werkt overal."

💬 *Vraag: 'Wie betaalt voor het protocol?'*

**Antwoord:** "Niemand. Open standaard. Anthropic heeft het in elkaar gezet, alle code is MIT-licensed op GitHub. Cursor, OpenAI en anderen hebben het overgenomen omdat het simpel is en werkt."

`[SLIDE 5 — Architectuur]`

**Vertel:** "De architectuur. Drie rollen. Client — de AI-assistent. Server — jouw code. Transport — hoe ze praten.

Twee transports. **Stdio** is de default — server runt als child-process van de client. Communicatie via stdin/stdout, JSON-RPC berichten. Snel, lokaal, geen netwerk nodig.

**HTTP/SSE** is voor remote — server runt op een cloud, AI client praat over HTTP met Server-Sent Events. Voor productie-deploys.

Onder de motorkap: JSON-RPC 2.0. Standaard sinds 2010, niks nieuws, robuust."

`[SLIDE 6 — Bestaande servers]`

**Vertel:** "Eerst — je hoeft niet alles zelf te bouwen. Er is een heel ecosysteem.

Anthropic heeft officiële servers voor de basics. Filesystem, GitHub, Slack, Postgres, Puppeteer. Allemaal via npm te installen.

Community heeft er 500+ gebouwd. Linear, Notion, Asana, Figma, Stripe, YouTube. Daar is een aware list voor — punkpeye/awesome-mcp-servers op GitHub.

`*[Toon Cursor config voorbeeld op slide]*`

In Cursor laad je ze via `~/.cursor/mcp.json`. Eén JSON met server-naam, command, args. Restart en de tools zijn beschikbaar in jouw chat.

Voor mij persoonlijk: GitHub MCP en filesystem MCP gebruik ik dagelijks in Cursor. Scheelt enorm veel context-switchen."

`[SLIDE 7 — Eigen server bouwen]`

**Vertel:** "Hoe maak je er zelf één. Anthropic heeft een TypeScript SDK. `npm install @modelcontextprotocol/sdk`.

`*[Wijs naar code op slide]*`

Drie regels. Eén — maak een McpServer. Twee — registreer een tool met naam, beschrijving, schema, execute-functie. Drie — connect een transport.

Dat is het. Echt. De rest is gewoon je tool-code."

---

## BLOK 3 — Demo 1: SDK + eerste server (25 min)

`[SLIDE 8 — Wat we bouwen]`

**Vertel:** "Vandaag bouwen we een MCP server voor de Polderfest-data. Vier tools — searchBands, getStats, addFavorite, getBandByName. Eén resource — bandenlijst. Eén prompt-template — daily recap.

Werkt straks in Cursor én Claude Desktop. Eén server, twee clients."

`[SLIDE 9 — DEMO 1]` `[SCHERM: terminal + editor]`

```bash
cd ~/novi/novi-lessons/Les16-MCP
mkdir mcp-polderfest && cd mcp-polderfest
pnpm init
pnpm add @modelcontextprotocol/sdk zod
pnpm add -D typescript @types/node
```

`*[Maak tsconfig.json en package.json type: module]*`

**Vertel:** "TypeScript config: `Node16` module resolution. Belangrijk. Anders krijg je `cannot find module` errors door .js extensies in imports."

`*[src/index.ts schrijven — minimal server]*`

```typescript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "polderfest-server",
  version: "1.0.0",
});

server.tool(
  "searchBands",
  "Zoek bands op dag, stage of genre",
  {
    day: z.enum(["Vrijdag", "Zaterdag", "Zondag"]).optional(),
    stage: z.string().optional(),
  },
  async ({ day, stage }) => {
    const bands = MOCK_DATA.filter(b =>
      (!day || b.day === day) && (!stage || b.stage === stage)
    );
    return {
      content: [{ type: "text", text: JSON.stringify(bands, null, 2) }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server started");
```

**Vertel:** "Let op `console.error`, NIET `console.log`. Stdout is gereserveerd voor het protocol. Logging via stderr."

```bash
pnpm tsc
```

**Vertel:** "Build genereert `dist/index.js`. Als je 'm runt direct met `node dist/index.js`, lijkt er niets te gebeuren — server wacht op JSON-RPC over stdio. Lijkt verkeerd, maar is correct."

`*[Inspector starten]*`

```bash
npx @modelcontextprotocol/inspector node dist/index.js
```

`*[Browser opent localhost:5173]*` `[SCHERM: browser]`

**Vertel:** "MCP Inspector. Jouw beste vriend tijdens dev. Links: lijst tools. Rechts: call uitvoeren.

Klik searchBands. Vul day: Zaterdag. Call Tool. Resultaat: JSON met bands. Werkt."

---

## BLOK 4 — Demo 2: Laden in Cursor + Claude (20 min)

`[SLIDE 10 — DEMO 2]` `[SCHERM: editor + cursor]`

**Vertel:** "Nu laden we onze server in Cursor."

`*[Open ~/.cursor/mcp.json]*`

```json
{
  "mcpServers": {
    "polderfest": {
      "command": "node",
      "args": ["/Users/tim/novi/novi-lessons/Les16-MCP/mcp-polderfest/dist/index.js"]
    }
  }
}
```

**Vertel:** "Absolute path, geen tilde, geen relative. Restart Cursor. Cmd+Q en opnieuw."

`*[Cursor herstart, Settings → MCP]*`

**Vertel:** "Server zichtbaar, groen vinkje. Test in chat."

`*[Cmd+L in Cursor]*` "Welke bands spelen zaterdag op de Beach Stage?"

`*[Cursor roept tool aan, geeft antwoord]*`

**Vertel:** "Werkt. Dezelfde tool nu in Claude Desktop laden."

`*[Open ~/Library/Application Support/Claude/claude_desktop_config.json]*`

```json
{
  "mcpServers": {
    "polderfest": {
      "command": "node",
      "args": ["/Users/tim/novi/novi-lessons/Les16-MCP/mcp-polderfest/dist/index.js"]
    }
  }
}
```

**Vertel:** "Identieke JSON, andere file. Claude Desktop herstart."

`*[Claude Desktop herstart, vraag stellen]*` "Welke jazz bands spelen er?"

**Vertel:** "Eén server, twee clients, geen duplicatie. Dit is de kern."

---

## BLOK 5 — Pauze (15 min)

`[SLIDE 11 — Pauze]`

`*[Reset server lokaal — switch naar werkende Polderfest backup voor demo 3]*`

---

## BLOK 6 — Demo 3: Polderfest server (30 min)

`[SLIDE 12 — DEMO 3]` `[SCHERM: editor]`

**Vertel:** "Nu echte data. We koppelen server aan Supabase."

```bash
pnpm add @supabase/supabase-js dotenv
```

`*[Voeg supabase client toe, env vars]*`

```typescript
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_KEY!);
```

**Vertel:** "Env vars komen uit Cursor mcp.json — daar geef je ze door."

`*[Pas Cursor config aan met env block]*`

```json
{
  "mcpServers": {
    "polderfest": {
      "command": "node",
      "args": ["/full/path/dist/index.js"],
      "env": {
        "SUPABASE_URL": "https://...supabase.co",
        "SUPABASE_KEY": "eyJ..."
      }
    }
  }
}
```

`*[Pas searchBands aan naar echte query]*`

```typescript
async ({ day, stage }) => {
  let q = supabase.from("bands").select("*");
  if (day) q = q.eq("day", day);
  if (stage) q = q.eq("stage", stage);
  const { data, error } = await q.limit(20);
  if (error) {
    return {
      content: [{ type: "text", text: error.message }],
      isError: true,
    };
  }
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
```

**Vertel:** "Errors retourneer je als data met `isError: true`. AI weet dan dat het mis ging. Niet `throw` — dat crasht je server."

`*[getBandStats tool toevoegen]*`

`*[addFavorite write-tool toevoegen — let op user-intent in description]*`

`*[Build, restart Cursor, test:]*` "Hoeveel jazz bands zijn er in totaal?" → AI roept getBandStats aan, antwoordt met aggregaten.

💬 *Vraag: 'Hoe weet AI dat hij Supabase tool moet gebruiken en niet z'n eigen kennis?'*

**Antwoord:** "Description. AI ziet 'Zoek bands op dag, stage of genre' en denkt 'oh, dat is wat de gebruiker vraagt'. Bij vage descriptions kiest AI vaak fout. Daarom zijn descriptions zo belangrijk — net als bij tool calling in Les 12."

---

## BLOK 7 — Demo 4: Resources + Prompts (20 min)

`[SLIDE 13 — DEMO 4]` `[SCHERM: editor + cursor]`

**Vertel:** "Naast tools heeft een MCP server twee andere primitieven. Resources en prompts."

`*[Resource toevoegen]*`

```typescript
server.resource(
  "bands-list",
  "bands://list",
  async () => {
    const { data } = await supabase.from("bands").select("*");
    return {
      contents: [{
        uri: "bands://list",
        mimeType: "application/json",
        text: JSON.stringify(data, null, 2),
      }],
    };
  }
);
```

**Vertel:** "Resource is read-only data. AI mag het inladen als context. Verschil met tool: tool wordt aangeroepen tijdens chat. Resource is statische data die je in chat invoegt."

`*[In Cursor: @polderfest:bands-list]*`

**Vertel:** "Cursor toont autocomplete voor resources. Selecteer, resource wordt in context geladen. Vraag erover — AI heeft alle data."

`*[Prompt toevoegen]*`

```typescript
server.prompt(
  "daily-recap",
  "Genereer een samenvatting van een festivaldag",
  { day: z.string() },
  ({ day }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `Geef een recap van ${day}:\n- Top 3 acts\n- Drukke momenten\n- Bijzonderheden`,
      },
    }],
  })
);
```

**Vertel:** "Prompts zijn herbruikbare templates. In Cursor command palette krijg je een MCP prompt-menu. Selecteer 'daily-recap', vul parameters in, AI start met die exacte prompt.

Nuttig voor team-templates. Code review template, bug report opmaak, vergader-recap structuur. Eén keer schrijven, hele team gebruikt 'm."

---

## BLOK 8 — MCP vs Tool Calling (10 min)

`[SLIDE 14 — Vergelijking]`

**Vertel:** "Wanneer MCP, wanneer gewoon tool calling?

Tool calling — Les 12 — is function in jouw app. Voor app-features. Snel, lokaal, alleen jouw users.

MCP server — Les 16 — is library voor AI-clients. Voor interne dev-tools, voor distributie, voor herbruikbaarheid over meerdere clients.

In productie gebruik je vaak beide. Tool calling voor je product-features. MCP voor je dev-workflow. Geen conflict — twee verschillende contexten."

---

## BLOK 9 — Lesopdracht + Huiswerk (10 min)

`[SLIDE 15 — Praktijk]`

**Vertel:** "Lesopdracht — half uur. MCP server skeleton met één mock-tool. Inspector test. Laden in Cursor of Claude. Test-vraag werkt.

Huiswerk. Vier dingen. A: 3 read-tools naar Supabase + 1 write-tool met user-intent. B: 1 resource. C: laden in beide clients. D: MCP.md met 5 secties — tools, resources, voorbeeld-prompts, setup-instructies voor anderen, één observatie.

Bonus: prompt-template, npm publish, HTTP/SSE variant, of je eigen eindopdracht-data als MCP server."

---

## BLOK 10 — Afsluiting (5 min)

`[SLIDE 16 — Afsluiting]`

**Vertel:** "Wat hebben we gedaan. MCP — open protocol. Tools, resources, prompts. Stdio en HTTP transports. Server in <50 regels TypeScript. Werkt in Cursor, Claude Desktop, custom clients.

Volgende les — les 17 — gaan we externe APIs in de diepte. OAuth flows, webhooks ontvangen, Stripe checkout, Resend email. Veel productie-patronen die je in je eindopdracht direct kunt toepassen.

En les 18, de laatste — Supabase Auth en RLS. Multi-user apps. Echte productie.

Vragen?"

`*[Vragenronde]*`

---

## Veelvoorkomende fouten

| Fout | Oplossing |
|------|-----------|
| `cannot find module` errors | `"type": "module"` in package.json + `.js` in imports |
| Server crash bij `process.env.X` | Env vars niet doorgegeven — check Cursor mcp.json `env` block |
| Cursor toont server niet | Cursor restart (Cmd+Q), absolute path checken |
| AI roept verkeerde tool aan | Description specifieker |
| Inspector werkt niet | Build eerst (`pnpm tsc`) |
| `console.log` breekt protocol | Switch naar `console.error` |
