@skilljack/mcp 0.7.0 → 0.7.1

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/dist/index.js CHANGED
@@ -6,58 +6,166 @@
6
6
  * Provides global skills with tools for progressive disclosure.
7
7
  *
8
8
  * Usage:
9
- * skilljack-mcp /path/to/skills [/path2 ...] # One or more directories
10
- * SKILLS_DIR=/path/to/skills skilljack-mcp # Single directory via env
11
- * SKILLS_DIR=/path1,/path2 skilljack-mcp # Multiple (comma-separated)
9
+ * skilljack-mcp /path/to/skills [/path2 ...] # Local directories
10
+ * skilljack-mcp --static /path/to/skills # Static mode (no file watching)
11
+ * skilljack-mcp github.com/owner/repo # GitHub repository
12
+ * skilljack-mcp /local github.com/owner/repo # Mixed local + GitHub
13
+ * SKILLS_DIR=/path,github.com/owner/repo skilljack-mcp # Via environment
14
+ * SKILLJACK_STATIC=true skilljack-mcp # Static mode via env
15
+ * (or configure local directories via the skill-config UI)
16
+ *
17
+ * Options:
18
+ * --static Freeze skills list at startup. Disables file watching and
19
+ * tools/prompts listChanged notifications. Resource subscriptions
20
+ * remain fully dynamic.
12
21
  */
13
22
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
24
  import chokidar from "chokidar";
16
25
  import * as fs from "node:fs";
17
26
  import * as path from "node:path";
18
- import { discoverSkills, createSkillMap } from "./skill-discovery.js";
27
+ import { fileURLToPath } from "node:url";
28
+ import { discoverSkills, createSkillMap, applyInvocationOverrides, DEFAULT_SKILL_SOURCE, BUNDLED_SKILL_SOURCE } from "./skill-discovery.js";
19
29
  import { registerSkillTool, getToolDescription } from "./skill-tool.js";
20
30
  import { registerSkillResources } from "./skill-resources.js";
31
+ import { registerSkillPrompts, refreshPrompts } from "./skill-prompts.js";
21
32
  import { createSubscriptionManager, registerSubscriptionHandlers, refreshSubscriptions, } from "./subscriptions.js";
33
+ import { getActiveDirectories, getSkillInvocationOverrides, getStaticModeFromConfig } from "./skill-config.js";
34
+ import { registerSkillConfigTool } from "./skill-config-tool.js";
35
+ import { registerSkillDisplayTool } from "./skill-display-tool.js";
36
+ import { isGitHubUrl, parseGitHubUrl, isRepoAllowed, getGitHubConfig, getRepoCachePath, } from "./github-config.js";
37
+ import { syncAllRepos } from "./github-sync.js";
38
+ import { createPollingManager } from "./github-polling.js";
22
39
  /**
23
40
  * Subdirectories to check for skills within the configured directory.
24
41
  */
25
42
  const SKILL_SUBDIRS = [".claude/skills", "skills"];
26
43
  /**
27
- * Separator for multiple paths in SKILLS_DIR environment variable.
28
- * Comma works cross-platform (not valid in file paths on any OS).
44
+ * Get the path to bundled skills directory.
45
+ * Resolves relative to the compiled module location.
29
46
  */
30
- const PATH_LIST_SEPARATOR = ",";
47
+ function getBundledSkillsDir() {
48
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
49
+ // From dist/index.js, go up one level to package root, then into skills/
50
+ return path.resolve(currentDir, "..", "skills");
51
+ }
31
52
  /**
32
- * Get the skills directories from command line args and/or environment.
33
- * Returns deduplicated, resolved paths.
53
+ * Build a directory-to-source map from current configuration.
54
+ * Maps both main directories and their standard subdirectories.
55
+ *
56
+ * @param localDirs - Local skill directories
57
+ * @param githubSpecs - GitHub repository specifications
58
+ * @param cacheDir - GitHub cache directory path
59
+ * @param bundledDir - Optional bundled skills directory
34
60
  */
35
- function getSkillsDirs() {
36
- const dirs = [];
37
- // Collect all non-flag command-line arguments (comma-separated supported)
38
- const args = process.argv.slice(2);
39
- for (const arg of args) {
40
- if (!arg.startsWith("-")) {
41
- const paths = arg
42
- .split(PATH_LIST_SEPARATOR)
43
- .map((p) => p.trim())
44
- .filter((p) => p.length > 0)
45
- .map((p) => path.resolve(p));
46
- dirs.push(...paths);
61
+ function buildDirectorySourceMap(localDirs, githubSpecs, cacheDir, bundledDir) {
62
+ const map = {};
63
+ // Map local directories
64
+ for (const dir of localDirs) {
65
+ const source = {
66
+ type: "local",
67
+ displayName: "Local",
68
+ };
69
+ map[dir] = source;
70
+ // Also map standard subdirectories
71
+ for (const subdir of SKILL_SUBDIRS) {
72
+ map[path.join(dir, subdir)] = source;
73
+ }
74
+ }
75
+ // Map GitHub cache directories
76
+ for (const spec of githubSpecs) {
77
+ const cachePath = getRepoCachePath(spec, cacheDir);
78
+ const source = {
79
+ type: "github",
80
+ displayName: `${spec.owner}/${spec.repo}`,
81
+ owner: spec.owner,
82
+ repo: spec.repo,
83
+ };
84
+ map[cachePath] = source;
85
+ // Also map standard subdirectories
86
+ for (const subdir of SKILL_SUBDIRS) {
87
+ map[path.join(cachePath, subdir)] = source;
47
88
  }
48
89
  }
49
- // Also check environment variable (comma-separated supported)
50
- const envDir = process.env.SKILLS_DIR;
51
- if (envDir) {
52
- const envPaths = envDir
53
- .split(PATH_LIST_SEPARATOR)
54
- .map((p) => p.trim())
55
- .filter((p) => p.length > 0)
56
- .map((p) => path.resolve(p));
57
- dirs.push(...envPaths);
90
+ // Map bundled skills directory
91
+ if (bundledDir) {
92
+ map[bundledDir] = BUNDLED_SKILL_SOURCE;
93
+ // Also map standard subdirectories
94
+ for (const subdir of SKILL_SUBDIRS) {
95
+ map[path.join(bundledDir, subdir)] = BUNDLED_SKILL_SOURCE;
96
+ }
58
97
  }
59
- // Deduplicate by resolved path
60
- return [...new Set(dirs)];
98
+ return map;
99
+ }
100
+ /**
101
+ * Current skill directories (mutable to support UI-driven changes).
102
+ * This includes both local directories and GitHub cache directories.
103
+ */
104
+ let currentSkillsDirs = [];
105
+ /**
106
+ * GitHub specs that are currently being polled.
107
+ */
108
+ let currentGithubSpecs = [];
109
+ /**
110
+ * Current directory-to-source map for skill discovery.
111
+ * Maps directory paths to their source info (local or GitHub).
112
+ */
113
+ let currentSourceMap = {};
114
+ /**
115
+ * Check if static mode is enabled.
116
+ * Static mode freezes the skills list at startup - no file watching,
117
+ * no listChanged notifications for tools/prompts.
118
+ * Priority: CLI flag > env var > config file
119
+ */
120
+ function getStaticMode() {
121
+ // Check CLI flag (highest priority)
122
+ const args = process.argv.slice(2);
123
+ if (args.includes("--static")) {
124
+ return true;
125
+ }
126
+ // Check environment variable
127
+ const envValue = process.env.SKILLJACK_STATIC?.toLowerCase();
128
+ if (envValue === "true" || envValue === "1" || envValue === "yes") {
129
+ return true;
130
+ }
131
+ // Check config file (lowest priority)
132
+ return getStaticModeFromConfig();
133
+ }
134
+ /**
135
+ * Classify paths as local directories or GitHub repositories.
136
+ * GitHub URLs are detected by checking for "github.com" in the path.
137
+ */
138
+ function classifyPaths(paths) {
139
+ const localDirs = [];
140
+ const githubSpecs = [];
141
+ for (const p of paths) {
142
+ if (isGitHubUrl(p)) {
143
+ try {
144
+ const spec = parseGitHubUrl(p);
145
+ githubSpecs.push(spec);
146
+ }
147
+ catch (error) {
148
+ console.error(`Warning: Invalid GitHub URL "${p}": ${error}`);
149
+ }
150
+ }
151
+ else {
152
+ // Local directory - resolve the path
153
+ localDirs.push(path.resolve(p));
154
+ }
155
+ }
156
+ // Deduplicate local dirs
157
+ const uniqueLocalDirs = [...new Set(localDirs)];
158
+ // Deduplicate GitHub specs by owner/repo
159
+ const seenRepos = new Set();
160
+ const uniqueGithubSpecs = githubSpecs.filter((spec) => {
161
+ const key = `${spec.owner}/${spec.repo}`;
162
+ if (seenRepos.has(key)) {
163
+ return false;
164
+ }
165
+ seenRepos.add(key);
166
+ return true;
167
+ });
168
+ return { localDirs: uniqueLocalDirs, githubSpecs: uniqueGithubSpecs };
61
169
  }
62
170
  /**
63
171
  * Shared state for skill management.
@@ -70,8 +178,11 @@ const skillState = {
70
178
  * Discover skills from multiple configured directories.
71
179
  * Each directory is checked along with its standard subdirectories.
72
180
  * Handles duplicate skill names by keeping first occurrence.
181
+ *
182
+ * @param skillsDirs - The skill directories to scan
183
+ * @param sourceMap - Map from directory paths to source info
73
184
  */
74
- function discoverSkillsFromDirs(skillsDirs) {
185
+ function discoverSkillsFromDirs(skillsDirs, sourceMap) {
75
186
  const allSkills = [];
76
187
  const seenNames = new Map(); // name -> source directory
77
188
  for (const skillsDir of skillsDirs) {
@@ -80,13 +191,17 @@ function discoverSkillsFromDirs(skillsDirs) {
80
191
  continue;
81
192
  }
82
193
  console.error(`Scanning skills directory: ${skillsDir}`);
194
+ // Get source info for this directory (default to local if not in map)
195
+ const dirSource = sourceMap[skillsDir] || DEFAULT_SKILL_SOURCE;
83
196
  // Check if the directory itself contains skills
84
- const dirSkills = discoverSkills(skillsDir);
197
+ const dirSkills = discoverSkills(skillsDir, dirSource);
85
198
  // Also check standard subdirectories
86
199
  for (const subdir of SKILL_SUBDIRS) {
87
200
  const subPath = path.join(skillsDir, subdir);
88
201
  if (fs.existsSync(subPath)) {
89
- dirSkills.push(...discoverSkills(subPath));
202
+ // Use subpath source if available, otherwise inherit from parent
203
+ const subSource = sourceMap[subPath] || dirSource;
204
+ dirSkills.push(...discoverSkills(subPath, subSource));
90
205
  }
91
206
  }
92
207
  // Add skills, checking for duplicates
@@ -114,13 +229,17 @@ const SKILL_REFRESH_DEBOUNCE_MS = 500;
114
229
  * @param skillsDirs - The configured skill directories
115
230
  * @param server - The MCP server instance
116
231
  * @param skillTool - The registered skill tool to update
232
+ * @param promptRegistry - For refreshing skill prompts
117
233
  * @param subscriptionManager - For refreshing resource subscriptions
118
234
  */
119
- function refreshSkills(skillsDirs, server, skillTool, subscriptionManager) {
235
+ function refreshSkills(skillsDirs, server, skillTool, promptRegistry, subscriptionManager) {
120
236
  console.error("Refreshing skills...");
121
- // Re-discover all skills
122
- const skills = discoverSkillsFromDirs(skillsDirs);
237
+ // Re-discover all skills using current source map
238
+ let skills = discoverSkillsFromDirs(skillsDirs, currentSourceMap);
123
239
  const oldCount = skillState.skillMap.size;
240
+ // Apply invocation overrides from config
241
+ const overrides = getSkillInvocationOverrides();
242
+ skills = applyInvocationOverrides(skills, overrides);
124
243
  // Update shared state
125
244
  skillState.skillMap = createSkillMap(skills);
126
245
  console.error(`Skills refreshed: ${oldCount} -> ${skills.length} skill(s)`);
@@ -128,6 +247,8 @@ function refreshSkills(skillsDirs, server, skillTool, subscriptionManager) {
128
247
  skillTool.update({
129
248
  description: getToolDescription(skillState),
130
249
  });
250
+ // Refresh prompts to match new skill state
251
+ refreshPrompts(server, skillState, promptRegistry);
131
252
  // Refresh resource subscriptions to match new skill state
132
253
  refreshSubscriptions(subscriptionManager, skillState, (uri) => {
133
254
  server.server.notification({
@@ -148,9 +269,10 @@ function refreshSkills(skillsDirs, server, skillTool, subscriptionManager) {
148
269
  * @param skillsDirs - The configured skill directories
149
270
  * @param server - The MCP server instance
150
271
  * @param skillTool - The registered skill tool to update
272
+ * @param promptRegistry - For refreshing skill prompts
151
273
  * @param subscriptionManager - For refreshing subscriptions
152
274
  */
153
- function watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager) {
275
+ function watchSkillDirectories(skillsDirs, server, skillTool, promptRegistry, subscriptionManager) {
154
276
  let refreshTimeout = null;
155
277
  const debouncedRefresh = () => {
156
278
  if (refreshTimeout) {
@@ -158,7 +280,7 @@ function watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManage
158
280
  }
159
281
  refreshTimeout = setTimeout(() => {
160
282
  refreshTimeout = null;
161
- refreshSkills(skillsDirs, server, skillTool, subscriptionManager);
283
+ refreshSkills(skillsDirs, server, skillTool, promptRegistry, subscriptionManager);
162
284
  }, SKILL_REFRESH_DEBOUNCE_MS);
163
285
  };
164
286
  // Build list of paths to watch
@@ -230,36 +352,170 @@ function watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManage
230
352
  */
231
353
  const subscriptionManager = createSubscriptionManager();
232
354
  async function main() {
233
- const skillsDirs = getSkillsDirs();
234
- if (skillsDirs.length === 0) {
235
- console.error("No skills directory configured.");
236
- console.error("Usage: skilljack-mcp /path/to/skills [/path/to/more/skills ...]");
237
- console.error(" or: SKILLS_DIR=/path/to/skills skilljack-mcp");
238
- console.error(" or: SKILLS_DIR=/path1,/path2 skilljack-mcp");
239
- process.exit(1);
355
+ // Check if static mode is enabled
356
+ const isStatic = getStaticMode();
357
+ // Get skill directories from CLI args, env var, or config file
358
+ // This returns paths that may include GitHub URLs
359
+ const allPaths = getActiveDirectories();
360
+ // Classify paths as local or GitHub
361
+ const { localDirs, githubSpecs } = classifyPaths(allPaths);
362
+ // Get GitHub configuration
363
+ const githubConfig = getGitHubConfig();
364
+ // Sync GitHub repositories
365
+ let githubDirs = [];
366
+ if (githubSpecs.length > 0) {
367
+ console.error(`GitHub repos: ${githubSpecs.map((s) => `${s.owner}/${s.repo}`).join(", ")}`);
368
+ // Filter by allowlist
369
+ for (const spec of githubSpecs) {
370
+ if (!isRepoAllowed(spec, githubConfig)) {
371
+ console.error(`Blocked: ${spec.owner}/${spec.repo} not in allowed orgs/users. ` +
372
+ `Set GITHUB_ALLOWED_ORGS or GITHUB_ALLOWED_USERS to permit.`);
373
+ continue;
374
+ }
375
+ currentGithubSpecs.push(spec);
376
+ }
377
+ if (currentGithubSpecs.length > 0) {
378
+ console.error(`Syncing ${currentGithubSpecs.length} GitHub repo(s)...`);
379
+ const syncOptions = {
380
+ cacheDir: githubConfig.cacheDir,
381
+ token: githubConfig.token,
382
+ shallowClone: true,
383
+ };
384
+ const results = await syncAllRepos(currentGithubSpecs, syncOptions);
385
+ // Collect successful sync paths
386
+ for (const result of results) {
387
+ if (!result.error) {
388
+ githubDirs.push(result.localPath);
389
+ }
390
+ }
391
+ console.error(`Successfully synced ${githubDirs.length}/${currentGithubSpecs.length} repo(s)`);
392
+ }
393
+ }
394
+ // Get bundled skills directory (ships with the package)
395
+ const bundledSkillsDir = getBundledSkillsDir();
396
+ const hasBundledSkills = fs.existsSync(bundledSkillsDir);
397
+ // Combine all skill directories
398
+ // User directories come first so they can override bundled skills (first-wins deduplication)
399
+ currentSkillsDirs = [...localDirs, ...githubDirs, ...(hasBundledSkills ? [bundledSkillsDir] : [])];
400
+ // Build source map for skill discovery
401
+ currentSourceMap = buildDirectorySourceMap(localDirs, currentGithubSpecs, githubConfig.cacheDir, hasBundledSkills ? bundledSkillsDir : undefined);
402
+ // Log configured directories
403
+ if (localDirs.length > 0) {
404
+ console.error(`Local directories: ${localDirs.join(", ")}`);
405
+ }
406
+ if (githubDirs.length > 0) {
407
+ console.error(`GitHub cache directories: ${githubDirs.join(", ")}`);
408
+ }
409
+ if (hasBundledSkills) {
410
+ console.error(`Bundled skills: ${bundledSkillsDir}`);
411
+ }
412
+ if (isStatic) {
413
+ console.error("Static mode enabled - skills list frozen at startup");
240
414
  }
241
- console.error(`Skills directories: ${skillsDirs.join(", ")}`);
242
415
  // Discover skills at startup
243
- const skills = discoverSkillsFromDirs(skillsDirs);
416
+ let skills = discoverSkillsFromDirs(currentSkillsDirs, currentSourceMap);
417
+ // Apply invocation overrides from config
418
+ const overrides = getSkillInvocationOverrides();
419
+ skills = applyInvocationOverrides(skills, overrides);
244
420
  skillState.skillMap = createSkillMap(skills);
245
421
  console.error(`Discovered ${skills.length} skill(s)`);
246
422
  // Create the MCP server
423
+ // In static mode, disable listChanged for tools/prompts (skills list is frozen)
424
+ // Resource subscriptions remain dynamic for individual skill file watching
247
425
  const server = new McpServer({
248
426
  name: "skilljack-mcp",
249
427
  version: "1.0.0",
250
428
  }, {
251
429
  capabilities: {
252
- tools: { listChanged: true },
430
+ tools: { listChanged: !isStatic },
253
431
  resources: { subscribe: true, listChanged: true },
432
+ prompts: { listChanged: !isStatic },
254
433
  },
255
434
  });
256
- // Register tools and resources
435
+ // Register tools, resources, and prompts
257
436
  const skillTool = registerSkillTool(server, skillState);
258
437
  registerSkillResources(server, skillState);
438
+ const promptRegistry = registerSkillPrompts(server, skillState);
259
439
  // Register subscription handlers for resource file watching
260
440
  registerSubscriptionHandlers(server, skillState, subscriptionManager);
261
- // Set up file watchers for skill directory changes
262
- watchSkillDirectories(skillsDirs, server, skillTool, subscriptionManager);
441
+ // Register skill-config tool for UI-based directory configuration
442
+ // Skip in static mode since skills list is frozen
443
+ if (!isStatic) {
444
+ registerSkillConfigTool(server, skillState, async () => {
445
+ // Callback when directories or GitHub settings change via UI
446
+ // Reload directories from config and refresh skills
447
+ const newPaths = getActiveDirectories();
448
+ const { localDirs: newLocalDirs, githubSpecs: newGithubSpecs } = classifyPaths(newPaths);
449
+ // Get fresh GitHub config (in case allowed orgs/users changed)
450
+ const freshGithubConfig = getGitHubConfig();
451
+ // Filter GitHub specs by allowlist and sync
452
+ const allowedGithubSpecs = [];
453
+ for (const spec of newGithubSpecs) {
454
+ if (isRepoAllowed(spec, freshGithubConfig)) {
455
+ allowedGithubSpecs.push(spec);
456
+ }
457
+ else {
458
+ console.error(`Blocked: ${spec.owner}/${spec.repo} not in allowed orgs/users.`);
459
+ }
460
+ }
461
+ // Sync any GitHub repos
462
+ let newGithubDirs = [];
463
+ if (allowedGithubSpecs.length > 0) {
464
+ console.error(`Syncing ${allowedGithubSpecs.length} GitHub repo(s)...`);
465
+ const syncOptions = {
466
+ cacheDir: freshGithubConfig.cacheDir,
467
+ token: freshGithubConfig.token,
468
+ shallowClone: true,
469
+ };
470
+ const results = await syncAllRepos(allowedGithubSpecs, syncOptions);
471
+ for (const result of results) {
472
+ if (!result.error) {
473
+ newGithubDirs.push(result.localPath);
474
+ }
475
+ }
476
+ console.error(`Successfully synced ${newGithubDirs.length}/${allowedGithubSpecs.length} repo(s)`);
477
+ }
478
+ // Update current state
479
+ currentGithubSpecs = allowedGithubSpecs;
480
+ githubDirs = newGithubDirs;
481
+ // Include bundled skills (last, so user skills take precedence)
482
+ currentSkillsDirs = [...newLocalDirs, ...newGithubDirs, ...(hasBundledSkills ? [bundledSkillsDir] : [])];
483
+ currentSourceMap = buildDirectorySourceMap(newLocalDirs, allowedGithubSpecs, freshGithubConfig.cacheDir, hasBundledSkills ? bundledSkillsDir : undefined);
484
+ console.error(`Config changed via UI. Directories: ${currentSkillsDirs.join(", ") || "(none)"}`);
485
+ refreshSkills(currentSkillsDirs, server, skillTool, promptRegistry, subscriptionManager);
486
+ });
487
+ // Register skill-display tool for UI-based invocation settings
488
+ registerSkillDisplayTool(server, skillState, () => {
489
+ // Callback when invocation settings change via UI
490
+ // Refresh skills to apply new overrides
491
+ console.error("Invocation settings changed via UI. Refreshing skills...");
492
+ refreshSkills(currentSkillsDirs, server, skillTool, promptRegistry, subscriptionManager);
493
+ });
494
+ }
495
+ // Set up file watchers for skill directory changes (skip in static mode)
496
+ if (!isStatic && currentSkillsDirs.length > 0) {
497
+ watchSkillDirectories(currentSkillsDirs, server, skillTool, promptRegistry, subscriptionManager);
498
+ }
499
+ // Set up GitHub polling for updates (skip in static mode)
500
+ let pollingManager = null;
501
+ if (!isStatic && currentGithubSpecs.length > 0 && githubConfig.pollIntervalMs > 0) {
502
+ const syncOptions = {
503
+ cacheDir: githubConfig.cacheDir,
504
+ token: githubConfig.token,
505
+ shallowClone: true,
506
+ };
507
+ pollingManager = createPollingManager(currentGithubSpecs, syncOptions, {
508
+ intervalMs: githubConfig.pollIntervalMs,
509
+ onUpdate: (spec, result) => {
510
+ console.error(`GitHub update detected for ${spec.owner}/${spec.repo}`);
511
+ refreshSkills(currentSkillsDirs, server, skillTool, promptRegistry, subscriptionManager);
512
+ },
513
+ onError: (spec, error) => {
514
+ console.error(`GitHub polling error for ${spec.owner}/${spec.repo}: ${error.message}`);
515
+ },
516
+ });
517
+ pollingManager.start();
518
+ }
263
519
  // Connect via stdio transport
264
520
  const transport = new StdioServerTransport();
265
521
  await server.connect(transport);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * MCP App tool registration for skill directory configuration.
3
+ *
4
+ * Registers:
5
+ * - skill-config: Opens the configuration UI
6
+ * - skill-config-add-directory: Adds a directory (UI-only)
7
+ * - skill-config-remove-directory: Removes a directory (UI-only)
8
+ */
9
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { SkillState } from "./skill-tool.js";
11
+ /**
12
+ * Callback type for when directories or GitHub settings change.
13
+ */
14
+ export type OnDirectoriesChangedCallback = () => void | Promise<void>;
15
+ /**
16
+ * Register skill-config MCP App tools and resource.
17
+ *
18
+ * @param server - The MCP server instance
19
+ * @param skillState - Shared skill state for getting skill counts
20
+ * @param onDirectoriesChanged - Callback when directories are added/removed
21
+ */
22
+ export declare function registerSkillConfigTool(server: McpServer, skillState: SkillState, onDirectoriesChanged: OnDirectoriesChangedCallback): void;