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

v0.1.0 2026-02-26 22:03 UTC

This package is auto-updated.

Last update: 2026-02-26 22:09:08 UTC


README

CI Static Analysis PHP 8.2+ Symfony 6.4+ License: MIT

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 explicit name: (otherwise: LogicException at compile time)
  • Tool names must be unique across the application (otherwise: LogicException indicating 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

License

MIT