#!/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 infra VM on port 5433: * ssh -L 5433:localhost:5432 -i ~/.ssh/hetzner_mash_deploy root@46.224.188.157 -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'; import { projectLock, projectUnlock, projectLockStatus, projectContext } from './tools/locks.js'; import { versionAdd, versionList, versionShow, versionUpdate, versionRelease, versionAssignTask } from './tools/versions.js'; import { taskCommitAdd, taskCommitRemove, taskCommitsList, taskLinkCommits, sessionTasks } from './tools/commits.js'; import { changelogAdd, changelogSinceSession, changelogList } from './tools/changelog.js'; import { componentRegister, componentList, componentAddDependency, componentAddFile, componentAddCheck, impactAnalysis, impactLearn, componentGraph, } from './tools/impact.js'; import { memoryAdd, memorySearch, memoryList, memoryContext } from './tools/memories.js'; import { toolDocAdd, toolDocSearch, toolDocGet, toolDocList, toolDocExport } from './tools/tool-docs.js'; import { sessionStart, sessionUpdate, sessionEnd, sessionList, sessionSearch, sessionContext, buildRecord, sessionCommitLink, } from './tools/sessions.js'; import { sessionNoteAdd, sessionNotesList, sessionPlanSave, sessionPlanUpdateStatus, sessionPlanList, projectDocUpsert, projectDocGet, projectDocList, sessionDocumentationGenerate, sessionSemanticSearch, sessionProductivityAnalytics, sessionPatternDetection, } from './tools/session-docs.js'; import { archiveAdd, archiveSearch, archiveList, archiveGet } from './tools/archives.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; // Project Locks case 'project_lock': result = await projectLock({ project: a.project, session_id: a.session_id, duration_minutes: a.duration_minutes, reason: a.reason, }); break; case 'project_unlock': result = await projectUnlock({ project: a.project, session_id: a.session_id, force: a.force, }); break; case 'project_lock_status': result = await projectLockStatus({ project: a.project, }); break; case 'project_context': result = await projectContext(); break; // Versions case 'version_add': result = await versionAdd({ project: a.project, version: a.version, build_number: a.build_number, status: a.status, release_notes: a.release_notes, }); break; case 'version_list': result = await versionList({ project: a.project, status: a.status, limit: a.limit, }); break; case 'version_show': result = await versionShow(a.id); break; case 'version_update': result = await versionUpdate({ id: a.id, status: a.status, git_tag: a.git_tag, git_sha: a.git_sha, release_notes: a.release_notes, release_date: a.release_date, }); break; case 'version_release': result = await versionRelease({ id: a.id, git_tag: a.git_tag, }); break; case 'version_assign_task': result = await versionAssignTask({ task_id: a.task_id, version_id: a.version_id, }); break; // Commits case 'task_commit_add': result = await taskCommitAdd({ task_id: a.task_id, commit_sha: a.commit_sha, repo: a.repo, source: a.source, }); break; case 'task_commit_remove': result = await taskCommitRemove({ task_id: a.task_id, commit_sha: a.commit_sha, }); break; case 'task_commits_list': result = await taskCommitsList(a.task_id); break; case 'task_link_commits': result = await taskLinkCommits({ repo: a.repo, commits: a.commits, dry_run: a.dry_run, }); break; case 'session_tasks': result = await sessionTasks({ session_id: a.session_id, limit: a.limit, }); break; // Infrastructure Changelog case 'changelog_add': result = await changelogAdd(a as any); break; case 'changelog_since_session': result = await changelogSinceSession(a as any); break; case 'changelog_list': result = await changelogList(a.days_back, a.limit); break; // Impact Analysis case 'component_register': result = JSON.stringify(await componentRegister(a.id, a.name, a.type, { path: a.path, repo: a.repo, description: a.description, health_check: a.health_check, }), null, 2); break; case 'component_list': result = JSON.stringify(await componentList(a.type), null, 2); break; case 'component_add_dependency': result = JSON.stringify(await componentAddDependency( a.component_id, a.depends_on, a.dependency_type, a.description ), null, 2); break; case 'component_add_file': result = JSON.stringify(await componentAddFile(a.component_id, a.file_pattern), null, 2); break; case 'component_add_check': result = JSON.stringify(await componentAddCheck(a.component_id, a.name, a.check_type, a.check_command, { expected_result: a.expected_result, timeout_seconds: a.timeout_seconds, }), null, 2); break; case 'impact_analysis': result = JSON.stringify(await impactAnalysis(a.changed_files), null, 2); break; case 'impact_learn': result = JSON.stringify(await impactLearn( a.changed_component, a.affected_component, a.impact_description, { error_id: a.error_id, task_id: a.task_id } ), null, 2); break; case 'component_graph': result = JSON.stringify(await componentGraph(a.component_id), null, 2); break; // Memories case 'memory_add': result = await memoryAdd({ category: a.category, title: a.title, content: a.content, context: a.context, project: a.project, session_id: a.session_id, task_id: a.task_id, }); break; case 'memory_search': result = await memorySearch({ query: a.query, project: a.project, category: a.category, limit: a.limit, }); break; case 'memory_list': result = await memoryList({ project: a.project, category: a.category, limit: a.limit, }); break; case 'memory_context': result = await memoryContext(a.project, a.task_description); break; // Tool Documentation case 'tool_doc_add': result = await toolDocAdd({ tool_name: a.tool_name, category: a.category, title: a.title, description: a.description, usage_example: a.usage_example, parameters: a.parameters, notes: a.notes, tags: a.tags, source_file: a.source_file, }); break; case 'tool_doc_search': result = await toolDocSearch({ query: a.query, category: a.category, tags: a.tags, limit: a.limit, }); break; case 'tool_doc_get': result = await toolDocGet({ tool_name: a.tool_name, }); break; case 'tool_doc_list': result = await toolDocList({ category: a.category, tag: a.tag, limit: a.limit, }); break; case 'tool_doc_export': result = await toolDocExport(); break; // Sessions case 'session_start': result = await sessionStart({ session_id: a.session_id, project: a.project, working_directory: a.working_directory, git_branch: a.git_branch, initial_prompt: a.initial_prompt, }); break; case 'session_update': result = await sessionUpdate({ session_id: a.session_id, message_count: a.message_count, token_count: a.token_count, tools_used: a.tools_used, }); break; case 'session_end': result = await sessionEnd({ session_id: a.session_id, summary: a.summary, status: a.status, }); break; case 'session_list': result = await sessionList({ project: a.project, status: a.status, since: a.since, limit: a.limit, }); break; case 'session_search': result = await sessionSearch({ query: a.query, project: a.project, limit: a.limit, }); break; case 'session_context': result = await sessionContext(a.session_id); break; case 'build_record': result = await buildRecord( a.session_id, a.version_id, a.build_number, a.git_commit_sha, a.status, a.started_at ); break; case 'session_commit_link': result = await sessionCommitLink( a.session_id, a.commit_sha, a.repo, a.commit_message, a.committed_at ); break; // Session Documentation case 'session_note_add': result = await sessionNoteAdd({ session_id: a.session_id, note_type: a.note_type, content: a.content, }); break; case 'session_notes_list': result = JSON.stringify( await sessionNotesList({ session_id: a.session_id, note_type: a.note_type, }), null, 2 ); break; case 'session_plan_save': result = await sessionPlanSave({ session_id: a.session_id, plan_content: a.plan_content, plan_file_name: a.plan_file_name, status: a.status, }); break; case 'session_plan_update_status': result = await sessionPlanUpdateStatus({ plan_id: a.plan_id, status: a.status, }); break; case 'session_plan_list': result = JSON.stringify( await sessionPlanList({ session_id: a.session_id, status: a.status, }), null, 2 ); break; case 'project_doc_upsert': result = await projectDocUpsert({ project: a.project, doc_type: a.doc_type, title: a.title, content: a.content, session_id: a.session_id, }); break; case 'project_doc_get': result = JSON.stringify( await projectDocGet({ project: a.project, doc_type: a.doc_type, }), null, 2 ); break; case 'project_doc_list': result = JSON.stringify( await projectDocList({ project: a.project, }), null, 2 ); break; case 'session_documentation_generate': result = await sessionDocumentationGenerate({ session_id: a.session_id, }); break; case 'session_semantic_search': result = JSON.stringify( await sessionSemanticSearch({ query: a.query, project: a.project, limit: a.limit, }), null, 2 ); break; case 'session_productivity_analytics': result = JSON.stringify( await sessionProductivityAnalytics({ project: a.project, time_period: a.time_period, }), null, 2 ); break; case 'session_pattern_detection': result = JSON.stringify( await sessionPatternDetection({ project: a.project, pattern_type: a.pattern_type, }), null, 2 ); break; // Archives case 'archive_add': result = await archiveAdd({ project: a.project, archive_type: a.archive_type, title: a.title, content: a.content, original_path: a.original_path, file_size: a.file_size, archived_by_session: a.archived_by_session, metadata: a.metadata, }); break; case 'archive_search': result = await archiveSearch({ query: a.query, project: a.project, archive_type: a.archive_type, limit: a.limit, }); break; case 'archive_list': result = await archiveList({ project: a.project, archive_type: a.archive_type, since: a.since, limit: a.limit, }); break; case 'archive_get': result = await archiveGet({ id: a.id, }); 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() { // Set up cleanup process.on('SIGINT', async () => { await close(); process.exit(0); }); process.on('SIGTERM', async () => { await close(); process.exit(0); }); // Start server FIRST - respond to MCP protocol immediately // This is critical: Claude Code sends initialize before we finish DB connection const transport = new StdioServerTransport(); await server.connect(transport); console.error('task-mcp: Server started'); // Test database connection in background (lazy - will connect on first tool call anyway) testConnection().then((connected) => { if (connected) { console.error('task-mcp: Connected to database'); } else { console.error('task-mcp: Warning - database not reachable, will retry on tool calls'); } }); } main().catch((error) => { console.error('task-mcp: Fatal error:', error); process.exit(1); });