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
45 changed files with 1082 additions and 65 deletions

View file

@ -125,10 +125,11 @@ jobs:
name: Open issue on failure name: Open issue on failure
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [server, client] needs: [server, client]
if: failure() && github.event_name == 'schedule' if: failure()
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
EVENT_NAME: ${{ github.event_name }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
steps: steps:
- name: File or comment tracking issue - name: File or comment tracking issue
@ -140,12 +141,13 @@ jobs:
gh issue create \ gh issue create \
--title '[conformance] Weekly conformance run failed' \ --title '[conformance] Weekly conformance run failed' \
--label conformance-weekly \ --label conformance-weekly \
--body "Weekly conformance against \`@modelcontextprotocol/conformance@latest\` failed. --body "Conformance against the newest published \`@modelcontextprotocol/conformance\` failed.
- Run: $RUN_URL - Run: $RUN_URL
- Triggered: $(date -u +%FT%TZ) - 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 \`tests/Conformance/conformance-baseline-2025-11-25.yml\`." 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 fi
publish: publish:

View file

@ -35,7 +35,7 @@ jobs:
uses: "ramsey/composer-install@v4" uses: "ramsey/composer-install@v4"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v9.0.0 uses: astral-sh/setup-uv@v10.1.0
with: with:
enable-cache: true enable-cache: true

View file

@ -2,6 +2,13 @@
All notable changes to `mcp/sdk` will be documented in this file. 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 0.8.0
----- -----

View file

@ -1,19 +1,10 @@
# MCP PHP SDK # MCP PHP SDK
<div align="center">
[![Latest Version](https://img.shields.io/packagist/v/mcp/sdk.svg)](https://packagist.org/packages/mcp/sdk) [![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) [![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) [![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) [![License](https://img.shields.io/packagist/l/mcp/sdk.svg)](LICENSE)
[![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)
[![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)
</div>
The official PHP SDK for the Model Context Protocol (MCP). It provides a framework-agnostic API for implementing MCP 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 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). protocol eras (the `initialize` handshake and the stateless `2026-07-28` revision).
@ -25,6 +16,17 @@ development practices and standards from the Symfony project, including [Coding
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 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. planned next steps and features.
## Protocol Conformance
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 ## Installation
```bash ```bash

View file

@ -35,6 +35,7 @@ The SDK dispatches 4 broad event types at the protocol level, allowing you to ob
**Dispatched**: When any request is received from the client, before it's processed by handlers. **Dispatched**: When any request is received from the client, before it's processed by handlers.
**Properties**: **Properties**:
- `getRequest(): Request` - The incoming request - `getRequest(): Request` - The incoming request
- `setRequest(Request $request): void` - Modify the request before processing - `setRequest(Request $request): void` - Modify the request before processing
- `getSession(): SessionInterface` - The current session - `getSession(): SessionInterface` - The current session
@ -45,6 +46,7 @@ The SDK dispatches 4 broad event types at the protocol level, allowing you to ob
**Dispatched**: When a successful response is ready to be sent to the client, after handler execution. **Dispatched**: When a successful response is ready to be sent to the client, after handler execution.
**Properties**: **Properties**:
- `getResponse(): Response` - The response being sent - `getResponse(): Response` - The response being sent
- `setResponse(Response $response): void` - Modify the response before sending - `setResponse(Response $response): void` - Modify the response before sending
- `getRequest(): Request` - The original request - `getRequest(): Request` - The original request
@ -56,6 +58,7 @@ The SDK dispatches 4 broad event types at the protocol level, allowing you to ob
**Dispatched**: When an error occurs during request processing. **Dispatched**: When an error occurs during request processing.
**Properties**: **Properties**:
- `getError(): Error` - The error being sent - `getError(): Error` - The error being sent
- `setError(Error $error): void` - Modify the error before sending - `setError(Error $error): void` - Modify the error before sending
- `getRequest(): Request` - The original request. Messages that fail to parse are rejected before this event, so a listener never sees them. - `getRequest(): Request` - The original request. Messages that fail to parse are rejected before this event, so a listener never sees them.
@ -67,6 +70,7 @@ The SDK dispatches 4 broad event types at the protocol level, allowing you to ob
**Dispatched**: When a notification is received from the client, before it's processed by handlers. **Dispatched**: When a notification is received from the client, before it's processed by handlers.
**Properties**: **Properties**:
- `getNotification(): Notification` - The incoming notification - `getNotification(): Notification` - The incoming notification
- `setNotification(Notification $notification): void` - Modify the notification before processing - `setNotification(Notification $notification): void` - Modify the notification before processing
- `getSession(): SessionInterface` - The current session - `getSession(): SessionInterface` - The current session

View file

@ -169,6 +169,7 @@ $client->connect($transport);
``` ```
The `connect()` method performs the MCP initialization handshake: The `connect()` method performs the MCP initialization handshake:
1. Opens the transport connection 1. Opens the transport connection
2. Sends InitializeRequest with client capabilities 2. Sends InitializeRequest with client capabilities
3. Waits for InitializeResult from server 3. Waits for InitializeResult from server

View file

@ -18,6 +18,7 @@ $transport = new StdioTransport(
``` ```
**Parameters:** **Parameters:**
- `command` (string): The command to execute - `command` (string): The command to execute
- `args` (array): Command arguments - `args` (array): Command arguments
- `cwd` (string|null): Working directory for the process - `cwd` (string|null): Working directory for the process
@ -39,6 +40,7 @@ $transport = new HttpTransport(
``` ```
**Parameters:** **Parameters:**
- `endpoint` (string): The MCP server URL - `endpoint` (string): The MCP server URL
- `headers` (array): Additional HTTP headers - `headers` (array): Additional HTTP headers
- `httpClient` (ClientInterface|null): PSR-18 HTTP client (auto-discovered) - `httpClient` (ClientInterface|null): PSR-18 HTTP client (auto-discovered)
@ -50,6 +52,7 @@ $transport = new HttpTransport(
**PSR-18 Auto-Discovery:** **PSR-18 Auto-Discovery:**
The transport automatically discovers PSR-18 HTTP clients from: The transport automatically discovers PSR-18 HTTP clients from:
- `php-http/guzzle7-adapter` - `php-http/guzzle7-adapter`
- `php-http/curl-client` - `php-http/curl-client`
- `symfony/http-client` - `symfony/http-client`

View file

@ -54,6 +54,7 @@ $server = Server::builder()
``` ```
**Parameters:** **Parameters:**
- `$name` (string): The server name - `$name` (string): The server name
- `$version` (string): Version string (semantic versioning recommended) - `$version` (string): Version string (semantic versioning recommended)
- `$description` (string|null): Optional description - `$description` (string|null): Optional description
@ -152,6 +153,7 @@ $server = Server::builder()
``` ```
**Parameters:** **Parameters:**
- `$basePath` (string): Base directory for discovery (typically `__DIR__`) - `$basePath` (string): Base directory for discovery (typically `__DIR__`)
- `$scanDirs` (array): Directories to recursively scan for `#[McpTool]`, `#[McpResource]`, etc. All subdirectories are included. (default: `['.', 'src']`) - `$scanDirs` (array): Directories to recursively scan for `#[McpTool]`, `#[McpResource]`, etc. All subdirectories are included. (default: `['.', 'src']`)
- `$excludeDirs` (array): Directory names to exclude **within** the scanned directories during recursive scanning - `$excludeDirs` (array): Directory names to exclude **within** the scanned directories during recursive scanning
@ -189,6 +191,7 @@ $server = Server::builder()
``` ```
**How `excludeDirs` works:** **How `excludeDirs` works:**
- If scanning `src/` and there's `src/vendor/`, it will be excluded - If scanning `src/` and there's `src/vendor/`, it will be excluded
- If scanning `lib/` and there's `lib/tests/`, it will be excluded - If scanning `lib/` and there's `lib/tests/`, it will be excluded
- But if `vendor/` and `tests/` are at the same level as `src/`, they're not scanned anyway (not in `scanDirs`) - But if `vendor/` and `tests/` are at the same level as `src/`, they're not scanned anyway (not in `scanDirs`)
@ -217,6 +220,7 @@ $server = Server::builder()
``` ```
**Basic Container Features:** **Basic Container Features:**
- Supports constructor auto-wiring for classes with parameterless constructors - Supports constructor auto-wiring for classes with parameterless constructors
- Resolves dependencies where all parameters are type-hinted classes/interfaces known to the container - Resolves dependencies where all parameters are type-hinted classes/interfaces known to the container
- Supports parameters with default values - Supports parameters with default values

View file

@ -74,6 +74,7 @@ $server = Server::builder()
``` ```
**Parameters:** **Parameters:**
- `$gcProbability` (int): The numerator of the GC probability fraction (default: `1`). Set to `0` to disable GC. - `$gcProbability` (int): The numerator of the GC probability fraction (default: `1`). Set to `0` to disable GC.
- `$gcDivisor` (int): The denominator of the GC probability fraction (default: `100`). Must be >= 1. - `$gcDivisor` (int): The denominator of the GC probability fraction (default: `100`). Must be >= 1.
@ -81,6 +82,7 @@ $server = Server::builder()
> the `gcProbability` and `gcDivisor` settings are ignored — you control GC behavior in your own implementation. > the `gcProbability` and `gcDivisor` settings are ignored — you control GC behavior in your own implementation.
**Available Session Stores:** **Available Session Stores:**
- `InMemorySessionStore`: Fast in-memory storage (default) - `InMemorySessionStore`: Fast in-memory storage (default)
- `FileSessionStore`: Persistent file-based storage - `FileSessionStore`: Persistent file-based storage
- `Psr16SessionStore`: PSR-16 compliant cache-based storage - `Psr16SessionStore`: PSR-16 compliant cache-based storage

View file

@ -87,6 +87,7 @@ public function getUserProfile(
``` ```
**Provider Resolution:** **Provider Resolution:**
- **Class strings** (`Provider::class`) → Resolved from PSR-11 container - **Class strings** (`Provider::class`) → Resolved from PSR-11 container
- **Instances** (`new Provider()`) → Used directly - **Instances** (`new Provider()`) → Used directly
- **Values** (`['a', 'b']`) → Wrapped in `ListCompletionProvider` - **Values** (`['a', 'b']`) → Wrapped in `ListCompletionProvider`

View file

@ -100,6 +100,7 @@ makes the prompt handler throw.
### Error Handling ### Error Handling
Prompt handlers can throw any exception, but the type determines how it's handled: Prompt handlers can throw any exception, but the type determines how it's handled:
- **`PromptGetException`**: Converted to JSON-RPC error response with the actual exception message - **`PromptGetException`**: Converted to JSON-RPC error response with the actual exception message
- **Any other exception**: Converted to JSON-RPC error response, but with a generic error message - **Any other exception**: Converted to JSON-RPC error response, but with a generic error message

View file

@ -6,6 +6,7 @@ registry somehow. There are three ways to get it there, and they mix freely.
## Attribute-Based Discovery ## Attribute-Based Discovery
**Advantages:** **Advantages:**
- Declarative and readable - Declarative and readable
- Automatic parameter inference - Automatic parameter inference
- DocBlock integration - DocBlock integration
@ -25,6 +26,7 @@ Register MCP elements programmatically without using attributes. The handler is
any PHP callable. any PHP callable.
**Advantages:** **Advantages:**
- Fine-grained control - Fine-grained control
- Runtime configuration - Runtime configuration
- Conditional registration - Conditional registration

View file

@ -24,9 +24,9 @@ class CompletionProvider
* @param class-string<ProviderInterface>|ProviderInterface|null $provider if a class-string, it will be resolved * @param class-string<ProviderInterface>|ProviderInterface|null $provider if a class-string, it will be resolved
* from the container at the point of use * from the container at the point of use
* @param ?array<int, int|float|string> $values a list of values to use for completion * @param ?array<int, int|float|string> $values a list of values to use for completion
* @param class-string|null $enum an enum whose cases are the completions
*/ */
public function __construct( public function __construct(
public ?string $providerClass = null,
public string|ProviderInterface|null $provider = null, public string|ProviderInterface|null $provider = null,
public ?array $values = null, public ?array $values = null,
public ?string $enum = null, public ?string $enum = null,

View file

@ -325,8 +325,6 @@ final class Discoverer implements DiscovererInterface
if ($attributeInstance->provider) { if ($attributeInstance->provider) {
$completionProviders[$param->getName()] = $attributeInstance->provider; $completionProviders[$param->getName()] = $attributeInstance->provider;
} elseif ($attributeInstance->providerClass) {
$completionProviders[$param->getName()] = $attributeInstance->provider;
} elseif ($attributeInstance->values) { } elseif ($attributeInstance->values) {
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values); $completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
} elseif ($attributeInstance->enum) { } elseif ($attributeInstance->enum) {

View file

@ -88,10 +88,27 @@ final class Registry implements RegistryInterface
return; return;
} }
$this->loadFrom($this->loader);
$this->loaded = true;
}
/**
* Runs $loader with the change events its registrations would dispatch suppressed, since they
* describe the registry filling up rather than changing.
*
* Re-entrant-safe, and failure propagates. Does not mark the registry loaded: $loader is the
* caller's, and the one the constructor took is still owed its run.
*/
public function loadFrom(LoaderInterface $loader): void
{
if ($this->loading) {
return;
}
$this->loading = true; $this->loading = true;
try { try {
$this->loader->load($this); $loader->load($this);
$this->loaded = true;
} finally { } finally {
$this->loading = false; $this->loading = false;
} }

View file

@ -287,7 +287,7 @@ final class ReflectedElementLoader implements LoaderInterface
} }
/** /**
* @return array<string, ProviderInterface> * @return array<string, class-string<ProviderInterface>|ProviderInterface>
*/ */
private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array
{ {
@ -307,8 +307,6 @@ final class ReflectedElementLoader implements LoaderInterface
if ($attributeInstance->provider) { if ($attributeInstance->provider) {
$completionProviders[$param->getName()] = $attributeInstance->provider; $completionProviders[$param->getName()] = $attributeInstance->provider;
} elseif ($attributeInstance->providerClass) {
$completionProviders[$param->getName()] = $attributeInstance->providerClass;
} elseif ($attributeInstance->values) { } elseif ($attributeInstance->values) {
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values); $completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
} elseif ($attributeInstance->enum) { } elseif ($attributeInstance->enum) {

View file

@ -97,10 +97,32 @@ final class ReferenceHandler implements ReferenceHandlerInterface
$finalArgs = []; $finalArgs = [];
foreach ($reflection->getParameters() as $parameter) { foreach ($reflection->getParameters() as $parameter) {
// TODO: Handle variadic parameters.
$paramName = $parameter->getName(); $paramName = $parameter->getName();
$paramPosition = $parameter->getPosition(); $paramPosition = $parameter->getPosition();
if ($parameter->isVariadic()) {
// Each element is cast individually below; a non-array value is
// wrapped as a single element, since prompt arguments arrive as
// plain strings (Record<string,string> per the protocol) even
// though tool arguments follow the advertised "array" schema.
$values = $arguments[$paramName] ?? [];
if (!\is_array($values)) {
$values = [$values];
} elseif (!array_is_list($values)) {
throw RegistryException::invalidParams(\sprintf('Parameter `%s` must be a list of values, not an object.', $paramName));
}
foreach ($values as $index => $value) {
try {
$finalArgs[] = $this->castArgumentType($value, $parameter);
} catch (InvalidArgumentException $e) {
throw RegistryException::invalidParams(\sprintf('Parameter `%s[%d]`: %s', $paramName, $index, $e->getMessage()), $e);
} catch (\Throwable $e) {
throw RegistryException::internalError(\sprintf('Error processing parameter `%s[%d]`: %s', $paramName, $index, $e->getMessage()), $e);
}
}
continue;
}
// Check if parameter is a special injectable type // Check if parameter is a special injectable type
$type = $parameter->getType(); $type = $parameter->getType();
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {

View file

@ -121,6 +121,22 @@ class HttpTransport extends BaseTransport implements HeaderAwareTransportInterfa
$this->headerCallback = $callback; $this->headerCallback = $callback;
} }
/**
* The session ID minted by the server for this connection, if any.
*
* Captured from the Mcp-Session-Id response header of a handshake-era
* server; always null on 2026-07-28, which removed protocol-level sessions
* (SEP-2567). A caller that cannot keep the transport alive between
* interactions can persist it and pass it back through the constructor's
* $headers on a later transport, where it wins over the tracked value.
*
* @return string|null null until the server mints a session, and again after close()
*/
public function getSessionId(): ?string
{
return $this->sessionId;
}
public function send(string $data): void public function send(string $data): void
{ {
$request = $this->requestFactory->createRequest('POST', $this->endpoint) $request = $this->requestFactory->createRequest('POST', $this->endpoint)

View file

@ -74,7 +74,7 @@ abstract class Notification implements HasMethodInterface, MessageInterface
$array['params'] = $params; $array['params'] = $params;
} }
if (null !== $this->meta && !isset($params['meta'])) { if (null !== $this->meta && !isset($params['_meta'])) {
$array['params']['_meta'] = $this->meta; $array['params']['_meta'] = $this->meta;
} }

View file

@ -122,7 +122,7 @@ abstract class Request implements HasMethodInterface, MessageInterface
$array['params'] = $params; $array['params'] = $params;
} }
if (null !== $this->meta && !isset($params['meta'])) { if (null !== $this->meta && !isset($params['_meta'])) {
$array['params']['_meta'] = $this->meta; $array['params']['_meta'] = $this->meta;
} }

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