Skip to content

Commit 6a830df

Browse files
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
1 parent 7679962 commit 6a830df

64 files changed

Lines changed: 9380 additions & 1646 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

assets/mcp-presets.seed.json

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
{
2+
"version": 1,
3+
"presets": [
4+
{
5+
"id": "github",
6+
"name": "GitHub",
7+
"description": "Read repos, issues, PRs via the GitHub MCP server.",
8+
"category": "Official Presets",
9+
"homepage": "https://github.com/modelcontextprotocol/servers",
10+
"tags": ["dev", "git"],
11+
"template": {
12+
"name": "github",
13+
"transportType": "stdio",
14+
"command": "npx",
15+
"args": ["-y", "@modelcontextprotocol/server-everything"],
16+
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "" },
17+
"authType": "none",
18+
"toolMode": "all"
19+
}
20+
},
21+
{
22+
"id": "filesystem",
23+
"name": "Filesystem",
24+
"description": "Read and write files within an allow-listed root.",
25+
"category": "Official Presets",
26+
"homepage": "https://github.com/modelcontextprotocol/servers",
27+
"tags": ["files"],
28+
"template": {
29+
"name": "filesystem",
30+
"transportType": "stdio",
31+
"command": "npx",
32+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/root"],
33+
"authType": "none",
34+
"toolMode": "all"
35+
}
36+
},
37+
{
38+
"id": "postgres",
39+
"name": "Postgres",
40+
"description": "Run read-only SQL against a Postgres database.",
41+
"category": "Official Presets",
42+
"homepage": "https://github.com/modelcontextprotocol/servers",
43+
"tags": ["db", "sql"],
44+
"template": {
45+
"name": "postgres",
46+
"transportType": "stdio",
47+
"command": "npx",
48+
"args": [
49+
"-y",
50+
"@modelcontextprotocol/server-postgres",
51+
"postgresql://user:pass@host:5432/db"
52+
],
53+
"authType": "none",
54+
"toolMode": "all"
55+
}
56+
},
57+
{
58+
"id": "slack",
59+
"name": "Slack",
60+
"description": "Read and post messages via the Slack MCP server.",
61+
"category": "Communication",
62+
"homepage": "https://github.com/modelcontextprotocol/servers",
63+
"tags": ["chat"],
64+
"template": {
65+
"name": "slack",
66+
"transportType": "stdio",
67+
"command": "npx",
68+
"args": ["-y", "@modelcontextprotocol/server-slack"],
69+
"env": { "SLACK_BOT_TOKEN": "", "SLACK_TEAM_ID": "" },
70+
"authType": "none",
71+
"toolMode": "all"
72+
}
73+
},
74+
{
75+
"id": "linear",
76+
"name": "Linear",
77+
"description": "Query Linear issues and projects.",
78+
"category": "Productivity",
79+
"homepage": "https://linear.app",
80+
"tags": ["issues"],
81+
"template": {
82+
"name": "linear",
83+
"transportType": "http",
84+
"url": "https://mcp.linear.app/mcp",
85+
"authType": "oauth",
86+
"toolMode": "all"
87+
}
88+
},
89+
{
90+
"id": "memory",
91+
"name": "Memory",
92+
"description": "Persistent knowledge-graph memory for agents.",
93+
"category": "Official Presets",
94+
"homepage": "https://github.com/modelcontextprotocol/servers",
95+
"tags": ["memory"],
96+
"template": {
97+
"name": "memory",
98+
"transportType": "stdio",
99+
"command": "npx",
100+
"args": ["-y", "@modelcontextprotocol/server-memory"],
101+
"authType": "none",
102+
"toolMode": "all"
103+
}
104+
},
105+
{
106+
"id": "fetch",
107+
"name": "Fetch",
108+
"description": "Fetch arbitrary HTTP content for the agent to read.",
109+
"category": "Official Presets",
110+
"homepage": "https://github.com/modelcontextprotocol/servers",
111+
"tags": ["web"],
112+
"template": {
113+
"name": "fetch",
114+
"transportType": "stdio",
115+
"command": "npx",
116+
"args": ["-y", "@modelcontextprotocol/server-fetch"],
117+
"authType": "none",
118+
"toolMode": "all"
119+
}
120+
}
121+
]
122+
}

src/components/settings/settings-sidebar.tsx

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export type SettingsNavId =
1111
| 'appearance'
1212
| 'chat'
1313
| 'notifications'
14-
| 'mcp'
1514
| 'language'
1615

1716
type NavItem = { id: SettingsNavId; label: string }
@@ -26,7 +25,6 @@ export const SETTINGS_NAV_ITEMS: Array<NavItem> = [
2625
{ id: 'appearance', label: 'Appearance' },
2726
{ id: 'chat', label: 'Chat' },
2827
{ id: 'notifications', label: 'Notifications' },
29-
{ id: 'mcp', label: 'MCP Servers' },
3028
{ id: 'language', label: 'Language' },
3129
]
3230

@@ -55,13 +53,6 @@ function renderItem({
5553
{item.label}
5654
</>
5755
)
58-
if (item.id === 'mcp') {
59-
return (
60-
<Link key={item.id} to="/settings/mcp" className={className}>
61-
{content}
62-
</Link>
63-
)
64-
}
6556
return (
6657
<Link
6758
key={item.id}
@@ -121,13 +112,6 @@ export function SettingsMobilePills({ activeId }: { activeId: SettingsNavId }) {
121112
'shrink-0 rounded-full px-3 py-1.5 text-xs font-medium transition-colors',
122113
isActive ? activeClass : inactiveClass,
123114
)
124-
if (item.id === 'mcp') {
125-
return (
126-
<Link key={item.id} to="/settings/mcp" className={className}>
127-
{item.label}
128-
</Link>
129-
)
130-
}
131115
return (
132116
<Link
133117
key={item.id}

src/lib/feature-gates.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type EnhancedFeature =
99
| 'config'
1010
| 'jobs'
1111
| 'mcp'
12+
| 'mcpFallback'
1213

1314
const FEATURE_LABELS: Record<EnhancedFeature, string> = {
1415
sessions: 'Sessions',
@@ -17,6 +18,7 @@ const FEATURE_LABELS: Record<EnhancedFeature, string> = {
1718
config: 'Configuration',
1819
jobs: 'Jobs',
1920
mcp: 'MCP Servers',
21+
mcpFallback: 'MCP Servers (config fallback)',
2022
}
2123

2224
function normalizeFeature(
@@ -29,9 +31,10 @@ function normalizeFeature(
2931
normalized === 'memory' ||
3032
normalized === 'config' ||
3133
normalized === 'jobs' ||
32-
normalized === 'mcp'
34+
normalized === 'mcp' ||
35+
normalized === 'mcpfallback'
3336
) {
34-
return normalized
37+
return normalized === 'mcpfallback' ? 'mcpFallback' : normalized
3538
}
3639

3740
return null

0 commit comments

Comments
 (0)