Search by

stribus / mcp-flightphp-server-skeleton

stribus

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

Statistics

Installs: 15

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

2.0.0 2026-09-27 23:03 UTC

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

  1. 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
  2. Configure the environment

    cp .env.example .env      # Linux / macOS
    copy .env.example .env    # Windows

    .env is git-ignored — keep your credentials out of version control. The skeleton also runs without a .env; see the comments in .env.example for the database variables.

  3. Test HTTP Server

    composer start
    # or
    php -S localhost:8000 -t public
  4. 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 structure
  • execute(): 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 expects
  • getPromptText(): 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 connection
  • tools/list - List all available tools
  • tools/call - Execute a specific tool
  • prompts/list - List all available prompts
  • prompts/get - Get a specific prompt
  • resources/list - List all available resources
  • resources/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

  1. Server won't start: Ensure PHP is in your PATH and dependencies are installed
  2. JSON parsing errors: Verify JSON-RPC message formatting
  3. Tool not found: Check tool is properly registered and follows naming conventions
  4. Permission errors: Verify write permissions for log files

Further reading

Requirements

  • PHP 8.0 or higher — the code uses union types (null|array|string) and mixed
  • Composer
  • ext-json extension

License

MIT License - see LICENSE file for details.