siyuan-note/siyuan · error
tools/list repeated cursor
Error message
tools/list repeated cursor %q
What it means
listAllMCPTools paginates a remote MCP server's tools/list results, tracking every NextCursor it has already followed. When the server returns a cursor identical to one already seen, pagination would loop forever, so the client aborts with this error. It indicates a buggy or non-conformant MCP server implementation rather than a client configuration problem.
Solutions
- Upgrade or fix the MCP server so each page returns a distinct NextCursor and empty string terminates the list
- Verify the server actually advances its cursor when the request carries a Cursor parameter (test with a raw tools/list call)
- Check for proxies/caches between client and server that replay responses
- If a third-party server cannot be fixed, reduce the tool count so it fits one page, or use a corrected server build
Example fix
// before: server always returns NextCursor "abc"
// after: server returns empty NextCursor on the last page
if hasMorePages {
result.NextCursor = nextCursorToken // unique per page
} else {
result.NextCursor = ""
} Defensive patterns
Strategy: try-catch
Validate before calling
null
Type guard
null
Try / catch
conn, err := listAllMCPTools(ctx, listPage)
if err != nil {
if strings.Contains(err.Error(), "repeated cursor") {
logging.LogWarnf("mcp: server cursor bug: %v", err)
return // mark server unhealthy, skip tool registration
}
return err
} Prevention
- Test new MCP server integrations against a known-good reference client first
- Pin and regularly update third-party MCP server versions
- Monitor logs for repeated-cursor errors to identify broken server builds early
When it happens
Trigger: connectOneServer calls listAllMCPTools; the server returns a tools/list page whose NextCursor equals a previously returned cursor (including returning the first page's cursor again).
Common situations: An MCP server with a broken cursor implementation (always returns the same cursor), a server that ignores the requested cursor parameter, a cached/stale middleware replaying the first page, or a mocked/test server misbehaving.
Related errors
- tools/list exceeded pages
- tools/list returned an empty response
- 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/4101a31d49e0222f.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/mcp/client/mcp.go:363
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)
for _, configured := range mcpServers {
if configured.ID != server.ID && sanitize(configured.Name) == sanitizedName {
return true
}
}
return false
}View on GitHub (pinned to 9f775e8a12)