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

Symfony Mcp Server Laravel Package

klapaudius/symfony-mcp-server

Symfony MCP Server lets you expose your Symfony app's tools, resources, and prompts to AI clients via the Model Context Protocol. Register capabilities with attributes or config, add MCP endpoints, and connect Claude and other MCP-compatible assistants to your PHP backend.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, so a Laravel developer's first job is deciding where the boundary sits: you can either run it as a standalone MCP server process alongside your Laravel app, or bridge it into Laravel via a service provider. Both are viable; the bridge is more work but keeps one codebase.

Minimal path:

  1. composer require klapaudius/symfony-mcp-server — pin the version (^1.9) once you've confirmed the schema changes work with your client.
  2. Read src/Attribute/ first. Tool definitions are attribute-driven; that directory tells you the supported shape of a tool class faster than the README does.
  3. Read the transport layer next (src/Transport/ or equivalent) to see whether stdio, HTTP, or SSE is the default and what the server bootstrap looks like.
  4. Copy the package's example tool, get it responding over stdio to a local MCP client (Claude Desktop, MCP Inspector), then wire it into Laravel.

For the Laravel bridge, the shape is: a McpServiceProvider that registers the server instance, a registry/collection that collects your tool classes, and an entry point — an artisan command for stdio (php artisan mcp:serve) or a route for HTTP. If you use the HTTP transport, mount it behind your existing auth middleware rather than exposing it unauthenticated.

First real use case: wrap two or three read-only internal APIs (a search endpoint, a "look up order by ID" endpoint) as tools. Resist write operations until you've seen how the client renders errors and partial results.

Implementation Patterns

Attribute-driven tool classes. Define one class per tool, keep the handler thin, and delegate to existing Laravel services/actions. The tool class is a transport adapter, not a place for business logic — you want the same code callable from a controller and from an agent.

Schema-first inputs. Derive the JSON Schema from a typed DTO or from explicit schema definitions rather than hand-writing arrays in the attribute. This matters more in 1.9.0 than before: with oneOf/anyOf/allOf now supported, the schema and the PHP type can drift apart quickly if they're maintained separately.

Composition for polymorphic tools. The headline feature of 1.9.0 is genuinely useful for tools that accept several shapes of input — e.g. a create_ticket tool accepting either a bug payload or a feature_request payload, or a query tool accepting one of several filter grammars. Pattern:

  • Model each variant as its own class.
  • Use oneOf when exactly one variant must match, with a discriminator-style required field (e.g. "type": "bug") so the model reliably picks a branch.
  • Use anyOf when several independently-valid optional shapes may apply.
  • Use allOf to compose shared base fields (pagination, ids, tenant) with a variant-specific schema — this is the cleanest way to avoid repeating envelope fields across tools.

Registry and tagging. In Symfony this is service tags plus autoconfiguration. In Laravel you replicate it: collect tool classes in a config/mcp.php array or via a #[AsMcpTool]-style marker scanned at boot, then resolve them from the container lazily. Lazy resolution matters — instantiating every tool on every request is wasted work when the client only calls one.

Transport per environment. stdio for local desktop clients, HTTP/SSE for anything shared. Keep the handler identical across both; only the bootstrap differs. This is where a thin Laravel command wrapping the server pays off.

Testing. Don't test through the transport. Instantiate the tool class, call the handler with a decoded argument array, assert on the structured result. Add one integration test per transport that does a real handshake, and keep those few.

Gotchas and Tips

Laravel will not autoconfigure Symfony tags. The single biggest source of "no tools registered" confusion. If tools are discovered via container tags, your Laravel provider must explicitly tag them or the registry will be empty with no error.

Schema composition is declared, not necessarily enforced. Supporting oneOf/anyOf/allOf in the emitted schema does not guarantee the server validates incoming arguments against it. Assume the model can still send a payload that violates the composition, and validate server-side with Laravel's validator or a typed DTO hydration step. Treat the schema as a hint to the model, and your validator as the contract.

Loose oneOf branches confuse models. If two variants differ only by optional fields, the model will pick arbitrarily and often wrongly. Make branches structurally distinct and require the discriminator. Prefer oneOf with a required tag over anyOf whenever intent is mutually exclusive.

allOf + required interactions. A field required in the base schema is required in every composed variant. If that's not what you want, anyOf or oneOf is the right tool — this is easy to get wrong and produces schemas that are technically valid but semantically over-constrained.

Draft/version drift with clients. Some MCP clients and LLM tool-callers only partially implement composition keywords. Test the emitted schema against your actual client before shipping. If a client chokes, a flattened schema with a single type enum often works better than composition — a documented, deliberate downgrade.

Upgrade check for 1.9.0. Since this adds capability rather than changing it, existing flat schemas should be unaffected. Still, diff the generated schema output for one of your existing tools before and after the bump; schema-shape regressions are silent and only show up as the model mis-calling tools.

Error surface. Return structured error content the model can read (validation failures with which field failed) rather than throwing exceptions that collapse into a generic transport error. The model can self-correct from a good validation message and cannot from a stack trace.

Extension points worth knowing. The tool registry and the schema serializer are the two seams you'll actually want to override — the registry to add discovery from Laravel-native sources, the serializer if you need to post-process schemas (e.g. flatten composition for a specific client). Keep those overrides in your own namespace, not by forking.

Security. Every tool is an unauthenticated capability by default from the agent's perspective. Scope tools per-token/tenant, and never expose a tool that trusts an ID passed in the arguments without an ownership check against the authenticated context.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core