Skip to content

Commit d37de9b

Browse files
bhosmer-antclaude
andcommitted
Add support for message field in progress notifications
Implements the new optional message field for progress notifications as defined in the MCP specification update. This allows servers to provide descriptive status updates alongside progress values. Changes: - Add optional message field to ProgressSchema in types.ts - Add tests for progress notifications with message field - Update protocol tests to verify message field handling Refs: modelcontextprotocol/modelcontextprotocol#197 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 1fd6265 commit d37de9b

File tree

3 files changed

+165
-0
lines changed

3 files changed

+165
-0
lines changed

src/server/mcp.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,99 @@ describe("McpServer", () => {
7979
}
8080
])
8181
});
82+
83+
/***
84+
* Test: Progress Notification with Message Field
85+
*/
86+
test("should send progress notifications with message field", async () => {
87+
const mcpServer = new McpServer(
88+
{
89+
name: "test server",
90+
version: "1.0",
91+
}
92+
);
93+
94+
// Create a tool that sends progress updates
95+
mcpServer.tool(
96+
"long-operation",
97+
"A long running operation with progress updates",
98+
{
99+
steps: z.number().min(1).describe("Number of steps to perform"),
100+
},
101+
async ({ steps }, { sendNotification, _meta }) => {
102+
const progressToken = _meta?.progressToken;
103+
104+
if (progressToken) {
105+
// Send progress notification for each step
106+
for (let i = 1; i <= steps; i++) {
107+
await sendNotification({
108+
method: "notifications/progress",
109+
params: {
110+
progressToken,
111+
progress: i,
112+
total: steps,
113+
message: `Completed step ${i} of ${steps}`,
114+
},
115+
});
116+
}
117+
}
118+
119+
return { content: [{ type: "text" as const, text: `Operation completed with ${steps} steps` }] };
120+
}
121+
);
122+
123+
const progressUpdates: Array<{ progress: number, total?: number, message?: string }> = [];
124+
125+
const client = new Client({
126+
name: "test client",
127+
version: "1.0",
128+
});
129+
130+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
131+
132+
await Promise.all([
133+
client.connect(clientTransport),
134+
mcpServer.server.connect(serverTransport),
135+
]);
136+
137+
// Call the tool with progress tracking
138+
await client.request(
139+
{
140+
method: "tools/call",
141+
params: {
142+
name: "long-operation",
143+
arguments: { steps: 3 },
144+
_meta: {
145+
progressToken: "progress-test-1"
146+
}
147+
}
148+
},
149+
CallToolResultSchema,
150+
{
151+
onprogress: (progress) => {
152+
progressUpdates.push(progress);
153+
}
154+
}
155+
);
156+
157+
// Verify progress notifications were received with message field
158+
expect(progressUpdates).toHaveLength(3);
159+
expect(progressUpdates[0]).toMatchObject({
160+
progress: 1,
161+
total: 3,
162+
message: "Completed step 1 of 3",
163+
});
164+
expect(progressUpdates[1]).toMatchObject({
165+
progress: 2,
166+
total: 3,
167+
message: "Completed step 2 of 3",
168+
});
169+
expect(progressUpdates[2]).toMatchObject({
170+
progress: 3,
171+
total: 3,
172+
message: "Completed step 3 of 3",
173+
});
174+
});
82175
});
83176

84177
describe("ResourceTemplate", () => {

src/shared/protocol.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,74 @@ describe("protocol tests", () => {
255255
await Promise.resolve();
256256
await expect(requestPromise).resolves.toEqual({ result: "success" });
257257
});
258+
259+
test("should handle progress notifications with message field", async () => {
260+
await protocol.connect(transport);
261+
const request = { method: "example", params: {} };
262+
const mockSchema: ZodType<{ result: string }> = z.object({
263+
result: z.string(),
264+
});
265+
const onProgressMock = jest.fn();
266+
267+
const requestPromise = protocol.request(request, mockSchema, {
268+
timeout: 1000,
269+
onprogress: onProgressMock,
270+
});
271+
272+
jest.advanceTimersByTime(200);
273+
274+
if (transport.onmessage) {
275+
transport.onmessage({
276+
jsonrpc: "2.0",
277+
method: "notifications/progress",
278+
params: {
279+
progressToken: 0,
280+
progress: 25,
281+
total: 100,
282+
message: "Initializing process...",
283+
},
284+
});
285+
}
286+
await Promise.resolve();
287+
288+
expect(onProgressMock).toHaveBeenCalledWith({
289+
progress: 25,
290+
total: 100,
291+
message: "Initializing process...",
292+
});
293+
294+
jest.advanceTimersByTime(200);
295+
296+
if (transport.onmessage) {
297+
transport.onmessage({
298+
jsonrpc: "2.0",
299+
method: "notifications/progress",
300+
params: {
301+
progressToken: 0,
302+
progress: 75,
303+
total: 100,
304+
message: "Processing data...",
305+
},
306+
});
307+
}
308+
await Promise.resolve();
309+
310+
expect(onProgressMock).toHaveBeenCalledWith({
311+
progress: 75,
312+
total: 100,
313+
message: "Processing data...",
314+
});
315+
316+
if (transport.onmessage) {
317+
transport.onmessage({
318+
jsonrpc: "2.0",
319+
id: 0,
320+
result: { result: "success" },
321+
});
322+
}
323+
await Promise.resolve();
324+
await expect(requestPromise).resolves.toEqual({ result: "success" });
325+
});
258326
});
259327
});
260328

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@ export const ProgressSchema = z
364364
* Total number of items to process (or total progress required), if known.
365365
*/
366366
total: z.optional(z.number()),
367+
/**
368+
* An optional message describing the current progress.
369+
*/
370+
message: z.optional(z.string()),
367371
})
368372
.passthrough();
369373

0 commit comments

Comments
 (0)