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.
Advantages:
- Declarative and readable
- Automatic parameter inference
- DocBlock integration
- Type-safe by default
- Caching support
Example:
$server = Server::builder()
->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor']) // Automatic discovery
->build();Register MCP elements programmatically without using attributes. The handler is the most important parameter and can be any PHP callable.
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 any PHP callable:
- Closure:
function(int $a, int $b): int { return $a + $b; } - 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
$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
);handler(callable|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'
);handler(callable|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'
);handler(callable|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'
);handler(callable|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 — they are then
derived from the method name and its docblock. A closure handler has neither, so it gets a generated name
(closure_tool_<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. 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 objects validate some of
their own input as well — Tool requires an object-typed input schema, ResourceDefinition and ResourceTemplate
check the name pattern and URI — but an invalid tool or prompt name is not rejected, it is only logged as a warning
when the element is registered.
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 both methods for maximum flexibility:
$server = Server::builder()
->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor']) // Discover most capabilities
->addTool([ExternalService::class, 'process'], 'external') // Add specific ones
->build();Manual registrations always take precedence over discovered elements with the same identifier — same name for tools
and prompts, same uri for resources, same uriTemplate for resource templates.
For runtime, config-driven elements whose shape is not known at compile time, see Explicit element registration.