Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
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
113 changes: 113 additions & 0 deletions apps/website/content/docs/core-concepts/notifications.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
title: "Notifications"
metadataTitle: "Notifications | xmcp Documentation"
publishedAt: "2026-03-19"
summary: "Learn how to handle MCP client notifications in your xmcp application."
description: "Notifications are one-way messages in the MCP protocol. They allow clients to inform servers about lifecycle events, cancellations, progress updates, and more, without expecting a response."
---

In the MCP protocol, notifications are **one-way messages**. Unlike requests, they don't expect a response. Clients send notifications to servers to inform them about events such as:

- The client has finished initializing and is ready.
- A previously issued request has been cancelled.
- Progress has been made on a long-running operation.
- The client's filesystem roots have changed.
- A task's status has been updated.

Notifications are fire-and-forget: the server receives them, acts on them if needed, and never sends a reply.

## Client-to-server notifications

These are all the notifications a server can receive from a client, as defined in the [MCP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/index#notifications):

| Key | MCP Method | Params | Description |
| ------------------ | ------------------------------------- | ------------------------------------------------------------- | --------------------------------------------- |
| `initialized` | `notifications/initialized` | _none_ | Client completed the initialization handshake |
| `cancelled` | `notifications/cancelled` | `requestId?`, `reason?` | Client cancelled an in-flight request |
| `progress` | `notifications/progress` | `progressToken`, `progress`, `total?`, `message?` | Progress update on a long-running request |
| `rootsListChanged` | `notifications/roots/list_changed` | _none_ | Client's root URIs changed |
| `taskStatus` | `notifications/tasks/status` | `taskId`, `status`, `statusMessage?`, `createdAt`, `lastUpdatedAt`, `ttl`, `pollInterval?` | Task status changed |

## Handling notifications in xmcp

Create a `src/notifications.ts` file that exports a `defineNotifications()` call. xmcp discovers this file at build time, following the same single-file pattern used by `src/middleware.ts`.

```typescript title="src/notifications.ts"
import { defineNotifications } from "xmcp";

export default defineNotifications({
initialized: async () => {
console.log("Client session initialized");
},
rootsListChanged: async () => {
console.log("Client roots changed, refreshing workspace context");
},
});
```

xmcp maps the short keys to full MCP method names automatically (e.g. `cancelled` to `notifications/cancelled`). All keys are optional, only subscribe to what you need.

## Handler signature

Notifications **without params** (`initialized`, `rootsListChanged`) take no arguments. Notifications **with params** receive them fully typed:

```typescript title="src/notifications.ts"
import { defineNotifications } from "xmcp";

export default defineNotifications({
initialized: async () => {
console.log("Client ready");
},

cancelled: async (params) => {
// params.requestId and params.reason are fully typed
console.log(`Request ${params.requestId} was cancelled: ${params.reason}`);
},

progress: async (params) => {
const pct = params.total
? ` (${Math.round((params.progress / params.total) * 100)}%)`
: "";
console.log(`Progress${pct}: ${params.message ?? "no message"}`);
},

taskStatus: async (params) => {
console.log(`Task ${params.taskId}: ${params.status}`);
},
});
```

## Custom notification methods

Beyond the standard MCP notifications, you can handle any custom method by using its full method string as the key. Custom methods have `params` typed as `Record<string, unknown>`:

```typescript title="src/notifications.ts"
import { defineNotifications } from "xmcp";

export default defineNotifications({
initialized: async () => {
console.log("Client ready");
},
"custom/my-event": async (params) => {
console.log("Custom event received:", params);
},
});
```

## SDK handler preservation

For `cancelled` and `progress`, the MCP SDK registers internal handlers that manage request abortion and progress tracking. Your handlers run **alongside** them and never replace the SDK's built-in behavior.

<Callout variant="info">
If a future SDK update changes its internal handler structure, xmcp will throw
a clear error at startup rather than silently losing cancellation or progress
tracking.
</Callout>

## Error handling

Handler errors are caught and logged automatically. A failing handler does not affect server stability or other handlers:

```
[xmcp] Notification handler error for "notifications/cancelled": Error: something went wrong
```
18 changes: 12 additions & 6 deletions apps/website/content/docs/getting-started/project-structure.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,21 @@ A basic project structure is as follows:
```
my-project/
├── src/
│ ├── middleware.ts # Middleware for http request/response processing
│ └── tools/ # Tool files are auto-discovered here
│ ├── middleware.ts # Middleware for http request/response processing
│ ├── notifications.ts # Notification handlers setup file
│ └── tools/ # Tool files are auto-discovered here
│ ├── greet.ts
│ ├── search.ts
│ └── prompts/ # Prompt files are auto-discovered here
│ ├── review-code.ts
│ ├── team-greeting.ts
│ └── resources/ # Resource files are auto-discovered here
│ └── resources/ # Resource files are auto-discovered here
│ ├── (config)/app.ts
│ ├── (users)/[userId]/profile.ts
├── dist/ # Built output (generated)
├── dist/ # Built output (generated)
├── package.json
├── tsconfig.json
└── xmcp.config.ts # Configuration file for xmcp
└── xmcp.config.ts # Configuration file for xmcp
```

## Top-level files
Expand All @@ -41,7 +42,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` and `notifications.ts` files at the root of `src/` handle HTTP middleware and MCP notification events respectively. 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.

<ConceptBoxes>
<ConceptBox
Expand All @@ -59,6 +60,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
113 changes: 113 additions & 0 deletions examples/notifications/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Notifications

Handle MCP client notifications using a single setup file.

## 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. The client-to-server notifications are:

| Key | MCP Method | When it fires |
| ------------------ | ------------------------------------- | ----------------------------------------------- |
| `initialized` | `notifications/initialized` | Client completed the initialization handshake |
| `cancelled` | `notifications/cancelled` | Client cancelled an in-flight request |
| `progress` | `notifications/progress` | Client sent a progress update |
| `rootsListChanged` | `notifications/roots/list_changed` | Client's root URIs changed |
| `taskStatus` | `notifications/tasks/status` | Task status changed |

## How it works

Create a `src/notifications.ts` file that exports a `defineNotifications()` call:

```ts
// src/notifications.ts
import { defineNotifications } from "xmcp";

export default defineNotifications({
initialized: async () => {
console.log("Client initialized");
},
cancelled: async (params) => {
console.log("Cancelled:", params.requestId);
},
progress: async (params) => {
console.log("Progress:", params.progress);
},
rootsListChanged: async () => {
console.log("Roots changed");
},
taskStatus: async (params) => {
console.log(`Task ${params.taskId}: ${params.status}`);
},
});
```

xmcp discovers this file at build time (similar to `src/middleware.ts`) and registers the handlers on the MCP server at runtime. Keys are mapped to full MCP method names automatically.

### Handler signature

```ts
defineNotifications({
// Paramless, no arguments needed
initialized: async () => { ... },

// With params, fully typed
cancelled: async (params) => {
params.requestId // string | number | undefined
params.reason // string | undefined
},

// Custom methods, params is Record<string, unknown>
"custom/my-event": async (params) => { ... },
});
```

- **Known keys** get fully typed `params` and autocomplete.
- **Custom methods** (any string) are also supported. `params` is typed as `Record<string, unknown>`.
- **Error isolation**: if a handler throws, it's caught and logged without affecting the server.

### SDK handler preservation

For `cancelled` and `progress`, the MCP SDK already has internal handlers (for aborting requests and tracking progress). Your handlers run **alongside** them and don't replace the SDK's built-in behavior.

## Project structure

```
src/
├── notifications.ts # All notification handlers in one place
└── 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
[notification] Request req-42 cancelled: User clicked cancel
[notification] Progress (75%): Almost done
[notification] Task task-1: completed
```
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"
}
}
11 changes: 11 additions & 0 deletions examples/notifications/src/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const eventCounts = new Map<string, number>();

export function track(name: string, properties?: Record<string, unknown>) {
const count = (eventCounts.get(name) ?? 0) + 1;
eventCounts.set(name, count);
console.log(
`[analytics] "${name}" (count=${count})`,
properties ?? {}
);
return count;
}
38 changes: 38 additions & 0 deletions examples/notifications/src/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { defineNotifications } from "xmcp";
import { track } from "./analytics";

export default defineNotifications({
initialized: async () => {
console.log("[notification] Client session initialized");
},
rootsListChanged: async () => {
console.log("[notification] Roots list changed");
},
cancelled: async (params) => {
console.log(
`[notification] Request ${params.requestId} cancelled${params.reason ? `: ${params.reason}` : ""}`
);
},
progress: async (params) => {
const pct =
params.total != null
? ` (${Math.round((params.progress / params.total) * 100)}%)`
: "";
console.log(
`[notification] Progress${pct}: ${params.message ?? "no message"}`
);
},
taskStatus: async (params) => {
console.log(
`[notification] Task ${params.taskId}: ${params.status}${params.statusMessage ? ` - ${params.statusMessage}` : ""}`
);
},

// Custom notification: track an app-level analytics event.
// Shares the same tracker as tool invocations, so both paths
// contribute to one running count per event name.
"app/analytics-event": async (params) => {
const name = typeof params.name === "string" ? params.name : "unknown";
track(name, params.properties as Record<string, unknown> | undefined);
},
});
12 changes: 12 additions & 0 deletions examples/notifications/src/tools/ping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { type ToolMetadata } from "xmcp";
import { track } from "../analytics";

export const metadata: ToolMetadata = {
name: "ping",
description: "Simple ping tool to verify the server is running",
};

export default function ping() {
track("tool_invoked", { tool: "ping" });
return "pong";
}
Loading
Loading