summaryrefslogtreecommitdiff
path: root/packages/core/src/tools/mcp-client.test.ts
diff options
context:
space:
mode:
authorJacob MacDonald <[email protected]>2025-08-06 13:19:15 -0700
committerGitHub <[email protected]>2025-08-06 20:19:15 +0000
commitb3cfaeb6d30101262dc2b7350f5a349cd0417386 (patch)
treebce3febb11a225b52e42e09573e6b086ff814561 /packages/core/src/tools/mcp-client.test.ts
parent024b8207eb75bdc0c031f6380d6759b9e342e502 (diff)
Add detection of tools with bad schemas and automatically omit them with a warning (#5694)
Diffstat (limited to 'packages/core/src/tools/mcp-client.test.ts')
-rw-r--r--packages/core/src/tools/mcp-client.test.ts336
1 files changed, 336 insertions, 0 deletions
diff --git a/packages/core/src/tools/mcp-client.test.ts b/packages/core/src/tools/mcp-client.test.ts
index 9997d60e..1ccba76a 100644
--- a/packages/core/src/tools/mcp-client.test.ts
+++ b/packages/core/src/tools/mcp-client.test.ts
@@ -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);
+ });
+ });
});