{"kind":"AgentDefinition","metadata":{"namespace":"community","name":"php-mcp-server","version":"0.1.0"},"spec":{"agents_md":"---\ndescription: 'Best practices for building Model Context Protocol servers in PHP using the official PHP SDK with attribute-based discovery and multiple transport options'\napplyTo: '**/*.php'\n---\n\n# PHP MCP Server Development Best Practices\n\nThis guide provides best practices for building Model Context Protocol (MCP) servers using the official PHP SDK maintained in collaboration with The PHP Foundation.\n\n## Installation and Setup\n\n### Install via Composer\n\n```bash\ncomposer require mcp/sdk\n```\n\n### Project Structure\n\nOrganize your PHP MCP server project:\n\n```\nmy-mcp-server/\n├── composer.json\n├── src/\n│   ├── Tools/\n│   │   ├── Calculator.php\n│   │   └── FileManager.php\n│   ├── Resources/\n│   │   ├── ConfigProvider.php\n│   │   └── DataProvider.php\n│   ├── Prompts/\n│   │   └── PromptGenerator.php\n│   └── Server.php\n├── server.php           # Server entry point\n└── tests/\n    └── ToolsTest.php\n```\n\n### Composer Configuration\n\n```json\n{\n    \"name\": \"your-org/mcp-server\",\n    \"description\": \"MCP Server for...\",\n    \"type\": \"project\",\n    \"require\": {\n        \"php\": \"^8.2\",\n        \"mcp/sdk\": \"^0.1\"\n    },\n    \"require-dev\": {\n        \"phpunit/phpunit\": \"^10.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"App\\\\\": \"src/\"\n        }\n    }\n}\n```\n\n## Server Implementation\n\n### Basic Server with Attribute Discovery\n\nCreate your server entry point:\n\n```php\n#!/usr/bin/env php\n\u003c?php\n\ndeclare(strict_types=1);\n\nrequire_once __DIR__ . '/vendor/autoload.php';\n\nuse Mcp\\Server;\nuse Mcp\\Server\\Transport\\StdioTransport;\n\n$server = Server::builder()\n    -\u003esetServerInfo('My MCP Server', '1.0.0')\n    -\u003esetDiscovery(__DIR__, ['.'])\n    -\u003ebuild();\n\n$transport = new StdioTransport();\n\n$server-\u003erun($transport);\n```\n\n### Server with Caching\n\nUse PSR-16 cache for better performance:\n\n```php\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\n$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery'));\n\n$server = Server::builder()\n    -\u003esetServerInfo('My MCP Server', '1.0.0')\n    -\u003esetDiscovery(\n        basePath: __DIR__,\n        scanDirs: ['.', 'src'],\n        excludeDirs: ['vendor', 'tests'],\n        cache: $cache\n    )\n    -\u003ebuild();\n```\n\n### Manual Registration\n\nRegister capabilities programmatically:\n\n```php\nuse App\\Tools\\Calculator;\nuse App\\Resources\\Config;\n\n$server = Server::builder()\n    -\u003esetServerInfo('My MCP Server', '1.0.0')\n    -\u003eaddTool([Calculator::class, 'add'], 'add')\n    -\u003eaddTool([Calculator::class, 'multiply'], 'multiply')\n    -\u003eaddResource([Config::class, 'getSettings'], 'config://app/settings')\n    -\u003ebuild();\n```\n\n## Tool Development\n\n### Simple Tool with Attribute\n\n```php\n\u003c?php\n\nnamespace App\\Tools;\n\nuse Mcp\\Capability\\Attribute\\McpTool;\n\nclass Calculator\n{\n    /**\n     * Adds two numbers together.\n     * \n     * @param int $a The first number\n     * @param int $b The second number\n     * @return int The sum of the two numbers\n     */\n    #[McpTool]\n    public function add(int $a, int $b): int\n    {\n        return $a + $b;\n    }\n}\n```\n\n### Tool with Custom Name\n\n```php\nuse Mcp\\Capability\\Attribute\\McpTool;\n\nclass FileManager\n{\n    /**\n     * Reads file content from the filesystem.\n     */\n    #[McpTool(name: 'read_file')]\n    public function readFileContent(string $path): string\n    {\n        if (!file_exists($path)) {\n            throw new \\InvalidArgumentException(\"File not found: {$path}\");\n        }\n        \n        return file_get_contents($path);\n    }\n}\n```\n\n### Tool with Validation and Schema\n\n```php\nuse Mcp\\Capability\\Attribute\\{McpTool, Schema};\n\nclass UserManager\n{\n    #[McpTool(name: 'create_user')]\n    public function createUser(\n        #[Schema(format: 'email')]\n        string $email,\n        \n        #[Schema(minimum: 18, maximum: 120)]\n        int $age,\n        \n        #[Schema(\n            pattern: '^[A-Z][a-z]+$',\n            description: 'Capitalized first name'\n        )]\n        string $firstName\n    ): array\n    {\n        return [\n            'id' =\u003e uniqid(),\n            'email' =\u003e $email,\n            'age' =\u003e $age,\n            'firstName' =\u003e $firstName\n        ];\n    }\n}\n```\n\n### Tool with Complex Return Types\n\n```php\nuse Mcp\\Schema\\Content\\{TextContent, ImageContent};\n\nclass ReportGenerator\n{\n    #[McpTool]\n    public function generateReport(string $type): array\n    {\n        return [\n            new TextContent('Report generated:'),\n            TextContent::code($this-\u003egenerateCode($type), 'php'),\n            new TextContent('Summary: All checks passed.')\n        ];\n    }\n    \n    #[McpTool]\n    public function getChart(string $chartType): ImageContent\n    {\n        $imageData = $this-\u003egenerateChartImage($chartType);\n        \n        return new ImageContent(\n            data: base64_encode($imageData),\n            mimeType: 'image/png'\n        );\n    }\n}\n```\n\n### Tool with Match Expression\n\n```php\n#[McpTool(name: 'calculate')]\npublic function performCalculation(float $a, float $b, string $operation): float\n{\n    return match($operation) {\n        'add' =\u003e $a + $b,\n        'subtract' =\u003e $a - $b,\n        'multiply' =\u003e $a * $b,\n        'divide' =\u003e $b != 0 ? $a / $b : \n            throw new \\InvalidArgumentException('Division by zero'),\n        default =\u003e throw new \\InvalidArgumentException('Invalid operation')\n    };\n}\n```\n\n## Resource Implementation\n\n### Static Resource\n\n```php\n\u003c?php\n\nnamespace App\\Resources;\n\nuse Mcp\\Capability\\Attribute\\McpResource;\n\nclass ConfigProvider\n{\n    /**\n     * Provides the current application configuration.\n     */\n    #[McpResource(\n        uri: 'config://app/settings',\n        name: 'app_settings',\n        mimeType: 'application/json'\n    )]\n    public function getSettings(): array\n    {\n        return [\n            'version' =\u003e '1.0.0',\n            'debug' =\u003e false,\n            'features' =\u003e ['auth', 'logging']\n        ];\n    }\n}\n```\n\n### Resource Template with Variables\n\n```php\nuse Mcp\\Capability\\Attribute\\McpResourceTemplate;\n\nclass UserProvider\n{\n    /**\n     * Retrieves user profile information by ID and section.\n     */\n    #[McpResourceTemplate(\n        uriTemplate: 'user://{userId}/profile/{section}',\n        name: 'user_profile',\n        description: 'User profile data by section',\n        mimeType: 'application/json'\n    )]\n    public function getUserProfile(string $userId, string $section): array\n    {\n        // Variable order must match URI template order\n        return $this-\u003eusers[$userId][$section] ?? \n            throw new \\InvalidArgumentException(\"Profile section not found\");\n    }\n}\n```\n\n### Resource with File Content\n\n```php\nuse Mcp\\Schema\\Content\\{TextResourceContents, BlobResourceContents};\n\nclass FileProvider\n{\n    #[McpResource(uri: 'file://readme.txt', mimeType: 'text/plain')]\n    public function getReadme(): TextResourceContents\n    {\n        return new TextResourceContents(\n            uri: 'file://readme.txt',\n            mimeType: 'text/plain',\n            text: file_get_contents(__DIR__ . '/README.txt')\n        );\n    }\n    \n    #[McpResource(uri: 'file://image.png', mimeType: 'image/png')]\n    public function getImage(): BlobResourceContents\n    {\n        $imageData = file_get_contents(__DIR__ . '/image.png');\n        \n        return new BlobResourceContents(\n            uri: 'file://image.png',\n            mimeType: 'image/png',\n            blob: base64_encode($imageData)\n        );\n    }\n}\n```\n\n## Prompt Implementation\n\n### Basic Prompt\n\n```php\n\u003c?php\n\nnamespace App\\Prompts;\n\nuse Mcp\\Capability\\Attribute\\McpPrompt;\n\nclass PromptGenerator\n{\n    /**\n     * Generates a code review request prompt.\n     */\n    #[McpPrompt(name: 'code_review')]\n    public function reviewCode(string $language, string $code, string $focus = 'general'): array\n    {\n        return [\n            ['role' =\u003e 'assistant', 'content' =\u003e 'You are an expert code reviewer.'],\n            ['role' =\u003e 'user', 'content' =\u003e \"Review this {$language} code focusing on {$focus}:\\n\\n```{$language}\\n{$code}\\n```\"]\n        ];\n    }\n}\n```\n\n### Prompt with Mixed Content\n\n```php\nuse Mcp\\Schema\\Content\\{TextContent, ImageContent};\nuse Mcp\\Schema\\PromptMessage;\nuse Mcp\\Schema\\Enum\\Role;\n\n#[McpPrompt]\npublic function analyzeImage(string $imageUrl, string $question): array\n{\n    $imageData = file_get_contents($imageUrl);\n    \n    return [\n        new PromptMessage(Role::Assistant, [\n            new TextContent('You are an image analysis expert.')\n        ]),\n        new PromptMessage(Role::User, [\n            new TextContent($question),\n            new ImageContent(\n                data: base64_encode($imageData),\n                mimeType: 'image/jpeg'\n            )\n        ])\n    ];\n}\n```\n\n## Completion Providers\n\n### Value List Completion\n\n```php\nuse Mcp\\Capability\\Attribute\\{McpPrompt, CompletionProvider};\n\n#[McpPrompt]\npublic function generateContent(\n    #[CompletionProvider(values: ['blog', 'article', 'tutorial', 'guide'])]\n    string $contentType,\n    \n    #[CompletionProvider(values: ['beginner', 'intermediate', 'advanced'])]\n    string $difficulty\n): array\n{\n    return [\n        ['role' =\u003e 'user', 'content' =\u003e \"Create a {$difficulty} level {$contentType}\"]\n    ];\n}\n```\n\n### Enum Completion\n\n```php\nenum Priority: string\n{\n    case LOW = 'low';\n    case MEDIUM = 'medium';\n    case HIGH = 'high';\n}\n\nenum Status\n{\n    case DRAFT;\n    case PUBLISHED;\n    case ARCHIVED;\n}\n\n#[McpResourceTemplate(uriTemplate: 'tasks/{taskId}')]\npublic function getTask(\n    string $taskId,\n    \n    #[CompletionProvider(enum: Priority::class)]\n    string $priority,\n    \n    #[CompletionProvider(enum: Status::class)]\n    string $status\n): array\n{\n    return $this-\u003etasks[$taskId] ?? [];\n}\n```\n\n### Custom Completion Provider\n\n```php\nuse Mcp\\Capability\\Prompt\\Completion\\ProviderInterface;\n\nclass UserIdCompletionProvider implements ProviderInterface\n{\n    public function __construct(\n        private DatabaseService $db\n    ) {}\n\n    public function getCompletions(string $currentValue): array\n    {\n        return $this-\u003edb-\u003esearchUserIds($currentValue);\n    }\n}\n\n#[McpResourceTemplate(uriTemplate: 'user://{userId}/profile')]\npublic function getUserProfile(\n    #[CompletionProvider(provider: UserIdCompletionProvider::class)]\n    string $userId\n): array\n{\n    return $this-\u003eusers[$userId] ?? \n        throw new \\InvalidArgumentException('User not found');\n}\n```\n\n## Transport Options\n\n### Stdio Transport\n\nFor command-line integration (default):\n\n```php\nuse Mcp\\Server\\Transport\\StdioTransport;\n\n$transport = new StdioTransport();\n$server-\u003erun($transport);\n```\n\n### HTTP Transport\n\nFor web-based integration:\n\n```php\nuse Mcp\\Server\\Transport\\StreamableHttpTransport;\nuse Nyholm\\Psr7\\Factory\\Psr17Factory;\n\n$psr17Factory = new Psr17Factory();\n\n$request = $psr17Factory-\u003ecreateServerRequestFromGlobals();\n\n$transport = new StreamableHttpTransport(\n    $request,\n    $psr17Factory,  // Response factory\n    $psr17Factory   // Stream factory\n);\n\n$response = $server-\u003erun($transport);\n\n// Send response in your web framework\nforeach ($response-\u003egetHeaders() as $name =\u003e $values) {\n    foreach ($values as $value) {\n        header(\"$name: $value\", false);\n    }\n}\n\nhttp_response_code($response-\u003egetStatusCode());\necho $response-\u003egetBody();\n```\n\n## Session Management\n\n### In-Memory Sessions (Default)\n\n```php\n$server = Server::builder()\n    -\u003esetServerInfo('My Server', '1.0.0')\n    -\u003esetSession(ttl: 7200) // 2 hours\n    -\u003ebuild();\n```\n\n### File-Based Sessions\n\n```php\nuse Mcp\\Server\\Session\\FileSessionStore;\n\n$server = Server::builder()\n    -\u003esetServerInfo('My Server', '1.0.0')\n    -\u003esetSession(new FileSessionStore(__DIR__ . '/sessions'))\n    -\u003ebuild();\n```\n\n### Custom Session Store\n\n```php\nuse Mcp\\Server\\Session\\InMemorySessionStore;\n\n$server = Server::builder()\n    -\u003esetServerInfo('My Server', '1.0.0')\n    -\u003esetSession(new InMemorySessionStore(3600))\n    -\u003ebuild();\n```\n\n## Error Handling\n\n### Exception Handling in Tools\n\n```php\n#[McpTool]\npublic function divideNumbers(float $a, float $b): float\n{\n    if ($b === 0.0) {\n        throw new \\InvalidArgumentException('Division by zero is not allowed');\n    }\n    \n    return $a / $b;\n}\n\n#[McpTool]\npublic function processFile(string $filename): string\n{\n    if (!file_exists($filename)) {\n        throw new \\InvalidArgumentException(\"File not found: {$filename}\");\n    }\n    \n    if (!is_readable($filename)) {\n        throw new \\RuntimeException(\"File not readable: {$filename}\");\n    }\n    \n    return file_get_contents($filename);\n}\n```\n\n### Custom Error Responses\n\nThe SDK automatically converts exceptions into JSON-RPC error responses that MCP clients understand.\n\n## Testing\n\n### PHPUnit Tests for Tools\n\n```php\n\u003c?php\n\nnamespace Tests;\n\nuse PHPUnit\\Framework\\TestCase;\nuse App\\Tools\\Calculator;\n\nclass CalculatorTest extends TestCase\n{\n    private Calculator $calculator;\n    \n    protected function setUp(): void\n    {\n        $this-\u003ecalculator = new Calculator();\n    }\n    \n    public function testAdd(): void\n    {\n        $result = $this-\u003ecalculator-\u003eadd(5, 3);\n        $this-\u003eassertSame(8, $result);\n    }\n    \n    public function testDivideByZero(): void\n    {\n        $this-\u003eexpectException(\\InvalidArgumentException::class);\n        $this-\u003eexpectExceptionMessage('Division by zero');\n        \n        $this-\u003ecalculator-\u003edivide(10, 0);\n    }\n}\n```\n\n### Testing Server Discovery\n\n```php\npublic function testServerDiscoversTools(): void\n{\n    $server = Server::builder()\n        -\u003esetServerInfo('Test Server', '1.0.0')\n        -\u003esetDiscovery(__DIR__ . '/../src', ['.'])\n        -\u003ebuild();\n    \n    $capabilities = $server-\u003egetCapabilities();\n    \n    $this-\u003eassertArrayHasKey('tools', $capabilities);\n    $this-\u003eassertNotEmpty($capabilities['tools']);\n}\n```\n\n## Performance Best Practices\n\n### Use Discovery Caching\n\nAlways use caching in production:\n\n```php\nuse Symfony\\Component\\Cache\\Adapter\\RedisAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\n$redis = new \\Redis();\n$redis-\u003econnect('127.0.0.1', 6379);\n\n$cache = new Psr16Cache(new RedisAdapter($redis));\n\n$server = Server::builder()\n    -\u003esetServerInfo('My Server', '1.0.0')\n    -\u003esetDiscovery(\n        basePath: __DIR__,\n        scanDirs: ['src'],\n        excludeDirs: ['vendor', 'tests', 'var'],\n        cache: $cache\n    )\n    -\u003ebuild();\n```\n\n### Optimize Scan Directories\n\nOnly scan necessary directories:\n\n```php\n$server = Server::builder()\n    -\u003esetDiscovery(\n        basePath: __DIR__,\n        scanDirs: ['src/Tools', 'src/Resources'],  // Specific dirs\n        excludeDirs: ['vendor', 'tests', 'var', 'cache']\n    )\n    -\u003ebuild();\n```\n\n### Use OPcache\n\nEnable OPcache in production for better PHP performance:\n\n```ini\n; php.ini\nopcache.enable=1\nopcache.memory_consumption=256\nopcache.interned_strings_buffer=16\nopcache.max_accelerated_files=10000\nopcache.validate_timestamps=0\n```\n\n## Framework Integration\n\n### Laravel Integration\n\n```php\n// app/Console/Commands/McpServer.php\nnamespace App\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Mcp\\Server;\nuse Mcp\\Server\\Transport\\StdioTransport;\n\nclass McpServer extends Command\n{\n    protected $signature = 'mcp:serve';\n    protected $description = 'Start MCP server';\n    \n    public function handle()\n    {\n        $server = Server::builder()\n            -\u003esetServerInfo('Laravel MCP Server', '1.0.0')\n            -\u003esetDiscovery(app_path(), ['Tools', 'Resources'])\n            -\u003ebuild();\n        \n        $transport = new StdioTransport();\n        $server-\u003erun($transport);\n    }\n}\n```\n\n### Symfony Integration\n\n```php\n// Use symfony/mcp-bundle for native integration\ncomposer require symfony/mcp-bundle\n```\n\n## Deployment\n\n### Docker Deployment\n\n```dockerfile\nFROM php:8.2-cli\n\n# Install extensions\nRUN docker-php-ext-install pdo pdo_mysql\n\n# Install Composer\nCOPY --from=composer:latest /usr/bin/composer /usr/bin/composer\n\n# Set working directory\nWORKDIR /app\n\n# Copy application\nCOPY . /app\n\n# Install dependencies\nRUN composer install --no-dev --optimize-autoloader\n\n# Make server executable\nRUN chmod +x /app/server.php\n\nCMD [\"php\", \"/app/server.php\"]\n```\n\n### Systemd Service\n\n```ini\n[Unit]\nDescription=MCP PHP Server\nAfter=network.target\n\n[Service]\nType=simple\nUser=www-data\nWorkingDirectory=/var/www/mcp-server\nExecStart=/usr/bin/php /var/www/mcp-server/server.php\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n```\n\n## Configuration for MCP Clients\n\n### Claude Desktop Configuration\n\n```json\n{\n  \"mcpServers\": {\n    \"php-server\": {\n      \"command\": \"php\",\n      \"args\": [\"/absolute/path/to/server.php\"]\n    }\n  }\n}\n```\n\n### MCP Inspector Testing\n\n```bash\nnpx @modelcontextprotocol/inspector php /path/to/server.php\n```\n\n## Additional Resources\n\n- [Official PHP SDK Repository](https://github.com/modelcontextprotocol/php-sdk)\n- [MCP Elements Documentation](https://github.com/modelcontextprotocol/php-sdk/blob/main/docs/mcp-elements.md)\n- [Server Builder Documentation](https://github.com/modelcontextprotocol/php-sdk/blob/main/docs/server-builder.md)\n- [Transport Documentation](https://github.com/modelcontextprotocol/php-sdk/blob/main/docs/transports.md)\n- [Examples](https://github.com/modelcontextprotocol/php-sdk/blob/main/docs/examples.md)\n- [MCP Specification](https://spec.modelcontextprotocol.io/)\n- [Model Context Protocol](https://modelcontextprotocol.io/)\n","description":"Best practices for building Model Context Protocol servers in PHP using the official PHP SDK with attribute-based discovery and multiple transport options","import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/blob/541b7819d8c3545c6df122491af4fa1eae415779/instructions/php-mcp-server.instructions.md"},"manifest":{}},"content_hash":[73,185,245,175,163,203,163,96,171,53,51,50,7,17,34,189,222,26,183,140,102,111,61,240,113,188,61,3,112,237,72,245],"trust_level":"unsigned","yanked":false}
