UNPKG

@geocoding-ai/mcp

Version:

Model Context Protocol server for geocoding

97 lines (96 loc) 4.46 kB
// src/tools/geocode.test.ts import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { registerGeocodeTool } from '../../tools/geocode.js'; import * as nominatimClient from '../../clients/nominatimClient.js'; // Use the actual implementation from prepareResponse import { handleGeocodeResult } from '../../tools/prepareResponse.js'; // Mock McpServer to capture the handler mock.module('@modelcontextprotocol/sdk/server/mcp.js', () => ({ McpServer: class { tool = mock((_name, _description, _schema, handler) => { // Store the handler so we can call it directly in tests ; this.handler = handler; }); }, })); // Mock only nominatimClient mock.module('@/clients/nominatimClient.js', () => ({ geocodeAddress: mock(async (params) => { // Default successful mock implementation, can be overridden in tests return [ { place_id: 123, display_name: `Mocked result for ${params.query}` }, ]; }), })); describe('registerGeocodeTool', () => { let serverInstance; let toolHandler; beforeEach(() => { serverInstance = new McpServer({ name: 'test-server', version: '1.0' }); registerGeocodeTool(serverInstance); // Access the handler registered by the tool method toolHandler = serverInstance.handler; }); afterEach(() => { mock.restore(); // Restore all mocks }); it("should register a tool named 'geocode'", () => { expect(serverInstance.tool).toHaveBeenCalled(); const mockCalls = serverInstance.tool.mock .calls; expect(mockCalls[0]?.[0]).toBe('geocode'); expect(typeof mockCalls[0]?.[1]).toBe('string'); // Description expect(mockCalls[0]?.[2]).toBeDefined(); // Schema expect(typeof mockCalls[0]?.[3]).toBe('function'); // Handler }); it('should call geocodeAddress and use actual handleGeocodeResult on successful execution', async () => { if (!toolHandler) throw new Error('Handler not registered'); const params = { query: '1600 Amphitheatre Parkway, Mountain View, CA', }; const mockGeocodeApiResult = [{ place_id: 1, display_name: 'Test Address' }]; // Expected result from the actual handleGeocodeResult const expectedCallToolResult = handleGeocodeResult(mockGeocodeApiResult); const geocodeAddressSpy = nominatimClient.geocodeAddress; geocodeAddressSpy.mockResolvedValue(mockGeocodeApiResult); const result = await toolHandler(params); expect(geocodeAddressSpy).toHaveBeenCalledWith(params); expect(result).toEqual(expectedCallToolResult); }); it('should pass params correctly to geocodeAddress', async () => { if (!toolHandler) throw new Error('Handler not registered'); const params = { query: 'Paris', format: 'json', addressdetails: 1, countrycodes: 'fr', }; const geocodeAddressSpy = nominatimClient.geocodeAddress; // Provide a default resolution for this spy instance for this test geocodeAddressSpy.mockResolvedValue([ { place_id: 456, display_name: 'Paris Result' }, ]); await toolHandler(params); expect(geocodeAddressSpy).toHaveBeenCalledWith(params); }); it('should handle errors from geocodeAddress by passing error to actual handleGeocodeResult', async () => { if (!toolHandler) throw new Error('Handler not registered'); const params = { query: 'trigger-api-error' }; const errorMessage = 'Nominatim API error during test'; // Use a distinct message const geocodeAddressSpy = nominatimClient.geocodeAddress; // Configure the mock to reject with a specific error when called geocodeAddressSpy.mockImplementation(async () => { throw new Error(errorMessage); }); // The handler should propagate the error thrown by geocodeAddress expect(toolHandler(params)).rejects.toThrow(errorMessage); expect(geocodeAddressSpy).toHaveBeenCalledWith(params); // In this scenario, handleGeocodeResult is NOT called by the tool's direct handler, // as the error from geocodeAddress propagates out of the handler first. }); });