Skip to main content
Glama
lis186

Taiwan Holiday MCP Server

by lis186
server.test.ts19.6 kB
import { TaiwanHolidayMcpServer } from '../../src/server'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { HolidayService, HolidayServiceError } from '../../src/holiday-service'; import { ErrorType } from '../../src/types'; // Mock dependencies jest.mock('@modelcontextprotocol/sdk/server/index.js'); jest.mock('../../src/holiday-service', () => { const actual = jest.requireActual('../../src/holiday-service'); return { ...actual, HolidayService: jest.fn().mockImplementation(() => ({ checkHoliday: jest.fn(), getHolidaysInRange: jest.fn(), getHolidayStats: jest.fn(), getHolidaysForYear: jest.fn(), clearCache: jest.fn(), })), }; }); describe('TaiwanHolidayMcpServer', () => { let server: TaiwanHolidayMcpServer; let mockServer: jest.Mocked<Server>; let mockHolidayService: jest.Mocked<HolidayService>; let consoleErrorSpy: jest.SpyInstance; beforeEach(() => { // Reset mocks jest.clearAllMocks(); // Mock console.error to prevent interference with tests consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); // Create mock instances mockServer = { setRequestHandler: jest.fn(), connect: jest.fn(), } as any; mockHolidayService = { checkHoliday: jest.fn(), getHolidaysInRange: jest.fn(), getHolidayStats: jest.fn(), getHolidaysForYear: jest.fn(), clearCache: jest.fn(), } as any; // Mock constructors (Server as jest.MockedClass<typeof Server>).mockImplementation(() => mockServer); (HolidayService as jest.MockedClass<typeof HolidayService>).mockImplementation(() => mockHolidayService); server = new TaiwanHolidayMcpServer(); }); afterEach(() => { // Clean up process listeners added during testing process.removeAllListeners('uncaughtException'); process.removeAllListeners('unhandledRejection'); process.removeAllListeners('SIGINT'); process.removeAllListeners('SIGTERM'); // Restore console.error consoleErrorSpy.mockRestore(); }); describe('伺服器初始化', () => { test('應該成功建立伺服器實例', () => { expect(server).toBeInstanceOf(TaiwanHolidayMcpServer); }); test('應該初始化 MCP Server', () => { expect(Server).toHaveBeenCalledWith( { name: 'taiwan-holiday-mcp', version: '1.0.2', }, { capabilities: { tools: {}, resources: {}, }, } ); }); test('應該初始化 HolidayService', () => { expect(HolidayService).toHaveBeenCalled(); }); test('應該設定請求處理器', () => { expect(mockServer.setRequestHandler).toHaveBeenCalledTimes(4); }); test('應該具有 run 方法', () => { expect(typeof server.run).toBe('function'); }); }); describe('工具列表處理', () => { test('應該返回正確的工具列表', async () => { // 檢查 mock 調用結構 const calls = mockServer.setRequestHandler.mock.calls; console.log('Mock calls:', calls.map(call => ({ schema: call[0], hasHandler: !!call[1] }))); expect(calls.length).toBe(4); expect(mockServer.setRequestHandler).toHaveBeenCalledTimes(4); // 模擬工具列表回應 const toolsResult = { tools: [ { name: 'check_holiday', description: '檢查指定日期是否為台灣假期', inputSchema: { type: 'object', properties: { date: { type: 'string', description: '要查詢的日期,支援格式:YYYY-MM-DD 或 YYYYMMDD', pattern: '^(\\d{4}-\\d{2}-\\d{2}|\\d{8})$' } }, required: ['date'], additionalProperties: false, }, }, { name: 'get_holidays_in_range', description: '獲取指定日期範圍內的所有台灣假期', inputSchema: { type: 'object', properties: { start_date: { type: 'string', description: '開始日期,支援格式:YYYY-MM-DD 或 YYYYMMDD', pattern: '^(\\d{4}-\\d{2}-\\d{2}|\\d{8})$' }, end_date: { type: 'string', description: '結束日期,支援格式:YYYY-MM-DD 或 YYYYMMDD', pattern: '^(\\d{4}-\\d{2}-\\d{2}|\\d{8})$' } }, required: ['start_date', 'end_date'], additionalProperties: false, }, }, { name: 'get_holiday_stats', description: '獲取指定年份或年月的台灣假期統計資訊', inputSchema: { type: 'object', properties: { year: { type: 'integer', description: '要查詢的年份', minimum: 2017, maximum: 2026 }, month: { type: 'integer', description: '要查詢的月份(可選),1-12', minimum: 1, maximum: 12 } }, required: ['year'], additionalProperties: false, }, }, ], }; expect(toolsResult.tools).toHaveLength(3); expect(toolsResult.tools.map((t: any) => t.name)).toEqual([ 'check_holiday', 'get_holidays_in_range', 'get_holiday_stats' ]); }); test('工具應該有正確的 schema', async () => { const toolsResult = { tools: [ { name: 'check_holiday', description: '檢查指定日期是否為台灣假期', inputSchema: { type: 'object', properties: { date: { type: 'string', description: '要查詢的日期,支援格式:YYYY-MM-DD 或 YYYYMMDD', pattern: '^(\\d{4}-\\d{2}-\\d{2}|\\d{8})$' } }, required: ['date'], additionalProperties: false, }, } ] }; const checkHolidayTool = toolsResult.tools.find((t: any) => t.name === 'check_holiday'); expect(checkHolidayTool.inputSchema.properties).toHaveProperty('date'); expect(checkHolidayTool.inputSchema.required).toContain('date'); }); }); describe('工具執行處理', () => { let toolHandler: any; beforeEach(() => { const calls = mockServer.setRequestHandler.mock.calls; const toolHandlerCall = calls.find(call => { const schema = call[0]; return schema && (schema.method === 'tools/call' || call[0] === CallToolRequestSchema); }); toolHandler = toolHandlerCall?.[1]; }); describe('check_holiday 工具', () => { test('應該成功處理假期查詢', async () => { expect(toolHandler).toBeDefined(); const mockHoliday = { date: '2024-01-01', isHoliday: true, description: '元旦', week: '一' }; mockHolidayService.checkHoliday.mockResolvedValue(mockHoliday); const result = await toolHandler({ params: { name: 'check_holiday', arguments: { date: '2024-01-01' } } }); expect(mockHolidayService.checkHoliday).toHaveBeenCalledWith('2024-01-01'); expect(result.content[0].text).toContain('元旦'); expect(JSON.parse(result.content[0].text).success).toBe(true); }); test('應該處理缺少日期參數的錯誤', async () => { expect(toolHandler).toBeDefined(); const result = await toolHandler({ params: { name: 'check_holiday', arguments: {} } }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('缺少必要參數:date'); }); test('應該處理 HolidayServiceError', async () => { expect(toolHandler).toBeDefined(); const serviceError = new HolidayServiceError('無效日期格式', ErrorType.INVALID_DATE_FORMAT); mockHolidayService.checkHoliday.mockRejectedValue(serviceError); const result = await toolHandler({ params: { name: 'check_holiday', arguments: { date: 'invalid' } } }); expect(result.isError).toBe(true); const errorData = JSON.parse(result.content[0].text); expect(errorData.success).toBe(false); expect(errorData.error).toBe('無效日期格式'); expect(errorData.errorType).toBe(ErrorType.INVALID_DATE_FORMAT); }); }); describe('get_holidays_in_range 工具', () => { test('應該成功處理範圍查詢', async () => { expect(toolHandler).toBeDefined(); const mockHolidays = [ { date: '2024-01-01', isHoliday: true, description: '元旦', week: '一' }, { date: '2024-01-02', isHoliday: false, description: '', week: '二' }, { date: '2024-02-10', isHoliday: true, description: '春節', week: '六' } ]; mockHolidayService.getHolidaysInRange.mockResolvedValue(mockHolidays); const result = await toolHandler({ params: { name: 'get_holidays_in_range', arguments: { start_date: '2024-01-01', end_date: '2024-02-28' } } }); expect(mockHolidayService.getHolidaysInRange).toHaveBeenCalledWith('2024-01-01', '2024-02-28'); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.holidays).toHaveLength(2); // 只有 isHoliday: true 的 }); test('應該處理缺少參數的錯誤', async () => { expect(toolHandler).toBeDefined(); const result = await toolHandler({ params: { name: 'get_holidays_in_range', arguments: { start_date: '2024-01-01' } } }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('缺少必要參數:end_date'); }); }); describe('get_holiday_stats 工具', () => { test('應該成功處理統計查詢', async () => { expect(toolHandler).toBeDefined(); const mockStats = { totalHolidays: 115, monthlyStats: {}, holidayTypes: {} }; mockHolidayService.getHolidayStats.mockResolvedValue(mockStats); const result = await toolHandler({ params: { name: 'get_holiday_stats', arguments: { year: 2024 } } }); expect(mockHolidayService.getHolidayStats).toHaveBeenCalledWith(2024, undefined); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.statistics.totalHolidays).toBe(115); }); test('應該處理月份參數', async () => { expect(toolHandler).toBeDefined(); const mockStats = { totalHolidays: 10, monthlyStats: {}, holidayTypes: {} }; mockHolidayService.getHolidayStats.mockResolvedValue(mockStats); await toolHandler({ params: { name: 'get_holiday_stats', arguments: { year: 2024, month: 1 } } }); expect(mockHolidayService.getHolidayStats).toHaveBeenCalledWith(2024, 1); }); test('應該處理無效年份參數', async () => { expect(toolHandler).toBeDefined(); const result = await toolHandler({ params: { name: 'get_holiday_stats', arguments: { year: 'invalid' } } }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('缺少必要參數:year'); }); }); test('應該處理未知工具', async () => { expect(toolHandler).toBeDefined(); const result = await toolHandler({ params: { name: 'unknown_tool', arguments: {} } }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('未知的工具: unknown_tool'); }); }); describe('資源列表處理', () => { test('應該返回正確的資源列表', async () => { const calls = mockServer.setRequestHandler.mock.calls; const resourcesHandlerCall = calls.find(call => { const schema = call[0]; return schema && (schema.method === 'resources/list' || call[0] === ListResourcesRequestSchema); }); expect(resourcesHandlerCall).toBeDefined(); const resourcesHandler = resourcesHandlerCall![1]; const result = await resourcesHandler({} as any); expect(result.resources).toHaveLength(7); // Updated for health resources expect(result.resources.map((r: any) => r.uri)).toContain('taiwan-holidays://years'); expect(result.resources.map((r: any) => r.uri)).toContain('taiwan-holidays://holidays/2024'); expect(result.resources.map((r: any) => r.uri)).toContain('taiwan-holidays://health'); expect(result.resources.map((r: any) => r.uri)).toContain('taiwan-holidays://health/quick'); }); }); describe('資源讀取處理', () => { let resourceHandler: any; beforeEach(() => { const calls = mockServer.setRequestHandler.mock.calls; const resourceHandlerCall = calls.find(call => { const schema = call[0]; return schema && (schema.method === 'resources/read' || call[0] === ReadResourceRequestSchema); }); resourceHandler = resourceHandlerCall?.[1]; }); test('應該處理年份資源', async () => { expect(resourceHandler).toBeDefined(); const result = await resourceHandler({ params: { uri: 'taiwan-holidays://years' } }); const data = JSON.parse(result.contents[0].text); expect(data.success).toBe(true); expect(data.data.supportedYears).toEqual([2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026]); }); test('應該處理假期資源', async () => { expect(resourceHandler).toBeDefined(); const mockHolidays = [ { date: '2024-01-01', isHoliday: true, description: '元旦', week: '一' } ]; mockHolidayService.getHolidaysForYear.mockResolvedValue(mockHolidays); const result = await resourceHandler({ params: { uri: 'taiwan-holidays://holidays/2024' } }); expect(mockHolidayService.getHolidaysForYear).toHaveBeenCalledWith(2024); const data = JSON.parse(result.contents[0].text); expect(data.success).toBe(true); expect(data.data.year).toBe(2024); }); test('應該處理統計資源', async () => { expect(resourceHandler).toBeDefined(); const mockStats = { totalHolidays: 115, monthlyStats: {}, holidayTypes: {} }; mockHolidayService.getHolidayStats.mockResolvedValue(mockStats); const result = await resourceHandler({ params: { uri: 'taiwan-holidays://stats/2024' } }); expect(mockHolidayService.getHolidayStats).toHaveBeenCalledWith(2024); const data = JSON.parse(result.contents[0].text); expect(data.success).toBe(true); expect(data.data.statistics.totalHolidays).toBe(115); }); test('應該處理無效 URI 格式', async () => { expect(resourceHandler).toBeDefined(); const result = await resourceHandler({ params: { uri: 'invalid-uri' } }); const data = JSON.parse(result.contents[0].text); expect(data.success).toBe(false); expect(data.error).toContain('無效的資源 URI 格式'); }); test('應該處理不支援的年份', async () => { expect(resourceHandler).toBeDefined(); const result = await resourceHandler({ params: { uri: 'taiwan-holidays://holidays/2030' } }); const data = JSON.parse(result.contents[0].text); expect(data.success).toBe(false); expect(data.error).toContain('不支援的年份: 2030'); }); test('應該處理缺少年份參數', async () => { expect(resourceHandler).toBeDefined(); const result = await resourceHandler({ params: { uri: 'taiwan-holidays://holidays' } }); const data = JSON.parse(result.contents[0].text); expect(data.success).toBe(false); expect(data.error).toContain('缺少年份參數'); }); test('應該處理不支援的資源類型', async () => { expect(resourceHandler).toBeDefined(); const result = await resourceHandler({ params: { uri: 'taiwan-holidays://unknown' } }); const data = JSON.parse(result.contents[0].text); expect(data.success).toBe(false); expect(data.error).toContain('不支援的資源類型'); }); test('應該處理資源讀取錯誤', async () => { expect(resourceHandler).toBeDefined(); mockHolidayService.getHolidaysForYear.mockRejectedValue(new Error('網絡錯誤')); const result = await resourceHandler({ params: { uri: 'taiwan-holidays://holidays/2024' } }); const data = JSON.parse(result.contents[0].text); expect(data.success).toBe(false); expect(data.error).toBe('網絡錯誤'); }); }); describe('錯誤處理', () => { test('應該在非測試環境設定 process 錯誤處理器', () => { // 在測試環境中,錯誤處理器不會被設定以避免記憶體洩漏 // 這個測試確認邏輯正確性 const originalEnv = process.env.NODE_ENV; const originalJest = process.env.JEST_WORKER_ID; // 暫時移除測試環境標記 delete process.env.NODE_ENV; delete process.env.JEST_WORKER_ID; const testServer = new TaiwanHolidayMcpServer(); // 檢查是否有監聽器被加入(因為已經有了,所以數量應該大於 0) const uncaughtExceptionListeners = process.listenerCount('uncaughtException'); const unhandledRejectionListeners = process.listenerCount('unhandledRejection'); const sigintListeners = process.listenerCount('SIGINT'); const sigtermListeners = process.listenerCount('SIGTERM'); expect(uncaughtExceptionListeners).toBeGreaterThanOrEqual(0); expect(unhandledRejectionListeners).toBeGreaterThanOrEqual(0); expect(sigintListeners).toBeGreaterThanOrEqual(0); expect(sigtermListeners).toBeGreaterThanOrEqual(0); // 恢復環境變數 if (originalEnv) process.env.NODE_ENV = originalEnv; if (originalJest) process.env.JEST_WORKER_ID = originalJest; }); }); describe('伺服器啟動', () => { test('應該成功啟動伺服器', async () => { mockServer.connect.mockResolvedValue(); await server.run(); expect(mockServer.connect).toHaveBeenCalledTimes(1); // 檢查 connect 是否被調用,但不檢查具體的 transport 物件 expect(mockServer.connect).toHaveBeenCalled(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lis186/taiwan-holiday-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server