feat(mcp): Sharable Inventory — portable secret-free manifest with export, parse, validate, and explicit per-entry import#2655
Conversation
…port, parse, validate, and explicit per-entry import
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a sharable MCP inventory: a secret-free manifest format with strict validation, export/import UI in a modal (copy/download, paste/upload, preview, per-entry install), tests, servers-tab integration, and translations. ChangesMCP Sharable Inventory
Sequence Diagram(s)sequenceDiagram
participant User
participant Panel as McpInventoryPanel
participant Export as McpInventoryExportTab
participant Import as McpInventoryImportTab
participant Parent
User->>Panel: Click "Open Sharable Inventory"
Panel->>Export: Render export tab
Export->>Export: Build manifest from servers
Export->>Export: Display JSON in pre
User->>Export: Click Copy
Export->>Export: navigator.clipboard.writeText(json)
User->>Panel: Switch to Import tab
Panel->>Import: Render import tab
User->>Import: Paste JSON + Click Preview
Import->>Import: parseManifest(input)
Import->>Import: classifyImport(manifest, installed)
User->>Import: Click Install for new server
Import->>Panel: onInstallServer(qualifiedName, prefillEnv)
Panel->>Parent: onInstallServer(qualifiedName, prefillEnv)
Parent->>Parent: Open install dialog
Panel->>Parent: onClose()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ 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
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/McpInventoryImportTab.tsx`:
- Around line 70-76: When upload validation or read fails (the file size check
branch and the FileReader error/read-failure branch around lines 91-93), clear
any stale preview/manifest state so the old preview isn't actionable: call the
preview/manifest state setters to reset them (e.g., setManifest(null) and
setPreview(null) or the equivalents used in this component) immediately before
returning or setting setFileError; do this in both the size-rejection block that
currently calls setFileError and in the FileReader error/abort handler so no
previous manifest/preview lingers after a failed upload.
In `@app/src/components/channels/mcp/McpInventoryManifest.ts`:
- Around line 111-166: parseManifest currently returns hard-coded English error
messages; change it to return stable i18n error codes (e.g. errorCode:
'manifest.empty', 'manifest.invalid_json', 'manifest.root_not_object',
'manifest.unsupported_schema', 'manifest.missing_exported_at',
'manifest.missing_exported_by', 'manifest.invalid_servers',
'manifest.server_not_object', 'manifest.server_missing_qualified_name',
'manifest.server_missing_display_name', 'manifest.server_env_keys_not_array',
'manifest.server_contains_env') alongside any machine details (e.g. JSON parse
detail) in ParseResult, then update McpInventoryImportTab to map that errorCode
to translated text using useT()/t(...) instead of rendering the raw string; also
add corresponding keys to app/src/lib/i18n/en.ts. Reference parseManifest,
ParseResult, and McpInventoryImportTab when locating code to modify.
- Around line 143-177: During parseManifest's validation loop over
parsed.servers (the for loop using variable raw) add a duplicate-check: track
seen qualified_name values (e.g., a Set) and if raw.qualified_name has already
been seen return { ok: false, error: `servers[${i}].qualified_name
"${raw.qualified_name}" is duplicated; manifests must not contain duplicate
qualified_name entries.` }; do this check after validating raw.qualified_name is
a non-empty string and before pushing the entry into servers so duplicates are
rejected early.
🪄 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: 16199ee5-2017-47b5-973c-6e000f8e28bd
📒 Files selected for processing (21)
app/src/components/channels/mcp/McpInventoryExportTab.tsxapp/src/components/channels/mcp/McpInventoryImportTab.tsxapp/src/components/channels/mcp/McpInventoryManifest.test.tsapp/src/components/channels/mcp/McpInventoryManifest.tsapp/src/components/channels/mcp/McpInventoryPanel.test.tsxapp/src/components/channels/mcp/McpInventoryPanel.tsxapp/src/components/channels/mcp/McpServersTab.tsxapp/src/lib/i18n/chunks/ar-1.tsapp/src/lib/i18n/chunks/bn-1.tsapp/src/lib/i18n/chunks/de-1.tsapp/src/lib/i18n/chunks/en-1.tsapp/src/lib/i18n/chunks/es-1.tsapp/src/lib/i18n/chunks/fr-1.tsapp/src/lib/i18n/chunks/hi-1.tsapp/src/lib/i18n/chunks/id-1.tsapp/src/lib/i18n/chunks/it-1.tsapp/src/lib/i18n/chunks/ko-1.tsapp/src/lib/i18n/chunks/pt-1.tsapp/src/lib/i18n/chunks/ru-1.tsapp/src/lib/i18n/chunks/zh-CN-1.tsapp/src/lib/i18n/en.ts
…ory)
Three review findings, all addressed:
1. McpInventoryImportTab.tsx: Clear stale manifest/preview state in both upload failure branches (size-reject + FileReader error). Previously a failed re-upload could leave the previous manifest's preview rows still rendering and actionable.
2. McpInventoryManifest.ts: Replace hard-coded English error strings with stable ParseErrorCode discriminated union (13 codes). UI now maps codes to translated text via t('mcp.inventory.parseError.<code>'). 13 new i18n keys added to en.ts and mirrored across all 13 locale chunks.
3. McpInventoryManifest.ts: Add duplicate qualified_name detection via Set<string> in the validation loop. Rejects malformed/malicious manifests that list the same server twice (potentially with diverging env_keys/config) before producing duplicate import rows.
Validation: 129/129 MCP tests passing, tsc clean, eslint clean, prettier clean, i18n:check 0 missing/extra/drifted across all 13 locales.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
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/McpInventoryManifest.ts`:
- Around line 166-168: The validation currently accepts parsed.exported_by when
it's a string but allows empty or whitespace-only values; update the check in
McpInventoryManifest (the parsing/validation block that checks
parsed.exported_by) to reject strings that are empty after trimming by verifying
typeof parsed.exported_by === 'string' && parsed.exported_by.trim().length > 0
and return { ok: false, errorCode: 'missingExportedBy' } when that condition
fails so blank or whitespace exporter metadata is treated as missing.
🪄 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: c89c63ec-33b2-4c01-83bb-ac41e928c566
📒 Files selected for processing (17)
app/src/components/channels/mcp/McpInventoryImportTab.tsxapp/src/components/channels/mcp/McpInventoryManifest.test.tsapp/src/components/channels/mcp/McpInventoryManifest.tsapp/src/lib/i18n/chunks/ar-1.tsapp/src/lib/i18n/chunks/bn-1.tsapp/src/lib/i18n/chunks/de-1.tsapp/src/lib/i18n/chunks/en-1.tsapp/src/lib/i18n/chunks/es-1.tsapp/src/lib/i18n/chunks/fr-1.tsapp/src/lib/i18n/chunks/hi-1.tsapp/src/lib/i18n/chunks/id-1.tsapp/src/lib/i18n/chunks/it-1.tsapp/src/lib/i18n/chunks/ko-1.tsapp/src/lib/i18n/chunks/pt-1.tsapp/src/lib/i18n/chunks/ru-1.tsapp/src/lib/i18n/chunks/zh-CN-1.tsapp/src/lib/i18n/en.ts
✅ Files skipped from review due to trivial changes (7)
- app/src/lib/i18n/chunks/ru-1.ts
- app/src/lib/i18n/chunks/de-1.ts
- app/src/lib/i18n/chunks/zh-CN-1.ts
- app/src/lib/i18n/chunks/pt-1.ts
- app/src/lib/i18n/chunks/en-1.ts
- app/src/lib/i18n/en.ts
- app/src/lib/i18n/chunks/ar-1.ts
Coderabbitai inline review: parseManifest accepted exported_by when it was a string but allowed empty or whitespace-only values. Now requires exported_by.trim().length > 0; an empty string would render as 'Exported from ' in the preview, which carries no information and is better surfaced as the missingExportedBy error code. Adds a regression test covering three blank forms (empty, spaces, tab+newline). Validation: 130/130 MCP tests passing, tsc clean, eslint clean, prettier clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…coverage gate Coverage Gate CI failed at 77% on PR tinyhumansai#2655 — diff-cover flagged the file-upload branch of McpInventoryImportTab (handleFileChange's FileReader onload/onerror + 1 MB size guard) and the Blob/URL/anchor flow of McpInventoryExportTab's handleDownload as untested. Adds 4 deterministic tests via FileReader stubs and a URL.createObjectURL/revokeObjectURL/anchor.click spy harness: (1) Download produces an application/json Blob, clicks the hidden link, and revokes the URL on the deferred setTimeout. (2) Valid JSON file upload populates the textarea and renders the preview row. (3) >1 MB file upload is refused with a file-error alert and clears any stale preview. (4) FileReader.onerror surfaces 'Could not read file' and clears any stale preview. Validation: 134/134 MCP tests passing (was 130), tsc clean, eslint clean, prettier clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eak Tauri build PR tinyhumansai#2655 Build Tauri App was failing with 'lightningcss minify: Unexpected token Semicolon' on a generated CSS rule '.\[-\:T\]{ -: T; }'. Root cause: Tailwind v4's content scanner greps every JS/TS source string for arbitrary-value class shapes ([prop:value]) and mis-identified the regex character class in suggestedFilename as a Tailwind class, then emitted an invalid CSS rule that broke lightningcss. Fix: build the character class from a String.fromCharCode array + new RegExp instead of a regex literal so the trigger pattern is not present in the source text the scanner sees. Pure refactor — suggestedFilename's behaviour is identical; the existing test still passes. Validation: 33/33 manifest tests passing, full local 'pnpm exec vite build' succeeds (built in 2m 3s), tsc clean, eslint clean, prettier clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
InstallDialogso secret-value collection is never re-implemented.package.jsonfor MCP servers — so a team's curated setup or a personal backup can travel between machines without leaking credentials.McpServersTab.tsx. 2,060 total insertions across 21 files, including 39 new i18n keys mirrored across all 13 locale chunks.Why this is genuinely new
Today every MCP client — Claude Desktop, Cursor, OpenHuman, etc. — keeps installs strictly per-machine and per-user. Teams that want everyone to share an MCP toolkit pass around env files, Slack screenshots, or copy-paste. There is no portable, versioned, secret-free artifact for "this is the set of MCP servers I trust." This PR creates one.
The privacy-by-design contract is the load-bearing part:
InstalledServer)server_id(UUID)installed_at,last_connected_atcommand,args,command_kindenvvaluesenv_keys(names)qualified_name,display_name,description,configThe parser refuses any manifest that smuggles back an
envvalue map — see the SECURITY-tagged test (McpInventoryManifest.test.ts) that pins this invariant from the import side too.Solution
New files (all under
app/src/components/channels/mcp/)McpInventoryManifest.ts(223 LOC) — pure-data layer. Types (McpInventoryEntry,McpInventoryManifest,ParseResult,ImportEntryStatus). Functions:buildManifest,serializeManifest,parseManifest,classifyImport,suggestedFilename. All pure, no React, no DOM. Schema sentinelopenhuman.mcp-inventory.v1so manifests are self-describing and version-checkable.McpInventoryManifest.test.ts(317 LOC, 28 tests) — pins the redaction contract (no per-machine IDs, no spawn shape, no env values), the deterministic-output contract (sorted servers + sorted env_keys so re-exports are byte-stable in source control), and the parser's positive + negative paths, including a SECURITY-tagged test that the parser refuses any input containing anenvvalue map.McpInventoryPanel.tsx(163 LOC) — tabbed modal.role="dialog" aria-modal="true" aria-labelledby, Esc closes, backdrop mousedown closes, click on dialog card does not. WAI-ARIAtablist/tab/tabpanelwith properaria-selected/aria-controls/ rovingtabIndex.McpInventoryExportTab.tsx(106 LOC) — renders the manifest as formatted JSON in a<pre>with Copy (clipboard, with transient "Copied" state, silently no-ops on platforms withoutnavigator.clipboard) and Download (Blob URL with a slug-style timestamped filename). Loud privacy banner above the JSON so the user sees the redaction contract before sharing the artifact.McpInventoryImportTab.tsx(282 LOC) — three-step flow:.jsonfile (1 MB cap as a defence against accidental upload of a giant blob).classifyImportcross-references each entry against the importer's installed servers byqualified_name; surfaces arole="status" aria-live="polite"counts summary ("N servers — X new, Y already installed") plus a per-entry row withNew/Already installedpills.newentry has its own "Install" button that hands thequalified_name+ empty env prefill (built fromenv_keys) to the parent's existing install-dialog flow. The Import panel closes so the provenInstallDialoghas the right pane to work with.Why no auto-bulk-install: An MCP server is a piece of trust the user is granting to their agent. A one-click-install-many action would invite supply-chain attacks via malicious manifests. The per-entry "Install" preserves friction at exactly the right step. Documented in the file-top doc comment.
Wiring in
McpServersTab.tsxinventoryOpenstate slot; modal renders conditionally.onInstallServercallback bridges the panel to the existingsetRightPane({ mode: 'install', qualifiedName, prefillEnv })flow.i18n
39 new keys under
mcp.inventory.*added toapp/src/lib/i18n/en.tsAND toapp/src/lib/i18n/chunks/en-1.ts. All 12 non-English locale chunks (ar-1.ts…zh-CN-1.ts) get the same keys with the English value as the untranslated placeholder, per the project's parity pattern enforced byscripts/i18n-coverage.ts.Verified locally:
Submission Checklist
envvalue smuggling; modal a11y attributes; Esc / button / backdrop close; tab navigation; Export-tab empty state, JSON render, count, privacy banner, clipboard write; Import-tab disabled Preview button until input present, parse-error alerting, unknown-schema rejection,env-smuggling rejection, preview classification (new vs already_installed), per-entry Install hands the right args to the callback, Install also closes the panel, Clear resets state, live-clearing stale errors on typing, empty-manifest case, env_keys rendering.## Related— N/A.mcpClientsApi.installedList(already polled by the parent tab) and the parent's existing install-dialog flow.Closes #NNN— no specific issue; organic UX + portability improvement.Impact
McpServersTabis desktop-only viaChannelConfigPanel. No iOS / web impact.envvalue map. One test pins this from the import side.McpServersTab. Every existing flow (connect / disconnect / uninstall / browse / install / detail) is byte-identical. All pre-existing MCP tests pass unchanged.role="dialog" aria-modal aria-labelledby; WAI-ARIA tablist witharia-selected/aria-controls/ rovingtabIndex;role="status" aria-live="polite"for preview counts;role="note"for the two banner regions;role="alert"for parse and file errors; all interactive buttons havearia-labels; icons arearia-hidden="true".Related
mcp.inventory.*keys across the 12 non-English locales.AI Authored PR Metadata
Linear Issue
Commit & Branch
feat/mcp-inventory-export-importValidation Run
All four key gates passed locally:
pnpm --filter openhuman-app compile— clean (tsc --noEmit, no output = success).pnpm --filter openhuman-app lint— 0 errors, 0 warnings attributable to PR files. (The repo currently shows 63 unrelated warnings on the same pre-existing files as every other recent PR.)pnpm vitest run src/components/channels/mcp/— 128/128 passing.pnpm i18n:check— exit 0, every locale at1:1245/1245parity, 0 missing keys, 0 extra keys, 0 drift.pnpm --filter openhuman-appprettier on all 7 PR-modified files — clean.Validation Blocked
pnpm --filter openhuman-app format:check— this chainscargo fmt --check; no Rust toolchain on the dev machine. This PR touches zero Rust files, socargo fmt --checkis a no-op for the changed files. Usedgit push --no-verifyper CLAUDE.md's allowance for unrelated pre-existing breakage; CI on Linux is the authoritative gate.Behavior Changes
Parity Contract
inventoryOpenstate). Closed-state DOM is byte-identical to before. All pre-existing MCP tests pass unchanged. The InstallDialog surface is reused as-is; this PR never re-implements env-value collection or any other security-sensitive install logic.parseManifestis a discriminated-union return — never throws. Clipboard write degrades silently whennavigator.clipboardis unavailable. File upload validates size before reading.Duplicate / Superseded PR Handling
Summary by CodeRabbit
New Features
Tests