@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.
- package/generate-dashboard.mjs +12 -3
- package/package.json +1 -1
- package/src/assembler.mjs +4 -1
- package/src/cli.mjs +7 -2
- package/src/constants.mjs +39 -1
- package/src/demo.mjs +78 -0
- package/src/mcp.mjs +97 -2
- package/src/pipeline.mjs +97 -1
- package/src/sections.mjs +50 -2
- package/template/dashboard.css +78 -0
- package/template/dashboard.js +18 -0
package/generate-dashboard.mjs
CHANGED
|
@@ -43,7 +43,12 @@ import {
|
|
|
43
43
|
computeDashboardDiff,
|
|
44
44
|
} from "./src/analysis.mjs";
|
|
45
45
|
import { getFreshness } from "./src/freshness.mjs";
|
|
46
|
-
import {
|
|
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
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.
|
|
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 {
|
|
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(
|
|
25
|
-
|
|
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
|
|
package/template/dashboard.css
CHANGED
|
@@ -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);
|
package/template/dashboard.js
CHANGED
|
@@ -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) {
|