Model Context Protocol server for Laravel — expose your application to MCP clients as RBAC-aware tools. Runs on PHP 7.3 / Laravel 8 and up.

Maintainers

Package info

github.com/wizcoders/mcp

pkg:composer/wizcoders/mcp

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-24 15:42 UTC

This package is auto-updated.

Last update: 2026-08-24 18:58:30 UTC


README

A Model Context Protocol server for Laravel. Expose your application to MCP clients — Claude Code, Claude Desktop, or anything else that speaks MCP — as a set of tools that respect your existing permission model.

Runs on PHP 7.3 / Laravel 8 and on PHP 8.x / Laravel 9–12 from the same source.

Why not an existing MCP SDK

logiscape/mcp-sdk-php and php-mcp/server both require PHP ≥ 8.1. MCP is JSON-RPC 2.0 over a byte stream, so the protocol layer here is a few hundred lines with no runtime dependency beyond illuminate/* and psr/log — which is cheaper than blocking on a platform upgrade.

Compatibility

The source deliberately avoids everything added after PHP 7.3, so one codebase serves every supported version: no arrow functions, typed properties, ??=, match, enums, constructor promotion, named arguments, or union types. Public interfaces declare no parameter or return types, so adding them later would be the breaking change — not the absence.

composer.json accepts illuminate/* ^8|^9|^10|^11|^12, and Composer picks the set your PHP version allows.

Install

composer require wizcoders/mcp
php artisan vendor:publish --tag=mcp-config

The provider is auto-discovered. Nothing is registered on the HTTP path, so a web request pays nothing for having this installed.

Configure

// config/mcp.php
'server_name' => 'my-app',
'authorizer'  => \App\Mcp\MyAuthorizer::class,
'tools'       => [
    \Wizcoders\Mcp\Tools\WhoAmITool::class,
    \App\Mcp\Tools\SalesSummaryTool::class,
],

mergeConfigFrom is a shallow merge, so tools replaces the package default rather than adding to it — re-list WhoAmITool unless you mean to drop it.

Packages can also contribute tools at runtime, without touching app config:

$this->app->make(\Wizcoders\Mcp\ToolRegistry::class)->register(new MyTool);

Authorization

Tools declare a permission string; an Authorizer decides what it means.

Authorizer Behaviour
NullAuthorizer (default) Denies everything gated — fails closed
GateAuthorizer Resolves through Laravel's Gate; works with policies and spatie/laravel-permission
your own Implement Wizcoders\Mcp\Contracts\Authorizer

The default denies rather than allows on purpose: an app that installs this and forgets to configure an authorizer gets a server exposing only ungated tools, not one that quietly hands an assistant the keys.

The check runs before a tool is listed, not just before it runs — a user without the permission never learns the tool exists, and tools/call returns the same "unknown tool" message for forbidden and nonexistent alike so the set cannot be enumerated by probing.

Bundled tools

Tool Purpose
whoami Acting user, app-supplied context, and which tools they can see
db_schema Table list, or one table's columns / indexes / foreign keys
app_packages Composer packages, flagging local path packages and their PSR-4 namespaces
app_routes Routes by URI, name or controller, with middleware

The last three are project-insight tools: they let an assistant learn your schema and layout before writing a query or hunting for a controller, instead of guessing column names. They return structure only — never row data.

db_schema works across three Laravel generations: native Schema::getTables() on Laravel 11+, doctrine/dbal on 8–10, and a name-only fallback otherwise. It also registers Doctrine type mappings for enum, set, year, bit and the spatial types, because DBAL throws Unknown database type enum requested rather than degrading — one ENUM column would otherwise make a whole table un-introspectable.

They are gated by mcp.introspection_permission, which defaults to null (ungated). That is reasonable for a local stdio server, where the client already had to be able to launch a PHP process and therefore already has database and filesystem access. Set a permission before serving over HTTP, where that assumption no longer holds — or drop the tools from mcp.tools.

Writing a tool

class SalesSummaryTool implements \Wizcoders\Mcp\Contracts\Tool
{
    public function name() { return 'sales_summary'; }

    public function description()
    {
        return 'Total sales for a date range, broken down by status. Use this for '
             . '"what did we sell last month" or any revenue figure over a period.';
    }

    public function inputSchema()
    {
        return [
            'type'       => 'object',
            'properties' => [
                'from' => ['type' => 'string', 'format' => 'date', 'description' => 'Inclusive start.'],
            ],
            'required'             => ['from'],
            'additionalProperties' => false,
        ];
    }

    public function permission() { return 'report_dailysales_view'; }

    public function handle(array $arguments) { /* ... */ }
}

Tools are resolved through the container, so constructor dependencies are injected.

Three things matter more than they look:

  • description() is the prompt. It decides whether the model reaches for the tool at all, and whether it reaches for the right one. Say when to call it, not only what it does.
  • Return aggregates, not rows. Whatever a tool returns is billed as input tokens on the model's next turn. A tool that returns six numbers beats one that returns the thousand invoices behind them.
  • Throw ToolException for anything the model can fix (unknown record, bad date range). Those come back as a tool result with isError: true, which the model reads and retries around. Anything else becomes a generic error and is logged server-side — stack traces and SQL must never reach the client.

For an empty argument list use new \stdClass(), not []: an empty PHP array encodes to [] and clients reject that as an invalid JSON Schema property bag.

Running

php artisan mcp:serve --user=1

It speaks newline-delimited JSON-RPC on stdin/stdout, so it is only useful by hand for a smoke test:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"1"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | php artisan mcp:serve --user=1

Nothing may write to stdout — that channel belongs to the protocol. The transport redirects PHP's own diagnostics to stderr for this reason; a stray dd() or echo in a tool will still corrupt the stream, and the client will report only "server disconnected".

Claude Code

claude mcp add my-app --scope project -- /usr/bin/php /path/to/artisan mcp:serve --user=1

--scope project writes .mcp.json at the repo root so the team shares it; --scope local (the default) keeps it to your machine. Verify with /mcp inside Claude Code.

Claude Desktop

{
  "mcpServers": {
    "my-app": {
      "command": "/usr/bin/php",
      "args": ["/path/to/artisan", "mcp:serve", "--user=1"]
    }
  }
}

Use an absolute path to the PHP binary in both cases — the client does not inherit your shell's PATH.

Architecture

Class Role
Protocol\JsonRpc JSON-RPC 2.0 envelopes
Server initialize, ping, tools/list, tools/call
ToolRegistry Holds tools, applies the authorizer
Transport\Transport Interface — moves messages
Transport\StdioTransport Newline-delimited JSON on stdin/stdout
Support\UserResolver Finds the acting user via the guard's provider

Server never touches a stream and UserResolver never names a user model, so adding a Streamable HTTP transport is one new class and no changes to any tool.

Not implemented

  • Streamable HTTP transport and OAuth. Required for remote or multi-user access, and required before the Claude API's MCP connector can reach this server at all — that connector takes a URL, so stdio is not an option there.
  • Resources and prompts. Only the tools capability is advertised.
  • Progress notifications and cancellation.

License

MIT — see LICENSE.