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
44 changes: 43 additions & 1 deletion apps/mcp-server/src/rules/skill.schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Content.
expect(result.agent).toBe('security-specialist');
});

it('should parse allowed-tools field', () => {
it('should parse allowed-tools field as array', () => {
const content = `---
name: tools-skill
description: Skill with allowed tools
Expand All @@ -125,6 +125,48 @@ Content.
expect(result.allowedTools).toEqual(['Read', 'Grep', 'Glob']);
});

it('should parse allowed-tools as comma-separated string', () => {
const content = `---
name: tools-skill
description: Skill with comma-string tools
allowed-tools: Read, Grep, Glob
---

Content.
`;
const result = parseSkill(content, 'path');

expect(result.allowedTools).toEqual(['Read', 'Grep', 'Glob']);
});

it('should parse allowed-tools comma string with parenthesized patterns', () => {
const content = `---
name: tools-skill
description: Skill with paren patterns
allowed-tools: "Read, Grep, Glob, Bash(gh:*, git:*)"
---

Content.
`;
const result = parseSkill(content, 'path');

expect(result.allowedTools).toEqual(['Read', 'Grep', 'Glob', 'Bash(gh:*, git:*)']);
});

it('should parse allowed-tools empty string as empty array', () => {
const content = `---
name: tools-skill
description: Skill with empty tools
allowed-tools: ""
---

Content.
`;
const result = parseSkill(content, 'path');

expect(result.allowedTools).toEqual([]);
});

it('should leave extended fields undefined when not present', () => {
const content = `---
name: basic-skill
Expand Down
29 changes: 28 additions & 1 deletion apps/mcp-server/src/rules/skill.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,31 @@ export class SkillSchemaError extends Error {
// Zod Schemas
// ============================================================================

/**
* Split a comma-separated allowed-tools string, respecting parenthesized
* patterns like `Bash(gh:*, git:*)` where commas inside parens are not
* delimiters.
*/
function splitAllowedTools(s: string): string[] {
const result: string[] = [];
let current = '';
let depth = 0;
for (const ch of s) {
if (ch === '(') depth++;
else if (ch === ')') depth--;
if (ch === ',' && depth === 0) {
const trimmed = current.trim();
if (trimmed) result.push(trimmed);
current = '';
} else {
current += ch;
}
}
const trimmed = current.trim();
if (trimmed) result.push(trimmed);
return result;
}

/**
* Trigger entry schema for SKILL.md frontmatter
* - pattern: regex pattern string (non-empty)
Expand Down Expand Up @@ -57,7 +82,9 @@ const SkillFrontmatterSchema = z.object({
'disable-model-invocation': z.boolean().optional(),
context: z.string().optional(),
agent: z.string().optional(),
'allowed-tools': z.array(z.string()).optional(),
'allowed-tools': z
.union([z.array(z.string()), z.string().transform(s => splitAllowedTools(s))])
.optional(),
});

// ============================================================================
Expand Down
Loading