@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.
375 lines (374 loc) • 11.5 kB
JavaScript
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 { RefactoredFrameManager } from "../../core/context/refactored-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 { 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 RefactoredStackMemoryMCP {
server;
db;
projectRoot;
projectId;
// Core components
frameManager;
taskStore;
linearAuthManager;
linearSync;
browserMCP;
traceDetector;
contextRetrieval;
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);
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 RefactoredFrameManager(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
};
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.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.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 RefactoredStackMemoryMCP(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 {
RefactoredStackMemoryMCP
};
//# sourceMappingURL=refactored-server.js.map