- Created migration 009: project_archives table with semantic search - Implemented archives.ts: archiveAdd, archiveSearch, archiveList, archiveGet - Registered archive tools in index.ts and tools/index.ts - Archive types: session, research, audit, investigation, completed, migration - Uses project_key (TEXT) FK to projects table - Tested: archive_add and archive_list working correctly Replaces filesystem archives with database-backed storage. Eliminates context pollution from Glob/Grep operations. Task: CF-264 Session: session_20260119111342_66de546b Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
685 lines
18 KiB
JavaScript
685 lines
18 KiB
JavaScript
#!/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);
|
|
});
|