fix: update lessons

This commit is contained in:
2026-06-03 16:58:25 +02:00
parent eb1ba2e28d
commit a852aa0d52
83 changed files with 16830 additions and 0 deletions

View File

@@ -0,0 +1 @@
,sharp-zealous-planck,claude,21.05.2026 11:11,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;

View File

@@ -0,0 +1,412 @@
# 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` |

301
Les16-MCP/Les16-Huiswerk.md Normal file
View File

@@ -0,0 +1,301 @@
# Les 16 — Huiswerk
## MCP server uitbreiden + Supabase + MCP.md
**Vak:** AI-Assisted Development
**Deadline:** Voor Les 17 — Externe APIs in diepte
**Inleveren:** GitHub repo + `MCP.md` in root
---
## Doel
Bouw je MCP server uit naar productie-niveau: meerdere tools incl. een write-tool, een resource, geladen in zowel Cursor als Claude Desktop, en gedocumenteerd.
---
## Onderdeel A — Tools naar Supabase (verplicht)
Vervang de mock-data uit de lesopdracht door echte Supabase queries.
### A1 — Supabase client in MCP server
`src/lib/supabase.ts`:
```typescript
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_KEY! // anon key voor read, service voor write
);
```
`package.json` install: `pnpm add @supabase/supabase-js`
### A2 — Drie read-tools
- **`searchBands`** — al gedaan, maak hem nu echt (query Supabase)
- **`getBandStats`** — counts per dag of per genre
- **`getBandByName`** — exact lookup
```typescript
server.tool(
"getBandStats",
"Aantal bands per groep (dag, stage, genre)",
{ groupBy: z.enum(["day", "stage", "genre"]) },
async ({ groupBy }) => {
const { data, error } = await supabase
.from("bands")
.select(groupBy)
.order(groupBy);
if (error) {
return {
content: [{ type: "text", text: error.message }],
isError: true,
};
}
const counts = data!.reduce((acc, row) => {
acc[row[groupBy]] = (acc[row[groupBy]] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return { content: [{ type: "text", text: JSON.stringify(counts, null, 2) }] };
}
);
```
### A3 — Eén write-tool
`addFavorite` — schrijft naar `user_favorites` tabel (uit Les 12 huiswerk).
```typescript
server.tool(
"addFavorite",
"Voeg band toe aan favorieten van gebruiker. Alleen bij expliciete user-intent.",
{
userEmail: z.string().email(),
bandName: z.string(),
},
async ({ userEmail, bandName }) => {
const { data: band } = await supabase.from("bands").select("id").ilike("name", bandName).single();
if (!band) return { content: [{ type: "text", text: `Band '${bandName}' niet gevonden` }], isError: true };
const { error } = await supabase.from("user_favorites").insert({ user_email: userEmail, band_id: band.id });
if (error) return { content: [{ type: "text", text: error.message }], isError: true };
return { content: [{ type: "text", text: `Toegevoegd: ${bandName}` }] };
}
);
```
### Eisen
- [ ] 3 read-tools werken tegen Supabase
- [ ] 1 write-tool werkt en heeft 'alleen bij user-intent' in description
- [ ] env vars worden gelezen vanuit MCP config (zie Cursor config voor `env`)
- [ ] Alle tools getest via MCP Inspector
---
## Onderdeel B — Resource toevoegen (verplicht)
Voeg minimaal 1 resource toe — read-only data die AI mag inladen als context.
Ideeën:
- `bands-list` — volledige bandenlijst (text/plain of JSON)
- `today-schedule` — vandaag's optredens
- `stages-info` — info over de stages
```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),
}],
};
}
);
```
In Cursor: `@your-server:bands-list` om resource in chat te laden.
### Eisen
- [ ] 1 resource gedefinieerd
- [ ] Test via Inspector: resource lijst en preview werken
- [ ] Test in Cursor: `@server:resource-name` werkt
---
## Onderdeel C — Laden in twee clients (verplicht)
Server moet werken in **zowel Cursor als Claude Desktop**.
### Cursor (`~/.cursor/mcp.json`):
```json
{
"mcpServers": {
"polderfest": {
"command": "node",
"args": ["/absolute/path/dist/index.js"],
"env": {
"SUPABASE_URL": "https://...supabase.co",
"SUPABASE_KEY": "eyJ..."
}
}
}
}
```
### Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json` op macOS):
Identieke JSON structuur, ander bestand.
### Eisen
- [ ] Cursor toont server met groen vinkje
- [ ] Claude Desktop toont MCP-icon, server beschikbaar
- [ ] Test-prompt werkt in beide
---
## Onderdeel D — `MCP.md` (verplicht)
Schrijf in repo-root.
### Sectie 1 — Tools
Tabel met je tools:
| Tool | Description | Type |
|------|-------------|------|
| searchBands | Filter op dag/stage/genre | Read |
| getBandStats | Counts per groep | Read |
| getBandByName | Exact lookup | Read |
| addFavorite | Toevoegen aan favorites | **Write** |
### Sectie 2 — Resources
| Resource | URI | Inhoud |
|----------|-----|--------|
| bands-list | bands://list | Volledige bandenlijst (JSON) |
### Sectie 3 — Voorbeeld-prompts
5 prompts die je tools triggeren — getest in Cursor of Claude Desktop:
```markdown
**Prompt 1:** "Welke jazz bands spelen op zaterdag?"
→ AI roept searchBands({ day: "Zaterdag", genre: "jazz" })
→ Antwoord: ...
**Prompt 2:** "Hoeveel bands per stage?"
→ AI roept getBandStats({ groupBy: "stage" })
→ Antwoord: ...
```
### Sectie 4 — Setup voor anderen
Hoe iemand anders jouw server kan installen:
```markdown
1. Clone repo
2. `pnpm install && pnpm tsc`
3. Maak `.env.local` met SUPABASE_URL en SUPABASE_KEY
4. Voeg toe aan ~/.cursor/mcp.json:
{...config...}
5. Restart Cursor
```
### Sectie 5 — Eén observatie
Iets dat opviel:
- Welke tool koos AI vaker dan verwacht?
- Was de description duidelijk genoeg?
- Verschil tussen Cursor en Claude Desktop in gedrag?
- Eén keer dat AI verkeerde tool koos — waarom?
### Vorm
- Max 600 woorden
- Concrete voorbeelden + tool-calls
- Mag wat informeel
---
## Bonus (optioneel)
### Bonus 1 — Eigen prompt-template
Voeg een `server.prompt()` toe — bv. "daily-recap" of "compare-stages". Documenteer.
### Bonus 2 — Publish naar npm
Maak je package npm-installeerbaar (`bin` in package.json, `pnpm publish`). Anderen kunnen je server installen met `npx`.
### Bonus 3 — HTTP/SSE variant
Naast stdio: maak ook een HTTP/SSE versie voor remote loading. Deploy bv. naar Vercel (zoals Les 15).
### Bonus 4 — Andere data-bron
In plaats van Polderfest: maak een MCP server voor jouw eindopdracht-data. Eigen API, eigen DB. Direct nuttig.
---
## Inleveren
1. **GitHub repo URL** in Brightspace
2. **`MCP.md`** in repo-root
3. **Screenshots:** server werkend in Cursor + Claude Desktop (in MCP.md)
4. **Een korte demo-video (optioneel)** van een chat-vraag
---
## Beoordeling
| Criterium | Punten |
|-----------|--------|
| A — 3 read-tools + 1 write-tool werkend | 3 |
| B — 1 resource werkend | 1 |
| C — Geladen in 2 clients | 2 |
| D — MCP.md compleet (5 secties) | 3 |
| Server runt zonder errors | 1 |
| **Totaal** | **10** |
Voldoende = 6+. Bonus telt mee.
---
## Tijd-indicatie
| Onderdeel | Tijd |
|-----------|------|
| A — Supabase tools (3 read + 1 write) | 45 min |
| B — Resource | 15 min |
| C — Beide clients laden + testen | 20 min |
| D — MCP.md | 30 min |
| **Totaal** | **~2 uur** |
---
## Veelvoorkomende valkuilen
| Probleem | Oplossing |
|----------|-----------|
| Env vars niet gelezen | Check `env` block in Cursor mcp.json |
| Server crash on Supabase call | Check service key (write needs more permissions than anon) |
| AI roept verkeerde tool aan | Description specifieker — wat doet het, wanneer gebruiken |
| Inspector kan server niet vinden | Build eerst (`pnpm tsc`) en check `dist/index.js` bestaat |
| Resource niet zichtbaar in Cursor | Cursor doet niet alles met resources — check Claude Desktop ook |
---
## Tips
- **Test in Inspector eerst** — sneller dan via Cursor heen-en-weer
- **Descriptions zijn cruciaal** — AI kiest op basis hiervan
- **isError: true** bij failures — AI weet dan dat het verkeerd ging
- **Server is gewoon Node** — log via `console.error` voor debug
- **Eindopdracht-tip:** als je MCP server voor je eindopdracht-data maakt, kun je 'm in Cursor laden en tijdens dev al data-vragen stellen

View File

@@ -0,0 +1,137 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 5 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
6 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/PageMode /UseNone /Pages 11 0 R /Type /Catalog
>>
endobj
10 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521111146+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521111146+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 16 Huiswerk) /Trapped /False
>>
endobj
11 0 obj
<<
/Count 4 /Kids [ 4 0 R 6 0 R 7 0 R 8 0 R ] /Type /Pages
>>
endobj
12 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1844
>>
stream
GauHK96Zjb']/F3m\Zn"-<Bd>+hC=pUrbWf:-8,j2VDJ7*>/bj&#V4Aq<`5,=C=41>YdG$%NlqNQgXh?#6TiJem+t1_!(u.?SGk--=;>7`.c<[31i2)%cNTJXp7?C.KU)*&EXL\R?N1LM%"XE5Z+.%hZ[(9U/[e9j$)@54qnpL*]XXb0<)-[+GtGBafYMn8Bt)?90=YUc2F2]b*WtVfia)B[f7`a=0Tt!.oj*41=,]&15F84hnE@3ANnRSFHbl:r)"1i\uFVVF!"/[`[>>cPa.9fX^:9L?d*Xn]n4?X9TG(7JUCOD`"gd/QiCBY^a*198bo$;]2n@?E0b#XY^rHC(t=fraZE.l@)NR<JVUKdif:s*F3!se2Tuk5*!CpnC#09809tJo=@2*$!Fp^XcXUR*5:;<W=TDJ@@mR`L.^Lpn1O,HOSME8oHYKig(-*)R8FbE9mqu_QPL-j[n=0e`M@9'IA$1V7CD97ua8+!aMA3cCqTaLU(%kPp3tJo&<gfL5^>"53h9OAR)XWggBA\\&O*HU4eiW",m]cbMAQ<7&f9X;>$e-=o[:!J6Y$;/[N$Zbs_$0UI,,K`B9]7=>=*_Gb!Y%])"sh,^\H*!r1DZY9?dCo&WJdT*TMDM$Tp&Skn-aWH(Z@2%2*U"4168'0&esdRR)(6i0!GNJ\_JgIGA-So'bE3^2MQ:2D6*UL-+$n'VtPT\;)#V@-RPjaO9Qgt?c'E6CISi:$8@lA#kjP.en%&V8aGXi#^)4BDflnp8Z&-SA>N[<BIg+W@:0.+.F/B8Em&BBlhjt`MbM%1a/]i9%,sZ.._W_U:(Qp^.SDL8L!H96D-m_kL\*3u0'ar%i$,X5Ccj3<I+Ls[KA;k?\+(N2*9>1**9t,f')lT4M(M.,HQQ=F<Z@mf00m0R/_KLj^V@\.LBc?Ebe]Q1$Y]mGnG2V.G.kZu]Q[[U20N$!_J-#)r)!VpROelD0MMUg>M:FO^q0/k>&P$YaF8qC#7Z<R9m1&4h!Kf'Ha(rg3PrIS907u)/*aQ>,:PD3K!/mtTY8dnc3S=GcQgN$(HRJ`Hm&M"'ZHF_P'VrT,\[!=f+6rXr%S]8-EoGkiOLcD83(2?):UJ2d/I`SK(tJoZq274ceY[OP>8e7UXQl)J6NKK4TViB/7;?9E?/&9*bWSmL\2URCL+k\^ec1a1Je[[">i#tLE`_ifZYBqj$K)X>t"O6Q,05`:.*KB0M0RTU-o.$_-%]-Ysp])n#CbTCo1f*cD0)9X3jB0Y5m1L`(/,HA-KJB@=o'>Z^acu;CLKr=!G>oZ=tEJ*0L`+'\$-83NfqnVVb!0SFKWq,0/!f"#>XdX4\r'3.]XbfglgG2:d<.7"bcISl:F'>LpQb<Y2ZB%,:;ulY19J\(S>In@Tu3Rj<_G$&*J!D3Ks0+Q_\>&5sd+6HWll`l[-E$#@GkGot+G8.m]08&u+%M3_MNae(DhfHdOE0\7OCj@,NqpVca*k?*ZKW6L7h`p#-Y1LW1oGoke$j0/k62Vlo2alqTQqcXem0"804gDG;V(pc#)O7fFEGacMfSL<O%<bmhf.sIY-Do>NU-M:n,mEnD,RI<@S,j'6mZ4CJ_BEZb_EJmnQ59n*GHA:$;ZBRC$U79UcMp#T5TZTBFY/71\Sc8:*SEl/>qQq#sT'cLS@-l&f2hJjM38F]>^U?X@*++BBgq:.GKa;&%b:lX"Lej4=W3Ie1'*R?sHJ)+^r*1n<7?ureN`br?mH;OL5c/dt/R]M4QkXfX2gm((J)_QMfebtC&#28aR`orcq*01NfJa0ZACmk:O`agC1'noWY-N)!bU3?"MTHGaIM=W,:QVR,BfsJX9r%jR?gm2T)u~>endstream
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1049
>>
stream
Gat=)@;jmY&H/b=EAR;3.FP7f;'_F$"YqVp$P_8$Gl9B.W?T_">?T*:3c,].^[ESmP1'n\43OkP'CWWHGL&*uQnHs<(2JG-k4%_h!TPk@!YZtirW^[2dGGOVad#Bt'-/G<#L&A#Jp%mZO@k`TcPYkJTiM5JAuKjLJQ&.<9L3H%q'9F"9#aI25g<3+jYuuA9$W8s?n,a]FoG40%cq7ML[(h^r_6e-K+1P7W/8<WV(.2p)NHG"HhSFb2(R^ES#31C&)Wi8`l!CL"_Y$2SiLGS2^SB:mS)0dY"e;E@K$;i:BK,h^.;-98ZWum^G-4cKNB,Y5+2+SO8F:<Z?6Y)&*PU%\jMX.W&0bH<PfCf7YE=)R3]bkH`qfn/0]q5V\IHb*=8Qg[DuAa#p"f>.5[&Q@Otur+47\`75W#O&u[,(<r4V*Yu1np^mWXLVQk_-6;Z.*MARhFpE%?g=?LR<;*5Rld:02"aIg&3^)#*je6+;,]`Xj3,`m-aN_p<6fc+1N)/!1U90'hO?,PHfC%"`RSij8;RcS++Ol&$uok&C<hr>eU"HdA`FXR6rGE#bAF^CEm)Zt@l_m2WbhUDgD<Fni5fCf<6caT/k66%o%rm]N+_rF$HXt`jQ%".eTo(b>UeLCEKcF:5g/WLUeBL47t46anubh)"9%bMhs@UVarY-m&5W=)fJm86X*YU.($0B)m1A=Oj-l)o>_U8a]]0$;u4VnHtPGQIW<gj3q/g\a*V#+'25VQkfpDRMNRS0L\J)k!9W!77.V'V6F`8bm$MqpEcjVTNOM'D05*30ag@4F`lTIMGF(kQotKfHk8';h-&<>2qPY9molg@B?Brk<8Sn[kr])2kN6qL)[6IaT8V$G>2A3H3W-=:;FI4[Va_snEks`4]$sY:^F3!2j0U0nq``CMHA!;k(dMB'o%1j^9"ai%)9-eQ0E/\Ps)9dfhWB#)/O>d+-E9kP^]QHf?96=g*P\231$hms%kRE_F_tL1;97RrL?U_R\s:cgHXTa]X</`<?.grFOl2]]Rb6Q_!/a"Cjr>KgBaO'!?u1U:&~>endstream
endobj
14 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1967
>>
stream
Gb"/'gMYb*&:N/3%.`Jd2FCB,7![2![V:Y3d<j]`XS7:3(e9E6\WUbf/b8?leC@.WglMa(U9P;''KOc$LI3ehpgN:a?j?+:"FDd>0FjjF@"JY$]nR=Jl`3Iab<=eaYpHf+%`Gj$+(S;#!QQLhc[?"\H:3&\KOdJC?NS8jT#Wt71`S]b(_G#iSeOF8F'Rs9>&P(@F&X`Ij1>6/osK4rR-,-l[LUQ`^*jD&YhH'WW\"KfNp"2k#/.M7^)XqhFK4rj-r"4H>\#lT.U^K2#>Yj7:\-]k$Q[**^!q$_TBW0(L!_W6*Z87!nqHHB@8lX-r7<t7k/+/fPco1P;TX?jVGX)'?5)r3KA&k:H#9NN-*`HN@!6,"/K5oZD;bg<AeoS]FkMeo(AA*26EZ;f=jI_6GqNGGELJn=eW/SlPDgAVm?!D#Hj<j.2QPuI"uHD_)VA<8(;m&OHUq@,[+,dk['LsrS(>Z,EU1W5kCIbdQ(Tn).ee9C%<tM;\jY-E=Nd+.)"TbS^eh0c*EhOkfih\fX298SG>5eUb4c4W/73AU0HYjm:B!bSVi*+Q:o&E*lD[Ndgfs&[*_-LLna:P(E.-HW_NZ6Dht(g-Ql0IHrR.]pNIAK!;%h-<MG9%E4,,Fl&t#UCbQfks]oRo%l?%P&Tu0=#qiDjc?77@f6'N@djICc?9m#mI'9nj)AJAX)[GLm`#VM6tj?Rr)UR<DjYMtC^/@L5-AmmXJP)2i1T\fO$Cutk$43!Xt6LU57g+eXJ_W[tQ,7:GBXTe(tMX#'fGWdM-*n*k$m;s6Db0C7GfikTJ]]h%-in"CL9HN!u?_9.BJo@Cm?^;#ejZ5a[OX@X!hG0EjSN^"s_nEb[?KJ(VJG:)P_NR4Ca(B4)'O^uY7j._h,(Le1ncF8*W!W]@n-%6570Vb1\XKI%PJ\rB2>VO:]-Q"5%LO2$KQhoNXig<n&5r(&<Mk[CQCok[=Wdt46hog_@i^aLgLdhrJ414_0D^1+#iQ?Hp1,[^(<uL8FH.*BUZSFY<=u(LhJo,6f86eMin9'$)Hhdn<XdsU:[d/']Z?gX)BGE0HR=u*8/?4##<q"X7OUblRiSaLHM6g\2,S4>c2=p>3S7TW\=#3sr:Ls`]YloapQj1n]ng^"%(C7I]0?[A>ph3G2s&DL)/i`AB"\=O2N5.gX5%1H3^JdokA<'h2_BgD(:C%*<p\S'/kgDd&!;gYcGH^[Y[sO>k>\AF<_eW2ZG84nE00.%/FYJ8G&G=I0g<Q.DP$*(UfUsX+Iud!nH1h"GCQ@jp(pkLI/U+?ggT77+Ajq#qH7&YiPW)E;qTaXEpESjocCKlg],C/hJ3iMp(8jp8o"^laR!3AC\It']Gb=.h13fYPoWKKcj[T`,rB%rlCtFD@n`l-X-qi=FJ\"9c_08;p1(K92'.)7P9QRLdi/J(m1!HIaMP"'kl2HUf5W9<CdfJM",uc@AgrIY)'pZNqU]?OoBEGAIC&s&&7PIdDg\.D6Z[?);(DInl$P2*<cq`68BL64*JKiD\<XaEL@$"Xni(n[.!n2Yam>"K-aOB$1g$+]5M5AhgMo1HSJE$N#3M99(sh$j:dmF[6/Tn6[\i@+PUl.u-rIF$<tM`f,M$P^^5;ASS(K,`j8@m`<4tmSe-'tJ_nLt*ZTa&`8Qdl/a0IlCGZ;S.AQje7X"5amKnQ!E.ed8d^i'bgpMODQhA]`>MU*@Ti%_Oi;7a:&BHcf_@J^6=Mn;"I,u(_&eo=g@o+o@eW#nADR?qXo(;LUVFK.-V-nl7LP$KO.?<8Gl$Dh'SijtJ\L`;#$h^pCOFKq4N1,ZaK&fN,Va!F@K6H-3<Xb"%.(+#5NiVL64mbGVL]/fk"Cca2!FCH."j45!\[=NoZ+J%PL1ThSLZmne.]3"Yar=UW5'>=1DiTWWMF:i=sLom.1oBa)Qq]#2;>Y:\XUJ;*]Vb*!76C3N\;3/u_j3#:?e!OJN.p2fPAHLQm~>endstream
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1937
>>
stream
Gb"/'8U&oI&AIa;9FpB/P%TFJ2]t7D[aV`>D4Od'0>>4I/A^=XJlLJ%q@:#lembK?96t8'=X&^=2[a44"W(OQrJWO_=#5*kRK@%S]*7I5+(fHMI-03GKS:uJ"!AY/4i)XSci-3fGB*sU&dfs6E&St'o"jLRE%X+(g_WC9JkElH1PZ$g69!mf&5Ik>LKjX=&i,3CI1h4,f\GB-a/sT<KY;IUB(nbKUnGD!,HBo?QNg:eZODkCq=VW/]IE!c`/C?\iO?BrnY1bR`ZdCQ3'K&dRp%+WVH,/ub"Upi;jcCWm$Zrm_=tWnpmrEf`.+frck,%jg3clMZGXA?F.Gtn-U@.IVuor=H:]?J'@0"bfqT=,4L>Okr6PrtF'(]=bOJa#Y_]flcR7$Un#:\$UQA3B"9VBbN6h!FE_7q5pdcNu#[kUEBLaic0Kb.g^u7=s.'D"kU,rFe,F@&&f2ADh1CDfR*n?Wk*]jb-FXm:RcMkdulTD<DL4o=#0f_$U!nV(D"%?IU#J<4+o?"NSg6m6XH\*?Jpk#_eI%QOO$?Y&.pofRf0lVP?PdN,aV:+\-HTYa/g!C*s:UkW(G'OW"eTG(.fGL3tIZ=]iQ:K_C+tjkg9%^/o@C@>/FA8OoJV[5JnK7pka#<MB#4,aKY\:**?-NUqck_,5bu#9,9$^KIrL,[H)-*e<7EVXOb51t5\)2:KXg0^I[G!,g#EO?rdgWn'a>@_BC%=$3ZFNQ7=eIRm4Aci^C>%Z.I]]tFJj2N`VNtqrndQ[fBF<mN4&ZR#^;q#FDH"39bq<@.?(Jl*<a+WCjAMEb9V(D,0t"cif)J!LJ%*ijT3ai##d>e9$LSTt9:ci/;FrEJa,-lD3_#jKU7k#E]%6KAG.>B#%@P9-'*sZ+g%bU4o9H<d(ZAEfH2Y.OO$[JN<3#OMH3\aqfloNY,#&DuB`u<9LF\OKG4(p]5WaeV6GDjgO-r5@J%_sf)W0F@6\Oh7;=fe9@2EO>6c^JL!oSkT`:\=(FT@cqe8i`J*Z"CiRA(2rX?FVQ9%U)k9t$r?FjW(O],BidS%MetKQsh]&<H<kU;]*Cn$>EN`=M2.Q'07/NBOV!k=G6mMUUF7D040d%a!72HFrF'WS=i-Y=>C&,%ZY/MUgn\kTT.R*gi@">E18s.[:=k:S$7)SSE!8blK.S#2@0CLt_O.OU!4NOsLZLo9(SWd<?:4,*-NAb_*F[`N0=!CGZG\]7/;\KaQ-G4r(L`!**#fF\%jJqd+Vl7lDl4.FSAq*@Z+YA2]GFK`?7`fJmLb-4GXXT@_A>*)d-Us3VtjHlOq/[Z2,MS&3_TEol$^28Y`tPdE5XH-@98m[Y^a$E#SVj\;bJ/Wk;5V&_d?(\3R&Nj"@Gr<UHe^uO?:Qt<s&AdKSdhf*!_%<mDuHN"gNphOuaNcD8,\oigRkmgp&M\P2QJBMh+d!`L-S1uSQpLfRL:G<WoF@!NrL]tN.q:_9abX,d-KuB(,5'sB(2L9R(1kF<CBP?/Jln)[coDtj3gUo(qXK=T8R4]GLp3FdfiGUVCes@_sODVbj4C?dmcTnWfShPoL-]I#mWl5>\00pbt9j6Hi*r0MK)=3]q\0Q9qkZD!;%]^,2\AD@%)gqrm9dt"V;,]U3GA0`<9D'[6%*6*&mI^\5aTYXt_YUjLfh,]'Kf26*5`%79V:uRn9W;KLX47h#B6't0AU6S:]iMmD)V_kC.BA:\'A6,Zfc[1KAX7I96;]9u^ZG&l@?3qFN?LcDOl/G96U\G9@lbh9<QAF'Lp"_m=!D?sRtWk*Qfd`?btR)PKVs$V>PG2GM9n[ch[GrYiRCsq,.h!`F0c=b4BoG%6*IrE0l!.Z@r3'35D,f@>gNp7K!^)_`k[_+m_(X;CNFBOeOujA7VrT!2C<@=:)e!R2Ls)No1uL3QWlgTpNo4c)RWGp9h4M7C<QXKJ-"gdJdq&1~>endstream
endobj
xref
0 16
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000536 00000 n
0000000641 00000 n
0000000846 00000 n
0000001051 00000 n
0000001256 00000 n
0000001325 00000 n
0000001618 00000 n
0000001696 00000 n
0000003632 00000 n
0000004773 00000 n
0000006832 00000 n
trailer
<<
/ID
[<a5e9ef526371aaece66bc9dca30f24bc><a5e9ef526371aaece66bc9dca30f24bc>]
% ReportLab generated PDF document -- digest (opensource)
/Info 10 0 R
/Root 9 0 R
/Size 16
>>
startxref
8861
%%EOF

View File

@@ -0,0 +1,218 @@
# Les 16 — Lesopdracht
## Eerste MCP server bouwen + laden in Cursor
**Vak:** AI-Assisted Development
**Duur:** 30 min in-class
---
## Doel
Werkende MCP server met 1 tool, geladen in Cursor of Claude Desktop, en getest met een echte chat-vraag.
---
## Stap 1 — Setup
```bash
mkdir mcp-demo && cd mcp-demo
pnpm init
pnpm add @modelcontextprotocol/sdk zod
pnpm add -D typescript @types/node tsx
```
`tsconfig.json`:
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
```
`package.json` — voeg `"type": "module"` toe.
---
## Stap 2 — Minimal server
`src/index.ts`:
```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: "demo-server",
version: "1.0.0",
});
// Mock data
const BANDS = [
{ name: "Lost Tigers", day: "Vrijdag", stage: "Main Stage", genre: "rock" },
{ name: "Polderfunk", day: "Zaterdag", stage: "Main Stage", genre: "funk" },
{ name: "Jazz Collective", day: "Zaterdag", stage: "Tent", genre: "jazz" },
{ name: "Echo Bay", day: "Zondag", stage: "Beach Stage", genre: "indie" },
// ... voeg er een paar toe
];
server.tool(
"searchBands",
"Zoek bands op dag, stage of genre",
{
day: z.enum(["Vrijdag", "Zaterdag", "Zondag"]).optional(),
stage: z.string().optional(),
genre: z.string().optional(),
},
async ({ day, stage, genre }) => {
const results = BANDS.filter(b =>
(!day || b.day === day) &&
(!stage || b.stage === stage) &&
(!genre || b.genre === genre)
);
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2),
}],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server started");
```
---
## Stap 3 — Build
```bash
pnpm tsc
# resultaat: dist/index.js
```
---
## Stap 4 — Test met Inspector
```bash
npx @modelcontextprotocol/inspector node dist/index.js
```
Browser opent automatisch op `http://localhost:5173`:
1. Klik "Tools" tab
2. Zie `searchBands` in de lijst
3. Klik "Call Tool"
4. Vul `day: Zaterdag` in
5. Verwacht: JSON met 2 bands
Werkt? Door naar Cursor.
---
## Stap 5 — Laden in Cursor
Open `~/.cursor/mcp.json` (maak aan als 'ie nog niet bestaat):
```json
{
"mcpServers": {
"demo": {
"command": "node",
"args": ["/FULL/PATH/TO/mcp-demo/dist/index.js"]
}
}
}
```
**Belangrijk:**
- Vervang `/FULL/PATH/TO/` met je echte absolute path (`pwd` in terminal)
- Geen `~`, geen relative paths
Restart Cursor (Cmd+Q + opnieuw openen).
Check: Cursor Settings → MCP → server "demo" zichtbaar met groen vinkje.
---
## Stap 6 — Test in Cursor chat
In Cursor chat (Cmd+L of Cmd+I):
> "Welke bands spelen op zaterdag?"
Cursor roept `searchBands` aan en toont resultaat. Werkt? Klaar.
---
## Alternatief — Claude Desktop
Als Cursor niet wil:
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
```json
{
"mcpServers": {
"demo": {
"command": "node",
"args": ["/FULL/PATH/TO/mcp-demo/dist/index.js"]
}
}
}
```
Restart Claude Desktop. Chat input toont MCP-icon. Vraag stellen werkt identiek.
---
## Eisen
- [ ] Project opgezet (package.json, tsconfig, ts source)
- [ ] Build succesvol (`dist/index.js` bestaat)
- [ ] MCP Inspector toont tool + tool werkt
- [ ] Geladen in Cursor of Claude Desktop
- [ ] Chat-vraag werkt — AI roept tool aan, geeft antwoord
---
## Veelvoorkomende problemen
| Symptoom | Oplossing |
|----------|-----------|
| `cannot find module` | Check `"type": "module"` in package.json + `.js` extensies in imports |
| Cursor toont server niet | Restart Cursor volledig (Cmd+Q). Check absolute path. Check JSON valid. |
| Tool niet aanroepbaar | Check tool name + parameters in Inspector eerst |
| Stdout pollution | `console.log``console.error` (stdout = protocol) |
| `tsc` errors | Check tsconfig en moduleResolution |
---
## Klaar?
Verder met huiswerk (`Les16-Huiswerk.md`):
- 3 tools + 1 write-tool naar Supabase
- 1 resource toevoegen
- `MCP.md` met tool-lijst + voorbeeldprompts
---
## Tijdsindeling (30 min)
| Stap | Tijd |
|------|------|
| 1-3 — Setup + minimal + build | 10 min |
| 4 — Inspector test | 5 min |
| 5 — Cursor config | 5 min |
| 6 — Chat test | 5 min |
| Buffer | 5 min |

View File

@@ -0,0 +1,143 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 7 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
8 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/PageMode /UseNone /Pages 12 0 R /Type /Catalog
>>
endobj
11 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521111146+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521111146+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 16 Lesopdracht) /Trapped /False
>>
endobj
12 0 obj
<<
/Count 4 /Kids [ 5 0 R 6 0 R 8 0 R 9 0 R ] /Type /Pages
>>
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1722
>>
stream
Gb"/&h/h((&:aF]Z-sSX1c1*(/BL"?'53d7$KVUJI;sYWjZ)/NZ$h2sYO>)l-/[f,);=&K8LCl0p:%Z^mVg\c./l"%^K:U94G2,G$lFcQ,qqn9b[//m=`U&EY.0cH&7MNY306hD+(uG3##[lh`\RgRC1=OQBc&QO1Vcj_CSmf1E-[s-+BhWZ,S7jkA^i;W5nrL\>*;jaCgVXqcT(MOU'Br1bl/-hE!I>P$E-9(b#4eIC^in<Is_1sI]XBo/Zu+RKKm&f(0;OSagm/(02A+ImBZ-G*7!r2/ZX272r0NKqZsN<Ve^l;\Kj;$H5HUXD1O.O7odm)4@$['(X#M_CkD<qcZK@hHn\S-7Waq']T96;HD45RSOf=F-V^;Q5t3h6DImhjap9'/$Ap);(__S".<De7:(a1&"p'^gXPXbWKpa-Q/=4JO/BlIs7\d75TkHoPs+gQj_p)X1-<CL[2.=LDJ\H16(Z>J:i+`+5^2nje6CXtC>SCjcC81/Z)g2Br@E/*a#/U$f6R*M/b5Sg;%P/nT@tmFm0G%0K@D7\7k'm2D3.6fJPY2i%/3[Ug,`4>"R20%X7M@6$feeS7jA&$e2\A/P0M`GCX3_a+3-.P3nh]$.HGQYg2pg:@`\qd5D*Q\_4G#>\j)_b^ZYe/lc(18SptDt^B$,3_aSSr+Yi.X:IX+$i[#TIUH(_ZE*Y4Y3,3W#+[nA7h@EoU3Z3,pGaeSV+lLBn^;l_Y^.!Gmi`Fca7G;I^N3csANHoU#$RsR-b9g/hVg>ur$q+En#b.MS0BmRCh1#-rP.H(Jp@dgbc#0"H6gNDZQ?Z>S":EK[SO=KQP#;[1Q*=a0ZV#g'HH#R2S^nJq)8rICaFAS4Fg(c9]77[q=E04cDPmXk@79lcN7+;Mr0ddZ)N[nWC5o'gkpg0UE50<oR1Ys6@*E%HL'US'gU&Rp%SHW97LL\s^&+!nWb86>hbYUDXbgmKMKOfMa)/)N7bWip0cSbGl.guJ;`6D&Y+MV?hoHE53d5/q39-;/eAP4+):7plsLHktY:=5/u&%5W7ZTF,P[N2C9Yl+M($bJnj:gLq@R;Ys`gpX8dWnjPt[[ibk\CWSHFq!3%i[XClDON[pOSag>1l>tJo3Vo=.35DJ;;]n/#eXRjUll'X,*mu_29]K;_D4MV?_$.gf]<BBn``_.mO=;QLG)bHI!lN(Qp(SR&(juhr17<33)Q?"j*R1J>\.o;"s=hk\-eWnlY/1r&.WSO4<FpnIc4d#3j8cB#HF!J'8G%o+g*M$VHnJ9p1K'Oa1VGBi;:BKh9LT:-TUF,ndbZ20!B9B(Y3f=gX^RK3M?k)RB5i^)+?q=\+#=_R`O+b\CO4>FU#"lTh[::7E-KM;E^R*j.8i[UbpImV/k9D.9G62n+_6Kc%\s/JhVgRESs/L)`(Wo&WB1b/fIFUJOWiKq<1P#4W"X\LA8rdLOrt$:h31GS*$kg]p^U:.uO16DV#UcHjI\R$kIh#N%b>/=E*-We=1cDoWnD3B>c49nOVKV:6`l`F*P!nk-ch@9kQ\7F]3E_;3e'HHj,"$o</6k]<6[.EXpo421GH[bIW<Rrr$qk3hU7Qh\h>$%C(/+T]2"X5*GB!QB,A6q$NC[/!nE_q:J^ec_p!a[^;+S2;3TFoZ184K=Bp_=kCCt'9C2!?><S`gg0*^.UQ;=H^fb.H\*?.rk%`NG5\$!b.e1nnt',o@EP_DEiAdq+T]\Lg[_q-~>endstream
endobj
14 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1715
>>
stream
Gau`SD0)19&H9tYfVgSD3H\-l`nfU64,o_ARPrF6=NuUhq1&Lf`!B>XE?*%!]mG8>jL?D+>"^WkZM3^l[sNH0",\2Or/]D/-ZbY<A\^:LQbXl/0RjSIcFptU_A1\N#9jQk!='5CqZl6*bJ@7J"[$1#W$Xu,OG4?W!WsG*C!AK!#qR0D%#@q`)[XG:?Q&OeHpJJ0BNc$UWSm4BNh<TK!hq`!8@/7=(i$;#.>KAuU!2+Pbh"Osnaap\poRXE<HMO])>mL$8T\:VJo_B^TDe8hWm[nB@X+&`9X8-6:@e5]jr\"1aP%Qj9.gM]Uu'(?SlEK>i2,)EAa#doKr[$@JIetkj19-E'^s+8Th+3`[.Xd#/5Wiimb0;->r3'nR;a80!N\G`Fh?R,Q?gnT40]U4W$jtYeO,2Hl9q5-$0i2AQ"cn]bRc8N9SY35d-<Rl:4c%8:$+1)N>XjU%ZC%H,*2Fs1;%U#]BR!^r@mff]dC5TVbM`q;ZT1J_j$N@9*ULd8AH*E^l9I?RO2q04d]MUjm4PCB1`(7nfgis&%e=4ED7]sURnVI79RM;r#'?HPO`3]_C[=^@L\Nnf*IX17:%@6l:kF279!g(.__9MNT+apD9.KIo&GNd+%JZd>!&kHZtSjOb:p+<nM_Dim-JXF/T%05*D\m]6_B9]'EYVp?4gV,P2/s)[W[aV<C=qDNZh3$qR'U&D]fSBaUq5+A6hk=!JONWj="Dh&itTu`PEB;`_#@C*[OF8Uk'L^3dLY_aXJuk[i_1Z6[[[kbtJ+-pj%E-o5o\>O`qdOU[1^CIYblVhQN*TZs%#Q?(_jd,;unQIUM5B)m>,M:<M[0Ic3u5%I9hF[F%)Y\"R>2R5u:n)%],(VYD-TX);Xb*si%5\4$i7p;_`(6NXAna>PJ<NTGLASQ&0_7cu.-*npjh]If9S34!g*Cs_tm'[,(hadAu"'<5agZ"E)]PH$7aI'o=o%S3k(1!@GXhRJ:;=,laX`Oa*S^PuT&?0;gPj@7<5]M2orrX!K9,6\.L@@mi+D6DT`e)W#^VHIapc5D6$LVcqBI#1Aj((-/O"(VOA^;(VkKB)!%9+dRTHtXV19Uj)icmglI`PI"5.NhV-_k"'[D<ktHfeGF\9^9eVa)Eo$0!C>oh$qlc)k5iqnmPGKb7XZ$qXR&kWcWO"R$NMh?Y7t7E#YhOE'%N3qEtX,3lAH(Pm*a1K(E%?)etKus0UXt$+<nC).&$][QG2p4AV\t0st$P^,*T;j"?18HOg_@6^9U4a.@b+'A/Wh+dH$ZH>oL`6RU&>N]W(i1Q^SpD$Q53e`&rQ^X\0'-c5s@N*.PV_T"kE4udAp=rg+@.u9;FG&/*.+^+i,3[!^hfXA]dg!cO'2qb_rnGM.O_mZ^BhkBK(40T[_L2hd@L'V5c2ESi"/;1FSI.o\;<t0UDpt,;(PI;dk_*>'>%2*Tjd$)X0MrB#-L3\=f;j=dNWd6KT&/YpDs8VOZV'#g3I5^F`-+4R_C$(j^WB6rJ_,b$7;Kq/i;CA/$)!u2=f3RX6(ecmaAdrktNKjH/A]Qg&i;V:\F[<Lm/]@FUK<dK4KK\7,$6\MK$J;gRBYV/t>ki)4Y'Geum5q-:F*^_"='2Nm7)f9;=V6A@3VGa/51>,!+'EmjF$.^J6`ar=;G9o2E3:;"04C/(.^2\5p0ca#1^)Y\jct8A3ncVL[A=oTL?^Op_ltILcFrqeqZkiGPkb~>endstream
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1896
>>
stream
Gb"/'gMYe)&:N/3$l0,S8RmW:;,gj/a/L![DNiS>jIn1-=\J5+ek1gqFkRPO[s+d_]tQ\K5ll<K.uB]T*0Q(iL1W^@_s6,m"n5t*mOeU]@<N&Q&dbmmpgrT8EX5b%QQ?^3W<bJF5hJ:L(/>4.+tDLO"&43<I0ie-U33,[US%!2*sr+j:E9YX=4Gh7Lh<7,C;XBi`Ar)nM5O<ldD(Z:g[G9=g?:67k)3V\_:h3JW'V->4PteE$DtSQI_Q,kZ@sa&AX4Y3Xt!,PXPf1]+1CpP[`^EY.l2sIk0,)R^+sEq"D0#8HYWEriCbd0&#=[rJ)?L+']::\k=+#j^Bk]r)M/f7]u>9a^_C^>or09):`n<Lg.?Jk:6$)f8HgW"H_e4>gCf;=KMnd>D-XPNOt^85EJrH%6r)Wo2bZ#>U&(Vdm/dfECr%$Z5(KdZ_>q4F`*rG,%52E+7/L:7LCR0,ps99!3qpN/NBD6I:<b<dBpRZ4[J[!1XGCaC10QS*$.k+D@#->?Vs@oXUNI`]RHBFZ&\b:8"2["e,ps`AOLs]F&#q;gfhc*AS=:5P'R&%rR2%Y863X`/`2@TN3(4UPe[r3F\($.d5rIP/!RoX\G#e:pmY]S'qTU<`h6jkPEcP&iBku*^9V7pblI)n!]^mH22,P71+li"YY74oUU7rV=hFu&6OhT^$6f41h;H)m@2"gF5#aNm]\*2*aEr%]64?M#lL?dN@i^qm#$dN_!C83tE^'13b#Kp+Sld9K/JC1&D93498JI\rnrkiE(gIpc\+K3s]04Sc_!m\<-5TiOhlNNp[Hat8A[Q"N/P=Sek$S]Cc4tssD.E]bR\A;5M6qXS,l\jV@Zc[1ZoBZkl_0S?<N3^t`9]tV%8[Gsq4M)Y$H:/'HNtK6<@:T:XipOP8hfmsocX+/gf9WgK2Cs'4;XMKgh#*-_T)<^o^(A4!S.?Wf^##>*Q#-/.m;K5sY1EC6'jJ8q82=$?hYH4p3\m'ATF-_mK8D4\+A1YKk!eoG+^nA<=:,?u@,CL.I2F>7TnZ&JU<sH[eu=/9.8Y$b;//A:qZqW?P-g%fEkcaj(+I+YLTbE-d/;<Qc3Ta*i@1SJ,EpD3`e'?bp`0#>-@X3nf+nk'NhD;J$Qkg":[8>Q2Fs`<N;)F)0BU]kD@0l@`(h1IYZ\)e<sh#_?^N*4(I`^lg2!CpW2=`07j@aM2\l2)Ln(\)'nf(gQE2S=GuuZWE4R_n`B$3=OQ$-/r1`U#4sBEr6-9-"$l8P??s$EVemOaGr\.mu(=e,g0g3^C`6@6Fp9q;rs7@(H2AH)V<D"ll4_!Fl'?nbM7&SAcGi6A:<Lb\aDQL-J\f,%(a?=ZSaR'rAD<K1"S]Y-ei3=#La7\4@M@=.b/5Au:biTUU%UC343V=GnXa@1Y]_eY9]VaDOiJne5/Vo^$F]67*n`\H,_B+,O\mMa'2FtB&Y,c_5mR).+p1Ed_aXq6XJS@)?gACEJQ/L]&$;W^QU#:*m@T6*ZVN;Fc&un\:SI`,1pK]Y9P4I[d8PT^7@7='\R@=>%,0Sq<=IEV>/q0DI`!YJ.ISoOWMO"mR:3hQ!'c,3f73+r:VLXe.=dE*ep>n?7_Q^V*F:%6_V:pQDh5bXsmjqc!$1]giS8XEBX!ZeODW%??:TH%]M4e,N4X9Agd-[XqF=W>*Oa`=S>6kt-4pa?SJ%ST4A/^etSN?ruH)-57Aq((S?I_N_+g>a]%tWQSE!Yf6kQ")-^@#.L(iFVr]]Kdh](!u#O,o1&prD49o-B?X2-C7klTELNb&,5[7ePm']]%5X-qfW]`Gk%UqEXXkiZ(^`s0I@?YWn8Wh>Y'n,kP`/F"0RoWP0)lO5NYN/ZYX#/8T`7=$(DjYA]W;Zlnqu^RY%i]_q5g]RG]1PFc!,E;fs\,$N=~>endstream
endobj
16 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1280
>>
stream
GauHJ9mbVQ%)2%/i7,e])b-VOMHWfX[s$HRXi]kiP`$a5[UNB=.GAY5lNc^uMA+`?G"A-*$q7hE$\/4*q"UQ-GIe"R0S>:E'I\Ue*%30SAtS%kRJ4rN9X'>.5Q^0T0`r$nmK^tQ.j5Tp`%7c.rJ8ON".,6C),"?c(I^.^UL=)TUHg$^8!3t:1_`hMOq3@V$rq?]$5)aMGP4U@?;*b"*Lb[\+rHoE*N9&si5DO%'Z+YqPJ=ie>f,rUU$?a!FTMMEUXGum>K.k3IsM%-Y4LAI%";2\<Tn2"8p^cKH^MKi\jlIbEPc0+4iIB%C7kXtTrZB_"8#U!aNF_]#jWE-qT)b^Hm1qM@m'7ghn3iP23IEuaKQg?]tB#s'0-])OmtVSBC<`/emgVCT%qaBH=5abaFKG8;FsBiT`%"R:*5'p)1A\d$bBN?Ihb"V;C1QW=X\;tlgcX[3n<=S?apj#H=KiqHZmaSM,&CXau`nTUASG`(E(_l1fa2ae*V=A@<u5?AYso!CUd<N9bMf;AftH@k"aO7B`:$*K$1I&0Y5tge[i-U]7;j0?($s@_N+FFa)7g@=mS>O,0<djGmVrZWKEmG_mDrmRDL^VYH4me&UZlnXfBEd/]s++a8mu_/e`TG2+b)eK@SR?0NqI^*0[1GV#G\d%)McY7O8?B5B><pCE-Z2"^VC<Duja8KqquaH\KKaiqlu@p6b2AEC(nd`]IUY-M<*V>bGT*K,V1-4;Wc,@&*Uf1P5`,7Z%EDlb]FlQL8)U"->`\8[2ZY46uj9Oe(u,A&LM&Q<r#Zh5;q!X9fc!?P(m*qb^=l:RIq/!/e:;$t[n6j=!?J[VjrONpoJi*u">bT/00>)#Nb(r#;JiXk%*\Ypj/I=%;'^$J22%)NT+<E+5%s3P;0*r,BsV0KO=DEG&bNe.E>*OTN)]4Zf?nS]'X\J)E)2'Xr;\_p1Dc2C=4:>ggaGTBci&g\U/["#GellkBoL^Jop,Br,'ncL28PnR8P%0-!TQPbkfBa-HjDSS\^V6@C6UlJ"e_GP5[Gg=7k'5$NthNTK%T3VC:iZHcJK1c!qCW.F;mCYM$fA[]`HRB$G]f3hE&IA;]eAbcB0J3DGmnRB)ThH\s0]nPOMf#>X<c6d^[a9p-WLGO#EcDbqTc]u;X,V^Z?kX;=AQ*+Ma-_k:PPc50I&2$'tmP`N5$Pa<,fNaajPdNh;.Np0.$GCsk-8FX'oNTHT4gs!KaZo:`*m9/"/ASm<E!QuCEEqL?KYK`s,_YXbj]_+.)ZM>GCKKi<q[`F`dVS~>endstream
endobj
xref
0 17
0000000000 65535 f
0000000061 00000 n
0000000122 00000 n
0000000229 00000 n
0000000341 00000 n
0000000446 00000 n
0000000651 00000 n
0000000856 00000 n
0000000933 00000 n
0000001138 00000 n
0000001343 00000 n
0000001413 00000 n
0000001709 00000 n
0000001787 00000 n
0000003601 00000 n
0000005408 00000 n
0000007396 00000 n
trailer
<<
/ID
[<335984f2468d85fa1a2d1b1369df2cab><335984f2468d85fa1a2d1b1369df2cab>]
% ReportLab generated PDF document -- digest (opensource)
/Info 11 0 R
/Root 10 0 R
/Size 17
>>
startxref
8768
%%EOF

523
Les16-MCP/Les16-Lesstof.md Normal file
View File

@@ -0,0 +1,523 @@
# Les 16 — Lesstof
## MCP — Model Context Protocol
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Vorige les:** Les 15 — Cursor + Vercel deploy
**Volgende les:** Les 17 — Externe APIs in diepte
---
## Inhoud
1. [Wat is MCP en waarom](#1-wat-is-mcp-en-waarom)
2. [Architectuur — client / server / transport](#2-architectuur--client--server--transport)
3. [De drie primitieven](#3-de-drie-primitieven)
4. [Bestaande MCP servers](#4-bestaande-mcp-servers)
5. [Eigen server bouwen — TypeScript SDK](#5-eigen-server-bouwen--typescript-sdk)
6. [Laden in Cursor + Claude Desktop](#6-laden-in-cursor--claude-desktop)
7. [Resources + Prompts](#7-resources--prompts)
8. [MCP Inspector](#8-mcp-inspector)
9. [MCP vs Tool Calling](#9-mcp-vs-tool-calling)
10. [Productie + distributie](#10-productie--distributie)
---
## 1. Wat is MCP en waarom
**Model Context Protocol** is een open standaard van Anthropic (gelanceerd november 2024) voor de communicatie tussen AI-assistenten en externe data/tools.
### Het probleem dat MCP oplost
Stel je hebt een handige tool — bijvoorbeeld 'zoek bands in onze database'. Je wilt 'm gebruiken in:
- Je eigen Next.js chat-app
- Cursor (tijdens coden vragen stellen over je data)
- Claude Desktop (research-werkflow)
- ChatGPT plugins (in de toekomst)
**Zonder MCP:** vier integraties, vier configuraties, vier onderhouds-aspecten.
**Met MCP:** één server, alle clients begrijpen het protocol.
> MCP is voor AI-tools wat USB-C is voor apparaten — één standaard connector.
### Waarom dit nu hot is
- Anthropic Claude Desktop ondersteunt het out-of-the-box
- Cursor ondersteunt MCP sinds early 2025
- OpenAI heeft mei 2025 MCP-support in ChatGPT aangekondigd
- Honderden bedrijven publiceren MCP servers (GitHub, Stripe, Linear, Notion, ...)
Voor jouw eindopdracht: optionele bonus, maar erg handig om interne tools te bouwen die je in elke AI-client kunt gebruiken.
---
## 2. Architectuur — client / server / transport
```
┌──────────────┐ JSON-RPC ┌──────────────┐
│ MCP Client │ ◄────────────────────► │ MCP Server │
│ │ stdio / HTTP/SSE │ (jouw code) │
│ Cursor, │ │ │
│ Claude │ │ exposes: │
│ Desktop, │ │ - tools │
│ custom apps │ │ - resources │
│ │ │ - prompts │
└──────────────┘ └──────────────┘
```
### Drie rollen
- **MCP Client** — de AI-assistent die jouw server gebruikt. Cursor, Claude Desktop, of een custom app die jij bouwt.
- **MCP Server** — JOUW code. Een proces dat tools, data, of prompts beschikbaar maakt.
- **Transport** — hoe ze communiceren. Twee opties:
- **stdio** (default) — server runt lokaal als child-process, communicatie via stdin/stdout
- **HTTP/SSE** — server runt remote, communicatie over HTTP met Server-Sent Events
### Protocol
JSON-RPC 2.0 onder de motorkap. Je hoeft dat niet zelf te schrijven — de SDK regelt het. Wat je wel zou kunnen zien als je inspecteert:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "searchBands",
"arguments": { "day": "Vrijdag" }
},
"id": 1
}
```
---
## 3. De drie primitieven
Een MCP server kan drie soorten dingen exposeren.
### 3.1 Tools
Uitvoerbare functies. Vergelijkbaar met tool calling uit Les 12.
```typescript
server.tool(
"searchBands", // naam
"Zoek bands op dag, stage of genre", // beschrijving
{ // input schema (Zod)
day: z.enum(["Vrijdag", "Zaterdag", "Zondag"]).optional(),
stage: z.string().optional(),
},
async ({ day, stage }) => { // execute
const bands = await query(day, stage);
return {
content: [{ type: "text", text: JSON.stringify(bands) }],
};
}
);
```
AI kan deze aanroepen tijdens een conversatie.
### 3.2 Resources
Read-only data die AI mag inladen als context.
```typescript
server.resource(
"bands-list", // naam
"bands://list", // URI scheme
async () => ({
contents: [{
uri: "bands://list",
mimeType: "application/json",
text: JSON.stringify(allBands),
}],
})
);
```
In Cursor type je `@polderfest:bands-list` om resource toe te voegen aan context.
### 3.3 Prompts
Herbruikbare prompt-templates.
```typescript
server.prompt(
"daily-recap",
"Genereer een samenvatting van een festivaldag",
{ day: z.string() },
({ day }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Geef een gedetailleerde recap van ${day}:
- Top 3 acts
- Drukke / rustige momenten
- Bijzonderheden`,
},
}],
})
);
```
In Cursor commando palette of Claude Desktop slash-menu krijg je deze prompts.
---
## 4. Bestaande MCP servers
### Officieel (Anthropic)
Geïnstalleerd via npm:
- `@modelcontextprotocol/server-filesystem` — file system access
- `@modelcontextprotocol/server-github` — repos, issues, PRs
- `@modelcontextprotocol/server-slack` — Slack workspace
- `@modelcontextprotocol/server-postgres` — Postgres queries
- `@modelcontextprotocol/server-puppeteer` — browser automation
- `@modelcontextprotocol/server-memory` — persistent memory
### Community
- `linear-mcp`, `notion-mcp`, `asana-mcp`, `figma-mcp`, `stripe-mcp`
- `youtube-mcp`, `arxiv-mcp`, `wikipedia-mcp`
- Lijst: https://github.com/punkpeye/awesome-mcp-servers (~500+ servers)
### Gebruik
Voorbeeld Cursor config (`~/.cursor/mcp.json`):
```json
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." }
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/tim/Documents"]
}
}
}
```
Restart Cursor → servers actief.
---
## 5. Eigen server bouwen — TypeScript SDK
### Setup
```bash
mkdir mcp-polderfest && cd mcp-polderfest
pnpm init
pnpm add @modelcontextprotocol/sdk zod
pnpm add -D typescript @types/node tsx
```
`tsconfig.json`:
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
```
### Minimal server
`src/index.ts`:
```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, genre",
{
day: z.enum(["Vrijdag", "Zaterdag", "Zondag"]).optional(),
stage: z.string().optional(),
},
async ({ day, stage }) => {
// Echte DB query of mock
const bands = MOCK_BANDS.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);
```
### Build + run
```bash
pnpm tsc # build → dist/index.js
node dist/index.js # runt server (wacht op stdio input)
```
Eerste keer: niets gebeurt zichtbaar — server wacht op JSON-RPC over stdio. Test via Inspector (zie sectie 8).
### Best practices
- **Logging via `console.error`** — `stdout` is gereserveerd voor protocol
- **Errors als data** — return `{ content: [...], isError: true }`
- **Schema's zo strict mogelijk** — Zod schema = AI weet exact wat het mag invullen
- **Description = belangrijk** — AI kiest tools op basis van description
---
## 6. Laden in Cursor + Claude Desktop
### Cursor
`~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"polderfest": {
"command": "node",
"args": ["/Users/jouw-naam/dev/mcp-polderfest/dist/index.js"],
"env": {
"SUPABASE_URL": "https://...supabase.co",
"SUPABASE_KEY": "..."
}
}
}
}
```
Belangrijk: gebruik **absolute paths** (geen `~` of relative).
Cursor → Settings → MCP → check server-status (groen = werkt).
### Claude Desktop
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
```json
{
"mcpServers": {
"polderfest": {
"command": "node",
"args": ["/Users/jouw-naam/dev/mcp-polderfest/dist/index.js"]
}
}
}
```
Op Windows: `%APPDATA%\Claude\claude_desktop_config.json`.
Restart Claude Desktop → MCP icon zichtbaar in chat input.
### Beide tegelijk
Zelfde server, beide configs. Eén keer onderhouden, beide AI-clients gebruiken hem.
---
## 7. Resources + Prompts
### Resources gebruik-cases
- **Documentatie** — handleiding, RFCs, design docs
- **Database snapshots** — read-only views
- **API responses** — gecachede externe data
- **Logs** — recent errors voor debugging
```typescript
server.resource(
"recent-logs",
"logs://recent",
async () => ({
contents: [{
uri: "logs://recent",
mimeType: "text/plain",
text: await fs.readFile("/var/log/app.log", "utf-8"),
}],
})
);
```
In Cursor: `@polderfest:recent-logs` voegt logs toe aan chat context. AI kan vragen erover beantwoorden.
### Prompts gebruik-cases
- **Code review template**
- **Bug report opmaak**
- **Vergader-recap structuur**
```typescript
server.prompt(
"code-review",
"Doe een grondige code review",
{ file: z.string() },
({ file }) => ({
messages: [{
role: "user",
content: { type: "text", text: `Review ${file} op:
1. Bugs / edge cases
2. Naming / readability
3. Performance
4. Security
Geef per punt concrete suggesties.` },
}],
})
);
```
Cursor command palette → "MCP: code-review" → file selecteren → AI start met die exacte prompt.
---
## 8. MCP Inspector
Debug-tool om je server te testen zonder Cursor/Claude.
```bash
npx @modelcontextprotocol/inspector dist/index.js
```
Opent browser-UI op `http://localhost:5173`:
- Linker panel: server tools/resources/prompts lijst
- Hoofdvenster: parameters invullen + call uitvoeren
- Result + raw JSON-RPC zichtbaar
**Workflow:**
1. Schrijf nieuwe tool
2. Build (`pnpm tsc`)
3. Test in Inspector
4. Werkt? Restart Cursor en gebruik daar
5. Werkt niet? Inspector toont errors direct
---
## 9. MCP vs Tool Calling
| Aspect | Tool Calling (Les 12) | MCP (Les 16) |
|--------|----------------------|--------------|
| Waar leeft tool | In jouw Next.js app | Aparte server, los proces |
| Clients | Alleen jouw chat | Alle MCP-clients |
| Setup | Function definitie in code | Server proces + config |
| Distributie | Niet — alleen in app | npm publish |
| State | Per-request, geen state | Server proces heeft state |
| Best voor | App-specifieke features | Herbruikbare tools |
### Mentale model
- **Tool calling** = function in mijn app
- **MCP server** = library die elke AI-client kan importeren
### Wanneer welke
- App-feature (alleen voor jouw users) → tool calling
- Interne dev-tools (jij + collega's) → MCP server
- Open-source tool voor de community → MCP server (publish naar npm)
- Beide patronen zijn complementair — productie-teams gebruiken vaak beide
---
## 10. Productie + distributie
### npm publish
Maak je server beschikbaar voor anderen:
```json
// package.json
{
"name": "@jouw-org/mcp-polderfest",
"version": "1.0.0",
"bin": {
"mcp-polderfest": "./dist/index.js"
},
"files": ["dist"]
}
```
```bash
pnpm publish --access public
```
Anderen installeren:
```json
{
"mcpServers": {
"polderfest": {
"command": "npx",
"args": ["-y", "@jouw-org/mcp-polderfest"]
}
}
}
```
### Remote servers (HTTP/SSE)
Voor cloud deploys — anders dan stdio:
```typescript
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
const app = express();
app.get("/sse", (req, res) => {
const transport = new SSEServerTransport("/messages", res);
server.connect(transport);
});
app.listen(3001);
```
Cursor config:
```json
{
"mcpServers": {
"polderfest-remote": {
"url": "https://mcp.example.com/sse"
}
}
}
```
### Security
- **Authenticate** — als server gevoelige data exposeert, auth-token in env-var
- **Validate input** — Zod schema's strict
- **Audit log** — log elke tool-call
- **Rate limit** — server is gewoon een proces, gebruik standaard rate-limit libs
- **Geen secrets in code** — env vars only
---
## Bronnen
- **MCP officiële site:** https://modelcontextprotocol.io/
- **Spec:** https://spec.modelcontextprotocol.io/
- **TS SDK:** https://github.com/modelcontextprotocol/typescript-sdk
- **MCP Inspector:** https://github.com/modelcontextprotocol/inspector
- **Cursor MCP docs:** https://docs.cursor.com/context/model-context-protocol
- **Claude Desktop MCP:** https://docs.anthropic.com/en/docs/build-with-claude/mcp
- **Anthropic launch post:** https://www.anthropic.com/news/model-context-protocol
- **Awesome MCP:** https://github.com/punkpeye/awesome-mcp-servers

257
Les16-MCP/Les16-Lesstof.pdf Normal file
View File

@@ -0,0 +1,257 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 6 0 R /F4 12 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
13 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/PageMode /UseNone /Pages 18 0 R /Type /Catalog
>>
endobj
17 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521111146+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521111146+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 16 Lesstof) /Trapped /False
>>
endobj
18 0 obj
<<
/Count 10 /Kids [ 4 0 R 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 13 0 R 14 0 R 15 0 R ] /Type /Pages
>>
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1746
>>
stream
Gatm;bBB@G&Dcpm[E1Fh1MQRcRO1,:G%Q-]bW)lIfm&.B/MT^;"1$t%:Gj7[08qcoHmCT7HJIZ6Xb$Sm(G"Bq)8PDIS:"b\3/m9Vfu/Wm'jQ^!7uM?.?/#B('GVYoiX,2]fu-LuO[ONNA"]bnP\YoR(fAFS^sUGMR1#F;qdYNbE@#am%7Dp@._/t@Lu]OCgVf6&>IUg-;^OYga>uCVZhd1)\:`>))R%")PW>lKUDY'MrQjjWr0:$!*_b$>L4>+T#L`bB3-X?Ro&FGU7sUdq37gqe:?OF('^oK&+,l4SXH%ebk/>+.<rHCi*1-m,aG]])EdR1LU;R;fnaddIFQl?e3`.=CEBQhM^$%Uu9;:7dZ<a@hAI:.E4Mn'S^N<4X.`'lIKYV=<N!VfkLDek?RO#a]*]2^jo;Z(jYi\PcFaf@Q3`pHW[bb[:/?gP"&`\5O^]#ogOb;67_V7bO;Ht>fN`6T:Bb[/YiNRBS/qp@;fXXYRU5iunF0FHN"Db.Wm*29@hHZbV2qeK5hq3oELH(5AecN/aGF4/nZ7Ia%W[cug$c\:*=HHXtY*KQMZ,2nZ\,[=DUt#(K$ndtFmHf.ar1uN[;BA60994=fRGd<!>8lcDcI/N8^C%KrdNT.)oZ@G#RcjlpSNZ3G`p)J#=&3P!R9C$Y#cfV.[;7OqCLd[nVRrN6!!b#s\`(r1jj2*`_N\a@LGrDG[&<,]1-q_ds/NpYh$cJgbdEX40TU,MLJKElGI-k%?05OLqIZeB?^I_AE"muZ$cLZLcX00se2c9*%]=8i,[dmui;P$`oY]M^*Yb.4G:SbWBfprgr]6@=+kEl^p#8qe5$MPXEq]ZDg]0>Vl?So6oYK5l'gg>?W$0^oma8fegL.Q,XG^G>c4USaf9\Kc$&"G4(O@=qpU(-m$?`RAEeUO%<h\mbe@"Cmbou&dT6Tn:e3=*mYbbDm+%e3ACbL(-]Z/p*okpk9"%2%.@uWMe'S+Ue2j9#2-E-C=BdT\S91Pr2B#97<Qfth_0GNc]]/4PH[r.S<>Qum?!:A$D>e;lNBk'Y+LOg$Zgd16heN5AS%:BN-O!1db+U'bX&W_6qin:%'3nP1'=3V7us'T+P+3gg#)hUQkfthDiXcS?9AnY/+V^-*3-$G>(L1*+N;]Y2"A<I77=#LLb_=4MMkNIr:AQ&Xp$`6dPW/Z0ekTn!QEHbA>N0C92S.qB`jRY2YC1/j2.M(]FUg]i`Z`&[V6PE"qQnk0;FVO-%',3DDk'u=:jQQa3(r+A&F>g=,;=!D>CgJH\eHRq\"cQdMqBj:XGl]ke?e5^)WpR'PiClPf\q,dGfO&#pTBlEK8EXc#-IO2L6JT(tU;tq2H<<)L:lcEV21sWr<>:dc:H=BXaF!'iEKj/T=VpbY[g&II?MM6sQ53I1%u^"/UBH(*A"DTQ%rM+#3e/Iu/l&o<F[4mC^YI35d':GNfna27AAB\r]1f*d,2B/XNDu9"[>d2V&5X+?G[$co`LN']3SLihZKQsfPDK82$]+BE?K++pp3ib,c=eV%l6(oX!(<ASDju5;L#7YR*X%<:Jjf$^jBP'^VF,c^m"5;DLB+XS"12&iqB5<\5]VTEiql$QfR,aE"*>AGO@'$8.0s!j*Tt>KL9hoV@C3ik!,bSHpp"YeM!DS#YIDD0N'O<BE;%e:Q"JnUj=nI/%03]3/o6q=HJfc4'Gqk=<VOC`g^4hu.Q6iK`$b(+)D?'[KBY(OL("E9.X.2I!H6"$#M*hX3W2e~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1186
>>
stream
GasangMYb*&:N/3bY(ejfJ"R>;:N=Vm^<SOFAo9Q"VuBGBuX0uM697Y7D?K'l=/l+D<-N)3BEZjS7+\Q@H">(JCdFRHcpqZ=Pa#k7)8?qr3_Kt7>+=*M%iLTBTdEZH/ejsh*_^JP/s0V$l$sBaoi!hLf1bbNcnRF12%-PS4!cV->B=[&B.PHj<OPT8C$bSJtu<,&+'UNb1"Oj*7uFZreFmkK+3U):_Q,C9=#=u28gU:rq?KNgPtIif%LHp]@Q2JK58ag357F9n%,fGAdr\&_,PSZS&W-nj]>QUpi"FHL#=4FHM?qoQk#JdCcZ3IHJ#9-;`4gK)9sW%km/R15W#6n,RM^L(;Fs9Pb;Mc,,C;s7m&)GGFGQVSe4SKRa#h0lk4t_P0[dhEK?-W3UAH=R"q--Enm"Z0OZ?1h_Pi]-SuGUqc1RRPoU\*"@T/kk_L9_<&g79%`ZsPAgc3X2@>oD:ETG>[pD-^]TT3Z*j^q_9"8=m!J\+@8lZ-7S&L)chTZSc)86OS3T6%h]cpR=]"Nn!V/T7SE4NI[e%k^PBPm]t;NR<:V!r7#$2b=4/DSK8LjDBYVuO.DLbJ/L<3WTd1Huja[FSJtX74AQ^OnuA8Ju&LGPLH.dB&Ja)@f1<kDe6)G#rFgNZ3=!ouaYW.2UU>i*^C0CBN;AotDktiCYmI-[,Bi6SuGH7=UG]6K=,d6ql>P=;S,"F*R:;EmFtdHU4Tu(OdBoUP"6ocV+[3#VgX6<>e_^#-]rpO'UGar<<R%KYJ6/p/EH+rhsdK$[f_%ZfW$(kihF\HnR#FC1(OPmCp5%&!`+rRrX/[@^l4:(AkPH/[eZa7t89.h6""CO(m_$<&YgK[Y9'P[@A3Ss(@fY"q$Xh-QGbFQ0A3AlQamk\AUaiV<]Do2'3oeVuY9f9`'3:VL4i0o;K<()9d!V$M5sJC$e/ejj%L][&QEbO@(Ii:uMD\G+/3B]L4%93(uU[#9rb/djW9%+c[Zo5GDXpUFeCSJ;@JlRS]'@Yt8r'%NG7'%6QLaiQDNad0nF)+.\,H1fkAa9[9nXjk08e3>=+bG`3V<ER<;e)Vd75NDUXR:F1-U9#An<+g11J4_MI<SuW51J,\X(l(Yqbk82Ta(CrCfUH;*N4tg=k(sGamh8Cl+TSFJQ:m(u3$,mcsg+sY9ZA>q`K+>7a#jP?5+SlCu:BnW~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1196
>>
stream
GauHJ9lo&I&A@[:lqu&1&AA0g3h:8]A<KV)4#,2jFO<phV3DZ/I\`P6qCDpcUa4^[j:IJ!Jd^N(_rC&M$n0u6qb;-^h\0,)B#$AOpV8&:OM$?M8b=ka>]V,)&7MPo3KR@M(R,HX%[8;$jtQs2%C&BS[#A*+>MqKe(hTF_E;q+k5`DS[8g'EKD=u"u5r@cg=q[ZSHp#1,B3T[:d60RekoU&G&Zf>=5S]GYVMp*PLY7HP5Ms>Iik\XZVC44p>aI)!e?J#h^t%38<E3n:g9'>1qrY1nT=\BW!Qc-K?F7Enpel.,OPV/[HWL6sJ?Pt*mQKq(l=%;/JXGAC`0_$"ReK2Z[6t:8E%ddlGU+ss<_+f"j:PJ32hN]?\07)/40`e[O-5/6c)2=7N(KdP_ZmH+[8=[Rj1&2W^_Y.T#m:H"M?J]X(,>/e@j_^N1jDS:3jQir,EV7>LLj.[Sjo3Jdo:\kX^A_Y[F9t6'AL]?Z=St-DAa8+&6DtF].N',I3Hgiq=m+hf,]<6paLZm#Ss1Q&+HdFFL/)T=uT1?"d*D$PSGaBEof5]@\YPW]4t];of8"Fe;."iTrgd$7;nPmiU+&$B`tc(E6fklUbP%VK.W"WV<u4K'XCaQ.]Jh>lnBcGJh:R_J"q)j2:r$d_AZBV%j%d#:gahc#W7(<1,7?T]'0Ji7piArm'm6\Ep`%NWu7Cf.RcVXD'XO'-o$Q=iu3#5UpQG3qtS5)/R81D"8I"&LNdr$+6K25Y@0Wp%8C^>?+6Mg(Q8Ao)5<o<X94/4fCMg:&M8s.flD8lPZ:Re%TBES]e*&b_mQ"_.@P`//_4fbnN<sf6dN:J`^R)"[DRl(rEU6:96cXG[u7l1<s.q3C+Q43o8jq<dM?m7R5S.gF'.i(eah_)FjI>R=%-&bmL8OQr]e4ee9tK\qmnhTAd\2mUtO?U<FM8RAZKAs.70T2AhV?0K<iQR[l\OjFW:lA`M\`1f0)[5/RTSO&*NL;hh&'qf;B.>-b+KHm]<$4R5%L#`"K3i(@9uF'[XaVYZN4M;a*kgW,j[GBGQWcPs9_Aoe/"tq,i%Ykrd'%s*;t`(QQ>`%tnFj02BCSmcO+J?++(JWl7AVor@_b[_UH$gqW`\Epi/<3/YUL%)WTE+ZVOX:4f]!*;ntR%!g^r[](e5%L\[XD/4@1C9i.`O@kiUojN"7A3Y5r7,J#<L24C~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1644
>>
stream
Gb!#[D/\/e&H8h>E<u9S.2&)F.#Q0o!=cVX*K%;k]RFN7TSgTG9(h17EjdO?J)?B7[GtE_$?V2Y830\qh=9oYG[Wl-_sH8/"ZW'hoE>?:@<MrN&-9+=nDha'E<qod(EO,h;)dDu!n[?8Z#DJf,`r*<(b"Y_jU2WW&Z#O4*8g@9k!Pj$nqk87plbJG!)^t(8j7RY-'E=k/VkccYk`_]T/,@/6oNG$R.W+ai<n-]eY0R+;!.QpbYjh@Ie3Obr1#E^)-u?L!l%fjNijk!F7-rPQcIFp1+k)*!RLG@:@_trZg&*6f._P\Hk#S!i5EP&f(!,3&srg7PZ<08_nHQrE>F-Ci;AIZg&ak&"R8oS>aP:hMk(gT\@=99'iIRn*U]'d[^.7ipBdZ_%;]9,WrukFj?njWY)/[oL,Vd\AtV1`JEm7I3c(/5CUsLL6\GKKpPHfW>Ln9q4!<7&M&b5>Ol/SCAFji.`ZXlo\T2L^R87,bkmPs%[Ws/r9m[:@`[Kf'E>KJO]#ms[AF?*CK[nhiGobg9M]WN0(uKE<$a=R$n=OL+-t3Y/,#7/J`kKU83^G7LFi)Bo:oknH0^]-l@!?!6__s167U3p_D6Afgd*QKl72S_/(<Y394CfbU3n^rP'GEkUGLsDhD-J4S$*Q91FbK58)UR.M7poNq@W1Ob64tcH5I@A4:#pCSL+rjp8dj2&4DYeW[0:2@er.*F3X\:I&#M.2V$:;U*fTg,9QR3t#MR5P]\dANhem:+(BReW\udmn&objqirsn6=C>@2eA\OB8;/@[:U\GCpE#JHn>5N'$,Aj+_]lZQ%C:u`i!B>XB0(_t%+>f-L8=QXd"6bgok-6CfT7cF,$Z*R=bR4m.!Z2k'tSCAi9*RDMtm-,c5%X"-uQckg'mCn"T:<83*WO85Z]uUGIn/d/5i-E2h9lES\ST#B<-,<q0;BMK>id'K7a\CV$h?o2ErJ_ZAS6)`'VZYO*8.NhK1o3$uK;&bqVu9Wka$:&GtD-J-_h_Dt"f;Q?teMhH@0I/FTkMk*(.UBj7Nkq&qm!!Ub.V$'.X/='ZkESs<;Spnn]&&]PR@XE8G@TCge-_s_fE\h<MYc?n21=0!O_MBdS(#OVnG]SGciM-G."ks$]"Kt3_h0>G2Gh<\QtM,%n5R*!5`4H)8]W#''5B_QJA8Xc\_]UmGMM%m\U8`\LgofHIAX!5uOAf!M@36nkk%]`Zbj(dsjokj7!HaUk7_aZC\CZMu>_X5mB2W=@Eh--m`[h$/U[hd\MdGEJ6<pMW?2elVT"e8?uCk!KR3)LSSYtlarC%L-NZ=Y<OF"\,=W)=P:lm2l:X!0a\N+P9p8E3[qC4:o\AJ.M,dB`"ZR:g#+O3#),fjU&6f&RmggZ8%\aSI'd;$=Xagb')</Yg%Ggi&*5>uhEBYAb1+<q0faO&\5H-@"Bp_*H!AS=g&[rWa-MSf)Y%#;Tof/Jnn,3%WfQ4iLW`V0Mma;nTNb*fTg,^+mUYk(SQL93Xk2pdH#IGK9J3A@OH)IqNZW2U"GP$nc[*`OB8bVhr)6LAQ-Bl0!cJ;h%.VRe+6VkXXarO\2GE0.`^W<a_TA'1;I.k)Xe2a7Pt`")L-M5Q.DN,o#1dI<U`l57g.FmAV?AUZC;HMtu>H*s9=S)i>^#1]~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1415
>>
stream
GauHKgJZcs&:N/3FSX&#Rdo/i)Wb[aKaeb489".*Cf*NXD'cb:aX#A:p=m'!Qbh>O^k+")T'8Fek"LiI@6jA#WkWucE%lq/0a#F(2?tRcK\7doc)a!bE5`i3!tgt/QNM4rq#f<eMSW=]\_LbhWC+$;6#&lrFu=72Eua=-AmkVh@R(o%*A)hUAh1!Tp^<fUa3^"%9"C#])h,Aok7Z=L6^m[$JtW?`2V"K.8LUnc)eK\@cd':q_#%YNEdsK&+abmtj"CeMinT+`PJ-oW&+[Tm_JeSnKLm)M\9J+^Y:t<-`Z!c.abc#Lf'uO1%SQm5H'-<dfF)U0`#dP5S<'U_$`&#7e=i<:5oi(GXb'aYL!`4:W?ALV:$Tc:0!r,U.$]*4a)tl<$$-e&cIf7YDfp-r^bfOI<&`p5SSLTJ/Qcl=&SNP>,hP;9(;j/'0@C>dG#<XFp+/Q.dZJ@9]57t"mTW>bb,Or1+8tiAbag5/("#a%%$$9D_DU?T(ubm>lfuRPi*6PgAUZ`On5Y22')iT[bR!??hQGfZR(Vq$pt!5&LDQ&Am+u:KY$=elG.DS$K,3dUYq,te<2_VjT?l\O*uG[.eTa;2Q<B@T526Aa`+a>dArXrp9Qu,*/I,g"[G360W`mC$h92Pcf7<=ARfE!4r!`TDZ[)Y3YF+?49+pj=Asc"mXJ^S+bk,!oY]t9_Dmk2dY>K=U69g;7o:nj_Ue(D/SCXUF2\/6=5N)D]WoEcb*S36^>_h#54Q"1Ps"$s*Ek\+^D60'<@hBEue]h\L16X'\"G/aC)&t:nD%f8"f+Xm<oo_Z&U$$p[XM\V["Q,F0>Fp/TfcgKqY9uXUlQo!8o.CfHaZ&O)dB(4@b%-(VQ4<a,Fnd%h=J_uGN6+!DE0DU*d1X)lO[NLf,k2P6+!7cT8cX$"_\1<pg*d52(Z[9,U/`0I&+:VL@cM#5Lm'Un"MENJ]a;Eb((h%%OTh`#b$\0E93[sq'SW-McnAFn^*H9nK*CNU%Tcf)A:SD>^YL.<<i5/UCmc-Zieos32qFBr[7o5;&mcC;aHDQ^pSZhPBLsbbkh&[R9roO<;J.ta>8uJbk*U)oPHLpYg,Y"<J>*mY!G><[7NI,!#,1lHA_6!V4]nRE)!tguNK`f)5^TYnJ%qKPEF\#S^obn7DX*]re<k7B`YeDu%X"h\AibWqVSo[`CE!!tq3nKupEkB)';k+G#KD73[rhlFF`hO/0Z"*^F`NN!_%'i%?e?!tYP(ER\p!K7<%a;2CYj?2YE)`kgGa3q`T`H2A35D1O>8X3,'_e8AGt\)mpS&(d=8+#UVCu*OZ$(RdDI@Z^+VDNUYejZmlqn!?Q=L*4?qRb^"tt94M&2<St1?j3\,EZ=Jj]'O:hEG*^^4m#gW;i47"u)IJNnHA&#pm*D>FbCH!98IfYQ5>2T~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2015
>>
stream
Gau`TD/\/e&H8h>ED9<Gko-"d.#Q0/TkVtbh>4^/)pl,4'U_OWaHA'q8^fNrI7Ho(jQ=D^#.1gIM'ql)j5"c=;@Ft&RJfqD!)FpqIa3I`QlH)>7)7k04I^Sf4MiBT70(8-e;lSNa>Y1h&%rqa8S#aa#)\.'K`Zp:7&`]Xa)PP/67=]24S0JP:sr/H+Gp>#aXcD"+ur0DJcnt@hi;XB;oZJ+aOWC/ZhYa]p0d9`*3(XtAO.0KA.GBdDuP%`^D;U98recQK0S["-V$$f3=fQm`R\p,V,aX1Ybt9><\3AoY;]m1%$QH9f)T.`:'*7j(&t,5J4[OL<>PlLm+_:hc!uh;S9O2G%S60Ad#]a:q_Z5nh!/k)8&m#b80MF0nUNI*:6p/f,a8WnOE&hE&qkt%%jaNn-h3]8DjY"X5WM\prLs93[a1C+c@AZOTRkpIjtV8+qM3Q$;5Y`1BQ$=o%;035fM,j0o>E`]/>p_$$2sY0iJo4f,c[]]+CpEDXr-na+g/!MAO@4Ai^-c-k&7F*Hti*q_QVr<7^*/D]7U&64k)Jk\ieLhlkdi$SZcOT_uqd<[nL0D0emh+[:+`"!p1E[NR#'^_QZTn-d+WGW#=6G>]*/#"V$5"eb)Kr-OVU\N)=Jd3gh1Vis.X7+DNtu$_.?nqM$.\W79`[mD3J>bO@gjF.g4KmJ),grU%"?n0,*rU=[d9+q\H-8`c/_kIcZ&,%&P;c-mW;^)Ou;(H+Tt9K\f\j*<B.=]4=gr`h,!U+VTQ)cs?Zpeo=R#OJTDG[#MkT.b/c.apfqO6um*7ibE-T@]53PFs/JV_u\1XqD"M_'"lAhaXfr,P*n\=?IU(AMR$PRJonFC:^rbb-f7a@>([m."=@SO_[?5[DHV&"kjh#a(Wb0e&bbN,_j^Li>Cr-Zj3A>Y4'SJ:+6c3ZEYD*^GfJ"j`j#7a#ZEg6_E,4aHRkL7`:<^kDQ(9i<!=3Mt]s<0B&QTk-/hMgW'at`q*_,NiS/3f;K''#pLZre\//!C!e?!P]:#4nXVO=B(TAN948.<Wr%]QKPA%6,2a<Td$fGW07a.&XAOY*=*9j&4-8d7hRh7a"m#2YDo9ShaC`"GcrIdL#k1]V(-.6iTnI=!j`GR7AQW<7:p.-`@6kE<@p*:TWGu]1*Gnb:/sE%XEB^GMJrL:p5(W[0[72q]#a7Of;F$;giq%$^6E7Ujo>Bg+\k.cJNRN_?dt#V!G`e%Id8:#hY0QVAN2I6=3!+8PJW&8+0\-+ei.BQG!&R!Ge%lm6HLpe'J+n+6]At*ZC(l>M"+&9Aq"ba!M7M<mfeFDs3aP9DfX1*hQ+C:f3VpQi2QsUKT*r9:@lOpOdC\PH%DTT+o=FTmH>nm6)"W]=5/QD"V*RYrr)g#lo(Loa+OUdiP.,Tu_ZG,(P1Y#gG+%*hr48\qY1Gn>NU^/AG.Bt:9<Jq]E/I]$XtN3EF/A+#f#9#[1n2<W:bMA4239&Br=H$Q&$LQ$-P6dh2QVHr'#nJ'>R_BUgSqZTPF*9P)Qj9*$15QBYNpPe^ahLR\k;D91V:i!iE<D@jr@G&pYdY]LW*_2'6WW%[ni\OCUbrT2t5+`6Wk,6n:1!Mlt\]u665N-)V9b?L*TD.jtr$lk3,Ikpma[*%6X"^^1e":f"UL?!@3;Z7qPTLMU9%7oH7b8e"PfMJn+>6[>9"`dLMrT.5<o*Hq`9)bs/UZ-?'*;5D7g=T_$j%\KIrJO9ZuHj1aN%d(V-*eq22Y/Sm8AHYOSP`Rp^3Cll2S$/pna_FUdF=&LlZ22p4t(,f*Z!Ot/+0n$=8_P3!Q3_a^-")'OG%*&HVo>?S8XM%3#.77:P/.OCbLN&^%d#Rb1%VGAa1/2jn(8ZY#+.D#,4Y)h1;=+!/m2"F>C!`K?OVPf;Kb^MG\$)_Iee1<ja%)4lG0i.M\kCk^s1R,-qElui.QPrK\AD_^Qth6cRhpi@992l_Z"'t#Hsj(rhW]"i8\%OC(s`9N.ham6N>F;sp>[X)i$cRP3PN'E^9_29rWBMq>ZU~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1514
>>
stream
GatU2D/\/e&H8h>EA.s>ko-"d8;c[kpMK431hl!O)!7MgAB%Ij9<KP<)i"'qmmp$7nlC7-D-56Nm^\*sa#c&LpgrkXh#P)m_!EWT`aNin9Fq5_@sG<c^''*>*XN,ue23+EO>sThW7F68;@sZ8R";+Hd?+F&)::Lr^sOcl=9On?b@^hX&h+`5!CS`C[!H+%'#clB(.9Vehm`(LBhL*+Wagf@gE2$@#4!&d;+!VtPECO;#,^ibIX_U;P\X3q=(Rf;GuL.'WduJZ&^9,Qc0IEk)mb.Jk18VTG%6El&/Nm%G\2A`n:fdg,/(V`a/0'<!&&In?FI)9aZ:pbLdj?G0W%!piZ6F&C(c7X_Hd!mi'E0Md@jB'_'\*'['sA-#<X2p$9aPlh.[24,/qS8g$>GbQf/D&N*.P^MGZ$mi3>Z#l?EBD+@Un\I3((ibD)A5"&\M5%HtSO`s5DG1\T!/XZR&q!`r=ogSTGnf%,j3?:As;*i"rS9"8Iq"GXF[QJ/0/NUfD<)s7%L&\`;\1#WZ5D+1ZGlh1hfEO%>'A-KX"<HVH\:[fi0Ft81k<o5aqG$eB#9mp/lfu&pI*A+A];*\AGVB(BPo_XTCmXDT9\)b+QMrin<,*,WaTm#C=(!7r-'t:Qb[[$,-'RKi=D(ISrLn_<uGZc'n>SJ(d,2Y0jTn5aDM=,N7^rmR\+W#_j-ngL8O>;J1S70f*--sA"&8HHo72fl'H;+Vu3r$"'WeWIYJSSm_XCetjZq`uE>up0!AK$c"1QKFM-"0EW9MYV9]F4DDEJXU'h<G&80WSi`\i!JNGl`8.0n,f1=@92B>&VT3AZAC%2uB]l`^sjg]f"NAj^s\j.l<h3jiU8grtn'l`;4+VLnn3Yl@2Q@k;BDOG$XOM*Yh8PRB=]8/_ZtNjD9N5H^5Bso/E!+jKC`7KHr%+o[oW^DXC&)EcummIlT`?oD/=EZ)(5%R62RJIs^cW08=C0Fq]TU$994+m*Xuu`cg-[qKoUHX&]/m6jrN0&DOVM[pO`>$\JNfRcGYO3*F]7dQH:<6&\h`gaoJ]//3,P^d3n5hKG.2<"IOK-m9TP6tD;T;iCfc$m:]Dk]i_o/RIuX&T;B%67l)m(Q#>0VpPEAa"rE"q"+uPi4p&-#A_/!'LuDc(f)'=LU'"@:rf5AGD$Lo_1K7KopaD?$F`+-5QCKb,-99_nP6Q)/o/ZOQsA*O(?"F`%F^Xj:Ps:[?/N0Q=g+u*j'6;JW+8gfd$alL/ui3#".]J]jiKqsk.AXK2JK=/&(eM6B"p5`JA),W7k4/bA@H/r8Dk`\S>9orZ%OKhZn*i925F<pb`E(ii58K'5tF-gYQ$VVSt:q&qrVAKqef=(3%88POE#ej$m'$t<DrcORr>c]Q==gU!mYuDD-8a&QHLh%Qd;^udcWM\DIu'FlV3P6S1P9-aff>Ac0tk_AELnIY@f6OTl-l9g?a"5F20DB.SSWT^r[?U@1Om'eh?k6pRJ,do@)1?o>UW8\SOhhQhPpccPH`a!?!/jir~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1652
>>
stream
Gb"/'bAu>q']&@2mOiAq_Z]`9]kJ+WR5j4RNiStFCg[mMBMem7J@=49n)[L?6WTIdSYf=W,>oY'E:3Ti'O6%7p,cCX]UZgSCC6<BC_"rB50Qd[kF(q\+Y7/d*rti@lbs9'lnpHCR8ZE`Ot^K&"n"j57m?^<'$ats%+do[NBg.(5BSB9.!-^%"5u'BH$Eu87mb47CS(`<(uJZWoA^:nR"RUaUtL/-$tmpnW"ISOM+`ILJCt;'hS&r2$u])l9sRTqJ<*aSA7=FQ@hH@5hLAs-DQHOjQ+W?8W`HF45JZUeAl"o0-MY=$8:^>Mih&aD1^(H.X6(GhWQ=X.!2&1s5b/BM+mXtS/gm7R7VI\b(%-=F)".3n+2Z0/!U4LBZ,0o]EF#24`Ye/Wq1M3\RuRWhSAZ;cn:ksU)lRa)`d)P(,IU+S3?s)`+@1UsIGn"M9%OU0&]bu:8K;s!U;.iqaaWb.`^KC9=ZENZ2=Ji_#Ho*(Bq71.]Q]Enaf%iLVp#7#\/l0*CU.V_(IC2@hnDkT_\PH`ln3bI&Mutl]n(Y[NN.,QNq"?aC:1(CJ\6Y.Ml"ZOQJ9%rjR24H?a^NA@mKq9ee<J=g6b^YjLMGbjaJD41cHgV8W5A<\nj^MD''9LC%U-I_8^ROXQPDdoA8i[)gHRoD$o"o9VZ8G8bB*7U*#AAh$*.Qf!&i#m\;SjH_ln:$$0Nrfl")2Bs.p&KdbD:k%S.6Z?ujhYE-m`8>XG7&lP!2efP-X5G%ej?G"?5X>i^/!)nct/<i$dCI(UUm]3Yg*A2DYooRPEI6=3Y]lk+d`T])aA1CRhrdRGp9I:rdaAdc([SiG);Uj2`@R$U:?R>?$au7s3_$C%?[F,73Q."mRkB2N5/%`^]__"o!WX?7#aV()bdJM!*QZ,o>U\#`%*5-p`o1Base8o?jU$bk3IOS<<US?>(0STI2flK^@46B:&%_jW?IZ)u91!.,eoHs?q_XPpX8I/_cW[`\XCm8k%(j;/-AY#M[Q-pGPq\*H7Y6t%FL&!!:-89K60tp'[`p,UiFfk[i8Ga#!qN1.))%s>\@0M?W'[g1<AXk-_*1\-['@_\7)p;$3*ZSl$eG`'D8Fn2(M?F`<1Wf?HpJGR?;u(nVrJ40)s+d(WoG0D=9dLI2*n>R/>G/dk.;i>tmDTo36/;8=FV,WiU>rZQNRfKHkXnsdqo\H(W&qtY@tirQ1QDjp6fV$&,d-T*dh+df8XseE@kGdk$XkH\rW1P:cPe_7)WUV;V6PKfGrt)ICYT,pYrQ0tF4Vu>-#9+<gO!f*6/7pI^!GaoM&Bm`G\mje<nHhgL$??C<,&U)_>ptn;8kgbR]1,'6]1&@*)#BN\M.5>'mZRNG0Y:"X/:Y4fBULdb56FB#oj;;Lgm'^E<p7=*H&(3*XsG)r`dqL,VPK^@id76ES*a@3>hTAF&;Ps9p&d"qgB:N2bM?D93E2"gTeg*NV*HnRre=8>njT_UH&*=!Hi95Q.3,;SuXuFlKc<]86T97gL&jQgHRb_Jj8g:@F'Y[U"D.bNP6>n;nfZns/,F/?Dsnl0ZcZE(\?4qL1];WV%4>p[uM[2'0*2e,2I[8`R@-E^WO,[ljRk[p!W9B$q$u\=Lr[2^q1RjQPSnb#ocj`qQIiNrhPhAfU:R[C;BWYJ4/Q#~>endstream
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2165
>>
stream
Gb"/'D/\/e&H8h>EA133m:qBO.#KK.?tm6\*P*k/ZPD-@&$em`8n?&+jKunQs1Y77.+tl"[6q8RN?(ZdmiL>Jcb7[6.)&gV)6Eo\T>C%n$lFc+"Y`4FS@sRtc^ZM@E63Oe!HAis!tHSqboc">-O!9Y)=W0-U-;Gg!216,.T!oXchEh<\VA'*LB6okop#NOQm\03U$iW93DM/^-Y9Nchq4icGla%eF_GCc7PL'7NDgVC'ne?gpY]is8$@C?irqQkQF_lQ=dHklAZhe.$0RiUE:RIa;A=+Jl8^uWf/h#3CRpdciTpWk*gjKapd/rZTm=9F06I:h!a^a>>L]mTiA+.q#^SJ6ED9X[aW!TqWeft=+8=dG"a=3V)!))R,ci$SOo!Xh,nBYUV*ajp\8%fB@D:h$&[.IV*'8m69JL.Plqi4QJHltFA[RISTH/j8_"Ks3(k0Z>0<#,LG*^4CSJ;Gp=<ebrlUK4laB:M<Vp)3uqW"1VG:N/UU)J@M[Z")#(#po#`-%?CW\*Q*$Xr]`mpCG:ZGI+X^PQkG_#i]b%fS+W:>rd71IJW!@?SRnEe"I^ZCVbr#p[8V9sf!S2s%m\>I-#K%&\kk878/'h;#V#>(IXspA#N_$8m^aDtg6e$sIMfJZV3&n9Xn+MN*W#@[L#RE5juur[,I,*R4N[raBXrOjcj/X)3$k;3F"5[*iXTAe<a,2Q[Y7gG$hpd<\YVM+3KXhe9\oi?I$jLKF=>8M1>U2k)PV)pOR@R$S]uSR:^\Y`8jN2mQ5B(t2qK@T+M!P+WVN=j\]=fEl=h:CD,)VpT@&@Iit>nY_b]o6@\q@m+:$U[TbaA!Ss7l"1Bom4=*PeW:@[Mam83)4"d]2km32TB>ubX,A\_>3p!2@$>5c*aFYs&q,_3E1LPB]4anFRXO'e(HO+Vq%0\dV,hiDo3&ee04uHJUnMC-c/c1&nc&YHWD_NjUrBUq]2droR)\'nB+FIi^!0180$_^O!cKb=^-5dE4V+RrQcc,CK6^Co^^==,pLY<O09kE(\V+P."=2I91`T?:lcXT\1TU#MU$u:jFJjJ*HPt1_W-aWM.+osah%LfbPT;gA8#V?4Ud;q'g8[u%']I$KYA#4XJ'uKb#ZgHg!Mn"(S^)0K638&RMVV(DZ(VNo>*k$lMopJMa+l^6mUr<./D@3Vel!Kif!<=$MRD4nJQ;)GQ%k\-(bSaS))6k5[!\FhFe-_"`1g:JhC*#=eE-?a)b7T)fi`Y)hnrom7V/*dJ=F3302e#$80ta#A+FXnZ0e82nLP4BO"cVPki>X20!J)3E#GOWe.p_\8p6*#U#-tgL%#CLDt^;qqauj<`758U<o'<9Mp\``adqSII#F6<n/]s=:rPFg#FqQc_psQO?'#@KAT&`lAJh@8"$CZSX3OXuL5_ikR"k"99,%kJ0#DCRf8fA!3ap[G`"@kF`GVWtI"VjlpIA15'00?1a+Kq%>uj8E0onjK>\LqkSfTC*COmKlW:%JUd9!@B2=81m2jY\_l?o7qXc7em9m:rGN,I+Kd6h.I9^]%:*TDC>.G)V\YR.)JE2g)MbL4:^[FLKB&H]"EV>3&DdVu<9O'qNGQ7n<Nd:Tu3P.AjS=P"o2Dhmn2i`48C."DW$]E0mb6">Q\3i4QL)-pUWlEf-AU.K]4+<3C"&TA_fF8Z,fSA;$7BGV&JbIPSu`/-)Xl*#pER_48K\*J*I9"SFfA]%F5eP*:5"Ue#%@>(a];%H')1!>j:L5n9Efb/E]7VrI6.cSh^Y">cj=XpZ>-$XVSLQHC;4;n`5N`ip<MZ7nE$5]0>ceF:Z+gD]F5-lu%Kru3V/8.OIbh<:@ZZ_"F@'lEhNaoNDWM<%LXk-uB2seQ/)@MfodjAM;XLUIl3-fa.Eo2jRNWk!iqYs_GhWSXBa5t?-_V(6Ci>QgErp/,HgL3,=lbn=IrE)'m^-?9V[MiJ#'U)l>Ltcr-/[NVM(."kMdq_'FS"gS(Fk<$Vih_BT=GDGD-ud_m\_57#=`45,c(TqoH6!c/RZaU5rh7Z-/`)4RN'>6C<G:8_:@HKODQ_=@b^!G[OtCJ4W`Ipq5H!XoE1:Wekh1NnP3=!Q>*o(uStfr\kE/)$\Ln9-dg48joaf*a=PUX'J1OE4L@X9m_cJ*;%E5g:a5%e$fN17c4O18ie[o5uc9G#:[PR~>endstream
endobj
28 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 717
>>
stream
GauI39lldX&;KZQ'mk$YK0Vaoioh`6eC1d!-FB[U*$O(dJ1-SEk2/\k_]NmaWH/%VHN<HFn$=M[6f:_pr5o*!@flWe##$/-"/^l@EW_qsdI.YkN,T[9$:PNG"Ag(U%]C2=&gE'49O\DSi(.eo<+$t>/:b5ic7t)X_>Ibq!HAlo5lh930u8n06c97MW@<]AMr2Nurkuf_N*,8gfk`;hk#UCMC9&7N']=d;O;'DJpAO.g53mnXEiF0k5A[I_d_Z)0*%"U&@SCC89&WB*WE`MY.d[I3IR/"ZagJ$B6\-3b5LdGF"u$/RB<7]6ZF@//<mC[MHU;qZW(0T#Th%QPFj4m/KB@:ae[-W9g-W5Bl/MqT*4.FJqQslu!4joN?^"g'l1F`OF+3%ehdqU@KX&>)AB0S0cH[sbh,Q+9r:TWhX[f'`PtPCdouC+@[T_r9Abr$e"ZV[]nG6C&dO"X,J+]WA>\&#gIRI'Mrngh+qbk!'=(dfk9Y*:$c093&IBGB:VkpAW[(?6$oW2DKJi[AY;>7WHUqU*c`Z]90FK20'Z?b*,GM$aA]^F)2X)NYR'Z73M\^"d4BBrO-!GPS8=)Vh-/+'Xn<@41jW7'QcocMdgSpJ_#Dg8REp'Y#oVudiUS<<P?ZUkZ7%]GW8[K_N^V0Y&_Dh.q%h>9[tjB3@+W*ST\55K#"Xt'+=r)pYQF,fmPWh37-Nr7Zi-]D@4/q"!5~>endstream
endobj
xref
0 29
0000000000 65535 f
0000000061 00000 n
0000000123 00000 n
0000000230 00000 n
0000000342 00000 n
0000000547 00000 n
0000000752 00000 n
0000000857 00000 n
0000001062 00000 n
0000001267 00000 n
0000001472 00000 n
0000001678 00000 n
0000001884 00000 n
0000001962 00000 n
0000002168 00000 n
0000002374 00000 n
0000002580 00000 n
0000002650 00000 n
0000002942 00000 n
0000003062 00000 n
0000004900 00000 n
0000006178 00000 n
0000007466 00000 n
0000009202 00000 n
0000010709 00000 n
0000012816 00000 n
0000014422 00000 n
0000016166 00000 n
0000018423 00000 n
trailer
<<
/ID
[<fd40511b2cfd3fa4f915567dab118c8a><fd40511b2cfd3fa4f915567dab118c8a>]
% ReportLab generated PDF document -- digest (opensource)
/Info 17 0 R
/Root 16 0 R
/Size 29
>>
startxref
19231
%%EOF

View File

@@ -0,0 +1,377 @@
# Les 16 — MCP (Model Context Protocol)
## Slide Overzicht (Klas A — 3 uur fysiek, demo-driven)
**Lesvorm:** Tim demonstreert klassikaal. Studenten kijken. Zelf bouwen = huiswerk.
**Demo-app:** Eigen MCP server voor Polderfest-data (Node/TypeScript)
**Vervolg op:** Les 15 — Cursor + Vercel deploy
**Aansluit op:** Les 17 — Externe APIs in diepte
---
## Slide 1: Title
### Les 16 — MCP
**Visual:** "Les 16" BLUE, "Model Context Protocol" BLACK, subtitle "Eén protocol, alle AI-clients, jouw tools beschikbaar"
---
## Slide 2: Terugblik
### Waar staan we?
**Lessen 11-15:**
- AI SDK, Tool Calling, Agents, RAG, Cursor+Vercel deploy
**Het probleem dat we nu oplossen:**
- Tools die je bouwt zitten VAST aan jouw app
- Wil je dezelfde tools in Cursor? Claude Desktop? Een script? Drie keer schrijven
- MCP = standaard die alle AI-clients begrijpen
**Visual:** 3 AI-clients (Cursor, Claude Desktop, ChatGPT) → 1 MCP-server → jouw data
---
## Slide 3: Planning
### Vandaag — 180 minuten
| Onderwerp | Duur |
|-----------|------|
| Terugblik + waarom MCP? | 15 min |
| Theorie: MCP architectuur | 20 min |
| Theorie: bestaande servers + eco | 15 min |
| **Live Demo 1** — MCP SDK + eerste server | 25 min |
| **Live Demo 2** — Server laden in Cursor/Claude | 20 min |
| **Pauze** | 15 min |
| **Live Demo 3** — Eigen Polderfest MCP server | 30 min |
| **Live Demo 4** — Resources + prompts toevoegen | 20 min |
| MCP vs Tool Calling | 10 min |
| Lesopdracht + Huiswerk | 10 min |
---
## Slide 4: Wat is MCP?
### Een open protocol voor AI ↔ tools
**Definitie:**
> Model Context Protocol (MCP) is an open standard for connecting AI assistants to data sources and tools — created by Anthropic in november 2024.
**Het idee:**
- AI-clients (Claude Desktop, Cursor, custom apps) zijn **MCP clients**
- Tools/data-bronnen zijn **MCP servers**
- Eén protocol voor de communicatie tussen beide
**Wat krijg je:**
- Bouw je tool ÉÉN keer, gebruik 'm in elke MCP-client
- Het Anthropic-ecosysteem heeft al 100+ MCP servers (filesystem, github, slack, postgres, ...)
- Voor productie-apps: complete decoupling tussen AI-frontend en jouw backend
**Visual:** USB-C analogie — één standaard connector voor verschillende apparaten
---
## Slide 5: MCP architectuur
### Client / Server / Transport
```
┌──────────────┐ JSON-RPC ┌──────────────┐
│ MCP Client │ ◄────────────────────► │ MCP Server │
│ (Cursor, │ stdio / HTTP/SSE │ (jouw code) │
│ Claude │ │ │
│ Desktop) │ │ exposes: │
└──────────────┘ │ - tools │
│ - resources │
│ - prompts │
└──────────────┘
```
**Drie primitieven die een server kan exposeren:**
- **Tools** — uitvoerbare functies (zoals Les 12 tool calling)
- **Resources** — data die de AI mag lezen (files, DB rows)
- **Prompts** — herbruikbare prompt-templates
**Twee transports:**
- **stdio** — server runt als lokaal proces, communicatie via stdin/stdout (default)
- **HTTP/SSE** — remote server, voor cloud-deploys
---
## Slide 6: Bestaande MCP servers
### Het ecosysteem
**Officiële Anthropic servers (npm @modelcontextprotocol/server-*):**
- `filesystem` — lees/schrijf files
- `github` — repos, issues, PRs
- `slack` — channels, messages
- `postgres` — query database
- `puppeteer` — browser automation
- `memory` — persistent geheugen tussen sessies
**Community servers:**
- `linear`, `notion`, `asana`, `figma`, `stripe`, `youtube`, ...
**Hoe gebruik je ze:**
- In Cursor: `~/.cursor/mcp.json` config
- In Claude Desktop: Settings → Developer → MCP config
- Beide laden de server bij startup, server wordt vanzelf beschikbaar
**Visual:** Lijst van populaire MCP-servers met logo's
---
## Slide 7: Eigen MCP server bouwen
### TypeScript SDK basics
**Package:**
```bash
pnpm add @modelcontextprotocol/sdk
```
**Skeleton:**
```typescript
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.string().optional(), stage: z.string().optional() },
async ({ day, stage }) => {
const bands = await queryDb(day, stage);
return { content: [{ type: "text", text: JSON.stringify(bands) }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
```
**Drie regels:** maak server, registreer tool, connect transport. Klaar.
---
## Slide 8: Wat we vandaag bouwen
### Polderfest MCP server
**Doel:** MCP server die Polderfest-data exposeert. Werkt in Cursor, Claude Desktop, en elk ander MCP-client.
**Wat zit erin:**
| Type | Naam | Wat |
|------|------|-----|
| Tool | `searchBands` | Filter op dag/stage/genre |
| Tool | `getBandStats` | Aggregate counts |
| Tool | `addFavorite` | Write — voeg toe aan favorites |
| Resource | `bands://list` | Volledige bandenlijst (read-only) |
| Prompt | `dailyRecap` | "Geef recap van dag X" template |
**Demo:**
- Eerst werkend in dev (stdio)
- Daarna geladen in Cursor → Cursor kan zelfstandig vragen beantwoorden over Polderfest
- Idem in Claude Desktop
---
## Slide 9: LIVE DEMO 1 — MCP SDK + eerste server
### ~25 min
**Wat ik laat zien:**
1. `pnpm create-typescript-app mcp-polderfest` + `pnpm add @modelcontextprotocol/sdk zod`
2. `src/index.ts` — minimal server skeleton (15 regels)
3. Eerste tool: `searchBands` met hardcoded data (mock)
4. `pnpm build``dist/index.js`
5. Test met MCP Inspector: `npx @modelcontextprotocol/inspector dist/index.js`
6. Inspector UI in browser → zien dat tool beschikbaar is → tool aanroepen → resultaat
**MCP Inspector is jouw vriend** — debug-tool zonder Claude/Cursor nodig
---
## Slide 10: LIVE DEMO 2 — Laden in Cursor + Claude Desktop
### ~20 min
**Cursor (`~/.cursor/mcp.json`):**
```json
{
"mcpServers": {
"polderfest": {
"command": "node",
"args": ["/full/path/to/mcp-polderfest/dist/index.js"]
}
}
}
```
**Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`):**
Idem — zelfde JSON structuur.
**Demo:**
1. Edit config, restart Cursor
2. Cursor → settings → MCP — server zichtbaar + groen vinkje
3. In chat: "Zoek bands op zaterdag voor mij" — Cursor roept onze tool aan
4. Idem in Claude Desktop chat
5. Eén server, twee clients, geen extra werk
---
## Slide 11: Pauze
### 15 min
---
## Slide 12: LIVE DEMO 3 — Echte Polderfest server
### ~30 min
**Wat ik laat zien:**
1. Server koppelen aan Supabase (env vars via dotenv)
2. `searchBands` echt naar database
3. `getBandStats` toevoegen
4. `addFavorite` (write-tool) toevoegen — let op user-intent
5. Errors netjes afhandelen (return `isError: true`)
6. Live testen in Cursor — vraag stellen, écht data uit database komt terug
7. Trick: ook nuttig in Claude Desktop voor research-werkflow
**Belangrijk:** MCP server is gewoon een node-proces. Logging via `console.error` (stdout is gereserveerd voor protocol).
---
## Slide 13: LIVE DEMO 4 — Resources + Prompts
### ~20 min
**Resources** zijn read-only data die AI mag inladen:
```typescript
server.resource(
"bands-list",
"bands://list",
async () => ({
contents: [{
uri: "bands://list",
mimeType: "text/plain",
text: bandList.map(b => b.name).join("\n"),
}],
})
);
```
In Cursor: `@polderfest:bands-list` — bands-data direct beschikbaar voor AI
**Prompts** zijn herbruikbare templates:
```typescript
server.prompt(
"daily-recap",
"Geef een samenvatting van een festivaldag",
{ day: z.string() },
({ day }) => ({
messages: [{
role: "user",
content: { type: "text", text: `Geef recap van ${day} ...` }
}]
})
);
```
In Cursor command palette: snel die prompt invoegen.
---
## Slide 14: MCP vs Tool Calling
### Wanneer welk?
| | Tool Calling (Les 12) | MCP (Les 16) |
|---|----------------------|--------------|
| Waar leeft de tool | In jouw Next.js app | Aparte server, los proces |
| Wie kan 'm gebruiken | Alleen jouw chat | Alle MCP-clients |
| Setup-complexiteit | Laag | Iets hoger |
| Distributie | Niet — alleen in jouw app | npm publish, anderen installen |
| Voor productie-app | Default | Voor herbruikbaarheid / multi-client |
**Mentale model:**
- **Tool calling** = "function in mijn app"
- **MCP server** = "library die elke AI-client kan gebruiken"
Veel productie-teams gebruiken beide: tool calling in hun product, MCP voor interne dev-tools.
---
## Slide 15: Lesopdracht + Huiswerk
### Bouw je eigen MCP server
**Lesopdracht (30 min):**
- MCP server skeleton opzetten
- 1 tool met mock-data
- MCP Inspector test
- Laden in Cursor of Claude Desktop
**Huiswerk (~2 uur, voor Les 17):**
- A: 3 tools (read) + 1 write-tool naar Supabase
- B: 1 resource toevoegen
- C: Laden in Cursor — werkt + screenshot in MCP.md
- D: `MCP.md` met tool-lijst + 3 voorbeeld-prompts + reflectie
**Bonus:** publish naar npm, OR maak een HTTP-transport variant voor remote loading
---
## Slide 16: Volgende les + Afsluiting
### Vragen?
**Vandaag gezien:**
- MCP = open protocol voor AI ↔ tools/data
- Drie primitieven: tools, resources, prompts
- Twee transports: stdio + HTTP/SSE
- Eigen server in <50 regels code
- Werkt in Cursor + Claude Desktop + custom clients
**Volgende les (Les 17): Externe APIs in diepte**
- OAuth flows (Google, GitHub login)
- Webhooks ontvangen + verifiëren
- Paid APIs (Stripe, Resend)
- Rate-limiting + retry patterns
**Daarna:** Les 18 — Supabase Auth + RLS multi-user (de laatste!)
**Vragen?**
---
## Slide Summary
| # | Title | Type |
|---|-------|------|
| 1 | Title | Opening |
| 2 | Terugblik | Recap |
| 3 | Planning | 180-min |
| 4 | Wat is MCP | Theorie |
| 5 | MCP architectuur | Theorie |
| 6 | Bestaande servers | Theorie |
| 7 | Eigen server bouwen | Theorie |
| 8 | Wat we bouwen | Intro |
| 9 | **DEMO 1** — SDK setup | Demo |
| 10 | **DEMO 2** — Laden Cursor/Claude | Demo |
| 11 | Pauze | Break |
| 12 | **DEMO 3** — Polderfest server | Demo |
| 13 | **DEMO 4** — Resources + Prompts | Demo |
| 14 | MCP vs Tool Calling | Reflectie |
| 15 | Lesopdracht + Huiswerk | Praktijk |
| 16 | Afsluiting | Closing |
---
## Bronnen
- **MCP officiële site:** https://modelcontextprotocol.io/
- **MCP SDK (TS):** https://github.com/modelcontextprotocol/typescript-sdk
- **MCP Inspector:** `npx @modelcontextprotocol/inspector`
- **Anthropic announcement:** https://www.anthropic.com/news/model-context-protocol
- **Cursor MCP docs:** https://docs.cursor.com/context/model-context-protocol
- **Claude Desktop MCP:** https://docs.anthropic.com/en/docs/build-with-claude/mcp
- **Awesome MCP servers:** https://github.com/punkpeye/awesome-mcp-servers

BIN
Les16-MCP/Les16-Slides.pdf Normal file

Binary file not shown.

BIN
Les16-MCP/Les16-Slides.pptx Normal file

Binary file not shown.

BIN
Les16-MCP/lu45lvnjc.tmp Normal file

Binary file not shown.