UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

555 lines (548 loc) 15.7 kB
#!/usr/bin/env node import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); 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 Database from "better-sqlite3"; import { join } from "path"; import { existsSync, mkdirSync } from "fs"; import { LinearTaskManager } from "../features/tasks/linear-task-manager.js"; import { FrameManager } from "../core/context/index.js"; import { AgentTaskManager } from "../agents/core/agent-task-manager.js"; import { logger } from "../core/monitoring/logger.js"; const PROJECT_ROOT = process.env["STACKMEMORY_PROJECT"] || process.cwd(); const stackmemoryDir = join(PROJECT_ROOT, ".stackmemory"); if (!existsSync(stackmemoryDir)) { mkdirSync(stackmemoryDir, { recursive: true }); } const db = new Database(join(stackmemoryDir, "cache.db")); const taskStore = new LinearTaskManager(PROJECT_ROOT, db); const frameManager = new FrameManager(db, PROJECT_ROOT, void 0); const agentTaskManager = new AgentTaskManager(taskStore, frameManager); let _claudeSessionId = null; let claudeFrameId = null; const TOOLS = [ { name: "create_task", description: "Create a new task in StackMemory with automatic agent assistance", inputSchema: { type: "object", properties: { title: { type: "string", description: "Task title" }, description: { type: "string", description: "Detailed task description" }, priority: { type: "string", enum: ["low", "medium", "high", "urgent"], description: "Task priority" }, tags: { type: "array", items: { type: "string" }, description: "Tags for categorization" }, autoExecute: { type: "boolean", description: "Automatically start agent execution" } }, required: ["title"] } }, { name: "execute_task", description: "Execute a task using AI agent with verification loops", inputSchema: { type: "object", properties: { taskId: { type: "string", description: "Task ID to execute" }, maxTurns: { type: "number", description: "Maximum turns (default 10)", minimum: 1, maximum: 20 } }, required: ["taskId"] } }, { name: "task_status", description: "Get status of a task or all active tasks", inputSchema: { type: "object", properties: { taskId: { type: "string", description: "Optional specific task ID" } } } }, { name: "save_context", description: "Save important context from current Claude conversation", inputSchema: { type: "object", properties: { content: { type: "string", description: "Context to save" }, type: { type: "string", enum: ["decision", "constraint", "learning", "code", "error"], description: "Type of context" }, importance: { type: "number", minimum: 0, maximum: 1, description: "Importance score (0-1)" } }, required: ["content", "type"] } }, { name: "load_context", description: "Load relevant context from StackMemory", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query for context" }, limit: { type: "number", description: "Maximum results", minimum: 1, maximum: 20 }, frameId: { type: "string", description: "Optional specific frame ID" } }, required: ["query"] } }, { name: "agent_turn", description: "Execute a single turn in an active agent session", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Active session ID" }, action: { type: "string", description: "Action to perform" }, context: { type: "object", description: "Additional context for the action" } }, required: ["sessionId", "action"] } }, { name: "session_feedback", description: "Get feedback from the last agent turn", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session ID" } }, required: ["sessionId"] } }, { name: "breakdown_task", description: "Break down a complex task into subtasks", inputSchema: { type: "object", properties: { taskId: { type: "string", description: "Task ID to break down" } }, required: ["taskId"] } }, { name: "list_active_sessions", description: "List all active agent sessions", inputSchema: { type: "object", properties: {} } }, { name: "retry_session", description: "Retry a failed session with learned context", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session ID to retry" } }, required: ["sessionId"] } } ]; const server = new Server( { name: "stackmemory", version: "1.0.0" }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (!args) { return { content: [ { type: "text", text: "Error: No arguments provided" } ] }; } try { switch (name) { case "create_task": { const taskArgs = args; if (!claudeFrameId) { claudeFrameId = frameManager.createFrame({ type: "task", name: "Claude AI Session", inputs: { source: "mcp", timestamp: (/* @__PURE__ */ new Date()).toISOString() } }); } const taskId = taskStore.createTask({ title: taskArgs.title, description: taskArgs.description, priority: taskArgs.priority || "medium", frameId: claudeFrameId, tags: taskArgs.tags || ["claude-generated"] }); if (taskArgs.autoExecute) { const session = await agentTaskManager.startTaskSession( taskId, claudeFrameId ); _claudeSessionId = session.id; return { content: [ { type: "text", text: `Task created: ${taskId} Agent session started: ${session.id} Ready for execution with ${session.maxTurns} turns available.` } ] }; } return { content: [ { type: "text", text: `Task created successfully: ${taskId}` } ] }; } case "execute_task": { const execArgs = args; if (!claudeFrameId) { claudeFrameId = frameManager.createFrame({ type: "task", name: "Claude Task Execution", inputs: { taskId: execArgs.taskId } }); } const session = await agentTaskManager.startTaskSession( execArgs.taskId, claudeFrameId ); if (execArgs.maxTurns) { session.maxTurns = execArgs.maxTurns; } _claudeSessionId = session.id; return { content: [ { type: "text", text: `Started agent session: ${session.id} Task: ${execArgs.taskId} Max turns: ${session.maxTurns} Use 'agent_turn' to execute actions.` } ] }; } case "agent_turn": { const turnArgs = args; const result = await agentTaskManager.executeTurn( turnArgs.sessionId, turnArgs.action, turnArgs.context || {} ); const verificationSummary = result.verificationResults.map((v) => `${v.passed ? "\u2713" : "\u2717"} ${v.verifierId}: ${v.message}`).join("\n"); return { content: [ { type: "text", text: `Turn executed: Success: ${result.success} Should Continue: ${result.shouldContinue} Feedback: ${result.feedback} Verifications: ${verificationSummary}` } ] }; } case "task_status": { const statusArgs = args; if (statusArgs.taskId) { const task = taskStore.getTask(statusArgs.taskId); if (!task) { return { content: [ { type: "text", text: `Task ${statusArgs.taskId} not found` } ] }; } return { content: [ { type: "text", text: `Task: ${task.title} Status: ${task.status} Priority: ${task.priority} Created: ${new Date(task.created_at * 1e3).toLocaleString()} Description: ${task.description || "N/A"}` } ] }; } const activeTasks = taskStore.getActiveTasks(); const taskList = activeTasks.map((t) => `- ${t.id}: ${t.title} (${t.status}, ${t.priority})`).join("\n"); return { content: [ { type: "text", text: `Active tasks (${activeTasks.length}): ${taskList || "No active tasks"}` } ] }; } case "save_context": { const saveArgs = args; if (!claudeFrameId) { claudeFrameId = frameManager.createFrame({ type: "task", name: "Claude Context", inputs: { source: "mcp" } }); } const eventId = frameManager.addEvent( "observation", { type: saveArgs.type, content: saveArgs.content, importance: saveArgs.importance || 0.5, source: "claude-mcp", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, claudeFrameId ); return { content: [ { type: "text", text: `Context saved to frame ${claudeFrameId} as event ${eventId}` } ] }; } case "load_context": { const loadArgs = args; const frames = frameManager.getActiveFramePath(); const limit = loadArgs.limit || 10; const events = loadArgs.frameId ? frameManager.getFrameEvents(loadArgs.frameId, limit) : []; const contextText = frames.map( (frame) => `[Frame ${frame.type}] ${frame.name}: ${frame.digest_text || "No digest"}` ).concat( events.map( (event) => `[Event ${event.event_type}] ${new Date(event.ts).toLocaleString()}: ${JSON.stringify( event.payload ).substring(0, 100)}...` ) ).join("\n\n"); return { content: [ { type: "text", text: `Query: ${loadArgs.query} Found ${frames.length} frames and ${events.length} events: ${contextText || "No matching context found"}` } ] }; } case "breakdown_task": { const breakdownArgs = args; const task = taskStore.getTask(breakdownArgs.taskId); if (!task) { return { content: [ { type: "text", text: `Task ${breakdownArgs.taskId} not found` } ] }; } const subtasks = [ `1. Analyze: ${task.title} - Understand requirements (2 turns)`, `2. Design: ${task.title} - Create implementation plan (2 turns)`, `3. Implement: ${task.title} - Build core functionality (5 turns)`, `4. Test: ${task.title} - Validate and verify (3 turns)`, `5. Polish: ${task.title} - Documentation and cleanup (1 turn)` ].join("\n"); return { content: [ { type: "text", text: `Task breakdown for: ${task.title} ${subtasks} Total estimated turns: 13` } ] }; } case "list_active_sessions": { const sessions = agentTaskManager.getActiveSessions(); const sessionList = sessions.map( (s) => `- ${s.sessionId}: Task ${s.taskId} (Turn ${s.turnCount}, ${s.status})` ).join("\n"); return { content: [ { type: "text", text: `Active sessions (${sessions.length}): ${sessionList || "No active sessions"}` } ] }; } case "retry_session": { const retryArgs = args; const newSession = await agentTaskManager.retrySession( retryArgs.sessionId ); if (!newSession) { return { content: [ { type: "text", text: "Cannot retry session (max retries reached or session is still active)" } ] }; } _claudeSessionId = newSession.id; return { content: [ { type: "text", text: `Retry session started: ${newSession.id} Task: ${newSession.taskId} Incorporating learned context from previous attempts.` } ] }; } case "session_feedback": { const feedbackArgs = args; const sessions = agentTaskManager.getActiveSessions(); const session = sessions.find( (s) => s.sessionId === feedbackArgs.sessionId ); if (!session) { return { content: [ { type: "text", text: `Session ${feedbackArgs.sessionId} not found or not active` } ] }; } return { content: [ { type: "text", text: `Session ${feedbackArgs.sessionId}: Turn: ${session.turnCount} Status: ${session.status} Ready for next action.` } ] }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { logger.error( "MCP tool execution failed", error instanceof Error ? error : void 0 ); return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` } ] }; } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); logger.info("StackMemory MCP Server started", { projectRoot: PROJECT_ROOT, tools: TOOLS.map((t) => t.name) }); } process.on("SIGINT", async () => { logger.info("Shutting down StackMemory MCP Server"); if (claudeFrameId) { try { frameManager.closeFrame(claudeFrameId, { summary: "Claude session ended", timestamp: (/* @__PURE__ */ new Date()).toISOString() }); } catch (error) { logger.error( "Error closing frame", error instanceof Error ? error : void 0 ); } } db.close(); process.exit(0); }); main().catch((error) => { logger.error( "Failed to start MCP server", error instanceof Error ? error : void 0 ); process.exit(1); }); //# sourceMappingURL=stackmemory-mcp-server.js.map