@geocoding-ai/mcp
Version:
Model Context Protocol server for geocoding
97 lines (96 loc) • 4.46 kB
JavaScript
// 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.
});
});