Skip to content

Commit 69f8ee4

Browse files
authored
Merge pull request #502 from modelcontextprotocol/basil/progress_message
Add support for message field in progress notifications
2 parents 3f42989 + d37de9b commit 69f8ee4

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)