feat: add roots-demo sample for client roots capability#763
Open
MichielDean wants to merge 8 commits into
Open
feat: add roots-demo sample for client roots capability#763MichielDean wants to merge 8 commits into
MichielDean wants to merge 8 commits into
Conversation
Closes modelcontextprotocol#5 Add a standalone sample project demonstrating the MCP Roots capability: - Client declares roots capability with listChanged=true - Client registers filesystem roots using addRoot() - Server queries roots from client via listRoots() - Client sends notifications/roots/list_changed after dynamic changes - Server reacts to change notifications by re-fetching the root list Uses ChannelTransport from kotlin-sdk-testing for in-memory client-server communication without external dependencies.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new standalone sample project under samples/roots-demo/ to demonstrate the MCP Roots client capability end-to-end (client advertises roots + list change support, server lists roots, client notifies changes, server re-fetches).
Changes:
- Added a new
roots-demoGradle sample project (wrapper, version catalog, build config, logging config). - Implemented an in-memory client/server demo using
ChannelTransportthat exercisesroots/listandnotifications/roots/list_changed. - Updated
samples/README.mdto list and describe the new sample.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| samples/roots-demo/src/main/resources/simplelogger.properties | Configures SLF4J SimpleLogger output for the demo. |
| samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt | Implements the in-memory client/server roots lifecycle demo. |
| samples/roots-demo/settings.gradle.kts | Declares sample project name, repositories, and optional MCP Kotlin version override. |
| samples/roots-demo/README.md | Documents what the sample demonstrates and how to run it. |
| samples/roots-demo/gradlew.bat | Adds Gradle wrapper Windows script for the standalone sample. |
| samples/roots-demo/gradlew | Adds Gradle wrapper POSIX script for the standalone sample. |
| samples/roots-demo/gradle/wrapper/gradle-wrapper.properties | Configures the Gradle distribution for the sample wrapper. |
| samples/roots-demo/gradle/libs.versions.toml | Declares dependency/plugin versions used by the sample. |
| samples/roots-demo/gradle.properties | Enables Gradle performance features and supports optional SDK override version. |
| samples/roots-demo/build.gradle.kts | Configures application plugin, dependencies, and toolchain for the sample. |
| samples/README.md | Adds the new roots demo to the sample index and provides a short description. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Remove Tools capability from server (not used in this demo)
- Use async instead of launch+CompletableDeferred in notification handler
so the returned Deferred represents actual work completion
- Construct file URIs from System.getProperty('user.home') for
cross-platform correctness (also ensures file:/// prefix)
- Apply null-safe name fallback consistently (?: '(unnamed)')
…c sync - Add try/catch in notification handler to surface errors instead of silently dropping them - Replace delay(500) with CompletableDeferred-based synchronization so the demo waits for the server to process each notification deterministically instead of relying on timing - Close server before client to avoid Not connected errors
…-demo The notification handler launches coroutines via on Dispatchers.Default, so the increment was a data race — two coroutines could read the same value and both write back 1, causing rootsUpdated to never complete. Replaced with AtomicInteger.incrementAndGet() for thread-safe counting.
- Await each notification separately (firstNotification/secondNotification) before sending the next, so the server processes each state change before the client sends the next one - Remove withTimeout(5000) that could throw TimeoutCancellationException and skip close() calls; now the main coroutine awaits each signal directly, so shutdown always runs - Keep AtomicInteger for thread-safe notification counting
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt:101
firstNotification.await()/secondNotification.await()have no timeout, so./gradlew runcan hang indefinitely if a notification is missed (or if the handler fails before completing the deferred). Consider adding a bounded wait (e.g.,withTimeout) and ensuring shutdown still happens viatry/finallyso the sample exits cleanly in failure cases.
client.sendRootsListChanged()
firstNotification.await()
println("\n[Client] Removing a root and sending list changed notification...")
client.removeRoot(backendRoot)
client.sendRootsListChanged()
secondNotification.await()
added 2 commits
May 12, 2026 17:39
…s on errors Move firstNotification/secondNotification completion into a finally block so that even if listRoots() throws, the main coroutine does not hang waiting on a CompletableDeferred that will never complete.
Every await() on a CompletableDeferred needs an escape hatch so the demo terminates even if notifications never arrive. Both notification awaits now use withTimeoutOrNull with a 5s timeout and print a clear message on timeout. Client/server close() moved into a finally block so resources are always released, even on timeout or unexpected errors.
Strip back the over-engineered async coordination (AtomicInteger, per-notification CompletableDeferred, withTimeoutOrNull, try/finally) and replace with a clear, linear flow that demonstrates the Roots lifecycle: declare capability, add roots, query, notify, react. The demo now matches the simplicity of other SDK samples and the integration tests. A short delay is used for notification propagation which is appropriate for sample code — it prioritizes readability over production-grade async patterns. Key changes: - Removed AtomicInteger counter and separate CompletableDeferred signals - Removed withTimeoutOrNull (not needed for a demo) - Removed try/finally on close (not needed — runBlocking handles cleanup) - Restored hardcoded file:// URIs (matches MCP spec examples and other SDK samples) - Added clear comments for each step in the Roots lifecycle - Total: 82 lines, down from 108
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #5
Adds a standalone sample project (
samples/roots-demo/) demonstrating the MCP Roots capability — how a client exposes filesystem roots to a server, and how the server reacts when the root list changes.What it demonstrates
listChanged = trueduring initializationaddRoot(uri, name)serverSession.listRoots()(roots/listrequest)sendRootsListChanged()after dynamically adding/removing rootsnotifications/roots/list_changedand re-fetching the updated root listImplementation
Uses
ChannelTransportfromkotlin-sdk-testingfor in-memory client-server communication — no external server, network, or API key required. The sample runs as a singlemain()that prints the full roots exchange lifecycle to the console.Files added
samples/roots-demo/— complete standalone Gradle project (build config, version catalog, wrapper)samples/roots-demo/src/main/kotlin/.../Main.kt— demo sourcesamples/roots-demo/README.md— documentationsamples/README.md— updated to include the new sampleBuild verification
./gradlew assemblesucceeds./gradlew runproduces the expected output showing roots lifecyclektlintCheckon the main project passes (sample projects don't include ktlint)