feat: add HTTP transport (CF-3081)
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.git
|
||||
*.log
|
||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package*.json tsconfig.json ./
|
||||
RUN npm install
|
||||
COPY src ./src
|
||||
RUN npm run build
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev && npm cache clean --force
|
||||
COPY --from=build /app/dist ./dist
|
||||
USER node
|
||||
EXPOSE 9216
|
||||
CMD ["node", "dist/http-server.js"]
|
||||
2811
package-lock.json
generated
2811
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "session-mcp",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "MCP server for session/memory/archive management with PostgreSQL/pgvector. Forked from task-mcp (CF-762).",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
@@ -8,19 +8,22 @@
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts",
|
||||
"clean": "rm -rf dist"
|
||||
"clean": "rm -rf dist",
|
||||
"start:http": "node dist/http-server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"@sentry/node": "^10.39.0",
|
||||
"@sentry/profiling-node": "^10.39.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"pg": "^8.11.3"
|
||||
"pg": "^8.11.3",
|
||||
"express": "^4.19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/pg": "^8.10.9",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.3.3",
|
||||
"@types/express": "^4.17.21"
|
||||
}
|
||||
}
|
||||
|
||||
44
src/http-server.ts
Normal file
44
src/http-server.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
import dotenv from "dotenv";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
dotenv.config({ path: join(__dirname, "..", ".env"), override: true });
|
||||
import { initSentry } from "./sentry.js";
|
||||
initSentry(process.env.SENTRY_ENVIRONMENT || "production");
|
||||
import express from "express";
|
||||
import { randomUUID } from "crypto";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { testConnection, close } from "./db.js";
|
||||
import { createServer } from "./server.js";
|
||||
const PORT = parseInt(process.env.MCP_HTTP_PORT || "9216");
|
||||
const HOST = process.env.MCP_HTTP_HOST || "0.0.0.0";
|
||||
const transports = new Map<string, StreamableHTTPServerTransport>();
|
||||
const sessionServers = new Map<string, Server>();
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.post("/mcp", async (req, res) => {
|
||||
try {
|
||||
const sid0 = req.headers["mcp-session-id"] as string | undefined;
|
||||
if (sid0 && transports.has(sid0)) { await transports.get(sid0)!.handleRequest(req, res, req.body); return; }
|
||||
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (sid) => { transports.set(sid, transport); },
|
||||
});
|
||||
transport.onclose = () => { const sid = transport.sessionId; if (sid) { transports.delete(sid); sessionServers.delete(sid); } };
|
||||
const ss = createServer(); await ss.connect(transport);
|
||||
const sid = transport.sessionId; if (sid) sessionServers.set(sid, ss);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (err) { console.error("[session-mcp]", err); if (!res.headersSent) res.status(500).json({ error: "Internal" }); }
|
||||
});
|
||||
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"] as string|undefined; if(!sid||!transports.has(sid)){res.status(400).json({error:"bad"});return;} await transports.get(sid)!.handleRequest(req,res); });
|
||||
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"] as string|undefined; if(!sid||!transports.has(sid)){res.status(400).json({error:"bad"});return;} await transports.get(sid)!.handleRequest(req,res); });
|
||||
app.get("/health", (_req, res) => res.json({ status: "ok", server: "session-mcp", activeSessions: transports.size }));
|
||||
(async () => {
|
||||
if (!(await testConnection())) { console.error("DB connect failed"); process.exit(1); }
|
||||
app.listen(PORT, HOST, () => console.error(`session-mcp: HTTP on http://${HOST}:${PORT}/mcp`));
|
||||
})();
|
||||
process.on("SIGINT", async () => { await close(); process.exit(0); });
|
||||
process.on("SIGTERM", async () => { await close(); process.exit(0); });
|
||||
578
src/index.ts
578
src/index.ts
@@ -1,566 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Session MCP Server
|
||||
*
|
||||
* Forked from task-mcp (CF-762): Sessions, memory, archives, infrastructure.
|
||||
* Task management now handled by Jira Cloud via mcp-atlassian.
|
||||
*
|
||||
* Uses PostgreSQL with pgvector for semantic search on sessions/memories.
|
||||
*/
|
||||
|
||||
// Load environment variables from .env file
|
||||
import dotenv from 'dotenv';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
import dotenv from "dotenv";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const envPath = join(__dirname, '..', '.env');
|
||||
const result = dotenv.config({ path: envPath, override: true });
|
||||
|
||||
// Initialize Sentry for error tracking
|
||||
import { initSentry, withSentryTransaction } from './sentry.js';
|
||||
initSentry(process.env.SENTRY_ENVIRONMENT || 'production');
|
||||
|
||||
if (result.error) {
|
||||
console.error('Failed to load .env from:', envPath, result.error);
|
||||
} else {
|
||||
console.error('Loaded .env from:', envPath);
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
// Kept tools (sessions, archives, infrastructure, docs, delegations, commits)
|
||||
import { taskDelegations, taskDelegationQuery } from './tools/delegations.js';
|
||||
import { projectLock, projectUnlock, projectLockStatus, projectContext } from './tools/locks.js';
|
||||
import { taskCommitAdd, taskCommitRemove, taskCommitsList, taskLinkCommits, sessionTasks } from './tools/commits.js';
|
||||
import { changelogAdd, changelogSinceSession, changelogList } from './tools/changelog.js';
|
||||
import { timeline } from './tools/timeline.js';
|
||||
import {
|
||||
componentRegister,
|
||||
componentList,
|
||||
componentAddDependency,
|
||||
componentAddFile,
|
||||
componentAddCheck,
|
||||
impactAnalysis,
|
||||
impactLearn,
|
||||
componentGraph,
|
||||
} from './tools/impact.js';
|
||||
import { toolDocAdd, toolDocSearch, toolDocGet, toolDocList, toolDocExport } from './tools/tool-docs.js';
|
||||
import {
|
||||
sessionStart,
|
||||
sessionUpdate,
|
||||
sessionEnd,
|
||||
sessionList,
|
||||
sessionSearch,
|
||||
sessionContext,
|
||||
buildRecord,
|
||||
sessionCommitLink,
|
||||
sessionRecoverOrphaned,
|
||||
sessionRecoverTempNotes,
|
||||
} 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';
|
||||
import { transcriptSearch } from './tools/transcripts.js';
|
||||
import { projectArchive } from './tools/project-archive.js';
|
||||
|
||||
// Create MCP server
|
||||
const server = new Server(
|
||||
{ name: 'session-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;
|
||||
|
||||
return withSentryTransaction(name, async () => {
|
||||
try {
|
||||
let result: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const a = args as any;
|
||||
|
||||
switch (name) {
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// Event Timeline (CF-2885)
|
||||
case 'timeline':
|
||||
result = await timeline(a as any);
|
||||
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;
|
||||
|
||||
// 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,
|
||||
jira_issue_key: a.jira_issue_key,
|
||||
});
|
||||
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,
|
||||
search_mode: a.search_mode,
|
||||
});
|
||||
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;
|
||||
case 'session_recover_orphaned':
|
||||
result = await sessionRecoverOrphaned({
|
||||
project: a.project,
|
||||
});
|
||||
break;
|
||||
case 'session_recover_temp_notes':
|
||||
result = await sessionRecoverTempNotes({
|
||||
session_id: a.session_id,
|
||||
temp_file_path: a.temp_file_path,
|
||||
});
|
||||
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,
|
||||
search_mode: a.search_mode,
|
||||
filter_topics: a.filter_topics,
|
||||
filter_projects: a.filter_projects,
|
||||
filter_issue_keys: a.filter_issue_keys,
|
||||
}),
|
||||
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;
|
||||
|
||||
// Transcripts (CF-2394)
|
||||
case 'session_transcript_search':
|
||||
result = await transcriptSearch({
|
||||
query: a.query,
|
||||
project: a.project,
|
||||
session_issue_key: a.session_issue_key,
|
||||
limit: a.limit,
|
||||
search_mode: a.search_mode,
|
||||
});
|
||||
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,
|
||||
search_mode: a.search_mode,
|
||||
});
|
||||
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;
|
||||
|
||||
// Project archival
|
||||
case 'project_archive':
|
||||
result = await projectArchive({
|
||||
project_key: a.project_key,
|
||||
project_path: a.project_path,
|
||||
delete_local: a.delete_local,
|
||||
session_id: a.session_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
|
||||
dotenv.config({ path: join(__dirname, "..", ".env"), override: true });
|
||||
import { initSentry } from "./sentry.js";
|
||||
initSentry(process.env.SENTRY_ENVIRONMENT || "production");
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { testConnection, close } from "./db.js";
|
||||
import { createServer } from "./server.js";
|
||||
async function main() {
|
||||
process.on('SIGINT', async () => {
|
||||
await close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('session-mcp: Server started');
|
||||
|
||||
testConnection().then((connected) => {
|
||||
if (connected) {
|
||||
console.error('session-mcp: Connected to database');
|
||||
} else {
|
||||
console.error('session-mcp: Warning - database not reachable, will retry on tool calls');
|
||||
if (!(await testConnection())) { console.error("DB connect failed"); process.exit(1); }
|
||||
const server = createServer();
|
||||
const t = new StdioServerTransport();
|
||||
await server.connect(t);
|
||||
console.error("session-mcp: stdio started");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('session-mcp: Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
process.on("SIGINT", async () => { await close(); process.exit(0); });
|
||||
process.on("SIGTERM", async () => { await close(); process.exit(0); });
|
||||
|
||||
521
src/server.ts
Normal file
521
src/server.ts
Normal file
@@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Session MCP Server
|
||||
*
|
||||
* Forked from task-mcp (CF-762): Sessions, memory, archives, infrastructure.
|
||||
* Task management now handled by Jira Cloud via mcp-atlassian.
|
||||
*
|
||||
* Uses PostgreSQL with pgvector for semantic search on sessions/memories.
|
||||
*/
|
||||
|
||||
import { withSentryTransaction } from "./sentry.js";
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
import { testConnection, close } from './db.js';
|
||||
import { toolDefinitions } from './tools/index.js';
|
||||
|
||||
// Kept tools (sessions, archives, infrastructure, docs, delegations, commits)
|
||||
import { taskDelegations, taskDelegationQuery } from './tools/delegations.js';
|
||||
import { projectLock, projectUnlock, projectLockStatus, projectContext } from './tools/locks.js';
|
||||
import { taskCommitAdd, taskCommitRemove, taskCommitsList, taskLinkCommits, sessionTasks } from './tools/commits.js';
|
||||
import { changelogAdd, changelogSinceSession, changelogList } from './tools/changelog.js';
|
||||
import { timeline } from './tools/timeline.js';
|
||||
import {
|
||||
componentRegister,
|
||||
componentList,
|
||||
componentAddDependency,
|
||||
componentAddFile,
|
||||
componentAddCheck,
|
||||
impactAnalysis,
|
||||
impactLearn,
|
||||
componentGraph,
|
||||
} from './tools/impact.js';
|
||||
import { toolDocAdd, toolDocSearch, toolDocGet, toolDocList, toolDocExport } from './tools/tool-docs.js';
|
||||
import {
|
||||
sessionStart,
|
||||
sessionUpdate,
|
||||
sessionEnd,
|
||||
sessionList,
|
||||
sessionSearch,
|
||||
sessionContext,
|
||||
buildRecord,
|
||||
sessionCommitLink,
|
||||
sessionRecoverOrphaned,
|
||||
sessionRecoverTempNotes,
|
||||
} 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';
|
||||
import { transcriptSearch } from './tools/transcripts.js';
|
||||
import { projectArchive } from './tools/project-archive.js';
|
||||
|
||||
// Create MCP server
|
||||
|
||||
export function createServer(): Server {
|
||||
const server = new Server(
|
||||
{ name: 'session-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;
|
||||
|
||||
return withSentryTransaction(name, async () => {
|
||||
try {
|
||||
let result: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const a = args as any;
|
||||
|
||||
switch (name) {
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// Event Timeline (CF-2885)
|
||||
case 'timeline':
|
||||
result = await timeline(a as any);
|
||||
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;
|
||||
|
||||
// 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,
|
||||
jira_issue_key: a.jira_issue_key,
|
||||
});
|
||||
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,
|
||||
search_mode: a.search_mode,
|
||||
});
|
||||
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;
|
||||
case 'session_recover_orphaned':
|
||||
result = await sessionRecoverOrphaned({
|
||||
project: a.project,
|
||||
});
|
||||
break;
|
||||
case 'session_recover_temp_notes':
|
||||
result = await sessionRecoverTempNotes({
|
||||
session_id: a.session_id,
|
||||
temp_file_path: a.temp_file_path,
|
||||
});
|
||||
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,
|
||||
search_mode: a.search_mode,
|
||||
filter_topics: a.filter_topics,
|
||||
filter_projects: a.filter_projects,
|
||||
filter_issue_keys: a.filter_issue_keys,
|
||||
}),
|
||||
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;
|
||||
|
||||
// Transcripts (CF-2394)
|
||||
case 'session_transcript_search':
|
||||
result = await transcriptSearch({
|
||||
query: a.query,
|
||||
project: a.project,
|
||||
session_issue_key: a.session_issue_key,
|
||||
limit: a.limit,
|
||||
search_mode: a.search_mode,
|
||||
});
|
||||
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,
|
||||
search_mode: a.search_mode,
|
||||
});
|
||||
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;
|
||||
|
||||
// Project archival
|
||||
case 'project_archive':
|
||||
result = await projectArchive({
|
||||
project_key: a.project_key,
|
||||
project_path: a.project_path,
|
||||
delete_local: a.delete_local,
|
||||
session_id: a.session_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,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
Reference in New Issue
Block a user