MCPServer Plugin

Model Context Protocol (MCP)

The Model Context Protocol (MCP) is an open standard that enables seamless integration between LLM applications and external data sources and tools. This PHP implementation allows you to build MCP servers that can expose:

  • Resources: Context and data for AI models to use
  • Prompts: Templated messages and workflows for users
  • Tools: Functions for AI models to execute

Installation

  1. Prerequisites: Ensure the RPC plugin is installed and configured
  2. Add the plugin:
    git submodule add [REPOSITORY_URL] plugins/MCPServer
  3. Run database migrations: Execute the SQL files from install/ directory for your database type:
  4. install.mysql.sql for MySQL
  5. install.pgsql.sql for PostgreSQL
  6. install.mssql.sql for SQL Server
  7. Register RPC methods as MCP tools: Each callable tool must exist in the rpc_methods table. The host project registers tools there (e.g. via its own migrations that insert or update rpc_methods).

Dependencies

This plugin depends on the RPC plugin and leverages its type handling utilities to maintain consistency and avoid code duplication. The TypeMapper class delegates basic type operations to RPC's ParameterProcessor, while adding MCP-specific functionality for JSON Schema generation.

Architecture

The MCPServer plugin acts as a bridge between the MCP protocol and your existing RPC methods:

  1. RPC Methods → MCP Tools: All RPC methods (marked with @rpc annotation) are automatically exposed as MCP tools
  2. Type Mapping: PHP types are converted to JSON Schema for MCP tool definitions
  3. Permission Checking: RPC method permissions (@section annotations) are enforced
  4. Parameter Conversion: Named parameters from MCP are converted to positional parameters for RPC calls

Domain Classes

The plugin is organized into several domain classes:

  • TypeMapper - Maps PHP types to JSON Schema types, handles nullable and union types
  • SchemaBuilder - Builds complete JSON Schema for tool input parameters
  • ClassJsonSchemaGenerator - Generates JSON Schema from ValuesObject classes using JsonProperty annotations
  • ParameterMetadataParser - Extracts parameter names and metadata from docblocks
  • UnionTypeResolver - Resolves union types by selecting the first valid type
  • ToolsCollector - Collects and filters RPC methods, converts them to MCP tool definitions
  • ResponseFormatter - Formats MCP responses (success and error)
  • ProtocolResponseFactory - Creates MCP protocol-compliant responses

RPC Method Caching

RPC methods are cached after the first call to getTools() to improve performance. The cache is stored in $_rpcMethods and persists for the lifetime of the plugin instance.

Configuration

The plugin uses the following settings (configured via festi_settings table):

  • mcp_protocol_version - MCP protocol version (default: 2025-06-18)
  • mcp_server_name - Server name identifier (default: festi-mcp)
  • mcp_server_version - Server version (default: 1.0.0)
  • mcp_tools_prefix - Prefix for all tool names in tools/list and tools/call (default: FESTI_MCP)

You can update these settings in the database.

Tool name prefix

When mcp_tools_prefix is set, tool names are built as PREFIX + separator + RPC method name. The separator is __ (double underscore). Example: with prefix FESTI_MCP, the RPC method onJsonExportToGoogleCalendar is exposed as tool FESTI_MCP__onJsonExportToGoogleCalendar.

Restriction: The full tool name (prefix + separator + method) must match the MCP pattern ^[a-zA-Z0-9_-]{1,64}+$. Only letters, digits, underscores, and hyphens are allowed. Do not use colons (::), spaces, dots, or other symbols in the prefix—they will break tool listing and calls.

Recommendations:

  • If different MCP servers expose tools with the same name, the client cannot tell them apart and may call the wrong server (unpredictable behavior). Use a distinct prefix per server so tool names are unique (e.g. FESTI_MCP__getUserInfo vs OTHER_SERVER__getUserInfo).
  • Store the prefix in uppercase (e.g. FESTI_MCP) for consistency with tools/list output.
  • Set mcp_tools_prefix to empty only when you have a single MCP server or when this server is the only one exposing those tool names; otherwise use a prefix to avoid clashes.

Creating MCP Tools from RPC Methods

Any RPC method in your application automatically becomes an MCP tool. Simply mark your methods with the @rpc annotation:

class MyPlugin extends DisplayPlugin
{
    /**
     * @rpc getUserInfo
     * @param int $idUser
     * @param bool $includeDetails
     * @return array
     * @section user_management
     */
    public function getUserInfo(int $idUser, bool $includeDetails = false): array
    {
        // Your implementation
        return [
            'id' => $idUser,
            'name' => 'John Doe',
            // ...
        ];
    }
}

After syncing RPC methods, this will be available as an MCP tool named getUserInfo with: - Name: getUserInfo - Description: From the method's docblock or RPC method description - Input Schema: Automatically generated from parameter types - Permissions: Enforced via @section annotation

Helping agents use tools: The agent only sees each tool's name, description, and inputSchema from tools/list. Put usage guidance in the tool's description in rpc_methods. For search-style tools, describe the query format (e.g. space- or comma-separated terms), optional filter keys, and that the response has items and total. For update/create-style tools, document each parameter (e.g. what requestIDs means, which keys data can have, required vs optional fields) and what the tool returns. See Tool usage guidance for agents for examples of both patterns.

Safety annotations (Claude catalog): For your server to be listed in the Claude MCP Directory, every tool must declare @mcpReadOnly (read-only) or @mcpDestructive (modifies data or has side effects). These are the only two supported safety annotations; in code they are McpAnnotationResolver::ANNOTATION_MCP_READ_ONLY and McpAnnotationResolver::ANNOTATION_MCP_DESTRUCTIVE. See Writing tools with required annotations.

How to annotate your RPC methods

Every @rpc method that is exposed as an MCP tool must have exactly one safety annotation in its docblock so the tool gets the correct readOnlyHint / destructiveHint in tools/list:

  • @mcpReadOnly – use for tools that only read data (no creates, updates, deletes, or side effects like emails/webhooks).
  • @mcpDestructive – use for tools that create, update, or delete data, write files, or perform any side effect.

Add one of these to each @rpc method; the full guide with examples and when to use which is in Writing MCP tools with required annotations.

Tool Naming Requirements

Tool names (RPC method names) must match the pattern: ^[a-zA-Z0-9_-]{1,64}+$ - Only alphanumeric characters, underscores, and hyphens are allowed - Maximum length: 64 characters - Tool names must be unique across all plugins - When mcp_tools_prefix is set, the exposed name is PREFIX__methodName (see Tool name prefix)

Parameter Types

The plugin automatically converts PHP types to JSON Schema: - intinteger - floatnumber - stringstring - boolboolean - arrayobject (associative arrays) or array with items schema - Typed arrays of primitives (int[], string[], float[], bool[]) → array with items schema (e.g. items: { type: "integer" }) - Custom classes → object with properties schema (using JsonProperty annotations) - Union types (e.g., int|string) → oneOf schema - Nullable types (e.g., ?string, string|null) → schema with null in type array - mixed → schema accepting all types (string, number, boolean, object, array, null)

Advanced Type Handling

Custom Classes (ValuesObject) Custom classes that extend ValuesObject are automatically converted to JSON Schema by inspecting JsonProperty annotations:

use core\json\JsonProperty;
use ValuesObject;

class UserRequest extends ValuesObject
{
    #[JsonProperty("userId")]
    private int $idUser;

    #[JsonProperty("includeDetails")]
    private bool $includeDetails = false;

    // Getters and setters...
}

The schema generator will create a JSON Schema with properties matching the JsonProperty annotations.

Arrays of primitives (int[], string[], float[], bool[])

Typed arrays of primitives produce type: "array" with an items schema for the element type: int[]items: { type: "integer" }, string[]items: { type: "string" }, float[]items: { type: "number" }, bool[]items: { type: "boolean" }. So the LLM sees that the parameter is a list of integers, strings, etc. PHP reflection only gives array for method parameters, so to get this schema you need to set params_types in the database (e.g. in a migration) to int[], string[], etc. for the corresponding parameter.

Arrays of objects (ClassName[])

When a parameter type is a typed array (e.g. CommitValuesObject[]), the MCP tool schema gets type: "array" with an items schema generated from that class. Use a ValuesObject (or DTO) with JsonProperty on the fields you accept; only those properties appear in the schema and are used for deserialization. Set params_types in the database (e.g. via a migration) to the full class name with [], e.g. plugin\Commits\domain\model\CommitValuesObject[], because reflection only sees array and sync would otherwise not expose the item shape. See QA: Describing RPC methods for LLMs.

Union Types Union types like int|string or bool|null are resolved by selecting the first non-null, non-mixed type. The resulting schema uses oneOf to represent the union.

Nullable Types Nullable types are handled by: - Stripping the ? prefix or |null suffix - Adding null to the type array in the schema - Example: ?string becomes ["string", "null"]

Parameter Name Extraction Parameter names are extracted from @param annotations in docblocks. If not found, default names like param1, param2 are used.

For guidance on describing RPC methods so LLMs fill params correctly (typed params, params_types, JsonProperty, etc.), see QA: Describing RPC methods for LLMs.

Permissions

RPC methods with @section annotations will have their permissions checked before execution. For detailed information about the permissions system, see the Permissions documentation.

The permission system uses the following database tables:

  • festi_sections - Defines permission sections (identified by the @section annotation)
  • festi_section_actions - Maps plugin methods to sections and required permission masks
  • festi_sections_user_permission - User-specific permissions (overrides user type permissions)
  • festi_sections_user_types_permission - User type/role-based permissions

How Permission Checking Works

  1. When an RPC method is called, the system looks up the method in festi_section_actions to find:
  2. The associated section (id_section)
  3. The required permission mask (2 = Read, 4 = Write, 6 = Execute)

  4. The system then checks the user's permissions by querying:

  5. festi_sections_user_permission for user-specific permissions (takes priority)
  6. festi_sections_user_types_permission for role-based permissions

  7. The user's granted mask must be greater than or equal to the required mask for the action.

Setting Up Permissions

To protect an RPC method, you need to:

  1. Add the @section annotation to your RPC method:

    /**
     * @rpc deleteUser
     * @param int $idUser
     * @section user_management  // References a section in festi_sections
     * @return bool
     */
    public function deleteUser(int $idUser): bool
    {
        // Implementation
    }

  2. Create the section in festi_sections (if it doesn't exist):

    INSERT INTO festi_sections (caption, ident, mask)
    VALUES ('User Management', 'user_management', '6');

  3. Register the action in festi_section_actions:

    INSERT INTO festi_section_actions (id_section, plugin, method, mask, comment)
    VALUES (
        (SELECT id FROM festi_sections WHERE ident = 'user_management'),
        'MyPlugin',
        'deleteUser',
        '6',  -- Execute permission required
        'Delete user action'
    );

  4. Assign permissions to users or user types:

    -- For a specific user type
    INSERT INTO festi_sections_user_types_permission (id_section, id_user_type, value)
    VALUES (
        (SELECT id FROM festi_sections WHERE ident = 'user_management'),
        1,  -- Admin user type
        6   -- Execute permission
    );
    
    -- For a specific user (overrides user type permissions)
    INSERT INTO festi_sections_user_permission (id_section, id_user, value)
    VALUES (
        (SELECT id FROM festi_sections WHERE ident = 'user_management'),
        123,  -- User ID
        6     -- Execute permission
    );

If a user attempts to call a tool without sufficient permissions, they will receive a PermissionsException error.

Response Object Handling

RPC methods that accept a Response object as the first parameter (common in display methods) are automatically handled:

/**
 * @rpc getProjects
 * @param Response $response
 * @return bool
 */
public function onDisplayList(Response &$response): bool
{
    $store = $this->createStoreInstance("projects");
    $store->onRequest($response);
    return true;
}

When such methods are called via MCP: 1. A Response object is automatically created and passed as the first parameter 2. The method's return value is ignored 3. The Response object's contents are extracted and returned as the tool result 4. This allows MCP tools to work seamlessly with existing display methods

Error Handling

The plugin includes comprehensive error handling:

  • Tool Not Found: Returns error if the requested tool name doesn't exist
  • Method Not Found: Returns error if the RPC method cannot be found
  • Permission Denied: Throws PermissionsException which is caught and logged
  • Internal Errors: All exceptions are caught, logged to IRpc::LOG_ERRORS, and a user-friendly error message is returned

Errors are logged with context including the tool name and exception details for debugging.

MCP Protocol Methods

The plugin implements the following MCP protocol methods:

Core Methods

  • initialize - Initializes the MCP connection, returns server capabilities and information
  • tools/list - Returns a list of all available tools (RPC methods)
  • tools/call - Executes a tool by name with provided arguments
  • resources/list - Returns available resources (currently returns a "tools" resource)
  • prompts/list - Returns available prompts (currently empty, infrastructure ready for future use)

Notifications

  • notifications/initialized - Called after initialization is complete (currently no-op)
  • notifications/cancelled - Called when a request is cancelled (currently no-op)

All methods follow the JSON-RPC 2.0 specification and are automatically registered when you sync RPC methods.

Resources

The plugin currently exposes one resource:

  • tools - Provides access to callable tools via tools/list and tools/call methods

Resources are defined in the getResources() method and can be extended to expose additional data sources for AI models.

Prompts

The prompts feature is currently implemented but returns an empty list. The infrastructure is in place to add templated messages and workflows. To add prompts, modify the getPrompts() method in MCPServerPlugin.php.

MCP HTTP Bridge

The mcp-http-bridge.js is a Node.js bridge that converts between stdio (used by Claude Desktop) and HTTP (used by your PHP MCP server). It acts as a transparent proxy, forwarding all MCP JSON-RPC messages between Claude Desktop and your HTTP-based MCP server.

Command Line Options

The bridge supports the following command line arguments (order-independent):

  • --protocol <http|https> - Protocol to use (default: http)
  • --host <hostname> - Server hostname (default: localhost)
  • --port <port> - Server port (default: 8080)
  • --path <path> - Server path/endpoint (default: /)
  • --token <token> - Authentication token (optional)

Arguments follow the logical URL structure: protocol://host:port/path + authentication.

Authentication Token Setup

The MCP bridge supports per-user authentication tokens. Each user needs their own token to identify themselves when using tools.

Setting Up Your Token

Using Command Line (Recommended for individual users)

For local server:

node mcp-http-bridge.js --token your-personal-token

For remote server:

node mcp-http-bridge.js --protocol http --host remote-server.com --port 8080 --token your-personal-token

For HTTPS server:

node mcp-http-bridge.js --protocol https --host remote-server.com --port 443 --token your-personal-token

For server with custom path:

node mcp-http-bridge.js --protocol http --host remote-server.com --port 8080 --path /rpc/v1/ --token your-personal-token

Testing with Claude for Desktop

To test your MCP server with Claude for Desktop, follow these steps:

  1. Install the MCP server: Ensure you have your MCP server running locally or on a remote host.
  2. Set up your authentication token (see above)
  3. Configure Claude for Desktop: Open the configuration file for Claude and add your MCP server details:
    code ~/Library/Application\ Support/Claude/claude_desktop_config.json
    For local server:
    {
      "mcpServers": {
        "festi-mcp": {
            "command": "node",
            "args": [
                "[PROJECT_PATH]/plugins/MCPServer/mcp-http-bridge.js",
                "--token",
                "[YOUR-PERSONAL-TOKEN]"
            ]
        }
      }
    }

For remote server with token:

{
  "mcpServers": {
    "festi-mcp": {
        "command": "node",
        "args": [
            "[PROJECT_PATH]/plugins/MCPServer/mcp-http-bridge.js",
            "--protocol",
            "http",
            "--host",
            "[HOST]",
            "--port",
            "[PORT]",
            "--token",
            "[YOUR-PERSONAL-TOKEN]"
        ]
    }
  }
}

For HTTPS server:

{
  "mcpServers": {
    "festi-mcp": {
        "command": "node",
        "args": [
            "[PROJECT_PATH]/plugins/MCPServer/mcp-http-bridge.js",
            "--protocol",
            "https",
            "--host",
            "[HOST]",
            "--port",
            "[PORT]",
            "--token",
            "[YOUR-PERSONAL-TOKEN]"
        ]
    }
  }
}

For server with custom path:

{
  "mcpServers": {
    "festi-mcp": {
        "command": "node",
        "args": [
            "[PROJECT_PATH]/plugins/MCPServer/mcp-http-bridge.js",
            "--protocol",
            "http",
            "--host",
            "[HOST]",
            "--port",
            "[PORT]",
            "--path",
            "/rpc/v1/",
            "--token",
            "[YOUR-PERSONAL-TOKEN]"
        ]
    }
  }
}

When installed via Claude's MCP catalog, it will ask the user to provide a token:

{
    "mcpServers": {
        "festi-mcp": {
            "command": "node",
            "args": [
                "[PROJECT_PATH]/plugins/MCPServer/mcp-http-bridge.js",
                "--host",
                "[HOST]",
                "--port",
                "[PORT]",
                "--protocol",
                "https",
                "--token",
                "${FESTI_MCP_TOKEN}"
            ]
        }
    }
}

Logs:

tail -n 20 -F ~/Library/Logs/Claude/mcp-server-festi-mcp.log

Testing

Manual Testing

Test the bridge directly:

node mcp-http-bridge.js

Test with custom options:

node mcp-http-bridge.js --protocol http --host localhost --port 8080 --token test-token

Test with custom path:

node mcp-http-bridge.js --protocol http --host localhost --port 8080 --path /rpc/v1/ --token test-token

http://localhost:8080/?rawRequest={%22method%22:%22initialize%22,%22params%22:{%22protocolVersion%22:%222024-11-05%22,%22capabilities%22:{},%22clientInfo%22:{%22name%22:%22claude-ai%22,%22version%22:%220.1.0%22}},%22jsonrpc%22:%222.0%22,%22id%22:0}

http://localhost:8080/?rawRequest=[{%22method%22:%22notifications/initialized%22,%22jsonrpc%22:%222.0%22}]
2025-05-31T23:30:43.570Z [info] [festi-mcp] Message from client: {"method":"notifications/initialized","jsonrpc":"2.0"}
2025-05-31T23:30:43.571Z [info] [festi-mcp] Message from client: {"method":"tools/list","params":{},"jsonrpc":"2.0","id":1}
2025-05-31T23:30:43.571Z [info] [festi-mcp] Message from client: {"method":"tools/list","params":{},"jsonrpc":"2.0","id":2}
2025-05-31T23:30:43.571Z [info] [festi-mcp] Message from client: {"method":"resources/list","params":{},"jsonrpc":"2.0","id":3}

{"method":"prompts/list","params":{},"jsonrpc":"2.0","id":4}

{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":4,"reason":"Error: MCP error -32001: Request timed out"}}

Further documentation