Skip to content

Latest commit

 

History

History
87 lines (67 loc) · 2.74 KB

first-server.md

File metadata and controls

87 lines (67 loc) · 2.74 KB

First server

A server is a plain PHP class plus a few lines of wiring. Create server.php next to your vendor/ directory:

<?php

require __DIR__.'/vendor/autoload.php';

use Mcp\Server\Capability\Attribute\McpResource;
use Mcp\Server\Capability\Attribute\McpTool;
use Mcp\Server\Server;
use Mcp\Server\Transport\StdioTransport;

class Calculator
{
    /**
     * Adds two numbers.
     */
    #[McpTool]
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }

    #[McpResource(uri: 'config://calculator/settings')]
    public function settings(): array
    {
        return ['precision' => 2];
    }
}

exit(Server::builder()
    ->setServerInfo('Calculator', '1.0.0')
    ->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor'])
    ->build()
    ->run(new StdioTransport()));

Discovery needs symfony/finder:

composer require symfony/finder

What each piece does

#[McpTool] marks a method as an action the model can call. Its name defaults to the method name, its description comes from the docblock (the summary, plus the longer description if you write one), and its input schema is generated from the parameter types — int $a, int $b becomes a JSON Schema with two required integers. See Tools.

#[McpResource] marks a method as read-only data the application can read, addressed by URI. See Resources.

setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor']) scans those directories for attributed classes. Scanning is lazy: it happens on the first request that needs the registry, not when build() returns — call setLazyLoading(false) if you would rather pay for it up front.

Always exclude vendor. The scan is recursive and autoloads every class it finds. One class that can't be autoloaded stops the scan, and the error only goes to the logger. A class in php-http/discovery does this on a default install, and the server can then list no tools.

If you would rather register elements manually, or mix both, see Registering elements.

run(new StdioTransport()) speaks JSON-RPC over stdin/stdout and returns an exit code. That is the transport local MCP hosts launch as a subprocess; for a web-facing server use the HTTP transport instead.

!!! warning "Never write to STDOUT" With the STDIO transport, STDOUT carries the protocol. echo, print_r(), or a stray var_dump() in a handler corrupts the stream. Write diagnostics to STDERR.

Run it

php server.php

Nothing happens — the server is waiting for JSON-RPC on stdin, which is exactly right. Stop it with Ctrl+C, and let a real client drive it instead: Try it with the Inspector.