diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 4bb42d7fc..4829193a5 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -846,6 +846,154 @@ describe('tool()', () => { expect(result.tools[1].annotations).toEqual(result.tools[0].annotations); }); + /*** + * Test: Tool with undefined in annotations position + */ + test('should correctly parse callback position when undefined is passed in annotations position', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (mcpServer.tool as any)( + 'test', + 'A tool description', + { name: z.string() }, + undefined, // Not officially supported, but internal logic should handle it + async ({ name }: { name: string }) => ({ + content: [{ type: 'text' as const, text: `Hello, ${name}!` }] + }) + ); + + expect(mcpServer['_registeredTools']['test']).toBeDefined(); + expect(mcpServer['_registeredTools']['test'].description).toBe('A tool description'); + expect(mcpServer['_registeredTools']['test'].inputSchema).toBeDefined(); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.listTools(); + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe('test'); + expect(result.tools[0].description).toBe('A tool description'); + + const callResult = await client.callTool({ name: 'test', arguments: { name: 'World' } }); + expect(callResult.content).toHaveLength(1); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((callResult.content as any)[0]).toMatchObject({ + type: 'text', + text: 'Hello, World!' + }); + }); + + /*** + * Test: Tool with undefined in description position + */ + test('should correctly parse callback position when undefined is passed in description position', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (mcpServer.tool as any)( + 'test', + undefined, // Not officially supported, but internal logic should handle it + { name: z.string() }, + async ({ name }: { name: string }) => ({ + content: [{ type: 'text' as const, text: `Hello, ${name}!` }] + }) + ); + + expect(mcpServer['_registeredTools']['test']).toBeDefined(); + expect(mcpServer['_registeredTools']['test'].description).toBeUndefined(); + expect(mcpServer['_registeredTools']['test'].inputSchema).toBeDefined(); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.listTools(); + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe('test'); + expect(result.tools[0].description).toBeUndefined(); + + const callResult = await client.callTool({ name: 'test', arguments: { name: 'World' } }); + expect(callResult.content).toHaveLength(1); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((callResult.content as any)[0]).toMatchObject({ + type: 'text', + text: 'Hello, World!' + }); + }); + + /*** + * Test: Tool with undefined in paramsSchema position + */ + test('should correctly parse callback position when undefined is passed in paramsSchema position', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (mcpServer.tool as any)( + 'test', + 'A tool description', + undefined, // Not officially supported, but internal logic should handle it + { title: 'Test Tool', readOnlyHint: true }, + async () => ({ + content: [{ type: 'text' as const, text: 'Hello!' }] + }) + ); + + expect(mcpServer['_registeredTools']['test']).toBeDefined(); + expect(mcpServer['_registeredTools']['test'].description).toBe('A tool description'); + expect(mcpServer['_registeredTools']['test'].inputSchema).toBeUndefined(); + expect(mcpServer['_registeredTools']['test'].annotations).toEqual({ + title: 'Test Tool', + readOnlyHint: true + }); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.listTools(); + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe('test'); + expect(result.tools[0].description).toBe('A tool description'); + expect(result.tools[0].annotations).toEqual({ + title: 'Test Tool', + readOnlyHint: true + }); + + const callResult = await client.callTool({ name: 'test' }); + expect(callResult.content).toHaveLength(1); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((callResult.content as any)[0]).toMatchObject({ + type: 'text', + text: 'Hello!' + }); + }); + /*** * Test: Tool Argument Validation */ diff --git a/src/server/mcp.ts b/src/server/mcp.ts index cef1722d6..3b85e113d 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -752,33 +752,53 @@ export class McpServer { // Support for this style is frozen as of protocol version 2025-03-26. Future additions // to tool definition should *NOT* be added. - if (typeof rest[0] === 'string') { - description = rest.shift() as string; + // Find the callback first (always the last function in rest) + let callbackIndex = rest.length - 1; + while (callbackIndex >= 0 && typeof rest[callbackIndex] !== 'function') { + callbackIndex--; } - // Handle the different overload combinations - if (rest.length > 1) { - // We have at least one more arg before the callback - const firstArg = rest[0]; - - if (isZodRawShape(firstArg)) { - // We have a params schema as the first arg - inputSchema = rest.shift() as ZodRawShape; - - // Check if the next arg is potentially annotations - if (rest.length > 1 && typeof rest[0] === 'object' && rest[0] !== null && !isZodRawShape(rest[0])) { - // Case: tool(name, paramsSchema, annotations, cb) - // Or: tool(name, description, paramsSchema, annotations, cb) - annotations = rest.shift() as ToolAnnotations; - } - } else if (typeof firstArg === 'object' && firstArg !== null) { - // Not a ZodRawShape, so must be annotations in this position - // Case: tool(name, annotations, cb) - // Or: tool(name, description, annotations, cb) - annotations = rest.shift() as ToolAnnotations; + if (callbackIndex < 0) { + throw new Error(`Tool ${name} registration requires a callback function`); + } + + const callback = rest[callbackIndex] as ToolCallback; + // Get all args before the callback + const args = rest.slice(0, callbackIndex); + + // Parse args: [description?], [paramsSchema?], [annotations?] + // Skip undefined values and parse based on type + let argIndex = 0; + + // Check for optional description (first string arg, skip if undefined) + if (argIndex < args.length) { + if (typeof args[argIndex] === 'string') { + description = args[argIndex] as string; + argIndex++; + } else if (args[argIndex] === undefined) { + argIndex++; // Skip undefined + } + } + + // Check for optional paramsSchema (ZodRawShape, skip if undefined) + if (argIndex < args.length) { + if (isZodRawShape(args[argIndex])) { + inputSchema = args[argIndex] as ZodRawShape; + argIndex++; + } else if (args[argIndex] === undefined) { + argIndex++; // Skip undefined + } + } + + // Check for optional annotations (non-null object that's not a ZodRawShape, skip if undefined) + if (argIndex < args.length) { + if (typeof args[argIndex] === 'object' && args[argIndex] !== null && !isZodRawShape(args[argIndex])) { + annotations = args[argIndex] as ToolAnnotations; + argIndex++; + } else if (args[argIndex] === undefined) { + argIndex++; // Skip undefined } } - const callback = rest[0] as ToolCallback; return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, undefined, callback); }