yoanbernabeu / webmcp-bundle
Expose Symfony routes as Web MCP tools via PHP attributes
Fund package maintenance!
yoanbernabeu
Installs: 3
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:symfony-bundle
pkg:composer/yoanbernabeu/webmcp-bundle
Requires
- php: >=8.2
- symfony/config: ^6.4|^7.0|^8.0
- symfony/dependency-injection: ^6.4|^7.0|^8.0
- symfony/http-kernel: ^6.4|^7.0|^8.0
- symfony/routing: ^6.4|^7.0|^8.0
- twig/twig: ^3.0|^4.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.0
- symfony/phpunit-bridge: ^6.4|^7.0|^8.0
README
Experimental — Web MCP is a draft specification currently being developed by the W3C Web Machine Learning Community Group. The protocol, the browser APIs, and this bundle's API may change at any time. See the Chrome for Developers blog post for more context.
A Symfony bundle that exposes your controller routes as Web MCP tools using simple PHP attributes. No JavaScript to write.
#[Route('/api/restaurants', name: 'api_restaurants', methods: ['GET'])] #[AsWebMcpTool( name: 'search_restaurants', description: 'Search restaurants by cuisine type', inputs: [ new WebMcpInput('cuisine', type: 'string', description: 'Cuisine type'), ] )] public function search(): JsonResponse { /* ... */ }
{{ webmcp_tools() }}
That's it. The browser AI can now call your API.
What is Web MCP?
Web MCP (Web Model Context Protocol) is an emerging standard, currently in draft at the W3C (Web Machine Learning Community Group), that provides a browser-level API allowing web pages to expose structured tools to AI.
It builds on top of the Model Context Protocol (MCP) created by Anthropic, but targets the browser: instead of server-side integrations, the page itself registers tools via navigator.modelContext.registerTool().
AI browser extensions (like Claude, ChatGPT, etc.) can then:
- Discover available tools, their names, descriptions, and input schemas
- Call them with structured parameters
- Read the JSON response and use it in the conversation
This turns any web page into an AI-capable endpoint. Instead of the user copy-pasting data between your app and an AI extension, the extension talks directly to your backend.
Example use case: A user browsing your app asks the Claude browser extension "What Italian restaurants are nearby?". The extension sees a search_restaurants tool is available on the page, calls it with {"cuisine": "italian"}, and returns the results in natural language.
You can test and inspect your Web MCP tools with the Model Context Tool Inspector Chrome extension.
Learn more:
Installation
composer require yoanbernabeu/webmcp-bundle php bin/console assets:install
The bundle registers itself automatically via Symfony Flex. The assets:install command copies the bundle's JavaScript runtime to public/bundles/webmcp/.
Usage
Annotate your controllers
Each controller method you want to expose as an MCP tool gets two attributes: #[Route] (with an explicit name:, required) and #[AsWebMcpTool].
Simple GET endpoint
use Symfony\Component\Routing\Attribute\Route; use YoanBernabeu\WebMcpBundle\Attribute\AsWebMcpTool; #[Route('/api/menu', name: 'api_menu', methods: ['GET'])] #[AsWebMcpTool( name: 'get_menu', description: 'Get the full restaurant menu', )] public function menu(): JsonResponse { return $this->json($this->menuRepository->findAll()); }
GET with query parameters
use YoanBernabeu\WebMcpBundle\Attribute\WebMcpInput; #[Route('/api/restaurants', name: 'api_restaurants', methods: ['GET'])] #[AsWebMcpTool( name: 'search_restaurants', description: 'Search restaurants, optionally filter by cuisine type', inputs: [ new WebMcpInput('cuisine', type: 'string', description: 'Cuisine type filter'), new WebMcpInput('limit', type: 'number', description: 'Max results'), ] )] public function search(Request $request): JsonResponse { $cuisine = $request->query->get('cuisine'); // ... }
Inputs without mapTo are sent as query string parameters for GET requests.
GET with path parameter
#[Route('/api/restaurants/{id}/menu', name: 'api_restaurant_menu', methods: ['GET'])] #[AsWebMcpTool( name: 'get_restaurant_menu', description: 'Get menu for a specific restaurant', inputs: [ new WebMcpInput('restaurant_id', type: 'number', description: 'Restaurant ID', required: true, mapTo: 'id'), ] )] public function restaurantMenu(int $id): JsonResponse { return $this->json($this->menuRepository->findByRestaurant($id)); }
mapTo: 'id' maps the input restaurant_id to the route parameter {id}.
POST with JSON body
#[Route('/api/orders', name: 'api_create_order', methods: ['POST'])] #[AsWebMcpTool( name: 'place_order', description: 'Place a new order', inputs: [ new WebMcpInput('restaurant_id', type: 'number', description: 'Restaurant ID', required: true), new WebMcpInput('items', type: 'array', description: 'List of item IDs', required: true), new WebMcpInput('notes', type: 'string', description: 'Special instructions'), ] )] public function placeOrder(Request $request): JsonResponse { $data = json_decode($request->getContent(), true); // ... }
For non-GET methods, inputs (except path params) are sent as a JSON body with Content-Type: application/json.
Render the JavaScript
Add the Twig function to your template, typically before </body>:
{# templates/base.html.twig #} <!DOCTYPE html> <html> <head>...</head> <body> {% block body %}{% endblock %} {{ webmcp_tools() }} </body> </html>
This outputs a <script> tag that registers all tools via navigator.modelContext. If the browser does not support Web MCP, the script does nothing.
Attribute reference
#[AsWebMcpTool]
Placed on a controller method. The method must also have a #[Route] with an explicit name:.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Unique tool name (duplicates throw at compile time) |
description |
string |
Yes | Tool description visible to the AI |
inputs |
WebMcpInput[] |
No | Input parameters |
WebMcpInput
Value object describing a tool input parameter.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
Yes | Parameter name | |
type |
string |
No | 'string' |
JSON Schema type (string, number, boolean, array) |
description |
string |
No | '' |
Parameter description visible to the AI |
required |
bool |
No | false |
Whether the parameter is required |
mapTo |
?string |
No | null |
Route parameter name if different from input name (mapTo: 'id' for {id}) |
Security
The bundle does not handle security. Your existing Symfony security applies as-is:
- Firewalls — the generated
fetch()calls carry the browser's session cookies #[IsGranted]— works normally, the AI will receive a 403 error- Voters — same, the controller is called through a standard HTTP request
Rules
- Each
#[Route]paired with#[AsWebMcpTool]must have an explicitname:(otherwise:LogicExceptionat compile time) - Tool names must be unique across the application (otherwise:
LogicExceptionindicating both locations) - Controllers should return JSON (the generated JS calls
response.json())
Requirements
- PHP >= 8.2
- Symfony 6.4 / 7.x / 8.x
- Twig 3.x / 4.x
- A browser supporting the Web MCP API (
navigator.modelContext)
Contributing
composer install # Full QA (PHPStan + CS-Fixer + PHPUnit) composer test:all # PHPUnit only composer test:fast # Fix code style composer test:cs-fix