Skip to content

🪢 fix: Paginate MCP tools/list to Load All Tools#66

Closed
TomasPalsson wants to merge 1 commit into
mainfrom
fix/mcp-tools-list-pagination
Closed

🪢 fix: Paginate MCP tools/list to Load All Tools#66
TomasPalsson wants to merge 1 commit into
mainfrom
fix/mcp-tools-list-pagination

Conversation

@TomasPalsson

@TomasPalsson TomasPalsson commented Jun 19, 2026

Copy link
Copy Markdown

Upstreamed in danny-avila#13840. Cherry-picked here (same commit, identical patch) so this fork carries the fix now and the eventual rebase onto upstream stays conflict-free.

Summary

I fixed MCP tool discovery silently truncating to the first page when a server paginates tools/list.

Root cause

tools/list is a cursor-paginated method in the MCP spec, but LibreChat only ever read the first page in two places:

  • MCPConnection.fetchTools (packages/api/src/mcp/connection.ts) called this.client.listTools() once and returned result.tools, discarding nextCursor.
  • MCPServerInspector (packages/api/src/mcp/registry/MCPServerInspector.ts) — which builds the agent-facing tool registry (config.toolFunctions, consumed by initializeMCPsMCPManager.getAppToolFunctions) at startup and per request — called the raw connection.client.listTools() directly in both getToolFunctions and fetchServerCapabilities.

So any server returning more tools than fit in one page only ever exposed page 1. With an aggregating gateway (observed: AWS Bedrock AgentCore Gateway returning 256 tools across 6 pages) only the first ~41 tools were registered; the rest were invisible to the agent, and invoking one failed with "This tool's MCP server is temporarily unavailable" because it was never in the registry.

Approach

  • fetchTools now follows nextCursor, passing { cursor } to listTools for each subsequent page and concatenating tools in order until the server stops returning a cursor. Two defensive guards bound the loop so a misbehaving server cannot stall discovery:
    • A page cap — mcpConfig.TOOLS_LIST_MAX_PAGES (env MCP_TOOLS_LIST_MAX_PAGES, default 50, clamped to >= 1). On hitting the cap the loop stops and emits a logger.warn.
    • A seenCursors set — if a server repeats a cursor it already returned (an infinite-loop signature), the loop stops early and warns.
  • MCPServerInspector.getToolFunctions and fetchServerCapabilities now route through connection.fetchTools() instead of the raw single-page client.listTools(), so the canonical startup and per-request tool registry is fully paginated.

What is deliberately not changed / out of scope

  • The no-throw contract is preserved: errors still route through emitError(error, 'Failed to fetch tools'). On a mid-pagination error the pages already fetched are returned (consistent with the page-cap partial return); a first-page error still yields [].
  • Pagination runs only at tool-discovery time — no new request is added to any per-tool-call or per-request hot path.
  • At startup the inspector reads the tool list twice — once for the logged capability summary and once for the tool registry. This double read pre-existed this PR (the old code called client.listTools() twice in the same two methods); pagination now applies to both. Collapsing them into one fetch is a reasonable follow-up.
  • fetchResources and fetchPrompts share the same single-page limitation. I left them out to keep this PR focused on the reported tool-loading bug; happy to follow up.

Changes

  • packages/api/src/mcp/connection.tsfetchTools follows nextCursor with page-cap and repeated-cursor guards, returning already-fetched pages on error.
  • packages/api/src/mcp/mcpConfig.ts — adds typed TOOLS_LIST_MAX_PAGES (env MCP_TOOLS_LIST_MAX_PAGES, default 50).
  • packages/api/src/mcp/registry/MCPServerInspector.tsgetToolFunctions and fetchServerCapabilities use the paginated fetchTools().
  • packages/api/src/mcp/__tests__/MCPConnectionFetchTools.test.ts — new unit test (8 cases) exercising the real MCPConnection.fetchTools via an injected listTools stub.
  • packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts and mcpConnectionsMock.helper.ts — updated so the inspector tests drive the paginated fetchTools path.
  • Five existing MCP test suites (lifecycle, SSRF, dbSourced, reinit-recovery, oauth-SDK) — their mcpConfig mocks now set TOOLS_LIST_MAX_PAGES so fetchTools runs the real pagination loop instead of short-circuiting on an undefined cap.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

Run on Node v24.16.0:

  • cd packages/api && npx jest src/mcp/__tests__/MCPConnectionFetchTools.test.ts — 8/8 passing. Cases: single page (one request), multi-page concat + cursor passthrough, page cap + warn, repeated cursor + warn, empty intermediate page, empty-string cursor (valid continuation), mid-pagination failure (already-fetched pages returned), first-page failure ([]).
  • cd packages/api && npx jest src/mcp/registry/__tests__/MCPServerInspector.test.ts — passing (inspector now drives the paginated path).
  • cd packages/api && npm run test:ci — full @librechat/api suite green, no regressions.
  • npx tsc --noEmit -p packages/api/tsconfig.json — clean.
  • npx eslint <changed files> — clean.
  • npx prettier --check <changed files> — formatted.
  • node scripts/sort-imports.mts --check <changed files> — sorted.

The new test constructs a real MCPConnection and injects a stub client.listTools, so it exercises the actual fetchTools implementation (matching how MCPConnectionAgentLifecycle.test.ts drives fetchTools) rather than a reimplementation of the loop.

Test Configuration:

  • Node.js: v24.16.0
  • Workspace: packages/api

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests that demonstrate my fix is effective
  • Local unit tests pass with my changes

Summary by CodeRabbit

  • New Features

    • Added pagination support when fetching tools from MCP servers to handle larger tool lists more efficiently.
    • Added configuration option to limit pagination depth.
  • Bug Fixes

    • Improved error handling during tool fetching—now returns previously collected tools if pagination encounters failures.
  • Tests

    • Expanded test coverage for pagination behavior, error scenarios, and edge cases.

MCP `tools/list` is cursor-paginated, but LibreChat only ever read the
first page. `MCPConnection.fetchTools()` called `client.listTools()` once
and discarded `nextCursor`, and `MCPServerInspector` — which builds the
agent-facing tool registry at startup and per request — called the raw
`client.listTools()` directly. Servers that paginate (e.g. an aggregating
gateway exposing hundreds of tools) only ever exposed page one; tools on
later pages were never registered, and invoking one returned
"This tool's MCP server is temporarily unavailable."

- `MCPConnection.fetchTools()` now follows `nextCursor` across pages and
  concatenates every page's tools, bounded by a configurable page cap
  (`MCP_TOOLS_LIST_MAX_PAGES`, default 50) and a repeated-cursor guard so a
  misbehaving server cannot loop forever. Tools already fetched are returned
  if a later page fails, and the no-throw error contract is unchanged.
- `MCPServerInspector.getToolFunctions()` and `fetchServerCapabilities()`
  now route through `fetchTools()`, so the canonical startup and
  per-request tool registry is fully paginated too.
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

mcpConfig gains a new TOOLS_LIST_MAX_PAGES setting (default 50, clamped to ≥1) sourced from MCP_TOOLS_LIST_MAX_PAGES. MCPConnection.fetchTools() is rewritten as a cursor-pagination loop bounded by that config, with repeated-cursor detection, per-page error recovery, and partial returns. MCPServerInspector callsites and all related test fixtures are updated accordingly.

Changes

fetchTools Cursor Pagination

Layer / File(s) Summary
TOOLS_LIST_MAX_PAGES config
packages/api/src/mcp/mcpConfig.ts
Adds TOOLS_LIST_MAX_PAGES type doc and runtime initialization from env var with default 50, clamped to minimum 1.
fetchTools pagination loop
packages/api/src/mcp/connection.ts
Replaces single client.listTools() with a cursor-accumulation loop bounded by TOOLS_LIST_MAX_PAGES, detecting repeated cursors, capping pages with a warning, logging errors on per-page failures, and returning partial results without throwing. JSDoc updated.
MCPServerInspector migration and mock helper
packages/api/src/mcp/registry/MCPServerInspector.ts, packages/api/src/mcp/registry/__tests__/mcpConnectionsMock.helper.ts
Migrates fetchServerCapabilities() and getToolFunctions() from connection.client.listTools() to connection.fetchTools(). Updates createMockConnection helper to expose a fetchTools mock resolving to the shared tools array.
MCPServerInspector test updates
packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts
Switches stubs from client.listTools() returning { tools: [...] } to fetchTools() returning a raw array for no-tools, conversion, and empty-list cases.
New fetchTools pagination unit tests
packages/api/src/mcp/__tests__/MCPConnectionFetchTools.test.ts
Adds full pagination test suite: single-page, multi-page cursor walking, page-cap warning, repeated-cursor stop, empty intermediate page, empty-string cursor, per-page error with partial return, first-page error returning [].
Existing test config fixture updates
packages/api/src/mcp/__tests__/MCPConnectionAgentLifecycle.test.ts, packages/api/src/mcp/__tests__/MCPConnectionSSRF.test.ts, packages/api/src/mcp/__tests__/dbSourced.integration.test.ts, packages/api/src/mcp/__tests__/MCPConnectionFactory.oauthSdk.integration.test.ts, packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts
Adds TOOLS_LIST_MAX_PAGES: 50 to the mcpConfig Jest mock in each existing test suite.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

📄 One page was never quite enough to see,
So cursors now march on, page after page, free.
A cap of fifty keeps the loop in check,
Repeated cursors? Caught — no infinite wreck.
Partial results returned, no throws, no fuss —
Paginate politely, and don't make a fuss! 🔄

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing pagination for MCP tools/list to load all tools instead of truncating to the first page.
Description check ✅ Passed The description comprehensively covers the root cause, approach, implementation details, testing methodology, and checklist items, though it doesn't strictly follow the template structure.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-tools-list-pagination

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/src/mcp/mcpConfig.ts`:
- Around line 53-54: The TOOLS_LIST_MAX_PAGES configuration uses Math.max(1,
math(...)) which can still produce NaN or Infinity values if the math() function
evaluates to non-finite numbers, breaking the loop bound guarantee in
fetchTools(). Ensure that after evaluating the math expression from the
environment variable, the result is clamped to a finite integer (validate with
Number.isFinite()), then apply Math.max with 1 to guarantee a minimum of 1. The
order should be: parse/evaluate with math(), validate/clamp to finite integer,
then apply Math.max to ensure the final value is always a valid positive
integer.

In `@packages/api/src/mcp/registry/MCPServerInspector.ts`:
- Around line 153-154: The issue is that fetchTools() is being called twice
during inspection—once around line 153-154 in the current context and again
around lines 174-175—causing redundant pagination traversals. Refactor by
fetching tools once and storing the result in a shared variable or Promise, then
reuse that single fetched tool list for both the tools name assignment (in the
code near line 153-154) and the tool functions retrieval (near line 174-175) to
eliminate the duplicate network calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0566bdc3-d70e-410f-b754-ed044fe23963

📥 Commits

Reviewing files that changed from the base of the PR and between a6b5343 and fdb4a6e.

📒 Files selected for processing (11)
  • packages/api/src/mcp/__tests__/MCPConnectionAgentLifecycle.test.ts
  • packages/api/src/mcp/__tests__/MCPConnectionFactory.oauthSdk.integration.test.ts
  • packages/api/src/mcp/__tests__/MCPConnectionFetchTools.test.ts
  • packages/api/src/mcp/__tests__/MCPConnectionSSRF.test.ts
  • packages/api/src/mcp/__tests__/dbSourced.integration.test.ts
  • packages/api/src/mcp/connection.ts
  • packages/api/src/mcp/mcpConfig.ts
  • packages/api/src/mcp/registry/MCPServerInspector.ts
  • packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts
  • packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts
  • packages/api/src/mcp/registry/__tests__/mcpConnectionsMock.helper.ts

Comment on lines +53 to +54
/** Max number of `tools/list` pages to request when an MCP server paginates its tool list. Clamped to >= 1. Default: 50 */
TOOLS_LIST_MAX_PAGES: Math.max(1, math(process.env.MCP_TOOLS_LIST_MAX_PAGES ?? 50)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clamp TOOLS_LIST_MAX_PAGES to a finite integer before applying Math.max.

The current expression can still produce NaN or Infinity (e.g., expression-based env values), which breaks the loop-bound guarantee in fetchTools().

Suggested fix
+const parsedToolsListMaxPages = math(process.env.MCP_TOOLS_LIST_MAX_PAGES ?? 50);
+const normalizedToolsListMaxPages = Number.isFinite(parsedToolsListMaxPages)
+  ? Math.floor(parsedToolsListMaxPages)
+  : 50;
+
 export const mcpConfig: {
@@
-  TOOLS_LIST_MAX_PAGES: Math.max(1, math(process.env.MCP_TOOLS_LIST_MAX_PAGES ?? 50)),
+  TOOLS_LIST_MAX_PAGES: Math.max(1, normalizedToolsListMaxPages),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/mcp/mcpConfig.ts` around lines 53 - 54, The
TOOLS_LIST_MAX_PAGES configuration uses Math.max(1, math(...)) which can still
produce NaN or Infinity values if the math() function evaluates to non-finite
numbers, breaking the loop bound guarantee in fetchTools(). Ensure that after
evaluating the math expression from the environment variable, the result is
clamped to a finite integer (validate with Number.isFinite()), then apply
Math.max with 1 to guarantee a minimum of 1. The order should be: parse/evaluate
with math(), validate/clamp to finite integer, then apply Math.max to ensure the
final value is always a valid positive integer.

Comment on lines +153 to +154
const tools = await this.connection!.fetchTools();
this.config.tools = tools.map((tool) => tool.name).join(', ');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid fetching paginated tools twice during one inspection pass.

fetchServerCapabilities() and getToolFunctions() now each call fetchTools(), so one inspection does two full pagination traversals. Reuse a single fetched tool list (or a shared Promise) for both config.tools and toolFunctions to prevent doubled network load and startup latency.

Refactor sketch
-      await Promise.allSettled([
-        this.fetchServerInstructions(),
-        this.fetchServerCapabilities(),
-        this.fetchToolFunctions(),
-      ]);
+      const toolsPromise = this.connection!.fetchTools();
+      await Promise.allSettled([
+        this.fetchServerInstructions(),
+        this.fetchServerCapabilities(toolsPromise),
+        this.fetchToolFunctions(toolsPromise),
+      ]);

Also applies to: 174-175

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/mcp/registry/MCPServerInspector.ts` around lines 153 - 154,
The issue is that fetchTools() is being called twice during inspection—once
around line 153-154 in the current context and again around lines
174-175—causing redundant pagination traversals. Refactor by fetching tools once
and storing the result in a shared variable or Promise, then reuse that single
fetched tool list for both the tools name assignment (in the code near line
153-154) and the tool functions retrieval (near line 174-175) to eliminate the
duplicate network calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant