@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 +34 -15
- package/dist/index.js +131 -8
- package/dist/skill-discovery.d.ts +1 -0
- package/dist/skill-discovery.js +9 -1
- package/dist/skill-resources.d.ts +1 -2
- package/dist/skill-resources.js +66 -19
- package/dist/skill-tool.d.ts +11 -3
- package/dist/skill-tool.js +21 -5
- package/dist/subscriptions.d.ts +1 -0
- package/dist/subscriptions.js +8 -0
- package/package.json +1 -1
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
|
|
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** -
|
|
10
|
-
- **
|
|
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**:
|
|
66
|
-
3. **On
|
|
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
|
-
│ •
|
|
74
|
+
│ • Starts watching for SKILL.md changes │
|
|
73
75
|
│ ↓ │
|
|
74
76
|
│ MCP Client connects │
|
|
75
|
-
│ •
|
|
77
|
+
│ • Skill tool description includes available skills │
|
|
76
78
|
│ ↓ │
|
|
77
|
-
│ LLM sees skill metadata in
|
|
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 (
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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,
|
|
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>;
|
package/dist/skill-discovery.js
CHANGED
|
@@ -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.
|
|
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";
|
package/dist/skill-resources.js
CHANGED
|
@@ -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
|
|
59
|
+
* Register a collection resource for skill directories.
|
|
61
60
|
*
|
|
62
|
-
* URI: skill://
|
|
61
|
+
* URI Pattern: skill://{skillName}/
|
|
63
62
|
*
|
|
64
|
-
* Returns
|
|
65
|
-
* This allows clients to fetch all
|
|
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
|
|
68
|
-
server.registerResource("
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
|
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(
|
|
78
|
-
mimeType:
|
|
124
|
+
uri: `skill://${encodeURIComponent(skillName)}/${file}`,
|
|
125
|
+
mimeType: getMimeType(file),
|
|
79
126
|
text: content,
|
|
80
127
|
});
|
|
81
128
|
}
|
|
82
129
|
catch (error) {
|
|
83
|
-
// Skip
|
|
84
|
-
console.error(`Failed to load skill "${
|
|
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 };
|
package/dist/skill-tool.d.ts
CHANGED
|
@@ -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
|
|
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
|
/**
|
package/dist/skill-tool.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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.
|
package/dist/subscriptions.d.ts
CHANGED
|
@@ -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";
|
package/dist/subscriptions.js
CHANGED
|
@@ -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) {
|