@skilljack/mcp 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- │ • Client refreshes tool definitions
100
+ │ • Sends prompts/listChanged notification
101
+ │ • Client refreshes tool and prompt definitions │
86
102
  │ ↓ │
87
- LLM calls "skill" tool with skill name
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 both **tools** and **resources**:
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
- | `skill://{name}/{path}` | Specific file within skill |
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. `tools/listChanged` notification is sent to connected clients
215
- 4. Clients that support this notification will refresh tool definitions
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
 
@@ -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
+ }
@@ -33,7 +33,11 @@ const SkillSchema = z.object({
33
33
  */
34
34
  export function getToolDescription(skillState) {
35
35
  const usage = "Load a skill's full instructions. Returns the complete SKILL.md content " +
36
- "with step-by-step guidance, examples, and file references to follow.\n\n";
36
+ "with step-by-step guidance, examples, and file references to follow.\n\n" +
37
+ "IMPORTANT: When a skill is relevant to the user's task, you must invoke this tool " +
38
+ "IMMEDIATELY as your first action. NEVER just announce or mention a skill without " +
39
+ "actually calling this tool. This is a BLOCKING REQUIREMENT: invoke this tool BEFORE " +
40
+ "generating any other response about the task.\n\n";
37
41
  const skills = Array.from(skillState.skillMap.values());
38
42
  return usage + generateInstructions(skills);
39
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilljack/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server that discovers and serves Agent Skills. I know kung fu.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -38,7 +38,15 @@
38
38
  "build": "tsc",
39
39
  "start": "node dist/index.js",
40
40
  "dev": "tsx watch src/index.ts",
41
- "inspector": "npx @modelcontextprotocol/inspector@latest node dist/index.js"
41
+ "inspector": "npx @modelcontextprotocol/inspector@latest node dist/index.js",
42
+ "eval": "tsx evals/eval.ts",
43
+ "eval:greeting": "tsx evals/eval.ts --task=greeting",
44
+ "eval:code-style": "tsx evals/eval.ts --task=code-style",
45
+ "eval:template": "tsx evals/eval.ts --task=template-generator",
46
+ "eval:xlsx-openpyxl": "tsx evals/eval.ts --task=xlsx-openpyxl",
47
+ "eval:xlsx-formulas": "tsx evals/eval.ts --task=xlsx-formulas",
48
+ "eval:xlsx-financial": "tsx evals/eval.ts --task=xlsx-financial",
49
+ "eval:xlsx-verify": "tsx evals/eval.ts --task=xlsx-verify"
42
50
  },
43
51
  "dependencies": {
44
52
  "@modelcontextprotocol/sdk": "^1.25.1",
@@ -47,6 +55,7 @@
47
55
  "zod": "^3.25.0"
48
56
  },
49
57
  "devDependencies": {
58
+ "@anthropic-ai/claude-agent-sdk": "^0.1.42",
50
59
  "@types/node": "^22.10.0",
51
60
  "tsx": "^4.19.2",
52
61
  "typescript": "^5.7.2"
@@ -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>;
@@ -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
- }