@stackmemoryai/stackmemory 0.5.56 → 0.5.57
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.
- package/dist/cli/commands/handoff.js +13 -8
- package/dist/cli/commands/handoff.js.map +2 -2
- package/dist/cli/commands/setup.js +266 -0
- package/dist/cli/commands/setup.js.map +2 -2
- package/dist/cli/index.js +26 -1
- package/dist/cli/index.js.map +2 -2
- package/dist/core/context/stack-merge-resolver.js +153 -8
- package/dist/core/context/stack-merge-resolver.js.map +2 -2
- package/dist/integrations/mcp/remote-server.js +691 -0
- package/dist/integrations/mcp/remote-server.js.map +7 -0
- package/package.json +1 -1
|
@@ -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
|