302 lines
7.9 KiB
Markdown
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
|