stackmem 1.0.0
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/README.md +223 -0
- package/dist/ast-index.js +272 -0
- package/dist/cli.js +718 -0
- package/dist/config.js +2 -0
- package/dist/device.js +30 -0
- package/dist/index.js +324 -0
- package/dist/scoring.js +217 -0
- package/dist/storage.js +53 -0
- package/dist/test-scoring.js +7 -0
- package/dist/tools.js +1 -0
- package/package.json +34 -0
- package/src/ast-index.ts +349 -0
- package/src/cli.ts +814 -0
- package/src/config.ts +3 -0
- package/src/device.ts +34 -0
- package/src/index.ts +436 -0
- package/src/scoring.ts +276 -0
- package/src/storage.ts +57 -0
- package/src/test-scoring.ts +11 -0
- package/src/tools.ts +0 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export const SUPABASE_URL = "https://vuplpnwwygojkwxwsnip.supabase.co";
|
|
2
|
+
export const SUPABASE_ANON_KEY =
|
|
3
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZ1cGxwbnd3eWdvamt3eHdzbmlwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3OTA0NjU2MjIsImV4cCI6MjEwNjA0MTYyMn0.gIIWuALWlOAzKRD3AAQVHnCNq-o4mVOHOgHJKc0fmYs";
|
package/src/device.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEVICE_DIR = path.join(os.homedir(), ".stackmem");
|
|
7
|
+
const DEVICE_FILE = path.join(DEVICE_DIR, "device.json");
|
|
8
|
+
|
|
9
|
+
let cachedDeviceId: string | null = null;
|
|
10
|
+
|
|
11
|
+
// Every request is scoped to this id (see storage.ts), so it's effectively
|
|
12
|
+
// a bearer credential for whatever this device has saved — treat device.json
|
|
13
|
+
// like a secret, not just a preference file.
|
|
14
|
+
export function getDeviceId(): string {
|
|
15
|
+
if (cachedDeviceId) return cachedDeviceId;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(DEVICE_FILE, "utf-8");
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
if (typeof parsed.device_id === "string" && parsed.device_id.length > 0) {
|
|
21
|
+
cachedDeviceId = parsed.device_id;
|
|
22
|
+
return parsed.device_id;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// No device file yet, or it's unreadable/corrupt — create a fresh one below.
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const deviceId = randomUUID();
|
|
29
|
+
fs.mkdirSync(DEVICE_DIR, { recursive: true });
|
|
30
|
+
fs.writeFileSync(DEVICE_FILE, JSON.stringify({ device_id: deviceId }, null, 2), "utf-8");
|
|
31
|
+
|
|
32
|
+
cachedDeviceId = deviceId;
|
|
33
|
+
return deviceId;
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { analyzeCodebase, getFileSummary, findDependencies } from "./ast-index.js";
|
|
6
|
+
import { getDeviceId } from "./device.js";
|
|
7
|
+
import {
|
|
8
|
+
applyContextBudget,
|
|
9
|
+
buildSummaryContent,
|
|
10
|
+
clusterMemories,
|
|
11
|
+
computeDecayScore,
|
|
12
|
+
computeMemoryLinks,
|
|
13
|
+
decideSaveAction,
|
|
14
|
+
estimateTokens,
|
|
15
|
+
} from "./scoring.js";
|
|
16
|
+
import { supabase } from "./storage.js";
|
|
17
|
+
|
|
18
|
+
const CONTEXT_BUDGET_TOKENS = 2000;
|
|
19
|
+
const COMPRESSION_THRESHOLD = 20;
|
|
20
|
+
|
|
21
|
+
const server = new McpServer({
|
|
22
|
+
name: "coding-memory",
|
|
23
|
+
version: "1.0.0",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// --- session_start -----------------------------------------------------
|
|
27
|
+
|
|
28
|
+
server.registerTool(
|
|
29
|
+
"session_start",
|
|
30
|
+
{
|
|
31
|
+
title: "Session Start",
|
|
32
|
+
description: "Signal the start of a coding session for a given project.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
project: z.string().describe("Project identifier"),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
async ({ project }) => {
|
|
38
|
+
const { data, error } = await supabase
|
|
39
|
+
.from("memories")
|
|
40
|
+
.select("*")
|
|
41
|
+
.eq("project", project)
|
|
42
|
+
.eq("resolved", false);
|
|
43
|
+
|
|
44
|
+
if (error) {
|
|
45
|
+
throw new Error(`Failed to load memories: ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const memories = data ?? [];
|
|
49
|
+
|
|
50
|
+
const scoredMemories = memories
|
|
51
|
+
.map((memory) => ({
|
|
52
|
+
...memory,
|
|
53
|
+
decay_score: computeDecayScore(new Date(memory.created_at), memory.access_count ?? 0),
|
|
54
|
+
}))
|
|
55
|
+
.sort((a, b) => b.decay_score - a.decay_score);
|
|
56
|
+
|
|
57
|
+
const budgetedMemories = applyContextBudget(scoredMemories, CONTEXT_BUDGET_TOKENS);
|
|
58
|
+
const omittedCount = scoredMemories.length - budgetedMemories.length;
|
|
59
|
+
const tokensUsed = budgetedMemories.reduce(
|
|
60
|
+
(sum, memory) => sum + estimateTokens(memory.content),
|
|
61
|
+
0
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const memoryIds = budgetedMemories.map((m) => m.id);
|
|
65
|
+
const linkedIdsByMemory = new Map<string, Set<string>>();
|
|
66
|
+
|
|
67
|
+
if (memoryIds.length > 0) {
|
|
68
|
+
const idList = memoryIds.join(",");
|
|
69
|
+
const { data: links, error: linksError } = await supabase
|
|
70
|
+
.from("memory_links")
|
|
71
|
+
.select("source_id, target_id")
|
|
72
|
+
.or(`source_id.in.(${idList}),target_id.in.(${idList})`);
|
|
73
|
+
|
|
74
|
+
if (linksError) {
|
|
75
|
+
throw new Error(`Failed to load memory links: ${linksError.message}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const link of links ?? []) {
|
|
79
|
+
if (!linkedIdsByMemory.has(link.source_id)) linkedIdsByMemory.set(link.source_id, new Set());
|
|
80
|
+
if (!linkedIdsByMemory.has(link.target_id)) linkedIdsByMemory.set(link.target_id, new Set());
|
|
81
|
+
linkedIdsByMemory.get(link.source_id)!.add(link.target_id);
|
|
82
|
+
linkedIdsByMemory.get(link.target_id)!.add(link.source_id);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const memoriesWithLinks = budgetedMemories.map((m) => ({
|
|
87
|
+
...m,
|
|
88
|
+
linked_memory_ids: Array.from(linkedIdsByMemory.get(m.id) ?? []),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
if (budgetedMemories.length > 0) {
|
|
92
|
+
const incrementResults = await Promise.all(
|
|
93
|
+
budgetedMemories.map((memory) =>
|
|
94
|
+
supabase
|
|
95
|
+
.from("memories")
|
|
96
|
+
.update({ access_count: (memory.access_count ?? 0) + 1 })
|
|
97
|
+
.eq("id", memory.id)
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
const incrementError = incrementResults.find((result) => result.error)?.error;
|
|
101
|
+
if (incrementError) {
|
|
102
|
+
throw new Error(`Failed to update access counts: ${incrementError.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const budgetText =
|
|
107
|
+
omittedCount > 0
|
|
108
|
+
? `Loaded ${memories.length} memory row(s) for "${project}" (${tokensUsed}/${CONTEXT_BUDGET_TOKENS} tokens used, ${omittedCount} omitted for budget).`
|
|
109
|
+
: `Loaded ${memories.length} memory row(s) for "${project}" (${tokensUsed}/${CONTEXT_BUDGET_TOKENS} tokens used).`;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: "text", text: budgetText }],
|
|
113
|
+
structuredContent: {
|
|
114
|
+
memories: memoriesWithLinks,
|
|
115
|
+
context_budget: {
|
|
116
|
+
budget_tokens: CONTEXT_BUDGET_TOKENS,
|
|
117
|
+
tokens_used: tokensUsed,
|
|
118
|
+
omitted_count: omittedCount,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// --- memory compression ----------------------------------------------------
|
|
126
|
+
|
|
127
|
+
async function compressMemories(
|
|
128
|
+
project: string
|
|
129
|
+
): Promise<{ totalUnresolved: number; clustersCompressed: number }> {
|
|
130
|
+
const { data, error } = await supabase
|
|
131
|
+
.from("memories")
|
|
132
|
+
.select("*")
|
|
133
|
+
.eq("project", project)
|
|
134
|
+
.eq("resolved", false);
|
|
135
|
+
|
|
136
|
+
if (error) {
|
|
137
|
+
throw new Error(`Failed to load memories for compression: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const memories = data ?? [];
|
|
141
|
+
if (memories.length < COMPRESSION_THRESHOLD) {
|
|
142
|
+
return { totalUnresolved: memories.length, clustersCompressed: 0 };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const clusters = clusterMemories(memories);
|
|
146
|
+
let clustersCompressed = 0;
|
|
147
|
+
|
|
148
|
+
for (const cluster of clusters) {
|
|
149
|
+
if (cluster.length < 3) continue;
|
|
150
|
+
|
|
151
|
+
const summaryContent = buildSummaryContent(cluster);
|
|
152
|
+
const { error: insertError } = await supabase
|
|
153
|
+
.from("memories")
|
|
154
|
+
.insert({ project, type: "discovery", content: summaryContent, device_id: getDeviceId() });
|
|
155
|
+
|
|
156
|
+
if (insertError) {
|
|
157
|
+
throw new Error(`Failed to insert summary memory: ${insertError.message}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const idsToResolve = cluster.map((memory) => memory.id);
|
|
161
|
+
const { error: resolveError } = await supabase
|
|
162
|
+
.from("memories")
|
|
163
|
+
.update({ resolved: true })
|
|
164
|
+
.in("id", idsToResolve);
|
|
165
|
+
|
|
166
|
+
if (resolveError) {
|
|
167
|
+
throw new Error(`Failed to resolve compressed memories: ${resolveError.message}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.error(`Compressed ${cluster.length} memories into 1 summary`);
|
|
171
|
+
clustersCompressed++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.error(`Compression complete. ${clustersCompressed} clusters compressed.`);
|
|
175
|
+
return { totalUnresolved: memories.length, clustersCompressed };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- save_memory ---------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
const memoryTypeEnum = z.enum([
|
|
181
|
+
"decision",
|
|
182
|
+
"rejection",
|
|
183
|
+
"constraint",
|
|
184
|
+
"discovery",
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
server.registerTool(
|
|
188
|
+
"save_memory",
|
|
189
|
+
{
|
|
190
|
+
title: "Save Memory",
|
|
191
|
+
description:
|
|
192
|
+
"Persist a piece of project memory (decision, rejection, constraint, or discovery).",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
project: z.string().describe("Project identifier"),
|
|
195
|
+
type: memoryTypeEnum.describe("Category of memory being saved"),
|
|
196
|
+
content: z.string().describe("The memory content to save"),
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
async ({ project, type, content }) => {
|
|
200
|
+
const { data: existingRows, error: existingError } = await supabase
|
|
201
|
+
.from("memories")
|
|
202
|
+
.select("id, content")
|
|
203
|
+
.eq("project", project)
|
|
204
|
+
.eq("resolved", false);
|
|
205
|
+
|
|
206
|
+
if (existingError) {
|
|
207
|
+
throw new Error(`Failed to load memories: ${existingError.message}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const existingMemories = existingRows ?? [];
|
|
211
|
+
const decision = decideSaveAction(content, existingMemories);
|
|
212
|
+
|
|
213
|
+
if (decision.action === "duplicate") {
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: "text", text: "Duplicate memory detected — skipping." }],
|
|
216
|
+
structuredContent: {
|
|
217
|
+
saved: false,
|
|
218
|
+
reason: "duplicate",
|
|
219
|
+
matched_id: decision.matchedId,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (decision.action === "superseded") {
|
|
225
|
+
const { error: resolveError } = await supabase
|
|
226
|
+
.from("memories")
|
|
227
|
+
.update({ resolved: true })
|
|
228
|
+
.eq("id", decision.oldId);
|
|
229
|
+
|
|
230
|
+
if (resolveError) {
|
|
231
|
+
throw new Error(`Failed to resolve superseded memory: ${resolveError.message}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const { data: inserted, error } = await supabase
|
|
236
|
+
.from("memories")
|
|
237
|
+
.insert({ project, type, content, device_id: getDeviceId() })
|
|
238
|
+
.select("id")
|
|
239
|
+
.single();
|
|
240
|
+
|
|
241
|
+
if (error || !inserted) {
|
|
242
|
+
throw new Error(`Failed to save memory: ${error?.message ?? "no row returned"}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const newMemoryId = inserted.id;
|
|
246
|
+
const links = computeMemoryLinks(newMemoryId, content, existingMemories);
|
|
247
|
+
|
|
248
|
+
if (links.length > 0) {
|
|
249
|
+
const { error: linkError } = await supabase.from("memory_links").insert(links);
|
|
250
|
+
if (linkError) {
|
|
251
|
+
throw new Error(`Failed to save memory links: ${linkError.message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const { count: unresolvedCount, error: countError } = await supabase
|
|
256
|
+
.from("memories")
|
|
257
|
+
.select("id", { count: "exact", head: true })
|
|
258
|
+
.eq("project", project)
|
|
259
|
+
.eq("resolved", false);
|
|
260
|
+
|
|
261
|
+
if (countError) {
|
|
262
|
+
throw new Error(`Failed to count memories: ${countError.message}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if ((unresolvedCount ?? 0) >= COMPRESSION_THRESHOLD) {
|
|
266
|
+
await compressMemories(project);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (decision.action === "superseded") {
|
|
270
|
+
return {
|
|
271
|
+
content: [{ type: "text", text: `Superseded previous memory [${decision.oldId}].` }],
|
|
272
|
+
structuredContent: { saved: true, action: "superseded", old_id: decision.oldId },
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
content: [{ type: "text", text: "Memory saved." }],
|
|
278
|
+
structuredContent: { saved: true, action: "created" },
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// --- search_past_solutions -------------------------------------------------
|
|
284
|
+
|
|
285
|
+
server.registerTool(
|
|
286
|
+
"search_past_solutions",
|
|
287
|
+
{
|
|
288
|
+
title: "Search Past Solutions",
|
|
289
|
+
description:
|
|
290
|
+
"Search previously recorded memory/fixes for a project matching a query.",
|
|
291
|
+
inputSchema: {
|
|
292
|
+
project: z.string().describe("Project identifier"),
|
|
293
|
+
query: z.string().describe("Search query"),
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
async ({ project, query }) => {
|
|
297
|
+
void query;
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
const { data, error } = await supabase
|
|
301
|
+
.from("execution_log")
|
|
302
|
+
.select("*")
|
|
303
|
+
.eq("project", project)
|
|
304
|
+
.eq("resolved", false);
|
|
305
|
+
|
|
306
|
+
if (error) {
|
|
307
|
+
throw new Error(`Failed to search past solutions: ${error.message}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const results = data ?? [];
|
|
311
|
+
return {
|
|
312
|
+
content: [{ type: "text", text: `Found ${results.length} result(s).` }],
|
|
313
|
+
structuredContent: { results },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// --- record_fix ----------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
server.registerTool(
|
|
321
|
+
"record_fix",
|
|
322
|
+
{
|
|
323
|
+
title: "Record Fix",
|
|
324
|
+
description: "Record a problem and its solution for future reference.",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
project: z.string().describe("Project identifier"),
|
|
327
|
+
problem: z.string().describe("Description of the problem"),
|
|
328
|
+
solution: z.string().describe("Description of the solution/fix"),
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
async ({ project, problem, solution }) => {
|
|
332
|
+
const { error } = await supabase
|
|
333
|
+
.from("execution_log")
|
|
334
|
+
.insert({ project, problem, solution, resolved: false, device_id: getDeviceId() });
|
|
335
|
+
|
|
336
|
+
if (error) {
|
|
337
|
+
throw new Error(`Failed to record fix: ${error.message}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const saved = true as const;
|
|
341
|
+
return {
|
|
342
|
+
content: [{ type: "text", text: "Fix recorded." }],
|
|
343
|
+
structuredContent: { saved },
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// --- analyze_codebase ------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
server.registerTool(
|
|
351
|
+
"analyze_codebase",
|
|
352
|
+
{
|
|
353
|
+
title: "Analyze Codebase",
|
|
354
|
+
description:
|
|
355
|
+
"Build a lightweight AST index of a project's .ts/.js files (functions, classes, imports) into a local SQLite database at <project_path>/.coding-memory/ast-index.db.",
|
|
356
|
+
inputSchema: {
|
|
357
|
+
project_path: z.string().describe("Absolute or relative path to the project to index"),
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
async ({ project_path }) => {
|
|
361
|
+
const summary = analyzeCodebase(project_path);
|
|
362
|
+
return {
|
|
363
|
+
content: [
|
|
364
|
+
{
|
|
365
|
+
type: "text",
|
|
366
|
+
text: `Indexed ${summary.files_indexed} file(s): ${summary.functions} function(s), ${summary.classes} class(es).`,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
structuredContent: summary,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// --- get_file_summary --------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
server.registerTool(
|
|
377
|
+
"get_file_summary",
|
|
378
|
+
{
|
|
379
|
+
title: "Get File Summary",
|
|
380
|
+
description:
|
|
381
|
+
"Look up the functions and classes recorded for a file in the project's AST index. Requires analyze_codebase to have been run first.",
|
|
382
|
+
inputSchema: {
|
|
383
|
+
project_path: z.string().describe("Path to the project that was indexed"),
|
|
384
|
+
file_name: z.string().describe("File name or relative path to look up"),
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
async ({ project_path, file_name }) => {
|
|
388
|
+
const summary = getFileSummary(project_path, file_name);
|
|
389
|
+
return {
|
|
390
|
+
content: [
|
|
391
|
+
{
|
|
392
|
+
type: "text",
|
|
393
|
+
text: `${summary.file_path}: ${summary.functions.length} function(s), ${summary.classes.length} class(es).`,
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
structuredContent: summary,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// --- find_dependencies ----------------------------------------------------
|
|
402
|
+
|
|
403
|
+
server.registerTool(
|
|
404
|
+
"find_dependencies",
|
|
405
|
+
{
|
|
406
|
+
title: "Find Dependencies",
|
|
407
|
+
description:
|
|
408
|
+
"Look up what a file imports, based on the project's AST index. Requires analyze_codebase to have been run first.",
|
|
409
|
+
inputSchema: {
|
|
410
|
+
project_path: z.string().describe("Path to the project that was indexed"),
|
|
411
|
+
file_name: z.string().describe("File name or relative path to look up"),
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
async ({ project_path, file_name }) => {
|
|
415
|
+
const deps = findDependencies(project_path, file_name);
|
|
416
|
+
return {
|
|
417
|
+
content: [
|
|
418
|
+
{ type: "text", text: `${deps.file_path} imports ${deps.imports.length} module(s).` },
|
|
419
|
+
],
|
|
420
|
+
structuredContent: deps,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// --- transport -------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
async function main() {
|
|
428
|
+
const transport = new StdioServerTransport();
|
|
429
|
+
await server.connect(transport);
|
|
430
|
+
console.error("coding-memory MCP server running on stdio");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
main().catch((error) => {
|
|
434
|
+
console.error("Fatal error starting coding-memory MCP server:", error);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
});
|