Skip to content

Commit d4d94b8

Browse files
committed
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
1 parent c7b6858 commit d4d94b8

3 files changed

Lines changed: 89 additions & 1 deletion

File tree

apps/mcp-server/src/skill/skill-recommendation.service.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ function createMockRulesService(
1414
triggers?: SkillFrontmatterTrigger[];
1515
userInvocable?: boolean;
1616
disableModelInvocation?: boolean;
17+
context?: string;
18+
agent?: string;
19+
allowedTools?: string[];
1720
}> = [],
1821
): RulesService {
1922
return {
@@ -764,5 +767,70 @@ describe('SkillRecommendationService', () => {
764767
expect(result.skills).toEqual([]);
765768
expect(result.total).toBe(0);
766769
});
770+
771+
describe('execution metadata', () => {
772+
it('should include execution metadata when present in skill', async () => {
773+
const rulesWithMetadata = createMockRulesService([
774+
{
775+
name: 'security-audit',
776+
description: 'Security audit skill',
777+
context: 'fork',
778+
agent: 'general-purpose',
779+
allowedTools: ['Read', 'Grep', 'Glob'],
780+
userInvocable: true,
781+
},
782+
]);
783+
const svc = new SkillRecommendationService(rulesWithMetadata);
784+
await svc.loadFrontmatterTriggers();
785+
786+
const result = await svc.listSkills();
787+
const skill = result.skills.find(s => s.name === 'security-audit');
788+
789+
expect(skill).toBeDefined();
790+
expect(skill!.context).toBe('fork');
791+
expect(skill!.agent).toBe('general-purpose');
792+
expect(skill!.allowedTools).toEqual(['Read', 'Grep', 'Glob']);
793+
expect(skill!.userInvocable).toBe(true);
794+
});
795+
796+
it('should omit execution metadata when not present (backward compat)', async () => {
797+
const rulesWithoutMetadata = createMockRulesService([
798+
{
799+
name: 'brainstorming',
800+
description: 'Brainstorming skill',
801+
},
802+
]);
803+
const svc = new SkillRecommendationService(rulesWithoutMetadata);
804+
await svc.loadFrontmatterTriggers();
805+
806+
const result = await svc.listSkills();
807+
const skill = result.skills.find(s => s.name === 'brainstorming');
808+
809+
expect(skill).toBeDefined();
810+
expect(skill!.context).toBeUndefined();
811+
expect(skill!.agent).toBeUndefined();
812+
expect(skill!.allowedTools).toBeUndefined();
813+
expect(skill!.userInvocable).toBeUndefined();
814+
expect(skill!.disableModelInvocation).toBeUndefined();
815+
});
816+
817+
it('should include disableModelInvocation when set', async () => {
818+
const rulesWithDisabled = createMockRulesService([
819+
{
820+
name: 'database-migration',
821+
description: 'DB migration skill',
822+
disableModelInvocation: true,
823+
},
824+
]);
825+
const svc = new SkillRecommendationService(rulesWithDisabled);
826+
await svc.loadFrontmatterTriggers();
827+
828+
const result = await svc.listSkills();
829+
const skill = result.skills.find(s => s.name === 'database-migration');
830+
831+
expect(skill).toBeDefined();
832+
expect(skill!.disableModelInvocation).toBe(true);
833+
});
834+
});
767835
});
768836
});

apps/mcp-server/src/skill/skill-recommendation.service.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,22 @@ export class SkillRecommendationService implements OnModuleInit {
236236

237237
let skills: SkillInfo[] = fsSkills.map(fsSkill => {
238238
const kwEntry = SKILL_KEYWORDS.find(k => k.skillName === fsSkill.name);
239-
return {
239+
const info: SkillInfo = {
240240
name: fsSkill.name,
241241
priority: kwEntry?.priority ?? DEFAULT_PRIORITY,
242242
description: fsSkill.description,
243243
concepts: kwEntry ? Object.keys(kwEntry.concepts) : [],
244244
};
245+
246+
// Include execution metadata only when present (backward compat)
247+
if (fsSkill.userInvocable !== undefined) info.userInvocable = fsSkill.userInvocable;
248+
if (fsSkill.disableModelInvocation !== undefined)
249+
info.disableModelInvocation = fsSkill.disableModelInvocation;
250+
if (fsSkill.context !== undefined) info.context = fsSkill.context;
251+
if (fsSkill.agent !== undefined) info.agent = fsSkill.agent;
252+
if (fsSkill.allowedTools !== undefined) info.allowedTools = fsSkill.allowedTools;
253+
254+
return info;
245255
});
246256

247257
// Apply filters

apps/mcp-server/src/skill/skill-recommendation.types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ export interface SkillInfo {
3636
* Use recommend_skills to get matched keywords for a specific prompt.
3737
*/
3838
concepts: string[];
39+
/** Whether the skill can be invoked by a user (e.g. via slash command) */
40+
userInvocable?: boolean;
41+
/** Whether the model should not auto-invoke this skill */
42+
disableModelInvocation?: boolean;
43+
/** Execution context: "fork" runs in a separate context */
44+
context?: string;
45+
/** Agent to use when executing the skill */
46+
agent?: string;
47+
/** Tools the skill is allowed to use */
48+
allowedTools?: string[];
3949
}
4050

4151
/**

0 commit comments

Comments
 (0)