mdkg 0.3.4 → 0.3.6
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/CHANGELOG.md +69 -2
- package/CLI_COMMAND_MATRIX.md +73 -2
- package/README.md +39 -1
- package/dist/cli.js +178 -1
- package/dist/command-contract.json +506 -2
- package/dist/commands/graph.js +704 -0
- package/dist/commands/mcp.js +647 -0
- package/dist/commands/validate.js +16 -11
- package/dist/init/CLI_COMMAND_MATRIX.md +28 -0
- package/dist/init/README.md +26 -1
- package/dist/init/init-manifest.json +3 -3
- package/dist/util/argparse.js +4 -0
- package/package.json +4 -2
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.handleMcpRequest = handleMcpRequest;
|
|
7
|
+
exports.handleMcpMessage = handleMcpMessage;
|
|
8
|
+
exports.runMcpServeCommand = runMcpServeCommand;
|
|
9
|
+
const readline_1 = __importDefault(require("readline"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const status_1 = require("./status");
|
|
13
|
+
const validate_1 = require("./validate");
|
|
14
|
+
const config_1 = require("../core/config");
|
|
15
|
+
const version_1 = require("../core/version");
|
|
16
|
+
const index_cache_1 = require("../graph/index_cache");
|
|
17
|
+
const node_body_1 = require("../graph/node_body");
|
|
18
|
+
const goal_scope_1 = require("../graph/goal_scope");
|
|
19
|
+
const node_1 = require("../graph/node");
|
|
20
|
+
const headings_1 = require("../templates/headings");
|
|
21
|
+
const pack_1 = require("../pack/pack");
|
|
22
|
+
const budget_1 = require("../pack/budget");
|
|
23
|
+
const profile_1 = require("../pack/profile");
|
|
24
|
+
const filter_1 = require("../util/filter");
|
|
25
|
+
const errors_1 = require("../util/errors");
|
|
26
|
+
const qid_1 = require("../util/qid");
|
|
27
|
+
const sort_1 = require("../util/sort");
|
|
28
|
+
const query_output_1 = require("./query_output");
|
|
29
|
+
const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
30
|
+
const SERVER_NAME = "mdkg";
|
|
31
|
+
const MAX_SEARCH_LIMIT = 100;
|
|
32
|
+
const DEFAULT_SEARCH_LIMIT = 20;
|
|
33
|
+
const MAX_PACK_NODES = 100;
|
|
34
|
+
const DEFAULT_PACK_NODES = 20;
|
|
35
|
+
const DEFAULT_PACK_MAX_CHARS = 120000;
|
|
36
|
+
function objectSchema(properties, required = []) {
|
|
37
|
+
return {
|
|
38
|
+
type: "object",
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
properties,
|
|
41
|
+
required,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const stringProp = (description) => ({ type: "string", description });
|
|
45
|
+
const numberProp = (description) => ({ type: "integer", description });
|
|
46
|
+
const booleanProp = (description) => ({ type: "boolean", description });
|
|
47
|
+
const MCP_TOOLS = [
|
|
48
|
+
{
|
|
49
|
+
name: "mdkg_status",
|
|
50
|
+
title: "mdkg status",
|
|
51
|
+
description: "Return read-only mdkg operator status for the selected root.",
|
|
52
|
+
inputSchema: objectSchema({}),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "mdkg_workspace_list",
|
|
56
|
+
title: "mdkg workspace list",
|
|
57
|
+
description: "List root workspaces and configured read-only subgraph aliases.",
|
|
58
|
+
inputSchema: objectSchema({}),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "mdkg_search",
|
|
62
|
+
title: "mdkg search",
|
|
63
|
+
description: "Search mdkg graph metadata across local and read-only subgraph indexes.",
|
|
64
|
+
inputSchema: objectSchema({
|
|
65
|
+
query: stringProp("Search query."),
|
|
66
|
+
ws: stringProp("Optional workspace or subgraph alias."),
|
|
67
|
+
type: stringProp("Optional node type filter."),
|
|
68
|
+
status: stringProp("Optional node status filter."),
|
|
69
|
+
limit: numberProp("Maximum result count, capped at 100."),
|
|
70
|
+
}, ["query"]),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "mdkg_show",
|
|
74
|
+
title: "mdkg show",
|
|
75
|
+
description: "Show one mdkg node by local id, qid, or subgraph qid.",
|
|
76
|
+
inputSchema: objectSchema({
|
|
77
|
+
id: stringProp("Node id or qid."),
|
|
78
|
+
ws: stringProp("Optional workspace or subgraph alias hint."),
|
|
79
|
+
meta_only: booleanProp("Return metadata without Markdown body."),
|
|
80
|
+
}, ["id"]),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "mdkg_pack",
|
|
84
|
+
title: "mdkg pack",
|
|
85
|
+
description: "Build a bounded in-memory context pack without writing .mdkg/pack files.",
|
|
86
|
+
inputSchema: objectSchema({
|
|
87
|
+
id: stringProp("Pack root id or qid."),
|
|
88
|
+
ws: stringProp("Optional workspace or subgraph alias hint."),
|
|
89
|
+
profile: stringProp("Pack profile: standard, concise, or headers. Defaults to concise."),
|
|
90
|
+
max_nodes: numberProp("Maximum node count, capped at 100."),
|
|
91
|
+
max_chars: numberProp("Maximum estimated character budget. Defaults to 120000."),
|
|
92
|
+
}, ["id"]),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "mdkg_goal_current",
|
|
96
|
+
title: "mdkg goal current",
|
|
97
|
+
description: "Return the selected local goal or the unique active local root goal fallback.",
|
|
98
|
+
inputSchema: objectSchema({ ws: stringProp("Optional local workspace alias.") }),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "mdkg_goal_next",
|
|
102
|
+
title: "mdkg goal next",
|
|
103
|
+
description: "Return the next actionable local node inside a goal scope without mutating active_node.",
|
|
104
|
+
inputSchema: objectSchema({
|
|
105
|
+
id: stringProp("Optional goal id or qid. Defaults to selected or unique active goal."),
|
|
106
|
+
ws: stringProp("Optional local workspace alias."),
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "mdkg_validate",
|
|
111
|
+
title: "mdkg validate",
|
|
112
|
+
description: "Return the read-only validation receipt for the selected root.",
|
|
113
|
+
inputSchema: objectSchema({}),
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
function toolDescription(tool) {
|
|
117
|
+
return {
|
|
118
|
+
name: tool.name,
|
|
119
|
+
title: tool.title,
|
|
120
|
+
description: tool.description,
|
|
121
|
+
inputSchema: tool.inputSchema,
|
|
122
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
123
|
+
annotations: {
|
|
124
|
+
readOnlyHint: true,
|
|
125
|
+
destructiveHint: false,
|
|
126
|
+
idempotentHint: true,
|
|
127
|
+
openWorldHint: false,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function asObject(value) {
|
|
132
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function requireString(args, key) {
|
|
138
|
+
const value = args[key];
|
|
139
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
140
|
+
throw new errors_1.UsageError(`${key} is required and must be a non-empty string`);
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
function optionalString(args, key) {
|
|
145
|
+
const value = args[key];
|
|
146
|
+
if (value === undefined || value === null) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
150
|
+
throw new errors_1.UsageError(`${key} must be a non-empty string`);
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function optionalBoolean(args, key) {
|
|
155
|
+
const value = args[key];
|
|
156
|
+
if (value === undefined || value === null) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
if (typeof value !== "boolean") {
|
|
160
|
+
throw new errors_1.UsageError(`${key} must be a boolean`);
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
function optionalInteger(args, key) {
|
|
165
|
+
const value = args[key];
|
|
166
|
+
if (value === undefined || value === null) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
170
|
+
throw new errors_1.UsageError(`${key} must be an integer`);
|
|
171
|
+
}
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
function clampLimit(raw, fallback, max) {
|
|
175
|
+
if (raw === undefined) {
|
|
176
|
+
return fallback;
|
|
177
|
+
}
|
|
178
|
+
if (raw < 1) {
|
|
179
|
+
throw new errors_1.UsageError("limit values must be positive integers");
|
|
180
|
+
}
|
|
181
|
+
return Math.min(raw, max);
|
|
182
|
+
}
|
|
183
|
+
function normalizeWorkspace(value) {
|
|
184
|
+
if (!value || value === "all") {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
return value.toLowerCase();
|
|
188
|
+
}
|
|
189
|
+
function loadReadOnlyIndex(root) {
|
|
190
|
+
const config = (0, config_1.loadConfig)(root);
|
|
191
|
+
const { index } = (0, index_cache_1.loadIndex)({
|
|
192
|
+
root,
|
|
193
|
+
config,
|
|
194
|
+
useCache: false,
|
|
195
|
+
allowReindex: false,
|
|
196
|
+
includeImports: true,
|
|
197
|
+
});
|
|
198
|
+
return { config, index };
|
|
199
|
+
}
|
|
200
|
+
function ensureKnownWorkspace(config, ws) {
|
|
201
|
+
if (!ws) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
if (!config.workspaces[ws] && !config.subgraphs[ws]) {
|
|
205
|
+
throw new errors_1.NotFoundError(`workspace or subgraph not found: ${ws}`);
|
|
206
|
+
}
|
|
207
|
+
return ws;
|
|
208
|
+
}
|
|
209
|
+
function buildSearchText(node) {
|
|
210
|
+
const attributeTokens = Object.entries(node.attributes ?? {}).flatMap(([key, value]) => {
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
return [key, ...value.map((item) => String(item))];
|
|
213
|
+
}
|
|
214
|
+
return [key, String(value)];
|
|
215
|
+
});
|
|
216
|
+
return [
|
|
217
|
+
node.id,
|
|
218
|
+
node.qid,
|
|
219
|
+
node.type,
|
|
220
|
+
node.title,
|
|
221
|
+
node.path,
|
|
222
|
+
...node.tags,
|
|
223
|
+
...node.owners,
|
|
224
|
+
...node.links,
|
|
225
|
+
...node.artifacts,
|
|
226
|
+
...node.refs,
|
|
227
|
+
...node.aliases,
|
|
228
|
+
...node.skills,
|
|
229
|
+
...attributeTokens,
|
|
230
|
+
]
|
|
231
|
+
.join(" ")
|
|
232
|
+
.toLowerCase();
|
|
233
|
+
}
|
|
234
|
+
function matchesSearch(node, terms) {
|
|
235
|
+
const haystack = buildSearchText(node);
|
|
236
|
+
return terms.every((term) => haystack.includes(term));
|
|
237
|
+
}
|
|
238
|
+
function selectedGoalState(root) {
|
|
239
|
+
const filePath = path_1.default.join(root, ".mdkg", "state", "selected-goal.json");
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
|
|
242
|
+
if (typeof parsed.qid === "string" &&
|
|
243
|
+
typeof parsed.id === "string" &&
|
|
244
|
+
typeof parsed.ws === "string" &&
|
|
245
|
+
typeof parsed.selected_at === "string") {
|
|
246
|
+
return {
|
|
247
|
+
qid: parsed.qid.toLowerCase(),
|
|
248
|
+
id: parsed.id.toLowerCase(),
|
|
249
|
+
ws: parsed.ws.toLowerCase(),
|
|
250
|
+
selected_at: parsed.selected_at,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
function activeGoalCandidates(index, ws) {
|
|
260
|
+
return Object.values(index.nodes)
|
|
261
|
+
.filter((node) => node.type === "goal")
|
|
262
|
+
.filter((node) => !node.source?.imported)
|
|
263
|
+
.filter((node) => (ws ? node.ws === ws : true))
|
|
264
|
+
.filter((node) => node.status === "progress" && node.attributes.goal_state === "active")
|
|
265
|
+
.sort((a, b) => a.qid.localeCompare(b.qid));
|
|
266
|
+
}
|
|
267
|
+
function isArchivedGoal(node) {
|
|
268
|
+
return Boolean(node &&
|
|
269
|
+
node.type === "goal" &&
|
|
270
|
+
(node.status === "archived" || node.attributes.goal_state === "archived"));
|
|
271
|
+
}
|
|
272
|
+
function resolveGoal(root, index, idOrQid, ws) {
|
|
273
|
+
const warnings = [];
|
|
274
|
+
if (idOrQid) {
|
|
275
|
+
const resolved = (0, qid_1.resolveQid)(index, idOrQid, ws);
|
|
276
|
+
if (resolved.status !== "ok") {
|
|
277
|
+
throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("goal", idOrQid, resolved, ws));
|
|
278
|
+
}
|
|
279
|
+
const node = index.nodes[resolved.qid];
|
|
280
|
+
if (!node || node.type !== "goal") {
|
|
281
|
+
throw new errors_1.UsageError(`mdkg_goal_next requires a goal node; got ${idOrQid}`);
|
|
282
|
+
}
|
|
283
|
+
return { source: "explicit", node, warnings };
|
|
284
|
+
}
|
|
285
|
+
const selected = selectedGoalState(root);
|
|
286
|
+
if (selected) {
|
|
287
|
+
const node = index.nodes[selected.qid];
|
|
288
|
+
if (node && (!ws || node.ws === ws)) {
|
|
289
|
+
return { source: "selected", node, warnings };
|
|
290
|
+
}
|
|
291
|
+
warnings.push("selected goal is missing or outside the requested workspace");
|
|
292
|
+
}
|
|
293
|
+
const active = activeGoalCandidates(index, ws);
|
|
294
|
+
if (active.length === 1) {
|
|
295
|
+
return { source: "unique_active", node: active[0], warnings };
|
|
296
|
+
}
|
|
297
|
+
if (active.length > 1) {
|
|
298
|
+
throw new errors_1.UsageError(`multiple active local goals found: ${active.map((node) => node.qid).join(", ")}`);
|
|
299
|
+
}
|
|
300
|
+
throw new errors_1.NotFoundError("no selected or active local goal found");
|
|
301
|
+
}
|
|
302
|
+
function isConcreteGoalCandidate(node, statusRanks) {
|
|
303
|
+
if (node.source?.imported) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (!goal_scope_1.GOAL_SCOPE_ACTIONABLE_TYPES.has(node.type)) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
if (!node.status || !statusRanks.has(node.status)) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
return node.status !== "done" && node.status !== "archived";
|
|
313
|
+
}
|
|
314
|
+
function toolResult(payload, isError = false) {
|
|
315
|
+
return {
|
|
316
|
+
content: [
|
|
317
|
+
{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: JSON.stringify(payload, null, 2),
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
structuredContent: payload,
|
|
323
|
+
isError,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function listWorkspaces(root) {
|
|
327
|
+
const config = (0, config_1.loadConfig)(root);
|
|
328
|
+
return {
|
|
329
|
+
action: "mcp.workspace_list",
|
|
330
|
+
root,
|
|
331
|
+
workspaces: Object.entries(config.workspaces)
|
|
332
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
333
|
+
.map(([alias, entry]) => ({
|
|
334
|
+
alias,
|
|
335
|
+
path: entry.path,
|
|
336
|
+
mdkg_dir: entry.mdkg_dir,
|
|
337
|
+
enabled: entry.enabled,
|
|
338
|
+
visibility: entry.visibility,
|
|
339
|
+
})),
|
|
340
|
+
subgraphs: Object.entries(config.subgraphs)
|
|
341
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
342
|
+
.map(([alias, entry]) => ({
|
|
343
|
+
alias,
|
|
344
|
+
enabled: entry.enabled,
|
|
345
|
+
visibility: entry.visibility,
|
|
346
|
+
permissions: [...entry.permissions],
|
|
347
|
+
source_path: entry.source_path,
|
|
348
|
+
source_repo: entry.source_repo,
|
|
349
|
+
sources: entry.sources.map((source) => ({
|
|
350
|
+
label: source.label,
|
|
351
|
+
path: source.path,
|
|
352
|
+
enabled: source.enabled,
|
|
353
|
+
expected_profile: source.expected_profile,
|
|
354
|
+
})),
|
|
355
|
+
})),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function searchTool(root, args) {
|
|
359
|
+
const query = requireString(args, "query");
|
|
360
|
+
const type = optionalString(args, "type")?.toLowerCase();
|
|
361
|
+
const status = optionalString(args, "status")?.toLowerCase();
|
|
362
|
+
const limit = clampLimit(optionalInteger(args, "limit"), DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT);
|
|
363
|
+
const { config, index } = loadReadOnlyIndex(root);
|
|
364
|
+
const ws = ensureKnownWorkspace(config, normalizeWorkspace(optionalString(args, "ws")));
|
|
365
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
366
|
+
const matches = (0, sort_1.sortNodesByQid)((0, filter_1.filterNodes)(Object.values(index.nodes), { ws, type, status }).filter((node) => matchesSearch(node, terms)));
|
|
367
|
+
return {
|
|
368
|
+
command: "mcp.search",
|
|
369
|
+
query,
|
|
370
|
+
count: matches.length,
|
|
371
|
+
limit,
|
|
372
|
+
truncated: matches.length > limit,
|
|
373
|
+
items: matches.slice(0, limit).map(query_output_1.toNodeSummaryJson),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function showTool(root, args) {
|
|
377
|
+
const id = requireString(args, "id");
|
|
378
|
+
const metaOnly = optionalBoolean(args, "meta_only") ?? false;
|
|
379
|
+
const { config, index } = loadReadOnlyIndex(root);
|
|
380
|
+
const ws = ensureKnownWorkspace(config, normalizeWorkspace(optionalString(args, "ws")));
|
|
381
|
+
const resolved = (0, qid_1.resolveQid)(index, id, ws);
|
|
382
|
+
if (resolved.status !== "ok") {
|
|
383
|
+
throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("id", id, resolved, ws));
|
|
384
|
+
}
|
|
385
|
+
const node = index.nodes[resolved.qid];
|
|
386
|
+
if (!node) {
|
|
387
|
+
throw new errors_1.NotFoundError(`node not found: ${id}`);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
command: "mcp.show",
|
|
391
|
+
item: (0, query_output_1.toNodeDetailJson)(node, metaOnly ? undefined : (0, node_body_1.readNodeBody)(root, node)),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function packTool(root, args) {
|
|
395
|
+
const id = requireString(args, "id");
|
|
396
|
+
const profile = optionalString(args, "profile") ?? "concise";
|
|
397
|
+
const maxNodes = clampLimit(optionalInteger(args, "max_nodes"), DEFAULT_PACK_NODES, MAX_PACK_NODES);
|
|
398
|
+
const maxChars = clampLimit(optionalInteger(args, "max_chars"), DEFAULT_PACK_MAX_CHARS, DEFAULT_PACK_MAX_CHARS);
|
|
399
|
+
const { config, index } = loadReadOnlyIndex(root);
|
|
400
|
+
const ws = ensureKnownWorkspace(config, normalizeWorkspace(optionalString(args, "ws")));
|
|
401
|
+
const resolved = (0, qid_1.resolveQid)(index, id, ws);
|
|
402
|
+
if (resolved.status !== "ok") {
|
|
403
|
+
throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("id", id, resolved, ws));
|
|
404
|
+
}
|
|
405
|
+
const resolvedProfile = (0, profile_1.resolvePackProfile)({ profile });
|
|
406
|
+
const built = (0, pack_1.buildPack)({
|
|
407
|
+
root,
|
|
408
|
+
index,
|
|
409
|
+
rootQid: resolved.qid,
|
|
410
|
+
depth: config.pack.default_depth,
|
|
411
|
+
edges: [...config.pack.default_edges],
|
|
412
|
+
verbose: false,
|
|
413
|
+
maxNodes,
|
|
414
|
+
verboseCoreListPath: path_1.default.resolve(root, config.pack.verbose_core_list_path),
|
|
415
|
+
wsHint: ws,
|
|
416
|
+
includeLatestCheckpoint: true,
|
|
417
|
+
});
|
|
418
|
+
const headingMap = resolvedProfile.bodyMode === "summary"
|
|
419
|
+
? (0, headings_1.loadTemplateHeadingMap)(root, config, Array.from(node_1.ALLOWED_TYPES))
|
|
420
|
+
: {};
|
|
421
|
+
const shaped = (0, profile_1.shapePackBodies)(built.pack, { resolved: resolvedProfile, templateHeadingMap: headingMap });
|
|
422
|
+
const budgeted = (0, budget_1.applyPackBudgets)(shaped, { maxChars }, resolvedProfile.profile).pack;
|
|
423
|
+
return {
|
|
424
|
+
command: "mcp.pack",
|
|
425
|
+
warnings: built.warnings,
|
|
426
|
+
pack: budgeted,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function goalCurrentTool(root, args) {
|
|
430
|
+
const { config, index } = loadReadOnlyIndex(root);
|
|
431
|
+
const ws = ensureKnownWorkspace(config, normalizeWorkspace(optionalString(args, "ws")));
|
|
432
|
+
const selected = selectedGoalState(root);
|
|
433
|
+
const selectedNode = selected ? index.nodes[selected.qid] : undefined;
|
|
434
|
+
if (selectedNode && (!ws || selectedNode.ws === ws)) {
|
|
435
|
+
return {
|
|
436
|
+
command: "mcp.goal_current",
|
|
437
|
+
source: "selected",
|
|
438
|
+
selected,
|
|
439
|
+
goal: (0, query_output_1.toNodeSummaryJson)(selectedNode),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const active = activeGoalCandidates(index, ws);
|
|
443
|
+
return {
|
|
444
|
+
command: "mcp.goal_current",
|
|
445
|
+
source: active.length === 1 ? "unique_active" : "none",
|
|
446
|
+
selected: selected ?? null,
|
|
447
|
+
goal: active.length === 1 ? (0, query_output_1.toNodeSummaryJson)(active[0]) : null,
|
|
448
|
+
active_count: active.length,
|
|
449
|
+
active_goals: active.map(query_output_1.toNodeSummaryJson),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function goalNextTool(root, args) {
|
|
453
|
+
const { config, index } = loadReadOnlyIndex(root);
|
|
454
|
+
const ws = ensureKnownWorkspace(config, normalizeWorkspace(optionalString(args, "ws")));
|
|
455
|
+
const loaded = resolveGoal(root, index, optionalString(args, "id"), ws);
|
|
456
|
+
const warnings = [...loaded.warnings];
|
|
457
|
+
if (isArchivedGoal(loaded.node)) {
|
|
458
|
+
warnings.push(`${loaded.node.qid} is archived and has no actionable next work`);
|
|
459
|
+
return {
|
|
460
|
+
command: "mcp.goal_next",
|
|
461
|
+
goal_source: loaded.source,
|
|
462
|
+
goal: (0, query_output_1.toNodeSummaryJson)(loaded.node),
|
|
463
|
+
node: null,
|
|
464
|
+
warnings,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const statusPreference = config.work.next.status_preference.map((status) => status.toLowerCase());
|
|
468
|
+
const statusRanks = new Set(statusPreference);
|
|
469
|
+
const scope = (0, goal_scope_1.collectGoalScope)(index, loaded.node);
|
|
470
|
+
for (const missing of scope.missingRefs) {
|
|
471
|
+
warnings.push(`scope_refs references missing node: ${missing}`);
|
|
472
|
+
}
|
|
473
|
+
for (const invalid of scope.invalidRefs) {
|
|
474
|
+
warnings.push(`scope contains non-actionable or unsupported node: ${invalid}`);
|
|
475
|
+
}
|
|
476
|
+
const activeNode = typeof loaded.node.attributes.active_node === "string"
|
|
477
|
+
? loaded.node.attributes.active_node
|
|
478
|
+
: undefined;
|
|
479
|
+
if (activeNode) {
|
|
480
|
+
const resolved = (0, qid_1.resolveQid)(index, activeNode, loaded.node.ws);
|
|
481
|
+
const node = resolved.status === "ok" ? index.nodes[resolved.qid] : undefined;
|
|
482
|
+
if (node && scope.actionableQids.has(node.qid) && isConcreteGoalCandidate(node, statusRanks)) {
|
|
483
|
+
return {
|
|
484
|
+
command: "mcp.goal_next",
|
|
485
|
+
goal_source: loaded.source,
|
|
486
|
+
goal: (0, query_output_1.toNodeSummaryJson)(loaded.node),
|
|
487
|
+
node: (0, query_output_1.toNodeSummaryJson)(node),
|
|
488
|
+
warnings,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
warnings.push(`active_node is not an actionable local concrete item: ${activeNode}`);
|
|
492
|
+
}
|
|
493
|
+
const candidates = Array.from(scope.actionableQids)
|
|
494
|
+
.map((qid) => index.nodes[qid])
|
|
495
|
+
.filter((node) => Boolean(node))
|
|
496
|
+
.filter((node) => isConcreteGoalCandidate(node, statusRanks));
|
|
497
|
+
const selected = (0, sort_1.sortNodesForNext)(candidates, {
|
|
498
|
+
statusPreference,
|
|
499
|
+
priorityMax: config.work.priority_max,
|
|
500
|
+
})[0];
|
|
501
|
+
return {
|
|
502
|
+
command: "mcp.goal_next",
|
|
503
|
+
goal_source: loaded.source,
|
|
504
|
+
goal: (0, query_output_1.toNodeSummaryJson)(loaded.node),
|
|
505
|
+
node: selected ? (0, query_output_1.toNodeSummaryJson)(selected) : null,
|
|
506
|
+
warnings,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function callTool(context, name, args) {
|
|
510
|
+
switch (name) {
|
|
511
|
+
case "mdkg_status":
|
|
512
|
+
return toolResult((0, status_1.collectStatus)(context.root));
|
|
513
|
+
case "mdkg_workspace_list":
|
|
514
|
+
return toolResult(listWorkspaces(context.root));
|
|
515
|
+
case "mdkg_search":
|
|
516
|
+
return toolResult(searchTool(context.root, args));
|
|
517
|
+
case "mdkg_show":
|
|
518
|
+
return toolResult(showTool(context.root, args));
|
|
519
|
+
case "mdkg_pack":
|
|
520
|
+
return toolResult(packTool(context.root, args));
|
|
521
|
+
case "mdkg_goal_current":
|
|
522
|
+
return toolResult(goalCurrentTool(context.root, args));
|
|
523
|
+
case "mdkg_goal_next":
|
|
524
|
+
return toolResult(goalNextTool(context.root, args));
|
|
525
|
+
case "mdkg_validate":
|
|
526
|
+
return toolResult((0, validate_1.collectValidateReceipt)({ root: context.root, json: true }));
|
|
527
|
+
default:
|
|
528
|
+
throw new errors_1.UsageError(`Unknown tool: ${name}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function jsonRpcError(id, code, message, data) {
|
|
532
|
+
return {
|
|
533
|
+
jsonrpc: "2.0",
|
|
534
|
+
id,
|
|
535
|
+
error: {
|
|
536
|
+
code,
|
|
537
|
+
message,
|
|
538
|
+
...(data === undefined ? {} : { data }),
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function jsonRpcResult(id, result) {
|
|
543
|
+
return { jsonrpc: "2.0", id, result };
|
|
544
|
+
}
|
|
545
|
+
function requestId(value) {
|
|
546
|
+
if (typeof value === "string" || typeof value === "number" || value === null) {
|
|
547
|
+
return value;
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
function errorCode(err) {
|
|
552
|
+
if (err instanceof errors_1.UsageError) {
|
|
553
|
+
return -32602;
|
|
554
|
+
}
|
|
555
|
+
if (err instanceof errors_1.NotFoundError) {
|
|
556
|
+
return -32004;
|
|
557
|
+
}
|
|
558
|
+
return -32000;
|
|
559
|
+
}
|
|
560
|
+
function handleMcpRequest(context, raw) {
|
|
561
|
+
const id = requestId(raw.id);
|
|
562
|
+
if (raw.jsonrpc !== "2.0" || typeof raw.method !== "string") {
|
|
563
|
+
return jsonRpcError(id, -32600, "Invalid Request");
|
|
564
|
+
}
|
|
565
|
+
if (raw.id === undefined && raw.method !== "notifications/initialized") {
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
switch (raw.method) {
|
|
570
|
+
case "initialize": {
|
|
571
|
+
const params = asObject(raw.params);
|
|
572
|
+
const requestedVersion = typeof params.protocolVersion === "string" ? params.protocolVersion : MCP_PROTOCOL_VERSION;
|
|
573
|
+
return jsonRpcResult(id, {
|
|
574
|
+
protocolVersion: requestedVersion === MCP_PROTOCOL_VERSION ? MCP_PROTOCOL_VERSION : MCP_PROTOCOL_VERSION,
|
|
575
|
+
capabilities: {
|
|
576
|
+
tools: {
|
|
577
|
+
listChanged: false,
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
serverInfo: {
|
|
581
|
+
name: SERVER_NAME,
|
|
582
|
+
title: "mdkg local read-only MCP server",
|
|
583
|
+
version: (0, version_1.readPackageVersion)(),
|
|
584
|
+
},
|
|
585
|
+
instructions: "Read-only mdkg graph inspection over stdio. No mutation tools, shell execution, arbitrary file reads, SQL, or environment secret access are exposed.",
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
case "notifications/initialized":
|
|
589
|
+
return undefined;
|
|
590
|
+
case "ping":
|
|
591
|
+
return jsonRpcResult(id, {});
|
|
592
|
+
case "tools/list":
|
|
593
|
+
return jsonRpcResult(id, { tools: MCP_TOOLS.map(toolDescription) });
|
|
594
|
+
case "tools/call": {
|
|
595
|
+
const params = asObject(raw.params);
|
|
596
|
+
if (typeof params.name !== "string" || params.name.trim().length === 0) {
|
|
597
|
+
throw new errors_1.UsageError("tools/call requires params.name");
|
|
598
|
+
}
|
|
599
|
+
return jsonRpcResult(id, callTool(context, params.name, asObject(params.arguments)));
|
|
600
|
+
}
|
|
601
|
+
default:
|
|
602
|
+
return jsonRpcError(id, -32601, `Method not found: ${raw.method}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
return jsonRpcError(id, errorCode(err), err instanceof Error ? err.message : String(err));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function handleMcpMessage(context, message) {
|
|
610
|
+
if (Array.isArray(message)) {
|
|
611
|
+
return message
|
|
612
|
+
.map((item) => handleMcpRequest(context, item))
|
|
613
|
+
.filter((item) => Boolean(item));
|
|
614
|
+
}
|
|
615
|
+
const response = handleMcpRequest(context, message);
|
|
616
|
+
return response ? [response] : [];
|
|
617
|
+
}
|
|
618
|
+
async function runMcpServeCommand(options) {
|
|
619
|
+
if (!options.stdio) {
|
|
620
|
+
throw new errors_1.UsageError("mdkg mcp serve requires --stdio");
|
|
621
|
+
}
|
|
622
|
+
const input = options.input ?? process.stdin;
|
|
623
|
+
const output = options.output ?? process.stdout;
|
|
624
|
+
const lines = readline_1.default.createInterface({
|
|
625
|
+
input,
|
|
626
|
+
crlfDelay: Infinity,
|
|
627
|
+
});
|
|
628
|
+
const context = { root: options.root };
|
|
629
|
+
for await (const line of lines) {
|
|
630
|
+
const trimmed = line.trim();
|
|
631
|
+
if (!trimmed) {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
let parsed;
|
|
635
|
+
try {
|
|
636
|
+
parsed = JSON.parse(trimmed);
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
output.write(`${JSON.stringify(jsonRpcError(null, -32700, "Parse error"))}\n`);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
for (const response of handleMcpMessage(context, parsed)) {
|
|
643
|
+
output.write(`${JSON.stringify(response)}\n`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return 0;
|
|
647
|
+
}
|