# 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); 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