From 253b22ca1378cf5d27620029f8974df169682863 Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Sat, 4 Apr 2026 10:47:41 +0900 Subject: [PATCH] feat(mcp): expose skill execution metadata in list_skills (#1259) Extend SkillInfo with optional userInvocable, disableModelInvocation, context, agent, and allowedTools fields. Map these from the parsed skill frontmatter in listSkills. Only include fields when present to maintain backward compatibility. Closes #1259 --- .../skill-recommendation.service.spec.ts | 68 +++++++++++++++++++ .../src/skill/skill-recommendation.service.ts | 12 +++- .../src/skill/skill-recommendation.types.ts | 10 +++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/apps/mcp-server/src/skill/skill-recommendation.service.spec.ts b/apps/mcp-server/src/skill/skill-recommendation.service.spec.ts index c3014a63..4fdbbfa7 100644 --- a/apps/mcp-server/src/skill/skill-recommendation.service.spec.ts +++ b/apps/mcp-server/src/skill/skill-recommendation.service.spec.ts @@ -14,6 +14,9 @@ function createMockRulesService( triggers?: SkillFrontmatterTrigger[]; userInvocable?: boolean; disableModelInvocation?: boolean; + context?: string; + agent?: string; + allowedTools?: string[]; }> = [], ): RulesService { return { @@ -764,5 +767,70 @@ describe('SkillRecommendationService', () => { expect(result.skills).toEqual([]); expect(result.total).toBe(0); }); + + describe('execution metadata', () => { + it('should include execution metadata when present in skill', async () => { + const rulesWithMetadata = createMockRulesService([ + { + name: 'security-audit', + description: 'Security audit skill', + context: 'fork', + agent: 'general-purpose', + allowedTools: ['Read', 'Grep', 'Glob'], + userInvocable: true, + }, + ]); + const svc = new SkillRecommendationService(rulesWithMetadata); + await svc.loadFrontmatterTriggers(); + + const result = await svc.listSkills(); + const skill = result.skills.find(s => s.name === 'security-audit'); + + expect(skill).toBeDefined(); + expect(skill!.context).toBe('fork'); + expect(skill!.agent).toBe('general-purpose'); + expect(skill!.allowedTools).toEqual(['Read', 'Grep', 'Glob']); + expect(skill!.userInvocable).toBe(true); + }); + + it('should omit execution metadata when not present (backward compat)', async () => { + const rulesWithoutMetadata = createMockRulesService([ + { + name: 'brainstorming', + description: 'Brainstorming skill', + }, + ]); + const svc = new SkillRecommendationService(rulesWithoutMetadata); + await svc.loadFrontmatterTriggers(); + + const result = await svc.listSkills(); + const skill = result.skills.find(s => s.name === 'brainstorming'); + + expect(skill).toBeDefined(); + expect(skill!.context).toBeUndefined(); + expect(skill!.agent).toBeUndefined(); + expect(skill!.allowedTools).toBeUndefined(); + expect(skill!.userInvocable).toBeUndefined(); + expect(skill!.disableModelInvocation).toBeUndefined(); + }); + + it('should include disableModelInvocation when set', async () => { + const rulesWithDisabled = createMockRulesService([ + { + name: 'database-migration', + description: 'DB migration skill', + disableModelInvocation: true, + }, + ]); + const svc = new SkillRecommendationService(rulesWithDisabled); + await svc.loadFrontmatterTriggers(); + + const result = await svc.listSkills(); + const skill = result.skills.find(s => s.name === 'database-migration'); + + expect(skill).toBeDefined(); + expect(skill!.disableModelInvocation).toBe(true); + }); + }); }); }); diff --git a/apps/mcp-server/src/skill/skill-recommendation.service.ts b/apps/mcp-server/src/skill/skill-recommendation.service.ts index 2ccbff50..2df2bbb6 100644 --- a/apps/mcp-server/src/skill/skill-recommendation.service.ts +++ b/apps/mcp-server/src/skill/skill-recommendation.service.ts @@ -236,12 +236,22 @@ export class SkillRecommendationService implements OnModuleInit { let skills: SkillInfo[] = fsSkills.map(fsSkill => { const kwEntry = SKILL_KEYWORDS.find(k => k.skillName === fsSkill.name); - return { + const info: SkillInfo = { name: fsSkill.name, priority: kwEntry?.priority ?? DEFAULT_PRIORITY, description: fsSkill.description, concepts: kwEntry ? Object.keys(kwEntry.concepts) : [], }; + + // Include execution metadata only when present (backward compat) + if (fsSkill.userInvocable !== undefined) info.userInvocable = fsSkill.userInvocable; + if (fsSkill.disableModelInvocation !== undefined) + info.disableModelInvocation = fsSkill.disableModelInvocation; + if (fsSkill.context !== undefined) info.context = fsSkill.context; + if (fsSkill.agent !== undefined) info.agent = fsSkill.agent; + if (fsSkill.allowedTools !== undefined) info.allowedTools = fsSkill.allowedTools; + + return info; }); // Apply filters diff --git a/apps/mcp-server/src/skill/skill-recommendation.types.ts b/apps/mcp-server/src/skill/skill-recommendation.types.ts index 294f21a5..85381d83 100644 --- a/apps/mcp-server/src/skill/skill-recommendation.types.ts +++ b/apps/mcp-server/src/skill/skill-recommendation.types.ts @@ -36,6 +36,16 @@ export interface SkillInfo { * Use recommend_skills to get matched keywords for a specific prompt. */ concepts: string[]; + /** Whether the skill can be invoked by a user (e.g. via slash command) */ + userInvocable?: boolean; + /** Whether the model should not auto-invoke this skill */ + disableModelInvocation?: boolean; + /** Execution context: "fork" runs in a separate context */ + context?: string; + /** Agent to use when executing the skill */ + agent?: string; + /** Tools the skill is allowed to use */ + allowedTools?: string[]; } /**