Skip to content

Fix MCP tools list overflow#2210

Closed
Jordan-SkyLF wants to merge 3 commits into
nesquena:masterfrom
Jordan-SkyLF:fix/mcp-tools-list-overflow
Closed

Fix MCP tools list overflow#2210
Jordan-SkyLF wants to merge 3 commits into
nesquena:masterfrom
Jordan-SkyLF:fix/mcp-tools-list-overflow

Conversation

@Jordan-SkyLF
Copy link
Copy Markdown
Contributor

@Jordan-SkyLF Jordan-SkyLF commented May 13, 2026

Thinking Path

  • Settings → System is where Hermes WebUI already surfaces MCP runtime visibility, so the tool inventory should stay inspectable there without making the whole settings panel unusable.
  • The existing MCP Tools renderer handled small inventories, but a large runtime inventory rendered every row into the normal settings flow with no summary, paging, or bounded scroll area.
  • This PR keeps the existing WebUI-only/runtime-only contract: it does not start MCP servers, probe inactive servers, or require any hermes-agent changes.
  • The fix is intentionally narrow: bound the MCP Tools list, page the rendered rows, preserve search, and add focused regression coverage for the large-inventory case.

What Changed

  • Added an MCP Tools toolbar with result summary text and a remembered 5/10/20/40 per-page selector.
  • Rendered only the current page of filtered MCP tools and added previous/next pager controls.
  • Bounded the MCP Tools list with an internal scroll region so large inventories no longer balloon Settings → System.
  • Reset the bounded list scroll position when changing search, page, or page size.
  • Kept new summary, pager, page-size, and inactive-server strings i18n-backed across the existing locale key sets.
  • Added regression tests for pagination, bounded-list CSS, i18n keys/aria label, search reset behavior, and empty/no-match states.
  • Added release-note coverage in CHANGELOG.md.

Why It Matters

Large MCP setups can expose dozens of tools. Without bounding/pagination, Settings → System becomes dominated by the tool list and adjacent settings are pushed far down the page. This keeps the inventory useful while preserving the settings panel layout and avoiding any new MCP runtime side effects.

Before / After

Before: a synthetic 95-tool runtime inventory rendered as one unbounded list, with no result summary or pager.

Before: unbounded MCP Tools list

After: the same 95-tool inventory renders as a bounded list with a 5-row default page, summary text, remembered page-size control, and pager.

After: paginated MCP Tools list

Verification

  • Base freshness
    • origin/master fetched before publish: 6aedb7e0cd9879f308b063f84a3c87d86d903230
    • Latest release checked: v0.51.57
    • Branch is based on current origin/master.
  • Scope/split gate
    • Single logical UI/UX bugfix: MCP Tools list overflow in Settings → System.
    • Changed files are one cluster: static UI/CSS/i18n, focused tests, changelog, and safe PR screenshots.
    • No hermes-agent repo changes, no new WebUI dependency, no new agent-side API contract.
  • Syntax checks
    • node --check static/panels.js && node --check static/i18n.js
  • Focused tests
    • python -m pytest tests/test_mcp_tools_list_overflow.py tests/test_issue697_mcp_tool_inventory.py tests/test_chinese_locale.py tests/test_japanese_locale.py tests/test_korean_locale.py tests/test_russian_locale.py tests/test_spanish_locale.py -q
    • result: 42 passed in 3.91s
  • Full local suite attempt
    • python -m pytest tests/ -q
    • result: 5438 passed, 43 skipped, 1 xfailed, 2 xpassed, 8 subtests passed, plus one unrelated/order-dependent failure in tests/test_issue1499_keyless_onboarding.py::TestKeylessChatReady::test_lmstudio_keyless_chat_ready_via_full_status
    • isolated rerun of that exact test: 1 passed in 3.00s
  • Browser verification with synthetic 95-tool inventory
    • before: baseline instance rendered 95 rows with no toolbar/pager and no bounded list height.
    • after: branch instance rendered Showing 1-5 of 95 MCP tools. Page 1 of 19. with 5 visible rows, bounded list overflow, working next/page-size controls, persisted page-size preference, search reset to page 1, and no-match empty state.
  • GitHub Actions
    • PR checks passed on Python 3.11, 3.12, and 3.13.
  • Screenshot safety
    • Before/after screenshots are cropped to the MCP Tools panel only and were inspected for secrets, credentials, env values, private IDs, private paths, and unrelated sensitive settings.

Risks / Follow-ups

  • Non-English locale values for the new MCP Tools pagination strings use English fallback wording to preserve locale key parity; translators can refine wording later.
  • This does not expand the MCP inventory contract. Inactive configured servers are still reported only as inactive/unavailable metadata; WebUI still does not probe or start MCP servers.
  • Security note: this is a read-only client-side rendering change over already-known runtime inventory data. It does not alter auth, uploads, path handling, workspace access, or agent-action surfaces.

Model Used

  • OpenAI Codex gpt-5.5 via Hermes Agent, with terminal, file, browser, vision, GitHub CLI, and delegated code-review/tooling support.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Summary

Reading static/panels.js:6285-6410 on the PR HEAD plus the existing master file at the same lines, the change converts the previously unbounded _renderMcpTools flat dump into a paginated, scrollable, searchable list with a 5/10/20/40 page-size selector, summary line, and a pager. Static and i18n surfaces (static/index.html:1138-1145, static/style.css:2493-2510, static/i18n.js new keys) all line up cleanly with the script changes, and the JS-as-source regression suite in tests/test_mcp_tools_list_overflow.py is comprehensive. The PR explicitly keeps the existing _handle_mcp_tools_list contract intact (api/routes.py:9697-9724) — no new probing, no MCP server starts, just better rendering of the already-known runtime inventory plus the existing unavailable_servers array.

Code reference

The core paginate-then-render path at static/panels.js:6321-6360:

function _renderMcpTools(tools, query){
  const list=$('mcpToolList');
  const toolbar=$('mcpToolToolbar');
  if(!list) return;
  const filtered=_filterMcpToolsForSearch(tools, query);
  const total=Array.isArray(tools)?tools.length:0;
  const pages=Math.max(1,Math.ceil(filtered.length/_mcpToolsPageSize));
  _mcpToolsPage=Math.min(Math.max(1,_mcpToolsPage||1),pages);
  if(toolbar) toolbar.innerHTML=`<span class="mcp-tool-summary">${esc(_mcpToolsSummary(total,filtered.length,_mcpToolsPage,pages,query))}</span>${_mcpToolPageSizeControl()}`;
  _renderMcpToolPager(filtered.length,_mcpToolsPage,pages);
  ...
  const visible=filtered.slice((_mcpToolsPage-1)*_mcpToolsPageSize,_mcpToolsPage*_mcpToolsPageSize);
  list.innerHTML=visible.map(tool=>{ ... }).join('');
}

and the inactive-server hint surfaced via the existing API field at static/panels.js:6325-6330:

function _mcpToolsEmptyMessage(query){
  const base=esc(t(query?'mcp_tools_no_matches':'mcp_tools_no_tools'));
  const unavailable=Array.isArray(_mcpToolsMeta.unavailable_servers)?_mcpToolsMeta.unavailable_servers:[];
  if(query||!unavailable.length) return base;
  return `${base}<br><span class="mcp-tool-empty-detail">${esc(t('mcp_tools_inactive_configured_servers',unavailable.join(', ')))}</span>`;
}

Diagnosis / Recommendation

This is good work — the change is bounded, the new i18n keys (14 of them, all listed in tests/test_mcp_tools_list_overflow.py:test_mcp_tool_pagination_strings_are_i18n_backed) are properly threaded through t() with parameter slots rather than string concatenation, and the bounded scroll region .mcp-tool-list{max-height:min(52vh,560px);overflow:auto} plus scrollbar-gutter:stable is the right default to avoid layout shift when the list grows past the bound.

A few small notes for follow-up (none blocking):

  1. Page state is module-global, not per-mount. _mcpToolsPage and _mcpToolsPageSize are file-scoped lets. If the Settings panel is closed and reopened, the user lands back on whichever page they were last on. That is probably what most users want, but it means a navigation away then back doesn't reset to page 1. Worth confirming this matches the intent.

  2. Page-size selector doesn't persist. Selecting "40 per page" resets to the default 5 on next page load, since _mcpToolsPageSize is not stored in localStorage. If users routinely have 200+ tools across servers, a sticky preference would be a small UX win. Easy to add later: localStorage.setItem('hermes-webui-mcp-tools-page-size', next) in setMcpToolsPageSize plus a one-line restore in loadMcpTools.

  3. setMcpToolsPage(page) does not clamp. The pager buttons disable themselves correctly via ?disabled, but the function is exposed via onclick and could in principle be called with a bad value. The existing clamp inside _renderMcpTools (_mcpToolsPage=Math.min(Math.max(1,_mcpToolsPage||1),pages)) catches this on the next render, so this is defense-in-depth rather than a bug.

  4. Live page-size update via raw <select> value. setMcpToolsPageSize(this.value) passes a string, then Number(size) coerces. The MCP_TOOLS_PAGE_SIZE_OPTIONS.includes(next) guard correctly rejects anything not in [5,10,20,40] after coercion, so this is safe. Just calling it out so a future reviewer doesn't accidentally remove the guard thinking it is redundant.

Test plan / verification

The new test file pins eight invariants — index.html mounts, paginate-not-flat rendering, page-size selector contract, search-resets-to-page-1, the inactive-servers hint surface, CSS for the bounded scroll region and pager chrome, the i18n key set, and the CHANGELOG mention. That is enough JS-as-source coverage for a static asset, given the project's convention.

Manual checks I would run before merge:

  1. Open Settings → System with no MCP servers configured. Expect mcp_tools_no_tools empty state and no pager.
  2. Configure one MCP server but stop the runtime. Expect the empty state plus mcp_tools_inactive_configured_servers detail line listing that server.
  3. With 12+ tools active, switch the page-size selector to 5 and confirm the pager shows 1 / 3 (or similar), prev disabled on page 1, next disabled on the last page.
  4. Type a search term that matches 7 tools; confirm page resets to 1 and summary line reads Showing 1-5 of 7 matching "<query>" or equivalent.
  5. With the list scrolled mid-way, click a pager button and confirm the list scrollTop resets to 0 (setMcpToolsPage and setMcpToolsPageSize both do this explicitly).

@Jordan-SkyLF
Copy link
Copy Markdown
Contributor Author

Pushed f7c50b8 to cover the two small follow-ups worth folding into this PR: the MCP Tools page-size selector now persists across reloads, and the public page setter clamps invalid input before rendering. Updated the PR body/verification; CI is green on 3.11, 3.12, and 3.13.

nesquena-hermes added a commit that referenced this pull request May 14, 2026
stage-351: net-positive ready batch — perf CLI scan cache #2149 + thinking-tag leading-only #2213 + MCP tools pagination #2210 + per-target update summaries #2207 + sweep animation tune #2212 + agent-mode cron badge #2206
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Shipped to master via stage-351 → v0.51.58. All four substantive changes from this PR (toolbar, pagination, search-with-page-reset, bounded scroll area) are live on master.

Why this PR auto-close didn't fire: the rebased-PR-auto-close gotcha — your contributor branch was rebased against an older origin/master than the stage HEAD when it merged, so GitHub's "branch SHA matches stage merge" check didn't trigger. PR contents are 100% in master either way.

Thanks for the clean, well-scoped fix! Closing manually.

pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 14, 2026
Fix MCP tools list overflow with pagination/search (Jordan-SkyLF)
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