@viren/claude-code-dashboard 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,7 +43,12 @@ import {
43
43
  computeDashboardDiff,
44
44
  } from "./src/analysis.mjs";
45
45
  import { getFreshness } from "./src/freshness.mjs";
46
- import { parseUserMcpConfig, parseProjectMcpConfig, scanHistoricalMcpServers } from "./src/mcp.mjs";
46
+ import {
47
+ parseUserMcpConfig,
48
+ parseProjectMcpConfig,
49
+ scanHistoricalMcpServers,
50
+ fetchRegistryServers,
51
+ } from "./src/mcp.mjs";
47
52
  import { handleInit } from "./src/templates.mjs";
48
53
  import { generateCatalogHtml } from "./src/render.mjs";
49
54
  import { generateDashboardHtml } from "./src/assembler.mjs";
@@ -87,7 +92,7 @@ if (cliArgs.demo) {
87
92
 
88
93
  // ── Collect Raw Inputs ────────────────────────────────────────────────────────
89
94
 
90
- function collectRawInputs() {
95
+ async function collectRawInputs() {
91
96
  const scanRoots = getScanRoots();
92
97
  const allRepoPaths = findGitRepos(scanRoots, MAX_DEPTH);
93
98
 
@@ -217,6 +222,9 @@ function collectRawInputs() {
217
222
  entry.projects = new Set([...entry.projects].map((p) => shortPath(p)));
218
223
  }
219
224
 
225
+ // MCP Registry servers
226
+ const registryServers = cliArgs.offline ? [] : await fetchRegistryServers();
227
+
220
228
  // Usage data — session meta files
221
229
  const SESSION_META_LIMIT = 1000;
222
230
  const sessionMetaDir = join(CLAUDE_DIR, "usage-data", "session-meta");
@@ -309,6 +317,7 @@ function collectRawInputs() {
309
317
  projectMcpByRepo,
310
318
  disabledMcpByRepo,
311
319
  historicalMcpMap,
320
+ registryServers,
312
321
  sessionMetaFiles,
313
322
  ccusageData,
314
323
  statsCache,
@@ -321,7 +330,7 @@ function collectRawInputs() {
321
330
 
322
331
  // ── Build Dashboard Data ─────────────────────────────────────────────────────
323
332
 
324
- const rawInputs = collectRawInputs();
333
+ const rawInputs = await collectRawInputs();
325
334
  const data = buildDashboardData(rawInputs);
326
335
 
327
336
  // ── Lint Subcommand ──────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viren/claude-code-dashboard",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "A visual dashboard for your Claude Code configuration across all repos",
5
5
  "type": "module",
6
6
  "bin": {
package/src/assembler.mjs CHANGED
@@ -47,6 +47,9 @@ export function generateDashboardHtml(data) {
47
47
  mcpSummary,
48
48
  mcpPromotions,
49
49
  formerMcpServers,
50
+ recommendedMcpServers,
51
+ availableMcpServers,
52
+ registryTotal,
50
53
  consolidationGroups,
51
54
  usageAnalytics,
52
55
  ccusageData,
@@ -90,7 +93,7 @@ export function generateDashboardHtml(data) {
90
93
  const tabOverview = `${overviewCommands}\n ${insightsHtml}\n ${chainsHtml}\n ${consolidationHtml}`;
91
94
 
92
95
  // Skills & MCP tab
93
- const tabSkillsMcp = `${renderSkillsCard(globalSkills)}\n ${renderMcpCard(mcpSummary, mcpPromotions, formerMcpServers)}`;
96
+ const tabSkillsMcp = `${renderSkillsCard(globalSkills)}\n ${renderMcpCard(mcpSummary, mcpPromotions, formerMcpServers, recommendedMcpServers, availableMcpServers, registryTotal)}`;
94
97
 
95
98
  // Analytics tab
96
99
  const insightsReportHtml = renderInsightsReportCard(insightsReport);
package/src/cli.mjs CHANGED
@@ -15,6 +15,7 @@ export function parseArgs(argv) {
15
15
  anonymize: false,
16
16
  demo: false,
17
17
  completions: false,
18
+ offline: false,
18
19
  };
19
20
  let i = 2; // skip node + script
20
21
  if (argv[2] === "init") {
@@ -46,6 +47,7 @@ Options:
46
47
  --watch Regenerate on file changes
47
48
  --diff Show changes since last generation
48
49
  --anonymize Anonymize all data for shareable export
50
+ --offline Skip network fetches (registry, etc.)
49
51
  --demo Generate dashboard with sample data (no scanning)
50
52
  --completions Output shell completion script for bash/zsh
51
53
  --version, -v Show version
@@ -113,6 +115,9 @@ Config file: ~/.claude/dashboard.conf
113
115
  case "--anonymize":
114
116
  args.anonymize = true;
115
117
  break;
118
+ case "--offline":
119
+ args.offline = true;
120
+ break;
116
121
  case "--demo":
117
122
  args.demo = true;
118
123
  break;
@@ -133,11 +138,11 @@ export function generateCompletions() {
133
138
  # eval "$(claude-code-dashboard --completions)"
134
139
  if [ -n "$ZSH_VERSION" ]; then
135
140
  _claude_code_dashboard() {
136
- local -a opts; opts=(init lint --output --open --no-open --json --catalog --quiet --watch --diff --anonymize --demo --completions --help --version)
141
+ local -a opts; opts=(init lint --output --open --no-open --json --catalog --quiet --watch --diff --anonymize --offline --demo --completions --help --version)
137
142
  if (( CURRENT == 2 )); then _describe 'option' opts; fi
138
143
  }; compdef _claude_code_dashboard claude-code-dashboard
139
144
  elif [ -n "$BASH_VERSION" ]; then
140
- _claude_code_dashboard() { COMPREPLY=( $(compgen -W "init lint --output --open --no-open --json --catalog --quiet --watch --diff --anonymize --demo --completions --help --version" -- "\${COMP_WORDS[COMP_CWORD]}") ); }
145
+ _claude_code_dashboard() { COMPREPLY=( $(compgen -W "init lint --output --open --no-open --json --catalog --quiet --watch --diff --anonymize --offline --demo --completions --help --version" -- "\${COMP_WORDS[COMP_CWORD]}") ); }
141
146
  complete -F _claude_code_dashboard claude-code-dashboard
142
147
  fi`);
143
148
  process.exit(0);
package/src/constants.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { join } from "path";
2
2
  import { homedir } from "os";
3
3
 
4
- export const VERSION = "0.0.7";
4
+ export const VERSION = "0.0.8";
5
5
  export const REPO_URL = "https://github.com/VirenMohindra/claude-code-dashboard";
6
6
 
7
7
  export const HOME = homedir();
@@ -98,6 +98,44 @@ export const SKILL_CATEGORIES = {
98
98
  "project-specific": ["storybook", "react-native"],
99
99
  };
100
100
 
101
+ export const MCP_REGISTRY_URL =
102
+ "https://api.anthropic.com/mcp-registry/v0/servers?visibility=commercial&limit=100";
103
+ export const MCP_REGISTRY_TTL_MS = 24 * 60 * 60 * 1000; // 24h
104
+
105
+ /**
106
+ * Maps detected tech stacks and description keywords to relevant MCP server slugs.
107
+ * Used by the pipeline to compute MCP recommendations.
108
+ *
109
+ * Keys: tech stack names (matching STACK_FILES values) or lowercase keywords
110
+ * found in repo descriptions.
111
+ * Values: array of MCP server slugs from the Anthropic registry.
112
+ */
113
+ export const MCP_STACK_HINTS = {
114
+ // Stack-based (keys match STACK_FILES values)
115
+ next: ["vercel", "figma"],
116
+ react: ["figma"],
117
+ python: ["sentry"],
118
+ go: ["sentry"],
119
+ rust: ["sentry"],
120
+ java: ["sentry"],
121
+ expo: ["figma"],
122
+
123
+ // Keyword-based (matched against lowercased repo descriptions)
124
+ supabase: ["supabase"],
125
+ stripe: ["stripe"],
126
+ vercel: ["vercel"],
127
+ sentry: ["sentry"],
128
+ notion: ["notion"],
129
+ linear: ["linear"],
130
+ jira: ["atlassian"],
131
+ confluence: ["atlassian"],
132
+ slack: ["slack"],
133
+ figma: ["figma"],
134
+ github: ["github"],
135
+ huggingface: ["hugging-face"],
136
+ "hugging face": ["hugging-face"],
137
+ };
138
+
101
139
  export const CATEGORY_ORDER = [
102
140
  "workflow",
103
141
  "code-quality",
package/src/demo.mjs CHANGED
@@ -458,6 +458,83 @@ const DEMO_INSIGHTS_HTML = `<!DOCTYPE html>
458
458
  </body>
459
459
  </html>`;
460
460
 
461
+ // ── MCP Registry servers ──────────────────────────────────────────────────────
462
+
463
+ const DEMO_REGISTRY_SERVERS = [
464
+ {
465
+ name: "Vercel",
466
+ slug: "vercel",
467
+ description: "Analyze, debug, and manage projects and deployments",
468
+ url: "https://mcp.vercel.com/",
469
+ installCommand: "claude mcp add --transport http vercel https://mcp.vercel.com",
470
+ worksWith: ["claude", "claude-api", "claude-code"],
471
+ tools: ["list_projects", "get_deployment"],
472
+ },
473
+ {
474
+ name: "Supabase",
475
+ slug: "supabase",
476
+ description: "Manage databases, authentication, and storage",
477
+ url: "https://mcp.supabase.com/mcp",
478
+ installCommand: "claude mcp add --transport http supabase https://mcp.supabase.com/mcp",
479
+ worksWith: ["claude", "claude-api", "claude-code"],
480
+ tools: ["execute_sql", "generate_typescript_types"],
481
+ },
482
+ {
483
+ name: "Stripe",
484
+ slug: "stripe",
485
+ description: "Payment processing and financial infrastructure tools",
486
+ url: "https://mcp.stripe.com",
487
+ installCommand: "claude mcp add --transport http stripe https://mcp.stripe.com",
488
+ worksWith: ["claude", "claude-api", "claude-code"],
489
+ tools: ["create_customer", "create_payment_link"],
490
+ },
491
+ {
492
+ name: "Linear",
493
+ slug: "linear",
494
+ description: "Manage issues, projects & team workflows in Linear",
495
+ url: "https://mcp.linear.app/mcp",
496
+ installCommand: "claude mcp add --transport http linear https://mcp.linear.app/mcp",
497
+ worksWith: ["claude", "claude-api", "claude-code"],
498
+ tools: ["create_issue", "update_issue"],
499
+ },
500
+ {
501
+ name: "Notion",
502
+ slug: "notion",
503
+ description: "Create, edit, search and organize Notion content",
504
+ url: "https://mcp.notion.com/mcp",
505
+ installCommand: "claude mcp add --transport http notion https://mcp.notion.com/mcp",
506
+ worksWith: ["claude", "claude-api", "claude-code"],
507
+ tools: ["search", "create-pages"],
508
+ },
509
+ {
510
+ name: "Atlassian",
511
+ slug: "atlassian",
512
+ description: "Access Jira & Confluence from Claude",
513
+ url: "https://mcp.atlassian.com/v1/mcp",
514
+ installCommand: "claude mcp add --transport http atlassian https://mcp.atlassian.com/v1/mcp",
515
+ worksWith: ["claude", "claude-code"],
516
+ tools: ["getJiraIssue", "createJiraIssue"],
517
+ },
518
+ {
519
+ name: "Slack",
520
+ slug: "slack",
521
+ description: "Send messages, create canvases, and fetch Slack data",
522
+ url: "https://mcp.slack.com/mcp",
523
+ installCommand: "claude mcp add --transport http slack-remote https://mcp.slack.com/mcp",
524
+ worksWith: ["claude", "claude-api", "claude-code"],
525
+ tools: ["slack_send_message", "slack_search_public_and_private"],
526
+ },
527
+ {
528
+ name: "Context7",
529
+ slug: "context7",
530
+ description: "Up-to-date docs for LLMs and AI code editors",
531
+ url: "https://mcp.context7.com/mcp",
532
+ installCommand: "claude mcp add --transport http context7 https://mcp.context7.com/mcp",
533
+ worksWith: ["claude", "claude-api", "claude-code"],
534
+ tools: ["resolve-library-id", "query-docs"],
535
+ },
536
+ ];
537
+
461
538
  // ── Main export ──────────────────────────────────────────────────────────────
462
539
 
463
540
  export function generateDemoRawInputs() {
@@ -473,6 +550,7 @@ export function generateDemoRawInputs() {
473
550
  projectMcpByRepo,
474
551
  disabledMcpByRepo,
475
552
  historicalMcpMap,
553
+ registryServers: DEMO_REGISTRY_SERVERS,
476
554
  sessionMetaFiles: buildDemoSessionMeta(),
477
555
  ccusageData: {
478
556
  totals: { totalCost: 47.82, totalTokens: 28_450_000 },
package/src/mcp.mjs CHANGED
@@ -1,6 +1,11 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync, statSync } from "fs";
2
2
  import { join } from "path";
3
- import { MAX_SESSION_SCAN } from "./constants.mjs";
3
+ import {
4
+ MAX_SESSION_SCAN,
5
+ MCP_REGISTRY_URL,
6
+ MCP_REGISTRY_TTL_MS,
7
+ CLAUDE_DIR,
8
+ } from "./constants.mjs";
4
9
 
5
10
  export function parseUserMcpConfig(content) {
6
11
  try {
@@ -175,3 +180,93 @@ export function classifyHistoricalServers(
175
180
  former.sort((a, b) => a.name.localeCompare(b.name));
176
181
  return { recent, former };
177
182
  }
183
+
184
+ /**
185
+ * Pure normalizer: extract claude-code compatible servers from raw registry API response.
186
+ * Returns [] on any malformed input.
187
+ */
188
+ export function normalizeRegistryResponse(raw) {
189
+ try {
190
+ if (!raw || !Array.isArray(raw.servers)) return [];
191
+ return raw.servers
192
+ .map((entry) => {
193
+ // The registry API nests data: entry.server has the MCP spec fields,
194
+ // entry._meta["com.anthropic.api/mcp-registry"] has Anthropic's curated metadata.
195
+ // Also support flat shape (used in tests and demo data).
196
+ const anth = entry?._meta?.["com.anthropic.api/mcp-registry"] || {};
197
+ const srv = entry?.server || entry || {};
198
+ const name = anth.displayName || entry.name || srv.title || "";
199
+ return {
200
+ name,
201
+ slug:
202
+ anth.slug ||
203
+ entry.slug ||
204
+ name
205
+ .toLowerCase()
206
+ .replace(/[^a-z0-9]+/g, "-")
207
+ .replace(/^-|-$/g, ""),
208
+ description: anth.oneLiner || entry.description || srv.description || "",
209
+ url: anth.url || entry.url || "",
210
+ installCommand: anth.claudeCodeCopyText || entry.installCommand || "",
211
+ worksWith: anth.worksWith || entry.worksWith || [],
212
+ tools: anth.toolNames || entry.tools || [],
213
+ };
214
+ })
215
+ .filter((s) => Array.isArray(s.worksWith) && s.worksWith.includes("claude-code"));
216
+ } catch {
217
+ return [];
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Fetch MCP registry servers with 24h file cache.
223
+ * Falls back to stale cache on network failure, returns [] on total failure.
224
+ */
225
+ export async function fetchRegistryServers() {
226
+ const cachePath = join(CLAUDE_DIR, "mcp-registry-cache.json");
227
+
228
+ // Try fresh cache
229
+ try {
230
+ const cached = JSON.parse(readFileSync(cachePath, "utf8"));
231
+ if (cached._ts && Date.now() - cached._ts < MCP_REGISTRY_TTL_MS) {
232
+ return normalizeRegistryResponse(cached.data);
233
+ }
234
+ } catch {
235
+ /* no cache or unreadable */
236
+ }
237
+
238
+ // Fetch from registry
239
+ try {
240
+ const controller = new AbortController();
241
+ const timeout = setTimeout(() => controller.abort(), 8000);
242
+ const res = await fetch(MCP_REGISTRY_URL, { signal: controller.signal });
243
+ clearTimeout(timeout);
244
+
245
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
246
+ const data = await res.json();
247
+ const normalized = normalizeRegistryResponse(data);
248
+
249
+ // Only cache valid responses to avoid 24h blackout on malformed data
250
+ if (Array.isArray(data?.servers) && data.servers.length > 0) {
251
+ try {
252
+ writeFileSync(cachePath, JSON.stringify({ _ts: Date.now(), data }));
253
+ } catch {
254
+ /* non-critical */
255
+ }
256
+ }
257
+
258
+ return normalized;
259
+ } catch {
260
+ /* network failure — try stale cache */
261
+ }
262
+
263
+ // Stale cache fallback (ignore TTL)
264
+ try {
265
+ const cached = JSON.parse(readFileSync(cachePath, "utf8"));
266
+ return normalizeRegistryResponse(cached.data);
267
+ } catch {
268
+ /* total failure */
269
+ }
270
+
271
+ return [];
272
+ }
package/src/pipeline.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  * All data comes in via the `raw` parameter.
7
7
  */
8
8
 
9
- import { SIMILARITY_THRESHOLD } from "./constants.mjs";
9
+ import { SIMILARITY_THRESHOLD, MCP_STACK_HINTS } from "./constants.mjs";
10
10
  import { relativeTime, freshnessClass } from "./freshness.mjs";
11
11
  import {
12
12
  computeHealthScore,
@@ -222,6 +222,86 @@ export function buildDashboardData(raw) {
222
222
  });
223
223
  const mcpCount = mcpSummary.length;
224
224
 
225
+ // ── 3b. MCP Registry — Available & Recommended ────────────────────────
226
+
227
+ const registryServers = raw.registryServers || [];
228
+ const registryTotal = registryServers.length;
229
+
230
+ // Build a set of installed server identifiers (lowercase names)
231
+ // Use mcpSummary which includes user, project, AND recently-active servers
232
+ const installedIds = new Set();
233
+ for (const s of mcpSummary) {
234
+ installedIds.add(s.name.toLowerCase());
235
+ }
236
+
237
+ // Filter out already-installed servers
238
+ const notInstalled = registryServers.filter(
239
+ (s) =>
240
+ !installedIds.has((s.slug || "").toLowerCase()) &&
241
+ !installedIds.has((s.name || "").toLowerCase()),
242
+ );
243
+
244
+ // Collect tech stacks and description text from all repos
245
+ const allRepos = [...configured, ...unconfigured];
246
+ const stackCounts = {}; // key -> count of repos with that stack
247
+ for (const repo of allRepos) {
248
+ for (const stack of repo.techStack || []) {
249
+ const k = stack.toLowerCase();
250
+ stackCounts[k] = (stackCounts[k] || 0) + 1;
251
+ }
252
+ }
253
+
254
+ // Join all descriptions into a single lowercased string for substring matching
255
+ // (supports multi-word keys like "hugging face")
256
+ const allDescText = allRepos
257
+ .flatMap((r) => r.desc || [])
258
+ .join(" ")
259
+ .toLowerCase();
260
+
261
+ // Match hints against stacks and descriptions
262
+ const recommendedSlugs = new Map(); // slug -> { reasons: [], matchCount: 0 }
263
+ for (const [key, slugs] of Object.entries(MCP_STACK_HINTS)) {
264
+ const stackCount = stackCounts[key] || 0;
265
+ const inDesc = allDescText.includes(key);
266
+
267
+ if (stackCount > 0 || inDesc) {
268
+ for (const slug of slugs) {
269
+ if (!recommendedSlugs.has(slug)) {
270
+ recommendedSlugs.set(slug, { reasons: [], matchCount: 0 });
271
+ }
272
+ const entry = recommendedSlugs.get(slug);
273
+ if (stackCount > 0) {
274
+ entry.reasons.push(`${stackCount} ${key} repo${stackCount > 1 ? "s" : ""} detected`);
275
+ entry.matchCount += stackCount;
276
+ }
277
+ if (inDesc) {
278
+ entry.reasons.push("mentioned in repo descriptions");
279
+ entry.matchCount += 1;
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ // Build recommended list from not-installed servers that match hints
286
+ const recommendedMcpServers = [];
287
+ const recommendedSlugSet = new Set();
288
+ for (const server of notInstalled) {
289
+ const slug = (server.slug || "").toLowerCase();
290
+ if (recommendedSlugs.has(slug)) {
291
+ const { reasons, matchCount } = recommendedSlugs.get(slug);
292
+ recommendedMcpServers.push({ ...server, reasons, matchCount });
293
+ recommendedSlugSet.add(slug);
294
+ }
295
+ }
296
+
297
+ // Sort by relevance (more match signals first)
298
+ recommendedMcpServers.sort((a, b) => b.matchCount - a.matchCount || a.name.localeCompare(b.name));
299
+
300
+ // Available = not-installed minus recommended
301
+ const availableMcpServers = notInstalled.filter(
302
+ (s) => !recommendedSlugSet.has((s.slug || "").toLowerCase()),
303
+ );
304
+
225
305
  // ── 4. Usage Analytics ────────────────────────────────────────────────
226
306
 
227
307
  const usageAnalytics = aggregateSessionMeta(raw.sessionMetaFiles || []);
@@ -406,6 +486,19 @@ export function buildDashboardData(raw) {
406
486
  });
407
487
  }
408
488
 
489
+ // MCP recommendations
490
+ if (recommendedMcpServers.length > 0) {
491
+ insights.push({
492
+ type: "tip",
493
+ title: `${recommendedMcpServers.length} MCP server${recommendedMcpServers.length > 1 ? "s" : ""} recommended for your repos`,
494
+ detail: recommendedMcpServers
495
+ .slice(0, 3)
496
+ .map((s) => `${s.name} (${s.reasons.join(", ")})`)
497
+ .join(", "),
498
+ action: "Check the Skills & MCP tab for install commands",
499
+ });
500
+ }
501
+
409
502
  // Skill sharing opportunities
410
503
  const skillMatchCounts = {};
411
504
  for (const r of configured) {
@@ -493,6 +586,9 @@ export function buildDashboardData(raw) {
493
586
  avgHealth,
494
587
  driftCount,
495
588
  mcpCount,
589
+ recommendedMcpServers,
590
+ availableMcpServers,
591
+ registryTotal,
496
592
  scanScope: raw.scanScope,
497
593
  insights,
498
594
  insightsReport,
package/src/sections.mjs CHANGED
@@ -21,8 +21,15 @@ export function renderSkillsCard(globalSkills) {
21
21
  </div>`;
22
22
  }
23
23
 
24
- export function renderMcpCard(mcpSummary, mcpPromotions, formerMcpServers) {
25
- if (!mcpSummary.length) return "";
24
+ export function renderMcpCard(
25
+ mcpSummary,
26
+ mcpPromotions,
27
+ formerMcpServers,
28
+ recommendedMcpServers,
29
+ availableMcpServers,
30
+ registryTotal,
31
+ ) {
32
+ if (!mcpSummary.length && !recommendedMcpServers.length && !availableMcpServers.length) return "";
26
33
  const rows = mcpSummary
27
34
  .map((s) => {
28
35
  const disabledClass = s.disabledIn > 0 ? " mcp-disabled" : "";
@@ -62,11 +69,52 @@ export function renderMcpCard(mcpSummary, mcpPromotions, formerMcpServers) {
62
69
  })
63
70
  .join("\n ")}`
64
71
  : "";
72
+ const recommendedHtml = recommendedMcpServers.length
73
+ ? `<details class="mcp-section"${recommendedMcpServers.length <= 5 ? " open" : ""}>
74
+ <summary class="label" style="cursor:pointer;margin-top:.75rem">Recommended <span class="cat-n">${recommendedMcpServers.length}</span></summary>
75
+ ${recommendedMcpServers
76
+ .map(
77
+ (s) =>
78
+ `<div class="mcp-recommended"><span class="mcp-name">${esc(s.name)}</span> <span class="mcp-rec-badge">recommended</span>` +
79
+ (s.description ? `<div class="mcp-desc">${esc(s.description)}</div>` : "") +
80
+ (s.reasons && s.reasons.length
81
+ ? `<div class="mcp-reason">${s.reasons.map((r) => esc(r)).join(", ")}</div>`
82
+ : "") +
83
+ (s.installCommand ? `<code class="mcp-install">${esc(s.installCommand)}</code>` : "") +
84
+ `</div>`,
85
+ )
86
+ .join("\n ")}
87
+ </details>`
88
+ : "";
89
+
90
+ const availableHtml = availableMcpServers.length
91
+ ? `<details class="mcp-section">
92
+ <summary class="label" style="cursor:pointer;margin-top:.75rem">Available <span class="cat-n">${availableMcpServers.length}</span></summary>
93
+ ${availableMcpServers
94
+ .map(
95
+ (s) =>
96
+ `<div class="mcp-available"><span class="mcp-name">${esc(s.name)}</span>` +
97
+ (s.description ? `<div class="mcp-desc">${esc(s.description)}</div>` : "") +
98
+ (s.installCommand ? `<code class="mcp-install">${esc(s.installCommand)}</code>` : "") +
99
+ `</div>`,
100
+ )
101
+ .join("\n ")}
102
+ </details>`
103
+ : "";
104
+
105
+ const registryNote =
106
+ registryTotal > 0
107
+ ? `<div class="mcp-registry-note">${registryTotal} servers in registry</div>`
108
+ : "";
109
+
65
110
  return `<div class="card" id="section-mcp">
66
111
  <h2>MCP Servers <span class="n">${mcpSummary.length}</span></h2>
67
112
  ${rows}
68
113
  ${promoteHtml}
69
114
  ${formerHtml}
115
+ ${recommendedHtml}
116
+ ${availableHtml}
117
+ ${registryNote}
70
118
  </div>`;
71
119
  }
72
120
 
@@ -805,6 +805,84 @@ details.cmd-detail > summary::-webkit-details-marker {
805
805
  .mcp-former {
806
806
  opacity: 0.4;
807
807
  }
808
+
809
+ /* ── MCP Registry Sections ──────────────────────────────── */
810
+ .mcp-section {
811
+ margin-top: 0.75rem;
812
+ }
813
+ .mcp-section > summary {
814
+ list-style: none;
815
+ }
816
+ .mcp-section > summary::-webkit-details-marker {
817
+ display: none;
818
+ }
819
+ .mcp-section > summary:hover {
820
+ color: var(--accent);
821
+ }
822
+ .mcp-recommended {
823
+ border-left: 2px solid var(--accent);
824
+ padding: 0.4rem 0.6rem;
825
+ margin-top: 0.35rem;
826
+ border-radius: 0 4px 4px 0;
827
+ background: var(--surface2);
828
+ }
829
+ .mcp-rec-badge {
830
+ font-size: 0.55rem;
831
+ font-weight: 600;
832
+ text-transform: uppercase;
833
+ letter-spacing: 0.04em;
834
+ padding: 0.1rem 0.35rem;
835
+ border-radius: 3px;
836
+ background: rgba(196, 149, 106, 0.15);
837
+ color: var(--accent);
838
+ border: 1px solid rgba(196, 149, 106, 0.3);
839
+ vertical-align: middle;
840
+ margin-left: 0.3rem;
841
+ }
842
+ .mcp-desc {
843
+ font-size: 0.7rem;
844
+ color: var(--text-dim);
845
+ margin-top: 0.15rem;
846
+ }
847
+ .mcp-reason {
848
+ font-size: 0.65rem;
849
+ color: var(--accent);
850
+ margin-top: 0.1rem;
851
+ font-style: italic;
852
+ }
853
+ .mcp-install {
854
+ display: block;
855
+ font-family: "SF Mono", "Fira Code", "JetBrains Mono", monospace;
856
+ font-size: 0.68rem;
857
+ color: var(--green);
858
+ background: var(--bg);
859
+ border: 1px solid var(--border);
860
+ border-radius: 4px;
861
+ padding: 0.3rem 0.5rem;
862
+ margin-top: 0.25rem;
863
+ cursor: pointer;
864
+ transition:
865
+ background 0.15s,
866
+ border-color 0.15s;
867
+ word-break: break-all;
868
+ }
869
+ .mcp-install:hover {
870
+ background: var(--surface2);
871
+ border-color: var(--accent-dim);
872
+ }
873
+ .mcp-available {
874
+ padding: 0.35rem 0.5rem;
875
+ margin-top: 0.3rem;
876
+ border-radius: 4px;
877
+ background: var(--surface2);
878
+ opacity: 0.85;
879
+ }
880
+ .mcp-registry-note {
881
+ font-size: 0.6rem;
882
+ color: var(--text-dim);
883
+ text-align: right;
884
+ margin-top: 0.75rem;
885
+ }
808
886
  .badge.mcp-former-badge {
809
887
  color: var(--text-dim);
810
888
  border-color: var(--border);
@@ -165,6 +165,24 @@ if (refreshBtn) {
165
165
  });
166
166
  }
167
167
 
168
+ // ── Click-to-copy MCP install commands ───────────────────
169
+ document.addEventListener("click", function (e) {
170
+ var install = e.target.closest(".mcp-install");
171
+ if (!install) return;
172
+ navigator.clipboard
173
+ .writeText(install.textContent)
174
+ .then(function () {
175
+ var orig = install.textContent;
176
+ install.textContent = "copied!";
177
+ setTimeout(function () {
178
+ install.textContent = orig;
179
+ }, 1500);
180
+ })
181
+ .catch(function () {
182
+ /* clipboard unavailable (file:// protocol, permissions) */
183
+ });
184
+ });
185
+
168
186
  // Custom tooltip for heatmap cells and peak bars
169
187
  var tip = document.getElementById("chart-tooltip");
170
188
  document.addEventListener("mouseover", function (e) {