Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitefd15d8

Browse files
findleyrgopherbot
authored andcommitted
internal/mcp: clean up handling of content
Add support for all content types, and formalize the conversion ofcontent to and from the wire format (as a discriminated union).Change-Id: I93678ce98c02176a524d2c82425fb1267ad68bf0Reviewed-on:https://go-review.googlesource.com/c/tools/+/669135LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>Reviewed-by: Jonathan Amsterdam <jba@google.com>Auto-Submit: Robert Findley <rfindley@google.com>
1 parent80e0fd8 commitefd15d8

File tree

14 files changed

+282
-104
lines changed

14 files changed

+282
-104
lines changed

‎internal/mcp/client.go‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package mcp
77
import (
88
"context"
99
"encoding/json"
10-
"errors"
1110
"fmt"
1211
"iter"
1312
"slices"
@@ -194,7 +193,7 @@ func (sc *ServerConnection) ListTools(ctx context.Context) ([]protocol.Tool, err
194193
// TODO(jba): make the following true:
195194
// If the provided arguments do not conform to the schema for the given tool,
196195
// the call fails.
197-
func (sc*ServerConnection)CallTool(ctx context.Context,namestring,argsmap[string]any) (_[]Content,errerror) {
196+
func (sc*ServerConnection)CallTool(ctx context.Context,namestring,argsmap[string]any) (_*protocol.CallToolResult,errerror) {
198197
deferfunc() {
199198
iferr!=nil {
200199
err=fmt.Errorf("calling tool %q: %w",name,err)
@@ -218,15 +217,5 @@ func (sc *ServerConnection) CallTool(ctx context.Context, name string, args map[
218217
iferr:=call(ctx,sc.conn,"tools/call",params,&result);err!=nil {
219218
returnnil,err
220219
}
221-
content,err:=unmarshalContent(result.Content)
222-
iferr!=nil {
223-
returnnil,fmt.Errorf("unmarshaling tool content: %v",err)
224-
}
225-
ifresult.IsError {
226-
iflen(content)!=1||!is[TextContent](content[0]) {
227-
returnnil,errors.New("malformed error content")
228-
}
229-
returnnil,errors.New(content[0].(TextContent).Text)
230-
}
231-
returncontent,nil
220+
return&result,nil
232221
}

‎internal/mcp/cmd_test.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/google/go-cmp/cmp"
1515
"golang.org/x/tools/internal/mcp"
16+
"golang.org/x/tools/internal/mcp/internal/protocol"
1617
)
1718

1819
construnAsServer="_MCP_RUN_AS_SERVER"
@@ -57,7 +58,9 @@ func TestCmdTransport(t *testing.T) {
5758
iferr!=nil {
5859
log.Fatal(err)
5960
}
60-
want:= []mcp.Content{mcp.TextContent{Text:"Hi user"}}
61+
want:=&protocol.CallToolResult{
62+
Content: []protocol.Content{{Type:"text",Text:"Hi user"}},
63+
}
6164
ifdiff:=cmp.Diff(want,got);diff!="" {
6265
t.Errorf("greet returned unexpected content (-want +got):\n%s",diff)
6366
}

‎internal/mcp/content.go‎

Lines changed: 100 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,60 +5,120 @@
55
package mcp
66

77
import (
8-
"encoding/json"
98
"fmt"
109

1110
"golang.org/x/tools/internal/mcp/internal/protocol"
1211
)
1312

14-
// Content is the abstract result of a Tool call.
13+
// Content is the union of supported content types: [TextContent],
14+
// [ImageContent], [AudioContent], and [ResourceContent].
1515
//
16-
//TODO: support allcontenttypes.
16+
//ToWire convertscontentto its jsonrpc2 wire format.
1717
typeContentinterface {
18-
toProtocol()any
18+
ToWire()protocol.Content
1919
}
2020

21-
funcmarshalContent(content []Content) []json.RawMessage {
22-
varmsgs []json.RawMessage
23-
for_,c:=rangecontent {
24-
msg,err:=json.Marshal(c.toProtocol())
25-
iferr!=nil {
26-
panic(fmt.Sprintf("marshaling content: %v",err))
27-
}
28-
msgs=append(msgs,msg)
29-
}
30-
returnmsgs
21+
// TextContent is a textual content.
22+
typeTextContentstruct {
23+
Textstring
3124
}
3225

33-
funcunmarshalContent(msgs []json.RawMessage) ([]Content,error) {
34-
varcontent []Content
35-
for_,msg:=rangemsgs {
36-
varallContentstruct {
37-
Typestring`json:"type"`
38-
Text json.RawMessage
39-
}
40-
iferr:=json.Unmarshal(msg,&allContent);err!=nil {
41-
returnnil,fmt.Errorf("content missing\"type\"")
42-
}
43-
switchallContent.Type {
44-
case"text":
45-
vartextstring
46-
iferr:=json.Unmarshal(allContent.Text,&text);err!=nil {
47-
returnnil,fmt.Errorf("unmarshalling text content: %v",err)
48-
}
49-
content=append(content,TextContent{Text:text})
50-
default:
51-
returnnil,fmt.Errorf("unsupported content type %q",allContent.Type)
52-
}
26+
func (cTextContent)ToWire() protocol.Content {
27+
return protocol.Content{Type:"text",Text:c.Text}
28+
}
29+
30+
// ImageContent contains base64-encoded image data.
31+
typeImageContentstruct {
32+
Datastring
33+
MimeTypestring
34+
}
35+
36+
func (cImageContent)ToWire() protocol.Content {
37+
return protocol.Content{Type:"image",MIMEType:c.MimeType,Data:c.Data}
38+
}
39+
40+
// AudioContent contains base64-encoded audio data.
41+
typeAudioContentstruct {
42+
Datastring
43+
MimeTypestring
44+
}
45+
46+
func (cAudioContent)ToWire() protocol.Content {
47+
return protocol.Content{Type:"audio",MIMEType:c.MimeType,Data:c.Data}
48+
}
49+
50+
// ResourceContent contains embedded resources.
51+
typeResourceContentstruct {
52+
ResourceResource
53+
}
54+
55+
func (rResourceContent)ToWire() protocol.Content {
56+
res:=r.Resource.ToWire()
57+
return protocol.Content{Type:"resource",Resource:&res}
58+
}
59+
60+
typeResourceinterface {
61+
ToWire() protocol.Resource
62+
}
63+
64+
typeTextResourcestruct {
65+
URIstring
66+
MimeTypestring
67+
Textstring
68+
}
69+
70+
func (rTextResource)ToWire() protocol.Resource {
71+
return protocol.Resource{
72+
URI:r.URI,
73+
MIMEType:r.MimeType,
74+
Text:r.Text,
5375
}
54-
returncontent,nil
5576
}
5677

57-
// TextContent is a textual content.
58-
typeTextContentstruct {
59-
Textstring
78+
typeBlobResourcestruct {
79+
URIstring
80+
MimeTypestring
81+
Blobstring
6082
}
6183

62-
func (cTextContent)toProtocol()any {
63-
return protocol.TextContent{Type:"text",Text:c.Text}
84+
func (rBlobResource)ToWire() protocol.Resource {
85+
blob:=r.Blob
86+
return protocol.Resource{
87+
URI:r.URI,
88+
MIMEType:r.MimeType,
89+
Blob:&blob,
90+
}
91+
}
92+
93+
// ContentFromWireContent converts content from the jsonrpc2 wire format to a
94+
// typed Content value.
95+
funcContentFromWireContent(c protocol.Content)Content {
96+
switchc.Type {
97+
case"text":
98+
returnTextContent{Text:c.Text}
99+
case"image":
100+
returnImageContent{Data:c.Data,MimeType:c.MIMEType}
101+
case"audio":
102+
returnAudioContent{Data:c.Data,MimeType:c.MIMEType}
103+
case"resource":
104+
r:=ResourceContent{}
105+
ifc.Resource!=nil {
106+
ifc.Resource.Blob!=nil {
107+
r.Resource=BlobResource{
108+
URI:c.Resource.URI,
109+
MimeType:c.Resource.MIMEType,
110+
Blob:*c.Resource.Blob,
111+
}
112+
}else {
113+
r.Resource=TextResource{
114+
URI:c.Resource.URI,
115+
MimeType:c.Resource.MIMEType,
116+
Text:c.Resource.Text,
117+
}
118+
}
119+
}
120+
returnr
121+
default:
122+
panic(fmt.Sprintf("unrecognized wire content type %q",c.Type))
123+
}
64124
}

‎internal/mcp/content_test.go‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package mcp_test
6+
7+
import (
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"golang.org/x/tools/internal/mcp"
12+
"golang.org/x/tools/internal/mcp/internal/protocol"
13+
)
14+
15+
funcTestContent(t*testing.T) {
16+
tests:= []struct {
17+
in mcp.Content
18+
want protocol.Content
19+
}{
20+
{mcp.TextContent{Text:"hello"}, protocol.Content{Type:"text",Text:"hello"}},
21+
{
22+
mcp.ImageContent{Data:"a1b2c3",MimeType:"image/png"},
23+
protocol.Content{Type:"image",Data:"a1b2c3",MIMEType:"image/png"},
24+
},
25+
{
26+
mcp.AudioContent{Data:"a1b2c3",MimeType:"audio/wav"},
27+
protocol.Content{Type:"audio",Data:"a1b2c3",MIMEType:"audio/wav"},
28+
},
29+
{
30+
mcp.ResourceContent{
31+
Resource: mcp.TextResource{
32+
URI:"file://foo",
33+
MimeType:"text",
34+
Text:"abc",
35+
},
36+
},
37+
protocol.Content{
38+
Type:"resource",
39+
Resource:&protocol.Resource{
40+
URI:"file://foo",
41+
MIMEType:"text",
42+
Text:"abc",
43+
},
44+
},
45+
},
46+
{
47+
mcp.ResourceContent{
48+
Resource: mcp.BlobResource{
49+
URI:"file://foo",
50+
MimeType:"text",
51+
Blob:"a1b2c3",
52+
},
53+
},
54+
protocol.Content{
55+
Type:"resource",
56+
Resource:&protocol.Resource{
57+
URI:"file://foo",
58+
MIMEType:"text",
59+
Blob:ptr("a1b2c3"),
60+
},
61+
},
62+
},
63+
}
64+
65+
for_,test:=rangetests {
66+
got:=test.in.ToWire()
67+
ifdiff:=cmp.Diff(test.want,got);diff!="" {
68+
t.Errorf("ToWire mismatch (-want +got):\n%s",diff)
69+
}
70+
}
71+
}
72+
73+
funcptr[Tany](tT)*T {
74+
return&t
75+
}

‎internal/mcp/examples/hello/main.go‎

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package main
66

77
import (
88
"context"
9-
"encoding/json"
109
"flag"
1110
"fmt"
1211
"net/http"
@@ -29,19 +28,10 @@ func SayHi(ctx context.Context, cc *mcp.ClientConnection, params *HiParams) ([]m
2928
}
3029

3130
funcPromptHi(ctx context.Context,cc*mcp.ClientConnection,params*HiParams) (*protocol.GetPromptResult,error) {
32-
// (see related TODOs about cleaning up content construction)
33-
content,err:=json.Marshal(protocol.TextContent{
34-
Type:"text",
35-
Text:"Say hi to "+params.Name,
36-
})
37-
iferr!=nil {
38-
returnnil,err
39-
}
4031
return&protocol.GetPromptResult{
4132
Description:"Code review prompt",
4233
Messages: []protocol.PromptMessage{
43-
// TODO: move 'Content' to the protocol package.
44-
{Role:"user",Content:json.RawMessage(content)},
34+
{Role:"user",Content: mcp.TextContent{Text:"Say hi to "+params.Name}.ToWire()},
4535
},
4636
},nil
4737
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package protocol
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
)
11+
12+
// Content is the wire format for content, including all fields.
13+
typeContentstruct {
14+
Typestring`json:"type"`
15+
Textstring`json:"text,omitempty"`
16+
MIMETypestring`json:"mimeType,omitempty"`
17+
Datastring`json:"data,omitempty"`
18+
Resource*Resource`json:"resource,omitempty"`
19+
}
20+
21+
// Resource is the wire format for embedded resources, including all fields.
22+
typeResourcestruct {
23+
URIstring`json:"uri,"`
24+
MIMETypestring`json:"mimeType,omitempty"`
25+
Textstring`json:"text"`
26+
Blob*string`json:"blob"`// blob is a pointer to distinguish empty from missing data
27+
}
28+
29+
func (c*Content)UnmarshalJSON(data []byte)error {
30+
typewireContentContent// for naive unmarshaling
31+
varc2wireContent
32+
iferr:=json.Unmarshal(data,&c2);err!=nil {
33+
returnerr
34+
}
35+
switchc2.Type {
36+
case"text","image","audio","resource":
37+
default:
38+
returnfmt.Errorf("unrecognized content type %s",c.Type)
39+
}
40+
*c=Content(c2)
41+
returnnil
42+
}

‎internal/mcp/internal/protocol/generate.go‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ var declarations = config{
9191
"Tools": {Name:"ToolCapabilities"},
9292
},
9393
},
94-
"TextContent": {Name:"TextContent"},
9594
"Tool": {
9695
Name:"Tool",
9796
Fields:config{"InputSchema": {Substitute:"*jsonschema.Schema"}},
@@ -249,8 +248,15 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
249248
}
250249

251250
ifdef.Type=="" {
252-
// E.g. union types.
253-
fmt.Fprintf(w,"json.RawMessage")
251+
// special case: recognize Content
252+
ifslices.ContainsFunc(def.AnyOf,func(s*jsonschema.Schema)bool {
253+
returns.Ref=="#/definitions/TextContent"
254+
}) {
255+
fmt.Fprintf(w,"Content")
256+
}else {
257+
// E.g. union types.
258+
fmt.Fprintf(w,"json.RawMessage")
259+
}
254260
}else {
255261
switchdef.Type {
256262
case"array":

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp