Skip to content

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

Merged
danny-avila merged 3 commits into
danny-avila:devfrom
TomasPalsson:fix/mcp-tools-list-pagination
Jun 20, 2026
Merged

🪢 fix: Paginate MCP tools/list to Load All Tools#13840
danny-avila merged 3 commits into
danny-avila:devfrom
TomasPalsson:fix/mcp-tools-list-pagination

Conversation

@TomasPalsson

@TomasPalsson TomasPalsson commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

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 page and concatenating tools in order until the server stops returning a cursor. Two defensive guards bound the loop:
    • 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 warns.
    • 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(), 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: on a mid-pagination error the pages already fetched are returned (consistent with the page-cap partial return); a first-page error still yields [].
  • MCPServerInspector reads the tool list twice at startup (capability summary + tool registry); this double read pre-existed the PR, so collapsing it into one fetch is left as a follow-up.
  • fetchResources and fetchPrompts share the same single-page limitation; left out to keep this PR focused.

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().
  • New unit test plus updated MCP test mocks so the suites exercise the paginated fetchTools (including five mcpConfig mocks that previously omitted the cap).

Change Type

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

Testing

Run from packages/api on Node v24.16.0:

  • npx jest src/mcp/__tests__/MCPConnectionFetchTools.test.ts — 8/8 passing. Covers single page, multi-page concat with cursor passthrough, page-cap warn, repeated-cursor warn, empty intermediate page, empty-string cursor, mid-pagination failure (already-fetched pages returned), and first-page failure ([]). Builds a real MCPConnection and injects a stub client.listTools, exercising the actual loop.
  • npx jest src/mcp/registry/__tests__/MCPServerInspector.test.ts — passing; inspector now drives the paginated path.
  • npm run test:ci — full @librechat/api suite green, no regressions.
  • tsc, ESLint, Prettier, and import-sort clean on changed files.

Test Configuration

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

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.
@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 1075d23a95

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila
danny-avila changed the base branch from main to dev June 20, 2026 15:03
@danny-avila
danny-avila merged commit 229c54c into danny-avila:dev Jun 20, 2026
33 checks passed
fuuuzzy pushed a commit to fuuuzzy/LibreChat that referenced this pull request Jul 7, 2026
* 🪢 fix: Paginate MCP tools/list to load all tools

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.

* style: Sort MCP test imports

* style: Sort mutation type imports

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
ThomasVuNguyen pushed a commit to ThomasVuNguyen/LibreChat that referenced this pull request Jul 15, 2026
* 🪢 fix: Paginate MCP tools/list to load all tools

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.

* style: Sort MCP test imports

* style: Sort mutation type imports

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
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.

2 participants