@skilljack/mcp 0.3.0 → 0.4.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 +86 -8
- package/dist/index.js +12 -5
- package/dist/skill-prompts.d.ts +47 -0
- package/dist/skill-prompts.js +236 -0
- package/dist/skill-resources.d.ts +6 -3
- package/dist/skill-resources.js +10 -94
- package/dist/subscriptions.d.ts +1 -1
- package/dist/subscriptions.js +1 -1
- package/package.json +1 -1
- package/dist/roots-handler.d.ts +0 -49
- package/dist/roots-handler.js +0 -199
package/README.md
CHANGED
|
@@ -9,9 +9,22 @@ An MCP server that jacks [Agent Skills](https://agentskills.io) directly into yo
|
|
|
9
9
|
- **Dynamic Skill Discovery** - Watches skill directories and automatically refreshes when skills change
|
|
10
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
|
+
- **MCP Prompts** - Load skills via `/skill` prompt with auto-completion or per-skill prompts
|
|
12
13
|
- **MCP Resources** - Access skills via `skill://` URIs with batch collection support
|
|
13
14
|
- **Resource Subscriptions** - Real-time file watching with `notifications/resources/updated`
|
|
14
15
|
|
|
16
|
+
## Motivation
|
|
17
|
+
|
|
18
|
+
This repo demonstrates a way to approach integrating skills using existing MCP primitives.
|
|
19
|
+
|
|
20
|
+
MCP already has the building blocks:
|
|
21
|
+
- **Tools** for on-demand skill loading (the `skill` tool with dynamically updated descriptions)
|
|
22
|
+
- **Resources** for explicit skill access (`skill://` URIs)
|
|
23
|
+
- **Notifications** for real-time updates (`tools/listChanged`, `resources/updated`)
|
|
24
|
+
- **Prompts** for explicitly invoking skills by name (`/my-server-skill`)
|
|
25
|
+
|
|
26
|
+
This approach provides separation of concerns. Rather than every MCP server needing to embed skill handling, the server acts as a dedicated 'skill gateway'. Server authors can bundle skills alongside their MCP servers without modifying the servers themselves. If MCP registries support robust tool discovery, skill tools become discoverable like any other tool.
|
|
27
|
+
|
|
15
28
|
## Installation
|
|
16
29
|
|
|
17
30
|
```bash
|
|
@@ -75,16 +88,20 @@ The server implements the [Agent Skills](https://agentskills.io) progressive dis
|
|
|
75
88
|
│ ↓ │
|
|
76
89
|
│ MCP Client connects │
|
|
77
90
|
│ • Skill tool description includes available skills │
|
|
91
|
+
│ • Prompts registered for each skill │
|
|
78
92
|
│ ↓ │
|
|
79
93
|
│ LLM sees skill metadata in tool description │
|
|
80
94
|
│ ↓ │
|
|
81
95
|
│ SKILL.md added/modified/removed │
|
|
82
96
|
│ • Server re-discovers skills │
|
|
83
97
|
│ • Updates skill tool description │
|
|
98
|
+
│ • Updates prompt list (add/remove/modify) │
|
|
84
99
|
│ • Sends tools/listChanged notification │
|
|
85
|
-
│ •
|
|
100
|
+
│ • Sends prompts/listChanged notification │
|
|
101
|
+
│ • Client refreshes tool and prompt definitions │
|
|
86
102
|
│ ↓ │
|
|
87
|
-
│
|
|
103
|
+
│ User invokes /skill prompt or /skill-name prompt │
|
|
104
|
+
│ OR LLM calls "skill" tool with skill name │
|
|
88
105
|
│ ↓ │
|
|
89
106
|
│ Server returns full SKILL.md content │
|
|
90
107
|
│ ↓ │
|
|
@@ -93,14 +110,41 @@ The server implements the [Agent Skills](https://agentskills.io) progressive dis
|
|
|
93
110
|
└─────────────────────────────────────────────────────────┘
|
|
94
111
|
```
|
|
95
112
|
|
|
96
|
-
## Tools vs Resources
|
|
113
|
+
## Tools vs Resources vs Prompts
|
|
97
114
|
|
|
98
|
-
This server exposes skills via
|
|
115
|
+
This server exposes skills via **tools**, **resources**, and **prompts**:
|
|
99
116
|
|
|
100
117
|
- **Tools** (`skill`, `skill-resource`) - For your agent to use autonomously. The LLM sees available skills in the tool description and calls them as needed.
|
|
118
|
+
- **Prompts** (`/skill`, `/skill-name`) - For explicit user invocation. Use `/skill` with auto-completion or select a skill directly by name.
|
|
101
119
|
- **Resources** (`skill://` URIs) - For manual selection in apps that support it (e.g., Claude Desktop's resource picker). Useful when you want to explicitly attach a skill to the conversation.
|
|
102
120
|
|
|
103
|
-
Most users will rely on tools for automatic skill activation. Resources provide an alternative for manual control.
|
|
121
|
+
Most users will rely on tools for automatic skill activation. Prompts provide user-initiated loading with auto-completion. Resources provide an alternative for manual control.
|
|
122
|
+
|
|
123
|
+
## Progressive Disclosure Design
|
|
124
|
+
|
|
125
|
+
This server implements the [Agent Skills progressive disclosure pattern](https://agentskills.io/specification#progressive-disclosure), which structures skills for efficient context usage:
|
|
126
|
+
|
|
127
|
+
| Level | Tokens | What's loaded | When |
|
|
128
|
+
|-------|--------|---------------|------|
|
|
129
|
+
| **Metadata** | ~100 | `name` and `description` | At startup, for all skills |
|
|
130
|
+
| **Instructions** | < 5000 | Full SKILL.md body | When skill is activated |
|
|
131
|
+
| **Resources** | As needed | Files in `scripts/`, `references/`, `assets/` | On demand via `skill-resource` |
|
|
132
|
+
|
|
133
|
+
### How it works
|
|
134
|
+
|
|
135
|
+
1. **Discovery** - Server loads metadata from all skills into the `skill` tool description
|
|
136
|
+
2. **Activation** - When a skill is loaded (via tool, prompt, or resource), only the SKILL.md content is returned
|
|
137
|
+
3. **Execution** - SKILL.md references additional files; agent fetches them with `skill-resource` as needed
|
|
138
|
+
|
|
139
|
+
### Why SKILL.md documents its own resources
|
|
140
|
+
|
|
141
|
+
The server doesn't automatically list all files in a skill directory. Instead, skill authors document available resources directly in their SKILL.md (e.g., "Copy the template from `templates/server.ts`"). This design choice follows the spec because:
|
|
142
|
+
|
|
143
|
+
- **Skill authors know best** - They decide which files are relevant and when to use them
|
|
144
|
+
- **Context efficiency** - Loading everything upfront wastes tokens on files the agent may not need
|
|
145
|
+
- **Natural flow** - SKILL.md guides the agent through resources in a logical order
|
|
146
|
+
|
|
147
|
+
**For skill authors:** Reference files using relative paths from the skill root (e.g., `snippets/tool.ts`, `references/api.md`). Keep your main SKILL.md under 500 lines; move detailed reference material to separate files. See the [Agent Skills specification](https://agentskills.io/specification) for complete authoring guidelines.
|
|
104
148
|
|
|
105
149
|
## Tools
|
|
106
150
|
|
|
@@ -150,6 +194,38 @@ Returns all files in the directory as multiple content items.
|
|
|
150
194
|
|
|
151
195
|
**Security:** Path traversal is prevented - only files within the skill directory can be accessed.
|
|
152
196
|
|
|
197
|
+
## Prompts
|
|
198
|
+
|
|
199
|
+
Skills can be loaded via MCP [Prompts](https://modelcontextprotocol.io/specification/2025-11-05/server/prompts) for explicit user invocation.
|
|
200
|
+
|
|
201
|
+
### `/skill` Prompt
|
|
202
|
+
|
|
203
|
+
Load a skill by name with auto-completion support.
|
|
204
|
+
|
|
205
|
+
**Arguments:**
|
|
206
|
+
- `name` (string, required) - Skill name with auto-completion
|
|
207
|
+
|
|
208
|
+
The prompt description includes all available skills for discoverability. As you type the skill name, matching skills are suggested.
|
|
209
|
+
|
|
210
|
+
### Per-Skill Prompts
|
|
211
|
+
|
|
212
|
+
Each discovered skill is also registered as its own prompt (e.g., `/mcp-server-ts`, `/algorithmic-art`).
|
|
213
|
+
|
|
214
|
+
- No arguments needed - just select and invoke
|
|
215
|
+
- Description shows the skill's own description
|
|
216
|
+
- List updates dynamically as skills change
|
|
217
|
+
|
|
218
|
+
**Example:** If you have a skill named `mcp-server-ts`, you can invoke it directly as `/mcp-server-ts`.
|
|
219
|
+
|
|
220
|
+
### Content Annotations
|
|
221
|
+
|
|
222
|
+
Prompt responses include MCP [content annotations](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts#embedded-resources) for proper handling:
|
|
223
|
+
|
|
224
|
+
- `audience: ["assistant"]` - Content is intended for the LLM, not the user
|
|
225
|
+
- `priority: 1.0` - High priority content that should be included in context
|
|
226
|
+
|
|
227
|
+
Prompts return embedded resources with the skill's `skill://` URI, allowing clients to track the content source.
|
|
228
|
+
|
|
153
229
|
## Resources
|
|
154
230
|
|
|
155
231
|
Skills are also accessible via MCP [Resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resources) using `skill://` URIs.
|
|
@@ -160,7 +236,8 @@ Skills are also accessible via MCP [Resources](https://modelcontextprotocol.io/s
|
|
|
160
236
|
|-----|---------|
|
|
161
237
|
| `skill://{name}` | Single skill's SKILL.md content |
|
|
162
238
|
| `skill://{name}/` | All files in skill directory (collection) |
|
|
163
|
-
|
|
239
|
+
|
|
240
|
+
Individual file URIs (`skill://{name}/{path}`) are not listed as resources to reduce noise. Use the `skill-resource` tool to fetch specific files on demand.
|
|
164
241
|
|
|
165
242
|
### Resource Subscriptions
|
|
166
243
|
|
|
@@ -211,8 +288,9 @@ The server watches skill directories for changes. When SKILL.md files are added,
|
|
|
211
288
|
|
|
212
289
|
1. Skills are re-discovered from all configured directories
|
|
213
290
|
2. The `skill` tool's description is updated with current skill names and metadata
|
|
214
|
-
3.
|
|
215
|
-
4.
|
|
291
|
+
3. Per-skill prompts are added, removed, or updated accordingly
|
|
292
|
+
4. `tools/listChanged` and `prompts/listChanged` notifications are sent to connected clients
|
|
293
|
+
5. Clients that support these notifications will refresh tool and prompt definitions
|
|
216
294
|
|
|
217
295
|
## Skill Metadata Format
|
|
218
296
|
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ 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";
|
|
21
22
|
import { createSubscriptionManager, registerSubscriptionHandlers, refreshSubscriptions, } from "./subscriptions.js";
|
|
22
23
|
/**
|
|
23
24
|
* Subdirectories to check for skills within the configured directory.
|
|
@@ -114,9 +115,10 @@ const SKILL_REFRESH_DEBOUNCE_MS = 500;
|
|
|
114
115
|
* @param skillsDirs - The configured skill directories
|
|
115
116
|
* @param server - The MCP server instance
|
|
116
117
|
* @param skillTool - The registered skill tool to update
|
|
118
|
+
* @param promptRegistry - For refreshing skill prompts
|
|
117
119
|
* @param subscriptionManager - For refreshing resource subscriptions
|
|
118
120
|
*/
|
|
119
|
-
function refreshSkills(skillsDirs, server, skillTool, subscriptionManager) {
|
|
121
|
+
function refreshSkills(skillsDirs, server, skillTool, promptRegistry, subscriptionManager) {
|
|
120
122
|
console.error("Refreshing skills...");
|
|
121
123
|
// Re-discover all skills
|
|
122
124
|
const skills = discoverSkillsFromDirs(skillsDirs);
|
|
@@ -128,6 +130,8 @@ function refreshSkills(skillsDirs, server, skillTool, subscriptionManager) {
|
|
|
128
130
|
skillTool.update({
|
|
129
131
|
description: getToolDescription(skillState),
|
|
130
132
|
});
|
|
133
|
+
// Refresh prompts to match new skill state
|
|
134
|
+
refreshPrompts(server, skillState, promptRegistry);
|
|
131
135
|
// Refresh resource subscriptions to match new skill state
|
|
132
136
|
refreshSubscriptions(subscriptionManager, skillState, (uri) => {
|
|
133
137
|
server.server.notification({
|
|
@@ -148,9 +152,10 @@ function refreshSkills(skillsDirs, server, skillTool, subscriptionManager) {
|
|
|
148
152
|
* @param skillsDirs - The configured skill directories
|
|
149
153
|
* @param server - The MCP server instance
|
|
150
154
|
* @param skillTool - The registered skill tool to update
|
|
155
|
+
* @param promptRegistry - For refreshing skill prompts
|
|
151
156
|
* @param subscriptionManager - For refreshing subscriptions
|
|
152
157
|
*/
|
|
153
|
-
function watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager) {
|
|
158
|
+
function watchSkillDirectories(skillsDirs, server, skillTool, promptRegistry, subscriptionManager) {
|
|
154
159
|
let refreshTimeout = null;
|
|
155
160
|
const debouncedRefresh = () => {
|
|
156
161
|
if (refreshTimeout) {
|
|
@@ -158,7 +163,7 @@ function watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManage
|
|
|
158
163
|
}
|
|
159
164
|
refreshTimeout = setTimeout(() => {
|
|
160
165
|
refreshTimeout = null;
|
|
161
|
-
refreshSkills(skillsDirs, server, skillTool, subscriptionManager);
|
|
166
|
+
refreshSkills(skillsDirs, server, skillTool, promptRegistry, subscriptionManager);
|
|
162
167
|
}, SKILL_REFRESH_DEBOUNCE_MS);
|
|
163
168
|
};
|
|
164
169
|
// Build list of paths to watch
|
|
@@ -251,15 +256,17 @@ async function main() {
|
|
|
251
256
|
capabilities: {
|
|
252
257
|
tools: { listChanged: true },
|
|
253
258
|
resources: { subscribe: true, listChanged: true },
|
|
259
|
+
prompts: { listChanged: true },
|
|
254
260
|
},
|
|
255
261
|
});
|
|
256
|
-
// Register tools and
|
|
262
|
+
// Register tools, resources, and prompts
|
|
257
263
|
const skillTool = registerSkillTool(server, skillState);
|
|
258
264
|
registerSkillResources(server, skillState);
|
|
265
|
+
const promptRegistry = registerSkillPrompts(server, skillState);
|
|
259
266
|
// Register subscription handlers for resource file watching
|
|
260
267
|
registerSubscriptionHandlers(server, skillState, subscriptionManager);
|
|
261
268
|
// Set up file watchers for skill directory changes
|
|
262
|
-
watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager);
|
|
269
|
+
watchSkillDirectories(skillsDirs, server, skillTool, promptRegistry, subscriptionManager);
|
|
263
270
|
// Connect via stdio transport
|
|
264
271
|
const transport = new StdioServerTransport();
|
|
265
272
|
await server.connect(transport);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP prompt registration for skill loading.
|
|
3
|
+
*
|
|
4
|
+
* Provides two patterns for loading skills:
|
|
5
|
+
* 1. /skill prompt - Single prompt with name argument + auto-completion
|
|
6
|
+
* 2. Per-skill prompts - Dynamic prompts for each skill (e.g., /mcp-server-ts)
|
|
7
|
+
*/
|
|
8
|
+
import { McpServer, RegisteredPrompt } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { SkillState } from "./skill-tool.js";
|
|
10
|
+
/**
|
|
11
|
+
* Track all registered prompts for dynamic updates.
|
|
12
|
+
*/
|
|
13
|
+
export interface PromptRegistry {
|
|
14
|
+
skillPrompt: RegisteredPrompt;
|
|
15
|
+
perSkillPrompts: Map<string, RegisteredPrompt>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate the description for the /skill prompt.
|
|
19
|
+
* Includes available skills list for discoverability.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getPromptDescription(skillState: SkillState): string;
|
|
22
|
+
/**
|
|
23
|
+
* Register skill prompts with the MCP server.
|
|
24
|
+
*
|
|
25
|
+
* Creates:
|
|
26
|
+
* 1. /skill prompt with name argument + auto-completion
|
|
27
|
+
* 2. Per-skill prompts for each discovered skill
|
|
28
|
+
*
|
|
29
|
+
* @param server - The McpServer instance
|
|
30
|
+
* @param skillState - Shared state object (allows dynamic updates)
|
|
31
|
+
* @returns Registry for tracking and updating prompts
|
|
32
|
+
*/
|
|
33
|
+
export declare function registerSkillPrompts(server: McpServer, skillState: SkillState): PromptRegistry;
|
|
34
|
+
/**
|
|
35
|
+
* Refresh prompts when skills change.
|
|
36
|
+
*
|
|
37
|
+
* Updates:
|
|
38
|
+
* - /skill prompt description with new skill list
|
|
39
|
+
* - Disables prompts for removed skills
|
|
40
|
+
* - Adds prompts for new skills
|
|
41
|
+
* - Updates descriptions for modified skills
|
|
42
|
+
*
|
|
43
|
+
* @param server - The McpServer instance
|
|
44
|
+
* @param skillState - Updated skill state
|
|
45
|
+
* @param registry - Prompt registry to update
|
|
46
|
+
*/
|
|
47
|
+
export declare function refreshPrompts(server: McpServer, skillState: SkillState, registry: PromptRegistry): void;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP prompt registration for skill loading.
|
|
3
|
+
*
|
|
4
|
+
* Provides two patterns for loading skills:
|
|
5
|
+
* 1. /skill prompt - Single prompt with name argument + auto-completion
|
|
6
|
+
* 2. Per-skill prompts - Dynamic prompts for each skill (e.g., /mcp-server-ts)
|
|
7
|
+
*/
|
|
8
|
+
import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { loadSkillContent, generateInstructions } from "./skill-discovery.js";
|
|
11
|
+
/**
|
|
12
|
+
* Auto-completion for /skill prompt name argument.
|
|
13
|
+
* Returns skill names that start with the given value (case-insensitive).
|
|
14
|
+
*/
|
|
15
|
+
function getSkillNameCompletions(value, skillState) {
|
|
16
|
+
const names = Array.from(skillState.skillMap.keys());
|
|
17
|
+
return names.filter((name) => name.toLowerCase().startsWith(value.toLowerCase()));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate the description for the /skill prompt.
|
|
21
|
+
* Includes available skills list for discoverability.
|
|
22
|
+
*/
|
|
23
|
+
export function getPromptDescription(skillState) {
|
|
24
|
+
const skills = Array.from(skillState.skillMap.values());
|
|
25
|
+
const usage = "Load a skill by name with auto-completion.\n\n";
|
|
26
|
+
return usage + generateInstructions(skills);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register skill prompts with the MCP server.
|
|
30
|
+
*
|
|
31
|
+
* Creates:
|
|
32
|
+
* 1. /skill prompt with name argument + auto-completion
|
|
33
|
+
* 2. Per-skill prompts for each discovered skill
|
|
34
|
+
*
|
|
35
|
+
* @param server - The McpServer instance
|
|
36
|
+
* @param skillState - Shared state object (allows dynamic updates)
|
|
37
|
+
* @returns Registry for tracking and updating prompts
|
|
38
|
+
*/
|
|
39
|
+
export function registerSkillPrompts(server, skillState) {
|
|
40
|
+
// 1. Register /skill prompt with argument + auto-completion
|
|
41
|
+
const skillPrompt = server.registerPrompt("skill", {
|
|
42
|
+
title: "Load Skill",
|
|
43
|
+
description: getPromptDescription(skillState),
|
|
44
|
+
argsSchema: {
|
|
45
|
+
name: completable(z.string().describe("Skill name"), (value) => getSkillNameCompletions(value, skillState)),
|
|
46
|
+
},
|
|
47
|
+
}, async ({ name }) => {
|
|
48
|
+
const skill = skillState.skillMap.get(name);
|
|
49
|
+
if (!skill) {
|
|
50
|
+
const availableSkills = Array.from(skillState.skillMap.keys()).join(", ");
|
|
51
|
+
return {
|
|
52
|
+
messages: [
|
|
53
|
+
{
|
|
54
|
+
role: "user",
|
|
55
|
+
content: {
|
|
56
|
+
type: "text",
|
|
57
|
+
text: `Skill "${name}" not found. Available skills: ${availableSkills || "none"}`,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const content = loadSkillContent(skill.path);
|
|
65
|
+
return {
|
|
66
|
+
messages: [
|
|
67
|
+
{
|
|
68
|
+
role: "user",
|
|
69
|
+
content: {
|
|
70
|
+
type: "resource",
|
|
71
|
+
resource: {
|
|
72
|
+
uri: `skill://${name}`,
|
|
73
|
+
mimeType: "text/markdown",
|
|
74
|
+
text: content,
|
|
75
|
+
},
|
|
76
|
+
annotations: {
|
|
77
|
+
audience: ["assistant"],
|
|
78
|
+
priority: 1.0,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
return {
|
|
88
|
+
messages: [
|
|
89
|
+
{
|
|
90
|
+
role: "user",
|
|
91
|
+
content: {
|
|
92
|
+
type: "text",
|
|
93
|
+
text: `Failed to load skill "${name}": ${message}`,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// 2. Register per-skill prompts (no arguments needed)
|
|
101
|
+
// Returns embedded resource with skill:// URI (MCP-idiomatic)
|
|
102
|
+
const perSkillPrompts = new Map();
|
|
103
|
+
for (const [name, skill] of skillState.skillMap) {
|
|
104
|
+
// Capture skill info in closure for this specific prompt
|
|
105
|
+
const skillPath = skill.path;
|
|
106
|
+
const skillName = name;
|
|
107
|
+
const prompt = server.registerPrompt(name, {
|
|
108
|
+
title: skill.name,
|
|
109
|
+
description: skill.description,
|
|
110
|
+
// No argsSchema - direct invocation
|
|
111
|
+
}, async () => {
|
|
112
|
+
try {
|
|
113
|
+
const content = loadSkillContent(skillPath);
|
|
114
|
+
return {
|
|
115
|
+
messages: [
|
|
116
|
+
{
|
|
117
|
+
role: "user",
|
|
118
|
+
content: {
|
|
119
|
+
type: "resource",
|
|
120
|
+
resource: {
|
|
121
|
+
uri: `skill://${skillName}`,
|
|
122
|
+
mimeType: "text/markdown",
|
|
123
|
+
text: content,
|
|
124
|
+
},
|
|
125
|
+
annotations: {
|
|
126
|
+
audience: ["assistant"],
|
|
127
|
+
priority: 1.0,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
136
|
+
return {
|
|
137
|
+
messages: [
|
|
138
|
+
{
|
|
139
|
+
role: "user",
|
|
140
|
+
content: {
|
|
141
|
+
type: "text",
|
|
142
|
+
text: `Failed to load skill "${skillName}": ${message}`,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
perSkillPrompts.set(name, prompt);
|
|
150
|
+
}
|
|
151
|
+
return { skillPrompt, perSkillPrompts };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Refresh prompts when skills change.
|
|
155
|
+
*
|
|
156
|
+
* Updates:
|
|
157
|
+
* - /skill prompt description with new skill list
|
|
158
|
+
* - Disables prompts for removed skills
|
|
159
|
+
* - Adds prompts for new skills
|
|
160
|
+
* - Updates descriptions for modified skills
|
|
161
|
+
*
|
|
162
|
+
* @param server - The McpServer instance
|
|
163
|
+
* @param skillState - Updated skill state
|
|
164
|
+
* @param registry - Prompt registry to update
|
|
165
|
+
*/
|
|
166
|
+
export function refreshPrompts(server, skillState, registry) {
|
|
167
|
+
// Update /skill prompt description with new skill list
|
|
168
|
+
registry.skillPrompt.update({
|
|
169
|
+
description: getPromptDescription(skillState),
|
|
170
|
+
});
|
|
171
|
+
// Disable removed skill prompts
|
|
172
|
+
for (const [name, prompt] of registry.perSkillPrompts) {
|
|
173
|
+
if (!skillState.skillMap.has(name)) {
|
|
174
|
+
prompt.update({ enabled: false });
|
|
175
|
+
registry.perSkillPrompts.delete(name);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Add/update per-skill prompts
|
|
179
|
+
for (const [name, skill] of skillState.skillMap) {
|
|
180
|
+
if (registry.perSkillPrompts.has(name)) {
|
|
181
|
+
// Update existing prompt description
|
|
182
|
+
registry.perSkillPrompts.get(name).update({
|
|
183
|
+
description: skill.description,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Register new skill prompt with embedded resource
|
|
188
|
+
const skillPath = skill.path;
|
|
189
|
+
const skillName = name;
|
|
190
|
+
const prompt = server.registerPrompt(name, {
|
|
191
|
+
title: skill.name,
|
|
192
|
+
description: skill.description,
|
|
193
|
+
}, async () => {
|
|
194
|
+
try {
|
|
195
|
+
const content = loadSkillContent(skillPath);
|
|
196
|
+
return {
|
|
197
|
+
messages: [
|
|
198
|
+
{
|
|
199
|
+
role: "user",
|
|
200
|
+
content: {
|
|
201
|
+
type: "resource",
|
|
202
|
+
resource: {
|
|
203
|
+
uri: `skill://${skillName}`,
|
|
204
|
+
mimeType: "text/markdown",
|
|
205
|
+
text: content,
|
|
206
|
+
},
|
|
207
|
+
annotations: {
|
|
208
|
+
audience: ["assistant"],
|
|
209
|
+
priority: 1.0,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
return {
|
|
219
|
+
messages: [
|
|
220
|
+
{
|
|
221
|
+
role: "user",
|
|
222
|
+
content: {
|
|
223
|
+
type: "text",
|
|
224
|
+
text: `Failed to load skill "${skillName}": ${message}`,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
registry.perSkillPrompts.set(name, prompt);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Notify clients that prompts have changed
|
|
235
|
+
server.sendPromptListChanged();
|
|
236
|
+
}
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* skill updates when MCP roots change.
|
|
9
9
|
*
|
|
10
10
|
* URI Scheme:
|
|
11
|
-
* skill://{skillName}
|
|
12
|
-
* skill://{skillName}/
|
|
13
|
-
*
|
|
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.
|
|
14
17
|
*/
|
|
15
18
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
19
|
import { SkillState } from "./skill-tool.js";
|
package/dist/skill-resources.js
CHANGED
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* skill updates when MCP roots change.
|
|
9
9
|
*
|
|
10
10
|
* URI Scheme:
|
|
11
|
-
* skill://{skillName}
|
|
12
|
-
* skill://{skillName}/
|
|
13
|
-
*
|
|
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.
|
|
14
17
|
*/
|
|
15
18
|
import * as fs from "node:fs";
|
|
16
19
|
import * as path from "node:path";
|
|
@@ -50,10 +53,11 @@ function getMimeType(filePath) {
|
|
|
50
53
|
export function registerSkillResources(server, skillState) {
|
|
51
54
|
// Register template for individual skill SKILL.md files
|
|
52
55
|
registerSkillTemplate(server, skillState);
|
|
53
|
-
// Register collection resource for skill directories
|
|
56
|
+
// Register collection resource for skill directories
|
|
54
57
|
registerSkillDirectoryCollection(server, skillState);
|
|
55
|
-
//
|
|
56
|
-
|
|
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.
|
|
57
61
|
}
|
|
58
62
|
/**
|
|
59
63
|
* Register a collection resource for skill directories.
|
|
@@ -196,91 +200,3 @@ function registerSkillTemplate(server, skillState) {
|
|
|
196
200
|
}
|
|
197
201
|
});
|
|
198
202
|
}
|
|
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/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 (subscribable but not listed as resource)
|
|
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 (subscribable but not listed as resource)
|
|
13
13
|
*/
|
|
14
14
|
import chokidar from "chokidar";
|
|
15
15
|
import * as path from "node:path";
|
package/package.json
CHANGED
package/dist/roots-handler.d.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Roots handler for dynamic skill discovery.
|
|
3
|
-
*
|
|
4
|
-
* Requests roots from the client, scans for skills in each root,
|
|
5
|
-
* and handles root change notifications.
|
|
6
|
-
*
|
|
7
|
-
* Pattern adapted from:
|
|
8
|
-
* - .claude/skills/mcp-server-ts/snippets/server/index.ts (oninitialized, syncRoots)
|
|
9
|
-
* - .claude/skills/mcp-client-ts/snippets/handlers/roots.ts (URI conversion)
|
|
10
|
-
*/
|
|
11
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
-
import { SkillMetadata } from "./skill-discovery.js";
|
|
13
|
-
/**
|
|
14
|
-
* Skill discovery locations within each root.
|
|
15
|
-
*/
|
|
16
|
-
export declare const SKILL_SUBDIRS: string[];
|
|
17
|
-
/**
|
|
18
|
-
* Discover skills from all roots provided by the client.
|
|
19
|
-
*
|
|
20
|
-
* Scans each root for skill directories (.claude/skills/, skills/)
|
|
21
|
-
* and handles naming conflicts by prefixing with root name.
|
|
22
|
-
*
|
|
23
|
-
* @param roots - Array of Root objects from client's roots/list response
|
|
24
|
-
* @returns Object containing discovered skills
|
|
25
|
-
*/
|
|
26
|
-
export declare function discoverSkillsFromRoots(roots: Array<{
|
|
27
|
-
uri: string;
|
|
28
|
-
name?: string;
|
|
29
|
-
}>): {
|
|
30
|
-
skills: SkillMetadata[];
|
|
31
|
-
rootSources: Map<string, string>;
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Callback type for when skills are updated.
|
|
35
|
-
*/
|
|
36
|
-
export type SkillsChangedCallback = (skillMap: Map<string, SkillMetadata>, instructions: string) => void;
|
|
37
|
-
/**
|
|
38
|
-
* Sync skills from roots or configured skills directory.
|
|
39
|
-
*
|
|
40
|
-
* Pattern from mcp-server-ts snippets/server/index.ts:
|
|
41
|
-
* - Check client capabilities
|
|
42
|
-
* - Request roots if supported
|
|
43
|
-
* - Use skills directory if not
|
|
44
|
-
*
|
|
45
|
-
* @param server - The McpServer instance
|
|
46
|
-
* @param skillsDir - Optional skills directory if client doesn't support roots
|
|
47
|
-
* @param onSkillsChanged - Callback when skills are updated
|
|
48
|
-
*/
|
|
49
|
-
export declare function syncSkills(server: McpServer, skillsDir: string | null, onSkillsChanged: SkillsChangedCallback): Promise<void>;
|
package/dist/roots-handler.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Roots handler for dynamic skill discovery.
|
|
3
|
-
*
|
|
4
|
-
* Requests roots from the client, scans for skills in each root,
|
|
5
|
-
* and handles root change notifications.
|
|
6
|
-
*
|
|
7
|
-
* Pattern adapted from:
|
|
8
|
-
* - .claude/skills/mcp-server-ts/snippets/server/index.ts (oninitialized, syncRoots)
|
|
9
|
-
* - .claude/skills/mcp-client-ts/snippets/handlers/roots.ts (URI conversion)
|
|
10
|
-
*/
|
|
11
|
-
import { RootsListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
-
import * as fs from "node:fs";
|
|
13
|
-
import * as path from "node:path";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { discoverSkills, generateInstructions, createSkillMap, } from "./skill-discovery.js";
|
|
16
|
-
/**
|
|
17
|
-
* Skill discovery locations within each root.
|
|
18
|
-
*/
|
|
19
|
-
export const SKILL_SUBDIRS = [".claude/skills", "skills"];
|
|
20
|
-
/**
|
|
21
|
-
* Convert a file:// URI to a filesystem path.
|
|
22
|
-
* Adapted from mcp-client-ts roots.ts pathToRoot() (reverse direction).
|
|
23
|
-
*/
|
|
24
|
-
function uriToPath(uri) {
|
|
25
|
-
return fileURLToPath(new URL(uri));
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Discover skills from all roots provided by the client.
|
|
29
|
-
*
|
|
30
|
-
* Scans each root for skill directories (.claude/skills/, skills/)
|
|
31
|
-
* and handles naming conflicts by prefixing with root name.
|
|
32
|
-
*
|
|
33
|
-
* @param roots - Array of Root objects from client's roots/list response
|
|
34
|
-
* @returns Object containing discovered skills
|
|
35
|
-
*/
|
|
36
|
-
export function discoverSkillsFromRoots(roots) {
|
|
37
|
-
const allSkills = [];
|
|
38
|
-
const rootSources = new Map(); // skill path -> root name
|
|
39
|
-
const nameCount = new Map(); // track duplicates
|
|
40
|
-
for (const root of roots) {
|
|
41
|
-
let rootPath;
|
|
42
|
-
try {
|
|
43
|
-
rootPath = uriToPath(root.uri);
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
console.error(`Failed to parse root URI "${root.uri}":`, error);
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
const rootName = root.name || path.basename(rootPath);
|
|
50
|
-
for (const subdir of SKILL_SUBDIRS) {
|
|
51
|
-
const skillsDir = path.join(rootPath, subdir);
|
|
52
|
-
if (fs.existsSync(skillsDir)) {
|
|
53
|
-
try {
|
|
54
|
-
const skills = discoverSkills(skillsDir);
|
|
55
|
-
for (const skill of skills) {
|
|
56
|
-
// Track which root this skill came from
|
|
57
|
-
rootSources.set(skill.path, rootName);
|
|
58
|
-
// Count occurrences of each name
|
|
59
|
-
const count = (nameCount.get(skill.name) || 0) + 1;
|
|
60
|
-
nameCount.set(skill.name, count);
|
|
61
|
-
allSkills.push(skill);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
console.error(`Failed to discover skills in "${skillsDir}":`, error);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// Handle naming conflicts by prefixing duplicates with root name
|
|
71
|
-
for (const skill of allSkills) {
|
|
72
|
-
if (nameCount.get(skill.name) > 1) {
|
|
73
|
-
const rootName = rootSources.get(skill.path);
|
|
74
|
-
skill.name = `${rootName}:${skill.name}`;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return { skills: allSkills, rootSources };
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Discover skills from a directory, checking both the directory itself
|
|
81
|
-
* and SKILL_SUBDIRS subdirectories.
|
|
82
|
-
*/
|
|
83
|
-
function discoverSkillsFromDirectory(skillsDir) {
|
|
84
|
-
const allSkills = [];
|
|
85
|
-
// Check if the directory itself contains skills
|
|
86
|
-
const directSkills = discoverSkills(skillsDir);
|
|
87
|
-
allSkills.push(...directSkills);
|
|
88
|
-
// Also check SKILL_SUBDIRS subdirectories
|
|
89
|
-
for (const subdir of SKILL_SUBDIRS) {
|
|
90
|
-
const subPath = path.join(skillsDir, subdir);
|
|
91
|
-
if (fs.existsSync(subPath)) {
|
|
92
|
-
const subdirSkills = discoverSkills(subPath);
|
|
93
|
-
allSkills.push(...subdirSkills);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return allSkills;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Sync skills from roots or configured skills directory.
|
|
100
|
-
*
|
|
101
|
-
* Pattern from mcp-server-ts snippets/server/index.ts:
|
|
102
|
-
* - Check client capabilities
|
|
103
|
-
* - Request roots if supported
|
|
104
|
-
* - Use skills directory if not
|
|
105
|
-
*
|
|
106
|
-
* @param server - The McpServer instance
|
|
107
|
-
* @param skillsDir - Optional skills directory if client doesn't support roots
|
|
108
|
-
* @param onSkillsChanged - Callback when skills are updated
|
|
109
|
-
*/
|
|
110
|
-
export async function syncSkills(server, skillsDir, onSkillsChanged) {
|
|
111
|
-
const capabilities = server.server.getClientCapabilities();
|
|
112
|
-
const allSkills = [];
|
|
113
|
-
const seenNames = new Set();
|
|
114
|
-
// Always discover from configured skills directory first
|
|
115
|
-
if (skillsDir) {
|
|
116
|
-
const dirSkills = discoverSkillsFromDirectory(skillsDir);
|
|
117
|
-
console.error(`Discovered ${dirSkills.length} skill(s) from skills directory`);
|
|
118
|
-
for (const skill of dirSkills) {
|
|
119
|
-
if (!seenNames.has(skill.name)) {
|
|
120
|
-
seenNames.add(skill.name);
|
|
121
|
-
allSkills.push(skill);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
// Also discover from roots if client supports them
|
|
126
|
-
if (capabilities?.roots) {
|
|
127
|
-
console.error("Client supports roots, requesting workspace roots...");
|
|
128
|
-
try {
|
|
129
|
-
const { roots } = await server.server.listRoots();
|
|
130
|
-
console.error(`Received ${roots.length} root(s) from client`);
|
|
131
|
-
const { skills: rootSkills } = discoverSkillsFromRoots(roots);
|
|
132
|
-
console.error(`Discovered ${rootSkills.length} skill(s) from roots`);
|
|
133
|
-
// Add roots skills, skipping duplicates (skillsDir takes precedence)
|
|
134
|
-
for (const skill of rootSkills) {
|
|
135
|
-
if (!seenNames.has(skill.name)) {
|
|
136
|
-
seenNames.add(skill.name);
|
|
137
|
-
allSkills.push(skill);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Listen for roots changes if client supports listChanged
|
|
141
|
-
if (capabilities.roots.listChanged) {
|
|
142
|
-
setupRootsChangeHandler(server, skillsDir, onSkillsChanged);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
console.error("Failed to get roots from client:", error);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
console.error("Client does not support roots");
|
|
151
|
-
}
|
|
152
|
-
console.error(`Total skills available: ${allSkills.length}`);
|
|
153
|
-
const skillMap = createSkillMap(allSkills);
|
|
154
|
-
const instructions = generateInstructions(allSkills);
|
|
155
|
-
onSkillsChanged(skillMap, instructions);
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Set up handler for roots/list_changed notifications.
|
|
159
|
-
*/
|
|
160
|
-
function setupRootsChangeHandler(server, skillsDir, onSkillsChanged) {
|
|
161
|
-
server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
|
162
|
-
console.error("Roots changed notification received, re-discovering skills...");
|
|
163
|
-
try {
|
|
164
|
-
const allSkills = [];
|
|
165
|
-
const seenNames = new Set();
|
|
166
|
-
// Always include skills from configured directory first
|
|
167
|
-
if (skillsDir) {
|
|
168
|
-
const dirSkills = discoverSkillsFromDirectory(skillsDir);
|
|
169
|
-
for (const skill of dirSkills) {
|
|
170
|
-
if (!seenNames.has(skill.name)) {
|
|
171
|
-
seenNames.add(skill.name);
|
|
172
|
-
allSkills.push(skill);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
// Add skills from roots
|
|
177
|
-
const { roots } = await server.server.listRoots();
|
|
178
|
-
const { skills: rootSkills } = discoverSkillsFromRoots(roots);
|
|
179
|
-
console.error(`Re-discovered ${rootSkills.length} skill(s) from updated roots`);
|
|
180
|
-
for (const skill of rootSkills) {
|
|
181
|
-
if (!seenNames.has(skill.name)) {
|
|
182
|
-
seenNames.add(skill.name);
|
|
183
|
-
allSkills.push(skill);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
console.error(`Total skills available: ${allSkills.length}`);
|
|
187
|
-
const skillMap = createSkillMap(allSkills);
|
|
188
|
-
const instructions = generateInstructions(allSkills);
|
|
189
|
-
onSkillsChanged(skillMap, instructions);
|
|
190
|
-
// Notify client that resources have changed
|
|
191
|
-
await server.server.notification({
|
|
192
|
-
method: "notifications/resources/list_changed",
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
catch (error) {
|
|
196
|
-
console.error("Failed to re-discover skills after roots change:", error);
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
}
|