The PHP MCP Server provides a server-side implementation of the Model Context Protocol (MCP) for PHP applications. It allows you to expose parts of your application as standardized MCP Tools, Resources, and Prompts that AI assistants like Claude or Cursor IDE can interact with using the MCP standard.
composer require php-mcp/server
Create src/MyMcpElements.php
:
<?php
namespace App;
use PhpMcp\Server\Attributes\McpTool;
class MyMcpElements
{
/**
* Adds two numbers together.
* @param int $a The first number.
* @param int $b The second number.
* @return int The sum.
*/
#[McpTool(name: 'simple_adder')]
public function add(int $a, int $b): int
{
fwrite(STDERR, "Executing simple_adder with a=$a, b=$b\n");
return $a + $b;
}
}
Create mcp-server.php
:
#!/usr/bin/env php
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\StdioServerTransport;
try {
// 1. Build the Server configuration
$server = Server::make()
->withServerInfo('My Discovery Server', '1.0.2')
->build();
// 2. Explicitly run discovery
$server->discover(
basePath: __DIR__,
scanDirs: ['src'],
);
// 3. Create the Stdio Transport
$transport = new StdioServerTransport();
// 4. Start Listening (BLOCKING call)
$server->listen($transport);
exit(0);
} catch (\Throwable $e) {
fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n" . $e . "\n");
exit(1);
}
Instruct your MCP client (e.g., Cursor, Claude Desktop) to use the stdio
transport:
// Example: .cursor/mcp.json
{
"mcpServers": {
"my-php-stdio": {
"command": "php",
"args": ["/full/path/to/your/project/mcp-server.php"]
}
}
}
You can define MCP elements using either attributes or manual registration.
Decorate public, non-static methods or invokable classes with #[Mcp*]
attributes:
use PhpMcp\Server\Attributes\McpTool;
use PhpMcp\Server\Attributes\McpResource;
use PhpMcp\Server\Attributes\McpResourceTemplate;
use PhpMcp\Server\Attributes\McpPrompt;
// Tool example
#[McpTool(name: 'add_numbers')]
public function addNumbers(int $a, int $b): int
{
return $a + $b;
}
// Resource example
#[McpResource(uri: 'status://system/load', mimeType: 'text/plain')]
public function getSystemLoad(): string
{
return file_get_contents('/proc/loadavg');
}
// Resource template example
#[McpResourceTemplate(uriTemplate: 'user://{userId}/profile', mimeType: 'application/json')]
public function getUserProfile(string $userId): array
{
return ['id' => $userId, 'name' => 'User ' . $userId];
}
// Prompt example
#[McpPrompt(name: 'summarize')]
public function generateSummaryPrompt(string $textToSummarize): array
{
return [
['role' => 'user', 'content' => "Summarize the following text:\n\n{$textToSummarize}"],
];
}
Use withTool
, withResource
, withResourceTemplate
, withPrompt
on the ServerBuilder
:
use App\Handlers\MyToolHandler;
use App\Handlers\MyResourceHandler;
$server = Server::make()
->withServerInfo('My Manual Server', '1.0.0')
->withTool(
[MyToolHandler::class, 'processData'], // Handler: [class, method]
'data_processor' // MCP Name (Optional)
)
->withResource(
MyResourceHandler::class, // Handler: Invokable class
'config://app/name' // URI (Required)
)
->build();
Configure the server using the builder pattern:
$server = Server::make()
// Required server identity
->withServerInfo('My MCP Server', '1.0.0')
// Optional dependencies
->withLogger($psr3Logger)
->withCache($psr16Cache, $ttl = 3600)
->withContainer($psr11Container)
->withLoop($reactPhpLoop)
// Optional settings
->withCapabilities(Capabilities::forServer(...))
->withPaginationLimit(50)
// Manual element registration
->withTool(...)
->withResource(...)
// Build the server instance
->build();
Ideal for servers launched directly by an MCP client (like Cursor):
use PhpMcp\Server\Transports\StdioServerTransport;
// ... build $server ...
$transport = new StdioServerTransport();
$server->listen($transport); // Blocks until the transport is closed
Warning: When using Stdio transport, avoid writing to STDOUT with
echo
,fwrite(STDERR, "Debug message\n");
For web-based server implementation:
use PhpMcp\Server\Transports\HttpServerTransport;
// ... build $server ...
$transport = new HttpServerTransport(
host: '127.0.0.1', // Listen address
port: 8080, // Port to listen on
mcpPathPrefix: 'mcp' // Base path for endpoints
);
$server->listen($transport); // Blocks, starting the HTTP server
The HTTP transport provides two endpoints:
GET /{mcpPathPrefix}/sse
- Client connects herePOST /{mcpPathPrefix}/message?clientId={clientId}
- Client sends requests hereTo find and register elements marked with attributes:
$server->discover(
basePath: __DIR__, // Project root
scanDirs: ['src'], // Directories to scan
excludeDirs: ['tests'], // Directories to skip
force: false, // Force re-scan
saveToCache: true // Save discovered elements to cache
);
// String, int, float, bool - wrapped in TextContent
#[McpTool]
public function returnString(): string {
return "Hello world";
}
// Arrays/objects - JSON-encoded and wrapped in TextContent
#[McpTool]
public function returnArray(): array {
return ['status' => 'success', 'data' => ['id' => 123]];
}
// Direct Content objects for full control
use PhpMcp\Server\JsonRpc\Contents\TextContent;
#[McpTool]
public function returnFormatted(): TextContent {
return TextContent::code('echo "Hello";', 'php');
}
// String content
#[McpResource(uri: 'text://example', mimeType: 'text/plain')]
public function getText(): string {
return "This is plain text";
}
// JSON content
#[McpResource(uri: 'config://app', mimeType: 'application/json')]
public function getConfig(): array {
return ['name' => 'MyApp', 'version' => '1.0.0'];
}
// File content
#[McpResource(uri: 'file://example')]
public function getFile(): SplFileInfo {
return new SplFileInfo('/path/to/file.txt');
}
use PhpMcp\Server\JsonRpc\Contents\PromptMessage;
#[McpPrompt(name: 'translation')]
public function translationPrompt(string $text, string $targetLanguage): array {
return [
PromptMessage::user("Translate the following text to {$targetLanguage}:\n\n{$text}"),
PromptMessage::assistant("Here's the translation:")
];
}
// Or using the simpler array format:
#[McpPrompt(name: 'translation_alt')]
public function translationPromptAlt(string $text, string $targetLanguage): array {
return [
['role' => 'user', 'content' => "Translate to {$targetLanguage}:\n\n{$text}"],
['role' => 'assistant', 'content' => "Here's the translation:"]
];
}
Use the Schema
attribute for enhanced input validation:
use PhpMcp\Server\Attributes\Schema;
#[McpTool]
public function validateUser(
#[Schema(format: 'email')]
string $email,
#[Schema(minItems: 2, uniqueItems: true)]
array $tags,
#[Schema(minimum: 1, maximum: 100)]
int $age
): bool {
// Implementation
}
There are two ways to add an MCP server to Cursor. The most common way is to add the server globally in the ~/.cursor/mcp.json
file so that it is available in all of your projects.
If you only need the server in a single project, you can add it to the project instead by creating or adding it to the .cursor/mcp.json
file.
To add a global MCP server go to Cursor Settings > MCP and click "Add new global MCP server".
When you click that button the ~/.cursor/mcp.json
file will be opened and you can add your server like this:
{
"mcpServers": {
"cursor-rules-mcp": {
"command": "npx",
"args": [
"-y",
"cursor-rules-mcp"
]
}
}
}
To add an MCP server to a project you can create a new .cursor/mcp.json
file or add it to the existing one. This will look exactly the same as the global MCP server example above.
Once the server is installed, you might need to head back to Settings > MCP and click the refresh button.
The Cursor agent will then be able to see the available tools the added MCP server has available and will call them when it needs to.
You can also explictly ask the agent to use the tool by mentioning the tool name and describing what the function does.