@vheins/local-memory-mcp 0.18.13 → 0.19.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.id.md +9 -1
- package/README.md +32 -17
- package/dist/{chunk-BK2QIQXS.js → chunk-AKFMCVQ4.js} +857 -680
- package/dist/dashboard/public/assets/{index-wAYh22Zy.css → index-CtKekPIh.css} +1 -1
- package/dist/dashboard/public/assets/index-DsmurBbn.js +150 -0
- package/dist/dashboard/public/index.html +2 -2
- package/dist/dashboard/server.js +227 -171
- package/dist/mcp/server.js +1919 -727
- package/dist/prompts/create-task.md +10 -2
- package/dist/prompts/documentation-sync.md +1 -1
- package/dist/prompts/learning-retrospective.md +1 -1
- package/dist/prompts/memory-agent-core.md +8 -2
- package/dist/prompts/project-briefing.md +2 -2
- package/dist/prompts/review-and-audit.md +2 -2
- package/dist/prompts/review-and-post-issue.md +3 -3
- package/dist/prompts/scrum-master.md +1 -1
- package/dist/prompts/sentinel-issue-resolver.md +1 -1
- package/dist/prompts/server/instructions.md +10 -2
- package/dist/prompts/session-planner.md +1 -1
- package/dist/prompts/task-memory-executor.md +2 -2
- package/dist/prompts/tool-usage-guidelines.md +9 -2
- package/package.json +5 -1
- package/dist/dashboard/public/assets/index-DWRMdSsg.js +0 -150
package/dist/mcp/server.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
AgentContextSchema,
|
|
4
|
+
CreateEntitySchema,
|
|
5
|
+
CreateRelationSchema,
|
|
6
|
+
DecisionLogSchema,
|
|
7
|
+
DeleteEntitySchema,
|
|
8
|
+
DeleteObservationSchema,
|
|
9
|
+
DeleteRelationSchema,
|
|
6
10
|
MemoryAcknowledgeSchema,
|
|
7
11
|
MemoryDeleteSchema,
|
|
8
12
|
MemoryDetailSchema,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
MemoryUpdateSchema,
|
|
15
19
|
RealVectorStore,
|
|
16
20
|
SQLiteStore,
|
|
21
|
+
SessionSummarizeSchema,
|
|
17
22
|
StandardDeleteSchema,
|
|
18
23
|
StandardDetailSchema,
|
|
19
24
|
StandardSearchSchema,
|
|
@@ -35,14 +40,9 @@ import {
|
|
|
35
40
|
createFileSink,
|
|
36
41
|
createMcpResponse,
|
|
37
42
|
createSessionContext,
|
|
38
|
-
decodeCursor,
|
|
39
|
-
encodeCursor,
|
|
40
|
-
extractRootsFromResult,
|
|
41
43
|
findContainingRoot,
|
|
42
44
|
getFilesystemRoots,
|
|
43
|
-
getLogLevel,
|
|
44
45
|
getPrimaryTextContent,
|
|
45
|
-
getPrompt,
|
|
46
46
|
handleClaimList,
|
|
47
47
|
handleClaimRelease,
|
|
48
48
|
handleHandoffCreate,
|
|
@@ -52,146 +52,73 @@ import {
|
|
|
52
52
|
inferOwnerFromSession,
|
|
53
53
|
inferRepoFromSession,
|
|
54
54
|
isPathWithinRoots,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
listPromptFiles,
|
|
56
|
+
loadPromptFromMarkdown,
|
|
57
|
+
loadServerInstructions,
|
|
58
58
|
logger,
|
|
59
59
|
normalizeRepo,
|
|
60
60
|
parseRepoInput,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
updateSessionFromInitialize,
|
|
65
|
-
updateSessionRoots
|
|
66
|
-
} from "../chunk-BK2QIQXS.js";
|
|
61
|
+
rankCompletionValues,
|
|
62
|
+
toContextSlug
|
|
63
|
+
} from "../chunk-AKFMCVQ4.js";
|
|
67
64
|
|
|
68
65
|
// src/mcp/server.ts
|
|
69
|
-
import
|
|
66
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
70
67
|
|
|
71
|
-
// src/mcp/
|
|
72
|
-
import
|
|
68
|
+
// src/mcp/mcp-server.ts
|
|
69
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/server";
|
|
73
70
|
|
|
74
|
-
// src/mcp/
|
|
75
|
-
import fs from "fs";
|
|
71
|
+
// src/mcp/capabilities.ts
|
|
76
72
|
import path from "path";
|
|
77
|
-
|
|
78
|
-
var
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (refType === "ref/prompt") {
|
|
94
|
-
const promptName = typeof params?.ref?.name === "string" ? params.ref.name : "";
|
|
95
|
-
if (!promptName) {
|
|
96
|
-
throw invalidCompletionParams("Prompt completion requires ref.name");
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
completion: buildCompletionResult(
|
|
100
|
-
await completePromptArgument(promptName, argumentName, argumentValue, contextArguments, dataSources)
|
|
101
|
-
)
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
if (refType === "ref/resource") {
|
|
105
|
-
const resourceUri = typeof params?.ref?.uri === "string" ? params.ref.uri : "";
|
|
106
|
-
if (!resourceUri) {
|
|
107
|
-
throw invalidCompletionParams("Resource completion requires ref.uri");
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
completion: buildCompletionResult(
|
|
111
|
-
completeResourceArgument(resourceUri, argumentName, argumentValue, contextArguments, dataSources)
|
|
112
|
-
)
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
throw invalidCompletionParams(`Unsupported completion ref type: ${refType}`);
|
|
116
|
-
}
|
|
117
|
-
function buildCompletionResult(values) {
|
|
118
|
-
const capped = values.slice(0, MAX_COMPLETION_VALUES);
|
|
119
|
-
return {
|
|
120
|
-
values: capped,
|
|
121
|
-
total: values.length,
|
|
122
|
-
hasMore: values.length > capped.length
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function getSuggestedRepos(db2, session2) {
|
|
126
|
-
const values = /* @__PURE__ */ new Set();
|
|
127
|
-
const inferredRepo = inferRepoFromSession(session2);
|
|
128
|
-
if (inferredRepo) values.add(inferredRepo);
|
|
129
|
-
for (const rootPath of getFilesystemRoots(session2)) {
|
|
130
|
-
values.add(path.basename(rootPath));
|
|
131
|
-
}
|
|
132
|
-
for (const repo of db2.system.listRepos()) {
|
|
133
|
-
values.add(repo);
|
|
134
|
-
}
|
|
135
|
-
return [...values].sort((a, b) => a.localeCompare(b));
|
|
136
|
-
}
|
|
137
|
-
function getSuggestedTags(db2) {
|
|
138
|
-
const values = /* @__PURE__ */ new Set();
|
|
139
|
-
const memories = db2.memories.getRecentMemories("", "", 1e3);
|
|
140
|
-
for (const memory of memories) {
|
|
141
|
-
for (const tag of memory.tags || []) {
|
|
142
|
-
if (typeof tag === "string" && tag.trim()) {
|
|
143
|
-
values.add(tag.trim());
|
|
73
|
+
import { fileURLToPath } from "url";
|
|
74
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
var pkgVersion = "0.1.0";
|
|
76
|
+
if ("0.19.0") {
|
|
77
|
+
pkgVersion = "0.19.0";
|
|
78
|
+
} else {
|
|
79
|
+
let searchDir = __dirname;
|
|
80
|
+
for (let i = 0; i < 5; i++) {
|
|
81
|
+
const candidate = path.join(searchDir, "package.json");
|
|
82
|
+
try {
|
|
83
|
+
if (fs.existsSync(candidate)) {
|
|
84
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
85
|
+
if (pkg.name === "@vheins/local-memory-mcp" && pkg.version) {
|
|
86
|
+
pkgVersion = pkg.version;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
144
89
|
}
|
|
90
|
+
} catch {
|
|
145
91
|
}
|
|
92
|
+
searchDir = path.dirname(searchDir);
|
|
146
93
|
}
|
|
147
|
-
return [...values].sort((a, b) => a.localeCompare(b));
|
|
148
|
-
}
|
|
149
|
-
function getSuggestedTasks(db2, session2, contextArguments) {
|
|
150
|
-
const repo = typeof contextArguments.repo === "string" && contextArguments.repo.trim() ? contextArguments.repo.trim() : inferRepoFromSession(session2);
|
|
151
|
-
if (!repo) return [];
|
|
152
|
-
return db2.tasks.getTasksByRepo("", repo, void 0, 100).map((task) => ({
|
|
153
|
-
id: task.id,
|
|
154
|
-
task_code: task.task_code,
|
|
155
|
-
title: task.title
|
|
156
|
-
}));
|
|
157
94
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
for (const entry of entries) {
|
|
176
|
-
if (results.length >= MAX_FILE_SCAN_RESULTS) return;
|
|
177
|
-
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
|
|
178
|
-
const fullPath = path.join(currentPath, entry.name);
|
|
179
|
-
if (entry.isDirectory()) {
|
|
180
|
-
collectFiles(rootPath, fullPath, results);
|
|
181
|
-
continue;
|
|
95
|
+
var CAPABILITIES = {
|
|
96
|
+
serverInfo: {
|
|
97
|
+
name: "local-memory-mcp",
|
|
98
|
+
version: pkgVersion
|
|
99
|
+
},
|
|
100
|
+
capabilities: {
|
|
101
|
+
completions: {},
|
|
102
|
+
logging: {},
|
|
103
|
+
resources: {
|
|
104
|
+
subscribe: true,
|
|
105
|
+
listChanged: true
|
|
106
|
+
},
|
|
107
|
+
tools: {
|
|
108
|
+
listChanged: false
|
|
109
|
+
},
|
|
110
|
+
prompts: {
|
|
111
|
+
listChanged: true
|
|
182
112
|
}
|
|
183
|
-
if (!entry.isFile()) continue;
|
|
184
|
-
results.push(path.relative(rootPath, fullPath) || entry.name);
|
|
185
113
|
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// src/mcp/tools/index.ts
|
|
117
|
+
import path2 from "path";
|
|
118
|
+
import { z } from "zod";
|
|
192
119
|
|
|
193
120
|
// src/mcp/tools/memory.store.ts
|
|
194
|
-
import { randomUUID } from "crypto";
|
|
121
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
195
122
|
|
|
196
123
|
// src/mcp/utils/code-generator.ts
|
|
197
124
|
var ENTITY_CONFIG = {
|
|
@@ -224,6 +151,275 @@ function generateNextCode(owner, repo, entityType, storage, batchCodes) {
|
|
|
224
151
|
return `${config.prefix}-${String(nextSeq).padStart(3, "0")}`;
|
|
225
152
|
}
|
|
226
153
|
|
|
154
|
+
// src/mcp/tools/kg-archivist.ts
|
|
155
|
+
import { randomUUID } from "crypto";
|
|
156
|
+
import nlp from "compromise";
|
|
157
|
+
var MAX_CONTENT_LENGTH = 5e3;
|
|
158
|
+
var PRONOUNS = /* @__PURE__ */ new Set([
|
|
159
|
+
"i",
|
|
160
|
+
"me",
|
|
161
|
+
"my",
|
|
162
|
+
"myself",
|
|
163
|
+
"we",
|
|
164
|
+
"us",
|
|
165
|
+
"our",
|
|
166
|
+
"ours",
|
|
167
|
+
"ourselves",
|
|
168
|
+
"you",
|
|
169
|
+
"your",
|
|
170
|
+
"yours",
|
|
171
|
+
"yourself",
|
|
172
|
+
"yourselves",
|
|
173
|
+
"he",
|
|
174
|
+
"him",
|
|
175
|
+
"his",
|
|
176
|
+
"himself",
|
|
177
|
+
"she",
|
|
178
|
+
"her",
|
|
179
|
+
"hers",
|
|
180
|
+
"herself",
|
|
181
|
+
"it",
|
|
182
|
+
"its",
|
|
183
|
+
"itself",
|
|
184
|
+
"they",
|
|
185
|
+
"them",
|
|
186
|
+
"their",
|
|
187
|
+
"theirs",
|
|
188
|
+
"themselves",
|
|
189
|
+
"this",
|
|
190
|
+
"that",
|
|
191
|
+
"these",
|
|
192
|
+
"those",
|
|
193
|
+
"someone",
|
|
194
|
+
"somebody",
|
|
195
|
+
"something",
|
|
196
|
+
"anyone",
|
|
197
|
+
"anybody",
|
|
198
|
+
"anything",
|
|
199
|
+
"everyone",
|
|
200
|
+
"everybody",
|
|
201
|
+
"everything",
|
|
202
|
+
"nobody",
|
|
203
|
+
"nothing"
|
|
204
|
+
]);
|
|
205
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
206
|
+
"a",
|
|
207
|
+
"an",
|
|
208
|
+
"the",
|
|
209
|
+
"and",
|
|
210
|
+
"but",
|
|
211
|
+
"or",
|
|
212
|
+
"if",
|
|
213
|
+
"because",
|
|
214
|
+
"as",
|
|
215
|
+
"until",
|
|
216
|
+
"while",
|
|
217
|
+
"of",
|
|
218
|
+
"at",
|
|
219
|
+
"by",
|
|
220
|
+
"for",
|
|
221
|
+
"with",
|
|
222
|
+
"about",
|
|
223
|
+
"against",
|
|
224
|
+
"between",
|
|
225
|
+
"into",
|
|
226
|
+
"through",
|
|
227
|
+
"during",
|
|
228
|
+
"before",
|
|
229
|
+
"after",
|
|
230
|
+
"above",
|
|
231
|
+
"below",
|
|
232
|
+
"to",
|
|
233
|
+
"from",
|
|
234
|
+
"up",
|
|
235
|
+
"down",
|
|
236
|
+
"in",
|
|
237
|
+
"out",
|
|
238
|
+
"on",
|
|
239
|
+
"off",
|
|
240
|
+
"over",
|
|
241
|
+
"under",
|
|
242
|
+
"again",
|
|
243
|
+
"further",
|
|
244
|
+
"then",
|
|
245
|
+
"once",
|
|
246
|
+
"here",
|
|
247
|
+
"there",
|
|
248
|
+
"when",
|
|
249
|
+
"where",
|
|
250
|
+
"why",
|
|
251
|
+
"how",
|
|
252
|
+
"all",
|
|
253
|
+
"each",
|
|
254
|
+
"every",
|
|
255
|
+
"both",
|
|
256
|
+
"few",
|
|
257
|
+
"more",
|
|
258
|
+
"most",
|
|
259
|
+
"other",
|
|
260
|
+
"some",
|
|
261
|
+
"such",
|
|
262
|
+
"no",
|
|
263
|
+
"nor",
|
|
264
|
+
"not",
|
|
265
|
+
"only",
|
|
266
|
+
"own",
|
|
267
|
+
"same",
|
|
268
|
+
"so",
|
|
269
|
+
"than",
|
|
270
|
+
"too",
|
|
271
|
+
"very",
|
|
272
|
+
"just",
|
|
273
|
+
"also",
|
|
274
|
+
"any",
|
|
275
|
+
"thing",
|
|
276
|
+
"things",
|
|
277
|
+
"way",
|
|
278
|
+
"ways",
|
|
279
|
+
"person",
|
|
280
|
+
"people",
|
|
281
|
+
"man",
|
|
282
|
+
"woman",
|
|
283
|
+
"child",
|
|
284
|
+
"time",
|
|
285
|
+
"year",
|
|
286
|
+
"day",
|
|
287
|
+
"number",
|
|
288
|
+
"world",
|
|
289
|
+
"life",
|
|
290
|
+
"hand",
|
|
291
|
+
"part",
|
|
292
|
+
"place",
|
|
293
|
+
"case",
|
|
294
|
+
"week",
|
|
295
|
+
"company",
|
|
296
|
+
"system",
|
|
297
|
+
"program",
|
|
298
|
+
"work",
|
|
299
|
+
"group",
|
|
300
|
+
"problem",
|
|
301
|
+
"fact",
|
|
302
|
+
"example",
|
|
303
|
+
"member",
|
|
304
|
+
"car",
|
|
305
|
+
"city",
|
|
306
|
+
"state",
|
|
307
|
+
"country",
|
|
308
|
+
"area",
|
|
309
|
+
"water",
|
|
310
|
+
"air",
|
|
311
|
+
"money",
|
|
312
|
+
"data",
|
|
313
|
+
"information",
|
|
314
|
+
"software",
|
|
315
|
+
"code",
|
|
316
|
+
"file",
|
|
317
|
+
"server",
|
|
318
|
+
"database",
|
|
319
|
+
"application",
|
|
320
|
+
"user",
|
|
321
|
+
"users",
|
|
322
|
+
"project",
|
|
323
|
+
"task",
|
|
324
|
+
"memory",
|
|
325
|
+
"value",
|
|
326
|
+
"name",
|
|
327
|
+
"type",
|
|
328
|
+
"list",
|
|
329
|
+
"set",
|
|
330
|
+
"number",
|
|
331
|
+
"id",
|
|
332
|
+
"key",
|
|
333
|
+
"text",
|
|
334
|
+
"content",
|
|
335
|
+
"title",
|
|
336
|
+
"description",
|
|
337
|
+
"status",
|
|
338
|
+
"time"
|
|
339
|
+
]);
|
|
340
|
+
var DETERMINERS = /^(a|an|the)\s+/i;
|
|
341
|
+
var TRAILING_PUNCTUATION = /[.,!?;:()"'[\]]+$/g;
|
|
342
|
+
function cleanText(raw) {
|
|
343
|
+
return raw.replace(TRAILING_PUNCTUATION, "").trim();
|
|
344
|
+
}
|
|
345
|
+
function stripLeadingDeterminer(phrase) {
|
|
346
|
+
return phrase.replace(DETERMINERS, "").trim();
|
|
347
|
+
}
|
|
348
|
+
function isExcludedNoun(candidate) {
|
|
349
|
+
const lower = candidate.toLowerCase();
|
|
350
|
+
if (lower.length < 2) return true;
|
|
351
|
+
if (PRONOUNS.has(lower)) return true;
|
|
352
|
+
if (STOPWORDS.has(lower)) return true;
|
|
353
|
+
if (/^\d+$/.test(candidate)) return true;
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
function extractEntities(content) {
|
|
357
|
+
if (!content || content.trim().length === 0) return [];
|
|
358
|
+
const text = content.length > MAX_CONTENT_LENGTH ? content.slice(0, MAX_CONTENT_LENGTH) : content;
|
|
359
|
+
const doc = nlp(text);
|
|
360
|
+
const seen = /* @__PURE__ */ new Set();
|
|
361
|
+
const entities = [];
|
|
362
|
+
function add(name, type) {
|
|
363
|
+
const trimmed = name.trim();
|
|
364
|
+
if (!trimmed || trimmed.length < 2) return;
|
|
365
|
+
const key = trimmed.toLowerCase();
|
|
366
|
+
if (seen.has(key)) return;
|
|
367
|
+
seen.add(key);
|
|
368
|
+
entities.push({ name: trimmed, type });
|
|
369
|
+
}
|
|
370
|
+
for (const match of doc.people().json()) {
|
|
371
|
+
add(cleanText(match.text), "person");
|
|
372
|
+
}
|
|
373
|
+
for (const match of doc.places().json()) {
|
|
374
|
+
add(cleanText(match.text), "place");
|
|
375
|
+
}
|
|
376
|
+
for (const match of doc.organizations().json()) {
|
|
377
|
+
add(cleanText(match.text), "organization");
|
|
378
|
+
}
|
|
379
|
+
for (const match of doc.nouns().json()) {
|
|
380
|
+
const raw = cleanText(match.text);
|
|
381
|
+
const candidate = stripLeadingDeterminer(raw);
|
|
382
|
+
if (!isExcludedNoun(candidate)) {
|
|
383
|
+
add(candidate, "concept");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return entities;
|
|
387
|
+
}
|
|
388
|
+
function saveExtractions(content, title, owner, repo, db2) {
|
|
389
|
+
if (!content || content.trim().length === 0) return;
|
|
390
|
+
let entities;
|
|
391
|
+
try {
|
|
392
|
+
entities = extractEntities(content);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
logger.warn("[KG-Archivist] Entity extraction failed, skipping", {
|
|
395
|
+
error: String(err)
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (entities.length === 0) return;
|
|
400
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
401
|
+
const observationText = `Mentioned in memory: ${title}`;
|
|
402
|
+
const insertEntity = db2.db.prepare(
|
|
403
|
+
`INSERT OR IGNORE INTO entities (name, type, description, repo, owner, created_at, updated_at)
|
|
404
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
405
|
+
);
|
|
406
|
+
const insertObservation = db2.db.prepare(
|
|
407
|
+
`INSERT INTO observations (id, entity_name, observation, repo, owner, created_at)
|
|
408
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
409
|
+
);
|
|
410
|
+
for (const entity of entities) {
|
|
411
|
+
try {
|
|
412
|
+
insertEntity.run(entity.name, entity.type, null, repo, owner ?? "", now, now);
|
|
413
|
+
insertObservation.run(randomUUID(), entity.name, observationText, repo, owner ?? "", now);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
logger.warn("[KG-Archivist] Failed to save extraction for entity", {
|
|
416
|
+
error: String(err),
|
|
417
|
+
entity: entity.name
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
227
423
|
// src/mcp/tools/memory.store.ts
|
|
228
424
|
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
229
425
|
function hasMetadataLikeTitle(title) {
|
|
@@ -280,7 +476,7 @@ async function storeSingleMemory(params, db2, vectors2) {
|
|
|
280
476
|
tags.push(params.scope.language.toLowerCase());
|
|
281
477
|
}
|
|
282
478
|
const entry = {
|
|
283
|
-
id:
|
|
479
|
+
id: randomUUID2(),
|
|
284
480
|
code: params.code || generateNextCode(params.scope.owner ?? "", params.scope.repo, "memory", db2),
|
|
285
481
|
type: params.type,
|
|
286
482
|
title: params.title,
|
|
@@ -309,6 +505,13 @@ async function storeSingleMemory(params, db2, vectors2) {
|
|
|
309
505
|
} catch (error) {
|
|
310
506
|
logger.warn("Failed to generate vector embedding", { error: String(error) });
|
|
311
507
|
}
|
|
508
|
+
try {
|
|
509
|
+
saveExtractions(entry.content, entry.title, entry.scope.owner, entry.scope.repo, db2);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
logger.warn("[KG-Archivist] NLP extraction failed, memory stored without KG enrichment", {
|
|
512
|
+
error: String(error)
|
|
513
|
+
});
|
|
514
|
+
}
|
|
312
515
|
return createMcpResponse(
|
|
313
516
|
{
|
|
314
517
|
success: true,
|
|
@@ -377,7 +580,7 @@ async function handleMemoryStore(params, db2, vectors2) {
|
|
|
377
580
|
const code = mem.code || generateNextCode(mem.scope.owner ?? "", mem.scope.repo, "memory", db2, batchCodes);
|
|
378
581
|
batchCodes.add(code);
|
|
379
582
|
entries.push({
|
|
380
|
-
id:
|
|
583
|
+
id: randomUUID2(),
|
|
381
584
|
code,
|
|
382
585
|
type: mem.type,
|
|
383
586
|
title: mem.title,
|
|
@@ -409,6 +612,13 @@ async function handleMemoryStore(params, db2, vectors2) {
|
|
|
409
612
|
} catch (error) {
|
|
410
613
|
logger.warn("Failed to generate vector embedding", { error: String(error) });
|
|
411
614
|
}
|
|
615
|
+
try {
|
|
616
|
+
saveExtractions(entry.content, entry.title, entry.scope.owner, entry.scope.repo, db2);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
logger.warn("[KG-Archivist] NLP extraction failed, memory stored without KG enrichment", {
|
|
619
|
+
error: String(error)
|
|
620
|
+
});
|
|
621
|
+
}
|
|
412
622
|
}
|
|
413
623
|
const codesStr = storedCodes.length > 0 ? `: ${storedCodes.join(", ")}` : "";
|
|
414
624
|
return createMcpResponse(
|
|
@@ -543,6 +753,134 @@ function expandQuery(query, prompt) {
|
|
|
543
753
|
return Array.from(expansions).join(" ");
|
|
544
754
|
}
|
|
545
755
|
|
|
756
|
+
// src/mcp/tools/time-tunnel.ts
|
|
757
|
+
function startOfDay(d) {
|
|
758
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
759
|
+
}
|
|
760
|
+
function endOfDay(d) {
|
|
761
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
|
|
762
|
+
}
|
|
763
|
+
function currentWeekMonday(now) {
|
|
764
|
+
const day = now.getDay();
|
|
765
|
+
const offset = day === 0 ? -6 : 1 - day;
|
|
766
|
+
return startOfDay(new Date(now.getFullYear(), now.getMonth(), now.getDate() + offset));
|
|
767
|
+
}
|
|
768
|
+
var PATTERNS = [
|
|
769
|
+
// Order matters: more specific phrases first to avoid partial matches.
|
|
770
|
+
// "last month" — full previous calendar month
|
|
771
|
+
{
|
|
772
|
+
regex: /\blast month\b/i,
|
|
773
|
+
handler: (_match, now) => {
|
|
774
|
+
const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
775
|
+
const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
|
|
776
|
+
return {
|
|
777
|
+
since: startOfLastMonth.toISOString(),
|
|
778
|
+
until: endOfLastMonth.toISOString()
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
// "last week" — full previous calendar week (Mon–Sun)
|
|
783
|
+
{
|
|
784
|
+
regex: /\blast week\b/i,
|
|
785
|
+
handler: (_match, now) => {
|
|
786
|
+
const thisMonday = currentWeekMonday(now);
|
|
787
|
+
const startOfLastWeek = new Date(thisMonday.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
788
|
+
const _endOfLastWeek = new Date(thisMonday.getTime() - 1);
|
|
789
|
+
const endOfSunday = new Date(
|
|
790
|
+
startOfLastWeek.getFullYear(),
|
|
791
|
+
startOfLastWeek.getMonth(),
|
|
792
|
+
startOfLastWeek.getDate() + 6,
|
|
793
|
+
23,
|
|
794
|
+
59,
|
|
795
|
+
59,
|
|
796
|
+
999
|
|
797
|
+
);
|
|
798
|
+
return {
|
|
799
|
+
since: startOfLastWeek.toISOString(),
|
|
800
|
+
until: endOfSunday.toISOString()
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
// "this week" — current week from Monday to today (no until bound)
|
|
805
|
+
{
|
|
806
|
+
regex: /\bthis week\b/i,
|
|
807
|
+
handler: (_match, now) => {
|
|
808
|
+
const monday = currentWeekMonday(now);
|
|
809
|
+
return {
|
|
810
|
+
since: monday.toISOString()
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
// "yesterday"
|
|
815
|
+
{
|
|
816
|
+
regex: /\byesterday\b/i,
|
|
817
|
+
handler: (_match, now) => {
|
|
818
|
+
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
819
|
+
return {
|
|
820
|
+
since: startOfDay(yesterday).toISOString(),
|
|
821
|
+
until: endOfDay(yesterday).toISOString()
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
// "today"
|
|
826
|
+
{
|
|
827
|
+
regex: /\btoday\b/i,
|
|
828
|
+
handler: (_match, now) => {
|
|
829
|
+
return {
|
|
830
|
+
since: startOfDay(now).toISOString(),
|
|
831
|
+
until: endOfDay(now).toISOString()
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
// "last N weeks" / "past N weeks"
|
|
836
|
+
{
|
|
837
|
+
regex: /\b(?:last|past)\s+(\d+)\s+weeks?\b/i,
|
|
838
|
+
handler: (match, now) => {
|
|
839
|
+
const n = parseInt(match[1], 10);
|
|
840
|
+
const since = new Date(now.getFullYear(), now.getMonth(), now.getDate() - n * 7);
|
|
841
|
+
return {
|
|
842
|
+
since: startOfDay(since).toISOString()
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
// "last N days" / "past N days"
|
|
847
|
+
{
|
|
848
|
+
regex: /\b(?:last|past)\s+(\d+)\s+days?\b/i,
|
|
849
|
+
handler: (match, now) => {
|
|
850
|
+
const n = parseInt(match[1], 10);
|
|
851
|
+
const since = new Date(now.getFullYear(), now.getMonth(), now.getDate() - n);
|
|
852
|
+
return {
|
|
853
|
+
since: startOfDay(since).toISOString()
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
// "last_hour" / "past_hour"
|
|
858
|
+
{
|
|
859
|
+
regex: /\b(?:last|past)_hour\b/i,
|
|
860
|
+
handler: (_match, now) => {
|
|
861
|
+
const since = new Date(now.getTime() - 60 * 60 * 1e3);
|
|
862
|
+
return {
|
|
863
|
+
since: since.toISOString()
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
];
|
|
868
|
+
function parseRelativeDate(query) {
|
|
869
|
+
if (!query || query.trim().length === 0) {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
const now = /* @__PURE__ */ new Date();
|
|
873
|
+
for (const { regex, handler } of PATTERNS) {
|
|
874
|
+
const match = query.match(regex);
|
|
875
|
+
if (match) {
|
|
876
|
+
const cleanedQuery = query.replace(regex, "").trim().replace(/\s+/g, " ");
|
|
877
|
+
const { since, until } = handler(match, now);
|
|
878
|
+
return { cleanedQuery, since, until };
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
|
|
546
884
|
// src/mcp/tools/memory.search.ts
|
|
547
885
|
var HYBRID_WEIGHTS_VECTOR = {
|
|
548
886
|
similarity: 0.4,
|
|
@@ -551,7 +889,9 @@ var HYBRID_WEIGHTS_VECTOR = {
|
|
|
551
889
|
};
|
|
552
890
|
async function handleMemorySearch(params, db2, vectors2) {
|
|
553
891
|
const validated = MemorySearchSchema.parse(params);
|
|
554
|
-
const
|
|
892
|
+
const timeTunnel = parseRelativeDate(validated.query);
|
|
893
|
+
const effectiveQuery = timeTunnel ? timeTunnel.cleanedQuery : validated.query;
|
|
894
|
+
const searchQuery = expandQuery(effectiveQuery, validated.prompt);
|
|
555
895
|
const fetchLimit = (validated.offset + validated.limit) * 3;
|
|
556
896
|
const similarityResults = db2.memoryVectors.searchBySimilarity(
|
|
557
897
|
searchQuery,
|
|
@@ -622,6 +962,9 @@ async function handleMemorySearch(params, db2, vectors2) {
|
|
|
622
962
|
const threshold = scoredMemories.length <= 5 ? 0.1 : 0.4;
|
|
623
963
|
let allMatches = scoredMemories.filter((sm) => sm.finalScore >= threshold).map((sm) => sm.memory);
|
|
624
964
|
if (allMatches.length === 0 && scoredMemories.length > 0) allMatches = [scoredMemories[0].memory];
|
|
965
|
+
if (timeTunnel) {
|
|
966
|
+
allMatches = applyTimeFilter(allMatches, timeTunnel);
|
|
967
|
+
}
|
|
625
968
|
const total = allMatches.length;
|
|
626
969
|
const paginatedResults = allMatches.slice(validated.offset, validated.offset + validated.limit);
|
|
627
970
|
db2.memories.incrementHitCounts(paginatedResults.map((m) => m.id));
|
|
@@ -678,6 +1021,56 @@ async function handleMemorySearch(params, db2, vectors2) {
|
|
|
678
1021
|
function capitalize(str) {
|
|
679
1022
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
680
1023
|
}
|
|
1024
|
+
function applyTimeFilter(memories, tunnel) {
|
|
1025
|
+
const sinceMs = tunnel.since ? new Date(tunnel.since).getTime() : 0;
|
|
1026
|
+
const untilMs = tunnel.until ? new Date(tunnel.until).getTime() : Infinity;
|
|
1027
|
+
return memories.filter((m) => {
|
|
1028
|
+
const createdAtMs = new Date(m.created_at).getTime();
|
|
1029
|
+
if (sinceMs > 0 && createdAtMs < sinceMs) return false;
|
|
1030
|
+
if (untilMs < Infinity && createdAtMs >= untilMs) return false;
|
|
1031
|
+
return true;
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/mcp/tools/memory.acknowledge.ts
|
|
1036
|
+
async function handleMemoryAcknowledge(params, db2) {
|
|
1037
|
+
const validated = MemoryAcknowledgeSchema.parse(params);
|
|
1038
|
+
let memoryId = validated.memory_id;
|
|
1039
|
+
if (!memoryId && validated.code) {
|
|
1040
|
+
const byCode = db2.memories.getByCode(validated.code, validated.owner, validated.repo);
|
|
1041
|
+
if (!byCode) throw new Error(`Memory not found: ${validated.code}`);
|
|
1042
|
+
memoryId = byCode.id;
|
|
1043
|
+
} else if (!memoryId) {
|
|
1044
|
+
throw new Error("Either memory_id or code must be provided");
|
|
1045
|
+
}
|
|
1046
|
+
const memory = db2.memories.getById(memoryId);
|
|
1047
|
+
if (!memory) {
|
|
1048
|
+
throw new Error(`Memory with ID ${memoryId} not found.`);
|
|
1049
|
+
}
|
|
1050
|
+
if (validated.status === "used") {
|
|
1051
|
+
db2.memories.incrementRecallCount(memory.id);
|
|
1052
|
+
logger.info("[Tool] memory.acknowledge - used", { id: memory.id, context: validated.application_context });
|
|
1053
|
+
} else if (validated.status === "contradictory") {
|
|
1054
|
+
logger.warn("[Tool] memory.acknowledge - contradiction reported", {
|
|
1055
|
+
id: memory.id,
|
|
1056
|
+
context: validated.application_context
|
|
1057
|
+
});
|
|
1058
|
+
} else {
|
|
1059
|
+
logger.info("[Tool] memory.acknowledge - irrelevant", { id: memory.id, context: validated.application_context });
|
|
1060
|
+
}
|
|
1061
|
+
return createMcpResponse(
|
|
1062
|
+
{
|
|
1063
|
+
success: true,
|
|
1064
|
+
id: memory.id,
|
|
1065
|
+
status: validated.status
|
|
1066
|
+
},
|
|
1067
|
+
`Acknowledged memory [${memory.code}] as "${validated.status}".`,
|
|
1068
|
+
{
|
|
1069
|
+
structuredContentPathHint: "status",
|
|
1070
|
+
includeSerializedStructuredContent: validated.structured
|
|
1071
|
+
}
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
681
1074
|
|
|
682
1075
|
// src/mcp/tools/memory.summarize.ts
|
|
683
1076
|
async function handleMemorySummarize(params, db2) {
|
|
@@ -895,11 +1288,11 @@ async function handleTaskList(args, storage) {
|
|
|
895
1288
|
// src/mcp/tools/memory.synthesize.ts
|
|
896
1289
|
async function handleMemorySynthesize(params, db2, vectors2, options = {}) {
|
|
897
1290
|
const validated = MemorySynthesizeSchema.parse(params);
|
|
898
|
-
const
|
|
899
|
-
if (!options.sampleMessage || !
|
|
1291
|
+
const session = options.session;
|
|
1292
|
+
if (!options.sampleMessage || !session?.supportsSampling) {
|
|
900
1293
|
throw new Error("Client does not advertise MCP sampling support");
|
|
901
1294
|
}
|
|
902
|
-
const repo = await resolveRepository(validated.repo,
|
|
1295
|
+
const repo = await resolveRepository(validated.repo, session, options.elicit);
|
|
903
1296
|
if (!repo) {
|
|
904
1297
|
throw new Error("repo is required when repo cannot be inferred from active MCP roots");
|
|
905
1298
|
}
|
|
@@ -940,7 +1333,7 @@ ${contextBlock || "No additional context provided."}`
|
|
|
940
1333
|
}
|
|
941
1334
|
}
|
|
942
1335
|
];
|
|
943
|
-
const toolDefinitions = buildSamplingTools(
|
|
1336
|
+
const toolDefinitions = buildSamplingTools(session, validated.use_tools);
|
|
944
1337
|
let lastResponse = null;
|
|
945
1338
|
let totalToolCalls = 0;
|
|
946
1339
|
let iterations = 0;
|
|
@@ -1011,11 +1404,11 @@ ${contextBlock || "No additional context provided."}`
|
|
|
1011
1404
|
}
|
|
1012
1405
|
);
|
|
1013
1406
|
}
|
|
1014
|
-
async function resolveRepository(repo,
|
|
1407
|
+
async function resolveRepository(repo, session, elicit) {
|
|
1015
1408
|
if (repo) return normalizeRepo(repo);
|
|
1016
|
-
const inferredRepo = inferRepoFromSession(
|
|
1409
|
+
const inferredRepo = inferRepoFromSession(session);
|
|
1017
1410
|
if (inferredRepo) return normalizeRepo(inferredRepo);
|
|
1018
|
-
if (!
|
|
1411
|
+
if (!session?.supportsElicitationForm || !elicit) {
|
|
1019
1412
|
return void 0;
|
|
1020
1413
|
}
|
|
1021
1414
|
const elicited = extractAcceptedElicitationContent(
|
|
@@ -1038,8 +1431,8 @@ async function resolveRepository(repo, session2, elicit) {
|
|
|
1038
1431
|
);
|
|
1039
1432
|
return typeof elicited.repo === "string" && elicited.repo.trim() ? normalizeRepo(elicited.repo.trim()) : void 0;
|
|
1040
1433
|
}
|
|
1041
|
-
function buildSamplingTools(
|
|
1042
|
-
if (!useTools || !
|
|
1434
|
+
function buildSamplingTools(session, useTools) {
|
|
1435
|
+
if (!useTools || !session?.supportsSamplingTools) {
|
|
1043
1436
|
return [];
|
|
1044
1437
|
}
|
|
1045
1438
|
return [
|
|
@@ -1203,58 +1596,18 @@ async function handleMemoryDelete(params, db2, vectors2, onProgress) {
|
|
|
1203
1596
|
);
|
|
1204
1597
|
}
|
|
1205
1598
|
|
|
1206
|
-
// src/mcp/tools/memory.
|
|
1207
|
-
async function
|
|
1208
|
-
const validated =
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
throw new Error("Either memory_id or code must be provided");
|
|
1599
|
+
// src/mcp/tools/memory.detail.ts
|
|
1600
|
+
async function handleMemoryDetail(args, storage) {
|
|
1601
|
+
const validated = MemoryDetailSchema.parse(args);
|
|
1602
|
+
const { id, code, owner, repo } = validated;
|
|
1603
|
+
let memory;
|
|
1604
|
+
if (id) {
|
|
1605
|
+
memory = storage.memories.getById(id);
|
|
1606
|
+
} else if (code) {
|
|
1607
|
+
memory = storage.memories.getByCode(code, owner, repo);
|
|
1216
1608
|
}
|
|
1217
|
-
const memory = db2.memories.getById(memoryId);
|
|
1218
1609
|
if (!memory) {
|
|
1219
|
-
throw new Error(`Memory
|
|
1220
|
-
}
|
|
1221
|
-
if (validated.status === "used") {
|
|
1222
|
-
db2.memories.incrementRecallCount(memory.id);
|
|
1223
|
-
logger.info("[Tool] memory.acknowledge - used", { id: memory.id, context: validated.application_context });
|
|
1224
|
-
} else if (validated.status === "contradictory") {
|
|
1225
|
-
logger.warn("[Tool] memory.acknowledge - contradiction reported", {
|
|
1226
|
-
id: memory.id,
|
|
1227
|
-
context: validated.application_context
|
|
1228
|
-
});
|
|
1229
|
-
} else {
|
|
1230
|
-
logger.info("[Tool] memory.acknowledge - irrelevant", { id: memory.id, context: validated.application_context });
|
|
1231
|
-
}
|
|
1232
|
-
return createMcpResponse(
|
|
1233
|
-
{
|
|
1234
|
-
success: true,
|
|
1235
|
-
id: memory.id,
|
|
1236
|
-
status: validated.status
|
|
1237
|
-
},
|
|
1238
|
-
`Acknowledged memory [${memory.code}] as "${validated.status}".`,
|
|
1239
|
-
{
|
|
1240
|
-
structuredContentPathHint: "status",
|
|
1241
|
-
includeSerializedStructuredContent: validated.structured
|
|
1242
|
-
}
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// src/mcp/tools/memory.detail.ts
|
|
1247
|
-
async function handleMemoryDetail(args, storage) {
|
|
1248
|
-
const validated = MemoryDetailSchema.parse(args);
|
|
1249
|
-
const { id, code, owner, repo } = validated;
|
|
1250
|
-
let memory;
|
|
1251
|
-
if (id) {
|
|
1252
|
-
memory = storage.memories.getById(id);
|
|
1253
|
-
} else if (code) {
|
|
1254
|
-
memory = storage.memories.getByCode(code, owner, repo);
|
|
1255
|
-
}
|
|
1256
|
-
if (!memory) {
|
|
1257
|
-
throw new Error(`Memory not found: ${id || code}`);
|
|
1610
|
+
throw new Error(`Memory not found: ${id || code}`);
|
|
1258
1611
|
}
|
|
1259
1612
|
storage.memories.incrementHitCount(memory.id);
|
|
1260
1613
|
const lines = [
|
|
@@ -1278,7 +1631,7 @@ async function handleMemoryDetail(args, storage) {
|
|
|
1278
1631
|
}
|
|
1279
1632
|
|
|
1280
1633
|
// src/mcp/tools/standard.store.ts
|
|
1281
|
-
import { randomUUID as
|
|
1634
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1282
1635
|
var UUID_REGEX3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1283
1636
|
function resolveStandardParentId(value, db2, owner, repo) {
|
|
1284
1637
|
if (!value) return null;
|
|
@@ -1321,7 +1674,7 @@ async function storeSingleStandard(params, db2, vectors2) {
|
|
|
1321
1674
|
}
|
|
1322
1675
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1323
1676
|
const entry = {
|
|
1324
|
-
id:
|
|
1677
|
+
id: randomUUID3(),
|
|
1325
1678
|
code: generateNextCode(params.owner, params.repo || "__global__", "standard", db2),
|
|
1326
1679
|
title: params.name,
|
|
1327
1680
|
content: params.content,
|
|
@@ -1404,7 +1757,7 @@ async function handleStandardStore(params, db2, vectors2) {
|
|
|
1404
1757
|
const code = generateNextCode(validated.owner ?? "", standardRepo, "standard", db2, batchCodes);
|
|
1405
1758
|
batchCodes.add(code);
|
|
1406
1759
|
entries.push({
|
|
1407
|
-
id:
|
|
1760
|
+
id: randomUUID3(),
|
|
1408
1761
|
code,
|
|
1409
1762
|
title: std.name,
|
|
1410
1763
|
content: std.content,
|
|
@@ -1845,7 +2198,7 @@ async function handleStandardDelete(params, db2, vectors2) {
|
|
|
1845
2198
|
}
|
|
1846
2199
|
|
|
1847
2200
|
// src/mcp/tools/task.create.ts
|
|
1848
|
-
import { randomUUID as
|
|
2201
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1849
2202
|
|
|
1850
2203
|
// src/mcp/tools/task.helpers.ts
|
|
1851
2204
|
var UUID_REGEX5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -1964,7 +2317,7 @@ async function handleTaskCreate(args, storage) {
|
|
|
1964
2317
|
let pendingInRequestCount = 0;
|
|
1965
2318
|
const localCodeMap = /* @__PURE__ */ new Map();
|
|
1966
2319
|
for (const taskData of bulkTasks) {
|
|
1967
|
-
localCodeMap.set(taskData.task_code,
|
|
2320
|
+
localCodeMap.set(taskData.task_code, randomUUID4());
|
|
1968
2321
|
}
|
|
1969
2322
|
for (const taskData of bulkTasks) {
|
|
1970
2323
|
const code = taskData.task_code;
|
|
@@ -2067,7 +2420,7 @@ async function handleTaskCreate(args, storage) {
|
|
|
2067
2420
|
);
|
|
2068
2421
|
}
|
|
2069
2422
|
}
|
|
2070
|
-
const taskId =
|
|
2423
|
+
const taskId = randomUUID4();
|
|
2071
2424
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2072
2425
|
const statusTimestamps = deriveTaskStatusTimestamps(status || "backlog", now);
|
|
2073
2426
|
const finalTags = [...singleTask.tags || []];
|
|
@@ -2215,7 +2568,7 @@ function addRequiredStringField(properties, required, task, field, schema) {
|
|
|
2215
2568
|
}
|
|
2216
2569
|
|
|
2217
2570
|
// src/mcp/tools/task.update.ts
|
|
2218
|
-
import { randomUUID as
|
|
2571
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2219
2572
|
async function handleTaskUpdate(args, storage, vectors2) {
|
|
2220
2573
|
const updateData = TaskUpdateSchema.parse(args);
|
|
2221
2574
|
const { owner, repo, id, ids, comment, force, ...updates } = updateData;
|
|
@@ -2301,7 +2654,7 @@ async function handleTaskUpdate(args, storage, vectors2) {
|
|
|
2301
2654
|
storage.tasks.updateTask(targetId, finalUpdates);
|
|
2302
2655
|
if (comment !== void 0 || isStatusChanging) {
|
|
2303
2656
|
storage.taskComments.insertTaskComment({
|
|
2304
|
-
id:
|
|
2657
|
+
id: randomUUID5(),
|
|
2305
2658
|
task_id: targetId,
|
|
2306
2659
|
owner,
|
|
2307
2660
|
repo,
|
|
@@ -2565,284 +2918,565 @@ async function handleTaskSearch(args, storage) {
|
|
|
2565
2918
|
});
|
|
2566
2919
|
}
|
|
2567
2920
|
|
|
2568
|
-
// src/mcp/
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
async function _dispatch(method, params, signal, onProgress) {
|
|
2583
|
-
switch (method) {
|
|
2584
|
-
// ---- tools ----
|
|
2585
|
-
case "tools/list":
|
|
2586
|
-
return listTools(getSessionContext?.(), params);
|
|
2587
|
-
case "tools/call":
|
|
2588
|
-
return await handleToolCall(
|
|
2589
|
-
params,
|
|
2590
|
-
params?.signal,
|
|
2591
|
-
onProgress
|
|
2592
|
-
);
|
|
2593
|
-
// ---- resources ----
|
|
2594
|
-
case "resources/list":
|
|
2595
|
-
return listResources(getSessionContext?.(), params);
|
|
2596
|
-
case "resources/templates/list":
|
|
2597
|
-
return listResourceTemplates(params);
|
|
2598
|
-
case "resources/read": {
|
|
2599
|
-
const result = readResource(params?.uri, db2, getSessionContext?.());
|
|
2600
|
-
if (result && Array.isArray(result.contents) && !result.content) {
|
|
2601
|
-
result.content = result.contents;
|
|
2602
|
-
}
|
|
2603
|
-
return result;
|
|
2604
|
-
}
|
|
2605
|
-
// ---- prompts ----
|
|
2606
|
-
case "prompts/list":
|
|
2607
|
-
return listPrompts(db2, getSessionContext?.(), params);
|
|
2608
|
-
case "logging/setLevel": {
|
|
2609
|
-
const requestedLevel = typeof params?.level === "string" ? params.level : "";
|
|
2610
|
-
const previousLevel = getLogLevel();
|
|
2611
|
-
const level = setLogLevel(requestedLevel);
|
|
2612
|
-
return {
|
|
2613
|
-
level,
|
|
2614
|
-
supportedLevels: LOG_LEVEL_VALUES,
|
|
2615
|
-
previousLevel
|
|
2616
|
-
};
|
|
2617
|
-
}
|
|
2618
|
-
case "prompts/get": {
|
|
2619
|
-
return getPrompt(
|
|
2620
|
-
params?.name,
|
|
2621
|
-
params?.arguments || {},
|
|
2622
|
-
db2,
|
|
2623
|
-
getSessionContext?.()
|
|
2624
|
-
);
|
|
2625
|
-
}
|
|
2626
|
-
case "completion/complete":
|
|
2627
|
-
return complete(params, db2, getSessionContext?.());
|
|
2628
|
-
default:
|
|
2629
|
-
throw new Error(`Unsupported method: ${method}`);
|
|
2630
|
-
}
|
|
2631
|
-
}
|
|
2632
|
-
const WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
2633
|
-
"memory-store",
|
|
2634
|
-
"memory-acknowledge",
|
|
2635
|
-
"memory-update",
|
|
2636
|
-
"memory-delete",
|
|
2637
|
-
"memory-bulk-delete",
|
|
2638
|
-
"memory-summarize",
|
|
2639
|
-
"handoff-create",
|
|
2640
|
-
"handoff-update",
|
|
2641
|
-
"standard-store",
|
|
2642
|
-
"standard-update",
|
|
2643
|
-
"standard-delete",
|
|
2644
|
-
"task-create",
|
|
2645
|
-
"task-create-interactive",
|
|
2646
|
-
"task-claim",
|
|
2647
|
-
"claim-release",
|
|
2648
|
-
"task-update",
|
|
2649
|
-
"task-delete"
|
|
2650
|
-
]);
|
|
2651
|
-
async function handleToolCall(params, signal, onProgress) {
|
|
2652
|
-
const { name } = params || {};
|
|
2653
|
-
const args = normalizeToolArguments(params?.arguments, getSessionContext?.());
|
|
2654
|
-
const toolName = String(name).replace(/\./g, "-");
|
|
2655
|
-
let result;
|
|
2656
|
-
const repo = args?.repo || args?.scope?.repo || "unknown";
|
|
2657
|
-
const isWrite = WRITE_TOOLS.has(toolName);
|
|
2658
|
-
logger.info(`[Tool] ${toolName}`, { repo, write: isWrite });
|
|
2659
|
-
const executeToolLogic = async () => {
|
|
2660
|
-
switch (toolName) {
|
|
2661
|
-
case "memory-store":
|
|
2662
|
-
return await handleMemoryStore(args, db2, vectors2);
|
|
2663
|
-
case "memory-acknowledge":
|
|
2664
|
-
return await handleMemoryAcknowledge(args, db2);
|
|
2665
|
-
case "memory-update":
|
|
2666
|
-
return await handleMemoryUpdate(args, db2, vectors2);
|
|
2667
|
-
case "memory-recap":
|
|
2668
|
-
return await handleMemoryRecap(args, db2);
|
|
2669
|
-
case "memory-search":
|
|
2670
|
-
return await handleMemorySearch(args, db2, vectors2);
|
|
2671
|
-
case "memory-summarize":
|
|
2672
|
-
return await handleMemorySummarize(args, db2);
|
|
2673
|
-
case "memory-synthesize":
|
|
2674
|
-
return await handleMemorySynthesize(args, db2, vectors2, {
|
|
2675
|
-
session: getSessionContext?.(),
|
|
2676
|
-
sampleMessage: options?.sampleMessage,
|
|
2677
|
-
elicit: options?.elicit
|
|
2678
|
-
});
|
|
2679
|
-
case "memory-delete":
|
|
2680
|
-
case "memory-bulk-delete":
|
|
2681
|
-
return await handleMemoryDelete(args, db2, vectors2, onProgress);
|
|
2682
|
-
case "memory-detail":
|
|
2683
|
-
return await handleMemoryDetail(args, db2);
|
|
2684
|
-
case "handoff-create":
|
|
2685
|
-
return await handleHandoffCreate(args, db2);
|
|
2686
|
-
case "handoff-list":
|
|
2687
|
-
return await handleHandoffList(args, db2);
|
|
2688
|
-
case "handoff-update":
|
|
2689
|
-
return await handleHandoffUpdate(args, db2);
|
|
2690
|
-
case "task-claim":
|
|
2691
|
-
return await handleTaskClaim(args, db2);
|
|
2692
|
-
case "claim-list":
|
|
2693
|
-
return await handleClaimList(args, db2);
|
|
2694
|
-
case "claim-release":
|
|
2695
|
-
return await handleClaimRelease(args, db2);
|
|
2696
|
-
case "standard-store":
|
|
2697
|
-
return await handleStandardStore(args, db2, vectors2);
|
|
2698
|
-
case "standard-update":
|
|
2699
|
-
return await handleStandardUpdate(args, db2, vectors2);
|
|
2700
|
-
case "standard-detail":
|
|
2701
|
-
return await handleStandardDetail(args, db2);
|
|
2702
|
-
case "standard-delete":
|
|
2703
|
-
return await handleStandardDelete(args, db2, vectors2);
|
|
2704
|
-
case "standard-search":
|
|
2705
|
-
return await handleStandardSearch(args, db2, vectors2);
|
|
2706
|
-
case "task-create":
|
|
2707
|
-
return await handleTaskCreate(args, db2);
|
|
2708
|
-
case "task-create-interactive":
|
|
2709
|
-
return await handleTaskCreateInteractive(args, db2, {
|
|
2710
|
-
session: getSessionContext?.(),
|
|
2711
|
-
elicit: options?.elicit
|
|
2712
|
-
});
|
|
2713
|
-
case "task-update":
|
|
2714
|
-
return await handleTaskUpdate(args, db2, vectors2);
|
|
2715
|
-
case "task-delete":
|
|
2716
|
-
return await handleTaskDelete(args, db2);
|
|
2717
|
-
case "task-list":
|
|
2718
|
-
return await handleTaskList(args, db2);
|
|
2719
|
-
case "task-search":
|
|
2720
|
-
return await handleTaskSearch(args, db2);
|
|
2721
|
-
case "task-detail":
|
|
2722
|
-
return await handleTaskGet(args, db2);
|
|
2723
|
-
default:
|
|
2724
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
2725
|
-
}
|
|
2726
|
-
};
|
|
2727
|
-
if (isWrite) {
|
|
2728
|
-
result = await db2.withWrite(executeToolLogic);
|
|
2729
|
-
} else {
|
|
2730
|
-
result = await executeToolLogic();
|
|
2921
|
+
// src/mcp/tools/agent-context.ts
|
|
2922
|
+
var ACTIVE_TASK_STATUSES = ["in_progress", "pending", "backlog", "blocked"];
|
|
2923
|
+
async function handleAgentContext(args, db2, _vectors) {
|
|
2924
|
+
const validated = AgentContextSchema.parse(args);
|
|
2925
|
+
const { owner, repo, objective, type_filter, limit, structured: isStructuredRequest } = validated;
|
|
2926
|
+
let memories;
|
|
2927
|
+
let decisionMemories = [];
|
|
2928
|
+
const shouldFetchDecisions = !type_filter || type_filter === "decision";
|
|
2929
|
+
if (objective) {
|
|
2930
|
+
memories = db2.memories.searchByRepo(owner, repo, objective, type_filter, limit);
|
|
2931
|
+
if (shouldFetchDecisions) {
|
|
2932
|
+
decisionMemories = db2.memories.searchByRepo(owner, repo, "", "decision", limit);
|
|
2731
2933
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2934
|
+
} else {
|
|
2935
|
+
const excludeTypes = type_filter ? [] : ["decision"];
|
|
2936
|
+
memories = db2.memories.getRecentMemories(owner, repo, limit, 0, false, excludeTypes);
|
|
2937
|
+
if (shouldFetchDecisions) {
|
|
2938
|
+
decisionMemories = db2.memories.searchByRepo(owner, repo, "", "decision", limit);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
const activeTasks = db2.tasks.getTasksByMultipleStatuses(owner, repo, ACTIVE_TASK_STATUSES, 10, 0);
|
|
2942
|
+
const memoryIds = new Set(memories.map((m) => m.id));
|
|
2943
|
+
const uniqueDecisions = decisionMemories.filter((d) => !memoryIds.has(d.id));
|
|
2944
|
+
const sections = [];
|
|
2945
|
+
sections.push(`--- Active Context for ${repo} ---`);
|
|
2946
|
+
sections.push("");
|
|
2947
|
+
sections.push("== Relevant Memories ==");
|
|
2948
|
+
if (memories.length === 0) {
|
|
2949
|
+
sections.push("(No relevant memories found)");
|
|
2950
|
+
} else {
|
|
2951
|
+
for (const m of memories) {
|
|
2952
|
+
const code = m.code || "-";
|
|
2953
|
+
const snippet = m.content.length > 120 ? m.content.slice(0, 120) + "..." : m.content;
|
|
2954
|
+
sections.push(`- [${code}] (${m.type}, importance: ${m.importance}) ${m.title}`);
|
|
2955
|
+
sections.push(` ${snippet}`);
|
|
2751
2956
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2957
|
+
}
|
|
2958
|
+
sections.push("");
|
|
2959
|
+
sections.push("== Active Tasks ==");
|
|
2960
|
+
if (activeTasks.length === 0) {
|
|
2961
|
+
sections.push("(No active tasks)");
|
|
2962
|
+
} else {
|
|
2963
|
+
for (const t of activeTasks) {
|
|
2964
|
+
sections.push(`- ${t.task_code} | ${t.status} | priority: ${t.priority} | ${t.title}`);
|
|
2755
2965
|
}
|
|
2756
|
-
return result;
|
|
2757
2966
|
}
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2967
|
+
sections.push("");
|
|
2968
|
+
sections.push("== Recent Decisions ==");
|
|
2969
|
+
if (uniqueDecisions.length === 0) {
|
|
2970
|
+
sections.push("(No recent decision memories)");
|
|
2971
|
+
} else {
|
|
2972
|
+
for (const d of uniqueDecisions) {
|
|
2973
|
+
const code = d.code || "-";
|
|
2974
|
+
const snippet = d.content.length > 150 ? d.content.slice(0, 150) + "..." : d.content;
|
|
2975
|
+
sections.push(`- [${code}] (importance: ${d.importance}) ${d.title}`);
|
|
2976
|
+
sections.push(` ${snippet}`);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
sections.push("");
|
|
2980
|
+
sections.push("Use memory-detail with a memory code for full content.");
|
|
2981
|
+
const contentSummary = sections.join("\n").trim();
|
|
2982
|
+
const structuredData = {
|
|
2983
|
+
schema: "agent-context",
|
|
2984
|
+
repo,
|
|
2985
|
+
objective: objective || null,
|
|
2986
|
+
memories: memories.map((m) => ({
|
|
2987
|
+
id: m.id,
|
|
2988
|
+
code: m.code || null,
|
|
2989
|
+
title: m.title,
|
|
2990
|
+
type: m.type,
|
|
2991
|
+
importance: m.importance
|
|
2992
|
+
})),
|
|
2993
|
+
decisions: uniqueDecisions.map((d) => ({
|
|
2994
|
+
id: d.id,
|
|
2995
|
+
code: d.code || null,
|
|
2996
|
+
title: d.title,
|
|
2997
|
+
importance: d.importance
|
|
2998
|
+
})),
|
|
2999
|
+
tasks: activeTasks.map((t) => ({
|
|
3000
|
+
task_code: t.task_code,
|
|
3001
|
+
title: t.title,
|
|
3002
|
+
status: t.status,
|
|
3003
|
+
priority: t.priority
|
|
3004
|
+
}))
|
|
3005
|
+
};
|
|
3006
|
+
logger.info("[Tool] agent-context", {
|
|
3007
|
+
repo,
|
|
3008
|
+
memories: memories.length,
|
|
3009
|
+
decisions: uniqueDecisions.length,
|
|
3010
|
+
tasks: activeTasks.length
|
|
2767
3011
|
});
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
3012
|
+
return createMcpResponse(structuredData, contentSummary, {
|
|
3013
|
+
contentSummary,
|
|
3014
|
+
includeSerializedStructuredContent: isStructuredRequest
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// src/mcp/tools/decision-log.ts
|
|
3019
|
+
async function handleDecisionLog(args, db2, vectors2) {
|
|
3020
|
+
const validated = DecisionLogSchema.parse(args);
|
|
3021
|
+
const owner = validated.owner ?? "";
|
|
3022
|
+
const repo = validated.repo ?? "";
|
|
3023
|
+
if (!owner || !repo) {
|
|
3024
|
+
return createMcpResponse(
|
|
3025
|
+
{
|
|
3026
|
+
success: false,
|
|
3027
|
+
error: "MISSING_SCOPE",
|
|
3028
|
+
message: "Both owner and repo are required. They can be provided explicitly or auto-detected from the session context."
|
|
3029
|
+
},
|
|
3030
|
+
"Failed to log decision: owner and repo could not be determined."
|
|
3031
|
+
);
|
|
3032
|
+
}
|
|
3033
|
+
const alternativesStr = validated.alternatives && validated.alternatives.length > 0 ? validated.alternatives.join(", ") : "None";
|
|
3034
|
+
const formattedContent = [
|
|
3035
|
+
`Decision: ${validated.summary}`,
|
|
3036
|
+
`Context: ${validated.context}`,
|
|
3037
|
+
`Rationale: ${validated.rationale}`,
|
|
3038
|
+
`Alternatives considered: ${alternativesStr}`
|
|
3039
|
+
].join("\n");
|
|
3040
|
+
const tags = ["decision", ...validated.tags ?? []];
|
|
3041
|
+
const memoryStoreParams = {
|
|
3042
|
+
type: "decision",
|
|
3043
|
+
title: validated.summary,
|
|
3044
|
+
content: formattedContent,
|
|
3045
|
+
importance: 4,
|
|
3046
|
+
agent: "agent",
|
|
3047
|
+
role: "unknown",
|
|
3048
|
+
model: "unknown",
|
|
3049
|
+
scope: {
|
|
3050
|
+
owner,
|
|
3051
|
+
repo
|
|
3052
|
+
},
|
|
3053
|
+
tags,
|
|
3054
|
+
structured: validated.structured
|
|
2773
3055
|
};
|
|
3056
|
+
return handleMemoryStore(memoryStoreParams, db2, vectors2);
|
|
2774
3057
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
3058
|
+
|
|
3059
|
+
// src/mcp/tools/session-summarize.ts
|
|
3060
|
+
async function handleSessionSummarize(args, db2, vectors2) {
|
|
3061
|
+
const validated = SessionSummarizeSchema.parse(args);
|
|
3062
|
+
const owner = validated.owner ?? "";
|
|
3063
|
+
const repo = validated.repo ?? "";
|
|
3064
|
+
if (!owner || !repo) {
|
|
3065
|
+
return createMcpResponse(
|
|
3066
|
+
{
|
|
3067
|
+
success: false,
|
|
3068
|
+
error: "MISSING_SCOPE",
|
|
3069
|
+
message: "Both owner and repo are required. They can be provided explicitly or auto-detected from the session context."
|
|
3070
|
+
},
|
|
3071
|
+
"Failed to log session summary: owner and repo could not be determined."
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
const decisionsStr = validated.key_decisions && validated.key_decisions.length > 0 ? validated.key_decisions.map((d) => `- ${d}`).join("\n") : "None";
|
|
3075
|
+
const nextStepsStr = validated.next_steps && validated.next_steps.length > 0 ? validated.next_steps.map((s) => `- ${s}`).join("\n") : "None";
|
|
3076
|
+
const formattedContent = [
|
|
3077
|
+
`Session Summary: ${validated.summary}`,
|
|
3078
|
+
``,
|
|
3079
|
+
`Key Decisions:`,
|
|
3080
|
+
decisionsStr,
|
|
3081
|
+
``,
|
|
3082
|
+
`Next Steps:`,
|
|
3083
|
+
nextStepsStr
|
|
3084
|
+
].join("\n");
|
|
3085
|
+
const tags = ["session-summary", ...validated.tags ?? []];
|
|
3086
|
+
const memoryStoreParams = {
|
|
3087
|
+
type: "task_archive",
|
|
3088
|
+
title: validated.summary.length > 100 ? validated.summary.slice(0, 97) + "..." : validated.summary,
|
|
3089
|
+
content: formattedContent,
|
|
3090
|
+
importance: 3,
|
|
3091
|
+
agent: "agent",
|
|
3092
|
+
role: "unknown",
|
|
3093
|
+
model: "unknown",
|
|
3094
|
+
scope: {
|
|
3095
|
+
owner,
|
|
3096
|
+
repo
|
|
3097
|
+
},
|
|
3098
|
+
tags,
|
|
3099
|
+
structured: validated.structured
|
|
3100
|
+
};
|
|
3101
|
+
return handleMemoryStore(memoryStoreParams, db2, vectors2);
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
// src/mcp/tools/kg.crud.ts
|
|
3105
|
+
async function handleCreateEntity(params, db2, _vectors) {
|
|
3106
|
+
const validated = CreateEntitySchema.parse(params);
|
|
3107
|
+
const { name, type, description, owner, repo, structured } = validated;
|
|
3108
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3109
|
+
try {
|
|
3110
|
+
const txn = db2.db.transaction(() => {
|
|
3111
|
+
db2.db.prepare(
|
|
3112
|
+
`INSERT INTO entities (name, type, description, repo, owner, created_at, updated_at)
|
|
3113
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
3114
|
+
).run(name, type, description ?? null, repo ?? "", owner ?? "", now, now);
|
|
3115
|
+
});
|
|
3116
|
+
txn();
|
|
3117
|
+
} catch (error) {
|
|
3118
|
+
const err = error;
|
|
3119
|
+
if (err.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
3120
|
+
const response = createMcpResponse(
|
|
3121
|
+
{
|
|
3122
|
+
success: false,
|
|
3123
|
+
error: "ENTITY_ALREADY_EXISTS",
|
|
3124
|
+
message: `Entity '${name}' already exists in this repo.`
|
|
3125
|
+
},
|
|
3126
|
+
`Entity '${name}' already exists.`,
|
|
3127
|
+
{ includeSerializedStructuredContent: structured }
|
|
3128
|
+
);
|
|
3129
|
+
response.isError = true;
|
|
3130
|
+
return response;
|
|
2779
3131
|
}
|
|
2780
|
-
|
|
2781
|
-
|
|
3132
|
+
logger.error("[Tool] create_entity failed", { error: String(error) });
|
|
3133
|
+
throw error;
|
|
3134
|
+
}
|
|
3135
|
+
logger.info("[Tool] create_entity", { repo: repo ?? "", entity: name });
|
|
3136
|
+
return createMcpResponse(
|
|
3137
|
+
{
|
|
3138
|
+
success: true,
|
|
3139
|
+
entity: { name, type, description, repo: repo ?? "", owner: owner ?? "" }
|
|
3140
|
+
},
|
|
3141
|
+
`Created entity '${name}' (${type}) in repo "${repo ?? ""}".`,
|
|
3142
|
+
{
|
|
3143
|
+
structuredContentPathHint: "entity",
|
|
3144
|
+
includeSerializedStructuredContent: structured
|
|
2782
3145
|
}
|
|
2783
|
-
|
|
2784
|
-
});
|
|
3146
|
+
);
|
|
2785
3147
|
}
|
|
2786
|
-
function
|
|
2787
|
-
const
|
|
2788
|
-
const
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
3148
|
+
async function handleDeleteEntity(params, db2, _vectors) {
|
|
3149
|
+
const validated = DeleteEntitySchema.parse(params);
|
|
3150
|
+
const { name, owner: _owner, repo, structured } = validated;
|
|
3151
|
+
let changes = 0;
|
|
3152
|
+
try {
|
|
3153
|
+
const txn = db2.db.transaction(() => {
|
|
3154
|
+
const result = db2.db.prepare(`DELETE FROM entities WHERE name = ?`).run(name);
|
|
3155
|
+
changes = result.changes;
|
|
3156
|
+
});
|
|
3157
|
+
txn();
|
|
3158
|
+
} catch (error) {
|
|
3159
|
+
const err = error;
|
|
3160
|
+
logger.error("[Tool] delete_entity failed", { error: String(error) });
|
|
3161
|
+
if (err.code === "SQLITE_CONSTRAINT_FOREIGNKEY" || err.code === "SQLITE_CONSTRAINT") {
|
|
3162
|
+
const response2 = createMcpResponse(
|
|
3163
|
+
{
|
|
3164
|
+
success: false,
|
|
3165
|
+
error: "ENTITY_IN_USE",
|
|
3166
|
+
message: `Entity '${name}' is referenced by existing relations and cannot be deleted.`
|
|
3167
|
+
},
|
|
3168
|
+
`Entity '${name}' is in use and cannot be deleted.`,
|
|
3169
|
+
{ includeSerializedStructuredContent: structured }
|
|
3170
|
+
);
|
|
3171
|
+
response2.isError = true;
|
|
3172
|
+
return response2;
|
|
3173
|
+
}
|
|
3174
|
+
const response = createMcpResponse(
|
|
3175
|
+
{
|
|
3176
|
+
success: false,
|
|
3177
|
+
error: "DELETE_FAILED",
|
|
3178
|
+
message: `Failed to delete entity '${name}'.`
|
|
3179
|
+
},
|
|
3180
|
+
`Failed to delete entity '${name}'.`,
|
|
3181
|
+
{ includeSerializedStructuredContent: structured }
|
|
3182
|
+
);
|
|
3183
|
+
response.isError = true;
|
|
3184
|
+
return response;
|
|
2794
3185
|
}
|
|
2795
|
-
|
|
2796
|
-
|
|
3186
|
+
logger.info("[Tool] delete_entity", { repo: repo ?? "", entity: name, changes });
|
|
3187
|
+
if (changes === 0) {
|
|
3188
|
+
return createMcpResponse(
|
|
3189
|
+
{
|
|
3190
|
+
success: false,
|
|
3191
|
+
error: "ENTITY_NOT_FOUND",
|
|
3192
|
+
message: `Entity '${name}' not found.`
|
|
3193
|
+
},
|
|
3194
|
+
`Entity '${name}' not found.`,
|
|
3195
|
+
{ includeSerializedStructuredContent: structured }
|
|
3196
|
+
);
|
|
2797
3197
|
}
|
|
2798
|
-
|
|
2799
|
-
|
|
3198
|
+
return createMcpResponse(
|
|
3199
|
+
{
|
|
3200
|
+
success: true,
|
|
3201
|
+
deletedCount: changes
|
|
3202
|
+
},
|
|
3203
|
+
`Deleted entity '${name}' and its cascade (relations, observations).`,
|
|
3204
|
+
{
|
|
3205
|
+
structuredContentPathHint: "deletedCount",
|
|
3206
|
+
includeSerializedStructuredContent: structured
|
|
3207
|
+
}
|
|
3208
|
+
);
|
|
3209
|
+
}
|
|
3210
|
+
async function handleCreateRelation(params, db2, _vectors) {
|
|
3211
|
+
const validated = CreateRelationSchema.parse(params);
|
|
3212
|
+
const { from_entity, to_entity, relation_type, owner, repo, structured } = validated;
|
|
3213
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3214
|
+
try {
|
|
3215
|
+
const txn = db2.db.transaction(() => {
|
|
3216
|
+
const fromExists = db2.db.prepare(`SELECT 1 FROM entities WHERE name = ?`).get(from_entity);
|
|
3217
|
+
if (!fromExists) {
|
|
3218
|
+
throw new Error("FROM_ENTITY_NOT_FOUND");
|
|
3219
|
+
}
|
|
3220
|
+
const toExists = db2.db.prepare(`SELECT 1 FROM entities WHERE name = ?`).get(to_entity);
|
|
3221
|
+
if (!toExists) {
|
|
3222
|
+
throw new Error("TO_ENTITY_NOT_FOUND");
|
|
3223
|
+
}
|
|
3224
|
+
db2.db.prepare(
|
|
3225
|
+
`INSERT INTO relations (from_entity, to_entity, relation_type, repo, owner, created_at)
|
|
3226
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
3227
|
+
).run(from_entity, to_entity, relation_type, repo ?? "", owner ?? "", now);
|
|
3228
|
+
});
|
|
3229
|
+
txn();
|
|
3230
|
+
} catch (error) {
|
|
3231
|
+
const err = error;
|
|
3232
|
+
if (err.message === "FROM_ENTITY_NOT_FOUND") {
|
|
3233
|
+
const response = createMcpResponse(
|
|
3234
|
+
{
|
|
3235
|
+
success: false,
|
|
3236
|
+
error: "FROM_ENTITY_NOT_FOUND",
|
|
3237
|
+
message: `Source entity '${from_entity}' not found. Create it first.`
|
|
3238
|
+
},
|
|
3239
|
+
`Source entity '${from_entity}' not found.`,
|
|
3240
|
+
{ includeSerializedStructuredContent: structured }
|
|
3241
|
+
);
|
|
3242
|
+
response.isError = true;
|
|
3243
|
+
return response;
|
|
3244
|
+
}
|
|
3245
|
+
if (err.message === "TO_ENTITY_NOT_FOUND") {
|
|
3246
|
+
const response = createMcpResponse(
|
|
3247
|
+
{
|
|
3248
|
+
success: false,
|
|
3249
|
+
error: "TO_ENTITY_NOT_FOUND",
|
|
3250
|
+
message: `Target entity '${to_entity}' not found. Create it first.`
|
|
3251
|
+
},
|
|
3252
|
+
`Target entity '${to_entity}' not found.`,
|
|
3253
|
+
{ includeSerializedStructuredContent: structured }
|
|
3254
|
+
);
|
|
3255
|
+
response.isError = true;
|
|
3256
|
+
return response;
|
|
3257
|
+
}
|
|
3258
|
+
if (err.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
3259
|
+
const response = createMcpResponse(
|
|
3260
|
+
{
|
|
3261
|
+
success: false,
|
|
3262
|
+
error: "RELATION_ALREADY_EXISTS",
|
|
3263
|
+
message: `Relation '${from_entity} \u2192[${relation_type}]\u2192 ${to_entity}' already exists.`
|
|
3264
|
+
},
|
|
3265
|
+
`Relation already exists.`,
|
|
3266
|
+
{ includeSerializedStructuredContent: structured }
|
|
3267
|
+
);
|
|
3268
|
+
response.isError = true;
|
|
3269
|
+
return response;
|
|
3270
|
+
}
|
|
3271
|
+
if (err.code === "SQLITE_CONSTRAINT_FOREIGNKEY" || err.code === "SQLITE_CONSTRAINT") {
|
|
3272
|
+
const response = createMcpResponse(
|
|
3273
|
+
{
|
|
3274
|
+
success: false,
|
|
3275
|
+
error: "ENTITY_NOT_FOUND",
|
|
3276
|
+
message: `Entity not found \u2014 cannot create relation.`
|
|
3277
|
+
},
|
|
3278
|
+
`Entity not found \u2014 cannot create relation.`,
|
|
3279
|
+
{ includeSerializedStructuredContent: structured }
|
|
3280
|
+
);
|
|
3281
|
+
response.isError = true;
|
|
3282
|
+
return response;
|
|
3283
|
+
}
|
|
3284
|
+
logger.error("[Tool] create_relation failed", { error: String(error) });
|
|
3285
|
+
throw error;
|
|
3286
|
+
}
|
|
3287
|
+
logger.info("[Tool] create_relation", {
|
|
3288
|
+
repo: repo ?? "",
|
|
3289
|
+
from: from_entity,
|
|
3290
|
+
to: to_entity,
|
|
3291
|
+
type: relation_type
|
|
3292
|
+
});
|
|
3293
|
+
return createMcpResponse(
|
|
3294
|
+
{
|
|
3295
|
+
success: true,
|
|
3296
|
+
relation: { from_entity, to_entity, relation_type }
|
|
3297
|
+
},
|
|
3298
|
+
`Created relation '${from_entity} \u2014[${relation_type}]\u2192 ${to_entity}' in repo "${repo ?? ""}".`,
|
|
3299
|
+
{
|
|
3300
|
+
structuredContentPathHint: "relation",
|
|
3301
|
+
includeSerializedStructuredContent: structured
|
|
3302
|
+
}
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
async function handleDeleteRelation(params, db2, _vectors) {
|
|
3306
|
+
const validated = DeleteRelationSchema.parse(params);
|
|
3307
|
+
const { from_entity, to_entity, relation_type, owner: _owner, repo, structured } = validated;
|
|
3308
|
+
let changes = 0;
|
|
3309
|
+
try {
|
|
3310
|
+
const txn = db2.db.transaction(() => {
|
|
3311
|
+
const result = db2.db.prepare(`DELETE FROM relations WHERE from_entity = ? AND to_entity = ? AND relation_type = ?`).run(from_entity, to_entity, relation_type);
|
|
3312
|
+
changes = result.changes;
|
|
3313
|
+
});
|
|
3314
|
+
txn();
|
|
3315
|
+
} catch (error) {
|
|
3316
|
+
logger.error("[Tool] delete_relation failed", { error: String(error) });
|
|
3317
|
+
const response = createMcpResponse(
|
|
3318
|
+
{
|
|
3319
|
+
success: false,
|
|
3320
|
+
error: "DELETE_FAILED",
|
|
3321
|
+
message: `Failed to delete relation '${from_entity} \u2192[${relation_type}]\u2192 ${to_entity}'.`
|
|
3322
|
+
},
|
|
3323
|
+
`Failed to delete relation.`,
|
|
3324
|
+
{ includeSerializedStructuredContent: structured }
|
|
3325
|
+
);
|
|
3326
|
+
response.isError = true;
|
|
3327
|
+
return response;
|
|
3328
|
+
}
|
|
3329
|
+
logger.info("[Tool] delete_relation", {
|
|
3330
|
+
repo: repo ?? "",
|
|
3331
|
+
from: from_entity,
|
|
3332
|
+
to: to_entity,
|
|
3333
|
+
type: relation_type,
|
|
3334
|
+
changes
|
|
3335
|
+
});
|
|
3336
|
+
if (changes === 0) {
|
|
3337
|
+
return createMcpResponse(
|
|
3338
|
+
{
|
|
3339
|
+
success: false,
|
|
3340
|
+
error: "RELATION_NOT_FOUND",
|
|
3341
|
+
message: `Relation '${from_entity} \u2192[${relation_type}]\u2192 ${to_entity}' not found.`
|
|
3342
|
+
},
|
|
3343
|
+
`Relation not found.`,
|
|
3344
|
+
{ includeSerializedStructuredContent: structured }
|
|
3345
|
+
);
|
|
2800
3346
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
3347
|
+
return createMcpResponse(
|
|
3348
|
+
{
|
|
3349
|
+
success: true,
|
|
3350
|
+
deletedCount: changes
|
|
3351
|
+
},
|
|
3352
|
+
`Deleted relation '${from_entity} \u2014[${relation_type}]\u2192 ${to_entity}'.`,
|
|
3353
|
+
{
|
|
3354
|
+
structuredContentPathHint: "deletedCount",
|
|
3355
|
+
includeSerializedStructuredContent: structured
|
|
3356
|
+
}
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
async function handleDeleteObservation(params, db2, _vectors) {
|
|
3360
|
+
const validated = DeleteObservationSchema.parse(params);
|
|
3361
|
+
const { id, owner: _owner, repo, structured } = validated;
|
|
3362
|
+
let changes = 0;
|
|
3363
|
+
try {
|
|
3364
|
+
const txn = db2.db.transaction(() => {
|
|
3365
|
+
const result = db2.db.prepare(`DELETE FROM observations WHERE id = ?`).run(id);
|
|
3366
|
+
changes = result.changes;
|
|
3367
|
+
});
|
|
3368
|
+
txn();
|
|
3369
|
+
} catch (error) {
|
|
3370
|
+
logger.error("[Tool] delete_observation failed", { error: String(error) });
|
|
3371
|
+
const response = createMcpResponse(
|
|
3372
|
+
{
|
|
3373
|
+
success: false,
|
|
3374
|
+
error: "DELETE_FAILED",
|
|
3375
|
+
message: `Failed to delete observation '${id}'.`
|
|
3376
|
+
},
|
|
3377
|
+
`Failed to delete observation '${id}'.`,
|
|
3378
|
+
{ includeSerializedStructuredContent: structured }
|
|
3379
|
+
);
|
|
3380
|
+
response.isError = true;
|
|
3381
|
+
return response;
|
|
2804
3382
|
}
|
|
2805
|
-
|
|
2806
|
-
if (
|
|
2807
|
-
|
|
3383
|
+
logger.info("[Tool] delete_observation", { repo: repo ?? "", id, changes });
|
|
3384
|
+
if (changes === 0) {
|
|
3385
|
+
return createMcpResponse(
|
|
3386
|
+
{
|
|
3387
|
+
success: false,
|
|
3388
|
+
error: "OBSERVATION_NOT_FOUND",
|
|
3389
|
+
message: `Observation '${id}' not found.`
|
|
3390
|
+
},
|
|
3391
|
+
`Observation '${id}' not found.`,
|
|
3392
|
+
{ includeSerializedStructuredContent: structured }
|
|
3393
|
+
);
|
|
2808
3394
|
}
|
|
2809
|
-
return
|
|
3395
|
+
return createMcpResponse(
|
|
3396
|
+
{
|
|
3397
|
+
success: true,
|
|
3398
|
+
deletedCount: changes
|
|
3399
|
+
},
|
|
3400
|
+
`Deleted observation '${id}'.`,
|
|
3401
|
+
{
|
|
3402
|
+
structuredContentPathHint: "deletedCount",
|
|
3403
|
+
includeSerializedStructuredContent: structured
|
|
3404
|
+
}
|
|
3405
|
+
);
|
|
2810
3406
|
}
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
3407
|
+
|
|
3408
|
+
// src/mcp/tools/index.ts
|
|
3409
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
3410
|
+
"memory-store",
|
|
3411
|
+
"memory-acknowledge",
|
|
3412
|
+
"memory-update",
|
|
3413
|
+
"memory-delete",
|
|
3414
|
+
"memory-bulk-delete",
|
|
3415
|
+
"memory-summarize",
|
|
3416
|
+
"handoff-create",
|
|
3417
|
+
"handoff-update",
|
|
3418
|
+
"standard-store",
|
|
3419
|
+
"standard-update",
|
|
3420
|
+
"standard-delete",
|
|
3421
|
+
"task-create",
|
|
3422
|
+
"task-create-interactive",
|
|
3423
|
+
"task-claim",
|
|
3424
|
+
"claim-release",
|
|
3425
|
+
"task-update",
|
|
3426
|
+
"task-delete",
|
|
3427
|
+
"decision-log",
|
|
3428
|
+
"session-summarize",
|
|
3429
|
+
// Upstream alias tools (write)
|
|
3430
|
+
"remember_fact",
|
|
3431
|
+
"remember_facts",
|
|
3432
|
+
"forget",
|
|
3433
|
+
// Knowledge graph tools (write)
|
|
3434
|
+
"create_entity",
|
|
3435
|
+
"delete_entity",
|
|
3436
|
+
"create_relation",
|
|
3437
|
+
"delete_relation",
|
|
3438
|
+
"delete_observation"
|
|
3439
|
+
]);
|
|
3440
|
+
function validateRootBoundPath(value, field, session) {
|
|
3441
|
+
if (typeof value !== "string" || !path2.isAbsolute(value)) {
|
|
3442
|
+
return;
|
|
2814
3443
|
}
|
|
3444
|
+
if (!isPathWithinRoots(value, session)) {
|
|
3445
|
+
throw new Error(`${field} must stay within the active MCP roots`);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
function normalizeToolArgs(args, session) {
|
|
2815
3449
|
const anyArgs = args;
|
|
2816
3450
|
const nextArgs = {
|
|
2817
3451
|
...anyArgs,
|
|
2818
3452
|
scope: anyArgs.scope ? { ...anyArgs.scope } : void 0
|
|
2819
3453
|
};
|
|
2820
|
-
validateRootBoundPath(nextArgs.current_file_path, "current_file_path",
|
|
2821
|
-
validateRootBoundPath(nextArgs.doc_path, "doc_path",
|
|
3454
|
+
validateRootBoundPath(nextArgs.current_file_path, "current_file_path", session);
|
|
3455
|
+
validateRootBoundPath(nextArgs.doc_path, "doc_path", session);
|
|
2822
3456
|
if (!nextArgs.repo) {
|
|
2823
|
-
nextArgs.repo = inferRepoFromSession(
|
|
3457
|
+
nextArgs.repo = inferRepoFromSession(session);
|
|
2824
3458
|
}
|
|
2825
3459
|
const scope = nextArgs.scope;
|
|
2826
3460
|
if (scope && !scope.repo) {
|
|
2827
|
-
scope.repo = nextArgs.repo ?? inferRepoFromSession(
|
|
3461
|
+
scope.repo = nextArgs.repo ?? inferRepoFromSession(session);
|
|
2828
3462
|
}
|
|
2829
3463
|
if (!nextArgs.owner) {
|
|
2830
3464
|
const repoVal2 = nextArgs.repo || "";
|
|
2831
3465
|
const parsed = parseRepoInput(repoVal2, void 0);
|
|
2832
|
-
nextArgs.owner = parsed.owner || inferOwnerFromSession(
|
|
3466
|
+
nextArgs.owner = parsed.owner || inferOwnerFromSession(session) || "";
|
|
2833
3467
|
if (nextArgs.owner && !repoVal2.includes("/")) {
|
|
2834
3468
|
logger.warn(
|
|
2835
|
-
`[
|
|
3469
|
+
`[tools] owner inferred from session (${nextArgs.owner}) \u2014 may be incorrect. Agents should pass explicit owner/repo.`
|
|
2836
3470
|
);
|
|
2837
3471
|
}
|
|
2838
3472
|
}
|
|
2839
3473
|
if (scope && !scope.owner) {
|
|
2840
3474
|
const repoVal2 = scope.repo || nextArgs.repo || "";
|
|
2841
3475
|
const parsed = parseRepoInput(repoVal2, void 0);
|
|
2842
|
-
scope.owner = parsed.owner || nextArgs.owner || inferOwnerFromSession(
|
|
3476
|
+
scope.owner = parsed.owner || nextArgs.owner || inferOwnerFromSession(session) || "";
|
|
2843
3477
|
}
|
|
2844
|
-
const ownerVal = nextArgs.owner || inferOwnerFromSession(
|
|
2845
|
-
const repoVal = nextArgs.repo || inferRepoFromSession(
|
|
3478
|
+
const ownerVal = nextArgs.owner || inferOwnerFromSession(session) || "";
|
|
3479
|
+
const repoVal = nextArgs.repo || inferRepoFromSession(session) || "";
|
|
2846
3480
|
const memories = nextArgs.memories;
|
|
2847
3481
|
if (memories) {
|
|
2848
3482
|
for (const mem of memories) {
|
|
@@ -2855,7 +3489,7 @@ function normalizeToolArguments(args, session2) {
|
|
|
2855
3489
|
}
|
|
2856
3490
|
}
|
|
2857
3491
|
if (typeof nextArgs.current_file_path === "string" && scope) {
|
|
2858
|
-
const containingRoot = path2.isAbsolute(nextArgs.current_file_path) ? findContainingRoot(nextArgs.current_file_path,
|
|
3492
|
+
const containingRoot = path2.isAbsolute(nextArgs.current_file_path) ? findContainingRoot(nextArgs.current_file_path, session) : null;
|
|
2859
3493
|
if (containingRoot) {
|
|
2860
3494
|
const relativePath = path2.relative(containingRoot, path2.resolve(nextArgs.current_file_path));
|
|
2861
3495
|
const relativeFolder = path2.dirname(relativePath);
|
|
@@ -2866,24 +3500,835 @@ function normalizeToolArguments(args, session2) {
|
|
|
2866
3500
|
}
|
|
2867
3501
|
return nextArgs;
|
|
2868
3502
|
}
|
|
2869
|
-
function
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
if (
|
|
2879
|
-
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3503
|
+
function collectAffectedResourceUris(toolName, args, result) {
|
|
3504
|
+
const res = result;
|
|
3505
|
+
const repo = args?.repo || args?.scope?.repo || res?.data?.repo;
|
|
3506
|
+
const uris = /* @__PURE__ */ new Set();
|
|
3507
|
+
const touchesMemory = toolName.startsWith("memory-") || toolName === "task-update" || toolName === "task-delete";
|
|
3508
|
+
const touchesTasks = toolName.startsWith("task-");
|
|
3509
|
+
if (touchesMemory && repo) {
|
|
3510
|
+
uris.add(`repository://${encodeURIComponent(repo)}/memories`);
|
|
3511
|
+
}
|
|
3512
|
+
if (touchesTasks && repo) {
|
|
3513
|
+
uris.add(`repository://${encodeURIComponent(repo)}/tasks`);
|
|
3514
|
+
}
|
|
3515
|
+
if (repo) {
|
|
3516
|
+
uris.add("repository://index");
|
|
3517
|
+
}
|
|
3518
|
+
const memoryId = args?.id || args?.memory_id || res?.data?.id;
|
|
3519
|
+
if (typeof memoryId === "string" && /^[0-9a-f-]{36}$/i.test(memoryId) && toolName.startsWith("memory-")) {
|
|
3520
|
+
uris.add(`memory://${memoryId}`);
|
|
3521
|
+
}
|
|
3522
|
+
const taskId = args?.id || args?.task_id || res?.structuredData?.id;
|
|
3523
|
+
if (typeof taskId === "string" && /^[0-9a-f-]{36}$/i.test(taskId) && toolName.startsWith("task-")) {
|
|
3524
|
+
uris.add(`task://${taskId}`);
|
|
3525
|
+
}
|
|
3526
|
+
return [...uris];
|
|
3527
|
+
}
|
|
3528
|
+
function logToolAction(toolName, args, result, db2, isWrite) {
|
|
3529
|
+
try {
|
|
3530
|
+
const actionType = toolName.split("-")[1] || toolName;
|
|
3531
|
+
const res = result;
|
|
3532
|
+
const sc = res?.structuredData;
|
|
3533
|
+
const repo = args?.repo || args?.scope?.repo || "unknown";
|
|
3534
|
+
const logOptions = {
|
|
3535
|
+
query: args?.query || args?.title || args?.task_code || (toolName === "memory-recap" ? `Offset: ${args?.offset || 0}` : void 0),
|
|
3536
|
+
response: res,
|
|
3537
|
+
memoryId: args?.id || args?.memory_id || sc?.id,
|
|
3538
|
+
taskId: args?.id || args?.task_id || sc?.id,
|
|
3539
|
+
resultCount: Array.isArray(sc?.results) ? sc.results.length : sc?.count || 0
|
|
3540
|
+
};
|
|
3541
|
+
if (isWrite) {
|
|
3542
|
+
db2.actions.logAction(actionType, "", repo, logOptions);
|
|
3543
|
+
} else {
|
|
3544
|
+
void db2.withWrite(() => {
|
|
3545
|
+
db2.actions.logAction(actionType, "", repo, logOptions);
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3548
|
+
} catch (e) {
|
|
3549
|
+
logger.error("Failed to log action", { toolName, error: String(e) });
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
function makePublicSchema() {
|
|
3553
|
+
return z.object({}).catchall(z.unknown());
|
|
3554
|
+
}
|
|
3555
|
+
function toCallToolResult(response) {
|
|
3556
|
+
const content = Array.isArray(response.content) ? response.content.map((item) => {
|
|
3557
|
+
if (item.type === "image") {
|
|
3558
|
+
return { type: "image", data: item.data, mimeType: item.mimeType };
|
|
3559
|
+
}
|
|
3560
|
+
if (item.type === "resource") {
|
|
3561
|
+
return {
|
|
3562
|
+
type: "text",
|
|
3563
|
+
text: item.resource.text ?? JSON.stringify(item.resource)
|
|
3564
|
+
};
|
|
3565
|
+
}
|
|
3566
|
+
return { type: "text", text: item.text ?? "" };
|
|
3567
|
+
}) : [];
|
|
3568
|
+
return {
|
|
3569
|
+
content,
|
|
3570
|
+
isError: response.isError ?? false,
|
|
3571
|
+
...response.structuredContent !== void 0 ? { structuredContent: response.structuredContent } : {}
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
function buildExecutors(session, options) {
|
|
3575
|
+
const sampleMessage = options?.sampleMessage;
|
|
3576
|
+
const elicit = options?.elicit;
|
|
3577
|
+
return {
|
|
3578
|
+
"memory-store": (args, db2, vectors2, _extra) => handleMemoryStore(args, db2, vectors2),
|
|
3579
|
+
"memory-acknowledge": (args, db2, _vectors, _extra) => handleMemoryAcknowledge(args, db2),
|
|
3580
|
+
"memory-update": (args, db2, vectors2, _extra) => handleMemoryUpdate(args, db2, vectors2),
|
|
3581
|
+
"memory-recap": (args, db2, _vectors, _extra) => handleMemoryRecap(args, db2),
|
|
3582
|
+
"memory-search": (args, db2, vectors2, _extra) => handleMemorySearch(args, db2, vectors2),
|
|
3583
|
+
"memory-summarize": (args, db2, _vectors, _extra) => handleMemorySummarize(args, db2),
|
|
3584
|
+
"memory-synthesize": (args, db2, vectors2, _extra) => handleMemorySynthesize(args, db2, vectors2, {
|
|
3585
|
+
session,
|
|
3586
|
+
sampleMessage,
|
|
3587
|
+
elicit
|
|
3588
|
+
}),
|
|
3589
|
+
"memory-delete": (args, db2, vectors2, extra) => handleMemoryDelete(args, db2, vectors2, extra?.onProgress),
|
|
3590
|
+
"memory-detail": (args, db2, _vectors, _extra) => handleMemoryDetail(args, db2),
|
|
3591
|
+
"handoff-create": (args, db2, _vectors, _extra) => handleHandoffCreate(args, db2),
|
|
3592
|
+
"handoff-list": (args, db2, _vectors, _extra) => handleHandoffList(args, db2),
|
|
3593
|
+
"handoff-update": (args, db2, _vectors, _extra) => handleHandoffUpdate(args, db2),
|
|
3594
|
+
"task-claim": (args, db2, _vectors, _extra) => handleTaskClaim(args, db2),
|
|
3595
|
+
"claim-list": (args, db2, _vectors, _extra) => handleClaimList(args, db2),
|
|
3596
|
+
"claim-release": (args, db2, _vectors, _extra) => handleClaimRelease(args, db2),
|
|
3597
|
+
"standard-store": (args, db2, vectors2, _extra) => handleStandardStore(args, db2, vectors2),
|
|
3598
|
+
"standard-update": (args, db2, vectors2, _extra) => handleStandardUpdate(args, db2, vectors2),
|
|
3599
|
+
"standard-detail": (args, db2, _vectors, _extra) => handleStandardDetail(args, db2),
|
|
3600
|
+
"standard-delete": (args, db2, vectors2, _extra) => handleStandardDelete(args, db2, vectors2),
|
|
3601
|
+
"standard-search": (args, db2, vectors2, _extra) => handleStandardSearch(args, db2, vectors2),
|
|
3602
|
+
"task-create": (args, db2, _vectors, _extra) => handleTaskCreate(args, db2),
|
|
3603
|
+
"task-create-interactive": (args, db2, _vectors, _extra) => handleTaskCreateInteractive(args, db2, { session, elicit }),
|
|
3604
|
+
"task-update": (args, db2, vectors2, _extra) => handleTaskUpdate(args, db2, vectors2),
|
|
3605
|
+
"task-delete": (args, db2, _vectors, _extra) => handleTaskDelete(args, db2),
|
|
3606
|
+
"task-list": (args, db2, _vectors, _extra) => handleTaskList(args, db2),
|
|
3607
|
+
"task-search": (args, db2, _vectors, _extra) => handleTaskSearch(args, db2),
|
|
3608
|
+
"task-detail": (args, db2, _vectors, _extra) => handleTaskGet(args, db2),
|
|
3609
|
+
"agent-context": (args, db2, vectors2, _extra) => handleAgentContext(args, db2, vectors2),
|
|
3610
|
+
"decision-log": (args, db2, vectors2, _extra) => handleDecisionLog(args, db2, vectors2),
|
|
3611
|
+
"session-summarize": (args, db2, vectors2, _extra) => handleSessionSummarize(args, db2, vectors2),
|
|
3612
|
+
// Upstream alias tools
|
|
3613
|
+
remember_fact: (args, db2, vectors2, _extra) => handleMemoryStore(args, db2, vectors2),
|
|
3614
|
+
remember_facts: (args, db2, vectors2, _extra) => handleMemoryStore(args, db2, vectors2),
|
|
3615
|
+
recall: (args, db2, vectors2, _extra) => handleMemorySearch(args, db2, vectors2),
|
|
3616
|
+
forget: (args, db2, vectors2, _extra) => handleMemoryDelete(args, db2, vectors2),
|
|
3617
|
+
// Knowledge graph tools
|
|
3618
|
+
create_entity: (args, db2, _vectors, _extra) => handleCreateEntity(args, db2, _vectors),
|
|
3619
|
+
delete_entity: (args, db2, _vectors, _extra) => handleDeleteEntity(args, db2, _vectors),
|
|
3620
|
+
create_relation: (args, db2, _vectors, _extra) => handleCreateRelation(args, db2, _vectors),
|
|
3621
|
+
delete_relation: (args, db2, _vectors, _extra) => handleDeleteRelation(args, db2, _vectors),
|
|
3622
|
+
delete_observation: (args, db2, _vectors, _extra) => handleDeleteObservation(args, db2, _vectors)
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
function registerAllTools(server, store, vectors2, session, options) {
|
|
3626
|
+
const executors = buildExecutors(session, options);
|
|
3627
|
+
const definitions = TOOL_DEFINITIONS.filter((def) => {
|
|
3628
|
+
if (def.name === "memory-synthesize" && !session.supportsSampling) {
|
|
3629
|
+
return false;
|
|
3630
|
+
}
|
|
3631
|
+
if (def.name === "task-create-interactive" && !session.supportsElicitationForm) {
|
|
3632
|
+
return false;
|
|
3633
|
+
}
|
|
3634
|
+
return true;
|
|
3635
|
+
});
|
|
3636
|
+
const publicSchema = makePublicSchema();
|
|
3637
|
+
for (const def of definitions) {
|
|
3638
|
+
const toolName = def.name;
|
|
3639
|
+
const executor = executors[toolName];
|
|
3640
|
+
if (!executor) {
|
|
3641
|
+
logger.warn(`[registerAllTools] No executor for tool: ${toolName} \u2014 skipping`);
|
|
3642
|
+
continue;
|
|
3643
|
+
}
|
|
3644
|
+
const isWrite = WRITE_TOOLS.has(toolName);
|
|
3645
|
+
server.registerTool(
|
|
3646
|
+
toolName,
|
|
3647
|
+
{
|
|
3648
|
+
description: def.description ?? "",
|
|
3649
|
+
inputSchema: publicSchema
|
|
3650
|
+
},
|
|
3651
|
+
async (args, extra) => {
|
|
3652
|
+
const rawArgs = args ?? {};
|
|
3653
|
+
const normalizedArgs = normalizeToolArgs(rawArgs, session);
|
|
3654
|
+
logger.info(`[Tool] ${toolName}`, {
|
|
3655
|
+
repo: normalizedArgs?.repo || "unknown",
|
|
3656
|
+
write: isWrite
|
|
3657
|
+
});
|
|
3658
|
+
const progressToken = extra?.mcpReq?._meta?.progressToken;
|
|
3659
|
+
const executorExtra = {
|
|
3660
|
+
onProgress: progressToken !== void 0 ? (progress, total) => {
|
|
3661
|
+
void extra.mcpReq.notify({
|
|
3662
|
+
method: "notifications/progress",
|
|
3663
|
+
params: { progressToken, progress, total }
|
|
3664
|
+
}).catch(() => {
|
|
3665
|
+
});
|
|
3666
|
+
} : void 0,
|
|
3667
|
+
signal: extra?.mcpReq?.signal
|
|
3668
|
+
};
|
|
3669
|
+
const executeFn = () => executor(normalizedArgs, store, vectors2, executorExtra);
|
|
3670
|
+
let result;
|
|
3671
|
+
try {
|
|
3672
|
+
if (isWrite) {
|
|
3673
|
+
result = await store.withWrite(executeFn);
|
|
3674
|
+
} else {
|
|
3675
|
+
result = await executeFn();
|
|
3676
|
+
}
|
|
3677
|
+
} catch (err) {
|
|
3678
|
+
logger.error(`[Tool] ${toolName} failed`, { error: String(err) });
|
|
3679
|
+
return {
|
|
3680
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
3681
|
+
isError: true
|
|
3682
|
+
};
|
|
3683
|
+
}
|
|
3684
|
+
logger.info(`[Tool] ${toolName} result`, {
|
|
3685
|
+
repo: normalizedArgs?.repo || "unknown"
|
|
3686
|
+
});
|
|
3687
|
+
logToolAction(toolName, normalizedArgs, result, store, isWrite);
|
|
3688
|
+
const affectedUris = collectAffectedResourceUris(toolName, normalizedArgs, result);
|
|
3689
|
+
if (affectedUris.length > 0) {
|
|
3690
|
+
options?.onResourcesMutated?.(affectedUris);
|
|
3691
|
+
}
|
|
3692
|
+
return toCallToolResult(result);
|
|
3693
|
+
}
|
|
3694
|
+
);
|
|
3695
|
+
logger.debug(`[registerAllTools] Registered tool: ${toolName}`);
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
// src/mcp/resources/sdk-index.ts
|
|
3700
|
+
import { ResourceTemplate } from "@modelcontextprotocol/server";
|
|
3701
|
+
function registerAllResources(server, store, _vectors, session) {
|
|
3702
|
+
const db2 = store;
|
|
3703
|
+
function getRepos() {
|
|
3704
|
+
const values = /* @__PURE__ */ new Set();
|
|
3705
|
+
for (const repo of db2.system.listRepos()) {
|
|
3706
|
+
values.add(repo);
|
|
3707
|
+
}
|
|
3708
|
+
if (session.roots.length > 0) {
|
|
3709
|
+
for (const root of session.roots) {
|
|
3710
|
+
const name = root.name || root.uri.split("/").filter(Boolean).pop() || "";
|
|
3711
|
+
if (name) values.add(name);
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
return [...values].sort((a, b) => a.localeCompare(b));
|
|
3715
|
+
}
|
|
3716
|
+
function getTags() {
|
|
3717
|
+
const values = /* @__PURE__ */ new Set();
|
|
3718
|
+
const memories = db2.memories.getRecentMemories("", "", 1e3);
|
|
3719
|
+
for (const memory of memories) {
|
|
3720
|
+
for (const tag of memory.tags || []) {
|
|
3721
|
+
if (typeof tag === "string" && tag.trim()) {
|
|
3722
|
+
values.add(tag.trim());
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
return [...values].sort((a, b) => a.localeCompare(b));
|
|
3727
|
+
}
|
|
3728
|
+
function _deriveLastModified(values) {
|
|
3729
|
+
const normalized = values.filter((v) => typeof v === "string" && v.length > 0);
|
|
3730
|
+
return normalized.sort().at(-1) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3731
|
+
}
|
|
3732
|
+
server.registerResource(
|
|
3733
|
+
"repository-index",
|
|
3734
|
+
"repository://index",
|
|
3735
|
+
{
|
|
3736
|
+
title: "Repository Index",
|
|
3737
|
+
description: "List of all known repositories with memory/task counts and last activity",
|
|
3738
|
+
mimeType: "application/json"
|
|
3739
|
+
},
|
|
3740
|
+
async (uri, _extra) => {
|
|
3741
|
+
const repos2 = db2.system.listRepoNavigation();
|
|
3742
|
+
const payload = JSON.stringify(repos2, null, 2);
|
|
3743
|
+
return {
|
|
3744
|
+
contents: [
|
|
3745
|
+
{
|
|
3746
|
+
uri: uri.toString(),
|
|
3747
|
+
mimeType: "application/json",
|
|
3748
|
+
text: payload
|
|
3749
|
+
}
|
|
3750
|
+
]
|
|
3751
|
+
};
|
|
3752
|
+
}
|
|
3753
|
+
);
|
|
3754
|
+
server.registerResource(
|
|
3755
|
+
"session-roots",
|
|
3756
|
+
"session://roots",
|
|
3757
|
+
{
|
|
3758
|
+
title: "Session Roots",
|
|
3759
|
+
description: session?.roots?.length ? "Active workspace roots provided by the MCP client" : "No active workspace roots were provided by the MCP client",
|
|
3760
|
+
mimeType: "application/json"
|
|
3761
|
+
},
|
|
3762
|
+
async (uri, _extra) => {
|
|
3763
|
+
const payload = JSON.stringify({ roots: session?.roots ?? [] }, null, 2);
|
|
3764
|
+
return {
|
|
3765
|
+
contents: [
|
|
3766
|
+
{
|
|
3767
|
+
uri: uri.toString(),
|
|
3768
|
+
mimeType: "application/json",
|
|
3769
|
+
text: payload
|
|
3770
|
+
}
|
|
3771
|
+
]
|
|
3772
|
+
};
|
|
3773
|
+
}
|
|
3774
|
+
);
|
|
3775
|
+
const repos = getRepos();
|
|
3776
|
+
const tags = getTags();
|
|
3777
|
+
server.registerResource(
|
|
3778
|
+
"repository-memories",
|
|
3779
|
+
new ResourceTemplate("repository://{name}/memories{?search,type,tag,limit,offset}", {
|
|
3780
|
+
list: void 0,
|
|
3781
|
+
complete: {
|
|
3782
|
+
name: async (value) => rankCompletionValues(repos, value),
|
|
3783
|
+
tag: async (value) => rankCompletionValues(tags, value)
|
|
3784
|
+
}
|
|
3785
|
+
}),
|
|
3786
|
+
{
|
|
3787
|
+
title: "Repository Memories",
|
|
3788
|
+
description: "All active memory entries for a specific repository, optionally filtered by search, type, or tag",
|
|
3789
|
+
mimeType: "application/json"
|
|
3790
|
+
},
|
|
3791
|
+
async (uri, variables, _extra) => {
|
|
3792
|
+
const name = variables.name;
|
|
3793
|
+
const search = variables.search || "";
|
|
3794
|
+
const type = variables.type;
|
|
3795
|
+
const tag = variables.tag;
|
|
3796
|
+
const result = db2.memories.listMemoriesForDashboard({
|
|
3797
|
+
repo: name,
|
|
3798
|
+
type: type || void 0,
|
|
3799
|
+
tag: tag || void 0,
|
|
3800
|
+
search: search || void 0,
|
|
3801
|
+
limit: 50
|
|
3802
|
+
});
|
|
3803
|
+
const entries = result.items;
|
|
3804
|
+
const payload = JSON.stringify(entries, null, 2);
|
|
3805
|
+
return {
|
|
3806
|
+
contents: [
|
|
3807
|
+
{
|
|
3808
|
+
uri: uri.toString(),
|
|
3809
|
+
mimeType: "application/json",
|
|
3810
|
+
text: payload
|
|
3811
|
+
}
|
|
3812
|
+
]
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
);
|
|
3816
|
+
server.registerResource(
|
|
3817
|
+
"memory-detail",
|
|
3818
|
+
new ResourceTemplate("memory://{id}", { list: void 0 }),
|
|
3819
|
+
{
|
|
3820
|
+
title: "Memory Detail",
|
|
3821
|
+
description: "Full content and statistics for a specific memory UUID",
|
|
3822
|
+
mimeType: "application/json"
|
|
3823
|
+
},
|
|
3824
|
+
async (uri, variables, _extra) => {
|
|
3825
|
+
const id = variables.id;
|
|
3826
|
+
const entry = db2.memories.getByIdWithStats(id);
|
|
3827
|
+
if (!entry) {
|
|
3828
|
+
throw new Error(`Memory with ID ${id} not found.`);
|
|
3829
|
+
}
|
|
3830
|
+
const payload = JSON.stringify(entry, null, 2);
|
|
3831
|
+
return {
|
|
3832
|
+
contents: [
|
|
3833
|
+
{
|
|
3834
|
+
uri: uri.toString(),
|
|
3835
|
+
mimeType: "application/json",
|
|
3836
|
+
text: payload
|
|
3837
|
+
}
|
|
3838
|
+
]
|
|
3839
|
+
};
|
|
3840
|
+
}
|
|
3841
|
+
);
|
|
3842
|
+
server.registerResource(
|
|
3843
|
+
"repository-tasks",
|
|
3844
|
+
new ResourceTemplate("repository://{name}/tasks{?status,priority,limit,offset}", {
|
|
3845
|
+
list: void 0,
|
|
3846
|
+
complete: {
|
|
3847
|
+
name: async (value) => rankCompletionValues(repos, value)
|
|
3848
|
+
}
|
|
3849
|
+
}),
|
|
3850
|
+
{
|
|
3851
|
+
title: "Repository Tasks",
|
|
3852
|
+
description: "All active tasks for a specific repository, optionally filtered by status or priority",
|
|
3853
|
+
mimeType: "application/json"
|
|
3854
|
+
},
|
|
3855
|
+
async (uri, variables, _extra) => {
|
|
3856
|
+
const name = variables.name;
|
|
3857
|
+
const status = variables.status;
|
|
3858
|
+
const priority = variables.priority;
|
|
3859
|
+
const owner = parseRepoInput(name).owner;
|
|
3860
|
+
let tasks;
|
|
3861
|
+
if (status && status !== "all") {
|
|
3862
|
+
const statuses = status.split(",").map((s) => s.trim());
|
|
3863
|
+
tasks = db2.tasks.getTasksByMultipleStatuses(owner, name, statuses);
|
|
3864
|
+
} else {
|
|
3865
|
+
tasks = db2.tasks.getTasksByMultipleStatuses(owner, name, ["backlog", "pending", "in_progress", "blocked"]);
|
|
3866
|
+
}
|
|
3867
|
+
if (priority) {
|
|
3868
|
+
const p = Number(priority);
|
|
3869
|
+
if (!isNaN(p)) {
|
|
3870
|
+
tasks = tasks.filter((t) => t.priority === p);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
const payload = JSON.stringify(tasks, null, 2);
|
|
3874
|
+
return {
|
|
3875
|
+
contents: [
|
|
3876
|
+
{
|
|
3877
|
+
uri: uri.toString(),
|
|
3878
|
+
mimeType: "application/json",
|
|
3879
|
+
text: payload
|
|
3880
|
+
}
|
|
3881
|
+
]
|
|
3882
|
+
};
|
|
3883
|
+
}
|
|
3884
|
+
);
|
|
3885
|
+
server.registerResource(
|
|
3886
|
+
"task-detail",
|
|
3887
|
+
new ResourceTemplate("task://{id}", { list: void 0 }),
|
|
3888
|
+
{
|
|
3889
|
+
title: "Task Detail",
|
|
3890
|
+
description: "Full content and comments for a specific task UUID",
|
|
3891
|
+
mimeType: "application/json"
|
|
3892
|
+
},
|
|
3893
|
+
async (uri, variables, _extra) => {
|
|
3894
|
+
const id = variables.id;
|
|
3895
|
+
const task = db2.tasks.getTaskById(id);
|
|
3896
|
+
if (!task) {
|
|
3897
|
+
throw new Error(`Task with ID ${id} not found.`);
|
|
3898
|
+
}
|
|
3899
|
+
const payload = JSON.stringify(task, null, 2);
|
|
3900
|
+
return {
|
|
3901
|
+
contents: [
|
|
3902
|
+
{
|
|
3903
|
+
uri: uri.toString(),
|
|
3904
|
+
mimeType: "application/json",
|
|
3905
|
+
text: payload
|
|
3906
|
+
}
|
|
3907
|
+
]
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
);
|
|
3911
|
+
server.registerResource(
|
|
3912
|
+
"repository-summary",
|
|
3913
|
+
new ResourceTemplate("repository://{name}/summary", {
|
|
3914
|
+
list: void 0,
|
|
3915
|
+
complete: {
|
|
3916
|
+
name: async (value) => rankCompletionValues(repos, value)
|
|
3917
|
+
}
|
|
3918
|
+
}),
|
|
3919
|
+
{
|
|
3920
|
+
title: "Repository Summary",
|
|
3921
|
+
description: "High-level architectural summary for a repository",
|
|
3922
|
+
mimeType: "text/plain"
|
|
3923
|
+
},
|
|
3924
|
+
async (uri, variables, _extra) => {
|
|
3925
|
+
const name = variables.name;
|
|
3926
|
+
const summary = db2.summaries.getSummary("", name);
|
|
3927
|
+
const text = summary?.summary || `No summary available for repository: ${name}`;
|
|
3928
|
+
return {
|
|
3929
|
+
contents: [
|
|
3930
|
+
{
|
|
3931
|
+
uri: uri.toString(),
|
|
3932
|
+
mimeType: "text/plain",
|
|
3933
|
+
text
|
|
3934
|
+
}
|
|
3935
|
+
]
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
);
|
|
3939
|
+
server.registerResource(
|
|
3940
|
+
"repository-actions",
|
|
3941
|
+
new ResourceTemplate("repository://{name}/actions{?limit,offset}", {
|
|
3942
|
+
list: void 0,
|
|
3943
|
+
complete: {
|
|
3944
|
+
name: async (value) => rankCompletionValues(repos, value)
|
|
3945
|
+
}
|
|
3946
|
+
}),
|
|
3947
|
+
{
|
|
3948
|
+
title: "Repository Actions",
|
|
3949
|
+
description: "Audit log of agent tool actions scoped to a repository",
|
|
3950
|
+
mimeType: "application/json"
|
|
3951
|
+
},
|
|
3952
|
+
async (uri, variables, _extra) => {
|
|
3953
|
+
const name = variables.name;
|
|
3954
|
+
const actions = db2.actions.getRecentActions("", name, 100);
|
|
3955
|
+
const payload = JSON.stringify(actions, null, 2);
|
|
3956
|
+
return {
|
|
3957
|
+
contents: [
|
|
3958
|
+
{
|
|
3959
|
+
uri: uri.toString(),
|
|
3960
|
+
mimeType: "application/json",
|
|
3961
|
+
text: payload
|
|
3962
|
+
}
|
|
3963
|
+
]
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
);
|
|
3967
|
+
server.registerResource(
|
|
3968
|
+
"action-detail",
|
|
3969
|
+
new ResourceTemplate("action://{id}", { list: void 0 }),
|
|
3970
|
+
{
|
|
3971
|
+
title: "Action Detail",
|
|
3972
|
+
description: "Full details of a specific audit log entry by integer ID",
|
|
3973
|
+
mimeType: "application/json"
|
|
3974
|
+
},
|
|
3975
|
+
async (uri, variables, _extra) => {
|
|
3976
|
+
const idStr = variables.id;
|
|
3977
|
+
const id = Number(idStr);
|
|
3978
|
+
const action = db2.actions.getActionById(id);
|
|
3979
|
+
if (!action) {
|
|
3980
|
+
throw new Error(`Action with ID ${id} not found.`);
|
|
3981
|
+
}
|
|
3982
|
+
const payload = JSON.stringify(action, null, 2);
|
|
3983
|
+
return {
|
|
3984
|
+
contents: [
|
|
3985
|
+
{
|
|
3986
|
+
uri: uri.toString(),
|
|
3987
|
+
mimeType: "application/json",
|
|
3988
|
+
text: payload
|
|
3989
|
+
}
|
|
3990
|
+
]
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3993
|
+
);
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
// src/mcp/prompts/sdk-index.ts
|
|
3997
|
+
function registerAllPrompts(server, store, _vectors, session) {
|
|
3998
|
+
const _db = store;
|
|
3999
|
+
const promptNames = listPromptFiles();
|
|
4000
|
+
for (const name of promptNames) {
|
|
4001
|
+
let loaded;
|
|
4002
|
+
try {
|
|
4003
|
+
loaded = loadPromptFromMarkdown(name);
|
|
4004
|
+
} catch (e) {
|
|
4005
|
+
logger.warn(`[prompts] Failed to load prompt ${name}: ${e}`);
|
|
4006
|
+
continue;
|
|
4007
|
+
}
|
|
4008
|
+
server.registerPrompt(
|
|
4009
|
+
loaded.name,
|
|
4010
|
+
{
|
|
4011
|
+
title: loaded.name,
|
|
4012
|
+
description: loaded.description
|
|
4013
|
+
},
|
|
4014
|
+
async (_args, _extra) => {
|
|
4015
|
+
const inferredRepo = inferRepoFromSession(session);
|
|
4016
|
+
const inferredOwner = inferOwnerFromSession(session);
|
|
4017
|
+
let text = loaded.content;
|
|
4018
|
+
text = text.replace(/\{\{current_repo\}\}/g, inferredRepo || "unknown-repo");
|
|
4019
|
+
text = text.replace(/\{\{current_owner\}\}/g, inferredOwner || "unknown-owner");
|
|
4020
|
+
return {
|
|
4021
|
+
description: loaded.description,
|
|
4022
|
+
messages: [
|
|
4023
|
+
{
|
|
4024
|
+
role: "user",
|
|
4025
|
+
content: {
|
|
4026
|
+
type: "text",
|
|
4027
|
+
text
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
],
|
|
4031
|
+
...loaded.agent ? { _meta: { agent: loaded.agent } } : {}
|
|
4032
|
+
};
|
|
4033
|
+
}
|
|
4034
|
+
);
|
|
4035
|
+
logger.debug(`[prompts] Registered prompt: ${loaded.name}`);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
// src/mcp/completion.ts
|
|
4040
|
+
import fs from "fs";
|
|
4041
|
+
import path3 from "path";
|
|
4042
|
+
var MAX_COMPLETION_VALUES = 100;
|
|
4043
|
+
var MAX_FILE_SCAN_RESULTS = 300;
|
|
4044
|
+
async function complete(params, db2, session) {
|
|
4045
|
+
const refType = params?.ref?.type;
|
|
4046
|
+
const argumentName = typeof params?.argument?.name === "string" ? params.argument.name : "";
|
|
4047
|
+
const argumentValue = typeof params?.argument?.value === "string" ? params.argument.value : "";
|
|
4048
|
+
const contextArguments = params?.context?.arguments ?? {};
|
|
4049
|
+
if (!refType || !argumentName) {
|
|
4050
|
+
throw invalidCompletionParams("completion/complete requires ref.type and argument.name");
|
|
4051
|
+
}
|
|
4052
|
+
const dataSources = {
|
|
4053
|
+
repos: getSuggestedRepos(db2, session),
|
|
4054
|
+
tags: getSuggestedTags(db2),
|
|
4055
|
+
filePaths: getSuggestedFilePaths(session),
|
|
4056
|
+
tasks: getSuggestedTasks(db2, session, contextArguments)
|
|
4057
|
+
};
|
|
4058
|
+
if (refType === "ref/prompt") {
|
|
4059
|
+
const promptName = typeof params?.ref?.name === "string" ? params.ref.name : "";
|
|
4060
|
+
if (!promptName) {
|
|
4061
|
+
throw invalidCompletionParams("Prompt completion requires ref.name");
|
|
4062
|
+
}
|
|
4063
|
+
return {
|
|
4064
|
+
completion: buildCompletionResult(
|
|
4065
|
+
await completePromptArgument(promptName, argumentName, argumentValue, contextArguments, dataSources)
|
|
4066
|
+
)
|
|
4067
|
+
};
|
|
4068
|
+
}
|
|
4069
|
+
if (refType === "ref/resource") {
|
|
4070
|
+
const resourceUri = typeof params?.ref?.uri === "string" ? params.ref.uri : "";
|
|
4071
|
+
if (!resourceUri) {
|
|
4072
|
+
throw invalidCompletionParams("Resource completion requires ref.uri");
|
|
4073
|
+
}
|
|
4074
|
+
return {
|
|
4075
|
+
completion: buildCompletionResult(
|
|
4076
|
+
completeResourceArgument(resourceUri, argumentName, argumentValue, contextArguments, dataSources)
|
|
4077
|
+
)
|
|
4078
|
+
};
|
|
4079
|
+
}
|
|
4080
|
+
throw invalidCompletionParams(`Unsupported completion ref type: ${refType}`);
|
|
4081
|
+
}
|
|
4082
|
+
function buildCompletionResult(values) {
|
|
4083
|
+
const capped = values.slice(0, MAX_COMPLETION_VALUES);
|
|
4084
|
+
return {
|
|
4085
|
+
values: capped,
|
|
4086
|
+
total: values.length,
|
|
4087
|
+
hasMore: values.length > capped.length
|
|
4088
|
+
};
|
|
4089
|
+
}
|
|
4090
|
+
function getSuggestedRepos(db2, session) {
|
|
4091
|
+
const values = /* @__PURE__ */ new Set();
|
|
4092
|
+
const inferredRepo = inferRepoFromSession(session);
|
|
4093
|
+
if (inferredRepo) values.add(inferredRepo);
|
|
4094
|
+
for (const rootPath of getFilesystemRoots(session)) {
|
|
4095
|
+
values.add(path3.basename(rootPath));
|
|
4096
|
+
}
|
|
4097
|
+
for (const repo of db2.system.listRepos()) {
|
|
4098
|
+
values.add(repo);
|
|
4099
|
+
}
|
|
4100
|
+
return [...values].sort((a, b) => a.localeCompare(b));
|
|
4101
|
+
}
|
|
4102
|
+
function getSuggestedTags(db2) {
|
|
4103
|
+
const values = /* @__PURE__ */ new Set();
|
|
4104
|
+
const memories = db2.memories.getRecentMemories("", "", 1e3);
|
|
4105
|
+
for (const memory of memories) {
|
|
4106
|
+
for (const tag of memory.tags || []) {
|
|
4107
|
+
if (typeof tag === "string" && tag.trim()) {
|
|
4108
|
+
values.add(tag.trim());
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
return [...values].sort((a, b) => a.localeCompare(b));
|
|
4113
|
+
}
|
|
4114
|
+
function getSuggestedTasks(db2, session, contextArguments) {
|
|
4115
|
+
const repo = typeof contextArguments.repo === "string" && contextArguments.repo.trim() ? contextArguments.repo.trim() : inferRepoFromSession(session);
|
|
4116
|
+
if (!repo) return [];
|
|
4117
|
+
return db2.tasks.getTasksByRepo("", repo, void 0, 100).map((task) => ({
|
|
4118
|
+
id: task.id,
|
|
4119
|
+
task_code: task.task_code,
|
|
4120
|
+
title: task.title
|
|
4121
|
+
}));
|
|
4122
|
+
}
|
|
4123
|
+
function getSuggestedFilePaths(session) {
|
|
4124
|
+
const roots = getFilesystemRoots(session);
|
|
4125
|
+
const results = [];
|
|
4126
|
+
for (const rootPath of roots) {
|
|
4127
|
+
collectFiles(rootPath, rootPath, results);
|
|
4128
|
+
if (results.length >= MAX_FILE_SCAN_RESULTS) break;
|
|
4129
|
+
}
|
|
4130
|
+
return results.sort((a, b) => a.localeCompare(b));
|
|
4131
|
+
}
|
|
4132
|
+
function collectFiles(rootPath, currentPath, results) {
|
|
4133
|
+
if (results.length >= MAX_FILE_SCAN_RESULTS) return;
|
|
4134
|
+
let entries;
|
|
4135
|
+
try {
|
|
4136
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
4137
|
+
} catch {
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
for (const entry of entries) {
|
|
4141
|
+
if (results.length >= MAX_FILE_SCAN_RESULTS) return;
|
|
4142
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
|
|
4143
|
+
const fullPath = path3.join(currentPath, entry.name);
|
|
4144
|
+
if (entry.isDirectory()) {
|
|
4145
|
+
collectFiles(rootPath, fullPath, results);
|
|
4146
|
+
continue;
|
|
4147
|
+
}
|
|
4148
|
+
if (!entry.isFile()) continue;
|
|
4149
|
+
results.push(path3.relative(rootPath, fullPath) || entry.name);
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
function invalidCompletionParams(message) {
|
|
4153
|
+
const error = new Error(message);
|
|
4154
|
+
error.code = -32602;
|
|
4155
|
+
return error;
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
// src/mcp/mcp-server.ts
|
|
4159
|
+
function createMcpServer(store, vectors2, session) {
|
|
4160
|
+
const instructions = loadServerInstructions();
|
|
4161
|
+
const ctx = session ?? createSessionContext();
|
|
4162
|
+
const server = new McpServer2(
|
|
4163
|
+
{
|
|
4164
|
+
name: "local-memory-mcp",
|
|
4165
|
+
version: CAPABILITIES.serverInfo.version
|
|
4166
|
+
},
|
|
4167
|
+
{
|
|
4168
|
+
instructions,
|
|
4169
|
+
capabilities: CAPABILITIES.capabilities
|
|
4170
|
+
}
|
|
4171
|
+
);
|
|
4172
|
+
registerAllTools(server, store, vectors2, ctx);
|
|
4173
|
+
registerAllResources(server, store, vectors2, ctx);
|
|
4174
|
+
registerAllPrompts(server, store, vectors2, ctx);
|
|
4175
|
+
const _removeLogSink = addLogSink((payload) => {
|
|
4176
|
+
void server.sendLoggingMessage({
|
|
4177
|
+
level: payload.level,
|
|
4178
|
+
data: payload.data,
|
|
4179
|
+
logger: payload.logger
|
|
4180
|
+
}).catch(() => {
|
|
4181
|
+
});
|
|
4182
|
+
});
|
|
4183
|
+
server.server.setRequestHandler("completion/complete", async (request, _serverCtx) => {
|
|
4184
|
+
const params = request.params;
|
|
4185
|
+
const completionResult = await complete(
|
|
4186
|
+
{
|
|
4187
|
+
ref: {
|
|
4188
|
+
type: params.ref.type,
|
|
4189
|
+
name: params.ref.name,
|
|
4190
|
+
uri: params.ref.uri
|
|
4191
|
+
},
|
|
4192
|
+
argument: {
|
|
4193
|
+
name: params.argument.name,
|
|
4194
|
+
value: params.argument.value
|
|
4195
|
+
},
|
|
4196
|
+
context: {
|
|
4197
|
+
arguments: params.context?.arguments
|
|
4198
|
+
}
|
|
4199
|
+
},
|
|
4200
|
+
store,
|
|
4201
|
+
ctx
|
|
4202
|
+
);
|
|
4203
|
+
return {
|
|
4204
|
+
completion: completionResult.completion
|
|
4205
|
+
};
|
|
4206
|
+
});
|
|
4207
|
+
return server;
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
// src/mcp/services/soul-maintenance.ts
|
|
4211
|
+
function applyDecay(db2, options) {
|
|
4212
|
+
const { immunizedTags = [], decayAfterDays = 7, decayRate = 0.5, archiveThreshold = 1 } = options ?? {};
|
|
4213
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4214
|
+
const cutoff = new Date(Date.now() - decayAfterDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
4215
|
+
const rows = db2.prepare(
|
|
4216
|
+
`SELECT id, importance, tags FROM memories
|
|
4217
|
+
WHERE status = 'active'
|
|
4218
|
+
AND (last_used_at IS NULL OR last_used_at < ?)`
|
|
4219
|
+
).all(cutoff);
|
|
4220
|
+
if (rows.length === 0) {
|
|
4221
|
+
logger.debug("[SoulMaintenance] No memories eligible for decay");
|
|
4222
|
+
return { decayed: 0, archived: 0, immunizedSkipped: 0 };
|
|
4223
|
+
}
|
|
4224
|
+
let decayed = 0;
|
|
4225
|
+
let archivedCount = 0;
|
|
4226
|
+
let immunizedSkipped = 0;
|
|
4227
|
+
const toDecay = [];
|
|
4228
|
+
const toArchive = [];
|
|
4229
|
+
for (const row of rows) {
|
|
4230
|
+
let tags = [];
|
|
4231
|
+
if (row.tags) {
|
|
4232
|
+
try {
|
|
4233
|
+
tags = JSON.parse(row.tags);
|
|
4234
|
+
} catch {
|
|
4235
|
+
tags = [];
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
if (immunizedTags.length > 0 && tags.some((t) => immunizedTags.includes(t))) {
|
|
4239
|
+
immunizedSkipped++;
|
|
4240
|
+
continue;
|
|
4241
|
+
}
|
|
4242
|
+
const newImportance = Math.max(1, Math.floor(row.importance - decayRate));
|
|
4243
|
+
toDecay.push({ id: row.id, newImportance });
|
|
4244
|
+
if (newImportance < archiveThreshold) {
|
|
4245
|
+
toArchive.push(row.id);
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
if (toDecay.length > 0) {
|
|
4249
|
+
const updateStmt = db2.prepare("UPDATE memories SET importance = ?, updated_at = ? WHERE id = ?");
|
|
4250
|
+
for (const item of toDecay) {
|
|
4251
|
+
updateStmt.run(item.newImportance, now, item.id);
|
|
4252
|
+
}
|
|
4253
|
+
decayed = toDecay.length;
|
|
4254
|
+
}
|
|
4255
|
+
if (toArchive.length > 0) {
|
|
4256
|
+
const placeholders = toArchive.map(() => "?").join(",");
|
|
4257
|
+
const archiveResult = db2.prepare(`UPDATE memories SET status = 'archived', updated_at = ? WHERE id IN (${placeholders})`).run(now, ...toArchive);
|
|
4258
|
+
archivedCount = archiveResult.changes;
|
|
4259
|
+
}
|
|
4260
|
+
if (decayed > 0 || archivedCount > 0 || immunizedSkipped > 0) {
|
|
4261
|
+
logger.info("[SoulMaintenance] Decay cycle complete", {
|
|
4262
|
+
decayed,
|
|
4263
|
+
archived: archivedCount,
|
|
4264
|
+
immunizedSkipped
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
return { decayed, archived: archivedCount, immunizedSkipped };
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
// src/mcp/services/maintenance-job.ts
|
|
4271
|
+
var MAINTENANCE_OWNER = "__soul__";
|
|
4272
|
+
var MAINTENANCE_REPO = "__maintenance__";
|
|
4273
|
+
var MAINTENANCE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
4274
|
+
function wasMaintenanceRunRecent(db2) {
|
|
4275
|
+
try {
|
|
4276
|
+
const row = db2.db.prepare("SELECT updated_at FROM memory_summary WHERE owner = ? AND repo = ?").get(MAINTENANCE_OWNER, MAINTENANCE_REPO);
|
|
4277
|
+
if (!row?.updated_at) return false;
|
|
4278
|
+
const lastRun = new Date(row.updated_at).getTime();
|
|
4279
|
+
return Date.now() - lastRun < MAINTENANCE_INTERVAL_MS;
|
|
4280
|
+
} catch (err) {
|
|
4281
|
+
logger.warn("[MaintenanceJob] Failed to check last maintenance time", { error: String(err) });
|
|
4282
|
+
return false;
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
function recordMaintenanceRun(db2) {
|
|
4286
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4287
|
+
try {
|
|
4288
|
+
db2.db.prepare(
|
|
4289
|
+
`INSERT INTO memory_summary (owner, repo, summary, updated_at)
|
|
4290
|
+
VALUES (?, ?, ?, ?)
|
|
4291
|
+
ON CONFLICT(owner, repo) DO UPDATE SET summary = excluded.summary, updated_at = excluded.updated_at`
|
|
4292
|
+
).run(MAINTENANCE_OWNER, MAINTENANCE_REPO, now, now);
|
|
4293
|
+
} catch (err) {
|
|
4294
|
+
logger.warn("[MaintenanceJob] Failed to record maintenance run time", { error: String(err) });
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
async function runStartupMaintenance(db2, decayOptions) {
|
|
4298
|
+
if (wasMaintenanceRunRecent(db2)) {
|
|
4299
|
+
logger.info("[MaintenanceJob] Skipping \u2014 maintenance already ran within the last 24 hours");
|
|
4300
|
+
return {
|
|
4301
|
+
decay: { decayed: 0, archived: 0, immunizedSkipped: 0 },
|
|
4302
|
+
expiredArchived: 0,
|
|
4303
|
+
lowScoreArchived: 0,
|
|
4304
|
+
skipped: true
|
|
4305
|
+
};
|
|
4306
|
+
}
|
|
4307
|
+
logger.info("[MaintenanceJob] Starting startup maintenance sweep");
|
|
4308
|
+
const decay = applyDecay(db2.db, decayOptions);
|
|
4309
|
+
const expiredArchived = db2.memoryArchives.archiveExpiredMemories(true);
|
|
4310
|
+
const lowScoreArchived = db2.memoryArchives.archiveLowScoreMemories(true);
|
|
4311
|
+
recordMaintenanceRun(db2);
|
|
4312
|
+
const totalArchived = (expiredArchived || 0) + (lowScoreArchived || 0) + (decay.archived || 0);
|
|
4313
|
+
logger.info("[MaintenanceJob] Startup maintenance complete", {
|
|
4314
|
+
decayed: decay.decayed,
|
|
4315
|
+
immunizedSkipped: decay.immunizedSkipped,
|
|
4316
|
+
expiredArchived,
|
|
4317
|
+
lowScoreArchived,
|
|
4318
|
+
decayArchived: decay.archived,
|
|
4319
|
+
totalArchived
|
|
4320
|
+
});
|
|
4321
|
+
return {
|
|
4322
|
+
decay,
|
|
4323
|
+
expiredArchived: expiredArchived || 0,
|
|
4324
|
+
lowScoreArchived: lowScoreArchived || 0,
|
|
4325
|
+
skipped: false
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
|
|
2884
4329
|
// src/mcp/server.ts
|
|
2885
4330
|
import fs2 from "fs";
|
|
2886
|
-
import
|
|
4331
|
+
import path4 from "path";
|
|
2887
4332
|
process.env.MCP_SERVER = "true";
|
|
2888
4333
|
if (process.argv.includes("doctor")) {
|
|
2889
4334
|
process.stderr.write("\n\u{1F3E5} MCP Local Memory - System Diagnosis\n\n");
|
|
@@ -2915,19 +4360,19 @@ if (process.argv.includes("doctor")) {
|
|
|
2915
4360
|
}
|
|
2916
4361
|
var db = await SQLiteStore.create();
|
|
2917
4362
|
var vectors = new RealVectorStore(db);
|
|
2918
|
-
addLogSink(createFileSink(
|
|
4363
|
+
addLogSink(createFileSink(path4.dirname(db.getDbPath())));
|
|
2919
4364
|
logger.info("[Server] startup", { pid: process.pid, version: CAPABILITIES.serverInfo.version, db: db.getDbPath() });
|
|
2920
4365
|
vectors.initialize().catch((err) => {
|
|
2921
4366
|
logger.warn("[Server] Initial vector model loading failed. Will retry on first use.", { error: String(err) });
|
|
2922
4367
|
});
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
}
|
|
4368
|
+
runStartupMaintenance(db).then((result) => {
|
|
4369
|
+
if (!result.skipped) {
|
|
4370
|
+
logger.info("[Server] Startup maintenance complete", {
|
|
4371
|
+
decayed: result.decay.decayed,
|
|
4372
|
+
archived: result.expiredArchived + result.lowScoreArchived + result.decay.archived
|
|
4373
|
+
});
|
|
4374
|
+
}
|
|
4375
|
+
});
|
|
2931
4376
|
process.stdout.on("error", (err) => {
|
|
2932
4377
|
if (err.code === "EPIPE") return;
|
|
2933
4378
|
logger.error("stdout error", { error: String(err) });
|
|
@@ -2936,265 +4381,12 @@ process.stderr.on("error", (err) => {
|
|
|
2936
4381
|
if (err.code === "EPIPE") return;
|
|
2937
4382
|
logger.error("stderr error", { error: String(err) });
|
|
2938
4383
|
});
|
|
2939
|
-
var
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
var handleMethod = createRouter(db, vectors, {
|
|
2943
|
-
getSessionContext: () => session,
|
|
2944
|
-
sampleMessage: (params) => requestClient("sampling/createMessage", params),
|
|
2945
|
-
elicit: (params) => requestClient("elicitation/create", params),
|
|
2946
|
-
onResourcesMutated: (uris) => notifyUpdatedResources(uris)
|
|
2947
|
-
});
|
|
2948
|
-
addLogSink((payload) => {
|
|
2949
|
-
if (!logNotificationsEnabled) {
|
|
2950
|
-
return;
|
|
2951
|
-
}
|
|
2952
|
-
reply({
|
|
2953
|
-
jsonrpc: "2.0",
|
|
2954
|
-
method: "notifications/message",
|
|
2955
|
-
params: payload
|
|
2956
|
-
});
|
|
2957
|
-
});
|
|
2958
|
-
process.on("SIGINT", () => {
|
|
2959
|
-
logger.info("[Server] shutdown", { signal: "SIGINT", pid: process.pid });
|
|
2960
|
-
for (const pending of pendingClientRequests.values()) {
|
|
2961
|
-
pending.reject(new Error("Server stopped"));
|
|
2962
|
-
}
|
|
2963
|
-
pendingClientRequests.clear();
|
|
4384
|
+
var shutdown = async (signal) => {
|
|
4385
|
+
logger.info("[Server] shutdown", { signal, pid: process.pid });
|
|
4386
|
+
await handle?.close();
|
|
2964
4387
|
db.close();
|
|
2965
4388
|
process.exit(0);
|
|
2966
|
-
}
|
|
2967
|
-
process.on("
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
pending.reject(new Error("Server stopped"));
|
|
2971
|
-
}
|
|
2972
|
-
pendingClientRequests.clear();
|
|
2973
|
-
db.close();
|
|
2974
|
-
process.exit(0);
|
|
2975
|
-
});
|
|
2976
|
-
var rl = readline.createInterface({
|
|
2977
|
-
input: process.stdin,
|
|
2978
|
-
output: process.stdout,
|
|
2979
|
-
terminal: false
|
|
2980
|
-
});
|
|
2981
|
-
function reply(payload) {
|
|
2982
|
-
try {
|
|
2983
|
-
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
2984
|
-
} catch (err) {
|
|
2985
|
-
logger.error("Reply error", { error: String(err) });
|
|
2986
|
-
}
|
|
2987
|
-
}
|
|
2988
|
-
var isInitialized = false;
|
|
2989
|
-
var activeRequests = /* @__PURE__ */ new Map();
|
|
2990
|
-
var pendingClientRequests = /* @__PURE__ */ new Map();
|
|
2991
|
-
var outgoingRequestId = 0;
|
|
2992
|
-
function requestClient(method, params = {}) {
|
|
2993
|
-
const id = `server:${++outgoingRequestId}`;
|
|
2994
|
-
reply({
|
|
2995
|
-
jsonrpc: "2.0",
|
|
2996
|
-
id,
|
|
2997
|
-
method,
|
|
2998
|
-
params
|
|
2999
|
-
});
|
|
3000
|
-
return new Promise((resolve, reject) => {
|
|
3001
|
-
pendingClientRequests.set(id, { method, resolve, reject });
|
|
3002
|
-
});
|
|
3003
|
-
}
|
|
3004
|
-
async function refreshRoots(trigger) {
|
|
3005
|
-
if (!session.supportsRoots) return;
|
|
3006
|
-
try {
|
|
3007
|
-
const result = await requestClient("roots/list");
|
|
3008
|
-
const changed = updateSessionRoots(session, extractRootsFromResult(result));
|
|
3009
|
-
logger.info("[Server] Refreshed client roots", {
|
|
3010
|
-
trigger,
|
|
3011
|
-
count: session.roots.length,
|
|
3012
|
-
changed
|
|
3013
|
-
});
|
|
3014
|
-
if (changed) {
|
|
3015
|
-
reply({
|
|
3016
|
-
jsonrpc: "2.0",
|
|
3017
|
-
method: "notifications/resources/list_changed"
|
|
3018
|
-
});
|
|
3019
|
-
reply({
|
|
3020
|
-
jsonrpc: "2.0",
|
|
3021
|
-
method: "notifications/prompts/list_changed"
|
|
3022
|
-
});
|
|
3023
|
-
}
|
|
3024
|
-
} catch (error) {
|
|
3025
|
-
logger.warn("[Server] Failed to refresh client roots", {
|
|
3026
|
-
trigger,
|
|
3027
|
-
error: String(error)
|
|
3028
|
-
});
|
|
3029
|
-
}
|
|
3030
|
-
}
|
|
3031
|
-
function notifyUpdatedResources(uris) {
|
|
3032
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3033
|
-
for (const uri of uris) {
|
|
3034
|
-
if (seen.has(uri)) continue;
|
|
3035
|
-
seen.add(uri);
|
|
3036
|
-
if (!resourceSubscriptions.has(uri)) {
|
|
3037
|
-
continue;
|
|
3038
|
-
}
|
|
3039
|
-
reply({
|
|
3040
|
-
jsonrpc: "2.0",
|
|
3041
|
-
method: "notifications/resources/updated",
|
|
3042
|
-
params: { uri }
|
|
3043
|
-
});
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
rl.on("line", async (line) => {
|
|
3047
|
-
if (!line.trim()) return;
|
|
3048
|
-
let msg;
|
|
3049
|
-
try {
|
|
3050
|
-
msg = JSON.parse(line);
|
|
3051
|
-
} catch {
|
|
3052
|
-
return;
|
|
3053
|
-
}
|
|
3054
|
-
const { id, method, params } = msg;
|
|
3055
|
-
const isNotification = id === void 0 || id === null;
|
|
3056
|
-
if ((method === void 0 || method === null) && id !== void 0 && pendingClientRequests.has(id)) {
|
|
3057
|
-
const pending = pendingClientRequests.get(id);
|
|
3058
|
-
pendingClientRequests.delete(id);
|
|
3059
|
-
if (msg.error) {
|
|
3060
|
-
pending.reject(new Error(msg.error.message || `Client request failed: ${pending.method}`));
|
|
3061
|
-
} else {
|
|
3062
|
-
pending.resolve(msg.result);
|
|
3063
|
-
}
|
|
3064
|
-
return;
|
|
3065
|
-
}
|
|
3066
|
-
if (method === "initialize" && !isNotification) {
|
|
3067
|
-
updateSessionFromInitialize(session, params);
|
|
3068
|
-
reply({
|
|
3069
|
-
jsonrpc: "2.0",
|
|
3070
|
-
id,
|
|
3071
|
-
result: {
|
|
3072
|
-
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
3073
|
-
serverInfo: CAPABILITIES.serverInfo,
|
|
3074
|
-
capabilities: CAPABILITIES.capabilities
|
|
3075
|
-
}
|
|
3076
|
-
});
|
|
3077
|
-
return;
|
|
3078
|
-
}
|
|
3079
|
-
if (isNotification) {
|
|
3080
|
-
if (method === "notifications/initialized") {
|
|
3081
|
-
isInitialized = true;
|
|
3082
|
-
logNotificationsEnabled = true;
|
|
3083
|
-
logger.debug("[Server] Client initialized");
|
|
3084
|
-
void refreshRoots("initialized");
|
|
3085
|
-
} else if (method === "notifications/roots/list_changed") {
|
|
3086
|
-
logger.debug("[Server] Client roots changed");
|
|
3087
|
-
void refreshRoots("roots/list_changed");
|
|
3088
|
-
} else if (method === "notifications/cancelled") {
|
|
3089
|
-
const requestId = params?.requestId;
|
|
3090
|
-
if (requestId !== void 0 && activeRequests.has(requestId)) {
|
|
3091
|
-
activeRequests.get(requestId).abort();
|
|
3092
|
-
activeRequests.delete(requestId);
|
|
3093
|
-
logger.debug(`[Server] Request ${requestId} cancelled`);
|
|
3094
|
-
} else {
|
|
3095
|
-
logger.debug(`[Server] Cancelled notification for unknown or completed request ${requestId}`);
|
|
3096
|
-
}
|
|
3097
|
-
}
|
|
3098
|
-
return;
|
|
3099
|
-
}
|
|
3100
|
-
if (method === "ping") {
|
|
3101
|
-
reply({
|
|
3102
|
-
jsonrpc: "2.0",
|
|
3103
|
-
id,
|
|
3104
|
-
result: {}
|
|
3105
|
-
});
|
|
3106
|
-
return;
|
|
3107
|
-
}
|
|
3108
|
-
if (method === "resources/subscribe") {
|
|
3109
|
-
const uri = typeof params?.uri === "string" ? params.uri : "";
|
|
3110
|
-
if (!uri) {
|
|
3111
|
-
reply({
|
|
3112
|
-
jsonrpc: "2.0",
|
|
3113
|
-
id,
|
|
3114
|
-
error: {
|
|
3115
|
-
code: -32602,
|
|
3116
|
-
message: "resources/subscribe requires a uri"
|
|
3117
|
-
}
|
|
3118
|
-
});
|
|
3119
|
-
return;
|
|
3120
|
-
}
|
|
3121
|
-
resourceSubscriptions.add(uri);
|
|
3122
|
-
reply({
|
|
3123
|
-
jsonrpc: "2.0",
|
|
3124
|
-
id,
|
|
3125
|
-
result: {}
|
|
3126
|
-
});
|
|
3127
|
-
return;
|
|
3128
|
-
}
|
|
3129
|
-
if (method === "resources/unsubscribe") {
|
|
3130
|
-
const uri = typeof params?.uri === "string" ? params.uri : "";
|
|
3131
|
-
if (!uri) {
|
|
3132
|
-
reply({
|
|
3133
|
-
jsonrpc: "2.0",
|
|
3134
|
-
id,
|
|
3135
|
-
error: {
|
|
3136
|
-
code: -32602,
|
|
3137
|
-
message: "resources/unsubscribe requires a uri"
|
|
3138
|
-
}
|
|
3139
|
-
});
|
|
3140
|
-
return;
|
|
3141
|
-
}
|
|
3142
|
-
resourceSubscriptions.delete(uri);
|
|
3143
|
-
reply({
|
|
3144
|
-
jsonrpc: "2.0",
|
|
3145
|
-
id,
|
|
3146
|
-
result: {}
|
|
3147
|
-
});
|
|
3148
|
-
return;
|
|
3149
|
-
}
|
|
3150
|
-
if (!isInitialized) {
|
|
3151
|
-
reply({
|
|
3152
|
-
jsonrpc: "2.0",
|
|
3153
|
-
id,
|
|
3154
|
-
error: {
|
|
3155
|
-
code: -32002,
|
|
3156
|
-
message: "Server is not fully initialized yet. Please send notifications/initialized."
|
|
3157
|
-
}
|
|
3158
|
-
});
|
|
3159
|
-
return;
|
|
3160
|
-
}
|
|
3161
|
-
const abortController = new AbortController();
|
|
3162
|
-
activeRequests.set(id, abortController);
|
|
3163
|
-
const progressToken = params?._meta?.progressToken;
|
|
3164
|
-
const onProgress = progressToken !== void 0 ? (progress, total) => {
|
|
3165
|
-
reply({
|
|
3166
|
-
jsonrpc: "2.0",
|
|
3167
|
-
method: "notifications/progress",
|
|
3168
|
-
params: {
|
|
3169
|
-
progressToken,
|
|
3170
|
-
progress,
|
|
3171
|
-
total
|
|
3172
|
-
}
|
|
3173
|
-
});
|
|
3174
|
-
} : void 0;
|
|
3175
|
-
try {
|
|
3176
|
-
const result = await handleMethod(method, params, abortController.signal, onProgress);
|
|
3177
|
-
if (!abortController.signal.aborted) {
|
|
3178
|
-
reply({
|
|
3179
|
-
jsonrpc: "2.0",
|
|
3180
|
-
id,
|
|
3181
|
-
result
|
|
3182
|
-
});
|
|
3183
|
-
}
|
|
3184
|
-
} catch (err) {
|
|
3185
|
-
if (!abortController.signal.aborted) {
|
|
3186
|
-
const error = err;
|
|
3187
|
-
logger.error("Method handler error", { method, id, message: error.message });
|
|
3188
|
-
reply({
|
|
3189
|
-
jsonrpc: "2.0",
|
|
3190
|
-
id,
|
|
3191
|
-
error: {
|
|
3192
|
-
code: typeof error?.code === "number" ? error.code : -32603,
|
|
3193
|
-
message: error.message || "Internal error"
|
|
3194
|
-
}
|
|
3195
|
-
});
|
|
3196
|
-
}
|
|
3197
|
-
} finally {
|
|
3198
|
-
activeRequests.delete(id);
|
|
3199
|
-
}
|
|
3200
|
-
});
|
|
4389
|
+
};
|
|
4390
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
4391
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
4392
|
+
var handle = serveStdio(() => createMcpServer(db, vectors));
|