-
Notifications
You must be signed in to change notification settings - Fork 4
fix(mcp): MCP schema validation error for array types #485
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,221 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { expect } from '@oclif/test' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import sinon from 'sinon' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { registerAllToolsWithServer } from './tools' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { DevCycleMCPServerInstance } from './server' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Type for tool configuration based on server definition | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ToolConfig = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| annotations?: any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inputSchema?: any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| outputSchema?: any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Type for tool handler based on server definition | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ToolHandler = (args: any) => Promise<any> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('MCP Schema Validation', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * This test ensures that all MCP tool schemas are properly formatted for the MCP protocol. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Specifically, it checks that array types have the required 'items' property. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Background: The MCP protocol requires that all array types in JSON schemas must have | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * an 'items' property. Using z.array(z.any()) in Zod schemas doesn't generate this | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * property, causing MCP validation errors. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Solution: Use z.array(z.unknown()) instead of z.array(z.any()) in zodClient.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('Tool Registration Schema Validation', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Helper function to recursively check schemas | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function validateSchemaArrays( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| schema: any, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errors: string[], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!schema || typeof schema !== 'object') return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if this is an array type without items | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (schema.type === 'array' && !schema.items) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `❌ Array at '${path}' is missing required 'items' property.\n` + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ` This will cause MCP validation errors.\n` + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ` Fix: In zodClient.ts, replace z.array(z.any()) with z.array(z.unknown())`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Recursively check nested schemas | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (schema.properties) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [key, value] of Object.entries(schema.properties)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validateSchemaArrays( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `${path}.properties.${key}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errors, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (schema.items) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validateSchemaArrays(schema.items, `${path}.items`, errors) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check schema combiners (anyOf, oneOf, allOf) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const combiner of ['anyOf', 'oneOf', 'allOf']) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (Array.isArray(schema[combiner])) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| schema[combiner].forEach((subSchema: any, idx: number) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| schema[combiner].forEach((subSchema: any, idx: number) => { | |
| schema[combiner].forEach((subSchema: unknown, idx: number) => { |
Copilot
AI
Aug 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mock API client is duplicated in both test cases (lines 86-96 and implicitly recreated in the second test). Consider extracting this to a helper function or beforeEach block to reduce code duplication.
| // Create a mock API client | |
| const mockApiClient = { | |
| executeWithLogging: sinon.stub(), | |
| executeWithDashboardLink: sinon.stub(), | |
| setSelectedProject: sinon.stub(), | |
| hasProjectKey: sinon.stub().resolves(true), | |
| getOrgId: sinon.stub().returns('test-org'), | |
| getUserId: sinon.stub().returns('test-user'), | |
| hasProjectAccess: sinon.stub().resolves(true), | |
| getUserContext: sinon.stub().resolves({}), | |
| } | |
| // Helper to create a fresh mock API client for each test | |
| function createMockApiClient() { | |
| return { | |
| executeWithLogging: sinon.stub(), | |
| executeWithDashboardLink: sinon.stub(), | |
| setSelectedProject: sinon.stub(), | |
| hasProjectKey: sinon.stub().resolves(true), | |
| getOrgId: sinon.stub().returns('test-org'), | |
| getUserId: sinon.stub().returns('test-user'), | |
| hasProjectAccess: sinon.stub().resolves(true), | |
| getUserContext: sinon.stub().resolves({}), | |
| } | |
| } | |
| let mockApiClient: ReturnType<typeof createMockApiClient> | |
| beforeEach(() => { | |
| mockApiClient = createMockApiClient() | |
| }) |
Copilot
AI
Aug 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable validationErrors is being incremented with individual tool errors, but the final error message states 'Found ${validationErrors.length} tools with invalid schemas' which counts array elements, not tools. This will show an incorrect count since each tool can have multiple errors. Consider tracking the number of invalid tools separately.
| if (errors.length > 0) { | |
| if (errors.length > 0) { | |
| invalidToolCount++ |
Copilot
AI
Aug 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message suggests checking for z.record(z.any()), but the validation function validateSchemaArrays only checks for array types with missing items, not record types. Either add validation for record types or remove this suggestion from the error message.
| `4. Also check for 'z.record(z.any())' and replace with 'z.record(z.unknown())'`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using 'any' type defeats TypeScript's type safety. Consider using a more specific type like 'object | unknown' or define a proper interface for the schema structure.