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
68 changes: 68 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 @@ -14,6 +14,9 @@ function createMockRulesService(
triggers?: SkillFrontmatterTrigger[];
userInvocable?: boolean;
disableModelInvocation?: boolean;
context?: string;
agent?: string;
allowedTools?: string[];
}> = [],
): RulesService {
return {
Expand Down Expand Up @@ -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);
});
});
});
});
12 changes: 11 additions & 1 deletion apps/mcp-server/src/skill/skill-recommendation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions apps/mcp-server/src/skill/skill-recommendation.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

/**
Expand Down
Loading