Skip to content

Commit b3b9adc

Browse files
fix(ca): optimize list rule tool for output token (#351)
1 parent af3f34a commit b3b9adc

File tree

4 files changed

+55
-16
lines changed

4 files changed

+55
-16
lines changed

packages/mcp-provider-code-analyzer/src/actions/list-rules.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ export type ListRulesOutput = {
2727
engine: string
2828
severity: number
2929
tags: string[]
30-
description: string
31-
resources: string[]
3230
}[]
3331
};
3432

@@ -82,8 +80,6 @@ export class ListRulesActionImpl implements ListRulesAction {
8280
name: r.getName(),
8381
engine: r.getEngineName(),
8482
severity: r.getSeverityLevel(),
85-
description: r.getDescription(),
86-
resources: r.getResourceUrls(),
8783
tags: r.getTags()
8884
}
8985
});

packages/mcp-provider-code-analyzer/src/tools/list_code_analyzer_rules.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ const ALLOWED_SELECTOR_TOKENS_LOWER: ReadonlySet<string> = new Set<string>([
3030
const DESCRIPTION: string = `A tool for selecting Code Analyzer rules based on a number of criteria.\n` +
3131
`This tool returns a JSON array describing Code Analyzer rules that match a "selector".\n` +
3232
`A selector is a colon-separated (:) string of tokens; tags and severity names are case-insensitive.\n` +
33+
`You can OR multiple tokens of the same type by grouping them in parentheses and separating with commas, e.g. "(Performance,Security)".\n` +
3334
`\n` +
3435
`Examples:\n` +
3536
`- "Recommended" → all rules tagged as Recommended.\n` +
3637
`- "Performance:pmd:Critical" → rules in the PMD engine with the Performance tag and Critical severity.\n` +
38+
`- "pmd:(Performance,Security):2" → PMD rules with Performance OR Security tags and severity 2.\n` +
39+
`- "(Apex,JavaScript):Recommended" → rules for Apex OR JavaScript languages that are Recommended.\n` +
3740
`- "Security:High" → rules tagged Security with High severity.\n` +
3841
`- "Apex:Recommended" → rules for the Apex language that are Recommended.\n` +
3942
`- "DevPreview" → rules marked as DevPreview.\n` +
@@ -46,7 +49,9 @@ const DESCRIPTION: string = `A tool for selecting Code Analyzer rules based on a
4649
`- General tags: Recommended, Custom, All\n` +
4750
`- Categories: BestPractices, CodeStyle, Design, Documentation, ErrorProne, Security, Performance\n` +
4851
`- Languages: Apex, CSS, HTML, JavaScript, TypeScript, Visualforce, XML\n` +
49-
`- Engine-specific tags: DevPreview, LWC
52+
`- Engine-specific tags: DevPreview, LWC\n` +
53+
`\n` +
54+
`Tip: Use the "describe_code_analyzer_rule" tool to get details for any listed rule.
5055
`;
5156

5257
export const inputSchema = z.object({
@@ -60,9 +65,7 @@ const outputSchema = z.object({
6065
name: z.string().describe('The name of the rule, equivalent to the `ruleName` input property.'),
6166
engine: z.string().describe('The name of the engine to which the rule belongs.'),
6267
severity: z.number().describe('An integer between 1 and 5 indicating the severity of the rule. Lower numbers are MORE severe.'),
63-
tags: z.array(z.string()).describe('An array of strings indicating tags applicable to the rule, e.g. "performance", "security", etc.'),
64-
description: z.string().describe('A string describing the purpose and functionality of the rule.'),
65-
resources: z.array(z.string()).describe('A possibly empty array of strings that represent links to documentation or other helpful material.')
68+
tags: z.array(z.string()).describe('An array of strings indicating tags applicable to the rule, e.g. "performance", "security", etc.')
6669
})).optional().describe('An array of rules that matched the selector. Empty if no rules matched.')
6770
});
6871
type OutputArgsShape = typeof outputSchema.shape;
@@ -89,6 +92,25 @@ export class CodeAnalyzerListRulesMcpTool extends McpTool<InputArgsShape, Output
8992

9093
const invalid: string[] = [];
9194
for (const token of rawTokens) {
95+
// Support OR groups inside parentheses, e.g., (Performance,Security)
96+
if (token.startsWith('(') && token.endsWith(')')) {
97+
const inner = token.slice(1, -1);
98+
const innerTokens = inner
99+
.split(',')
100+
.map(t => t.trim())
101+
.filter(t => t.length > 0);
102+
if (innerTokens.length === 0) {
103+
invalid.push(token);
104+
continue;
105+
}
106+
for (const innerToken of innerTokens) {
107+
const normInner = innerToken.toLowerCase();
108+
if (!ALLOWED_SELECTOR_TOKENS_LOWER.has(normInner)) {
109+
invalid.push(innerToken);
110+
}
111+
}
112+
continue;
113+
}
92114
const normalized = token.toLowerCase();
93115
if (!ALLOWED_SELECTOR_TOKENS_LOWER.has(normalized)) {
94116
invalid.push(token);
@@ -160,7 +182,10 @@ export class CodeAnalyzerListRulesMcpTool extends McpTool<InputArgsShape, Output
160182
output = { status: getErrorMessage(e) }
161183
}
162184
return {
163-
content: [{ type: "text", text: JSON.stringify(output)}],
185+
content: [
186+
{ type: "text", text: `Tip: Use the "describe_code_analyzer_rule" tool to get details for any listed rule.` },
187+
{ type: "text", text: JSON.stringify(output)}
188+
],
164189
structuredContent: output
165190
};
166191
}

packages/mcp-provider-code-analyzer/test/actions/list-rules.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ describe('ListRulesActionImpl', () => {
2727
const found = output.rules!.find(r => r.name === 'stub1RuleA' && r.engine === 'EngineThatLogsError');
2828
expect(found).toBeDefined();
2929
expect(found!.tags).toContain('Recommended');
30-
expect(found!.description).toContain('Some description');
31-
expect(Array.isArray(found!.resources)).toBe(true);
3230
});
3331

3432
it('emits telemetry for engine selection', async () => {

packages/mcp-provider-code-analyzer/test/tools/list_code_analyzer_rules.test.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,17 @@ describe('CodeAnalyzerListRulesMcpTool', () => {
3434
name: 'stub1RuleA',
3535
engine: 'EngineThatLogsError',
3636
severity: 5,
37-
tags: ['Recommended'],
38-
description: 'Some description',
39-
resources: ['https://example.com/stub1RuleA']
37+
tags: ['Recommended']
4038
}]
4139
};
4240
const tool = new CodeAnalyzerListRulesMcpTool(new StubListRulesAction(expected));
4341

4442
const result = await tool.exec({ selector: 'Recommended' });
4543

4644
expect(result.structuredContent).toEqual(expected);
47-
const text = result.content[0]?.type === 'text' ? result.content[0].text : '';
48-
expect(JSON.parse(text)).toEqual(expected);
45+
const jsonItem: any = result.content.find((c: any) => c.type === 'text' && typeof c.text === 'string' && c.text.trim().startsWith('{'));
46+
const jsonText: string = typeof jsonItem?.text === 'string' ? jsonItem.text : '';
47+
expect(JSON.parse(jsonText)).toEqual(expected);
4948
});
5049

5150
it('catches errors and returns error status', async () => {
@@ -104,4 +103,25 @@ describe('CodeAnalyzerListRulesMcpTool', () => {
104103
expect(result.isError).toBe(true);
105104
expect(result.structuredContent?.status).toContain('Invalid selector token(s): <empty>');
106105
});
106+
107+
it('validateSelector accepts OR groups in parentheses', () => {
108+
const res = CodeAnalyzerListRulesMcpTool.validateSelector('pmd:(Performance,Security):2');
109+
expect(res.valid).toBe(true);
110+
});
111+
112+
it('validateSelector rejects empty OR group', () => {
113+
const res = CodeAnalyzerListRulesMcpTool.validateSelector('pmd:()');
114+
expect(res.valid).toBe(false);
115+
if (res.valid === false) {
116+
expect(res.invalidTokens).toContain('()');
117+
}
118+
});
119+
120+
it('validateSelector rejects group with unknown token', () => {
121+
const res = CodeAnalyzerListRulesMcpTool.validateSelector('pmd:(Performance,NotATag):2');
122+
expect(res.valid).toBe(false);
123+
if (res.valid === false) {
124+
expect(res.invalidTokens).toContain('NotATag');
125+
}
126+
});
107127
});

0 commit comments

Comments
 (0)