feat(mcp): replace /settings/mcp with full-featured /mcp page (catalog + marketplace + sources) (#231)
* feat(mcp): MCP server management page (Phase 1) Implements the MCP management plan (.omc/plans/mcp-management.md) Phase 1 end-to-end on a single feature branch (PR1+PR2+PR3+PR4 collapsed): - New `/mcp` route with capability gate + BackendUnavailableState fallback. - New `/api/mcp` (GET list, POST create), `/api/mcp/test` (POST connection probe), `/api/mcp/discover` (POST tool discovery for a draft config), `/api/mcp/configure` (PUT enable/toolMode/include/exclude), and `/api/mcp/$name` (DELETE). - Strict `mcp` capability probe in gateway-capabilities: hits `GET /api/mcp` directly and validates the body parses through `normalizeMcpList` — dashboard-up-but-route-missing returns false (resolves Open Question #4). - Type split: read shapes in `src/types/mcp.ts` (client+server), write shapes in `src/types/mcp-input.ts` (server-only; secrets contained here). - Runtime normalization layer `src/server/mcp-normalize.ts` mirrors the Skills `asRecord`/`readString`/`normalizeSkill` defense — strips unknown fields, coerces enums, masks secrets via `MASK_SENTINEL`, re-applies via `maskSecretsInPlace` before every `json(...)`. - All write endpoints CSRF-checked via `requireJsonContentType`. - Capability-off responses use `createCapabilityUnavailablePayload('mcp')` with `{ servers: [], total: 0, categories }` for GET (200) and 503 for writes — feature gates fall open without throwing. - Static preset catalog (`src/screens/mcp/presets.ts`) with GitHub, Filesystem, Postgres, Slack, Linear; Catalog tab installs prefilled drafts through the same dialog flow. - Screens: `McpScreen` (Installed/Catalog/All tabs + search + category filter), `McpServerCard` (status badge + Test/Edit/Delete + enable toggle), `McpServerDialog` (HTTP/stdio + auth + Discover + Save with bearer-token clear-on-submit). - TanStack Query hooks (`useMcpServers`, `useTestMcpServer`, `useDiscoverMcpTools`, `useUpsertMcpServer`, `useConfigureMcpServer`, `useDeleteMcpServer`). Tests (vitest): - `src/server/mcp-normalize.test.ts` — 13 tests covering enum coercion, list-shape variants, malformed-entry drop, presence flags without echo, env/header masking by key hint, idempotency, test-result normalization, payload-string scanner. - `src/routes/api/-mcp.test.ts` — 8 tests covering input validation, capability fall-open shape, CSRF gate (415 on non-JSON POST, pass on JSON, pass on GET), and the **secret echo guard**: a worst-case agent that echoes a submitted bearer token in body/env/headers must never surface the original string in the workspace response. Build, lint, and the new test files are clean. Pre-existing unrelated test failures on `local` (router-route-resolution, context-usage, markdown math, slash-command-menu, chat-message-list, gateway-capabilities env-source) are unchanged by this PR. Worked with Interstellar Code * fix(mcp): strip secret fields from client-safe McpClientInput Architect review flagged that `McpClientInput` in `src/types/mcp.ts` (the file explicitly designated for client+server read shapes with no secrets) contained `bearerToken` and `oauth.clientSecret`, allowing the browser bundle to import a secret-bearing type via the dialog component. Resolves the type-split violation: - `src/types/mcp.ts`: drop `bearerToken` and `oauth` from `McpClientInput`. Now strictly the browser-safe form payload, no secret fields. - `src/screens/mcp/components/mcp-server-dialog.tsx`: hold `bearerToken` in ephemeral component-local `useState<string>` typed inline. Cleared on submit and on dialog open. No exported type carries the field. - `src/screens/mcp/hooks/use-mcp-mutations.ts`: `useUpsertMcpServer` accepts `McpClientInput & { bearerToken?: string }` inline at the call-site, again with no exported secret-bearing type. Server route `parseMcpServerInput` re-validates and forwards to the agent. The full server-side write shape (`McpServerInput` with secrets) remains in `src/types/mcp-input.ts`, server-only. Worked with Interstellar Code * fix(mcp): block client imports of server-only mcp-input types Add no-restricted-imports rule scoped to src/screens/** and src/components/** that blocks importing @/types/mcp-input. That file may carry unmasked secrets and is server-only — clients should import McpClientInput from @/types/mcp instead. Worked with Interstellar Code * feat(mcp): wire /mcp into all sidebar/nav surfaces Mirror the existing /skills registration across every nav and command surface so the MCP screen is reachable from the dashboard overflow grid, command palette, mobile hamburger drawer, mobile tab bar, slash menu, search modal quick actions, and workspace shell (active-tab tracking + mobile page title). Inspector panel gets a parallel MCP tab that lists configured servers via /api/mcp. Worked with Interstellar Code * feat(mcp): catalog tab search, category badges, and nav coverage tests Catalog tab now reuses the screen's search state to filter presets by name/description, surfaces an empty-state when no presets match, and renders each preset as a card with an Official Presets category badge styled to match the skills-screen design vocabulary. Tests: - src/components/-mcp-nav.test.tsx: each modified nav file references the /mcp route (or registers an mcp tab id for inspector-panel) - src/screens/mcp/-presets.test.ts: filtering MCP_PRESETS by query narrows results by name and description, returns full catalog for empty queries, and returns nothing for unknown queries Worked with Interstellar Code * feat(mcp): add MCP entry to chat-sidebar Knowledge group The primary visible left rail (`chat-sidebar.tsx`) was missed by the prior nav-coverage commit. Slot MCP between Skills and Profiles in `knowledgeItems`, mirroring the McpServerIcon used elsewhere. Worked with Interstellar Code * feat(mcp): Phase 3 — live tool refresh, OAuth reauth, per-server SSE logs - `useMcpServers`: enable refetchOnWindowFocus for live state. - `McpServerCard`: per-card Refresh button (re-runs Test, updates discoveredToolsCount), Reauth button when authType === 'oauth' (uses new useMcpOAuth hook), Logs button (opens McpLogsDrawer). - `use-mcp-oauth.ts`: opens auth URL in new tab, polls /api/mcp/test every 2s until status === 'connected' or 60s timeout. Returns mutation-style { start, isPending, isError, error, data }. - `mcp-logs-drawer.tsx`: fixed-right slide-in drawer subscribing via EventSource to /api/mcp/<name>/logs. Newest-first, max 500 lines, auto-scroll, tear down on close (no zombie EventSource). - `routes/api/mcp/$name.logs.ts`: SSE proxy with auth + capability gates. Capability-off → 503. Pattern follows chat-events.ts. - Tests: 3 new for logs route (input validation, capability-off, auth gate); smoke test for useMcpOAuth shape. Total tests: 24 passing (13 normalize + 8 mcp + 3 logs). Build clean. Worked with Interstellar Code * feat(mcp): localhost-only config-fallback transport (Phase 1.5) Adds an `mcpFallback` capability that lets the workspace perform CRUD on `config.mcp_servers` via the existing dashboard `/api/config` route when the agent does not yet expose the new `/api/mcp*` runtime endpoints. Gated to loopback-only deployments by `isLocalhostDeployment()` (both URLs loopback AND HOST unset/loopback). Test/Discover/Logs return a structured "not yet available" payload in fallback mode; the MCP screen renders an amber banner so the limitation is visible. Worked with Interstellar Code * feat(mcp): full catalog + marketplace + sources manager (Phase 2-3.2) Workspace-only end-to-end MCP catalog + marketplace replacing the static presets.ts and the upstream /settings/mcp surfaces. Phase 2 — File-backed catalog: - assets/mcp-presets.seed.json + ~/.hermes/mcp-presets.json (atomic bootstrap via tmp+linkSync, mtime+ino+ctime+size cache, malformed-file preservation, schema validation: id regex, transport-specific fields, env key regex, https URLs, category allowlist, duplicate-id rejection, unknown-field warnings) - src/server/mcp-input-validate.ts: shared parseMcpServerInput returning per-field {path, message} errors; promoted from inline definition - src/routes/api/mcp/presets.ts GET handler Phase 3.0 — Federated marketplace: - src/server/mcp-hub/{cache,trust,index,types}.ts + sources/{mcp-get, local-file}.ts: Smithery registry adapter (replaces speculative registry.mcp.run NXDOMAIN), ETag/If-Modified-Since with 304 reuse, rate-limit handling, parallel Promise.allSettled across sources with 8s per-source timeout, dedupe by source+id+name, fallback to local-file when remote degraded - Trust hardening: shell metachar reject, transport allowlist, env-key regex, control-char + absolute-path attack defenses, inline-exec flag detection (-c, -lc, -e for sh/bash/python/node/perl/ruby) - src/screens/mcp/components/install-confirmation-dialog.tsx: 2-click commit with full template preview (command/args/env masked) and AbortController on dismiss - Disk persistence for tool-discovery cache (mcp-tools-cache.ts) + hermes-mcp CLI bridge (mcp-cli-bridge.ts) for live test/tool enumeration in fallback mode Phase 3.2 — User-configurable sources: - ~/.hermes/mcp-hub-sources.json schema (built-ins always present, protected from mutation; user can add HTTPS-only generic-json sources with trust+format) - src/routes/api/mcp/hub-sources{,.$id}.ts CRUD with per-process mutex (read-modify-write race protection) - generic-json adapter: SSRF guard (private/loopback/link-local/IPv6 ULA all rejected after DNS resolution, redirects disabled), 5MB response-size cap (streaming read), trust hard-cap at 'community' for user-source entries, source field 'user:<id>' for dedupe - src/screens/mcp/components/sources-manager-dialog.tsx UI Polish: - Placeholder detection at install confirmation (inline fill form blocks commit until /path/to/, <your-...>, empty *_TOKEN/_KEY/etc resolved) - Test result UX hints when stdio Connection closed + placeholder args or http fetch failed + placeholder url - Env-ref preserved in normalize (${VAR_NAME} no longer masked) + Edit dialog diagnostic UI: Skills-pattern parity for /mcp screen (Tabs + Marketplace tab, Switch primitive, Button primitives, DialogRoot/Content, primary-* Tailwind classes matching skills-screen.tsx). Single-row toolbar (tabs + search + filter). Removed All + Catalog tabs, kept Installed + Marketplace. Backend: - gateway-capabilities probeMcp uses authenticated dashboardFetch (Codex MAJOR fix); probeMcpConfigKey + isLocalhostDeployment for mcpFallback capability - routes/mcp.tsx route gate accepts mcp || mcpFallback - mcp-normalize.ts headers.Authorization + env *_TOKEN/_KEY/_SECRET /_AUTH/_APIKEY auth detection upgrades authType to 'bearer' Removed (replaced by /mcp): - src/screens/settings/mcp-settings-screen.tsx (759 LOC) - src/routes/settings/mcp.tsx - src/routes/api/mcp/{servers,reload}.ts (orphaned endpoints; reload posted to gateway 404s) - src/screens/mcp/presets.ts (static array, replaced by file-backed) - settings-sidebar MCP nav entries (replaced by main /mcp route) Tests: 263+ passing across 19+ MCP suites — input-validate, presets- store, hub-cache/trust/unified-search, sources/{mcp-get,local-file, generic-json}, hub-sources-store, mcp-tools-cache, ssrf-guard, marketplace-install-confirmation, marketplace-placeholder-detection, hub-search/-presets/-hub-sources route tests. Pre-existing 2 gateway-capabilities env-resolution failures unrelated. Reviewers: Codex critic 4 passes (Phase 2 REJECTED → 8 fixes applied, Phase 3.0 APPROVED-WITH-CHANGES → 4 fixes, Phase 3.2 REJECTED → 6 fixes including SSRF guard + response-size cap + concurrent-CRUD mutex + trust cap). Architect approved final pass. Worked with Interstellar Code
This commit is contained in:
committed by
GitHub
parent
b72b47544d
commit
c021ef5fcc
@@ -419,6 +419,18 @@ const config = defineConfig(({ mode, command }) => {
|
||||
'**/skills-bundle/**',
|
||||
'**/.{idea,git,cache,output,temp}/**',
|
||||
],
|
||||
// Force vitest to run React through its own transform pipeline so ESM
|
||||
// `import` and CJS `require('react')` share a single module instance.
|
||||
// Without this, react-dom sets the dispatcher on its CJS React copy while
|
||||
// components call hooks on the ESM React copy → null dispatcher → crash.
|
||||
deps: {
|
||||
inline: [
|
||||
'react',
|
||||
'react-dom',
|
||||
'@testing-library/react',
|
||||
'@testing-library/dom',
|
||||
],
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// Note: Do NOT set 'process.env': {} here — TanStack Start uses environment-based
|
||||
|
||||
Reference in New Issue
Block a user