Files
novi-lessons/Les16-MCP/Les16-Huiswerk.md
2026-06-03 16:58:25 +02:00

302 lines
7.9 KiB
Markdown

# 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