@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 +86 -8
- package/dist/skill-prompts.d.ts +47 -0
- package/dist/skill-prompts.js +236 -0
- package/dist/skill-tool.js +5 -1
- package/package.json +11 -2
- 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
|
|
|
@@ -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
|
+
}
|
package/dist/skill-tool.js
CHANGED
|
@@ -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
|
+
"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"
|
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
|
-
}
|