* [Docs] Render guides with Zensical, deploy via GitHub Pages actions
phpDocumentor's guide renderer copies the markdown through mostly verbatim:
relative links between pages keep pointing at `*.md` targets that do not exist
in the built site, and nothing validates them, so the published guides are full
of dead links.
Zensical (the Material for MkDocs team's successor to MkDocs) resolves internal
links against the page tree and fails the build on a broken one. `zensical
build --strict` already found four dead links on the first run — three
repo-relative links escaping docs/ (fixed to point at GitHub) and one wrong
in-page anchor.
phpDocumentor stays on for the class-level API reference only; `make docs`
builds the guides into site/ and mounts the reference at site/api/, so its two
header links back to the guides now target the site root.
Deployment moves from pushing a gh-pages branch to the official GitHub Pages
actions, and from release-only to every push on main, with pull requests
building (but not deploying) so a broken link fails review instead of the site.
NOTE: this needs the repository's Pages source switched to "GitHub Actions"
(Settings -> Pages) once.
* [Docs] Adopt the Python SDK's documentation look and feel
The docs site now uses the same theme configuration as
https://py.sdk.modelcontextprotocol.io/ so the language SDKs read as one set
of docs: the MCP mark as logo and favicon, Inter/JetBrains Mono, the
black/slate palette with a three-way (system/light/dark) toggle, instant
navigation, code copy/annotate, and a right-hand table of contents.
The markdown extension set is widened to the same list (tabbed blocks,
task lists, footnotes, emoji/icons, mermaid fences), which the content
restructure builds on.
Styling is otherwise stock: no custom stylesheet, and Zensical's own
`modern` theme variant, pinned explicitly rather than left to the default.
The one departure is code highlighting, which is broken out of the box here:
Pygments only highlights PHP after a `<?php` tag, so the guides — whose code
blocks are almost all fragments — rendered as flat plain text. The `php`
lexer is extended with `startinline`, and complete-file blocks use a
`php-file` lexer that keeps the literal open tag highlighted.
* [Docs] Restructure the guides into task-oriented sections
The guides were ten flat pages, each opening with a hand-maintained table of
contents and each mixing several audiences: `mcp-elements.md` covered tools,
prompts, schema generation and handler-side logging, `transports.md` covered
both transports plus framework integration, and `server-builder.md` covered
configuration, sessions and custom message handlers.
They are now split along the same lines as the Python SDK's documentation
(https://py.sdk.modelcontextprotocol.io/), one topic per page:
Get started installation, first server, the Inspector
Servers tools, resources, resource templates, prompts,
completions, schema generation, registration
Inside your handler the ClientGateway, logging
Running your server builder, STDIO, HTTP, framework integration,
sessions, authorization
Clients connecting, transports, capabilities, server-initiated
requests, error handling
Advanced events, protocol extensions, custom message handlers
Prose is carried over as-is apart from the seams; what is new is the landing
page and one index page per section, which say what the section is for and
where to go next, so no page is a dead end.
The per-page "Table of Contents" lists are gone — the theme renders one from
the headings — and GitHub's `> [!IMPORTANT]` blockquotes became admonitions,
which Zensical renders as callouts rather than plain quotes.
Every code sample and factual claim was then checked against src/ and the
runnable examples, which turned up long-standing errors in the carried-over
prose. Samples that could not run: a prompt using a `system` role (MCP has
only user/assistant), `Mcp\Schema\PromptMessage` (it is under `Schema\Content`)
constructed with an array instead of a single content object,
`Mcp\Capability\Prompt\Completion\ProviderInterface` (it is
`Mcp\Capability\Completion\ProviderInterface`), `new EmbeddedResource(type:,
resource: [...])` (neither parameter exists), a `: resource` return type
(not a PHP type), `#[McpResource]` with a `{path}` variable (that is a
template), a stray quote in the builder example, a `middlewares:` argument
(it is `middleware:`), and `getRequest()->getAttribute()` in the OAuth guide
(no such method — the values arrive on the request meta). Claims corrected:
sampling's `system_prompt` option (it is `systemPrompt`), `SampleMessage`
(it is `SamplingMessage`), `Notification` called an interface (abstract
class), "parameter order matters" for URI templates (bound by name), full
RFC 6570 support (only simple `{var}`), the tool description fallback chain,
`void` returning empty content, a non-zero STDIO exit code, `ErrorEvent`
being null for parse errors, handlers being "prepended", `Psr16StoreSession`,
and PSR-3 log context being sent to the client (it is dropped). Sixteen
`examples/` paths were missing their `server/` segment.
README, the OAuth ADR and one source comment now point at the published site
instead of at markdown files that moved.
* [Docs] Run the docs workflow unfiltered, keep README links relative
The `paths:` filters were mostly noise: `src/**` had to be in the list for
the phpDocumentor build, which meant the workflow ran on nearly every PR
anyway. They also missed two real inputs — `.phpdoc/template/**` and
`composer.lock` — so a template tweak or a phpDocumentor bump could change
the rendered site without triggering a build.
README links go back to relative repo paths. The README is not part of the
Zensical site (`docs_dir` is `docs/`), so those links are only ever resolved
by GitHub and Packagist, where absolute URLs break in-repo navigation for no
gain. The site pointer and the generated API reference stay absolute.
* [Docs] Adopt the doc changes that landed on main into the new structure
Twelve commits landed while the restructure was open, seven of them touching
the flat guides this branch replaces. Renames merged on their own; the three
files that were split apart needed their new sections placed by hand:
- Connection retries, protocol version negotiation, sampling with tools and
roots from `client.md` into `client/connecting.md` and
`client/server-requests.md`
- `ResourceLink` and structured output from `mcp-elements.md` into
`servers/tools.md`
- Protocol version negotiation from `server-builder.md` into
`run/server-builder.md`
The `setMaxRetries()` note claiming the value is never acted on is gone —
#413 implemented it. Callouts became admonitions and cross-links were
repointed at the new paths, as everywhere else in this branch.
* [Docs] Document the elicitation and roots client examples
Both were missing from the examples guide: the elicitation client predates
this branch, the roots client arrived with #395.
* [Docs] Fold the 2026-07-28 lifecycle into the new structure
Main's stateless-lifecycle.md becomes a docs/lifecycle/ section, and the pages
main touched — deprecations, the middleware split, the client's modern era —
land where the restructure moved them.
* [Docs] Link the Inspector to its documentation, not its repo
* [Docs] Distribute the 2026-07-28 material into the task sections
The Python and TypeScript SDKs file each feature where the task lives and keep
only an era comparison on its own page. Follow them: input-required moves next
to the other handler concerns, caching, subscriptions and era routing to
"Running your server", and one Protocol versions page carries the rest.
* [Docs] Move protocol version negotiation onto the Protocol versions page
The builder page keeps the setProtocolVersion() knob and links out; how a
revision is agreed now sits beside the era it belongs to.
* [Docs] Slim the README to a funnel, turn Examples into an index
* [Docs] Fix the code samples and API listings flagged by the audit
* [Docs] Link the negotiation spec section at latest, not draft
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.
- Rename conformance-baseline.yml to conformance-baseline-2025-11-25.yml
so both baseline files carry their spec revision symmetrically, and
bring conformance-server/conformance-client up to the same rigor
(--suite all, pinned CONFORMANCE, baseline check) as the draft targets
- score.php now takes a required spec-version and writes
<suite>-conformance-<spec-version>.json, so 2025-11-25 and 2026-07-28
each get their own badge instead of only the dated one
- conformance-weekly.yaml generates and publishes badges for both
matrix entries instead of gating on 2025-11-25 only, and drops the
now-stale unversioned badge files from the badges branch
- README shows all four badges alongside the existing packagist/CI/PHP
version/license row
- Trimmed workflow comments to the minimum needed
* [CI] Matrix the conformance jobs over both spec revisions
Runs server/client conformance against both 2025-11-25 and 2026-07-28,
each against its own endpoint and baseline. PR pipeline pins an exact
conformance package version; conformance-weekly tracks the moving
target on both dist-tags.
* [CI] Run the draft conformance suite from the alpha package
The conformance repo now ships 2026-07-28 scenarios on its alpha
dist-tag, so the draft targets no longer need a local checkout.
Pinned to the same version the pipeline runs.
* [CI] Baseline the failures --suite all newly exposes
The old workflow ran without --suite all/--spec-version, so several
pre-existing gaps never surfaced in CI. Now that both revisions run
the full suite:
- server: json-schema-2020-12 and resources-templates-read are
fixture bugs shared by both revisions; 2026-07-28 also needs
resources/subscribe and resources/unsubscribe declared removed
under the stateless wire, which the server doesn't yet do
- client: the example client doesn't call add_numbers or echo back
a tool's inputSchema; against 2026-07-28 it doesn't speak the
lifecycle or OAuth at all yet, so every such scenario fails
Verified locally: all four conformance jobs (server/client x
2025-11-25/2026-07-28) pass against these baselines.
The revision has no way for a server to put a request on a response stream,
so a handler that needs input from the user says so in its result instead: a
handler returning InputRequiredResult comes back as
resultType: "input_required" with the asks and an opaque requestState, and
the client retries the same request carrying inputResponses.
RequestContext::getInputContext() is what the handler reads them through.
RequestStateCodec signs and time-bounds the state, so a tampered or expired
one is refused rather than trusted - the state travels through the client,
which is not a place to keep something the server later believes.
* [Server][Tests] Serve the modern (SEP-2575) lifecycle
The revision removed the initialize handshake: every request carries its own
_meta - protocol version, client capabilities, client info - and answers
server/discover for what initialize used to return. There is no session to
keep, so there is nothing for the handshake-era dispatcher to do.
Builder::buildStateless() produces StatelessProtocol, and
StatelessHttpTransport serves it over one POST-only endpoint - GET and
DELETE are 405, and no Mcp-Session-Id exists to hand out.
Results go through a per-era wire codec rather than the handlers, so
resultType and the caching hints are stamped for 2026-07-28 and left off the
revisions that do not define them.
Covered by the modern conformance suite (make conformance-draft-server)
against its own baseline; unit coverage arrives with the fixes stacked on
this.
* Apply suggestions from code review
Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
* Fix critical stateless-lifecycle bugs from review
- Don't leak internal exception messages to clients on uncaught
\Throwable; use a generic message like the handshake-era protocol
and every built-in handler already do.
- Default a missing/invalid request id to null instead of "", and
stop answering notifications and malformed messages as if they
were "method not found" 404s.
- Read the POST body in a loop instead of a single read() call,
which PSR-7 allows to return short.
* [Tests] Introduce client-server integration tests
* Add an in-process loopback that connects a real client transport to a real
server transport, draining both Fiber loops in one synchronous pass instead
of polling. A stalled exchange fails where it happened rather than hanging.
* Cover protocol version negotiation across both implementations, where the
unit tests only ever drive one side against a canned counter-offer.
* Cover the server-to-client round-trips end to end: elicitation, sampling,
roots, plus progress and logging notifications.
* Add the `integration` test suite and a `make integration-tests` target.
* [Tests] Run the integration suite in CI
The unit job pins `--testsuite=unit`, so the new suite would never have run on
GitHub. It goes in that job rather than its own: the loopback leans on Fibers,
and the matrix is what covers them down to the PHP 8.1 floor.
* [Tests] Run integration tests against a real server process
Replaces the in-process loopback with the setup from examples/client: the
client spawns a fixture server over the real StdioTransport, so the tests
exercise the shipped transports and no longer nest both sides' Fibers.
Moves the suite into its own CI job, matrixed over PHP 8.1-8.5.
* Add API documentation generation with phpDocumentor
- Add phpDocumentor configuration and `docs` Makefile target
- Generate docs during CI to catch errors before releases
- Set up GitHub Pages with Jekyll for hosting generated docs
- Add GitHub Actions workflow to deploy docs to GitHub Pages on releases
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix phpDocumentor errors in type annotations
Change class-string union types to use phpDocumentor-compatible syntax
and reorder docblock tags so class descriptions precede `@author` tags.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix CI: pin phar-io/composer-distributor ^1.0.2
phar-io/composer-distributor 1.0.0 has a bug where it uses the wrong
package version when determining the phpDocumentor download URL,
causing CI to fail with 404 errors when using --prefer-lowest.
Version 1.0.2 fixes this issue.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Integrate markdown guides into phpDocumentor output
Configure phpDocumentor to generate both API documentation and user
guides in a single unified site. This replaces the previous Jekyll-based
approach where API docs were copied into the docs folder.
- Add `<guide>` configuration to `phpdoc.dist.xml`
- Create custom template with "Guides" navigation link
- Replace `docs/index.html` redirect with `docs/index.md` guide index
- Update workflow to publish `build/docs` directly without Jekyll
- Track `.phpdoc/template/` while still ignoring `.phpdoc/cache/`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fine-tuning for path, config, and layout
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>