@skilljack/mcp 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/LICENSE +21 -0
- package/README.md +260 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +271 -0
- package/dist/roots-handler.d.ts +49 -0
- package/dist/roots-handler.js +199 -0
- package/dist/skill-discovery.d.ts +33 -0
- package/dist/skill-discovery.js +144 -0
- package/dist/skill-resources.d.ts +26 -0
- package/dist/skill-resources.js +286 -0
- package/dist/skill-tool.d.ts +46 -0
- package/dist/skill-tool.js +362 -0
- package/dist/subscriptions.d.ts +78 -0
- package/dist/subscriptions.js +285 -0
- package/package.json +54 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool registration for skill-related tools.
|
|
3
|
+
*
|
|
4
|
+
* - skill: Load and activate a skill by name (returns SKILL.md content)
|
|
5
|
+
* - skill-resource: Read files within a skill directory (scripts/, references/, assets/, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Tools reference a shared SkillState object to support dynamic skill updates
|
|
8
|
+
* when MCP roots change.
|
|
9
|
+
*/
|
|
10
|
+
import { McpServer, RegisteredTool } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
import { SkillMetadata } from "./skill-discovery.js";
|
|
12
|
+
/**
|
|
13
|
+
* Shared state for dynamic skill management.
|
|
14
|
+
* Tools reference this state object, allowing updates when roots change.
|
|
15
|
+
*/
|
|
16
|
+
export interface SkillState {
|
|
17
|
+
skillMap: Map<string, SkillMetadata>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Register the "skill" tool with the MCP server.
|
|
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
|
+
*
|
|
25
|
+
* @param server - The McpServer instance
|
|
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.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getToolDescription(skillState: SkillState): string;
|
|
34
|
+
export declare function registerSkillTool(server: McpServer, skillState: SkillState): RegisteredTool;
|
|
35
|
+
export declare const MAX_FILE_SIZE: number;
|
|
36
|
+
export declare const MAX_DIRECTORY_DEPTH = 10;
|
|
37
|
+
/**
|
|
38
|
+
* Check if a path is within the allowed base directory.
|
|
39
|
+
* Uses fs.realpathSync to resolve symlinks and prevent symlink escape attacks.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isPathWithinBase(targetPath: string, baseDir: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* List files in a skill directory for discovery.
|
|
44
|
+
* Limits recursion depth to prevent DoS from deeply nested directories.
|
|
45
|
+
*/
|
|
46
|
+
export declare function listSkillFiles(skillDir: string, subPath?: string, depth?: number): string[];
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool registration for skill-related tools.
|
|
3
|
+
*
|
|
4
|
+
* - skill: Load and activate a skill by name (returns SKILL.md content)
|
|
5
|
+
* - skill-resource: Read files within a skill directory (scripts/, references/, assets/, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Tools reference a shared SkillState object to support dynamic skill updates
|
|
8
|
+
* when MCP roots change.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { loadSkillContent, generateInstructions } from "./skill-discovery.js";
|
|
14
|
+
/**
|
|
15
|
+
* Input schema for the skill tool.
|
|
16
|
+
*/
|
|
17
|
+
const SkillSchema = z.object({
|
|
18
|
+
name: z.string().describe("Skill name from <available_skills>"),
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Register the "skill" tool with the MCP server.
|
|
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
|
+
*
|
|
26
|
+
* @param server - The McpServer instance
|
|
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.
|
|
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
|
+
}
|
|
40
|
+
export function registerSkillTool(server, skillState) {
|
|
41
|
+
const skillTool = server.registerTool("skill", {
|
|
42
|
+
title: "Activate Skill",
|
|
43
|
+
description: getToolDescription(skillState),
|
|
44
|
+
inputSchema: SkillSchema,
|
|
45
|
+
annotations: {
|
|
46
|
+
readOnlyHint: true,
|
|
47
|
+
destructiveHint: false,
|
|
48
|
+
idempotentHint: true,
|
|
49
|
+
openWorldHint: false,
|
|
50
|
+
},
|
|
51
|
+
}, async (args) => {
|
|
52
|
+
const { name } = SkillSchema.parse(args);
|
|
53
|
+
const skill = skillState.skillMap.get(name);
|
|
54
|
+
if (!skill) {
|
|
55
|
+
const availableSkills = Array.from(skillState.skillMap.keys()).join(", ");
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: `Skill "${name}" not found. Available skills: ${availableSkills || "none"}`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const content = loadSkillContent(skill.path);
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: content,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: `Failed to load skill "${name}": ${message}`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
isError: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Register the skill-resource tool
|
|
91
|
+
registerSkillResourceTool(server, skillState);
|
|
92
|
+
return skillTool;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Input schema for the skill-resource tool.
|
|
96
|
+
*
|
|
97
|
+
* Per the Agent Skills spec, file references use relative paths from the skill root.
|
|
98
|
+
* Common directories: scripts/, references/, assets/
|
|
99
|
+
*/
|
|
100
|
+
const SkillResourceSchema = z.object({
|
|
101
|
+
skill: z.string().describe("Skill name"),
|
|
102
|
+
path: z
|
|
103
|
+
.string()
|
|
104
|
+
.describe("Relative path to file or directory. Examples: 'snippets/tool.ts' (single file), 'templates' (all files in directory), '' (list available files)."),
|
|
105
|
+
});
|
|
106
|
+
// Security constants (exported for reuse in skill-resources.ts)
|
|
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
|
|
110
|
+
export const MAX_DIRECTORY_DEPTH = 10; // Prevent deeply nested traversal
|
|
111
|
+
/**
|
|
112
|
+
* Check if a path is within the allowed base directory.
|
|
113
|
+
* Uses fs.realpathSync to resolve symlinks and prevent symlink escape attacks.
|
|
114
|
+
*/
|
|
115
|
+
export function isPathWithinBase(targetPath, baseDir) {
|
|
116
|
+
try {
|
|
117
|
+
// Resolve symlinks to get the real paths
|
|
118
|
+
const realBase = fs.realpathSync(baseDir);
|
|
119
|
+
const realTarget = fs.realpathSync(targetPath);
|
|
120
|
+
const normalizedBase = realBase + path.sep;
|
|
121
|
+
return realTarget === realBase || realTarget.startsWith(normalizedBase);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// If realpathSync fails (e.g., file doesn't exist), fall back to resolve check
|
|
125
|
+
// This is safe because we'll get an error when trying to read anyway
|
|
126
|
+
const normalizedBase = path.resolve(baseDir) + path.sep;
|
|
127
|
+
const normalizedPath = path.resolve(targetPath);
|
|
128
|
+
return normalizedPath.startsWith(normalizedBase);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* List files in a skill directory for discovery.
|
|
133
|
+
* Limits recursion depth to prevent DoS from deeply nested directories.
|
|
134
|
+
*/
|
|
135
|
+
export function listSkillFiles(skillDir, subPath = "", depth = 0) {
|
|
136
|
+
// Prevent excessive recursion
|
|
137
|
+
if (depth > MAX_DIRECTORY_DEPTH) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const files = [];
|
|
141
|
+
const dirPath = path.join(skillDir, subPath);
|
|
142
|
+
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
|
143
|
+
return files;
|
|
144
|
+
}
|
|
145
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
const relativePath = path.join(subPath, entry.name);
|
|
148
|
+
// Skip symlinks to prevent escape and infinite loops
|
|
149
|
+
if (entry.isSymbolicLink()) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
// Skip node_modules and hidden directories
|
|
154
|
+
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
155
|
+
files.push(...listSkillFiles(skillDir, relativePath, depth + 1));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Skip SKILL.md (use skill tool for that) and common non-resource files
|
|
160
|
+
if (entry.name !== "SKILL.md" && entry.name !== "skill.md") {
|
|
161
|
+
files.push(relativePath.replace(/\\/g, "/"));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return files;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Register the "skill-resource" tool with the MCP server.
|
|
169
|
+
*
|
|
170
|
+
* This tool provides access to files within a skill's directory structure,
|
|
171
|
+
* following the Agent Skills spec for progressive disclosure of resources.
|
|
172
|
+
*
|
|
173
|
+
* @param server - The McpServer instance
|
|
174
|
+
* @param skillState - Shared state object (allows dynamic updates)
|
|
175
|
+
*/
|
|
176
|
+
function registerSkillResourceTool(server, skillState) {
|
|
177
|
+
server.registerTool("skill-resource", {
|
|
178
|
+
title: "Read Skill File",
|
|
179
|
+
description: "Read files referenced by skill instructions (scripts, snippets, templates). " +
|
|
180
|
+
"Use when skill instructions mention specific files to read or copy. " +
|
|
181
|
+
"Pass a directory path (e.g., 'templates') to read all files in that directory at once.",
|
|
182
|
+
inputSchema: SkillResourceSchema,
|
|
183
|
+
annotations: {
|
|
184
|
+
readOnlyHint: true,
|
|
185
|
+
destructiveHint: false,
|
|
186
|
+
idempotentHint: true,
|
|
187
|
+
openWorldHint: false,
|
|
188
|
+
},
|
|
189
|
+
}, async (args) => {
|
|
190
|
+
const { skill: skillName, path: resourcePath } = SkillResourceSchema.parse(args);
|
|
191
|
+
const skill = skillState.skillMap.get(skillName);
|
|
192
|
+
if (!skill) {
|
|
193
|
+
const availableSkills = Array.from(skillState.skillMap.keys()).join(", ");
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: `Skill "${skillName}" not found. Available skills: ${availableSkills || "none"}`,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
isError: true,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// Get the skill directory (parent of SKILL.md)
|
|
205
|
+
const skillDir = path.dirname(skill.path);
|
|
206
|
+
// If path is empty, list available files
|
|
207
|
+
if (!resourcePath || resourcePath.trim() === "") {
|
|
208
|
+
const files = listSkillFiles(skillDir);
|
|
209
|
+
if (files.length === 0) {
|
|
210
|
+
return {
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: "text",
|
|
214
|
+
text: `No resource files found in skill "${skillName}". The skill only contains SKILL.md.`,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
content: [
|
|
221
|
+
{
|
|
222
|
+
type: "text",
|
|
223
|
+
text: `Available resources in skill "${skillName}":\n\n${files.map((f) => `- ${f}`).join("\n")}`,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Resolve the full path and validate it's within the skill directory
|
|
229
|
+
const fullPath = path.resolve(skillDir, resourcePath);
|
|
230
|
+
if (!isPathWithinBase(fullPath, skillDir)) {
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{
|
|
234
|
+
type: "text",
|
|
235
|
+
text: `Invalid path: "${resourcePath}" is outside the skill directory. Use relative paths like "scripts/example.py" or "references/guide.md".`,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
isError: true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// Check if file exists
|
|
242
|
+
if (!fs.existsSync(fullPath)) {
|
|
243
|
+
const files = listSkillFiles(skillDir);
|
|
244
|
+
const suggestions = files.slice(0, 10).join("\n- ");
|
|
245
|
+
return {
|
|
246
|
+
content: [
|
|
247
|
+
{
|
|
248
|
+
type: "text",
|
|
249
|
+
text: `Resource "${resourcePath}" not found in skill "${skillName}".\n\nAvailable files:\n- ${suggestions}${files.length > 10 ? `\n... and ${files.length - 10} more` : ""}`,
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
isError: true,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// Check file stats
|
|
256
|
+
const stat = fs.statSync(fullPath);
|
|
257
|
+
// Reject symlinks that point outside (defense in depth)
|
|
258
|
+
if (stat.isSymbolicLink()) {
|
|
259
|
+
return {
|
|
260
|
+
content: [
|
|
261
|
+
{
|
|
262
|
+
type: "text",
|
|
263
|
+
text: `Cannot read symlink "${resourcePath}". Only regular files within the skill directory are accessible.`,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
isError: true,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// Handle directories - return all file contents
|
|
270
|
+
if (stat.isDirectory()) {
|
|
271
|
+
const files = listSkillFiles(skillDir, resourcePath);
|
|
272
|
+
if (files.length === 0) {
|
|
273
|
+
return {
|
|
274
|
+
content: [
|
|
275
|
+
{
|
|
276
|
+
type: "text",
|
|
277
|
+
text: `Directory "${resourcePath}" is empty or contains no readable files.`,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
// Read all files and return as multiple content items
|
|
283
|
+
const contents = [];
|
|
284
|
+
for (const file of files) {
|
|
285
|
+
const filePath = path.join(skillDir, file);
|
|
286
|
+
try {
|
|
287
|
+
const fileStat = fs.statSync(filePath);
|
|
288
|
+
if (fileStat.size > MAX_FILE_SIZE) {
|
|
289
|
+
contents.push({
|
|
290
|
+
type: "text",
|
|
291
|
+
text: `--- ${file} ---\n[File too large: ${(fileStat.size / 1024 / 1024).toFixed(2)}MB]`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
296
|
+
contents.push({
|
|
297
|
+
type: "text",
|
|
298
|
+
text: `--- ${file} ---\n${fileContent}`,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
contents.push({
|
|
304
|
+
type: "text",
|
|
305
|
+
text: `--- ${file} ---\n[Error reading file: ${error instanceof Error ? error.message : "unknown error"}]`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { content: contents };
|
|
310
|
+
}
|
|
311
|
+
// Check file size to prevent memory exhaustion
|
|
312
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
313
|
+
const sizeMB = (stat.size / 1024 / 1024).toFixed(2);
|
|
314
|
+
const maxMB = (MAX_FILE_SIZE / 1024 / 1024).toFixed(0);
|
|
315
|
+
return {
|
|
316
|
+
content: [
|
|
317
|
+
{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: `File "${resourcePath}" is too large (${sizeMB}MB). Maximum allowed size is ${maxMB}MB.`,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
isError: true,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// Final symlink check using realpath (defense in depth)
|
|
326
|
+
if (!isPathWithinBase(fullPath, skillDir)) {
|
|
327
|
+
return {
|
|
328
|
+
content: [
|
|
329
|
+
{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: `Access denied: "${resourcePath}" resolves to a location outside the skill directory.`,
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
isError: true,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// Read and return the file content
|
|
338
|
+
try {
|
|
339
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
340
|
+
return {
|
|
341
|
+
content: [
|
|
342
|
+
{
|
|
343
|
+
type: "text",
|
|
344
|
+
text: content,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
351
|
+
return {
|
|
352
|
+
content: [
|
|
353
|
+
{
|
|
354
|
+
type: "text",
|
|
355
|
+
text: `Failed to read resource "${resourcePath}": ${message}`,
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
isError: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource subscription management with file watching.
|
|
3
|
+
*
|
|
4
|
+
* Tracks client subscriptions to resource URIs and watches underlying files
|
|
5
|
+
* using chokidar. When files change, sends notifications/resources/updated
|
|
6
|
+
* to subscribed clients.
|
|
7
|
+
*
|
|
8
|
+
* URI patterns supported:
|
|
9
|
+
* - skill:// → Watch all skill directories
|
|
10
|
+
* - skill://{name} → Watch that skill's SKILL.md
|
|
11
|
+
* - skill://{name}/ → Watch entire skill directory (directory collection)
|
|
12
|
+
* - skill://{name}/{path} → Watch specific file
|
|
13
|
+
*/
|
|
14
|
+
import { FSWatcher } from "chokidar";
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { SkillState } from "./skill-tool.js";
|
|
17
|
+
/**
|
|
18
|
+
* Manages active subscriptions and their associated file watchers.
|
|
19
|
+
*/
|
|
20
|
+
export interface SubscriptionManager {
|
|
21
|
+
/** URI -> Set of file paths being watched for this URI */
|
|
22
|
+
uriToFilePaths: Map<string, Set<string>>;
|
|
23
|
+
/** File path -> Set of URIs that depend on this file */
|
|
24
|
+
filePathToUris: Map<string, Set<string>>;
|
|
25
|
+
/** File path -> chokidar watcher instance */
|
|
26
|
+
watchers: Map<string, FSWatcher>;
|
|
27
|
+
/** Pending notification timeouts for debouncing (URI -> timeout) */
|
|
28
|
+
pendingNotifications: Map<string, NodeJS.Timeout>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a new subscription manager.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createSubscriptionManager(): SubscriptionManager;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a skill:// URI to the file paths it depends on.
|
|
36
|
+
*
|
|
37
|
+
* @param uri - The resource URI
|
|
38
|
+
* @param skillState - Current skill state for lookups
|
|
39
|
+
* @returns Array of absolute file paths to watch
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveUriToFilePaths(uri: string, skillState: SkillState): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Add a subscription for a URI.
|
|
44
|
+
*
|
|
45
|
+
* @param manager - The subscription manager
|
|
46
|
+
* @param uri - The resource URI to subscribe to
|
|
47
|
+
* @param skillState - Current skill state for resolving URIs
|
|
48
|
+
* @param onNotify - Callback to send notification when file changes
|
|
49
|
+
* @returns True if subscription was added, false if URI couldn't be resolved
|
|
50
|
+
*/
|
|
51
|
+
export declare function subscribe(manager: SubscriptionManager, uri: string, skillState: SkillState, onNotify: (uri: string) => void): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Remove a subscription for a URI.
|
|
54
|
+
*
|
|
55
|
+
* @param manager - The subscription manager
|
|
56
|
+
* @param uri - The resource URI to unsubscribe from
|
|
57
|
+
*/
|
|
58
|
+
export declare function unsubscribe(manager: SubscriptionManager, uri: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Update subscriptions when skills change.
|
|
61
|
+
*
|
|
62
|
+
* Re-resolves all existing URIs with the new skill state and updates
|
|
63
|
+
* watchers accordingly. Sends notifications for any URIs whose underlying
|
|
64
|
+
* files have changed.
|
|
65
|
+
*
|
|
66
|
+
* @param manager - The subscription manager
|
|
67
|
+
* @param skillState - Updated skill state
|
|
68
|
+
* @param onNotify - Callback to send notification
|
|
69
|
+
*/
|
|
70
|
+
export declare function refreshSubscriptions(manager: SubscriptionManager, skillState: SkillState, onNotify: (uri: string) => void): void;
|
|
71
|
+
/**
|
|
72
|
+
* Register subscribe/unsubscribe request handlers with the server.
|
|
73
|
+
*
|
|
74
|
+
* @param server - The MCP server instance
|
|
75
|
+
* @param skillState - Shared skill state
|
|
76
|
+
* @param manager - The subscription manager
|
|
77
|
+
*/
|
|
78
|
+
export declare function registerSubscriptionHandlers(server: McpServer, skillState: SkillState, manager: SubscriptionManager): void;
|