-
Notifications
You must be signed in to change notification settings - Fork 92
Adds first-class notification subscription support via a notifications/ directory convention #526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
0xKoller
wants to merge
7
commits into
canary
Choose a base branch
from
first-class-notification-subscription-support-via-a-xmcp-411
base: canary
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
59b3e8f
Notificacions
0xKoller 66a05c5
Merge branch 'canary' into first-class-notification-subscription-supp…
0xKoller d632f46
Update pnpm-lock.yaml
0xKoller c093646
Asks and fixes
0xKoller 367cde1
Update test-notifications.ts
0xKoller e3e9e8d
Merge branch 'canary' into first-class-notification-subscription-supp…
0xKoller 8cfc142
custom notification example
0xKoller File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| "tools", | ||
| "prompts", | ||
| "resources", | ||
| "notifications", | ||
| "middlewares", | ||
| "css", | ||
| "external-clients" | ||
|
|
||
102 changes: 102 additions & 0 deletions
102
apps/website/content/docs/core-concepts/notifications.mdx
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| --- | ||
| title: "Notifications" | ||
| metadataTitle: "Notifications | xmcp Documentation" | ||
| publishedAt: "2026-03-19" | ||
| summary: "Learn how to subscribe to MCP client notifications in your xmcp application." | ||
| description: "Notifications are one-way messages from clients to servers. xmcp provides a file-based convention for subscribing to these events using the subscribe() function." | ||
| --- | ||
|
|
||
| By default, `xmcp` detects files under the `/src/notifications/` directory and registers them as notification handlers, but you can specify a [custom directory](/docs/configuration/custom-directories) if you prefer. | ||
|
|
||
| A notification file exports a single default using the `subscribe()` function: | ||
|
|
||
| ```typescript title="src/notifications/roots-changed.ts" | ||
| import { subscribe } from "xmcp"; | ||
|
|
||
| export default subscribe("notifications/roots/list_changed", async (params, extra) => { | ||
| console.log("Client roots changed — refreshing workspace context"); | ||
| }); | ||
| ``` | ||
|
|
||
| ## What are MCP notifications? | ||
|
|
||
| In the MCP protocol, notifications are **one-way messages** that don't expect a response. Clients send notifications to servers to signal events like session initialization, request cancellation, or workspace changes. | ||
|
|
||
| ## Available notification methods | ||
|
|
||
| These are the standard MCP client-to-server notification methods: | ||
|
|
||
| | Method | Params | Description | | ||
| | ------------------------------------ | --------------------------------------------------- | --------------------------------------------------- | | ||
| | `notifications/initialized` | _none_ | Client completed the initialization handshake | | ||
| | `notifications/roots/list_changed` | _none_ | Client's root URIs changed | | ||
| | `notifications/cancelled` | `requestId`, `reason?` | Client cancelled an in-flight request | | ||
| | `notifications/progress` | `progressToken`, `progress`, `total?`, `message?` | Client sent a progress update | | ||
|
|
||
| Custom notification methods are also supported — `params` will be typed as `Record<string, unknown>`. | ||
|
|
||
| ## Handler signature | ||
|
|
||
| The handler receives two arguments: | ||
|
|
||
| - **`params`**: The notification parameters, fully typed for known methods. | ||
| - **`extra`**: Context object with `signal` (AbortSignal). | ||
|
|
||
| ```typescript title="src/notifications/on-cancel.ts" | ||
| import { subscribe } from "xmcp"; | ||
|
|
||
| export default subscribe("notifications/cancelled", async (params, extra) => { | ||
| // params.requestId and params.reason are fully typed | ||
| console.log(`Request ${params.requestId} was cancelled: ${params.reason}`); | ||
| }); | ||
| ``` | ||
|
|
||
| ## Multiple handlers | ||
|
|
||
| Multiple files can subscribe to the same notification method. All handlers run **concurrently** and are **isolated** — if one throws, the others still execute. | ||
|
|
||
| ```typescript title="src/notifications/log-cancel.ts" | ||
| import { subscribe } from "xmcp"; | ||
|
|
||
| export default subscribe("notifications/cancelled", async (params) => { | ||
| console.log(`Cancelled: ${params.requestId}`); | ||
| }); | ||
| ``` | ||
|
|
||
| ```typescript title="src/notifications/metrics-cancel.ts" | ||
| import { subscribe } from "xmcp"; | ||
|
|
||
| export default subscribe("notifications/cancelled", async (params) => { | ||
| // Track cancellation metrics | ||
| metrics.increment("request.cancelled"); | ||
| }); | ||
| ``` | ||
|
|
||
| Both handlers fire when a cancellation notification arrives. | ||
|
|
||
| ## SDK handler preservation | ||
|
|
||
| For `notifications/cancelled` and `notifications/progress`, the MCP SDK has built-in internal handlers that manage request abortion and progress tracking. Your handlers run **alongside** them — they never replace the SDK's built-in behavior. | ||
|
|
||
| ## Custom notification methods | ||
|
|
||
| You can subscribe to any notification method string, not just the standard ones. Custom methods have `params` typed as `Record<string, unknown>`: | ||
|
|
||
| ```typescript title="src/notifications/custom-event.ts" | ||
| import { subscribe } from "xmcp"; | ||
|
|
||
| export default subscribe("custom/my-event", async (params) => { | ||
| // params is Record<string, unknown> | ||
| console.log("Custom event received:", params); | ||
| }); | ||
| ``` | ||
|
|
||
| ## Error handling | ||
|
|
||
| Handler errors are logged automatically and don't affect other handlers or server stability: | ||
|
|
||
| ``` | ||
| [xmcp] Notification handler error for "notifications/cancelled": Error: something went wrong | ||
| ``` | ||
|
|
||
| Invalid notification files (missing or malformed `subscribe()` export) produce a warning but don't prevent the server from starting. |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # Notifications | ||
|
|
||
| Subscribe to MCP client notifications using xmcp's file-based convention. | ||
|
|
||
| ## What are MCP notifications? | ||
|
|
||
| In MCP, notifications are **one-way messages** from client to server (or server to client). Unlike requests, they don't expect a response. Common client-to-server notifications include: | ||
|
|
||
| | Notification | When it fires | | ||
| | ------------------------------------ | ----------------------------------------------- | | ||
| | `notifications/initialized` | Client completed the initialization handshake | | ||
| | `notifications/roots/list_changed` | Client's root URIs changed | | ||
| | `notifications/cancelled` | Client cancelled an in-flight request | | ||
| | `notifications/progress` | Client sent a progress update | | ||
|
|
||
| ## How it works | ||
|
|
||
| Create files in `src/notifications/` that export a `subscribe()` call: | ||
|
|
||
| ```ts | ||
| // src/notifications/roots-changed.ts | ||
| import { subscribe } from "xmcp"; | ||
|
|
||
| export default subscribe("notifications/roots/list_changed", async () => { | ||
| console.log("Roots list changed!"); | ||
| }); | ||
| ``` | ||
|
|
||
| xmcp discovers these files at build time (just like tools, prompts, and resources) and registers the handlers on the MCP server at runtime. | ||
|
|
||
| ### Handler signature | ||
|
|
||
| ```ts | ||
| subscribe(method, async (params, extra) => { | ||
| // params — typed based on the notification method | ||
| // extra — { signal: AbortSignal } | ||
| }); | ||
| ``` | ||
|
|
||
| - **Known methods** (`notifications/cancelled`, `notifications/progress`, etc.) get fully typed `params`. | ||
| - **Custom methods** are also supported — `params` is typed as `Record<string, unknown>`. | ||
| - **Multiple files** can subscribe to the same notification. All handlers run concurrently. | ||
| - **Error isolation** — if one handler throws, others still execute. | ||
|
|
||
| ### SDK handler preservation | ||
|
|
||
| For `notifications/cancelled` and `notifications/progress`, the MCP SDK already has internal handlers (for aborting requests and tracking progress). Your handlers run **alongside** them — they don't replace the SDK's built-in behavior. | ||
|
|
||
| ## Project structure | ||
|
|
||
| ``` | ||
| src/ | ||
| ├── notifications/ | ||
| │ ├── roots-changed.ts # Reacts to root URI changes | ||
| │ ├── on-cancel.ts # Logs cancelled requests | ||
| │ ├── on-progress.ts # Logs progress updates | ||
| │ └── on-initialized.ts # Logs session initialization | ||
| └── tools/ | ||
| └── ping.ts # Simple ping tool | ||
| ``` | ||
|
|
||
| ## Running the example | ||
|
|
||
| ### 1. Start the server | ||
|
|
||
| ```bash | ||
| pnpm dev | ||
| ``` | ||
|
|
||
| ### 2. Send test notifications | ||
|
|
||
| In a second terminal: | ||
|
|
||
| ```bash | ||
| pnpm run test:notifications | ||
| ``` | ||
|
|
||
| If the server started on a different port (check the server terminal output), set the URL: | ||
|
|
||
| ```bash | ||
| SERVER_URL=http://localhost:3002/mcp pnpm run test:notifications | ||
| ``` | ||
|
|
||
| ### 3. Check the server terminal | ||
|
|
||
| You should see output like: | ||
|
|
||
| ``` | ||
| [notification] Client session initialized | ||
| [notification] Roots list changed — workspace context may need updating | ||
| [notification] Request req-42 was cancelled: User clicked cancel | ||
| [notification] Progress (75%): Almost done | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| The notifications directory path is configurable in `xmcp.config.ts`: | ||
|
|
||
| ```ts | ||
| const config: XmcpConfig = { | ||
| paths: { | ||
| notifications: "src/notifications", // default | ||
| // notifications: "src/events", // custom path | ||
| // notifications: false, // disable | ||
| }, | ||
| }; | ||
| ``` |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "notifications", | ||
| "description": "Learn how to subscribe to MCP client notifications using xmcp", | ||
| "keywords": [ | ||
| "notifications", | ||
| "subscribe", | ||
| "events" | ||
| ], | ||
| "scripts": { | ||
| "build": "xmcp build", | ||
| "dev": "xmcp dev", | ||
| "start": "node dist/http.js", | ||
| "test:notifications": "node --experimental-strip-types test-notifications.ts" | ||
| }, | ||
| "dependencies": { | ||
| "xmcp": "workspace:*", | ||
| "zod": "^4.0.10" | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { subscribe } from "xmcp"; | ||
|
|
||
| /** | ||
| * Fires when the client cancels an in-flight request. | ||
| * The SDK already handles aborting the request internally — | ||
| * this handler runs alongside it (additive) for logging or cleanup. | ||
| */ | ||
| export default subscribe("notifications/cancelled", async (params) => { | ||
| console.log( | ||
| `[notification] Request ${params.requestId} was cancelled` + | ||
| (params.reason ? `: ${params.reason}` : "") | ||
| ); | ||
| }); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { subscribe } from "xmcp"; | ||
|
|
||
| /** | ||
| * Fires when the client completes the initialization handshake. | ||
| * Use this for one-time setup that requires a live session. | ||
| */ | ||
| export default subscribe("notifications/initialized", async () => { | ||
| console.log("[notification] Client session initialized"); | ||
| }); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { subscribe } from "xmcp"; | ||
|
|
||
| /** | ||
| * Fires when the client sends a progress update. | ||
| * The SDK already tracks progress internally — | ||
| * this handler runs alongside it (additive) for logging or custom metrics. | ||
| */ | ||
| export default subscribe("notifications/progress", async (params) => { | ||
| const pct = params.total | ||
| ? ` (${Math.round((params.progress / params.total) * 100)}%)` | ||
| : ""; | ||
| console.log( | ||
| `[notification] Progress${pct}: ${params.message ?? "no message"}` | ||
| ); | ||
| }); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { subscribe } from "xmcp"; | ||
|
|
||
| /** | ||
| * Fires when the client's list of root URIs changes. | ||
| * Use this to update workspace context, refresh file watchers, etc. | ||
| */ | ||
| export default subscribe("notifications/roots/list_changed", async () => { | ||
| console.log( | ||
| "[notification] Roots list changed — workspace context may need updating" | ||
| ); | ||
| }); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { type ToolMetadata } from "xmcp"; | ||
|
|
||
| export const metadata: ToolMetadata = { | ||
| name: "ping", | ||
| description: "Simple ping tool to verify the server is running", | ||
| }; | ||
|
|
||
| export default function ping() { | ||
| return "pong"; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.