Skip to content

Getting started

This tutorial takes you from an empty directory to a working pair. You build a stdio MCP server that exposes one tool, and a PHP client that spawns the server and calls that tool. You can copy and run every step.

Requirements

Install

composer require nexusphp/mcp

The umbrella package carries the server, the client, and the official extensions. To install one side only, require nexusphp/mcp-server or nexusphp/mcp-client instead. The README lists every component package.

The SDK targets MCP spec 2026-07-28. It runs on AMPHP and Revolt, so its synchronous-looking API is fiber-driven. You need no further setup to build a server.

Your first MCP server

Create hello.php:

<?php

declare(strict_types=1);

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

use Nexus\Mcp\Core\Schema\ContentBlock\TextContent;
use Nexus\Mcp\Core\Schema\Result\CallToolResult;
use Nexus\Mcp\Core\Schema\Tool\Tool;
use Nexus\Mcp\Server\ServerBuilder;
use Nexus\Mcp\Server\ServerContext;
use Nexus\Mcp\Server\Transport\StdioServerTransport;

$server = (new ServerBuilder())
    ->setServerInfo(name: 'hello', version: '0.1.0')
    ->addTool(
        tool: new Tool(
            name: 'greet',
            inputSchema: [
                'type' => 'object',
                'properties' => [
                    'name' => ['type' => 'string'],
                ],
                'required' => ['name'],
            ],
            description: 'Greets the named person.',
        ),
        executor: static function (?array $args, ServerContext $context): CallToolResult {
            $name = is_string($args['name'] ?? null) ? $args['name'] : 'stranger';

            return new CallToolResult(content: [new TextContent(text: sprintf('Hello, %s!', $name))]);
        },
    )
    ->build()
;

$server->run(new StdioServerTransport());

That is a full, runnable MCP server. It advertises one tool, greet. It exposes the server/discover, tools/list, and tools/call handlers the SDK ships by default. It speaks line-framed JSON-RPC over STDIN and STDOUT.

Run it

MCP Inspector gives you an interactive UI for tools, prompts, and resources. You write no client:

npx @modelcontextprotocol/inspector php hello.php

Open the URL it prints. Click Connect, then invoke greet from the Tools panel.

Claude Desktop, Cursor, or any mcpServers-aware client

Add this to the client's MCP configuration:

{
    "mcpServers": {
        "hello": {
            "command": "php",
            "args": ["/absolute/path/to/hello.php"]
        }
    }
}

Direct CLI

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"cli","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}' | php hello.php

Every request carries a _meta block with the client's identity. The keys io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities are required. The key io.modelcontextprotocol/clientInfo is optional. The server reads one JSON-RPC envelope per line on STDIN and writes responses to STDOUT. This is useful for scripted smoke tests, and less useful for interactive exploration.

Your first MCP client

The client ships in the same package. Create hello-client.php next to hello.php:

<?php

declare(strict_types=1);

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

use Nexus\Mcp\Client\ClientBuilder;
use Nexus\Mcp\Client\Transport\StdioClientTransport;
use Nexus\Mcp\Core\Schema\ContentBlock\TextContent;
use Nexus\Mcp\Core\Schema\Result\CallToolResult;

$client = (new ClientBuilder())
    ->setClientInfo(name: 'hello-client', version: '0.1.0')
    ->build()
;

$client->connect(new StdioClientTransport(command: [PHP_BINARY, __DIR__.'/hello.php']));

try {
    $client->discover();

    $result = $client->callTool(name: 'greet', arguments: ['name' => 'Ada']);

    if ($result instanceof CallToolResult) {
        foreach ($result->content as $block) {
            if ($block instanceof TextContent) {
                echo $block->text, PHP_EOL;
            }
        }
    }
} finally {
    $client->disconnect();
}

StdioClientTransport spawns the server as a subprocess. It speaks the same line-framed JSON-RPC over the subprocess's STDIN and STDOUT.

discover() sends server/discover. The client learns the server's identity and records its capabilities, which the typed request methods check before they send. The protocol is sessionless, so this call establishes nothing. It is a plain request, and any other request may precede it.

callTool() answers with a typed result. The union includes InputRequiredResult, which When the server asks for input first covers. Branch on the type. Do not assume the happy path.

php hello-client.php

This prints Hello, Ada!. For a client and server in one process with no subprocess, see examples/in-memory.php.

Logging

MCP servers MUST NOT write to STDOUT outside of the JSON-RPC stream. The SDK uses PSR-3 for diagnostic logging. It writes nothing by default, because it uses a NullLogger unless you provide one. Pass a real logger with ServerBuilder::setLogger() and target STDERR. See examples/stdio-server.php for a worked example that routes server diagnostics to a PSR-3 logger on STDERR.

Next steps

  • Server API: the full ServerBuilder reference. It covers tools, prompts, resources, completions, custom request handlers, and the request and notification lifecycle.
  • Client API: the ClientBuilder and Client reference. It covers server/discover, the typed request methods, and streaming progress from callTool.
  • Transports: what each transport guarantees, and what it does not.
  • Error handling: the exception model, the JSON-RPC error codes, and which calls throw what.
  • Best practices: conventions for servers and clients.
  • Architecture: namespacing, layering rules, and the dispatch kernel.
  • Design rationale: the choices behind the SDK.
  • examples/: runnable demo servers and clients over stdio, in-memory, and HTTP.