@skilljack/mcp 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +271 -0
- package/dist/roots-handler.d.ts +49 -0
- package/dist/roots-handler.js +199 -0
- package/dist/skill-discovery.d.ts +33 -0
- package/dist/skill-discovery.js +144 -0
- package/dist/skill-resources.d.ts +26 -0
- package/dist/skill-resources.js +286 -0
- package/dist/skill-tool.d.ts +46 -0
- package/dist/skill-tool.js +362 -0
- package/dist/subscriptions.d.ts +78 -0
- package/dist/subscriptions.js +285 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ola Hungerford
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Skilljack MCP
|
|
2
|
+
|
|
3
|
+
An MCP server that jacks [Agent Skills](https://agentskills.io) directly into your LLM's brain.
|
|
4
|
+
|
|
5
|
+
> **Recommended:** For best results, use an MCP client that supports `tools/listChanged` notifications (e.g., Claude Code). This enables dynamic skill discovery - when skills are added or modified, the client automatically refreshes its understanding of available skills.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Dynamic Skill Discovery** - Watches skill directories and automatically refreshes when skills change
|
|
10
|
+
- **Tool List Changed Notifications** - Sends `tools/listChanged` so clients can refresh available skills
|
|
11
|
+
- **Skill Tool** - Load full skill content on demand (progressive disclosure)
|
|
12
|
+
- **MCP Resources** - Access skills via `skill://` URIs with batch collection support
|
|
13
|
+
- **Resource Subscriptions** - Real-time file watching with `notifications/resources/updated`
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @skilljack/mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or run directly with npx:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @skilljack/mcp /path/to/skills
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### From Source
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/olaservo/skilljack-mcp.git
|
|
31
|
+
cd skilljack-mcp
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Configure one or more skills directories containing your Agent Skills:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Single directory
|
|
42
|
+
skilljack-mcp /path/to/skills
|
|
43
|
+
|
|
44
|
+
# Multiple directories (separate args or comma-separated)
|
|
45
|
+
skilljack-mcp /path/to/skills /path/to/more/skills
|
|
46
|
+
skilljack-mcp /path/to/skills,/path/to/more/skills
|
|
47
|
+
|
|
48
|
+
# Using environment variable (comma-separated for multiple)
|
|
49
|
+
SKILLS_DIR=/path/to/skills skilljack-mcp
|
|
50
|
+
SKILLS_DIR=/path/to/skills,/path/to/more/skills skilljack-mcp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Each directory is scanned along with its `.claude/skills/` and `skills/` subdirectories for skills. Duplicate skill names are handled by keeping the first occurrence.
|
|
54
|
+
|
|
55
|
+
**Windows note**: Use forward slashes in paths when using with MCP Inspector:
|
|
56
|
+
```bash
|
|
57
|
+
skilljack-mcp "C:/Users/you/skills"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## How It Works
|
|
61
|
+
|
|
62
|
+
The server implements the [Agent Skills](https://agentskills.io) progressive disclosure pattern with dynamic updates:
|
|
63
|
+
|
|
64
|
+
1. **At startup**: Discovers skills from configured directories and starts file watchers
|
|
65
|
+
2. **On connection**: Skill tool description includes available skills metadata
|
|
66
|
+
3. **On file change**: Re-discovers skills, updates tool description, sends `tools/listChanged`
|
|
67
|
+
4. **On tool call**: Agent calls `skill` tool to load full SKILL.md content
|
|
68
|
+
5. **As needed**: Agent calls `skill-resource` to load additional files
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
┌─────────────────────────────────────────────────────────┐
|
|
72
|
+
│ Server starts │
|
|
73
|
+
│ • Discovers skills from configured directories │
|
|
74
|
+
│ • Starts watching for SKILL.md changes │
|
|
75
|
+
│ ↓ │
|
|
76
|
+
│ MCP Client connects │
|
|
77
|
+
│ • Skill tool description includes available skills │
|
|
78
|
+
│ ↓ │
|
|
79
|
+
│ LLM sees skill metadata in tool description │
|
|
80
|
+
│ ↓ │
|
|
81
|
+
│ SKILL.md added/modified/removed │
|
|
82
|
+
│ • Server re-discovers skills │
|
|
83
|
+
│ • Updates skill tool description │
|
|
84
|
+
│ • Sends tools/listChanged notification │
|
|
85
|
+
│ • Client refreshes tool definitions │
|
|
86
|
+
│ ↓ │
|
|
87
|
+
│ LLM calls "skill" tool with skill name │
|
|
88
|
+
│ ↓ │
|
|
89
|
+
│ Server returns full SKILL.md content │
|
|
90
|
+
│ ↓ │
|
|
91
|
+
│ LLM calls "skill-resource" for additional files │
|
|
92
|
+
│ • Scripts, snippets, references, assets, etc. │
|
|
93
|
+
└─────────────────────────────────────────────────────────┘
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Tools vs Resources
|
|
97
|
+
|
|
98
|
+
This server exposes skills via both **tools** and **resources**:
|
|
99
|
+
|
|
100
|
+
- **Tools** (`skill`, `skill-resource`) - For your agent to use autonomously. The LLM sees available skills in the tool description and calls them as needed.
|
|
101
|
+
- **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
|
+
|
|
103
|
+
Most users will rely on tools for automatic skill activation. Resources provide an alternative for manual control.
|
|
104
|
+
|
|
105
|
+
## Tools
|
|
106
|
+
|
|
107
|
+
### `skill`
|
|
108
|
+
|
|
109
|
+
Load and activate an Agent Skill by name. Returns the full SKILL.md content.
|
|
110
|
+
|
|
111
|
+
**Input:**
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"name": "skill-name"
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Output:** Full SKILL.md content including frontmatter and instructions.
|
|
119
|
+
|
|
120
|
+
### `skill-resource`
|
|
121
|
+
|
|
122
|
+
Read files within a skill's directory (`scripts/`, `references/`, `assets/`, `snippets/`, etc.).
|
|
123
|
+
|
|
124
|
+
This follows the Agent Skills spec's progressive disclosure pattern - resources are loaded only when needed.
|
|
125
|
+
|
|
126
|
+
**Read a single file:**
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"skill": "mcp-server-ts",
|
|
130
|
+
"path": "snippets/tools/echo.ts"
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Read all files in a directory:**
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"skill": "algorithmic-art",
|
|
138
|
+
"path": "templates"
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
Returns all files in the directory as multiple content items.
|
|
142
|
+
|
|
143
|
+
**List available files** (pass empty path):
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"skill": "mcp-server-ts",
|
|
147
|
+
"path": ""
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Security:** Path traversal is prevented - only files within the skill directory can be accessed.
|
|
152
|
+
|
|
153
|
+
## Resources
|
|
154
|
+
|
|
155
|
+
Skills are also accessible via MCP [Resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resources) using `skill://` URIs.
|
|
156
|
+
|
|
157
|
+
### URI Patterns
|
|
158
|
+
|
|
159
|
+
| URI | Returns |
|
|
160
|
+
|-----|---------|
|
|
161
|
+
| `skill://{name}` | Single skill's SKILL.md content |
|
|
162
|
+
| `skill://{name}/` | All files in skill directory (collection) |
|
|
163
|
+
| `skill://{name}/{path}` | Specific file within skill |
|
|
164
|
+
|
|
165
|
+
### Resource Subscriptions
|
|
166
|
+
|
|
167
|
+
Clients can subscribe to resources for real-time updates when files change.
|
|
168
|
+
|
|
169
|
+
**Capability:** `resources: { subscribe: true, listChanged: true }`
|
|
170
|
+
|
|
171
|
+
**Subscribe to a resource:**
|
|
172
|
+
```
|
|
173
|
+
→ resources/subscribe { uri: "skill://mcp-server-ts" }
|
|
174
|
+
← {} (success)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Receive notifications when files change:**
|
|
178
|
+
```
|
|
179
|
+
← notifications/resources/updated { uri: "skill://mcp-server-ts" }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Unsubscribe:**
|
|
183
|
+
```
|
|
184
|
+
→ resources/unsubscribe { uri: "skill://mcp-server-ts" }
|
|
185
|
+
← {} (success)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**How it works:**
|
|
189
|
+
1. Client subscribes to a `skill://` URI
|
|
190
|
+
2. Server resolves URI to file path(s) and starts watching with chokidar
|
|
191
|
+
3. When files change, server debounces (100ms) and sends notification
|
|
192
|
+
4. Client can re-read the resource to get updated content
|
|
193
|
+
|
|
194
|
+
## Security
|
|
195
|
+
|
|
196
|
+
**Skills are treated as trusted content.** This server reads and serves skill files directly to clients without sanitization. Only configure skills directories containing content you trust.
|
|
197
|
+
|
|
198
|
+
Protections in place:
|
|
199
|
+
- Path traversal prevention (symlink-aware)
|
|
200
|
+
- File size limits (1MB default, configurable via `MAX_FILE_SIZE_MB` env var)
|
|
201
|
+
- Directory depth limits
|
|
202
|
+
- Skill content is confined to configured directories
|
|
203
|
+
|
|
204
|
+
Not protected against:
|
|
205
|
+
- Malicious content within trusted skill directories
|
|
206
|
+
- Prompt injection via skill instructions (skills can influence LLM behavior by design)
|
|
207
|
+
|
|
208
|
+
## Dynamic Skill Discovery
|
|
209
|
+
|
|
210
|
+
The server watches skill directories for changes. When SKILL.md files are added, modified, or removed:
|
|
211
|
+
|
|
212
|
+
1. Skills are re-discovered from all configured directories
|
|
213
|
+
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
|
|
216
|
+
|
|
217
|
+
## Skill Metadata Format
|
|
218
|
+
|
|
219
|
+
The `skill` tool description includes metadata for all available skills in XML format:
|
|
220
|
+
|
|
221
|
+
```markdown
|
|
222
|
+
# Skills
|
|
223
|
+
|
|
224
|
+
When a user's task matches a skill description below: 1) activate it, 2) follow its instructions completely.
|
|
225
|
+
|
|
226
|
+
<available_skills>
|
|
227
|
+
<skill>
|
|
228
|
+
<name>mcp-server-ts</name>
|
|
229
|
+
<description>Build TypeScript MCP servers with composable code snippets...</description>
|
|
230
|
+
<location>C:/path/to/mcp-server-ts/SKILL.md</location>
|
|
231
|
+
</skill>
|
|
232
|
+
</available_skills>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
This metadata is dynamically updated when skills change - clients supporting `tools/listChanged` will automatically refresh.
|
|
236
|
+
|
|
237
|
+
## Skill Discovery
|
|
238
|
+
|
|
239
|
+
Skills are discovered at startup from the configured directories. For each directory, the server checks:
|
|
240
|
+
- The directory itself for skill subdirectories
|
|
241
|
+
- `.claude/skills/` subdirectory
|
|
242
|
+
- `skills/` subdirectory
|
|
243
|
+
|
|
244
|
+
Each skill subdirectory must contain a `SKILL.md` file with YAML frontmatter including `name` and `description` fields.
|
|
245
|
+
|
|
246
|
+
## Testing
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# Build first
|
|
250
|
+
npm run build
|
|
251
|
+
|
|
252
|
+
# Test with MCP Inspector
|
|
253
|
+
npx @modelcontextprotocol/inspector@latest node dist/index.js /path/to/skills
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Related
|
|
257
|
+
|
|
258
|
+
- [Agent Skills Specification](https://agentskills.io)
|
|
259
|
+
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
260
|
+
- [Example MCP Clients](https://modelcontextprotocol.io/clients)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skilljack MCP - "I know kung fu."
|
|
4
|
+
*
|
|
5
|
+
* MCP server that jacks Agent Skills directly into your LLM's brain.
|
|
6
|
+
* Provides global skills with tools for progressive disclosure.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* skilljack-mcp /path/to/skills [/path2 ...] # One or more directories
|
|
10
|
+
* SKILLS_DIR=/path/to/skills skilljack-mcp # Single directory via env
|
|
11
|
+
* SKILLS_DIR=/path1,/path2 skilljack-mcp # Multiple (comma-separated)
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skilljack MCP - "I know kung fu."
|
|
4
|
+
*
|
|
5
|
+
* MCP server that jacks Agent Skills directly into your LLM's brain.
|
|
6
|
+
* Provides global skills with tools for progressive disclosure.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* skilljack-mcp /path/to/skills [/path2 ...] # One or more directories
|
|
10
|
+
* SKILLS_DIR=/path/to/skills skilljack-mcp # Single directory via env
|
|
11
|
+
* SKILLS_DIR=/path1,/path2 skilljack-mcp # Multiple (comma-separated)
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import chokidar from "chokidar";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { discoverSkills, createSkillMap } from "./skill-discovery.js";
|
|
19
|
+
import { registerSkillTool, getToolDescription } from "./skill-tool.js";
|
|
20
|
+
import { registerSkillResources } from "./skill-resources.js";
|
|
21
|
+
import { createSubscriptionManager, registerSubscriptionHandlers, refreshSubscriptions, } from "./subscriptions.js";
|
|
22
|
+
/**
|
|
23
|
+
* Subdirectories to check for skills within the configured directory.
|
|
24
|
+
*/
|
|
25
|
+
const SKILL_SUBDIRS = [".claude/skills", "skills"];
|
|
26
|
+
/**
|
|
27
|
+
* Separator for multiple paths in SKILLS_DIR environment variable.
|
|
28
|
+
* Comma works cross-platform (not valid in file paths on any OS).
|
|
29
|
+
*/
|
|
30
|
+
const PATH_LIST_SEPARATOR = ",";
|
|
31
|
+
/**
|
|
32
|
+
* Get the skills directories from command line args and/or environment.
|
|
33
|
+
* Returns deduplicated, resolved paths.
|
|
34
|
+
*/
|
|
35
|
+
function getSkillsDirs() {
|
|
36
|
+
const dirs = [];
|
|
37
|
+
// Collect all non-flag command-line arguments (comma-separated supported)
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
for (const arg of args) {
|
|
40
|
+
if (!arg.startsWith("-")) {
|
|
41
|
+
const paths = arg
|
|
42
|
+
.split(PATH_LIST_SEPARATOR)
|
|
43
|
+
.map((p) => p.trim())
|
|
44
|
+
.filter((p) => p.length > 0)
|
|
45
|
+
.map((p) => path.resolve(p));
|
|
46
|
+
dirs.push(...paths);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Also check environment variable (comma-separated supported)
|
|
50
|
+
const envDir = process.env.SKILLS_DIR;
|
|
51
|
+
if (envDir) {
|
|
52
|
+
const envPaths = envDir
|
|
53
|
+
.split(PATH_LIST_SEPARATOR)
|
|
54
|
+
.map((p) => p.trim())
|
|
55
|
+
.filter((p) => p.length > 0)
|
|
56
|
+
.map((p) => path.resolve(p));
|
|
57
|
+
dirs.push(...envPaths);
|
|
58
|
+
}
|
|
59
|
+
// Deduplicate by resolved path
|
|
60
|
+
return [...new Set(dirs)];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Shared state for skill management.
|
|
64
|
+
* Tools and resources reference this state.
|
|
65
|
+
*/
|
|
66
|
+
const skillState = {
|
|
67
|
+
skillMap: new Map(),
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Discover skills from multiple configured directories.
|
|
71
|
+
* Each directory is checked along with its standard subdirectories.
|
|
72
|
+
* Handles duplicate skill names by keeping first occurrence.
|
|
73
|
+
*/
|
|
74
|
+
function discoverSkillsFromDirs(skillsDirs) {
|
|
75
|
+
const allSkills = [];
|
|
76
|
+
const seenNames = new Map(); // name -> source directory
|
|
77
|
+
for (const skillsDir of skillsDirs) {
|
|
78
|
+
if (!fs.existsSync(skillsDir)) {
|
|
79
|
+
console.error(`Warning: Skills directory not found: ${skillsDir}`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
console.error(`Scanning skills directory: ${skillsDir}`);
|
|
83
|
+
// Check if the directory itself contains skills
|
|
84
|
+
const dirSkills = discoverSkills(skillsDir);
|
|
85
|
+
// Also check standard subdirectories
|
|
86
|
+
for (const subdir of SKILL_SUBDIRS) {
|
|
87
|
+
const subPath = path.join(skillsDir, subdir);
|
|
88
|
+
if (fs.existsSync(subPath)) {
|
|
89
|
+
dirSkills.push(...discoverSkills(subPath));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Add skills, checking for duplicates
|
|
93
|
+
for (const skill of dirSkills) {
|
|
94
|
+
if (seenNames.has(skill.name)) {
|
|
95
|
+
console.error(`Warning: Duplicate skill "${skill.name}" found in ${path.dirname(skill.path)} ` +
|
|
96
|
+
`(already loaded from ${seenNames.get(skill.name)})`);
|
|
97
|
+
continue; // Skip duplicate
|
|
98
|
+
}
|
|
99
|
+
seenNames.set(skill.name, path.dirname(skill.path));
|
|
100
|
+
allSkills.push(skill);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return allSkills;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Debounce delay for skill directory changes (ms).
|
|
107
|
+
* Multiple rapid changes are coalesced into one refresh.
|
|
108
|
+
*/
|
|
109
|
+
const SKILL_REFRESH_DEBOUNCE_MS = 500;
|
|
110
|
+
/**
|
|
111
|
+
* Refresh skills and notify clients of changes.
|
|
112
|
+
* Called when skill files change on disk.
|
|
113
|
+
*
|
|
114
|
+
* @param skillsDirs - The configured skill directories
|
|
115
|
+
* @param server - The MCP server instance
|
|
116
|
+
* @param skillTool - The registered skill tool to update
|
|
117
|
+
* @param subscriptionManager - For refreshing resource subscriptions
|
|
118
|
+
*/
|
|
119
|
+
function refreshSkills(skillsDirs, server, skillTool, subscriptionManager) {
|
|
120
|
+
console.error("Refreshing skills...");
|
|
121
|
+
// Re-discover all skills
|
|
122
|
+
const skills = discoverSkillsFromDirs(skillsDirs);
|
|
123
|
+
const oldCount = skillState.skillMap.size;
|
|
124
|
+
// Update shared state
|
|
125
|
+
skillState.skillMap = createSkillMap(skills);
|
|
126
|
+
console.error(`Skills refreshed: ${oldCount} -> ${skills.length} skill(s)`);
|
|
127
|
+
// Update the skill tool description with new instructions
|
|
128
|
+
skillTool.update({
|
|
129
|
+
description: getToolDescription(skillState),
|
|
130
|
+
});
|
|
131
|
+
// Refresh resource subscriptions to match new skill state
|
|
132
|
+
refreshSubscriptions(subscriptionManager, skillState, (uri) => {
|
|
133
|
+
server.server.notification({
|
|
134
|
+
method: "notifications/resources/updated",
|
|
135
|
+
params: { uri },
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
// Notify clients that tools have changed
|
|
139
|
+
// This prompts clients to call tools/list again
|
|
140
|
+
server.sendToolListChanged();
|
|
141
|
+
// Also notify that resources have changed
|
|
142
|
+
server.sendResourceListChanged();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Set up file watchers on skill directories to detect changes.
|
|
146
|
+
* Watches for SKILL.md additions, modifications, and deletions.
|
|
147
|
+
*
|
|
148
|
+
* @param skillsDirs - The configured skill directories
|
|
149
|
+
* @param server - The MCP server instance
|
|
150
|
+
* @param skillTool - The registered skill tool to update
|
|
151
|
+
* @param subscriptionManager - For refreshing subscriptions
|
|
152
|
+
*/
|
|
153
|
+
function watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager) {
|
|
154
|
+
let refreshTimeout = null;
|
|
155
|
+
const debouncedRefresh = () => {
|
|
156
|
+
if (refreshTimeout) {
|
|
157
|
+
clearTimeout(refreshTimeout);
|
|
158
|
+
}
|
|
159
|
+
refreshTimeout = setTimeout(() => {
|
|
160
|
+
refreshTimeout = null;
|
|
161
|
+
refreshSkills(skillsDirs, server, skillTool, subscriptionManager);
|
|
162
|
+
}, SKILL_REFRESH_DEBOUNCE_MS);
|
|
163
|
+
};
|
|
164
|
+
// Build list of paths to watch
|
|
165
|
+
const watchPaths = [];
|
|
166
|
+
for (const dir of skillsDirs) {
|
|
167
|
+
if (fs.existsSync(dir)) {
|
|
168
|
+
watchPaths.push(dir);
|
|
169
|
+
// Also watch standard subdirectories
|
|
170
|
+
for (const subdir of SKILL_SUBDIRS) {
|
|
171
|
+
const subPath = path.join(dir, subdir);
|
|
172
|
+
if (fs.existsSync(subPath)) {
|
|
173
|
+
watchPaths.push(subPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (watchPaths.length === 0) {
|
|
179
|
+
console.error("No skill directories to watch");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
console.error(`Watching for skill changes in: ${watchPaths.join(", ")}`);
|
|
183
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
184
|
+
persistent: true,
|
|
185
|
+
ignoreInitial: true,
|
|
186
|
+
depth: 2, // Watch skill subdirectories but not too deep
|
|
187
|
+
ignored: ["**/node_modules/**", "**/.git/**"],
|
|
188
|
+
awaitWriteFinish: {
|
|
189
|
+
stabilityThreshold: 200,
|
|
190
|
+
pollInterval: 50,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
// Watch for SKILL.md changes specifically
|
|
194
|
+
watcher.on("add", (filePath) => {
|
|
195
|
+
if (path.basename(filePath).toLowerCase() === "skill.md") {
|
|
196
|
+
console.error(`Skill added: ${filePath}`);
|
|
197
|
+
debouncedRefresh();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
watcher.on("change", (filePath) => {
|
|
201
|
+
if (path.basename(filePath).toLowerCase() === "skill.md") {
|
|
202
|
+
console.error(`Skill modified: ${filePath}`);
|
|
203
|
+
debouncedRefresh();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
watcher.on("unlink", (filePath) => {
|
|
207
|
+
if (path.basename(filePath).toLowerCase() === "skill.md") {
|
|
208
|
+
console.error(`Skill removed: ${filePath}`);
|
|
209
|
+
debouncedRefresh();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
// Also watch for directory additions (new skill folders)
|
|
213
|
+
watcher.on("addDir", (dirPath) => {
|
|
214
|
+
// Check if this might be a new skill directory
|
|
215
|
+
const skillMdPath = path.join(dirPath, "SKILL.md");
|
|
216
|
+
const skillMdPathLower = path.join(dirPath, "skill.md");
|
|
217
|
+
if (fs.existsSync(skillMdPath) || fs.existsSync(skillMdPathLower)) {
|
|
218
|
+
console.error(`Skill directory added: ${dirPath}`);
|
|
219
|
+
debouncedRefresh();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
watcher.on("unlinkDir", (dirPath) => {
|
|
223
|
+
// A skill directory was removed
|
|
224
|
+
console.error(`Directory removed: ${dirPath}`);
|
|
225
|
+
debouncedRefresh();
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Subscription manager for resource file watching.
|
|
230
|
+
*/
|
|
231
|
+
const subscriptionManager = createSubscriptionManager();
|
|
232
|
+
async function main() {
|
|
233
|
+
const skillsDirs = getSkillsDirs();
|
|
234
|
+
if (skillsDirs.length === 0) {
|
|
235
|
+
console.error("No skills directory configured.");
|
|
236
|
+
console.error("Usage: skilljack-mcp /path/to/skills [/path/to/more/skills ...]");
|
|
237
|
+
console.error(" or: SKILLS_DIR=/path/to/skills skilljack-mcp");
|
|
238
|
+
console.error(" or: SKILLS_DIR=/path1,/path2 skilljack-mcp");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
console.error(`Skills directories: ${skillsDirs.join(", ")}`);
|
|
242
|
+
// Discover skills at startup
|
|
243
|
+
const skills = discoverSkillsFromDirs(skillsDirs);
|
|
244
|
+
skillState.skillMap = createSkillMap(skills);
|
|
245
|
+
console.error(`Discovered ${skills.length} skill(s)`);
|
|
246
|
+
// Create the MCP server
|
|
247
|
+
const server = new McpServer({
|
|
248
|
+
name: "skilljack-mcp",
|
|
249
|
+
version: "1.0.0",
|
|
250
|
+
}, {
|
|
251
|
+
capabilities: {
|
|
252
|
+
tools: { listChanged: true },
|
|
253
|
+
resources: { subscribe: true, listChanged: true },
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
// Register tools and resources
|
|
257
|
+
const skillTool = registerSkillTool(server, skillState);
|
|
258
|
+
registerSkillResources(server, skillState);
|
|
259
|
+
// Register subscription handlers for resource file watching
|
|
260
|
+
registerSubscriptionHandlers(server, skillState, subscriptionManager);
|
|
261
|
+
// Set up file watchers for skill directory changes
|
|
262
|
+
watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager);
|
|
263
|
+
// Connect via stdio transport
|
|
264
|
+
const transport = new StdioServerTransport();
|
|
265
|
+
await server.connect(transport);
|
|
266
|
+
console.error("Skilljack ready. I know kung fu.");
|
|
267
|
+
}
|
|
268
|
+
main().catch((error) => {
|
|
269
|
+
console.error("Fatal error:", error);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
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>;
|