skillmesh-mcp 1.0.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/README.md +174 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2090 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2090 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os2 from 'os';
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import * as yaml from 'js-yaml';
|
|
10
|
+
|
|
11
|
+
var { existsSync } = fs;
|
|
12
|
+
var AGENT_PATHS = {
|
|
13
|
+
"claude-code": {
|
|
14
|
+
projectPath: ".claude/skills",
|
|
15
|
+
globalPath: "~/.claude/skills"
|
|
16
|
+
},
|
|
17
|
+
"antigravity": {
|
|
18
|
+
projectPath: ".agent/skills",
|
|
19
|
+
globalPath: "~/.gemini/antigravity/global_skills"
|
|
20
|
+
},
|
|
21
|
+
"cursor": {
|
|
22
|
+
projectPath: ".cursor/skills",
|
|
23
|
+
globalPath: "~/.cursor/skills"
|
|
24
|
+
},
|
|
25
|
+
"windsurf": {
|
|
26
|
+
projectPath: ".windsurf/skills",
|
|
27
|
+
globalPath: "~/.codeium/windsurf/skills"
|
|
28
|
+
},
|
|
29
|
+
"gemini-cli": {
|
|
30
|
+
projectPath: ".gemini/skills",
|
|
31
|
+
globalPath: "~/.gemini/skills"
|
|
32
|
+
},
|
|
33
|
+
"codex-cli": {
|
|
34
|
+
projectPath: ".codex/skills",
|
|
35
|
+
globalPath: "~/.codex/skills"
|
|
36
|
+
},
|
|
37
|
+
"kilocode": {
|
|
38
|
+
projectPath: ".kilocode/skills",
|
|
39
|
+
globalPath: "~/.kilocode/skills"
|
|
40
|
+
},
|
|
41
|
+
"trae": {
|
|
42
|
+
projectPath: ".trae/skills",
|
|
43
|
+
globalPath: "~/.trae/skills"
|
|
44
|
+
},
|
|
45
|
+
"void": {
|
|
46
|
+
projectPath: ".void/skills",
|
|
47
|
+
globalPath: "~/.void/skills"
|
|
48
|
+
},
|
|
49
|
+
"continue": {
|
|
50
|
+
projectPath: ".continue/skills",
|
|
51
|
+
globalPath: "~/.continue/skills"
|
|
52
|
+
},
|
|
53
|
+
"cline": {
|
|
54
|
+
projectPath: ".cline/skills",
|
|
55
|
+
globalPath: "~/.cline/skills"
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function getSkillPath(agentId, projectPath, skillName, global = false) {
|
|
59
|
+
const agentConfig = AGENT_PATHS[agentId];
|
|
60
|
+
if (!agentConfig) {
|
|
61
|
+
const fallbackPath = global ? `~/.${agentId}/skills` : `.${agentId}/skills`;
|
|
62
|
+
const basePath2 = global ? expandTilde(fallbackPath) : path.join(projectPath, fallbackPath);
|
|
63
|
+
return path.join(basePath2, skillName, "SKILL.md");
|
|
64
|
+
}
|
|
65
|
+
const basePath = global ? expandTilde(agentConfig.globalPath) : path.join(projectPath, agentConfig.projectPath);
|
|
66
|
+
return path.join(basePath, skillName, "SKILL.md");
|
|
67
|
+
}
|
|
68
|
+
async function detectInstalledAgents(projectPath) {
|
|
69
|
+
const detected = [];
|
|
70
|
+
for (const [agentId, config] of Object.entries(AGENT_PATHS)) {
|
|
71
|
+
const baseDir = config.projectPath.split("/")[0];
|
|
72
|
+
const agentDirPath = path.join(projectPath, baseDir);
|
|
73
|
+
if (existsSync(agentDirPath)) {
|
|
74
|
+
detected.push(agentId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return detected;
|
|
78
|
+
}
|
|
79
|
+
function expandTilde(filepath) {
|
|
80
|
+
if (filepath.startsWith("~/") || filepath === "~") {
|
|
81
|
+
return path.join(os2.homedir(), filepath.slice(1));
|
|
82
|
+
}
|
|
83
|
+
return filepath;
|
|
84
|
+
}
|
|
85
|
+
var { existsSync: existsSync2 } = fs;
|
|
86
|
+
var DetectAgentsInputSchema = z.object({
|
|
87
|
+
projectPath: z.string().describe("Absolute path to project root directory"),
|
|
88
|
+
includeGlobal: z.boolean().optional().default(false).describe("Also scan home directory for globally installed agents")
|
|
89
|
+
});
|
|
90
|
+
async function detectAgents(input) {
|
|
91
|
+
try {
|
|
92
|
+
const { projectPath, includeGlobal } = input;
|
|
93
|
+
const projectAgents = await detectInstalledAgents(projectPath);
|
|
94
|
+
const agentMap = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const agentId of projectAgents) {
|
|
96
|
+
const config = AGENT_PATHS[agentId];
|
|
97
|
+
if (!config) continue;
|
|
98
|
+
const projectSkillPath = path.join(projectPath, config.projectPath);
|
|
99
|
+
const globalSkillPath = config.globalPath.replace(/^~/, os2.homedir());
|
|
100
|
+
agentMap.set(agentId, {
|
|
101
|
+
id: agentId,
|
|
102
|
+
name: agentId,
|
|
103
|
+
projectSkillPath,
|
|
104
|
+
globalSkillPath,
|
|
105
|
+
foundInProject: true,
|
|
106
|
+
foundGlobally: false
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (includeGlobal) {
|
|
110
|
+
const homeDir = os2.homedir();
|
|
111
|
+
for (const [agentId, config] of Object.entries(AGENT_PATHS)) {
|
|
112
|
+
const globalSkillPath = config.globalPath.replace(/^~/, homeDir);
|
|
113
|
+
const globalConfigDir = path.dirname(globalSkillPath);
|
|
114
|
+
const foundGlobally = existsSync2(globalConfigDir);
|
|
115
|
+
if (agentMap.has(agentId)) {
|
|
116
|
+
agentMap.get(agentId).foundGlobally = foundGlobally;
|
|
117
|
+
} else if (foundGlobally) {
|
|
118
|
+
const projectSkillPath = path.join(projectPath, config.projectPath);
|
|
119
|
+
agentMap.set(agentId, {
|
|
120
|
+
id: agentId,
|
|
121
|
+
name: agentId,
|
|
122
|
+
projectSkillPath,
|
|
123
|
+
globalSkillPath,
|
|
124
|
+
foundInProject: false,
|
|
125
|
+
foundGlobally: true
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const agents = Array.from(agentMap.values());
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
agents,
|
|
134
|
+
projectRoot: projectPath
|
|
135
|
+
};
|
|
136
|
+
} catch (err) {
|
|
137
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
agents: [],
|
|
141
|
+
projectRoot: input.projectPath,
|
|
142
|
+
error: message
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
var { ensureDirSync, readJsonSync, writeJsonSync, existsSync: existsSync3 } = fs;
|
|
147
|
+
var DEFAULT_CONFIG = {
|
|
148
|
+
version: "1.0.0",
|
|
149
|
+
installedSkills: []
|
|
150
|
+
};
|
|
151
|
+
function getConfigPath(projectRoot) {
|
|
152
|
+
return path.join(projectRoot, ".skillmesh", "skillmesh.config.json");
|
|
153
|
+
}
|
|
154
|
+
async function readConfig(projectRoot) {
|
|
155
|
+
const configPath = getConfigPath(projectRoot);
|
|
156
|
+
try {
|
|
157
|
+
if (!existsSync3(configPath)) {
|
|
158
|
+
return { ...DEFAULT_CONFIG };
|
|
159
|
+
}
|
|
160
|
+
const config = readJsonSync(configPath);
|
|
161
|
+
return {
|
|
162
|
+
version: config.version || DEFAULT_CONFIG.version,
|
|
163
|
+
installedSkills: config.installedSkills || []
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error("Failed to read config, returning default:", error);
|
|
167
|
+
return { ...DEFAULT_CONFIG };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function writeConfig(projectRoot, config) {
|
|
171
|
+
const configPath = getConfigPath(projectRoot);
|
|
172
|
+
try {
|
|
173
|
+
ensureDirSync(path.dirname(configPath));
|
|
174
|
+
writeJsonSync(configPath, config, { spaces: 2 });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error("Failed to write config:", error);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function trackInstalledSkill(projectRoot, skillName, agents, source) {
|
|
181
|
+
try {
|
|
182
|
+
const config = await readConfig(projectRoot);
|
|
183
|
+
const existingIndex = config.installedSkills.findIndex(
|
|
184
|
+
(skill) => skill.name === skillName
|
|
185
|
+
);
|
|
186
|
+
if (existingIndex !== -1) {
|
|
187
|
+
const existing = config.installedSkills[existingIndex];
|
|
188
|
+
const mergedAgents = Array.from(/* @__PURE__ */ new Set([...existing.agents, ...agents]));
|
|
189
|
+
config.installedSkills[existingIndex] = {
|
|
190
|
+
name: skillName,
|
|
191
|
+
source: source || existing.source,
|
|
192
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
|
+
agents: mergedAgents
|
|
194
|
+
};
|
|
195
|
+
} else {
|
|
196
|
+
config.installedSkills.push({
|
|
197
|
+
name: skillName,
|
|
198
|
+
source,
|
|
199
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
200
|
+
agents
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
await writeConfig(projectRoot, config);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error("Failed to track installed skill:", error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function untrackSkill(projectRoot, skillName, agentName) {
|
|
209
|
+
try {
|
|
210
|
+
const config = await readConfig(projectRoot);
|
|
211
|
+
const skillIndex = config.installedSkills.findIndex(
|
|
212
|
+
(skill2) => skill2.name === skillName
|
|
213
|
+
);
|
|
214
|
+
if (skillIndex === -1) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const skill = config.installedSkills[skillIndex];
|
|
218
|
+
skill.agents = skill.agents.filter((agent) => agent !== agentName);
|
|
219
|
+
if (skill.agents.length === 0) {
|
|
220
|
+
config.installedSkills.splice(skillIndex, 1);
|
|
221
|
+
} else {
|
|
222
|
+
config.installedSkills[skillIndex] = skill;
|
|
223
|
+
}
|
|
224
|
+
await writeConfig(projectRoot, config);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error("Failed to untrack skill:", error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
var { ensureDirSync: ensureDirSync2, existsSync: existsSync4 } = fs;
|
|
230
|
+
var CreateFolderInputSchema = z.object({
|
|
231
|
+
projectPath: z.string().describe("Absolute path to project root directory"),
|
|
232
|
+
agents: z.array(z.string()).describe("List of agent names to create folders for"),
|
|
233
|
+
global: z.boolean().optional().default(false).describe("Create in global scope instead of project scope")
|
|
234
|
+
});
|
|
235
|
+
async function createFolder(input) {
|
|
236
|
+
try {
|
|
237
|
+
const { projectPath, agents, global } = input;
|
|
238
|
+
const results = [];
|
|
239
|
+
for (const agentId of agents) {
|
|
240
|
+
const config = AGENT_PATHS[agentId];
|
|
241
|
+
if (!config) {
|
|
242
|
+
results.push({
|
|
243
|
+
agent: agentId,
|
|
244
|
+
path: "",
|
|
245
|
+
created: false,
|
|
246
|
+
alreadyExists: false
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
let skillDirPath;
|
|
251
|
+
if (global) {
|
|
252
|
+
skillDirPath = config.globalPath.replace(/^~/, os2.homedir());
|
|
253
|
+
} else {
|
|
254
|
+
skillDirPath = path.join(projectPath, config.projectPath);
|
|
255
|
+
}
|
|
256
|
+
const alreadyExists = existsSync4(skillDirPath);
|
|
257
|
+
try {
|
|
258
|
+
ensureDirSync2(skillDirPath);
|
|
259
|
+
results.push({
|
|
260
|
+
agent: agentId,
|
|
261
|
+
path: skillDirPath,
|
|
262
|
+
created: !alreadyExists,
|
|
263
|
+
alreadyExists
|
|
264
|
+
});
|
|
265
|
+
} catch (err) {
|
|
266
|
+
results.push({
|
|
267
|
+
agent: agentId,
|
|
268
|
+
path: skillDirPath,
|
|
269
|
+
created: false,
|
|
270
|
+
alreadyExists: false
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
let configCreated = false;
|
|
275
|
+
let configPath;
|
|
276
|
+
if (!global) {
|
|
277
|
+
try {
|
|
278
|
+
const skillmeshDir = path.join(projectPath, ".skillmesh");
|
|
279
|
+
const configFilePath = path.join(skillmeshDir, "skillmesh.config.json");
|
|
280
|
+
const configExists = existsSync4(configFilePath);
|
|
281
|
+
ensureDirSync2(skillmeshDir);
|
|
282
|
+
if (!configExists) {
|
|
283
|
+
await writeConfig(projectPath, {
|
|
284
|
+
version: "1.0.0",
|
|
285
|
+
installedSkills: []
|
|
286
|
+
});
|
|
287
|
+
configCreated = true;
|
|
288
|
+
}
|
|
289
|
+
configPath = configFilePath;
|
|
290
|
+
} catch (err) {
|
|
291
|
+
configCreated = false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
success: true,
|
|
296
|
+
results,
|
|
297
|
+
configCreated,
|
|
298
|
+
configPath
|
|
299
|
+
};
|
|
300
|
+
} catch (err) {
|
|
301
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
302
|
+
return {
|
|
303
|
+
success: false,
|
|
304
|
+
results: [],
|
|
305
|
+
configCreated: false,
|
|
306
|
+
error: message
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/lib/tavily.ts
|
|
312
|
+
async function searchTavily(query, maxResults = 10) {
|
|
313
|
+
const tavilyKey = process.env.TAVILY_API_KEY;
|
|
314
|
+
if (!tavilyKey) {
|
|
315
|
+
console.error("TAVILY_API_KEY not found in environment, trying Firecrawl fallback");
|
|
316
|
+
return searchFirecrawl(query, maxResults);
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const controller = new AbortController();
|
|
320
|
+
const timeoutId = setTimeout(() => controller.abort(), 15e3);
|
|
321
|
+
const response = await fetch("https://api.tavily.com/search", {
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: {
|
|
324
|
+
"Content-Type": "application/json"
|
|
325
|
+
},
|
|
326
|
+
body: JSON.stringify({
|
|
327
|
+
api_key: tavilyKey,
|
|
328
|
+
query,
|
|
329
|
+
max_results: maxResults,
|
|
330
|
+
search_depth: "basic",
|
|
331
|
+
include_answer: false,
|
|
332
|
+
include_images: false
|
|
333
|
+
}),
|
|
334
|
+
signal: controller.signal
|
|
335
|
+
});
|
|
336
|
+
clearTimeout(timeoutId);
|
|
337
|
+
if (!response.ok) {
|
|
338
|
+
console.error("Tavily API error:", response.status, response.statusText);
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
const data = await response.json();
|
|
342
|
+
if (!data.results || !Array.isArray(data.results)) {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
return data.results.map((result) => ({
|
|
346
|
+
title: result.title || "",
|
|
347
|
+
url: result.url || "",
|
|
348
|
+
snippet: result.content || "",
|
|
349
|
+
score: result.score
|
|
350
|
+
}));
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error("Tavily search failed:", error);
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function searchFirecrawl(query, maxResults = 10) {
|
|
357
|
+
const firecrawlKey = process.env.FIRECRAWL_API_KEY;
|
|
358
|
+
if (!firecrawlKey) {
|
|
359
|
+
console.error("Neither TAVILY_API_KEY nor FIRECRAWL_API_KEY found in environment");
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const controller = new AbortController();
|
|
364
|
+
const timeoutId = setTimeout(() => controller.abort(), 15e3);
|
|
365
|
+
const response = await fetch("https://api.firecrawl.dev/v1/search", {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: {
|
|
368
|
+
"Content-Type": "application/json",
|
|
369
|
+
"Authorization": `Bearer ${firecrawlKey}`
|
|
370
|
+
},
|
|
371
|
+
body: JSON.stringify({
|
|
372
|
+
query,
|
|
373
|
+
limit: maxResults
|
|
374
|
+
}),
|
|
375
|
+
signal: controller.signal
|
|
376
|
+
});
|
|
377
|
+
clearTimeout(timeoutId);
|
|
378
|
+
if (!response.ok) {
|
|
379
|
+
console.error("Firecrawl API error:", response.status, response.statusText);
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
const data = await response.json();
|
|
383
|
+
if (!data.data || !Array.isArray(data.data)) {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
return data.data.map((result) => ({
|
|
387
|
+
title: result.title || "",
|
|
388
|
+
url: result.url || "",
|
|
389
|
+
snippet: result.description || result.content || "",
|
|
390
|
+
score: result.score
|
|
391
|
+
}));
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error("Firecrawl search failed:", error);
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/lib/github.ts
|
|
399
|
+
async function fetchSkillFromGitHub(owner, repo, skillName, githubToken) {
|
|
400
|
+
const baseUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main`;
|
|
401
|
+
const paths = [
|
|
402
|
+
`${baseUrl}/.claude/skills/${skillName}/SKILL.md`,
|
|
403
|
+
`${baseUrl}/SKILL.md`,
|
|
404
|
+
`${baseUrl}/skills/${skillName}/SKILL.md`
|
|
405
|
+
];
|
|
406
|
+
for (const url of paths) {
|
|
407
|
+
const result = await fetchWithTimeout(url, githubToken);
|
|
408
|
+
if (result.found) {
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const searchResult = await searchGitHubForSkill(owner, repo, skillName, githubToken);
|
|
413
|
+
if (searchResult.found) {
|
|
414
|
+
return searchResult;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
content: "",
|
|
418
|
+
url: "",
|
|
419
|
+
found: false
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
async function getGitHubStars(owner, repo, githubToken) {
|
|
423
|
+
const url = `https://api.github.com/repos/${owner}/${repo}`;
|
|
424
|
+
try {
|
|
425
|
+
const controller = new AbortController();
|
|
426
|
+
const timeoutId = setTimeout(() => controller.abort(), 15e3);
|
|
427
|
+
const headers = {
|
|
428
|
+
"Accept": "application/vnd.github.v3+json",
|
|
429
|
+
"User-Agent": "skillmesh-mcp"
|
|
430
|
+
};
|
|
431
|
+
if (githubToken) {
|
|
432
|
+
headers["Authorization"] = `Bearer ${githubToken}`;
|
|
433
|
+
}
|
|
434
|
+
const response = await fetch(url, {
|
|
435
|
+
headers,
|
|
436
|
+
signal: controller.signal
|
|
437
|
+
});
|
|
438
|
+
clearTimeout(timeoutId);
|
|
439
|
+
if (!response.ok) {
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
const data = await response.json();
|
|
443
|
+
return data.stargazers_count || 0;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.error(`Failed to fetch star count for ${owner}/${repo}:`, error);
|
|
446
|
+
return 0;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function getLastUpdated(owner, repo, githubToken) {
|
|
450
|
+
const url = `https://api.github.com/repos/${owner}/${repo}`;
|
|
451
|
+
try {
|
|
452
|
+
const controller = new AbortController();
|
|
453
|
+
const timeoutId = setTimeout(() => controller.abort(), 15e3);
|
|
454
|
+
const headers = {
|
|
455
|
+
"Accept": "application/vnd.github.v3+json",
|
|
456
|
+
"User-Agent": "skillmesh-mcp"
|
|
457
|
+
};
|
|
458
|
+
if (githubToken) {
|
|
459
|
+
headers["Authorization"] = `Bearer ${githubToken}`;
|
|
460
|
+
}
|
|
461
|
+
const response = await fetch(url, {
|
|
462
|
+
headers,
|
|
463
|
+
signal: controller.signal
|
|
464
|
+
});
|
|
465
|
+
clearTimeout(timeoutId);
|
|
466
|
+
if (!response.ok) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
const data = await response.json();
|
|
470
|
+
const updatedAt = data.updated_at || data.pushed_at;
|
|
471
|
+
return updatedAt ? new Date(updatedAt) : null;
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error(`Failed to fetch last updated for ${owner}/${repo}:`, error);
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function fetchWithTimeout(url, githubToken) {
|
|
478
|
+
try {
|
|
479
|
+
const controller = new AbortController();
|
|
480
|
+
const timeoutId = setTimeout(() => controller.abort(), 15e3);
|
|
481
|
+
const headers = {
|
|
482
|
+
"User-Agent": "skillmesh-mcp"
|
|
483
|
+
};
|
|
484
|
+
if (githubToken && url.includes("api.github.com")) {
|
|
485
|
+
headers["Authorization"] = `Bearer ${githubToken}`;
|
|
486
|
+
}
|
|
487
|
+
const response = await fetch(url, {
|
|
488
|
+
headers,
|
|
489
|
+
signal: controller.signal
|
|
490
|
+
});
|
|
491
|
+
clearTimeout(timeoutId);
|
|
492
|
+
if (!response.ok) {
|
|
493
|
+
return {
|
|
494
|
+
content: "",
|
|
495
|
+
url: "",
|
|
496
|
+
found: false
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const content = await response.text();
|
|
500
|
+
return {
|
|
501
|
+
content,
|
|
502
|
+
url,
|
|
503
|
+
found: true
|
|
504
|
+
};
|
|
505
|
+
} catch (error) {
|
|
506
|
+
return {
|
|
507
|
+
content: "",
|
|
508
|
+
url: "",
|
|
509
|
+
found: false
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function searchGitHubForSkill(owner, repo, skillName, githubToken) {
|
|
514
|
+
if (!githubToken) {
|
|
515
|
+
return {
|
|
516
|
+
content: "",
|
|
517
|
+
url: "",
|
|
518
|
+
found: false
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const controller = new AbortController();
|
|
523
|
+
const timeoutId = setTimeout(() => controller.abort(), 15e3);
|
|
524
|
+
const query = encodeURIComponent(
|
|
525
|
+
`filename:SKILL.md repo:${owner}/${repo}`
|
|
526
|
+
);
|
|
527
|
+
const searchUrl = `https://api.github.com/search/code?q=${query}`;
|
|
528
|
+
const response = await fetch(searchUrl, {
|
|
529
|
+
headers: {
|
|
530
|
+
"Accept": "application/vnd.github.v3+json",
|
|
531
|
+
"Authorization": `Bearer ${githubToken}`,
|
|
532
|
+
"User-Agent": "skillmesh-mcp"
|
|
533
|
+
},
|
|
534
|
+
signal: controller.signal
|
|
535
|
+
});
|
|
536
|
+
clearTimeout(timeoutId);
|
|
537
|
+
if (!response.ok) {
|
|
538
|
+
return {
|
|
539
|
+
content: "",
|
|
540
|
+
url: "",
|
|
541
|
+
found: false
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const data = await response.json();
|
|
545
|
+
if (!data.items || data.items.length === 0) {
|
|
546
|
+
return {
|
|
547
|
+
content: "",
|
|
548
|
+
url: "",
|
|
549
|
+
found: false
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
let bestMatch = data.items[0];
|
|
553
|
+
for (const item of data.items) {
|
|
554
|
+
if (item.path.includes(skillName)) {
|
|
555
|
+
bestMatch = item;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/${bestMatch.path}`;
|
|
560
|
+
return await fetchWithTimeout(rawUrl, githubToken);
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.error("GitHub search failed:", error);
|
|
563
|
+
return {
|
|
564
|
+
content: "",
|
|
565
|
+
url: "",
|
|
566
|
+
found: false
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/tools/search-skills.ts
|
|
572
|
+
var SearchSkillsInputSchema = z.object({
|
|
573
|
+
keyword: z.string().describe('Search keyword or skill domain (e.g., "nextjs-app-router", "prisma")'),
|
|
574
|
+
minStars: z.number().optional().default(50).describe("Minimum GitHub stars filter"),
|
|
575
|
+
maxAgeMonths: z.number().optional().default(12).describe("Maximum age in months (updated within)"),
|
|
576
|
+
limit: z.number().optional().default(10).describe("Maximum number of results")
|
|
577
|
+
});
|
|
578
|
+
async function searchSkills(input) {
|
|
579
|
+
try {
|
|
580
|
+
const { keyword, minStars, maxAgeMonths, limit } = input;
|
|
581
|
+
const searchQuery = `site:skills.sh ${keyword}`;
|
|
582
|
+
const searchResults = await searchTavily(searchQuery, limit * 3);
|
|
583
|
+
const qualityResults = [];
|
|
584
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
585
|
+
for (const result of searchResults) {
|
|
586
|
+
const githubMatch = result.url.match(/github\.com\/([^\/]+)\/([^\/\?#]+)/);
|
|
587
|
+
if (!githubMatch) {
|
|
588
|
+
const pathMatch = result.url.match(/skills\.sh\/([^\/]+)\/([^\/\?#]+)/);
|
|
589
|
+
if (pathMatch) {
|
|
590
|
+
const [, owner2, repo2] = pathMatch;
|
|
591
|
+
try {
|
|
592
|
+
const stars = await getGitHubStars(owner2, repo2, githubToken);
|
|
593
|
+
const lastUpdated = await getLastUpdated(owner2, repo2, githubToken);
|
|
594
|
+
if (stars < minStars) continue;
|
|
595
|
+
if (lastUpdated) {
|
|
596
|
+
const ageMonths = (Date.now() - lastUpdated.getTime()) / (1e3 * 60 * 60 * 24 * 30);
|
|
597
|
+
if (ageMonths > maxAgeMonths) continue;
|
|
598
|
+
}
|
|
599
|
+
const recencyBonus = lastUpdated ? Math.max(0, 100 - (Date.now() - lastUpdated.getTime()) / (1e3 * 60 * 60 * 24 * 30)) : 0;
|
|
600
|
+
const score = stars + recencyBonus + (result.score || 0) * 10;
|
|
601
|
+
qualityResults.push({
|
|
602
|
+
name: repo2,
|
|
603
|
+
repo: repo2,
|
|
604
|
+
owner: owner2,
|
|
605
|
+
description: result.snippet,
|
|
606
|
+
stars,
|
|
607
|
+
lastUpdated: lastUpdated ? lastUpdated.toISOString() : "unknown",
|
|
608
|
+
url: result.url,
|
|
609
|
+
score
|
|
610
|
+
});
|
|
611
|
+
} catch (err) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const [, owner, repo] = githubMatch;
|
|
618
|
+
try {
|
|
619
|
+
const stars = await getGitHubStars(owner, repo, githubToken);
|
|
620
|
+
const lastUpdated = await getLastUpdated(owner, repo, githubToken);
|
|
621
|
+
if (stars < minStars) continue;
|
|
622
|
+
if (lastUpdated) {
|
|
623
|
+
const ageMonths = (Date.now() - lastUpdated.getTime()) / (1e3 * 60 * 60 * 24 * 30);
|
|
624
|
+
if (ageMonths > maxAgeMonths) continue;
|
|
625
|
+
}
|
|
626
|
+
const recencyBonus = lastUpdated ? Math.max(0, 100 - (Date.now() - lastUpdated.getTime()) / (1e3 * 60 * 60 * 24 * 30)) : 0;
|
|
627
|
+
const score = stars + recencyBonus + (result.score || 0) * 10;
|
|
628
|
+
qualityResults.push({
|
|
629
|
+
name: repo,
|
|
630
|
+
repo,
|
|
631
|
+
owner,
|
|
632
|
+
description: result.snippet,
|
|
633
|
+
stars,
|
|
634
|
+
lastUpdated: lastUpdated ? lastUpdated.toISOString() : "unknown",
|
|
635
|
+
url: result.url,
|
|
636
|
+
score
|
|
637
|
+
});
|
|
638
|
+
} catch (err) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
qualityResults.sort((a, b) => b.score - a.score);
|
|
643
|
+
const limitedResults = qualityResults.slice(0, limit);
|
|
644
|
+
return {
|
|
645
|
+
success: true,
|
|
646
|
+
results: limitedResults,
|
|
647
|
+
query: keyword,
|
|
648
|
+
totalFound: qualityResults.length
|
|
649
|
+
};
|
|
650
|
+
} catch (err) {
|
|
651
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
652
|
+
return {
|
|
653
|
+
success: false,
|
|
654
|
+
results: [],
|
|
655
|
+
query: input.keyword,
|
|
656
|
+
totalFound: 0,
|
|
657
|
+
error: message
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
var SkillMetadataSchema = z.object({
|
|
662
|
+
name: z.string().min(1, "Skill name is required"),
|
|
663
|
+
description: z.string().min(1, "Description is required"),
|
|
664
|
+
tags: z.array(z.string()).optional().default([]),
|
|
665
|
+
sources: z.array(z.string().url()).optional().default([]),
|
|
666
|
+
created_by: z.string().optional(),
|
|
667
|
+
created_at: z.string().optional()
|
|
668
|
+
});
|
|
669
|
+
function parseSkillMd(content) {
|
|
670
|
+
try {
|
|
671
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
672
|
+
const match = content.match(frontmatterRegex);
|
|
673
|
+
if (!match) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
const [, frontmatterStr, bodyContent] = match;
|
|
677
|
+
const parsedYaml = yaml.load(frontmatterStr);
|
|
678
|
+
if (!parsedYaml || typeof parsedYaml !== "object") {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
const validationResult = SkillMetadataSchema.safeParse(parsedYaml);
|
|
682
|
+
if (!validationResult.success) {
|
|
683
|
+
console.error("Skill metadata validation failed:", validationResult.error.flatten());
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
metadata: validationResult.data,
|
|
688
|
+
content: bodyContent.trim(),
|
|
689
|
+
raw: content
|
|
690
|
+
};
|
|
691
|
+
} catch (error) {
|
|
692
|
+
console.error("Failed to parse SKILL.md:", error);
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function validateSkillMd(content) {
|
|
697
|
+
const errors = [];
|
|
698
|
+
const parsed = parseSkillMd(content);
|
|
699
|
+
if (!parsed) {
|
|
700
|
+
errors.push("Invalid or missing YAML frontmatter");
|
|
701
|
+
return { valid: false, errors };
|
|
702
|
+
}
|
|
703
|
+
const requiredSections = [
|
|
704
|
+
{ name: "Overview", pattern: /##\s+Overview/i },
|
|
705
|
+
{ name: "Core Rules", pattern: /##\s+Core\s+Rules/i },
|
|
706
|
+
{ name: "Patterns", pattern: /##\s+Patterns/i },
|
|
707
|
+
{ name: "Anti-Patterns", pattern: /##\s+Anti-?Patterns/i }
|
|
708
|
+
];
|
|
709
|
+
for (const section of requiredSections) {
|
|
710
|
+
if (!section.pattern.test(parsed.content)) {
|
|
711
|
+
errors.push(`Missing required section: ${section.name}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
valid: errors.length === 0,
|
|
716
|
+
errors
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function checkSkillQuality(parsed) {
|
|
720
|
+
const feedback = [];
|
|
721
|
+
let score = 100;
|
|
722
|
+
const rulesMatch = parsed.content.match(/##\s+Core\s+Rules([\s\S]*?)(?=##|$)/i);
|
|
723
|
+
if (rulesMatch) {
|
|
724
|
+
const rulesSection = rulesMatch[1];
|
|
725
|
+
const ruleCount = (rulesSection.match(/^\d+\./gm) || []).length;
|
|
726
|
+
if (ruleCount < 6) {
|
|
727
|
+
feedback.push(`Core Rules should have 6-10 rules (found ${ruleCount})`);
|
|
728
|
+
score -= 15;
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
feedback.push("Core Rules section is missing or improperly formatted");
|
|
732
|
+
score -= 20;
|
|
733
|
+
}
|
|
734
|
+
const patternsMatch = parsed.content.match(/##\s+Patterns(?:\s+and\s+Examples)?([\s\S]*?)(?=##|$)/i);
|
|
735
|
+
if (patternsMatch) {
|
|
736
|
+
const patternsSection = patternsMatch[1];
|
|
737
|
+
const patternCount = (patternsSection.match(/###\s+Pattern\s+\d+/gi) || []).length;
|
|
738
|
+
if (patternCount < 3) {
|
|
739
|
+
feedback.push(`Patterns section should have 3-5 patterns (found ${patternCount})`);
|
|
740
|
+
score -= 15;
|
|
741
|
+
}
|
|
742
|
+
const codeBlockCount = (patternsSection.match(/```/g) || []).length / 2;
|
|
743
|
+
if (codeBlockCount < patternCount) {
|
|
744
|
+
feedback.push("Each pattern should include a complete runnable code example");
|
|
745
|
+
score -= 10;
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
feedback.push("Patterns section is missing or improperly formatted");
|
|
749
|
+
score -= 20;
|
|
750
|
+
}
|
|
751
|
+
const antiPatternsMatch = parsed.content.match(/##\s+Anti-?Patterns([\s\S]*?)(?=##|$)/i);
|
|
752
|
+
if (antiPatternsMatch) {
|
|
753
|
+
const antiPatternsSection = antiPatternsMatch[1];
|
|
754
|
+
const antiPatternCount = (antiPatternsSection.match(/^-\s+/gm) || []).length;
|
|
755
|
+
if (antiPatternCount < 4) {
|
|
756
|
+
feedback.push(`Anti-Patterns section should have 4-6 items (found ${antiPatternCount})`);
|
|
757
|
+
score -= 10;
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
feedback.push("Anti-Patterns section is missing");
|
|
761
|
+
score -= 15;
|
|
762
|
+
}
|
|
763
|
+
if (!/##\s+File\s+and\s+Folder\s+Conventions/i.test(parsed.content)) {
|
|
764
|
+
feedback.push("Consider adding File and Folder Conventions section");
|
|
765
|
+
score -= 5;
|
|
766
|
+
}
|
|
767
|
+
if (!/##\s+Useful\s+References/i.test(parsed.content)) {
|
|
768
|
+
feedback.push("Consider adding Useful References section");
|
|
769
|
+
score -= 5;
|
|
770
|
+
}
|
|
771
|
+
score = Math.max(0, score);
|
|
772
|
+
if (score === 100) {
|
|
773
|
+
feedback.push("Excellent! This skill meets gold-standard quality criteria.");
|
|
774
|
+
} else if (score >= 80) {
|
|
775
|
+
feedback.push("Good quality skill with minor improvements suggested.");
|
|
776
|
+
} else if (score >= 60) {
|
|
777
|
+
feedback.push("Acceptable skill but significant improvements recommended.");
|
|
778
|
+
} else {
|
|
779
|
+
feedback.push("This skill needs substantial improvements to meet quality standards.");
|
|
780
|
+
}
|
|
781
|
+
return { score, feedback };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/tools/fetch-skill.ts
|
|
785
|
+
var FetchSkillInputSchema = z.object({
|
|
786
|
+
owner: z.string().describe("GitHub repository owner"),
|
|
787
|
+
repo: z.string().describe("GitHub repository name"),
|
|
788
|
+
skillName: z.string().describe("Skill name to fetch"),
|
|
789
|
+
githubToken: z.string().optional().describe("Optional GitHub token for higher rate limits")
|
|
790
|
+
});
|
|
791
|
+
async function fetchSkillContent(input) {
|
|
792
|
+
try {
|
|
793
|
+
const { owner, repo, skillName, githubToken } = input;
|
|
794
|
+
const token = githubToken || process.env.GITHUB_TOKEN;
|
|
795
|
+
const fetchResult = await fetchSkillFromGitHub(owner, repo, skillName, token);
|
|
796
|
+
if (!fetchResult.found || !fetchResult.content) {
|
|
797
|
+
return {
|
|
798
|
+
success: false,
|
|
799
|
+
content: "",
|
|
800
|
+
sourceUrl: fetchResult.url,
|
|
801
|
+
found: false,
|
|
802
|
+
error: "SKILL.md not found in repository"
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
const parsed = parseSkillMd(fetchResult.content);
|
|
807
|
+
const validation = validateSkillMd(fetchResult.content);
|
|
808
|
+
if (!parsed) {
|
|
809
|
+
return {
|
|
810
|
+
success: true,
|
|
811
|
+
content: fetchResult.content,
|
|
812
|
+
sourceUrl: fetchResult.url,
|
|
813
|
+
found: true,
|
|
814
|
+
quality: {
|
|
815
|
+
valid: false,
|
|
816
|
+
issues: ["Failed to parse SKILL.md frontmatter"]
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
success: true,
|
|
822
|
+
content: fetchResult.content,
|
|
823
|
+
sourceUrl: fetchResult.url,
|
|
824
|
+
found: true,
|
|
825
|
+
metadata: {
|
|
826
|
+
name: parsed.metadata.name,
|
|
827
|
+
description: parsed.metadata.description,
|
|
828
|
+
tags: parsed.metadata.tags,
|
|
829
|
+
sources: parsed.metadata.sources
|
|
830
|
+
},
|
|
831
|
+
quality: {
|
|
832
|
+
valid: validation.valid,
|
|
833
|
+
issues: validation.errors
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
} catch (parseErr) {
|
|
837
|
+
const parseMessage = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
838
|
+
return {
|
|
839
|
+
success: true,
|
|
840
|
+
content: fetchResult.content,
|
|
841
|
+
sourceUrl: fetchResult.url,
|
|
842
|
+
found: true,
|
|
843
|
+
quality: {
|
|
844
|
+
valid: false,
|
|
845
|
+
issues: [`Failed to parse SKILL.md: ${parseMessage}`]
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
} catch (err) {
|
|
850
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
851
|
+
return {
|
|
852
|
+
success: false,
|
|
853
|
+
content: "",
|
|
854
|
+
sourceUrl: "",
|
|
855
|
+
found: false,
|
|
856
|
+
error: message
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/lib/skill-sanitizer.ts
|
|
862
|
+
function sanitizeSkillName(name) {
|
|
863
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/lib/llm.ts
|
|
867
|
+
var PROVIDER_MODELS = {
|
|
868
|
+
anthropic: "claude-sonnet-4-5-20250929",
|
|
869
|
+
google: "gemini-3.0-pro",
|
|
870
|
+
openai: "gpt-4o"
|
|
871
|
+
};
|
|
872
|
+
function detectLLMProvider() {
|
|
873
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
874
|
+
return {
|
|
875
|
+
provider: "anthropic",
|
|
876
|
+
model: PROVIDER_MODELS.anthropic,
|
|
877
|
+
apiKey: process.env.ANTHROPIC_API_KEY
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
if (process.env.GOOGLE_API_KEY) {
|
|
881
|
+
return {
|
|
882
|
+
provider: "google",
|
|
883
|
+
model: PROVIDER_MODELS.google,
|
|
884
|
+
apiKey: process.env.GOOGLE_API_KEY
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
if (process.env.OPENAI_API_KEY) {
|
|
888
|
+
return {
|
|
889
|
+
provider: "openai",
|
|
890
|
+
model: PROVIDER_MODELS.openai,
|
|
891
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
async function generateWithLLM(systemPrompt, userPrompt, config) {
|
|
897
|
+
const llmConfig = config || detectLLMProvider();
|
|
898
|
+
if (!llmConfig) {
|
|
899
|
+
return 'ERROR: No LLM API key found. Please add one of the following environment variables to your MCP config:\n - ANTHROPIC_API_KEY (recommended)\n - GOOGLE_API_KEY\n - OPENAI_API_KEY\n\nAdd the key to your MCP server configuration under the "env" section.';
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
switch (llmConfig.provider) {
|
|
903
|
+
case "anthropic":
|
|
904
|
+
return await callAnthropic(systemPrompt, userPrompt, llmConfig.apiKey);
|
|
905
|
+
case "google":
|
|
906
|
+
return await callGoogle(systemPrompt, userPrompt, llmConfig.apiKey);
|
|
907
|
+
case "openai":
|
|
908
|
+
return await callOpenAI(systemPrompt, userPrompt, llmConfig.apiKey);
|
|
909
|
+
default:
|
|
910
|
+
return `ERROR: Unknown LLM provider: ${llmConfig.provider}`;
|
|
911
|
+
}
|
|
912
|
+
} catch (error) {
|
|
913
|
+
console.error("LLM generation failed:", error);
|
|
914
|
+
return `ERROR: LLM generation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
async function callAnthropic(systemPrompt, userPrompt, apiKey) {
|
|
918
|
+
const controller = new AbortController();
|
|
919
|
+
const timeoutId = setTimeout(() => controller.abort(), 6e4);
|
|
920
|
+
try {
|
|
921
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
922
|
+
method: "POST",
|
|
923
|
+
headers: {
|
|
924
|
+
"Content-Type": "application/json",
|
|
925
|
+
"x-api-key": apiKey,
|
|
926
|
+
"anthropic-version": "2023-06-01"
|
|
927
|
+
},
|
|
928
|
+
body: JSON.stringify({
|
|
929
|
+
model: PROVIDER_MODELS.anthropic,
|
|
930
|
+
max_tokens: 4096,
|
|
931
|
+
system: systemPrompt,
|
|
932
|
+
messages: [
|
|
933
|
+
{
|
|
934
|
+
role: "user",
|
|
935
|
+
content: userPrompt
|
|
936
|
+
}
|
|
937
|
+
]
|
|
938
|
+
}),
|
|
939
|
+
signal: controller.signal
|
|
940
|
+
});
|
|
941
|
+
clearTimeout(timeoutId);
|
|
942
|
+
if (!response.ok) {
|
|
943
|
+
const errorText = await response.text();
|
|
944
|
+
throw new Error(`Anthropic API error ${response.status}: ${errorText}`);
|
|
945
|
+
}
|
|
946
|
+
const data = await response.json();
|
|
947
|
+
if (!data.content || !data.content[0] || !data.content[0].text) {
|
|
948
|
+
throw new Error("Invalid response format from Anthropic API");
|
|
949
|
+
}
|
|
950
|
+
return data.content[0].text;
|
|
951
|
+
} catch (error) {
|
|
952
|
+
clearTimeout(timeoutId);
|
|
953
|
+
throw error;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
async function callGoogle(systemPrompt, userPrompt, apiKey) {
|
|
957
|
+
const controller = new AbortController();
|
|
958
|
+
const timeoutId = setTimeout(() => controller.abort(), 6e4);
|
|
959
|
+
try {
|
|
960
|
+
const combinedPrompt = `${systemPrompt}
|
|
961
|
+
|
|
962
|
+
${userPrompt}`;
|
|
963
|
+
const response = await fetch(
|
|
964
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${PROVIDER_MODELS.google}:generateContent?key=${apiKey}`,
|
|
965
|
+
{
|
|
966
|
+
method: "POST",
|
|
967
|
+
headers: {
|
|
968
|
+
"Content-Type": "application/json"
|
|
969
|
+
},
|
|
970
|
+
body: JSON.stringify({
|
|
971
|
+
contents: [
|
|
972
|
+
{
|
|
973
|
+
parts: [
|
|
974
|
+
{
|
|
975
|
+
text: combinedPrompt
|
|
976
|
+
}
|
|
977
|
+
]
|
|
978
|
+
}
|
|
979
|
+
],
|
|
980
|
+
generationConfig: {
|
|
981
|
+
maxOutputTokens: 4096,
|
|
982
|
+
temperature: 0.7
|
|
983
|
+
}
|
|
984
|
+
}),
|
|
985
|
+
signal: controller.signal
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
clearTimeout(timeoutId);
|
|
989
|
+
if (!response.ok) {
|
|
990
|
+
const errorText = await response.text();
|
|
991
|
+
throw new Error(`Google API error ${response.status}: ${errorText}`);
|
|
992
|
+
}
|
|
993
|
+
const data = await response.json();
|
|
994
|
+
if (!data.candidates || !data.candidates[0] || !data.candidates[0].content || !data.candidates[0].content.parts || !data.candidates[0].content.parts[0]) {
|
|
995
|
+
throw new Error("Invalid response format from Google API");
|
|
996
|
+
}
|
|
997
|
+
return data.candidates[0].content.parts[0].text;
|
|
998
|
+
} catch (error) {
|
|
999
|
+
clearTimeout(timeoutId);
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async function callOpenAI(systemPrompt, userPrompt, apiKey) {
|
|
1004
|
+
const controller = new AbortController();
|
|
1005
|
+
const timeoutId = setTimeout(() => controller.abort(), 6e4);
|
|
1006
|
+
try {
|
|
1007
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
headers: {
|
|
1010
|
+
"Content-Type": "application/json",
|
|
1011
|
+
"Authorization": `Bearer ${apiKey}`
|
|
1012
|
+
},
|
|
1013
|
+
body: JSON.stringify({
|
|
1014
|
+
model: PROVIDER_MODELS.openai,
|
|
1015
|
+
max_tokens: 4096,
|
|
1016
|
+
messages: [
|
|
1017
|
+
{
|
|
1018
|
+
role: "system",
|
|
1019
|
+
content: systemPrompt
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
role: "user",
|
|
1023
|
+
content: userPrompt
|
|
1024
|
+
}
|
|
1025
|
+
]
|
|
1026
|
+
}),
|
|
1027
|
+
signal: controller.signal
|
|
1028
|
+
});
|
|
1029
|
+
clearTimeout(timeoutId);
|
|
1030
|
+
if (!response.ok) {
|
|
1031
|
+
const errorText = await response.text();
|
|
1032
|
+
throw new Error(`OpenAI API error ${response.status}: ${errorText}`);
|
|
1033
|
+
}
|
|
1034
|
+
const data = await response.json();
|
|
1035
|
+
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
|
1036
|
+
throw new Error("Invalid response format from OpenAI API");
|
|
1037
|
+
}
|
|
1038
|
+
return data.choices[0].message.content;
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
clearTimeout(timeoutId);
|
|
1041
|
+
throw error;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/tools/generate-skill.ts
|
|
1046
|
+
var GenerateSkillInputSchema = z.object({
|
|
1047
|
+
skillName: z.string().describe("Skill name/domain to generate"),
|
|
1048
|
+
projectContext: z.string().describe("Rich project context: frameworks, versions, patterns, structure"),
|
|
1049
|
+
researchQuery: z.string().optional().describe("Optional custom web research query"),
|
|
1050
|
+
maxResearchResults: z.number().optional().default(10).describe("Maximum web research results")
|
|
1051
|
+
});
|
|
1052
|
+
async function generateSkill(input) {
|
|
1053
|
+
try {
|
|
1054
|
+
const { skillName, projectContext, researchQuery, maxResearchResults } = input;
|
|
1055
|
+
const sanitizedName = sanitizeSkillName(skillName);
|
|
1056
|
+
const llmConfig = detectLLMProvider();
|
|
1057
|
+
if (!llmConfig) {
|
|
1058
|
+
return {
|
|
1059
|
+
success: false,
|
|
1060
|
+
content: "",
|
|
1061
|
+
skillName: sanitizedName,
|
|
1062
|
+
researchSources: [],
|
|
1063
|
+
error: 'No LLM API key found. Please add one of the following to your MCP server config:\n - ANTHROPIC_API_KEY (for Claude Sonnet 4.5)\n - GOOGLE_API_KEY (for Gemini 3.0 Pro)\n - OPENAI_API_KEY (for GPT-4o)\n\nAdd the key to the "env" section of your MCP config file.'
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
const query = researchQuery || `${skillName} best practices patterns conventions documentation`;
|
|
1067
|
+
let researchResults = [];
|
|
1068
|
+
let researchSources = [];
|
|
1069
|
+
try {
|
|
1070
|
+
researchResults = await searchTavily(query, maxResearchResults || 10);
|
|
1071
|
+
researchSources = researchResults.map((r) => r.url);
|
|
1072
|
+
} catch (searchErr) {
|
|
1073
|
+
researchResults = [];
|
|
1074
|
+
}
|
|
1075
|
+
const researchContext = researchResults.map((result, idx) => {
|
|
1076
|
+
return `[${idx + 1}] ${result.title}
|
|
1077
|
+
${result.snippet}
|
|
1078
|
+
Source: ${result.url}`;
|
|
1079
|
+
}).join("\n\n");
|
|
1080
|
+
const systemPrompt = `You are an expert technical writer creating Agent Skills (SKILL.md files) that teach AI coding agents about specific technologies, frameworks, and best practices.
|
|
1081
|
+
|
|
1082
|
+
CRITICAL REQUIREMENTS:
|
|
1083
|
+
1. Follow the EXACT structure from the gold-standard example below
|
|
1084
|
+
2. Include 6-10 Core Rules (numbered list, specific and actionable)
|
|
1085
|
+
3. Include 3-5 Patterns with complete runnable code examples
|
|
1086
|
+
4. Include 4-6 Anti-Patterns (bulleted list with explanations)
|
|
1087
|
+
5. Include File and Folder Conventions section
|
|
1088
|
+
6. Include Useful References section
|
|
1089
|
+
7. Use proper YAML frontmatter with name, description, tags, sources
|
|
1090
|
+
8. All code examples must be complete and runnable
|
|
1091
|
+
|
|
1092
|
+
GOLD-STANDARD EXAMPLE STRUCTURE:
|
|
1093
|
+
---
|
|
1094
|
+
name: ${sanitizedName}
|
|
1095
|
+
description: One-line description of what this skill covers
|
|
1096
|
+
tags: [tag1, tag2, tag3]
|
|
1097
|
+
sources: [https://example.com/docs]
|
|
1098
|
+
created_by: skillmesh
|
|
1099
|
+
created_at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1100
|
+
---
|
|
1101
|
+
|
|
1102
|
+
## Overview
|
|
1103
|
+
2-3 sentence explanation of what this skill covers and why it matters.
|
|
1104
|
+
|
|
1105
|
+
## Core Rules
|
|
1106
|
+
1. Rule one \u2014 specific and actionable with rationale
|
|
1107
|
+
2. Rule two \u2014 specific and actionable with rationale
|
|
1108
|
+
... (6-10 total)
|
|
1109
|
+
|
|
1110
|
+
## Patterns and Examples
|
|
1111
|
+
|
|
1112
|
+
### Pattern 1 \u2014 Descriptive Name
|
|
1113
|
+
Explain what this pattern does and when to use it.
|
|
1114
|
+
\`\`\`language
|
|
1115
|
+
// Complete runnable code example
|
|
1116
|
+
\`\`\`
|
|
1117
|
+
|
|
1118
|
+
### Pattern 2 \u2014 Descriptive Name
|
|
1119
|
+
... (3-5 total patterns)
|
|
1120
|
+
|
|
1121
|
+
## Anti-Patterns (What NOT To Do)
|
|
1122
|
+
- NEVER do X because Y (specific reason)
|
|
1123
|
+
- NEVER do A because B (specific reason)
|
|
1124
|
+
... (4-6 total)
|
|
1125
|
+
|
|
1126
|
+
## File and Folder Conventions
|
|
1127
|
+
Project-specific folder structure and naming rules.
|
|
1128
|
+
|
|
1129
|
+
## Useful References
|
|
1130
|
+
- https://official-docs-link
|
|
1131
|
+
- https://github.com/source-repo`;
|
|
1132
|
+
const userPrompt = `Generate a gold-standard SKILL.md for: ${skillName}
|
|
1133
|
+
|
|
1134
|
+
PROJECT CONTEXT:
|
|
1135
|
+
${projectContext}
|
|
1136
|
+
|
|
1137
|
+
WEB RESEARCH RESULTS:
|
|
1138
|
+
${researchContext || "No research results available. Use your knowledge of best practices."}
|
|
1139
|
+
|
|
1140
|
+
INSTRUCTIONS:
|
|
1141
|
+
1. Create a comprehensive skill that follows the exact structure shown in the system prompt
|
|
1142
|
+
2. Use the project context to make rules and patterns specific to this project
|
|
1143
|
+
3. Reference the research sources in the "sources" frontmatter field
|
|
1144
|
+
4. Ensure all code examples are complete, runnable, and production-ready
|
|
1145
|
+
5. Make the skill immediately actionable for an AI agent working in this codebase
|
|
1146
|
+
|
|
1147
|
+
Generate the complete SKILL.md now:`;
|
|
1148
|
+
const generatedContent = await generateWithLLM(systemPrompt, userPrompt, llmConfig);
|
|
1149
|
+
const parsed = parseSkillMd(generatedContent);
|
|
1150
|
+
let quality;
|
|
1151
|
+
if (parsed) {
|
|
1152
|
+
quality = checkSkillQuality(parsed);
|
|
1153
|
+
} else {
|
|
1154
|
+
quality = {
|
|
1155
|
+
score: 0,
|
|
1156
|
+
feedback: ["Generated content does not have valid YAML frontmatter"]
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
success: true,
|
|
1161
|
+
content: generatedContent,
|
|
1162
|
+
skillName: sanitizedName,
|
|
1163
|
+
researchSources,
|
|
1164
|
+
provider: llmConfig.provider,
|
|
1165
|
+
model: llmConfig.model,
|
|
1166
|
+
quality
|
|
1167
|
+
};
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1170
|
+
return {
|
|
1171
|
+
success: false,
|
|
1172
|
+
content: "",
|
|
1173
|
+
skillName: sanitizeSkillName(input.skillName),
|
|
1174
|
+
researchSources: [],
|
|
1175
|
+
error: message
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// src/tools/install-skill.ts
|
|
1181
|
+
var { ensureDirSync: ensureDirSync3, writeFileSync, existsSync: existsSync5 } = fs;
|
|
1182
|
+
var InstallSkillInputSchema = z.object({
|
|
1183
|
+
projectPath: z.string().describe("Absolute path to project root directory"),
|
|
1184
|
+
skillName: z.string().describe("Skill name to install"),
|
|
1185
|
+
agents: z.array(z.string()).describe("List of target agent names"),
|
|
1186
|
+
source: z.object({
|
|
1187
|
+
type: z.enum(["github", "content"]).describe("Source type"),
|
|
1188
|
+
owner: z.string().optional().describe("GitHub owner (for type: github)"),
|
|
1189
|
+
repo: z.string().optional().describe("GitHub repo (for type: github)"),
|
|
1190
|
+
content: z.string().optional().describe("Direct SKILL.md content (for type: content)"),
|
|
1191
|
+
stars: z.number().optional().describe("GitHub star count (for quality check)")
|
|
1192
|
+
}).describe("Skill source"),
|
|
1193
|
+
overwrite: z.boolean().optional().default(false).describe("Overwrite existing skill if present"),
|
|
1194
|
+
global: z.boolean().optional().default(false).describe("Install to global scope")
|
|
1195
|
+
});
|
|
1196
|
+
async function installSkill(input) {
|
|
1197
|
+
try {
|
|
1198
|
+
const { projectPath, skillName, agents, source, overwrite, global } = input;
|
|
1199
|
+
const sanitizedSkillName = sanitizeSkillName(skillName);
|
|
1200
|
+
if (!sanitizedSkillName || sanitizedSkillName.length === 0) {
|
|
1201
|
+
return {
|
|
1202
|
+
method: "skipped",
|
|
1203
|
+
results: [],
|
|
1204
|
+
skillName,
|
|
1205
|
+
error: `Invalid skill name: "${skillName}". Skill names must contain alphanumeric characters.`
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
if (!overwrite) {
|
|
1209
|
+
const existingPaths = [];
|
|
1210
|
+
for (const agent of agents) {
|
|
1211
|
+
const skillPath = getSkillPath(agent, projectPath, sanitizedSkillName, global);
|
|
1212
|
+
if (existsSync5(skillPath)) {
|
|
1213
|
+
existingPaths.push(skillPath);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (existingPaths.length > 0) {
|
|
1217
|
+
return {
|
|
1218
|
+
method: "skipped",
|
|
1219
|
+
results: agents.map((agent) => ({
|
|
1220
|
+
agent,
|
|
1221
|
+
path: getSkillPath(agent, projectPath, sanitizedSkillName, global),
|
|
1222
|
+
method: "skipped",
|
|
1223
|
+
success: true,
|
|
1224
|
+
error: "Skill already exists. Use overwrite flag to replace."
|
|
1225
|
+
})),
|
|
1226
|
+
skillName: sanitizedSkillName
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (source.type === "github" && source.owner && source.repo) {
|
|
1231
|
+
const stars = source.stars ?? 0;
|
|
1232
|
+
if (stars >= 50) {
|
|
1233
|
+
return executePathA(source.owner, source.repo, sanitizedSkillName, agents, projectPath, global);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
let skillContent = null;
|
|
1237
|
+
if (source.type === "content" && source.content) {
|
|
1238
|
+
skillContent = source.content;
|
|
1239
|
+
} else if (source.type === "github" && source.owner && source.repo) {
|
|
1240
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
1241
|
+
const fetchResult = await fetchSkillFromGitHub(
|
|
1242
|
+
source.owner,
|
|
1243
|
+
source.repo,
|
|
1244
|
+
sanitizedSkillName,
|
|
1245
|
+
githubToken
|
|
1246
|
+
);
|
|
1247
|
+
if (fetchResult.found) {
|
|
1248
|
+
skillContent = fetchResult.content;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (skillContent) {
|
|
1252
|
+
return await executePathB(
|
|
1253
|
+
skillContent,
|
|
1254
|
+
sanitizedSkillName,
|
|
1255
|
+
agents,
|
|
1256
|
+
projectPath,
|
|
1257
|
+
global,
|
|
1258
|
+
overwrite,
|
|
1259
|
+
source.type === "github" ? `${source.owner}/${source.repo}` : "direct-content"
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
return await executePathC(skillName, sanitizedSkillName, agents, projectPath, global, overwrite);
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1265
|
+
return {
|
|
1266
|
+
method: "skipped",
|
|
1267
|
+
results: [],
|
|
1268
|
+
skillName: input.skillName,
|
|
1269
|
+
error: `Installation failed: ${message}`
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function executePathA(owner, repo, skillName, agents, projectPath, global) {
|
|
1274
|
+
const agentFlags = agents.map((agent) => `--agent ${agent}`).join(" ");
|
|
1275
|
+
const globalFlag = global ? "-g" : "";
|
|
1276
|
+
const repoPath = `${owner}/${repo}`;
|
|
1277
|
+
const command = `npx skills add ${repoPath} --skill ${skillName} ${agentFlags} ${globalFlag} -y`.trim().replace(/\s+/g, " ");
|
|
1278
|
+
const envNote = process.env.GITHUB_TOKEN ? "GITHUB_TOKEN is available in environment (higher rate limits)." : "Note: Set GITHUB_TOKEN environment variable for higher GitHub API rate limits.";
|
|
1279
|
+
const instruction = `Execute this command in your terminal now. The command will install the skill to all specified agents without any interactive prompts.
|
|
1280
|
+
|
|
1281
|
+
${envNote}`;
|
|
1282
|
+
const results = agents.map((agent) => ({
|
|
1283
|
+
agent,
|
|
1284
|
+
path: getSkillPath(agent, projectPath, skillName, global),
|
|
1285
|
+
method: "npx_command",
|
|
1286
|
+
success: true
|
|
1287
|
+
// Command generation succeeded; actual execution is agent's responsibility
|
|
1288
|
+
}));
|
|
1289
|
+
return {
|
|
1290
|
+
method: "npx_command",
|
|
1291
|
+
command,
|
|
1292
|
+
instruction,
|
|
1293
|
+
results,
|
|
1294
|
+
skillName
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
async function executePathB(content, skillName, agents, projectPath, global, overwrite, source) {
|
|
1298
|
+
const results = [];
|
|
1299
|
+
const parsed = parseSkillMd(content);
|
|
1300
|
+
if (!parsed) {
|
|
1301
|
+
return {
|
|
1302
|
+
method: "direct_write",
|
|
1303
|
+
results: [],
|
|
1304
|
+
skillName,
|
|
1305
|
+
error: "Invalid SKILL.md content: missing or invalid YAML frontmatter"
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
for (const agent of agents) {
|
|
1309
|
+
try {
|
|
1310
|
+
const skillPath = getSkillPath(agent, projectPath, skillName, global);
|
|
1311
|
+
const skillDir = path.dirname(skillPath);
|
|
1312
|
+
if (existsSync5(skillPath) && !overwrite) {
|
|
1313
|
+
results.push({
|
|
1314
|
+
agent,
|
|
1315
|
+
path: skillPath,
|
|
1316
|
+
method: "skipped",
|
|
1317
|
+
success: true,
|
|
1318
|
+
error: "Skill already exists. Use overwrite flag to replace."
|
|
1319
|
+
});
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
ensureDirSync3(skillDir);
|
|
1323
|
+
const controller = new AbortController();
|
|
1324
|
+
const timeoutId = setTimeout(() => controller.abort(), 15e3);
|
|
1325
|
+
try {
|
|
1326
|
+
writeFileSync(skillPath, content, "utf-8");
|
|
1327
|
+
clearTimeout(timeoutId);
|
|
1328
|
+
results.push({
|
|
1329
|
+
agent,
|
|
1330
|
+
path: skillPath,
|
|
1331
|
+
method: "direct_write",
|
|
1332
|
+
success: true
|
|
1333
|
+
});
|
|
1334
|
+
} catch (writeErr) {
|
|
1335
|
+
clearTimeout(timeoutId);
|
|
1336
|
+
throw writeErr;
|
|
1337
|
+
}
|
|
1338
|
+
} catch (agentErr) {
|
|
1339
|
+
const message = agentErr instanceof Error ? agentErr.message : String(agentErr);
|
|
1340
|
+
results.push({
|
|
1341
|
+
agent,
|
|
1342
|
+
path: getSkillPath(agent, projectPath, skillName, global),
|
|
1343
|
+
method: "direct_write",
|
|
1344
|
+
success: false,
|
|
1345
|
+
error: `Write failed: ${message}`
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
const successfulAgents = results.filter((r) => r.success).map((r) => r.agent);
|
|
1350
|
+
if (successfulAgents.length > 0) {
|
|
1351
|
+
await trackInstalledSkill(projectPath, skillName, successfulAgents, source);
|
|
1352
|
+
}
|
|
1353
|
+
const allSucceeded = results.every((r) => r.success);
|
|
1354
|
+
return {
|
|
1355
|
+
method: "direct_write",
|
|
1356
|
+
results,
|
|
1357
|
+
skillName,
|
|
1358
|
+
error: allSucceeded ? void 0 : "Some installations failed. Check individual agent results."
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
async function executePathC(originalSkillName, sanitizedSkillName, agents, projectPath, global, overwrite) {
|
|
1362
|
+
try {
|
|
1363
|
+
const projectContext = `Generating skill for: ${originalSkillName}
|
|
1364
|
+
Project path: ${projectPath}
|
|
1365
|
+
Target agents: ${agents.join(", ")}`;
|
|
1366
|
+
const generateResult = await generateSkill({
|
|
1367
|
+
skillName: originalSkillName,
|
|
1368
|
+
projectContext,
|
|
1369
|
+
maxResearchResults: 10
|
|
1370
|
+
});
|
|
1371
|
+
if (!generateResult.success || !generateResult.content) {
|
|
1372
|
+
return {
|
|
1373
|
+
method: "generated",
|
|
1374
|
+
results: [],
|
|
1375
|
+
skillName: sanitizedSkillName,
|
|
1376
|
+
error: generateResult.error || "Skill generation failed"
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
const pathBResult = await executePathB(
|
|
1380
|
+
generateResult.content,
|
|
1381
|
+
sanitizedSkillName,
|
|
1382
|
+
agents,
|
|
1383
|
+
projectPath,
|
|
1384
|
+
global,
|
|
1385
|
+
overwrite,
|
|
1386
|
+
"generated"
|
|
1387
|
+
);
|
|
1388
|
+
return {
|
|
1389
|
+
...pathBResult,
|
|
1390
|
+
method: "generated"
|
|
1391
|
+
};
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1394
|
+
return {
|
|
1395
|
+
method: "generated",
|
|
1396
|
+
results: [],
|
|
1397
|
+
skillName: sanitizedSkillName,
|
|
1398
|
+
error: `Generation failed: ${message}`
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
var { readdirSync, statSync, readFileSync, existsSync: existsSync6 } = fs;
|
|
1403
|
+
var ListSkillsInputSchema = z.object({
|
|
1404
|
+
projectPath: z.string().describe("Absolute path to project root directory"),
|
|
1405
|
+
agent: z.string().optional().describe("Filter by specific agent name"),
|
|
1406
|
+
global: z.boolean().optional().default(false).describe("List global skills instead of project skills")
|
|
1407
|
+
});
|
|
1408
|
+
async function listSkills(input) {
|
|
1409
|
+
try {
|
|
1410
|
+
const { projectPath, agent, global } = input;
|
|
1411
|
+
const skills = [];
|
|
1412
|
+
const byAgent = {};
|
|
1413
|
+
let agentsToScan;
|
|
1414
|
+
if (agent) {
|
|
1415
|
+
agentsToScan = [agent];
|
|
1416
|
+
} else {
|
|
1417
|
+
if (global) {
|
|
1418
|
+
agentsToScan = Object.keys(AGENT_PATHS);
|
|
1419
|
+
} else {
|
|
1420
|
+
agentsToScan = await detectInstalledAgents(projectPath);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
for (const agentId of agentsToScan) {
|
|
1424
|
+
const agentConfig = AGENT_PATHS[agentId];
|
|
1425
|
+
if (!agentConfig) continue;
|
|
1426
|
+
let skillsDir;
|
|
1427
|
+
if (global) {
|
|
1428
|
+
skillsDir = agentConfig.globalPath.replace(/^~/, os2.homedir());
|
|
1429
|
+
} else {
|
|
1430
|
+
skillsDir = path.join(projectPath, agentConfig.projectPath);
|
|
1431
|
+
}
|
|
1432
|
+
if (!existsSync6(skillsDir)) {
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
1437
|
+
for (const entry of entries) {
|
|
1438
|
+
if (!entry.isDirectory()) continue;
|
|
1439
|
+
const skillName = entry.name;
|
|
1440
|
+
const skillPath = path.join(skillsDir, skillName, "SKILL.md");
|
|
1441
|
+
if (!existsSync6(skillPath)) {
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
1446
|
+
const stats = statSync(skillPath);
|
|
1447
|
+
const parsed = parseSkillMd(content);
|
|
1448
|
+
const skillInfo = {
|
|
1449
|
+
name: skillName,
|
|
1450
|
+
agent: agentId,
|
|
1451
|
+
path: skillPath,
|
|
1452
|
+
description: parsed?.metadata.description || "No description",
|
|
1453
|
+
tags: parsed?.metadata.tags || [],
|
|
1454
|
+
source: parsed?.metadata.sources?.[0],
|
|
1455
|
+
lastModified: stats.mtime.toISOString()
|
|
1456
|
+
};
|
|
1457
|
+
skills.push(skillInfo);
|
|
1458
|
+
byAgent[agentId] = (byAgent[agentId] || 0) + 1;
|
|
1459
|
+
} catch (readErr) {
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
} catch (dirErr) {
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
skills.sort((a, b) => {
|
|
1468
|
+
if (a.agent !== b.agent) {
|
|
1469
|
+
return a.agent.localeCompare(b.agent);
|
|
1470
|
+
}
|
|
1471
|
+
return a.name.localeCompare(b.name);
|
|
1472
|
+
});
|
|
1473
|
+
return {
|
|
1474
|
+
success: true,
|
|
1475
|
+
skills,
|
|
1476
|
+
totalCount: skills.length,
|
|
1477
|
+
byAgent
|
|
1478
|
+
};
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1481
|
+
return {
|
|
1482
|
+
success: false,
|
|
1483
|
+
skills: [],
|
|
1484
|
+
totalCount: 0,
|
|
1485
|
+
byAgent: {},
|
|
1486
|
+
error: message
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
var { removeSync, existsSync: existsSync7 } = fs;
|
|
1491
|
+
var DeleteSkillInputSchema = z.object({
|
|
1492
|
+
projectPath: z.string().describe("Absolute path to project root directory"),
|
|
1493
|
+
skillName: z.string().describe("Skill name to delete"),
|
|
1494
|
+
agents: z.array(z.string()).optional().describe("Target agents (if omitted, deletes from all agents)"),
|
|
1495
|
+
global: z.boolean().optional().default(false).describe("Delete from global scope")
|
|
1496
|
+
});
|
|
1497
|
+
async function deleteSkill(input) {
|
|
1498
|
+
try {
|
|
1499
|
+
const { projectPath, skillName, agents, global } = input;
|
|
1500
|
+
const sanitizedName = sanitizeSkillName(skillName);
|
|
1501
|
+
let targetAgents;
|
|
1502
|
+
if (agents && agents.length > 0) {
|
|
1503
|
+
targetAgents = agents;
|
|
1504
|
+
} else {
|
|
1505
|
+
if (global) {
|
|
1506
|
+
targetAgents = Object.keys(AGENT_PATHS);
|
|
1507
|
+
} else {
|
|
1508
|
+
targetAgents = await detectInstalledAgents(projectPath);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
const results = [];
|
|
1512
|
+
let totalDeleted = 0;
|
|
1513
|
+
for (const agentId of targetAgents) {
|
|
1514
|
+
const agentConfig = AGENT_PATHS[agentId];
|
|
1515
|
+
if (!agentConfig) {
|
|
1516
|
+
results.push({
|
|
1517
|
+
agent: agentId,
|
|
1518
|
+
path: "",
|
|
1519
|
+
deleted: false,
|
|
1520
|
+
notFound: true,
|
|
1521
|
+
error: "Unknown agent"
|
|
1522
|
+
});
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
let skillDir;
|
|
1526
|
+
if (global) {
|
|
1527
|
+
const skillsDir = agentConfig.globalPath.replace(/^~/, os2.homedir());
|
|
1528
|
+
skillDir = path.join(skillsDir, sanitizedName);
|
|
1529
|
+
} else {
|
|
1530
|
+
const skillsDir = path.join(projectPath, agentConfig.projectPath);
|
|
1531
|
+
skillDir = path.join(skillsDir, sanitizedName);
|
|
1532
|
+
}
|
|
1533
|
+
if (!existsSync7(skillDir)) {
|
|
1534
|
+
results.push({
|
|
1535
|
+
agent: agentId,
|
|
1536
|
+
path: skillDir,
|
|
1537
|
+
deleted: false,
|
|
1538
|
+
notFound: true
|
|
1539
|
+
});
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
try {
|
|
1543
|
+
removeSync(skillDir);
|
|
1544
|
+
if (!global) {
|
|
1545
|
+
try {
|
|
1546
|
+
await untrackSkill(projectPath, sanitizedName, agentId);
|
|
1547
|
+
} catch (configErr) {
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
results.push({
|
|
1551
|
+
agent: agentId,
|
|
1552
|
+
path: skillDir,
|
|
1553
|
+
deleted: true,
|
|
1554
|
+
notFound: false
|
|
1555
|
+
});
|
|
1556
|
+
totalDeleted++;
|
|
1557
|
+
} catch (deleteErr) {
|
|
1558
|
+
const message = deleteErr instanceof Error ? deleteErr.message : String(deleteErr);
|
|
1559
|
+
results.push({
|
|
1560
|
+
agent: agentId,
|
|
1561
|
+
path: skillDir,
|
|
1562
|
+
deleted: false,
|
|
1563
|
+
notFound: false,
|
|
1564
|
+
error: message
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
return {
|
|
1569
|
+
success: true,
|
|
1570
|
+
results,
|
|
1571
|
+
skillName: sanitizedName,
|
|
1572
|
+
totalDeleted
|
|
1573
|
+
};
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1576
|
+
return {
|
|
1577
|
+
success: false,
|
|
1578
|
+
results: [],
|
|
1579
|
+
skillName: input.skillName,
|
|
1580
|
+
totalDeleted: 0,
|
|
1581
|
+
error: message
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/prompts/system-prompt.ts
|
|
1587
|
+
var SYSTEM_PROMPT = `You are connected to SkillMesh, an intelligent Agent Skills manager.
|
|
1588
|
+
You have 8 tools to discover, fetch, generate, and install SKILL.md files that permanently teach you and other agents about this project.
|
|
1589
|
+
|
|
1590
|
+
## WHAT IS AN AGENT SKILL
|
|
1591
|
+
|
|
1592
|
+
A SKILL.md file that lives in your agent's skill directory. You read all SKILL.md files at the start of every session \u2014 skills are always present when the user prompts. Installing a skill permanently teaches you project-specific rules, patterns, conventions, and best practices.
|
|
1593
|
+
|
|
1594
|
+
## THE 8 SKILLMESH TOOLS
|
|
1595
|
+
|
|
1596
|
+
1. detect_installed_agents \u2014 Scans project root and home directory for installed AI coding agents. Returns list with their skill directory paths.
|
|
1597
|
+
|
|
1598
|
+
2. create_skills_folder \u2014 Initializes skill directory structure for selected agents. Creates .claude/skills/, .cursor/skills/, etc. Also creates .skillmesh/skillmesh.config.json for tracking.
|
|
1599
|
+
|
|
1600
|
+
3. search_skills \u2014 Searches skills.sh via Tavily for skills matching a keyword. Fetches GitHub star count and recency. Returns ranked list filtered by quality (stars >= 50, updated within 12 months).
|
|
1601
|
+
|
|
1602
|
+
4. fetch_skill_content \u2014 Fetches full SKILL.md content from a GitHub repository using raw URL fallbacks. Validates YAML frontmatter. Returns content, source URL, and parsed metadata.
|
|
1603
|
+
|
|
1604
|
+
5. install_skill \u2014 Core installation tool. PATH A: returns npx skills add command string for you to execute via your bash tool. PATH B: writes SKILL.md directly to disk if npx fails. PATH C: calls generate_skill if both fail. Handles all selected agents simultaneously.
|
|
1605
|
+
|
|
1606
|
+
6. generate_skill \u2014 LLM-powered skill generation. Searches web via Tavily for best practices, sends research + project context to LLM, generates a gold-standard SKILL.md. Only called when PATH A and B fail.
|
|
1607
|
+
|
|
1608
|
+
7. list_installed_skills \u2014 Reads all agent skill directories in the project. Returns table of installed skills grouped by agent with name, description, source, and last modified date.
|
|
1609
|
+
|
|
1610
|
+
8. delete_skill \u2014 Removes a specific SKILL.md from one or all agent directories. Returns success/failure per agent.
|
|
1611
|
+
|
|
1612
|
+
## WHEN TO RUN THE FULL SKILL SETUP WORKFLOW
|
|
1613
|
+
|
|
1614
|
+
Automatically trigger when the user says anything resembling:
|
|
1615
|
+
- 'set up skills'
|
|
1616
|
+
- 'add skills'
|
|
1617
|
+
- 'install skills'
|
|
1618
|
+
- 'analyze my project and add skills'
|
|
1619
|
+
- 'make my agent smarter'
|
|
1620
|
+
- 'skillmesh'
|
|
1621
|
+
- 'initialize skills'
|
|
1622
|
+
|
|
1623
|
+
Run immediately without asking for confirmation.
|
|
1624
|
+
|
|
1625
|
+
## FULL SKILL SETUP WORKFLOW (execute in this exact order)
|
|
1626
|
+
|
|
1627
|
+
STEP 1: Detect agents
|
|
1628
|
+
- Call detect_installed_agents with current project path
|
|
1629
|
+
- If no agents detected, inform user and stop
|
|
1630
|
+
|
|
1631
|
+
STEP 2: Initialize directories
|
|
1632
|
+
- Call create_skills_folder for all detected agents
|
|
1633
|
+
- This ensures skill directories exist before installation
|
|
1634
|
+
|
|
1635
|
+
STEP 3: Analyze codebase thoroughly
|
|
1636
|
+
Scan the project and identify:
|
|
1637
|
+
- Primary language and framework (e.g., TypeScript, React, Next.js)
|
|
1638
|
+
- Key dependencies and their versions (read package.json, requirements.txt, etc.)
|
|
1639
|
+
- Architectural patterns (e.g., App Router, REST API, GraphQL)
|
|
1640
|
+
- Folder structure and conventions (e.g., /src, /app, /components)
|
|
1641
|
+
- Test setup (Jest, Vitest, Playwright, etc.)
|
|
1642
|
+
- Database and ORM (Prisma, Drizzle, MongoDB, etc.)
|
|
1643
|
+
- Auth system (NextAuth, Clerk, Auth0, etc.)
|
|
1644
|
+
- API patterns (REST, tRPC, GraphQL)
|
|
1645
|
+
- Deployment config (Vercel, Docker, AWS, etc.)
|
|
1646
|
+
|
|
1647
|
+
STEP 4: Derive skill domains
|
|
1648
|
+
From your analysis, identify 4-8 specific skill domains using kebab-case names.
|
|
1649
|
+
|
|
1650
|
+
GOOD examples:
|
|
1651
|
+
- nextjs-app-router-patterns
|
|
1652
|
+
- prisma-multi-tenant-schema
|
|
1653
|
+
- react-hook-form-validation
|
|
1654
|
+
- tailwind-component-architecture
|
|
1655
|
+
- playwright-e2e-testing
|
|
1656
|
+
- trpc-api-patterns
|
|
1657
|
+
|
|
1658
|
+
BAD examples (too generic):
|
|
1659
|
+
- javascript
|
|
1660
|
+
- frontend
|
|
1661
|
+
- react
|
|
1662
|
+
- coding
|
|
1663
|
+
- testing
|
|
1664
|
+
|
|
1665
|
+
STEP 5: Install skills for each domain
|
|
1666
|
+
For each domain you identified:
|
|
1667
|
+
|
|
1668
|
+
a. Call search_skills(domain)
|
|
1669
|
+
- This searches skills.sh via Tavily
|
|
1670
|
+
- Returns quality-filtered results (stars >= 50, updated within 12 months)
|
|
1671
|
+
|
|
1672
|
+
b. If quality results found (stars >= 50):
|
|
1673
|
+
- Call fetch_skill_content for the top result
|
|
1674
|
+
- Review the content to ensure it matches your needs
|
|
1675
|
+
- Call install_skill with source type 'github' and the owner/repo/stars data
|
|
1676
|
+
- install_skill returns a PATH A response: an npx command string
|
|
1677
|
+
- YOU MUST EXECUTE THIS COMMAND using your bash tool immediately
|
|
1678
|
+
- Example command: npx skills add vercel-labs/next-skills --skill nextjs-patterns --agent claude-code --agent cursor -y
|
|
1679
|
+
- The command is fully non-interactive (no prompts) thanks to --agent flags and -y flag
|
|
1680
|
+
|
|
1681
|
+
c. If no quality results OR PATH A fails:
|
|
1682
|
+
- Call install_skill with source type 'github' (if you have owner/repo) or 'content' (if you have raw content)
|
|
1683
|
+
- This triggers PATH B: direct write to disk
|
|
1684
|
+
- If PATH B also fails OR no GitHub source exists:
|
|
1685
|
+
- Call generate_skill with rich projectContext
|
|
1686
|
+
- Then call install_skill with source type 'content' using the generated content
|
|
1687
|
+
|
|
1688
|
+
STEP 6: Report results
|
|
1689
|
+
- Call list_installed_skills to see all installed skills
|
|
1690
|
+
- Present a summary table to the user grouped by agent
|
|
1691
|
+
- Highlight which skills were installed (PATH A, PATH B, or generated)
|
|
1692
|
+
|
|
1693
|
+
## CRITICAL REQUIREMENTS
|
|
1694
|
+
|
|
1695
|
+
PROJECTCONTEXT QUALITY:
|
|
1696
|
+
When calling generate_skill, always provide detailed projectContext. Include:
|
|
1697
|
+
- Exact framework versions from package.json/requirements.txt
|
|
1698
|
+
- Key architectural patterns observed in the code
|
|
1699
|
+
- Specific folder structure and file naming conventions
|
|
1700
|
+
- Main dependencies and how they interact
|
|
1701
|
+
- Any coding conventions found in existing files
|
|
1702
|
+
The richer the context, the more useful the generated skill.
|
|
1703
|
+
|
|
1704
|
+
EXECUTION RULES:
|
|
1705
|
+
- NEVER ask the user to confirm before running the workflow
|
|
1706
|
+
- NEVER install the same skill twice \u2014 call list_installed_skills first and check for duplicates
|
|
1707
|
+
- ALWAYS install to all detected agents simultaneously
|
|
1708
|
+
- ALWAYS execute npx commands returned by install_skill (PATH A) using your bash tool
|
|
1709
|
+
- NEVER manually write SKILL.md files \u2014 always use install_skill tool
|
|
1710
|
+
|
|
1711
|
+
ERROR HANDLING:
|
|
1712
|
+
- If search_skills returns no results, immediately fall back to generate_skill
|
|
1713
|
+
- If install_skill PATH A fails (npx error), the tool automatically falls back to PATH B
|
|
1714
|
+
- If PATH B fails (fetch error), the tool automatically falls back to PATH C (generate)
|
|
1715
|
+
- Always report errors clearly to the user and show which skills succeeded vs failed
|
|
1716
|
+
|
|
1717
|
+
## SKILL INSTALLATION PATHS EXPLAINED
|
|
1718
|
+
|
|
1719
|
+
PATH A (npx command \u2014 preferred):
|
|
1720
|
+
- Used for high-quality GitHub repos (stars >= 50)
|
|
1721
|
+
- install_skill returns an npx command string
|
|
1722
|
+
- YOU execute it using your bash tool
|
|
1723
|
+
- Command format: npx skills add <owner/repo> --skill <name> --agent <agent1> --agent <agent2> -y
|
|
1724
|
+
- The npx CLI handles cloning, writing, and directory creation
|
|
1725
|
+
- Fully automated with zero interactive prompts
|
|
1726
|
+
|
|
1727
|
+
PATH B (direct write \u2014 fallback):
|
|
1728
|
+
- Used when PATH A not applicable or GitHub fetch available but stars < 50
|
|
1729
|
+
- install_skill fetches raw SKILL.md from GitHub
|
|
1730
|
+
- Writes directly to all agent directories using fs-extra
|
|
1731
|
+
- Updates .skillmesh/skillmesh.config.json for tracking
|
|
1732
|
+
|
|
1733
|
+
PATH C (LLM generation \u2014 last resort):
|
|
1734
|
+
- Used when no existing skill found on GitHub
|
|
1735
|
+
- Requires ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY in env
|
|
1736
|
+
- Searches web via Tavily for best practices
|
|
1737
|
+
- Generates gold-standard SKILL.md from research + project context
|
|
1738
|
+
- Writes to all agent directories
|
|
1739
|
+
|
|
1740
|
+
## TYPICAL USER INTERACTION EXAMPLE
|
|
1741
|
+
|
|
1742
|
+
User: "Set up skills for my Next.js project"
|
|
1743
|
+
|
|
1744
|
+
You:
|
|
1745
|
+
1. [Call detect_installed_agents] \u2192 Found: claude-code, cursor
|
|
1746
|
+
2. [Call create_skills_folder] \u2192 Created .claude/skills/ and .cursor/skills/
|
|
1747
|
+
3. [Analyze project] \u2192 Next.js 15, App Router, TypeScript, Prisma, TailwindCSS
|
|
1748
|
+
4. [Derive domains] \u2192 nextjs-app-router-patterns, prisma-schema-design, tailwindcss-components
|
|
1749
|
+
5. For nextjs-app-router-patterns:
|
|
1750
|
+
- [Call search_skills] \u2192 Found vercel-labs/next-skills (2,341 stars)
|
|
1751
|
+
- [Call fetch_skill_content] \u2192 Retrieved SKILL.md
|
|
1752
|
+
- [Call install_skill] \u2192 Returns npx command
|
|
1753
|
+
- [Execute via bash] \u2192 npx skills add vercel-labs/next-skills --skill nextjs-app-router-patterns --agent claude-code --agent cursor -y
|
|
1754
|
+
6. [Repeat for other domains]
|
|
1755
|
+
7. [Call list_installed_skills] \u2192 Report summary table
|
|
1756
|
+
|
|
1757
|
+
User sees: "Installed 3 skills across 2 agents (claude-code, cursor). You now have permanent knowledge of Next.js App Router patterns, Prisma schema design, and TailwindCSS component architecture."
|
|
1758
|
+
|
|
1759
|
+
## NOTES
|
|
1760
|
+
|
|
1761
|
+
- Skill names on disk are sanitized to kebab-case: toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
1762
|
+
- GITHUB_TOKEN is optional but recommended for higher API rate limits (5000/hr vs 60/hr)
|
|
1763
|
+
- LLM API keys are only needed for generate_skill (PATH C)
|
|
1764
|
+
- Always use absolute paths, never relative paths
|
|
1765
|
+
- Target directories: .claude/skills/, .cursor/skills/, .agent/skills/, .windsurf/skills/, etc.
|
|
1766
|
+
`;
|
|
1767
|
+
|
|
1768
|
+
// src/index.ts
|
|
1769
|
+
var server = new Server(
|
|
1770
|
+
{
|
|
1771
|
+
name: "skillmesh-mcp",
|
|
1772
|
+
version: "1.0.0"
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
capabilities: {
|
|
1776
|
+
tools: {}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
);
|
|
1780
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1781
|
+
return {
|
|
1782
|
+
tools: [
|
|
1783
|
+
{
|
|
1784
|
+
name: "detect_installed_agents",
|
|
1785
|
+
description: "Scans project root and home directory for installed AI coding agents. Returns list of detected agents with their skill directory paths.",
|
|
1786
|
+
inputSchema: {
|
|
1787
|
+
type: "object",
|
|
1788
|
+
properties: {
|
|
1789
|
+
projectPath: {
|
|
1790
|
+
type: "string",
|
|
1791
|
+
description: "Absolute path to project root directory"
|
|
1792
|
+
},
|
|
1793
|
+
includeGlobal: {
|
|
1794
|
+
type: "boolean",
|
|
1795
|
+
description: "Also scan home directory for globally installed agents",
|
|
1796
|
+
default: false
|
|
1797
|
+
}
|
|
1798
|
+
},
|
|
1799
|
+
required: ["projectPath"]
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
name: "create_skills_folder",
|
|
1804
|
+
description: "Initializes skill directory structure for selected agents. Creates .claude/skills/, .cursor/skills/, etc. Also creates .skillmesh/skillmesh.config.json for tracking.",
|
|
1805
|
+
inputSchema: {
|
|
1806
|
+
type: "object",
|
|
1807
|
+
properties: {
|
|
1808
|
+
projectPath: {
|
|
1809
|
+
type: "string",
|
|
1810
|
+
description: "Absolute path to project root directory"
|
|
1811
|
+
},
|
|
1812
|
+
agents: {
|
|
1813
|
+
type: "array",
|
|
1814
|
+
items: { type: "string" },
|
|
1815
|
+
description: "List of agent names to create folders for"
|
|
1816
|
+
},
|
|
1817
|
+
global: {
|
|
1818
|
+
type: "boolean",
|
|
1819
|
+
description: "Create in global scope instead of project scope",
|
|
1820
|
+
default: false
|
|
1821
|
+
}
|
|
1822
|
+
},
|
|
1823
|
+
required: ["projectPath", "agents"]
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
name: "search_skills",
|
|
1828
|
+
description: "Searches skills.sh via Tavily for skills matching a keyword. Fetches GitHub star count and recency. Returns ranked list filtered by quality (stars >= 50, updated within 12 months).",
|
|
1829
|
+
inputSchema: {
|
|
1830
|
+
type: "object",
|
|
1831
|
+
properties: {
|
|
1832
|
+
keyword: {
|
|
1833
|
+
type: "string",
|
|
1834
|
+
description: 'Search keyword or skill domain (e.g., "nextjs-app-router", "prisma")'
|
|
1835
|
+
},
|
|
1836
|
+
minStars: {
|
|
1837
|
+
type: "number",
|
|
1838
|
+
description: "Minimum GitHub stars filter",
|
|
1839
|
+
default: 50
|
|
1840
|
+
},
|
|
1841
|
+
maxAgeMonths: {
|
|
1842
|
+
type: "number",
|
|
1843
|
+
description: "Maximum age in months (updated within)",
|
|
1844
|
+
default: 12
|
|
1845
|
+
},
|
|
1846
|
+
limit: {
|
|
1847
|
+
type: "number",
|
|
1848
|
+
description: "Maximum number of results",
|
|
1849
|
+
default: 10
|
|
1850
|
+
}
|
|
1851
|
+
},
|
|
1852
|
+
required: ["keyword"]
|
|
1853
|
+
}
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
name: "fetch_skill_content",
|
|
1857
|
+
description: "Fetches full SKILL.md content from a GitHub repository using raw URL fallbacks. Validates YAML frontmatter. Returns content, source URL, and parsed metadata.",
|
|
1858
|
+
inputSchema: {
|
|
1859
|
+
type: "object",
|
|
1860
|
+
properties: {
|
|
1861
|
+
owner: {
|
|
1862
|
+
type: "string",
|
|
1863
|
+
description: "GitHub repository owner"
|
|
1864
|
+
},
|
|
1865
|
+
repo: {
|
|
1866
|
+
type: "string",
|
|
1867
|
+
description: "GitHub repository name"
|
|
1868
|
+
},
|
|
1869
|
+
skillName: {
|
|
1870
|
+
type: "string",
|
|
1871
|
+
description: "Skill name to fetch"
|
|
1872
|
+
},
|
|
1873
|
+
githubToken: {
|
|
1874
|
+
type: "string",
|
|
1875
|
+
description: "Optional GitHub token for higher rate limits"
|
|
1876
|
+
}
|
|
1877
|
+
},
|
|
1878
|
+
required: ["owner", "repo", "skillName"]
|
|
1879
|
+
}
|
|
1880
|
+
},
|
|
1881
|
+
{
|
|
1882
|
+
name: "install_skill",
|
|
1883
|
+
description: "Core installation tool. PATH A: returns npx skills add command string for agent to execute. PATH B: writes SKILL.md directly to disk if npx fails. Handles all selected agents simultaneously.",
|
|
1884
|
+
inputSchema: {
|
|
1885
|
+
type: "object",
|
|
1886
|
+
properties: {
|
|
1887
|
+
projectPath: {
|
|
1888
|
+
type: "string",
|
|
1889
|
+
description: "Absolute path to project root directory"
|
|
1890
|
+
},
|
|
1891
|
+
skillName: {
|
|
1892
|
+
type: "string",
|
|
1893
|
+
description: "Skill name to install"
|
|
1894
|
+
},
|
|
1895
|
+
agents: {
|
|
1896
|
+
type: "array",
|
|
1897
|
+
items: { type: "string" },
|
|
1898
|
+
description: "List of target agent names"
|
|
1899
|
+
},
|
|
1900
|
+
source: {
|
|
1901
|
+
type: "object",
|
|
1902
|
+
properties: {
|
|
1903
|
+
type: {
|
|
1904
|
+
type: "string",
|
|
1905
|
+
enum: ["github", "content"],
|
|
1906
|
+
description: "Source type"
|
|
1907
|
+
},
|
|
1908
|
+
owner: {
|
|
1909
|
+
type: "string",
|
|
1910
|
+
description: "GitHub owner (for type: github)"
|
|
1911
|
+
},
|
|
1912
|
+
repo: {
|
|
1913
|
+
type: "string",
|
|
1914
|
+
description: "GitHub repo (for type: github)"
|
|
1915
|
+
},
|
|
1916
|
+
content: {
|
|
1917
|
+
type: "string",
|
|
1918
|
+
description: "Direct SKILL.md content (for type: content)"
|
|
1919
|
+
}
|
|
1920
|
+
},
|
|
1921
|
+
required: ["type"],
|
|
1922
|
+
description: "Skill source"
|
|
1923
|
+
},
|
|
1924
|
+
overwrite: {
|
|
1925
|
+
type: "boolean",
|
|
1926
|
+
description: "Overwrite existing skill if present",
|
|
1927
|
+
default: false
|
|
1928
|
+
},
|
|
1929
|
+
global: {
|
|
1930
|
+
type: "boolean",
|
|
1931
|
+
description: "Install to global scope",
|
|
1932
|
+
default: false
|
|
1933
|
+
}
|
|
1934
|
+
},
|
|
1935
|
+
required: ["projectPath", "skillName", "agents", "source"]
|
|
1936
|
+
}
|
|
1937
|
+
},
|
|
1938
|
+
{
|
|
1939
|
+
name: "generate_skill",
|
|
1940
|
+
description: "LLM-powered skill generation. Searches web via Tavily for best practices, sends research + project context to LLM, generates a gold-standard SKILL.md. Only called when search and fetch fail.",
|
|
1941
|
+
inputSchema: {
|
|
1942
|
+
type: "object",
|
|
1943
|
+
properties: {
|
|
1944
|
+
skillName: {
|
|
1945
|
+
type: "string",
|
|
1946
|
+
description: "Skill name/domain to generate"
|
|
1947
|
+
},
|
|
1948
|
+
projectContext: {
|
|
1949
|
+
type: "string",
|
|
1950
|
+
description: "Rich project context: frameworks, versions, patterns, structure"
|
|
1951
|
+
},
|
|
1952
|
+
researchQuery: {
|
|
1953
|
+
type: "string",
|
|
1954
|
+
description: "Optional custom web research query"
|
|
1955
|
+
},
|
|
1956
|
+
maxResearchResults: {
|
|
1957
|
+
type: "number",
|
|
1958
|
+
description: "Maximum web research results",
|
|
1959
|
+
default: 10
|
|
1960
|
+
}
|
|
1961
|
+
},
|
|
1962
|
+
required: ["skillName", "projectContext"]
|
|
1963
|
+
}
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
name: "list_installed_skills",
|
|
1967
|
+
description: "Reads all agent skill directories in the project. Returns table of installed skills grouped by agent with name, description, source, and last modified date.",
|
|
1968
|
+
inputSchema: {
|
|
1969
|
+
type: "object",
|
|
1970
|
+
properties: {
|
|
1971
|
+
projectPath: {
|
|
1972
|
+
type: "string",
|
|
1973
|
+
description: "Absolute path to project root directory"
|
|
1974
|
+
},
|
|
1975
|
+
agent: {
|
|
1976
|
+
type: "string",
|
|
1977
|
+
description: "Filter by specific agent name"
|
|
1978
|
+
},
|
|
1979
|
+
global: {
|
|
1980
|
+
type: "boolean",
|
|
1981
|
+
description: "List global skills instead of project skills",
|
|
1982
|
+
default: false
|
|
1983
|
+
}
|
|
1984
|
+
},
|
|
1985
|
+
required: ["projectPath"]
|
|
1986
|
+
}
|
|
1987
|
+
},
|
|
1988
|
+
{
|
|
1989
|
+
name: "delete_skill",
|
|
1990
|
+
description: "Removes a specific SKILL.md from one or all agent directories. Returns success/failure per agent.",
|
|
1991
|
+
inputSchema: {
|
|
1992
|
+
type: "object",
|
|
1993
|
+
properties: {
|
|
1994
|
+
projectPath: {
|
|
1995
|
+
type: "string",
|
|
1996
|
+
description: "Absolute path to project root directory"
|
|
1997
|
+
},
|
|
1998
|
+
skillName: {
|
|
1999
|
+
type: "string",
|
|
2000
|
+
description: "Skill name to delete"
|
|
2001
|
+
},
|
|
2002
|
+
agents: {
|
|
2003
|
+
type: "array",
|
|
2004
|
+
items: { type: "string" },
|
|
2005
|
+
description: "Target agents (if omitted, deletes from all agents)"
|
|
2006
|
+
},
|
|
2007
|
+
global: {
|
|
2008
|
+
type: "boolean",
|
|
2009
|
+
description: "Delete from global scope",
|
|
2010
|
+
default: false
|
|
2011
|
+
}
|
|
2012
|
+
},
|
|
2013
|
+
required: ["projectPath", "skillName"]
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
]
|
|
2017
|
+
};
|
|
2018
|
+
});
|
|
2019
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2020
|
+
const { name, arguments: args } = request.params;
|
|
2021
|
+
try {
|
|
2022
|
+
switch (name) {
|
|
2023
|
+
case "detect_installed_agents": {
|
|
2024
|
+
const validated = DetectAgentsInputSchema.parse(args);
|
|
2025
|
+
const result = await detectAgents(validated);
|
|
2026
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2027
|
+
}
|
|
2028
|
+
case "create_skills_folder": {
|
|
2029
|
+
const validated = CreateFolderInputSchema.parse(args);
|
|
2030
|
+
const result = await createFolder(validated);
|
|
2031
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2032
|
+
}
|
|
2033
|
+
case "search_skills": {
|
|
2034
|
+
const validated = SearchSkillsInputSchema.parse(args);
|
|
2035
|
+
const result = await searchSkills(validated);
|
|
2036
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2037
|
+
}
|
|
2038
|
+
case "fetch_skill_content": {
|
|
2039
|
+
const validated = FetchSkillInputSchema.parse(args);
|
|
2040
|
+
const result = await fetchSkillContent(validated);
|
|
2041
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2042
|
+
}
|
|
2043
|
+
case "install_skill": {
|
|
2044
|
+
const validated = InstallSkillInputSchema.parse(args);
|
|
2045
|
+
const result = await installSkill(validated);
|
|
2046
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2047
|
+
}
|
|
2048
|
+
case "generate_skill": {
|
|
2049
|
+
const validated = GenerateSkillInputSchema.parse(args);
|
|
2050
|
+
const result = await generateSkill(validated);
|
|
2051
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2052
|
+
}
|
|
2053
|
+
case "list_installed_skills": {
|
|
2054
|
+
const validated = ListSkillsInputSchema.parse(args);
|
|
2055
|
+
const result = await listSkills(validated);
|
|
2056
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2057
|
+
}
|
|
2058
|
+
case "delete_skill": {
|
|
2059
|
+
const validated = DeleteSkillInputSchema.parse(args);
|
|
2060
|
+
const result = await deleteSkill(validated);
|
|
2061
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2062
|
+
}
|
|
2063
|
+
default:
|
|
2064
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2065
|
+
}
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2068
|
+
return {
|
|
2069
|
+
content: [
|
|
2070
|
+
{
|
|
2071
|
+
type: "text",
|
|
2072
|
+
text: JSON.stringify({ error: errorMessage }, null, 2)
|
|
2073
|
+
}
|
|
2074
|
+
],
|
|
2075
|
+
isError: true
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
async function main() {
|
|
2080
|
+
const transport = new StdioServerTransport();
|
|
2081
|
+
await server.connect(transport);
|
|
2082
|
+
console.error("SkillMesh MCP Server running on stdio");
|
|
2083
|
+
console.error("System prompt loaded:", SYSTEM_PROMPT.substring(0, 100) + "...");
|
|
2084
|
+
}
|
|
2085
|
+
main().catch((error) => {
|
|
2086
|
+
console.error("Fatal error in main():", error);
|
|
2087
|
+
process.exit(1);
|
|
2088
|
+
});
|
|
2089
|
+
//# sourceMappingURL=index.js.map
|
|
2090
|
+
//# sourceMappingURL=index.js.map
|