engram-mcp-server 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +645 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +81 -0
- package/dist/database.d.ts +30 -0
- package/dist/database.js +134 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +67 -0
- package/dist/migrations.d.ts +4 -0
- package/dist/migrations.js +342 -0
- package/dist/scripts/install-hooks.d.ts +3 -0
- package/dist/scripts/install-hooks.js +89 -0
- package/dist/tools/intelligence.d.ts +3 -0
- package/dist/tools/intelligence.js +427 -0
- package/dist/tools/maintenance.d.ts +3 -0
- package/dist/tools/maintenance.js +646 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.js +446 -0
- package/dist/tools/scheduler.d.ts +3 -0
- package/dist/tools/scheduler.js +363 -0
- package/dist/tools/sessions.d.ts +3 -0
- package/dist/tools/sessions.js +355 -0
- package/dist/tools/tasks.d.ts +3 -0
- package/dist/tools/tasks.js +206 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +58 -0
- package/dist/utils.js +190 -0
- package/docs/scheduled-events.md +150 -0
- package/package.json +43 -0
- package/scripts/install-mcp.js +175 -0
- package/src/constants.ts +86 -0
- package/src/database.ts +162 -0
- package/src/index.ts +79 -0
- package/src/migrations.ts +367 -0
- package/src/scripts/install-hooks.ts +96 -0
- package/src/tools/intelligence.ts +469 -0
- package/src/tools/maintenance.ts +783 -0
- package/src/tools/memory.ts +543 -0
- package/src/tools/scheduler.ts +413 -0
- package/src/tools/sessions.ts +430 -0
- package/src/tools/tasks.ts +215 -0
- package/src/types.ts +267 -0
- package/src/utils.ts +216 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Engram MCP Server — Core Memory Tools
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { getDb, now, getCurrentSessionId } from "../database.js";
|
|
6
|
+
import { TOOL_PREFIX } from "../constants.js";
|
|
7
|
+
export function registerMemoryTools(server) {
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
9
|
+
// CHANGE TRACKING
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
11
|
+
server.registerTool(`${TOOL_PREFIX}_record_change`, {
|
|
12
|
+
title: "Record Change",
|
|
13
|
+
description: `Record a file change so future sessions know what happened and why. Call this after making significant modifications. Bulk recording is supported — pass multiple changes at once.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
- changes (array): Array of change objects, each with:
|
|
17
|
+
- file_path (string): Relative path to the changed file
|
|
18
|
+
- change_type: "created" | "modified" | "deleted" | "refactored" | "renamed" | "moved" | "config_changed"
|
|
19
|
+
- description (string): What was changed and why
|
|
20
|
+
- diff_summary (string, optional): Brief summary of the diff
|
|
21
|
+
- impact_scope: "local" | "module" | "cross_module" | "global" (default: "local")
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Confirmation with number of changes recorded.`,
|
|
25
|
+
inputSchema: {
|
|
26
|
+
changes: z.array(z.object({
|
|
27
|
+
file_path: z.string().describe("Relative path to the changed file"),
|
|
28
|
+
change_type: z.enum(["created", "modified", "deleted", "refactored", "renamed", "moved", "config_changed"]),
|
|
29
|
+
description: z.string().describe("What was changed and why"),
|
|
30
|
+
diff_summary: z.string().optional().describe("Brief diff summary"),
|
|
31
|
+
impact_scope: z.enum(["local", "module", "cross_module", "global"]).default("local"),
|
|
32
|
+
})).min(1).describe("Array of changes to record"),
|
|
33
|
+
},
|
|
34
|
+
annotations: {
|
|
35
|
+
readOnlyHint: false,
|
|
36
|
+
destructiveHint: false,
|
|
37
|
+
idempotentHint: false,
|
|
38
|
+
openWorldHint: false,
|
|
39
|
+
},
|
|
40
|
+
}, async ({ changes }) => {
|
|
41
|
+
const db = getDb();
|
|
42
|
+
const timestamp = now();
|
|
43
|
+
const sessionId = getCurrentSessionId();
|
|
44
|
+
const insert = db.prepare("INSERT INTO changes (session_id, timestamp, file_path, change_type, description, diff_summary, impact_scope) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
45
|
+
const transaction = db.transaction(() => {
|
|
46
|
+
for (const c of changes) {
|
|
47
|
+
insert.run(sessionId, timestamp, c.file_path, c.change_type, c.description, c.diff_summary || null, c.impact_scope);
|
|
48
|
+
// Auto-update file_notes last_modified_session
|
|
49
|
+
if (sessionId) {
|
|
50
|
+
db.prepare("UPDATE file_notes SET last_modified_session = ? WHERE file_path = ?").run(sessionId, c.file_path);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
transaction();
|
|
55
|
+
return {
|
|
56
|
+
content: [{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: `Recorded ${changes.length} change(s) in session #${sessionId ?? "none"}.`,
|
|
59
|
+
}],
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
server.registerTool(`${TOOL_PREFIX}_get_file_history`, {
|
|
63
|
+
title: "Get File History",
|
|
64
|
+
description: `Get the complete change history for a specific file — all recorded modifications, related decisions, and file notes.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
- file_path (string): Path to the file
|
|
68
|
+
- limit (number, optional): Max changes to return (default 20)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
File notes, change history, and related decisions.`,
|
|
72
|
+
inputSchema: {
|
|
73
|
+
file_path: z.string().describe("Relative path to the file"),
|
|
74
|
+
limit: z.number().int().min(1).max(100).default(20).describe("Max changes to return"),
|
|
75
|
+
},
|
|
76
|
+
annotations: {
|
|
77
|
+
readOnlyHint: true,
|
|
78
|
+
destructiveHint: false,
|
|
79
|
+
idempotentHint: true,
|
|
80
|
+
openWorldHint: false,
|
|
81
|
+
},
|
|
82
|
+
}, async ({ file_path, limit }) => {
|
|
83
|
+
const db = getDb();
|
|
84
|
+
const notes = db.prepare("SELECT * FROM file_notes WHERE file_path = ?").get(file_path);
|
|
85
|
+
const changes = db.prepare("SELECT * FROM changes WHERE file_path = ? ORDER BY timestamp DESC LIMIT ?").all(file_path, limit);
|
|
86
|
+
const decisions = db.prepare("SELECT * FROM decisions WHERE affected_files LIKE ? AND status = 'active' ORDER BY timestamp DESC").all(`%${file_path}%`);
|
|
87
|
+
return {
|
|
88
|
+
content: [{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: JSON.stringify({
|
|
91
|
+
file_path,
|
|
92
|
+
notes: notes || null,
|
|
93
|
+
change_count: changes.length,
|
|
94
|
+
changes,
|
|
95
|
+
related_decisions: decisions,
|
|
96
|
+
}, null, 2),
|
|
97
|
+
}],
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
101
|
+
// ARCHITECTURAL DECISIONS
|
|
102
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
103
|
+
server.registerTool(`${TOOL_PREFIX}_record_decision`, {
|
|
104
|
+
title: "Record Decision",
|
|
105
|
+
description: `Record an architectural or design decision with its rationale. These persist across all future sessions and are surfaced during start_session. Use this for any choice that future agents or sessions need to respect.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
- decision (string): The decision that was made
|
|
109
|
+
- rationale (string, optional): Why this decision was made — context, tradeoffs, alternatives considered
|
|
110
|
+
- affected_files (array of strings, optional): Files impacted by this decision
|
|
111
|
+
- tags (array of strings, optional): Categorization tags (e.g., "architecture", "database", "ui", "api")
|
|
112
|
+
- status: "active" | "experimental" (default: "active")
|
|
113
|
+
- supersedes (number, optional): ID of a previous decision this replaces
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Decision ID and confirmation.`,
|
|
117
|
+
inputSchema: {
|
|
118
|
+
decision: z.string().min(5).describe("The decision that was made"),
|
|
119
|
+
rationale: z.string().optional().describe("Why — context, tradeoffs, alternatives considered"),
|
|
120
|
+
affected_files: z.array(z.string()).optional().describe("Files impacted by this decision"),
|
|
121
|
+
tags: z.array(z.string()).optional().describe("Tags for categorization"),
|
|
122
|
+
status: z.enum(["active", "experimental"]).default("active"),
|
|
123
|
+
supersedes: z.number().int().optional().describe("ID of a previous decision this replaces"),
|
|
124
|
+
},
|
|
125
|
+
annotations: {
|
|
126
|
+
readOnlyHint: false,
|
|
127
|
+
destructiveHint: false,
|
|
128
|
+
idempotentHint: false,
|
|
129
|
+
openWorldHint: false,
|
|
130
|
+
},
|
|
131
|
+
}, async ({ decision, rationale, affected_files, tags, status, supersedes }) => {
|
|
132
|
+
const db = getDb();
|
|
133
|
+
const timestamp = now();
|
|
134
|
+
const sessionId = getCurrentSessionId();
|
|
135
|
+
const result = db.prepare("INSERT INTO decisions (session_id, timestamp, decision, rationale, affected_files, tags, status, superseded_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(sessionId, timestamp, decision, rationale || null, affected_files ? JSON.stringify(affected_files) : null, tags ? JSON.stringify(tags) : null, status, supersedes || null);
|
|
136
|
+
const newDecisionId = result.lastInsertRowid;
|
|
137
|
+
// If superseding, mark old decision with the new decision's ID
|
|
138
|
+
if (supersedes) {
|
|
139
|
+
db.prepare("UPDATE decisions SET status = 'superseded', superseded_by = ? WHERE id = ?")
|
|
140
|
+
.run(newDecisionId, supersedes);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
content: [{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: JSON.stringify({
|
|
146
|
+
decision_id: newDecisionId,
|
|
147
|
+
message: `Decision #${newDecisionId} recorded${supersedes ? ` (supersedes #${supersedes})` : ""}.`,
|
|
148
|
+
decision,
|
|
149
|
+
}, null, 2),
|
|
150
|
+
}],
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
server.registerTool(`${TOOL_PREFIX}_get_decisions`, {
|
|
154
|
+
title: "Get Decisions",
|
|
155
|
+
description: `Retrieve recorded architectural decisions. Filter by status, tags, or affected files.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
- status (string, optional): Filter by status — "active", "superseded", "deprecated", "experimental"
|
|
159
|
+
- tag (string, optional): Filter by tag
|
|
160
|
+
- file_path (string, optional): Find decisions affecting a specific file
|
|
161
|
+
- limit (number, optional): Max results (default 20)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Array of decisions with rationale and metadata.`,
|
|
165
|
+
inputSchema: {
|
|
166
|
+
status: z.enum(["active", "superseded", "deprecated", "experimental"]).optional(),
|
|
167
|
+
tag: z.string().optional().describe("Filter by tag"),
|
|
168
|
+
file_path: z.string().optional().describe("Find decisions affecting this file"),
|
|
169
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
170
|
+
},
|
|
171
|
+
annotations: {
|
|
172
|
+
readOnlyHint: true,
|
|
173
|
+
destructiveHint: false,
|
|
174
|
+
idempotentHint: true,
|
|
175
|
+
openWorldHint: false,
|
|
176
|
+
},
|
|
177
|
+
}, async ({ status, tag, file_path, limit }) => {
|
|
178
|
+
const db = getDb();
|
|
179
|
+
let query = "SELECT * FROM decisions WHERE 1=1";
|
|
180
|
+
const params = [];
|
|
181
|
+
if (status) {
|
|
182
|
+
query += " AND status = ?";
|
|
183
|
+
params.push(status);
|
|
184
|
+
}
|
|
185
|
+
if (tag) {
|
|
186
|
+
query += " AND EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)";
|
|
187
|
+
params.push(tag);
|
|
188
|
+
}
|
|
189
|
+
if (file_path) {
|
|
190
|
+
query += " AND EXISTS (SELECT 1 FROM json_each(affected_files) WHERE value = ?)";
|
|
191
|
+
params.push(file_path);
|
|
192
|
+
}
|
|
193
|
+
query += " ORDER BY timestamp DESC LIMIT ?";
|
|
194
|
+
params.push(limit);
|
|
195
|
+
const decisions = db.prepare(query).all(...params);
|
|
196
|
+
return {
|
|
197
|
+
content: [{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: JSON.stringify({ count: decisions.length, decisions }, null, 2),
|
|
200
|
+
}],
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
server.registerTool(`${TOOL_PREFIX}_update_decision`, {
|
|
204
|
+
title: "Update Decision Status",
|
|
205
|
+
description: `Update the status of an existing decision. Use to deprecate, supersede, or reactivate decisions.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
- id (number): Decision ID to update
|
|
209
|
+
- status: "active" | "superseded" | "deprecated" | "experimental"
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Confirmation.`,
|
|
213
|
+
inputSchema: {
|
|
214
|
+
id: z.number().int().describe("Decision ID"),
|
|
215
|
+
status: z.enum(["active", "superseded", "deprecated", "experimental"]),
|
|
216
|
+
},
|
|
217
|
+
annotations: {
|
|
218
|
+
readOnlyHint: false,
|
|
219
|
+
destructiveHint: false,
|
|
220
|
+
idempotentHint: true,
|
|
221
|
+
openWorldHint: false,
|
|
222
|
+
},
|
|
223
|
+
}, async ({ id, status }) => {
|
|
224
|
+
const db = getDb();
|
|
225
|
+
const result = db.prepare("UPDATE decisions SET status = ? WHERE id = ?").run(status, id);
|
|
226
|
+
if (result.changes === 0) {
|
|
227
|
+
return { isError: true, content: [{ type: "text", text: `Decision #${id} not found.` }] };
|
|
228
|
+
}
|
|
229
|
+
return { content: [{ type: "text", text: `Decision #${id} status updated to "${status}".` }] };
|
|
230
|
+
});
|
|
231
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
232
|
+
// FILE NOTES
|
|
233
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
234
|
+
server.registerTool(`${TOOL_PREFIX}_set_file_notes`, {
|
|
235
|
+
title: "Set File Notes",
|
|
236
|
+
description: `Store persistent notes about a file: its purpose, dependencies, architectural layer, complexity, and any important details. This creates a knowledge base that eliminates the need to re-read and re-analyze files across sessions.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
- file_path (string): Relative path to the file
|
|
240
|
+
- purpose (string, optional): What this file does — its responsibility
|
|
241
|
+
- dependencies (array, optional): Files this file depends on
|
|
242
|
+
- dependents (array, optional): Files that depend on this file
|
|
243
|
+
- layer: "ui" | "viewmodel" | "domain" | "data" | "network" | "database" | "di" | "util" | "test" | "config" | "build" | "other"
|
|
244
|
+
- complexity: "trivial" | "simple" | "moderate" | "complex" | "critical"
|
|
245
|
+
- notes (string, optional): Any important context, gotchas, or warnings
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Confirmation.`,
|
|
249
|
+
inputSchema: {
|
|
250
|
+
file_path: z.string().describe("Relative path to the file"),
|
|
251
|
+
purpose: z.string().optional().describe("What this file does"),
|
|
252
|
+
dependencies: z.array(z.string()).optional().describe("Files this depends on"),
|
|
253
|
+
dependents: z.array(z.string()).optional().describe("Files that depend on this"),
|
|
254
|
+
layer: z.enum(["ui", "viewmodel", "domain", "data", "network", "database", "di", "util", "test", "config", "build", "other"]).optional(),
|
|
255
|
+
complexity: z.enum(["trivial", "simple", "moderate", "complex", "critical"]).optional(),
|
|
256
|
+
notes: z.string().optional().describe("Important context, gotchas, warnings"),
|
|
257
|
+
},
|
|
258
|
+
annotations: {
|
|
259
|
+
readOnlyHint: false,
|
|
260
|
+
destructiveHint: false,
|
|
261
|
+
idempotentHint: true,
|
|
262
|
+
openWorldHint: false,
|
|
263
|
+
},
|
|
264
|
+
}, async ({ file_path, purpose, dependencies, dependents, layer, complexity, notes }) => {
|
|
265
|
+
const db = getDb();
|
|
266
|
+
const timestamp = now();
|
|
267
|
+
const sessionId = getCurrentSessionId();
|
|
268
|
+
db.prepare(`
|
|
269
|
+
INSERT INTO file_notes (file_path, purpose, dependencies, dependents, layer, last_reviewed, last_modified_session, notes, complexity)
|
|
270
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
271
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
272
|
+
purpose = COALESCE(?, purpose),
|
|
273
|
+
dependencies = COALESCE(?, dependencies),
|
|
274
|
+
dependents = COALESCE(?, dependents),
|
|
275
|
+
layer = COALESCE(?, layer),
|
|
276
|
+
last_reviewed = ?,
|
|
277
|
+
last_modified_session = COALESCE(?, last_modified_session),
|
|
278
|
+
notes = COALESCE(?, notes),
|
|
279
|
+
complexity = COALESCE(?, complexity)
|
|
280
|
+
`).run(file_path, purpose || null, dependencies ? JSON.stringify(dependencies) : null, dependents ? JSON.stringify(dependents) : null, layer || null, timestamp, sessionId, notes || null, complexity || null,
|
|
281
|
+
// Update values
|
|
282
|
+
purpose || null, dependencies ? JSON.stringify(dependencies) : null, dependents ? JSON.stringify(dependents) : null, layer || null, timestamp, sessionId, notes || null, complexity || null);
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: `File notes saved for ${file_path}.` }],
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
server.registerTool(`${TOOL_PREFIX}_get_file_notes`, {
|
|
288
|
+
title: "Get File Notes",
|
|
289
|
+
description: `Retrieve stored notes for one or more files. Use to quickly understand a file's purpose and context without reading it.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
- file_path (string, optional): Specific file to query
|
|
293
|
+
- layer (string, optional): Filter by architectural layer
|
|
294
|
+
- complexity (string, optional): Filter by complexity level
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
File notes with purpose, dependencies, layer, and complexity.`,
|
|
298
|
+
inputSchema: {
|
|
299
|
+
file_path: z.string().optional().describe("Specific file to query"),
|
|
300
|
+
layer: z.enum(["ui", "viewmodel", "domain", "data", "network", "database", "di", "util", "test", "config", "build", "other"]).optional(),
|
|
301
|
+
complexity: z.enum(["trivial", "simple", "moderate", "complex", "critical"]).optional(),
|
|
302
|
+
},
|
|
303
|
+
annotations: {
|
|
304
|
+
readOnlyHint: true,
|
|
305
|
+
destructiveHint: false,
|
|
306
|
+
idempotentHint: true,
|
|
307
|
+
openWorldHint: false,
|
|
308
|
+
},
|
|
309
|
+
}, async ({ file_path, layer, complexity }) => {
|
|
310
|
+
const db = getDb();
|
|
311
|
+
if (file_path) {
|
|
312
|
+
const note = db.prepare("SELECT * FROM file_notes WHERE file_path = ?").get(file_path);
|
|
313
|
+
return { content: [{ type: "text", text: JSON.stringify(note || { message: "No notes found for this file." }, null, 2) }] };
|
|
314
|
+
}
|
|
315
|
+
let query = "SELECT * FROM file_notes WHERE 1=1";
|
|
316
|
+
const params = [];
|
|
317
|
+
if (layer) {
|
|
318
|
+
query += " AND layer = ?";
|
|
319
|
+
params.push(layer);
|
|
320
|
+
}
|
|
321
|
+
if (complexity) {
|
|
322
|
+
query += " AND complexity = ?";
|
|
323
|
+
params.push(complexity);
|
|
324
|
+
}
|
|
325
|
+
query += " ORDER BY file_path";
|
|
326
|
+
const notes = db.prepare(query).all(...params);
|
|
327
|
+
return { content: [{ type: "text", text: JSON.stringify({ count: notes.length, files: notes }, null, 2) }] };
|
|
328
|
+
});
|
|
329
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
330
|
+
// CONVENTIONS
|
|
331
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
332
|
+
server.registerTool(`${TOOL_PREFIX}_add_convention`, {
|
|
333
|
+
title: "Add Convention",
|
|
334
|
+
description: `Record a project convention that the agent should always follow. Conventions are surfaced during start_session and serve as persistent rules.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
- category: "naming" | "architecture" | "styling" | "testing" | "git" | "documentation" | "error_handling" | "performance" | "security" | "other"
|
|
338
|
+
- rule (string): The convention rule in clear, actionable language
|
|
339
|
+
- examples (array of strings, optional): Code or usage examples
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Convention ID and confirmation.`,
|
|
343
|
+
inputSchema: {
|
|
344
|
+
category: z.enum(["naming", "architecture", "styling", "testing", "git", "documentation", "error_handling", "performance", "security", "other"]),
|
|
345
|
+
rule: z.string().min(5).describe("The convention rule"),
|
|
346
|
+
examples: z.array(z.string()).optional().describe("Examples of the convention in use"),
|
|
347
|
+
},
|
|
348
|
+
annotations: {
|
|
349
|
+
readOnlyHint: false,
|
|
350
|
+
destructiveHint: false,
|
|
351
|
+
idempotentHint: false,
|
|
352
|
+
openWorldHint: false,
|
|
353
|
+
},
|
|
354
|
+
}, async ({ category, rule, examples }) => {
|
|
355
|
+
const db = getDb();
|
|
356
|
+
const timestamp = now();
|
|
357
|
+
const sessionId = getCurrentSessionId();
|
|
358
|
+
const result = db.prepare("INSERT INTO conventions (session_id, timestamp, category, rule, examples) VALUES (?, ?, ?, ?, ?)").run(sessionId, timestamp, category, rule, examples ? JSON.stringify(examples) : null);
|
|
359
|
+
return {
|
|
360
|
+
content: [{
|
|
361
|
+
type: "text",
|
|
362
|
+
text: JSON.stringify({
|
|
363
|
+
convention_id: result.lastInsertRowid,
|
|
364
|
+
message: `Convention #${result.lastInsertRowid} added to [${category}].`,
|
|
365
|
+
rule,
|
|
366
|
+
}, null, 2),
|
|
367
|
+
}],
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
server.registerTool(`${TOOL_PREFIX}_get_conventions`, {
|
|
371
|
+
title: "Get Conventions",
|
|
372
|
+
description: `Retrieve all active project conventions. Optionally filter by category.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
- category (string, optional): Filter by convention category
|
|
376
|
+
- include_disabled (boolean, optional): Include unenforced conventions (default: false)
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Array of conventions grouped by category.`,
|
|
380
|
+
inputSchema: {
|
|
381
|
+
category: z.enum(["naming", "architecture", "styling", "testing", "git", "documentation", "error_handling", "performance", "security", "other"]).optional(),
|
|
382
|
+
include_disabled: z.boolean().default(false),
|
|
383
|
+
},
|
|
384
|
+
annotations: {
|
|
385
|
+
readOnlyHint: true,
|
|
386
|
+
destructiveHint: false,
|
|
387
|
+
idempotentHint: true,
|
|
388
|
+
openWorldHint: false,
|
|
389
|
+
},
|
|
390
|
+
}, async ({ category, include_disabled }) => {
|
|
391
|
+
const db = getDb();
|
|
392
|
+
let query = "SELECT * FROM conventions WHERE 1=1";
|
|
393
|
+
const params = [];
|
|
394
|
+
if (!include_disabled) {
|
|
395
|
+
query += " AND enforced = 1";
|
|
396
|
+
}
|
|
397
|
+
if (category) {
|
|
398
|
+
query += " AND category = ?";
|
|
399
|
+
params.push(category);
|
|
400
|
+
}
|
|
401
|
+
query += " ORDER BY category, id";
|
|
402
|
+
const conventions = db.prepare(query).all(...params);
|
|
403
|
+
// Group by category
|
|
404
|
+
const grouped = {};
|
|
405
|
+
for (const c of conventions) {
|
|
406
|
+
if (!grouped[c.category])
|
|
407
|
+
grouped[c.category] = [];
|
|
408
|
+
grouped[c.category].push(c);
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
content: [{
|
|
412
|
+
type: "text",
|
|
413
|
+
text: JSON.stringify({ total: conventions.length, by_category: grouped }, null, 2),
|
|
414
|
+
}],
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
server.registerTool(`${TOOL_PREFIX}_toggle_convention`, {
|
|
418
|
+
title: "Toggle Convention",
|
|
419
|
+
description: `Enable or disable a convention. Disabled conventions are not surfaced during start_session.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
- id (number): Convention ID
|
|
423
|
+
- enforced (boolean): Whether the convention should be enforced
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Confirmation.`,
|
|
427
|
+
inputSchema: {
|
|
428
|
+
id: z.number().int().describe("Convention ID"),
|
|
429
|
+
enforced: z.boolean().describe("Enable or disable"),
|
|
430
|
+
},
|
|
431
|
+
annotations: {
|
|
432
|
+
readOnlyHint: false,
|
|
433
|
+
destructiveHint: false,
|
|
434
|
+
idempotentHint: true,
|
|
435
|
+
openWorldHint: false,
|
|
436
|
+
},
|
|
437
|
+
}, async ({ id, enforced }) => {
|
|
438
|
+
const db = getDb();
|
|
439
|
+
const result = db.prepare("UPDATE conventions SET enforced = ? WHERE id = ?").run(enforced ? 1 : 0, id);
|
|
440
|
+
if (result.changes === 0) {
|
|
441
|
+
return { isError: true, content: [{ type: "text", text: `Convention #${id} not found.` }] };
|
|
442
|
+
}
|
|
443
|
+
return { content: [{ type: "text", text: `Convention #${id} ${enforced ? "enabled" : "disabled"}.` }] };
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
//# sourceMappingURL=memory.js.map
|