Skip to content

Commit 08d2851

Browse files
authored
feat: implement tool name and parameter validation system with eslint integration (#337)
# Fix Claude API Errors with Tool Name Validation This PR fixes a problem where the Azure DevOps MCP server was getting error messages from Claude API because some tool names didn't follow the required format. Claude API is very strict about tool names, they can only contain letters, numbers, underscores, dots, and dashes, and must be 64 characters or less. When tool names don't follow these rules, Claude returns a 400 error and refuses to work. To solve this, we added validation that checks all tool names before they get sent to Claude. The validation happens in three places: when you're writing code (through ESLint), when you build the project, and when tests run. We tested all 60+ existing tools in the project and they all pass the validation. The longest tool name is only 40 characters, so we're well within the limits. The validation code is organized so there's no duplication, one shared module handles all the checking logic. We also added comprehensive tests to make sure the validation works correctly, and wrote documentation to help developers understand the naming rules. This prevents future API errors and makes the development process smoother since developers get immediate feedback when they use invalid tool names. Example linter error while developing; <img width="1075" height="121" alt="image" src="https://github.com/user-attachments/assets/04320b1a-a833-45d8-870e-90ad8a05645d" /> Output of validation for current tools; <img width="667" height="1331" alt="image" src="https://github.com/user-attachments/assets/5d903b57-efa1-4ecc-ac46-b9fb7dbfcc45" /> ## GitHub issue number Fixes #332 ## **Associated Risks** None ## ✅ **PR Checklist** - [x] **I have read the [contribution guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CONTRIBUTING.md)** - [x] **I have read the [code of conduct guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CODE_OF_CONDUCT.md)** - [x] Title of the pull request is clear and informative. - [x] 👌 Code hygiene - [x] 🔭 Telemetry added, updated, or N/A - [x] 📄 Documentation added, updated, or N/A - [x] 🛡️ Automated tests added, or N/A ## 🧪 **How did you test it?** Run `npm run validate-tools` command
1 parent 17a1cc9 commit 08d2851

File tree

9 files changed

+905
-11
lines changed

9 files changed

+905
-11
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ jobs:
4242
- name: Build the project
4343
run: npm run build
4444

45+
- name: Validate tool names and parameters
46+
run: npm run validate-tools
47+
4548
- name: Run tests
4649
run: npm test
4750

docs/TOOL-NAME-VALIDATION.md

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# Tool Name Validation Guardrails
2+
3+
This document describes the validation system for Azure DevOps MCP server tool names and parameter names to prevent Claude API validation errors.
4+
5+
## Problem Statement
6+
7+
Claude API has strict requirements for tool names and parameter names:
8+
9+
- Must match pattern: `^[a-zA-Z0-9_.-]{1,64}$`
10+
- Maximum length: 64 characters
11+
- Only alphanumeric characters, underscores, dots, and hyphens are allowed
12+
- No spaces or special characters
13+
14+
Failing to comply results in errors like:
15+
16+
```json
17+
API Error: 400
18+
{"type":"error","error":{"type":"invalid_request_error","message":"tools.127.custom.input_schema.properties: Property keys should match pattern '^[a-zA-Z0-9_.-]{1,64}$'"}}
19+
```
20+
21+
## Guardrails Implemented
22+
23+
### 1. Build-time Validation Script
24+
25+
**Location:** `scripts/build-validate-tools.js`
26+
27+
**Description:** Scans all tool files and validates tool names and parameter names.
28+
29+
**Usage:**
30+
31+
```bash
32+
npm run validate-tools
33+
```
34+
35+
**Features:**
36+
37+
- Extracts tool names from `*_TOOLS` constant objects
38+
- Extracts parameter names from Zod schema definitions
39+
- Validates against Claude API requirements
40+
- Shows character counts for each name
41+
- Warns about parameter names longer than 32 characters (recommendation)
42+
- Fails build if invalid names are found
43+
44+
### 2. ESLint Rule
45+
46+
**Location:** `eslint-rules/tool-name-lint-rule.js`
47+
48+
**Description:** Custom ESLint rule that validates tool names and parameter names during development.
49+
50+
**Features:**
51+
52+
- Real-time validation in IDE
53+
- Integrated with existing ESLint configuration
54+
- Shows errors for invalid names
55+
- Warnings for long parameter names
56+
57+
**Configuration in `eslint.config.mjs`:**
58+
59+
```javascript
60+
{
61+
files: ["src/tools/*.ts"],
62+
plugins: {
63+
"custom": {
64+
rules: {
65+
"validate-tool-names": validateToolNamesRule,
66+
},
67+
},
68+
},
69+
rules: {
70+
"custom/validate-tool-names": "error",
71+
},
72+
}
73+
```
74+
75+
### 3. Shared Validation Module
76+
77+
**Location:** `src/shared/tool-validation.ts`
78+
79+
**Functions:**
80+
81+
- `validateToolName(toolName: string)`: Validates a single tool name
82+
- `validateParameterName(paramName: string)`: Validates a single parameter name
83+
- `validateName(name: string)`: Core validation function used by both tool and parameter validation
84+
- `extractToolNames(fileContent: string)`: Extracts tool names from TypeScript files
85+
- `extractParameterNames(fileContent: string)`: Extracts parameter names from Zod schemas
86+
87+
**Usage:**
88+
89+
```typescript
90+
import { validateToolName, validateParameterName } from "./shared/tool-validation.js";
91+
92+
const validation = validateToolName("my_tool_name");
93+
if (!validation.isValid) {
94+
console.error(validation.error);
95+
}
96+
```
97+
98+
### 4. Test Coverage
99+
100+
**Location:** `test/src/tool-name-validation.test.ts`
101+
102+
**Coverage:** 100% statements, branches, functions, and lines
103+
104+
**Test Categories:**
105+
106+
- **Validation Functions**: Tests for `validateToolName` and `validateParameterName` with valid/invalid inputs
107+
- **Character Patterns**: Tests for all valid characters (a-z, A-Z, 0-9, \_, ., -) and invalid characters
108+
- **Length Limits**: Edge cases for 64-character maximum length
109+
- **Extraction Functions**: Tests for `extractToolNames` and `extractParameterNames` with real-world patterns
110+
- **Edge Cases**: Empty inputs, malformed content, mixed file content
111+
112+
**Running Tests:**
113+
114+
```bash
115+
# Run all validation tests
116+
npm test test/src/tool-name-validation.test.ts
117+
118+
# Run with coverage report
119+
npm test test/src/tool-name-validation.test.ts -- --coverage
120+
```
121+
122+
## Architecture Overview
123+
124+
The validation system uses a **consolidated architecture** with shared validation logic:
125+
126+
```bash
127+
src/shared/tool-validation.ts # Single source of truth for validation logic
128+
├── validateName() # Core validation function
129+
├── validateToolName() # Tool-specific validation
130+
├── validateParameterName() # Parameter-specific validation
131+
├── extractToolNames() # Extract tools from TypeScript files
132+
└── extractParameterNames() # Extract parameters from Zod schemas
133+
134+
scripts/build-validate-tools.js # Build-time validation (imports shared module)
135+
eslint-rules/tool-name-lint-rule.js # ESLint rule (imports shared module)
136+
test/src/tool-name-validation.test.ts # Comprehensive tests (100% coverage)
137+
```
138+
139+
**Benefits:**
140+
141+
- **No Code Duplication**: Single validation implementation
142+
- **Consistent Behavior**: Same logic across build-time and development-time validation
143+
- **Maintainable**: Changes in one place update all validation systems
144+
- **Well-Tested**: 100% test coverage ensures reliability
145+
146+
## Best Practices
147+
148+
### Tool Naming
149+
150+
- Use descriptive but concise names
151+
- Follow the pattern: `{category}_{action}_{object}`
152+
- Examples: `repo_create_pull_request`, `build_get_status`
153+
- Maximum 64 characters (current longest is 40 characters)
154+
155+
### Parameter Naming
156+
157+
- Use clear, descriptive names
158+
- Avoid abbreviations unless commonly understood
159+
- Keep under 32 characters for readability (recommendation)
160+
- Examples: `project`, `repositoryId`, `includeDetails`
161+
162+
### Development Workflow
163+
164+
1. Create new tools with descriptive names
165+
2. Run `npm run validate-tools` to check compliance
166+
3. Fix any validation errors before committing
167+
4. ESLint will catch issues in real-time during development
168+
169+
## Validation Rules
170+
171+
### Valid Characters
172+
173+
- Alphanumeric: `a-z`, `A-Z`, `0-9`
174+
- Underscore: `_`
175+
- Dot: `.`
176+
- Hyphen: `-`
177+
178+
### Invalid Characters
179+
180+
- Spaces: ` `
181+
- Forward slash: `/`
182+
- Special characters: `@`, `#`, `$`, `%`, etc.
183+
184+
### Length Limits
185+
186+
- Minimum: 1 character
187+
- Maximum: 64 characters
188+
- Recommended for parameters: ≤32 characters
189+
190+
## Troubleshooting
191+
192+
### Common Issues
193+
194+
1. **Tool name contains spaces**
195+
196+
```text
197+
Error: Tool name 'my tool name' contains invalid characters
198+
```
199+
200+
Solution: Use underscores instead: `my_tool_name`
201+
202+
2. **Tool name too long**
203+
204+
```text
205+
Error: Tool name 'very_long_descriptive_tool_name_that_exceeds_limit' is 45 characters long, maximum allowed is 64
206+
```
207+
208+
Solution: Shorten the name: `long_tool_name`
209+
210+
3. **Special characters in name**
211+
212+
```text
213+
Error: Tool name 'tool/name' contains invalid characters
214+
```
215+
216+
Solution: Use allowed characters: `tool_name`
217+
218+
### Running Validation
219+
220+
```bash
221+
# Validate all tools
222+
npm run validate-tools
223+
224+
# Run ESLint on tool files
225+
npx eslint src/tools/*.ts
226+
```

eslint-rules/tool-name-lint-rule.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { validateName } from "../dist/shared/tool-validation.js";
5+
6+
/**
7+
* Custom ESLint rule to validate Azure DevOps MCP tool names and parameter names
8+
*/
9+
export default {
10+
meta: {
11+
type: "problem",
12+
docs: {
13+
description: "Validate tool names and parameter names conform to Claude API requirements",
14+
category: "Possible Errors",
15+
recommended: true,
16+
},
17+
fixable: null,
18+
schema: [],
19+
messages: {
20+
invalidToolName: "Tool name '{{name}}' is invalid: {{reason}}",
21+
invalidParameterName: "Parameter name '{{name}}' is invalid: {{reason}}",
22+
longParameterName: "Parameter name '{{name}}' is {{length}} characters long, consider shortening for better readability",
23+
},
24+
},
25+
26+
create(context) {
27+
return {
28+
// Check tool constant definitions like: toolName: "actual_tool_name"
29+
Property(node) {
30+
if (node.value && node.value.type === "Literal" && typeof node.value.value === "string" && node.parent && node.parent.type === "ObjectExpression") {
31+
const parentObject = node.parent.parent;
32+
33+
// Check if this is a tool constants object (contains "_TOOLS")
34+
if (parentObject && parentObject.type === "VariableDeclarator" && parentObject.id && parentObject.id.name && parentObject.id.name.includes("_TOOLS")) {
35+
const toolName = node.value.value;
36+
const validation = validateName(toolName);
37+
38+
if (!validation.isValid) {
39+
context.report({
40+
node: node.value,
41+
messageId: "invalidToolName",
42+
data: {
43+
name: toolName,
44+
reason: validation.reason,
45+
},
46+
});
47+
}
48+
}
49+
}
50+
},
51+
52+
// Check parameter names in function calls like: paramName: z.string()
53+
CallExpression(node) {
54+
if (
55+
node.callee &&
56+
node.callee.type === "MemberExpression" &&
57+
node.callee.object &&
58+
node.callee.object.name === "z" &&
59+
node.parent &&
60+
node.parent.type === "Property" &&
61+
node.parent.key &&
62+
node.parent.key.type === "Identifier"
63+
) {
64+
const paramName = node.parent.key.name;
65+
const validation = validateName(paramName);
66+
67+
if (!validation.isValid) {
68+
context.report({
69+
node: node.parent.key,
70+
messageId: "invalidParameterName",
71+
data: {
72+
name: paramName,
73+
reason: validation.reason,
74+
},
75+
});
76+
} else if (paramName.length > 32) {
77+
// Warning for long parameter names
78+
context.report({
79+
node: node.parent.key,
80+
messageId: "longParameterName",
81+
data: {
82+
name: paramName,
83+
length: paramName.length,
84+
},
85+
});
86+
}
87+
}
88+
},
89+
};
90+
},
91+
};

eslint.config.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pluginHeader from "eslint-plugin-header";
22
import tseslint from "typescript-eslint";
33
import eslintConfigPrettier from "eslint-config-prettier";
4+
import validateToolNamesRule from "./eslint-rules/tool-name-lint-rule.js";
45

56
pluginHeader.rules.header.meta.schema = false; // workaround for https://github.com/Stuk/eslint-plugin-header/issues/57
67

@@ -27,6 +28,21 @@ export default tseslint.config(
2728
},
2829
},
2930

31+
// Tool name validation for MCP tools
32+
{
33+
files: ["src/tools/*.ts"],
34+
plugins: {
35+
custom: {
36+
rules: {
37+
"validate-tool-names": validateToolNamesRule,
38+
},
39+
},
40+
},
41+
rules: {
42+
"custom/validate-tool-names": "error",
43+
},
44+
},
45+
3046
// Prettier integration (must be last)
3147
eslintConfigPrettier
3248
);

0 commit comments

Comments
 (0)