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

  1. Check the MCP server implementation: ensure tools/list returns a result object (empty tools array is valid) rather than null
  2. Capture the raw JSON-RPC response for the failing server and compare against the MCP spec
  3. 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

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


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)