Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Mcp Server Laravel Package

opgginc/laravel-mcp-server

Deep Wiki
Context7

Getting Started

Prerequisites: PHP 8.2+, Laravel 11/12, and a cache store that survives across requests (Redis, database, file — not array). The package speaks JSON-RPC over two transports: Streamable HTTP (single endpoint, POST + optional GET stream) and legacy SSE (a GET event stream plus a separate POST message endpoint).

1. Install and publish

composer require opgginc/laravel-mcp-server
php artisan vendor:publish --tag=mcp-server-config   # config/mcp-server.php

The published config is the map of your whole setup. Read it top to bottom before writing any code — it controls the route prefix, which transport(s) are exposed, the middleware stack applied to MCP routes, and the list of server classes to mount.

2. Scaffold a server and a tool

php artisan make:mcp-server WeatherServer
php artisan make:mcp-tool CurrentWeatherTool

The generator gives you a server class (a container of tools) and a tool class with the four things the protocol cares about: a name, a description, an input schema (JSON Schema), and a handler that returns content.

class WeatherServer extends McpServer
{
    protected array $tools = [
        CurrentWeatherTool::class,
    ];
}

Register it in config/mcp-server.php (typically under a servers key mapping a path/name to the class), then confirm the routes exist:

php artisan route:list | grep -i mcp

3. Connect a client

The fastest feedback loop is the official inspector, pointed at your Streamable HTTP endpoint:

npx @modelcontextprotocol/inspector
# Transport: Streamable HTTP
# URL: http://localhost:8000/mcp   (plus your auth header)

For Claude Desktop / Cursor, add an entry under mcpServers using the same URL and a bearer token if you've put auth middleware on the route.

First use case to build: pick one read-only capability that already exists as an Eloquent query or an internal API — "find customer by email", "list open incidents for a service", "get latest deploy status". Read-only, single-argument tools validate your transport, auth, and schema wiring in an afternoon without any risk of an agent mutating production data.

Implementation Patterns

Keep tools thin; put logic in services. A tool class is an adapter between an LLM's JSON arguments and your domain layer. Validate, delegate, format. Anything more and you'll be duplicating business rules.

class CurrentWeatherTool extends Tool
{
    public function __construct(private WeatherService $weather) {}

    public function name(): string { return 'get_current_weather'; }

    public function description(): string
    {
        return 'Returns current conditions for a city. Use when the user asks about weather now.';
    }

    public function inputSchema(): array
    {
        return [
            'type' => 'object',
            'properties' => [
                'city'   => ['type' => 'string', 'description' => 'City name, e.g. "Seoul"'],
                'units'  => ['type' => 'string', 'enum' => ['metric', 'imperial'], 'default' => 'metric'],
            ],
            'required' => ['city'],
            'additionalProperties' => false,
        ];
    }

    public function handle(array $arguments): mixed
    {
        $data = validator($arguments, [
            'city'  => ['required', 'string', 'max:120'],
            'units' => ['nullable', 'in:metric,imperial'],
        ])->validate();

        return $this->weather->current($data['city'], $data['units'] ?? 'metric');
    }
}

Treat the description and schema as prompt engineering, not documentation. The description is the only thing the model sees when choosing a tool. Write it as a routing rule ("Use when…", "Do not use for…"), keep the property set small, and use enum and default aggressively — every optional free-text field is a future hallucination.

One server per audience, not per feature. A common production layout:

Server Path Auth Tools
PublicServer /mcp/public none / IP allowlist docs lookup, status page
InternalServer /mcp Sanctum token tickets, deploys, metrics
AdminServer /mcp/admin token + role gate anything that writes

This keeps the tool list (and the token cost of the tool list) small per connection, and gives you a clean place to hang different middleware.

Scope by the authenticated principal, not by argument. Never accept user_id as a tool argument. Resolve the user from the request in the tool's constructor or via auth()->user(), and filter every query by it. The model can and will pass someone else's ID if you let it.

public function handle(array $arguments): mixed
{
    return Incident::where('team_id', auth()->user()->team_id)
        ->findOrFail($arguments['incident_id']);
}

Wrap mutations in confirmation-shaped tools. Instead of delete_ticket(id), expose propose_ticket_deletion(id) that returns a summary plus a one-time token, and a second tool that consumes the token. For destructive operations with no undo, consider exposing them only on an admin server that isn't attached to everyday chat sessions.

Return structured, bounded content. Return arrays/Arrayable and let the package serialise, or return a text block. Either way, page and trim: a tool that dumps 400 rows burns the client's context window and gets truncated mid-JSON. Return the top N plus a total and a hint to narrow the query.

Test at the right layer. Unit-test handle() directly with an array of arguments — that's where 90% of your bugs live and it's fast:

it('rejects an unknown city', function () {
    $tool = app(CurrentWeatherTool::class);
    expect(fn () => $tool->handle(['city' => 'Atlantis']))->toThrow(ValidationException::class);
});

Then one feature test per server that posts a real JSON-RPC tools/list and tools/call body to the route, asserting on the envelope. That catches route, middleware, and serialisation regressions that unit tests miss.

Log tool invocations as a first-class event. You will be asked "why did the agent do that?" Log tool name, argument keys (not values — they may contain PII), duration, user ID, and error. This is also how you find tools that look popular but always fail.

Gotchas and Tips

The cache store is load-bearing for SSE. SSE sessions are persisted between the GET stream and the POST message request. If CACHE_STORE=array (the default in some test/bootstrap configs) or if you're behind a load balancer without sticky sessions, tool calls will mysteriously 404 or hang. Use Redis, and make sure every web node shares it. If SSE behaves oddly, this is the first thing to check.

Long-lived responses break default web-server timeouts. An SSE stream is one request held open for minutes. Set fastcgi_read_timeout / proxy_read_timeout well above your longest tool call, and disable buffering on the MCP location or events arrive in clumps:

location /mcp {
    proxy_buffering off;
    proxy_read_timeout 3600s;
    fastcgi_buffering off;
    fastcgi_read_timeout 3600s;
    chunked_transfer_encoding on;
}

Also raise max_execution_time for those routes and make sure no middleware calls ob_start() without flushing.

CSRF will silently kill your POST. MCP clients don't send CSRF tokens. Mount the MCP routes on the api middleware group (or add the paths to $except in bootstrap/app.php), not web.

Don't hold state on the server or tool instance. Workers are shared; a property set during one tool call can leak into the next. Resolve fresh per call.

Throwing is not the same as failing gracefully. An uncaught exception becomes a protocol-level error, which many clients render as a broken connection rather than a message the model can reason about. Catch expected domain errors (ModelNotFoundException, validation) and return a readable message so the model can retry or apologise. Reserve exceptions for genuine bugs.

Config changes need php artisan config:cache (and a clear). After editing config/mcp-server.php in a cached environment, a stale server list means tools "disappear" with no error. Same for route caching.

Tool names are an API contract. Renaming get_weather to weather_current invalidates any prompt caching, saved conversation, or client-side allowlist on the other end. Namespace from day one (jira_search_issues, github_list_prs) and treat renames as breaking changes.

Schemas drift from validators. If your JSON Schema says city is required but your validator says nullable, the model will omit it and you'll get a 422 the model can't see the shape of. Generate the schema and the validation rules from one source, or at minimum keep them adjacent in the file with a comment.

Debugging order of operations: MCP Inspector first (isolates client from server) → php artisan route:list (is the route even there?) → storage/logs/laravel.log with a Log::info at the top of handle() (is it reaching the tool?) → curl the raw JSON-RPC body (is it middleware?). Nine times out of ten it's config cache, CSRF, or the cache store.

Extension points worth knowing: the config's middleware array (add auth, rate limiting, tenancy scoping per server), the ability to define multiple servers with overlapping tool classes but different middleware, and the tool's dependency injection — because tools are resolved from the container you can inject request-scoped services, but not singletons that cache per-user data.

Rate-limit per token, not per IP. Agents retry aggressively on error. A throttle:60,1 on the MCP route applied per authenticated token prevents one runaway client from starving the rest, and stops a retry loop from hammering a downstream API.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core