@massu/core 1.6.1 → 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +543 -285
- package/package.json +1 -1
- package/src/db.ts +24 -1
- package/src/security/registry-pubkey.generated.ts +1 -1
- package/src/server-dispatch.ts +225 -0
- package/src/server.ts +9 -114
- package/src/tool-db-needs.ts +226 -0
- package/src/tools.ts +110 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
|
|
6
6
|
"main": "src/server.ts",
|
package/src/db.ts
CHANGED
|
@@ -6,14 +6,37 @@ import { dirname, join } from 'path';
|
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
7
7
|
import { getResolvedPaths } from './config.ts';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Thrown by `getCodeGraphDb()` when `.codegraph/codegraph.db` is missing.
|
|
11
|
+
*
|
|
12
|
+
* Caught at the JSON-RPC dispatch layer (server.ts) and translated to a
|
|
13
|
+
* structured `-32001` error response carrying a remedy hint and the resolved
|
|
14
|
+
* DB path. The thrown error is INTERNAL; user-facing copy lives in the
|
|
15
|
+
* dispatcher's error envelope.
|
|
16
|
+
*
|
|
17
|
+
* @see `docs/plans/2026-05-10-server-lazy-db-deps.md` P-C-001 + P-A-004
|
|
18
|
+
*/
|
|
19
|
+
export class CodegraphDbNotInitializedError extends Error {
|
|
20
|
+
readonly dbPath: string;
|
|
21
|
+
constructor(dbPath: string) {
|
|
22
|
+
super(`CodeGraph database not found at ${dbPath}`);
|
|
23
|
+
this.name = 'CodegraphDbNotInitializedError';
|
|
24
|
+
this.dbPath = dbPath;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
/**
|
|
10
29
|
* Connection to CodeGraph's read-only SQLite database.
|
|
11
30
|
* We NEVER write to this DB - it belongs to vanilla CodeGraph.
|
|
31
|
+
*
|
|
32
|
+
* Throws `CodegraphDbNotInitializedError` (internal signal) when the DB is
|
|
33
|
+
* missing. The MCP dispatcher catches and translates to a structured
|
|
34
|
+
* JSON-RPC error pointing at `npx @colbymchenry/codegraph init`.
|
|
12
35
|
*/
|
|
13
36
|
export function getCodeGraphDb(): Database.Database {
|
|
14
37
|
const dbPath = getResolvedPaths().codegraphDbPath;
|
|
15
38
|
if (!existsSync(dbPath)) {
|
|
16
|
-
throw new
|
|
39
|
+
throw new CodegraphDbNotInitializedError(dbPath);
|
|
17
40
|
}
|
|
18
41
|
const db = new Database(dbPath, { readonly: true });
|
|
19
42
|
db.pragma('journal_mode = WAL');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-11T22:54:34.471Z.
|
|
2
2
|
// Source pem: packages/core/security/registry-pubkey.pem
|
|
3
3
|
// RAW-bytes sha256: 3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c
|
|
4
4
|
// DO NOT EDIT — regenerate via `node scripts/bundle-pubkey.mjs` or
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MCP server dispatch logic — pure, factory-based, no module-level mutable state.
|
|
6
|
+
*
|
|
7
|
+
* Production (`server.ts`) calls `createDispatcher()` once at startup and wires
|
|
8
|
+
* its `processLine` into stdin. Tests call `createDispatcher()` per test for
|
|
9
|
+
* fresh DB cache state (no test bleed).
|
|
10
|
+
*
|
|
11
|
+
* Three error envelopes live here (CR-12 / plan-1.6.2-server-lazy-db-deps):
|
|
12
|
+
* -32001 Tool needs CodeGraph but `.codegraph/codegraph.db` is missing
|
|
13
|
+
* (CodegraphDbNotInitializedError → structured remedy data)
|
|
14
|
+
* -32602 Tool not registered in `TOOL_DB_NEEDS` manifest
|
|
15
|
+
* (UnknownToolError → points at tool-db-needs.ts)
|
|
16
|
+
* -32603 Other internal errors raised by handleToolCall — request id
|
|
17
|
+
* is preserved (NOT id:null, which is reserved for -32700 parse
|
|
18
|
+
* failures per JSON-RPC §5.1).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type Database from 'better-sqlite3';
|
|
22
|
+
import { getCodeGraphDb, getDataDb, CodegraphDbNotInitializedError } from './db.ts';
|
|
23
|
+
import { getConfig } from './config.ts';
|
|
24
|
+
import { getToolDefinitions, handleToolCall } from './tools.ts';
|
|
25
|
+
import { getToolDbNeeds, UnknownToolError, type DbNeed } from './tool-db-needs.ts';
|
|
26
|
+
|
|
27
|
+
export interface JsonRpcRequest {
|
|
28
|
+
jsonrpc: '2.0';
|
|
29
|
+
id?: number | string;
|
|
30
|
+
method: string;
|
|
31
|
+
params?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface JsonRpcResponse {
|
|
35
|
+
jsonrpc: '2.0';
|
|
36
|
+
id: number | string | null;
|
|
37
|
+
result?: unknown;
|
|
38
|
+
error?: { code: number; message: string; data?: unknown };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Per-line dispatch result. `emit=false` when the request was a notification (no id). */
|
|
42
|
+
export interface ProcessLineResult {
|
|
43
|
+
response: JsonRpcResponse;
|
|
44
|
+
emit: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DispatcherOptions {
|
|
48
|
+
/** Version string surfaced in `initialize.result.serverInfo.version`. */
|
|
49
|
+
serverInfoVersion: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Dispatcher {
|
|
53
|
+
handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse>;
|
|
54
|
+
processLine(line: string): Promise<ProcessLineResult | null>;
|
|
55
|
+
closeCachedDbs(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createDispatcher(options: DispatcherOptions): Dispatcher {
|
|
59
|
+
let codegraphDbCache: Database.Database | null = null;
|
|
60
|
+
let dataDbCache: Database.Database | null = null;
|
|
61
|
+
|
|
62
|
+
function resolveDbsForTool(toolName: string): {
|
|
63
|
+
needs: readonly DbNeed[];
|
|
64
|
+
dataDb?: Database.Database;
|
|
65
|
+
codegraphDb?: Database.Database;
|
|
66
|
+
} {
|
|
67
|
+
const needs = getToolDbNeeds(toolName, getConfig().toolPrefix);
|
|
68
|
+
|
|
69
|
+
let dataDbResolved: Database.Database | undefined;
|
|
70
|
+
let codegraphDbResolved: Database.Database | undefined;
|
|
71
|
+
|
|
72
|
+
if (needs.includes('data')) {
|
|
73
|
+
if (!dataDbCache) dataDbCache = getDataDb();
|
|
74
|
+
dataDbResolved = dataDbCache;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (needs.includes('codegraph')) {
|
|
78
|
+
// Throws CodegraphDbNotInitializedError when .codegraph/codegraph.db is missing.
|
|
79
|
+
if (!codegraphDbCache) codegraphDbCache = getCodeGraphDb();
|
|
80
|
+
codegraphDbResolved = codegraphDbCache;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { needs, dataDb: dataDbResolved, codegraphDb: codegraphDbResolved };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
87
|
+
const { method, params, id } = request;
|
|
88
|
+
|
|
89
|
+
switch (method) {
|
|
90
|
+
case 'initialize': {
|
|
91
|
+
return {
|
|
92
|
+
jsonrpc: '2.0',
|
|
93
|
+
id: id ?? null,
|
|
94
|
+
result: {
|
|
95
|
+
protocolVersion: '2024-11-05',
|
|
96
|
+
capabilities: { tools: {} },
|
|
97
|
+
serverInfo: {
|
|
98
|
+
name: getConfig().toolPrefix || 'massu',
|
|
99
|
+
version: options.serverInfoVersion,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'notifications/initialized': {
|
|
106
|
+
return { jsonrpc: '2.0', id: id ?? null, result: {} };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'tools/list': {
|
|
110
|
+
const tools = getToolDefinitions();
|
|
111
|
+
return { jsonrpc: '2.0', id: id ?? null, result: { tools } };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case 'tools/call': {
|
|
115
|
+
const toolName = (params as { name?: string })?.name ?? '';
|
|
116
|
+
const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const { dataDb, codegraphDb } = resolveDbsForTool(toolName);
|
|
120
|
+
const result = await handleToolCall(toolName, toolArgs, dataDb, codegraphDb);
|
|
121
|
+
return { jsonrpc: '2.0', id: id ?? null, result };
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err instanceof CodegraphDbNotInitializedError) {
|
|
124
|
+
return {
|
|
125
|
+
jsonrpc: '2.0',
|
|
126
|
+
id: id ?? null,
|
|
127
|
+
error: {
|
|
128
|
+
code: -32001,
|
|
129
|
+
message: 'Tool requires CodeGraph database which is not initialized for this repo',
|
|
130
|
+
data: {
|
|
131
|
+
remedy: 'npx @colbymchenry/codegraph@0.7.4 init . && npx @colbymchenry/codegraph@0.7.4 index .',
|
|
132
|
+
codegraphDbPath: err.dbPath,
|
|
133
|
+
tool: toolName,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (err instanceof UnknownToolError) {
|
|
139
|
+
return {
|
|
140
|
+
jsonrpc: '2.0',
|
|
141
|
+
id: id ?? null,
|
|
142
|
+
error: {
|
|
143
|
+
code: -32602,
|
|
144
|
+
message: `Unknown tool: ${err.toolName}`,
|
|
145
|
+
data: {
|
|
146
|
+
remedy: 'Tool not registered in TOOL_DB_NEEDS manifest. See packages/core/src/tool-db-needs.ts.',
|
|
147
|
+
tool: toolName,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case 'ping': {
|
|
157
|
+
return { jsonrpc: '2.0', id: id ?? null, result: {} };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
default: {
|
|
161
|
+
return {
|
|
162
|
+
jsonrpc: '2.0',
|
|
163
|
+
id: id ?? null,
|
|
164
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function processLine(line: string): Promise<ProcessLineResult | null> {
|
|
171
|
+
const trimmed = line.trim();
|
|
172
|
+
if (!trimmed) return null;
|
|
173
|
+
|
|
174
|
+
let request: JsonRpcRequest;
|
|
175
|
+
try {
|
|
176
|
+
request = JSON.parse(trimmed) as JsonRpcRequest;
|
|
177
|
+
} catch (parseError) {
|
|
178
|
+
// JSON-RPC §5.1: parse failure → -32700 + id:null (no id is extractable).
|
|
179
|
+
return {
|
|
180
|
+
response: {
|
|
181
|
+
jsonrpc: '2.0',
|
|
182
|
+
id: null,
|
|
183
|
+
error: {
|
|
184
|
+
code: -32700,
|
|
185
|
+
message: `Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
emit: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const response = await handleRequest(request);
|
|
194
|
+
// Notifications (no id) MUST NOT receive a response per JSON-RPC §4.1.
|
|
195
|
+
return { response, emit: request.id !== undefined };
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// Request-processing failure: -32603 with the request id preserved.
|
|
198
|
+
// Specific subclasses (-32001/-32602) are handled inside tools/call.
|
|
199
|
+
return {
|
|
200
|
+
response: {
|
|
201
|
+
jsonrpc: '2.0',
|
|
202
|
+
id: request.id ?? null,
|
|
203
|
+
error: {
|
|
204
|
+
code: -32603,
|
|
205
|
+
message: `Internal error: ${error instanceof Error ? error.message : String(error)}`,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
emit: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function closeCachedDbs(): void {
|
|
214
|
+
if (codegraphDbCache) {
|
|
215
|
+
codegraphDbCache.close();
|
|
216
|
+
codegraphDbCache = null;
|
|
217
|
+
}
|
|
218
|
+
if (dataDbCache) {
|
|
219
|
+
dataDbCache.close();
|
|
220
|
+
dataDbCache = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { handleRequest, processLine, closeCachedDbs };
|
|
225
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -14,11 +14,9 @@
|
|
|
14
14
|
import { readFileSync } from 'fs';
|
|
15
15
|
import { resolve, dirname } from 'path';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
|
-
import { getCodeGraphDb, getDataDb } from './db.ts';
|
|
18
|
-
import { getConfig } from './config.ts';
|
|
19
|
-
import { getToolDefinitions, handleToolCall } from './tools.ts';
|
|
20
17
|
import { getMemoryDb, pruneOldConversationTurns, pruneOldObservations } from './memory-db.ts';
|
|
21
18
|
import { getCurrentTier } from './license.ts';
|
|
19
|
+
import { createDispatcher } from './server-dispatch.ts';
|
|
22
20
|
|
|
23
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
22
|
const PKG_VERSION = (() => {
|
|
@@ -30,92 +28,7 @@ const PKG_VERSION = (() => {
|
|
|
30
28
|
}
|
|
31
29
|
})();
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
jsonrpc: '2.0';
|
|
35
|
-
id?: number | string;
|
|
36
|
-
method: string;
|
|
37
|
-
params?: Record<string, unknown>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface JsonRpcResponse {
|
|
41
|
-
jsonrpc: '2.0';
|
|
42
|
-
id: number | string | null;
|
|
43
|
-
result?: unknown;
|
|
44
|
-
error?: { code: number; message: string; data?: unknown };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Server state
|
|
48
|
-
let codegraphDb: ReturnType<typeof getCodeGraphDb> | null = null;
|
|
49
|
-
let dataDb: ReturnType<typeof getDataDb> | null = null;
|
|
50
|
-
|
|
51
|
-
function getDb() {
|
|
52
|
-
if (!codegraphDb) codegraphDb = getCodeGraphDb();
|
|
53
|
-
if (!dataDb) dataDb = getDataDb();
|
|
54
|
-
return { codegraphDb, dataDb: dataDb };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
58
|
-
const { method, params, id } = request;
|
|
59
|
-
|
|
60
|
-
switch (method) {
|
|
61
|
-
case 'initialize': {
|
|
62
|
-
return {
|
|
63
|
-
jsonrpc: '2.0',
|
|
64
|
-
id: id ?? null,
|
|
65
|
-
result: {
|
|
66
|
-
protocolVersion: '2024-11-05',
|
|
67
|
-
capabilities: {
|
|
68
|
-
tools: {},
|
|
69
|
-
},
|
|
70
|
-
serverInfo: {
|
|
71
|
-
name: getConfig().toolPrefix || 'massu',
|
|
72
|
-
version: PKG_VERSION,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
case 'notifications/initialized': {
|
|
79
|
-
// Client acknowledges initialization - no response needed for notifications
|
|
80
|
-
return { jsonrpc: '2.0', id: id ?? null, result: {} };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
case 'tools/list': {
|
|
84
|
-
const tools = getToolDefinitions();
|
|
85
|
-
return {
|
|
86
|
-
jsonrpc: '2.0',
|
|
87
|
-
id: id ?? null,
|
|
88
|
-
result: { tools },
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
case 'tools/call': {
|
|
93
|
-
const toolName = (params as { name: string })?.name;
|
|
94
|
-
const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
|
|
95
|
-
|
|
96
|
-
const { codegraphDb: cgDb, dataDb: lDb } = getDb();
|
|
97
|
-
const result = await handleToolCall(toolName, toolArgs, lDb, cgDb);
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
jsonrpc: '2.0',
|
|
101
|
-
id: id ?? null,
|
|
102
|
-
result,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
case 'ping': {
|
|
107
|
-
return { jsonrpc: '2.0', id: id ?? null, result: {} };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
default: {
|
|
111
|
-
return {
|
|
112
|
-
jsonrpc: '2.0',
|
|
113
|
-
id: id ?? null,
|
|
114
|
-
error: { code: -32601, message: `Method not found: ${method}` },
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
31
|
+
const dispatcher = createDispatcher({ serverInfoVersion: PKG_VERSION });
|
|
119
32
|
|
|
120
33
|
// === Startup: prune stale memory data (non-blocking) ===
|
|
121
34
|
|
|
@@ -167,38 +80,20 @@ process.stdin.on('data', async (chunk: string) => {
|
|
|
167
80
|
// Process complete messages (newline-delimited JSON-RPC)
|
|
168
81
|
let newlineIndex: number;
|
|
169
82
|
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
170
|
-
const line = buffer.slice(0, newlineIndex)
|
|
83
|
+
const line = buffer.slice(0, newlineIndex);
|
|
171
84
|
buffer = buffer.slice(newlineIndex + 1);
|
|
172
85
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const request = JSON.parse(line) as JsonRpcRequest;
|
|
177
|
-
const response = await handleRequest(request);
|
|
178
|
-
|
|
179
|
-
// Don't send responses for notifications (no id)
|
|
180
|
-
if (request.id !== undefined) {
|
|
181
|
-
const responseStr = JSON.stringify(response);
|
|
182
|
-
process.stdout.write(responseStr + '\n');
|
|
183
|
-
}
|
|
184
|
-
} catch (error) {
|
|
185
|
-
const errorResponse: JsonRpcResponse = {
|
|
186
|
-
jsonrpc: '2.0',
|
|
187
|
-
id: null,
|
|
188
|
-
error: {
|
|
189
|
-
code: -32700,
|
|
190
|
-
message: `Parse error: ${error instanceof Error ? error.message : String(error)}`,
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
process.stdout.write(JSON.stringify(errorResponse) + '\n');
|
|
86
|
+
const result = await dispatcher.processLine(line);
|
|
87
|
+
if (result && result.emit) {
|
|
88
|
+
process.stdout.write(JSON.stringify(result.response) + '\n');
|
|
194
89
|
}
|
|
195
90
|
}
|
|
196
91
|
});
|
|
197
92
|
|
|
198
93
|
process.stdin.on('end', () => {
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
94
|
+
// Close cached CodeGraph + Data connections. Memory + Knowledge are
|
|
95
|
+
// per-call (closed inside their routing branches in tools.ts).
|
|
96
|
+
dispatcher.closeCachedDbs();
|
|
202
97
|
process.exit(0);
|
|
203
98
|
});
|
|
204
99
|
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-tool SQLite database dependency manifest.
|
|
6
|
+
*
|
|
7
|
+
* **Role**: SOLE source of truth declaring which SQLite connections each MCP
|
|
8
|
+
* tool needs. The dispatcher (`server.ts` → `tools.ts:handleToolCall`) reads
|
|
9
|
+
* this map to lazy-resolve connections, opening ONLY the DBs a tool requires.
|
|
10
|
+
*
|
|
11
|
+
* **Why this exists**:
|
|
12
|
+
* Before plan `plan-1.6.2-server-lazy-db-deps`, the dispatcher eagerly opened
|
|
13
|
+
* BOTH CodeGraph + Data DBs on every tool/call (see legacy `server.ts:51-55,96`
|
|
14
|
+
* and `tools.ts:279`). When `.codegraph/codegraph.db` was missing, EVERY tool
|
|
15
|
+
* call failed — even memory/audit/knowledge tools that have no codegraph
|
|
16
|
+
* dependency. This manifest makes that bug class structurally impossible:
|
|
17
|
+
* a missing peripheral DB only blocks the tools that need it.
|
|
18
|
+
*
|
|
19
|
+
* **Structural drift-prevention (3 layers)**:
|
|
20
|
+
* - L1: TypeScript compile time — exhaustiveness check via `keyof TOOL_DB_NEEDS`.
|
|
21
|
+
* - L2: `tool-db-needs-completeness.test.ts` — TypeScript AST walk of every
|
|
22
|
+
* tool module verifies declared needs match actual DB access pattern.
|
|
23
|
+
* Aliasing/destructuring renames cannot bypass the AST walk.
|
|
24
|
+
* - L3: `scripts/massu-pattern-scanner.sh` Check 14 — grep-level enforcement
|
|
25
|
+
* that every tool in `getToolDefinitions()` has a manifest entry.
|
|
26
|
+
*
|
|
27
|
+
* **Adding a new MCP tool**:
|
|
28
|
+
* 1. Register in `tools.ts` (CR-11).
|
|
29
|
+
* 2. Add an entry here. Missing entries throw `UnknownToolError` at first
|
|
30
|
+
* dispatch AND fail L2 + L3 above.
|
|
31
|
+
*
|
|
32
|
+
* @see `docs/plans/2026-05-10-server-lazy-db-deps.md` (`plan-1.6.2-server-lazy-db-deps`)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** SQLite connections the MCP server can resolve for a tool call. */
|
|
36
|
+
export type DbNeed = 'codegraph' | 'data' | 'memory' | 'knowledge';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Custom error thrown when `getToolDbNeeds()` is called with a tool name that
|
|
40
|
+
* isn't in the manifest. Caught at the JSON-RPC layer and translated to a
|
|
41
|
+
* structured `-32602` (Invalid params) error to the client.
|
|
42
|
+
*/
|
|
43
|
+
export class UnknownToolError extends Error {
|
|
44
|
+
readonly toolName: string;
|
|
45
|
+
constructor(toolName: string) {
|
|
46
|
+
super(`Tool not registered in TOOL_DB_NEEDS manifest: ${toolName}. Add an entry to packages/core/src/tool-db-needs.ts.`);
|
|
47
|
+
this.name = 'UnknownToolError';
|
|
48
|
+
this.toolName = toolName;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Per-tool DB-need declarations. Keys are tool SHORT-NAMES (without the
|
|
54
|
+
* `${toolPrefix}_` prefix). Values are the SQLite connections the handler
|
|
55
|
+
* (or its routed module) actually accesses.
|
|
56
|
+
*
|
|
57
|
+
* Sourced from exhaustive grep of `packages/core/src/{*-tools,analytics,
|
|
58
|
+
* cost-tracker,prompt-analyzer,audit-trail,validation-engine,adr-generator,
|
|
59
|
+
* security-scorer,dependency-scorer,team-knowledge,regression-detector,
|
|
60
|
+
* python-tools,license}.ts` on 2026-05-10. Verified line citations in
|
|
61
|
+
* `docs/plans/2026-05-10-server-lazy-db-deps.md §1.4`.
|
|
62
|
+
*/
|
|
63
|
+
export const TOOL_DB_NEEDS = {
|
|
64
|
+
// === Core code-intel tools (tools.ts:393-406) ===
|
|
65
|
+
// Use CodeGraph DB (read-only AST) + Data DB (Massu's import/trpc/sentinel
|
|
66
|
+
// tables). All call `ensureIndexes` directly or via shared infrastructure.
|
|
67
|
+
sync: ['codegraph', 'data'],
|
|
68
|
+
context: ['codegraph', 'data'],
|
|
69
|
+
coupling_check: ['codegraph', 'data'],
|
|
70
|
+
impact: ['codegraph', 'data'],
|
|
71
|
+
domains: ['codegraph', 'data'],
|
|
72
|
+
|
|
73
|
+
// `trpc_map` reads only Data DB (tRPC index lives there); no CodeGraph access.
|
|
74
|
+
trpc_map: ['data'],
|
|
75
|
+
|
|
76
|
+
// `schema` reads filesystem (Prisma schema files); no DB access at all.
|
|
77
|
+
schema: [],
|
|
78
|
+
|
|
79
|
+
// === Memory tools (memory-tools.ts) ===
|
|
80
|
+
// Routed via `name.startsWith(pfx + '_memory_')` at tools.ts:284-290.
|
|
81
|
+
// Handler opens memory DB per-call (with try/finally close).
|
|
82
|
+
memory_search: ['memory'],
|
|
83
|
+
memory_timeline: ['memory'],
|
|
84
|
+
memory_detail: ['memory'],
|
|
85
|
+
memory_sessions: ['memory'],
|
|
86
|
+
memory_failures: ['memory'],
|
|
87
|
+
memory_ingest: ['memory'],
|
|
88
|
+
memory_backfill: ['memory'],
|
|
89
|
+
|
|
90
|
+
// === Observability tools (observability-tools.ts) ===
|
|
91
|
+
// Routed via `isObservabilityTool(name)` at tools.ts:294-300. Memory DB only.
|
|
92
|
+
session_replay: ['memory'],
|
|
93
|
+
session_stats: ['memory'],
|
|
94
|
+
tool_patterns: ['memory'],
|
|
95
|
+
prompt_analysis: ['memory'],
|
|
96
|
+
|
|
97
|
+
// === Docs tools (docs-tools.ts) ===
|
|
98
|
+
// Routed via `name.startsWith(pfx + '_docs_')` at tools.ts:303-306.
|
|
99
|
+
// No DB access — pure filesystem traversal.
|
|
100
|
+
docs_audit: [],
|
|
101
|
+
docs_coverage: [],
|
|
102
|
+
|
|
103
|
+
// === Sentinel registry tools (sentinel-tools.ts:180-184) ===
|
|
104
|
+
// Routed via `name.startsWith(pfx + '_sentinel_')` at tools.ts:308.
|
|
105
|
+
// Handler signature: `(name, args, dataDb)` — Data DB only.
|
|
106
|
+
sentinel_register: ['data'],
|
|
107
|
+
sentinel_validate: ['data'],
|
|
108
|
+
sentinel_search: ['data'],
|
|
109
|
+
sentinel_detail: ['data'],
|
|
110
|
+
sentinel_impact: ['data'],
|
|
111
|
+
sentinel_parity: ['data'],
|
|
112
|
+
|
|
113
|
+
// === Knowledge layer tools (knowledge-tools.ts) ===
|
|
114
|
+
// Routed via `isKnowledgeTool(name)` at tools.ts:372-376. Primary DB is
|
|
115
|
+
// `knowledgeDb` (separate SQLite file). Handlers ALSO call `getDataDb()`
|
|
116
|
+
// (knowledge-tools.ts:1187,1275) and `getMemoryDb()` (knowledge-tools.ts:1332)
|
|
117
|
+
// for cross-DB joins — declare all three so the AST completeness test
|
|
118
|
+
// (P-B-002) verifies the full access pattern.
|
|
119
|
+
knowledge_search: ['knowledge', 'data', 'memory'],
|
|
120
|
+
knowledge_pattern: ['knowledge', 'data', 'memory'],
|
|
121
|
+
knowledge_rule: ['knowledge', 'data', 'memory'],
|
|
122
|
+
knowledge_correct: ['knowledge', 'data', 'memory'],
|
|
123
|
+
knowledge_incident: ['knowledge', 'data', 'memory'],
|
|
124
|
+
knowledge_plan: ['knowledge', 'data', 'memory'],
|
|
125
|
+
knowledge_command: ['knowledge', 'data', 'memory'],
|
|
126
|
+
knowledge_gaps: ['knowledge', 'data', 'memory'],
|
|
127
|
+
knowledge_verification: ['knowledge', 'data', 'memory'],
|
|
128
|
+
knowledge_effectiveness: ['knowledge', 'data', 'memory'],
|
|
129
|
+
knowledge_graph: ['knowledge', 'data', 'memory'],
|
|
130
|
+
knowledge_schema_check: ['knowledge', 'data', 'memory'],
|
|
131
|
+
|
|
132
|
+
// === Analytics / quality (analytics.ts) ===
|
|
133
|
+
// Routed via `isAnalyticsTool(name)`. Memory DB only.
|
|
134
|
+
quality_score: ['memory'],
|
|
135
|
+
quality_report: ['memory'],
|
|
136
|
+
quality_trend: ['memory'],
|
|
137
|
+
|
|
138
|
+
// === Cost tracker (cost-tracker.ts) ===
|
|
139
|
+
cost_session: ['memory'],
|
|
140
|
+
cost_feature: ['memory'],
|
|
141
|
+
cost_trend: ['memory'],
|
|
142
|
+
|
|
143
|
+
// === Prompt analyzer (prompt-analyzer.ts) ===
|
|
144
|
+
prompt_effectiveness: ['memory'],
|
|
145
|
+
prompt_suggestions: ['memory'],
|
|
146
|
+
|
|
147
|
+
// === Audit trail (audit-trail.ts) ===
|
|
148
|
+
audit_chain: ['memory'],
|
|
149
|
+
audit_log: ['memory'],
|
|
150
|
+
audit_report: ['memory'],
|
|
151
|
+
|
|
152
|
+
// === Validation engine (validation-engine.ts) ===
|
|
153
|
+
validation_check: ['memory'],
|
|
154
|
+
validation_report: ['memory'],
|
|
155
|
+
|
|
156
|
+
// === ADR generator (adr-generator.ts) ===
|
|
157
|
+
adr_create: ['memory'],
|
|
158
|
+
adr_list: ['memory'],
|
|
159
|
+
adr_detail: ['memory'],
|
|
160
|
+
|
|
161
|
+
// === Security scorer (security-scorer.ts) ===
|
|
162
|
+
security_score: ['memory'],
|
|
163
|
+
security_heatmap: ['memory'],
|
|
164
|
+
security_trend: ['memory'],
|
|
165
|
+
|
|
166
|
+
// === Dependency scorer (dependency-scorer.ts) ===
|
|
167
|
+
dep_score: ['memory'],
|
|
168
|
+
dep_alternatives: ['memory'],
|
|
169
|
+
|
|
170
|
+
// === Team knowledge (team-knowledge.ts) ===
|
|
171
|
+
team_expertise: ['memory'],
|
|
172
|
+
team_conflicts: ['memory'],
|
|
173
|
+
team_search: ['memory'],
|
|
174
|
+
|
|
175
|
+
// === Regression detector (regression-detector.ts) ===
|
|
176
|
+
regression_risk: ['memory'],
|
|
177
|
+
feature_health: ['memory'],
|
|
178
|
+
|
|
179
|
+
// === Python code-intel tools (python-tools.ts) ===
|
|
180
|
+
// Routed via `isPythonTool(name)` at tools.ts:379-381. Data DB only.
|
|
181
|
+
py_imports: ['data'],
|
|
182
|
+
py_routes: ['data'],
|
|
183
|
+
py_models: ['data'],
|
|
184
|
+
py_migrations: ['data'],
|
|
185
|
+
py_coupling: ['data'],
|
|
186
|
+
py_context: ['data'],
|
|
187
|
+
py_impact: ['data'],
|
|
188
|
+
py_domains: ['data'],
|
|
189
|
+
|
|
190
|
+
// === License tool (license.ts) ===
|
|
191
|
+
license_status: ['memory'],
|
|
192
|
+
} as const satisfies Readonly<Record<string, readonly DbNeed[]>>;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Configured tool-prefix-stripping helper. Pulled from the runtime config
|
|
196
|
+
* so this module stays project-prefix-agnostic.
|
|
197
|
+
*/
|
|
198
|
+
function stripConfiguredPrefix(toolName: string, prefix: string): string {
|
|
199
|
+
const pfx = `${prefix}_`;
|
|
200
|
+
return toolName.startsWith(pfx) ? toolName.slice(pfx.length) : toolName;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Look up the DB needs for a tool by its full name (with prefix). Strips the
|
|
205
|
+
* configured prefix and consults `TOOL_DB_NEEDS`. Throws `UnknownToolError`
|
|
206
|
+
* for tool names not in the manifest — the dispatcher MUST catch this and
|
|
207
|
+
* translate to a structured JSON-RPC error.
|
|
208
|
+
*
|
|
209
|
+
* @param toolName Full tool name including prefix (e.g., `"massu_memory_search"`)
|
|
210
|
+
* @param prefix Tool prefix (e.g., `"massu"`) — read from config at dispatch time
|
|
211
|
+
* @returns Array of DB connections the tool requires (may be empty)
|
|
212
|
+
* @throws {UnknownToolError} if `stripPrefix(toolName)` not in the manifest
|
|
213
|
+
*/
|
|
214
|
+
export function getToolDbNeeds(toolName: string, prefix: string): readonly DbNeed[] {
|
|
215
|
+
const shortName = stripConfiguredPrefix(toolName, prefix);
|
|
216
|
+
const needs = (TOOL_DB_NEEDS as Record<string, readonly DbNeed[]>)[shortName];
|
|
217
|
+
if (needs === undefined) {
|
|
218
|
+
throw new UnknownToolError(toolName);
|
|
219
|
+
}
|
|
220
|
+
return needs;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Convenience predicate: does a tool need CodeGraph DB? */
|
|
224
|
+
export function toolNeedsCodegraph(toolName: string, prefix: string): boolean {
|
|
225
|
+
return getToolDbNeeds(toolName, prefix).includes('codegraph');
|
|
226
|
+
}
|