@skilljack/mcp 0.4.0 → 0.5.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/dist/index.js +5 -12
- package/dist/skill-resources.d.ts +3 -6
- package/dist/skill-resources.js +94 -10
- package/dist/skill-tool.js +5 -1
- package/dist/subscriptions.d.ts +1 -1
- package/dist/subscriptions.js +1 -1
- package/package.json +11 -2
package/dist/index.js
CHANGED
|
@@ -18,7 +18,6 @@ import * as path from "node:path";
|
|
|
18
18
|
import { discoverSkills, createSkillMap } from "./skill-discovery.js";
|
|
19
19
|
import { registerSkillTool, getToolDescription } from "./skill-tool.js";
|
|
20
20
|
import { registerSkillResources } from "./skill-resources.js";
|
|
21
|
-
import { registerSkillPrompts, refreshPrompts } from "./skill-prompts.js";
|
|
22
21
|
import { createSubscriptionManager, registerSubscriptionHandlers, refreshSubscriptions, } from "./subscriptions.js";
|
|
23
22
|
/**
|
|
24
23
|
* Subdirectories to check for skills within the configured directory.
|
|
@@ -115,10 +114,9 @@ const SKILL_REFRESH_DEBOUNCE_MS = 500;
|
|
|
115
114
|
* @param skillsDirs - The configured skill directories
|
|
116
115
|
* @param server - The MCP server instance
|
|
117
116
|
* @param skillTool - The registered skill tool to update
|
|
118
|
-
* @param promptRegistry - For refreshing skill prompts
|
|
119
117
|
* @param subscriptionManager - For refreshing resource subscriptions
|
|
120
118
|
*/
|
|
121
|
-
function refreshSkills(skillsDirs, server, skillTool,
|
|
119
|
+
function refreshSkills(skillsDirs, server, skillTool, subscriptionManager) {
|
|
122
120
|
console.error("Refreshing skills...");
|
|
123
121
|
// Re-discover all skills
|
|
124
122
|
const skills = discoverSkillsFromDirs(skillsDirs);
|
|
@@ -130,8 +128,6 @@ function refreshSkills(skillsDirs, server, skillTool, promptRegistry, subscripti
|
|
|
130
128
|
skillTool.update({
|
|
131
129
|
description: getToolDescription(skillState),
|
|
132
130
|
});
|
|
133
|
-
// Refresh prompts to match new skill state
|
|
134
|
-
refreshPrompts(server, skillState, promptRegistry);
|
|
135
131
|
// Refresh resource subscriptions to match new skill state
|
|
136
132
|
refreshSubscriptions(subscriptionManager, skillState, (uri) => {
|
|
137
133
|
server.server.notification({
|
|
@@ -152,10 +148,9 @@ function refreshSkills(skillsDirs, server, skillTool, promptRegistry, subscripti
|
|
|
152
148
|
* @param skillsDirs - The configured skill directories
|
|
153
149
|
* @param server - The MCP server instance
|
|
154
150
|
* @param skillTool - The registered skill tool to update
|
|
155
|
-
* @param promptRegistry - For refreshing skill prompts
|
|
156
151
|
* @param subscriptionManager - For refreshing subscriptions
|
|
157
152
|
*/
|
|
158
|
-
function watchSkillDirectories(skillsDirs, server, skillTool,
|
|
153
|
+
function watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager) {
|
|
159
154
|
let refreshTimeout = null;
|
|
160
155
|
const debouncedRefresh = () => {
|
|
161
156
|
if (refreshTimeout) {
|
|
@@ -163,7 +158,7 @@ function watchSkillDirectories(skillsDirs, server, skillTool, promptRegistry, su
|
|
|
163
158
|
}
|
|
164
159
|
refreshTimeout = setTimeout(() => {
|
|
165
160
|
refreshTimeout = null;
|
|
166
|
-
refreshSkills(skillsDirs, server, skillTool,
|
|
161
|
+
refreshSkills(skillsDirs, server, skillTool, subscriptionManager);
|
|
167
162
|
}, SKILL_REFRESH_DEBOUNCE_MS);
|
|
168
163
|
};
|
|
169
164
|
// Build list of paths to watch
|
|
@@ -256,17 +251,15 @@ async function main() {
|
|
|
256
251
|
capabilities: {
|
|
257
252
|
tools: { listChanged: true },
|
|
258
253
|
resources: { subscribe: true, listChanged: true },
|
|
259
|
-
prompts: { listChanged: true },
|
|
260
254
|
},
|
|
261
255
|
});
|
|
262
|
-
// Register tools
|
|
256
|
+
// Register tools and resources
|
|
263
257
|
const skillTool = registerSkillTool(server, skillState);
|
|
264
258
|
registerSkillResources(server, skillState);
|
|
265
|
-
const promptRegistry = registerSkillPrompts(server, skillState);
|
|
266
259
|
// Register subscription handlers for resource file watching
|
|
267
260
|
registerSubscriptionHandlers(server, skillState, subscriptionManager);
|
|
268
261
|
// Set up file watchers for skill directory changes
|
|
269
|
-
watchSkillDirectories(skillsDirs, server, skillTool,
|
|
262
|
+
watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager);
|
|
270
263
|
// Connect via stdio transport
|
|
271
264
|
const transport = new StdioServerTransport();
|
|
272
265
|
await server.connect(transport);
|
|
@@ -8,12 +8,9 @@
|
|
|
8
8
|
* skill updates when MCP roots change.
|
|
9
9
|
*
|
|
10
10
|
* URI Scheme:
|
|
11
|
-
* skill://{skillName}
|
|
12
|
-
* skill://{skillName}/
|
|
13
|
-
*
|
|
14
|
-
* Note: Individual file URIs (skill://{skillName}/{path}) are not listed
|
|
15
|
-
* as resources to reduce noise. Use the skill-resource tool to fetch
|
|
16
|
-
* specific files on demand.
|
|
11
|
+
* skill://{skillName} -> SKILL.md content (template)
|
|
12
|
+
* skill://{skillName}/ -> Collection: all files in skill directory
|
|
13
|
+
* skill://{skillName}/{path} -> File within skill directory (template)
|
|
17
14
|
*/
|
|
18
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
16
|
import { SkillState } from "./skill-tool.js";
|
package/dist/skill-resources.js
CHANGED
|
@@ -8,12 +8,9 @@
|
|
|
8
8
|
* skill updates when MCP roots change.
|
|
9
9
|
*
|
|
10
10
|
* URI Scheme:
|
|
11
|
-
* skill://{skillName}
|
|
12
|
-
* skill://{skillName}/
|
|
13
|
-
*
|
|
14
|
-
* Note: Individual file URIs (skill://{skillName}/{path}) are not listed
|
|
15
|
-
* as resources to reduce noise. Use the skill-resource tool to fetch
|
|
16
|
-
* specific files on demand.
|
|
11
|
+
* skill://{skillName} -> SKILL.md content (template)
|
|
12
|
+
* skill://{skillName}/ -> Collection: all files in skill directory
|
|
13
|
+
* skill://{skillName}/{path} -> File within skill directory (template)
|
|
17
14
|
*/
|
|
18
15
|
import * as fs from "node:fs";
|
|
19
16
|
import * as path from "node:path";
|
|
@@ -53,11 +50,10 @@ function getMimeType(filePath) {
|
|
|
53
50
|
export function registerSkillResources(server, skillState) {
|
|
54
51
|
// Register template for individual skill SKILL.md files
|
|
55
52
|
registerSkillTemplate(server, skillState);
|
|
56
|
-
// Register collection resource for skill directories
|
|
53
|
+
// Register collection resource for skill directories (must be before file template)
|
|
57
54
|
registerSkillDirectoryCollection(server, skillState);
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
// specific files on demand.
|
|
55
|
+
// Register resource template for skill files
|
|
56
|
+
registerSkillFileTemplate(server, skillState);
|
|
61
57
|
}
|
|
62
58
|
/**
|
|
63
59
|
* Register a collection resource for skill directories.
|
|
@@ -200,3 +196,91 @@ function registerSkillTemplate(server, skillState) {
|
|
|
200
196
|
}
|
|
201
197
|
});
|
|
202
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Register the resource template for accessing files within skills.
|
|
201
|
+
*
|
|
202
|
+
* URI Pattern: skill://{skillName}/{filePath}
|
|
203
|
+
*/
|
|
204
|
+
function registerSkillFileTemplate(server, skillState) {
|
|
205
|
+
server.registerResource("Skill File", new ResourceTemplate("skill://{skillName}/{+filePath}", {
|
|
206
|
+
list: async () => {
|
|
207
|
+
// Return all listable skill files (dynamic based on current skillMap)
|
|
208
|
+
const resources = [];
|
|
209
|
+
for (const [name, skill] of skillState.skillMap) {
|
|
210
|
+
const skillDir = path.dirname(skill.path);
|
|
211
|
+
const files = listSkillFiles(skillDir);
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
const uri = `skill://${encodeURIComponent(name)}/${file}`;
|
|
214
|
+
resources.push({
|
|
215
|
+
uri,
|
|
216
|
+
name: `${name}/${file}`,
|
|
217
|
+
mimeType: getMimeType(file),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { resources };
|
|
222
|
+
},
|
|
223
|
+
complete: {
|
|
224
|
+
skillName: (value) => {
|
|
225
|
+
const names = Array.from(skillState.skillMap.keys());
|
|
226
|
+
return names.filter((name) => name.toLowerCase().startsWith(value.toLowerCase()));
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
}), {
|
|
230
|
+
mimeType: "text/plain",
|
|
231
|
+
description: "Files within a skill directory (scripts, snippets, assets, etc.)",
|
|
232
|
+
}, async (resourceUri, variables) => {
|
|
233
|
+
// Extract skill name and file path from URI
|
|
234
|
+
const uriStr = resourceUri.toString();
|
|
235
|
+
const match = uriStr.match(/^skill:\/\/([^/]+)\/(.+)$/);
|
|
236
|
+
if (!match) {
|
|
237
|
+
throw new Error(`Invalid skill file URI: ${uriStr}`);
|
|
238
|
+
}
|
|
239
|
+
const skillName = decodeURIComponent(match[1]);
|
|
240
|
+
const filePath = match[2];
|
|
241
|
+
const skill = skillState.skillMap.get(skillName);
|
|
242
|
+
if (!skill) {
|
|
243
|
+
const available = Array.from(skillState.skillMap.keys()).join(", ");
|
|
244
|
+
throw new Error(`Skill "${skillName}" not found. Available: ${available || "none"}`);
|
|
245
|
+
}
|
|
246
|
+
const skillDir = path.dirname(skill.path);
|
|
247
|
+
const fullPath = path.resolve(skillDir, filePath);
|
|
248
|
+
// Security: Validate path is within skill directory
|
|
249
|
+
if (!isPathWithinBase(fullPath, skillDir)) {
|
|
250
|
+
throw new Error(`Path "${filePath}" is outside the skill directory`);
|
|
251
|
+
}
|
|
252
|
+
// Check file exists
|
|
253
|
+
if (!fs.existsSync(fullPath)) {
|
|
254
|
+
const files = listSkillFiles(skillDir).slice(0, 10);
|
|
255
|
+
throw new Error(`File "${filePath}" not found in skill "${skillName}". ` +
|
|
256
|
+
`Available: ${files.join(", ")}${files.length >= 10 ? "..." : ""}`);
|
|
257
|
+
}
|
|
258
|
+
const stat = fs.statSync(fullPath);
|
|
259
|
+
// Reject symlinks
|
|
260
|
+
if (stat.isSymbolicLink()) {
|
|
261
|
+
throw new Error(`Cannot read symlink "${filePath}"`);
|
|
262
|
+
}
|
|
263
|
+
// Reject directories
|
|
264
|
+
if (stat.isDirectory()) {
|
|
265
|
+
const files = listSkillFiles(skillDir, filePath);
|
|
266
|
+
throw new Error(`"${filePath}" is a directory. Files within: ${files.join(", ")}`);
|
|
267
|
+
}
|
|
268
|
+
// Check file size
|
|
269
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
270
|
+
const sizeMB = (stat.size / 1024 / 1024).toFixed(2);
|
|
271
|
+
throw new Error(`File too large (${sizeMB}MB). Maximum: 10MB`);
|
|
272
|
+
}
|
|
273
|
+
// Read and return content
|
|
274
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
275
|
+
const mimeType = getMimeType(fullPath);
|
|
276
|
+
return {
|
|
277
|
+
contents: [
|
|
278
|
+
{
|
|
279
|
+
uri: uriStr,
|
|
280
|
+
mimeType,
|
|
281
|
+
text: content,
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
}
|
package/dist/skill-tool.js
CHANGED
|
@@ -33,7 +33,11 @@ const SkillSchema = z.object({
|
|
|
33
33
|
*/
|
|
34
34
|
export function getToolDescription(skillState) {
|
|
35
35
|
const usage = "Load a skill's full instructions. Returns the complete SKILL.md content " +
|
|
36
|
-
"with step-by-step guidance, examples, and file references to follow.\n\n"
|
|
36
|
+
"with step-by-step guidance, examples, and file references to follow.\n\n" +
|
|
37
|
+
"IMPORTANT: When a skill is relevant to the user's task, you must invoke this tool " +
|
|
38
|
+
"IMMEDIATELY as your first action. NEVER just announce or mention a skill without " +
|
|
39
|
+
"actually calling this tool. This is a BLOCKING REQUIREMENT: invoke this tool BEFORE " +
|
|
40
|
+
"generating any other response about the task.\n\n";
|
|
37
41
|
const skills = Array.from(skillState.skillMap.values());
|
|
38
42
|
return usage + generateInstructions(skills);
|
|
39
43
|
}
|
package/dist/subscriptions.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - skill:// → Watch all skill directories
|
|
10
10
|
* - skill://{name} → Watch that skill's SKILL.md
|
|
11
11
|
* - skill://{name}/ → Watch entire skill directory (directory collection)
|
|
12
|
-
* - skill://{name}/{path} → Watch specific file
|
|
12
|
+
* - skill://{name}/{path} → Watch specific file
|
|
13
13
|
*/
|
|
14
14
|
import { FSWatcher } from "chokidar";
|
|
15
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
package/dist/subscriptions.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - skill:// → Watch all skill directories
|
|
10
10
|
* - skill://{name} → Watch that skill's SKILL.md
|
|
11
11
|
* - skill://{name}/ → Watch entire skill directory (directory collection)
|
|
12
|
-
* - skill://{name}/{path} → Watch specific file
|
|
12
|
+
* - skill://{name}/{path} → Watch specific file
|
|
13
13
|
*/
|
|
14
14
|
import chokidar from "chokidar";
|
|
15
15
|
import * as path from "node:path";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skilljack/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server that discovers and serves Agent Skills. I know kung fu.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -38,7 +38,15 @@
|
|
|
38
38
|
"build": "tsc",
|
|
39
39
|
"start": "node dist/index.js",
|
|
40
40
|
"dev": "tsx watch src/index.ts",
|
|
41
|
-
"inspector": "npx @modelcontextprotocol/inspector@latest node dist/index.js"
|
|
41
|
+
"inspector": "npx @modelcontextprotocol/inspector@latest node dist/index.js",
|
|
42
|
+
"eval": "tsx evals/eval.ts",
|
|
43
|
+
"eval:greeting": "tsx evals/eval.ts --task=greeting",
|
|
44
|
+
"eval:code-style": "tsx evals/eval.ts --task=code-style",
|
|
45
|
+
"eval:template": "tsx evals/eval.ts --task=template-generator",
|
|
46
|
+
"eval:xlsx-openpyxl": "tsx evals/eval.ts --task=xlsx-openpyxl",
|
|
47
|
+
"eval:xlsx-formulas": "tsx evals/eval.ts --task=xlsx-formulas",
|
|
48
|
+
"eval:xlsx-financial": "tsx evals/eval.ts --task=xlsx-financial",
|
|
49
|
+
"eval:xlsx-verify": "tsx evals/eval.ts --task=xlsx-verify"
|
|
42
50
|
},
|
|
43
51
|
"dependencies": {
|
|
44
52
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
@@ -47,6 +55,7 @@
|
|
|
47
55
|
"zod": "^3.25.0"
|
|
48
56
|
},
|
|
49
57
|
"devDependencies": {
|
|
58
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.42",
|
|
50
59
|
"@types/node": "^22.10.0",
|
|
51
60
|
"tsx": "^4.19.2",
|
|
52
61
|
"typescript": "^5.7.2"
|