fix: update lessons
This commit is contained in:
1
Les16-MCP/.~lock.Les16-Slides.pdf#
Normal file
1
Les16-MCP/.~lock.Les16-Slides.pdf#
Normal file
@@ -0,0 +1 @@
|
||||
,sharp-zealous-planck,claude,21.05.2026 11:11,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;
|
||||
412
Les16-MCP/Les16-Docenttekst.md
Normal file
412
Les16-MCP/Les16-Docenttekst.md
Normal 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
301
Les16-MCP/Les16-Huiswerk.md
Normal 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
|
||||
137
Les16-MCP/Les16-Huiswerk.pdf
Normal file
137
Les16-MCP/Les16-Huiswerk.pdf
Normal 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)_QMfebtCaR`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
|
||||
218
Les16-MCP/Les16-Lesopdracht.md
Normal file
218
Les16-MCP/Les16-Lesopdracht.md
Normal 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 |
|
||||
143
Les16-MCP/Les16-Lesopdracht.pdf
Normal file
143
Les16-MCP/Les16-Lesopdracht.pdf
Normal 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
523
Les16-MCP/Les16-Lesstof.md
Normal 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
257
Les16-MCP/Les16-Lesstof.pdf
Normal 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_$`e=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
|
||||
377
Les16-MCP/Les16-Slide-Overzicht.md
Normal file
377
Les16-MCP/Les16-Slide-Overzicht.md
Normal 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
BIN
Les16-MCP/Les16-Slides.pdf
Normal file
Binary file not shown.
BIN
Les16-MCP/Les16-Slides.pptx
Normal file
BIN
Les16-MCP/Les16-Slides.pptx
Normal file
Binary file not shown.
BIN
Les16-MCP/lu45lvnjc.tmp
Normal file
BIN
Les16-MCP/lu45lvnjc.tmp
Normal file
Binary file not shown.
Reference in New Issue
Block a user