Skip to content

Commit 93fd553

Browse files
committed
enhance long-running tasks documentation with progress and cancellation examples
1 parent beb62d8 commit 93fd553

File tree

7 files changed

+317
-1
lines changed

7 files changed

+317
-1
lines changed

exercises/02.elicitation/01.problem/README.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,7 @@ if (!confirmed) {
5151
5252
This approach ensures users are always in control of important actions, and the system responds with empathy and clarity.
5353
54+
Test this one out by using the `delete_tag` tool.
55+
5456
🐨 Kody will be there in <InlineFile file="src/tools.ts" /> to help you
5557
implement this.
Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,67 @@
11
# Progress Updates
22

3-
{/* Don't forget to add sample code to instructions here */}
3+
👨‍💼 When users ask our mcp server to generate their journal wrapped video, it can take some time and they want to know that things are moving along. Nobody likes staring at a blank screen, wondering if the app is frozen or if their request was even received. Progress updates are how we keep users in the loop, making the wait feel shorter and the experience more delightful.
4+
5+
Here's an example of how to perform progress updates in your MCP server:
6+
7+
```ts
8+
agent.server.registerTool(
9+
'make_sandwich',
10+
{
11+
description: 'Make a sandwich',
12+
inputSchema: {
13+
ingredients: z.array(z.string()),
14+
},
15+
},
16+
async ({ ingredients }, { sendNotification, _meta }) => {
17+
// ...
18+
const orderStatus = await assembleSandwichOrder({
19+
ingredients,
20+
onProgress: (progress) => {
21+
const { progressToken } = _meta ?? {}
22+
if (!progressToken) return
23+
void sendNotification({
24+
method: 'notifications/progress',
25+
params: {
26+
progressToken,
27+
progress,
28+
total: 1,
29+
message: `Stacking pickles...`,
30+
},
31+
})
32+
},
33+
})
34+
},
35+
// ...
36+
)
37+
```
38+
39+
<callout-success>
40+
Progress updates can be as simple as a percentage, or as fun as a custom
41+
message. The key is to keep users informed while they wait.
42+
</callout-success>
43+
44+
```mermaid
45+
sequenceDiagram
46+
User->>App: Place custom sandwich order (with progressToken)
47+
loop While processing
48+
App-->>User: notifications/progress (progress, total, message)
49+
end
50+
App-->>User: Sandwich ready!
51+
```
52+
53+
<callout-muted>
54+
📜 For more details on how to implement progress notifications, see the [MCP
55+
Progress
56+
Documentation](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress).
57+
</callout-muted>
58+
59+
The goal is to make waiting feel like part of the experience, not a chore. Progress updates turn a long-running task into a journey the user can follow, one delicious layer at a time.
60+
61+
Test this one out by using the `create_wrapped_video` tool.
62+
63+
<callout-info>
64+
**NOTE**: This particular tool requires ffmpeg to be installed globally on
65+
your machine. If it is not, you can still test this out using the mock time
66+
input (you just won't get the video actually generated).
67+
</callout-info>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
# Progress Updates
2+
3+
👨‍💼 Fantastic news! We've just made waiting for a journal wrapped video a much better experience. Now, when users request their video, they receive real-time progress updates throughout the process. Instead of staring at a blank screen and wondering if anything is happening, users see clear, friendly notifications that keep them in the loop from start to finish.
4+
5+
This means users always know their request is being handled, and they can follow along as their video is assembled—making the wait feel shorter and the experience more delightful. Progress updates transform a long-running task into a transparent, engaging journey, building trust and satisfaction with every step.
6+
7+
Let's keep this momentum going and continue making our app a joy to use!
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,69 @@
11
# Cancellation
2+
3+
👨‍💼 When users embark on creating their personalized wrapped video, the process can take a while (especially if they journal a lot). Sometimes, halfway through, they realize they picked the wrong year, or maybe their cat walks across the keyboard and they want to stop the operation immediately. It's essential that our app gives users the power to cancel these long-running tasks, so they always feel in control and never stuck waiting for something they no longer want.
4+
5+
Let's make sure users can hit the "Stop" button and have their request canceled right away, freeing up system resources and making the experience feel responsive and modern.
6+
7+
<callout-warning>
8+
**NOTE**: As of this writing, there is no support for a stop button in the MCP
9+
inspector. Follow [this
10+
issue](https://github.com/modelcontextprotocol/inspector/issues/591) for
11+
updates.
12+
</callout-warning>
13+
14+
Here's an example of how cancellation can be handled in our MCP server using the `AbortController` and signals:
15+
16+
```ts
17+
// the signal comes from the SDK and is passed to our tool
18+
const controller = new AbortController()
19+
const signal = controller.signal
20+
21+
async function createPlaylistMashup({
22+
songs,
23+
onProgress,
24+
// we pass the signal to our utility so we can add an event listener to it
25+
signal,
26+
}: {
27+
songs: string[]
28+
onProgress?: (progress: number) => void
29+
signal?: AbortSignal
30+
}) {
31+
if (signal?.aborted) throw new Error('Mashup creation was cancelled')
32+
33+
// our logic...
34+
const promise = doTheAsyncThing()
35+
36+
function onAbort() {
37+
// we can do something here, like send a notification to the user
38+
// that the mashup was cancelled. This should trigger the async thing to throw an error.
39+
}
40+
signal.addEventListener('abort', onAbort)
41+
await promise.finally(() => {
42+
// make sure to clean up regardless of whether the promise resolves or rejects
43+
signal.removeEventListener('abort', onAbort)
44+
})
45+
46+
return 'Mashup complete!'
47+
}
48+
49+
// Somewhere in the SDK, this gets called when the user wants to cancel:
50+
controller.abort()
51+
```
52+
53+
```mermaid
54+
sequenceDiagram
55+
User->>App: Start playlist mashup (id: 42)
56+
Note over User,App: Mashup in progress
57+
User-->>App: notifications/cancelled (requestId: 42, reason: "Changed my mind!")
58+
App--xUser: (No response for cancelled request)
59+
```
60+
61+
<callout-muted>
62+
📜 For more details on how to implement cancellation, see the [MCP
63+
Cancellation
64+
Documentation](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation).
65+
</callout-muted>
66+
67+
The goal is to empower users to stop long-running operations at any time, making the app feel fast, flexible, and user-friendly—even when things don't go as planned.
68+
69+
Now, please update our `create_wrapped_video` tool to support cancellation.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
# Cancellation
2+
3+
👨‍💼 Outstanding progress! We've just empowered our users with the ability to cancel long-running operations—like generating their personalized wrapped video—at any time. This means that if a user changes their mind, makes a mistake, or simply needs to stop the process, they can do so instantly, without waiting for the task to finish.
4+
5+
This enhancement puts users in control, making the app feel faster, more flexible, and responsive. No more feeling stuck or frustrated by accidental requests or long waits. And it's more efficient for our servers as well to not continue processing a request that was cancelled. Now, users can confidently start and stop operations as needed, knowing the system will respect their choices and free up resources right away.
6+
7+
Let's keep building on this momentum to deliver even more delightful, user-centric experiences!
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# Long Running Tasks
2+
3+
You can now manage long-running tasks in your tools, reporting progress and allowing users to cancel operations. Good job!
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,169 @@
11
# Long Running Tasks
2+
3+
In this exercise, you'll learn how to handle long-running tasks in the context of the Model Context Protocol (MCP), with a focus on **progress reporting** and **cancellation**. (As a bonus, you'll also get hands-on experience with JavaScript's `AbortController` and signals, which are essential for managing asynchronous operations that can be cancelled).
4+
5+
## Background
6+
7+
Long-running operations are common in modern applications—think of file uploads, data processing, or external API calls. It's important to:
8+
9+
- **Report progress** to users so they know something is happening.
10+
- **Allow cancellation** so users can stop an operation if they change their mind or if the operation is taking too long.
11+
12+
The Model Context Protocol (MCP) provides built-in support for both progress and cancellation via notification messages:
13+
14+
- [MCP Progress Documentation](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress)
15+
- [MCP Cancellation Documentation](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation)
16+
17+
---
18+
19+
## Progress Reporting
20+
21+
When a client wants to receive progress updates for a long-running request, it includes a `progressToken` in the request. The server can then send progress notifications as the operation advances.
22+
23+
### Sequence Diagram
24+
25+
```mermaid
26+
sequenceDiagram
27+
participant Client
28+
participant Server
29+
Client->>Server: Request (with progressToken)
30+
loop While processing
31+
Server-->>Client: notifications/progress (progress, total, message)
32+
end
33+
Server-->>Client: Response (when done)
34+
```
35+
36+
### Example Messages
37+
38+
**Client Request with Progress Token:**
39+
40+
```json
41+
{
42+
"jsonrpc": "2.0",
43+
"id": 5,
44+
"method": "play_fetch",
45+
"params": {
46+
"_meta": {
47+
"progressToken": "abc123"
48+
}
49+
}
50+
}
51+
```
52+
53+
**Server Progress Notification:**
54+
55+
```json
56+
{
57+
"jsonrpc": "2.0",
58+
"method": "notifications/progress",
59+
"params": {
60+
"progressToken": "abc123",
61+
"progress": 50,
62+
"total": 100,
63+
"message": "Stick thrown, dog is running"
64+
}
65+
}
66+
```
67+
68+
**Server Final Response:**
69+
70+
```json
71+
{
72+
"jsonrpc": "2.0",
73+
"id": 5,
74+
"result": {
75+
"content": [
76+
{
77+
"type": "text",
78+
"text": "Dog successfully fetched stick"
79+
},
80+
{
81+
"type": "text",
82+
"text": "{\"status\": \"success\"}"
83+
}
84+
],
85+
"structuredContent": {
86+
"status": "success"
87+
}
88+
}
89+
}
90+
```
91+
92+
For more details, see the [MCP Progress Documentation](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress).
93+
94+
---
95+
96+
## Cancellation
97+
98+
Either the client or server can request cancellation of an in-progress request by sending a cancellation notification. The receiver should stop processing the request and free any associated resources.
99+
100+
### Sequence Diagram
101+
102+
```mermaid
103+
sequenceDiagram
104+
participant Client
105+
participant Server
106+
Client->>Server: Request (id: 123)
107+
Note over Client,Server: Request is in progress
108+
Client-->>Server: notifications/cancelled (requestId: 123, reason)
109+
Server--xClient: (No response for cancelled request)
110+
```
111+
112+
### Example Messages
113+
114+
**Client Cancellation Notification:**
115+
116+
```json
117+
{
118+
"jsonrpc": "2.0",
119+
"method": "notifications/cancelled",
120+
"params": {
121+
"requestId": 5,
122+
"reason": "User requested cancellation, dog is too tired"
123+
}
124+
}
125+
```
126+
127+
- The server should stop processing the request and not send a response for the cancelled request.
128+
- If the request is already complete or unknown, the server may ignore the cancellation notification.
129+
130+
For more details, see the [MCP Cancellation Documentation](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation).
131+
132+
---
133+
134+
## Example: Using AbortController and Signals
135+
136+
Not everyone is familiar with signals and the `AbortController` API. Here's a simple example to illustrate how it works:
137+
138+
```js
139+
const controller = new AbortController()
140+
const signal = controller.signal
141+
142+
async function doLongTask(signal) {
143+
for (let i = 0; i < 10; i++) {
144+
if (signal.aborted) {
145+
throw new Error('Operation cancelled')
146+
}
147+
// Simulate work
148+
await new Promise((r) => setTimeout(r, 500))
149+
console.log(`Step ${i + 1} complete`)
150+
}
151+
return 'Done!'
152+
}
153+
154+
doLongTask(signal)
155+
.then((result) => console.log(result))
156+
.catch((err) => console.error(err.message))
157+
158+
// Cancel the operation after 2 seconds
159+
setTimeout(() => {
160+
controller.abort()
161+
console.log('Cancellation requested')
162+
}, 2000)
163+
```
164+
165+
- The `AbortController` creates a `signal` that can be passed to any async function that supports cancellation.
166+
- The function checks `signal.aborted` to know if it should stop early.
167+
- Calling `controller.abort()` triggers the cancellation.
168+
169+
For more details, see the [MDN AbortController documentation](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal).

0 commit comments

Comments
 (0)