mevisoft/laravel-mcp
Laravel MCP helps you build Model Context Protocol servers in Laravel, exposing tools, resources, and prompts to AI clients through an expressive PHP API. It follows Laravel conventions and Artisan workflows so you can ship MCP endpoints quickly.
Install the package with Composer and publish its configuration:
composer require mevisoft/laravel-mcp
php artisan vendor:publish --tag="laravel-mcp-config"
Where to look first, in order:
config/mcp.php — server name/version, transport(s), route/middleware registration, and any tool-discovery paths. This file tells you the shape of everything else.src/Contracts or similar) — the Tool, Resource, and Prompt interfaces define what a handler must implement (name, description, input schema, handle method).php artisan route:list | grep mcp) — confirms whether an HTTP/SSE endpoint is exposed and under which middleware.php artisan make:mcp-tool / mcp:serve) — the fastest way to see a correct, working skeleton.First use case: wrap one read-only, high-value query as a Tool — e.g. search_orders that accepts a status and date range and returns a trimmed array of order summaries. Register it on the default server, connect from Claude Desktop (or Cursor / the MCP Inspector) over stdio locally, confirm it appears in the tool list, then point the same server at a staging HTTP endpoint behind Sanctum. Read-only first: you get the full loop (schema → validation → serialization → client) with zero blast radius.
Thin tool, fat action. A tool class should only describe and validate. Delegate the real work to an existing Action/Service/Query object so the same logic is reachable from HTTP, jobs, and MCP:
public function handle(array $arguments): array
{
$data = validator($arguments, [
'status' => ['nullable', Rule::in(['open', 'paid', 'cancelled'])],
'limit' => ['nullable', 'integer', 'min:1', 'max:50'],
])->validate();
return SearchOrders::run($data, $this->user())->toArray();
}
Bind identity, not parameters. Resolve the acting user from the authenticated token/session and inject it, rather than accepting user_id as a tool argument. This keeps Gate/Policy checks, global scopes, and tenancy resolution intact — an MCP call should be indistinguishable from a normal web request in the authorization path.
Resources for reads, tools for writes. Expose Eloquent models as Resources (list/read with cursors) and reserve Tools for actions with side effects. This maps cleanly onto how clients present the two, and it keeps your destructive surface small and auditable.
Transport per environment. stdio for local development and desktop clients; Streamable HTTP/SSE behind an authenticated route group for remote clients. Keep the HTTP group under the same middleware stack as your API (auth:sanctum, throttle) so rate limiting and token expiry apply for free.
Test at two levels. Unit-test the tool class directly (schema validation, happy path, authorization failure), and add one integration test that POSTs a JSON-RPC tools/call to the endpoint and asserts the envelope. The MCP Inspector is the right tool for manual smoke tests, but it should not be your only coverage.
Instrument every invocation. Log tool name, argument keys (not values, where sensitive), duration, and outcome. The caller is an LLM — you need a trail to explain why a write happened. Queue anything above a few seconds and return a job handle the model can poll.
dump(), dd(), or echo corrupts it and the client disconnects with an opaque error. Route logs to files/stderr and keep LOG_STACK off stdout.$hidden-adjacent attributes and produces enormous payloads. Always return explicit arrays or API Resources with a selected column set.limit=100000 or a one-year date range. Clamp page size server-side and return a cursor; treat every argument as untrusted input.proxy_buffering off (nginx) and output buffering disabled; otherwise responses arrive in one lump or not at all. Test through the actual proxy, not just artisan serve."status must be one of: open, paid, cancelled"), not as stack traces.php artisan config:clear after editing config/mcp.php; some boot-time reads silently ignore changes under a cached config.composer.json and re-run your integration test on each bump.How can I help you explore Laravel packages today?