@olaservo/skill-jack-mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  An MCP server that jacks [Agent Skills](https://agentskills.io) directly into your LLM's brain.
4
4
 
5
- > **Recommended:** For best results, use an [MCP client](https://modelcontextprotocol.io/clients) that supports server instructions. This allows the LLM to see available skills in its system prompt, enabling automatic skill discovery and activation. Without this support, the model will still be able to call these tools, but you might need to provide more explicit instructions on what skills are available and the intended activation patterns.
5
+ > **Recommended:** For best results, use an MCP client that supports `tools/listChanged` notifications (e.g., Claude Code). This enables dynamic skill discovery - when skills are added or modified, the client automatically refreshes its understanding of available skills.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Skill Discovery** - Discovers skills from a configured directory at startup
10
- - **Server Instructions** - Injects skill metadata into the system prompt (for clients supporting instructions)
9
+ - **Dynamic Skill Discovery** - Watches skill directories and automatically refreshes when skills change
10
+ - **Tool List Changed Notifications** - Sends `tools/listChanged` so clients can refresh available skills
11
11
  - **Skill Tool** - Load full skill content on demand (progressive disclosure)
12
12
  - **MCP Resources** - Access skills via `skill://` URIs with batch collection support
13
13
  - **Resource Subscriptions** - Real-time file watching with `notifications/resources/updated`
@@ -59,26 +59,37 @@ skill-jack-mcp "C:/Users/you/skills"
59
59
 
60
60
  ## How It Works
61
61
 
62
- The server implements the [Agent Skills](https://agentskills.io) progressive disclosure pattern:
62
+ The server implements the [Agent Skills](https://agentskills.io) progressive disclosure pattern with dynamic updates:
63
63
 
64
- 1. **At startup**: Discovers skills from configured directories
65
- 2. **On connection**: Server instructions (with skill metadata) are sent in the initialize response
66
- 3. **On tool call**: Agent calls `skill` tool to load full SKILL.md content
64
+ 1. **At startup**: Discovers skills from configured directories and starts file watchers
65
+ 2. **On connection**: Skill tool description includes available skills metadata
66
+ 3. **On file change**: Re-discovers skills, updates tool description, sends `tools/listChanged`
67
+ 4. **On tool call**: Agent calls `skill` tool to load full SKILL.md content
68
+ 5. **As needed**: Agent calls `skill-resource` to load additional files
67
69
 
68
70
  ```
69
71
  ┌─────────────────────────────────────────────────────────┐
70
72
  │ Server starts │
71
73
  │ • Discovers skills from configured directories │
72
- │ • Generates instructions with skill metadata
74
+ │ • Starts watching for SKILL.md changes
73
75
  │ ↓ │
74
76
  │ MCP Client connects │
75
- │ • Server instructions included in initialize response
77
+ │ • Skill tool description includes available skills
76
78
  │ ↓ │
77
- │ LLM sees skill metadata in system prompt
79
+ │ LLM sees skill metadata in tool description
80
+ │ ↓ │
81
+ │ SKILL.md added/modified/removed │
82
+ │ • Server re-discovers skills │
83
+ │ • Updates skill tool description │
84
+ │ • Sends tools/listChanged notification │
85
+ │ • Client refreshes tool definitions │
78
86
  │ ↓ │
79
87
  │ LLM calls "skill" tool with skill name │
80
88
  │ ↓ │
81
89
  │ Server returns full SKILL.md content │
90
+ │ ↓ │
91
+ │ LLM calls "skill-resource" for additional files │
92
+ │ • Scripts, snippets, references, assets, etc. │
82
93
  └─────────────────────────────────────────────────────────┘
83
94
  ```
84
95
 
@@ -138,7 +149,6 @@ Skills are also accessible via MCP [Resources](https://modelcontextprotocol.io/s
138
149
 
139
150
  | URI | Returns |
140
151
  |-----|---------|
141
- | `skill://` | All SKILL.md contents (collection) |
142
152
  | `skill://{name}` | Single skill's SKILL.md content |
143
153
  | `skill://{name}/` | All files in skill directory (collection) |
144
154
  | `skill://{name}/{path}` | Specific file within skill |
@@ -178,7 +188,7 @@ Clients can subscribe to resources for real-time updates when files change.
178
188
 
179
189
  Protections in place:
180
190
  - Path traversal prevention (symlink-aware)
181
- - File size limits (10MB max)
191
+ - File size limits (1MB default, configurable via `MAX_FILE_SIZE_MB` env var)
182
192
  - Directory depth limits
183
193
  - Skill content is confined to configured directories
184
194
 
@@ -186,9 +196,18 @@ Not protected against:
186
196
  - Malicious content within trusted skill directories
187
197
  - Prompt injection via skill instructions (skills can influence LLM behavior by design)
188
198
 
189
- ## Server Instructions Format
199
+ ## Dynamic Skill Discovery
200
+
201
+ The server watches skill directories for changes. When SKILL.md files are added, modified, or removed:
202
+
203
+ 1. Skills are re-discovered from all configured directories
204
+ 2. The `skill` tool's description is updated with current skill names and metadata
205
+ 3. `tools/listChanged` notification is sent to connected clients
206
+ 4. Clients that support this notification will refresh tool definitions
207
+
208
+ ## Skill Metadata Format
190
209
 
191
- The server generates [instructions](https://blog.modelcontextprotocol.io/posts/2025-11-03-using-server-instructions/) that include a usage preamble and skill metadata:
210
+ The `skill` tool description includes metadata for all available skills in XML format:
192
211
 
193
212
  ```markdown
194
213
  # Skills
@@ -204,7 +223,7 @@ When a user's task matches a skill description below: 1) activate it, 2) follow
204
223
  </available_skills>
205
224
  ```
206
225
 
207
- These are loaded into the model's system prompt by [clients](https://modelcontextprotocol.io/clients) that support instructions.
226
+ This metadata is dynamically updated when skills change - clients supporting `tools/listChanged` will automatically refresh.
208
227
 
209
228
  ## Skill Discovery
210
229
 
package/dist/index.js CHANGED
@@ -12,12 +12,13 @@
12
12
  */
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import chokidar from "chokidar";
15
16
  import * as fs from "node:fs";
16
17
  import * as path from "node:path";
17
- import { discoverSkills, generateInstructions, createSkillMap } from "./skill-discovery.js";
18
- import { registerSkillTool } from "./skill-tool.js";
18
+ import { discoverSkills, createSkillMap } from "./skill-discovery.js";
19
+ import { registerSkillTool, getToolDescription } from "./skill-tool.js";
19
20
  import { registerSkillResources } from "./skill-resources.js";
20
- import { createSubscriptionManager, registerSubscriptionHandlers, } from "./subscriptions.js";
21
+ import { createSubscriptionManager, registerSubscriptionHandlers, refreshSubscriptions, } from "./subscriptions.js";
21
22
  /**
22
23
  * Subdirectories to check for skills within the configured directory.
23
24
  */
@@ -64,7 +65,6 @@ function getSkillsDirs() {
64
65
  */
65
66
  const skillState = {
66
67
  skillMap: new Map(),
67
- instructions: "",
68
68
  };
69
69
  /**
70
70
  * Discover skills from multiple configured directories.
@@ -102,6 +102,129 @@ function discoverSkillsFromDirs(skillsDirs) {
102
102
  }
103
103
  return allSkills;
104
104
  }
105
+ /**
106
+ * Debounce delay for skill directory changes (ms).
107
+ * Multiple rapid changes are coalesced into one refresh.
108
+ */
109
+ const SKILL_REFRESH_DEBOUNCE_MS = 500;
110
+ /**
111
+ * Refresh skills and notify clients of changes.
112
+ * Called when skill files change on disk.
113
+ *
114
+ * @param skillsDirs - The configured skill directories
115
+ * @param server - The MCP server instance
116
+ * @param skillTool - The registered skill tool to update
117
+ * @param subscriptionManager - For refreshing resource subscriptions
118
+ */
119
+ function refreshSkills(skillsDirs, server, skillTool, subscriptionManager) {
120
+ console.error("Refreshing skills...");
121
+ // Re-discover all skills
122
+ const skills = discoverSkillsFromDirs(skillsDirs);
123
+ const oldCount = skillState.skillMap.size;
124
+ // Update shared state
125
+ skillState.skillMap = createSkillMap(skills);
126
+ console.error(`Skills refreshed: ${oldCount} -> ${skills.length} skill(s)`);
127
+ // Update the skill tool description with new instructions
128
+ skillTool.update({
129
+ description: getToolDescription(skillState),
130
+ });
131
+ // Refresh resource subscriptions to match new skill state
132
+ refreshSubscriptions(subscriptionManager, skillState, (uri) => {
133
+ server.server.notification({
134
+ method: "notifications/resources/updated",
135
+ params: { uri },
136
+ });
137
+ });
138
+ // Notify clients that tools have changed
139
+ // This prompts clients to call tools/list again
140
+ server.sendToolListChanged();
141
+ // Also notify that resources have changed
142
+ server.sendResourceListChanged();
143
+ }
144
+ /**
145
+ * Set up file watchers on skill directories to detect changes.
146
+ * Watches for SKILL.md additions, modifications, and deletions.
147
+ *
148
+ * @param skillsDirs - The configured skill directories
149
+ * @param server - The MCP server instance
150
+ * @param skillTool - The registered skill tool to update
151
+ * @param subscriptionManager - For refreshing subscriptions
152
+ */
153
+ function watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager) {
154
+ let refreshTimeout = null;
155
+ const debouncedRefresh = () => {
156
+ if (refreshTimeout) {
157
+ clearTimeout(refreshTimeout);
158
+ }
159
+ refreshTimeout = setTimeout(() => {
160
+ refreshTimeout = null;
161
+ refreshSkills(skillsDirs, server, skillTool, subscriptionManager);
162
+ }, SKILL_REFRESH_DEBOUNCE_MS);
163
+ };
164
+ // Build list of paths to watch
165
+ const watchPaths = [];
166
+ for (const dir of skillsDirs) {
167
+ if (fs.existsSync(dir)) {
168
+ watchPaths.push(dir);
169
+ // Also watch standard subdirectories
170
+ for (const subdir of SKILL_SUBDIRS) {
171
+ const subPath = path.join(dir, subdir);
172
+ if (fs.existsSync(subPath)) {
173
+ watchPaths.push(subPath);
174
+ }
175
+ }
176
+ }
177
+ }
178
+ if (watchPaths.length === 0) {
179
+ console.error("No skill directories to watch");
180
+ return;
181
+ }
182
+ console.error(`Watching for skill changes in: ${watchPaths.join(", ")}`);
183
+ const watcher = chokidar.watch(watchPaths, {
184
+ persistent: true,
185
+ ignoreInitial: true,
186
+ depth: 2, // Watch skill subdirectories but not too deep
187
+ ignored: ["**/node_modules/**", "**/.git/**"],
188
+ awaitWriteFinish: {
189
+ stabilityThreshold: 200,
190
+ pollInterval: 50,
191
+ },
192
+ });
193
+ // Watch for SKILL.md changes specifically
194
+ watcher.on("add", (filePath) => {
195
+ if (path.basename(filePath).toLowerCase() === "skill.md") {
196
+ console.error(`Skill added: ${filePath}`);
197
+ debouncedRefresh();
198
+ }
199
+ });
200
+ watcher.on("change", (filePath) => {
201
+ if (path.basename(filePath).toLowerCase() === "skill.md") {
202
+ console.error(`Skill modified: ${filePath}`);
203
+ debouncedRefresh();
204
+ }
205
+ });
206
+ watcher.on("unlink", (filePath) => {
207
+ if (path.basename(filePath).toLowerCase() === "skill.md") {
208
+ console.error(`Skill removed: ${filePath}`);
209
+ debouncedRefresh();
210
+ }
211
+ });
212
+ // Also watch for directory additions (new skill folders)
213
+ watcher.on("addDir", (dirPath) => {
214
+ // Check if this might be a new skill directory
215
+ const skillMdPath = path.join(dirPath, "SKILL.md");
216
+ const skillMdPathLower = path.join(dirPath, "skill.md");
217
+ if (fs.existsSync(skillMdPath) || fs.existsSync(skillMdPathLower)) {
218
+ console.error(`Skill directory added: ${dirPath}`);
219
+ debouncedRefresh();
220
+ }
221
+ });
222
+ watcher.on("unlinkDir", (dirPath) => {
223
+ // A skill directory was removed
224
+ console.error(`Directory removed: ${dirPath}`);
225
+ debouncedRefresh();
226
+ });
227
+ }
105
228
  /**
106
229
  * Subscription manager for resource file watching.
107
230
  */
@@ -119,7 +242,6 @@ async function main() {
119
242
  // Discover skills at startup
120
243
  const skills = discoverSkillsFromDirs(skillsDirs);
121
244
  skillState.skillMap = createSkillMap(skills);
122
- skillState.instructions = generateInstructions(skills);
123
245
  console.error(`Discovered ${skills.length} skill(s)`);
124
246
  // Create the MCP server
125
247
  const server = new McpServer({
@@ -127,16 +249,17 @@ async function main() {
127
249
  version: "1.0.0",
128
250
  }, {
129
251
  capabilities: {
130
- tools: {},
252
+ tools: { listChanged: true },
131
253
  resources: { subscribe: true, listChanged: true },
132
254
  },
133
- instructions: skillState.instructions,
134
255
  });
135
256
  // Register tools and resources
136
- registerSkillTool(server, skillState);
257
+ const skillTool = registerSkillTool(server, skillState);
137
258
  registerSkillResources(server, skillState);
138
259
  // Register subscription handlers for resource file watching
139
260
  registerSubscriptionHandlers(server, skillState, subscriptionManager);
261
+ // Set up file watchers for skill directory changes
262
+ watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager);
140
263
  // Connect via stdio transport
141
264
  const transport = new StdioServerTransport();
142
265
  await server.connect(transport);
@@ -28,5 +28,6 @@ export declare function generateInstructions(skills: SkillMetadata[]): string;
28
28
  export declare function loadSkillContent(skillPath: string): string;
29
29
  /**
30
30
  * Create a map from skill name to skill metadata for fast lookup.
31
+ * Uses first-wins behavior: if duplicate names exist, the first occurrence is kept.
31
32
  */
32
33
  export declare function createSkillMap(skills: SkillMetadata[]): Map<string, SkillMetadata>;
@@ -126,11 +126,19 @@ export function loadSkillContent(skillPath) {
126
126
  }
127
127
  /**
128
128
  * Create a map from skill name to skill metadata for fast lookup.
129
+ * Uses first-wins behavior: if duplicate names exist, the first occurrence is kept.
129
130
  */
130
131
  export function createSkillMap(skills) {
131
132
  const map = new Map();
132
133
  for (const skill of skills) {
133
- map.set(skill.name, skill);
134
+ if (map.has(skill.name)) {
135
+ const existing = map.get(skill.name);
136
+ console.error(`Warning: Duplicate skill name "${skill.name}" found at ${skill.path} - ` +
137
+ `keeping first occurrence from ${existing.path}`);
138
+ }
139
+ else {
140
+ map.set(skill.name, skill);
141
+ }
134
142
  }
135
143
  return map;
136
144
  }
@@ -8,9 +8,8 @@
8
8
  * skill updates when MCP roots change.
9
9
  *
10
10
  * URI Scheme:
11
- * skill:// -> Collection: all SKILL.md contents
12
11
  * skill://{skillName} -> SKILL.md content (template)
13
- * skill://{skillName}/ -> Collection: all files in skill
12
+ * skill://{skillName}/ -> Collection: all files in skill directory
14
13
  * skill://{skillName}/{path} -> File within skill directory (template)
15
14
  */
16
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -8,9 +8,8 @@
8
8
  * skill updates when MCP roots change.
9
9
  *
10
10
  * URI Scheme:
11
- * skill:// -> Collection: all SKILL.md contents
12
11
  * skill://{skillName} -> SKILL.md content (template)
13
- * skill://{skillName}/ -> Collection: all files in skill
12
+ * skill://{skillName}/ -> Collection: all files in skill directory
14
13
  * skill://{skillName}/{path} -> File within skill directory (template)
15
14
  */
16
15
  import * as fs from "node:fs";
@@ -49,39 +48,87 @@ function getMimeType(filePath) {
49
48
  * @param skillState - Shared state object (allows dynamic updates)
50
49
  */
51
50
  export function registerSkillResources(server, skillState) {
52
- // Register collection resource for all skills
53
- registerAllSkillsCollection(server, skillState);
54
51
  // Register template for individual skill SKILL.md files
55
52
  registerSkillTemplate(server, skillState);
53
+ // Register collection resource for skill directories (must be before file template)
54
+ registerSkillDirectoryCollection(server, skillState);
56
55
  // Register resource template for skill files
57
56
  registerSkillFileTemplate(server, skillState);
58
57
  }
59
58
  /**
60
- * Register a collection resource that returns all skills at once.
59
+ * Register a collection resource for skill directories.
61
60
  *
62
- * URI: skill://
61
+ * URI Pattern: skill://{skillName}/
63
62
  *
64
- * Returns multiple ResourceContents, one per skill, each with its own URI.
65
- * This allows clients to fetch all skill content in a single request.
63
+ * Returns all files in the skill directory (excluding SKILL.md) in a single request.
64
+ * This allows clients to fetch all resource files for a skill at once.
66
65
  */
67
- function registerAllSkillsCollection(server, skillState) {
68
- server.registerResource("All Skills", "skill://", {
69
- mimeType: "text/markdown",
70
- description: "Collection of all available skills. Returns all SKILL.md contents in one request.",
71
- }, async () => {
66
+ function registerSkillDirectoryCollection(server, skillState) {
67
+ server.registerResource("Skill Directory", new ResourceTemplate("skill://{skillName}/", {
68
+ list: async () => {
69
+ // Return one entry per skill (the directory collection)
70
+ const resources = [];
71
+ for (const [name, skill] of skillState.skillMap) {
72
+ resources.push({
73
+ uri: `skill://${encodeURIComponent(name)}/`,
74
+ name: `${name}/`,
75
+ mimeType: "text/plain",
76
+ description: `All files in ${name} skill directory`,
77
+ });
78
+ }
79
+ return { resources };
80
+ },
81
+ complete: {
82
+ skillName: (value) => {
83
+ const names = Array.from(skillState.skillMap.keys());
84
+ return names.filter((n) => n.toLowerCase().startsWith(value.toLowerCase()));
85
+ },
86
+ },
87
+ }), {
88
+ mimeType: "text/plain",
89
+ description: "Collection of all files in a skill directory (excluding SKILL.md)",
90
+ }, async (resourceUri) => {
91
+ // Extract skill name from URI
92
+ const uriStr = resourceUri.toString();
93
+ const match = uriStr.match(/^skill:\/\/([^/]+)\/$/);
94
+ if (!match) {
95
+ throw new Error(`Invalid skill directory URI: ${uriStr}`);
96
+ }
97
+ const skillName = decodeURIComponent(match[1]);
98
+ const skill = skillState.skillMap.get(skillName);
99
+ if (!skill) {
100
+ const available = Array.from(skillState.skillMap.keys()).join(", ");
101
+ throw new Error(`Skill "${skillName}" not found. Available: ${available || "none"}`);
102
+ }
103
+ const skillDir = path.dirname(skill.path);
104
+ const files = listSkillFiles(skillDir);
72
105
  const contents = [];
73
- for (const [name, skill] of skillState.skillMap) {
106
+ for (const file of files) {
107
+ const fullPath = path.join(skillDir, file);
108
+ // Security: Validate path is within skill directory
109
+ if (!isPathWithinBase(fullPath, skillDir)) {
110
+ continue; // Skip files outside skill directory
111
+ }
74
112
  try {
75
- const content = loadSkillContent(skill.path);
113
+ const stat = fs.statSync(fullPath);
114
+ // Skip symlinks and directories
115
+ if (stat.isSymbolicLink() || stat.isDirectory()) {
116
+ continue;
117
+ }
118
+ // Check file size
119
+ if (stat.size > MAX_FILE_SIZE) {
120
+ continue; // Skip large files
121
+ }
122
+ const content = fs.readFileSync(fullPath, "utf-8");
76
123
  contents.push({
77
- uri: `skill://${encodeURIComponent(name)}`,
78
- mimeType: "text/markdown",
124
+ uri: `skill://${encodeURIComponent(skillName)}/${file}`,
125
+ mimeType: getMimeType(file),
79
126
  text: content,
80
127
  });
81
128
  }
82
129
  catch (error) {
83
- // Skip skills that fail to load, but continue with others
84
- console.error(`Failed to load skill "${name}":`, error);
130
+ // Skip files that fail to load
131
+ console.error(`Failed to load file "${file}" in skill "${skillName}":`, error);
85
132
  }
86
133
  }
87
134
  return { contents };
@@ -7,7 +7,7 @@
7
7
  * Tools reference a shared SkillState object to support dynamic skill updates
8
8
  * when MCP roots change.
9
9
  */
10
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { McpServer, RegisteredTool } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
  import { SkillMetadata } from "./skill-discovery.js";
12
12
  /**
13
13
  * Shared state for dynamic skill management.
@@ -15,15 +15,23 @@ import { SkillMetadata } from "./skill-discovery.js";
15
15
  */
16
16
  export interface SkillState {
17
17
  skillMap: Map<string, SkillMetadata>;
18
- instructions: string;
19
18
  }
20
19
  /**
21
20
  * Register the "skill" tool with the MCP server.
22
21
  *
22
+ * The tool description includes the full skill discovery instructions (same format as
23
+ * server instructions) to enable dynamic updates via tools/listChanged notifications.
24
+ *
23
25
  * @param server - The McpServer instance
24
26
  * @param skillState - Shared state object (allows dynamic updates)
27
+ * @returns The registered tool, which can be updated when skills change
28
+ */
29
+ /**
30
+ * Generate the full tool description including usage guidance and skill list.
31
+ * Exported so index.ts can use it when refreshing skills.
25
32
  */
26
- export declare function registerSkillTool(server: McpServer, skillState: SkillState): void;
33
+ export declare function getToolDescription(skillState: SkillState): string;
34
+ export declare function registerSkillTool(server: McpServer, skillState: SkillState): RegisteredTool;
27
35
  export declare const MAX_FILE_SIZE: number;
28
36
  export declare const MAX_DIRECTORY_DEPTH = 10;
29
37
  /**
@@ -10,7 +10,7 @@
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
12
  import { z } from "zod";
13
- import { loadSkillContent } from "./skill-discovery.js";
13
+ import { loadSkillContent, generateInstructions } from "./skill-discovery.js";
14
14
  /**
15
15
  * Input schema for the skill tool.
16
16
  */
@@ -20,14 +20,27 @@ const SkillSchema = z.object({
20
20
  /**
21
21
  * Register the "skill" tool with the MCP server.
22
22
  *
23
+ * The tool description includes the full skill discovery instructions (same format as
24
+ * server instructions) to enable dynamic updates via tools/listChanged notifications.
25
+ *
23
26
  * @param server - The McpServer instance
24
27
  * @param skillState - Shared state object (allows dynamic updates)
28
+ * @returns The registered tool, which can be updated when skills change
29
+ */
30
+ /**
31
+ * Generate the full tool description including usage guidance and skill list.
32
+ * Exported so index.ts can use it when refreshing skills.
25
33
  */
34
+ export function getToolDescription(skillState) {
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";
37
+ const skills = Array.from(skillState.skillMap.values());
38
+ return usage + generateInstructions(skills);
39
+ }
26
40
  export function registerSkillTool(server, skillState) {
27
- server.registerTool("skill", {
41
+ const skillTool = server.registerTool("skill", {
28
42
  title: "Activate Skill",
29
- description: "Load a skill's full instructions. Returns the complete SKILL.md content " +
30
- "with step-by-step guidance, examples, and file references to follow.",
43
+ description: getToolDescription(skillState),
31
44
  inputSchema: SkillSchema,
32
45
  annotations: {
33
46
  readOnlyHint: true,
@@ -76,6 +89,7 @@ export function registerSkillTool(server, skillState) {
76
89
  });
77
90
  // Register the skill-resource tool
78
91
  registerSkillResourceTool(server, skillState);
92
+ return skillTool;
79
93
  }
80
94
  /**
81
95
  * Input schema for the skill-resource tool.
@@ -90,7 +104,9 @@ const SkillResourceSchema = z.object({
90
104
  .describe("Relative path to file or directory. Examples: 'snippets/tool.ts' (single file), 'templates' (all files in directory), '' (list available files)."),
91
105
  });
92
106
  // Security constants (exported for reuse in skill-resources.ts)
93
- export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB max file size
107
+ const DEFAULT_MAX_FILE_SIZE_MB = 1;
108
+ const maxFileSizeMB = parseInt(process.env.MAX_FILE_SIZE_MB || "", 10) || DEFAULT_MAX_FILE_SIZE_MB;
109
+ export const MAX_FILE_SIZE = maxFileSizeMB * 1024 * 1024; // Configurable via MAX_FILE_SIZE_MB env var
94
110
  export const MAX_DIRECTORY_DEPTH = 10; // Prevent deeply nested traversal
95
111
  /**
96
112
  * Check if a path is within the allowed base directory.
@@ -8,6 +8,7 @@
8
8
  * URI patterns supported:
9
9
  * - skill:// → Watch all skill directories
10
10
  * - skill://{name} → Watch that skill's SKILL.md
11
+ * - skill://{name}/ → Watch entire skill directory (directory collection)
11
12
  * - skill://{name}/{path} → Watch specific file
12
13
  */
13
14
  import { FSWatcher } from "chokidar";
@@ -8,6 +8,7 @@
8
8
  * URI patterns supported:
9
9
  * - skill:// → Watch all skill directories
10
10
  * - skill://{name} → Watch that skill's SKILL.md
11
+ * - skill://{name}/ → Watch entire skill directory (directory collection)
11
12
  * - skill://{name}/{path} → Watch specific file
12
13
  */
13
14
  import chokidar from "chokidar";
@@ -50,6 +51,13 @@ export function resolveUriToFilePaths(uri, skillState) {
50
51
  const skill = skillState.skillMap.get(skillName);
51
52
  return skill ? [skill.path] : [];
52
53
  }
54
+ // skill://{skillName}/ → Watch entire skill directory (directory collection)
55
+ const dirMatch = uri.match(/^skill:\/\/([^/]+)\/$/);
56
+ if (dirMatch) {
57
+ const skillName = decodeURIComponent(dirMatch[1]);
58
+ const skill = skillState.skillMap.get(skillName);
59
+ return skill ? [path.dirname(skill.path)] : [];
60
+ }
53
61
  // skill://{skillName}/{path} → Specific file
54
62
  const fileMatch = uri.match(/^skill:\/\/([^/]+)\/(.+)$/);
55
63
  if (fileMatch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olaservo/skill-jack-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",