From 9a384966c65af701815b430144dbdf5480ffe03e Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Sat, 4 Apr 2026 09:51:05 +0900 Subject: [PATCH] fix(mcp): filter disable-model-invocation skills from auto-recommendations (#1254) Skills with `disable-model-invocation: true` frontmatter were still being auto-recommended. Extend skillMetadataCache to cache and filter the disableModelInvocation field alongside userInvocable. --- .../skill-recommendation.service.spec.ts | 53 +++++++++++++++++++ .../src/skill/skill-recommendation.service.ts | 17 ++++-- 2 files changed, 66 insertions(+), 4 deletions(-) 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 5eeb8987..4e97edba 100644 --- a/apps/mcp-server/src/skill/skill-recommendation.service.spec.ts +++ b/apps/mcp-server/src/skill/skill-recommendation.service.spec.ts @@ -13,6 +13,7 @@ function createMockRulesService( description: string; triggers?: SkillFrontmatterTrigger[]; userInvocable?: boolean; + disableModelInvocation?: boolean; }> = [], ): RulesService { return { @@ -595,6 +596,58 @@ describe('SkillRecommendationService', () => { }); }); + describe('disableModelInvocation filtering', () => { + it('should exclude skills with disableModelInvocation: true from recommendations', async () => { + const skillsWithDisabled = [ + ...FILESYSTEM_SKILLS, + { + name: 'incident-response', + description: 'Incident response skill', + disableModelInvocation: true, + triggers: [{ pattern: 'incident.*response', confidence: 'high' as const }], + }, + ]; + const filteredService = new SkillRecommendationService( + createMockRulesService(skillsWithDisabled), + ); + await filteredService.loadFrontmatterTriggers(); + + const result = filteredService.recommendSkills('handle incident response procedure'); + + const incident = result.recommendations.find(r => r.skillName === 'incident-response'); + expect(incident).toBeUndefined(); + }); + + it('should include skills with disableModelInvocation: false in recommendations', async () => { + const skillsWithEnabled = [ + ...FILESYSTEM_SKILLS, + { + name: 'enabled-skill', + description: 'Model invocation enabled skill', + disableModelInvocation: false, + triggers: [{ pattern: 'enabled-model-test-xyz', confidence: 'high' as const }], + }, + ]; + const enabledService = new SkillRecommendationService( + createMockRulesService(skillsWithEnabled), + ); + await enabledService.loadFrontmatterTriggers(); + + const result = enabledService.recommendSkills('enabled-model-test-xyz'); + + const skill = result.recommendations.find(r => r.skillName === 'enabled-skill'); + expect(skill).toBeDefined(); + }); + + it('should include skills without disableModelInvocation field (default behavior)', async () => { + // Skills without disableModelInvocation should be included (backwards compatible) + const result = service.recommendSkills('widget slot architecture'); + + const wsa = result.recommendations.find(r => r.skillName === 'widget-slot-architecture'); + expect(wsa).toBeDefined(); + }); + }); + describe('listSkills', () => { it('should return all filesystem skills sorted by priority descending', async () => { const result = await service.listSkills(); diff --git a/apps/mcp-server/src/skill/skill-recommendation.service.ts b/apps/mcp-server/src/skill/skill-recommendation.service.ts index ab4c85db..2ccbff50 100644 --- a/apps/mcp-server/src/skill/skill-recommendation.service.ts +++ b/apps/mcp-server/src/skill/skill-recommendation.service.ts @@ -43,7 +43,10 @@ function getSkillDescription(skillName: string): string { @Injectable() export class SkillRecommendationService implements OnModuleInit { private frontmatterTriggerCache = new Map(); - private skillMetadataCache = new Map(); + private skillMetadataCache = new Map< + string, + { userInvocable?: boolean; disableModelInvocation?: boolean } + >(); constructor(private readonly rulesService: RulesService) {} @@ -61,8 +64,11 @@ export class SkillRecommendationService implements OnModuleInit { this.skillMetadataCache.clear(); for (const skill of skills) { - if (skill.userInvocable !== undefined) { - this.skillMetadataCache.set(skill.name, { userInvocable: skill.userInvocable }); + if (skill.userInvocable !== undefined || skill.disableModelInvocation !== undefined) { + this.skillMetadataCache.set(skill.name, { + userInvocable: skill.userInvocable, + disableModelInvocation: skill.disableModelInvocation, + }); } if (skill.triggers && skill.triggers.length > 0) { const compiled: CachedFrontmatterTrigger[] = []; @@ -170,11 +176,14 @@ export class SkillRecommendationService implements OnModuleInit { const recommendations: SkillRecommendation[] = []; for (const [skillName, match] of skillMatches) { - // Filter out skills explicitly marked as user-invocable: false + // Filter out skills explicitly marked as non-invocable const metadata = this.skillMetadataCache.get(skillName); if (metadata?.userInvocable === false) { continue; } + if (metadata?.disableModelInvocation === true) { + continue; + } recommendations.push({ skillName,