One handler asks the user something on any revision now. Where the client can be asked mid-request it still is; where it cannot, the ask becomes the input_required result that revision carries and the same call returns the answer once the client re-sends it. Costs one handler entry per ask, so side effects belong after the last question. Answers from earlier rounds travel in the requestState, which is why asking more than once needs Builder::setRequestState(). sample() and listRoots() still raise a LogicException there: that revision removed them outright.
8.2 KiB
Examples
Every example lives in examples/
and runs against the dependencies installed in the repository root. Each server example is a
server.php whose bootstrap picks the transport from the SAPI it runs under:
# STDIO transport
php examples/server/discovery-calculator/server.php
# Streamable HTTP transport
php -S 127.0.0.1:8000 examples/server/discovery-userprofile/server.php
# Interactive testing with the MCP Inspector
npx @modelcontextprotocol/inspector php examples/server/discovery-calculator/server.php
Server examples
| Example | What it shows | Docs |
|---|---|---|
discovery-calculator |
Attribute discovery of tools, resources and prompts; ResourceLink content |
Tools |
discovery-userprofile |
Resource templates with completion providers; FileSessionStore |
Resource templates, Completions |
explicit-registration |
Manual addTool()/addResource()/addResourceTemplate()/addPrompt() without discovery |
Registering elements |
combined-registration |
Discovery and manual registration combined, and which wins on conflict | Registering elements |
cached-discovery |
Caching the discovery scan in a PSR-16 cache | Server builder |
custom-dependencies |
Handlers pulling services from a PSR-11 container | Server builder |
complex-tool-schema |
Rich input schemas from typed parameters, PHP enums and defaults | Schema generation |
schema-showcase |
#[Schema] constraint attributes: formats, patterns, ranges |
Schema generation |
env-variables |
Configuring a server through environment variables | Server builder |
client-communication |
Sampling, roots, progress and log messages from inside a handler | Talking back to the client |
client-logging |
Structured log notifications through the ClientLogger |
Logging |
elicitation |
Asking the user for input mid-call with ClientGateway::elicit() and typed elicitation schemas, on either protocol era |
Asking for input |
custom-method-handlers |
Registering handlers for custom JSON-RPC methods | Custom message handlers |
mcp-apps |
The MCP Apps extension: a tool that ships an interactive HTML view | Protocol extensions |
stateless-lifecycle |
Revision 2026-07-28: cache policy, request state, notification bus |
Serving both eras, Caching, Subscriptions |
oauth-keycloak |
OAuth authorization against a Keycloak instance (own README) | Authorization |
oauth-microsoft |
OAuth authorization against Microsoft Entra ID (own README) | Authorization |
Client examples
| Example | What it shows | Docs |
|---|---|---|
stdio_discovery_calculator.php |
Connecting over STDIO, listing and calling tools, reading resources | Connecting to a server |
http_discovery_calculator.php |
The same conversation over the Streamable HTTP transport | Transports |
stdio_client_communication.php |
Answering server-initiated sampling, log and progress messages | Server-initiated requests |
http_client_communication.php |
The same handlers over HTTP — see the note on PHP's built-in server below | Server-initiated requests |
stdio_elicitation.php |
Answering elicitation requests from an interactive prompt | Server-initiated requests |
stdio_roots.php |
Exposing workspace roots and signalling roots/list_changed |
Server-initiated requests |
stateless_lifecycle_client.php |
A client pinned to revision 2026-07-28 — see Modern-era client |
Connecting to a server |
Client examples run directly:
php examples/client/stdio_discovery_calculator.php
Note
round-trip in
http_client_communication.phpwill not complete under a plainphp -S. Start it with worker processes instead:PHP_CLI_SERVER_WORKERS=2 php -S 127.0.0.1:8000 ….
The 2026-07-28 lifecycle
stateless-lifecycle/server.php answers both eras on the same URL. On revision 2026-07-28
every request carries its own protocol version and client capabilities, so a call is a single
POST with no handshake before it:
php -S 127.0.0.1:8000 examples/server/stateless-lifecycle/server.php
curl -sS http://127.0.0.1:8000/ \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":{}}}}'
The example's header comments document the equivalent handshake-era conversation against the same endpoint. Serving both eras explains how the routing works.
Modern-era client
stateless_lifecycle_client.php drives the server above from PHP: it pins
ProtocolVersion::V2026_07_28, skips the handshake, discovers the server through
server/discover and calls a tool — one process, no session:
php -S 127.0.0.1:8000 examples/server/stateless-lifecycle/server.php &
php examples/client/stateless_lifecycle_client.php
See Clients on the modern revision for the API it uses.