Skip to content

Commit 257007e

Browse files
committed
mcp: add an ergonomic progress API
Add a more ergonomic API for reporting progress from server requests. Requested in #460 DO NOT SUBMIT: needs a proposal
1 parent c2c810b commit 257007e

File tree

3 files changed

+178
-10
lines changed

3 files changed

+178
-10
lines changed

mcp/conformance_test.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ func incTool(_ context.Context, _ *CallToolRequest, args incInput) (*CallToolRes
135135
return nil, incOutput{args.X + 1}, nil
136136
}
137137

138+
func progressTool(ctx context.Context, req *CallToolRequest, args any) (*CallToolResult, any, error) {
139+
for i := range 10 {
140+
if err := req.Progress(ctx, fmt.Sprintf("message %d", i), float64(i+1), 10); err != nil {
141+
return nil, nil, err
142+
}
143+
}
144+
return nil, nil, nil
145+
}
146+
138147
// runServerTest runs the server conformance test.
139148
// It must be executed in a synctest bubble.
140149
func runServerTest(t *testing.T, test *conformanceTest) {
@@ -152,6 +161,8 @@ func runServerTest(t *testing.T, test *conformanceTest) {
152161
AddTool(s, &Tool{Name: "structured"}, structuredTool)
153162
case "tomorrow":
154163
AddTool(s, &Tool{Name: "tomorrow"}, tomorrowTool)
164+
case "progress":
165+
AddTool(s, &Tool{Name: "progress"}, progressTool)
155166
case "inc":
156167
inSchema, err := jsonschema.For[incInput](nil)
157168
if err != nil {
@@ -233,15 +244,17 @@ func runServerTest(t *testing.T, test *conformanceTest) {
233244
return nil, err, false
234245
}
235246
serverMessages = append(serverMessages, msg)
236-
if req, ok := msg.(*jsonrpc.Request); ok && req.IsCall() {
237-
// Pair up the next outgoing response with this request.
238-
// We assume requests arrive in the same order every time.
239-
if len(outResponses) == 0 {
240-
t.Fatalf("no outgoing response for request %v", req)
247+
if req, ok := msg.(*jsonrpc.Request); ok {
248+
if req.IsCall() {
249+
// Pair up the next outgoing response with this request.
250+
// We assume requests arrive in the same order every time.
251+
if len(outResponses) == 0 {
252+
t.Fatalf("no outgoing response for request %v", req)
253+
}
254+
outResponses[0].ID = req.ID
255+
writeMsg(outResponses[0])
256+
outResponses = outResponses[1:]
241257
}
242-
outResponses[0].ID = req.ID
243-
writeMsg(outResponses[0])
244-
outResponses = outResponses[1:]
245258
continue
246259
}
247260
return msg.(*jsonrpc.Response), nil, true

mcp/progress.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"errors"
6+
)
7+
8+
var ErrNoProgressToken = errors.New("no progress token")
9+
10+
// Progress reports progress on the current request.
11+
//
12+
// An error is returned if sending progress failed. If there was no progress
13+
// token, this error is ErrNoProgressToken.
14+
func (r *ServerRequest[P]) Progress(ctx context.Context, msg string, progress, total float64) error {
15+
token, ok := r.Params.GetMeta()[progressTokenKey]
16+
if !ok {
17+
return ErrNoProgressToken
18+
}
19+
params := &ProgressNotificationParams{
20+
Message: msg,
21+
ProgressToken: token,
22+
Progress: progress,
23+
Total: total,
24+
}
25+
return r.Session.NotifyProgress(ctx, params)
26+
}

mcp/testdata/conformance/server/tools.txtar

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ greet
1616
structured
1717
tomorrow
1818
inc
19+
progress
1920

2021
-- client --
2122
{
@@ -40,7 +41,9 @@ inc
4041
{ "jsonrpc": "2.0", "id": 9, "method": "tools/call", "params": {"name": "tomorrow", "arguments": { "Now": "2025-06-18T15:04:05Z" } } }
4142
{ "jsonrpc": "2.0", "id": 10, "method": "tools/call", "params": {"name": "greet" } }
4243
{ "jsonrpc": "2.0", "id": 11, "method": "tools/call", "params": {"name": "inc", "arguments": { "x": 3 } } }
43-
{ "jsonrpc": "2.0", "id": 11, "method": "tools/call", "params": {"name": "inc" } }
44+
{ "jsonrpc": "2.0", "id": 12, "method": "tools/call", "params": {"name": "inc" } }
45+
{ "jsonrpc": "2.0", "id": 13, "method": "tools/call", "params": {"name": "progress" } }
46+
{ "jsonrpc": "2.0", "id": 14, "method": "tools/call", "params": {"name": "progress", "_meta": { "progressToken": "abc123" } } }
4447

4548
-- server --
4649
{
@@ -106,6 +109,12 @@ inc
106109
"additionalProperties": false
107110
}
108111
},
112+
{
113+
"inputSchema": {
114+
"type": "object"
115+
},
116+
"name": "progress"
117+
},
109118
{
110119
"inputSchema": {
111120
"type": "object",
@@ -267,7 +276,7 @@ inc
267276
}
268277
{
269278
"jsonrpc": "2.0",
270-
"id": 11,
279+
"id": 12,
271280
"result": {
272281
"content": [
273282
{
@@ -280,3 +289,123 @@ inc
280289
}
281290
}
282291
}
292+
{
293+
"jsonrpc": "2.0",
294+
"id": 13,
295+
"result": {
296+
"content": [
297+
{
298+
"type": "text",
299+
"text": "no progress token"
300+
}
301+
],
302+
"isError": true
303+
}
304+
}
305+
{
306+
"jsonrpc": "2.0",
307+
"method": "notifications/progress",
308+
"params": {
309+
"message": "message 0",
310+
"progress": 1,
311+
"progressToken": "abc123",
312+
"total": 10
313+
}
314+
}
315+
{
316+
"jsonrpc": "2.0",
317+
"method": "notifications/progress",
318+
"params": {
319+
"message": "message 1",
320+
"progress": 2,
321+
"progressToken": "abc123",
322+
"total": 10
323+
}
324+
}
325+
{
326+
"jsonrpc": "2.0",
327+
"method": "notifications/progress",
328+
"params": {
329+
"message": "message 2",
330+
"progress": 3,
331+
"progressToken": "abc123",
332+
"total": 10
333+
}
334+
}
335+
{
336+
"jsonrpc": "2.0",
337+
"method": "notifications/progress",
338+
"params": {
339+
"message": "message 3",
340+
"progress": 4,
341+
"progressToken": "abc123",
342+
"total": 10
343+
}
344+
}
345+
{
346+
"jsonrpc": "2.0",
347+
"method": "notifications/progress",
348+
"params": {
349+
"message": "message 4",
350+
"progress": 5,
351+
"progressToken": "abc123",
352+
"total": 10
353+
}
354+
}
355+
{
356+
"jsonrpc": "2.0",
357+
"method": "notifications/progress",
358+
"params": {
359+
"message": "message 5",
360+
"progress": 6,
361+
"progressToken": "abc123",
362+
"total": 10
363+
}
364+
}
365+
{
366+
"jsonrpc": "2.0",
367+
"method": "notifications/progress",
368+
"params": {
369+
"message": "message 6",
370+
"progress": 7,
371+
"progressToken": "abc123",
372+
"total": 10
373+
}
374+
}
375+
{
376+
"jsonrpc": "2.0",
377+
"method": "notifications/progress",
378+
"params": {
379+
"message": "message 7",
380+
"progress": 8,
381+
"progressToken": "abc123",
382+
"total": 10
383+
}
384+
}
385+
{
386+
"jsonrpc": "2.0",
387+
"method": "notifications/progress",
388+
"params": {
389+
"message": "message 8",
390+
"progress": 9,
391+
"progressToken": "abc123",
392+
"total": 10
393+
}
394+
}
395+
{
396+
"jsonrpc": "2.0",
397+
"method": "notifications/progress",
398+
"params": {
399+
"message": "message 9",
400+
"progress": 10,
401+
"progressToken": "abc123",
402+
"total": 10
403+
}
404+
}
405+
{
406+
"jsonrpc": "2.0",
407+
"id": 14,
408+
"result": {
409+
"content": []
410+
}
411+
}

0 commit comments

Comments
 (0)