@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 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, promptRegistry, subscriptionManager) {
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, promptRegistry, subscriptionManager) {
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, promptRegistry, subscriptionManager);
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, resources, and prompts
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, promptRegistry, subscriptionManager);
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} -> SKILL.md content (template)
12
- * skill://{skillName}/ -> Collection: all files in skill directory
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";
@@ -8,12 +8,9 @@
8
8
  * skill updates when MCP roots change.
9
9
  *
10
10
  * URI Scheme:
11
- * skill://{skillName} -> SKILL.md content (template)
12
- * skill://{skillName}/ -> Collection: all files in skill directory
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
- // Note: Individual file resources (skill://{name}/{path}) are intentionally
59
- // not registered to reduce noise. Use the skill-resource tool to fetch
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
+ }
@@ -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
  }
@@ -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 (subscribable but not listed as resource)
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";
@@ -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 (subscribable but not listed as resource)
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.4.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"