wordpress/php-mcp-schema
PHP implementation of the Model Context Protocol (MCP) schema, offering lightweight, strictly typed classes, enums, and helpers for building MCP servers and clients. Use it to connect WordPress and PHP applications with MCP-compatible AI tools, agents, and services.
composer require wordpress/php-mcp-schema — v0.2.0 is a plain, dependency-free runtime. There is no service provider, no config file to publish, and nothing to register in bootstrap/providers.php.Record\ (concrete protocol objects you receive and branch on), Contract\ (object unions), Value\ (enum-like string constants for methods, error codes, roles). The old Client\, Common\, Server\ namespaces and the generated skill/ type reference no longer exist — ignore any snippet, blog post, or LLM completion that references them.2025-11-25 and 2026-07-28 exist; anything else throws UnsupportedRevisionException from forVersion(). Put it in config/mcp.php as 'revision' => env('MCP_REVISION', '2025-11-25').// App\Providers\McpServiceProvider::register()
$this->app->singleton(Schema::class, fn () => Schemas::create()->forVersion(config('mcp.revision')));
initialize then tools/call. Take the raw request body, hydrate it with fromJson() (or fromArray() off $request->all()), and branch on the resulting Record\* class. That single flow exercises the whole API surface you'll use day-to-day.validate: true flag to pass — if hydration returns, the payload conformed.POST /mcp route guarded by an Mcp\ValidateMessage middleware that hydrates the body and stashes the immutable record on the request. Controllers then type-hint the Record\ class they expect; hydration failures short-circuit to a JSON-RPC error before any tool code runs.$message = $schema->fromJson($request->getContent());
return match (true) {
$message instanceof Record\InitializeRequest => $this->initialize($message),
$message instanceof Record\ToolsCallRequest => $this->dispatchTool($message),
$message instanceof Record\ToolsListRequest => $this->listTools(),
default => $this->methodNotFound($message),
};
toArray() variants were removed with no aliases. Write one App\Mcp\Presenter (or per-record toArray() mappers) and use it everywhere outbound; keep snapshot tests on the JSON so protocol drift is caught in CI rather than by a client.config/mcp.php maps tool names to invokable classes; dispatchTool resolves from the container, validates $record->arguments against that tool's own JSON schema, and returns the result wrapped in the response record. This is the seam where Laravel DI, policies, and rate limiting enter.\InvalidArgumentException. Catch it in the middleware and emit a Record\Error with code -32602; reserve -32603 for your own handler failures. Hand-rolled error arrays still hydrate to Record\Error, so instanceof Record\Error is a reliable guard.tests/Fixtures/mcp/*.json (one folder per revision) and assert hydrate-then-present round-trips. That is the cheapest regression net for a package that regenerates its object model between releases.validate: flags, and all toArray() variants are gone with no compatibility aliases. Read docs/MIGRATION.md before touching call sites; budget this as a rewrite of the hydration layer, not a patch bump.\InvalidArgumentException is very broad. Don't catch it globally in bootstrap/app.php — scope the handler to the package's exception namespace so real TypeError-class bugs and your own argument errors don't get reported as invalid params.Record\ class. Assert instanceof at your dispatch boundary and reject unexpected extras in your own tool schemas rather than trusting the union resolver.2025-11-25 NumberSchema accepts fractional default/minimum/maximum, and 2026-07-28 JSONValue accepts fractional numbers and null. Numeric-literal getters faithfully preserve integral floats like -32700.0, so $record->code === -32700 can fail. Cast explicitly before using values as JSON-RPC error codes or array keys.memory_limit), but rejection is cheaper than validation. Cap request bodies at the web server and again in middleware so a hostile client can't reach hydration at all.INF/NAN-prone computations, sanitize on the way out rather than discovering it as a parse error.UnsupportedRevisionException is thrown by forVersion(), so resolve the schema in a health check or deploy smoke test — a typo in MCP_REVISION should surface as a failed deploy, not a 500 on the first client handshake.skill/ type lookup. With per-class @since annotations and the generated reference removed, the source tree and the changelog are now the only authority on what a field means in a given revision. Diffing Record\ between the two revision branches is the fastest way to answer "does 2025-11-25 support this?" — worth writing down for the team.How can I help you explore Laravel packages today?