Long Running Tasks
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).Background
Long-running operations are common in modern applications—think of file uploads, data processing, or external API calls. It's important to:
- Report progress to users so they know something is happening.
- Allow cancellation so users can stop an operation if they change their mind or if the operation is taking too long.
The Model Context Protocol (MCP) provides built-in support for both progress and cancellation via notification messages:
Progress Reporting
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.Sequence Diagram
Example Messages
Client Request with Progress Token:
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "play_fetch",
"arguments": {},
"_meta": {
"progressToken": "abc123"
}
}
}
Server Progress Notification:
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "abc123",
"progress": 50,
"total": 100,
"message": "Stick thrown, dog is running"
}
}
Server Final Response:
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "text",
"text": "Dog successfully fetched stick"
},
{
"type": "text",
"text": "{\"status\": \"success\"}"
}
],
"structuredContent": {
"status": "success"
}
}
}
For more details, see the MCP Progress Documentation.
Cancellation
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.
Sequence Diagram
Example Messages
Client Cancellation Notification:
{
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": {
"requestId": 5,
"reason": "User requested cancellation, dog is too tired"
}
}
- The server should stop processing the request and not send a response for the cancelled request.
- If the request is already complete or unknown, the server may ignore the cancellation notification.
For more details, see the MCP Cancellation Documentation.
Example: Using AbortController and Signals
Not everyone is familiar with signals and the
AbortController
API. Here's a simple example to illustrate how it works:const { spawn } = require('child_process')
const controller = new AbortController()
const signal = controller.signal
// Track the child process that needs cleanup
let childProcess = null
// Add an event listener to the signal - this is crucial for cleanup!
signal.addEventListener('abort', () => {
console.log('Signal was aborted! Killing child process...')
if (childProcess) {
childProcess.kill('SIGTERM')
childProcess = null
console.log('Child process killed')
}
})
async function doLongTask(signal) {
// Start a child process that processes data
childProcess = spawn('data-processor', ['--input', 'large-dataset.csv', '--output', 'processed-data.json'], {
stdio: ['pipe', 'pipe', 'pipe']
})
try {
// Listen to the child process output
childProcess.stdout.on('data', (data) => {
const output = data.toString().trim()
console.log('Child output:', output)
if (output.includes('Processing complete!')) {
console.log('All items processed successfully!')
}
})
// Wait for the child process to complete
await new Promise((resolve, reject) => {
childProcess.on('close', (code) => {
if (code === 0) {
resolve('Processing completed successfully')
} else {
reject(new Error(\`Child process exited with code \${code}\``))
}
})
childProcess.on('error', reject)
})
return 'Done!'
} finally {
// Clean up child process when task completes normally
if (childProcess) {
childProcess.kill('SIGTERM')
childProcess = null
console.log('Child process cleaned up normally')
}
}
}
doLongTask(signal)
.then((result) => console.log(result))
.catch((err) => console.error(err.message))
// Cancel the operation after 2 seconds
setTimeout(() => {
controller.abort()
console.log('Cancellation requested')
}, 2000)
- The
AbortController
creates asignal
that can be passed to any async function that supports cancellation. - The function checks
signal.aborted
to know if it should stop early. - Calling
controller.abort()
triggers the cancellation.
For more details, see the MDN AbortController documentation.