MCP Bundle

MCP Bundle

Version Downloads Stars

composer require ufo-tech/mcp-bundle
  • PHP 8.4+
  • Symfony 7.2+ · 8
  • Built on JsonRpcBundle 10.4.3+ · mcp/sdk 0.8
  • Protocol MCP · Streamable HTTP
  • Type Symfony Bundle
  • Category API · AI
  • License MIT

GitHub Packagist Changelog Known issues

An MCP server (Model Context Protocol) for JsonRpcBundle: the methods of your JSON-RPC API become tools that AI agents (Claude Code, Cursor, Gemini CLI, OpenAI) call over Streamable HTTP.

It is a thin adapter, like REST Bundle: business logic stays in the RPC services, and every tool call is a regular RPC request executed by JsonRpcBundle. Parameter conversion, DTOs, validation, caching, locking, events and the error format are the same as on /rpc.

From method to tool

Your code

use Ufo\JsonRpcBundle\ApiMethod\Interfaces\IRpcService;
use Ufo\McpBundle\Attribute\McpTool;
use Ufo\RpcObject\RPC;

#[RPC\Info('Catalog')]
#[RPC\APIGroups(['mcp'], withDefault: false)]
final class CatalogProcedure implements IRpcService
{
    /**
     * Search products by name.
     * @param string $query Part of the product name
     */
    #[McpTool(readOnlyHint: true)]
    public function search(string $query, int $limit = 10): array
    {
        // ...
    }
}

tools/list on /mcp (abridged)

{
    "name": "Catalog.search",
    "description": "Search products by name.",
    "inputSchema": {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "Part of the product name"},
            "limit": {"type": "integer", "default": 10}
        },
        "additionalProperties": false,
        "required": ["query"]
    },
    "annotations": {"readOnlyHint": true}
}

Nothing else to write:

  • POST /mcp is an MCP server with the Catalog.search tool, protected by the RPC token;
  • GET /mcp/manifest/claude.json returns a ready Claude Code connection config (likewise cursor, gemini, openai);
  • GET /mcp/server.json is the document for publishing in the MCP Registry.

Features

  • The API group decides what becomes a tool. Every method of a group is a tool named after the RPC method. Several endpoints (/mcp, /mcp/internal, …) side by side, each with its own methods (Configuration).
  • API versions. /mcp/v2 serves version 2 of the group.
  • The core does the work. A tool call goes through the JsonRpcBundle RpcExecutor (Tools).
  • Schemas and texts for agents. inputSchema / outputSchema from method metadata; titles and descriptions from the core translations by Accept-Language, including per group; #[McpTool] hints.
  • Both MCP protocol eras on one URL: session-based (sessions in the core cache pool) and stateless 2026-07-28 (HTTP and security).
  • The same security. Core RPC tokens (401/403), 404 for a disabled group or a missing version, CORS, DNS rebinding protection, request body limit.
  • Discovery. server.json for the MCP Registry and client manifests (server.json and manifests).
  • Extensibility. Availability check, access policy, custom client manifests (For developers).

Limitations

  • No OAuth: access is the RPC token in a header (HTTP and security).
  • Tools only: no MCP resources or prompts. The SDK declares the logging and completions capabilities by default, but the bundle does not use them.
  • No server-initiated messages (GET on the endpoint answers 405) and no asynchronous or webhook calls through MCP.

Version 1.0 (2026-10-19)

First release: the RPC methods of an API group as MCP tools over Streamable HTTP.

Key changes

  • An endpoint per route import: api_group is the core API group, /mcp/v2 is API version 2; route names contain the group (mcp_ufo_mcp_endpoint, …).
  • Tools: every method of the group; titles and descriptions from the core translations (including translations/rpc/<group>.<locale>.yaml) by Accept-Language; self-contained inputSchema / outputSchema; #[McpTool] hints.
  • tools/call through the core RpcExecutor: structuredContent: {"result": …} plus the same JSON as text; errors are the core RPC error with isError: true; rpc.post_response after the response is sent, once per call (also in a batch).
  • HTTP and security: the session-based protocol (sessions in the core cache pool) and stateless 2026-07-28 on one URL; core tokens (401/403); 404 for a disabled group or a missing version; CORS, DNS rebinding protection, max_body_bytes.
  • Discovery: server.json for the MCP Registry, manifests for Claude Code, Cursor, Gemini CLI, OpenAI.

What to keep in mind

Only applies to applications that used the bundle from dev-main before 1.0.0:

  • The session_directory option (bundle-wide and per group) was removed, sessions live in the core cache pool; remove the key, otherwise "Unrecognized option". groups may now be empty.
  • Route names contain the group (<group>_ufo_mcp_endpoint); a manual name_prefix in YAML imports is no longer needed.
  • Titles and descriptions come only from the core translations: ToolTextResolverInterface and Ufo\McpBundle\I18n\* were removed.
  • Classes moved from Ufo\McpBundle\Services\* to Server\*, Server\Transport\*, Tool\*; the group, availability and token checks moved from McpController to EventListener\McpRequestListener.

Environment changes

  • PHP >= 8.4, Symfony ^7.2 | ^8.0 (through JsonRpcBundle).
  • ufo-tech/json-rpc-bundle ^10.4.3, mcp/sdk ^0.8.1, symfony/psr-http-message-bridge ^7.2 | ^8.0, symfony/yaml ^7.2 | ^8.0, nyholm/psr7 ^1.8.

Getting started

  1. Install the bundle and add the MCP routes.
  2. Add RPC methods to the endpoint's group and check tools/list.
  3. Configure the group's server and, if needed, CORS and hosts.
  4. Connect a client using a manifest.