@skilljack/mcp 0.6.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.
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Configuration management for skill directories.
3
+ *
4
+ * Handles loading/saving skill directory configuration from:
5
+ * 1. CLI args (highest priority)
6
+ * 2. SKILLS_DIR environment variable
7
+ * 3. Config file (~/.skilljack/config.json)
8
+ *
9
+ * Supports both local directories and GitHub repository URLs.
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import * as os from "node:os";
14
+ import { isGitHubUrl, parseGitHubUrl, isRepoAllowed, getGitHubConfig } from "./github-config.js";
15
+ /**
16
+ * Check if a path is valid (exists for local, always valid for GitHub).
17
+ */
18
+ function isValidPath(p) {
19
+ if (isGitHubUrl(p)) {
20
+ return true; // GitHub URLs are validated during sync
21
+ }
22
+ return fs.existsSync(p);
23
+ }
24
+ /**
25
+ * Get the source type for a path.
26
+ */
27
+ function getSourceType(p) {
28
+ return isGitHubUrl(p) ? "github" : "local";
29
+ }
30
+ /**
31
+ * Check if a path is allowed (local paths are always allowed,
32
+ * GitHub URLs must have their org/user in the allowlist).
33
+ */
34
+ function isPathAllowed(p) {
35
+ if (!isGitHubUrl(p)) {
36
+ return true; // Local paths are always allowed
37
+ }
38
+ try {
39
+ const spec = parseGitHubUrl(p);
40
+ const config = getGitHubConfig();
41
+ return isRepoAllowed(spec, config);
42
+ }
43
+ catch {
44
+ return false; // Invalid GitHub URL
45
+ }
46
+ }
47
+ /**
48
+ * Separator for multiple paths in SKILLS_DIR environment variable.
49
+ */
50
+ const PATH_LIST_SEPARATOR = ",";
51
+ /**
52
+ * Get the platform-appropriate config directory path.
53
+ * Returns ~/.skilljack on Unix, %USERPROFILE%\.skilljack on Windows.
54
+ */
55
+ export function getConfigDir() {
56
+ return path.join(os.homedir(), ".skilljack");
57
+ }
58
+ /**
59
+ * Get the full path to the config file.
60
+ */
61
+ export function getConfigPath() {
62
+ return path.join(getConfigDir(), "config.json");
63
+ }
64
+ /**
65
+ * Ensure the config directory exists.
66
+ */
67
+ function ensureConfigDir() {
68
+ const configDir = getConfigDir();
69
+ if (!fs.existsSync(configDir)) {
70
+ fs.mkdirSync(configDir, { recursive: true });
71
+ }
72
+ }
73
+ /**
74
+ * Load config from the config file.
75
+ * Returns empty config if file doesn't exist.
76
+ */
77
+ export function loadConfigFile() {
78
+ const configPath = getConfigPath();
79
+ if (!fs.existsSync(configPath)) {
80
+ return { skillDirectories: [], skillInvocationOverrides: {} };
81
+ }
82
+ try {
83
+ const content = fs.readFileSync(configPath, "utf-8");
84
+ const parsed = JSON.parse(content);
85
+ // Validate and normalize directories (handle both local paths and GitHub URLs)
86
+ const skillDirectories = Array.isArray(parsed.skillDirectories)
87
+ ? parsed.skillDirectories
88
+ .filter((p) => typeof p === "string")
89
+ .map((p) => isGitHubUrl(p) ? p : path.resolve(p))
90
+ : [];
91
+ // Validate and normalize invocation overrides
92
+ const skillInvocationOverrides = {};
93
+ if (parsed.skillInvocationOverrides && typeof parsed.skillInvocationOverrides === "object") {
94
+ for (const [name, override] of Object.entries(parsed.skillInvocationOverrides)) {
95
+ if (typeof override === "object" && override !== null) {
96
+ const validOverride = {};
97
+ const o = override;
98
+ if (typeof o.assistant === "boolean")
99
+ validOverride.assistant = o.assistant;
100
+ if (typeof o.user === "boolean")
101
+ validOverride.user = o.user;
102
+ if (Object.keys(validOverride).length > 0) {
103
+ skillInvocationOverrides[name] = validOverride;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return {
109
+ skillDirectories,
110
+ skillInvocationOverrides,
111
+ staticMode: parsed.staticMode === true,
112
+ githubAllowedOrgs: Array.isArray(parsed.githubAllowedOrgs) ? parsed.githubAllowedOrgs : [],
113
+ githubAllowedUsers: Array.isArray(parsed.githubAllowedUsers) ? parsed.githubAllowedUsers : [],
114
+ };
115
+ }
116
+ catch (error) {
117
+ console.error(`Warning: Failed to parse config file: ${error}`);
118
+ return { skillDirectories: [], skillInvocationOverrides: {} };
119
+ }
120
+ }
121
+ /**
122
+ * Save config to the config file.
123
+ */
124
+ export function saveConfigFile(config) {
125
+ ensureConfigDir();
126
+ const configPath = getConfigPath();
127
+ try {
128
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
129
+ }
130
+ catch (error) {
131
+ throw new Error(`Failed to save config file: ${error}`);
132
+ }
133
+ }
134
+ /**
135
+ * Parse CLI arguments for skill directories.
136
+ * Returns resolved absolute paths for local dirs, unchanged for GitHub URLs.
137
+ */
138
+ export function parseCLIArgs() {
139
+ const dirs = [];
140
+ const args = process.argv.slice(2);
141
+ for (const arg of args) {
142
+ if (!arg.startsWith("-")) {
143
+ const paths = arg
144
+ .split(PATH_LIST_SEPARATOR)
145
+ .map((p) => p.trim())
146
+ .filter((p) => p.length > 0)
147
+ .map((p) => isGitHubUrl(p) ? p : path.resolve(p));
148
+ dirs.push(...paths);
149
+ }
150
+ }
151
+ return [...new Set(dirs)]; // Deduplicate
152
+ }
153
+ /**
154
+ * Parse SKILLS_DIR environment variable.
155
+ * Returns resolved absolute paths for local dirs, unchanged for GitHub URLs.
156
+ */
157
+ export function parseEnvVar() {
158
+ const envDir = process.env.SKILLS_DIR;
159
+ if (!envDir) {
160
+ return [];
161
+ }
162
+ const dirs = envDir
163
+ .split(PATH_LIST_SEPARATOR)
164
+ .map((p) => p.trim())
165
+ .filter((p) => p.length > 0)
166
+ .map((p) => isGitHubUrl(p) ? p : path.resolve(p));
167
+ return [...new Set(dirs)]; // Deduplicate
168
+ }
169
+ /**
170
+ * Get all skill directories with their source information.
171
+ * Priority: CLI args > env var > config file
172
+ */
173
+ export function getConfigState() {
174
+ // Check CLI args first
175
+ const cliDirs = parseCLIArgs();
176
+ if (cliDirs.length > 0) {
177
+ return {
178
+ directories: cliDirs.map((p) => ({
179
+ path: p,
180
+ source: "cli",
181
+ type: getSourceType(p),
182
+ skillCount: 0, // Will be filled in by caller
183
+ valid: isValidPath(p),
184
+ allowed: isPathAllowed(p),
185
+ })),
186
+ activeSource: "cli",
187
+ isOverridden: true,
188
+ };
189
+ }
190
+ // Check env var next
191
+ const envDirs = parseEnvVar();
192
+ if (envDirs.length > 0) {
193
+ return {
194
+ directories: envDirs.map((p) => ({
195
+ path: p,
196
+ source: "env",
197
+ type: getSourceType(p),
198
+ skillCount: 0,
199
+ valid: isValidPath(p),
200
+ allowed: isPathAllowed(p),
201
+ })),
202
+ activeSource: "env",
203
+ isOverridden: true,
204
+ };
205
+ }
206
+ // Fall back to config file
207
+ const config = loadConfigFile();
208
+ return {
209
+ directories: config.skillDirectories.map((p) => ({
210
+ path: p,
211
+ source: "config",
212
+ type: getSourceType(p),
213
+ skillCount: 0,
214
+ valid: isValidPath(p),
215
+ allowed: isPathAllowed(p),
216
+ })),
217
+ activeSource: "config",
218
+ isOverridden: false,
219
+ };
220
+ }
221
+ /**
222
+ * Get skill directories from all sources combined.
223
+ * Used for the UI to show all configured directories.
224
+ */
225
+ export function getAllDirectoriesWithSources() {
226
+ const all = [];
227
+ const seen = new Set();
228
+ // CLI dirs
229
+ for (const p of parseCLIArgs()) {
230
+ if (!seen.has(p)) {
231
+ seen.add(p);
232
+ all.push({
233
+ path: p,
234
+ source: "cli",
235
+ type: getSourceType(p),
236
+ skillCount: 0,
237
+ valid: isValidPath(p),
238
+ allowed: isPathAllowed(p),
239
+ });
240
+ }
241
+ }
242
+ // Env dirs
243
+ for (const p of parseEnvVar()) {
244
+ if (!seen.has(p)) {
245
+ seen.add(p);
246
+ all.push({
247
+ path: p,
248
+ source: "env",
249
+ type: getSourceType(p),
250
+ skillCount: 0,
251
+ valid: isValidPath(p),
252
+ allowed: isPathAllowed(p),
253
+ });
254
+ }
255
+ }
256
+ // Config file dirs
257
+ const config = loadConfigFile();
258
+ for (const p of config.skillDirectories) {
259
+ if (!seen.has(p)) {
260
+ seen.add(p);
261
+ all.push({
262
+ path: p,
263
+ source: "config",
264
+ type: getSourceType(p),
265
+ skillCount: 0,
266
+ valid: isValidPath(p),
267
+ allowed: isPathAllowed(p),
268
+ });
269
+ }
270
+ }
271
+ return all;
272
+ }
273
+ /**
274
+ * Add a directory or GitHub URL to the config file.
275
+ * Does not affect CLI or env var configurations.
276
+ */
277
+ export function addDirectoryToConfig(directory) {
278
+ // For GitHub URLs, store as-is; for local paths, resolve to absolute
279
+ const normalized = isGitHubUrl(directory) ? directory : path.resolve(directory);
280
+ // Validate local directories exist
281
+ if (!isGitHubUrl(directory)) {
282
+ if (!fs.existsSync(normalized)) {
283
+ throw new Error(`Directory does not exist: ${normalized}`);
284
+ }
285
+ if (!fs.statSync(normalized).isDirectory()) {
286
+ throw new Error(`Path is not a directory: ${normalized}`);
287
+ }
288
+ }
289
+ const config = loadConfigFile();
290
+ // Check for duplicate
291
+ if (config.skillDirectories.includes(normalized)) {
292
+ throw new Error(`Already configured: ${normalized}`);
293
+ }
294
+ config.skillDirectories.push(normalized);
295
+ saveConfigFile(config);
296
+ }
297
+ /**
298
+ * Remove a directory or GitHub URL from the config file.
299
+ * Only removes from config file, not CLI or env var.
300
+ */
301
+ export function removeDirectoryFromConfig(directory) {
302
+ // For GitHub URLs, use as-is; for local paths, resolve to absolute
303
+ const normalized = isGitHubUrl(directory) ? directory : path.resolve(directory);
304
+ const config = loadConfigFile();
305
+ const index = config.skillDirectories.indexOf(normalized);
306
+ if (index === -1) {
307
+ throw new Error(`Not found in config: ${normalized}`);
308
+ }
309
+ config.skillDirectories.splice(index, 1);
310
+ saveConfigFile(config);
311
+ }
312
+ /**
313
+ * Get only the active skill directories (respecting priority).
314
+ * This is what the server should use for skill discovery.
315
+ */
316
+ export function getActiveDirectories() {
317
+ const state = getConfigState();
318
+ return state.directories.map((d) => d.path);
319
+ }
320
+ /**
321
+ * Get static mode setting from config file.
322
+ */
323
+ export function getStaticModeFromConfig() {
324
+ const config = loadConfigFile();
325
+ return config.staticMode === true;
326
+ }
327
+ /**
328
+ * Set static mode setting in config file.
329
+ */
330
+ export function setStaticModeInConfig(enabled) {
331
+ const config = loadConfigFile();
332
+ config.staticMode = enabled;
333
+ saveConfigFile(config);
334
+ }
335
+ /**
336
+ * Get all skill invocation overrides from the config file.
337
+ */
338
+ export function getSkillInvocationOverrides() {
339
+ const config = loadConfigFile();
340
+ return config.skillInvocationOverrides || {};
341
+ }
342
+ /**
343
+ * Set an invocation override for a skill.
344
+ * @param skillName - The name of the skill
345
+ * @param setting - Which setting to override ("assistant" or "user")
346
+ * @param value - The new value for the setting
347
+ */
348
+ export function setSkillInvocationOverride(skillName, setting, value) {
349
+ const config = loadConfigFile();
350
+ if (!config.skillInvocationOverrides) {
351
+ config.skillInvocationOverrides = {};
352
+ }
353
+ if (!config.skillInvocationOverrides[skillName]) {
354
+ config.skillInvocationOverrides[skillName] = {};
355
+ }
356
+ config.skillInvocationOverrides[skillName][setting] = value;
357
+ saveConfigFile(config);
358
+ }
359
+ /**
360
+ * Clear an invocation override for a skill (revert to frontmatter default).
361
+ * @param skillName - The name of the skill
362
+ * @param setting - Which setting to clear (omit to clear both)
363
+ */
364
+ export function clearSkillInvocationOverride(skillName, setting) {
365
+ const config = loadConfigFile();
366
+ if (!config.skillInvocationOverrides || !config.skillInvocationOverrides[skillName]) {
367
+ return; // Nothing to clear
368
+ }
369
+ if (setting) {
370
+ delete config.skillInvocationOverrides[skillName][setting];
371
+ // Clean up empty override objects
372
+ if (Object.keys(config.skillInvocationOverrides[skillName]).length === 0) {
373
+ delete config.skillInvocationOverrides[skillName];
374
+ }
375
+ }
376
+ else {
377
+ delete config.skillInvocationOverrides[skillName];
378
+ }
379
+ saveConfigFile(config);
380
+ }
381
+ /**
382
+ * Get the GitHub allowed orgs from config file.
383
+ */
384
+ export function getGitHubAllowedOrgs() {
385
+ const config = loadConfigFile();
386
+ return config.githubAllowedOrgs || [];
387
+ }
388
+ /**
389
+ * Get the GitHub allowed users from config file.
390
+ */
391
+ export function getGitHubAllowedUsers() {
392
+ const config = loadConfigFile();
393
+ return config.githubAllowedUsers || [];
394
+ }
395
+ /**
396
+ * Add a GitHub org to the allowed list.
397
+ * @param org - The org name to allow
398
+ */
399
+ export function addGitHubAllowedOrg(org) {
400
+ const config = loadConfigFile();
401
+ if (!config.githubAllowedOrgs) {
402
+ config.githubAllowedOrgs = [];
403
+ }
404
+ const normalized = org.toLowerCase().trim();
405
+ if (!config.githubAllowedOrgs.some((o) => o.toLowerCase() === normalized)) {
406
+ config.githubAllowedOrgs.push(org.trim());
407
+ saveConfigFile(config);
408
+ }
409
+ }
410
+ /**
411
+ * Remove a GitHub org from the allowed list.
412
+ * @param org - The org name to remove
413
+ */
414
+ export function removeGitHubAllowedOrg(org) {
415
+ const config = loadConfigFile();
416
+ if (!config.githubAllowedOrgs) {
417
+ return;
418
+ }
419
+ const normalized = org.toLowerCase().trim();
420
+ config.githubAllowedOrgs = config.githubAllowedOrgs.filter((o) => o.toLowerCase() !== normalized);
421
+ saveConfigFile(config);
422
+ }
423
+ /**
424
+ * Add a GitHub user to the allowed list.
425
+ * @param user - The user name to allow
426
+ */
427
+ export function addGitHubAllowedUser(user) {
428
+ const config = loadConfigFile();
429
+ if (!config.githubAllowedUsers) {
430
+ config.githubAllowedUsers = [];
431
+ }
432
+ const normalized = user.toLowerCase().trim();
433
+ if (!config.githubAllowedUsers.some((u) => u.toLowerCase() === normalized)) {
434
+ config.githubAllowedUsers.push(user.trim());
435
+ saveConfigFile(config);
436
+ }
437
+ }
438
+ /**
439
+ * Remove a GitHub user from the allowed list.
440
+ * @param user - The user name to remove
441
+ */
442
+ export function removeGitHubAllowedUser(user) {
443
+ const config = loadConfigFile();
444
+ if (!config.githubAllowedUsers) {
445
+ return;
446
+ }
447
+ const normalized = user.toLowerCase().trim();
448
+ config.githubAllowedUsers = config.githubAllowedUsers.filter((u) => u.toLowerCase() !== normalized);
449
+ saveConfigFile(config);
450
+ }
@@ -4,6 +4,24 @@
4
4
  * Discovers Agent Skills from a directory, parses YAML frontmatter,
5
5
  * and generates server instructions XML.
6
6
  */
7
+ /**
8
+ * Source information for a skill.
9
+ * Indicates whether the skill comes from a local directory or GitHub repository.
10
+ */
11
+ export interface SkillSource {
12
+ type: "local" | "github" | "bundled";
13
+ displayName: string;
14
+ owner?: string;
15
+ repo?: string;
16
+ }
17
+ /**
18
+ * Default source for skills discovered without explicit source info.
19
+ */
20
+ export declare const DEFAULT_SKILL_SOURCE: SkillSource;
21
+ /**
22
+ * Source for skills bundled with the server package.
23
+ */
24
+ export declare const BUNDLED_SKILL_SOURCE: SkillSource;
7
25
  /**
8
26
  * Metadata extracted from a skill's SKILL.md frontmatter.
9
27
  */
@@ -11,12 +29,22 @@ export interface SkillMetadata {
11
29
  name: string;
12
30
  description: string;
13
31
  path: string;
32
+ disableModelInvocation?: boolean;
33
+ userInvocable?: boolean;
34
+ effectiveAssistantInvocable: boolean;
35
+ effectiveUserInvocable: boolean;
36
+ isAssistantOverridden: boolean;
37
+ isUserOverridden: boolean;
38
+ source: SkillSource;
14
39
  }
15
40
  /**
16
41
  * Discover all skills in a directory.
17
42
  * Scans for subdirectories containing SKILL.md files.
43
+ *
44
+ * @param skillsDir - The directory to scan for skills
45
+ * @param source - Optional source info to attach to discovered skills
18
46
  */
19
- export declare function discoverSkills(skillsDir: string): SkillMetadata[];
47
+ export declare function discoverSkills(skillsDir: string, source?: SkillSource): SkillMetadata[];
20
48
  /**
21
49
  * Generate the server instructions with available skills.
22
50
  * Includes a brief preamble about skill usage following the Agent Skills spec.
@@ -31,3 +59,26 @@ export declare function loadSkillContent(skillPath: string): string;
31
59
  * Uses first-wins behavior: if duplicate names exist, the first occurrence is kept.
32
60
  */
33
61
  export declare function createSkillMap(skills: SkillMetadata[]): Map<string, SkillMetadata>;
62
+ /**
63
+ * Invocation override settings per skill (imported type reference).
64
+ */
65
+ interface SkillInvocationOverrides {
66
+ assistant?: boolean;
67
+ user?: boolean;
68
+ }
69
+ /**
70
+ * Apply invocation overrides from config to compute effective values.
71
+ * Returns a new array with updated effective* fields.
72
+ */
73
+ export declare function applyInvocationOverrides(skills: SkillMetadata[], overrides: Record<string, SkillInvocationOverrides>): SkillMetadata[];
74
+ /**
75
+ * Filter skills that can be invoked by the model (appear in tool description).
76
+ * Uses effective value which considers config overrides.
77
+ */
78
+ export declare function getModelInvocableSkills(skills: SkillMetadata[]): SkillMetadata[];
79
+ /**
80
+ * Filter skills that can be invoked by the user (appear in prompts menu).
81
+ * Uses effective value which considers config overrides.
82
+ */
83
+ export declare function getUserInvocableSkills(skills: SkillMetadata[]): SkillMetadata[];
84
+ export {};
@@ -7,6 +7,20 @@
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { parse as parseYaml } from "yaml";
10
+ /**
11
+ * Default source for skills discovered without explicit source info.
12
+ */
13
+ export const DEFAULT_SKILL_SOURCE = {
14
+ type: "local",
15
+ displayName: "Local",
16
+ };
17
+ /**
18
+ * Source for skills bundled with the server package.
19
+ */
20
+ export const BUNDLED_SKILL_SOURCE = {
21
+ type: "bundled",
22
+ displayName: "Bundled",
23
+ };
10
24
  /**
11
25
  * Find the SKILL.md file in a skill directory.
12
26
  * Prefers SKILL.md (uppercase) but accepts skill.md (lowercase).
@@ -43,8 +57,11 @@ function parseFrontmatter(content) {
43
57
  /**
44
58
  * Discover all skills in a directory.
45
59
  * Scans for subdirectories containing SKILL.md files.
60
+ *
61
+ * @param skillsDir - The directory to scan for skills
62
+ * @param source - Optional source info to attach to discovered skills
46
63
  */
47
- export function discoverSkills(skillsDir) {
64
+ export function discoverSkills(skillsDir, source) {
48
65
  const skills = [];
49
66
  if (!fs.existsSync(skillsDir)) {
50
67
  console.error(`Skills directory not found: ${skillsDir}`);
@@ -63,6 +80,8 @@ export function discoverSkills(skillsDir) {
63
80
  const { metadata } = parseFrontmatter(content);
64
81
  const name = metadata.name;
65
82
  const description = metadata.description;
83
+ const disableModelInvocation = metadata["disable-model-invocation"];
84
+ const userInvocable = metadata["user-invocable"];
66
85
  if (typeof name !== "string" || !name.trim()) {
67
86
  console.error(`Skill at ${skillDir}: missing or invalid 'name' field`);
68
87
  continue;
@@ -71,10 +90,21 @@ export function discoverSkills(skillsDir) {
71
90
  console.error(`Skill at ${skillDir}: missing or invalid 'description' field`);
72
91
  continue;
73
92
  }
93
+ const effectiveAssistant = disableModelInvocation !== true;
94
+ const effectiveUser = userInvocable !== false;
74
95
  skills.push({
75
96
  name: name.trim(),
76
97
  description: description.trim(),
77
98
  path: skillMdPath,
99
+ disableModelInvocation: disableModelInvocation === true,
100
+ userInvocable: userInvocable !== false, // Default to true
101
+ // Initialize effective values from frontmatter (overrides applied later)
102
+ effectiveAssistantInvocable: effectiveAssistant,
103
+ effectiveUserInvocable: effectiveUser,
104
+ isAssistantOverridden: false,
105
+ isUserOverridden: false,
106
+ // Source info (local or GitHub)
107
+ source: source || DEFAULT_SKILL_SOURCE,
78
108
  });
79
109
  }
80
110
  catch (error) {
@@ -142,3 +172,42 @@ export function createSkillMap(skills) {
142
172
  }
143
173
  return map;
144
174
  }
175
+ /**
176
+ * Apply invocation overrides from config to compute effective values.
177
+ * Returns a new array with updated effective* fields.
178
+ */
179
+ export function applyInvocationOverrides(skills, overrides) {
180
+ return skills.map((skill) => {
181
+ const override = overrides[skill.name];
182
+ if (!override) {
183
+ return skill; // No override, keep frontmatter defaults
184
+ }
185
+ const hasAssistantOverride = override.assistant !== undefined;
186
+ const hasUserOverride = override.user !== undefined;
187
+ return {
188
+ ...skill,
189
+ effectiveAssistantInvocable: hasAssistantOverride
190
+ ? override.assistant
191
+ : !skill.disableModelInvocation,
192
+ effectiveUserInvocable: hasUserOverride
193
+ ? override.user
194
+ : skill.userInvocable !== false,
195
+ isAssistantOverridden: hasAssistantOverride,
196
+ isUserOverridden: hasUserOverride,
197
+ };
198
+ });
199
+ }
200
+ /**
201
+ * Filter skills that can be invoked by the model (appear in tool description).
202
+ * Uses effective value which considers config overrides.
203
+ */
204
+ export function getModelInvocableSkills(skills) {
205
+ return skills.filter((skill) => skill.effectiveAssistantInvocable);
206
+ }
207
+ /**
208
+ * Filter skills that can be invoked by the user (appear in prompts menu).
209
+ * Uses effective value which considers config overrides.
210
+ */
211
+ export function getUserInvocableSkills(skills) {
212
+ return skills.filter((skill) => skill.effectiveUserInvocable);
213
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * MCP App tool registration for skill display UI.
3
+ *
4
+ * Registers:
5
+ * - skill-display: Opens the skill display UI
6
+ * - skill-display-update-invocation: Updates invocation settings (UI-only)
7
+ * - skill-display-reset-override: Resets a skill to frontmatter defaults (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 invocation settings change.
13
+ */
14
+ export type OnInvocationChangedCallback = () => void;
15
+ /**
16
+ * Register skill-display MCP App tools and resource.
17
+ *
18
+ * @param server - The MCP server instance
19
+ * @param skillState - Shared skill state for getting skill info
20
+ * @param onInvocationChanged - Callback when invocation settings are changed
21
+ */
22
+ export declare function registerSkillDisplayTool(server: McpServer, skillState: SkillState, onInvocationChanged: OnInvocationChangedCallback): void;