php-mcp/schema
A PHP package for defining and working with Model Context Protocol (MCP) schemas. It provides typed schema objects and helpers for describing tools, resources, and prompts, making it easier to build and validate MCP servers cleanly in PHP applications.
Install the schema package alongside whatever MCP transport/server layer you are building:
composer require php-mcp/schema
The package is a pure data layer: DTO classes and backed enums that mirror the Model Context Protocol spec. There is no runtime server or client inside it. Look at these first:
src/ — top-level message DTOs (initialize request/response, tool/resource/prompt definitions, results, JSON-RPC envelopes).Role (user/assistant), content Type (text/image/resource), and logging levels. These are the values you should never hard-code as strings.composer.json — confirm the minimum PHP version and whether it pulls in a serializer. Most schema-only packages have zero runtime dependencies, which is the main reason to use them.First use case: describing a tool and returning a tools/list style result.
use PhpMcp\Schema\Tool;
use PhpMcp\Schema\Content\TextContent;
use PhpMcp\Schema\Result\CallToolResult;
$tool = new Tool(
name: 'get_weather',
description: 'Look up current weather for a city',
inputSchema: [
'type' => 'object',
'properties' => ['city' => ['type' => 'string']],
'required' => ['city'],
],
);
$result = new CallToolResult(
content: [new TextContent(type: Type::Text, text: '22°C and clear')],
isError: false,
);
echo json_encode($result, JSON_THROW_ON_ERROR);
If json_encode produces the structure from the spec examples, the package is wired correctly and you can build on it.
Use DTOs as your tool/resource registry. Register each MCP capability as a schema DTO in a Laravel service provider, then let your server loop read the registry instead of hand-building arrays in every endpoint:
// AppServiceProvider
$this->app->singleton(ToolRegistry::class, fn () => (new ToolRegistry())
->add(new Tool(name: 'search', description: '...', inputSchema: [...]))
->add(new Tool(name: 'fetch', description: '...', inputSchema: [...])));
Hydrate incoming JSON-RPC into DTOs at the edge. Decode the request once, switch on the method, and construct the typed message. Keep controller actions thin and push mapping into a small factory:
$payload = json_decode($request->getContent(), true, flags: JSON_THROW_ON_ERROR);
$toolCall = match ($payload['method']) {
'tools/call' => CallToolRequest::fromArray($payload['params']),
'resources/read' => ReadResourceRequest::fromArray($payload['params']),
default => throw new InvalidArgumentException('Unsupported method'),
};
If the package does not ship a fromArray, keep a local mapper class; it is a handful of lines and gives you one place to absorb spec changes.
Serialize with the enum's backed value, not the enum itself. In hand-written serializers, do $content['type'] = $type->value. Where the DTO implements JsonSerializable, delegate to json_encode($dto) so enum handling stays consistent across every endpoint.
Compose results rather than mutating them. CallToolResult should hold an immutable array of content parts; build new instances per call. This keeps long-running workers (Octane, queue workers, a php artisan mcp:serve loop) from leaking state between requests.
Test against fixtures from the spec. Store canonical request/response JSON in tests/Fixtures, hydrate to a DTO, re-serialize, and assert array equality. This catches casing and nullability regressions when the package updates.
null vs omitted. The spec distinguishes absent optional fields from explicit nulls. Avoid blanket array_filter on serialized output; filter only keys you know are optional, otherwise you will silently drop legitimate false and 0 values.Content is effectively a union keyed on type. A generic recursive hydrator will not choose TextContent vs ImageContent correctly; switch on the type enum before constructing.jsonrpc, protocolVersion, and inputSchema are not snake_case. Verify the DTO's serialization once with print_r(json_decode(json_encode($dto), true)) and build a fixture around it.json_encode. A bare enum encodes as its name, not its value, in older PHP serialization contexts. Always convert with ->value or rely on DTOs that implement JsonSerializable. Prefer Role::User->value over (string) Role::User for clarity."php-mcp/schema": "^1.0") and read the changelog before upgrading. Add a schema_version constant to your own code so mismatches surface early.inputSchema and tool arguments with Laravel's validator or a JSON-Schema library before invoking handlers.composer.json require.php against your app; readonly properties and enums need 8.1+, and named arguments in examples need 8.0+.final or have no extension hooks, do not subclass. Wrap them in your own application-level value objects instead, and keep the schema DTOs at the boundary.json_encode($dto) and the raw wire bytes. Whitespace, trailing commas, and numeric vs string IDs ("1" vs 1) are frequent culprits, and the DTO layer cannot fix an ID type you set incorrectly.How can I help you explore Laravel packages today?