Skip to content

[Client][Server] Add Roots support - #395

Open
wachterjohannes wants to merge 6 commits into
modelcontextprotocol:mainfrom
wachterjohannes:eve/roots
Open

[Client][Server] Add Roots support#395
wachterjohannes wants to merge 6 commits into
modelcontextprotocol:mainfrom
wachterjohannes:eve/roots

Conversation

@wachterjohannes

@wachterjohannes wachterjohannes commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Adds support for the MCP Roots capability, letting a client expose file:// "workspace folders" that the server is allowed to operate on.

Client side

  • ListRootsRequestHandler + RootsCallbackInterface — answer server roots/list requests from a user-provided callback (mirrors the existing SamplingRequestHandler / SamplingCallbackInterface pattern).
  • Client::sendRootsListChanged() — send notifications/roots/list_changed. Guarded: it throws unless the client advertised the roots.listChanged capability, so a misconfigured client can't emit an undeclared notification.
  • Mcp\Exception\RootsException — thrown from a roots callback, its message is forwarded to the server (same contract as SamplingException); any other throwable is logged and returned as a generic internal error.

Server side

  • ClientGateway::listRoots() — request the client's roots (returns ListRootsResult, throws ClientException on an error response).
  • ClientGateway::supportsRoots() — capability-gating helper alongside supportsElicitation().

Schema

  • ListRootsResult::fromArray() — parse the client's response; Root continues to enforce the file:// URI scheme required by the spec.

Docs / examples / tests

  • README + docs/client.md sections and a runnable examples/client/stdio_roots.php.
  • Unit tests covering the handler, result parsing (incl. non-file:// rejection), capability serialization/round-trip, sendRootsListChanged() gating, and the ClientGateway roots methods.
  • CHANGELOG entry under 0.7.0.

Conformance notes

  • Root URIs are restricted to file:// per the current spec.
  • roots capability presence gates roots/list; roots.listChanged gates the list_changed notification.

@chr-hertel chr-hertel added Client Issues & PRs related to the Client component Server Issues & PRs related to the Server component improves spec compliance Improves consistency with other SDKs such as TyepScript labels Jul 14, 2026
Comment thread src/Schema/Result/ListRootsResult.php Outdated
@chr-hertel chr-hertel added the needs more work Not ready to be merged yet, needs additional follow-up from the author(s). label Jul 14, 2026
wachterjohannes and others added 4 commits July 20, 2026 08:59
…y::listRoots)

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.
- Add ListRootsResult::fromArray() (used by listRoots()).

Includes unit tests, a runnable examples/client/stdio_roots.php example,
and docs/README/CHANGELOG updates. phpunit, php-cs-fixer and phpstan green.

Co-Authored-By: Claude <noreply@anthropic.com>
0.6.0 is already released; the Roots additions belong under a new unreleased
0.7.0 section rather than the shipped 0.6.0.

Co-Authored-By: Claude <noreply@anthropic.com>
…rding, docs & tests

Applies the five findings from the PR #1 code review:

1. Spec conformance: examples/docs/README now advertise
   `new ClientCapabilities(roots: true, rootsListChanged: true)` before
   sending `notifications/roots/list_changed`.
2. `ClientGateway::supportsRoots()`/`supportsElicitation()` normalize the
   stored capabilities to an array, avoiding a TypeError when a client
   advertised an empty capabilities object (\stdClass).
3. `Client::sendRootsListChanged()` now throws when the client did not
   advertise the `roots.listChanged` capability.
4. Add tests: capability serialization/round-trip, sendRootsListChanged
   gating (sends when declared, throws otherwise), RootsException message
   forwarding, and rejection of non-file:// root URIs.
5. `ListRootsRequestHandler` forwards `RootsException` messages to the
   server (new Mcp\Exception\RootsException), mirroring SamplingRequestHandler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NrvzihxUvQzDSoWzjsm58
@chr-hertel chr-hertel added this to the 0.8.0 milestone Jul 27, 2026
@chr-hertel chr-hertel removed the needs more work Not ready to be merged yet, needs additional follow-up from the author(s). label Jul 27, 2026
Comment thread CHANGELOG.md Outdated
* 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).
* Add client-side `roots/list` handler (`ListRootsRequestHandler` + `RootsCallbackInterface`) and `Client::sendRootsListChanged()`, plus server-side `ClientGateway::listRoots()` / `supportsRoots()` and `ListRootsResult::fromArray()`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to move to a 0.8.0 section please

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to a new 0.8.0 section in 4c21047.

Comment thread src/Client.php
throw new RuntimeException('Cannot send a "roots/list_changed" notification without advertising the "roots.listChanged" capability. Build the client with new ClientCapabilities(roots: true, rootsListChanged: true).');
}

$this->protocol->sendNotification(new RootsListChangedNotification());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i get why you're not using sendRequest - it's just a notification, but is this sufficient - we're skipping isConnected check here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — it was worse than just skipping the check: Protocol::sendNotification() sends via $this->transport?->send(), so on a disconnected client the notification was silently dropped instead of failing. It now throws a ConnectionException like sendRequest() does (capability check stays first), with a test for it. 5d278a9

@chr-hertel chr-hertel Jul 27, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think we need the try-catch block here

and, not sure, but can we demo it the other way around so that the list roots request actually is executed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both done in 5d278a9. try/catch is gone, and the demo is turned around: the client-communication server now has an inspect_workspace_roots tool using ClientGateway::supportsRoots()/listRoots(), and the example calls it — so the server really issues roots/list and the client's handler answers. Output when run:

Calling 'inspect_workspace_roots'...
[ROOTS] Server requested the client's list of roots

Result:
{
    "status": "ok",
    "message": "Client exposed 2 root(s).",
    "roots": [
        {"uri": "file:///home/user/projects/app", "name": "Application"},
        {"uri": "file:///home/user/projects/library", "name": "Library"}
    ]
}

The sendRootsListChanged() notification is kept at the end as a secondary demo.

@chr-hertel chr-hertel added the needs more work Not ready to be merged yet, needs additional follow-up from the author(s). label Jul 27, 2026
wachterjohannes and others added 2 commits July 27, 2026 19:56
v0.7.0 is released, so the Roots entry belongs in an unreleased 0.8.0
section instead of the shipped 0.7.0 one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ts/list end-to-end

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 capability, so the roots/list handler
never 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.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Client Issues & PRs related to the Client component improves spec compliance Improves consistency with other SDKs such as TyepScript needs more work Not ready to be merged yet, needs additional follow-up from the author(s). Server Issues & PRs related to the Server component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants