UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

395 lines (394 loc) 11.9 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 { z } from "zod"; import Database from "better-sqlite3"; import { existsSync, mkdirSync } from "fs"; import { join, dirname } from "path"; import { execSync } from "child_process"; import { v4 as uuidv4 } from "uuid"; import { FrameManager } from "../../core/context/frame-manager.js"; import { LinearTaskManager } from "../../features/tasks/linear-task-manager.js"; import { LinearAuthManager } from "../linear/auth.js"; import { LinearSyncEngine, DEFAULT_SYNC_CONFIG } from "../linear/sync.js"; import { BrowserMCPIntegration } from "../../features/browser/browser-mcp.js"; import { TraceDetector } from "../../core/trace/trace-detector.js"; import { LLMContextRetrieval } from "../../core/retrieval/index.js"; import { SQLiteAdapter } from "../../core/database/sqlite-adapter.js"; import { ConfigManager } from "../../core/config/config-manager.js"; import { logger } from "../../core/monitoring/logger.js"; import { MCPHandlerFactory } from "./handlers/index.js"; import { MCPToolDefinitions } from "./tool-definitions.js"; import { ToolScoringMiddleware } from "./middleware/tool-scoring.js"; function _getEnv(key, defaultValue) { const value = process.env[key]; if (value === void 0) { if (defaultValue !== void 0) return defaultValue; throw new Error(`Environment variable ${key} is required`); } return value; } function _getOptionalEnv(key) { return process.env[key]; } class StackMemoryMCP { server; db; projectRoot; projectId; // Core components frameManager; taskStore; linearAuthManager; linearSync; browserMCP; traceDetector; contextRetrieval; dbAdapter; configManager; toolScoringMiddleware; // Handler factory handlerFactory; toolDefinitions; constructor(config = {}) { this.projectRoot = this.findProjectRoot(); this.projectId = this.getProjectId(); this.initializeDatabase(); this.initializeComponents(config); this.initializeServer(); this.setupHandlers(); } /** * Initialize database connection */ initializeDatabase() { const dbDir = join(this.projectRoot, ".stackmemory"); if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }); } const dbPath = join(dbDir, "context.db"); this.db = new Database(dbPath); this.dbAdapter = new SQLiteAdapter(this.projectId, { dbPath }); logger.info("Database initialized", { dbPath }); } /** * Initialize core components */ initializeComponents(config) { const configPath = join(this.projectRoot, ".stackmemory", "config.yaml"); this.configManager = new ConfigManager(configPath); this.frameManager = new FrameManager(this.db, this.projectId); this.taskStore = new LinearTaskManager(this.projectRoot, this.db); this.linearAuthManager = new LinearAuthManager(this.projectRoot); this.linearSync = new LinearSyncEngine( this.taskStore, this.linearAuthManager, DEFAULT_SYNC_CONFIG ); if (config.enableBrowser !== false) { this.browserMCP = new BrowserMCPIntegration({ headless: config.headless ?? process.env["BROWSER_HEADLESS"] !== "false", defaultViewport: { width: config.viewportWidth ?? 1280, height: config.viewportHeight ?? 720 } }); } if (config.enableTracing !== false) { this.traceDetector = new TraceDetector({}, this.configManager, this.db); } this.toolScoringMiddleware = new ToolScoringMiddleware( this.configManager, this.traceDetector, this.db ); this.contextRetrieval = new LLMContextRetrieval( this.db, this.frameManager, this.projectId, {} ); logger.info("Core components initialized"); } /** * Initialize MCP server */ initializeServer() { this.server = new Server( { name: "stackmemory-refactored", version: "0.2.0" }, { capabilities: { tools: {} } } ); logger.info("MCP server initialized"); } /** * Setup MCP handlers */ setupHandlers() { const dependencies = { frameManager: this.frameManager, contextRetrieval: this.contextRetrieval, taskStore: this.taskStore, projectId: this.projectId, linearAuthManager: this.linearAuthManager, linearSync: this.linearSync, traceDetector: this.traceDetector, browserMCP: this.browserMCP, dbAdapter: this.dbAdapter }; this.handlerFactory = new MCPHandlerFactory(dependencies); this.toolDefinitions = new MCPToolDefinitions(); this.setupToolListHandler(); this.setupToolExecutionHandler(); logger.info("MCP handlers configured"); } /** * Setup tool listing handler */ setupToolListHandler() { this.server.setRequestHandler( z.object({ method: z.literal("tools/list") }), async () => { const tools = this.toolDefinitions.getAllToolDefinitions(); logger.debug("Listed tools", { count: tools.length }); return { tools }; } ); } /** * Setup tool execution handler */ setupToolExecutionHandler() { this.server.setRequestHandler( z.object({ method: z.literal("tools/call"), params: z.object({ name: z.string(), arguments: z.record(z.unknown()) }) }), async (request) => { const { name, arguments: args } = request.params; const callId = uuidv4(); const startTime = Date.now(); logger.info("Tool call started", { toolName: name, callId }); try { const currentFrameId = this.frameManager.getCurrentFrameId(); if (currentFrameId) { this.frameManager.addEvent("tool_call", { tool_name: name, arguments: args, timestamp: startTime, call_id: callId }); } if (!this.handlerFactory.hasHandler(name)) { throw new Error(`Unknown tool: ${name}`); } const handler = this.handlerFactory.getHandler(name); const result = await handler(args); const duration = Date.now() - startTime; const score = await this.toolScoringMiddleware.scoreToolCall( name, args, result, void 0 // no error ); if (currentFrameId) { this.frameManager.addEvent("tool_result", { tool_name: name, call_id: callId, duration, success: true, result_size: JSON.stringify(result).length, importance_score: score, profile: this.configManager.getConfig().profile || "default" }); } if (this.traceDetector) { this.traceDetector.addToolCall({ id: callId, tool: name, arguments: args, timestamp: startTime, result, duration }); } logger.info("Tool call completed", { toolName: name, callId, duration }); return result; } catch (error) { const duration = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : String(error); const score = await this.toolScoringMiddleware.scoreToolCall( name, args, void 0, errorMessage ); const currentFrameId = this.frameManager.getCurrentFrameId(); if (currentFrameId) { this.frameManager.addEvent("tool_result", { tool_name: name, call_id: callId, duration, success: false, error: errorMessage, importance_score: score, profile: this.configManager.getConfig().profile || "default" }); } logger.error("Tool call failed", { toolName: name, callId, duration, error: errorMessage }); return { content: [ { type: "text", text: `Error executing ${name}: ${errorMessage}` } ], isError: true }; } } ); } /** * Start the MCP server */ async start() { try { await this.dbAdapter.connect(); await this.dbAdapter.initializeSchema(); await this.frameManager.initialize(); const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info("StackMemory MCP Server started", { projectRoot: this.projectRoot, projectId: this.projectId, availableTools: this.handlerFactory.getAvailableTools().length }); this.setupCleanup(); } catch (error) { logger.error( "Failed to start MCP server", error instanceof Error ? error : new Error(String(error)) ); throw error; } } /** * Setup cleanup handlers */ setupCleanup() { const cleanup = async () => { logger.info("Shutting down MCP server..."); try { if (this.browserMCP) { await this.browserMCP.cleanup(); } if (this.dbAdapter) { await this.dbAdapter.disconnect(); } if (this.db) { this.db.close(); } logger.info("MCP server shutdown complete"); } catch (error) { logger.error( "Error during cleanup", error instanceof Error ? error : new Error(String(error)) ); } process.exit(0); }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); process.on("uncaughtException", (error) => { logger.error( "Uncaught exception", error instanceof Error ? error : new Error(String(error)) ); cleanup(); }); } /** * Find project root directory */ findProjectRoot() { let currentDir = process.cwd(); const rootDir = "/"; while (currentDir !== rootDir) { if (existsSync(join(currentDir, ".git"))) { return currentDir; } currentDir = dirname(currentDir); } return process.cwd(); } /** * Get project ID from git remote or directory name */ getProjectId() { try { const remoteUrl = execSync("git remote get-url origin", { cwd: this.projectRoot, encoding: "utf8" }).trim(); const match = remoteUrl.match(/([^/]+\/[^/]+)(?:\.git)?$/); if (match) { return match[1]; } } catch (error) { logger.debug("Could not get git remote URL", error); } return this.projectRoot.split("/").pop() || "unknown"; } } async function main() { try { const config = { headless: process.env["BROWSER_HEADLESS"] !== "false", enableTracing: process.env["DISABLE_TRACING"] !== "true", enableBrowser: process.env["DISABLE_BROWSER"] !== "true" }; const server = new StackMemoryMCP(config); await server.start(); } catch (error) { logger.error( "Failed to start server", error instanceof Error ? error : new Error(String(error)) ); process.exit(1); } } if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { console.error("Fatal error:", error); process.exit(1); }); } export { StackMemoryMCP };