@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.
@@ -0,0 +1,4 @@
1
+ [2026-03-17 12:11:54] [✓] 步骤1: 获取项目 - Fork并克隆GitHub项目成功
2
+ [2026-03-17 12:12:12] [✓] 步骤2: 阅读代码 - 项目分析完成,确认为Node.js MCP服务端项目
3
+ [2026-03-17 12:13:04] [✓] 步骤3: 本地测试 - 成功检测到5个工具,本地测试通过
4
+ [2026-03-17 12:14:45] [✓] 步骤4: 推送iflow分支 - 成功推送到远程仓库
@@ -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
+ [![npm version](https://img.shields.io/npm/v/agent-skill-loader)](https://www.npmjs.com/package/agent-skill-loader)
4
+ [![MCP Registry](https://img.shields.io/badge/MCP-Registry-green)](https://registry.modelcontextprotocol.io)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.4-blue)](https://www.typescriptlang.org/)
8
+ [![MCP](https://img.shields.io/badge/MCP-Compatible-purple)](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
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": [
4
+ "back1ply"
5
+ ]
6
+ }
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
@@ -0,0 +1,5 @@
1
+ {
2
+ "push_platform": "github",
3
+ "fork_url": "https://github.com/iflow-mcp/back1ply-agent-skill-loader",
4
+ "fork_branch": "iflow"
5
+ }
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
+ }