Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions apps/mcp-server/src/skill/skill-recommendation.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function createMockRulesService(
description: string;
triggers?: SkillFrontmatterTrigger[];
userInvocable?: boolean;
disableModelInvocation?: boolean;
}> = [],
): RulesService {
return {
Expand Down Expand Up @@ -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();
Expand Down
17 changes: 13 additions & 4 deletions apps/mcp-server/src/skill/skill-recommendation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ function getSkillDescription(skillName: string): string {
@Injectable()
export class SkillRecommendationService implements OnModuleInit {
private frontmatterTriggerCache = new Map<string, FrontmatterTriggerEntry>();
private skillMetadataCache = new Map<string, { userInvocable?: boolean }>();
private skillMetadataCache = new Map<
string,
{ userInvocable?: boolean; disableModelInvocation?: boolean }
>();

constructor(private readonly rulesService: RulesService) {}

Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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,
Expand Down
Loading