@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.
- package/.netlify/netlify.toml +332 -0
- package/index.js +326 -28
- package/package.json +2 -2
|
@@ -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.
|
|
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
|
|
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
|
-
'
|
|
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
|
|
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
|
-
'
|
|
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
|
|
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: '
|
|
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.
|
|
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: '
|
|
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
|
|
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
|
|
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.
|
|
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: '
|
|
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 ||
|
|
567
|
+
if (!repo || repo.trim().length === 0) {
|
|
465
568
|
return {
|
|
466
569
|
content: [
|
|
467
570
|
{
|
|
468
571
|
type: "text",
|
|
469
|
-
text: '
|
|
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.
|
|
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 ||
|
|
602
|
+
if (!repo || repo.trim().length === 0) {
|
|
497
603
|
return {
|
|
498
604
|
content: [
|
|
499
605
|
{
|
|
500
606
|
type: "text",
|
|
501
|
-
text: '
|
|
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 ||
|
|
664
|
+
if (!repo || repo.trim().length === 0) {
|
|
559
665
|
return {
|
|
560
|
-
content: [{ type: "text", text: '
|
|
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 ||
|
|
702
|
+
if (!repo || repo.trim().length === 0) {
|
|
597
703
|
return {
|
|
598
|
-
content: [{ type: "text", text: '
|
|
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.
|
|
707
|
-
results.push(`❌
|
|
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 ||
|
|
849
|
+
if (!repo || repo.trim().length === 0) {
|
|
744
850
|
return {
|
|
745
|
-
content: [{ type: "text", text: '
|
|
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.
|
|
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,
|
|
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": {
|