mcp-tool-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.
Files changed (46) hide show
  1. package/README.md +269 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +70 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +3 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/linter.d.ts +11 -0
  11. package/dist/linter.d.ts.map +1 -0
  12. package/dist/linter.js +28 -0
  13. package/dist/linter.js.map +1 -0
  14. package/dist/rules/description-has-verb.d.ts +3 -0
  15. package/dist/rules/description-has-verb.d.ts.map +1 -0
  16. package/dist/rules/description-has-verb.js +59 -0
  17. package/dist/rules/description-has-verb.js.map +1 -0
  18. package/dist/rules/description-length.d.ts +3 -0
  19. package/dist/rules/description-length.d.ts.map +1 -0
  20. package/dist/rules/description-length.js +29 -0
  21. package/dist/rules/description-length.js.map +1 -0
  22. package/dist/rules/index.d.ts +10 -0
  23. package/dist/rules/index.d.ts.map +1 -0
  24. package/dist/rules/index.js +16 -0
  25. package/dist/rules/index.js.map +1 -0
  26. package/dist/rules/no-duplicate-names.d.ts +3 -0
  27. package/dist/rules/no-duplicate-names.d.ts.map +1 -0
  28. package/dist/rules/no-duplicate-names.js +21 -0
  29. package/dist/rules/no-duplicate-names.js.map +1 -0
  30. package/dist/rules/no-vague-verbs.d.ts +3 -0
  31. package/dist/rules/no-vague-verbs.d.ts.map +1 -0
  32. package/dist/rules/no-vague-verbs.js +38 -0
  33. package/dist/rules/no-vague-verbs.js.map +1 -0
  34. package/dist/rules/require-param-descriptions.d.ts +3 -0
  35. package/dist/rules/require-param-descriptions.d.ts.map +1 -0
  36. package/dist/rules/require-param-descriptions.js +23 -0
  37. package/dist/rules/require-param-descriptions.js.map +1 -0
  38. package/dist/rules/require-required-array.d.ts +3 -0
  39. package/dist/rules/require-required-array.d.ts.map +1 -0
  40. package/dist/rules/require-required-array.js +21 -0
  41. package/dist/rules/require-required-array.js.map +1 -0
  42. package/dist/types.d.ts +33 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +33 -0
package/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # mcp-tool-lint
2
+
3
+ Static linter for [MCP (Model Context Protocol)](https://modelcontextprotocol.io) tool definitions. Catches quality defects before deployment so AI agents use your tools correctly.
4
+
5
+ A February 2026 research paper analyzing 1,899 MCP tools from 200 servers found that **97.1% have at least one quality defect** -- unclear descriptions, missing parameter documentation, vague naming, and more. These defects cause AI agents to misuse tools, leading to failed actions and poor user experiences.
6
+
7
+ `mcp-tool-lint` catches these issues statically, before your tools reach production.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install mcp-tool-lint
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### CLI
18
+
19
+ ```bash
20
+ # Lint a JSON file of tool definitions
21
+ npx mcp-tool-lint tools.json
22
+ ```
23
+
24
+ The JSON file should contain a single tool object or an array of tool objects:
25
+
26
+ ```json
27
+ [
28
+ {
29
+ "name": "get_user",
30
+ "description": "Retrieves a user by their unique identifier from the database",
31
+ "inputSchema": {
32
+ "type": "object",
33
+ "properties": {
34
+ "userId": { "type": "string", "description": "The unique user ID" }
35
+ },
36
+ "required": ["userId"]
37
+ }
38
+ }
39
+ ]
40
+ ```
41
+
42
+ Output:
43
+
44
+ ```
45
+ ✓ get_user (0 issues)
46
+ ```
47
+
48
+ ### Programmatic API
49
+
50
+ ```typescript
51
+ import { lintTools, validateTools } from 'mcp-tool-lint';
52
+
53
+ const tools = [
54
+ {
55
+ name: 'get_data',
56
+ description: 'gets data',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ id: { type: 'string' },
61
+ },
62
+ },
63
+ },
64
+ ];
65
+
66
+ // Get detailed results
67
+ const results = lintTools(tools);
68
+ for (const result of results) {
69
+ console.log(`${result.tool}: ${result.passed ? 'PASSED' : 'FAILED'}`);
70
+ for (const issue of result.issues) {
71
+ console.log(` [${issue.severity}] ${issue.rule}: ${issue.message}`);
72
+ }
73
+ }
74
+
75
+ // Quick pass/fail check (true if no error-severity issues)
76
+ const ok = validateTools(tools);
77
+ ```
78
+
79
+ ## Rules
80
+
81
+ ### `description-length`
82
+
83
+ **Severity:** warn (error if under 10 characters)
84
+
85
+ Tool descriptions must be at least 20 characters. Descriptions under 10 characters are flagged as errors.
86
+
87
+ | Status | Example |
88
+ |--------|---------|
89
+ | Pass | `"Searches the product catalog by keyword and returns matching items"` |
90
+ | Warn | `"Gets user data"` (15 chars) |
91
+ | Error | `"Get data"` (8 chars) |
92
+
93
+ ### `require-param-descriptions`
94
+
95
+ **Severity:** warn
96
+
97
+ Every property in `inputSchema.properties` should have a `description` field. Without descriptions, AI agents must guess what values to pass.
98
+
99
+ | Status | Example |
100
+ |--------|---------|
101
+ | Pass | `{ "userId": { "type": "string", "description": "The unique user ID" } }` |
102
+ | Fail | `{ "userId": { "type": "string" } }` |
103
+
104
+ ### `no-vague-verbs`
105
+
106
+ **Severity:** warn
107
+
108
+ Descriptions should not start with or prominently use vague verbs. The following are flagged:
109
+
110
+ - "does", "handles", "manages", "works with", "interacts with", "deals with", "takes care of", "is responsible for", "processes"
111
+
112
+ Use specific verbs like: creates, returns, searches, deletes, updates, fetches, sends, validates, transforms, filters.
113
+
114
+ | Status | Example |
115
+ |--------|---------|
116
+ | Pass | `"Creates a payment record in the billing system"` |
117
+ | Fail | `"Handles payments for the checkout flow"` |
118
+
119
+ ### `require-required-array`
120
+
121
+ **Severity:** warn
122
+
123
+ If `inputSchema.properties` has entries, `inputSchema.required` should be present (even if empty `[]`) to make intent explicit. Without it, consumers cannot distinguish "all optional" from "the author forgot."
124
+
125
+ | Status | Example |
126
+ |--------|---------|
127
+ | Pass | `{ "properties": { "id": ... }, "required": ["id"] }` |
128
+ | Pass | `{ "properties": { "id": ... }, "required": [] }` |
129
+ | Fail | `{ "properties": { "id": ... } }` (no required field) |
130
+
131
+ ### `description-has-verb`
132
+
133
+ **Severity:** warn
134
+
135
+ Descriptions should contain at least one action verb to convey what the tool does. Checked against a list of 200+ common action verbs.
136
+
137
+ | Status | Example |
138
+ |--------|---------|
139
+ | Pass | `"Creates a new user account"` |
140
+ | Fail | `"A utility for user data in the system"` |
141
+
142
+ ### `no-duplicate-names`
143
+
144
+ **Severity:** error
145
+
146
+ When linting multiple tools, no two tools should have the same `name`. Duplicate names cause ambiguity for AI agents selecting tools.
147
+
148
+ | Status | Example |
149
+ |--------|---------|
150
+ | Pass | Tools named `get_user`, `create_user`, `delete_user` |
151
+ | Fail | Two tools both named `get_user` |
152
+
153
+ ## CLI Usage
154
+
155
+ ```bash
156
+ # Basic usage
157
+ npx mcp-tool-lint tools.json
158
+
159
+ # Show help
160
+ npx mcp-tool-lint --help
161
+
162
+ # Show version
163
+ npx mcp-tool-lint --version
164
+ ```
165
+
166
+ Exit codes:
167
+ - `0` -- all tools pass (warnings are OK)
168
+ - `1` -- at least one tool has an error-severity issue
169
+
170
+ Output format:
171
+
172
+ ```
173
+ ✗ get_data (2 issues)
174
+ warn [description-length] Description is too short (8 chars, min 20)
175
+ warn [require-param-descriptions] Parameter 'id' has no description
176
+ ✓ create_user (0 issues)
177
+ ```
178
+
179
+ ## Custom Rules
180
+
181
+ You can pass custom rules to `lintTools`:
182
+
183
+ ```typescript
184
+ import { lintTools, Rule, McpToolDefinition, LintIssue } from 'mcp-tool-lint';
185
+
186
+ const noUnderscores: Rule = {
187
+ name: 'no-underscores-in-name',
188
+ description: 'Tool names should use camelCase, not snake_case',
189
+ check(tool: McpToolDefinition): LintIssue[] {
190
+ if (tool.name.includes('_')) {
191
+ return [{
192
+ rule: 'no-underscores-in-name',
193
+ severity: 'warn',
194
+ message: `Tool name "${tool.name}" uses underscores; consider camelCase`,
195
+ tool: tool.name,
196
+ field: 'name',
197
+ }];
198
+ }
199
+ return [];
200
+ },
201
+ };
202
+
203
+ const results = lintTools(tools, { rules: [noUnderscores] });
204
+ ```
205
+
206
+ You can also override severity for built-in rules:
207
+
208
+ ```typescript
209
+ const results = lintTools(tools, {
210
+ severity: {
211
+ 'description-length': 'error', // Upgrade to error
212
+ 'require-param-descriptions': 'info', // Downgrade to info
213
+ },
214
+ });
215
+ ```
216
+
217
+ ## API Reference
218
+
219
+ ### `lintTool(tool, opts?)`
220
+
221
+ Lint a single tool definition. Returns a `LintResult`.
222
+
223
+ ### `lintTools(tools, opts?)`
224
+
225
+ Lint an array of tool definitions. Returns `LintResult[]`. Cross-tool rules (like `no-duplicate-names`) operate across the full set.
226
+
227
+ ### `validateTools(tools, opts?)`
228
+
229
+ Convenience function. Returns `true` if all tools pass (no error-severity issues).
230
+
231
+ ### Types
232
+
233
+ ```typescript
234
+ interface McpToolDefinition {
235
+ name: string;
236
+ description: string;
237
+ inputSchema: {
238
+ type: 'object';
239
+ properties?: Record<string, { type?: string; description?: string; [key: string]: unknown }>;
240
+ required?: string[];
241
+ };
242
+ }
243
+
244
+ type Severity = 'error' | 'warn' | 'info';
245
+
246
+ interface LintIssue {
247
+ rule: string;
248
+ severity: Severity;
249
+ message: string;
250
+ tool: string;
251
+ field?: string;
252
+ }
253
+
254
+ interface LintResult {
255
+ tool: string;
256
+ issues: LintIssue[];
257
+ passed: boolean; // true if no 'error' severity issues
258
+ }
259
+
260
+ interface Rule {
261
+ name: string;
262
+ description: string;
263
+ check(tool: McpToolDefinition, allTools?: McpToolDefinition[]): LintIssue[];
264
+ }
265
+ ```
266
+
267
+ ## License
268
+
269
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { lintTools } from './linter.js';
5
+ const RESET = '\x1b[0m';
6
+ const RED = '\x1b[31m';
7
+ const YELLOW = '\x1b[33m';
8
+ const GREEN = '\x1b[32m';
9
+ const CYAN = '\x1b[36m';
10
+ const BOLD = '\x1b[1m';
11
+ function colorize(severity) {
12
+ switch (severity) {
13
+ case 'error': return RED;
14
+ case 'warn': return YELLOW;
15
+ case 'info': return CYAN;
16
+ default: return RESET;
17
+ }
18
+ }
19
+ function main() {
20
+ const args = process.argv.slice(2);
21
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
22
+ console.log(`${BOLD}mcp-tool-lint${RESET} - Static linter for MCP tool definitions\n`);
23
+ console.log('Usage: mcp-tool-lint <tools.json>\n');
24
+ console.log('Options:');
25
+ console.log(' -h, --help Show this help message');
26
+ console.log(' -v, --version Show version\n');
27
+ console.log('The JSON file should contain a single tool object or an array of tool objects.');
28
+ process.exit(0);
29
+ }
30
+ if (args.includes('--version') || args.includes('-v')) {
31
+ console.log('0.1.0');
32
+ process.exit(0);
33
+ }
34
+ const filePath = resolve(args[0]);
35
+ let fileContent;
36
+ try {
37
+ fileContent = readFileSync(filePath, 'utf-8');
38
+ }
39
+ catch {
40
+ console.error(`${RED}Error: Could not read file "${filePath}"${RESET}`);
41
+ process.exit(1);
42
+ }
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(fileContent);
46
+ }
47
+ catch {
48
+ console.error(`${RED}Error: Invalid JSON in "${filePath}"${RESET}`);
49
+ process.exit(1);
50
+ }
51
+ const tools = Array.isArray(parsed) ? parsed : [parsed];
52
+ const results = lintTools(tools);
53
+ let hasErrors = false;
54
+ for (const result of results) {
55
+ const issueCount = result.issues.length;
56
+ const icon = result.passed ? `${GREEN}\u2713${RESET}` : `${RED}\u2717${RESET}`;
57
+ const countLabel = issueCount === 1 ? '1 issue' : `${issueCount} issues`;
58
+ console.log(`${icon} ${BOLD}${result.tool}${RESET} (${countLabel})`);
59
+ for (const issue of result.issues) {
60
+ const color = colorize(issue.severity);
61
+ const severity = issue.severity.padEnd(5);
62
+ console.log(` ${color}${severity}${RESET} [${issue.rule}] ${issue.message}`);
63
+ }
64
+ if (!result.passed)
65
+ hasErrors = true;
66
+ }
67
+ process.exit(hasErrors ? 1 : 0);
68
+ }
69
+ main();
70
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,KAAK,GAAG,SAAS,CAAC;AACxB,MAAM,GAAG,GAAG,UAAU,CAAC;AACvB,MAAM,MAAM,GAAG,UAAU,CAAC;AAC1B,MAAM,KAAK,GAAG,UAAU,CAAC;AACzB,MAAM,IAAI,GAAG,UAAU,CAAC;AACxB,MAAM,IAAI,GAAG,SAAS,CAAC;AAEvB,SAAS,QAAQ,CAAC,QAAgB;IAChC,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,OAAO,CAAC,CAAC,OAAO,GAAG,CAAC;QACzB,KAAK,MAAM,CAAC,CAAC,OAAO,MAAM,CAAC;QAC3B,KAAK,MAAM,CAAC,CAAC,OAAO,IAAI,CAAC;QACzB,OAAO,CAAC,CAAC,OAAO,KAAK,CAAC;IACxB,CAAC;AACH,CAAC;AAED,SAAS,IAAI;IACX,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEnC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACxE,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,gBAAgB,KAAK,6CAA6C,CAAC,CAAC;QACvF,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;QACnD,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;QACvD,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;QAC/C,OAAO,CAAC,GAAG,CAAC,gFAAgF,CAAC,CAAC;QAC9F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACtD,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,IAAI,WAAmB,CAAC;IAExB,IAAI,CAAC;QACH,WAAW,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,+BAA+B,QAAQ,IAAI,KAAK,EAAE,CAAC,CAAC;QACxE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,2BAA2B,QAAQ,IAAI,KAAK,EAAE,CAAC,CAAC;QACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,KAAK,GAAwB,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC7E,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAEjC,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;QACxC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,SAAS,KAAK,EAAE,CAAC;QAC/E,MAAM,UAAU,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,UAAU,SAAS,CAAC;QAEzE,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,IAAI,IAAI,GAAG,MAAM,CAAC,IAAI,GAAG,KAAK,KAAK,UAAU,GAAG,CAAC,CAAC;QAErE,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACvC,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAC1C,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,GAAG,QAAQ,GAAG,KAAK,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAChF,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,SAAS,GAAG,IAAI,CAAC;IACvC,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { lintTool, lintTools, validateTools } from './linter.js';
2
+ export type { LintOptions } from './linter.js';
3
+ export { allRules, descriptionLength, requireParamDescriptions, noVagueVerbs, requireRequiredArray, descriptionHasVerb, noDuplicateNames } from './rules/index.js';
4
+ export type { McpToolDefinition, Severity, LintIssue, LintResult, Rule } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,YAAY,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACnK,YAAY,EAAE,iBAAiB,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { lintTool, lintTools, validateTools } from './linter.js';
2
+ export { allRules, descriptionLength, requireParamDescriptions, noVagueVerbs, requireRequiredArray, descriptionHasVerb, noDuplicateNames } from './rules/index.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,YAAY,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { McpToolDefinition, LintResult, Rule, Severity } from './types.js';
2
+ export interface LintOptions {
3
+ rules?: Rule[];
4
+ severity?: {
5
+ [ruleName: string]: Severity;
6
+ };
7
+ }
8
+ export declare function lintTool(tool: McpToolDefinition, opts?: LintOptions, allTools?: McpToolDefinition[]): LintResult;
9
+ export declare function lintTools(tools: McpToolDefinition[], opts?: LintOptions): LintResult[];
10
+ export declare function validateTools(tools: McpToolDefinition[], opts?: LintOptions): boolean;
11
+ //# sourceMappingURL=linter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAa,IAAI,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtF,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE;QACT,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;KAC9B,CAAC;CACH;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,iBAAiB,EAAE,GAAG,UAAU,CAqBhH;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,iBAAiB,EAAE,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,UAAU,EAAE,CAEtF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,iBAAiB,EAAE,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAGrF"}
package/dist/linter.js ADDED
@@ -0,0 +1,28 @@
1
+ import { allRules } from './rules/index.js';
2
+ export function lintTool(tool, opts, allTools) {
3
+ const rules = opts?.rules ?? allRules;
4
+ const severityOverrides = opts?.severity ?? {};
5
+ const issues = [];
6
+ for (const rule of rules) {
7
+ const ruleIssues = rule.check(tool, allTools);
8
+ for (const issue of ruleIssues) {
9
+ if (severityOverrides[issue.rule]) {
10
+ issue.severity = severityOverrides[issue.rule];
11
+ }
12
+ issues.push(issue);
13
+ }
14
+ }
15
+ return {
16
+ tool: tool.name,
17
+ issues,
18
+ passed: !issues.some(i => i.severity === 'error'),
19
+ };
20
+ }
21
+ export function lintTools(tools, opts) {
22
+ return tools.map(tool => lintTool(tool, opts, tools));
23
+ }
24
+ export function validateTools(tools, opts) {
25
+ const results = lintTools(tools, opts);
26
+ return results.every(r => r.passed);
27
+ }
28
+ //# sourceMappingURL=linter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linter.js","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAS5C,MAAM,UAAU,QAAQ,CAAC,IAAuB,EAAE,IAAkB,EAAE,QAA8B;IAClG,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,QAAQ,CAAC;IACtC,MAAM,iBAAiB,GAAG,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;IAE/C,MAAM,MAAM,GAAgB,EAAE,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC9C,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;YAC/B,IAAI,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAClC,KAAK,CAAC,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACjD,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,MAAM;QACN,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC;KAClD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAA0B,EAAE,IAAkB;IACtE,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAA0B,EAAE,IAAkB;IAC1E,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvC,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AACtC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Rule } from '../types.js';
2
+ export declare const descriptionHasVerb: Rule;
3
+ //# sourceMappingURL=description-has-verb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"description-has-verb.d.ts","sourceRoot":"","sources":["../../src/rules/description-has-verb.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgC,MAAM,aAAa,CAAC;AAuCjE,eAAO,MAAM,kBAAkB,EAAE,IAyBhC,CAAC"}
@@ -0,0 +1,59 @@
1
+ const ACTION_VERBS = [
2
+ 'accept', 'add', 'aggregate', 'analyze', 'append', 'apply', 'authenticate',
3
+ 'build', 'calculate', 'cancel', 'capture', 'check', 'clear', 'clone', 'close',
4
+ 'collect', 'combine', 'compare', 'compile', 'compose', 'compute', 'configure',
5
+ 'connect', 'convert', 'copy', 'count', 'create', 'decode', 'decrypt', 'delete',
6
+ 'deliver', 'deploy', 'detect', 'determine', 'disable', 'disconnect', 'display',
7
+ 'download', 'emit', 'enable', 'encode', 'encrypt', 'establish', 'evaluate',
8
+ 'execute', 'export', 'extract', 'fetch', 'filter', 'find', 'flush', 'format',
9
+ 'forward', 'generate', 'get', 'grant', 'group', 'import', 'index', 'initialize',
10
+ 'inject', 'insert', 'inspect', 'install', 'invoke', 'iterate', 'join', 'launch',
11
+ 'link', 'list', 'listen', 'load', 'locate', 'log', 'look', 'map', 'match',
12
+ 'measure', 'merge', 'migrate', 'modify', 'monitor', 'move', 'normalize',
13
+ 'notify', 'open', 'optimize', 'orchestrate', 'output', 'override', 'parse',
14
+ 'patch', 'pause', 'perform', 'persist', 'ping', 'poll', 'populate', 'post',
15
+ 'print', 'provision', 'publish', 'pull', 'purge', 'push', 'put', 'query',
16
+ 'queue', 'read', 'receive', 'record', 'redirect', 'reduce', 'refresh',
17
+ 'register', 'reject', 'reload', 'remove', 'rename', 'render', 'replace',
18
+ 'replicate', 'report', 'request', 'reset', 'resize', 'resolve', 'restart',
19
+ 'restore', 'retrieve', 'return', 'revoke', 'rollback', 'rotate', 'route',
20
+ 'run', 'save', 'scan', 'schedule', 'scrape', 'search', 'select', 'send',
21
+ 'serialize', 'set', 'shut', 'sign', 'snapshot', 'sort', 'spawn', 'split',
22
+ 'start', 'stop', 'store', 'stream', 'submit', 'subscribe', 'summarize',
23
+ 'suspend', 'sync', 'terminate', 'test', 'toggle', 'trace', 'track',
24
+ 'transfer', 'transform', 'translate', 'trigger', 'truncate', 'uninstall',
25
+ 'unlink', 'unlock', 'unsubscribe', 'update', 'upgrade', 'upload', 'upsert',
26
+ 'validate', 'verify', 'watch', 'write',
27
+ // Past tense / -s / -ing forms will be matched by stemming below
28
+ 'creates', 'returns', 'searches', 'deletes', 'updates', 'fetches', 'sends',
29
+ 'validates', 'transforms', 'filters', 'reads', 'writes', 'lists', 'finds',
30
+ 'checks', 'runs', 'sets', 'gets', 'adds', 'removes', 'moves', 'copies',
31
+ 'loads', 'saves', 'starts', 'stops', 'opens', 'closes', 'connects',
32
+ 'disconnects', 'enables', 'disables', 'parses', 'formats', 'converts',
33
+ 'computes', 'calculates', 'generates', 'extracts', 'inserts', 'selects',
34
+ ];
35
+ const verbSet = new Set(ACTION_VERBS);
36
+ export const descriptionHasVerb = {
37
+ name: 'description-has-verb',
38
+ description: 'Description should contain at least one action verb',
39
+ check(tool) {
40
+ const issues = [];
41
+ const desc = (tool.description || '').toLowerCase();
42
+ const words = desc.split(/\s+/);
43
+ const hasVerb = words.some(word => {
44
+ const cleaned = word.replace(/[^a-z]/g, '');
45
+ return verbSet.has(cleaned);
46
+ });
47
+ if (!hasVerb) {
48
+ issues.push({
49
+ rule: 'description-has-verb',
50
+ severity: 'warn',
51
+ message: 'Description should contain at least one action verb (e.g., "creates", "returns", "searches")',
52
+ tool: tool.name,
53
+ field: 'description',
54
+ });
55
+ }
56
+ return issues;
57
+ },
58
+ };
59
+ //# sourceMappingURL=description-has-verb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"description-has-verb.js","sourceRoot":"","sources":["../../src/rules/description-has-verb.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { Rule } from '../types.js';
2
+ export declare const descriptionLength: Rule;
3
+ //# sourceMappingURL=description-length.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"description-length.d.ts","sourceRoot":"","sources":["../../src/rules/description-length.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgC,MAAM,aAAa,CAAC;AAEjE,eAAO,MAAM,iBAAiB,EAAE,IA4B/B,CAAC"}
@@ -0,0 +1,29 @@
1
+ export const descriptionLength = {
2
+ name: 'description-length',
3
+ description: 'Tool description must be at least 20 characters (error if < 10)',
4
+ check(tool) {
5
+ const issues = [];
6
+ const desc = (tool.description || '').trim();
7
+ const len = desc.length;
8
+ if (len < 10) {
9
+ issues.push({
10
+ rule: 'description-length',
11
+ severity: 'error',
12
+ message: `Description is too short (${len} chars, min 10)`,
13
+ tool: tool.name,
14
+ field: 'description',
15
+ });
16
+ }
17
+ else if (len < 20) {
18
+ issues.push({
19
+ rule: 'description-length',
20
+ severity: 'warn',
21
+ message: `Description is too short (${len} chars, min 20)`,
22
+ tool: tool.name,
23
+ field: 'description',
24
+ });
25
+ }
26
+ return issues;
27
+ },
28
+ };
29
+ //# sourceMappingURL=description-length.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"description-length.js","sourceRoot":"","sources":["../../src/rules/description-length.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,iBAAiB,GAAS;IACrC,IAAI,EAAE,oBAAoB;IAC1B,WAAW,EAAE,iEAAiE;IAC9E,KAAK,CAAC,IAAuB;QAC3B,MAAM,MAAM,GAAgB,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC;QAExB,IAAI,GAAG,GAAG,EAAE,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,oBAAoB;gBAC1B,QAAQ,EAAE,OAAO;gBACjB,OAAO,EAAE,6BAA6B,GAAG,iBAAiB;gBAC1D,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,KAAK,EAAE,aAAa;aACrB,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,GAAG,GAAG,EAAE,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,oBAAoB;gBAC1B,QAAQ,EAAE,MAAM;gBAChB,OAAO,EAAE,6BAA6B,GAAG,iBAAiB;gBAC1D,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,KAAK,EAAE,aAAa;aACrB,CAAC,CAAC;QACL,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { Rule } from '../types.js';
2
+ import { descriptionLength } from './description-length.js';
3
+ import { requireParamDescriptions } from './require-param-descriptions.js';
4
+ import { noVagueVerbs } from './no-vague-verbs.js';
5
+ import { requireRequiredArray } from './require-required-array.js';
6
+ import { descriptionHasVerb } from './description-has-verb.js';
7
+ import { noDuplicateNames } from './no-duplicate-names.js';
8
+ export declare const allRules: Rule[];
9
+ export { descriptionLength, requireParamDescriptions, noVagueVerbs, requireRequiredArray, descriptionHasVerb, noDuplicateNames, };
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,eAAO,MAAM,QAAQ,EAAE,IAAI,EAO1B,CAAC;AAEF,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,GACjB,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { descriptionLength } from './description-length.js';
2
+ import { requireParamDescriptions } from './require-param-descriptions.js';
3
+ import { noVagueVerbs } from './no-vague-verbs.js';
4
+ import { requireRequiredArray } from './require-required-array.js';
5
+ import { descriptionHasVerb } from './description-has-verb.js';
6
+ import { noDuplicateNames } from './no-duplicate-names.js';
7
+ export const allRules = [
8
+ descriptionLength,
9
+ requireParamDescriptions,
10
+ noVagueVerbs,
11
+ requireRequiredArray,
12
+ descriptionHasVerb,
13
+ noDuplicateNames,
14
+ ];
15
+ export { descriptionLength, requireParamDescriptions, noVagueVerbs, requireRequiredArray, descriptionHasVerb, noDuplicateNames, };
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,MAAM,CAAC,MAAM,QAAQ,GAAW;IAC9B,iBAAiB;IACjB,wBAAwB;IACxB,YAAY;IACZ,oBAAoB;IACpB,kBAAkB;IAClB,gBAAgB;CACjB,CAAC;AAEF,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,GACjB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Rule } from '../types.js';
2
+ export declare const noDuplicateNames: Rule;
3
+ //# sourceMappingURL=no-duplicate-names.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-duplicate-names.d.ts","sourceRoot":"","sources":["../../src/rules/no-duplicate-names.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgC,MAAM,aAAa,CAAC;AAEjE,eAAO,MAAM,gBAAgB,EAAE,IAsB9B,CAAC"}
@@ -0,0 +1,21 @@
1
+ export const noDuplicateNames = {
2
+ name: 'no-duplicate-names',
3
+ description: 'Tool names must be unique across the set',
4
+ check(tool, allTools) {
5
+ const issues = [];
6
+ if (!allTools || allTools.length <= 1)
7
+ return issues;
8
+ const duplicates = allTools.filter(t => t !== tool && t.name === tool.name);
9
+ if (duplicates.length > 0) {
10
+ issues.push({
11
+ rule: 'no-duplicate-names',
12
+ severity: 'error',
13
+ message: `Duplicate tool name "${tool.name}" found`,
14
+ tool: tool.name,
15
+ field: 'name',
16
+ });
17
+ }
18
+ return issues;
19
+ },
20
+ };
21
+ //# sourceMappingURL=no-duplicate-names.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-duplicate-names.js","sourceRoot":"","sources":["../../src/rules/no-duplicate-names.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAS;IACpC,IAAI,EAAE,oBAAoB;IAC1B,WAAW,EAAE,0CAA0C;IACvD,KAAK,CAAC,IAAuB,EAAE,QAA8B;QAC3D,MAAM,MAAM,GAAgB,EAAE,CAAC;QAE/B,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC;YAAE,OAAO,MAAM,CAAC;QAErD,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC;QAE5E,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,oBAAoB;gBAC1B,QAAQ,EAAE,OAAO;gBACjB,OAAO,EAAE,wBAAwB,IAAI,CAAC,IAAI,SAAS;gBACnD,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,KAAK,EAAE,MAAM;aACd,CAAC,CAAC;QACL,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Rule } from '../types.js';
2
+ export declare const noVagueVerbs: Rule;
3
+ //# sourceMappingURL=no-vague-verbs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-vague-verbs.d.ts","sourceRoot":"","sources":["../../src/rules/no-vague-verbs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgC,MAAM,aAAa,CAAC;AAmBjE,eAAO,MAAM,YAAY,EAAE,IAuB1B,CAAC"}
@@ -0,0 +1,38 @@
1
+ const VAGUE_PATTERNS = [
2
+ 'does',
3
+ 'handles',
4
+ 'manages',
5
+ 'works with',
6
+ 'interacts with',
7
+ 'deals with',
8
+ 'takes care of',
9
+ 'is responsible for',
10
+ 'processes',
11
+ ];
12
+ const SPECIFIC_VERB_SUGGESTIONS = [
13
+ 'creates', 'returns', 'searches', 'deletes', 'updates',
14
+ 'fetches', 'sends', 'validates', 'transforms', 'filters',
15
+ ];
16
+ export const noVagueVerbs = {
17
+ name: 'no-vague-verbs',
18
+ description: 'Description should not use vague verbs like "handles", "manages", "does"',
19
+ check(tool) {
20
+ const issues = [];
21
+ const desc = (tool.description || '').toLowerCase();
22
+ for (const pattern of VAGUE_PATTERNS) {
23
+ const regex = new RegExp(`\\b${pattern}\\b`, 'i');
24
+ if (regex.test(desc)) {
25
+ issues.push({
26
+ rule: 'no-vague-verbs',
27
+ severity: 'warn',
28
+ message: `Description uses vague verb "${pattern}". Use specific verbs like: ${SPECIFIC_VERB_SUGGESTIONS.join(', ')}`,
29
+ tool: tool.name,
30
+ field: 'description',
31
+ });
32
+ break; // One issue per tool is enough
33
+ }
34
+ }
35
+ return issues;
36
+ },
37
+ };
38
+ //# sourceMappingURL=no-vague-verbs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-vague-verbs.js","sourceRoot":"","sources":["../../src/rules/no-vague-verbs.ts"],"names":[],"mappings":"AAEA,MAAM,cAAc,GAAG;IACrB,MAAM;IACN,SAAS;IACT,SAAS;IACT,YAAY;IACZ,gBAAgB;IAChB,YAAY;IACZ,eAAe;IACf,oBAAoB;IACpB,WAAW;CACZ,CAAC;AAEF,MAAM,yBAAyB,GAAG;IAChC,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS;IACtD,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS;CACzD,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAS;IAChC,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,0EAA0E;IACvF,KAAK,CAAC,IAAuB;QAC3B,MAAM,MAAM,GAAgB,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QAEpD,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;YACrC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,OAAO,KAAK,EAAE,GAAG,CAAC,CAAC;YAClD,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrB,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,gBAAgB;oBACtB,QAAQ,EAAE,MAAM;oBAChB,OAAO,EAAE,gCAAgC,OAAO,+BAA+B,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;oBACrH,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,KAAK,EAAE,aAAa;iBACrB,CAAC,CAAC;gBACH,MAAM,CAAC,+BAA+B;YACxC,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Rule } from '../types.js';
2
+ export declare const requireParamDescriptions: Rule;
3
+ //# sourceMappingURL=require-param-descriptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-param-descriptions.d.ts","sourceRoot":"","sources":["../../src/rules/require-param-descriptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgC,MAAM,aAAa,CAAC;AAEjE,eAAO,MAAM,wBAAwB,EAAE,IAuBtC,CAAC"}
@@ -0,0 +1,23 @@
1
+ export const requireParamDescriptions = {
2
+ name: 'require-param-descriptions',
3
+ description: 'Every property in inputSchema.properties should have a description',
4
+ check(tool) {
5
+ const issues = [];
6
+ const properties = tool.inputSchema?.properties;
7
+ if (!properties)
8
+ return issues;
9
+ for (const [paramName, paramDef] of Object.entries(properties)) {
10
+ if (!paramDef.description || paramDef.description.trim().length === 0) {
11
+ issues.push({
12
+ rule: 'require-param-descriptions',
13
+ severity: 'warn',
14
+ message: `Parameter '${paramName}' has no description`,
15
+ tool: tool.name,
16
+ field: `inputSchema.properties.${paramName}.description`,
17
+ });
18
+ }
19
+ }
20
+ return issues;
21
+ },
22
+ };
23
+ //# sourceMappingURL=require-param-descriptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-param-descriptions.js","sourceRoot":"","sources":["../../src/rules/require-param-descriptions.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,wBAAwB,GAAS;IAC5C,IAAI,EAAE,4BAA4B;IAClC,WAAW,EAAE,oEAAoE;IACjF,KAAK,CAAC,IAAuB;QAC3B,MAAM,MAAM,GAAgB,EAAE,CAAC;QAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC;QAEhD,IAAI,CAAC,UAAU;YAAE,OAAO,MAAM,CAAC;QAE/B,KAAK,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/D,IAAI,CAAC,QAAQ,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACtE,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,4BAA4B;oBAClC,QAAQ,EAAE,MAAM;oBAChB,OAAO,EAAE,cAAc,SAAS,sBAAsB;oBACtD,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,KAAK,EAAE,0BAA0B,SAAS,cAAc;iBACzD,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Rule } from '../types.js';
2
+ export declare const requireRequiredArray: Rule;
3
+ //# sourceMappingURL=require-required-array.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-required-array.d.ts","sourceRoot":"","sources":["../../src/rules/require-required-array.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgC,MAAM,aAAa,CAAC;AAEjE,eAAO,MAAM,oBAAoB,EAAE,IAqBlC,CAAC"}
@@ -0,0 +1,21 @@
1
+ export const requireRequiredArray = {
2
+ name: 'require-required-array',
3
+ description: 'inputSchema should declare a required array when properties are defined',
4
+ check(tool) {
5
+ const issues = [];
6
+ const properties = tool.inputSchema?.properties;
7
+ if (properties && Object.keys(properties).length > 0) {
8
+ if (!Array.isArray(tool.inputSchema.required)) {
9
+ issues.push({
10
+ rule: 'require-required-array',
11
+ severity: 'warn',
12
+ message: 'inputSchema has properties but no "required" array — declare required fields explicitly (even if empty)',
13
+ tool: tool.name,
14
+ field: 'inputSchema.required',
15
+ });
16
+ }
17
+ }
18
+ return issues;
19
+ },
20
+ };
21
+ //# sourceMappingURL=require-required-array.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-required-array.js","sourceRoot":"","sources":["../../src/rules/require-required-array.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,oBAAoB,GAAS;IACxC,IAAI,EAAE,wBAAwB;IAC9B,WAAW,EAAE,yEAAyE;IACtF,KAAK,CAAC,IAAuB;QAC3B,MAAM,MAAM,GAAgB,EAAE,CAAC;QAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC;QAEhD,IAAI,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC9C,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,wBAAwB;oBAC9B,QAAQ,EAAE,MAAM;oBAChB,OAAO,EAAE,yGAAyG;oBAClH,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,KAAK,EAAE,sBAAsB;iBAC9B,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
@@ -0,0 +1,33 @@
1
+ export interface McpToolDefinition {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: 'object';
6
+ properties?: Record<string, {
7
+ type?: string;
8
+ description?: string;
9
+ enum?: string[];
10
+ [key: string]: unknown;
11
+ }>;
12
+ required?: string[];
13
+ };
14
+ }
15
+ export type Severity = 'error' | 'warn' | 'info';
16
+ export interface LintIssue {
17
+ rule: string;
18
+ severity: Severity;
19
+ message: string;
20
+ tool: string;
21
+ field?: string;
22
+ }
23
+ export interface LintResult {
24
+ tool: string;
25
+ issues: LintIssue[];
26
+ passed: boolean;
27
+ }
28
+ export interface Rule {
29
+ name: string;
30
+ description: string;
31
+ check(tool: McpToolDefinition, allTools?: McpToolDefinition[]): LintIssue[];
32
+ }
33
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,WAAW,CAAC,EAAE,MAAM,CAAC;YACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;YAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;SACxB,CAAC,CAAC;QACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,IAAI,EAAE,iBAAiB,EAAE,QAAQ,CAAC,EAAE,iBAAiB,EAAE,GAAG,SAAS,EAAE,CAAC;CAC7E"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "mcp-tool-lint",
3
+ "version": "0.1.0",
4
+ "description": "Static linter for MCP tool definitions — catch quality defects before deployment",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "mcp-tool-lint": "dist/cli.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "test": "tsc && node --experimental-strip-types --no-warnings --test tests/*.test.ts"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "linting",
19
+ "tools",
20
+ "ai",
21
+ "claude"
22
+ ],
23
+ "license": "MIT",
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "^5.9.3"
32
+ }
33
+ }