[Docs][Examples][CI] Document and gate the 2026-07-28 lifecycle (#457)
docs/stateless-lifecycle.md walks the revision end to end - per-request _meta, server/discover, multi round-trip requests, caching and subscriptions - and a runnable example on each side shows what that looks like in code. CI matrixes both conformance roles over both revisions. --spec-version is cumulative across the dated revisions, so 2025-11-25 covers the two before it; 2026-07-28 is not cumulative with them and needs its own run against its own baseline. The runner is pinned so a PR only goes red for reasons in the PR, and conformance-weekly tracks the moving target instead - on both revisions, since the draft scenarios ship only on the alpha dist-tag. The Inspector cannot reach a modern-lifecycle server (it opens with initialize), so the example is covered by integration tests instead: one drives it with hand-built HTTP the way a conforming client would, one drives it with this SDK's client, and one drives the single endpoint from both eras.
This commit is contained in:
parent
8c06dcecc6
commit
7fe88677ad
1 changed files with 1300 additions and 6 deletions
109
examples/client/stateless_lifecycle_client.php
Normal file
109
examples/client/stateless_lifecycle_client.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Modern-era client (2026-07-28).
|
||||
*
|
||||
* The counterpart to examples/server/stateless-lifecycle. What is worth noticing
|
||||
* is how little of it is about the lifecycle: one call to setProtocolVersion()
|
||||
* and everything else is the same API as any other client.
|
||||
*
|
||||
* Underneath, that one call changes the wire completely:
|
||||
* - no `initialize` handshake — the connection is usable immediately, and
|
||||
* `server/discover` is asked only for the server's identity
|
||||
* - every request carries its own `_meta` envelope naming the revision and
|
||||
* what this client can be asked to do (SEP-2575)
|
||||
* - every POST carries `Mcp-Method`, and `Mcp-Name` where the method
|
||||
* addresses a subject, so an intermediary can route without reading the
|
||||
* body (SEP-2243)
|
||||
* - a tool that needs input is answered and retried by the client, so the
|
||||
* caller sees one call and one result (SEP-2322)
|
||||
*
|
||||
* Usage:
|
||||
* 1. Start the server: php -S 127.0.0.1:8000 examples/server/stateless-lifecycle/server.php
|
||||
* 2. Run this script: php examples/client/stateless_lifecycle_client.php
|
||||
*/
|
||||
|
||||
require_once __DIR__.'/../../vendor/autoload.php';
|
||||
|
||||
use Mcp\Client;
|
||||
use Mcp\Client\Handler\Request\RequestHandlerInterface;
|
||||
use Mcp\Client\Transport\HttpTransport;
|
||||
use Mcp\Schema\ClientCapabilities;
|
||||
use Mcp\Schema\Content\TextContent;
|
||||
use Mcp\Schema\Enum\ElicitAction;
|
||||
use Mcp\Schema\Enum\ProtocolVersion;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ElicitRequest;
|
||||
use Mcp\Schema\Result\CallToolResult;
|
||||
use Mcp\Schema\Result\ElicitResult;
|
||||
|
||||
/**
|
||||
* Answers the server's request for input.
|
||||
*
|
||||
* In this revision the server cannot interrupt a call to ask — it returns the
|
||||
* question as the result. The handler is the same shape either way, which is
|
||||
* the point: a client written for the handshake era keeps working.
|
||||
*/
|
||||
$answerWithAName = new class implements RequestHandlerInterface {
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
return $request instanceof ElicitRequest;
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
assert($request instanceof ElicitRequest);
|
||||
|
||||
echo " server asked: {$request->message}\n";
|
||||
|
||||
return new Response($request->getId(), new ElicitResult(ElicitAction::Accept, ['name' => 'Ada']));
|
||||
}
|
||||
};
|
||||
|
||||
$client = Client::builder()
|
||||
->setClientInfo('stateless-example-client', '1.0.0')
|
||||
// The only line that selects the modern lifecycle.
|
||||
->setProtocolVersion(ProtocolVersion::V2026_07_28)
|
||||
// Declared in the envelope of every request, so the server knows what it
|
||||
// may ask for before it decides how to answer.
|
||||
->setCapabilities(new ClientCapabilities(elicitation: true))
|
||||
->addRequestHandler($answerWithAName)
|
||||
->build();
|
||||
|
||||
$client->connect(new HttpTransport('http://127.0.0.1:8000/'));
|
||||
|
||||
printf("Connected to %s (revision %s)\n\n", $client->getServerInfo()?->name, $client->getProtocolVersion()?->value);
|
||||
|
||||
echo "Tools:\n";
|
||||
foreach ($client->listTools()->tools as $tool) {
|
||||
printf(" %-12s %s\n", $tool->name, $tool->description ?? '');
|
||||
}
|
||||
|
||||
echo "\nA plain call:\n";
|
||||
echo ' '.text($client->callTool('get_weather', ['city' => 'Munich']))."\n";
|
||||
|
||||
echo "\nA call the server cannot finish in one round:\n";
|
||||
// One call from here. Two on the wire: the server returns its question, the
|
||||
// handler above answers it, and the client retries carrying both the answer and
|
||||
// the server's sealed `requestState`.
|
||||
echo ' '.text($client->callTool('greet', []))."\n";
|
||||
|
||||
$client->disconnect();
|
||||
|
||||
/** The first block of text in a tool result. */
|
||||
function text(CallToolResult $result): string
|
||||
{
|
||||
$first = $result->content[0] ?? null;
|
||||
|
||||
return $first instanceof TextContent ? $first->text : '(no text)';
|
||||
}
|
||||
Loading…
Reference in a new issue