Compare commits

...
Author SHA1 Message Date
dependabot[bot]
16836d4e9a
chore(deps): bump astral-sh/setup-uv from 10.0.1 to 10.1.0 (#512)
Some checks failed
Documentation / build (push) Has been cancelled
Documentation / deploy (push) Has been cancelled
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 10.0.1 to 10.1.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v10.0.1...v10.1.0)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-15 15:57:24 +02:00
Дмитрий Воротынцев
ef6bee5531
[Server] fix OIDC discovery for issuers with trailing slash (#506)
* fix OIDC discovery for issuers with trailing slash

* add trailing-slash issuer tests and changelog entry

---------

Co-authored-by: Dmitry Vorotyntsev <d.vorotyntsev@dats.team>
Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-09-15 00:14:48 +02:00
Christopher Hertel
5a64bab1c7
[CI] Open conformance issue on manual runs too (#502) 2026-09-08 01:15:25 +02:00
Christopher Hertel
e7aa2d77de
[Docs] Add blank line before lists so they render (#503) 2026-09-08 01:14:59 +02:00
Christopher Hertel
7047019030
[README] Show conformance badges in a table (#501) 2026-09-08 00:49:47 +02:00
Christopher Hertel
f39625b257
[Capability] Remove CompletionProvider's dead providerClass argument (#499)
* [Capability] Remove CompletionProvider's dead providerClass argument

It always threw on construction, so it never worked in any release. `provider:`
takes the same class-string and is now the first positional argument.

* Reword Changelog
2026-09-07 23:27:53 +02:00
Christopher Hertel
749c0353ef
[Server] Keep inbound request intact when queueing an outbound one (#486) 2026-09-05 01:36:39 +02:00
Christopher Hertel
1bd7895b5f
[Schema] Fix _meta guard key in Request and Notification jsonSerialize() (#485) 2026-09-05 01:18:54 +02:00
Christopher Hertel
b458008f07
[Server] Fix StreamableHttpTransport mutable state and clock (#487) 2026-09-05 01:13:09 +02:00
Christopher Hertel
ebe067f6b3
[Server] Log filesystem failures in FileSessionStore (#488)
* [Server] Log filesystem failures in FileSessionStore

* [Server] Warn on failed unlink during session garbage collection
2026-09-05 01:11:20 +02:00
Christopher Hertel
4630d7424e
[Server] Stop Session::forget() from creating missing segments (#484) 2026-09-04 23:55:48 +02:00
Rob Loach
418034f2b0
[Client] Expose session ID from HttpTransport (#497)
* Expose session ID from HttpTransport

* [Client] State what getSessionId() returns on 2026-07-28

That revision removed protocol-level sessions (SEP-2567), so the getter is always null there. Adds the changelog entry.

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-09-04 23:55:20 +02:00
Christopher Hertel
c5dbfb64e5
[Server][Capability] Do not announce an externally loaded registry as changed (#490)
* [Capability] Do not announce an externally loaded registry as changed

`Registry` suppresses its `*ListChangedEvent`s while it is loading — `dispatch()`
returns early on the `loading` guard, so the elements a loader registers are the
registry's initial contents rather than a change to them.
`testListChangedEventsAreSuppressedDuringTheDeferredLoad` pins that.

The guard only covers `Registry::load()`, which needs the loader the constructor
took. A registry the caller built cannot be given one that way, so
`Builder::resolve()` loads it from the outside instead — by calling
`$chainLoader->load($registry)` directly, which never sets the guard. Every
element then dispatches on the way in.

With a notification bus configured that is not quiet. `PublishingEventDispatcher`
turns each event into a published notification, so every `build()` puts one
`list_changed` per element on the bus for a registry that did not change. Under
PHP-FPM, where the server is built per request and `Psr16NotificationBus` is
shared and persistent, every request broadcasts its whole element list to every
open `subscriptions/listen` stream and consumes the 256-entry backlog — after
which a reader that fell behind silently skips real notifications.

Split the guarded body of `load()` into `loadFrom(LoaderInterface $loader)` and
route the custom-registry branch through it.

`loadFrom()` takes only the `loading` guard, not the `loaded` bookkeeping, which
stays with `load()`. The loader it runs belongs to the caller, so it cannot stand
in for the one the registry was constructed with: marking the registry loaded
would retire a constructor loader that never ran, and would make a second
`loadFrom()` — one registry handed to two builders — a silent no-op. Keeping
`loaded` out of it also keeps `load()`'s promise that a transient failure is
retried on the next read, including when the failing loader reads the registry
during its own run.

`RegistryInterface` declares no `load()`, so this stays on the concrete
`Registry`, and a third-party implementation keeps the path it has today.

* Apply batched suggestions from code review

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-08-30 01:10:56 +02:00
Sviatoslav
46628fbe6a
[Capability] Cast variadic tool parameters element-by-element (#465)
* Cast variadic tool parameters element-by-element

SchemaGenerator::buildVariadicParameterSchema() already advertises a
variadic parameter (e.g. `int ...$scores`) as a JSON "array" schema, so
a spec-conformant client sends an array for it. But
ReferenceHandler::prepareArguments() never special-cased
ReflectionParameter::isVariadic(): it fell through to the regular
single-value branch and passed the whole array to castArgumentType(),
which tried to cast it as one scalar and failed - e.g. "Cannot cast
value to integer" for `int ...$scores` given [1, 2, 3].

Any tool handler declaring a variadic parameter was therefore
unreachable through the advertised schema, confirmed by direct
reproduction against ReferenceHandler::handle() before this change.

Casts each array element individually via the existing
castArgumentType() (unchanged - it already inspects the variadic
parameter's element type, not "array", per PHP reflection semantics)
and appends to the result in order; variadic is always the last
parameter, so this preserves correct positional ordering. A non-array
value for a variadic argument now fails clearly with an invalid-params
error instead of the confusing cast message.

* Address chr-hertel's review: wrap scalars, reject objects, index cast errors

- Non-array variadic argument is now wrapped as a single-element list
  instead of being rejected: MCP prompt arguments are always
  Record<string,string> per protocol, so a variadic prompt parameter
  legitimately receives a plain string, not an array. Rejecting it
  regressed behaviour that worked on main.
- A JSON object (non-list array) is now explicitly rejected instead of
  being silently flattened into positional args by array_values().
- Cast-failure errors now include the parameter name and element index,
  so a failure inside a large array is actually actionable.
- Trimmed the inline rationale comment; the PR description already
  covers it.

Tests updated: replaced the "throws on non-array" test (no longer
correct) with tests for the wrap-as-single-element, reject-object, and
indexed-error-message behaviours.

* Pin argument order for an injectable before a variadic

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-08-29 15:46:40 +02:00
dependabot[bot]
e473c3c115
chore(deps): bump astral-sh/setup-uv from 9.0.0 to 10.0.1 (#483)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 9.0.0 to 10.0.1.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v9.0.0...v10.0.1)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 07:45:13 +02:00
Christopher Hertel
e173367c5f
[Server] Keep injectable parameters out of the published inputSchema (#482) 2026-08-25 01:12:29 +02:00
Christopher Hertel
ff0b5fadc8
[Capability] Extract shared element name/description resolution (#481) 2026-08-25 00:46:21 +02:00
Christopher Hertel
312c621417
[Capability] Suppress list-changed events during the initial registry load (#480)
* [Capability] Suppress list-changed events during the initial registry load

* Shorten dispatch() comment to a one-liner

* Use multi-line docblock format for dispatch() comment
2026-08-25 00:45:00 +02:00
Christopher Hertel
c36e805086
[Server] Restrict FileSessionStore::gc() to owned session files (#479) 2026-08-25 00:43:08 +02:00
Christopher Hertel
d59950f1ac
[Server][Client] Bridge W3C trace context between HTTP headers and _meta (SEP-414) (#473)
RequestMeta::parseTraceContext() now falls back to the native traceparent/
tracestate/baggage HTTP headers when _meta carries none, and HeaderFactory
mirrors an application-set _meta trace context onto those same headers on
outbound requests. _meta wins when both are present.
2026-08-24 01:26:22 +02:00
Christopher Hertel
1e9520e731
[Docs] Deprecation policy, SDK tier target, and CONTRIBUTING.md (#472)
* [Docs] Add deprecation policy doc (SEP-2596)

Formalizes the Active -> Deprecated -> Removed lifecycle and the
twelve-month deprecation window the SDK already applies via SEP-2577,
and states the SDK's own BC promise ahead of 1.0.

* [Docs] Add SDK Tier 2 gap analysis (SEP-1730)

* [Docs] Wire deprecation policy and SDK tier docs into the guide nav

Adds both pages under a new Project section in mkdocs.yml, links them
from index.md's "Where to go next", and points their repo-root
references at GitHub URLs instead of paths escaping docs/ (dead under
Zensical's --strict build).

* [Docs] Add CONTRIBUTING.md

Root-level contribution guide: dev setup, the make ci checks to run
before a PR, coding standards, testing expectations, the docs-guide
link-resolution rule Zensical enforces, and licensing terms. Linked
from README's existing Contributing section.

* [Docs] Trim deprecation policy per review

Drop the worked SEP-2577 example and the "relationship to the
protocol lifecycle" section; both were redundant restating of detail
that lives in the code/CHANGELOG and protocol-versions.md already.

* [Docs] Cite the Tier 3 audit and add a versioning policy (SEP-1730)

Replace our own hand-rolled conformance numbers with the authoritative
audit in modelcontextprotocol/modelcontextprotocol#3274: SDK is
currently Tier 3, blocked from Tier 2 by client OAuth conformance
(20%) and no stable 1.0.0+ release. Add the Versioning section the
audit found missing — SemVer commitment pre-1.0, Symfony's BC promise
after — and point CONTRIBUTING.md at it.

* [Docs] Link Symfony's BC promise from the deprecation policy

Was plain text; sdk-tier.md's Versioning section already links it.

* [Docs] Drop the "process gaps addressed" aside from sdk-tier.md

* Apply suggestion from @chr-hertel

* Apply suggestion from @chr-hertel
2026-08-24 01:23:52 +02:00
Christopher Hertel
d09fe3019e
[Server] Make resource subscribe/unsubscribe errors protocol-version-aware (SEP-2164) (#474)
* [Server] Make resource subscribe/unsubscribe errors protocol-version-aware (SEP-2164)

ReadResourceHandler already picked -32602 vs -32002 based on the
negotiated revision; ResourceSubscribeHandler and
ResourceUnsubscribeHandler still hard-coded the retired -32002 for
every client. Bring them in line.

* Remove unnecessary comments

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-08-24 01:17:09 +02:00
Christopher Hertel
32464ec32e
[Changelog] Restructure 0.8.0 section (#478) 2026-08-24 01:16:10 +02:00
Christopher Hertel
c882401ee7
[Server] Log failures in CompletionCompleteHandler (#471) 2026-08-22 13:08:17 +02:00
Ben Younes
b1c7272761
[Server] Lower expected tool failure logging (#433)
Expected tool failures are part of normal tool execution, so log them at debug severity while preserving error logging for unexpected exceptions.
2026-08-22 01:46:29 +02:00
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
Christopher Hertel
0cd5d56164
[Docs] Render the guides with Zensical instead of phpDocumentor (#414)
* [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
2026-08-20 00:03:54 +02:00
Christopher Hertel
7fe88677ad
[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.
2026-08-19 03:24:17 +02:00
Christopher Hertel
8c06dcecc6
[Client] Speak the 2026-07-28 lifecycle (#456)
* [Client] Speak the 2026-07-28 lifecycle

On that revision the client opens with server/discover instead of
initialize, stamps each request's _meta with the protocol version, its own
capabilities and client info, and sends the standard Mcp-Method / Mcp-Name /
Mcp-Param-* headers an intermediary routes on. ToolCatalog is what knows,
from the tool list, which arguments a call has to mirror.

An input_required result is answered rather than surfaced: InputRequestResolver
asks the host's elicitation, sampling and roots handlers and retries the same
request carrying inputResponses and the requestState the server sent.

McpHeader holds the header names and the =?base64?…?= sentinel, so the two
sides no longer keep their own copies of the same rules.

* Fix CI baseline drift and address Copilot review findings

Drop stale expected-failure entries the new client fixture now passes.

Copilot review: fix McpHeader round-trip for a wrapper-shaped literal,
encode empty/numeric-keyed inputResponses as a JSON object, stop the
incident example repeating side effects on MRTR retry, and reset
ToolCatalog on reconnect instead of carrying the previous server's
verdicts.

* Skip DualEraExampleTestCase on PHP 8.1

PHP_CLI_SERVER_WORKERS does not reliably fork multiple php -S
workers there (php/php-src#9400), reproducing the single-worker
deadlock the extra workers exist to avoid.
2026-08-19 02:41:02 +02:00
Christopher Hertel
cbceef06eb
[Server] Serve both protocol eras from one endpoint (#455)
* [Server] Serve both protocol eras from one endpoint

The two lifecycles share a transport, not a dispatcher. StreamableHttpTransport
classifies each request - a 2026-07-28 envelope, an initialize handshake, or a
session-bound follow-up - through InboundClassifier and routes it to the
dispatcher that owns it, so one URL answers a modern client and a handshake-era
one alike. Builder::build() carries both; withoutModernEra() opts out and
setModernVersions() narrows what the modern leg answers for.

InputRequiredShim lets a handler written for multi round-trip requests also
serve a handshake-era client, by turning each ask into the request/response
exchange that era has - so a handler is written once rather than twice.

The conformance fixture collapses into one server for the same reason: both
legs now hit the same URL.

* Trim redundant comments, fix stale in-memory claim in conformance fixture

* Drop stale json-schema-2020-12 baseline entries

The new fixture tool makes this scenario pass in both eras now.

* Reject handshake revisions in setModernVersions(), warn on unsafe custom middleware

Copilot review: setModernVersions() accepted revisions InboundClassifier
would never route to the modern leg. StreamableHttpTransport now warns
when a custom middleware list carries ProtocolVersionMiddleware, since
it runs before era classification and rejects modern-era traffic by
default. Also fixed a misleading docblock claim on the same middleware.
2026-08-19 01:36:56 +02:00
Christopher Hertel
b97389325f
[Server] Serve an extension's methods under the modern lifecycle (#454)
* [Server] Serve an extension's methods under the modern lifecycle

The modern dispatcher takes the method-to-extension map the builder collects,
so a method belonging to an extension this server does not serve is answered
-32601 naming the extension instead of a bare "no handler found". It is still
an unknown method - the server genuinely does not implement it - but the
caller can now act on the answer.

* [Server] Reject two extensions claiming the same RPC method

The message factory resolves a contested method to whichever class
registered first, while extensionMethods kept the last one — so error
messages could name the wrong extension. Also fixes a test that
claimed to prove a method gets named by its extension while asserting
the opposite; the case it meant to cover (an enabled extension with no
handler for one of its methods) had no coverage at all.
2026-08-19 01:02:22 +02:00
Christopher Hertel
7e38c94954
[Server] Enforce x-mcp-header where a request uses it (SEP-2243) (#453)
* [Server] Enforce x-mcp-header where a request uses it

Only top-level properties were inspected, so an annotation on a nested
property was silently unenforced - the header could say anything. The walk
now follows a chain of properties keys to any depth and reads the argument
at that exact path.

Integer values compare numerically, so a client writing 42.0 for a body
value of 42 is no longer rejected.

* [Server] Stop comparing numeric-looking strings as numbers

is_numeric() and loose == both treat a string like "042" as equal to
"42", so a string argument could pass header validation against a
header that doesn't match it exactly. Gate the numeric comparison on
the argument's actual type and a decimal-only header instead.
2026-08-19 00:42:40 +02:00
Christopher Hertel
be9fad4115
Add badge linebreak and v1.0 roadmap items (#461) 2026-08-19 00:30:10 +02:00
Christopher Hertel
855011b8dc
[Server] Carry trace context through a request (#452)
traceparent, tracestate and baggage reach handlers through RequestContext
and are echoed onto the notifications the request causes, so a span is not
broken by the response stream. Passed through verbatim: what they mean is
the tracing library's business, and no dependency is taken for it.
2026-08-19 00:20:32 +02:00
Christopher Hertel
f6a995bc44
[Tests] Fix resources-templates-read to return a resolved URI (#460)
The fixture returned the literal template pattern test://template/{id}/data
instead of substituting id, failing the wire-schema uri format check. Also
switches the json-schema-2020-12 baseline entry to a bare scenario name: the
latest (0.1.x) conformance CLI doesn't parse scenario:check-id syntax, only
alpha (0.2.x) does, and the two need to agree since conformance-weekly runs
2025-11-25 against latest while pipeline.yaml pins alpha.

Verified locally against both dist-tags and both spec revisions.
2026-08-19 00:14:46 +02:00
Christopher Hertel
daac88d626
[Server] Make subscriptions/listen actually deliver (#451)
The stream acknowledged, slept thirty seconds and closed, so tools and
resource changes never reached a client — and with resources/subscribe
gone, that left the revision with no server-push at all. A notification bus
carries them: in-memory where publisher and stream share a process, PSR-16
where php-fpm puts them in different ones. The conformance server proves
the latter — a tools/call in one worker reaches a stream held in another.
2026-08-19 00:08:02 +02:00
Christopher Hertel
7dd72d82de
[Server] Let a server say how long its answers stay fresh (#450)
Every cacheable result went out as ttlMs 0, private — the letter of
SEP-2549 and none of its point, with no way to change it. CachePolicy adds
a default and per-method overrides; a resource can override per read. The
conservative default stands, because public means a shared cache may serve
one caller's answer to another.
2026-08-18 23:53:16 +02:00
Christopher Hertel
78a8926f17
[CI] Version both baseline files and split conformance badges per spec (#459)
- 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
2026-08-18 23:42:36 +02:00
Christopher Hertel
cad686aba7
[Server] Complete the multi round-trip request surface (SEP-2322) (#449)
* [Server] Let resources/read ask for input, and stop caching retries

The MRTR pattern names three methods and resources/read was the one whose
handler dropped an InputRequiredResult into the resource formatter. A
retry's answer also carried ttlMs and cacheScope, which the caching rules
forbid: its inputs are not part of any cache key.

* [Server] Refuse an ask the client cannot answer

A handler could put an elicitation into inputRequests for a client that
never declared one, and the only symptom was a retry that never came.
Checked against the request's own capabilities now, and reported as -32021
with the missing set — which is the code the spec defines for exactly this.

* [Server] Hand multi round-trip answers back typed

response() returned whatever JSON arrived, so every handler re-implemented
the same array poking. A malformed answer now reads as absent, which sends
the handler back to asking rather than to failing.

* [Server] Gate streamed input_required asks by capability too

checkInputRequests() only ran on the non-streaming path, so a handler
that emits a notification before returning InputRequiredResult could
still leak an undeclared-capability ask under HTTP 200.
2026-08-18 23:41:00 +02:00
Christopher Hertel
7ffc739fe7
[CI] Matrix conformance jobs over both spec revisions (#458)
* [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.
2026-08-18 23:30:57 +02:00
Christopher Hertel
dd3783a720
[Server] Stream a modern-era response when there is something to stream (#448)
tools/call answered with one JSON object, so a handler's progress and log
notifications had nowhere to go — and reached for a fiber that was not
there, coming back as -32603. Handlers now run in a fiber and the stream
opens after the first suspension, late enough that an error still gets the
status the spec fixes for it. logLevel finally gates the log messages it
was already being parsed for, and a server-initiated request is refused by
name rather than as a FiberError.
2026-08-18 23:22:45 +02:00
Christopher Hertel
5e73683079
[Server] Close the conformance gaps in the modern lifecycle (#447)
* [Server] Let the client gateway see modern-era capabilities

The six supports*() probes read session state only InitializeHandler wrote,
so every request served statelessly reported no client capabilities at all —
a tool guarding on them took the unsupported branch without a signal.
StatelessProtocol now writes the request's declaration under the same keys.

* [Server] Reject the RPCs 2026-07-28 removed

resources/subscribe and resources/unsubscribe were still dispatched and
answered 200 OK, recording subscriptions the modern era never reads. They
join initialize, ping and logging/setLevel in the era guard; the handlers
stay registered because the handshake era still serves them.

* [Server] Decode a wrapped Mcp-Name before comparing it

The Base64 sentinel was only unwrapped for Mcp-Param-*, so any resource URI
or tool name outside the header-safe ASCII set — which the spec explicitly
tells clients to wrap — was compared encoded and refused with -32020.

* [Server] Require the MCP-Protocol-Version header

It was only checked for contradicting the body, so omitting it entirely
passed — leaving the value intermediaries route on unenforced. Tied to the
header validator's presence, since that is what a header-bearing transport
installs and stdio carries its metadata inline.

* [Server] Read the modern-era request body whole

A single read() takes whatever the stream cares to give — 64 bytes from a
chunked transfer is legal — so an oversized-but-valid POST came back as a
parse error. Shares the legacy transport's incremental read as a trait.

* [Schema][Server] Answer a notification with a status, not a response

A body with no id was dispatched as a request and came back as a JSON-RPC
error carrying "id": "" — an id nobody issued, and on a path a client uses
to tell a modern server from a legacy one. Notifications now get 202 and no
body, and Error omits an id it could not read instead of emptying it.

* [Server] Stop emitting the error codes 2026-07-28 reserved

resources/read answered -32002, which this revision forbids; prompts/get
and completion/complete answered it for an unknown *name*, which it never
meant; tools/call answered -32601, which belongs to an unknown method.
Only the resources/read code is revision-gated — older peers expect -32002
there, and nowhere else. Also routes the -32021 capability exception out of
the prompt and resource handlers, which were swallowing it into -32603.
2026-08-18 23:09:34 +02:00
Christopher Hertel
ef0c1c2808
[Server][Tests] Complete the multi round-trip request flow (SEP-2322) (#446)
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.
2026-08-18 22:45:35 +02:00
Christopher Hertel
de35ca40bf
[Server][Tests] Validate the standard MCP request headers (SEP-2243) (#445)
The revision puts the method, the subject name and the mirrored arguments in
headers so an intermediary can route and cache without parsing the body. A
header that contradicts the body it travels with therefore has to be
refused, not ignored: StandardHeaderValidator compares each against the
request and answers -32020 on a mismatch.

Set with Builder::setHeaderValidator(); a transport with no header layer is
unaffected.
2026-08-18 22:27:05 +02:00
Christopher Hertel
3d596ca75b
[Server][Tests] Serve the modern (SEP-2575) lifecycle (#444)
* [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.
2026-08-18 22:08:54 +02:00
Christopher Hertel
60711951d4
[Schema][Server] Add the extensions framework SEP-2133 defines (#443)
* [Schema][Server] Add the extensions framework SEP-2133 defines

enableExtension() took any string and advertised it, with no way for an
extension to add a method: MessageFactory could not decode one, so nothing
downstream ever saw it.

Identifiers are now checked against the _meta key naming rules through
ExtensionIdentifier - a prefix is mandatory, since an unprefixed name has no
owner, and the modelcontextprotocol/mcp second labels the specification
reserves are recognised. An extension implementing
MethodProvidingExtensionInterface contributes both its message classes and
the handlers serving them.

MessageFactory::make() takes an $additional list of message classes, and
RequestHandlerInterface's result template is covariant so a handler
declaring a concrete result satisfies a collection typed by the interface.

* Fix stale @see reference in MethodProvidingExtensionInterface docblock

* [Schema][Server][Client] Make ExtensionIdentifier a value object

ExtensionInterface::getId() now returns ExtensionIdentifier, which
validates the SEP-2133 naming rules at construction instead of the
callers checking it. getMessages()/getRequestHandlers() move into
ExtensionInterface itself; AbstractExtension gives them empty
defaults for extensions that only announce a capability.
2026-08-18 20:47:27 +02:00
Christopher Hertel
5e3e69acc4
[Tests] Pin that protected resource metadata advertises no scope of its own (#440)
SEP-2207 says a resource server must not advertise `offline_access` as a
required scope. Nothing in src/ writes it - `scopesSupported` is entirely
operator-supplied - so there was nothing to fix, but also nothing stopping a
future default from quietly starting to advertise one.
2026-08-18 20:20:55 +02:00
Christopher Hertel
605df8771f
[Schema] Refuse an invalid x-mcp-header annotation at definition time (#441)
* [Schema] Refuse an invalid x-mcp-header annotation at definition time

SEP-2243 calls a tool definition invalid when its x-mcp-header annotation is
empty, is not an HTTP field name, collides case-insensitively with another,
or sits on a number, array or object property - none of which can be
mirrored onto a header.

Nothing said so. The earliest place to is where the tool is defined, rather
than on the request that trips over it.

* Clarify x-mcp-header validation only reaches properties

* Make checkHeaderAnnotations() a private instance method

* Make headerAnnotations() a private instance method

* Apply suggestion from @chr-hertel
2026-08-18 20:16:35 +02:00
Christopher Hertel
cf2c4d4ead
[Schema] Stop rejecting elicitation schemas the specification allows (#437)
* [Schema] Stop rejecting elicitation schemas the specification allows

The specification makes `title` optional on every elicitation schema
definition. AbstractSchemaDefinition required it, so a client reading a
conformant server's elicitation/create refused the request - and a server
building one could not omit it either.

`title` is now nullable on all seven definitions, omitted from the
serialized form when absent, and rejected only when present and not a
string.

* Fix stale title docblocks; allow omitting title where safe

* Give Number's title param a default too

* Apply suggestions from code review

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-08-18 19:49:15 +02:00
Valeriu Gutu
1961414432
[Schema][Server] Preserve request id in JSON-RPC error responses instead of fabricating id:"" (#379)
* Preserve request id in JSON-RPC error responses instead of fabricating id:""

When the server received input it could not turn into a valid message, the
error response carried a fabricated empty-string id that was never in the
request, breaking JSON-RPC client correlation.

The id default for the error factories was '' and that default reached the
wire unchanged: unrecoverable parse errors emitted id:"", and for invalid-
but-parseable messages MessageFactory discarded the decoded id so it also
fell back to "".

Now unrecoverable parse errors (-32700) return id: null per JSON-RPC 2.0,
and invalid-but-parseable requests (-32600) preserve the original request
id. Error::$id / getId() and the $id parameter of forParseError() /
forInvalidRequest() are widened to string|int|null, and the recoverable id
is threaded through InvalidInputMessageException.

Fixes #333

* Preserve nullable request id consistently across Error factories and fromArray

- Widen the remaining for*() factories (forMethodNotFound, forInvalidParams,
  forInternalError, forServerError, forResourceNotFound) to string|int|null
  defaulting to null, so no error path can fabricate id:"" again. The only
  defaulting call site (ProtocolVersionMiddleware) now emits id:null instead
  of id:"" for a pre-parse rejection, which is the spec-correct value.
- Accept a null id in Error::fromArray(): require the id key to be present
  (JSON-RPC responses must carry it) but allow its value to be null, so a
  spec-compliant id:null error response round-trips and incoming error
  responses with id:null are accepted instead of rejected.
- Widen the @phpstan-type ErrorData id to string|int|null to match the ctor,
  getId(), and jsonSerialize() shape.

* Document intentional id recovery guard in MessageFactory

* Cover id: 0 and string ids in recoverable-id preservation test

* Fixed client handling of null-id error responses
2026-08-18 15:47:27 +02:00
Christopher Hertel
5041caf77a
[Schema] Omit the id of an error response that never had one (#442)
* [Schema] Omit the id of an error response that never had one

An error response whose id could not be read sent `"id": ""`, which claims
the peer issued a request with an empty-string id - a different statement
from "the id could not be read". JSON-RPC spells the latter as a null id,
and a receiver that cannot read the id is exactly the case the code covers.

[BC Break] Error::$id accepts null and getId() may return it. All the for*()
factories default to null, fromArray() accepts a missing or explicitly-null
id, and the member is omitted from the serialized form when absent.

* Guard null response id in Client/Server Protocol handlers

Also fix Error::jsonSerialize()'s phpdoc: data belongs inside error, not beside it.
2026-08-18 15:24:19 +02:00
Christopher Hertel
cfe5e8f570
[Capability] Apply a parameter's complete schema definition (#435)
* [Capability] Apply a parameter's complete schema definition

Schema(definition:) is documented as taking precedence over the individual
keywords, and generate() unwraps it that way at method level. At parameter
level it was merged as an ordinary key instead, in both the plain and the
variadic path, so the property reached clients carrying a literal
"definition" member - not JSON Schema, read by nothing, and everything
inside it lost.

Both paths now replace the schema with the definition. The variadic one
still forces type: array afterwards, since a variadic arrives as an array
however the definition describes it, and the signature default survives a
definition that states none - it is what the handler receives when the
argument is omitted, and default is an annotation rather than a constraint.

* Fix docblock description overwriting definition on variadic params

* Apply suggestion from @chr-hertel
2026-08-18 15:09:58 +02:00
Christopher Hertel
08d332c7ac
[Schema] Serialize a settingless extension as an object (#438)
* [Schema] Serialize a settingless extension as an object

An extension declaring support with no settings was advertised as `[]`, an
empty JSON array, which the wire schema rejects - `extensions` maps an id to
a settings *object*. Both ServerCapabilities and ClientCapabilities now cast
each entry, so it goes out as `{}`.

* Apply suggestion from @chr-hertel
2026-08-18 14:59:12 +02:00
Christopher Hertel
ced762614b
[Capability] Stop a union type losing a branch (#434)
* [Capability] Stop a union type losing a branch

mapPhpTypeToJsonSchemaType() tested for array syntax before splitting the
union, so string[]|int mapped to array alone; applyArrayConstraints() then
overwrote the type with array whenever the union contained one, dropping the
scalar branch a second time. A tool declaring such a parameter rejected a
valid integer argument.

The split now happens first and only at the top level, so array<int|string>
and array{a: int|string} stay one type rather than two that do not exist, and
the array branch keeps its items alongside the others - which is what 2020-12
means by items: a constraint that applies only when the instance is an array.

* Apply suggestion from @chr-hertel

* Isolate the array branch before inferring its element type

* Don't apply items to arrays whose union branches disagree on element type
2026-08-18 14:54:52 +02:00
Christopher Hertel
0751a83658
[Examples][Tests] Make the MCP Apps example work over HTTP, and pin it (#439)
* [Examples][Tests] Make the MCP Apps example work over HTTP, and pin it

It was the only example without a session store, so every request under
`php -S` or php-fpm - each its own process - was answered "Session not found
or has expired". It now uses a FileSessionStore like the others.

Inspector snapshots pin what the extension actually puts on the wire: the
ui:// resource's `text/html;profile=mcp-app` MIME type and `ui` marker, and
the `_meta.ui` on the tool that links the two.

* Apply suggestion from @chr-hertel

* Apply suggestion from @chr-hertel
2026-08-18 14:51:34 +02:00
Christopher Hertel
7407661260
Tighten the 0.8.0 CHANGELOG section (#431)
* Add missing security fixes and bug fixes to 0.8.0 CHANGELOG

* Drop security-fixes bullet from CHANGELOG

* Drop bugfix bullets from CHANGELOG
2026-08-18 02:48:16 +02:00
Christopher Hertel
d5351759d7
[Schema] Drop SDK-only name pattern on resources and resource templates (#430)
* [Schema] Drop SDK-only name pattern on resources and resource templates

* Update src/Schema/ResourceDefinition.php

* Update src/Schema/ResourceTemplate.php
2026-08-18 02:09:07 +02:00
Christopher Hertel
6f58fe596a
[Schema][Client][Server] Close MCP Apps gaps: extension negotiation checks (#429)
* [Schema][Client][Server] Close MCP Apps gaps: extension negotiation checks

Add ClientGateway::supportsExtension(), Client\Builder::enableExtension() and
ClientCapabilities::withExtensions(); introduce side-agnostic ExtensionInterface
and deprecate ServerExtensionInterface; document attribute-based UI linkage.

* Drop ServerExtensionInterface in favour of ExtensionInterface
2026-08-18 01:46:53 +02:00
Christopher Hertel
590afb575b
[Schema][Client][Server] Deprecate Roots, Sampling and Logging (SEP-2577) (#427)
* [Schema][Client][Server] Deprecate Roots, Sampling and Logging (SEP-2577)

* Emit runtime deprecation notices and document the migration (SEP-2596)
2026-08-18 01:23:32 +02:00
Christopher Hertel
1ff9a74521
[Tests] Bump MCP Inspector to 2.2.0 (#426) 2026-08-17 23:26:05 +02:00
Christopher Hertel
f84d8f5aa9
[Schema][Server] Close the 2025-11-25 schema gaps and add the 2026-07-28 surface (#421)
* [Schema][Server] Close the 2025-11-25 schema gaps and add the 2026-07-28 surface

Ports the type definitions this SDK still misses outside of sampling tool use,
which #409 and #420 already cover. Every addition is optional and defaults to
current behaviour, so a connection negotiated on an older revision is
unaffected.

From 2025-11-25, elicitation gains modes. ElicitationMode splits `form` —
build a form from the requested schema — from `url`, which sends the user out
of band and returns only the accept/decline/cancel outcome. That is why
requestedSchema becomes optional and `url` appears beside it.
ClientCapabilities learns the matching sub-capabilities, where an
`elicitation` naming no mode declares form, the only shape that existed
before. Icon gains `theme` from the same revision, and Implementation gains
the `title` BaseMetadata has carried since 2025-06-18.

From 2026-07-28, schemas loosen where the revision loosens them: SEP-2106
drops the object-only restriction, so Tool::outputSchema may describe any JSON
value and CallToolResult::structuredContent follows.

The same revision defines three error codes (-32020 header mismatch, -32021
missing required client capability, -32022 unsupported protocol version).
ProtocolVersionMiddleware switches to the last of them, so a rejected version
carries the supported set as structured data the client can retry from rather
than only as prose.

* [Schema] Reject what the 2026-07-28 types cannot represent

`Implementation::title` reached the typed constructor unchecked, so malformed
wire data raised a TypeError instead of InvalidArgumentException.

`ToolUseContent::input` accepted a list and serialized it as a JSON array,
where the protocol requires an object. The empty array stays exempt: it is
also an empty map and still emits `{}`.

`ToolChoice` and `ElicitRequest` read their mode with isset(), which is false
for an explicit null, so `{"mode": null}` silently became the default instead
of being rejected. Both use array_key_exists() now, letting the existing type
check refuse null.

* [Client][Server] Advertise the implementation title through both builders

`Implementation::title` could be parsed but never sent: neither
`Client\Builder::setClientInfo()` nor `Server\Builder::setServerInfo()`
accepted one, so every SDK user emitted null.

Both gain a trailing optional `$title`. On the server it sits where the
Implementation constructor already puts it, so existing positional calls keep
their meaning; the client builder forwards it by name, leaving the icons and
websiteUrl slots defaulted.

* [Schema] Serialize every non-null structuredContent

The object-only hydration guard was never that: `!is_array()` admitted
`[1, 2, 3]` and `[]`, which serialize to JSON arrays, while rejecting the
scalars 2026-07-28 permits. The truthiness emission gate was backwards in the
same way — it dropped `[]`, `0`, `false` and `""`, yet emitted lists, strings
and an empty stdClass.

Hydration now accepts any JSON value, and `null` alone means absent, matching
`ToolResultContent` which already carries this field that way. Which values a
given revision permits is a question for version-aware serialization, which
results cannot answer yet.

* [Schema][Server] Make url elicitation reachable

`ElicitRequest::forUrl()` built a request no SDK user could send: the only
public gateway method always constructed form mode from an ElicitationSchema,
and `request()` is private.

`elicitUrl()` joins `elicit()`, and both funnel through one send path that
hydrates the result with the request's own mode. Without that, a url-mode
accept — contentless by design — threw, because ElicitResult requires content
whenever the action is accept. The result carries no discriminator of its own,
so the mode has to come from the request it answers.

`supportsElicitationUrl()` reports whether the client named the mode, reusing
the sub-capability reader the sampling checks already use.

* [Schema] Add CHANGELOG entry for the 2026-07-28 surface

* [Capability][Schema][Server] Carry the widened structuredContent end to end

Review follow-up on the SEP-2106 widening, which stopped at the type.

`ToolReference::extractStructuredContent()` returned `?array`, so a tool
declaring a scalar `outputSchema` had its result dropped and logged as
unsendable. It returns `mixed` now and keeps a scalar — but only from
2026-07-28 on, and only when the tool declared an outputSchema: without one
the value is already carried in `content`, and advertising it twice is not an
improvement.

`CallToolHandler` only warned about list-shaped `structuredContent` on a
self-built `CallToolResult`, so a scalar reached revisions that require an
object unremarked. The check is on the shape now, not on the list case alone.

`ElicitResult` retained a `content` the spec says is absent from url-mode
results, leaving a malformed response indistinguishable from a valid one.

`Tool::jsonSerialize()` emitted the empty root schema as `[]`, which is not a
schema at all. Sub-schemas already had this treatment.
2026-08-17 23:09:39 +02:00
wWzZb
76778a4d9e
[Schema] Support PHPStan/Psalm integer ranges (#422) 2026-08-16 09:31:28 +02:00
Christopher Hertel
43815e38a8
[Schema][Server] Harden sampling tools against the spec (#420)
* harden sampling tools against the spec

Report tool-flow violations as -32602 instead of dropping the request,
accept resource_link in tool results, gate tools on sampling.tools, and
reject empty content.

* normalize keyed content arrays to lists

array_filter() and friends preserve keys, and a keyed array serializes as a
JSON object instead of the content-block array the schema requires.
2026-08-15 04:13:53 +02:00
Christopher Hertel
becce29c41
[Capability] Harden structuredContent extraction (#417)
* Address review feedback on resource_link

Three follow-ups from review of the resource_link work:

* PromptResultFormatter: add a regression test proving the optional
  fields of a typed resource_link block (title, description, mimeType,
  size, annotations, _meta) survive formatting. The delegation to
  ResourceLink::fromArray() already landed while rebasing onto the
  PromptResultFormatter refactor, but the existing test only supplied
  uri/name and so could not catch a reintroduced field drop.

* ResourceLink::fromArray(): validate optional fields consistently, the
  way the equivalent ResourceDefinition::fromArray() already does.
  description, mimeType and size now raise InvalidArgumentException
  instead of surfacing a TypeError (or silently coercing, for size's
  (int) cast); annotations goes through Annotations::tryFromArray(),
  which carries the is_array() guard that icons already had; icons uses
  Icon::listFromArray() so a non-array entry is reported in context.

* ToolReference::extractStructuredContent(): never emit a list as
  structuredContent. The Content guard added earlier fixed only one
  instance of the real invariant - structuredContent must be a JSON
  object, and a PHP list can never be one. Tool::fromArray() already
  enforces the matching rule by rejecting an outputSchema whose type is
  not "object", so this aligns the runtime path with it. The test
  asserting the opposite for an array-typed outputSchema contradicted
  the phpstan type and the fromArray() check introduced alongside it in
  the same commit, so it is replaced by tests for the two list shapes.

* Apply the structuredContent object rule to object results too

`extractStructuredContent()` guards raw array results against being emitted
as a JSON array, but the object branch handed back whatever `json_decode()`
produced. A `JsonSerializable` returning a list or a scalar slipped straight
through, producing exactly the `structuredContent` the array guard exists to
prevent — and a return value that contradicts the method's own
`array<string, mixed>|null` signature.

Check the decoded value before returning it, and cover the object branch,
which had no tests at all.

* Document structured tool output

`outputSchema` and `structuredContent` were undocumented: the tool return
value docs covered only the `content` side, and the schema generation
section is about tool parameters.

Add a "Structured Output" subsection covering how to declare the schema,
which return values populate `structuredContent`, and why a list has to be
wrapped in a key to get structured output at all.

* Gate structuredContent on the negotiated protocol revision

SEP-2106 (revision 2026-07-28) widens structuredContent to any JSON value;
earlier revisions require an object. Resolve the revision per request and
apply the matching rule, and warn when a declared outputSchema yields none.

* Resolve the protocol revision on RequestContext

Keeps the `_meta`-then-session lookup in one place instead of per handler,
and exposes the negotiated revision to tool handlers.

* Document reading the negotiated revision from RequestContext

* Warn when a self-built CallToolResult carries an invalid structuredContent

Returning a CallToolResult opts out of the extraction rules, so the value is
still sent unchanged — but a JSON array is not valid before SEP-2106.
2026-08-15 04:03:20 +02:00
wWzZb
82aac972c1
[Schema][Server] feat: support sampling with tools (#409)
* feat: support sampling with tools

* address sampling tools review feedback

* tighten sampling tools spec compliance

* drop SamplingStopReason enum in favor of open string

The spec leaves stopReason open for provider-specific values, so upcasting
the four known ones to enum cases makes every future enum addition a silent
BC break for string comparisons.

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-08-15 03:28:11 +02:00
Christopher Hertel
d8cc21a345
[Client] Implement setMaxRetries connection retries (#413)
* [Client] Implement setMaxRetries connection retries

* [Client] Reject a negative retry count in Configuration

* [Client] Use a positive init timeout in the retry test

* [Client] Drop redundant docblock note on setMaxRetries

* [Client] Clear the initialized flag on a failed connection

* [Client] Trim comments to the non-obvious ones

* [Client] Cover retries and timeout cleanup with integration tests

The retry fixture counts the processes it was started as, which is what
separates a real retry from a second call into the same server. The timeout
fixture outlives the client's patience and then goes idle, so the next
request runs on a connection the client already gave up on once.
2026-08-15 02:43:50 +02:00
Christopher Hertel
7b12dc4010
[Tests] Introduce client-server integration tests (#419)
* [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.
2026-08-15 02:24:48 +02:00
Christopher Hertel
c4b163b4c4
[Server] Add ClientGateway::supportsSampling() (#418)
Sampling was the one client capability a tool could not check before asking.
`supportsRoots()` and `supportsElicitation()` both read the capabilities the
client advertised during the handshake; sampling is stored in that same session
data but had no accessor, so a tool had to issue the request and catch the
refusal.
2026-08-15 02:11:15 +02:00
Christopher Hertel
eb20453f49
[Client][Schema][Server] Implement protocol version negotiation (#403)
* [Client][Docs][Schema][Server] Implement protocol version negotiation

The `initialize` handler never looked at the version the client asked for:
it always answered with the server's configured version (or the SDK default),
so a client offering a revision the server supports could still be told to
speak a different one. The spec requires echoing back a supported request and
only counter-offering when the requested version cannot be honoured.

Turns ProtocolVersion into the registry the other SDKs converged on
(typescript-sdk's protocolEras.ts, python-sdk's mcp_types/version.py):
`latestHandshake()`, `handshakeVersions()`, `modernVersions()` and ordered
comparison through `isAtLeast()`. Comparison goes through declaration order
rather than string collation, so an unrecognized peer string compares
conservatively instead of accidentally sorting above a known revision. The
one throw the enum can produce is the SDK's own LogicException, so a consumer
catching `Mcp\Exception\ExceptionInterface` does not miss it.

Adds 2026-07-28 as a known revision and splits the set into two eras:
handshake (2025-11-25 and earlier, negotiated via `initialize`) and modern
(2026-07-28+, per-request `_meta`). The lists are kept apart so a modern
revision can never leak into an `initialize` exchange, which has no
`initialize` at all. Serving the modern era is follow-up work; this only
teaches the SDK that the revision exists and must not be mis-negotiated.
Deliberately absent is any "newest revision in any era" accessor, which
would hand back the one revision the handshake must never offer.

Server: negotiates and stores the result on the session. A version pinned in
Configuration still pins the handshake to exactly that revision.
Client: validates the counter-offer and fails the handshake instead of
continuing on a revision it cannot speak; records the negotiated version on
the client state, readable through the new Client::getProtocolVersion().

The HTTP middleware now defaults to the handshake versions rather than every
declared case, so it stops accepting a modern version the server cannot yet
serve. When the header is absent it falls back to
ProtocolVersion::DEFAULT_NEGOTIATED_VERSION (2025-03-26), the revision that
introduced both Streamable HTTP and the header itself.

The negotiation behaviour is one provideNegotiationTable() data provider
whose rows are the documented table -- supported revision echoed back,
unknown or malformed counter-offered, modern counter-offered -- so the docs
and the suite cannot drift apart. The supported-revision rows iterate
handshakeVersions() rather than a literal list, so declaring a new handshake
revision extends the table automatically, and the "never answer with a modern
revision" assertion runs on every row.

Documented in server-builder.md (the enum registry, how the server answers a
handshake, what pinning changes), transports.md (the header check) and
client.md (the client's side), all linking the spec's versioning section.

* [Client][Docs][Schema][Server] Warn when the configured protocol version cannot be negotiated

* Log a warning instead of silently falling back when a modern revision is
  configured on the client or pinned on the server builder.
* Document that setProtocolVersion() does not restrict the MCP-Protocol-Version
  header check, which validates against a fixed set.
* Pin every revision's era in a test, so a new case declared past the modern
  boundary fails instead of dropping out of the handshake set.
* Add CHANGELOG entry.

* [Client][Docs][Schema][Server] Rename version fallback constant and trim comments
2026-08-15 01:41:19 +02:00
Alex Rothberg
fc456125e2
[Schema] Add resource_link content block (#400)
* Add resource_link content block

The spec's `resource_link` content type (protocol revision 2025-06-18,
still present in 2025-11-25) had no corresponding schema class: it lets a
tool result or prompt message reference a resource by URI/name without
embedding its full contents, the way `EmbeddedResource` does. Without it,
tools returning many or large resource references (e.g. search results
over a DAM) are stuck smuggling URIs through text/structured content.

Add `Mcp\Schema\Content\ResourceLink`, mirroring the spec's `Resource`
shape (uri, name, title, description, mimeType, annotations, size, icons,
_meta) with a fixed `type: "resource_link"`, following the conventions of
the sibling `EmbeddedResource`/`ResourceDefinition` classes.

Register it everywhere `resource` content blocks are (de)serialized by
type per the spec's `ContentBlock` union:
- `CallToolResult::fromArray()`
- `PromptMessage` (constructor type, `fromArray()`, phpstan types)
- `PromptResultFormatter` (typed-array and direct-instance paths)

`SamplingMessage`/`CreateSamplingMessageResult` are intentionally left
unchanged: the spec's `SamplingMessageContentBlock` does not include
`ResourceLink`.

Fixes #399

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add resource_link example coverage to discovery-userprofile

Add a lookup_user tool to the discovery-userprofile example: given a
userId, it returns a short text summary plus a resource_link pointing
at the existing user://{userId}/profile resource template, instead of
embedding the profile. This mirrors the PR's motivating use case (a
tool referencing many/large resources by URI rather than inlining
them) and exercises resource_link through the example's own inspector
snapshot tests (tools_call-lookup_user[_not_found], tools_list).

Wiring this up surfaced a real bug: ToolReference::extractStructuredContent()
passed any raw array return value straight through as structuredContent,
even when it held Content instances (e.g. the TextContent/ResourceLink
pair above). That serializes to a JSON list, which fails the spec's
requirement that structuredContent be an object - the inspector's MCP
client rejected the tool call outright until fixed. Skip structuredContent
extraction whenever the raw result contains a Content item, since those
are already represented in `content` and were never meant to double as
structured data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Rothberg <51059+cancan101@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 23:20:56 +02:00
Johannes Wachter
bdc9d86671
[Client][Server] Add Roots support (#395)
* [Client][Server] Add Roots support

Wire up the MCP "roots" capability on both sides:

Client (php-sdk as client):
- RootsCallbackInterface + ListRootsRequestHandler answer server roots/list
  requests, mirroring the Sampling handler.
- Client::sendRootsListChanged() emits notifications/roots/list_changed.

Server (php-sdk as server):
- ClientGateway::listRoots() requests the client's roots, and
  supportsRoots() reports whether the client advertised the capability,
  normalizing the stored capabilities to an array so an empty
  capabilities object (\stdClass) doesn't cause a TypeError.
- Add ListRootsResult::fromArray() (used by listRoots()).
- ListRootsRequestHandler forwards RootsException messages to the server
  (new Mcp\Exception\RootsException), mirroring SamplingRequestHandler.
- Client::sendRootsListChanged() throws when the client did not advertise
  the roots.listChanged capability.

Examples, docs and tests:
- examples/README/docs advertise
  `new ClientCapabilities(roots: true, rootsListChanged: true)` before
  sending notifications/roots/list_changed.
- CHANGELOG entry for Roots (unreleased 0.7.0 section at the time).
- Unit tests for capability serialization/round-trip, sendRootsListChanged
  gating, and RootsException message forwarding.

phpunit, php-cs-fixer and phpstan green.

* [Client][Server] Address review: schema validation, changelog section, connection guard, working example

Responds to chr-hertel's review of the Roots support:

- ListRootsResult::fromArray() rejects non-array root elements instead of
  accepting malformed entries silently.
- CHANGELOG entry moved to the next unreleased section as the release
  train advanced past it (0.7.0 -> 0.8.0).
- Client::sendRootsListChanged() went straight to
  Protocol::sendNotification(), which sends via
  `$this->transport?->send()` -- on a client that is not connected the
  notification was silently dropped. It now throws a ConnectionException
  like sendRequest() does.
- The stdio example only advertised the roots capability, so the
  roots/list handler never actually ran. The client-communication demo
  server gets an `inspect_workspace_roots` tool that calls
  ClientGateway::supportsRoots()/listRoots(); the example calls it, so
  the server actually issues roots/list and the client answers it.
  Dropped the try/catch wrapper from the example.
- ClientTest's connected-client mock now goes through setState() so
  isConnected() reflects a real handshake instead of a stubbed flag.

phpunit, php-cs-fixer and phpstan green.
2026-08-14 23:04:25 +02:00
Christopher Hertel
8bae3c054e
[Capability] Preserve optional content fields in PromptResultFormatter (#406)
* Preserve optional content fields in PromptResultFormatter

The formatter's typed-content path rebuilt each Content object from only
its required fields, silently dropping everything else a prompt returned
(annotations on every type, _meta and explicit mimeType on embedded
resource contents). Delegate to the schema classes' fromArray() instead,
keeping the historical mimeType defaulting and RuntimeException-with-
index error contract.

ImageContent was the only content class without annotations support, so
it gains the same constructor/fromArray()/jsonSerialize() handling as
TextContent and AudioContent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Reword CHANGELOG entry to be user-facing

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 22:47:26 +02:00
Chris Coutinho
071476ebd8
[Client] Parse CRLF- and CR-delimited SSE events (#401)
HttpTransport looked for an event boundary with strpos($buffer, "\n\n") and
split event lines on "\n". Servers that terminate SSE lines with CRLF therefore
never produced a recognised event: the response accumulated in the buffer, no
message was ever dispatched, and connect() failed with "Initialization failed:
Request timed out".

This affects every MCP server built on the Python SDK. FastMCP's
streamable_http_app() returns sse_starlette's EventSourceResponse without a sep
argument, and its DEFAULT_SEPARATOR is "\r\n". The bug is invisible from within
this repository because the PHP server implementation emits LF, so client and
server agree in the test suite while disagreeing with most servers in the wild.

The SSE specification requires a parser to accept CRLF, LF or CR, so the
previous behaviour was non-compliant rather than merely unlucky.

Event extraction now accepts all three terminators, choosing whichever appears
first so a buffer split mid-sequence is handled correctly, and line splitting
uses the same set. A stream that ends without a trailing blank line now
dispatches its final event instead of discarding it.

Adds the first tests for src/Client/Transport/, covering LF, CRLF and CR
framing, an id field, a comment preamble, and a missing trailing blank line.
Without the fix four of the six fail with the timeout above.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 22:34:23 +02:00
Christopher Hertel
c8ed07bba2
[Server] Handle malformed JSON-RPC input graceful (#412)
* Never let a throwable escape Protocol::processInput()

* Validate payload types in the schema hydration methods

* Stop leaking handler exception messages to the client
2026-08-14 22:34:01 +02:00
Björn Meyer
9236824b6d
[Schema] Always emit {} for empty tool schema properties (#407)
* [Schema] Always emit {} for empty tool schema properties

Normalize empty properties arrays (including nested object schemas and
outputSchema) in Tool::__construct so tools/list never serializes
invalid JSON Schema properties: [].

Fixes #405

* [Schema] Normalize every empty sub-schema, not only properties

Walking into `items` and `additionalProperties` without normalizing an empty
sub-schema at those positions left the `[]`-instead-of-`{}` bug intact one
level down. Most notably it regressed #151 on a `Tool::fromArray()`
round-trip: `SchemaGenerator` emits `items: {}` for untyped array parameters,
which decodes to `[]` and was re-serialized as `[]` — the exact schema strict
clients reject.

Empty sub-schemas are now replaced with a `\stdClass` at every schema
position, and the keyword list is completed to cover draft-07 through
2020-12: `$defs`, `definitions`, `dependentSchemas`, `patternProperties`,
`propertyNames`, `contains`, `not`, `if`/`then`/`else`, `additionalItems`,
and `unevaluatedItems`/`unevaluatedProperties`. Keywords holding plain JSON
arrays (`allOf: []`, `enum: []`, `dependentRequired`) keep encoding as `[]`.

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-08-14 21:50:08 +02:00
Christopher Hertel
a987a3a248
[Server] Throw the SDK's own RuntimeException from FileSessionStore (#404)
The constructor guarded its unwritable-directory case with the global
`\RuntimeException`, the only throw in `src/` still outside
`Mcp\Exception\ExceptionInterface`. A consumer wrapping server setup in a
`catch (ExceptionInterface)` missed it and got an unhandled SPL exception on
a misconfigured session directory instead.

`Mcp\Exception\RuntimeException` extends `\RuntimeException`, so this is
additive: existing `catch (\RuntimeException)` blocks keep working.

The constructor had no test coverage at all, so this adds one for the throw
plus the directory-creation and write/read paths around it.
2026-08-14 21:29:26 +02:00
Christopher Hertel
d9b3c0ba4e
[Client] Reject non-positive timeouts in Configuration (#415)
* [Client] Reject non-positive timeouts in Configuration

* [Client] Drop redundant comments on timeout validation
2026-08-14 08:22:20 +02:00
Recoordinate
785fc3b9b7
Fix 'notifcation' typo in docs (#410) 2026-08-10 22:09:23 +02:00
Christopher Hertel
9a6d24c417
Fix codestyle pipeline for 0.7.1 (#402) 2026-07-27 12:16:10 +02:00
Antoine Bluchet
9aa18f391a
Merge commit from fork
processInput() called fgets($this->input) with no length argument. PHP
fgets() reads until a newline or EOF, so a peer that streams bytes
without ever sending a newline forced the whole stream into a single
allocation and exhausted the process memory limit (GHSA-vm75-qjc7-xh6w).

Bound each read with fgets($this->input, $this->maxLineBytes) (default
4 MiB, configurable via the constructor). When a read fills the cap with
no trailing newline the line exceeds the limit: discard it and keep
discarding one bounded chunk per tick until its terminating newline,
rather than buffering it. The discard is stateful so the run loop keeps
servicing fibers and outgoing messages instead of blocking on a drain
loop, and the over-length line is logged once.

Reimplements tonghuaroot's advisory fix: makes the cap configurable
rather than a hardcoded constant, drains the over-length line without an
inner blocking loop (responsive run loop, single log line), and adds
regression tests.

Co-authored-by: tonghuaroot <23011166+tonghuaroot@users.noreply.github.com>
2026-07-27 11:50:31 +02:00
Antoine Bluchet
529099f463
Merge commit from fork
processInput() appended every 8 KiB stdout chunk to $inputBuffer and only
drained it on a "\n". A spawned server that streams stdout without ever
emitting a newline grew the buffer without bound, exhausting client
memory (GHSA-6vrr-g4hw-f3hj).

Cap the buffer (default 4 MiB, configurable via the constructor). When
appending the next chunk would exceed the cap before a newline is seen,
abort the read and resolve the in-flight request with an error
immediately, so the caller fails fast instead of spinning until its
request timeout.

Based on localden's suggested fix in the advisory: makes the cap
configurable, but resolves the pending request with an error (keeping
runRequest's Response|Error contract) instead of throwing a
TransportException out of the read loop, and leaves connection teardown
to the caller. Adds regression tests.

Co-authored-by: localden <53200638+localden@users.noreply.github.com>
2026-07-27 11:16:14 +02:00
Antoine Bluchet
055593f259
Merge commit from fork
processSSEStream() appended every 4 KiB chunk to $sseBuffer and only
flushed on the "\n\n" SSE event delimiter. A server streaming data
without ever sending the delimiter grew the buffer without bound,
exhausting client memory (GHSA-7m52-jw36-44r3).

Cap the buffer (default 8 MiB, configurable via the constructor). When
appending the next chunk would exceed the cap before a complete event is
seen, abort the stream and resolve the in-flight request with an error
immediately, so the caller fails fast instead of spinning until its
request timeout.

Reimplements tonghuaroot's advisory fix: makes the cap configurable
rather than a hardcoded constant, fails the pending request fast instead
of relying on the timeout, and adds regression tests.

Co-authored-by: tonghuaroot <23011166+tonghuaroot@users.noreply.github.com>
2026-07-27 11:15:43 +02:00
Christopher Hertel
a6f415578f
Fix v0.7.0 CHANGELOG entries (#396) 2026-07-15 00:58:00 +02:00
Christopher Hertel
0058d93086
[Server] Expose OAuth protected resource metadata as a reusable request handler (#391)
* [Server] Expose OAuth protected resource metadata as a reusable request handler

* docs: show handler DI wiring in Symfony/Laravel controller examples

* Focus PR on transport refactor: drop docs and example changes

* docs: add changelog entry for ProtectedResourceMetadataHandler
2026-07-15 00:24:04 +02:00
Christopher Hertel
1fed089d71
[Examples] Demo pre-built instance handlers + inspector coverage (#390)
* [Tests] Bump MCP Inspector to 0.22.0 for 2025-11-25 protocol

The pinned inspector 0.17.2 predates the protocol version bump to
2025-11-25 (v0.6.0) and rejects the handshake with "Server's protocol
version is not supported", failing every inspector snapshot test. 0.22.0
negotiates the current protocol; the full inspector suite passes against
it (95 tests, 7 pre-existing skips).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [Examples] Demo pre-built instance handler in combined-registration

Extends the combined-registration example with a third handler style:
a pre-built object instance `[$instance, 'method']`, alongside the
existing discovered and manual class-string handlers. PreconfiguredGreeter
takes a scalar constructor argument neither the container-less
`new $className()` fallback nor the auto-wiring container can build, so it
can only be registered as an already-constructed instance — exercising the
resolver path added in the parent branch.

Adds an inspector snapshot case (tools/call instance_greeter) and updates
the tools/list snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 00:23:20 +02:00
King Star
d187002d87
[Server] Reject malformed MCP session ID headers (#384)
* [Server] Reject malformed MCP session ID headers

* [Server] Narrow invalid session ID exception handling

* [Server] Keep UUID catch compatible with supported Symfony
2026-07-15 00:20:43 +02:00
Valeriu Gutu
8e18bfbdf1
[Server] Always emit items for array tool parameter schemas (#378)
Array tool parameters without an inferable element type generated `{"type":"array"}` with no `items`, which strict MCP clients (VS Code / Copilot) reject. Always emit `items` (`{}` when the element type is unknown) and recover the element type for nullable typed arrays such as `string[]|null`.

Fixes #151
2026-07-14 23:56:50 +02:00
Antoine Bluchet
ff91ca6d60
[Server] Defer element loading to first registry read (#389)
Builder::build() runs all loaders eagerly and snapshots capabilities from
the resulting registry. Both are computed once, when the server is built.
Under a persistent runtime (e.g. FrankenPHP worker mode) the server is
built a single time, so a loader whose data source is not yet ready at
that moment (cold cache, un-warmed metadata) leaves the registry empty for
the whole process — tools/list stays empty while tools/call still works.

Make deferred loading the default behavior of Registry itself: it takes a
loader and runs it on the first read (has*/get*), moving loading to
request time when the application is initialized. The load runs once; the
loaded flag is set only after a successful load, so a transient failure is
retried on the next read. A re-entrancy guard lets loaders that read the
registry during their own run (e.g. discovery's identity check) observe
the partial state instead of recursing. Registrations made before the
first read survive it.

Expose the switch on Builder via setLazyLoading() (default on) so callers
can opt back into eager loading at build time. In lazy mode capabilities
are advertised from the configured sources so the initialize handshake
does not force a load; in eager mode (or for a registry supplied via
setRegistry(), which is always loaded eagerly) they are read from the
loaded registry.
2026-07-14 23:55:31 +02:00
Johannes Wachter
e4a8a224ff
[Client] Add client-side elicitation support (#371)
* [Client] Add client-side elicitation support

The SDK could send elicitation/create requests from a server tool via
ClientGateway::elicit(), but the client could not receive them: there was
no request handler, and ElicitRequest was not registered in MessageFactory,
so incoming elicitation/create messages failed to parse.

This adds the client half, mirroring the existing Sampling pattern:

- Register ElicitRequest in MessageFactory so elicitation/create parses.
- ElicitationRequestHandler wraps a user callback and returns an
  ElicitResult (accept/decline/cancel) or an Error.
- ElicitationCallbackInterface defines the callback contract.
- ElicitationException forwards a specific message to the server; any
  other throwable returns a generic error.

Includes unit tests, a runnable client example against the elicitation
demo server, and client docs/README updates.

* [Client] Simplify elicitation example labelFor() per review

Type the parameter as AbstractSchemaDefinition and read ->title directly
instead of matching on each concrete definition type. title is a required
non-null string on the base class, so the match and the $name fallback were
redundant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 23:55:06 +02:00
Dev
206aa9da52
[Schema] Fix Implementation::fromArray rejecting falsy-but-valid name/version (#393)
* [Schema] Fix Implementation::fromArray rejecting falsy-but-valid name/version

`empty()` treats the string "0" as empty, so a valid clientInfo/serverInfo
with `"version": "0"` (or `"name": "0"`) was rejected with an
InvalidArgumentException. During initialize this aborted request parsing and
surfaced as a misleading "A valid session id is REQUIRED" error.

Replace the `empty()` guards with explicit isset/is_string/empty-string checks
so genuinely missing, non-string, and empty values are still rejected while
"0" is accepted. Adds ImplementationTest covering the regression and the
existing validation paths.

Fixes #392

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [Schema] Suppress PHPStan argument.type on invalid-input tests

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 23:41:51 +02:00
dependabot[bot]
12f4a78813
chore(deps): bump actions/setup-node from 6 to 7 (#394)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 16:03:48 +02:00
Valery Gutu
55f7884368
[Server] Allow pre-built instance handlers in element registration (#375)
* Allow pre-built instance handlers in element registration

`HandlerResolver::resolve()` rejected `[$instance, 'methodName']` array
handlers even though the `Handler` type alias and the runtime invoker
already support them, so `Server::builder()->addTool(handler: [$obj, 'bar'])`
threw "Invalid array handler format" at build time.

Relax the array-shape guard to accept an object in slot 0 and normalize it
to its class name for reflection (the same idiom getHandlerDescription()
already uses). This unblocks dependency-injected handler objects that the
container-less `new $className()` fallback cannot build.

Fixes #369

* Use 0.6.1 changelog heading and test instance handler end-to-end

This is a bugfix with no new public API or BC break, so the changelog
entry belongs under 0.6.1 rather than a new 0.7.0 section.

Add an end-to-end test that registers a pre-built instance handler with a
constructor dependency the container-less new $className() fallback cannot
satisfy, then drives a real tools/call through CallToolHandler and asserts
the injected instance is the one invoked.

* Address review: reject Closure array handlers, fix abstract-method message

- HandlerResolver: extract the array-handler guard into a readable
  $hasValidTarget check that accepts strings and objects but excludes
  \Closure, so [$closure, 'method'] falls through to the format error
  instead of the misleading "method not found in class Closure".
- Fix the abstract-method rejection message ("must be abstract" ->
  "must not be abstract"), a pre-existing typo in the same branch.
- Tighten the CHANGELOG entry wording.
- Cover Closure rejection with a regression test.

* Move instance-handler entry under 0.7.0 changelog heading

Per review: registering pre-built instance handlers is a feature, not a
patch, so it belongs in a minor release heading.
2026-07-06 01:13:41 +02:00
Antoine Bluchet
1587ac5bd2
docs(adr): scope OAuth to resource server, not authorization server (#382)
Record ADR 0001: the MCP server is an OAuth 2.1 Resource Server that MAY
delegate to an upstream authorization server, and will NOT issue tokens or
act as an Identity Provider. Gives a single citable ruling to decline the
recurring "native authorization server" PRs (e.g. #373).

Also remove the surface signal that invites them: reframe the
OAuthProxyMiddleware class docblock to lead with its delegation intent and
state explicit non-goals (never mints/signs/stores/rotates tokens), and add
a "Scope" section to docs/authorization.md describing the three OAuth roles
and what the SDK does not do.

No behavior change — docblock, docs, and ADR only.
2026-07-06 00:37:19 +02:00
Antoine Bluchet
df5379c047
[Server] Harden JSON-RPC input parsing against malformed input and resource exhaustion (#383)
* fix: harden JSON-RPC input parsing against malformed input and DoS

MessageFactory::create() detected batches via $input[0] === '{', which
mishandled valid-but-non-object JSON: a scalar ("5") or leading-whitespace
object silently returned [] (with a PHP warning) or could reach a foreach
over a non-array. It also had no bound on batch size, letting one small
request expand into arbitrarily many operations (amplification).

- Detect single-vs-batch from the decoded JSON type (object vs list array)
  instead of the raw first byte; reject scalars and empty payloads as
  InvalidInputMessageException rather than crashing or no-oping.
- Cap batch size (maxBatchSize, default 100) and reject oversized batches.
- Reject non-object batch elements as InvalidInputMessageException instead
  of letting them hit a TypeError.
- Bound the StreamableHttpTransport POST body (maxBodyBytes, default 4 MiB),
  returning 413, with an incremental read so unknown-size/chunked bodies
  cannot exhaust memory.

* docs: document input-hardening limits in CHANGELOG and transports guide

Add CHANGELOG entries and docs/transports.md sections for the JSON-RPC
batch size cap (maxBatchSize) and the StreamableHttpTransport request
body size cap (maxBodyBytes), plus the type-based single-vs-batch
message detection.
2026-07-06 00:10:13 +02:00
Nariman Gardi
00a568e8d9
[Server] Fix completion/complete crash for plain ref/resource (#388)
A non-template resource resolves to a ResourceReference, which has no completionProviders property; only prompts and resource templates do. Read providers from those instead, returning an empty completion result when a resource has none.
2026-07-06 00:09:45 +02:00
dependabot[bot]
30e520a019
chore(deps): bump actions/checkout from 6 to 7 (#385)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 19:18:40 +02:00
Christopher Hertel
ca849aa75b
Preserve custom domain on docs deploy (#374) 2026-06-19 13:48:48 +02:00
Christopher Hertel
433c84b58a
Document v0.6.0 changes in CHANGELOG and docs (#311)
Wrap-up for the v0.6.0 release: capture features/BC breaks merged since
v0.5.0 that were missing from documentation.

- CHANGELOG: add the `title` field on `Resource`/`ResourceTemplate` (#301),
  a feature plus three positional-argument BC breaks.
- mcp-elements.md: add the missing `title` parameter to the McpTool,
  McpResource, McpResourceTemplate and McpPrompt attribute reference
  sections; fix a missing closing paren in an `#[McpPrompt]` example.
- server-builder.md: add `title?` to the `addPrompt()` row of the method
  reference table (stale since v0.5.0).
- client.md: bump the protocol-version example to `V2025_11_25` to match
  the new default.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 17:47:04 +02:00
Christopher Hertel
d150772e24
[Schema][Server] Add MCP Apps extension (io.modelcontextprotocol/ui) support (#281)
* feat: add MCP Apps extension (io.modelcontextprotocol/ui) support

Adds support for the MCP Apps protocol extension, which lets servers
expose interactive HTML UIs as resources rendered by spec-conforming
clients (e.g. Goose) in sandboxed iframes.

- Generic extension plumbing: ServerExtensionInterface and
  Builder::enableExtension(), with extensions advertised under
  capabilities.extensions during the initialize handshake.
  ClientCapabilities/ServerCapabilities gain an extensions field.
- Schema DTOs under Mcp\Schema\Extension\Apps: the McpApps marker,
  UiToolMeta, ToolVisibility enum, UiResourceContentMeta,
  UiResourceCsp, and UiResourcePermissions (with empty-object
  presence markers per spec).
- A worked weather example under examples/server/mcp-apps/ with a
  minimal spec-conforming view (ui/initialize handshake, postMessage
  JSONRPCMessage objects, and ResizeObserver-driven size reporting).
- docs/extensions.md guide with pointers from index.md and
  examples.md to the ext-apps repository for the TypeScript SDK and
  richer view-side patterns.
- Unit and inspector snapshot test coverage.

Spec: https://github.com/modelcontextprotocol/ext-apps

* Type UiToolMeta visibility as ToolVisibility enum

Also fix UiResourceContentMeta::jsonSerialize() return phpdoc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address review nitpicks for MCP Apps extension

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Apply PR review feedback for MCP Apps extension

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add regression tests and CHANGELOG entry for capabilities extensions

- Pin the is_array() guards on extensions in ServerCapabilities and
  ClientCapabilities::fromArray() with explicit non-array payload tests.
- Pin the isset()-rejects-null contract in UiResourcePermissions::fromArray().
- Surface the new extensions field on both capability classes in CHANGELOG.
- Inline the ?: idiom in Builder::build() to match the surrounding style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 09:35:54 +01:00
Mateu Aguiló Bosch
0347dc85b4
[Server] Allow runtime-resolved element handlers (#294)
* [Server] feat: allow runtime handlers

Adds Mcp\Server\Handler\RunTimeHandlerInterface so integrators
(e.g. configuration-driven element sources) can register handlers
whose arguments and execution are resolved at runtime rather than
via PHP reflection of a callable.

ReferenceHandler::handle() dispatches instanceof RunTimeHandler
ahead of the callable/array/string branches, calls filterArguments
to narrow the argument map, and forwards execute() a ClientGateway
built from the active session.

The Registry-layer surface (ElementReference and the four
*Reference subclasses, RegistryInterface, Registry::register*,
ArrayLoader's handler-description helper) is widened to accept the
new type. The Builder/ArrayLoader path keeps its narrow callable
union; exposing runtime handlers there requires reworking
HandlerResolver and is left for a follow-up.

* feat(server): add runtime handler metadata API

Extend RunTimeHandlerInterface with four nullable metadata
accessors (getInputSchema, getOutputSchema, getPromptArguments,
getCompletionProviders) so the loader can obtain element
metadata without reflection.

Add RunTimeHandlerTrait providing null defaults for all four
accessors so single-purpose handlers can override only the
methods relevant to their element kind.

* feat(server): expose runtime handlers via Builder

Widen Builder::addTool, addResource, addResourceTemplate, and
addPrompt to accept RunTimeHandlerInterface instances alongside
the existing closure/array/string handler forms.

Branch ArrayLoader::load() on instanceof RunTimeHandlerInterface
in each of the four registration loops so runtime handlers skip
the reflection-based HandlerResolver/HandlerSchema path and pull
metadata from the interface accessors instead. Require explicit
name and description for runtime handlers and raise
ConfigurationException when missing or when a runtime tool has
no input schema source.

Replace direct $data['handler'] use in error log context with
getHandlerDescription() to avoid serializing large runtime
handler object graphs into log records.

Document the runtime handler API in docs/server-builder.md with a
new Runtime Handlers subsection and add a cross-reference from
docs/mcp-elements.md.

* test(server): cover runtime handler Builder path

Add ArrayLoaderRunTimeHandlerTest covering:
- successful tool registration with handler-supplied input schema,
  with kwarg input schema (precedence over the handler), and the
  same precedence rule for output schema
- ConfigurationException for tools missing name, description, or
  any input schema source
- successful resource registration via Builder kwargs and the
  matching missing-name failure mode
- successful resource template registration with completion
  providers sourced from the handler and the missing-description
  failure mode
- successful prompt registration with arguments and completion
  providers sourced from the handler, plus the missing-name
  failure mode
- a trait-only fixture verifying the four metadata accessors
  return null by default

Update existing ReferenceHandlerTest anonymous classes to use
RunTimeHandlerTrait so they satisfy the widened interface.

* refactor(server): split runtime handler interface

Splits RunTimeHandlerInterface into a minimal base contract (only
execute()) plus element-specific subtypes: RunTimeToolHandlerInterface,
RunTimePromptHandlerInterface, and
RunTimeResourceTemplateHandlerInterface. Each subtype declares only
the metadata accessors relevant to its element kind, so a tool
handler no longer has to no-op out prompt or template methods.

Drops the now-redundant RunTimeHandlerTrait and removes
filterArguments; execute() receives the full argument map directly.

Updates ArrayLoader, ReferenceHandler, and Builder type signatures
to use the per-element interfaces, and rewrites the Runtime
Handlers documentation accordingly.

* refactor(server): rename runtime handler interfaces to Runtime casing

Renames RunTimeHandlerInterface and its element-specific subtypes to
Runtime casing, adds a dedicated RuntimeResourceHandlerInterface so
addResource() rejects mismatched runtime handlers at the type level,
and strips _session and _request from arguments forwarded to runtime
handlers. Hardens ArrayLoader to re-throw ConfigurationException
without wrapping, and extracts inline test fixtures into
tests/Fixtures/Runtime/.

* chore: please the linting gods

* test(server): use Uuid::v4 for symfony/uid 5.4 compatibility

Uuid::v7() was added in symfony/uid 6.2 and breaks the Symfony 5.4
matrix job. Match the convention used elsewhere in the suite.

* refactor(server): split ArrayLoader registration

Extract per-element preparation into dedicated private prepare*
methods for tools, resources, templates, and prompts. Factor the
shared required-field assertion and reflected name/description
resolution into helpers.

* refactor(server): non-null runtime handler types

Drop nullable return types on RuntimeToolHandlerInterface::
getInputSchema(), RuntimePromptHandlerInterface::
getPromptArguments()/getCompletionProviders(), and
RuntimeResourceTemplateHandlerInterface::
getCompletionProviders() in favor of plain arrays.
ArrayLoader no longer needs the null-coalescing
fallbacks, and NullSchemaToolHandler plus its
ConfigurationException test become redundant.

* doc: update docs/server-builder.md

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>

* feat: peer revie feedback

Introduce a new `Builder::add(...)` pattern for a more generic runtime
handler.

* refactor(server): apply PR 294 review feedback

Move per-interface handler dispatch from ReferenceHandler into
ExplicitElementLoader by wrapping each ElementHandlerInterface in a
Closure that satisfies the existing callable contract, revert the
*Reference / Registry handler-type widening PR 294 introduced, throw
Mcp\Exception\InvalidArgumentException (not \TypeError) on mismatched
Builder::add() pairings, and rename two classes for clarity:

* Mcp\Capability\Registry\Loader\ArrayLoader -> ReflectedElementLoader
* Mcp\Schema\Resource -> Mcp\Schema\ResourceDefinition (no alias)

ReferenceHandler::prepareArguments() now injects ClientGateway by type
and binds an `array $arguments` / `array $variables` closure parameter
from the leftover inbound arguments. This addresses Antoine's coupling
concern (ReferenceHandler no longer knows about the four handler
interfaces) and Chris's package-exception preference, and clears the
phpdoc reserved-word collision Antoine flagged on the resource schema
class. Existing tests are re-targeted to exercise the closure path
through ExplicitElementLoader / ReferenceHandler.

* doc: address PR 294 review feedback

Apply Chris's line-level suggestions on docs/server-builder.md
(rewrap to 120 chars, smoothed Drupal mcp module name-drop in the
explicit-element registration section, exception name reads
Mcp\Exception\InvalidArgumentException), rewrap docs/mcp-elements.md
to 120 chars, and add 0.6.0 CHANGELOG entries covering the
Builder::add() entry point, the new handler interfaces, the new
ExplicitElementLoader, plus three BC breaks (Resource ->
ResourceDefinition rename, ArrayLoader -> ReflectedElementLoader
rename, mismatched-pairing exception swap).

* refactor: address self-review feedback

* CHANGELOG: drop spurious [BC Break] entry for the exception
  type Builder::add() raises (method is new in this PR).
* docs/server-builder: revert wrap-only edits to pre-existing
  main content; keep 120ch wrapping only for new sections.
* ExplicitElementLoader: trim verbose class docblock.
* ReferenceHandler: rename SPLAT_PARAMETER_NAMES constant to
  ARGUMENT_BAG_PARAMETERS.

* refactor: rename CallableHandler to Handler

* style: fix phpdoc param alignment

* refactor: inline $reference->handler access

* fix: maintainer review feedback

docs: mention ExplicitElementLoader in comment

docs: move Drupal reference to README list

docs: drop implementation-only changelog entry

docs: revert rewrap noise from mcp-elements.md

* style: fix phpdoc param alignment

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
Co-authored-by: soyuka <soyuka@users.noreply.github.com>
2026-05-29 07:16:12 +02:00
Volodymyr Panivko
293a5880e4
[Server] Compose StreamableHttpTransport security middleware via defaultMiddleware() factory (#307)
* [Server] Add CORS, DNS rebinding and protocol version middleware

Introduce three PSR-15 middleware for `StreamableHttpTransport` exposed
through a public `StreamableHttpTransport::defaultMiddleware()` factory
composed automatically when no middleware is passed.

- `CorsMiddleware`: secure-by-default (no `Access-Control-Allow-Origin`),
  configurable allowlist, reflects matching origin with `Vary: Origin`
  to protect shared caches.
- `DnsRebindingProtectionMiddleware`: validates `Origin`/`Host` against
  a hostname allowlist (localhost-only by default).
- `ProtocolVersionMiddleware`: rejects requests carrying an unsupported
  `Mcp-Protocol-Version` header with `400 Bad Request`.

The transport no longer applies CORS via an `instanceof + array_unshift`
post-hook; the middleware parameter is nullable — `null` installs the
secure defaults, `[]` disables them, and users compose by spreading
`StreamableHttpTransport::defaultMiddleware()`. `SESSION_HEADER` and
`PROTOCOL_VERSION_HEADER` are promoted to public constants so middleware
can reuse them.

BC breaks:
- The `corsHeaders` constructor parameter is removed; the `middleware`
  parameter shifts one position. Positional callers passing the old
  `corsHeaders` argument must switch to named arguments or drop it.
- Default `Access-Control-Allow-Origin` is no longer `*`.

Addresses #260 (DNS rebinding), #277 (CORS extraction) and #306
(protocol version validation).

* [Server] Address review feedback on middleware defaults

- CorsMiddleware: emit Access-Control-Allow-Methods/Headers only on
  preflight per CORS spec; tokenize Vary header to avoid substring
  false-positives; add allowCredentials flag that emits
  Access-Control-Allow-Credentials and throws when combined with
  wildcard origin.
- DnsRebindingProtectionMiddleware: return plain-text 403 body
  (HTTP-level rejection, not JSON-RPC); use parse_url(..., PHP_URL_HOST)
  for origin parsing; drop redundant unbracketed '::1' from default
  allowlist.
- ProtocolVersionMiddleware: use Error::forInvalidParams instead of
  forInvalidRequest -- JSON-RPC -32602 fits header-value rejection
  better.
- JsonRpcErrorResponse::create now accepts a JsonRpc\Error so callers
  choose the error code.
- StreamableHttpTransport: emit a warning log when an explicit empty
  middleware list is passed, so operators can spot accidental bypass
  of the default security stack.

* [Server] Fix CI: switch warning-log tests to PHPUnit mocks

Replace the anonymous AbstractLogger fixture in
StreamableHttpTransportTest with `$this->createMock(LoggerInterface::class)`
so the test does not declare a `log()` signature at all. That sidesteps
both the psr/log v1 LSP incompatibility (untyped parameter) and the
phpstan `missingType.parameter` complaint in one move, and matches how
the rest of the suite (CallToolHandlerTest etc.) fakes loggers.

* Update src/Server/Transport/Http/Middleware/CorsMiddleware.php

Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>

---------

Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
2026-05-25 13:49:43 +02:00
dependabot[bot]
ee40ff523f
chore(deps): bump actions/download-artifact from 4 to 8 (#330)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 08:48:16 +02:00
dependabot[bot]
d2fc743553
chore(deps): bump actions/upload-artifact from 4 to 7 (#331)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 08:47:44 +02:00
Christopher Hertel
f12ea0ab58
Fix conformance score calculcation (#313) 2026-05-19 14:40:17 +02:00
Christopher Hertel
3e45b1f486
Streamline README and add conformance score badges (#312)
- Add a shields.io badge row to the README header (version, CI, PHP
  version, license, conformance scores, protocol/spec) to match the
  other official MCP SDKs.
- Expand "PHP Libraries Using the MCP SDK" into a curated, alphabetically
  sorted list of downstream projects.
- Add tests/Conformance/bin/score.php, a Symfony Console command that
  turns a conformance run's --output-dir into a shields.io endpoint JSON.
- Extend the weekly conformance workflow to score each run and publish
  the client/server pass-rate to the orphan `badges` branch that the
  README badges read.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:02:26 +02:00
Christopher Hertel
1300d2ee7d
Add title parameter to Builder::addResource() and addResourceTemplate() (#310)
#301 added the `title` field to the `Resource`/`ResourceTemplate` schema
classes and the `McpResource`/`McpResourceTemplate` attributes, but the
manual registration methods on `Builder` were never updated — so manual
registration could not set a resource title, unlike tools and prompts.

Add a `$title` parameter between `$name` and `$description` (matching
`addTool()`/`addPrompt()`) and propagate it through `ArrayLoader` when
constructing the `Resource`/`ResourceTemplate` objects.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:02:01 +02:00
Dayna Blackwell
a820497b62
Add title field to Resource and ResourceTemplate (#301)
The MCP specification (2025-11-25) defines an optional title field for
Resource and ResourceTemplate as a human-readable display label, distinct
from name (a short identifier). Tool and Prompt already implement this
field; Resource and ResourceTemplate were missing it.

Changes:
- Add title parameter to Resource and ResourceTemplate constructors
- Add title to fromArray(), jsonSerialize(), and PHPStan type arrays
- Add title to McpResource and McpResourceTemplate attributes
- Pass title through Discoverer when constructing schema objects
- Add tests for deserialization, serialization, and null omission

Fixes #296
2026-05-17 14:10:31 +02:00
Christopher Hertel
967c5cda4d
[Server] Untangle origin tracking from Registry (#302)
Drop the isManual flag from ElementReference and the discovery-state methods
from RegistryInterface. The Registry becomes a flat last-write-wins map;
DiscoveryLoader owns its own owned-set bookkeeping so re-running discovery
only removes elements it itself contributed. Adds ChainLoader for explicit
composition. Manual-over-discovered precedence is preserved via loader
ordering, not a flag on the reference.
2026-05-17 14:03:31 +02:00
Christopher Hertel
e00450a218
Bump protocol version to nov 25 (#308) 2026-05-17 14:02:40 +02:00
Antoine Bluchet
1d7100060f
[CI] Add weekly conformance run against latest upstream release (#304)
The PR pipeline only catches conformance changes when someone opens a
PR. Upstream `@modelcontextprotocol/conformance` releases between PRs
go untested. This adds a weekly cron + `workflow_dispatch` trigger
pinned to `@latest`, and opens a tracking issue on scheduled failures.
2026-05-17 14:02:19 +02:00
Christopher Hertel
5e0731f706
Add configurable session garbage collection (gcProbability/gcDivisor) (#258)
Make session GC probability configurable via gcProbability and gcDivisor
parameters, mirroring PHP's session.gc_probability/session.gc_divisor.
Exposed through both SessionManager constructor and Builder::setSession().

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-15 08:14:38 +02:00
Sven Münnich
73414d65f6
Add PSR-3 compliant exception context when logging exceptions (#300)
* Add PSR-3 exception context to caught error logs

* Apply review feedback
2026-05-12 23:20:42 +02:00
Christopher Hertel
bfc4728792
Fix case sensitivity for http header (#283) 2026-05-12 23:20:14 +02:00
SBNTT
06917f6af6
fix: Cannot pass null to nullable enum parameters (#299)
* nullable enum value

* fix tests
2026-05-08 10:15:14 +02:00
Dayna Blackwell
797472777f
[Schema] fix: allow URIs without double slash in Resource and ResourceTemplate (RFC 3986) (#297)
* fix: allow URIs without double slash in Resource and ResourceTemplate (RFC 3986)

The URI regex required "://" after the scheme, rejecting valid URIs
like urn:isbn:123, mailto:user@example.com, config:key, and data:
URIs. RFC 3986 only requires ":" after the scheme; the "//" is part
of the authority component which is optional.

Changed both Resource.URI_PATTERN and ResourceTemplate.URI_TEMPLATE_PATTERN
to require ":" instead of "://".

Adds data-provider tests for URIs without double slash in both classes.

Fixes #293

* Reword unit tests

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-05-08 10:14:08 +02:00
johnhunt-lc
a929548f33
force application/json for the 202 if there are no messages in the queue when building a json response (#295) 2026-05-08 09:56:36 +02:00
johnhunt-lc
86bd845924
[Server] Allow overriding the default file name pattern for Discovery (#292)
* Add optional namePatterns capability to discovery so that the default scan for `*.php` files can be overridden.

* add default to namePatterns docs

* add an error check if we manage to pass in an empty list of patterns

* add unit tests (and a static entry to classmap for composer.json so it autoloads from a non .php file for the test)

* run php-cs-fixer

* change how default namePatterns are set and make sure all the annotations are correct

* Fix bad whitespace

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>

* switch to a const default instead of nullable with a check in the function to convert null to a default

* make sure we handle empty arrays

* formatting fixes

* Apply suggestion from @chr-hertel

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>

* Add changelog entry

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-05-08 09:48:29 +02:00
Mateu Aguiló Bosch
fb2c8c2ee4
[Schema][Server] Add top-level title to Tool (#288)
* feat(schema): add top-level title to Tool

Mirror PR #278's Prompt change for Tool:

- Add optional ?string $title to Mcp\Schema\Tool between $name
  and $inputSchema. fromArray() now reads $data['title'];
  jsonSerialize() emits 'title' right after 'name'. The
  @phpstan-type ToolData gains title?: string.
- Add ?string $title to the #[McpTool] attribute between $name
  and $description.
- Note ToolAnnotations::$title as deprecated-for-display in
  favor of Tool::$title per MCP spec 2025-06-18 (PHPDoc only,
  field retained for BC).

Phase 1 of plan 01--tool-title-spec-compliance. Later phases
update Discoverer, Builder::addTool(), ArrayLoader, and the
conformance fixture to match the new signature.

* feat(server): thread title through discovery

- Discoverer::processMethod() now propagates McpTool::$title
  into every discovered Tool via named arguments.
- Builder::addTool() accepts ?string $title between $name and
  $description. [BC Break] positional callers passing the old
  $description as 3rd positional argument must switch to named
  arguments, matching the precedent set for addPrompt() in
  0.5.0.
- tests/Conformance/server.php migrated to named-argument form
  for every addTool() call.

Phase 2 of plan 01--tool-title-spec-compliance.

* feat(loader): forward title in ArrayLoader

ArrayLoader now reads 'title' from each tool config entry and
passes it into the Tool constructor, completing the wiring
from Builder::addTool() through array-driven registration.

The $tools @param array-shape typedef gains title: ?string
between name and description.

Phase 3 of plan 01--tool-title-spec-compliance.

* test(schema): cover Tool.title end-to-end

- New tests/Unit/Schema/ToolTest.php: constructor round-trip,
  jsonSerialize key-order when title set vs null, fromArray
  tolerance.
- New tests/Unit/Capability/Discovery/DiscovererToolTitleTest
  plus a TitleFixture class verifying McpTool(title: ...)
  propagates through discovery into the registered Tool.
- New tests/Unit/Capability/Registry/Loader/ArrayLoader...
  test verifying array-driven title propagation.
- Extend McpToolTest for title named-arg and default-null.
- Extend BuilderTest with addTool(title: 't') propagation.
- Update tests/Unit/.../RegistryTest, CallToolHandlerTest,
  ListToolsHandlerTest and examples/.../server.php
  new Tool(...) call sites to pass title (named args) so
  they match the new constructor signature.
- CHANGELOG 0.6.0 section: feature bullet + [BC Break]
  note for addTool() signature, mirroring 0.5.0 Prompt
  entries.
- docs/server-builder.md: addTool() row and prose now
  list title? between name and description.

Phase 4 of plan 01--tool-title-spec-compliance. All 730
phpunit tests pass; remaining phpstan warnings are
pre-existing and unrelated to Tool.title.

* refactor(schema): make Tool.title non-BC-breaking

Move `title` to the end of the Tool, Builder::addTool, and McpTool
attribute constructors with a default of null. This preserves
positional-argument compatibility and removes the need for
`title: null` boilerplate at call sites. JSON serialization order
(name, title, inputSchema, ...) is unchanged.

Also tighten the new tests: parameterize ToolTest with data
providers, fold the Discoverer title fixture into the existing
DiscoverableToolHandler, and drop the reflection-heavy Builder
test that overlapped with ArrayLoaderToolTitleTest.

* fix: move title after name

---------

Co-authored-by: DDEV User <nobody@example.com>
2026-04-26 15:37:40 +02:00
Christopher Hertel
97979ec48d
CS update after release of v3.95 (#282) 2026-04-26 12:39:27 +02:00
Christopher Hertel
abae36e888
[Client] add client conformance test suite (#279)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 12:33:21 +02:00
Sergei Predvoditelev
d5b00cd8ae
Fix PHPDoc type of GetPromptRequest::$arguments (#285) 2026-04-20 21:13:58 +02:00
Mateu Aguiló Bosch
73e4cc2456
style: apply php-cs-fixer and PHPStan fixes (#289)
* style: apply php-cs-fixer to test files

Addresses drift flagged by the default PHP-CS-Fixer config — mostly
adds missing `: void` return types on pre-existing test methods.

No behavioural changes.

* fix: resolve phpstan errors surfaced by qa

- Drop the obsolete `Method ::test*() has no return type specified`
  ignore rule — every test method now declares a return type, so
  the pattern never matches and phpstan reports it as unmatched.
- Remove `?? []` from two reads of `CallToolRequest::$arguments`;
  the property is typed `array` (non-nullable), so the coalesce is
  a no-op that phpstan flags.

---------

Co-authored-by: DDEV User <nobody@example.com>
2026-04-20 20:49:15 +02:00
Christopher Hertel
7af5a345c1
Add title field to Prompt for MCP spec compliance (#278)
The MCP 2025-11-25 specification defines an optional `title` property on
Prompt for human-readable display in UI contexts, distinct from the
programmatic `name` identifier. This was missing from the SDK.

BC Break: Builder::addPrompt() signature changed — $title parameter added
between $name and $description. Callers using positional arguments for
$description must switch to named arguments.

Closes #276
2026-04-14 10:04:35 +02:00
simon chrzanowski
863cfc1e78
[Server] feat: relax StrictOidcDiscoveryMetadataPolicy and add Dynamic Client Registration middleware (RFC 7591) (#269)
* feat: relax OIDC discovery policy and add Dynamic Client Registration middleware (RFC 7591)

- Add LenientOidcDiscoveryMetadataPolicy for providers that omit
  code_challenge_methods_supported (e.g. FusionAuth, Microsoft Entra ID)
- Keep StrictOidcDiscoveryMetadataPolicy RFC-aligned
- Add ClientRegistrationMiddleware handling POST /register and enriching
  /.well-known/oauth-authorization-server with registration_endpoint
- Update Microsoft example to use built-in LenientOidcDiscoveryMetadataPolicy

* fix: move metadata policy note under server.php in Files section

Address review comment: the LenientOidcDiscoveryMetadataPolicy bullet
was a behavioral note, not a file entry. Inline it into the server.php
description to keep the Files list consistent.

* fix: address Copilot review comments on ClientRegistrationMiddleware

- Decode JSON with assoc=true for registrar data so nested objects
  are associative arrays, not stdClass instances
- Add Cache-Control: no-store to all error responses (400), not just
  the success response (201)
- Rewind response body stream before returning unmodified response
  when metadata is not valid JSON, preventing empty body on read

* fix: harden ClientRegistrationMiddleware and improve test coverage

- Revert spurious .gitignore trailing newline
- Use JSON_THROW_ON_ERROR consistently in enrichAuthServerMetadata()
- Add comment clarifying the second json_decode in handleRegistration()
- Assert Cache-Control: no-store on registrar exception error response
- Add test for non-object JSON body in metadata enrichment path
- Extract createPlainTextHandler helper for non-JSON response tests

* fix: reject JSON array bodies in metadata enrichment

Treat JSON list arrays (e.g. [] or ["..."]) as invalid metadata in
enrichAuthServerMetadata() to prevent corrupting the response shape
by adding registration_endpoint to a non-object.

* fix: add RFC 7591 error codes, Content-Type validation, and docblock safety warning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:27:27 +02:00
Sule-Balogun Olanrewaju
5d57b685a2
Fix to let schema attribute enum type hint allow null values (#271) 2026-04-11 17:21:14 +02:00
Sule-Balogun Olanrewaju
ea38ef7b62
Fix to make Symfony Finder component optional (#267) 2026-04-11 17:19:47 +02:00
Kyrian Obikwelu
85fddf5f0f
[Client] docs: introduce client SDK documentation and improve README structure (#259)
* docs: add comprehensive client SDK documentation

- Create new client documentation with examples and API reference
- Restructure README for better clarity and flow
- Add client examples to documentation index
- Update roadmap to mark client component as complete

* Update README.md

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>

* Apply suggestions from code review

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-03-31 21:37:32 +02:00
Christopher Hertel
7a5d5f5cfb
Add soyuka to codeowners to make approvals count (#272) 2026-03-23 11:52:00 +01:00
Christopher Hertel
e8f5a8ba22
Extend elicitation enum schema compliance (#261) 2026-03-22 18:46:40 +01:00
dependabot[bot]
6a99196ca0
chore(deps): bump ramsey/composer-install from 3 to 4 (#268)
Bumps [ramsey/composer-install](https://github.com/ramsey/composer-install) from 3 to 4.
- [Release notes](https://github.com/ramsey/composer-install/releases)
- [Commits](https://github.com/ramsey/composer-install/compare/v3...v4)

---
updated-dependencies:
- dependency-name: ramsey/composer-install
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 13:42:50 +01:00
simon chrzanowski
faac4a525c
[Server] add a reference handler setter to server builder (#265)
* feat: add setReferenceHandler to Builder

Allow consumers to provide a custom ReferenceHandlerInterface
implementation (e.g. a security-aware decorator) instead of
always using the default ReferenceHandler.

* test: add BuilderTest for setReferenceHandler

- Fluent API returns builder instance
- build() succeeds with and without custom handler
- Integration test verifies custom handler intercepts tool calls

* style: fix codestyle in BuilderTest

* style: use imports instead of inline FQCNs in BuilderTest

* docs: add changelog entry for setReferenceHandler
2026-03-13 14:39:13 +01:00
Christopher Hertel
eac6f01d58
Improve middleware pipeline consistency (#262)
- Make MiddlewareRequestHandler final and use index counter instead of
  array_shift to avoid mutating internal state
- Make OAuthProxyMiddleware factory discovery consistently lazy via
  getter methods, matching httpClient and requestFactory pattern

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:25:23 +01:00
dependabot[bot]
591abb4620
chore(deps): update phpdocumentor/reflection-docblock requirement || ^6.0 (#256)
Updates the requirements on [phpdocumentor/reflection-docblock](https://github.com/phpDocumentor/ReflectionDocBlock) to permit the latest version.
- [Release notes](https://github.com/phpDocumentor/ReflectionDocBlock/releases)
- [Commits](https://github.com/phpDocumentor/ReflectionDocBlock/compare/5.6.0...6.0.2)

---
updated-dependencies:
- dependency-name: phpdocumentor/reflection-docblock
  dependency-version: 6.0.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-10 23:52:01 +01:00
Christopher Hertel
ee3132a2f9
Add changelog entries for client and server auth (#257) 2026-03-10 22:52:59 +01:00
Christopher Hertel
ae320646bf
Encapsulate session handling from protocol to new SessionManager (#247) 2026-03-10 18:09:15 +01:00
dependabot[bot]
a6ed01bec0
chore(deps): bump actions/checkout from 5 to 6 (#255)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-10 17:26:45 +01:00
dependabot[bot]
085e8d9ad6
chore(deps): bump actions/setup-node from 5 to 6 (#254)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-10 17:23:15 +01:00
Kyrian Obikwelu
3ef864c444
[Client] Feat: Implement MCP client component (#192)
* feat(client): add complete MCP client SDK with STDIO and HTTP transports

Add Client, Builder, ClientProtocol, ClientSession, and transport
implementations for communicating with MCP servers. Supports tools, resources, prompts, and real-time progress/logging notifications.

* refactor(examples): reorganize examples into server/ and client/ folders

* feat: add SamplingRequestHandler for handling server sampling requests

* feat: add setLoggingLevel method to control server logging verbosity

* refactor: make progress token dependent on request ID

* docs: Update server IP in usage instructions and add notes on sampling server requirements.

* feat: add complete() method for completion/complete requests

* refactor: properly parse and type initialize result

* refactor: standardize method names

* fix: revert changes to server related components

* fix: revert changes to server related components

* refactor: Restructure client transports and handlers into dedicated namespaces.

* fix: cs and php-stan errors fixes(partial)

* feat: Refactor request handlers to return `Response` or `Error` objects directly, adding a `SamplingException` and error logging for sampling requests.

* feat: Replace `ClientTransportInterface` with `TransportInterface` and move response result deserialization to client methods.

* refactor: remove redundant timeout check for intialize on transports

The processFiber that executes per tick while fiber is suspended already handles timeouts while waiting

* feat: broaden symfony/http-client dependency to support Symfony 5.4 to 8.0

* Update examples/client/README.md

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>

* Update examples/client/README.md

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>

* refactor: remove outgoing message queue from session and simplify transport send method to directly dispatch messages.

* refactor: Replace ClientSession with ClientState for clearer intent on runtime state management

* refactor: rename connectAndInitialize to connect for consistency

* chore: cs fix

* chore: use SamplingCallbackInterface instead of a closure for sampling request handler

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-03-10 17:22:40 +01:00
Christopher Hertel
a5a738396d
Add dependabot configuration for composer and github actions (#248) 2026-03-10 17:11:17 +01:00
Christopher Hertel
528cff269f
Add basic roadmap (#249) 2026-03-10 17:10:58 +01:00
Volodymyr Panivko
e3f4b88768
[Server] Oauth2 based on middleware (#221)
* Add Middleware handlers to StreamableHttpTransport

* OAuth Implementation based on middleware

* OAuth Implementation based on middleware

* Add Middleware handlers to StreamableHttpTransport

* OAuth Implementation based on middleware

* OAuth Implementation based on middleware

Signed-off-by: Volodymyr Panivko <sveneld300@gmail.com>

* Address PR review feedback for OAuth middleware

* Apply php-cs-fixer to satisfy QA

* Use Mcp exception classes in OAuth middleware

* Refactor OAuth HTTP stack and add Microsoft example policies

* Fix QA PHPStan findings in OAuth examples and auth middleware

* Require OIDC discovery in JWKS provider and refine OAuth proxy behavior

* Remove local development docker artifacts from VCS

* Revert .gitignore changes for local docker files

* Fix body retrieval in OAuthRequestMetaMiddleware for proper string conversion

Signed-off-by: Volodymyr Panivko <sveneld300@gmail.com>

* Address OAuth PR review feedback and align example configs

* Fix QA style issues and add authorization docs

* Fix PHPStan typing issues in OAuth middleware

* Remove duplicate Middleware namespace, consolidate under Http\Middleware

Remove old Mcp\Server\Transport\Middleware\ classes that were duplicated
in Mcp\Server\Transport\Http\Middleware\ and Http\OAuth\. Update docs
to reference the correct namespaces.

* Address PR review: replace Throwable catch with specific exceptions, add JSON_THROW_ON_ERROR

- JwtTokenValidator: replace catch(\Throwable) with explicit
  \InvalidArgumentException|\UnexpectedValueException|\DomainException,
  letting RuntimeException from JwksProvider propagate as infrastructure errors
- OAuthProxyMiddleware: add \JSON_THROW_ON_ERROR to json_encode for metadata response
- Update tests to expect RuntimeException for JWKS infrastructure failures

---------

Signed-off-by: Volodymyr Panivko <sveneld300@gmail.com>
Co-authored-by: Volodymyr Panivko <volodymyr.panivko@togethernetworks.com>
2026-03-10 17:10:07 +01:00
Antoine Bluchet
61c20f1450
fix(session): initialize data lazily in save() to prevent uninitialized property access (#252) 2026-03-05 00:07:49 +01:00
Jonathan Renard
a98e01a4be
chore: remove final keyword on attributes classes (#240)
Co-authored-by: Jonathan Renard <jRenard@PRESTAMAC-175.local>
2026-03-01 21:54:03 +01:00
Edouard Courty
1f5f7e16a3
chore(http): create constant for session id header (#246) 2026-02-23 22:42:54 +01:00
Edouard Courty
158e64627f
feat(server): add server lifecycle events (#175)
Signed-off-by: Edouard Courty <edouard.courty2@gmail.com>
2026-02-23 22:40:43 +01:00
Den Delimarsky
04a9a16be2
Add SECURITY.md with GitHub Security Advisories guidance (#250) 2026-02-22 19:20:00 +01:00
Sule-Balogun Olanrewaju
2166b38d56
Add missing handler for resource subscribe and unsubscribe (#220) 2026-02-18 00:11:35 +01:00
Johannes Wachter
02e86377e4
Add elicitation support for server-to-client user input requests (#228)
- Add ElicitAction enum (accept/decline/cancel)
- Add schema definitions for elicitation fields (string, number, boolean, enum)
- Add ElicitationSchema wrapper for requestedSchema
- Add ElicitRequest and ElicitResult for elicitation/create method
- Update ClientCapabilities to support elicitation capability
- Add elicit() method to ClientGateway
- Store client capabilities in session during initialization
- Add comprehensive unit tests
- Add elicitation example server with book_restaurant, confirm_action, and collect_feedback tools
2026-02-15 23:34:19 +01:00
Sule-Balogun Olanrewaju
aafac3ddf5
Feat: Improve invalid resource template messages (#245) 2026-02-13 21:46:03 +01:00
Alexis Lefebvre
0b1d0380c9
feat: test Symfony 8 (#243) 2026-02-13 18:03:43 +01:00
Alexis Lefebvre
6e97aafb9c
feat: improve invalid resource messages (#242) 2026-02-13 18:02:43 +01:00
Volodymyr Panivko
8f8af90007
Fix Invalid data in Session (#241)
Signed-off-by: Volodymyr Panivko <sveneld300@gmail.com>
2026-02-11 20:35:41 +01:00
Volodymyr Panivko
43e7a39a21
Add Middleware handlers to StreamableHttpTransport (#218) 2026-02-11 20:33:31 +01:00
Ridwan Kasim
f3ecb2ebd8
[Server] Fix HTTP Transport Header Compliance (#227)
* fix: added the Allow header for bad method status code

* fix code style

---------

Co-authored-by: Tobias Nyholm <tobias.nyholm@gmail.com>
2026-02-08 15:58:13 +01:00
Christopher Hertel
c19d550c27
[Server] Shift away session decode from constructor (#238)
* Shift away session decode from constructor

* add a small test

---------

Co-authored-by: Tobias Nyholm <tobias.nyholm@gmail.com>
2026-02-08 15:49:14 +01:00
Christopher Hertel
6503059a4b
Throw package specific exceptions in CallbackStream (#233) 2026-02-08 15:42:10 +01:00
Christopher Hertel
9621039e54
Rename Psr16StoreSession to Psr16SessionStore for consistency (#236) 2026-02-08 15:31:19 +01:00
Christopher Hertel
77d6433e4b
Extend gitattributes for latest additions on root level (#237) 2026-02-08 15:30:29 +01:00
Jonathan Hefner
940eb90872
Add API documentation generation with phpDocumentor (#184)
* 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>
2026-02-01 16:22:40 +01:00
Christopher Hertel
2fe982505f
Switch pipeline to conformance baseline (#230) 2026-02-01 11:59:44 +01:00
Christopher Hertel
c081b90d74
Update codestyle based on updated Symfony rules (#229) 2026-02-01 11:54:31 +01:00
Sule-Balogun Olanrewaju
e485fde727
Refactor the schema tool array to use normalizeSchemaProperties method (#223) 2026-01-18 23:38:16 +01:00
Mateu Aguiló Bosch
ab4f686e18
refactor: put formatters into dedicated classes (#224)
Split formatting logic from Reference classes into dedicated formatter classes:
- ToolResultFormatter: Formats tool execution results into Content items
- PromptResultFormatter: Formats prompt results into PromptMessage items
- ResourceResultFormatter: Formats resource results into ResourceContents items

This refactor:
- Eliminates 122 lines of code duplication between ResourceReference and
  ResourceTemplateReference (identical formatting logic)
- Improves code organization by separating formatting concerns from
  capability reference management
- Makes formatters independently testable and reusable
- Reduces Reference classes by ~450 lines while maintaining all functionality

Changes:
- Create src/Capability/Formatter/* with three formatter classes
- Update Reference classes to delegate to formatters instead of inline logic
- Add comprehensive test coverage for all formatter classes
- All existing tests pass; no behavioral changes
2026-01-18 22:17:44 +01:00
adam jones
48af7bd547
chore: update licensing to Apache 2.0 for new contributions (#222) 2026-01-18 21:29:44 +01:00
Tobias Nyholm
42fe1430ce
make static methods in Error final (#204) 2026-01-11 22:16:27 +01:00
Tobias Nyholm
8b57fdf3d0
Add $runnerControl parameter to StdioTransport constructor (#205)
* Add $runnerControl parameter to StdioTransport constructor

* fix cs

* fix phpstan baseline

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-01-11 22:12:33 +01:00
落月
2c16b3e084
[Server] Use getDescription to get all descriptions. (#188)
* Remove `getSummary` method and update descriptions in snapshots and code

* Add changelog entry

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-01-11 22:08:05 +01:00
Antoine Bluchet
7cb29125bd
Add interface abstractions for SchemaGenerator and Discoverer with DI improvements (#215)
* schema generator injection

* add interface for schema generator

* improve discoverer dependency

* remove some comments

* make some classes final

* simplify condition

* code review

* Extend changelog for covering interfaces

* Adopt SchemaGeneratorInterface after rebase and #153

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-01-11 22:03:43 +01:00
落月
1de4ea933f
Rename schema to resource in Capability and related classes (#217) 2026-01-11 21:52:04 +01:00
Tobias Nyholm
ed205ef793
Use SchemaValidator to validate tool input (#203)
* Use SchemaValidator to validate tool input

* add test

* add changelog

* fix cs

* Improve error message

* fix tests

* fix tests

* fix baseline

* update snapshot files

* Remove BC compat while switching to v0.3.0

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-01-11 21:46:53 +01:00
Sule-Balogun Olanrewaju
c0938a19bd
feat: Add output schema support to MCP tools (#153) 2026-01-11 21:14:48 +01:00
Kyrian Obikwelu
4c3e5e61c7
fix: use Docker PHP-FPM to support sampling in conformance tests (#219) 2026-01-02 15:59:56 +01:00
落月
96ab01712b
Add support for binary resource in conformance server example (#216) 2025-12-31 00:55:04 +01:00
606 changed files with 55557 additions and 4604 deletions

9
.gitattributes vendored
View file

@ -1,7 +1,12 @@
/.git* export-ignore
/.github export-ignore
/.phpdoc export-ignore
/docs export-ignore
/examples export-ignore
/tests export-ignore
/.php-cs-fixer.dist.php export-ignore
/Makefile export-ignore
/mkdocs.yml export-ignore
/requirements-docs.txt export-ignore
/phpdoc.dist.xml
/phpstan* export-ignore
/phpunit.xml.dist export-ignore
/Makefile export-ignore

2
.github/CODEOWNERS vendored
View file

@ -1 +1 @@
* @chr-hertel @Nyholm @CodeWithKyrian
* @chr-hertel @Nyholm @CodeWithKyrian @soyuka

11
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: composer
directory: /
schedule:
interval: weekly
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly

View file

@ -0,0 +1,189 @@
name: conformance-weekly
# Tracks the newest published @modelcontextprotocol/conformance; the
# on:pull_request pipeline pins an exact version instead. Also publishes
# score badges to the orphan `badges` branch, consumed by the README.
on:
schedule:
- cron: '0 6 * * 1' # Mondays 06:00 UTC
workflow_dispatch:
permissions:
contents: write
issues: write
jobs:
server:
name: conformance / server (${{ matrix.spec-version }}, ${{ matrix.dist-tag }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- spec-version: '2025-11-25'
dist-tag: latest
path: '/'
baseline: conformance-baseline-2025-11-25.yml
- spec-version: '2026-07-28'
dist-tag: alpha
path: '/'
baseline: conformance-baseline-2026-07-28.yml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
- run: composer install --prefer-dist --no-progress --no-interaction
- name: Start conformance server
run: |
mkdir -p tests/Conformance/sessions tests/Conformance/logs
chmod -R 777 tests/Conformance/sessions tests/Conformance/logs
docker compose -f tests/Conformance/Fixtures/docker-compose.yml up -d
sleep 5
- name: Run conformance tests
working-directory: ./tests/Conformance
run: >-
npx --yes @modelcontextprotocol/conformance@${{ matrix.dist-tag }} server
--url http://localhost:8000${{ matrix.path }}
--suite all
--spec-version ${{ matrix.spec-version }}
--expected-failures ${{ matrix.baseline }}
--output-dir results
- name: Generate score badge
if: always()
run: php tests/Conformance/score.php server ${{ matrix.spec-version }}
- name: Show docker logs on failure
if: failure()
run: docker compose -f tests/Conformance/Fixtures/docker-compose.yml logs
- name: Upload conformance results
if: failure()
uses: actions/upload-artifact@v7
with:
name: conformance-server-results-${{ matrix.spec-version }}
path: |
tests/Conformance/logs
tests/Conformance/results
- name: Upload score badge
if: always()
uses: actions/upload-artifact@v7
with:
name: badge-server-${{ matrix.spec-version }}
path: tests/Conformance/server-conformance-${{ matrix.spec-version }}.json
client:
name: conformance / client (${{ matrix.spec-version }}, ${{ matrix.dist-tag }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- spec-version: '2025-11-25'
dist-tag: latest
baseline: conformance-baseline-2025-11-25.yml
- spec-version: '2026-07-28'
dist-tag: alpha
baseline: conformance-baseline-2026-07-28.yml
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
- uses: actions/setup-node@v7
with:
node-version: '22'
- run: composer install --prefer-dist --no-progress --no-interaction
- run: mkdir -p tests/Conformance/logs
- name: Run conformance tests
working-directory: ./tests/Conformance
run: >-
npx --yes @modelcontextprotocol/conformance@${{ matrix.dist-tag }} client
--command "php ${{ github.workspace }}/tests/Conformance/client.php"
--suite all
--spec-version ${{ matrix.spec-version }}
--expected-failures ${{ matrix.baseline }}
--output-dir results
- name: Generate score badge
if: always()
run: php tests/Conformance/score.php client ${{ matrix.spec-version }}
- name: Upload conformance results
if: failure()
uses: actions/upload-artifact@v7
with:
name: conformance-client-results-${{ matrix.spec-version }}
path: |
tests/Conformance/logs
tests/Conformance/results
- name: Upload score badge
if: always()
uses: actions/upload-artifact@v7
with:
name: badge-client-${{ matrix.spec-version }}
path: tests/Conformance/client-conformance-${{ matrix.spec-version }}.json
notify:
name: Open issue on failure
runs-on: ubuntu-latest
needs: [server, client]
if: failure()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
EVENT_NAME: ${{ github.event_name }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
steps:
- name: File or comment tracking issue
run: |
existing=$(gh issue list --label conformance-weekly --state open --json number --jq '.[0].number // empty')
if [ -n "$existing" ]; then
gh issue comment "$existing" --body "New failure on $(date -u +%FT%TZ): $RUN_URL"
else
gh issue create \
--title '[conformance] Weekly conformance run failed' \
--label conformance-weekly \
--body "Conformance against the newest published \`@modelcontextprotocol/conformance\` failed.
- Run: $RUN_URL
- Trigger: $EVENT_NAME
- Date: $(date -u +%FT%TZ)
Upstream likely published a release whose scenarios the SDK does not satisfy. Either fix the SDK, update the conformance fixtures, or add the new failure to the matching \`tests/Conformance/conformance-baseline-<spec-version>.yml\`."
fi
publish:
name: Publish conformance badges
runs-on: ubuntu-latest
needs: [server, client]
if: ${{ !cancelled() && github.repository == 'modelcontextprotocol/php-sdk' }}
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
pattern: badge-*
path: badges-in
merge-multiple: true
- name: Publish to badges branch
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git ls-remote --exit-code --heads origin badges >/dev/null 2>&1; then
git fetch origin badges
git worktree add badges-wt badges
else
git worktree add --detach badges-wt
git -C badges-wt checkout --orphan badges
git -C badges-wt rm -rf --quiet . >/dev/null 2>&1 || true
fi
rm -f badges-wt/server-conformance.json badges-wt/client-conformance.json
cp badges-in/*.json badges-wt/
cd badges-wt
git add -A
if git diff --cached --quiet; then
echo "Conformance scores unchanged."
else
git commit -m "Update conformance score badges"
git push origin badges
fi

63
.github/workflows/docs.yml vendored Normal file
View file

@ -0,0 +1,63 @@
name: Documentation
# The site is built by `make docs`: Zensical renders the guides under docs/
# (see mkdocs.yml) and phpDocumentor renders the API reference into /api/.
# Pull requests build only — the build is strict, so a broken internal link
# fails CI instead of shipping a dead link to the site.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: "none"
- name: Install Composer dependencies
uses: "ramsey/composer-install@v4"
- name: Install uv
uses: astral-sh/setup-uv@v10.1.0
with:
enable-cache: true
- name: Build documentation
run: make docs
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v5
with:
path: ./site
deploy:
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5

View file

@ -5,6 +5,10 @@ permissions:
contents: read
pull-requests: write
env:
# Pinned exact version; conformance-weekly.yaml tracks upstream releases.
CONFORMANCE_PKG: "@modelcontextprotocol/conformance@0.2.0-alpha.11"
jobs:
unit:
runs-on: ubuntu-latest
@ -23,13 +27,16 @@ jobs:
- symfony-version: '7.4.*'
dependencies: 'highest'
php: '8.4'
- symfony-version: '8.0.*'
dependencies: 'highest'
php: '8.4'
env:
SYMFONY_REQUIRE: ${{ matrix.symfony-version || '>=6.4' }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -39,18 +46,40 @@ jobs:
coverage: "none"
- name: Install Composer
uses: "ramsey/composer-install@v3"
uses: "ramsey/composer-install@v4"
with:
dependency-versions: "${{ matrix.dependencies }}"
- name: Tests
run: vendor/bin/phpunit --testsuite=unit
integration:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: "none"
- name: Install Composer
uses: "ramsey/composer-install@v4"
- name: Tests
run: vendor/bin/phpunit --testsuite=integration
inspector:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -59,21 +88,98 @@ jobs:
coverage: "none"
- name: Setup Node
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Install Composer
uses: "ramsey/composer-install@v3"
uses: "ramsey/composer-install@v4"
- name: Tests
run: vendor/bin/phpunit --testsuite=inspector
conformance:
conformance-server:
name: conformance / server (${{ matrix.spec-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- spec-version: '2025-11-25'
path: '/'
baseline: conformance-baseline-2025-11-25.yml
- spec-version: '2026-07-28'
path: '/'
baseline: conformance-baseline-2026-07-28.yml
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Install Composer
uses: "ramsey/composer-install@v4"
- name: Start conformance server
run: |
mkdir -p tests/Conformance/sessions tests/Conformance/logs
chmod -R 777 tests/Conformance/sessions tests/Conformance/logs
docker compose -f tests/Conformance/Fixtures/docker-compose.yml up -d
sleep 5
- name: Run conformance tests
working-directory: ./tests/Conformance
run: >-
npx --yes ${{ env.CONFORMANCE_PKG }} server
--url http://localhost:8000${{ matrix.path }}
--suite all
--spec-version ${{ matrix.spec-version }}
--expected-failures ${{ matrix.baseline }}
- name: Show logs on failure
if: failure()
run: |
echo "=== Docker Compose Logs ==="
docker compose -f tests/Conformance/Fixtures/docker-compose.yml logs
echo ""
echo "=== Conformance Log ==="
cat tests/Conformance/logs/conformance.log 2>/dev/null || echo "No conformance log found"
echo ""
echo "=== Test Results (first failed test) ==="
find tests/Conformance/results -name "checks.json" 2>/dev/null | head -3 | while read f; do
echo "--- $f ---"
cat "$f"
echo ""
done || echo "No results found"
echo ""
echo "=== Directory permissions ==="
ls -la tests/Conformance/
ls -la tests/Conformance/logs/ 2>/dev/null || echo "logs dir issue"
ls -la tests/Conformance/sessions/ 2>/dev/null || echo "sessions dir issue"
- name: Cleanup
if: always()
run: docker compose -f tests/Conformance/Fixtures/docker-compose.yml down
conformance-client:
name: conformance / client (${{ matrix.spec-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- spec-version: '2025-11-25'
baseline: conformance-baseline-2025-11-25.yml
- spec-version: '2026-07-28'
baseline: conformance-baseline-2026-07-28.yml
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -82,38 +188,43 @@ jobs:
coverage: "none"
- name: Setup Node
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Install Composer
uses: "ramsey/composer-install@v3"
uses: "ramsey/composer-install@v4"
- name: Run server
run: php -S localhost:8000 examples/server/conformance/server.php &
- name: Create log directory
run: mkdir -p tests/Conformance/logs
- name: Wait for server to start
run: sleep 5
- name: Run client conformance tests
working-directory: ./tests/Conformance
run: >-
npx --yes ${{ env.CONFORMANCE_PKG }} client
--command "php ${{ github.workspace }}/tests/Conformance/client.php"
--suite all
--spec-version ${{ matrix.spec-version }}
--expected-failures ${{ matrix.baseline }}
- name: Tests
- name: Show logs on failure
if: failure()
run: |
exit_code=0
OUTPUT=$(npx @modelcontextprotocol/conformance server --url http://localhost:8000/) || exit_code=1
echo "$OUTPUT"
# Example: "Total: 3 passed, 16 failed"
passedTests=$(echo "$OUTPUT" | sed -nE 's/.*Total: ([0-9]+) passed.*/\1/p')
passedTests=${passedTests:-0}
REQUIRED_TESTS_TO_PASS=20
echo "Required tests to pass: $REQUIRED_TESTS_TO_PASS"
[ "$passedTests" -ge "$REQUIRED_TESTS_TO_PASS" ] || exit $exit_code
echo "=== Client Conformance Log ==="
cat tests/Conformance/logs/client-conformance.log 2>/dev/null || echo "No client conformance log found"
echo ""
echo "=== Test Results ==="
find tests/Conformance/results -name "checks.json" 2>/dev/null | head -3 | while read f; do
echo "--- $f ---"
cat "$f"
echo ""
done || echo "No results found"
qa:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -125,10 +236,15 @@ jobs:
run: composer validate --strict
- name: Install Composer
uses: "ramsey/composer-install@v3"
uses: "ramsey/composer-install@v4"
- name: Code Style PHP
run: vendor/bin/php-cs-fixer fix --dry-run
- name: PHPStan
run: vendor/bin/phpstan analyse
# Only the phpDocumentor half: this job is PHP-only, and the Zensical
# guides are built (strictly) by the Documentation workflow.
- name: Documentation
run: make docs-api

14
.gitignore vendored
View file

@ -6,4 +6,16 @@ vendor
examples/**/dev.log
examples/**/cache
examples/**/sessions
results
tests/Conformance/client-conformance.json
tests/Conformance/server-conformance.json
tests/Conformance/results*
tests/Conformance/sessions
tests/Conformance/logs/*.log
# phpDocumentor
.phpdoc/build/
.phpdoc/cache/
# Documentation site (make docs)
/site/
/.cache/

View file

@ -31,8 +31,6 @@ return (new Config())
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'protected_to_private' => false,
'declare_strict_types' => false,
'header_comment' => ['header' => $fileHeader],
'php_unit_test_case_static_method_calls' => ['call_type' => 'this'],
])

View file

@ -0,0 +1,13 @@
{% extends 'layout.html.twig' %}
{% set topMenu = {
"menu": [
{ "name": "Guides", "url": "/"},
{ "name": "Specification", "url": "https://modelcontextprotocol.io/" }
],
"social": [
{ "iconClass": "fab fa-github", "url": "https://github.com/modelcontextprotocol/php-sdk"},
{ "iconClass": "fab fa-discord", "url": "https://discord.gg/6CSzBmMkjX"}
]
}
%}

View file

@ -0,0 +1,10 @@
<h1 class="phpdocumentor-title">
<a href="/" class="phpdocumentor-title__link">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 195 195" fill="none">
<path d="M25 97.8528L92.8823 29.9706C102.255 20.598 117.451 20.598 126.823 29.9706V29.9706C136.196 39.3431 136.196 54.5391 126.823 63.9117L75.5581 115.177" stroke="black" stroke-width="12" stroke-linecap="round"/>
<path d="M76.2653 114.47L126.823 63.9117C136.196 54.5391 151.392 54.5391 160.765 63.9117L161.118 64.2652C170.491 73.6378 170.491 88.8338 161.118 98.2063L99.7248 159.6C96.6006 162.724 96.6006 167.789 99.7248 170.913L112.331 183.52" stroke="black" stroke-width="12" stroke-linecap="round"/>
<path d="M109.853 46.9411L59.6482 97.1457C50.2757 106.518 50.2757 121.714 59.6482 131.087V131.087C69.0208 140.459 84.2168 140.459 93.5894 131.087L143.794 80.8822" stroke="black" stroke-width="12" stroke-linecap="round"/>
</svg>
<span style="margin-left: 0.25em;">{{ project.name }}</span>
</a>
</h1>

View file

@ -2,6 +2,122 @@
All notable changes to `mcp/sdk` will be documented in this file.
0.9.0
-----
* [BC Break] Remove the `providerClass` argument of `#[CompletionProvider]`. Use `provider:`, which takes the same class-string and is now the first positional argument.
* Add `HttpTransport::getSessionId()` to read the server-minted `Mcp-Session-Id`: a request-scoped caller can persist it and pass it back through the constructor's `$headers` on a later transport. Always `null` on `2026-07-28`, which removed protocol-level sessions.
* Fix OIDC discovery rejecting issuers with a trailing slash (e.g. Authentik, Auth0).
0.8.0
-----
* Support for MCP spec version 2026-07-28 incl. stateless HTTP transport
* Negotiate the protocol revision during `initialize`: the server counter-offers a revision it supports and the client fails the handshake instead of continuing on an unagreed one. Adds `Client::getProtocolVersion()`.
* Serve both protocol eras from one endpoint: `StreamableHttpTransport` classifies each request (`InboundClassifier`) and routes it to the matching dispatcher, so one URL answers modern and handshake-era clients alike. `Builder::withoutModernEra()` opts out, `Builder::setModernVersions()` narrows the modern leg.
* Speak the lifecycle from the client: `Client` opens with `server/discover`, stamps `_meta` with version, capabilities and client info, and sends the `Mcp-Method`/`Mcp-Name`/`Mcp-Param-*` headers (via `Client\Stateless\ToolCatalog`). `Schema\Wire\McpHeader` holds the shared header names.
* Add multi round-trip requests (SEP-2322): a handler returning `InputRequiredResult` yields `resultType: "input_required"` with an opaque, signed `requestState` (`RequestStateCodec`, key via `Builder::setRequestState()`); the client retries with `inputResponses`, read through `RequestContext::getInputContext()` and its typed `elicitResult()`/`samplingResult()`/`rootsResult()`. On the client, `InputRequestResolver` answers such results automatically from the host's elicitation, sampling and roots handlers.
* Serve `ClientGateway::elicit()`/`elicitUrl()` on every revision: where the client cannot be asked mid-request, `Server\Stateless\ElicitationReplay` turns the ask into `input_required` and resumes once re-sent — the handler is entered once per ask. `InputRequiredShim` does the reverse for handshake-era clients. `sample()`/`listRoots()` raise a `LogicException` on `2026-07-28`, which removed them.
* Run stateless handlers in a fiber so `$gateway->progress()`/`log()` stream over SSE when the handler emits something and the client accepts `text/event-stream`; honour `io.modelcontextprotocol/logLevel` (SEP-2575). Adds `LoggingLevel::severity()`/`isAtLeast()`.
* Deliver notifications on `subscriptions/listen` (SEP-2575) via `NotificationBusInterface` — `InMemoryNotificationBus` for persistent runtimes, `Psr16NotificationBus` for PHP-FPM — set with `Builder::setNotificationBus()`; `Builder::setSubscriptionLifetime()` replaces the hard-coded 30s ceiling.
* Validate the standard request headers (SEP-2243) with `StandardHeaderValidator` (`Builder::setHeaderValidator()`), answering `-32020` when they contradict the body.
* Add `Wire\CachePolicy` (`Builder::setCachePolicy()`) for SEP-2549 caching hints; defaults to `ttlMs: 0, cacheScope: private`. A `ReadResourceResult` may override with its own values.
* Carry W3C trace context (SEP-414): `traceparent`/`tracestate`/`baggage` from `_meta` are exposed via `RequestContext::getTraceContext()` and echoed onto the request's notifications.
* Close the schema gaps for `2025-06-18`/`2025-11-25` and add the `2026-07-28` surface (SEP-2106): url-mode elicitation (`ClientGateway::elicitUrl()`/`supportsElicitationUrl()`), `Implementation::title`, and `outputSchema`/`structuredContent` accepting any JSON value.
* Deprecate Roots, Sampling and Logging (SEP-2577, earliest removal `2027-07-28`); they keep working but trigger a deprecation notice.
* [BC Break] Answer a not-found subject with `-32602` instead of `-32002` (SEP-2164): `resources/read` picks the code by revision (`-32602` from `2026-07-28` on), `prompts/get`, `completion/complete` and `tools/call` switch on every revision. Adds `ProtocolVersion::usesInvalidParamsForResourceNotFound()`.
* [BC Break] A list-shaped tool result is only sent as `structuredContent` on `2026-07-28`+; older revisions keep the JSON-encoded value in `content`.
* [BC Break] Add the extensions framework (SEP-2133) MCP Apps sits on: `ExtensionInterface::getId()` returns an `ExtensionIdentifier` value object, and the interface gains `getMessages()`/`getRequestHandlers()` (extend `AbstractExtension` to skip both). `MessageFactory::make()` takes an `$additional` message list; `RequestHandlerInterface`'s result template is covariant. `ServerExtensionInterface` is replaced by the side-agnostic `Schema\Extension\ExtensionInterface`.
* Add client-side extension negotiation: `ClientGateway::supportsExtension()`, `Client\Builder::enableExtension()`, `ClientCapabilities::withExtensions()`.
* Add sampling-with-tools: sampling requests can carry tools and tool-choice preferences, messages support tool-use/tool-result blocks, and clients advertise `sampling.context`/`sampling.tools` (`ClientGateway::supportsSamplingTools()`/`supportsSamplingContext()`). Requests violating the tool-flow rules are rejected with a JSON-RPC error.
* [BC Break] `SamplingMessage::$content` and `CreateSamplingMessageResult::$content` may be a list of content blocks — use `getContentBlocks()`. `CreateSamplingMessageResult` rejects roles other than `assistant` and empty content.
* Add client-side Roots support (`RootsCallbackInterface`, `Client::sendRootsListChanged()`) and server-side `ClientGateway::listRoots()`/`supportsRoots()`/`supportsSampling()`.
* Add `Schema\Content\ResourceLink` to reference a resource by URI in tool results and prompt messages.
* [BC Break] `Schema\JsonRpc\Error` accepts `null` as `$id`; an unreadable id now omits the member instead of sending `"id": ""`. `MessageFactory` decodes a missing or null id as an id-less error.
* Preserve the request `id` on an invalid-but-parseable message (`-32600`) via `InvalidInputMessageException::getRequestId()`.
* [BC Break] Drop the SDK-only name pattern on `ResourceDefinition`/`ResourceTemplate` `$name`; the spec allows any string.
* Log expected tool failures (`ToolCallException`) at debug level instead of error.
* Add `annotations` to `ImageContent`.
* Fix empty tool/resource schemas serializing as `[]` instead of `{}`.
* Fix `PromptResultFormatter` dropping `annotations`, `_meta` and `mimeType` for plain-array content.
0.7.0
-----
* Add client-side elicitation support: `ElicitationCallbackInterface`, `ElicitationRequestHandler`, and `ElicitationException` let clients respond to server elicitation requests.
* Defer element loading to the first registry read: loaders now run at request time (first `has*`/`get*` call) instead of eagerly at `Builder::build()`, fixing empty registries under persistent runtimes (e.g. FrankenPHP worker mode) where a loader's data source is not ready at build time. Adds `Builder::setLazyLoading()` (default on), a public `Registry::load()`, and an optional `LoaderInterface` constructor argument on `Registry`.
* [BC Break] Element loading is lazy by default: loader failures now surface on the first request rather than at `Builder::build()`, and `initialize` advertises capabilities from the configured sources rather than the loaded registry. Call `Builder::setLazyLoading(false)` to restore eager build-time loading.
* Allow `[$instance, 'methodName']` as an element handler in `Builder::addTool()`, `addResource()`, `addResourceTemplate()`, and `addPrompt()`. Unblocks handlers with constructor dependencies that the container-less `new $className()` fallback cannot build.
* Always emit an `items` schema for array tool parameters: untyped arrays get `items: {}` and nullable typed arrays (e.g. `string[]|null`) keep their element type. Fixes strict clients rejecting tools with "array type must have items" (#151).
* Harden JSON-RPC input parsing: single-message vs batch is now decided from the decoded JSON type (object → single, list array → batch) instead of the raw first byte. Scalars, empty payloads, and non-object batch elements are surfaced as `InvalidInputMessageException` entries instead of triggering warnings or a `TypeError`.
* Add `maxBatchSize` (default `100`) to `MessageFactory` — oversized JSON-RPC batches are rejected before any message is constructed, guarding against amplification.
* Add `maxBodyBytes` (default 4 MiB) to `StreamableHttpTransport` — POST bodies exceeding the cap are rejected with `413`. Unknown-size/chunked bodies are read incrementally and stopped at the cap so they cannot exhaust memory.
* Reject malformed `Mcp-Session-Id` headers with a `400` response: a repeated header or a value that is not a valid UUID is now rejected up front instead of surfacing as an uncaught `Uuid::fromString()` error.
* Extract RFC 9728 metadata serving into `ProtectedResourceMetadataHandler`, a transport-neutral PSR-15 `RequestHandlerInterface` that can be mounted directly as a Symfony/Laravel controller; `ProtectedResourceMetadataMiddleware` now delegates to it (no BC break).
0.6.0
-----
* Add `Builder::add(Tool|ResourceDefinition|ResourceTemplate|Prompt $definition, ElementHandlerInterface $handler)` for explicit registration of elements whose schema is only known at runtime.
* Add handler interfaces `ToolHandlerInterface`, `ResourceHandlerInterface`, `ResourceTemplateHandlerInterface`, `PromptHandlerInterface`, and the `ElementHandlerInterface` marker.
* [BC Break] Renamed `Mcp\Schema\Resource` to `Mcp\Schema\ResourceDefinition`. No alias.
* [BC Break] Renamed `Mcp\Capability\Registry\Loader\ArrayLoader` to `Mcp\Capability\Registry\Loader\ReflectedElementLoader`.
* [BC Break] Bump default protocol version to `2025-11-25`
* Add support for MCP Apps extension in schema and server
* Add `extensions` to `ServerCapabilities` and `ClientCapabilities` and `Builder::enableExtension()`
* Allow overriding the default name pattern for Discovery
* Add configurable session garbage collection (`gcProbability`/`gcDivisor`)
* Add optional `title` field to `ResourceDefinition` and `ResourceTemplate` for MCP spec compliance
* Add `ChainLoader` to compose multiple `LoaderInterface` implementations via explicit ordering.
* Add `RegistryInterface::unregisterTool()`, `unregisterResource()`, `unregisterResourceTemplate()`, `unregisterPrompt()` — idempotent removals.
* Add `RegistryInterface::hasTool()`, `hasResource()`, `hasResourceTemplate()`, `hasPrompt()` — by-name existence checks.
* `DiscoveryLoader` now refreshes only its own previously written entries; manual registrations (via `Builder::addTool()` etc. or runtime `$registry->registerTool()` calls) survive rediscovery, and a same-name manual registration takes precedence over discovery on collision.
* [BC Break] Removed `ElementReference::$isManual` public property and the `bool $isManual` parameter from all `*Reference` constructors. Origin tracking is no longer carried on the element; manual-over-discovered precedence is encoded by loader execution order.
* [BC Break] `RegistryInterface::registerTool()`, `registerResource()`, `registerResourceTemplate()`, `registerPrompt()` lost their trailing `bool $isManual = false` parameter. Callers using positional arguments must drop the flag.
* [BC Break] Removed `RegistryInterface::clear()`, `getDiscoveryState()`, `setDiscoveryState()`. Rediscovery now goes through `DiscoveryLoader::load()` directly.
* `Registry::register*()` semantics changed to plain last-write-wins (overwrites silently) and the methods now return the stored `*Reference`. The previous "discovered registration is ignored when a manual one already exists" precedence rule still applies, but is now enforced by `DiscoveryLoader` via reference-identity tracking — and still emits a debug log when a discovery is skipped due to a conflicting registration.
* Add optional `title` parameter to `Builder::addResource()` and `Builder::addResourceTemplate()` for MCP spec compliance
* [BC Break] `Builder::addResource()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
* [BC Break] `Builder::addResourceTemplate()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
* Add `CorsMiddleware`, `DnsRebindingProtectionMiddleware`, and `ProtocolVersionMiddleware` for `StreamableHttpTransport`, composed automatically as the default stack via `StreamableHttpTransport::defaultMiddleware()`
* [BC BREAK] `StreamableHttpTransport` constructor: `$corsHeaders` parameter removed; CORS is now configured via `CorsMiddleware`. The `$middleware` parameter is nullable — `null` (or omitted) installs the default stack; `[]` disables all defaults. Default `Access-Control-Allow-Origin` is no longer set (was `*`).
* [BC Break] `ResourceDefinition::__construct()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
* [BC Break] `ResourceTemplate::__construct()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
* [BC Break] `McpResource` and `McpResourceTemplate` attribute signatures changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
0.5.0
-----
* Add built-in authentication middleware for HTTP transport using OAuth
* Add client component for building MCP clients
* Add `Builder::setReferenceHandler()` to allow custom `ReferenceHandlerInterface` implementations (e.g. authorization decorators)
* Add elicitation enum schema types per SEP-1330: `TitledEnumSchemaDefinition`, `MultiSelectEnumSchemaDefinition`, `TitledMultiSelectEnumSchemaDefinition`
* [BC break] Make Symfony Finder component optional. Users would need to install `symfony/finder` now themselves
* Add `LenientOidcDiscoveryMetadataPolicy` for identity providers that omit `code_challenge_methods_supported` (e.g. FusionAuth, Microsoft Entra ID)
* Add OAuth 2.0 Dynamic Client Registration middleware (RFC 7591)
* Add optional `title` field to `Prompt` and `McpPrompt` for MCP spec compliance
* [BC Break] `Builder::addPrompt()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments for `$description` must switch to named arguments.
* Add optional `title` field to `Tool` and `McpTool` for MCP spec compliance
* [BC Break] `Tool::__construct()` signature changed — `$title` parameter added between `$name` and `$inputSchema`. Callers using positional arguments must switch to named arguments or pass `null` for `$title`.
* [BC Break] `McpTool` attribute signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments for `$description` must switch to named arguments.
* [BC Break] `Builder::addTool()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments for `$description` must switch to named arguments.
0.4.0
-----
* Rename `Mcp\Server\Session\Psr16StoreSession` to `Mcp\Server\Session\Psr16SessionStore`
* Add missing handlers for resource subscribe/unsubscribe and persist subscriptions via session
* Introduce `SessionManager` to encapsulate session handling (replaces `SessionFactory`) and move garbage collection logic from `Protocol`.
0.3.0
-----
* Add output schema support to MCP tools
* Add validation of the input parameters given to a Tool.
* Rename `Mcp\Capability\Registry\ResourceReference::$schema` to `Mcp\Capability\Registry\ResourceReference::$resource`.
* Introduce `SchemaGeneratorInterface` and `DiscovererInterface` to allow custom schema generation and discovery implementations.
* Remove `DocBlockParser::getSummary()` method, use `DocBlockParser::getDescription()` instead.
0.2.2
-----

84
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,84 @@
# Contributing
Thanks for considering a contribution to the PHP SDK for the Model Context Protocol. This is a
collaboration between [the PHP Foundation](https://thephp.foundation/) and the
[Symfony project](https://symfony.com/), and it follows Symfony's conventions throughout.
## Ways to contribute
- **Report a bug** or **propose a feature** by [opening an issue](https://github.com/modelcontextprotocol/php-sdk/issues).
Check existing issues first — many spec-driven changes are already tracked under the relevant
`2026-07-28`-style release label.
- **Send a pull request** for a fix, a new capability, or a docs improvement.
- **Improve the guides** under `docs/` — see [Documentation](#documentation) below for how they're built.
- **Help close a conformance gap** — `make conformance-tests` runs the official MCP conformance
suite against this SDK; a failing scenario there is a concrete, well-scoped contribution.
## Development setup
Requires PHP 8.1+.
```bash
composer install
```
## Before opening a pull request
Run the full CI suite locally:
```bash
make ci
```
This runs, in order: `make cs` (PHP CS Fixer, auto-fixes style), `make phpstan` (static analysis),
and `make tests` (unit + inspector tests). All three must pass. If your change touches
protocol-observable behavior, also run:
```bash
make conformance-tests # requires Docker
```
## Coding standards
This project follows [Symfony's coding standards](https://symfony.com/doc/current/contributing/code/standards.html)
and [backward compatibility promise](https://symfony.com/doc/current/contributing/code/bc.html). In short:
See [CLAUDE.md](CLAUDE.md) for a fuller tour of the codebase's architecture and layout.
## Tests
New capabilities need unit tests (`tests/Unit/`) covering the core logic, and — for anything
reachable over the wire — inspector tests (`tests/Inspector/`) for end-to-end coverage. If you're
adding a documented pattern, consider adding or updating an example under `examples/`.
## Documentation
The guides under `docs/` are built with [Zensical](https://zensical.org/) in `--strict` mode,
which fails the build on a broken internal link:
```bash
make docs-guides
```
Links between guide pages must be relative paths that resolve within `docs/` (e.g.
`protocol-versions.md`, `../CLAUDE.md` will *not* resolve — Zensical only follows the `docs/` tree).
For anything at the repo root (`CLAUDE.md`, `ROADMAP.md`, `CHANGELOG.md`), link to it by its GitHub
URL instead, matching the pattern already used across `docs/`. The class-level API reference is
generated separately by phpDocumentor (`make docs-api`) and isn't hand-written.
## Versioning
The SDK follows [Semantic Versioning](https://semver.org/) — see
[SDK tier target](docs/sdk-tier.md#versioning) for what that means pre- and post-1.0, and how it
lines up with Symfony's backward compatibility promise.
## Licensing
New contributions are licensed under Apache License, Version 2.0. Existing code predating this
policy remains under the MIT License — see [LICENSE](LICENSE) for the details. By opening a pull
request, you agree your contribution is provided under those terms.
## Getting help
If something is unclear or you want early feedback on an approach before writing code, open an
issue or a draft pull request — that's the right place to ask, rather than guessing at scope.

205
LICENSE
View file

@ -1,11 +1,200 @@
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.
No rights beyond those granted by the applicable original license are conveyed for such contributions.
---
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright
owner or by an individual or Legal Entity authorized to submit on behalf
of the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
---
MIT License
Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
@ -15,5 +204,13 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
Creative Commons Attribution 4.0 International (CC-BY-4.0)
Documentation in this project (excluding specifications) is licensed under
CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
the full license text.

View file

@ -1,4 +1,15 @@
.PHONY: deps-stable deps-low cs phpstan tests unit-tests inspector-tests coverage ci ci-stable ci-lowest
.PHONY: deps-stable deps-low cs phpstan tests unit-tests integration-tests inspector-tests coverage ci ci-stable ci-lowest conformance-tests conformance-server conformance-client conformance-draft conformance-draft-server conformance-draft-client docs docs-guides docs-api docs-serve
# The 2026-07-28 scenarios ship on the `alpha` dist-tag; `latest` (0.1.x) has
# none of them. Pinned to the same version CI runs (see
# .github/workflows/pipeline.yaml), so a local pass means a green pipeline.
CONFORMANCE_VERSION ?= 0.2.0-alpha.11
CONFORMANCE = npx --yes @modelcontextprotocol/conformance@$(CONFORMANCE_VERSION)
# The documentation toolchain is Python (Zensical, see requirements-docs.txt),
# run through uv so no virtualenv has to be managed by hand:
# https://docs.astral.sh/uv/getting-started/installation/
DOCS_RUN = uv run --no-project --with-requirements requirements-docs.txt --
deps-stable:
composer update --prefer-stable
@ -18,14 +29,45 @@ tests:
unit-tests:
vendor/bin/phpunit --testsuite=unit
integration-tests:
vendor/bin/phpunit --testsuite=integration
inspector-tests:
vendor/bin/phpunit --testsuite=inspector
conformance-server:
php -S localhost:8000 examples/server/conformance/server.php
conformance-tests: conformance-server conformance-client
conformance-tests:
npx @modelcontextprotocol/conformance server --url http://localhost:8000/
conformance-server:
docker compose -f tests/Conformance/Fixtures/docker-compose.yml up -d
@echo "Waiting for server to start..."
@sleep 5
rm -rf tests/Conformance/results
cd tests/Conformance && $(CONFORMANCE) server --url http://localhost:8000/ --suite all --spec-version 2025-11-25 --expected-failures conformance-baseline-2025-11-25.yml --output-dir results || true
php tests/Conformance/score.php server 2025-11-25
docker compose -f tests/Conformance/Fixtures/docker-compose.yml down
conformance-client:
rm -rf tests/Conformance/results
cd tests/Conformance && $(CONFORMANCE) client --command "php $(CURDIR)/tests/Conformance/client.php" --suite all --spec-version 2025-11-25 --expected-failures conformance-baseline-2025-11-25.yml --output-dir results || true
php tests/Conformance/score.php client 2025-11-25
# --- 2026-07-28 (SEP-2575 stateless lifecycle) ------------------------------
conformance-draft: conformance-draft-server conformance-draft-client
conformance-draft-server:
docker compose -f tests/Conformance/Fixtures/docker-compose.yml up -d
@echo "Waiting for server to start..."
@sleep 5
rm -rf tests/Conformance/results-2026-07-28
cd tests/Conformance && $(CONFORMANCE) server --url http://localhost:8000/ --suite all --spec-version 2026-07-28 --expected-failures conformance-baseline-2026-07-28.yml --output-dir results-2026-07-28 || true
php tests/Conformance/score.php server 2026-07-28 results-2026-07-28
docker compose -f tests/Conformance/Fixtures/docker-compose.yml down
conformance-draft-client:
rm -rf tests/Conformance/results-2026-07-28
cd tests/Conformance && $(CONFORMANCE) client --command "php $(CURDIR)/tests/Conformance/client.php" --suite all --spec-version 2026-07-28 --expected-failures conformance-baseline-2026-07-28.yml --output-dir results-2026-07-28 || true
php tests/Conformance/score.php client 2026-07-28 results-2026-07-28
coverage:
XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=unit --coverage-html=coverage
@ -35,3 +77,20 @@ ci: ci-stable
ci-stable: deps-stable cs phpstan tests
ci-lowest: deps-low cs phpstan tests
# The published site is the guides (Zensical) with the phpDocumentor API
# reference mounted at /api/. `zensical build` wipes site/, so it runs first.
docs: docs-guides docs-api
rm -rf site/api
cp -a .phpdoc/build/api site/api
docs-guides:
$(DOCS_RUN) zensical build --strict
docs-api:
vendor/bin/phpdoc --no-interaction
@grep -q 'No errors have been found' .phpdoc/build/api/reports/errors.html || \
(echo "Documentation errors found. See .phpdoc/build/api/reports/errors.html" && exit 1)
docs-serve:
$(DOCS_RUN) zensical serve

301
README.md
View file

@ -1,27 +1,31 @@
# MCP PHP SDK
The official PHP SDK for Model Context Protocol (MCP). It provides a framework-agnostic API for implementing MCP servers
and clients in PHP.
[![Latest Version](https://img.shields.io/packagist/v/mcp/sdk.svg)](https://packagist.org/packages/mcp/sdk)
[![CI](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/pipeline.yaml/badge.svg)](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/pipeline.yaml)
[![PHP Version](https://img.shields.io/packagist/php-v/mcp/sdk.svg)](https://packagist.org/packages/mcp/sdk)
[![License](https://img.shields.io/packagist/l/mcp/sdk.svg)](LICENSE)
> [!IMPORTANT]
> This SDK is currently in active development with ongoing refinement of its architecture and features. While
> functional, the API may experience changes as we work toward stabilization.
>
> If you want to help us stabilize the SDK, please see the [issue tracker](https://github.com/modelcontextprotocol/php-sdk/issues).
The official PHP SDK for the Model Context Protocol (MCP). It provides a framework-agnostic API for implementing MCP
servers and clients in PHP — tools, resources, prompts, STDIO and HTTP transports, sessions, authorization, and both
protocol eras (the `initialize` handshake and the stateless `2026-07-28` revision).
This project represents a collaboration between [the PHP Foundation](https://thephp.foundation/) and the [Symfony project](https://symfony.com/). It adopts
development practices and standards from the Symfony project, including [Coding Standards](https://symfony.com/doc/current/contributing/code/standards.html) and the
[Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html).
Until the first major release, this SDK is considered [experimental](https://symfony.com/doc/current/contributing/code/experimental.html).
Until the first major release, this SDK is considered [experimental](https://symfony.com/doc/current/contributing/code/experimental.html), please see the [roadmap](./ROADMAP.md) for
planned next steps and features.
## Roadmap
## Protocol Conformance
**Features**
- [ ] Stabilize server component with all needed handlers and functional tests
- [ ] Extend documentation, including integration guides for popular frameworks
- [ ] Implement Client component
- [ ] Support multiple schema versions
Measured weekly against the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance),
as passing checks out of total. The `2026-07-28` scores run against the framework's `alpha` releases, so they move as
upstream publishes new scenarios.
| Revision | Server | Client |
|:--------:|:------:|:------:|
| `2026-07-28` | [![Server conformance 2026-07-28](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/modelcontextprotocol/php-sdk/badges/server-conformance-2026-07-28.json)](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml) | [![Client conformance 2026-07-28](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/modelcontextprotocol/php-sdk/badges/client-conformance-2026-07-28.json)](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml) |
| `2025-11-25` | [![Server conformance 2025-11-25](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/modelcontextprotocol/php-sdk/badges/server-conformance-2025-11-25.json)](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml) | [![Client conformance 2025-11-25](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/modelcontextprotocol/php-sdk/badges/client-conformance-2025-11-25.json)](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml) |
## Installation
@ -29,30 +33,20 @@ Until the first major release, this SDK is considered [experimental](https://sym
composer require mcp/sdk
```
## Quick Start
## Build a server
This example demonstrates the most common usage pattern - a STDIO server using attribute discovery.
### 1. Define Your MCP Elements
Create a class with MCP capabilities using attributes:
A server is a plain PHP class plus three lines of wiring:
```php
<?php
namespace App;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;
class CalculatorElements
class Calculator
{
/**
* Adds two numbers together.
*
* @param int $a The first number
* @param int $b The second number
* @return int The sum of the two numbers
* Adds two numbers.
*/
#[McpTool]
public function add(int $a, int $b): int
@ -60,227 +54,88 @@ class CalculatorElements
return $a + $b;
}
/**
* Performs basic arithmetic operations.
*/
#[McpTool(name: 'calculate')]
public function calculate(float $a, float $b, string $operation): float|string
#[McpResource(uri: 'config://calculator/settings')]
public function settings(): array
{
return match($operation) {
'add' => $a + $b,
'subtract' => $a - $b,
'multiply' => $a * $b,
'divide' => $b != 0 ? $a / $b : 'Error: Division by zero',
default => 'Error: Unknown operation'
};
}
#[McpResource(
uri: 'config://calculator/settings',
name: 'calculator_config',
mimeType: 'application/json'
)]
public function getSettings(): array
{
return ['precision' => 2, 'allow_negative' => true];
return ['precision' => 2];
}
}
exit(Server::builder()
->setServerInfo('Calculator', '1.0.0')
->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor'])
->build()
->run(new StdioTransport()));
```
### 2. Create the Server Script
The walkthrough in [First server](docs/get-started/first-server.md) explains each piece, and
[Try it with the Inspector](docs/get-started/inspector.md) shows it running.
Create your MCP server:
## Build a client
```php
#!/usr/bin/env php
<?php
use Mcp\Client;
use Mcp\Client\Transport\StdioTransport;
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;
$server = Server::builder()
->setServerInfo('Calculator Server', '1.0.0')
->setDiscovery(__DIR__, ['.'])
$client = Client::builder()
->setClientInfo('My Application', '1.0.0')
->build();
$transport = new StdioTransport();
$client->connect(new StdioTransport(command: 'php', args: ['/path/to/server.php']));
$server->run($transport);
$tools = $client->listTools();
$result = $client->callTool('add', ['a' => 5, 'b' => 3]);
$client->disconnect();
```
### 3. Configure Your MCP Client
Add to your client configuration (e.g., Claude Desktop's `mcp.json`):
```json
{
"mcpServers": {
"php-calculator": {
"command": "php",
"args": ["/absolute/path/to/your/server.php"]
}
}
}
```
### 4. Test Your Server
```bash
# Test with MCP Inspector
npx @modelcontextprotocol/inspector php /path/to/server.php
# Your AI assistant can now call:
# - add: Add two integers
# - calculate: Perform arithmetic operations
# - Read config://calculator/settings resource
```
## Key Features
### Attribute-Based Discovery
Define MCP elements using PHP attributes with automatic discovery:
```php
// Tool with automatic name and description from method
#[McpTool]
public function generateReport(): string { /* ... */ }
// Tool with custom name
#[McpTool(name: 'custom_name')]
public function myMethod(): string { /* ... */ }
// Resource with URI and metadata
#[McpResource(uri: 'config://app/settings', mimeType: 'application/json')]
public function getConfig(): array { /* ... */ }
```
### Manual Registration
Register capabilities programmatically:
```php
$server = Server::builder()
->addTool([MyClass::class, 'myMethod'], 'tool_name')
->addResource([MyClass::class, 'getData'], 'data://config')
->build();
```
### Multiple Transport Options
**STDIO Transport** (Command-line integration):
```php
$transport = new StdioTransport();
$server->run($transport);
```
**HTTP Transport** (Web-based communication):
```php
$transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory);
$response = $server->run($transport);
// Handle $response in your web application
```
### Session Management
By default, the SDK uses in-memory sessions. You can configure different session stores:
```php
use Mcp\Server\Session\FileSessionStore;
use Mcp\Server\Session\InMemorySessionStore;
use Mcp\Server\Session\Psr16StoreSession;
use Symfony\Component\Cache\Psr16Cache;
use Symfony\Component\Cache\Adapter\RedisAdapter;
// Use default in-memory sessions with custom TTL
$server = Server::builder()
->setSession(ttl: 7200) // 2 hours
->build();
// Override with file-based storage
$server = Server::builder()
->setSession(new FileSessionStore(__DIR__ . '/sessions'))
->build();
// Override with in-memory storage and custom TTL
$server = Server::builder()
->setSession(new InMemorySessionStore(3600))
->build();
// Override with PSR-16 cache-based storage
// Requires psr/simple-cache and symfony/cache (or any other PSR-16 implementation)
// composer require psr/simple-cache symfony/cache
$redisAdapter = new RedisAdapter(
RedisAdapter::createConnection('redis://localhost:6379'),
'mcp_sessions'
);
$server = Server::builder()
->setSession(new Psr16StoreSession(
cache: new Psr16Cache($redisAdapter),
prefix: 'mcp-',
ttl: 3600
))
->build();
```
### Discovery Caching
Use any PSR-16 cache implementation to cache discovery results and avoid running discovery on every server start:
```php
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery'));
$server = Server::builder()
->setDiscovery(
basePath: __DIR__,
scanDirs: ['.', 'src'], // Default: ['.', 'src']
excludeDirs: ['vendor'], // Default: ['vendor', 'node_modules']
cache: $cache
)
->build();
```
See [Connecting to a server](docs/client/connecting.md) for transports, timeouts, and the
handlers that answer server-initiated requests.
## Documentation
**Core Concepts:**
- [Server Builder](docs/server-builder.md) - Complete ServerBuilder reference and configuration
- [Transports](docs/transports.md) - STDIO and HTTP transport setup and usage
- [MCP Elements](docs/mcp-elements.md) - Creating tools, resources, and prompts
- [Client Communication](docs/client-communication.md) - Communicating back to the client from server-side
The full documentation is published at **[php.sdk.modelcontextprotocol.io](https://php.sdk.modelcontextprotocol.io/)**.
**Learning:**
- [Examples](docs/examples.md) - Comprehensive example walkthroughs
- **[Get started](docs/get-started/index.md)** — Install the SDK and build your first server
- **[Servers](docs/servers/index.md)** — Tools, resources, resource templates, prompts, and how to register them
- **[Inside your handler](docs/handlers/index.md)** — Talking back to the client, logging, and asking for input
- **[Running your server](docs/run/index.md)** — Server builder, STDIO and HTTP transports, framework integration, sessions, authorization
- **[Clients](docs/client/index.md)** — Client SDK for connecting to and communicating with MCP servers
- **[Protocol versions](docs/protocol-versions.md)** — The two protocol eras, and what revision `2026-07-28` changed
- **[Advanced](docs/advanced/index.md)** — Events, protocol extensions (including MCP Apps), and custom message handlers
- **[Examples](docs/examples.md)** — Runnable server and client examples
- **[API Reference](https://php.sdk.modelcontextprotocol.io/api/)** — Generated class reference
**External Resources:**
- [Model Context Protocol documentation](https://modelcontextprotocol.io)
- [Model Context Protocol specification](https://spec.modelcontextprotocol.io)
- [Officially supported servers](https://github.com/modelcontextprotocol/servers)
## External Resources
- **[Model Context Protocol Documentation](https://modelcontextprotocol.io)** — Official MCP documentation
- **[Model Context Protocol Specification](https://spec.modelcontextprotocol.io)** — Protocol specification
- **[Officially Supported Servers](https://github.com/modelcontextprotocol/servers)** — Reference server implementations
## PHP Libraries Using the MCP SDK
* [pronskiy/mcp](https://github.com/pronskiy/mcp) - Additional DX layer
* [symfony/mcp-bundle](https://github.com/symfony/mcp-bundle) - Symfony integration bundle
* [josbeir/cakephp-synapse](https://github.com/josbeir/cakephp-synapse) - CakePHP integration plugin
- [api-platform/mcp](https://github.com/api-platform/mcp) — MCP integration for API Platform
- [bnomei/kirby-mcp](https://github.com/bnomei/kirby-mcp) — MCP server for the Kirby CMS
- [drupal/mcp_server](https://www.drupal.org/project/mcp_server) — MCP server for Drupal exposing configuration and entities as MCP elements
- [josbeir/cakephp-synapse](https://github.com/josbeir/cakephp-synapse) — CakePHP plugin exposing application functionality over MCP
- [nette/mcp-inspector](https://github.com/nette/mcp-inspector) — MCP server for introspecting Nette applications
- [symfony/ai-mate](https://github.com/symfony/ai-mate) — AI development assistant MCP server for Symfony projects
- [symfony/mcp-bundle](https://github.com/symfony/mcp-bundle) — Symfony integration bundle
Building something on top of the SDK? Open a pull request to add it to this list.
## Contributing
We are passionate about supporting contributors of all levels of experience and would love to see you get involved in
the project. See the [contributing guide](CONTRIBUTING.md) to get started before you [report issues](https://github.com/modelcontextprotocol/php-sdk/issues) and [send pull requests](https://github.com/modelcontextprotocol/php-sdk/pulls).
the project. Start by [reporting issues](https://github.com/modelcontextprotocol/php-sdk/issues) or
[sending pull requests](https://github.com/modelcontextprotocol/php-sdk/pulls). See
[CONTRIBUTING.md](CONTRIBUTING.md) for development setup, coding standards, and what to run before
opening a PR.
## Credits
The starting point for this SDK was the [PHP-MCP](https://github.com/php-mcp/server) project, initiated by
[Kyrian Obikwelu](https://github.com/CodeWithKyrian), and the [Symfony AI initiative](https://github.com/symfony/ai). We are grateful for the work
done by both projects and their contributors, which created a solid foundation for this SDK.
The starting point for this SDK was the [PHP-MCP](https://github.com/php-mcp/server) project, initiated by [Kyrian Obikwelu](https://github.com/CodeWithKyrian), and the [Symfony AI initiative](https://github.com/symfony/ai). We are grateful for the work done by both projects and their contributors, which created a solid foundation for this SDK.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
This project is licensed under the Apache License, Version 2.0 for new contributions, with existing code under the MIT License — see the [LICENSE](LICENSE) file for details.

21
ROADMAP.md Normal file
View file

@ -0,0 +1,21 @@
# Roadmap
This roadmap is a living document that outlines the planned features and improvements for our project.
## Goals for the First Major Release
- **Server**
- [x] Implement full support for elicitations
- [ ] Implement OAuth2 authentication for server
- **Client**
- [x] Implement client-side support
- [x] Implement client examples and documentation
- [ ] Implement OAuth2 authentication for client
- **Schema**
- [ ] Implement schema generation based on TS or JSON Schema
- [ ] Achieve full spec conformance
- **Extensions**
- [ ] Implement the Tasks extension
- **Architecture**
- [ ] Split the SDK into multiple packages

21
SECURITY.md Normal file
View file

@ -0,0 +1,21 @@
# Security Policy
Thank you for helping keep the Model Context Protocol and its ecosystem secure.
## Reporting Security Issues
If you discover a security vulnerability in this repository, please report it through
the [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)
for this repository.
Please **do not** report security vulnerabilities through public GitHub issues, discussions,
or pull requests.
## What to Include
To help us triage and respond quickly, please include:
- A description of the vulnerability
- Steps to reproduce the issue
- The potential impact
- Any suggested fixes (optional)

View file

@ -0,0 +1,97 @@
# 0001 — The MCP server is an OAuth Resource Server, not an Authorization Server
- Status: Accepted
- Date: 2026-06-15
## Context
OAuth 2.1 defines three distinct roles:
| Role | What it does | Status in this SDK | Scope |
|------|--------------|--------------------|-------|
| Resource Server | Validates incoming bearer tokens, serves Protected Resource Metadata (RFC 9728), emits `WWW-Authenticate` | Shipped (`AuthorizationMiddleware`, `JwtTokenValidator`, `ProtectedResourceMetadata`) | **IN scope** |
| Delegation / proxy to an upstream AS | Forwards `/authorize` and `/token` to your existing IdP | Shipped (`OAuthProxyMiddleware`) | **IN scope — delegation ONLY** |
| Authorization Server / Identity Provider (IdP) | Mints its own tokens, registers clients, runs login and consent | Absent | **OUT of scope** |
The SDK repeatedly receives pull requests that move it toward becoming a full OAuth 2.1
**authorization server** — asking the MCP server to mint its own access and refresh tokens,
register clients, and run login and consent flows, that is, to become an Identity Provider.
The most explicit example is [#373](https://github.com/modelcontextprotocol/php-sdk/pull/373)
("[Server] Add native OAuth 2.1 authorization server", ~3,400 lines across 50+ files), but the
direction has crept in incrementally rather than in one PR.
A contributing factor is that the SDK already exposes the *authorization-server endpoints in
proxy form*, and that surface has grown:
[#221](https://github.com/modelcontextprotocol/php-sdk/pull/221) added the OAuth resource-server
middleware, and [#269](https://github.com/modelcontextprotocol/php-sdk/pull/269) added Dynamic
Client Registration (RFC 7591). Today `OAuthProxyMiddleware` answers `/authorize`, `/token`, and
`/.well-known/oauth-authorization-server`, and DCR endpoints exist. The shape of that surface
invites contributors to "finish the job" by backing those endpoints with a real token issuer.
It is not scaffolding to be completed — it is a delegating proxy, and that is the whole of its
intent.
## Decision
**The MCP server is an OAuth 2.1 Resource Server that MAY delegate to an upstream
authorization server. It will NOT issue tokens or act as an Identity Provider.**
Concretely:
- The SDK validates bearer tokens, serves RFC 9728 Protected Resource Metadata, and emits
`WWW-Authenticate` challenges. This is the Resource Server role and is fully supported.
- The SDK MAY delegate `/authorize` and `/token` to an upstream authorization server via
`OAuthProxyMiddleware`. This is delegation only: the middleware redirects the browser to
the upstream `/authorize` endpoint and proxies `/token` requests to the upstream token
endpoint. It never mints, signs, stores, or rotates tokens of its own.
- The SDK will NOT implement an authorization server: no token issuance, no token signing or
key management, no login UI, no consent UI, no authorization-code or refresh-token storage,
no first-party Dynamic Client Registration acting as an issuer.
Pull requests that add authorization-server / IdP behavior are declined by reference to this
ADR.
## Rationale
- **Security liability.** Issuing tokens means owning signing-key generation, storage, and
rotation; authorization-code and refresh-token persistence; refresh-token rotation and
replay detection; and consent. A defect in any of these is a credential-issuance
vulnerability affecting every consumer of the SDK. This is precisely the surface an MCP SDK
should not own.
- **RFC footprint.** A correct authorization server must implement and keep current with
RFC 6749 (OAuth 2.0), RFC 7591 (Dynamic Client Registration), RFC 8414 (Authorization
Server Metadata), PKCE (RFC 7636), and refresh-token rotation guidance, among others. That
is an open-ended maintenance and conformance burden far outside the SDK's purpose.
- **Mature implementations already exist.** Token issuance is a solved problem.
`league/oauth2-server` provides it as a PHP library, and every production IdP — Keycloak,
Auth0, Microsoft Entra ID, Okta — provides it as a service. Re-implementing it inside an
MCP SDK adds risk without adding value.
## Boundary statement
`OAuthProxyMiddleware` **delegates** to an upstream IdP. It is **not** authorization-server
scaffolding to be completed. Backing its `/authorize` and `/token` endpoints with a
first-party token issuer is out of scope and will be declined.
## Consequences
- The supported authorization architecture is: an external authorization server (your IdP or
`league/oauth2-server` running in your own application) issues tokens; the MCP server
validates them as a Resource Server and optionally proxies the OAuth flow to that upstream.
- Contributors get a single, citable ruling for why authorization-server PRs are declined,
reducing repeated large-PR churn.
- Resource Server and proxy/delegation features remain welcome and supported.
## Alternatives / what to do instead
If you need an authorization server (token issuance, client registration, login, consent), do
**not** add it to this SDK. Instead:
- **Front the MCP server with an existing IdP** — Keycloak, Auth0, Microsoft Entra ID, or
Okta. Point `JwtTokenValidator` and `ProtectedResourceMetadata` at that issuer, and
optionally use `OAuthProxyMiddleware` to delegate `/authorize` and `/token` to it.
- **Run `league/oauth2-server` in your own application**, behind the SDK's existing proxy and
validator seams. The MCP server validates the tokens it issues; it does not issue them
itself.
See [`../docs/run/authorization.md`](../docs/run/authorization.md) for the supported Resource Server
and delegation setup.

10
adr/README.md Normal file
View file

@ -0,0 +1,10 @@
# Architecture Decision Records
This directory holds Architecture Decision Records (ADRs): short documents that capture a
significant architectural decision, the context that led to it, and its consequences. They
exist so a decision can be cited once and referenced thereafter — for example, when declining
a pull request that conflicts with an accepted decision.
## Records
- [0001 — The MCP server is an OAuth Resource Server, not an Authorization Server](0001-oauth-authorization-server-out-of-scope.md)

View file

@ -1,7 +1,7 @@
{
"name": "mcp/sdk",
"description": "Model Context Protocol SDK for Client and Server applications in PHP",
"license": "MIT",
"license": "Apache-2.0",
"type": "library",
"authors": [
{
@ -22,26 +22,39 @@
"ext-fileinfo": "*",
"opis/json-schema": "^2.4",
"php-http/discovery": "^1.20",
"phpdocumentor/reflection-docblock": "^5.6",
"phpdocumentor/reflection-docblock": "^5.6 || ^6.0",
"psr/clock": "^1.0",
"psr/container": "^1.0 || ^2.0",
"psr/event-dispatcher": "^1.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.1",
"psr/http-message": "^1.1 || ^2.0",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"symfony/finder": "^5.4 || ^6.4 || ^7.3 || ^8.0",
"symfony/deprecation-contracts": "^2.5 || ^3.0",
"symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0"
},
"suggest": {
"symfony/finder": "Required for file-based discovery."
},
"require-dev": {
"ext-openssl": "*",
"composer/semver": "^3.0",
"firebase/php-jwt": "^6.10 || ^7.0",
"laminas/laminas-httphandlerrunner": "^2.12",
"nyholm/psr7": "^1.8",
"nyholm/psr7-server": "^1.1",
"phar-io/composer-distributor": "^1.0.2",
"php-cs-fixer/shim": "^3.91",
"phpdocumentor/shim": "^3",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.5",
"psr/simple-cache": "^2.0 || ^3.0",
"symfony/cache": "^5.4 || ^6.4 || ^7.3 || ^8.0",
"symfony/console": "^5.4 || ^6.4 || ^7.3 || ^8.0",
"symfony/finder": "^5.4 || ^6.4 || ^7.3 || ^8.0",
"symfony/http-client": "^5.4 || ^6.4 || ^7.3 || ^8.0",
"symfony/process": "^5.4 || ^6.4 || ^7.3 || ^8.0"
},
"autoload": {
@ -55,6 +68,7 @@
"Mcp\\Example\\Server\\ClientCommunication\\": "examples/server/client-communication/",
"Mcp\\Example\\Server\\ClientLogging\\": "examples/server/client-logging/",
"Mcp\\Example\\Server\\CombinedRegistration\\": "examples/server/combined-registration/",
"Mcp\\Example\\Server\\Elicitation\\": "examples/server/elicitation/",
"Mcp\\Example\\Server\\ComplexToolSchema\\": "examples/server/complex-tool-schema/",
"Mcp\\Example\\Server\\Conformance\\": "examples/server/conformance/",
"Mcp\\Example\\Server\\CustomDependencies\\": "examples/server/custom-dependencies/",
@ -63,13 +77,20 @@
"Mcp\\Example\\Server\\DiscoveryUserProfile\\": "examples/server/discovery-userprofile/",
"Mcp\\Example\\Server\\EnvVariables\\": "examples/server/env-variables/",
"Mcp\\Example\\Server\\ExplicitRegistration\\": "examples/server/explicit-registration/",
"Mcp\\Example\\Server\\McpApps\\": "examples/server/mcp-apps/",
"Mcp\\Example\\Server\\OAuthKeycloak\\": "examples/server/oauth-keycloak/",
"Mcp\\Example\\Server\\OAuthMicrosoft\\": "examples/server/oauth-microsoft/",
"Mcp\\Example\\Server\\SchemaShowcase\\": "examples/server/schema-showcase/",
"Mcp\\Tests\\": "tests/"
}
},
"classmap": [
"tests/Unit/Capability/Discovery/Fixtures/AlternativeFileNameToolHandler.class.inc"
]
},
"config": {
"allow-plugins": {
"php-http/discovery": false
"php-http/discovery": false,
"phpdocumentor/shim": true
},
"sort-packages": true
}

Some files were not shown because too many files have changed in this diff Show more