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

7.9 KiB

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:

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
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).

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
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):

{
  "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:

**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:

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