Zo zou ik een veilige MCP-server bouwen met Laravel 13
Een praktisch beveiligingsmodel voor Laravel MCP met authenticatie, autorisatie, smalle tools, idempotency, auditlogs en tests.
Laravel 13 maakt een MCP-server vertrouwd: routes, middleware, validatie, policies, dependency injection en tests. Dat is precies het juiste denkmodel. Een MCP-tool is geen magische AI-functie, maar een API-endpoint waarvan de aanroeper toevallig een model is. Juist daarom moet de toegestane capaciteit heel duidelijk zijn.
Ontwerp eerst de bevoegdheid, daarna het protocol
Ik begin met wat de gebruiker mag bereiken, niet met welke interne services bestaan. create_support_draft is een begrijpelijke bevoegdheid. execute_query, call_service en update_model zijn dat niet. Hun macht is niet uit de naam af te leiden en hun invoerruimte is veel te breed.
Een tool heeft één eigenaar, één doel, een smal schema, voorspelbare kosten en een kleine response. Read-only betekent niet automatisch veilig. Een zoektool kan nog steeds gegevens van een andere tenant, interne notities of veel te veel data tonen.
- Gebruik zakelijke werkwoorden in plaats van infrastructuurtermen
- Retourneer alleen wat nodig is voor de volgende beslissing
- Scheid zoeken van wijzigen
- Sta geen willekeurige SQL, shellcommando’s, URL’s of klassennamen toe
Authenticeer de server en autoriseer iedere tool
Laravel beschrijft OAuth 2.1 met Passport als breed compatibele keuze voor externe MCP-clients. Sanctum is praktisch voor applicaties die dat al gebruiken. Authenticatie vertelt wie er belt, maar niet of die persoon deze order mag zien, deze refund mag uitvoeren of binnen deze tenant mag zoeken.
Daarom staat autorisatie in iedere tool-handler, zo dicht mogelijk bij het laden van de resource. De query wordt eerst op tenant beperkt en daarna controleert een policy de actie. Wereldwijd laden en achteraf filteren is trager en veel makkelijker fout te doen.
Mcp::web('/mcp/support', SupportServer::class)
->middleware(['auth:sanctum', 'throttle:mcp']);
public function handle(Request $request): Response
{
$ticket = Ticket::query()
->whereBelongsTo($request->user()->organisation)
->findOrFail($request->get('ticket_id'));
if (! $request->user()->can('draftReply', $ticket)) {
return Response::error('Permission denied.');
}
// Return only fields required by the tool contract.
}Behandel instructies en gegevens als aparte trust-zones
Toolbeschrijvingen zijn vertrouwde applicatie-instructies. Tickettekst, uploads, productomschrijvingen en webcontent zijn onbetrouwbare data, ook wanneer daarin staat dat eerdere instructies moeten worden genegeerd. Prompts kunnen dat onderscheid uitleggen, maar PHP blijft verantwoordelijk voor autorisatie en toegestane acties.
Voor externe URL’s gebruik ik allowlists, blokkeer ik privé- en metadata-netwerken, beperk ik redirects en responsegrootte en gebruik ik een aparte client. Anders wordt een handige lees-URL-tool een SSRF-mogelijkheid. Bestanden controleer ik op hun werkelijke inhoudstype en ruwe opslagpaden geef ik niet terug.
Maak wijzigingen idempotent en controleerbaar
MCP-clients voeren retries uit en een model kan dezelfde tool tweemaal aanroepen. Iedere relevante wijziging accepteert daarom een operation key die bij gebruiker en canonieke invoer hoort. De database bewaart het resultaat. Dezelfde key met andere invoer levert een conflict op in plaats van een tweede effect.
Voor handelingen met veel impact maakt de eerste tool alleen een voorstel met een samenvatting. Een aparte bevestigingstool voert het uit nadat rechten en actuele staat opnieuw zijn gecontroleerd. Zo ontstaat een natuurlijke menselijke goedkeuring en wordt verouderde context geen actie.
- Audit actor, client, tool, resource, input-hash, resultaat en duur
- Log nooit tokens of gevoelige payloads
- Geef voorstellen een vervaldatum
- Controleer autorisatie opnieuw bij bevestiging
Test misbruik vóór de happy flow
Ik test identifiers van andere tenants, ontbrekende scopes, onverwachte enums, te grote arrays, dubbele calls, oude versies, ingetrokken goedkeuringen, prompt-injection in opgeslagen inhoud en dure zoekacties onder rate limits. Foutresponses mogen bovendien niet verklappen of een verboden resource bestaat.
Tot slot test ik de server met een echte MCP-inspector of client buiten productie. Unittests bewijzen de handlers; protocoltests bewijzen discovery, schema’s, authenticatie en responsevorm. Beide zijn nodig.
Praktische checklist
- Geef iedere tool één smalle zakelijke bevoegdheid
- Authenticeer de MCP-route en autoriseer iedere resourceactie
- Beperk databasequeries voordat records worden geladen
- Gebruik operation keys en voorstel plus bevestiging voor wijzigingen
- Test tenant-escape, retries, injection, limieten en verouderde staat
