@mcpskillsio/server 2.1.1 → 2.3.0

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.
@@ -0,0 +1,332 @@
1
+ functionsDirectory = "/Users/michaelbrowne/Desktop/mcpskills/mcp-server/netlify/functions"
2
+ functionsDirectoryOrigin = "config-v1"
3
+ redirectsOrigin = "config"
4
+ plugins = []
5
+ headers = []
6
+
7
+ [functions]
8
+
9
+ [functions.nightly-crawl]
10
+ schedule = "0 2 * * *"
11
+
12
+ [functions.daily-scan]
13
+ schedule = "0 8 * * *"
14
+
15
+ [functions.weekly-digest]
16
+ schedule = "0 18 * * 0"
17
+
18
+ [functions.monthly-recap]
19
+ schedule = "0 14 1-5 * *"
20
+
21
+ [functions."*"]
22
+ included_files = ["lib/fonts/**", "data/**"]
23
+ external_node_modules = ["satori", "@resvg/resvg-js", "@netlify/blobs"]
24
+ node_bundler = "esbuild"
25
+
26
+ [build]
27
+ publish = "/Users/michaelbrowne/Desktop/mcpskills/mcp-server/public"
28
+ publishOrigin = "config"
29
+ base = "/Users/michaelbrowne/Desktop/mcpskills/mcp-server"
30
+ functions = "/Users/michaelbrowne/Desktop/mcpskills/mcp-server/netlify/functions"
31
+
32
+ [build.environment]
33
+
34
+ [build.processing]
35
+
36
+ [build.processing.css]
37
+
38
+ [build.processing.html]
39
+
40
+ [build.processing.images]
41
+
42
+ [build.processing.js]
43
+
44
+ [build.services]
45
+
46
+ [[redirects]]
47
+ from = "/blog/state-of-ai-skill-security"
48
+ to = "/blog/state-of-ai-skill-security.html"
49
+ status = 200.0
50
+ force = false
51
+
52
+ [redirects.query]
53
+
54
+ [redirects.conditions]
55
+
56
+ [redirects.headers]
57
+
58
+ [[redirects]]
59
+ from = "/blog/clawhavoc-missing-trust-layer"
60
+ to = "/blog/clawhavoc-missing-trust-layer.html"
61
+ status = 200.0
62
+ force = false
63
+
64
+ [redirects.query]
65
+
66
+ [redirects.conditions]
67
+
68
+ [redirects.headers]
69
+
70
+ [[redirects]]
71
+ from = "/blog/how-to-check-ai-skill-safe"
72
+ to = "/blog/how-to-check-ai-skill-safe.html"
73
+ status = 200.0
74
+ force = false
75
+
76
+ [redirects.query]
77
+
78
+ [redirects.conditions]
79
+
80
+ [redirects.headers]
81
+
82
+ [[redirects]]
83
+ from = "/blog/score-without-github-repo"
84
+ to = "/blog/score-without-github-repo.html"
85
+ status = 200.0
86
+ force = false
87
+
88
+ [redirects.query]
89
+
90
+ [redirects.conditions]
91
+
92
+ [redirects.headers]
93
+
94
+ [[redirects]]
95
+ from = "/privacy"
96
+ to = "/privacy.html"
97
+ status = 200.0
98
+ force = false
99
+
100
+ [redirects.query]
101
+
102
+ [redirects.conditions]
103
+
104
+ [redirects.headers]
105
+
106
+ [[redirects]]
107
+ from = "/terms"
108
+ to = "/terms.html"
109
+ status = 200.0
110
+ force = false
111
+
112
+ [redirects.query]
113
+
114
+ [redirects.conditions]
115
+
116
+ [redirects.headers]
117
+
118
+ [[redirects]]
119
+ from = "/badge/*"
120
+ to = "/.netlify/functions/badge/:splat"
121
+ status = 200.0
122
+ force = false
123
+
124
+ [redirects.query]
125
+
126
+ [redirects.conditions]
127
+
128
+ [redirects.headers]
129
+
130
+ [[redirects]]
131
+ from = "/score/*"
132
+ to = "/.netlify/functions/score-page/:splat"
133
+ status = 200.0
134
+ force = false
135
+
136
+ [redirects.query]
137
+
138
+ [redirects.conditions]
139
+
140
+ [redirects.headers]
141
+
142
+ [[redirects]]
143
+ from = "/digest/rss.xml"
144
+ to = "/.netlify/functions/digest-rss"
145
+ status = 200.0
146
+ force = false
147
+
148
+ [redirects.query]
149
+
150
+ [redirects.conditions]
151
+
152
+ [redirects.headers]
153
+
154
+ [[redirects]]
155
+ from = "/digest/*"
156
+ to = "/.netlify/functions/digest-page/:splat"
157
+ status = 200.0
158
+ force = false
159
+
160
+ [redirects.query]
161
+
162
+ [redirects.conditions]
163
+
164
+ [redirects.headers]
165
+
166
+ [[redirects]]
167
+ from = "/digest"
168
+ to = "/.netlify/functions/digest-page"
169
+ status = 200.0
170
+ force = false
171
+
172
+ [redirects.query]
173
+
174
+ [redirects.conditions]
175
+
176
+ [redirects.headers]
177
+
178
+ [[redirects]]
179
+ from = "/certified"
180
+ to = "/.netlify/functions/certified-wall"
181
+ status = 200.0
182
+ force = false
183
+
184
+ [redirects.query]
185
+
186
+ [redirects.conditions]
187
+
188
+ [redirects.headers]
189
+
190
+ [[redirects]]
191
+ from = "/api/limit-capture"
192
+ to = "/.netlify/functions/limit-capture"
193
+ status = 200.0
194
+ force = false
195
+
196
+ [redirects.query]
197
+
198
+ [redirects.conditions]
199
+
200
+ [redirects.headers]
201
+
202
+ [[redirects]]
203
+ from = "/api/trending"
204
+ to = "/.netlify/functions/trending"
205
+ status = 200.0
206
+ force = false
207
+
208
+ [redirects.query]
209
+
210
+ [redirects.conditions]
211
+
212
+ [redirects.headers]
213
+
214
+ [[redirects]]
215
+ from = "/trending"
216
+ to = "/.netlify/functions/trending"
217
+ status = 200.0
218
+ force = false
219
+
220
+ [redirects.query]
221
+
222
+ [redirects.conditions]
223
+
224
+ [redirects.headers]
225
+
226
+ [[redirects]]
227
+ from = "/sitemap.xml"
228
+ to = "/.netlify/functions/sitemap"
229
+ status = 200.0
230
+ force = false
231
+
232
+ [redirects.query]
233
+
234
+ [redirects.conditions]
235
+
236
+ [redirects.headers]
237
+
238
+ [[redirects]]
239
+ from = "/data/latest.json"
240
+ to = "/.netlify/functions/data-export"
241
+ status = 200.0
242
+ force = false
243
+
244
+ [redirects.query]
245
+
246
+ [redirects.conditions]
247
+
248
+ [redirects.headers]
249
+
250
+ [[redirects]]
251
+ from = "/data/latest.csv"
252
+ to = "/.netlify/functions/data-export"
253
+ status = 200.0
254
+ force = false
255
+
256
+ [redirects.query]
257
+
258
+ [redirects.conditions]
259
+
260
+ [redirects.headers]
261
+
262
+ [[redirects]]
263
+ from = "/methodology"
264
+ to = "/methodology.html"
265
+ status = 200.0
266
+ force = false
267
+
268
+ [redirects.query]
269
+
270
+ [redirects.conditions]
271
+
272
+ [redirects.headers]
273
+
274
+ [[redirects]]
275
+ from = "/roadmap"
276
+ to = "/roadmap.html"
277
+ status = 200.0
278
+ force = false
279
+
280
+ [redirects.query]
281
+
282
+ [redirects.conditions]
283
+
284
+ [redirects.headers]
285
+
286
+ [[redirects]]
287
+ from = "/certify"
288
+ to = "/certify.html"
289
+ status = 200.0
290
+ force = false
291
+
292
+ [redirects.query]
293
+
294
+ [redirects.conditions]
295
+
296
+ [redirects.headers]
297
+
298
+ [[redirects]]
299
+ from = "/api/certify"
300
+ to = "/.netlify/functions/certify"
301
+ status = 200.0
302
+ force = false
303
+
304
+ [redirects.query]
305
+
306
+ [redirects.conditions]
307
+
308
+ [redirects.headers]
309
+
310
+ [[redirects]]
311
+ from = "/api/webhook"
312
+ to = "/.netlify/functions/webhook"
313
+ status = 200.0
314
+ force = false
315
+
316
+ [redirects.query]
317
+
318
+ [redirects.conditions]
319
+
320
+ [redirects.headers]
321
+
322
+ [[redirects]]
323
+ from = "/api/unlock"
324
+ to = "/.netlify/functions/unlock"
325
+ status = 200.0
326
+ force = false
327
+
328
+ [redirects.query]
329
+
330
+ [redirects.conditions]
331
+
332
+ [redirects.headers]
package/index.js CHANGED
@@ -15,6 +15,7 @@
15
15
  * - check_watched: Re-scan all watched repos
16
16
  * - batch_check: Check up to 5 repos in one call (Pro)
17
17
  * - auto_gate: "Should I install this?" → boolean + reason
18
+ * - build_stack: "What tools do I need?" → vetted stack from live trust data
18
19
  *
19
20
  * Free tier returns compact agent response.
20
21
  * Full reports require MCPSKILLS_API_KEY env var.
@@ -30,6 +31,44 @@ import {
30
31
 
31
32
  const API_BASE = "https://mcpskills.io/.netlify/functions";
32
33
  const PACKAGES_URL = "https://mcpskills.io/.netlify/functions/packages";
34
+ const DATA_URL = "https://mcpskills.io/data/latest.json";
35
+
36
+ // --- Stack Builder: category definitions + keyword matching ---
37
+ // Mirrors data/registry.json categories but kept inline so the MCP server
38
+ // is fully self-contained (no fetch to a private data file).
39
+ const STACK_CATEGORIES = {
40
+ 'ai-sdk': { name: 'AI SDK & LLM Tools', keywords: ['ai', 'llm', 'gpt', 'claude', 'anthropic', 'openai', 'chat', 'agent', 'embedding', 'langchain'] },
41
+ 'mcp-server': { name: 'MCP Servers & Protocol', keywords: ['mcp', 'model-context-protocol', 'mcp-server', 'tool-server', 'skill'] },
42
+ 'database': { name: 'Database & ORM', keywords: ['database', 'db', 'orm', 'sql', 'postgres', 'mysql', 'sqlite', 'prisma', 'drizzle', 'supabase', 'mongo'] },
43
+ 'auth': { name: 'Authentication & Security', keywords: ['auth', 'authentication', 'login', 'oauth', 'jwt', 'session', 'nextauth', 'clerk', 'passport'] },
44
+ 'payments': { name: 'Payments & Billing', keywords: ['payment', 'stripe', 'billing', 'checkout', 'subscription', 'commerce', 'lemon'] },
45
+ 'ui': { name: 'UI Components & Design', keywords: ['ui', 'component', 'design', 'shadcn', 'radix', 'tailwind', 'css', 'frontend'] },
46
+ 'email': { name: 'Email & Messaging', keywords: ['email', 'smtp', 'mail', 'resend', 'sendgrid', 'notification', 'messaging'] },
47
+ 'testing': { name: 'Testing & Quality', keywords: ['test', 'testing', 'jest', 'vitest', 'playwright', 'cypress', 'e2e', 'unit-test'] },
48
+ 'devops': { name: 'DevOps & Infrastructure', keywords: ['deploy', 'docker', 'ci', 'cd', 'infrastructure', 'kubernetes', 'monitoring', 'netlify', 'vercel'] },
49
+ 'web-framework': { name: 'Web Frameworks', keywords: ['next', 'nextjs', 'express', 'fastify', 'hono', 'remix', 'nuxt', 'svelte', 'framework', 'server'] },
50
+ 'validation': { name: 'Validation & Schema', keywords: ['validation', 'schema', 'zod', 'joi', 'yup', 'typebox', 'form'] },
51
+ 'search': { name: 'Search & RAG', keywords: ['search', 'vector', 'rag', 'embedding', 'pinecone', 'chromadb', 'semantic', 'retrieval'] },
52
+ 'scraping': { name: 'Web Scraping & Data', keywords: ['scrape', 'scraping', 'crawl', 'puppeteer', 'playwright', 'cheerio', 'data-extraction'] },
53
+ 'file-system': { name: 'File System & Storage', keywords: ['file', 'storage', 's3', 'upload', 'filesystem', 'blob', 'bucket'] },
54
+ };
55
+
56
+ const STACK_CROSS_CATEGORY = {
57
+ 'ai-sdk': ['mcp-server', 'database', 'search'],
58
+ 'mcp-server': ['ai-sdk', 'auth', 'database'],
59
+ 'database': ['auth', 'validation', 'web-framework'],
60
+ 'auth': ['database', 'payments', 'web-framework'],
61
+ 'payments': ['auth', 'email', 'web-framework'],
62
+ 'ui': ['web-framework', 'testing', 'validation'],
63
+ 'email': ['payments', 'auth'],
64
+ 'testing': ['devops', 'ai-sdk'],
65
+ 'devops': ['testing', 'database'],
66
+ 'web-framework': ['database', 'auth', 'ui'],
67
+ 'validation': ['database', 'ai-sdk'],
68
+ 'search': ['ai-sdk', 'database'],
69
+ 'scraping': ['database', 'ai-sdk'],
70
+ 'file-system': ['database', 'devops'],
71
+ };
33
72
 
34
73
  // --- API Client ---
35
74
 
@@ -115,6 +154,55 @@ function formatRecommendations(recs) {
115
154
 
116
155
  // --- Agent Response Formatting ---
117
156
 
157
+ function formatPartialResult(data) {
158
+ const lines = [
159
+ `# Limited Trust Score: ${data.package || 'Unknown Package'}`,
160
+ "",
161
+ `⚠️ **Limited Score — No Source Repo Found**`,
162
+ `Score based on npm registry metadata only (${data.signalCount}/${data.totalPossibleSignals} signals).`,
163
+ "",
164
+ `**Score:** ${data.composite}/10`,
165
+ `**Tier:** ${formatTier(data.tier)}`,
166
+ "",
167
+ "## Dimensions (limited)",
168
+ formatDimensions(data.dimensions),
169
+ "",
170
+ "## Signals (npm metadata only)",
171
+ ];
172
+
173
+ const signalLabels = {
174
+ publish_recency: "Publish Recency",
175
+ publish_cadence: "Publish Cadence",
176
+ download_adoption: "Download Adoption",
177
+ maintainer_count: "Maintainer Count",
178
+ package_age: "Package Age",
179
+ dependency_count: "Dependency Count",
180
+ license_clarity: "License Clarity",
181
+ };
182
+
183
+ for (const [key, val] of Object.entries(data.signals || {})) {
184
+ const label = signalLabels[key] || key;
185
+ const v = typeof val === 'number' && !isNaN(val) ? val : 0;
186
+ const bar = "█".repeat(Math.round(v)) + "░".repeat(10 - Math.round(v));
187
+ lines.push(` ${bar} ${v}/10 ${label}`);
188
+ }
189
+
190
+ lines.push(
191
+ "",
192
+ `⚠️ ${data.limitedReason}`,
193
+ "",
194
+ `📦 ${data.meta?.versions || '?'} versions | 👥 ${data.meta?.maintainerCount || '?'} maintainers | 📄 ${data.meta?.license || 'Unknown'}`,
195
+ `📥 ${(data.meta?.npmDownloads || 0).toLocaleString()} downloads/month`,
196
+ );
197
+
198
+ if (data.meta?.homepage) {
199
+ lines.push(`🔗 ${data.meta.homepage}`);
200
+ }
201
+
202
+ lines.push("", `Scanned at: ${data.scannedAt}`, "Powered by mcpskills.io");
203
+ return lines.join("\n");
204
+ }
205
+
118
206
  function formatAgentResponse(data) {
119
207
  // Compact agent response (from free tier API)
120
208
  const rec = data.recommendation || (data.safe ? 'install' : 'caution');
@@ -308,7 +396,7 @@ function formatSafetyResult(data) {
308
396
  const server = new Server(
309
397
  {
310
398
  name: "mcpskills",
311
- version: "2.1.0",
399
+ version: "2.2.0",
312
400
  },
313
401
  {
314
402
  capabilities: {
@@ -324,14 +412,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
324
412
  {
325
413
  name: "check_trust_score",
326
414
  description:
327
- "Score any GitHub repo for trustworthiness. Returns a trust score (0-10) across 4 dimensions: Alive (maintained?), Legit (credible author?), Solid (secure?), Usable (good docs?). AI skills and MCP servers get enhanced scanning with 5 safety checks. Set MCPSKILLS_API_KEY env var for full 14-signal reports.",
415
+ "Score any AI skill, MCP server, or GitHub repo for trustworthiness. Returns a trust score (0-10) across 4 dimensions: Alive, Legit, Solid, Usable. Accepts: owner/repo, GitHub URL, npm package (npm:@scope/name or @scope/name), Smithery URL, or OpenClaw URL. AI skills get enhanced safety scanning. Set MCPSKILLS_API_KEY for full reports.",
328
416
  inputSchema: {
329
417
  type: "object",
330
418
  properties: {
331
419
  repo: {
332
420
  type: "string",
333
421
  description:
334
- 'GitHub repo in "owner/repo" format (e.g., "anthropics/anthropic-sdk-typescript")',
422
+ 'Any of: "owner/repo", GitHub URL, "npm:@scope/package", "@scope/package", Smithery URL, or OpenClaw URL',
335
423
  },
336
424
  },
337
425
  required: ["repo"],
@@ -340,14 +428,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
340
428
  {
341
429
  name: "scan_safety",
342
430
  description:
343
- "Run a focused safety scan on an AI skill or MCP server repo. Checks for prompt injection, shell execution, network exfiltration, credential theft, and obfuscated payloads. Returns detailed findings with file locations. Only works on repos detected as AI skills.",
431
+ "Run a focused safety scan on an AI skill or MCP server. Checks for prompt injection, shell execution, network exfiltration, credential theft, and obfuscated payloads. Accepts any input format (owner/repo, npm package, Smithery URL, etc.).",
344
432
  inputSchema: {
345
433
  type: "object",
346
434
  properties: {
347
435
  repo: {
348
436
  type: "string",
349
437
  description:
350
- 'GitHub repo in "owner/repo" format (e.g., "modelcontextprotocol/servers")',
438
+ 'Any of: "owner/repo", GitHub URL, "npm:@scope/package", Smithery URL, or OpenClaw URL',
351
439
  },
352
440
  },
353
441
  required: ["repo"],
@@ -371,13 +459,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
371
459
  {
372
460
  name: "get_badge",
373
461
  description:
374
- "Get a trust badge URL for any GitHub repo. Returns a shields.io-style SVG badge showing the trust score and tier. Embed in READMEs to show verified trust status. Badge auto-updates hourly.",
462
+ "Get a trust badge URL for any repo or package. Returns a shields.io-style SVG badge showing the trust score and tier. Embed in READMEs. Badge auto-updates hourly.",
375
463
  inputSchema: {
376
464
  type: "object",
377
465
  properties: {
378
466
  repo: {
379
467
  type: "string",
380
- description: 'GitHub repo in "owner/repo" format',
468
+ description: 'Any of: "owner/repo", GitHub URL, "npm:@scope/package", or Smithery URL',
381
469
  },
382
470
  },
383
471
  required: ["repo"],
@@ -386,13 +474,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
386
474
  {
387
475
  name: "watch_repo",
388
476
  description:
389
- "Start monitoring a repo for trust score changes. You'll be alerted when a repo's score changes significantly (±0.3 points or tier change). Requires a paid API key.",
477
+ "Start monitoring a repo or package for trust score changes. Alerts when score changes significantly (±0.3 points or tier change). Requires a paid API key.",
390
478
  inputSchema: {
391
479
  type: "object",
392
480
  properties: {
393
481
  repo: {
394
482
  type: "string",
395
- description: 'GitHub repo in "owner/repo" format',
483
+ description: 'Any of: "owner/repo", GitHub URL, "npm:@scope/package", or Smithery URL',
396
484
  },
397
485
  email: {
398
486
  type: "string",
@@ -420,14 +508,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
420
508
  {
421
509
  name: "batch_check",
422
510
  description:
423
- "Check up to 5 repos in one call. Returns a trust assessment for each repo. Requires a Pro API key (MCPSKILLS_API_KEY). Great for bulk-vetting dependencies.",
511
+ "Check up to 5 repos or packages in one call. Returns a trust assessment for each. Requires a Pro API key. Accepts any mix of formats (owner/repo, npm packages, registry URLs).",
424
512
  inputSchema: {
425
513
  type: "object",
426
514
  properties: {
427
515
  repos: {
428
516
  type: "array",
429
517
  items: { type: "string" },
430
- description: 'Array of GitHub repos in "owner/repo" format (max 5)',
518
+ description: 'Array of repos/packages in any format (max 5). E.g., ["owner/repo", "npm:@scope/pkg", "https://smithery.ai/server/name"]',
431
519
  },
432
520
  },
433
521
  required: ["repos"],
@@ -436,18 +524,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
436
524
  {
437
525
  name: "auto_gate",
438
526
  description:
439
- 'Should I install this? Returns a simple go/no-go decision with reasoning. The fastest way to check if a repo is safe to use. Returns { proceed: true/false, reason: "..." }.',
527
+ 'Should I install this? Returns a simple go/no-go decision with reasoning. Accepts any format: owner/repo, npm package, Smithery URL, etc. Returns { proceed: true/false, reason: "..." }.',
440
528
  inputSchema: {
441
529
  type: "object",
442
530
  properties: {
443
531
  repo: {
444
532
  type: "string",
445
- description: 'GitHub repo in "owner/repo" format',
533
+ description: 'Any of: "owner/repo", "npm:@scope/package", "@scope/package", Smithery URL, or OpenClaw URL',
446
534
  },
447
535
  },
448
536
  required: ["repo"],
449
537
  },
450
538
  },
539
+ {
540
+ name: "build_stack",
541
+ description:
542
+ 'Recommend a vetted stack of trusted tools for a described task. Describe what you\'re building (e.g., "Next.js app with auth, payments, and AI chat") and get back a curated list of the highest-scoring repos in each relevant category, pre-scored and ready to install. Returns tool names, trust scores, tiers, and install hints. Use this instead of guessing which tools to recommend — every suggestion is backed by live trust data.',
543
+ inputSchema: {
544
+ type: "object",
545
+ properties: {
546
+ description: {
547
+ type: "string",
548
+ description: 'What the user wants to build or accomplish. Can be a full sentence ("I need a Next.js app with auth and Stripe") or just keywords ("auth payments database mcp")',
549
+ },
550
+ },
551
+ required: ["description"],
552
+ },
553
+ },
451
554
  ],
452
555
  };
453
556
  });
@@ -461,12 +564,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
461
564
  switch (name) {
462
565
  case "check_trust_score": {
463
566
  const repo = args.repo;
464
- if (!repo || !repo.includes("/")) {
567
+ if (!repo || repo.trim().length === 0) {
465
568
  return {
466
569
  content: [
467
570
  {
468
571
  type: "text",
469
- text: 'Invalid repo format. Use "owner/repo" (e.g., "anthropics/anthropic-sdk-typescript").',
572
+ text: 'Missing input. Accepts: "owner/repo", npm package, GitHub URL, Smithery URL, or OpenClaw URL.',
470
573
  },
471
574
  ],
472
575
  isError: true,
@@ -475,9 +578,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
475
578
 
476
579
  const data = await fetchScore(repo, apiKey);
477
580
 
478
- // Determine if we got a full response or agent compact response
581
+ // Determine if we got a full response, partial, or agent compact response
479
582
  let formatted;
480
- if (data.signals && data.dimensions) {
583
+ if (data.limited || data.mode === 'partial') {
584
+ // Partial score — no source repo found
585
+ formatted = formatPartialResult(data);
586
+ } else if (data.signals && data.dimensions) {
481
587
  // Full paid response
482
588
  formatted = formatFullResult(data);
483
589
  } else if (data.safe !== undefined) {
@@ -485,7 +591,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
485
591
  formatted = formatAgentResponse(data);
486
592
  } else {
487
593
  // Human free response — format it for the agent
488
- formatted = `${formatTier(data.tier)} ${data.repo} — ${data.composite}/10\n\nSet MCPSKILLS_API_KEY for full signal breakdown.`;
594
+ formatted = `${formatTier(data.tier)} ${data.repo || data.package} — ${data.composite}/10\n\nSet MCPSKILLS_API_KEY for full signal breakdown.`;
489
595
  }
490
596
 
491
597
  return { content: [{ type: "text", text: formatted }] };
@@ -493,12 +599,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
493
599
 
494
600
  case "scan_safety": {
495
601
  const repo = args.repo;
496
- if (!repo || !repo.includes("/")) {
602
+ if (!repo || repo.trim().length === 0) {
497
603
  return {
498
604
  content: [
499
605
  {
500
606
  type: "text",
501
- text: 'Invalid repo format. Use "owner/repo".',
607
+ text: 'Missing input. Accepts: "owner/repo", npm package, GitHub URL, Smithery URL, or OpenClaw URL.',
502
608
  },
503
609
  ],
504
610
  isError: true,
@@ -555,9 +661,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
555
661
 
556
662
  case "get_badge": {
557
663
  const repo = args.repo;
558
- if (!repo || !repo.includes("/")) {
664
+ if (!repo || repo.trim().length === 0) {
559
665
  return {
560
- content: [{ type: "text", text: 'Invalid repo format. Use "owner/repo".' }],
666
+ content: [{ type: "text", text: 'Missing input. Accepts: "owner/repo", npm package, or registry URL.' }],
561
667
  isError: true,
562
668
  };
563
669
  }
@@ -593,9 +699,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
593
699
  case "watch_repo": {
594
700
  const repo = args.repo;
595
701
  const email = args.email;
596
- if (!repo || !repo.includes("/")) {
702
+ if (!repo || repo.trim().length === 0) {
597
703
  return {
598
- content: [{ type: "text", text: 'Invalid repo format. Use "owner/repo".' }],
704
+ content: [{ type: "text", text: 'Missing input. Accepts: "owner/repo", npm package, or registry URL.' }],
599
705
  isError: true,
600
706
  };
601
707
  }
@@ -703,8 +809,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
703
809
  const results = [];
704
810
 
705
811
  for (const repo of batch) {
706
- if (!repo.includes("/")) {
707
- results.push(`❌ ${repo} — invalid format`);
812
+ if (!repo || repo.trim().length === 0) {
813
+ results.push(`❌ (empty) — invalid input`);
708
814
  continue;
709
815
  }
710
816
  try {
@@ -740,9 +846,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
740
846
 
741
847
  case "auto_gate": {
742
848
  const repo = args.repo;
743
- if (!repo || !repo.includes("/")) {
849
+ if (!repo || repo.trim().length === 0) {
744
850
  return {
745
- content: [{ type: "text", text: 'Invalid repo format. Use "owner/repo".' }],
851
+ content: [{ type: "text", text: 'Missing input. Accepts: "owner/repo", npm package, or registry URL.' }],
746
852
  isError: true,
747
853
  };
748
854
  }
@@ -782,6 +888,198 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
782
888
  return { content: [{ type: "text", text }] };
783
889
  }
784
890
 
891
+ case "build_stack": {
892
+ const desc = args.description;
893
+ if (!desc || desc.trim().length === 0) {
894
+ return {
895
+ content: [{ type: "text", text: 'Describe what you\'re building. E.g., "Next.js app with auth, payments, and AI chat" or just "auth payments database".' }],
896
+ isError: true,
897
+ };
898
+ }
899
+
900
+ // Parse description into lowercase tokens for keyword matching
901
+ const tokens = desc.toLowerCase()
902
+ .replace(/[^a-z0-9\s\-_.]/g, ' ')
903
+ .split(/\s+/)
904
+ .filter(t => t.length > 1);
905
+
906
+ // Match categories by keyword overlap
907
+ const matchedCategories = [];
908
+ for (const [catId, cat] of Object.entries(STACK_CATEGORIES)) {
909
+ const overlap = cat.keywords.filter(kw => tokens.some(t => t.includes(kw) || kw.includes(t)));
910
+ if (overlap.length > 0) {
911
+ matchedCategories.push({ id: catId, name: cat.name, matchCount: overlap.length, matchedKeywords: overlap });
912
+ }
913
+ }
914
+ // Sort by match strength
915
+ matchedCategories.sort((a, b) => b.matchCount - a.matchCount);
916
+
917
+ // If no direct matches, try to infer from common phrases
918
+ if (matchedCategories.length === 0) {
919
+ // Fallback: match any token that's a substring of any keyword
920
+ for (const [catId, cat] of Object.entries(STACK_CATEGORIES)) {
921
+ if (tokens.some(t => cat.name.toLowerCase().includes(t))) {
922
+ matchedCategories.push({ id: catId, name: cat.name, matchCount: 1, matchedKeywords: [] });
923
+ }
924
+ }
925
+ }
926
+
927
+ if (matchedCategories.length === 0) {
928
+ return {
929
+ content: [{
930
+ type: "text",
931
+ text: `Couldn't match "${desc}" to any tool categories. Try being more specific, e.g.:\n- "auth and payments for a Next.js app"\n- "database orm ai agent"\n- "mcp server testing devops"\n\nAvailable categories: ${Object.values(STACK_CATEGORIES).map(c => c.name).join(', ')}`,
932
+ }],
933
+ };
934
+ }
935
+
936
+ // Fetch the full scored dataset (CDN-cached, one call)
937
+ let allRepos = [];
938
+ try {
939
+ const res = await fetch(DATA_URL, {
940
+ headers: { 'Accept': 'application/json', 'User-Agent': 'mcpskills-mcp-server' },
941
+ });
942
+ if (res.ok) {
943
+ const data = await res.json();
944
+ allRepos = data.rows || [];
945
+ }
946
+ } catch {}
947
+
948
+ // Build keyword-to-category index for repo matching
949
+ // For each repo in the dataset, check if its description/key matches a category
950
+ function categorizeRepo(row) {
951
+ const searchText = `${row.key || ''} ${row.description || ''}`.toLowerCase();
952
+ const cats = [];
953
+ for (const [catId, cat] of Object.entries(STACK_CATEGORIES)) {
954
+ const hits = cat.keywords.filter(kw => searchText.includes(kw));
955
+ if (hits.length >= 1) cats.push(catId);
956
+ }
957
+ return cats;
958
+ }
959
+
960
+ // For each matched category, find the top-scored non-blocked repo
961
+ const stack = [];
962
+ const usedRepos = new Set();
963
+
964
+ for (const cat of matchedCategories) {
965
+ const candidates = allRepos
966
+ .filter(r => {
967
+ if (usedRepos.has(r.key)) return false;
968
+ if (r.tier === 'blocked') return false;
969
+ const repoCats = categorizeRepo(r);
970
+ return repoCats.includes(cat.id);
971
+ })
972
+ .sort((a, b) => (b.composite || 0) - (a.composite || 0));
973
+
974
+ const pick = candidates[0];
975
+ if (pick) {
976
+ usedRepos.add(pick.key);
977
+ stack.push({
978
+ category: cat.name,
979
+ categoryId: cat.id,
980
+ repo: pick.key,
981
+ score: pick.composite,
982
+ tier: pick.tier,
983
+ description: (pick.description || '').slice(0, 120),
984
+ });
985
+ } else {
986
+ stack.push({
987
+ category: cat.name,
988
+ categoryId: cat.id,
989
+ repo: null,
990
+ score: null,
991
+ tier: null,
992
+ description: 'No scored repos in this category yet',
993
+ });
994
+ }
995
+ }
996
+
997
+ // Add cross-category suggestions for any categories not already matched
998
+ const matchedIds = new Set(matchedCategories.map(c => c.id));
999
+ const suggestions = new Set();
1000
+ for (const cat of matchedCategories) {
1001
+ const adjacent = STACK_CROSS_CATEGORY[cat.id] || [];
1002
+ for (const adjId of adjacent) {
1003
+ if (!matchedIds.has(adjId) && !suggestions.has(adjId)) {
1004
+ suggestions.add(adjId);
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ const suggestedTools = [];
1010
+ for (const adjId of suggestions) {
1011
+ const catDef = STACK_CATEGORIES[adjId];
1012
+ if (!catDef) continue;
1013
+ const candidates = allRepos
1014
+ .filter(r => !usedRepos.has(r.key) && r.tier !== 'blocked' && categorizeRepo(r).includes(adjId))
1015
+ .sort((a, b) => (b.composite || 0) - (a.composite || 0));
1016
+ const pick = candidates[0];
1017
+ if (pick) {
1018
+ suggestedTools.push({
1019
+ category: catDef.name,
1020
+ repo: pick.key,
1021
+ score: pick.composite,
1022
+ tier: pick.tier,
1023
+ reason: `Often used alongside ${matchedCategories[0]?.name || 'your stack'}`,
1024
+ });
1025
+ }
1026
+ if (suggestedTools.length >= 3) break;
1027
+ }
1028
+
1029
+ // Compute stack trust score
1030
+ const scoredItems = stack.filter(s => typeof s.score === 'number');
1031
+ const avgScore = scoredItems.length > 0
1032
+ ? +(scoredItems.reduce((sum, s) => sum + s.score, 0) / scoredItems.length).toFixed(1)
1033
+ : null;
1034
+ const allVerified = scoredItems.every(s => s.tier === 'verified');
1035
+
1036
+ // Format output
1037
+ const lines = [
1038
+ `# Recommended Stack`,
1039
+ ``,
1040
+ `Based on: "${desc}"`,
1041
+ `Stack Trust Score: ${avgScore ?? '—'}/10${allVerified ? ' ✅ All Verified' : ''}`,
1042
+ ``,
1043
+ `## Core Tools (${stack.length})`,
1044
+ ];
1045
+
1046
+ for (const item of stack) {
1047
+ if (item.repo) {
1048
+ const icon = item.tier === 'verified' ? '✅' : item.tier === 'established' ? '🟡' : '⚪';
1049
+ lines.push(` ${icon} **${item.repo}** — ${item.score}/10 ${item.tier}`);
1050
+ if (item.description) lines.push(` ${item.description}`);
1051
+ lines.push(` Category: ${item.category}`);
1052
+ lines.push(` Score page: https://mcpskills.io/score/${encodeURI(item.repo)}`);
1053
+ } else {
1054
+ lines.push(` ❓ **${item.category}** — no scored repos yet`);
1055
+ }
1056
+ lines.push('');
1057
+ }
1058
+
1059
+ if (suggestedTools.length > 0) {
1060
+ lines.push(`## You Might Also Need`);
1061
+ for (const sug of suggestedTools) {
1062
+ const icon = sug.tier === 'verified' ? '✅' : sug.tier === 'established' ? '🟡' : '⚪';
1063
+ lines.push(` ${icon} **${sug.repo}** — ${sug.score}/10 ${sug.tier}`);
1064
+ lines.push(` ${sug.reason}`);
1065
+ }
1066
+ lines.push('');
1067
+ }
1068
+
1069
+ if (apiKey) {
1070
+ lines.push(`---`);
1071
+ lines.push(`Full signal breakdown available for each tool via check_trust_score.`);
1072
+ } else {
1073
+ lines.push(`---`);
1074
+ lines.push(`Set MCPSKILLS_API_KEY for full 14-signal reports on each tool.`);
1075
+ lines.push(`Get a key at https://mcpskills.io`);
1076
+ }
1077
+
1078
+ lines.push('', 'Powered by mcpskills.io — every recommendation backed by live trust data.');
1079
+
1080
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1081
+ }
1082
+
785
1083
  default:
786
1084
  return {
787
1085
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mcpskillsio/server",
3
- "version": "2.1.1",
4
- "description": "Trust-score any AI skill or MCP server from inside Claude Code, Cursor, or any MCP client. 14 signals, safety scanning, recommendations, badges, monitoring, batch checking, and auto-gate decisions.",
3
+ "version": "2.3.0",
4
+ "description": "Trust-score any AI skill or MCP server from inside Claude Code, Cursor, or any MCP client. Accepts GitHub repos, npm packages, Smithery URLs, and OpenClaw skills. 14 signals, safety scanning, recommendations, badges, monitoring, batch checking, auto-gate decisions, and stack building from live trust data.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {