@iflow-mcp/back1ply-agent-skill-loader 1.0.2
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/25014_process.log +4 -0
- package/ARCHITECTURE.md +47 -0
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/build/index.js +364 -0
- package/build/utils.js +79 -0
- package/glama.json +6 -0
- package/language.json +1 -0
- package/package.json +36 -0
- package/package_name +1 -0
- package/push_info.json +5 -0
- package/tsconfig.json +15 -0
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Architecture & Context 🏗️
|
|
2
|
+
|
|
3
|
+
This document provides technical context for AI agents working on this codebase.
|
|
4
|
+
|
|
5
|
+
## System Overview
|
|
6
|
+
|
|
7
|
+
**Agent Skill Loader** is a Node.js-based MCP Server. It does not maintain a persistent database; it scans the file system in real-time (or near real-time) to discover "Skills".
|
|
8
|
+
|
|
9
|
+
### Core Definitions
|
|
10
|
+
|
|
11
|
+
* **Skill**: A directory containing a `SKILL.md` file. The `SKILL.md` contains the instructions (system prompt fragment) for the AI.
|
|
12
|
+
* **Skill Library**: A root directory containing multiple Skill directories.
|
|
13
|
+
|
|
14
|
+
## Logic Flow (`src/index.ts`)
|
|
15
|
+
|
|
16
|
+
1. **Initialization**:
|
|
17
|
+
* The server starts using `StdioServerTransport`.
|
|
18
|
+
* It loads configuration from `.env`.
|
|
19
|
+
* It reads `MCP_SKILL_PATHS` to determine where to scan.
|
|
20
|
+
|
|
21
|
+
2. **Tool: `list_skills`**:
|
|
22
|
+
* Recursively scans `SEARCH_PATHS`.
|
|
23
|
+
* Looks for `SKILL.md`.
|
|
24
|
+
* Parses frontmatter (YAML-style) to extract `description`.
|
|
25
|
+
* Returns a simplified JSON list to save context tokens.
|
|
26
|
+
|
|
27
|
+
3. **Tool: `read_skill`**:
|
|
28
|
+
* Finds the specific skill by name.
|
|
29
|
+
* Reads `SKILL.md`.
|
|
30
|
+
* Returns the raw text Content.
|
|
31
|
+
|
|
32
|
+
4. **Tool: `install_skill`**:
|
|
33
|
+
* Locates the source directory.
|
|
34
|
+
* **Security**: Validates that target path is within current workspace.
|
|
35
|
+
* Uses `fs.cpSync` to recursively copy the content to the User's active workspace.
|
|
36
|
+
|
|
37
|
+
## Key Design Decisions
|
|
38
|
+
|
|
39
|
+
* **Recursive Scanning**: We scan subdirectories to find skills, allowing for categorized folder structures (e.g., `dax/skills/writing-dax-measures`).
|
|
40
|
+
* **No Database**: To keep it lightweight and stateless, we scan the FS. Performance is acceptable for <1000 skills.
|
|
41
|
+
* **TypeScript**: Used for type safety with the MCP SDK.
|
|
42
|
+
|
|
43
|
+
## Development Guidelines
|
|
44
|
+
|
|
45
|
+
* **Building**: `npm run build` runs `tsc`.
|
|
46
|
+
* **Modifying Tools**: Add new tools in `src/index.ts` using `server.tool()`.
|
|
47
|
+
* **Error Handling**: All filesystem operations should be wrapped in try/catch blocks to prevent server crashes.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 back1ply
|
|
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,118 @@
|
|
|
1
|
+
# Agent Skill Loader 🧠
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/agent-skill-loader)
|
|
4
|
+
[](https://registry.modelcontextprotocol.io)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://modelcontextprotocol.io)
|
|
9
|
+
|
|
10
|
+
**Agent Skill Loader** is a Model Context Protocol (MCP) server that acts as a bridge between your static Claude Code Skills library and dynamic AI agents (like Antigravity, Claude Desktop, or Cursor).
|
|
11
|
+
|
|
12
|
+
It allows agents to "learn" skills on demand without requiring you to manually copy files into every project.
|
|
13
|
+
|
|
14
|
+
## 🚀 Features
|
|
15
|
+
|
|
16
|
+
* **Discovery**: `list_skills` - Scans your configured skill directories.
|
|
17
|
+
* **Dynamic Learning**: `read_skill` - Fetches the `SKILL.md` content for the agent to read.
|
|
18
|
+
* **Persistence**: `install_skill` - Copies the skill permanently to your project if needed.
|
|
19
|
+
* **Configuration**: `manage_search_paths` - Add/remove skill directories at runtime.
|
|
20
|
+
* **Troubleshooting**: `debug_info` - Diagnose configuration and path issues.
|
|
21
|
+
|
|
22
|
+
## 🛠️ Setup
|
|
23
|
+
|
|
24
|
+
### Prerequisites
|
|
25
|
+
- Node.js >= 18
|
|
26
|
+
|
|
27
|
+
### Option A: Install from npm (Recommended)
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g agent-skill-loader
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then register in `.mcp.json`:
|
|
33
|
+
```json
|
|
34
|
+
"agent-skill-loader": {
|
|
35
|
+
"command": "agent-skill-loader"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Option B: Build from Source
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/back1ply/agent-skill-loader.git
|
|
42
|
+
cd agent-skill-loader
|
|
43
|
+
npm install
|
|
44
|
+
npm run build
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then register in `.mcp.json`:
|
|
48
|
+
```json
|
|
49
|
+
"agent-skill-loader": {
|
|
50
|
+
"command": "node",
|
|
51
|
+
"args": ["<path-to-repo>/build/index.js"]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 📂 Configuration
|
|
56
|
+
|
|
57
|
+
The server automatically detects its workspace and aggregates skill paths from:
|
|
58
|
+
|
|
59
|
+
1. **Default**: `%USERPROFILE%\.claude\plugins\cache` (Standard location)
|
|
60
|
+
2. **Dynamic Config**: `skill-paths.json` (Located in the project root)
|
|
61
|
+
|
|
62
|
+
### Dynamic Path Management
|
|
63
|
+
You do not need to manually edit config files. Use the tool to manage paths at runtime:
|
|
64
|
+
* **Add**: `manage_search_paths(operation="add", path="F:\\My\\Deep\\Skills")`
|
|
65
|
+
* **Remove**: `manage_search_paths(operation="remove", path="...")`
|
|
66
|
+
* **List**: `manage_search_paths(operation="list")` creates/updates `skill-paths.json`.
|
|
67
|
+
|
|
68
|
+
## 🤖 Usage
|
|
69
|
+
|
|
70
|
+
### For Agents
|
|
71
|
+
The agent will see five tools:
|
|
72
|
+
* `list_skills()`: Returns a JSON list of available skills.
|
|
73
|
+
* `read_skill(skill_name)`: Returns the markdown instructions.
|
|
74
|
+
* `install_skill(skill_name, target_path?)`: Copies the folder to `.agent/skills/<name>`. For security, `target_path` must be within the current workspace.
|
|
75
|
+
* `manage_search_paths(operation, path?)`: Add, remove, or list skill search paths.
|
|
76
|
+
* `debug_info()`: Returns diagnostic information (paths, status, warnings).
|
|
77
|
+
|
|
78
|
+
### Example Agent Prompt
|
|
79
|
+
> "I need to write a DAX measure but I'm not sure about the best practices."
|
|
80
|
+
|
|
81
|
+
The agent will automatically call `list_skills`, find `writing-dax-measures`, call `read_skill`, and then answer you with expert knowledge.
|
|
82
|
+
|
|
83
|
+
## 🔧 Troubleshooting
|
|
84
|
+
|
|
85
|
+
If skills aren't being discovered, use `debug_info()` to see:
|
|
86
|
+
* **search_paths**: Which directories are being scanned
|
|
87
|
+
* **path_status**: Whether each path exists and is readable
|
|
88
|
+
* **warnings**: Any errors encountered during scanning (permission denied, empty files, etc.)
|
|
89
|
+
|
|
90
|
+
Example output:
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"workspace_root": "C:/projects/agent-skill-loader",
|
|
94
|
+
"search_paths": {
|
|
95
|
+
"base": ["C:/Users/pc/.claude/plugins/cache"],
|
|
96
|
+
"dynamic": ["F:/My/Skills"],
|
|
97
|
+
"effective": ["C:/Users/pc/.claude/plugins/cache", "F:/My/Skills"]
|
|
98
|
+
},
|
|
99
|
+
"path_status": [
|
|
100
|
+
{ "path": "C:/Users/pc/.claude/plugins/cache", "exists": true, "readable": true },
|
|
101
|
+
{ "path": "F:/My/Skills", "exists": false, "readable": false }
|
|
102
|
+
],
|
|
103
|
+
"skills_found": 12,
|
|
104
|
+
"warnings": [
|
|
105
|
+
{ "path": "F:/My/Skills", "reason": "Directory does not exist" }
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 📦 Project Structure
|
|
111
|
+
|
|
112
|
+
* `src/index.ts`: Main server logic.
|
|
113
|
+
* `build/`: Compiled JavaScript output.
|
|
114
|
+
* `package.json`: Dependencies (`@modelcontextprotocol/sdk`, `zod`).
|
|
115
|
+
|
|
116
|
+
## 🤝 Contributing
|
|
117
|
+
|
|
118
|
+
To add new skills, simply add a folder with a `SKILL.md` file to one of the watched directories. The server picks them up automatically (no restart required for new files, though caching implementation may vary).
|
package/build/index.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { findSkillsInDir, getPathStatus } from "./utils.js";
|
|
8
|
+
// Load environment variables manually (dotenv v17 outputs to stdout which corrupts MCP)
|
|
9
|
+
function loadEnvFile() {
|
|
10
|
+
try {
|
|
11
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
12
|
+
if (fs.existsSync(envPath)) {
|
|
13
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
14
|
+
for (const line of content.split(/\r?\n/)) {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
17
|
+
const eqIndex = trimmed.indexOf("=");
|
|
18
|
+
if (eqIndex > 0) {
|
|
19
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
20
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
21
|
+
// Remove surrounding quotes if present
|
|
22
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
23
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
24
|
+
value = value.slice(1, -1);
|
|
25
|
+
}
|
|
26
|
+
if (!process.env[key]) {
|
|
27
|
+
process.env[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Silently ignore .env loading errors
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
loadEnvFile();
|
|
39
|
+
// --- Configuration ---
|
|
40
|
+
import * as os from "os";
|
|
41
|
+
import { fileURLToPath } from 'url';
|
|
42
|
+
// --- Workspace & Config Logic ---
|
|
43
|
+
// Auto-detect workspace root (project root)
|
|
44
|
+
// We are in /build/index.js, so project root is one level up
|
|
45
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
46
|
+
const __dirname = path.dirname(__filename);
|
|
47
|
+
const AUTO_DETECTED_ROOT = path.resolve(__dirname, "..");
|
|
48
|
+
function getWorkspaceRoot() {
|
|
49
|
+
// Allow override, else use auto-detected
|
|
50
|
+
return process.env.MCP_WORKSPACE_ROOT || AUTO_DETECTED_ROOT;
|
|
51
|
+
}
|
|
52
|
+
function getBasePaths() {
|
|
53
|
+
// 1. Always start with default path
|
|
54
|
+
const defaultPath = path.join(os.homedir(), ".claude", "plugins", "cache");
|
|
55
|
+
const paths = [defaultPath];
|
|
56
|
+
// 2. Add process.env paths if present
|
|
57
|
+
let envPaths = process.env.MCP_SKILL_PATHS;
|
|
58
|
+
if (envPaths) {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(envPaths);
|
|
61
|
+
if (Array.isArray(parsed)) {
|
|
62
|
+
paths.push(...parsed);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Assume separated string
|
|
66
|
+
paths.push(...envPaths.split(/;|,/).map((p) => p.trim()).filter(Boolean));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Not valid JSON, assume separated string
|
|
71
|
+
paths.push(...envPaths.split(/;|,/).map((p) => p.trim()).filter(Boolean));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Deduplicate
|
|
75
|
+
return Array.from(new Set(paths));
|
|
76
|
+
}
|
|
77
|
+
function getDynamicPaths() {
|
|
78
|
+
const basePaths = getBasePaths();
|
|
79
|
+
const cwd = getWorkspaceRoot();
|
|
80
|
+
const configPath = path.join(cwd, "skill-paths.json");
|
|
81
|
+
let dynamicPaths = [];
|
|
82
|
+
if (fs.existsSync(configPath)) {
|
|
83
|
+
try {
|
|
84
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
85
|
+
const parsed = JSON.parse(content);
|
|
86
|
+
if (Array.isArray(parsed)) {
|
|
87
|
+
dynamicPaths = parsed;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error("Error reading skill-paths.json:", err);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Deduplicate
|
|
95
|
+
return Array.from(new Set([...basePaths, ...dynamicPaths]));
|
|
96
|
+
}
|
|
97
|
+
// --- Types ---
|
|
98
|
+
// --- Helpers ---
|
|
99
|
+
function getDynamicPathsOnly() {
|
|
100
|
+
const cwd = getWorkspaceRoot();
|
|
101
|
+
const configPath = path.join(cwd, "skill-paths.json");
|
|
102
|
+
if (fs.existsSync(configPath)) {
|
|
103
|
+
try {
|
|
104
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
105
|
+
const parsed = JSON.parse(content);
|
|
106
|
+
if (Array.isArray(parsed)) {
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Ignore errors
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
function scanAllPaths() {
|
|
117
|
+
let allSkills = [];
|
|
118
|
+
let allWarnings = [];
|
|
119
|
+
const searchPaths = getDynamicPaths();
|
|
120
|
+
for (const searchPath of searchPaths) {
|
|
121
|
+
const result = findSkillsInDir(searchPath);
|
|
122
|
+
allSkills = allSkills.concat(result.skills);
|
|
123
|
+
allWarnings = allWarnings.concat(result.warnings);
|
|
124
|
+
}
|
|
125
|
+
return { skills: allSkills, warnings: allWarnings };
|
|
126
|
+
}
|
|
127
|
+
function getAllSkills() {
|
|
128
|
+
return scanAllPaths().skills;
|
|
129
|
+
}
|
|
130
|
+
function getAllWarnings() {
|
|
131
|
+
return scanAllPaths().warnings;
|
|
132
|
+
}
|
|
133
|
+
// --- Server Setup ---
|
|
134
|
+
const server = new McpServer({
|
|
135
|
+
name: "agent-skill-loader",
|
|
136
|
+
version: "1.0.0",
|
|
137
|
+
});
|
|
138
|
+
// --- Tools ---
|
|
139
|
+
// 1. list_skills - Lists all available skills from configured directories
|
|
140
|
+
server.tool("list_skills", "Returns a JSON list of all available skills with their names, descriptions, and source directories. Use this to discover what skills are available before reading or installing them.", {}, async () => {
|
|
141
|
+
const skills = getAllSkills();
|
|
142
|
+
// Debug: include examined paths if no skills found OR always for now
|
|
143
|
+
if (skills.length === 0) {
|
|
144
|
+
return {
|
|
145
|
+
content: [
|
|
146
|
+
{
|
|
147
|
+
type: "text",
|
|
148
|
+
text: JSON.stringify({
|
|
149
|
+
error: "No skills found",
|
|
150
|
+
env_var: process.env.MCP_SKILL_PATHS,
|
|
151
|
+
parsed_paths: getDynamicPaths(),
|
|
152
|
+
cwd: process.cwd()
|
|
153
|
+
}, null, 2)
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: JSON.stringify(skills.map((s) => ({
|
|
163
|
+
name: s.name,
|
|
164
|
+
description: s.description,
|
|
165
|
+
source_root: s.source,
|
|
166
|
+
})), null, 2),
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
// 2. read_skill - Fetches the full SKILL.md content for a skill
|
|
172
|
+
server.tool("read_skill", "Fetches and returns the full SKILL.md content for a specific skill. The content includes instructions and context that can be used to learn the skill's capabilities.", {
|
|
173
|
+
skill_name: z
|
|
174
|
+
.string()
|
|
175
|
+
.describe("The name of the skill to read (e.g., 'writing-dax-measures')"),
|
|
176
|
+
}, async ({ skill_name }) => {
|
|
177
|
+
const skills = getAllSkills();
|
|
178
|
+
const skill = skills.find((s) => s.name === skill_name);
|
|
179
|
+
if (!skill) {
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: "text", text: `Skill '${skill_name}' not found.` }],
|
|
182
|
+
isError: true,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const skillMdPath = path.join(skill.path, "SKILL.md");
|
|
186
|
+
try {
|
|
187
|
+
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: content }],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{ type: "text", text: `Failed to read skill: ${err.message}` },
|
|
196
|
+
],
|
|
197
|
+
isError: true,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// 3. install_skill - Copies a skill to the target workspace
|
|
202
|
+
server.tool("install_skill", "Copies an entire skill directory (including SKILL.md and any supporting files) to the target workspace. By default, installs to .agent/skills/<skill_name> in the current working directory.", {
|
|
203
|
+
skill_name: z.string().describe("Name of the skill to install"),
|
|
204
|
+
target_path: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("Destination path within current workspace. Defaults to .agent/skills/<skill_name>. Must be within the current working directory for security."),
|
|
208
|
+
}, async ({ skill_name, target_path }) => {
|
|
209
|
+
const skills = getAllSkills();
|
|
210
|
+
const skill = skills.find((s) => s.name === skill_name);
|
|
211
|
+
if (!skill) {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: `Skill '${skill_name}' not found.` }],
|
|
214
|
+
isError: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// Default target: .agent/skills/name
|
|
218
|
+
const cwd = getWorkspaceRoot();
|
|
219
|
+
let dest;
|
|
220
|
+
if (target_path) {
|
|
221
|
+
dest = path.resolve(cwd, target_path);
|
|
222
|
+
// Security: Ensure target path is within current working directory
|
|
223
|
+
const normalizedDest = path.normalize(dest);
|
|
224
|
+
const normalizedCwd = path.normalize(cwd);
|
|
225
|
+
if (!normalizedDest.startsWith(normalizedCwd)) {
|
|
226
|
+
return {
|
|
227
|
+
content: [
|
|
228
|
+
{
|
|
229
|
+
type: "text",
|
|
230
|
+
text: `Security error: Target path must be within the current workspace (${cwd}). Received: ${dest}`,
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
isError: true,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
dest = path.join(cwd, ".agent", "skills", skill.name);
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
fs.cpSync(skill.path, dest, { recursive: true, force: true });
|
|
242
|
+
return {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: `Successfully installed skill '${skill_name}' to '${dest}'`,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
return {
|
|
253
|
+
content: [
|
|
254
|
+
{ type: "text", text: `Failed to install skill: ${err.message}` },
|
|
255
|
+
],
|
|
256
|
+
isError: true,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
// 4. manage_search_paths - Runtime management of skill paths
|
|
261
|
+
server.tool("manage_search_paths", "Add, remove, or list dynamic skill search paths without restarting the server. Persists to skill-paths.json in the workspace root.", {
|
|
262
|
+
operation: z.enum(["add", "remove", "list"]).describe("Operation to perform"),
|
|
263
|
+
path: z.string().optional().describe("Absolute path to add or remove (not required for 'list')"),
|
|
264
|
+
}, async ({ operation, path: inputPath }) => {
|
|
265
|
+
const cwd = getWorkspaceRoot();
|
|
266
|
+
const configPath = path.join(cwd, "skill-paths.json");
|
|
267
|
+
// Read current config
|
|
268
|
+
let currentPaths = [];
|
|
269
|
+
if (fs.existsSync(configPath)) {
|
|
270
|
+
try {
|
|
271
|
+
currentPaths = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
272
|
+
if (!Array.isArray(currentPaths))
|
|
273
|
+
currentPaths = [];
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
currentPaths = [];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (operation === "list") {
|
|
280
|
+
const globalPaths = getBasePaths();
|
|
281
|
+
return {
|
|
282
|
+
content: [{
|
|
283
|
+
type: "text",
|
|
284
|
+
text: JSON.stringify({
|
|
285
|
+
dynamic: currentPaths,
|
|
286
|
+
global: globalPaths,
|
|
287
|
+
effective: Array.from(new Set([...globalPaths, ...currentPaths]))
|
|
288
|
+
}, null, 2)
|
|
289
|
+
}]
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (!inputPath) {
|
|
293
|
+
return {
|
|
294
|
+
content: [{ type: "text", text: "Path argument is required for add/remove operations." }],
|
|
295
|
+
isError: true
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// Normalize input path
|
|
299
|
+
// Remove "file:///" prefix if present (common agent error)
|
|
300
|
+
const cleanPath = inputPath.replace(/^file:\/\/\/?/, "");
|
|
301
|
+
// On Windows, if it looks like /c:/..., make it c:/...
|
|
302
|
+
// But path.resolve usually handles normal paths well.
|
|
303
|
+
const absPath = path.resolve(cleanPath);
|
|
304
|
+
if (operation === "add") {
|
|
305
|
+
if (!currentPaths.includes(absPath)) {
|
|
306
|
+
currentPaths.push(absPath);
|
|
307
|
+
fs.writeFileSync(configPath, JSON.stringify(currentPaths, null, 2));
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: `Added path: ${absPath}` }]
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: "text", text: `Path already exists: ${absPath}` }]
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (operation === "remove") {
|
|
317
|
+
const initialLen = currentPaths.length;
|
|
318
|
+
currentPaths = currentPaths.filter(p => p !== absPath);
|
|
319
|
+
if (currentPaths.length !== initialLen) {
|
|
320
|
+
fs.writeFileSync(configPath, JSON.stringify(currentPaths, null, 2));
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: "text", text: `Removed path: ${absPath}` }]
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "text", text: `Path not found in config: ${absPath}` }]
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return { content: [{ type: "text", text: "Invalid operation" }], isError: true };
|
|
330
|
+
});
|
|
331
|
+
// 5. debug_info - Diagnostic information for troubleshooting
|
|
332
|
+
server.tool("debug_info", "Returns diagnostic information about server configuration, search paths, and any warnings from the last scan. Use this when skills aren't being found or to verify configuration.", {}, async () => {
|
|
333
|
+
const effectivePaths = getDynamicPaths();
|
|
334
|
+
const scanResult = scanAllPaths();
|
|
335
|
+
return {
|
|
336
|
+
content: [{
|
|
337
|
+
type: "text",
|
|
338
|
+
text: JSON.stringify({
|
|
339
|
+
workspace_root: getWorkspaceRoot(),
|
|
340
|
+
search_paths: {
|
|
341
|
+
base: getBasePaths(),
|
|
342
|
+
dynamic: getDynamicPathsOnly(),
|
|
343
|
+
effective: effectivePaths
|
|
344
|
+
},
|
|
345
|
+
path_status: getPathStatus(effectivePaths),
|
|
346
|
+
env: {
|
|
347
|
+
MCP_SKILL_PATHS: process.env.MCP_SKILL_PATHS || null,
|
|
348
|
+
MCP_WORKSPACE_ROOT: process.env.MCP_WORKSPACE_ROOT || null
|
|
349
|
+
},
|
|
350
|
+
skills_found: scanResult.skills.length,
|
|
351
|
+
warnings: scanResult.warnings
|
|
352
|
+
}, null, 2)
|
|
353
|
+
}]
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
// --- Start Server ---
|
|
357
|
+
async function main() {
|
|
358
|
+
const transport = new StdioServerTransport();
|
|
359
|
+
await server.connect(transport);
|
|
360
|
+
}
|
|
361
|
+
main().catch((error) => {
|
|
362
|
+
console.error("Server error:", error);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
});
|
package/build/utils.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
// --- Configuration ---
|
|
4
|
+
export const DO_NOT_SCAN = ["node_modules", ".git", "dist", "build"];
|
|
5
|
+
export function extractDescription(content) {
|
|
6
|
+
const match = content.match(/^description:\s*(.+)$/m);
|
|
7
|
+
return match ? match[1].trim() : "No description provided.";
|
|
8
|
+
}
|
|
9
|
+
export function findSkillsInDir(startPath) {
|
|
10
|
+
const skills = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
if (!fs.existsSync(startPath)) {
|
|
13
|
+
warnings.push({ path: startPath, reason: "Directory does not exist" });
|
|
14
|
+
return { skills, warnings };
|
|
15
|
+
}
|
|
16
|
+
function scan(currentPath) {
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
warnings.push({
|
|
23
|
+
path: currentPath,
|
|
24
|
+
reason: `Cannot read directory: ${e.code || e.message}`
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Check if this directory is a skill (has SKILL.md)
|
|
29
|
+
const skillFile = entries.find((e) => e.isFile() && e.name === "SKILL.md");
|
|
30
|
+
if (skillFile) {
|
|
31
|
+
const fullPath = path.join(currentPath, skillFile.name);
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
34
|
+
if (!content.trim()) {
|
|
35
|
+
warnings.push({ path: fullPath, reason: "SKILL.md is empty" });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
skills.push({
|
|
39
|
+
name: path.basename(currentPath),
|
|
40
|
+
description: extractDescription(content),
|
|
41
|
+
path: currentPath,
|
|
42
|
+
source: startPath,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
warnings.push({
|
|
47
|
+
path: fullPath,
|
|
48
|
+
reason: `Cannot read SKILL.md: ${err.code || err.message}`
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// If we found a skill, we assume subdirectories are part of the skill
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Otherwise, recurse
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (entry.isDirectory() && !DO_NOT_SCAN.includes(entry.name)) {
|
|
57
|
+
scan(path.join(currentPath, entry.name));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
scan(startPath);
|
|
62
|
+
return { skills, warnings };
|
|
63
|
+
}
|
|
64
|
+
export function getPathStatus(paths) {
|
|
65
|
+
return paths.map((p) => {
|
|
66
|
+
const exists = fs.existsSync(p);
|
|
67
|
+
let readable = false;
|
|
68
|
+
if (exists) {
|
|
69
|
+
try {
|
|
70
|
+
fs.accessSync(p, fs.constants.R_OK);
|
|
71
|
+
readable = true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
readable = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { path: p, exists, readable };
|
|
78
|
+
});
|
|
79
|
+
}
|
package/glama.json
ADDED
package/language.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodejs
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iflow-mcp/back1ply-agent-skill-loader",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"mcpName": "io.github.back1ply/agent-skill-loader",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "MCP Server to expose Claude Code Skills to agents",
|
|
7
|
+
"main": "build/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"iflow-mcp-back1ply-agent-skill-loader": "build/index.js"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"skills",
|
|
18
|
+
"claude"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"start": "node build/index.js",
|
|
24
|
+
"dev": "tsc --watch",
|
|
25
|
+
"test": "vitest"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.0.1",
|
|
29
|
+
"zod": "^3.23.8"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20.12.12",
|
|
33
|
+
"typescript": "^5.4.5",
|
|
34
|
+
"vitest": "^4.0.17"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package_name
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@iflow-mcp/back1ply-agent-skill-loader
|
package/push_info.json
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./build",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules"]
|
|
15
|
+
}
|