@stackmemoryai/stackmemory 0.5.56 → 0.5.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,691 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath as __fileURLToPath } from 'url';
3
+ import { dirname as __pathDirname } from 'path';
4
+ const __filename = __fileURLToPath(import.meta.url);
5
+ const __dirname = __pathDirname(__filename);
6
+ import express from "express";
7
+ import cors from "cors";
8
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
10
+ import { z } from "zod";
11
+ import Database from "better-sqlite3";
12
+ import { validateInput, StartFrameSchema, AddAnchorSchema } from "./schemas.js";
13
+ import { readFileSync, existsSync, mkdirSync } from "fs";
14
+ import { join, dirname } from "path";
15
+ import { execSync } from "child_process";
16
+ import { FrameManager } from "../../core/context/index.js";
17
+ import { logger } from "../../core/monitoring/logger.js";
18
+ import { isFeatureEnabled } from "../../core/config/feature-flags.js";
19
+ const DEFAULT_PORT = 3847;
20
+ class RemoteStackMemoryMCP {
21
+ server;
22
+ db;
23
+ projectRoot;
24
+ frameManager;
25
+ taskStore = null;
26
+ linearAuthManager = null;
27
+ linearSync = null;
28
+ projectId;
29
+ contexts = /* @__PURE__ */ new Map();
30
+ transports = /* @__PURE__ */ new Map();
31
+ constructor(projectRoot) {
32
+ this.projectRoot = projectRoot || this.findProjectRoot();
33
+ this.projectId = this.getProjectId();
34
+ const dbDir = join(this.projectRoot, ".stackmemory");
35
+ if (!existsSync(dbDir)) {
36
+ mkdirSync(dbDir, { recursive: true });
37
+ }
38
+ const dbPath = join(dbDir, "context.db");
39
+ this.db = new Database(dbPath);
40
+ this.initDB();
41
+ this.frameManager = new FrameManager(this.db, this.projectId);
42
+ this.initLinearIfEnabled();
43
+ this.server = new Server(
44
+ {
45
+ name: "stackmemory-remote",
46
+ version: "0.1.0"
47
+ },
48
+ {
49
+ capabilities: {
50
+ tools: {}
51
+ }
52
+ }
53
+ );
54
+ this.setupHandlers();
55
+ this.loadInitialContext();
56
+ logger.info("StackMemory Remote MCP Server initialized", {
57
+ projectRoot: this.projectRoot,
58
+ projectId: this.projectId
59
+ });
60
+ }
61
+ findProjectRoot() {
62
+ let dir = process.cwd();
63
+ while (dir !== "/") {
64
+ if (existsSync(join(dir, ".git"))) {
65
+ return dir;
66
+ }
67
+ dir = dirname(dir);
68
+ }
69
+ return process.cwd();
70
+ }
71
+ async initLinearIfEnabled() {
72
+ if (!isFeatureEnabled("linear")) {
73
+ return;
74
+ }
75
+ try {
76
+ const { LinearTaskManager } = await import("../../features/tasks/linear-task-manager.js");
77
+ const { LinearAuthManager } = await import("../linear/auth.js");
78
+ const { LinearSyncEngine, DEFAULT_SYNC_CONFIG } = await import("../linear/sync.js");
79
+ this.taskStore = new LinearTaskManager(this.projectRoot, this.db);
80
+ this.linearAuthManager = new LinearAuthManager(this.projectRoot);
81
+ this.linearSync = new LinearSyncEngine(
82
+ this.taskStore,
83
+ this.linearAuthManager,
84
+ DEFAULT_SYNC_CONFIG
85
+ );
86
+ } catch (error) {
87
+ logger.warn("Failed to initialize Linear integration", { error });
88
+ }
89
+ }
90
+ initDB() {
91
+ this.db.exec(`
92
+ CREATE TABLE IF NOT EXISTS contexts (
93
+ id TEXT PRIMARY KEY,
94
+ type TEXT NOT NULL,
95
+ content TEXT NOT NULL,
96
+ importance REAL DEFAULT 0.5,
97
+ created_at INTEGER DEFAULT (unixepoch()),
98
+ last_accessed INTEGER DEFAULT (unixepoch()),
99
+ access_count INTEGER DEFAULT 1
100
+ );
101
+
102
+ CREATE TABLE IF NOT EXISTS attention_log (
103
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ context_id TEXT,
105
+ query TEXT,
106
+ response TEXT,
107
+ influence_score REAL,
108
+ timestamp INTEGER DEFAULT (unixepoch())
109
+ );
110
+ `);
111
+ }
112
+ loadInitialContext() {
113
+ const projectInfo = this.getProjectInfo();
114
+ this.addContext(
115
+ "project",
116
+ `Project: ${projectInfo.name}
117
+ Path: ${projectInfo.path}`,
118
+ 0.9
119
+ );
120
+ try {
121
+ const recentCommits = execSync("git log --oneline -10", {
122
+ cwd: this.projectRoot
123
+ }).toString();
124
+ this.addContext("git_history", `Recent commits:
125
+ ${recentCommits}`, 0.6);
126
+ } catch {
127
+ }
128
+ const readmePath = join(this.projectRoot, "README.md");
129
+ if (existsSync(readmePath)) {
130
+ const readme = readFileSync(readmePath, "utf-8");
131
+ const summary = readme.substring(0, 500);
132
+ this.addContext("readme", `Project README:
133
+ ${summary}...`, 0.8);
134
+ }
135
+ this.loadStoredContexts();
136
+ }
137
+ getProjectId() {
138
+ let identifier;
139
+ try {
140
+ identifier = execSync("git config --get remote.origin.url", {
141
+ cwd: this.projectRoot,
142
+ stdio: "pipe",
143
+ timeout: 5e3
144
+ }).toString().trim();
145
+ } catch {
146
+ identifier = this.projectRoot;
147
+ }
148
+ const cleaned = identifier.replace(/\.git$/, "").replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
149
+ return cleaned.substring(cleaned.length - 50) || "unknown";
150
+ }
151
+ getProjectInfo() {
152
+ const packageJsonPath = join(this.projectRoot, "package.json");
153
+ if (existsSync(packageJsonPath)) {
154
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
155
+ return {
156
+ name: pkg.name || "unknown",
157
+ path: this.projectRoot
158
+ };
159
+ }
160
+ return {
161
+ name: this.projectRoot.split("/").pop() || "unknown",
162
+ path: this.projectRoot
163
+ };
164
+ }
165
+ addContext(type, content, importance = 0.5) {
166
+ const id = `${type}_${Date.now()}`;
167
+ this.db.prepare(
168
+ `
169
+ INSERT OR REPLACE INTO contexts (id, type, content, importance)
170
+ VALUES (?, ?, ?, ?)
171
+ `
172
+ ).run(id, type, content, importance);
173
+ this.contexts.set(id, { type, content, importance });
174
+ return id;
175
+ }
176
+ loadStoredContexts() {
177
+ const stored = this.db.prepare(
178
+ `
179
+ SELECT * FROM contexts
180
+ ORDER BY importance DESC, last_accessed DESC
181
+ LIMIT 50
182
+ `
183
+ ).all();
184
+ stored.forEach((ctx) => {
185
+ this.contexts.set(ctx.id, ctx);
186
+ });
187
+ }
188
+ setupHandlers() {
189
+ this.server.setRequestHandler(
190
+ z.object({
191
+ method: z.literal("tools/list")
192
+ }),
193
+ async () => {
194
+ return {
195
+ tools: [
196
+ {
197
+ name: "get_context",
198
+ description: "Get current project context",
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: {
202
+ query: {
203
+ type: "string",
204
+ description: "What you want to know"
205
+ },
206
+ limit: {
207
+ type: "number",
208
+ description: "Max contexts to return"
209
+ }
210
+ }
211
+ }
212
+ },
213
+ {
214
+ name: "add_decision",
215
+ description: "Record a decision or important information",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {
219
+ content: {
220
+ type: "string",
221
+ description: "The decision or information"
222
+ },
223
+ type: {
224
+ type: "string",
225
+ enum: ["decision", "constraint", "learning"]
226
+ }
227
+ },
228
+ required: ["content", "type"]
229
+ }
230
+ },
231
+ {
232
+ name: "start_frame",
233
+ description: "Start a new frame (task/subtask) on the call stack",
234
+ inputSchema: {
235
+ type: "object",
236
+ properties: {
237
+ name: { type: "string", description: "Frame name/goal" },
238
+ type: {
239
+ type: "string",
240
+ enum: [
241
+ "task",
242
+ "subtask",
243
+ "tool_scope",
244
+ "review",
245
+ "write",
246
+ "debug"
247
+ ],
248
+ description: "Frame type"
249
+ },
250
+ constraints: {
251
+ type: "array",
252
+ items: { type: "string" },
253
+ description: "Constraints for this frame"
254
+ }
255
+ },
256
+ required: ["name", "type"]
257
+ }
258
+ },
259
+ {
260
+ name: "close_frame",
261
+ description: "Close current frame and generate digest",
262
+ inputSchema: {
263
+ type: "object",
264
+ properties: {
265
+ result: {
266
+ type: "string",
267
+ description: "Frame completion result"
268
+ },
269
+ outputs: {
270
+ type: "object",
271
+ description: "Final outputs from frame"
272
+ }
273
+ }
274
+ }
275
+ },
276
+ {
277
+ name: "add_anchor",
278
+ description: "Add anchored fact/decision/constraint to current frame",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ type: {
283
+ type: "string",
284
+ enum: [
285
+ "FACT",
286
+ "DECISION",
287
+ "CONSTRAINT",
288
+ "INTERFACE_CONTRACT",
289
+ "TODO",
290
+ "RISK"
291
+ ],
292
+ description: "Anchor type"
293
+ },
294
+ text: { type: "string", description: "Anchor content" },
295
+ priority: {
296
+ type: "number",
297
+ description: "Priority (0-10)",
298
+ minimum: 0,
299
+ maximum: 10
300
+ }
301
+ },
302
+ required: ["type", "text"]
303
+ }
304
+ },
305
+ {
306
+ name: "get_hot_stack",
307
+ description: "Get current active frames and context",
308
+ inputSchema: {
309
+ type: "object",
310
+ properties: {
311
+ maxEvents: {
312
+ type: "number",
313
+ description: "Max recent events per frame",
314
+ default: 20
315
+ }
316
+ }
317
+ }
318
+ },
319
+ {
320
+ name: "sm_search",
321
+ description: "Search across StackMemory - frames, events, decisions, tasks",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {
325
+ query: { type: "string", description: "Search query" },
326
+ scope: {
327
+ type: "string",
328
+ enum: ["all", "frames", "events", "decisions", "tasks"],
329
+ description: "Scope of search"
330
+ },
331
+ limit: { type: "number", description: "Maximum results" }
332
+ },
333
+ required: ["query"]
334
+ }
335
+ }
336
+ ]
337
+ };
338
+ }
339
+ );
340
+ this.server.setRequestHandler(
341
+ z.object({
342
+ method: z.literal("tools/call"),
343
+ params: z.object({
344
+ name: z.string(),
345
+ arguments: z.record(z.unknown())
346
+ })
347
+ }),
348
+ async (request) => {
349
+ const { name, arguments: args } = request.params;
350
+ try {
351
+ switch (name) {
352
+ case "get_context":
353
+ return this.handleGetContext(args);
354
+ case "add_decision":
355
+ return this.handleAddDecision(args);
356
+ case "start_frame":
357
+ return this.handleStartFrame(args);
358
+ case "close_frame":
359
+ return this.handleCloseFrame(args);
360
+ case "add_anchor":
361
+ return this.handleAddAnchor(args);
362
+ case "get_hot_stack":
363
+ return this.handleGetHotStack(args);
364
+ case "sm_search":
365
+ return this.handleSmSearch(args);
366
+ default:
367
+ throw new Error(`Unknown tool: ${name}`);
368
+ }
369
+ } catch (error) {
370
+ return {
371
+ content: [{ type: "text", text: `Error: ${error.message}` }]
372
+ };
373
+ }
374
+ }
375
+ );
376
+ }
377
+ async handleGetContext(args) {
378
+ const { query = "", limit = 10 } = args;
379
+ const contexts = Array.from(this.contexts.values()).sort((a, b) => b.importance - a.importance).slice(0, limit);
380
+ const response = contexts.map(
381
+ (ctx) => `[${ctx.type.toUpperCase()}] (importance: ${ctx.importance.toFixed(2)})
382
+ ${ctx.content}`
383
+ ).join("\n\n---\n\n");
384
+ return {
385
+ content: [
386
+ {
387
+ type: "text",
388
+ text: response || "No context available yet."
389
+ }
390
+ ]
391
+ };
392
+ }
393
+ async handleAddDecision(args) {
394
+ const { content, type = "decision" } = args;
395
+ const id = this.addContext(type, content, 0.8);
396
+ return {
397
+ content: [
398
+ {
399
+ type: "text",
400
+ text: `Added ${type}: ${content}
401
+ ID: ${id}`
402
+ }
403
+ ]
404
+ };
405
+ }
406
+ async handleStartFrame(args) {
407
+ const { name, type, constraints } = validateInput(
408
+ StartFrameSchema,
409
+ args,
410
+ "start_frame"
411
+ );
412
+ const inputs = {};
413
+ if (constraints) {
414
+ inputs.constraints = constraints;
415
+ }
416
+ const frameId = this.frameManager.createFrame({
417
+ type,
418
+ name,
419
+ inputs
420
+ });
421
+ this.addContext("active_frame", `Active frame: ${name} (${type})`, 0.9);
422
+ return {
423
+ content: [
424
+ {
425
+ type: "text",
426
+ text: `Started ${type}: ${name}
427
+ Frame ID: ${frameId}
428
+ Stack depth: ${this.frameManager.getStackDepth()}`
429
+ }
430
+ ]
431
+ };
432
+ }
433
+ async handleCloseFrame(args) {
434
+ const { result, outputs } = args;
435
+ const currentFrameId = this.frameManager.getCurrentFrameId();
436
+ if (!currentFrameId) {
437
+ return {
438
+ content: [{ type: "text", text: "No active frame to close" }]
439
+ };
440
+ }
441
+ this.frameManager.closeFrame(currentFrameId, outputs);
442
+ return {
443
+ content: [
444
+ {
445
+ type: "text",
446
+ text: `Closed frame: ${result || "completed"}
447
+ Stack depth: ${this.frameManager.getStackDepth()}`
448
+ }
449
+ ]
450
+ };
451
+ }
452
+ async handleAddAnchor(args) {
453
+ const { type, text, priority } = validateInput(
454
+ AddAnchorSchema,
455
+ args,
456
+ "add_anchor"
457
+ );
458
+ const anchorId = this.frameManager.addAnchor(type, text, priority);
459
+ return {
460
+ content: [
461
+ {
462
+ type: "text",
463
+ text: `Added ${type}: ${text}
464
+ Anchor ID: ${anchorId}`
465
+ }
466
+ ]
467
+ };
468
+ }
469
+ async handleGetHotStack(args) {
470
+ const { maxEvents = 20 } = args;
471
+ const hotStack = this.frameManager.getHotStackContext(maxEvents);
472
+ const activePath = this.frameManager.getActiveFramePath();
473
+ if (hotStack.length === 0) {
474
+ return {
475
+ content: [
476
+ {
477
+ type: "text",
478
+ text: "No active frames. Start a frame with start_frame tool."
479
+ }
480
+ ]
481
+ };
482
+ }
483
+ let response = "Active Call Stack:\n\n";
484
+ activePath.forEach((frame, index) => {
485
+ const indent = " ".repeat(index);
486
+ const context = hotStack[index];
487
+ response += `${indent}${index + 1}. ${frame.name} (${frame.type})
488
+ `;
489
+ if (context?.anchors?.length > 0) {
490
+ response += `${indent} Anchors: ${context.anchors.length}
491
+ `;
492
+ }
493
+ if (context?.recentEvents?.length > 0) {
494
+ response += `${indent} Events: ${context.recentEvents.length}
495
+ `;
496
+ }
497
+ response += "\n";
498
+ });
499
+ response += `Total stack depth: ${hotStack.length}`;
500
+ return {
501
+ content: [{ type: "text", text: response }]
502
+ };
503
+ }
504
+ async handleSmSearch(args) {
505
+ const { query, scope = "all", limit = 20 } = args;
506
+ if (!query) {
507
+ throw new Error("Query is required");
508
+ }
509
+ const results = [];
510
+ if (scope === "all" || scope === "frames") {
511
+ const frames = this.db.prepare(
512
+ `
513
+ SELECT frame_id, name, type, created_at
514
+ FROM frames
515
+ WHERE project_id = ? AND (name LIKE ? OR inputs LIKE ? OR outputs LIKE ?)
516
+ ORDER BY created_at DESC
517
+ LIMIT ?
518
+ `
519
+ ).all(
520
+ this.projectId,
521
+ `%${query}%`,
522
+ `%${query}%`,
523
+ `%${query}%`,
524
+ limit
525
+ );
526
+ frames.forEach((f) => {
527
+ results.push({
528
+ type: "frame",
529
+ id: f.frame_id,
530
+ name: f.name,
531
+ frameType: f.type
532
+ });
533
+ });
534
+ }
535
+ if (scope === "all" || scope === "decisions") {
536
+ const anchors = this.db.prepare(
537
+ `
538
+ SELECT a.anchor_id, a.type, a.text, f.name as frame_name
539
+ FROM anchors a
540
+ JOIN frames f ON a.frame_id = f.frame_id
541
+ WHERE f.project_id = ? AND a.text LIKE ?
542
+ ORDER BY a.created_at DESC
543
+ LIMIT ?
544
+ `
545
+ ).all(this.projectId, `%${query}%`, limit);
546
+ anchors.forEach((a) => {
547
+ results.push({
548
+ type: "decision",
549
+ id: a.anchor_id,
550
+ decisionType: a.type,
551
+ text: a.text,
552
+ frame: a.frame_name
553
+ });
554
+ });
555
+ }
556
+ let response = `Search Results for "${query}"
557
+
558
+ `;
559
+ response += `Found ${results.length} results
560
+
561
+ `;
562
+ results.slice(0, 10).forEach((r) => {
563
+ if (r.type === "frame") {
564
+ response += `[Frame] ${r.name} (${r.frameType})
565
+ `;
566
+ } else if (r.type === "decision") {
567
+ response += `[${r.decisionType}] ${r.text.slice(0, 60)}...
568
+ `;
569
+ }
570
+ });
571
+ return {
572
+ content: [{ type: "text", text: response }]
573
+ };
574
+ }
575
+ /**
576
+ * Start the HTTP/SSE server
577
+ */
578
+ async startHttpServer(port = DEFAULT_PORT) {
579
+ const app = express();
580
+ app.use(
581
+ cors({
582
+ origin: [
583
+ "https://claude.ai",
584
+ "https://console.anthropic.com",
585
+ /^http:\/\/localhost:\d+$/
586
+ ],
587
+ credentials: true
588
+ })
589
+ );
590
+ app.use(express.json());
591
+ app.get("/health", (req, res) => {
592
+ res.json({
593
+ status: "ok",
594
+ server: "stackmemory-remote",
595
+ projectId: this.projectId,
596
+ projectRoot: this.projectRoot
597
+ });
598
+ });
599
+ app.get("/sse", async (req, res) => {
600
+ logger.info("New SSE connection request");
601
+ const transport = new SSEServerTransport("/message", res);
602
+ const sessionId = transport.sessionId;
603
+ this.transports.set(sessionId, transport);
604
+ transport.onclose = () => {
605
+ this.transports.delete(sessionId);
606
+ logger.info("SSE connection closed", { sessionId });
607
+ };
608
+ transport.onerror = (error) => {
609
+ logger.error("SSE transport error", { sessionId, error });
610
+ this.transports.delete(sessionId);
611
+ };
612
+ await this.server.connect(transport);
613
+ await transport.start();
614
+ logger.info("SSE connection established", { sessionId });
615
+ });
616
+ app.post("/message", async (req, res) => {
617
+ const sessionId = req.query.sessionId;
618
+ if (!sessionId) {
619
+ res.status(400).json({ error: "Missing sessionId" });
620
+ return;
621
+ }
622
+ const transport = this.transports.get(sessionId);
623
+ if (!transport) {
624
+ res.status(404).json({ error: "Session not found" });
625
+ return;
626
+ }
627
+ await transport.handlePostMessage(req, res);
628
+ });
629
+ app.get("/info", (req, res) => {
630
+ res.json({
631
+ name: "stackmemory-remote",
632
+ version: "0.1.0",
633
+ protocol: "mcp",
634
+ transport: "sse",
635
+ endpoints: {
636
+ sse: "/sse",
637
+ message: "/message",
638
+ health: "/health"
639
+ },
640
+ project: {
641
+ id: this.projectId,
642
+ root: this.projectRoot,
643
+ name: this.getProjectInfo().name
644
+ }
645
+ });
646
+ });
647
+ return new Promise((resolve) => {
648
+ app.listen(port, () => {
649
+ console.log(
650
+ `StackMemory Remote MCP Server running on http://localhost:${port}`
651
+ );
652
+ console.log(`
653
+ Endpoints:`);
654
+ console.log(` SSE: http://localhost:${port}/sse`);
655
+ console.log(` Message: http://localhost:${port}/message`);
656
+ console.log(` Health: http://localhost:${port}/health`);
657
+ console.log(` Info: http://localhost:${port}/info`);
658
+ console.log(
659
+ `
660
+ Project: ${this.getProjectInfo().name} (${this.projectId})`
661
+ );
662
+ console.log(
663
+ `
664
+ For Claude.ai connector, use: http://localhost:${port}/sse`
665
+ );
666
+ resolve();
667
+ });
668
+ });
669
+ }
670
+ }
671
+ var remote_server_default = RemoteStackMemoryMCP;
672
+ async function runRemoteMCPServer(port = DEFAULT_PORT, projectRoot) {
673
+ const server = new RemoteStackMemoryMCP(projectRoot);
674
+ await server.startHttpServer(port);
675
+ }
676
+ if (import.meta.url === `file://${process.argv[1]}`) {
677
+ const port = parseInt(
678
+ process.env.PORT || process.argv[2] || String(DEFAULT_PORT),
679
+ 10
680
+ );
681
+ const projectRoot = process.argv[3] || process.cwd();
682
+ runRemoteMCPServer(port, projectRoot).catch((error) => {
683
+ console.error("Failed to start remote MCP server:", error);
684
+ process.exit(1);
685
+ });
686
+ }
687
+ export {
688
+ remote_server_default as default,
689
+ runRemoteMCPServer
690
+ };
691
+ //# sourceMappingURL=remote-server.js.map