@skilljack/mcp
Advanced tools
+5
-12
@@ -21,3 +21,2 @@ #!/usr/bin/env node | ||
| import { registerSkillResources } from "./skill-resources.js"; | ||
| import { registerSkillPrompts, refreshPrompts } from "./skill-prompts.js"; | ||
| import { createSubscriptionManager, registerSubscriptionHandlers, refreshSubscriptions, } from "./subscriptions.js"; | ||
@@ -119,6 +118,5 @@ /** | ||
| * @param skillTool - The registered skill tool to update | ||
| * @param promptRegistry - For refreshing skill prompts | ||
| * @param subscriptionManager - For refreshing resource subscriptions | ||
| */ | ||
| function refreshSkills(skillsDirs, server, skillTool, promptRegistry, subscriptionManager) { | ||
| function refreshSkills(skillsDirs, server, skillTool, subscriptionManager) { | ||
| console.error("Refreshing skills..."); | ||
@@ -135,4 +133,2 @@ // Re-discover all skills | ||
| }); | ||
| // Refresh prompts to match new skill state | ||
| refreshPrompts(server, skillState, promptRegistry); | ||
| // Refresh resource subscriptions to match new skill state | ||
@@ -158,6 +154,5 @@ refreshSubscriptions(subscriptionManager, skillState, (uri) => { | ||
| * @param skillTool - The registered skill tool to update | ||
| * @param promptRegistry - For refreshing skill prompts | ||
| * @param subscriptionManager - For refreshing subscriptions | ||
| */ | ||
| function watchSkillDirectories(skillsDirs, server, skillTool, promptRegistry, subscriptionManager) { | ||
| function watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager) { | ||
| let refreshTimeout = null; | ||
@@ -170,3 +165,3 @@ const debouncedRefresh = () => { | ||
| refreshTimeout = null; | ||
| refreshSkills(skillsDirs, server, skillTool, promptRegistry, subscriptionManager); | ||
| refreshSkills(skillsDirs, server, skillTool, subscriptionManager); | ||
| }, SKILL_REFRESH_DEBOUNCE_MS); | ||
@@ -264,13 +259,11 @@ }; | ||
| resources: { subscribe: true, listChanged: true }, | ||
| prompts: { listChanged: true }, | ||
| }, | ||
| }); | ||
| // Register tools, resources, and prompts | ||
| // Register tools and resources | ||
| const skillTool = registerSkillTool(server, skillState); | ||
| registerSkillResources(server, skillState); | ||
| const promptRegistry = registerSkillPrompts(server, skillState); | ||
| // Register subscription handlers for resource file watching | ||
| registerSubscriptionHandlers(server, skillState, subscriptionManager); | ||
| // Set up file watchers for skill directory changes | ||
| watchSkillDirectories(skillsDirs, server, skillTool, promptRegistry, subscriptionManager); | ||
| watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager); | ||
| // Connect via stdio transport | ||
@@ -277,0 +270,0 @@ const transport = new StdioServerTransport(); |
@@ -11,8 +11,5 @@ /** | ||
| * URI Scheme: | ||
| * skill://{skillName} -> SKILL.md content (template) | ||
| * skill://{skillName}/ -> Collection: all files in skill directory | ||
| * | ||
| * Note: Individual file URIs (skill://{skillName}/{path}) are not listed | ||
| * as resources to reduce noise. Use the skill-resource tool to fetch | ||
| * specific files on demand. | ||
| * skill://{skillName} -> SKILL.md content (template) | ||
| * skill://{skillName}/ -> Collection: all files in skill directory | ||
| * skill://{skillName}/{path} -> File within skill directory (template) | ||
| */ | ||
@@ -19,0 +16,0 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
+94
-10
@@ -11,8 +11,5 @@ /** | ||
| * URI Scheme: | ||
| * skill://{skillName} -> SKILL.md content (template) | ||
| * skill://{skillName}/ -> Collection: all files in skill directory | ||
| * | ||
| * Note: Individual file URIs (skill://{skillName}/{path}) are not listed | ||
| * as resources to reduce noise. Use the skill-resource tool to fetch | ||
| * specific files on demand. | ||
| * skill://{skillName} -> SKILL.md content (template) | ||
| * skill://{skillName}/ -> Collection: all files in skill directory | ||
| * skill://{skillName}/{path} -> File within skill directory (template) | ||
| */ | ||
@@ -57,7 +54,6 @@ import * as fs from "node:fs"; | ||
| registerSkillTemplate(server, skillState); | ||
| // Register collection resource for skill directories | ||
| // Register collection resource for skill directories (must be before file template) | ||
| registerSkillDirectoryCollection(server, skillState); | ||
| // Note: Individual file resources (skill://{name}/{path}) are intentionally | ||
| // not registered to reduce noise. Use the skill-resource tool to fetch | ||
| // specific files on demand. | ||
| // Register resource template for skill files | ||
| registerSkillFileTemplate(server, skillState); | ||
| } | ||
@@ -205,1 +201,89 @@ /** | ||
| } | ||
| /** | ||
| * Register the resource template for accessing files within skills. | ||
| * | ||
| * URI Pattern: skill://{skillName}/{filePath} | ||
| */ | ||
| function registerSkillFileTemplate(server, skillState) { | ||
| server.registerResource("Skill File", new ResourceTemplate("skill://{skillName}/{+filePath}", { | ||
| list: async () => { | ||
| // Return all listable skill files (dynamic based on current skillMap) | ||
| const resources = []; | ||
| for (const [name, skill] of skillState.skillMap) { | ||
| const skillDir = path.dirname(skill.path); | ||
| const files = listSkillFiles(skillDir); | ||
| for (const file of files) { | ||
| const uri = `skill://${encodeURIComponent(name)}/${file}`; | ||
| resources.push({ | ||
| uri, | ||
| name: `${name}/${file}`, | ||
| mimeType: getMimeType(file), | ||
| }); | ||
| } | ||
| } | ||
| return { resources }; | ||
| }, | ||
| complete: { | ||
| skillName: (value) => { | ||
| const names = Array.from(skillState.skillMap.keys()); | ||
| return names.filter((name) => name.toLowerCase().startsWith(value.toLowerCase())); | ||
| }, | ||
| }, | ||
| }), { | ||
| mimeType: "text/plain", | ||
| description: "Files within a skill directory (scripts, snippets, assets, etc.)", | ||
| }, async (resourceUri, variables) => { | ||
| // Extract skill name and file path from URI | ||
| const uriStr = resourceUri.toString(); | ||
| const match = uriStr.match(/^skill:\/\/([^/]+)\/(.+)$/); | ||
| if (!match) { | ||
| throw new Error(`Invalid skill file URI: ${uriStr}`); | ||
| } | ||
| const skillName = decodeURIComponent(match[1]); | ||
| const filePath = match[2]; | ||
| const skill = skillState.skillMap.get(skillName); | ||
| if (!skill) { | ||
| const available = Array.from(skillState.skillMap.keys()).join(", "); | ||
| throw new Error(`Skill "${skillName}" not found. Available: ${available || "none"}`); | ||
| } | ||
| const skillDir = path.dirname(skill.path); | ||
| const fullPath = path.resolve(skillDir, filePath); | ||
| // Security: Validate path is within skill directory | ||
| if (!isPathWithinBase(fullPath, skillDir)) { | ||
| throw new Error(`Path "${filePath}" is outside the skill directory`); | ||
| } | ||
| // Check file exists | ||
| if (!fs.existsSync(fullPath)) { | ||
| const files = listSkillFiles(skillDir).slice(0, 10); | ||
| throw new Error(`File "${filePath}" not found in skill "${skillName}". ` + | ||
| `Available: ${files.join(", ")}${files.length >= 10 ? "..." : ""}`); | ||
| } | ||
| const stat = fs.statSync(fullPath); | ||
| // Reject symlinks | ||
| if (stat.isSymbolicLink()) { | ||
| throw new Error(`Cannot read symlink "${filePath}"`); | ||
| } | ||
| // Reject directories | ||
| if (stat.isDirectory()) { | ||
| const files = listSkillFiles(skillDir, filePath); | ||
| throw new Error(`"${filePath}" is a directory. Files within: ${files.join(", ")}`); | ||
| } | ||
| // Check file size | ||
| if (stat.size > MAX_FILE_SIZE) { | ||
| const sizeMB = (stat.size / 1024 / 1024).toFixed(2); | ||
| throw new Error(`File too large (${sizeMB}MB). Maximum: 10MB`); | ||
| } | ||
| // Read and return content | ||
| const content = fs.readFileSync(fullPath, "utf-8"); | ||
| const mimeType = getMimeType(fullPath); | ||
| return { | ||
| contents: [ | ||
| { | ||
| uri: uriStr, | ||
| mimeType, | ||
| text: content, | ||
| }, | ||
| ], | ||
| }; | ||
| }); | ||
| } |
@@ -36,3 +36,7 @@ /** | ||
| const usage = "Load a skill's full instructions. Returns the complete SKILL.md content " + | ||
| "with step-by-step guidance, examples, and file references to follow.\n\n"; | ||
| "with step-by-step guidance, examples, and file references to follow.\n\n" + | ||
| "IMPORTANT: When a skill is relevant to the user's task, you must invoke this tool " + | ||
| "IMMEDIATELY as your first action. NEVER just announce or mention a skill without " + | ||
| "actually calling this tool. This is a BLOCKING REQUIREMENT: invoke this tool BEFORE " + | ||
| "generating any other response about the task.\n\n"; | ||
| const skills = Array.from(skillState.skillMap.values()); | ||
@@ -39,0 +43,0 @@ return usage + generateInstructions(skills); |
@@ -12,3 +12,3 @@ /** | ||
| * - skill://{name}/ → Watch entire skill directory (directory collection) | ||
| * - skill://{name}/{path} → Watch specific file (subscribable but not listed as resource) | ||
| * - skill://{name}/{path} → Watch specific file | ||
| */ | ||
@@ -15,0 +15,0 @@ import { FSWatcher } from "chokidar"; |
@@ -12,3 +12,3 @@ /** | ||
| * - skill://{name}/ → Watch entire skill directory (directory collection) | ||
| * - skill://{name}/{path} → Watch specific file (subscribable but not listed as resource) | ||
| * - skill://{name}/{path} → Watch specific file | ||
| */ | ||
@@ -15,0 +15,0 @@ import chokidar from "chokidar"; |
+11
-2
| { | ||
| "name": "@skilljack/mcp", | ||
| "version": "0.4.0", | ||
| "version": "0.5.0", | ||
| "description": "MCP server that discovers and serves Agent Skills. I know kung fu.", | ||
@@ -41,3 +41,11 @@ "type": "module", | ||
| "dev": "tsx watch src/index.ts", | ||
| "inspector": "npx @modelcontextprotocol/inspector@latest node dist/index.js" | ||
| "inspector": "npx @modelcontextprotocol/inspector@latest node dist/index.js", | ||
| "eval": "tsx evals/eval.ts", | ||
| "eval:greeting": "tsx evals/eval.ts --task=greeting", | ||
| "eval:code-style": "tsx evals/eval.ts --task=code-style", | ||
| "eval:template": "tsx evals/eval.ts --task=template-generator", | ||
| "eval:xlsx-openpyxl": "tsx evals/eval.ts --task=xlsx-openpyxl", | ||
| "eval:xlsx-formulas": "tsx evals/eval.ts --task=xlsx-formulas", | ||
| "eval:xlsx-financial": "tsx evals/eval.ts --task=xlsx-financial", | ||
| "eval:xlsx-verify": "tsx evals/eval.ts --task=xlsx-verify" | ||
| }, | ||
@@ -51,2 +59,3 @@ "dependencies": { | ||
| "devDependencies": { | ||
| "@anthropic-ai/claude-agent-sdk": "^0.1.42", | ||
| "@types/node": "^22.10.0", | ||
@@ -53,0 +62,0 @@ "tsx": "^4.19.2", |
87364
4.51%1829
4.45%4
33.33%