mcp-lint 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/cli/commands/check.d.ts +2 -0
- package/dist/cli/commands/check.js +89 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/fix.d.ts +2 -0
- package/dist/cli/commands/fix.js +60 -0
- package/dist/cli/commands/fix.js.map +1 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +42 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/formatters/json.d.ts +2 -0
- package/dist/cli/formatters/json.js +4 -0
- package/dist/cli/formatters/json.js.map +1 -0
- package/dist/cli/formatters/markdown.d.ts +2 -0
- package/dist/cli/formatters/markdown.js +44 -0
- package/dist/cli/formatters/markdown.js.map +1 -0
- package/dist/cli/formatters/terminal.d.ts +2 -0
- package/dist/cli/formatters/terminal.js +48 -0
- package/dist/cli/formatters/terminal.js.map +1 -0
- package/dist/config/config.d.ts +3 -0
- package/dist/config/config.js +38 -0
- package/dist/config/config.js.map +1 -0
- package/dist/core/diagnostic.d.ts +15 -0
- package/dist/core/diagnostic.js +2 -0
- package/dist/core/diagnostic.js.map +1 -0
- package/dist/core/engine.d.ts +8 -0
- package/dist/core/engine.js +35 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/fixer.d.ts +3 -0
- package/dist/core/fixer.js +37 -0
- package/dist/core/fixer.js.map +1 -0
- package/dist/core/rule.d.ts +52 -0
- package/dist/core/rule.js +2 -0
- package/dist/core/rule.js.map +1 -0
- package/dist/core/severity.d.ts +1 -0
- package/dist/core/severity.js +2 -0
- package/dist/core/severity.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/loaders/file-loader.d.ts +2 -0
- package/dist/loaders/file-loader.js +30 -0
- package/dist/loaders/file-loader.js.map +1 -0
- package/dist/loaders/sse-loader.d.ts +2 -0
- package/dist/loaders/sse-loader.js +19 -0
- package/dist/loaders/sse-loader.js.map +1 -0
- package/dist/loaders/stdio-loader.d.ts +2 -0
- package/dist/loaders/stdio-loader.js +19 -0
- package/dist/loaders/stdio-loader.js.map +1 -0
- package/dist/rules/client-specific/claude.d.ts +2 -0
- package/dist/rules/client-specific/claude.js +30 -0
- package/dist/rules/client-specific/claude.js.map +1 -0
- package/dist/rules/client-specific/cursor.d.ts +2 -0
- package/dist/rules/client-specific/cursor.js +49 -0
- package/dist/rules/client-specific/cursor.js.map +1 -0
- package/dist/rules/client-specific/gemini.d.ts +3 -0
- package/dist/rules/client-specific/gemini.js +60 -0
- package/dist/rules/client-specific/gemini.js.map +1 -0
- package/dist/rules/client-specific/vscode.d.ts +2 -0
- package/dist/rules/client-specific/vscode.js +22 -0
- package/dist/rules/client-specific/vscode.js.map +1 -0
- package/dist/rules/description-exists.d.ts +2 -0
- package/dist/rules/description-exists.js +35 -0
- package/dist/rules/description-exists.js.map +1 -0
- package/dist/rules/index.d.ts +2 -0
- package/dist/rules/index.js +28 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/max-depth.d.ts +2 -0
- package/dist/rules/max-depth.js +59 -0
- package/dist/rules/max-depth.js.map +1 -0
- package/dist/rules/no-content-encoding.d.ts +2 -0
- package/dist/rules/no-content-encoding.js +33 -0
- package/dist/rules/no-content-encoding.js.map +1 -0
- package/dist/rules/no-empty-enum.d.ts +2 -0
- package/dist/rules/no-empty-enum.js +33 -0
- package/dist/rules/no-empty-enum.js.map +1 -0
- package/dist/rules/no-recursive-refs.d.ts +2 -0
- package/dist/rules/no-recursive-refs.js +68 -0
- package/dist/rules/no-recursive-refs.js.map +1 -0
- package/dist/rules/no-required-false.d.ts +2 -0
- package/dist/rules/no-required-false.js +33 -0
- package/dist/rules/no-required-false.js.map +1 -0
- package/dist/rules/no-unsupported-formats.d.ts +2 -0
- package/dist/rules/no-unsupported-formats.js +39 -0
- package/dist/rules/no-unsupported-formats.js.map +1 -0
- package/dist/rules/valid-json-schema-subset.d.ts +2 -0
- package/dist/rules/valid-json-schema-subset.js +43 -0
- package/dist/rules/valid-json-schema-subset.js.map +1 -0
- package/dist/utils/schema-walker.d.ts +3 -0
- package/dist/utils/schema-walker.js +52 -0
- package/dist/utils/schema-walker.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
async function readConfigFile(filePath) {
|
|
5
|
+
const content = await readFile(filePath, 'utf-8');
|
|
6
|
+
return JSON.parse(content);
|
|
7
|
+
}
|
|
8
|
+
export async function loadConfig(configPath) {
|
|
9
|
+
if (configPath) {
|
|
10
|
+
try {
|
|
11
|
+
return await readConfigFile(configPath);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
throw new Error(`Cannot load config from "${configPath}": ${err.message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
let dir = process.cwd();
|
|
18
|
+
const home = homedir();
|
|
19
|
+
while (true) {
|
|
20
|
+
try {
|
|
21
|
+
return await readConfigFile(resolve(dir, '.mcplintrc.json'));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// not found here, try parent
|
|
25
|
+
}
|
|
26
|
+
const parent = dirname(dir);
|
|
27
|
+
if (parent === dir || dir === home)
|
|
28
|
+
break;
|
|
29
|
+
dir = parent;
|
|
30
|
+
}
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
export const DEFAULT_CONFIG = {
|
|
34
|
+
clients: ['claude', 'cursor', 'gemini', 'vscode'],
|
|
35
|
+
ignore: [],
|
|
36
|
+
maxDepth: 5,
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGlC,KAAK,UAAU,cAAc,CAAC,QAAgB;IAC5C,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAW,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAmB;IAClD,IAAI,UAAU,EAAE,CAAC;QACf,IAAI,CAAC;YACH,OAAO,MAAM,cAAc,CAAC,UAAU,CAAC,CAAC;QAC1C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,4BAA4B,UAAU,MAAO,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;IAED,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IACxB,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IAEvB,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,MAAM,cAAc,CAAC,OAAO,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,6BAA6B;QAC/B,CAAC;QAED,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG,IAAI,GAAG,KAAK,IAAI;YAAE,MAAM;QAC1C,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAoC;IAC7D,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACjD,MAAM,EAAE,EAAE;IACV,QAAQ,EAAE,CAAC;CACZ,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Severity } from './severity.js';
|
|
2
|
+
import type { ClientId } from './rule.js';
|
|
3
|
+
export interface Fix {
|
|
4
|
+
description: string;
|
|
5
|
+
apply: (schema: Record<string, unknown>) => Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface Diagnostic {
|
|
8
|
+
ruleId: string;
|
|
9
|
+
severity: Severity;
|
|
10
|
+
message: string;
|
|
11
|
+
toolName: string;
|
|
12
|
+
path: string;
|
|
13
|
+
clients: ClientId[];
|
|
14
|
+
fix?: Fix;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostic.js","sourceRoot":"","sources":["../../src/core/diagnostic.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Rule, MCPTool, Config } from './rule.js';
|
|
2
|
+
import type { Diagnostic } from './diagnostic.js';
|
|
3
|
+
export declare class LintEngine {
|
|
4
|
+
private readonly rules;
|
|
5
|
+
private readonly config;
|
|
6
|
+
constructor(rules: Rule[], config: Config);
|
|
7
|
+
lint(tools: MCPTool[]): Diagnostic[];
|
|
8
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class LintEngine {
|
|
2
|
+
rules;
|
|
3
|
+
config;
|
|
4
|
+
constructor(rules, config) {
|
|
5
|
+
this.rules = rules;
|
|
6
|
+
this.config = config;
|
|
7
|
+
}
|
|
8
|
+
lint(tools) {
|
|
9
|
+
const context = { config: this.config };
|
|
10
|
+
const diagnostics = [];
|
|
11
|
+
for (const tool of tools) {
|
|
12
|
+
if (this.config.ignore?.includes(tool.name))
|
|
13
|
+
continue;
|
|
14
|
+
for (const rule of this.rules) {
|
|
15
|
+
const severityOverride = this.config.rules?.[rule.id];
|
|
16
|
+
if (severityOverride === 'off')
|
|
17
|
+
continue;
|
|
18
|
+
// Filter by clients: skip rule if none of its clients are in config.clients
|
|
19
|
+
if (this.config.clients &&
|
|
20
|
+
this.config.clients.length > 0 &&
|
|
21
|
+
!rule.clients.some((c) => this.config.clients.includes(c))) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const ruleDiagnostics = rule.check(tool, context);
|
|
25
|
+
for (const diag of ruleDiagnostics) {
|
|
26
|
+
diagnostics.push(severityOverride
|
|
27
|
+
? { ...diag, severity: severityOverride }
|
|
28
|
+
: diag);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return diagnostics;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AAIA,MAAM,OAAO,UAAU;IAEF;IACA;IAFnB,YACmB,KAAa,EACb,MAAc;QADd,UAAK,GAAL,KAAK,CAAQ;QACb,WAAM,GAAN,MAAM,CAAQ;IAC9B,CAAC;IAEJ,IAAI,CAAC,KAAgB;QACnB,MAAM,OAAO,GAAgB,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;QACrD,MAAM,WAAW,GAAiB,EAAE,CAAC;QAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,SAAS;YAEtD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBAC9B,MAAM,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACtD,IAAI,gBAAgB,KAAK,KAAK;oBAAE,SAAS;gBAEzC,4EAA4E;gBAC5E,IACE,IAAI,CAAC,MAAM,CAAC,OAAO;oBACnB,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;oBAC9B,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,OAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAC3D,CAAC;oBACD,SAAS;gBACX,CAAC;gBAED,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;gBAElD,KAAK,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC;oBACnC,WAAW,CAAC,IAAI,CACd,gBAAgB;wBACd,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,gBAA4B,EAAE;wBACrD,CAAC,CAAC,IAAI,CACT,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;CACF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function parsePath(path) {
|
|
2
|
+
return path.split('.').flatMap((part) => {
|
|
3
|
+
const m = part.match(/^([^\[]+)\[(\d+)\]$/);
|
|
4
|
+
return m ? [m[1], m[2]] : [part];
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
export function applyFixes(tools, diagnostics, onlyRules) {
|
|
8
|
+
const fixable = diagnostics.filter((d) => d.fix != null && (!onlyRules || onlyRules.includes(d.ruleId)));
|
|
9
|
+
if (fixable.length === 0)
|
|
10
|
+
return tools;
|
|
11
|
+
const result = structuredClone(tools);
|
|
12
|
+
for (const diag of fixable) {
|
|
13
|
+
const toolIndex = result.findIndex((t) => t.name === diag.toolName);
|
|
14
|
+
if (toolIndex === -1)
|
|
15
|
+
continue;
|
|
16
|
+
const parts = parsePath(diag.path);
|
|
17
|
+
if (parts.length === 0)
|
|
18
|
+
continue;
|
|
19
|
+
const toolObj = result[toolIndex];
|
|
20
|
+
let parent = toolObj;
|
|
21
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
22
|
+
const next = parent[parts[i]];
|
|
23
|
+
if (next == null || typeof next !== 'object') {
|
|
24
|
+
parent = {}; // will be skipped below
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
parent = next;
|
|
28
|
+
}
|
|
29
|
+
const lastKey = parts[parts.length - 1];
|
|
30
|
+
const node = parent[lastKey];
|
|
31
|
+
if (node != null && typeof node === 'object') {
|
|
32
|
+
parent[lastKey] = diag.fix.apply(node);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=fixer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixer.js","sourceRoot":"","sources":["../../src/core/fixer.ts"],"names":[],"mappings":"AAGA,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACtC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;QAC5C,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,KAAgB,EAChB,WAAyB,EACzB,SAAoB;IAEpB,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAChC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,CAAC,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CACrE,CAAC;IACF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAEvC,MAAM,MAAM,GAAc,eAAe,CAAC,KAAK,CAAC,CAAC;IAEjD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpE,IAAI,SAAS,KAAK,CAAC,CAAC;YAAE,SAAS;QAE/B,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAEjC,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAuC,CAAC;QAExE,IAAI,MAAM,GAA4B,OAAO,CAAC;QAC9C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC7C,MAAM,GAAG,EAA6B,CAAC,CAAC,wBAAwB;gBAChE,MAAM;YACR,CAAC;YACD,MAAM,GAAG,IAA+B,CAAC;QAC3C,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7C,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,GAAI,CAAC,KAAK,CAAC,IAA+B,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Severity } from './severity.js';
|
|
2
|
+
export type { Severity } from './severity.js';
|
|
3
|
+
import type { Diagnostic } from './diagnostic.js';
|
|
4
|
+
export type ClientId = 'claude' | 'cursor' | 'gemini' | 'vscode';
|
|
5
|
+
export interface JSONSchema {
|
|
6
|
+
type?: string | string[];
|
|
7
|
+
properties?: Record<string, JSONSchema>;
|
|
8
|
+
required?: string[] | boolean;
|
|
9
|
+
items?: JSONSchema | JSONSchema[];
|
|
10
|
+
enum?: unknown[];
|
|
11
|
+
$ref?: string;
|
|
12
|
+
$defs?: Record<string, JSONSchema>;
|
|
13
|
+
definitions?: Record<string, JSONSchema>;
|
|
14
|
+
if?: JSONSchema;
|
|
15
|
+
then?: JSONSchema;
|
|
16
|
+
else?: JSONSchema;
|
|
17
|
+
oneOf?: JSONSchema[];
|
|
18
|
+
anyOf?: JSONSchema[];
|
|
19
|
+
allOf?: JSONSchema[];
|
|
20
|
+
not?: JSONSchema;
|
|
21
|
+
patternProperties?: Record<string, JSONSchema>;
|
|
22
|
+
additionalItems?: JSONSchema | boolean;
|
|
23
|
+
dependencies?: Record<string, JSONSchema | string[]>;
|
|
24
|
+
$dynamicRef?: string;
|
|
25
|
+
contentEncoding?: string;
|
|
26
|
+
format?: string;
|
|
27
|
+
default?: unknown;
|
|
28
|
+
description?: string;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface MCPTool {
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
inputSchema: JSONSchema;
|
|
35
|
+
}
|
|
36
|
+
export interface Config {
|
|
37
|
+
rules?: Record<string, Severity | 'off'>;
|
|
38
|
+
clients?: ClientId[];
|
|
39
|
+
ignore?: string[];
|
|
40
|
+
maxDepth?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface RuleContext {
|
|
43
|
+
config: Config;
|
|
44
|
+
}
|
|
45
|
+
export interface Rule {
|
|
46
|
+
id: string;
|
|
47
|
+
severity: Severity;
|
|
48
|
+
description: string;
|
|
49
|
+
url?: string;
|
|
50
|
+
clients: ClientId[];
|
|
51
|
+
check: (tool: MCPTool, context: RuleContext) => Diagnostic[];
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.js","sourceRoot":"","sources":["../../src/core/rule.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Severity = 'error' | 'warning' | 'info';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"severity.js","sourceRoot":"","sources":["../../src/core/severity.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerCheckCommand } from './cli/commands/check.js';
|
|
4
|
+
import { registerFixCommand } from './cli/commands/fix.js';
|
|
5
|
+
import { registerInitCommand } from './cli/commands/init.js';
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name('mcp-lint')
|
|
9
|
+
.description('Lint MCP server tool schemas for cross-client compatibility')
|
|
10
|
+
.version('0.1.0');
|
|
11
|
+
registerCheckCommand(program);
|
|
12
|
+
registerFixCommand(program);
|
|
13
|
+
registerInitCommand(program);
|
|
14
|
+
program.parse();
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,UAAU,CAAC;KAChB,WAAW,CAAC,6DAA6D,CAAC;KAC1E,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAC9B,kBAAkB,CAAC,OAAO,CAAC,CAAC;AAC5B,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAE7B,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { extname } from 'node:path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
export async function loadFile(filePath) {
|
|
5
|
+
let content;
|
|
6
|
+
try {
|
|
7
|
+
content = await readFile(filePath, 'utf-8');
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
throw new Error(`Cannot read file "${filePath}": ${err.message}`);
|
|
11
|
+
}
|
|
12
|
+
const ext = extname(filePath).toLowerCase();
|
|
13
|
+
let data;
|
|
14
|
+
try {
|
|
15
|
+
if (ext === '.yaml' || ext === '.yml') {
|
|
16
|
+
data = parseYaml(content);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
data = JSON.parse(content);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
throw new Error(`Failed to parse "${filePath}": ${err.message}`);
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(data)) {
|
|
26
|
+
throw new Error(`Expected an array of MCP tools in "${filePath}", got ${typeof data}`);
|
|
27
|
+
}
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=file-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-loader.js","sourceRoot":"","sources":["../../src/loaders/file-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAG1C,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,QAAgB;IAC7C,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,MAAO,GAA6B,CAAC,OAAO,EAAE,CAAC,CAAC;IAC/F,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5C,IAAI,IAAa,CAAC;IAElB,IAAI,CAAC;QACH,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;YACtC,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC;aAAM,CAAC;YACN,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,oBAAoB,QAAQ,MAAO,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,sCAAsC,QAAQ,UAAU,OAAO,IAAI,EAAE,CAAC,CAAC;IACzF,CAAC;IAED,OAAO,IAAiB,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
3
|
+
export async function loadSSE(url) {
|
|
4
|
+
const transport = new SSEClientTransport(new URL(url));
|
|
5
|
+
const client = new Client({ name: 'mcp-lint', version: '0.1.0' }, { capabilities: {} });
|
|
6
|
+
await client.connect(transport);
|
|
7
|
+
try {
|
|
8
|
+
const result = await client.listTools();
|
|
9
|
+
return result.tools.map((t) => ({
|
|
10
|
+
name: t.name,
|
|
11
|
+
description: t.description,
|
|
12
|
+
inputSchema: t.inputSchema,
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await client.close();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=sse-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-loader.js","sourceRoot":"","sources":["../../src/loaders/sse-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAG7E,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,GAAW;IACvC,MAAM,SAAS,GAAG,IAAI,kBAAkB,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;IAExF,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;QACxC,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9B,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,WAAW,EAAE,CAAC,CAAC,WAAqC;SACrD,CAAC,CAAC,CAAC;IACN,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
export async function loadStdio(command, args) {
|
|
4
|
+
const transport = new StdioClientTransport({ command, args });
|
|
5
|
+
const client = new Client({ name: 'mcp-lint', version: '0.1.0' }, { capabilities: {} });
|
|
6
|
+
await client.connect(transport);
|
|
7
|
+
try {
|
|
8
|
+
const result = await client.listTools();
|
|
9
|
+
return result.tools.map((t) => ({
|
|
10
|
+
name: t.name,
|
|
11
|
+
description: t.description,
|
|
12
|
+
inputSchema: t.inputSchema,
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await client.close();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=stdio-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-loader.js","sourceRoot":"","sources":["../../src/loaders/stdio-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAGjF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAe,EAAE,IAAc;IAC7D,MAAM,SAAS,GAAG,IAAI,oBAAoB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;IAExF,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;QACxC,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9B,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,WAAW,EAAE,CAAC,CAAC,WAAqC;SACrD,CAAC,CAAC,CAAC;IACN,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { walkSchema } from '../../utils/schema-walker.js';
|
|
2
|
+
export const claudeNoTypeArray = {
|
|
3
|
+
id: 'claude/no-type-array',
|
|
4
|
+
severity: 'warning',
|
|
5
|
+
description: 'Claude may not handle array syntax for `type` (e.g. `["string", "null"]`) correctly.',
|
|
6
|
+
clients: ['claude'],
|
|
7
|
+
check(tool, _context) {
|
|
8
|
+
const diagnostics = [];
|
|
9
|
+
walkSchema(tool.inputSchema, (schema, path) => {
|
|
10
|
+
if (Array.isArray(schema.type)) {
|
|
11
|
+
const diagnosticPath = path ? `inputSchema.${path}` : 'inputSchema';
|
|
12
|
+
const primaryType = schema.type.find((t) => t !== 'null') ?? schema.type[0];
|
|
13
|
+
diagnostics.push({
|
|
14
|
+
ruleId: 'claude/no-type-array',
|
|
15
|
+
severity: 'warning',
|
|
16
|
+
message: `Array type syntax \`"type": ${JSON.stringify(schema.type)}\` at "${diagnosticPath}" may not be handled correctly by Claude. Use a single type string.`,
|
|
17
|
+
toolName: tool.name,
|
|
18
|
+
path: diagnosticPath,
|
|
19
|
+
clients: ['claude'],
|
|
20
|
+
fix: {
|
|
21
|
+
description: `Convert to single type "${primaryType}"`,
|
|
22
|
+
apply: (s) => ({ ...s, type: primaryType }),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return diagnostics;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=claude.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude.js","sourceRoot":"","sources":["../../../src/rules/client-specific/claude.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE1D,MAAM,CAAC,MAAM,iBAAiB,GAAS;IACrC,EAAE,EAAE,sBAAsB;IAC1B,QAAQ,EAAE,SAAS;IACnB,WAAW,EAAE,sFAAsF;IACnG,OAAO,EAAE,CAAC,QAAQ,CAAC;IAEnB,KAAK,CAAC,IAAa,EAAE,QAAqB;QACxC,MAAM,WAAW,GAAiB,EAAE,CAAC;QAErC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE;YAC5C,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC/B,MAAM,cAAc,GAAG,IAAI,CAAC,CAAC,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;gBACpE,MAAM,WAAW,GAAI,MAAM,CAAC,IAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC1F,WAAW,CAAC,IAAI,CAAC;oBACf,MAAM,EAAE,sBAAsB;oBAC9B,QAAQ,EAAE,SAAS;oBACnB,OAAO,EAAE,+BAA+B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,cAAc,qEAAqE;oBAChK,QAAQ,EAAE,IAAI,CAAC,IAAI;oBACnB,IAAI,EAAE,cAAc;oBACpB,OAAO,EAAE,CAAC,QAAQ,CAAC;oBACnB,GAAG,EAAE;wBACH,WAAW,EAAE,2BAA2B,WAAW,GAAG;wBACtD,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;qBAC5C;iBACF,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,WAAW,CAAC;IACrB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { walkSchema } from '../../utils/schema-walker.js';
|
|
2
|
+
function inferType(value) {
|
|
3
|
+
if (value === null)
|
|
4
|
+
return undefined;
|
|
5
|
+
if (typeof value === 'string')
|
|
6
|
+
return 'string';
|
|
7
|
+
if (typeof value === 'number')
|
|
8
|
+
return Number.isInteger(value) ? 'integer' : 'number';
|
|
9
|
+
if (typeof value === 'boolean')
|
|
10
|
+
return 'boolean';
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return 'array';
|
|
13
|
+
if (typeof value === 'object')
|
|
14
|
+
return 'object';
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
export const cursorNoDefaultWithoutType = {
|
|
18
|
+
id: 'cursor/no-default-without-type',
|
|
19
|
+
severity: 'error',
|
|
20
|
+
description: 'Cursor requires an explicit `type` when a `default` value is present.',
|
|
21
|
+
clients: ['cursor'],
|
|
22
|
+
check(tool, _context) {
|
|
23
|
+
const diagnostics = [];
|
|
24
|
+
walkSchema(tool.inputSchema, (schema, path) => {
|
|
25
|
+
if (schema.default !== undefined && schema.type === undefined) {
|
|
26
|
+
const diagnosticPath = path ? `inputSchema.${path}` : 'inputSchema';
|
|
27
|
+
const inferred = inferType(schema.default);
|
|
28
|
+
diagnostics.push({
|
|
29
|
+
ruleId: 'cursor/no-default-without-type',
|
|
30
|
+
severity: 'error',
|
|
31
|
+
message: `Property at "${diagnosticPath}" has a \`default\` value but no \`type\`. Cursor requires an explicit type when defaults are present.`,
|
|
32
|
+
toolName: tool.name,
|
|
33
|
+
path: diagnosticPath,
|
|
34
|
+
clients: ['cursor'],
|
|
35
|
+
...(inferred
|
|
36
|
+
? {
|
|
37
|
+
fix: {
|
|
38
|
+
description: `Add \`"type": "${inferred}"\` inferred from the default value`,
|
|
39
|
+
apply: (s) => ({ ...s, type: inferred }),
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
: {}),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return diagnostics;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../../src/rules/client-specific/cursor.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE1D,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;IACrF,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACjD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACzC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC/C,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,MAAM,0BAA0B,GAAS;IAC9C,EAAE,EAAE,gCAAgC;IACpC,QAAQ,EAAE,OAAO;IACjB,WAAW,EAAE,uEAAuE;IACpF,OAAO,EAAE,CAAC,QAAQ,CAAC;IAEnB,KAAK,CAAC,IAAa,EAAE,QAAqB;QACxC,MAAM,WAAW,GAAiB,EAAE,CAAC;QAErC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE;YAC5C,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC9D,MAAM,cAAc,GAAG,IAAI,CAAC,CAAC,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;gBACpE,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAC3C,WAAW,CAAC,IAAI,CAAC;oBACf,MAAM,EAAE,gCAAgC;oBACxC,QAAQ,EAAE,OAAO;oBACjB,OAAO,EAAE,gBAAgB,cAAc,wGAAwG;oBAC/I,QAAQ,EAAE,IAAI,CAAC,IAAI;oBACnB,IAAI,EAAE,cAAc;oBACpB,OAAO,EAAE,CAAC,QAAQ,CAAC;oBACnB,GAAG,CAAC,QAAQ;wBACV,CAAC,CAAC;4BACE,GAAG,EAAE;gCACH,WAAW,EAAE,kBAAkB,QAAQ,qCAAqC;gCAC5E,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;6BACzC;yBACF;wBACH,CAAC,CAAC,EAAE,CAAC;iBACR,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,WAAW,CAAC;IACrB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const geminiNoOptionalWithoutDefault = {
|
|
2
|
+
id: 'gemini/no-optional-without-default',
|
|
3
|
+
severity: 'warning',
|
|
4
|
+
description: 'Gemini handles optional parameters better when they have explicit default values.',
|
|
5
|
+
clients: ['gemini'],
|
|
6
|
+
check(tool, _context) {
|
|
7
|
+
const diagnostics = [];
|
|
8
|
+
const { properties, required } = tool.inputSchema;
|
|
9
|
+
if (!properties)
|
|
10
|
+
return diagnostics;
|
|
11
|
+
const requiredSet = new Set(Array.isArray(required) ? required : []);
|
|
12
|
+
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
13
|
+
if (!requiredSet.has(propName) && propSchema.default === undefined) {
|
|
14
|
+
diagnostics.push({
|
|
15
|
+
ruleId: 'gemini/no-optional-without-default',
|
|
16
|
+
severity: 'warning',
|
|
17
|
+
message: `Optional parameter "${propName}" in tool "${tool.name}" has no \`default\` value. Gemini handles optional params better with explicit defaults.`,
|
|
18
|
+
toolName: tool.name,
|
|
19
|
+
path: `inputSchema.properties.${propName}`,
|
|
20
|
+
clients: ['gemini'],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return diagnostics;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
function getObjectDepth(schema, depth = 0) {
|
|
28
|
+
if (!schema || typeof schema !== 'object')
|
|
29
|
+
return depth;
|
|
30
|
+
if (!schema.properties)
|
|
31
|
+
return depth;
|
|
32
|
+
let max = depth;
|
|
33
|
+
for (const child of Object.values(schema.properties)) {
|
|
34
|
+
if (child.type === 'object' || child.properties) {
|
|
35
|
+
max = Math.max(max, getObjectDepth(child, depth + 1));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return max;
|
|
39
|
+
}
|
|
40
|
+
export const geminiNoNestedObjects = {
|
|
41
|
+
id: 'gemini/no-nested-objects',
|
|
42
|
+
severity: 'warning',
|
|
43
|
+
description: 'Gemini has limited support for object properties nested more than 2 levels deep.',
|
|
44
|
+
clients: ['gemini'],
|
|
45
|
+
check(tool, _context) {
|
|
46
|
+
const depth = getObjectDepth(tool.inputSchema);
|
|
47
|
+
if (depth > 2) {
|
|
48
|
+
return [{
|
|
49
|
+
ruleId: 'gemini/no-nested-objects',
|
|
50
|
+
severity: 'warning',
|
|
51
|
+
message: `Tool "${tool.name}" has object properties nested ${depth} level(s) deep. Gemini's function calling has limited support beyond 2 levels.`,
|
|
52
|
+
toolName: tool.name,
|
|
53
|
+
path: 'inputSchema',
|
|
54
|
+
clients: ['gemini'],
|
|
55
|
+
}];
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=gemini.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../../src/rules/client-specific/gemini.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,8BAA8B,GAAS;IAClD,EAAE,EAAE,oCAAoC;IACxC,QAAQ,EAAE,SAAS;IACnB,WAAW,EAAE,mFAAmF;IAChG,OAAO,EAAE,CAAC,QAAQ,CAAC;IAEnB,KAAK,CAAC,IAAa,EAAE,QAAqB;QACxC,MAAM,WAAW,GAAiB,EAAE,CAAC;QACrC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC;QAClD,IAAI,CAAC,UAAU;YAAE,OAAO,WAAW,CAAC;QAEpC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAErE,KAAK,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;YAChE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,UAAU,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBACnE,WAAW,CAAC,IAAI,CAAC;oBACf,MAAM,EAAE,oCAAoC;oBAC5C,QAAQ,EAAE,SAAS;oBACnB,OAAO,EAAE,uBAAuB,QAAQ,cAAc,IAAI,CAAC,IAAI,2FAA2F;oBAC1J,QAAQ,EAAE,IAAI,CAAC,IAAI;oBACnB,IAAI,EAAE,0BAA0B,QAAQ,EAAE;oBAC1C,OAAO,EAAE,CAAC,QAAQ,CAAC;iBACpB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;CACF,CAAC;AAEF,SAAS,cAAc,CAAC,MAAkB,EAAE,KAAK,GAAG,CAAC;IACnD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACxD,IAAI,CAAC,MAAM,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAErC,IAAI,GAAG,GAAG,KAAK,CAAC;IAChB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QACrD,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;YAChD,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAS;IACzC,EAAE,EAAE,0BAA0B;IAC9B,QAAQ,EAAE,SAAS;IACnB,WAAW,EAAE,kFAAkF;IAC/F,OAAO,EAAE,CAAC,QAAQ,CAAC;IAEnB,KAAK,CAAC,IAAa,EAAE,QAAqB;QACxC,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC/C,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,OAAO,CAAC;oBACN,MAAM,EAAE,0BAA0B;oBAClC,QAAQ,EAAE,SAAS;oBACnB,OAAO,EAAE,SAAS,IAAI,CAAC,IAAI,kCAAkC,KAAK,gFAAgF;oBAClJ,QAAQ,EAAE,IAAI,CAAC,IAAI;oBACnB,IAAI,EAAE,aAAa;oBACnB,OAAO,EAAE,CAAC,QAAQ,CAAC;iBACpB,CAAC,CAAC;QACL,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const MAX_PARAMS = 15;
|
|
2
|
+
export const vscodeMaxParams = {
|
|
3
|
+
id: 'vscode/max-params',
|
|
4
|
+
severity: 'warning',
|
|
5
|
+
description: `VS Code Copilot performance degrades with more than ${MAX_PARAMS} parameters.`,
|
|
6
|
+
clients: ['vscode'],
|
|
7
|
+
check(tool, _context) {
|
|
8
|
+
const count = Object.keys(tool.inputSchema.properties ?? {}).length;
|
|
9
|
+
if (count > MAX_PARAMS) {
|
|
10
|
+
return [{
|
|
11
|
+
ruleId: 'vscode/max-params',
|
|
12
|
+
severity: 'warning',
|
|
13
|
+
message: `Tool "${tool.name}" has ${count} parameters. VS Code Copilot performance degrades with more than ${MAX_PARAMS}. Consider splitting into multiple tools.`,
|
|
14
|
+
toolName: tool.name,
|
|
15
|
+
path: 'inputSchema.properties',
|
|
16
|
+
clients: ['vscode'],
|
|
17
|
+
}];
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=vscode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vscode.js","sourceRoot":"","sources":["../../../src/rules/client-specific/vscode.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB,MAAM,CAAC,MAAM,eAAe,GAAS;IACnC,EAAE,EAAE,mBAAmB;IACvB,QAAQ,EAAE,SAAS;IACnB,WAAW,EAAE,uDAAuD,UAAU,cAAc;IAC5F,OAAO,EAAE,CAAC,QAAQ,CAAC;IAEnB,KAAK,CAAC,IAAa,EAAE,QAAqB;QACxC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACpE,IAAI,KAAK,GAAG,UAAU,EAAE,CAAC;YACvB,OAAO,CAAC;oBACN,MAAM,EAAE,mBAAmB;oBAC3B,QAAQ,EAAE,SAAS;oBACnB,OAAO,EAAE,SAAS,IAAI,CAAC,IAAI,SAAS,KAAK,oEAAoE,UAAU,2CAA2C;oBAClK,QAAQ,EAAE,IAAI,CAAC,IAAI;oBACnB,IAAI,EAAE,wBAAwB;oBAC9B,OAAO,EAAE,CAAC,QAAQ,CAAC;iBACpB,CAAC,CAAC;QACL,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const descriptionExists = {
|
|
2
|
+
id: 'description-exists',
|
|
3
|
+
severity: 'warning',
|
|
4
|
+
description: 'Tools and parameters should have descriptions for better LLM tool-use quality.',
|
|
5
|
+
clients: ['claude', 'cursor', 'gemini', 'vscode'],
|
|
6
|
+
check(tool, _context) {
|
|
7
|
+
const diagnostics = [];
|
|
8
|
+
if (!tool.description || tool.description.trim() === '') {
|
|
9
|
+
diagnostics.push({
|
|
10
|
+
ruleId: 'description-exists',
|
|
11
|
+
severity: 'warning',
|
|
12
|
+
message: `Tool "${tool.name}" is missing a description. All clients use descriptions for prompt construction.`,
|
|
13
|
+
toolName: tool.name,
|
|
14
|
+
path: 'description',
|
|
15
|
+
clients: ['claude', 'cursor', 'gemini', 'vscode'],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (tool.inputSchema.properties) {
|
|
19
|
+
for (const [propName, propSchema] of Object.entries(tool.inputSchema.properties)) {
|
|
20
|
+
if (!propSchema.description || propSchema.description.trim() === '') {
|
|
21
|
+
diagnostics.push({
|
|
22
|
+
ruleId: 'description-exists',
|
|
23
|
+
severity: 'warning',
|
|
24
|
+
message: `Parameter "${propName}" in tool "${tool.name}" is missing a description.`,
|
|
25
|
+
toolName: tool.name,
|
|
26
|
+
path: `inputSchema.properties.${propName}`,
|
|
27
|
+
clients: ['claude', 'cursor', 'gemini', 'vscode'],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return diagnostics;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=description-exists.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"description-exists.js","sourceRoot":"","sources":["../../src/rules/description-exists.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,iBAAiB,GAAS;IACrC,EAAE,EAAE,oBAAoB;IACxB,QAAQ,EAAE,SAAS;IACnB,WAAW,EAAE,gFAAgF;IAC7F,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC;IAEjD,KAAK,CAAC,IAAa,EAAE,QAAqB;QACxC,MAAM,WAAW,GAAiB,EAAE,CAAC;QAErC,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACxD,WAAW,CAAC,IAAI,CAAC;gBACf,MAAM,EAAE,oBAAoB;gBAC5B,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,SAAS,IAAI,CAAC,IAAI,mFAAmF;gBAC9G,QAAQ,EAAE,IAAI,CAAC,IAAI;gBACnB,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC;aAClD,CAAC,CAAC;QACL,CAAC;QAED,IAAI,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC;YAChC,KAAK,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC;gBACjF,IAAI,CAAC,UAAU,CAAC,WAAW,IAAI,UAAU,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;oBACpE,WAAW,CAAC,IAAI,CAAC;wBACf,MAAM,EAAE,oBAAoB;wBAC5B,QAAQ,EAAE,SAAS;wBACnB,OAAO,EAAE,cAAc,QAAQ,cAAc,IAAI,CAAC,IAAI,6BAA6B;wBACnF,QAAQ,EAAE,IAAI,CAAC,IAAI;wBACnB,IAAI,EAAE,0BAA0B,QAAQ,EAAE;wBAC1C,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC;qBAClD,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;CACF,CAAC"}
|