php-sdk/docs/server-builder.md
Christopher Hertel eb20453f49
[Client][Schema][Server] Implement protocol version negotiation (#403)
* [Client][Docs][Schema][Server] Implement protocol version negotiation

The `initialize` handler never looked at the version the client asked for:
it always answered with the server's configured version (or the SDK default),
so a client offering a revision the server supports could still be told to
speak a different one. The spec requires echoing back a supported request and
only counter-offering when the requested version cannot be honoured.

Turns ProtocolVersion into the registry the other SDKs converged on
(typescript-sdk's protocolEras.ts, python-sdk's mcp_types/version.py):
`latestHandshake()`, `handshakeVersions()`, `modernVersions()` and ordered
comparison through `isAtLeast()`. Comparison goes through declaration order
rather than string collation, so an unrecognized peer string compares
conservatively instead of accidentally sorting above a known revision. The
one throw the enum can produce is the SDK's own LogicException, so a consumer
catching `Mcp\Exception\ExceptionInterface` does not miss it.

Adds 2026-07-28 as a known revision and splits the set into two eras:
handshake (2025-11-25 and earlier, negotiated via `initialize`) and modern
(2026-07-28+, per-request `_meta`). The lists are kept apart so a modern
revision can never leak into an `initialize` exchange, which has no
`initialize` at all. Serving the modern era is follow-up work; this only
teaches the SDK that the revision exists and must not be mis-negotiated.
Deliberately absent is any "newest revision in any era" accessor, which
would hand back the one revision the handshake must never offer.

Server: negotiates and stores the result on the session. A version pinned in
Configuration still pins the handshake to exactly that revision.
Client: validates the counter-offer and fails the handshake instead of
continuing on a revision it cannot speak; records the negotiated version on
the client state, readable through the new Client::getProtocolVersion().

The HTTP middleware now defaults to the handshake versions rather than every
declared case, so it stops accepting a modern version the server cannot yet
serve. When the header is absent it falls back to
ProtocolVersion::DEFAULT_NEGOTIATED_VERSION (2025-03-26), the revision that
introduced both Streamable HTTP and the header itself.

The negotiation behaviour is one provideNegotiationTable() data provider
whose rows are the documented table -- supported revision echoed back,
unknown or malformed counter-offered, modern counter-offered -- so the docs
and the suite cannot drift apart. The supported-revision rows iterate
handshakeVersions() rather than a literal list, so declaring a new handshake
revision extends the table automatically, and the "never answer with a modern
revision" assertion runs on every row.

Documented in server-builder.md (the enum registry, how the server answers a
handshake, what pinning changes), transports.md (the header check) and
client.md (the client's side), all linking the spec's versioning section.

* [Client][Docs][Schema][Server] Warn when the configured protocol version cannot be negotiated

* Log a warning instead of silently falling back when a modern revision is
  configured on the client or pinned on the server builder.
* Document that setProtocolVersion() does not restrict the MCP-Protocol-Version
  header check, which validates against a fixed set.
* Pin every revision's era in a test, so a new case declared past the modern
  boundary fails instead of dropping out of the handshake set.
* Add CHANGELOG entry.

* [Client][Docs][Schema][Server] Rename version fallback constant and trim comments
2026-08-15 01:41:19 +02:00

27 KiB

Server Builder

The server Builder is a fluent builder class that simplifies the creation and configuration of an MCP server instance. It provides methods for setting server information, configuring discovery, registering capabilities, and customizing various aspects of the server behavior.

Table of Contents

Basic Usage

There are two ways to obtain a server builder instance:

use Mcp\Server;

$server = Server::builder()
    ->setServerInfo('My MCP Server', '1.0.0')
    ->setDiscovery(__DIR__, ['.'])
    ->build();

Method 2: Direct Instantiation

use Mcp\Server\Builder;

$server = (new Builder())
    ->setServerInfo('My MCP Server', '1.0.0')
    ->setDiscovery(__DIR__, ['.'])
    ->build();

Both methods return a Builder instance that you can configure with fluent methods. The build() method returns the final Server instance ready for use.

Server Configuration

Server Information

Set the server's identity with name, version, and optional description:

use Mcp\Schema\Icon;
use Mcp\Server;

$server = Server::builder()
    ->setServerInfo(
        name: 'Calculator Server',
        version: '1.2.0',
        description: 'Advanced mathematical calculations',
        icons: [new Icon('https://example.com/icon.png', 'image/png', ['64x64'])],
        websiteUrl: 'https://example.com'
    ');

Parameters:

  • $name (string): The server name
  • $version (string): Version string (semantic versioning recommended)
  • $description (string|null): Optional description
  • $icons (Icon[]|null): Optional array of server icons
  • $websiteUrl (string|null): Optional server website URL

Pagination Limit

Configure the maximum number of items returned in paginated responses:

$server = Server::builder()
    ->setPaginationLimit(100); // Default: 50

Instructions

Provide hints to help AI models understand how to use your server:

$server = Server::builder()
    ->setInstructions('This calculator supports basic arithmetic operations. Use the calculate tool for math operations and check the config resource for current settings.');

Protocol Version

By default the server negotiates the protocol revision with each client during the initialize handshake, and you do not need to configure anything. See Protocol Version Negotiation below for how that negotiation resolves, and for what setProtocolVersion() changes:

use Mcp\Schema\Enum\ProtocolVersion;

$server = Server::builder()
    ->setProtocolVersion(ProtocolVersion::V2025_06_18);

Protocol Version Negotiation

MCP revisions are identified by a date string such as 2025-11-25. The client names the revision it wants to speak in its initialize request, and the server answers with the revision the connection will actually use. Both sides disconnect if they cannot agree. This follows the protocol version negotiation section of the specification.

The SDK's known revisions live in Mcp\Schema\Enum\ProtocolVersion, declared oldest to newest:

use Mcp\Schema\Enum\ProtocolVersion;

ProtocolVersion::latestHandshake();   // newest revision reachable via `initialize`
ProtocolVersion::handshakeVersions(); // every revision the server will negotiate, oldest first
ProtocolVersion::V2025_11_25->isAtLeast(ProtocolVersion::V2025_06_18); // true

Comparisons go through declaration order rather than string collation. The identifiers happen to be ISO dates today, but they are an enumerated set rather than an ordered scalar, so nothing should assume they sort chronologically.

How the server answers

Client requests Server responds with
A revision the server supports That same revision
An unknown or malformed revision ProtocolVersion::latestHandshake() as a counter-offer
A modern revision such as 2026-07-28 ProtocolVersion::latestHandshake() as a counter-offer

A counter-offer is not an error: the client decides whether it can continue on the offered revision or must close the connection. The negotiated revision is stored on the session under protocol_version.

The last row is not a rejection of an unknown revision — the SDK knows 2026-07-28, it just cannot be reached through this handshake. The modern era replaced initialize with per-request metadata, so answering with one of its revisions would leave a connection neither side could use. Serving that era is separate work; today the server only knows not to mis-negotiate it.

This table is mirrored by the provideNegotiationTable() data provider in tests/Unit/Server/Handler/Request/InitializeHandlerTest.php, which drives its supported-revision rows off the enum so a newly declared revision is covered automatically.

Pinning a revision

setProtocolVersion() pins the handshake to exactly one revision instead of negotiating across the supported set. The pin wins over the client's request, so a client asking for anything else receives the pinned revision as a counter-offer and has to decide whether to continue. Leave it unset unless you have a reason to refuse other revisions.

Note

On the Streamable HTTP transport, every request after the handshake also carries an MCP-Protocol-Version header, which is validated separately by ProtocolVersionMiddleware. The pin does not reach that check: the transport builds the middleware without access to the server configuration, so the header keeps being accepted for every revision in ProtocolVersion::handshakeVersions(). To narrow it too, construct the middleware yourself with the same revision — see Protocol Version Validation.

Discovery Configuration

Required when using MCP attributes. If you're using PHP attributes (#[McpTool], #[McpResource], #[McpResourceTemplate], #[McpPrompt]) to define your MCP elements, you MUST configure discovery to tell the server where to look for these attributes.

$server = Server::builder()
    ->setDiscovery(
        basePath: __DIR__,
        scanDirs: ['.', 'src', 'lib'],           // Where to look for MCP attributes
        excludeDirs: ['vendor', 'tests'],        // Where NOT to look
        cache: $cacheInstance,                   // Optional: cache discovered elements
        namePatterns: ['*.php', '*.inc'],        // Optional: list of filename patterns to match
    );

Parameters:

  • $basePath (string): Base directory for discovery (typically __DIR__)
  • $scanDirs (array): Directories to recursively scan for #[McpTool], #[McpResource], etc. All subdirectories are included. (default: ['.', 'src'])
  • $excludeDirs (array): Directory names to exclude within the scanned directories during recursive scanning
  • $cache (CacheInterface|null): Optional PSR-16 cache to store discovered elements for performance
  • $namePatterns (array): Optional list of patterns (regexp, glob, or string) for file names (default: ['*.php'])

Basic Discovery (scans current directory and src/):

$server = Server::builder()
    ->setDiscovery(__DIR__)  // Minimal setup
    ->build();

Production Setup with Caching:

use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;

// Cache discovered elements to avoid filesystem scanning on every server start
$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery'));

$server = Server::builder()
    ->setDiscovery(
        basePath: __DIR__,
        scanDirs: ['src', 'lib'],                    // Scan these directories recursively
        excludeDirs: ['vendor', 'tests', 'temp'],    // Skip these directory names within scanned dirs
        cache: $cache                                // Cache for performance
    )
    ->build();

How excludeDirs works:

  • If scanning src/ and there's src/vendor/, it will be excluded
  • If scanning lib/ and there's lib/tests/, it will be excluded
  • But if vendor/ and tests/ are at the same level as src/, they're not scanned anyway (not in scanDirs)

Performance: Always use a cache in production. The first run scans and caches all discovered MCP elements, making subsequent server startups nearly instantaneous.

Session Management

Configure session storage and lifecycle. By default, the SDK uses InMemorySessionStore:

use Mcp\Server\Session\FileSessionStore;
use Mcp\Server\Session\InMemorySessionStore;
use Mcp\Server\Session\Psr16SessionStore;
use Symfony\Component\Cache\Psr16Cache;
use Symfony\Component\Cache\Adapter\RedisAdapter;

// Override with file-based storage
$server = Server::builder()
    ->setSession(new FileSessionStore(__DIR__ . '/sessions'))
    ->build();

// Override with in-memory storage and custom TTL
$server = Server::builder()
    ->setSession(new InMemorySessionStore(3600))
    ->build();

// Override with PSR-16 cache-based storage
// Requires psr/simple-cache and symfony/cache (or any other PSR-16 implementation)
// composer require psr/simple-cache symfony/cache
$redisAdapter = new RedisAdapter(
    RedisAdapter::createConnection('redis://localhost:6379'),
    'mcp_sessions'
);

$server = Server::builder()
    ->setSession(new Psr16SessionStore(
        cache: new Psr16Cache($redisAdapter),
        prefix: 'mcp-',
        ttl: 3600
    ))
    ->build();

Garbage Collection Configuration

The SDK periodically runs garbage collection to clean up expired sessions, similar to PHP's native session.gc_probability and session.gc_divisor settings. The probability that GC runs on any given request is gcProbability / gcDivisor.

// Default: 1/100 (1% chance per request)
$server = Server::builder()
    ->setSession(new FileSessionStore(__DIR__ . '/sessions'))
    ->build();

// Higher frequency: 1/10 (10% chance per request)
$server = Server::builder()
    ->setSession(
        new FileSessionStore(__DIR__ . '/sessions'),
        gcProbability: 1,
        gcDivisor: 10,
    )
    ->build();

// Run GC on every request
$server = Server::builder()
    ->setSession(gcProbability: 1, gcDivisor: 1)
    ->build();

// Disable GC entirely (e.g. when using an external cleanup process)
$server = Server::builder()
    ->setSession(gcProbability: 0)
    ->build();

Parameters:

  • $gcProbability (int): The numerator of the GC probability fraction (default: 1). Set to 0 to disable GC.
  • $gcDivisor (int): The denominator of the GC probability fraction (default: 100). Must be >= 1.

Note

: When providing a custom SessionManagerInterface via the $sessionManager parameter, the gcProbability and gcDivisor settings are ignored — you control GC behavior in your own implementation.

Available Session Stores:

  • InMemorySessionStore: Fast in-memory storage (default)
  • FileSessionStore: Persistent file-based storage
  • Psr16StoreSession: PSR-16 compliant cache-based storage

Custom Session Stores:

Implement SessionStoreInterface to create custom session storage:

use Mcp\Server\Session\SessionStoreInterface;
use Symfony\Component\Uid\Uuid;

class RedisSessionStore implements SessionStoreInterface
{
    public function __construct(private $redis, private int $ttl = 3600) {}

    public function exists(Uuid $id): bool
    {
        return $this->redis->exists($id->toRfc4122());
    }

    public function read(Uuid $sessionId): string|false
    {
        $data = $this->redis->get($sessionId->toRfc4122());
        return $data !== false ? $data : false;
    }

    public function write(Uuid $sessionId, string $data): bool
    {
        return $this->redis->setex($sessionId->toRfc4122(), $this->ttl, $data);
    }

    public function destroy(Uuid $sessionId): bool
    {
        return $this->redis->del($sessionId->toRfc4122()) > 0;
    }

    public function gc(): array
    {
        // Redis handles TTL automatically
        return [];
    }
}

Manual Capability Registration

Register MCP elements programmatically without using attributes. The handler is the most important parameter and can be any PHP callable.

Handler Types

Handler can be any PHP callable:

  1. Closure: function(int $a, int $b): int { return $a + $b; }
  2. Class and method name pair: [ClassName::class, 'methodName'] - the class is instantiated lazily on first call, so it must be constructable through the container (or have a no-arg constructor)
  3. Class instance and method name: [$instance, 'methodName'] - the given, already-constructed object is invoked as-is. Use this for handlers the container cannot build, e.g. those with scalar constructor arguments or dependencies wired at runtime
  4. Invokable class name: InvokableClass::class - class must be constructable through the container and have __invoke method

Manual Tool Registration

$server = Server::builder()
    // Using closure
    ->addTool(
        handler: function(int $a, int $b): int { return $a + $b; },
        name: 'add_numbers',
        description: 'Adds two numbers together'
    )

    // Using class method pair
    ->addTool(
        handler: [Calculator::class, 'multiply'],
        name: 'multiply_numbers'
        // name and description are optional - derived from method name and docblock
    )

    // Using instance method
    ->addTool(
        handler: [$calculatorInstance, 'divide']
    )

    // Using invokable class
    ->addTool(
        handler: InvokableCalculator::class
    );

Parameters

  • handler (callable|string): The tool handler
  • name (string|null): Optional tool name
  • title (string|null): Optional human-readable title for display in UI
  • description (string|null): Optional tool description
  • annotations (ToolAnnotations|null): Optional annotations for the tool
  • inputSchema (array|null): Optional input schema for the tool
  • icons (Icon[]|null): Optional array of icons for the tool
  • meta (array|null): Optional metadata for the tool

Manual Resource Registration

Register static resources:

$server = Server::builder()
    ->addResource(
        handler: [Config::class, 'getSettings'],
        uri: 'config://app/settings',
        name: 'app_config',
        description: 'Application configuration',
        mimeType: 'application/json'
    );

Parameters

  • handler (callable|string): The resource handler
  • uri (string): The resource URI
  • name (string|null): Optional resource name
  • description (string|null): Optional resource description
  • mimeType (string|null): Optional MIME type of the resource
  • size (int|null): Optional size of the resource in bytes
  • annotations (Annotations|null): Optional annotations for the resource
  • icons (Icon[]|null): Optional array of icons for the resource
  • meta (array|null): Optional metadata for the resource

Manual Resource Template Registration

Register dynamic resources with URI templates:

$server = Server::builder()
    ->addResourceTemplate(
        handler: [UserService::class, 'getUserProfile'],
        uriTemplate: 'user://{userId}/profile',
        name: 'user_profile',
        description: 'User profile by ID',
        mimeType: 'application/json'
    );

Parameters

  • handler (callable|string): The resource template handler
  • uriTemplate (string): The resource URI template
  • name (string|null): Optional resource template name
  • description (string|null): Optional resource template description
  • mimeType (string|null): Optional MIME type of the resource
  • annotations (Annotations|null): Optional annotations for the resource template

Manual Prompt Registration

Register prompt generators:

$server = Server::builder()
    ->addPrompt(
        handler: [PromptService::class, 'generatePrompt'],
        name: 'custom_prompt',
        description: 'A custom prompt generator'
    );

Parameters

  • handler (callable|string): The prompt handler
  • name (string|null): Optional prompt name
  • title (string|null): Optional human-readable title for display in UI
  • description (string|null): Optional prompt description
  • icons (Icon[]|null): Optional array of icons for the prompt

Note: name and description are optional for all manual registrations. If not provided, they will be derived from the handler's method name and docblock.

For more details on MCP elements, handlers, and attribute-based discovery, see MCP Elements.

Explicit element registration

When an element's name, schema, or description is only known at runtime, pair an Mcp\Schema\* value object with one of the four handler interfaces below and register it through Builder::add().

Element kind Handler interface
Tool Mcp\Server\Handler\ToolHandlerInterface
Resource Mcp\Server\Handler\ResourceHandlerInterface
Resource template Mcp\Server\Handler\ResourceTemplateHandlerInterface
Prompt Mcp\Server\Handler\PromptHandlerInterface

Each handler interface declares a single execution method. Tool and prompt handlers receive an arguments map and a ClientGateway. Resource handlers receive the requested URI; resource template handlers additionally receive the parsed template variables.

use Mcp\Schema\Tool;
use Mcp\Server;
use Mcp\Server\ClientGateway;
use Mcp\Server\Handler\ToolHandlerInterface;

final class WeatherHandler implements ToolHandlerInterface
{
    public function execute(array $arguments, ClientGateway $gateway): mixed
    {
        return ['temperature' => 21, 'unit' => 'C'];
    }
}

$tool = new Tool(
    name: 'get_weather',
    title: null,
    inputSchema: [
        'type' => 'object',
        'properties' => ['city' => ['type' => 'string']],
        'required' => ['city'],
    ],
    description: 'Returns the current weather for a city.',
    annotations: null,
);

$server = Server::builder()
    ->add($tool, new WeatherHandler())
    ->build();

Builder::add() validates the pairing at registration time. Pairing a Tool definition with, for example, a PromptHandlerInterface raises Mcp\Exception\InvalidArgumentException. The schema value object validates its own inputs (name pattern, schema shape, etc.), so passing an incomplete definition fails before add() returns.

Use add() when the metadata cannot be inferred from a handler class via reflection. For statically-known elements, prefer addTool/addResource/addResourceTemplate/addPrompt, which can derive metadata from the handler's signature and docblock.

Service Dependencies

Container

The container is used to resolve handlers and their dependencies when handlers inject dependencies in their constructors. The SDK includes a basic container with simple auto-wiring capabilities.

use Mcp\Capability\Registry\Container;

// Use the default basic container
$container = new Container();
$container->set(DatabaseService::class, new DatabaseService($pdo));
$container->set(\PDO::class, $pdo);

$server = Server::builder()
    ->setContainer($container)
    ->build();

Basic Container Features:

  • Supports constructor auto-wiring for classes with parameterless constructors
  • Resolves dependencies where all parameters are type-hinted classes/interfaces known to the container
  • Supports parameters with default values
  • Does NOT support scalar/built-in type injection without defaults
  • Detects circular dependencies

You can also use any PSR-11 compatible container (Symfony DI, PHP-DI, Laravel Container, etc.).

Logger

Provide a PSR-3 logger instance for internal server logging (request/response processing, errors, session management, transport events):

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$logger = new Logger('mcp-server');
$logger->pushHandler(new StreamHandler('mcp.log', Logger::INFO));

$server = Server::builder()
    ->setLogger($logger);

Event Dispatcher

Configure event dispatching:

$server = Server::builder()
    ->setEventDispatcher($eventDispatcher);

Custom Message Handlers

Low-level escape hatch. Custom message handlers run before the SDK's built-in handlers and give you total control over individual JSON-RPC messages. They do not receive the builder's registry, container, or discovery output unless you pass those dependencies in yourself.

Warning

: Custom message handlers bypass discovery, manual capability registration, and container lookups (unless you explicitly pass them). Tools, resources, and prompts you register elsewhere will not show up unless your handler loads and executes them manually. Reach for this API only when you need that level of control and are comfortable taking on the additional plumbing.

Request Handlers

Handle JSON-RPC requests (messages with an id that expect a response). Request handlers must return either a Response or an Error object.

Attach request handlers with addRequestHandler() (single) or addRequestHandlers() (multiple). You can call these methods as many times as needed; each call prepends the handlers so they execute before the defaults:

$server = Server::builder()
    ->addRequestHandler(new CustomListToolsHandler())
    ->addRequestHandlers([
        new CustomCallToolHandler(),
        new CustomGetPromptHandler(),
    ])
    ->build();

Request handlers implement RequestHandlerInterface:

use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Server\Handler\Request\RequestHandlerInterface;
use Mcp\Server\Session\SessionInterface;

interface RequestHandlerInterface
{
    public function supports(Request $request): bool;

    public function handle(Request $request, SessionInterface $session): Response|Error;
}
  • supports() decides if the handler should process the incoming request
  • handle() must return a Response (on success) or an Error (on failure)

Notification Handlers

Handle JSON-RPC notifications (messages without an id that don't expect a response). Notification handlers do not return anything - they perform side effects only.

Attach notification handlers with addNotificationHandler() (single) or addNotificationHandlers() (multiple):

$server = Server::builder()
    ->addNotificationHandler(new LoggingNotificationHandler())
    ->addNotificationHandlers([
        new InitializedNotificationHandler(),
        new ProgressNotificationHandler(),
    ])
    ->build();

Notification handlers implement NotificationHandlerInterface:

use Mcp\Schema\JsonRpc\Notification;
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
use Mcp\Server\Session\SessionInterface;

interface NotificationHandlerInterface
{
    public function supports(Notification $notification): bool;

    public function handle(Notification $notification, SessionInterface $session): void;
}
  • supports() decides if the handler should process the incoming notification
  • handle() performs side effects but does not return a value (notifications have no response)

Key Differences

Handler Type Interface Returns Use Case
Request Handler RequestHandlerInterface Response|Error Handle requests that need responses (e.g., tools/list, tools/call)
Notification Handler NotificationHandlerInterface void Handle fire-and-forget notifications (e.g., notifications/initialized, notifications/progress)

Example

Check out examples/custom-method-handlers/server.php for a complete example showing how to implement custom tools/list and tools/call request handlers independently of the registry.

Complete Example

Here's a comprehensive example showing all major configuration options:

use Mcp\Server;
use Mcp\Server\Session\FileSessionStore;
use Mcp\Capability\Registry\Container;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

// Setup dependencies
$logger = new Logger('mcp-server');
$logger->pushHandler(new StreamHandler('mcp.log', Logger::INFO));

$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery'));
$sessionStore = new FileSessionStore(__DIR__ . '/sessions');

// Setup container with dependencies
$container = new Container();
$container->set(\PDO::class, new \PDO('sqlite::memory:'));
$container->set(DatabaseService::class, new DatabaseService($container->get(\PDO::class)));

// Build server
$server = Server::builder()
    // Server identity
    ->setServerInfo('Advanced Calculator', '2.1.0')

    // Performance and behavior
    ->setPaginationLimit(100)
    ->setInstructions('Use calculate tool for math operations. Check config resource for current settings.')

    // Discovery with caching
    ->setDiscovery(__DIR__, ['src'], ['vendor', 'tests'], $cache)

    // Session management
    ->setSession($sessionStore)

    // Services
    ->setLogger($logger)
    ->setContainer($container)

    // Manual capability registration
    ->addTool([Calculator::class, 'advancedCalculation'], 'advanced_calc')
    ->addResource([Config::class, 'getSettings'], 'config://app/settings', 'app_settings')

    // Build the server
    ->build();

Method Reference

Method Parameters Description
setServerInfo() name, version, description? Set server identity
setPaginationLimit() limit Set max items per page
setInstructions() instructions Set usage instructions
setProtocolVersion() protocolVersion Pin the handshake to one protocol revision
setDiscovery() basePath, scanDirs?, excludeDirs?, cache? Configure attribute discovery
setSession() sessionStore?, sessionManager?, gcProbability?, gcDivisor? Configure session management
setLogger() logger Set PSR-3 logger
setContainer() container Set PSR-11 container
setEventDispatcher() dispatcher Set PSR-14 event dispatcher
addRequestHandler() handler Prepend a single custom request handler
addRequestHandlers() handlers Prepend multiple custom request handlers
addNotificationHandler() handler Prepend a single custom notification handler
addNotificationHandlers() handlers Prepend multiple custom notification handlers
addTool() handler, name?, title?, description?, annotations?, inputSchema?, ... Register tool
addResource() handler, uri, name?, title?, description?, mimeType?, size?, annotations?, icons?, meta? Register resource
addResourceTemplate() handler, uriTemplate, name?, title?, description?, mimeType?, annotations?, meta? Register resource template
addPrompt() handler, name?, title?, description?, icons?, meta? Register prompt
add() definition, handler Register an element from a schema VO + handler pair
build() - Create the server instance