#!/usr/bin/env node /** * Task MCP Server * * Exposes task management tools via Model Context Protocol. * Uses PostgreSQL with pgvector for semantic search. * * Requires SSH tunnel to docker-host: * ssh -L 5432:172.27.0.3:5432 docker-host -N & */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { testConnection, close } from './db.js'; import { toolDefinitions } from './tools/index.js'; import { taskAdd, taskList, taskShow, taskClose, taskUpdate } from './tools/crud.js'; import { taskSimilar, taskContext } from './tools/search.js'; import { taskLink, checklistAdd, checklistToggle, taskResolveDuplicate } from './tools/relations.js'; import { epicAdd, epicList, epicShow, epicAssign, epicClose } from './tools/epics.js'; import { taskDelegations, taskDelegationQuery } from './tools/delegations.js'; // Create MCP server const server = new Server( { name: 'task-mcp', version: '1.0.0' }, { capabilities: { tools: {} } } ); // Register tool list handler server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions, })); // Register tool call handler server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { let result: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any const a = args as any; switch (name) { // CRUD case 'task_add': result = await taskAdd({ title: a.title, project: a.project, type: a.type, priority: a.priority, description: a.description, }); break; case 'task_list': result = await taskList({ project: a.project, status: a.status, type: a.type, priority: a.priority, limit: a.limit, }); break; case 'task_show': result = await taskShow(a.id); break; case 'task_close': result = await taskClose(a.id); break; case 'task_update': result = await taskUpdate({ id: a.id, status: a.status, priority: a.priority, type: a.type, title: a.title, }); break; // Search case 'task_similar': result = await taskSimilar({ query: a.query, project: a.project, limit: a.limit, }); break; case 'task_context': result = await taskContext({ description: a.description, project: a.project, limit: a.limit, }); break; // Relations case 'task_link': result = await taskLink({ from_id: a.from_id, to_id: a.to_id, link_type: a.link_type, }); break; case 'task_checklist_add': result = await checklistAdd({ task_id: a.task_id, item: a.item, }); break; case 'task_checklist_toggle': result = await checklistToggle({ item_id: a.item_id, checked: a.checked, }); break; case 'task_resolve_duplicate': result = await taskResolveDuplicate({ duplicate_id: a.duplicate_id, dominant_id: a.dominant_id, }); break; // Epics case 'epic_add': result = await epicAdd({ title: a.title, project: a.project, description: a.description, }); break; case 'epic_list': result = await epicList({ project: a.project, status: a.status, limit: a.limit, }); break; case 'epic_show': result = await epicShow(a.id); break; case 'epic_assign': result = await epicAssign({ task_id: a.task_id, epic_id: a.epic_id, }); break; case 'epic_close': result = await epicClose(a.id); break; // Delegations case 'task_delegations': result = await taskDelegations({ task_id: a.task_id }); break; case 'task_delegation_query': result = await taskDelegationQuery({ status: a.status, backend: a.backend, limit: a.limit, }); break; default: throw new Error(`Unknown tool: ${name}`); } return { content: [{ type: 'text', text: result }], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true, }; } }); // Main entry point async function main() { // Test database connection const connected = await testConnection(); if (!connected) { console.error('Failed to connect to database. Ensure SSH tunnel is active:'); console.error(' ssh -L 5432:172.27.0.3:5432 docker-host -N &'); process.exit(1); } console.error('task-mcp: Connected to database'); // Set up cleanup process.on('SIGINT', async () => { await close(); process.exit(0); }); process.on('SIGTERM', async () => { await close(); process.exit(0); }); // Start server const transport = new StdioServerTransport(); await server.connect(transport); console.error('task-mcp: Server started'); } main().catch((error) => { console.error('task-mcp: Fatal error:', error); process.exit(1); });