mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
Add detection of tools with bad schemas and automatically omit them with a warning (#5694)
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
isEnabled,
|
||||
discoverTools,
|
||||
discoverPrompts,
|
||||
hasValidTypes,
|
||||
} from './mcp-client.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import * as SdkClientStdioLib from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
@@ -97,6 +98,182 @@ describe('mcp-client', () => {
|
||||
`Error discovering tool: 'invalid tool name' from MCP server 'test-server': ${testError.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip tools if a parameter is missing a type', async () => {
|
||||
const mockedClient = {} as unknown as ClientLib.Client;
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
vi.mocked(GenAiLib.mcpToTool).mockReturnValue({
|
||||
tool: () =>
|
||||
Promise.resolve({
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: 'validTool',
|
||||
parametersJsonSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'invalidTool',
|
||||
parametersJsonSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: { description: 'a param with no type' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
} as unknown as GenAiLib.CallableTool);
|
||||
|
||||
const tools = await discoverTools('test-server', {}, mockedClient);
|
||||
|
||||
expect(tools.length).toBe(1);
|
||||
expect(vi.mocked(DiscoveredMCPTool).mock.calls[0][2]).toBe('validTool');
|
||||
expect(consoleWarnSpy).toHaveBeenCalledOnce();
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
`Skipping tool 'invalidTool' from MCP server 'test-server' because it has ` +
|
||||
`missing types in its parameter schema. Please file an issue with the owner of the MCP server.`,
|
||||
);
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should skip tools if a nested parameter is missing a type', async () => {
|
||||
const mockedClient = {} as unknown as ClientLib.Client;
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
vi.mocked(GenAiLib.mcpToTool).mockReturnValue({
|
||||
tool: () =>
|
||||
Promise.resolve({
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: 'invalidTool',
|
||||
parametersJsonSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nestedParam: {
|
||||
description: 'a nested param with no type',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
} as unknown as GenAiLib.CallableTool);
|
||||
|
||||
const tools = await discoverTools('test-server', {}, mockedClient);
|
||||
|
||||
expect(tools.length).toBe(0);
|
||||
expect(consoleWarnSpy).toHaveBeenCalledOnce();
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
`Skipping tool 'invalidTool' from MCP server 'test-server' because it has ` +
|
||||
`missing types in its parameter schema. Please file an issue with the owner of the MCP server.`,
|
||||
);
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should skip tool if an array item is missing a type', async () => {
|
||||
const mockedClient = {} as unknown as ClientLib.Client;
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
vi.mocked(GenAiLib.mcpToTool).mockReturnValue({
|
||||
tool: () =>
|
||||
Promise.resolve({
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: 'invalidTool',
|
||||
parametersJsonSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: {
|
||||
type: 'array',
|
||||
items: {
|
||||
description: 'an array item with no type',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
} as unknown as GenAiLib.CallableTool);
|
||||
|
||||
const tools = await discoverTools('test-server', {}, mockedClient);
|
||||
|
||||
expect(tools.length).toBe(0);
|
||||
expect(consoleWarnSpy).toHaveBeenCalledOnce();
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
`Skipping tool 'invalidTool' from MCP server 'test-server' because it has ` +
|
||||
`missing types in its parameter schema. Please file an issue with the owner of the MCP server.`,
|
||||
);
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should discover tool with no properties in schema', async () => {
|
||||
const mockedClient = {} as unknown as ClientLib.Client;
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
vi.mocked(GenAiLib.mcpToTool).mockReturnValue({
|
||||
tool: () =>
|
||||
Promise.resolve({
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: 'validTool',
|
||||
parametersJsonSchema: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
} as unknown as GenAiLib.CallableTool);
|
||||
|
||||
const tools = await discoverTools('test-server', {}, mockedClient);
|
||||
|
||||
expect(tools.length).toBe(1);
|
||||
expect(vi.mocked(DiscoveredMCPTool).mock.calls[0][2]).toBe('validTool');
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should discover tool with empty properties object in schema', async () => {
|
||||
const mockedClient = {} as unknown as ClientLib.Client;
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
vi.mocked(GenAiLib.mcpToTool).mockReturnValue({
|
||||
tool: () =>
|
||||
Promise.resolve({
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: 'validTool',
|
||||
parametersJsonSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
} as unknown as GenAiLib.CallableTool);
|
||||
|
||||
const tools = await discoverTools('test-server', {}, mockedClient);
|
||||
|
||||
expect(tools.length).toBe(1);
|
||||
expect(vi.mocked(DiscoveredMCPTool).mock.calls[0][2]).toBe('validTool');
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('discoverPrompts', () => {
|
||||
@@ -433,4 +610,163 @@ describe('mcp-client', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasValidTypes', () => {
|
||||
it('should return true for a valid schema with anyOf', () => {
|
||||
const schema = {
|
||||
anyOf: [{ type: 'string' }, { type: 'number' }],
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an invalid schema with anyOf', () => {
|
||||
const schema = {
|
||||
anyOf: [{ type: 'string' }, { description: 'no type' }],
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for a valid schema with allOf', () => {
|
||||
const schema = {
|
||||
allOf: [
|
||||
{ type: 'string' },
|
||||
{ type: 'object', properties: { foo: { type: 'string' } } },
|
||||
],
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an invalid schema with allOf', () => {
|
||||
const schema = {
|
||||
allOf: [{ type: 'string' }, { description: 'no type' }],
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for a valid schema with oneOf', () => {
|
||||
const schema = {
|
||||
oneOf: [{ type: 'string' }, { type: 'number' }],
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an invalid schema with oneOf', () => {
|
||||
const schema = {
|
||||
oneOf: [{ type: 'string' }, { description: 'no type' }],
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for a valid schema with nested subschemas', () => {
|
||||
const schema = {
|
||||
anyOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
allOf: [
|
||||
{ type: 'object', properties: { a: { type: 'string' } } },
|
||||
{ type: 'object', properties: { b: { type: 'number' } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an invalid schema with nested subschemas', () => {
|
||||
const schema = {
|
||||
anyOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
allOf: [
|
||||
{ type: 'object', properties: { a: { type: 'string' } } },
|
||||
{ description: 'no type' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for a schema with a type and subschemas', () => {
|
||||
const schema = {
|
||||
type: 'string',
|
||||
anyOf: [{ minLength: 1 }, { maxLength: 5 }],
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a schema with no type and no subschemas', () => {
|
||||
const schema = {
|
||||
description: 'a schema with no type',
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for a valid schema', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: { type: 'string' },
|
||||
},
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if a parameter is missing a type', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: { description: 'a param with no type' },
|
||||
},
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if a nested parameter is missing a type', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nestedParam: {
|
||||
description: 'a nested param with no type',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if an array item is missing a type', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: {
|
||||
type: 'array',
|
||||
items: {
|
||||
description: 'an array item with no type',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for a schema with no properties', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for a schema with an empty properties object', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
};
|
||||
expect(hasValidTypes(schema)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user