@olaservo/skill-jack-mcp 0.1.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 +218 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +109 -0
- package/dist/roots-handler.d.ts +49 -0
- package/dist/roots-handler.js +199 -0
- package/dist/skill-discovery.d.ts +32 -0
- package/dist/skill-discovery.js +136 -0
- package/dist/skill-resources.d.ts +27 -0
- package/dist/skill-resources.js +239 -0
- package/dist/skill-tool.d.ts +38 -0
- package/dist/skill-tool.js +346 -0
- package/dist/subscriptions.d.ts +77 -0
- package/dist/subscriptions.js +277 -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,218 @@
|
|
|
1
|
+
# Skill Jack MCP
|
|
2
|
+
|
|
3
|
+
An MCP server that jacks [Agent Skills](https://agentskills.dev) directly into your LLM's brain.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Skill Discovery** - Discovers skills from a configured directory at startup
|
|
8
|
+
- **Server Instructions** - Injects skill metadata into the system prompt (for clients supporting instructions)
|
|
9
|
+
- **Skill Tool** - Load full skill content on demand (progressive disclosure)
|
|
10
|
+
- **MCP Resources** - Access skills via `skill://` URIs with batch collection support
|
|
11
|
+
- **Resource Subscriptions** - Real-time file watching with `notifications/resources/updated`
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @olaservo/skill-jack-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or run directly with npx:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @olaservo/skill-jack-mcp /path/to/skills
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### From Source
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/olaservo/skill-jack-mcp.git
|
|
29
|
+
cd skill-jack-mcp
|
|
30
|
+
npm install
|
|
31
|
+
npm run build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Configure a skills directory containing your Agent Skills:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Pass skills directory as argument
|
|
40
|
+
skill-jack-mcp /path/to/skills
|
|
41
|
+
|
|
42
|
+
# Or use environment variable
|
|
43
|
+
SKILLS_DIR=/path/to/skills skill-jack-mcp
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The server scans the directory and its `.claude/skills/` and `skills/` subdirectories for skills.
|
|
47
|
+
|
|
48
|
+
**Windows note**: Use forward slashes in paths when using with MCP Inspector:
|
|
49
|
+
```bash
|
|
50
|
+
skill-jack-mcp "C:/Users/you/skills"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## How It Works
|
|
54
|
+
|
|
55
|
+
The server implements the [Agent Skills](https://agentskills.dev) progressive disclosure pattern:
|
|
56
|
+
|
|
57
|
+
1. **At startup**: Discovers skills from configured directory
|
|
58
|
+
2. **On connection**: Server instructions (with skill metadata) are sent in the initialize response
|
|
59
|
+
3. **On tool call**: Agent calls `skill` tool to load full SKILL.md content
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
┌─────────────────────────────────────────────────────────┐
|
|
63
|
+
│ Server starts │
|
|
64
|
+
│ • Discovers skills from configured directory │
|
|
65
|
+
│ • Generates instructions with skill metadata │
|
|
66
|
+
│ ↓ │
|
|
67
|
+
│ MCP Client connects │
|
|
68
|
+
│ • Server instructions included in initialize response │
|
|
69
|
+
│ ↓ │
|
|
70
|
+
│ LLM sees skill metadata in system prompt │
|
|
71
|
+
│ ↓ │
|
|
72
|
+
│ LLM calls "skill" tool with skill name │
|
|
73
|
+
│ ↓ │
|
|
74
|
+
│ Server returns full SKILL.md content │
|
|
75
|
+
└─────────────────────────────────────────────────────────┘
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Tools
|
|
79
|
+
|
|
80
|
+
### `skill`
|
|
81
|
+
|
|
82
|
+
Load and activate an Agent Skill by name. Returns the full SKILL.md content.
|
|
83
|
+
|
|
84
|
+
**Input:**
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"name": "skill-name"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Output:** Full SKILL.md content including frontmatter and instructions.
|
|
92
|
+
|
|
93
|
+
### `skill-resource`
|
|
94
|
+
|
|
95
|
+
Read files within a skill's directory (`scripts/`, `references/`, `assets/`, `snippets/`, etc.).
|
|
96
|
+
|
|
97
|
+
This follows the Agent Skills spec's progressive disclosure pattern - resources are loaded only when needed.
|
|
98
|
+
|
|
99
|
+
**Input:**
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"skill": "mcp-server-ts",
|
|
103
|
+
"path": "snippets/tools/echo.ts"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Output:** File content.
|
|
108
|
+
|
|
109
|
+
**List available files** (pass empty path):
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"skill": "mcp-server-ts",
|
|
113
|
+
"path": ""
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Security:** Path traversal is prevented - only files within the skill directory can be accessed.
|
|
118
|
+
|
|
119
|
+
## Resources
|
|
120
|
+
|
|
121
|
+
Skills are also accessible via MCP [Resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resources) using `skill://` URIs.
|
|
122
|
+
|
|
123
|
+
### URI Patterns
|
|
124
|
+
|
|
125
|
+
| URI | Returns |
|
|
126
|
+
|-----|---------|
|
|
127
|
+
| `skill://` | All SKILL.md contents (collection) |
|
|
128
|
+
| `skill://{name}` | Single skill's SKILL.md content |
|
|
129
|
+
| `skill://{name}/` | All files in skill directory (collection) |
|
|
130
|
+
| `skill://{name}/{path}` | Specific file within skill |
|
|
131
|
+
|
|
132
|
+
### Resource Subscriptions
|
|
133
|
+
|
|
134
|
+
Clients can subscribe to resources for real-time updates when files change.
|
|
135
|
+
|
|
136
|
+
**Capability:** `resources: { subscribe: true, listChanged: true }`
|
|
137
|
+
|
|
138
|
+
**Subscribe to a resource:**
|
|
139
|
+
```
|
|
140
|
+
→ resources/subscribe { uri: "skill://mcp-server-ts" }
|
|
141
|
+
← {} (success)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Receive notifications when files change:**
|
|
145
|
+
```
|
|
146
|
+
← notifications/resources/updated { uri: "skill://mcp-server-ts" }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Unsubscribe:**
|
|
150
|
+
```
|
|
151
|
+
→ resources/unsubscribe { uri: "skill://mcp-server-ts" }
|
|
152
|
+
← {} (success)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**How it works:**
|
|
156
|
+
1. Client subscribes to a `skill://` URI
|
|
157
|
+
2. Server resolves URI to file path(s) and starts watching with chokidar
|
|
158
|
+
3. When files change, server debounces (100ms) and sends notification
|
|
159
|
+
4. Client can re-read the resource to get updated content
|
|
160
|
+
|
|
161
|
+
## Security
|
|
162
|
+
|
|
163
|
+
**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.
|
|
164
|
+
|
|
165
|
+
Protections in place:
|
|
166
|
+
- Path traversal prevention (symlink-aware)
|
|
167
|
+
- File size limits (10MB max)
|
|
168
|
+
- Directory depth limits
|
|
169
|
+
- Skill content is confined to configured directories
|
|
170
|
+
|
|
171
|
+
Not protected against:
|
|
172
|
+
- Malicious content within trusted skill directories
|
|
173
|
+
- Prompt injection via skill instructions (skills can influence LLM behavior by design)
|
|
174
|
+
|
|
175
|
+
## Server Instructions Format
|
|
176
|
+
|
|
177
|
+
The server generates [instructions](https://blog.modelcontextprotocol.io/posts/2025-11-03-using-server-instructions/) that include a usage preamble and skill metadata:
|
|
178
|
+
|
|
179
|
+
```markdown
|
|
180
|
+
# Skills
|
|
181
|
+
|
|
182
|
+
When a user's task matches a skill description below: 1) activate it, 2) follow its instructions completely.
|
|
183
|
+
|
|
184
|
+
<available_skills>
|
|
185
|
+
<skill>
|
|
186
|
+
<name>mcp-server-ts</name>
|
|
187
|
+
<description>Build TypeScript MCP servers with composable code snippets...</description>
|
|
188
|
+
<location>C:/path/to/mcp-server-ts/SKILL.md</location>
|
|
189
|
+
</skill>
|
|
190
|
+
</available_skills>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
These are loaded into the model's system prompt by [clients](https://modelcontextprotocol.io/clients) that support instructions.
|
|
194
|
+
|
|
195
|
+
## Skill Discovery
|
|
196
|
+
|
|
197
|
+
Skills are discovered at startup from the configured directory. The server checks:
|
|
198
|
+
- The directory itself for skill subdirectories
|
|
199
|
+
- `.claude/skills/` subdirectory
|
|
200
|
+
- `skills/` subdirectory
|
|
201
|
+
|
|
202
|
+
Each skill subdirectory must contain a `SKILL.md` file with YAML frontmatter including `name` and `description` fields.
|
|
203
|
+
|
|
204
|
+
## Testing
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Build first
|
|
208
|
+
npm run build
|
|
209
|
+
|
|
210
|
+
# Test with MCP Inspector
|
|
211
|
+
npx @modelcontextprotocol/inspector@latest node dist/index.js /path/to/skills
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Related
|
|
215
|
+
|
|
216
|
+
- [Agent Skills Specification](https://agentskills.dev)
|
|
217
|
+
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
218
|
+
- [Example MCP Clients](https://modelcontextprotocol.io/clients)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill Jack 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
|
+
* skill-jack-mcp /path/to/skills # Skills directory (required)
|
|
10
|
+
* SKILLS_DIR=/path/to/skills skill-jack-mcp
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill Jack 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
|
+
* skill-jack-mcp /path/to/skills # Skills directory (required)
|
|
10
|
+
* SKILLS_DIR=/path/to/skills skill-jack-mcp
|
|
11
|
+
*/
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { discoverSkills, generateInstructions, createSkillMap } from "./skill-discovery.js";
|
|
17
|
+
import { registerSkillTool } from "./skill-tool.js";
|
|
18
|
+
import { registerSkillResources } from "./skill-resources.js";
|
|
19
|
+
import { createSubscriptionManager, registerSubscriptionHandlers, } from "./subscriptions.js";
|
|
20
|
+
/**
|
|
21
|
+
* Subdirectories to check for skills within the configured directory.
|
|
22
|
+
*/
|
|
23
|
+
const SKILL_SUBDIRS = [".claude/skills", "skills"];
|
|
24
|
+
/**
|
|
25
|
+
* Get the skills directory from command line args or environment.
|
|
26
|
+
*/
|
|
27
|
+
function getSkillsDir() {
|
|
28
|
+
// Check command line argument first
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
if (args.length > 0 && args[0] && !args[0].startsWith("-")) {
|
|
31
|
+
return path.resolve(args[0]);
|
|
32
|
+
}
|
|
33
|
+
// Fall back to environment variable
|
|
34
|
+
const envDir = process.env.SKILLS_DIR;
|
|
35
|
+
if (envDir) {
|
|
36
|
+
return path.resolve(envDir);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Shared state for skill management.
|
|
42
|
+
* Tools and resources reference this state.
|
|
43
|
+
*/
|
|
44
|
+
const skillState = {
|
|
45
|
+
skillMap: new Map(),
|
|
46
|
+
instructions: "",
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Discover skills from configured directory.
|
|
50
|
+
* Checks both the directory itself and standard subdirectories.
|
|
51
|
+
*/
|
|
52
|
+
function discoverSkillsFromDir(skillsDir) {
|
|
53
|
+
const allSkills = [];
|
|
54
|
+
// Check if the directory itself contains skills
|
|
55
|
+
const directSkills = discoverSkills(skillsDir);
|
|
56
|
+
allSkills.push(...directSkills);
|
|
57
|
+
// Also check standard subdirectories
|
|
58
|
+
for (const subdir of SKILL_SUBDIRS) {
|
|
59
|
+
const subPath = path.join(skillsDir, subdir);
|
|
60
|
+
if (fs.existsSync(subPath)) {
|
|
61
|
+
const subdirSkills = discoverSkills(subPath);
|
|
62
|
+
allSkills.push(...subdirSkills);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return allSkills;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Subscription manager for resource file watching.
|
|
69
|
+
*/
|
|
70
|
+
const subscriptionManager = createSubscriptionManager();
|
|
71
|
+
async function main() {
|
|
72
|
+
const skillsDir = getSkillsDir();
|
|
73
|
+
if (!skillsDir) {
|
|
74
|
+
console.error("No skills directory configured.");
|
|
75
|
+
console.error("Usage: skill-jack-mcp /path/to/skills");
|
|
76
|
+
console.error(" or: SKILLS_DIR=/path/to/skills skill-jack-mcp");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
console.error(`Skills directory: ${skillsDir}`);
|
|
80
|
+
// Discover skills at startup
|
|
81
|
+
const skills = discoverSkillsFromDir(skillsDir);
|
|
82
|
+
skillState.skillMap = createSkillMap(skills);
|
|
83
|
+
skillState.instructions = generateInstructions(skills);
|
|
84
|
+
console.error(`Discovered ${skills.length} skill(s)`);
|
|
85
|
+
// Create the MCP server
|
|
86
|
+
const server = new McpServer({
|
|
87
|
+
name: "skill-jack-mcp",
|
|
88
|
+
version: "1.0.0",
|
|
89
|
+
}, {
|
|
90
|
+
capabilities: {
|
|
91
|
+
tools: {},
|
|
92
|
+
resources: { subscribe: true, listChanged: true },
|
|
93
|
+
},
|
|
94
|
+
instructions: skillState.instructions,
|
|
95
|
+
});
|
|
96
|
+
// Register tools and resources
|
|
97
|
+
registerSkillTool(server, skillState);
|
|
98
|
+
registerSkillResources(server, skillState);
|
|
99
|
+
// Register subscription handlers for resource file watching
|
|
100
|
+
registerSubscriptionHandlers(server, skillState, subscriptionManager);
|
|
101
|
+
// Connect via stdio transport
|
|
102
|
+
const transport = new StdioServerTransport();
|
|
103
|
+
await server.connect(transport);
|
|
104
|
+
console.error("Skill Jack ready. I know kung fu.");
|
|
105
|
+
}
|
|
106
|
+
main().catch((error) => {
|
|
107
|
+
console.error("Fatal error:", error);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
@@ -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>;
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill discovery and metadata parsing module.
|
|
3
|
+
*
|
|
4
|
+
* Discovers Agent Skills from a directory, parses YAML frontmatter,
|
|
5
|
+
* and generates server instructions XML.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Metadata extracted from a skill's SKILL.md frontmatter.
|
|
9
|
+
*/
|
|
10
|
+
export interface SkillMetadata {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
path: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Discover all skills in a directory.
|
|
17
|
+
* Scans for subdirectories containing SKILL.md files.
|
|
18
|
+
*/
|
|
19
|
+
export declare function discoverSkills(skillsDir: string): SkillMetadata[];
|
|
20
|
+
/**
|
|
21
|
+
* Generate the server instructions with available skills.
|
|
22
|
+
* Includes a brief preamble about skill usage following the Agent Skills spec.
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateInstructions(skills: SkillMetadata[]): string;
|
|
25
|
+
/**
|
|
26
|
+
* Load the full content of a skill's SKILL.md file.
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadSkillContent(skillPath: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Create a map from skill name to skill metadata for fast lookup.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createSkillMap(skills: SkillMetadata[]): Map<string, SkillMetadata>;
|