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
- Prerequisites: Ensure the RPC plugin is installed and configured
- Add the plugin:
git submodule add [REPOSITORY_URL] plugins/MCPServer - Run database migrations: Execute the SQL files from
install/directory for your database type: install.mysql.sqlfor MySQLinstall.pgsql.sqlfor PostgreSQLinstall.mssql.sqlfor SQL Server- Register RPC methods as MCP tools: Each callable tool must exist in the
rpc_methodstable. The host project registers tools there (e.g. via its own migrations that insert or updaterpc_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:
- RPC Methods → MCP Tools: All RPC methods (marked with
@rpcannotation) are automatically exposed as MCP tools - Type Mapping: PHP types are converted to JSON Schema for MCP tool definitions
- Permission Checking: RPC method permissions (
@sectionannotations) are enforced - 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 typesSchemaBuilder- Builds complete JSON Schema for tool input parametersClassJsonSchemaGenerator- Generates JSON Schema from ValuesObject classes using JsonProperty annotationsParameterMetadataParser- Extracts parameter names and metadata from docblocksUnionTypeResolver- Resolves union types by selecting the first valid typeToolsCollector- Collects and filters RPC methods, converts them to MCP tool definitionsResponseFormatter- 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 intools/listandtools/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__getUserInfovsOTHER_SERVER__getUserInfo). - Store the prefix in uppercase (e.g.
FESTI_MCP) for consistency withtools/listoutput. - Set
mcp_tools_prefixto 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:
- int → integer
- float → number
- string → string
- bool → boolean
- array → object (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@sectionannotation)festi_section_actions- Maps plugin methods to sections and required permission masksfesti_sections_user_permission- User-specific permissions (overrides user type permissions)festi_sections_user_types_permission- User type/role-based permissions
How Permission Checking Works
- When an RPC method is called, the system looks up the method in
festi_section_actionsto find: - The associated section (
id_section) -
The required permission mask (2 = Read, 4 = Write, 6 = Execute)
-
The system then checks the user's permissions by querying:
festi_sections_user_permissionfor user-specific permissions (takes priority)-
festi_sections_user_types_permissionfor role-based permissions -
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:
-
Add the
@sectionannotation 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 } -
Create the section in
festi_sections(if it doesn't exist):INSERT INTO festi_sections (caption, ident, mask) VALUES ('User Management', 'user_management', '6'); -
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' ); -
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
PermissionsExceptionwhich 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 informationtools/list- Returns a list of all available tools (RPC methods)tools/call- Executes a tool by name with provided argumentsresources/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 viatools/listandtools/callmethods
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:
- Install the MCP server: Ensure you have your MCP server running locally or on a remote host.
- Set up your authentication token (see above)
- Configure Claude for Desktop: Open the configuration file for Claude and add your MCP server details:
For local server:code ~/Library/Application\ Support/Claude/claude_desktop_config.json{ "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
- QA: Describing RPC methods for LLMs — How to describe RPC methods and parameters so LLMs (and other MCP clients) can fill tool arguments correctly (typed params,
params_types,JsonProperty,rpc_methodsin DB). - MCP Claude Server Registration Checklist — Detailed checklist and reference for preparing and submitting your MCP server to the Claude Connectors Directory.
- Writing tools with required annotations — How to add
@mcpReadOnlyand@mcpDestructiveto your@rpcmethods so tools meet Claude catalog safety requirements. - Tool result size limit (25,000 tokens) — How to prevent oversized tool results: pagination, limit params, filtering, and server-side safeguard.