Skip to content

Commit 7c03aa9

Browse files
Copilotlpcox
andauthored
Fix go-sdk integration: image/audio/resource content, error messages, pagination, and tool annotations
Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/12ad1499-dedf-449f-906b-7ec1b9be9ada Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 20539d7 commit 7c03aa9

6 files changed

Lines changed: 196 additions & 28 deletions

File tree

internal/mcp/connection.go

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -533,14 +533,26 @@ func callParamMethod[P any](c *Connection, rawParams interface{}, fn func(P) (in
533533
}
534534

535535
func (c *Connection) listTools() (*Response, error) {
536+
if err := c.requireSession(); err != nil {
537+
return nil, err
538+
}
536539
logConn.Printf("listTools: requesting tool list from backend serverID=%s", c.serverID)
537-
return c.callListMethod(func() (interface{}, error) {
538-
result, err := c.getSDKSession().ListTools(c.ctx, &sdk.ListToolsParams{})
539-
if err == nil {
540-
logConn.Printf("listTools: received %d tools from serverID=%s", len(result.Tools), c.serverID)
540+
var allTools []*sdk.Tool
541+
cursor := ""
542+
for {
543+
result, err := c.getSDKSession().ListTools(c.ctx, &sdk.ListToolsParams{Cursor: cursor})
544+
if err != nil {
545+
return nil, err
541546
}
542-
return result, err
543-
})
547+
allTools = append(allTools, result.Tools...)
548+
logConn.Printf("listTools: received page of %d tools (total so far: %d) from serverID=%s", len(result.Tools), len(allTools), c.serverID)
549+
if result.NextCursor == "" {
550+
break
551+
}
552+
cursor = result.NextCursor
553+
}
554+
logConn.Printf("listTools: received %d tools total from serverID=%s", len(allTools), c.serverID)
555+
return marshalToResponse(&sdk.ListToolsResult{Tools: allTools})
544556
}
545557

546558
func (c *Connection) callTool(params interface{}) (*Response, error) {
@@ -559,14 +571,26 @@ func (c *Connection) callTool(params interface{}) (*Response, error) {
559571
}
560572

561573
func (c *Connection) listResources() (*Response, error) {
574+
if err := c.requireSession(); err != nil {
575+
return nil, err
576+
}
562577
logConn.Printf("listResources: requesting resource list from backend serverID=%s", c.serverID)
563-
return c.callListMethod(func() (interface{}, error) {
564-
result, err := c.getSDKSession().ListResources(c.ctx, &sdk.ListResourcesParams{})
565-
if err == nil {
566-
logConn.Printf("listResources: received %d resources from serverID=%s", len(result.Resources), c.serverID)
578+
var allResources []*sdk.Resource
579+
cursor := ""
580+
for {
581+
result, err := c.getSDKSession().ListResources(c.ctx, &sdk.ListResourcesParams{Cursor: cursor})
582+
if err != nil {
583+
return nil, err
567584
}
568-
return result, err
569-
})
585+
allResources = append(allResources, result.Resources...)
586+
logConn.Printf("listResources: received page of %d resources (total so far: %d) from serverID=%s", len(result.Resources), len(allResources), c.serverID)
587+
if result.NextCursor == "" {
588+
break
589+
}
590+
cursor = result.NextCursor
591+
}
592+
logConn.Printf("listResources: received %d resources total from serverID=%s", len(allResources), c.serverID)
593+
return marshalToResponse(&sdk.ListResourcesResult{Resources: allResources})
570594
}
571595

572596
func (c *Connection) readResource(params interface{}) (*Response, error) {
@@ -582,14 +606,26 @@ func (c *Connection) readResource(params interface{}) (*Response, error) {
582606
}
583607

584608
func (c *Connection) listPrompts() (*Response, error) {
609+
if err := c.requireSession(); err != nil {
610+
return nil, err
611+
}
585612
logConn.Printf("listPrompts: requesting prompt list from backend serverID=%s", c.serverID)
586-
return c.callListMethod(func() (interface{}, error) {
587-
result, err := c.getSDKSession().ListPrompts(c.ctx, &sdk.ListPromptsParams{})
588-
if err == nil {
589-
logConn.Printf("listPrompts: received %d prompts from serverID=%s", len(result.Prompts), c.serverID)
613+
var allPrompts []*sdk.Prompt
614+
cursor := ""
615+
for {
616+
result, err := c.getSDKSession().ListPrompts(c.ctx, &sdk.ListPromptsParams{Cursor: cursor})
617+
if err != nil {
618+
return nil, err
590619
}
591-
return result, err
592-
})
620+
allPrompts = append(allPrompts, result.Prompts...)
621+
logConn.Printf("listPrompts: received page of %d prompts (total so far: %d) from serverID=%s", len(result.Prompts), len(allPrompts), c.serverID)
622+
if result.NextCursor == "" {
623+
break
624+
}
625+
cursor = result.NextCursor
626+
}
627+
logConn.Printf("listPrompts: received %d prompts total from serverID=%s", len(allPrompts), c.serverID)
628+
return marshalToResponse(&sdk.ListPromptsResult{Prompts: allPrompts})
593629
}
594630

595631
func (c *Connection) getPrompt(params interface{}) (*Response, error) {

internal/mcp/tool_result.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ import (
88
sdk "github.com/modelcontextprotocol/go-sdk/mcp"
99
)
1010

11+
// resourceContents mirrors sdk.ResourceContents for JSON unmarshaling of
12+
// embedded resource content items returned by backend MCP servers.
13+
type resourceContents struct {
14+
URI string `json:"uri"`
15+
MIMEType string `json:"mimeType,omitempty"`
16+
Text string `json:"text,omitempty"`
17+
Blob []byte `json:"blob,omitempty"`
18+
}
19+
1120
var logToolResult = logger.New("mcp:tool_result")
1221

1322
// ConvertToCallToolResult converts backend result data to SDK CallToolResult format.
@@ -61,8 +70,11 @@ func ConvertToCallToolResult(data interface{}) (*sdk.CallToolResult, error) {
6170
// Parse the backend result structure (standard MCP CallToolResult format)
6271
var backendResult struct {
6372
Content []struct {
64-
Type string `json:"type"`
65-
Text string `json:"text,omitempty"`
73+
Type string `json:"type"`
74+
Text string `json:"text,omitempty"`
75+
Data []byte `json:"data,omitempty"` // base64-encoded image/audio data
76+
MIMEType string `json:"mimeType,omitempty"` // image/audio MIME type
77+
Resource *resourceContents `json:"resource,omitempty"` // embedded resource
6678
} `json:"content"`
6779
IsError bool `json:"isError,omitempty"`
6880
}
@@ -89,8 +101,31 @@ func ConvertToCallToolResult(data interface{}) (*sdk.CallToolResult, error) {
89101
content = append(content, &sdk.TextContent{
90102
Text: item.Text,
91103
})
104+
case "image":
105+
content = append(content, &sdk.ImageContent{
106+
Data: item.Data,
107+
MIMEType: item.MIMEType,
108+
})
109+
case "audio":
110+
content = append(content, &sdk.AudioContent{
111+
Data: item.Data,
112+
MIMEType: item.MIMEType,
113+
})
114+
case "resource":
115+
if item.Resource != nil {
116+
content = append(content, &sdk.EmbeddedResource{
117+
Resource: &sdk.ResourceContents{
118+
URI: item.Resource.URI,
119+
MIMEType: item.Resource.MIMEType,
120+
Text: item.Resource.Text,
121+
Blob: item.Resource.Blob,
122+
},
123+
})
124+
} else {
125+
logToolResult.Printf("Resource content item missing 'resource' field, skipping")
126+
}
92127
default:
93-
// For unknown types, try to preserve as text
128+
// For unknown types, preserve as text with whatever text field is present
94129
logToolResult.Printf("Unknown content type '%s', treating as text", item.Type)
95130
content = append(content, &sdk.TextContent{
96131
Text: item.Text,

internal/mcp/tool_result_test.go

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,93 @@ func TestConvertToCallToolResult(t *testing.T) {
9999
assert.Contains(t, text.Text, "value")
100100
})
101101

102+
t.Run("image content type is converted to ImageContent", func(t *testing.T) {
103+
// base64("hello") = "aGVsbG8="
104+
input := map[string]interface{}{
105+
"content": []interface{}{
106+
map[string]interface{}{"type": "image", "data": "aGVsbG8=", "mimeType": "image/png"},
107+
},
108+
}
109+
110+
result, err := ConvertToCallToolResult(input)
111+
112+
require.NoError(t, err)
113+
require.NotNil(t, result)
114+
assert.Len(t, result.Content, 1)
115+
116+
img, ok := result.Content[0].(*sdk.ImageContent)
117+
require.True(t, ok, "Expected ImageContent")
118+
assert.Equal(t, "image/png", img.MIMEType)
119+
assert.Equal(t, []byte("hello"), img.Data)
120+
})
121+
122+
t.Run("audio content type is converted to AudioContent", func(t *testing.T) {
123+
// base64("world") = "d29ybGQ="
124+
input := map[string]interface{}{
125+
"content": []interface{}{
126+
map[string]interface{}{"type": "audio", "data": "d29ybGQ=", "mimeType": "audio/wav"},
127+
},
128+
}
129+
130+
result, err := ConvertToCallToolResult(input)
131+
132+
require.NoError(t, err)
133+
require.NotNil(t, result)
134+
assert.Len(t, result.Content, 1)
135+
136+
audio, ok := result.Content[0].(*sdk.AudioContent)
137+
require.True(t, ok, "Expected AudioContent")
138+
assert.Equal(t, "audio/wav", audio.MIMEType)
139+
assert.Equal(t, []byte("world"), audio.Data)
140+
})
141+
142+
t.Run("resource content type is converted to EmbeddedResource", func(t *testing.T) {
143+
input := map[string]interface{}{
144+
"content": []interface{}{
145+
map[string]interface{}{
146+
"type": "resource",
147+
"resource": map[string]interface{}{
148+
"uri": "file:///path/to/resource.txt",
149+
"mimeType": "text/plain",
150+
"text": "resource content",
151+
},
152+
},
153+
},
154+
}
155+
156+
result, err := ConvertToCallToolResult(input)
157+
158+
require.NoError(t, err)
159+
require.NotNil(t, result)
160+
assert.Len(t, result.Content, 1)
161+
162+
res, ok := result.Content[0].(*sdk.EmbeddedResource)
163+
require.True(t, ok, "Expected EmbeddedResource")
164+
require.NotNil(t, res.Resource)
165+
assert.Equal(t, "file:///path/to/resource.txt", res.Resource.URI)
166+
assert.Equal(t, "text/plain", res.Resource.MIMEType)
167+
assert.Equal(t, "resource content", res.Resource.Text)
168+
})
169+
170+
t.Run("resource content type without resource field is skipped", func(t *testing.T) {
171+
input := map[string]interface{}{
172+
"content": []interface{}{
173+
map[string]interface{}{"type": "resource"},
174+
},
175+
}
176+
177+
result, err := ConvertToCallToolResult(input)
178+
179+
require.NoError(t, err)
180+
require.NotNil(t, result)
181+
// Resource item without "resource" field is skipped
182+
assert.Empty(t, result.Content)
183+
})
184+
102185
t.Run("unknown content type is treated as text", func(t *testing.T) {
103186
input := map[string]interface{}{
104187
"content": []interface{}{
105-
map[string]interface{}{"type": "image", "text": "image data"},
188+
map[string]interface{}{"type": "custom_type", "text": "custom data"},
106189
},
107190
}
108191

@@ -114,7 +197,7 @@ func TestConvertToCallToolResult(t *testing.T) {
114197

115198
text, ok := result.Content[0].(*sdk.TextContent)
116199
require.True(t, ok)
117-
assert.Equal(t, "image data", text.Text)
200+
assert.Equal(t, "custom data", text.Text)
118201
})
119202

120203
t.Run("simple string value is wrapped as text", func(t *testing.T) {

internal/server/call_tool_result_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,13 @@ func TestNewErrorCallToolResult(t *testing.T) {
187187
require.NotNil(t, result, "CallToolResult should not be nil")
188188
assert.True(t, result.IsError, "IsError should be true")
189189

190-
t.Logf("✓ Error CallToolResult properly created with IsError=%v", result.IsError)
190+
// Verify error message is included in content
191+
require.Len(t, result.Content, 1, "Should have one content item with error message")
192+
textContent, ok := result.Content[0].(*sdk.TextContent)
193+
require.True(t, ok, "Content should be TextContent")
194+
assert.Equal(t, tt.err.Error(), textContent.Text, "Error message should be in content")
195+
196+
t.Logf("✓ Error CallToolResult properly created with IsError=%v, message=%q", result.IsError, textContent.Text)
191197
})
192198
}
193199
}

internal/server/tool_registry.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ func (us *UnifiedServer) registerToolsFromBackend(serverID string) error {
143143
Name string `json:"name"`
144144
Description string `json:"description"`
145145
InputSchema map[string]interface{} `json:"inputSchema"`
146+
Annotations *sdk.ToolAnnotations `json:"annotations,omitempty"`
146147
} `json:"tools"`
147148
}
148149

@@ -179,6 +180,7 @@ func (us *UnifiedServer) registerToolsFromBackend(serverID string) error {
179180
Name: prefixedName,
180181
Description: toolDesc,
181182
InputSchema: normalizedSchema,
183+
Annotations: tool.Annotations,
182184
BackendID: serverID,
183185
}
184186
us.toolsMu.Unlock()
@@ -253,6 +255,7 @@ func (us *UnifiedServer) registerToolsFromBackend(serverID string) error {
253255
Name: prefixedName,
254256
Description: toolDesc,
255257
InputSchema: normalizedSchema, // Include the schema for clients to understand parameters
258+
Annotations: tool.Annotations,
256259
}, wrappedHandler)
257260

258261
log.Printf("Registered tool: %s", logName)

internal/server/unified.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type ToolInfo struct {
6060
Name string
6161
Description string
6262
InputSchema map[string]interface{}
63+
Annotations *sdk.ToolAnnotations
6364
BackendID string // Which backend this tool belongs to
6465
Handler func(context.Context, *sdk.CallToolRequest, interface{}) (*sdk.CallToolResult, interface{}, error)
6566
}
@@ -249,11 +250,15 @@ func (g *guardBackendCaller) CallTool(ctx context.Context, toolName string, args
249250
return executeBackendToolCall(g.ctx, g.server.launcher, g.serverID, sessionID.(string), toolName, args)
250251
}
251252

252-
// newErrorCallToolResult creates a standard error CallToolResult
253-
// This helper reduces code duplication for error returns following the pattern:
254-
// return &sdk.CallToolResult{IsError: true}, nil, err
253+
// newErrorCallToolResult creates a standard error CallToolResult with the error message
254+
// included as text content, so MCP clients can understand what went wrong.
255255
func newErrorCallToolResult(err error) (*sdk.CallToolResult, interface{}, error) {
256-
return &sdk.CallToolResult{IsError: true}, nil, err
256+
return &sdk.CallToolResult{
257+
IsError: true,
258+
Content: []sdk.Content{
259+
&sdk.TextContent{Text: err.Error()},
260+
},
261+
}, nil, err
257262
}
258263

259264
// callBackendTool calls a tool on a backend server with DIFC enforcement

0 commit comments

Comments
 (0)