yoanbernabeu/webmcp-bundle
Symfony bundle that turns your app into a WebMCP/Model Context Protocol server, letting AI agents discover and call your tools, resources, and prompts over HTTP. Register capabilities with PHP attributes and integrate cleanly with Symfony services, routing, and security.
Framework reality check first: this is a Symfony bundle, so a Laravel app cannot composer require it and get anything working. You're evaluating it as a reference implementation of an idea — attribute-declared HTTP endpoints projected into a Web MCP tool manifest that the browser exposes to an on-page AI agent via navigator.modelContext. The port cost to Laravel is small (one service provider, one attribute, one route scanner, one Blade/JSON endpoint), but it is not zero.
Day-one steps in a Symfony host, and the equivalent you'd build in Laravel:
composer require yoanbernabeu/webmcp-bundle and register it in config/bundles.php (Flex does this automatically; check the README for the exact bundle class name). Laravel analogue: install a local package, register a WebMcpServiceProvider in bootstrap/providers.php.navigator.modelContext (the exact accessor varies by browser build — Chrome's experimental implementation is the one to test against).Attribute-per-action, DTO-in. The bundle infers the tool's input schema from the route signature and request binding. That only works reliably if your action takes a typed object rather than Request $request. Symfony: a form-backed DTO or #[MapRequestPayload]. Laravel: a FormRequest-style DTO plus explicit schema, because Laravel has no equivalent inference — you'd hand-write the JSON Schema in the attribute and assert it matches the validator in a test.
Split safe and unsafe tools at the routing layer. Agents call tools speculatively. Keep GET tools (search, lookup, summarize) in one group annotated freely, and POST/DELETE tools behind an explicit confirmation flag in the attribute plus server-side authorization that ignores the tool description entirely. A pattern that survives contact with production:
#[McpTool(name: 'orders.find', description: 'Find orders by customer email', scopes: ['orders:read'])]
public function find(OrderSearchQuery $query): OrderList { ... }
The scopes aren't a security boundary — they're a hint for the agent. Your voter/policy is the boundary.
Keep the per-page tool set small. Agents degrade noticeably past ~10–15 tools, and every tool's description burns context. Expose page-scoped tool sets (product page → product tools only), not a global registry. If the bundle has a "group" or "context" concept, use it; if not, gate registration by route name or a request attribute so the injected manifest differs per page.
Thin actions, thick services. Since the controller is now both an HTTP endpoint and an agent-facing contract, keep the action to validation + delegation. The service underneath is what your feature tests should target directly — you don't need to simulate an agent to test business logic, only to test the manifest.
Laravel integration sketch. If you port it:
// app/WebMcp/McpTool.php — attribute
#[Attribute(Attribute::TARGET_METHOD)]
final class McpTool {
public function __construct(
public string $name,
public string $description,
public array $schema = [],
public bool $confirm = false,
) {}
}
// Route registration
Route::get('/products/search', [ProductController::class, 'search'])
->middleware(['web', 'webmcp']); // middleware collects attribute into a request-scoped registry
Then a view()->share('mcpTools', ...) or a GET /_webmcp/manifest endpoint emitting navigator.modelContext.provideContext({ tools }) from an inline script. Feature-detect on the client:
if (navigator.modelContext) {
navigator.modelContext.provideContext({ tools: @json($mcpTools) });
}
Cache the manifest, not the page. If you full-page cache, a user-scoped tool list will leak across users. Either exclude authenticated pages from the cache or emit the manifest from an unauthenticated-aware endpoint keyed by role.
composer.json, read the CHANGELOG between bumps, and expect attribute signatures to move. Do not make it a hard dependency of anything you can't fork in an afternoon.orders.delete with id 42" is a legitimate test case. Enforce authorization server-side, log every tool invocation with the acting user, and rate-limit per session.bin/console cache:clear) — attribute scanning is cached and a stale scan is the #1 cause of "my tool doesn't appear." Then inspect the emitted manifest payload directly in the network tab before blaming the browser. On the Laravel side the equivalent trap is php artisan route:cache freezing an attribute scan.if (navigator.modelContext) and no-op otherwise.debug flag that disables all write tools in non-prod). Schema normalization is usually a swappable service; that's where you'd add support for enums, money objects, or date formats your DTOs use.How can I help you explore Laravel packages today?