Every tool, resource, resource template, and prompt has to reach the server's
registry somehow. There are three ways to get it there, and they mix freely: attribute
discovery, manual registration with addTool() and its siblings, and explicit element
registration with Builder::add().
Advantages:
- Declarative and readable
- Automatic parameter inference
- DocBlock integration
- Type-safe by default
- Caching support
Example:
use Mcp\Server\Server;
$server = Server::builder()
->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor']) // Automatic discovery
->build();!!! warning Discovery registers only the first class in a file and ignores the others. Put each class with MCP attributes in its own file.
Register MCP elements programmatically without using attributes. The handler is the most important parameter. See Handler Types for the accepted forms.
Advantages:
- Fine-grained control
- Runtime configuration
- Conditional registration
- External handler support
Example:
$server = Server::builder()
->addTool([Calculator::class, 'add'], 'add_numbers')
->addResource([Config::class, 'get'], 'config://app')
->addPrompt([Prompts::class, 'email'], 'write_email')
->build();Handler can be one of the following forms. Other callables, such as a function name, a 'Class::method' string or
an invokable object, fail when the elements load:
- Closure:
function(int $a, int $b): int { return $a + $b; }, or a first-class callable such as$service->add(...) - 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) - 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 - Invokable class name:
InvokableClass::class- class must be constructable through the container and have__invokemethod
!!! note
A custom loader implements Mcp\Server\Capability\Registry\Loader\LoaderInterface and is added with Builder::addLoader(). It can register an element with the id of a callable service from your container as the handler (e.g. app.weather_handler).
`addTool()`, `addResource()`, `addResourceTemplate()` and `addPrompt()` don't accept service ids. A bad handler fails when the elements load, by default on the first registry read. The SDK logs a `ConfigurationException` and answers that request with `-32603` `Internal server error.`. With `setLazyLoading(false)`, `build()` throws the exception.
$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
)
->build();handler(\Closure|array|string): The tool handlername(string|null): Optional tool nametitle(string|null): Optional human-readable title for display in UIdescription(string|null): Optional tool descriptionannotations(ToolAnnotations|null): Optional annotations for the toolinputSchema(array|null): Optional input schema for the toolicons(Icon[]|null): Optional array of icons for the toolmeta(array|null): Optional metadata for the tooloutputSchema(array|null): Optional JSON schema describing the tool'sstructuredContent
Register static resources:
$server = Server::builder()
->addResource(
handler: [Config::class, 'getSettings'],
uri: 'config://app/settings',
name: 'app_config',
description: 'Application configuration',
mimeType: 'application/json'
)
->build();handler(\Closure|array|string): The resource handleruri(string): The resource URIname(string|null): Optional resource nametitle(string|null): Optional human-readable title for display in UIdescription(string|null): Optional resource descriptionmimeType(string|null): Optional MIME type of the resourcesize(int|null): Optional size of the resource in bytesannotations(Annotations|null): Optional annotations for the resourceicons(Icon[]|null): Optional array of icons for the resourcemeta(array|null): Optional metadata for the resource
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'
)
->build();handler(\Closure|array|string): The resource template handleruriTemplate(string): The resource URI templatename(string|null): Optional resource template nametitle(string|null): Optional human-readable title for display in UIdescription(string|null): Optional resource template descriptionmimeType(string|null): Optional MIME type of the resourceannotations(Annotations|null): Optional annotations for the resource templatemeta(array|null): Optional metadata for the resource template
Register prompt generators:
$server = Server::builder()
->addPrompt(
handler: [PromptService::class, 'generatePrompt'],
name: 'custom_prompt',
description: 'A custom prompt generator'
)
->build();handler(\Closure|array|string): The prompt handlername(string|null): Optional prompt nametitle(string|null): Optional human-readable title for display in UIdescription(string|null): Optional prompt descriptionicons(Icon[]|null): Optional array of icons for the promptmeta(array|null): Optional metadata for the prompt
Note: name and description are optional when the handler is a method or an invokable class. The name defaults to
the method name, or to the class short name for an invokable class. The description comes from the docblock.
A closure handler gets a generated name (closure_tool_<id>, closure_resource_<id>, closure_template_<id> or
closure_prompt_<id>) and no description. Name your closures explicitly.
For more details on the elements themselves, see Tools, Resources, Resource templates, and Prompts.
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, and each method receives a ClientGateway as its last
argument. Before it, tool and prompt handlers receive an arguments map. Resource handlers receive the requested URI, and
resource template handlers receive the URI and the parsed template variables.
use Mcp\Schema\Tool;
use Mcp\Server\ClientGateway;
use Mcp\Server\Handler\ToolHandlerInterface;
use Mcp\Server\Server;
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\Server\Exception\InvalidArgumentException.
The schema value objects validate some of their own input as well. Tool requires an object-typed input schema.
ResourceDefinition and ResourceTemplate check the URI.
An invalid tool name is not rejected. The registry only logs a warning when you register the tool.
For Prompt and ResourceTemplate definitions, add() takes completion providers as a third argument. See
Registering providers without discovery.
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.
Combine discovery with manual registration for maximum flexibility:
$server = Server::builder()
->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor']) // Discover most capabilities
->addTool([ExternalService::class, 'process'], 'external') // Add specific ones
->build();Manual registrations and explicit elements always take precedence over discovered elements with the same identifier:
the same name for tools and prompts, the same uri for resources, the same uriTemplate for resource templates.
For runtime, config-driven elements whose shape is not known at compile time, see Explicit element registration.