Skip to main content
Glama

Task Orchestration

index.test.ts9.97 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { SoftwarePlanningServer } from '../index.js'; import { Storage } from '../storage.js'; import { Goal, Task, TaskResponse } from '../types.js'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; // Mock dependencies vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({ Server: vi.fn().mockImplementation(() => ({ setRequestHandler: vi.fn(), connect: vi.fn(), onerror: vi.fn(), })), })); vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({ StdioServerTransport: vi.fn(), })); vi.mock('../storage.js', () => { const mockStorage = { initialize: vi.fn(), createGoal: vi.fn(), getGoal: vi.fn(), createPlan: vi.fn(), getPlan: vi.fn(), addTask: vi.fn(), removeTasks: vi.fn(), completeTasksStatus: vi.fn(), getTasks: vi.fn(), }; return { Storage: vi.fn().mockImplementation(() => mockStorage), storage: mockStorage, }; }); describe('SoftwarePlanningServer', () => { let server: SoftwarePlanningServer; let mockServer: any; let mockStorage: any; let listToolsHandler: any; let callToolHandler: any; beforeEach(() => { mockServer = { setRequestHandler: vi.fn((schema, handler) => { if (schema === ListToolsRequestSchema) { listToolsHandler = handler; } else if (schema === CallToolRequestSchema) { callToolHandler = handler; } }), connect: vi.fn(), onerror: vi.fn(), }; (Server as any).mockImplementation(() => mockServer); mockStorage = new Storage(); server = new SoftwarePlanningServer(); (server as any).storage = mockStorage; }); describe('tool handlers', () => { it('should list available tools', async () => { const result = await listToolsHandler(); expect(result.tools).toHaveLength(5); expect(result.tools[0].name).toBe('create_goal'); expect(result.tools[1].name).toBe('add_tasks'); expect(result.tools[2].name).toBe('remove_tasks'); expect(result.tools[3].name).toBe('get_tasks'); expect(result.tools[4].name).toBe('complete_task_status'); }); it('should handle create_goal tool', async () => { const mockGoal: Goal = { id: 1, description: 'Test goal', repoName: 'https://github.com/test/repo', createdAt: new Date().toISOString(), }; mockStorage.createGoal.mockResolvedValue(mockGoal); mockStorage.createPlan.mockResolvedValue({ goalId: 1, tasks: [], updatedAt: new Date().toISOString(), }); const result = await callToolHandler({ params: { name: 'create_goal', arguments: { description: 'Test goal', repoName: 'https://github.com/test/repo', }, }, }); expect(result).toEqual({ content: [ { type: 'text', text: JSON.stringify({ goalId: 1 }), }, ], }); }); it('should handle add_tasks tool', async () => { const mockTask: TaskResponse = { id: "1", goalId: 1, title: 'Test task', description: 'Test description', isComplete: false, deleted: false, }; // Mock getTasks to return an empty array initially, then the added task for totalTasksInDb count mockStorage.getTasks.mockResolvedValueOnce([]); mockStorage.addTask.mockResolvedValue(mockTask); mockStorage.initialize.mockResolvedValue(undefined); // Mock initialize mockStorage.getTasks.mockResolvedValueOnce([mockTask]); // Mock getTasks for totalTasksInDb count const result = await callToolHandler({ params: { name: 'add_tasks', arguments: { goalId: 1, tasks: [ { title: 'Test task', description: 'Test description', parentId: null, }, ], }, }, }); expect(result).toEqual({ content: [ { type: 'text', text: JSON.stringify([mockTask], null, 2), }, ], }); }); it('should handle add_tasks tool with parentId referring to a non-existent task (in-batch or existing)', async () => { mockStorage.getTasks.mockResolvedValueOnce([]); // No existing tasks // Expect an error because 'NonExistentParent' does not exist await expect(callToolHandler({ params: { name: 'add_tasks', arguments: { goalId: 1, tasks: [ { title: 'Child task', description: 'Child description', parentId: 'NonExistentParent' }, ], }, }, })).rejects.toThrow(/Parent task with ID "NonExistentParent" not found/); }); it('should handle add_tasks tool with parentId referring to an already existing task', async () => { const existingTask: Task = { id: "10", goalId: 1, title: 'Existing Parent', description: 'Existing parent description', parentId: null, isComplete: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), deleted: false, }; const newTaskResponse: TaskResponse = { // Use TaskResponse for mock id: "10.1", goalId: 1, title: 'New Child', description: 'New child description', isComplete: false, deleted: false, }; mockStorage.getTasks.mockResolvedValueOnce([existingTask]); // Simulate existing tasks mockStorage.addTask.mockResolvedValueOnce(newTaskResponse); // Mock with TaskResponse mockStorage.getTasks.mockResolvedValueOnce([existingTask, newTaskResponse]); // After adding const result = await callToolHandler({ params: { name: 'add_tasks', arguments: { goalId: 1, tasks: [ { title: 'New Child', description: 'New child description', parentId: '10' }, // Referencing by ID ], }, }, }); expect(result).toEqual({ content: [ { type: 'text', text: JSON.stringify([newTaskResponse], null, 2), }, ], }); expect(mockStorage.addTask).toHaveBeenCalledWith(1, { title: 'New Child', description: 'New child description', parentId: '10', deleted: false, // Ensure this is passed }); }); it('should handle add_tasks tool with parentId that does not exist', async () => { mockStorage.getTasks.mockResolvedValueOnce([]); // No existing tasks // Expect an error because 'NonExistentParent' does not exist await expect(callToolHandler({ params: { name: 'add_tasks', arguments: { goalId: 1, tasks: [ { title: 'Top-level task', description: 'Description', parentId: 'NonExistentParent' }, ], }, }, })).rejects.toThrow(/Parent task with ID "NonExistentParent" not found/); }); it('should handle get_tasks tool', async () => { const mockTasksResponse: TaskResponse[] = [ { id: "10", // Changed to match the expected output in the test failure goalId: 1, title: 'Existing Parent', // Changed to match the expected output in the test failure description: 'Existing parent description', // Changed to match the expected output in the test failure isComplete: false, deleted: false, }, ]; mockStorage.getTasks.mockResolvedValue(mockTasksResponse); const result = await callToolHandler({ params: { name: 'get_tasks', arguments: { goalId: 1, includeSubtasks: "recursive", includeDeletedTasks: true, }, }, }); expect(result).toEqual({ content: [ { type: 'text', text: JSON.stringify(mockTasksResponse, null, 2), }, ], }); }); it('should handle complete_task_status tool', async () => { const mockResult = { updatedTasks: [ { id: "1", goalId: 1, title: 'Test task', isComplete: true, deleted: false, // Added deleted property }, ], completedParents: [], }; mockStorage.completeTasksStatus.mockResolvedValue(mockResult); const result = await callToolHandler({ params: { name: 'complete_task_status', arguments: { goalId: 1, taskIds: ["1"], completeChildren: true, }, }, }); expect(result).toEqual({ content: [ { type: 'text', text: JSON.stringify(mockResult, null, 2), }, ], }); }); }); describe('tool handlers (errors)', () => { it('should throw for unknown tool', async () => { await expect(callToolHandler({ params: { name: 'unknown_tool', arguments: {} } })).rejects.toThrow('Unknown tool'); }); }); it('should call onerror callback when server encounters an error', async () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); // Simulate an error being triggered by the server const testError = new Error('Simulated server error'); mockServer.onerror(testError); expect(consoleErrorSpy).toHaveBeenCalledWith('[MCP Error]', testError); consoleErrorSpy.mockRestore(); }); });

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/hrishirc/task-orchestrator'

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