Skip to content

feat(mcp): search + keyboard navigation for installed MCP servers#2639

Open
aashir-athar wants to merge 2 commits into
tinyhumansai:mainfrom
aashir-athar:feat/mcp-servers-search
Open

feat(mcp): search + keyboard navigation for installed MCP servers#2639
aashir-athar wants to merge 2 commits into
tinyhumansai:mainfrom
aashir-athar:feat/mcp-servers-search

Conversation

@aashir-athar
Copy link
Copy Markdown

@aashir-athar aashir-athar commented May 25, 2026

Summary

  • New search/filter feature for the installed MCP servers list in Channels → ChannelConfigPanel → McpServersTab. As users install more MCP servers, scanning the left-pane list becomes a real chore — this PR adds a controlled search input, live filtering, an accessible result counter, and arrow-key navigation across the list.
  • New component McpServerSearch.tsx: controlled <input type="search"> wrapped in a role="search" landmark, with a clear button that appears when the value is non-empty. Intentionally no global keyboard shortcut to avoid colliding with the app-wide CommandProvider in App.tsx.
  • Extended InstalledServerList.tsx: optional filter prop case-insensitively matches against display_name, qualified_name, and description. New "X of Y servers" indicator announced via role="status" aria-live="polite". New "No servers match "{query}"" empty state. ArrowUp/ArrowDown move focus across the visible server buttons (clamped at the edges; only the filtered set is traversed).
  • 6 new i18n keys under the mcp.installed.search.* namespace, mirrored across all 13 locale chunks (English values used as the untranslated placeholder per the repo's standard pattern; counted in each locale's "untranslated" total exactly like the existing keys).
  • 18 new Vitest tests (7 for McpServerSearch, 11 for the new behaviour in InstalledServerList).

Problem

The MCP feature surface ships today via pages/Channels.tsxChannelConfigPanel.tsxMcpServersTab.tsx. The left pane is a flat scrolling list of installed servers (InstalledServerList). With three or four installs it's fine. The moment a user installs the eighth or fifteenth — which is the actual usage shape with the Smithery catalog — finding a specific server requires either scrolling or page-search via the browser/CEF shortcut.

There's also no keyboard-only path for moving through the list. Each server is a <button> so Tab moves between them, but power users with even a small inventory want ArrowUp/ArrowDown.

These are the two paper cuts this PR addresses. They're contained, complementary, and they scale with the platform's growth — every Smithery server added makes them more valuable.

Solution

New: McpServerSearch.tsx

Controlled component with two props (value, onChange). Renders:

  • A <div role="search" aria-label={t('mcp.installed.search.landmarkAria')}> landmark so assistive tech can jump straight to it.
  • <input type="search"> with aria-label and placeholder, both translated.
  • A clear <button> that only appears when value.length > 0, with aria-label and a decorative <svg aria-hidden="true"> X icon.

Intentional non-features:

  • No global Cmd/Ctrl+K binding. The app already has a CommandProvider in App.tsx; binding another global shortcut here would silently override or fight with it. Users click or Tab into the field.

Extended: InstalledServerList.tsx

New optional filter?: string prop. When set, the list:

  1. Trims and lowercases the filter.
  2. Builds a haystack per server from display_name + qualified_name + description and matches case-insensitively (substring).
  3. Renders, in order:
    • If filter is non-empty: a <p role="status" aria-live="polite"> showing "{shown} of {total} servers" so screen readers announce the new total as the user types.
    • If the filtered set is empty: a centered "No servers match "{query}"" message. Distinct from the existing zero-installed empty state, which still wins when servers.length === 0 regardless of filter.
    • Otherwise: the existing <ul> of server buttons.
  4. The original (zero-installed) empty state is preserved untouched and still wins over the no-match state — verified by an explicit test.

Each server button gets a stable data-server-id={server.server_id} attribute and an onKeyDown handler:

  • ArrowDown / ArrowUp find the next/previous sibling button via listRef.current.querySelectorAll('button[data-server-id]'), call focus(), and preventDefault. Clamps at the edges (no wrap, simpler mental model; matches WAI-ARIA listbox guidance for explicit single-selection lists).
  • Other keys are not intercepted — Enter/Space still activate the button, Tab still escapes the list. Verified by an explicit test that asserts fireEvent.keyDown returns true for an unrelated key.

Wiring in McpServersTab.tsx

A new useState<string>('') in the tab owns the filter. The <McpServerSearch> renders directly above <InstalledServerList> in the left pane, but only when servers.length > 0 — there's nothing to search when nothing is installed, and the search box would visually compete with the existing "Browse catalog" empty-state CTA.

The filter is intentionally not persisted — it's a transient scan helper, not a saved view. Reloading the tab or revisiting the page returns to an unfiltered list.

i18n

6 new keys under mcp.installed.search.* added to app/src/lib/i18n/en.ts AND to app/src/lib/i18n/chunks/en-1.ts (the runtime chunk that holds the mcp.installed.* namespace). Per the project's parity rule enforced by scripts/i18n-coverage.ts, the same six keys are also added to all 12 non-English locale chunks (ar-1.tszh-CN-1.ts) with English values as untranslated placeholders. They show up in each locale's "untranslated" count exactly like every other not-yet-translated key.

Verified locally:

$ pnpm i18n:check
…
## zh-CN  (2988 keys)
  missing:        0
  extra:          0
  …
  per-chunk:      1:1197/1197  2:387/387  3:389/389  4:391/391  5:629/629
(same shape for ar, bn, de, es, fr, hi, id, it, ko, pt, ru)
EXIT: 0

Submission Checklist

  • Tests added — 18 new tests in two files. 7 cover the new McpServerSearch (landmark, input attributes, clear button visibility / firing, controlled value, decorative-icon aria-hidden). 11 cover the new behaviour on InstalledServerList (filter by name / qualified_name / description / undefined-description; whitespace trimming; no-match state; "X of Y" with correct role="status" + aria-live="polite" attributes; count hidden when filter empty or whitespace-only; original empty state precedence; ArrowDown/Up focus moves; clamping at edges; unrelated keys not intercepted; arrow nav restricted to visible items).
  • Diff coverage ≥ 80% — every new branch in InstalledServerList.tsx and every new line in McpServerSearch.tsx is exercised by the new tests. Local Vitest: 99/99 passing, including all pre-existing MCP component tests (no regression).
  • Coverage matrix updated — N/A: enhancement to an existing feature row, no new feature ID added or removed.
  • All affected feature IDs listed under ## RelatedN/A: enhancement to existing MCP feature surface.
  • No new external network dependencies introduced — purely client-side filter and keyboard nav; no new API calls, no new packages.
  • Manual smoke checklist updated if release-cut surfaces touched — N/A: in-app component only, no release-cut surface.
  • Linked issue closed via Closes #NNN in ## Related — no specific issue; this is an organic UX improvement on the MCP surface.

Impact

  • Runtime/platform: Desktop only — McpServersTab is only rendered in ChannelConfigPanel which only shows on desktop. No iOS / web impact.
  • Performance: Filter is O(n × m) where n = installed servers and m = filter length. With realistic n (~10–100 installs) and m (~tens of chars), this is microseconds per keystroke. useMemo guards the filtered list so the filter only re-runs when servers or trimmedFilter change.
  • Security / migration / compatibility: None.
  • Backward compatibility: filter is an optional prop with default ''. Any existing caller of InstalledServerList that doesn't pass it gets exactly the previous behaviour byte-for-byte (verified via the existing test suite passing unchanged).
  • i18n: English text everywhere by default; the same 6 keys exist in every locale's chunks as untranslated placeholders ready for native-speaker translation in follow-up PRs.

Related

  • Closes:
  • Follow-up PR(s)/TODOs:
    • Native-speaker translation of the 6 new mcp.installed.search.* keys across the 12 non-English locales (they currently fall back to the English values; flagged in each locale's untranslated count).
    • Optional future: also surface tool names in the filter haystack (search by "git" → matches any server that ships a tool named git_*). Needs the tool inventory pre-loaded across all connected servers; out of scope for this PR (would change the data model).

AI Authored PR Metadata

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/mcp-servers-search
  • Commit SHA: 13d70bda (will be referenced from this PR)

Validation Run

All four key gates passed locally:

  • pnpm --filter openhuman-app compileclean (tsc --noEmit, no output = success).
  • pnpm --filter openhuman-app lintclean (exit 0).
  • pnpm vitest run src/components/channels/mcp/99/99 passing including the 18 new tests and the pre-existing Skills.mcp-coming-soon.test.tsx (the latter confirms no regression to the intentionally-pinned Skills-page placeholder).
  • pnpm i18n:checkexit 0, every locale at 1:1197/1197 parity, 0 missing keys, 0 extra keys, 0 drift.

Validation Blocked

  • command: pnpm --filter openhuman-app format:check (which chains cargo fmt --check via the app's format:check script) and the husky pre-push hook (which runs pnpm formatcargo fmt).
  • error: 'cargo' is not recognized as an internal or external command, operable program or batch file. — no Rust toolchain installed on the dev machine.
  • impact: Used git push --no-verify per CLAUDE.md's allowance for unrelated pre-existing breakage. This PR touches zero Rust files, so cargo fmt --check is not actually applicable to this change set — CI's Linux runner will run it as a no-op for the changed files. Prettier on the changed TS files runs in CI on a clean Linux checkout where line endings are LF; verified locally that the new files were written with LF.

Behavior Changes

  • Intended behavior change: Search/filter and keyboard navigation become available in the installed MCP server list when at least one server is installed.
  • User-visible effect: Faster scanning at any install count >1; keyboard-only users can move through the list with Arrow keys; screen-reader users hear the live-region announce the filtered count as they type.

Parity Contract

  • Legacy behavior preserved: The filter prop defaults to ''. When omitted (every existing call site), InstalledServerList renders identically to the previous version — verified by all pre-existing tests still passing unchanged.
  • Guard/fallback/dispatch parity checks: The original zero-installed empty state still wins over the new no-match state (explicit test). The fallback to 'disconnected' for missing status entries is unchanged.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): None known.
  • Canonical PR: This one.
  • Resolution: N/A.

Summary by CodeRabbit

  • New Features

    • Local search UI to filter installed servers (matches name, identifier, description); clear button resets filter.
    • Live match-count announcements while filtering; preserves original empty-state message when no servers exist.
    • Arrow-key navigation across visible (filtered) server items with proper focus clamping.
  • Bug Fixes

    • Status indicator tooltips now use localized status text casing.
  • Tests

    • Expanded tests for filtering, keyboard navigation, status tooltip titles, and accessible messaging.
  • Localization

    • Added translations for the search UI in 16 languages.

Review Change Stack

@aashir-athar aashir-athar requested a review from a team May 25, 2026 14:06
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: faa6232c-9b00-46dc-a2c8-f29e12f85539

📥 Commits

Reviewing files that changed from the base of the PR and between 13d70bd and 2cd8d18.

📒 Files selected for processing (4)
  • app/src/components/channels/mcp/InstalledServerList.test.tsx
  • app/src/components/channels/mcp/InstalledServerList.tsx
  • app/src/components/channels/mcp/McpServerSearch.test.tsx
  • app/src/components/channels/mcp/McpServerSearch.tsx

📝 Walkthrough

Walkthrough

Adds a controlled McpServerSearch input and wires it into McpServersTab; InstalledServerList gains an optional filter prop, filtering (display_name/qualified_name/description), aria-live match counts, localized status titles, and ArrowUp/ArrowDown keyboard navigation. Translation keys for the search UI are added across language chunks.

Changes

MCP Server Search and Filtering

Layer / File(s) Summary
Search Input Component and Tests
app/src/components/channels/mcp/McpServerSearch.tsx, app/src/components/channels/mcp/McpServerSearch.test.tsx
New controlled McpServerSearch with role="search", i18n labels, type="search", conditional clear button, and tests covering accessibility, placeholder, value, onChange, and clear behavior.
Filtering Logic and Keyboard Navigation in InstalledServerList
app/src/components/channels/mcp/InstalledServerList.tsx, app/src/components/channels/mcp/InstalledServerList.test.tsx
InstalledServerList accepts an optional filter?: string, computes trimmed/case-insensitive filteredServers matching display_name, qualified_name, and description, renders an aria-live match-count while filtering, localizes status dot title, and adds ArrowUp/ArrowDown focus traversal limited to visible items. Tests validate matching, trimming, undefined descriptions, empty/no-match messaging, aria announcements, and keyboard navigation and edge cases.
Tab Integration and State Wiring
app/src/components/channels/mcp/McpServersTab.tsx
Imports McpServerSearch, adds local searchFilter state, conditionally shows the search UI when servers exist, and forwards the filter to InstalledServerList.
Internationalization Strings
app/src/lib/i18n/chunks/{ar,bn,de,en-1,es,fr,hi,id,it,ko,pt,ru,zh-CN}-1.ts, app/src/lib/i18n/en.ts
Adds mcp.installed.search.* translation keys (aria labels, placeholder, clear-filter label, {shown}/{total} match-count template, and {query} no-matches message) across language chunks and the base English map.

Sequence Diagram

sequenceDiagram
  participant User
  participant McpServerSearch
  participant McpServersTab
  participant InstalledServerList
  User->>McpServerSearch: type in search field
  McpServerSearch->>McpServersTab: onChange(filterText)
  McpServersTab->>McpServersTab: setState(searchFilter)
  McpServersTab->>InstalledServerList: pass filter prop
  InstalledServerList->>InstalledServerList: filter servers by display_name/qualified_name/description
  InstalledServerList->>User: render filtered list + match count
  User->>InstalledServerList: press ArrowUp/ArrowDown
  InstalledServerList->>User: move focus between visible servers
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • graycyrus

Poem

🐰 A little search hops into the tab,
Filters names and descriptions in a snap,
Arrow keys bound to guide each hop,
Counts announce how many match the crop.
Translations hum — now every hare can map.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(mcp): search + keyboard navigation for installed MCP servers' accurately summarizes the main changes: adding search/filter functionality and arrow-key navigation to the MCP servers list, which are the primary features introduced across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 added feature Net-new user-facing capability or product behavior. working A PR that is being worked on by the team. labels May 25, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@app/src/components/channels/mcp/InstalledServerList.tsx`:
- Around line 143-146: InstalledServerList renders a status dot using raw enum
values in title={status}; update the component (InstalledServerList) to use the
i18n hook (useT from app/src/lib/i18n/I18nContext) and supply a localized label
instead of the raw enum. Add or reuse a mapping (e.g., STATUS_LABELS or compute
with t(`status.${status}`)) to translate each status value, and pass the
translated string to the span title and any other user-facing status text while
keeping STATUS_DOT for CSS classes.
🪄 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: 49377314-0775-4bde-9897-403dfd58363e

📥 Commits

Reviewing files that changed from the base of the PR and between d997394 and 13d70bd.

📒 Files selected for processing (19)
  • app/src/components/channels/mcp/InstalledServerList.test.tsx
  • app/src/components/channels/mcp/InstalledServerList.tsx
  • app/src/components/channels/mcp/McpServerSearch.test.tsx
  • app/src/components/channels/mcp/McpServerSearch.tsx
  • app/src/components/channels/mcp/McpServersTab.tsx
  • app/src/lib/i18n/chunks/ar-1.ts
  • app/src/lib/i18n/chunks/bn-1.ts
  • app/src/lib/i18n/chunks/de-1.ts
  • app/src/lib/i18n/chunks/en-1.ts
  • app/src/lib/i18n/chunks/es-1.ts
  • app/src/lib/i18n/chunks/fr-1.ts
  • app/src/lib/i18n/chunks/hi-1.ts
  • app/src/lib/i18n/chunks/id-1.ts
  • app/src/lib/i18n/chunks/it-1.ts
  • app/src/lib/i18n/chunks/ko-1.ts
  • app/src/lib/i18n/chunks/pt-1.ts
  • app/src/lib/i18n/chunks/ru-1.ts
  • app/src/lib/i18n/chunks/zh-CN-1.ts
  • app/src/lib/i18n/en.ts

Comment thread app/src/components/channels/mcp/InstalledServerList.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior. working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant