php-sdk/docs/examples.md
Christopher Hertel e0b9c1a26f
[Server] Serve ClientGateway::elicit() under the modern lifecycle (#466)
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.
2026-08-20 01:25:59 +02:00

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.php will not complete under a plain php -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.