/**
* Contract tests for MCP Tools
* Validates that tool implementations match the specification contracts
*/
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import contractSpec from '../../specs/001-hackernews-mcp-server/contracts/mcp-tools.json' assert { type: 'json' };
import type { HNClient } from '../../src/lib/hn-client.js';
import { registerGetAskHN } from '../../src/tools/get-ask-hn.js';
import { registerGetFrontPage } from '../../src/tools/get-front-page.js';
import { registerGetLatestStories } from '../../src/tools/get-latest-stories.js';
import { registerGetShowHN } from '../../src/tools/get-show-hn.js';
import { registerGetStory } from '../../src/tools/get-story.js';
import { registerGetUser } from '../../src/tools/get-user.js';
import { registerSearchByDate } from '../../src/tools/search-by-date.js';
import { registerSearchComments } from '../../src/tools/search-comments.js';
import { registerSearchStories } from '../../src/tools/search-stories.js';
describe('MCP Tools Contract Tests', () => {
let mockServer: McpServer;
let mockHnClient: HNClient;
let registeredTools: Map<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
beforeEach(() => {
registeredTools = new Map();
// Mock server that captures tool registrations
mockServer = {
registerTool: vi.fn((name: string, schema: any, handler: any) => {
// eslint-disable-line @typescript-eslint/no-explicit-any
registeredTools.set(name, { schema, handler });
}),
} as unknown as McpServer;
// Mock HN client
mockHnClient = {} as any; // eslint-disable-line @typescript-eslint/no-explicit-any
// Register all tools
const toolRegistrations = [
registerSearchStories,
registerSearchByDate,
registerGetStory,
registerGetUser,
registerGetFrontPage,
registerGetLatestStories,
registerGetAskHN,
registerGetShowHN,
registerSearchComments,
];
for (const registerFn of toolRegistrations) {
registerFn(mockServer, mockHnClient);
}
});
for (const contractTool of contractSpec.tools) {
describe(`Tool: ${contractTool.name}`, () => {
it('should be registered', () => {
expect(registeredTools.has(contractTool.name)).toBe(true);
});
it('should have correct title', () => {
const registeredTool = registeredTools.get(contractTool.name);
expect(registeredTool.schema.title).toBe(contractTool.title);
});
it('should have correct description', () => {
const registeredTool = registeredTools.get(contractTool.name);
expect(registeredTool.schema.description).toBe(contractTool.description);
});
it('should have input schema matching contract', () => {
const registeredTool = registeredTools.get(contractTool.name);
const registeredSchema = registeredTool.schema.inputSchema;
// Check that all required properties from contract are present
// Note: This is a simplified check since Zod schemas have complex internal structure
// The unit tests already validate the schema behavior
expect(registeredSchema).toBeDefined();
expect(typeof registeredSchema).toBe('object');
});
it('should have output schema matching contract', () => {
const registeredTool = registeredTools.get(contractTool.name);
const registeredOutputSchema = registeredTool.schema.outputSchema;
// Check that output schema has expected structure
// Note: This is a simplified check since Zod schemas have complex internal structure
// The unit tests already validate the schema behavior
expect(registeredOutputSchema).toBeDefined();
expect(typeof registeredOutputSchema).toBe('object');
});
it('should have a handler function', () => {
const registeredTool = registeredTools.get(contractTool.name);
expect(typeof registeredTool.handler).toBe('function');
});
});
}
describe('Contract Coverage', () => {
it('should implement all tools specified in contract', () => {
const contractToolNames = contractSpec.tools.map((tool: any) => tool.name); // eslint-disable-line @typescript-eslint/no-explicit-any
const registeredToolNames = Array.from(registeredTools.keys());
for (const contractName of contractToolNames) {
expect(registeredToolNames).toContain(contractName);
}
});
it('should not have extra tools not in contract', () => {
const contractToolNames = contractSpec.tools.map((tool: any) => tool.name); // eslint-disable-line @typescript-eslint/no-explicit-any
const registeredToolNames = Array.from(registeredTools.keys());
for (const registeredName of registeredToolNames) {
expect(contractToolNames).toContain(registeredName);
}
});
});
describe('Error Codes', () => {
it('should define all error codes from contract', () => {
// This test ensures the error handling matches contract expectations
const contractErrorCodes = Object.keys(contractSpec.errorCodes);
expect(contractErrorCodes).toContain('RATE_LIMIT_EXCEEDED');
expect(contractErrorCodes).toContain('ITEM_NOT_FOUND');
expect(contractErrorCodes).toContain('INVALID_PARAMETERS');
expect(contractErrorCodes).toContain('API_ERROR');
expect(contractErrorCodes).toContain('NETWORK_ERROR');
});
});
});