Skip to content

fix(mcp): follow tools/list pagination so later pages are not dropped - #3042

Open
Johnx-w wants to merge 2 commits into
Chainlit:mainfrom
Johnx-w:fix/mcp-list-tools-pagination
Open

Johnx-w wants to merge 2 commits into
Chainlit:mainfrom
Johnx-w:fix/mcp-list-tools-pagination

Conversation

@Johnx-w

@Johnx-w Johnx-w commented Sep 8, 2026

Copy link
Copy Markdown

Description

POST /mcp called ClientSession.list_tools() once. The MCP Python SDK returns a single page and leaves nextCursor to the caller, so servers that paginate tools/list silently dropped later tools from the connect payload and the composer MCP list.

This walks nextCursor (and the next_cursor alias), caps the walk at 100 pages, and stops if a cursor repeats.

Why

MCP pagination is part of the spec; list_tools() does not auto-paginate. The UI only sees the tools returned here.

Testing

  • uv run pytest backend/tests/test_mcp.py::TestListAllMcpTools
  • uv run pytest backend/tests/test_mcp.py::TestConnectMcpEndpoint::test_connect_follows_list_tools_pagination (needs frontend build)
  • uv run scripts/lint.py backend/chainlit/mcp.py backend/chainlit/server.py backend/tests/test_mcp.py
  • uv run scripts/format.py --check on the same files

Summary by cubic

Fixes POST /mcp so paginated MCP servers no longer silently drop later tools from the connect payload and composer MCP list, and ensures a failed or hung tools/list cannot evict a working session.

  • Walks nextCursor (and the next_cursor alias) until the server omits it, capped at 100 pages and stopping on a repeated cursor.
  • Bounds each page and the whole walk with the connect timeout, so a server that never finishes paginating cannot hang the connect handler.
  • Lists tools before swapping in the replacement session, so a reconnect that fails during listing keeps the old session intact.
  • Adds tests covering pagination, cursor aliases, repeated cursors, the page cap, timeouts, and session eviction.

Written for commit de819a6. Summary will update on new commits.

Review in cubic

POST /mcp only called list_tools once, so paginated MCP servers dropped later tools from the connect payload and composer list.

Walk nextCursor, cap at 100 pages, and stop on a repeated cursor.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread backend/chainlit/mcp.py Outdated
Comment thread backend/chainlit/server.py Outdated
Pagination awaits were unbounded after initialize, and a later tools/list
page could fail after the new session was already stored. Bound each page
and the whole walk with the connect timeout, and only swap after listing
succeeds so a failed reconnect does not evict a working session.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/chainlit/server.py">

<violation number="1" location="backend/chainlit/server.py:1825">
P2: When `/mcp` disconnects the same name while pagination is in progress, the later `swap_mcp_session` recreates the session after DELETE has completed. Coordinate pending connects with disconnects, or invalidate the in-flight connect before allowing it to store the new session.</violation>

<violation number="2" location="backend/chainlit/server.py:1830">
P1: When the request is cancelled during `list_all_mcp_tools`, `asyncio.CancelledError` bypasses this `except Exception`, so `stop_mcp_task` never runs. Catch cancellation, stop the task, then re-raise; the pre-swap listing otherwise leaves the transport unreachable from `mcp_sessions`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

page_timeout=connect_timeout,
total_timeout=connect_timeout,
)
except Exception as e:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: When the request is cancelled during list_all_mcp_tools, asyncio.CancelledError bypasses this except Exception, so stop_mcp_task never runs. Catch cancellation, stop the task, then re-raise; the pre-swap listing otherwise leaves the transport unreachable from mcp_sessions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/server.py, line 1830:

<comment>When the request is cancelled during `list_all_mcp_tools`, `asyncio.CancelledError` bypasses this `except Exception`, so `stop_mcp_task` never runs. Catch cancellation, stop the task, then re-raise; the pre-swap listing otherwise leaves the transport unreachable from `mcp_sessions`.</comment>

<file context>
@@ -1821,11 +1821,41 @@ async def _mcp_session_runner() -> None:
+            page_timeout=connect_timeout,
+            total_timeout=connect_timeout,
+        )
+    except Exception as e:
+        # Same teardown as a rejecting on_mcp_connect: tools/list is part of
+        # making the connection usable, and it runs *before* swap so a
</file context>
Suggested change
except Exception as e:
except asyncio.CancelledError:
await stop_mcp_task(task, stop_event, payload.name)
raise
except Exception as e:

)

try:
tools = await list_all_mcp_tools(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When /mcp disconnects the same name while pagination is in progress, the later swap_mcp_session recreates the session after DELETE has completed. Coordinate pending connects with disconnects, or invalidate the in-flight connect before allowing it to store the new session.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/server.py, line 1825:

<comment>When `/mcp` disconnects the same name while pagination is in progress, the later `swap_mcp_session` recreates the session after DELETE has completed. Coordinate pending connects with disconnects, or invalidate the in-flight connect before allowing it to store the new session.</comment>

<file context>
@@ -1821,11 +1821,41 @@ async def _mcp_session_runner() -> None:
             )
 
+    try:
+        tools = await list_all_mcp_tools(
+            mcp_client_session,
+            page_timeout=connect_timeout,
</file context>

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