siyuan-note/siyuan · error
tools/list returned an empty response
Error message
tools/list returned an empty response
What it means
listAllMCPTools returns this error when an MCP server's tools/list pagination call yields a nil result despite no transport error. The MCP client treats a nil result as a protocol violation — a conforming server must return a tools/list result object (possibly with zero tools). Fired inside the bounded pagination loop before appending tools.
Solutions
- Check the MCP server implementation: ensure tools/list returns a result object (empty tools array is valid) rather than null
- Capture the raw JSON-RPC response for the failing server and compare against the MCP spec
- Retry/upgrade the MCP server; if it is third-party, report the non-conforming tools/list response
Example fix
// server side (non-conforming)
return nil, nil
// after
return &mcp.ListToolsResult{Tools: []mcp.Tool{}}, nil Defensive patterns
Strategy: try-catch
Try / catch
tools, err := listAllMCPTools(ctx, client)
if err != nil {
if strings.Contains(err.Error(), "tools/list returned an empty response") {
// skip the server or mark it unhealthy; do not retry blindly (page bound already applies)
}
return err
} Prevention
- Validate MCP server conformance before registration (tools/list must return a result object)
- Test third-party servers with a null-result probe during onboarding
- Keep server and client MCP protocol versions aligned
When it happens
Trigger: A connected MCP server responds to tools/list (any page, including the first) with an empty/nil result — e.g. a non-conforming server implementation, a proxy stripping the result, or JSON-RPC success with null result.
Common situations: Third-party MCP servers that return null instead of {tools: []}, middleware/API gateways mangling responses, version mismatch where the peer speaks an older MCP dialect.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- tools/list exceeded pages
- tools/list repeated cursor
- connect
- attr must be a string or null (got %T)
- command is required for stdio server
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/c64d58078e81b922.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/mcp/client/mcp.go:356
setMCPRuntimeStateForContext(ctx, server.ID, "connected", registered, "", "")
logging.LogInfof("mcp: server [%s] connected, %d tools registered", server.Name, registered)
return connection
}
func listAllMCPTools(ctx context.Context,
listPage func(context.Context, *mcp.ListToolsParams) (*mcp.ListToolsResult, error)) ([]*mcp.Tool, error) {
var (
allTools []*mcp.Tool
params *mcp.ListToolsParams
)
seenCursors := map[string]struct{}{}
for page := 0; page < maxMCPToolListPages; page++ {
result, err := listPage(ctx, params)
if err != nil {
return nil, err
}
if result == nil {
return nil, fmt.Errorf("tools/list returned an empty response")
}
allTools = append(allTools, result.Tools...)
if result.NextCursor == "" {
return allTools, nil
}
if _, exists := seenCursors[result.NextCursor]; exists {
return nil, fmt.Errorf("tools/list repeated cursor %q", result.NextCursor)
}
seenCursors[result.NextCursor] = struct{}{}
params = &mcp.ListToolsParams{Cursor: result.NextCursor}
}
return nil, fmt.Errorf("tools/list exceeded %d pages", maxMCPToolListPages)
}
func sanitizedServerNameCollision(server conf.MCPServer) bool {
mcpMu.Lock()
defer mcpMu.Unlock()
sanitizedName := sanitize(server.Name)View on GitHub (pinned to 9f775e8a12)