Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ summary: "Learn how to customize the directories for tools, prompts, and resourc
description: "Customize where xmcp looks for tools, prompts, and resources."
---

Customize where `xmcp` looks for tools, prompts, and resources by configuring the `paths` option. If not specified, these are the defaults:
Customize where `xmcp` looks for tools, prompts, resources, and notifications by configuring the `paths` option. If not specified, these are the defaults:

```typescript title="xmcp.config.ts"
const config: XmcpConfig = {
paths: {
tools: "src/tools",
prompts: "src/prompts",
resources: "src/resources",
notifications: "src/notifications",
},
};

Expand All @@ -30,6 +31,7 @@ const config: XmcpConfig = {
tools: "src/tools",
prompts: false, // Prompts directory disabled
resources: "src/resources",
notifications: false, // Notifications directory disabled
},
};

Expand Down
1 change: 1 addition & 0 deletions apps/website/content/docs/core-concepts/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"tools",
"prompts",
"resources",
"notifications",
"middlewares",
"css",
"external-clients"
Expand Down
102 changes: 102 additions & 0 deletions apps/website/content/docs/core-concepts/notifications.mdx
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.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ my-project/
│ └── resources/ # Resource files are auto-discovered here
│ ├── (config)/app.ts
│ ├── (users)/[userId]/profile.ts
│ └── notifications/ # Notification handlers are auto-discovered here
│ ├── roots-changed.ts
│ ├── on-cancel.ts
Comment thread
0xKoller marked this conversation as resolved.
Outdated
├── dist/ # Built output (generated)
├── package.json
├── tsconfig.json
Expand All @@ -41,7 +44,7 @@ There are the three top-level files that are required for your project:

The `src/` directory houses your project's implementation. xmcp follows a declarative, file-system based approach—simply create a file in the appropriate directory, and it will be automatically discovered and registered.

The optional `middleware.ts` file at the root of `src/` processes HTTP requests and responses. You can customize the location of `tools/`, `prompts/`, and `resources/` directories in your `xmcp.config.ts` file. See the [custom directories](../../configuration/custom-directories) documentation for details.
The optional `middleware.ts` file at the root of `src/` processes HTTP requests and responses. You can customize the location of `tools/`, `prompts/`, `resources/`, and `notifications/` directories in your `xmcp.config.ts` file. See the [custom directories](../../configuration/custom-directories) documentation for details.

<ConceptBoxes>
<ConceptBox
Expand All @@ -59,6 +62,11 @@ The optional `middleware.ts` file at the root of `src/` processes HTTP requests
description="Understand how to expose data through resources."
href="/docs/core-concepts/resources"
/>
<ConceptBox
title="Notifications"
description="Subscribe to MCP client notifications like cancellation or root changes."
href="/docs/core-concepts/notifications"
/>
<ConceptBox
title="Middleware"
description="Add custom logic to process requests and responses."
Expand Down
107 changes: 107 additions & 0 deletions examples/notifications/README.md
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
},
};
```
19 changes: 19 additions & 0 deletions examples/notifications/package.json
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"
}
}
13 changes: 13 additions & 0 deletions examples/notifications/src/notifications/on-cancel.ts
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}` : "")
);
});
9 changes: 9 additions & 0 deletions examples/notifications/src/notifications/on-initialized.ts
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");
});
15 changes: 15 additions & 0 deletions examples/notifications/src/notifications/on-progress.ts
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"}`
);
});
11 changes: 11 additions & 0 deletions examples/notifications/src/notifications/roots-changed.ts
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"
);
});
10 changes: 10 additions & 0 deletions examples/notifications/src/tools/ping.ts
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";
}
Loading
Loading