413 lines
13 KiB
Markdown
413 lines
13 KiB
Markdown
# 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` |
|