Zum Hauptinhalt springen

Was ist ein MCP-Server (fortgeschritten)

Model Context Protocol (MCP) ist die Spezifikation für die standardisierte Kommunikation zwischen KI-Clients (Claude, Cursor, GitHub Copilot etc.) und Anwendungen. MCP kennt drei Kernprimitiven:
PrimitivRolleTypische Verwendung
ToolsFunktionen, die die KI aufrufen kannDatenoperationen, externe API-Integration, Berechnungen
ResourcesKontext, den die KI einlesen kannDokumente, Konfigurationen, dynamische Daten
PromptsWiederverwendbare TemplatesStandardabfragen, geführte Workflows
Der Vorteil, MCP-Server mit Laravel zu bauen: Sie nutzen das gesamte Ökosystem (Eloquent, Cache, Auth, Validierung) direkt weiter.
Diese fortgeschrittene Anleitung geht in die praktische Umsetzung. Für die MCP-Grundlagen siehe Mittelstufe: Laravel MCP.

Installation und Ersteinrichtung

1

Paket installieren

Über Composer:
composer require laravel/mcp
2

Routen-Datei veröffentlichen

Mit vendor:publish erzeugen Sie die Registrierungsdatei routes/ai.php.
php artisan vendor:publish --tag=ai-routes
3

Server-Klasse erzeugen

Erzeugen Sie eine Server-Klasse per Artisan.
php artisan make:mcp-server DatabaseServer
Registrieren Sie Tools, Ressourcen und Prompts in app/Mcp/Servers/DatabaseServer.php.
4

Server registrieren

Registrieren Sie den Server als Route in routes/ai.php.
use App\Mcp\Servers\DatabaseServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/database', DatabaseServer::class);
use App\Mcp\Servers\DatabaseServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::local('database', DatabaseServer::class);
Der Web-Server wird per HTTP POST angesprochen. Der lokale Server läuft als Artisan-Command und dient CLI-basierten KI-Clients.

Tools implementieren

Tools sind Funktionen, die KI-Clients aufrufen. Sie nutzen Service Container, Validierung und Eloquent von Laravel direkt.

Tool erstellen

php artisan make:mcp-tool SearchUsersTool
Implementieren Sie in der erzeugten Klasse die Methoden handle und schema.

Parameter definieren (Schema)

In schema definieren Sie mit dem Illuminate\Contracts\JsonSchema\JsonSchema-Builder die akzeptierten Parameter.
<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('Sucht Nutzer nach Namen oder E-Mail-Adresse.')]
class SearchUsersTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'query'   => 'required|string|max:100',
            'limit'   => 'integer|min:1|max:50',
            'role'    => 'nullable|string|in:admin,editor,viewer',
        ], [
            'query.required' => 'Bitte geben Sie einen Suchbegriff an, z. B. "Tanaka" oder "[email protected]".',
            'limit.max'      => 'Es können höchstens 50 Einträge abgefragt werden.',
            'role.in'        => 'Die Rolle muss admin, editor oder viewer sein.',
        ]);

        $users = \App\Models\User::query()
            ->where(function ($q) use ($validated) {
                $q->where('name', 'like', "%{$validated['query']}%")
                  ->orWhere('email', 'like', "%{$validated['query']}%");
            })
            ->when(isset($validated['role']), fn ($q) => $q->where('role', $validated['role']))
            ->limit($validated['limit'] ?? 10)
            ->get(['id', 'name', 'email', 'role']);

        if ($users->isEmpty()) {
            return Response::text('Es wurden keine Nutzer gefunden, die den Kriterien entsprechen.');
        }

        $result = $users->map(fn ($u) => "ID:{$u->id} {$u->name} <{$u->email}> [{$u->role}]")
                        ->implode("\n");

        return Response::text($result);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'query' => $schema->string()
                ->description('Suchbegriff. Sucht in Name oder E-Mail-Adresse.')
                ->required(),

            'limit' => $schema->integer()
                ->description('Maximale Trefferzahl (Default: 10, Maximum: 50).')
                ->minimum(1)
                ->maximum(50)
                ->default(10),

            'role' => $schema->string()
                ->description('Einzuschränkende Rolle: admin, editor oder viewer.')
                ->enum(['admin', 'editor', 'viewer']),
        ];
    }
}

Tool-Annotationen

Die Annotationen aus dem MCP-Protokoll helfen KI-Clients, die Sicherheit von Tools einzuschätzen.
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;

#[IsReadOnly]      // Read-only-Tool ohne Nebenwirkungen
#[IsIdempotent]    // Bei gleichen Argumenten stets das gleiche Ergebnis
class SearchUsersTool extends Tool
{
    // ...
}
AnnotationBeschreibung
#[IsReadOnly]Verändert keine Daten
#[IsDestructive]Führt destruktive Operationen aus (z. B. Löschen)
#[IsIdempotent]Erneute Ausführung mit gleichen Argumenten ist sicher
#[IsOpenWorld]Kommuniziert mit externen Entitäten

Strukturierte Antworten

Für JSON-Antworten, die vom Client leichter geparst werden können, verwenden Sie Response::structured.
return Response::structured([
    'total' => $users->count(),
    'users' => $users->map(fn ($u) => [
        'id'    => $u->id,
        'name'  => $u->name,
        'email' => $u->email,
    ])->toArray(),
]);

Streaming-Antworten

Bei langen Vorgängen können Sie einen Generator zurückgeben und Zwischenergebnisse streamen.
use Generator;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;

public function handle(Request $request): Generator
{
    $items = \App\Models\Product::all();
    $total = $items->count();

    foreach ($items as $index => $item) {
        yield Response::notification('processing/progress', [
            'current' => $index + 1,
            'total'   => $total,
            'name'    => $item->name,
        ]);

        // Rechenintensive Arbeit …
        yield Response::text("Fertig: {$item->name}");
    }
}
Beim Web-Server wird der Stream automatisch als SSE (Server-Sent Events) ausgeliefert.

Bedingte Registrierung

Sie können Tools nur bestimmten Nutzern anzeigen.
public function shouldRegister(Request $request): bool
{
    return $request?->user()?->hasRole('admin') ?? false;
}

Ressourcen implementieren

Ressourcen sind Daten, die der KI-Client als Kontext einliest — Dokumente, Konfigurationen oder dynamische Daten.

Statische Ressource

php artisan make:mcp-resource AppDocumentationResource
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Attributes\Uri;
use Laravel\Mcp\Server\Resource;

#[Uri('app://resources/documentation')]
#[MimeType('text/markdown')]
#[Description('Überblick über API-Dokumentation und Geschäftsregeln der Anwendung.')]
class AppDocumentationResource extends Resource
{
    public function handle(Request $request): Response
    {
        $content = \Illuminate\Support\Facades\Storage::get('docs/api-overview.md')
            ?? 'Dokumentation nicht gefunden.';

        return Response::text($content);
    }
}

Dynamische Ressource (URI-Template)

URI-Templates liefern Ressourcen abhängig von URL-Parametern.
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Contracts\HasUriTemplate;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Support\UriTemplate;

#[MimeType('application/json')]
#[Description('Liefert Nutzerinformationen anhand der Nutzer-ID.')]
class UserProfileResource extends Resource implements HasUriTemplate
{
    public function uriTemplate(): UriTemplate
    {
        return new UriTemplate('app://users/{userId}/profile');
    }

    public function handle(Request $request): Response
    {
        $userId = $request->get('userId');

        $user = \App\Models\User::find($userId);

        if (! $user) {
            return Response::error("Nutzer mit ID {$userId} nicht gefunden.");
        }

        return Response::text(json_encode([
            'id'         => $user->id,
            'name'       => $user->name,
            'email'      => $user->email,
            'created_at' => $user->created_at->toIso8601String(),
        ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
    }
}
Der KI-Client fragt die Ressource z. B. per URI app://users/42/profile ab; {userId} erhalten Sie über $request->get('userId').

Ressourcen-Annotationen

Sie können Priorität und Zielgruppe explizit machen.
use Laravel\Mcp\Enums\Role;
use Laravel\Mcp\Server\Annotations\Audience;
use Laravel\Mcp\Server\Annotations\Priority;

#[Audience(Role::Assistant)]   // Für den KI-Assistenten gedacht
#[Priority(0.8)]               // Priorität (0.0–1.0)
class AppDocumentationResource extends Resource
{
    // ...
}

Prompts implementieren

Prompts sind wiederverwendbare Templates für KI-Clients — für Standardabfragen oder komplexe Workflows.

Prompt erstellen

php artisan make:mcp-prompt DataAnalysisPrompt
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Prompts\Argument;

#[Description('Prompt-Vorlage zur Erstellung eines Datenanalyse-Reports.')]
class DataAnalysisPrompt extends Prompt
{
    public function arguments(): array
    {
        return [
            new Argument(
                name: 'target',
                description: 'Analysegegenstand, z. B. "Umsatz des letzten Monats", "Verlauf der Neuregistrierungen"',
                required: true,
            ),
            new Argument(
                name: 'format',
                description: 'Ausgabeformat: "summary" (Zusammenfassung) oder "detail" (Details)',
                required: false,
            ),
        ];
    }

    public function handle(Request $request): array
    {
        $validated = $request->validate([
            'target' => 'required|string|max:200',
            'format' => 'nullable|in:summary,detail',
        ]);

        $target = $validated['target'];
        $format = $validated['format'] ?? 'summary';
        $instruction = $format === 'detail'
            ? 'Erstelle einen ausführlichen Report mit Zahlen, Trends, Ausreißern und Handlungsempfehlungen.'
            : 'Erstelle eine kompakte Zusammenfassung mit den wichtigsten Kennzahlen und drei zentralen Erkenntnissen.';

        return [
            Response::text(
                "Du bist Datenanalyst. {$instruction}"
            )->asAssistant(),
            Response::text(
                "Analysiere folgende Daten: {$target}"
            ),
        ];
    }
}
Mit asAssistant() werden Nachrichten als Aussagen des KI-Assistenten behandelt. So kombinieren Sie System-Prompt und Nutzermitteilungen, um das Verhalten der KI genau zu steuern.

Authentifizierung und Autorisierung

Token-Authentifizierung mit Sanctum

Die einfachste Variante. Der MCP-Client sendet Authorization: Bearer <token>.
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:sanctum');

OAuth-2.1-Authentifizierung

Für robustere Authentifizierung nutzen Sie Laravel Passport.
// routes/ai.php
use Laravel\Mcp\Facades\Mcp;

Mcp::oauthRoutes();

Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:api');
Wenn Sie OAuth nutzen, veröffentlichen Sie die MCP-Autorisierungs-Views und binden sie im AppServiceProvider ein.
php artisan vendor:publish --tag=mcp-views
// app/Providers/AppServiceProvider.php
use Laravel\Passport\Passport;

public function boot(): void
{
    Passport::authorizationView(function ($parameters) {
        return view('mcp.authorize', $parameters);
    });
}

Authentifizierung per eigener Middleware

Bei eigenen API-Tokens prüfen Sie den Authorization-Header per Middleware.
// app/Http/Middleware/McpTokenMiddleware.php
public function handle($request, Closure $next)
{
    $token = $request->bearerToken();

    if (! $token || ! \App\Models\ApiToken::where('token', hash('sha256', $token))->exists()) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $next($request);
}

Autorisierung innerhalb eines Tools

Innerhalb der handle-Methode von Tools oder Ressourcen prüfen Sie Berechtigungen fein granular über $request->user().
public function handle(Request $request): Response
{
    $user = $request->user();

    if (! $user?->can('manage-users')) {
        return Response::error('Sie haben keine Berechtigung, dieses Tool zu verwenden. Bitte wenden Sie sich an die Administration.');
    }

    // Autorisierte Verarbeitung …
}
shouldRegister blendet ein Tool lediglich aus der Liste aus. Die eigentliche Autorisierung beim Aufruf des Tools muss in handle erfolgen.

Praxisbeispiel: Datenbankoperationen

Vollständige Implementierung eines Tools, das Datensätze via Eloquent sucht und anlegt.

Server-Klasse

<?php

namespace App\Mcp\Servers;

use App\Mcp\Tools\CreatePostTool;
use App\Mcp\Tools\SearchPostsTool;
use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;
use Laravel\Mcp\Server;

#[Name('Blog Management Server')]
#[Version('1.0.0')]
#[Instructions('MCP-Server zum Suchen und Anlegen von Blogbeiträgen.')]
class BlogServer extends Server
{
    protected array $tools = [
        SearchPostsTool::class,
        CreatePostTool::class,
    ];
}

Such-Tool (Read-only)

<?php

namespace App\Mcp\Tools;

use App\Models\Post;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[Description('Sucht Blogbeiträge nach Keyword und Status.')]
#[IsReadOnly]
#[IsIdempotent]
class SearchPostsTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'keyword' => 'nullable|string|max:100',
            'status'  => 'nullable|in:draft,published,archived',
            'limit'   => 'integer|min:1|max:20',
        ]);

        $posts = Post::query()
            ->when($validated['keyword'] ?? null, fn ($q, $kw) =>
                $q->where('title', 'like', "%{$kw}%")
                  ->orWhere('body', 'like', "%{$kw}%")
            )
            ->when($validated['status'] ?? null, fn ($q, $s) => $q->where('status', $s))
            ->latest()
            ->limit($validated['limit'] ?? 5)
            ->get(['id', 'title', 'status', 'published_at']);

        if ($posts->isEmpty()) {
            return Response::text('Keine passenden Beiträge gefunden.');
        }

        return Response::structured([
            'total' => $posts->count(),
            'posts' => $posts->map(fn ($p) => [
                'id'           => $p->id,
                'title'        => $p->title,
                'status'       => $p->status,
                'published_at' => $p->published_at?->toIso8601String(),
            ])->toArray(),
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'keyword' => $schema->string()
                ->description('Keyword, das in Titel oder Text enthalten sein soll.'),

            'status' => $schema->string()
                ->description('Status des Beitrags.')
                ->enum(['draft', 'published', 'archived']),

            'limit' => $schema->integer()
                ->description('Maximale Trefferzahl (Default: 5, Maximum: 20).')
                ->default(5),
        ];
    }
}

Erstellungs-Tool (Schreiben)

<?php

namespace App\Mcp\Tools;

use App\Models\Post;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('Legt einen neuen Blogbeitrag als Entwurf an.')]
class CreatePostTool extends Tool
{
    public function handle(Request $request): Response
    {
        $user = $request->user();

        if (! $user?->can('create-posts')) {
            return Response::error('Sie haben keine Berechtigung, Beiträge zu erstellen.');
        }

        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'body'  => 'required|string',
            'tags'  => 'nullable|array',
            'tags.*' => 'string|max:50',
        ], [
            'title.required' => 'Bitte geben Sie einen Titel für den Beitrag an.',
            'body.required'  => 'Bitte geben Sie den Inhalt des Beitrags an.',
        ]);

        $post = Post::create([
            'title'   => $validated['title'],
            'body'    => $validated['body'],
            'status'  => 'draft',
            'user_id' => $user->id,
        ]);

        if (! empty($validated['tags'])) {
            $post->syncTags($validated['tags']);
        }

        return Response::structured([
            'id'         => $post->id,
            'title'      => $post->title,
            'status'     => $post->status,
            'created_at' => $post->created_at->toIso8601String(),
            'message'    => 'Beitrag als Entwurf gespeichert.',
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'title' => $schema->string()
                ->description('Titel des Beitrags (max. 255 Zeichen).')
                ->required(),

            'body' => $schema->string()
                ->description('Inhalt des Beitrags. Markdown ist erlaubt.')
                ->required(),

            'tags' => $schema->array()
                ->description('Array von Tags, z. B. ["Laravel", "PHP"]'),
        ];
    }
}

Praxisbeispiel: Dateisystem-Tool

Implementierung eines Tools, das per Storage-Facade Dateien liest.
<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Storage;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[Description('Listet Dateien im Storage und liest den Inhalt von Textdateien.')]
#[IsReadOnly]
class ReadFileTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'path'    => 'required|string|max:500',
            'disk'    => 'nullable|string|in:local,public,s3',
        ]);

        $disk = $validated['disk'] ?? 'local';
        $path = $validated['path'];

        // Directory-Traversal verhindern
        if (str_contains($path, '..')) {
            return Response::error('Ungültiger Pfad.');
        }

        if (! Storage::disk($disk)->exists($path)) {
            return Response::error("Datei nicht gefunden: {$path}");
        }

        $mimeType = Storage::disk($disk)->mimeType($path);

        // Textdateien inhaltlich zurückgeben
        if (str_starts_with($mimeType, 'text/') || $mimeType === 'application/json') {
            $content = Storage::disk($disk)->get($path);
            return Response::text($content);
        }

        // Bilder als Binärdaten liefern
        if (str_starts_with($mimeType, 'image/')) {
            return Response::fromStorage($path, disk: $disk);
        }

        return Response::error("Dieses Dateiformat wird nicht unterstützt: {$mimeType}");
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'path' => $schema->string()
                ->description('Pfad der zu lesenden Datei, z. B. "reports/2025-01.csv".')
                ->required(),

            'disk' => $schema->string()
                ->description('Zu verwendender Storage-Disk.')
                ->enum(['local', 'public', 's3'])
                ->default('local'),
        ];
    }
}
Bei Datei-Tools ist Pfad-Sanitisierung Pflicht. Verweigern Sie Pfade mit .., damit kein Zugriff außerhalb erlaubter Verzeichnisse möglich ist.

Tests

Sie können MCP-Server, Tools, Ressourcen und Prompts mit Laravels Standard-Testwerkzeugen prüfen.

Tools testen

Rufen Sie das Tool direkt über Server::tool() auf.
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

test('Beiträge können gesucht werden', function () {
    Post::factory()->create(['title' => 'Laravel-Grundlagen', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP für Fortgeschrittene', 'status' => 'draft']);

    $response = BlogServer::tool(SearchPostsTool::class, [
        'keyword' => 'Laravel',
        'status'  => 'published',
    ]);

    $response
        ->assertOk()
        ->assertSee('Laravel-Grundlagen');
});

test('Nutzer ohne Berechtigung kann keinen Beitrag erstellen', function () {
    $user = User::factory()->create();

    $response = BlogServer::actingAs($user)
        ->tool(\App\Mcp\Tools\CreatePostTool::class, [
            'title' => 'Testbeitrag',
            'body'  => 'Inhalt',
        ]);

    $response->assertSee('keine Berechtigung, Beiträge zu erstellen');
});
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

public function test_beitraege_koennen_gesucht_werden(): void
{
    Post::factory()->create(['title' => 'Laravel-Grundlagen', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP für Fortgeschrittene', 'status' => 'draft']);

    $response = BlogServer::tool(SearchPostsTool::class, [
        'keyword' => 'Laravel',
        'status'  => 'published',
    ]);

    $response
        ->assertOk()
        ->assertSee('Laravel-Grundlagen');
}

public function test_nutzer_ohne_berechtigung_kann_keinen_beitrag_erstellen(): void
{
    $user = User::factory()->create();

    $response = BlogServer::actingAs($user)
        ->tool(\App\Mcp\Tools\CreatePostTool::class, [
            'title' => 'Testbeitrag',
            'body'  => 'Inhalt',
        ]);

    $response->assertSee('keine Berechtigung, Beiträge zu erstellen');
}

Ressourcen und Prompts testen

// Ressource testen
$response = BlogServer::resource(\App\Mcp\Resources\AppDocumentationResource::class);
$response->assertOk()->assertSee('API');

// Prompt testen
$response = BlogServer::prompt(\App\Mcp\Prompts\DataAnalysisPrompt::class, [
    'target' => 'Umsatz des letzten Monats',
    'format' => 'summary',
]);
$response->assertOk()->assertSee('Datenanalyst');

Wichtige Assertion-Methoden

MethodeBeschreibung
assertOk()Response enthält keinen Fehler
assertSee($text)Response enthält den angegebenen Text
assertHasErrors()Response enthält Fehler
assertHasNoErrors()Response enthält keine Fehler
assertName($name)Tool-Name prüfen
assertSentNotification($method, $data)Notification wurde gesendet
assertNotificationCount($count)Anzahl gesendeter Notifications

Debugging mit dem MCP Inspector

Für interaktives Debugging nutzen Sie den MCP Inspector.
# Web-Server inspizieren
php artisan mcp:inspector mcp/database

# Lokalen Server inspizieren
php artisan mcp:inspector database

Hinweise zum Deployment

HTTP-Streaming und SSE

Wenn Sie im Web-Server Streaming-Responses (Generatoren) verwenden, prüfen Sie die Server-Konfiguration.
location /mcp {
    proxy_pass http://127.0.0.1:8000;
    proxy_buffering off;          # Für SSE Buffering deaktivieren
    proxy_cache off;
    proxy_read_timeout 3600s;     # Für lange Verbindungen
    proxy_set_header Connection '';
    chunked_transfer_encoding on;
}
# Bei Nutzung von mod_proxy_http
ProxyPass /mcp http://127.0.0.1:8000/mcp
ProxyPassReverse /mcp http://127.0.0.1:8000/mcp
SetEnv proxy-sendchunks 1

Kombination mit Laravel Octane

Für stark frequentierte MCP-Server bietet sich Laravel Octane (FrankenPHP oder Swoole) an — der Overhead pro Request sinkt deutlich.
Bei Octane wird Zustand über Requests hinweg geteilt. Vermeiden Sie in Tools statische Properties oder globalen Zustand.

Rate Limiting

Beschränken Sie Requests am MCP-Server über die throttle-Middleware.
// Eigenen Rate Limiter in config/cache.php definieren
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware(['auth:sanctum', 'throttle:60,1']);

Caching

Bei häufig genutzten Read-only-Tools lohnt sich Caching.
public function handle(Request $request): Response
{
    $data = \Illuminate\Support\Facades\Cache::remember(
        "mcp:search:{$request->get('keyword')}",
        now()->addMinutes(5),
        fn () => $this->fetchFromDatabase($request)
    );

    return Response::structured($data);
}

Logging und Monitoring

Loggen Sie MCP-Tool-Aufrufe, um die Nutzung durch KI-Clients nachzuvollziehen.
public function handle(Request $request): Response
{
    \Illuminate\Support\Facades\Log::info('MCP tool called', [
        'tool'   => static::class,
        'user'   => $request->user()?->id,
        'params' => $request->all(),
    ]);

    // ...
}
In der Produktion empfiehlt es sich, MCP-Server-Performance und Exceptions mit Laravel Telescope oder Sentry zu überwachen.
Zuletzt geändert am 13. Juli 2026