siyuan-note/siyuan · error

tools/list returned an empty response

Error message

tools/list returned an empty response

What it means

Returned by the MCP client's `tools/list` pagination helper when a page returns a nil `*mcp.ListToolsResult`. The protocol expects every successful `tools/list` response to carry a result object (even one with an empty `Tools` slice and no `NextCursor`); a nil result violates that contract and is treated as a server bug.

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 251596fc0d)

Solutions

  1. Update or patch the MCP server so it always returns a non-nil `ListToolsResult` (use `{"tools":[]}` if it has none).
  2. Verify the server speaks the MCP `tools/list` method and the correct JSON-RPC result envelope.
  3. If you control the client integration, log the raw response to identify whether the server or the transport is dropping the result.
Defensive patterns

Strategy: try-catch

Validate before calling

// Before listing, do a lightweight handshake/initialize to confirm the server conforms:
if _, err := client.Initialize(ctx, ...); err != nil {
    return fmt.Errorf("mcp server initialize failed: %w", err)
}

Try / catch

tools, err := listAllTools(ctx, client.ListTools)
if err != nil {
    if strings.Contains(err.Error(), "empty response") {
        // server returned nil result; flag the server as non-conformant
    }
    return err
}

Prevention

When it happens

Trigger: An MCP server (stdio or HTTP/streamable) replies to `tools/list` with no result object — e.g. an empty JSON-RPC success payload, a server that returns `null`, or a transport that decoded the response into a nil pointer.

Common situations: Connecting to a third-party or in-development MCP server that does not conform to the result envelope; a proxy stripping the result field; a transport deserialization edge case.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/c64d58078e81b922. Report an issue: GitHub.