stribus / mcp-flightphp-server-skeleton
A Flight PHP framework skeleton app for MCP Server
Package info
github.com/stribus/mcp-flightphp-server-skeleton
pkg:composer/stribus/mcp-flightphp-server-skeleton
Requires
- php: ^8.0
- ext-json: *
- flightphp/core: ^3.0
- flightphp/runway: ^1.2
- tracy/tracy: ^2.9
- vlucas/phpdotenv: ^5.6
Requires (Dev)
- flightphp/tracy-extensions: ^0.1 || ^0.2
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/phpstan: ^2.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-27 23:04:30 UTC
README
A PHP-based Model Context Protocol (MCP) server skeleton that supports both HTTP and stdio communication methods. This server can be integrated with VS Code Copilot and other MCP-compatible clients.
Features
- Dual Communication: Streamable HTTP and stdio, both JSON-RPC 2.0
- Spec-compliant: implements MCP revision
2025-06-18 - Auto-discovery: Automatically discovers tools, prompts, and resources
- Code Generation: Built-in commands to generate new tools and prompts
- Flight PHP Framework: Lightweight and fast PHP framework
- Logging: Comprehensive logging for debugging
- Testing Scripts: Ready-to-use test scripts for both modes
Quick Start
-
Install Run this command from the directory in which you want to install your new Flight PHP application. (requires PHP 8.0 or newer)
composer create-project stribus/mcp-flightphp-server-skeleton cool-project-name
-
Configure the environment
cp .env.example .env # Linux / macOS copy .env.example .env # Windows
.envis git-ignored — keep your credentials out of version control. The skeleton also runs without a.env; see the comments in.env.examplefor the database variables. -
Test HTTP Server
composer start # or php -S localhost:8000 -t public -
Test stdio Server
php mcp-server.php
Usage Methods
1. HTTP Server Mode
The HTTP server implements the Streamable HTTP transport:
Start the server:
composer start
Available endpoints:
MCP is a JSON-RPC protocol, not REST: every method travels in the body of a POST to a
single endpoint. There is no /tools/list URL.
POST /mcp- the MCP endpoint. Every JSON-RPC method goes here.GET /mcp- Server-Sent Events stream. Disabled by default; see below.DELETE /mcp- terminate a session.GET /health- health check.GET /- this documentation, as JSON.
Sessions: a successful initialize returns an Mcp-Session-Id header. Send it back on
every subsequent request. After initialization, clients must also send
MCP-Protocol-Version: 2025-06-18.
Origin validation: the endpoint rejects browser requests from unknown origins with 403,
as the spec requires to prevent DNS rebinding attacks. Configure with MCP_ALLOWED_ORIGINS.
Test the HTTP server:
# Windows PowerShell - full transport test suite .\test-http-server.ps1 # Or manually curl -i -X POST http://localhost:8000/mcp -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}'
Server-Sent Events are off by default. GET /mcp answers 405, which the specification
explicitly permits. The reason is measured, not theoretical: PHP's built-in server
(composer start) handles one request at a time, and on Windows it cannot fork at all -
setting PHP_CLI_SERVER_WORKERS makes PHP print forking is not supported on this platform.
An open stream would therefore make the whole server unresponsive. Set MCP_HTTP_SSE=true
only when running behind Apache or nginx with php-fpm.
Even then, every open stream occupies one php-fpm worker for as long as it lasts. With the
default MCP_HTTP_SSE_MAX_SECONDS=0 (unbounded), a few idle clients can exhaust a small pool
and starve every other request. Set a limit — clients reconnect on their own — and size
pm.max_children for the number of concurrent streams you expect.
Server-initiated messages are queued with MCPSessionStore::push($sessionId, $message); the
open stream for that session delivers them. Nothing in the skeleton calls push() yet — it is
the extension point for notifications your own server needs to send.
2. stdio Mode (JSON-RPC 2.0)
The stdio server communicates via standard input/output using JSON-RPC 2.0 protocol:
Start the server:
php mcp-server.php
Test JSON-RPC commands:
# Windows PowerShell .\test-mcp-server.ps1 # Or send commands manually echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | php mcp-server.php echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | php mcp-server.php echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hello-world-tool","arguments":{"firstName":"VS Code","lastName":"Copilot"}}}' | php mcp-server.php
3. VS Code Integration
This repository ships a ready-to-use .vscode/mcp.json. It needs no editing, because
VS Code expands ${workspaceFolder} to wherever you cloned the project:
{
"servers": {
"flightphp-mcp-skeleton": {
"type": "stdio",
"command": "php",
"args": ["${workspaceFolder}/mcp-server.php"]
}
}
}
For clients that cannot expand variables, such as Claude Desktop, composer create-project
generates mcp-config.json with this installation's absolute path already filled in. A
template lives in mcp-config.example.json; regenerate at any time with:
php scripts/generate-mcp-config.php --force
mcp-config.json is git-ignored, since its contents are specific to one machine.
4. Claude Code Integration
.mcp.json ships in the repository root and needs no editing:
{
"mcpServers": {
"flightphp-mcp-skeleton": {
"type": "stdio",
"command": "php",
"args": ["${CLAUDE_PROJECT_DIR:-.}/mcp-server.php"]
}
}
}
CLAUDE_PROJECT_DIR names the project root when Claude Code provides it; the . fallback covers
versions that do not, since project-scoped servers are launched from the project directory.
Open the project in Claude Code and approve the project-scoped server when prompted, then check it
with /mcp or claude mcp list.
The HTTP transport works too, though you have to start the server yourself — with stdio, Claude Code launches and stops the process for you:
composer start claude mcp add --transport http flightphp-mcp-http http://localhost:8000/mcp
Avoid --transport sse: SSE is deprecated in Claude Code, which directs you to HTTP instead.
Full instructions for all three clients, including troubleshooting, are in
CLIENTS-MCP-SETUP.md.
Creating Tools
Tools are executable functions that can be called by MCP clients.
Generate a new tool:
vendor/bin/runway make:tool MyCustomTool
Tool Structure:
<?php namespace app\tools; use app\helpers\AbstractMCPTool; class MyCustomTool extends AbstractMCPTool { protected string $name = 'my-custom-tool'; protected string $description = 'Description of what this tool does'; protected ?string $title = 'My Custom Tool'; protected array $arguments = [ [ 'name' => 'input', 'type' => 'string', 'description' => 'Input parameter description', 'required' => true, ], ]; protected null|array|string $outputSchema = [ 'type' => 'object', 'properties' => [ 'result' => [ 'type' => 'string', 'description' => 'The result of the operation', ], ], ]; public function execute(array $arguments): mixed { $input = $arguments['input'] ?? ''; // Your tool logic here return [ 'result' => 'Processed: ' . $input, ]; } }
Tool Properties:
$name: Unique identifier for the tool$description: What the tool does$title: Human-readable title$arguments: Array of input parameters with types and validation$outputSchema: Expected output structureexecute(): Main logic that processes arguments and returns results
Creating Prompts
Prompts are templates that can generate dynamic text based on context.
Generate a new prompt:
vendor/bin/runway make:prompt MyCustomPrompt
Prompt Structure:
<?php namespace app\prompts; use app\helpers\AbstractMCPPrompt; class MyCustomPrompt extends AbstractMCPPrompt { protected string $name = 'my_custom'; protected string $description = 'Generate custom content based on input'; protected ?string $title = 'My Custom Prompt'; protected array $arguments = [ 'topic' => [ 'type' => 'string', 'description' => 'The topic to generate content about', 'required' => true, ], 'style' => [ 'type' => 'string', 'description' => 'Writing style (formal, casual, technical)', 'required' => false, ], ]; public function getPromptText(array $context): string { $topic = $context['topic'] ?? ''; $style = $context['style'] ?? 'formal'; return "Write a {$style} explanation about {$topic}. " . "Include examples and practical applications."; } }
Prompt Properties:
$name: Unique identifier for the prompt$description: What the prompt generates$title: Human-readable title$arguments: Context variables the prompt expectsgetPromptText(): Method that generates the prompt text based on context
Creating Resources
Resources expose readable data. They are keyed by the URI scheme in getSchema(), so
everything under config:// reaches a resource declaring protected string $schema = 'config';.
See app/resources/ServerInfoResource.php for a working example.
<?php namespace app\resources; use app\helpers\AbstractMCPResource; class MyResource extends AbstractMCPResource { protected string $name = 'my-resource'; protected string $description = 'What this resource exposes'; protected string $schema = 'mydata'; protected ?string $uri = 'mydata://items'; protected string $mimeType = 'application/json'; public function listResources(string $uri): array { return [[ 'uri' => 'mydata://items', 'name' => 'my-resource', 'title' => 'My Resource', 'description' => 'What this resource exposes', 'mimeType' => 'application/json', ]]; } public function getContent(string $uri) { return ['items' => []]; } }
What you return, and what the client receives
Tools, prompts and resources return whatever is natural to write. The framework converts that into the envelopes the MCP specification requires, so you never assemble them by hand:
| You return from | The client receives |
|---|---|
Tool::execute() — any array or scalar |
content[] with a text block, plus structuredContent when the tool declares an outputSchema |
Prompt::getPromptText() — a string |
{description, messages[]} with a typed content block |
Resource::getContent() — an array or string |
{contents[]} carrying uri, mimeType and text |
If you need finer control, return a complete envelope yourself and it is passed through
untouched — that is how a tool emits an image or a resource_link:
public function execute(array $arguments): mixed { return ['content' => [ ['type' => 'text', 'text' => 'See also:'], ['type' => 'resource_link', 'uri' => 'file:///README.md', 'name' => 'README.md'], ]]; }
Argument validation is automatic. Anything marked 'required' => true in $arguments
is checked before execute() runs, and a missing value is rejected with -32602. Do not
hand-roll the check.
Failures inside a tool are not protocol errors. Throw, and the client receives a normal
result with isError: true and your message — the distinction the specification draws
between a protocol problem and a tool that could not do its job.
Project Structure
├── app/
│ ├── tools/ # Tool implementations
│ ├── prompts/ # Prompt implementations
│ ├── resources/ # Resource implementations
│ ├── helpers/ # Interfaces, abstract bases, MCPResultBuilder
│ ├── config/ # Bootstrap, routes, environment access
│ ├── controllers/ # JSON-RPC dispatch
│ └── core/ # Registries, discovery, HTTP session and SSE
├── commands/ # Runway commands for code generation
├── scripts/ # Install-time helpers
├── public/ # HTTP server entry point
├── mcp-server.php # stdio server entry point
└── vendor/ # Dependencies
Available MCP Methods
JSON-RPC 2.0 Methods:
initialize- Initialize the MCP connectiontools/list- List all available toolstools/call- Execute a specific toolprompts/list- List all available promptsprompts/get- Get a specific promptresources/list- List all available resourcesresources/read- Read a specific resource
Logging and Debugging
The server generates detailed logs in mcp-server.log for debugging purposes. Monitor this file to see:
- Incoming requests
- Server responses
- Errors and exceptions
- Tool execution details
Common Issues
- Server won't start: Ensure PHP is in your PATH and dependencies are installed
- JSON parsing errors: Verify JSON-RPC message formatting
- Tool not found: Check tool is properly registered and follows naming conventions
- Permission errors: Verify write permissions for log files
Further reading
CHANGELOG.md— what changed in each releaseUPGRADE.md— migrating from 1.x to 2.0CLIENTS-MCP-SETUP.md— connecting Claude Code, VS Code and Claude Desktop, with troubleshooting (Portuguese)
Requirements
- PHP 8.0 or higher — the code uses union types (
null|array|string) andmixed - Composer
- ext-json extension
License
MIT License - see LICENSE file for details.