Skip to content

feat: add roots-demo sample for client roots capability#763

Open
MichielDean wants to merge 8 commits into
modelcontextprotocol:mainfrom
MichielDean:feat/add-roots-sample
Open

feat: add roots-demo sample for client roots capability#763
MichielDean wants to merge 8 commits into
modelcontextprotocol:mainfrom
MichielDean:feat/add-roots-sample

Conversation

@MichielDean
Copy link
Copy Markdown

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

  1. Client declares roots capability with listChanged = true during initialization
  2. Client registers roots using addRoot(uri, name)
  3. Server queries roots via serverSession.listRoots() (roots/list request)
  4. Client sends change notification via sendRootsListChanged() after dynamically adding/removing roots
  5. Server reacts to changes by listening for notifications/roots/list_changed and re-fetching the updated root list

Implementation

Uses ChannelTransport from kotlin-sdk-testing for in-memory client-server communication — no external server, network, or API key required. The sample runs as a single main() 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 source
  • samples/roots-demo/README.md — documentation
  • samples/README.md — updated to include the new sample

Build verification

  • ./gradlew assemble succeeds
  • ./gradlew run produces the expected output showing roots lifecycle
  • ktlintCheck on the main project passes (sample projects don't include ktlint)

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.
Copilot AI review requested due to automatic review settings May 12, 2026 19:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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-demo Gradle sample project (wrapper, version catalog, build config, logging config).
  • Implemented an in-memory client/server demo using ChannelTransport that exercises roots/list and notifications/roots/list_changed.
  • Updated samples/README.md to 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.

Comment thread samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt Outdated
Comment thread samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt Outdated
Comment thread samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt Outdated
Comment thread samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt Outdated
- 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)')
MichielDean

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Comment thread samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt Outdated
Comment thread samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt Outdated
…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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.

Comment thread samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt Outdated
…-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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Comment thread samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt Outdated
Comment thread samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt Outdated
- 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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 run can 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 via try/finally so 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()

Comment thread samples/roots-demo/src/main/kotlin/io/modelcontextprotocol/sample/roots/Main.kt Outdated
Lobsterdog Contributors 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.
Copilot AI review requested due to automatic review settings May 12, 2026 23:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated no new comments.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add sample for using Client Roots

2 participants