Skip to content

Commit 9164d52

Browse files
authored
fix: [AG-99] Fixing NULL resource response (#9)
We released a bug when upgrating to mark3labs/mcp-go v0.43.0 in how it handled empty resources (returning NULL instead of an empty array) Downgrading back to mark3labs/mcp-go v0.31.0 and adding tests.
1 parent f71c169 commit 9164d52

8 files changed

Lines changed: 169 additions & 293 deletions

File tree

go.mod

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/adrg/xdg v0.5.3
77
github.com/golang/mock v1.6.0
88
github.com/google/uuid v1.6.0
9-
github.com/mark3labs/mcp-go v0.43.0
9+
github.com/mark3labs/mcp-go v0.31.0
1010
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
1111
github.com/pkg/errors v0.9.1
1212
github.com/rs/zerolog v1.34.0
@@ -25,8 +25,6 @@ require (
2525
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
2626
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
2727
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
28-
github.com/bahlo/generic-list-go v0.2.0 // indirect
29-
github.com/buger/jsonparser v1.1.1 // indirect
3028
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
3129
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
3230
github.com/charmbracelet/lipgloss v1.1.0 // indirect
@@ -45,7 +43,6 @@ require (
4543
github.com/gofrs/flock v0.12.1 // indirect
4644
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
4745
github.com/hashicorp/go-uuid v1.0.3 // indirect
48-
github.com/invopop/jsonschema v0.13.0 // indirect
4946
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
5047
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
5148
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
@@ -55,7 +52,6 @@ require (
5552
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
5653
github.com/kevinburke/ssh_config v1.2.0 // indirect
5754
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
58-
github.com/mailru/easyjson v0.9.0 // indirect
5955
github.com/mattn/go-colorable v0.1.14 // indirect
6056
github.com/mattn/go-isatty v0.0.20 // indirect
6157
github.com/mattn/go-runewidth v0.0.16 // indirect
@@ -78,7 +74,6 @@ require (
7874
github.com/spf13/cast v1.7.1 // indirect
7975
github.com/spf13/viper v1.20.1 // indirect
8076
github.com/subosito/gotenv v1.6.0 // indirect
81-
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
8277
github.com/xanzy/ssh-agent v0.3.3 // indirect
8378
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
8479
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect

go.sum

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
1818
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
1919
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
2020
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
21-
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
22-
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
2321
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
24-
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
25-
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
2622
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
2723
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
2824
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
@@ -94,8 +90,6 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI
9490
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
9591
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
9692
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
97-
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
98-
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
9993
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
10094
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
10195
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@@ -122,10 +116,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
122116
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
123117
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
124118
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
125-
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
126-
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
127-
github.com/mark3labs/mcp-go v0.43.0 h1:lgiKcWMddh4sngbU+hoWOZ9iAe/qp/m851RQpj3Y7jA=
128-
github.com/mark3labs/mcp-go v0.43.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
119+
github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4=
120+
github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
129121
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
130122
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
131123
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -220,8 +212,6 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
220212
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
221213
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
222214
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
223-
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
224-
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
225215
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
226216
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
227217
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* © 2025 Snyk Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package mcp
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"testing"
23+
24+
"github.com/mark3labs/mcp-go/mcp"
25+
"github.com/mark3labs/mcp-go/server"
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
)
29+
30+
// TestMCPResponseFormat verifies that MCP server list responses return empty arrays
31+
// instead of null when no items are registered. This is required by the MCP specification
32+
// and JSON API best practices.
33+
// See: https://modelcontextprotocol.io/docs/concepts/resources
34+
func TestMCPResponseFormat(t *testing.T) {
35+
testCases := []struct {
36+
name string
37+
serverOptions []server.ServerOption
38+
setupServer func(*server.MCPServer)
39+
method string
40+
expectedArrayField string
41+
shouldBeEmpty bool
42+
expectedToolName string
43+
}{
44+
{
45+
name: "empty resources list returns empty array not null",
46+
serverOptions: []server.ServerOption{
47+
server.WithResourceCapabilities(true, true),
48+
},
49+
setupServer: nil,
50+
method: "resources/list",
51+
expectedArrayField: "resources",
52+
shouldBeEmpty: true,
53+
},
54+
{
55+
name: "empty tools list returns empty array not null after removing tool",
56+
serverOptions: nil,
57+
setupServer: func(s *server.MCPServer) {
58+
// Add and then remove a tool to get an empty list with tool capabilities enabled
59+
tool := mcp.NewTool("temp_tool", mcp.WithDescription("A temporary tool"))
60+
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
61+
return mcp.NewToolResultText("test"), nil
62+
})
63+
s.DeleteTools("temp_tool")
64+
},
65+
method: "tools/list",
66+
expectedArrayField: "tools",
67+
shouldBeEmpty: true,
68+
},
69+
{
70+
name: "empty prompts list returns empty array not null",
71+
serverOptions: []server.ServerOption{
72+
server.WithPromptCapabilities(true),
73+
},
74+
setupServer: nil,
75+
method: "prompts/list",
76+
expectedArrayField: "prompts",
77+
shouldBeEmpty: true,
78+
},
79+
{
80+
name: "empty resource templates list returns empty array not null",
81+
serverOptions: []server.ServerOption{
82+
server.WithResourceCapabilities(true, true),
83+
},
84+
setupServer: nil,
85+
method: "resources/templates/list",
86+
expectedArrayField: "resourceTemplates",
87+
shouldBeEmpty: true,
88+
},
89+
{
90+
name: "registered tool appears in non-empty array",
91+
serverOptions: nil,
92+
setupServer: func(s *server.MCPServer) {
93+
tool := mcp.NewTool("test_tool", mcp.WithDescription("A test tool"))
94+
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
95+
return mcp.NewToolResultText("test"), nil
96+
})
97+
},
98+
method: "tools/list",
99+
expectedArrayField: "tools",
100+
shouldBeEmpty: false,
101+
expectedToolName: "test_tool",
102+
},
103+
}
104+
105+
for _, tc := range testCases {
106+
t.Run(tc.name, func(t *testing.T) {
107+
// Create server with options
108+
mcpServer := createTestServer(t, tc.serverOptions)
109+
110+
// Apply additional setup if provided
111+
if tc.setupServer != nil {
112+
tc.setupServer(mcpServer)
113+
}
114+
115+
// Initialize the server session
116+
initializeServer(t, mcpServer)
117+
118+
// Send the list request
119+
responseStr := sendListRequest(t, mcpServer, tc.method)
120+
121+
// Verify the response format
122+
if tc.shouldBeEmpty {
123+
assert.Contains(t, responseStr, `"`+tc.expectedArrayField+`":[]`,
124+
"Response should contain empty %s array, not null. Got: %s", tc.expectedArrayField, responseStr)
125+
assert.NotContains(t, responseStr, `"`+tc.expectedArrayField+`":null`,
126+
"Response should NOT contain null %s. Got: %s", tc.expectedArrayField, responseStr)
127+
} else {
128+
assert.NotContains(t, responseStr, `"`+tc.expectedArrayField+`":null`,
129+
"Response should NOT contain null %s. Got: %s", tc.expectedArrayField, responseStr)
130+
assert.NotContains(t, responseStr, `"`+tc.expectedArrayField+`":[]`,
131+
"Response should NOT contain empty %s array when items are registered. Got: %s", tc.expectedArrayField, responseStr)
132+
if tc.expectedToolName != "" {
133+
assert.Contains(t, responseStr, `"`+tc.expectedToolName+`"`,
134+
"Response should contain the registered tool %s. Got: %s", tc.expectedToolName, responseStr)
135+
}
136+
}
137+
})
138+
}
139+
}
140+
141+
// createTestServer creates an MCP server with the given options for testing.
142+
func createTestServer(t *testing.T, options []server.ServerOption) *server.MCPServer {
143+
t.Helper()
144+
return server.NewMCPServer("test-server", "1.0.0", options...)
145+
}
146+
147+
// initializeServer sends an initialize request to the MCP server.
148+
func initializeServer(t *testing.T, mcpServer *server.MCPServer) {
149+
t.Helper()
150+
initRequest := `{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test-client","version":"1.0.0"},"capabilities":{}}}`
151+
initResponse := mcpServer.HandleMessage(context.Background(), []byte(initRequest))
152+
require.NotNil(t, initResponse, "Initialize response should not be nil")
153+
}
154+
155+
// sendListRequest sends a list request for the given method and returns the JSON response string.
156+
func sendListRequest(t *testing.T, mcpServer *server.MCPServer, method string) string {
157+
t.Helper()
158+
request := `{"jsonrpc":"2.0","method":"` + method + `","id":2,"params":{}}`
159+
response := mcpServer.HandleMessage(context.Background(), []byte(request))
160+
require.NotNil(t, response, "Response should not be nil")
161+
162+
responseJSON, err := json.Marshal(response)
163+
require.NoError(t, err, "Failed to marshal response to JSON")
164+
165+
return string(responseJSON)
166+
}

licenses/github.com/bahlo/generic-list-go/LICENSE

Lines changed: 0 additions & 27 deletions
This file was deleted.

licenses/github.com/buger/jsonparser/LICENSE

Lines changed: 0 additions & 21 deletions
This file was deleted.

licenses/github.com/invopop/jsonschema/COPYING

Lines changed: 0 additions & 19 deletions
This file was deleted.

licenses/github.com/mailru/easyjson/LICENSE

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)