@productbrain/mcp 0.0.1-beta.5 → 0.0.1-beta.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/.env.mcp.example +8 -13
- package/dist/chunk-YLVX2CON.js +1203 -0
- package/dist/chunk-YLVX2CON.js.map +1 -0
- package/dist/index.js +1559 -1063
- package/dist/index.js.map +1 -1
- package/dist/smart-capture-QGIC5N47.js +13 -0
- package/dist/smart-capture-QGIC5N47.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
bootstrap,
|
|
4
|
+
closeAgentSession,
|
|
5
|
+
getAgentSessionId,
|
|
6
|
+
getApiKeyScope,
|
|
7
|
+
getAuditLog,
|
|
8
|
+
getWorkspaceContext,
|
|
9
|
+
getWorkspaceId,
|
|
10
|
+
initAnalytics,
|
|
11
|
+
isSessionOriented,
|
|
12
|
+
mcpCall,
|
|
13
|
+
mcpMutation,
|
|
14
|
+
mcpQuery,
|
|
15
|
+
orphanAgentSession,
|
|
16
|
+
recordSessionActivity,
|
|
17
|
+
recoverSessionState,
|
|
18
|
+
registerSmartCaptureTools,
|
|
19
|
+
requireWriteAccess,
|
|
20
|
+
setSessionOriented,
|
|
21
|
+
shutdownAnalytics,
|
|
22
|
+
startAgentSession,
|
|
23
|
+
trackSessionStarted
|
|
24
|
+
} from "./chunk-YLVX2CON.js";
|
|
25
|
+
|
|
2
26
|
// src/index.ts
|
|
3
27
|
import { readFileSync as readFileSync3 } from "fs";
|
|
4
28
|
import { resolve as resolve3 } from "path";
|
|
@@ -7,179 +31,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
31
|
|
|
8
32
|
// src/tools/knowledge.ts
|
|
9
33
|
import { z } from "zod";
|
|
10
|
-
|
|
11
|
-
// src/analytics.ts
|
|
12
|
-
import { userInfo } from "os";
|
|
13
|
-
import { PostHog } from "posthog-node";
|
|
14
|
-
var client = null;
|
|
15
|
-
var distinctId = "anonymous";
|
|
16
|
-
var POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
17
|
-
function log(msg) {
|
|
18
|
-
if (process.env.MCP_DEBUG === "1") {
|
|
19
|
-
process.stderr.write(msg);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
function initAnalytics() {
|
|
23
|
-
const apiKey = process.env.POSTHOG_MCP_KEY || "";
|
|
24
|
-
if (!apiKey) {
|
|
25
|
-
log("[MCP-ANALYTICS] No PostHog key \u2014 tracking disabled (set SYNERGYOS_POSTHOG_KEY at build time for publish)\n");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
client = new PostHog(apiKey, {
|
|
29
|
-
host: POSTHOG_HOST,
|
|
30
|
-
flushAt: 1,
|
|
31
|
-
flushInterval: 5e3
|
|
32
|
-
});
|
|
33
|
-
distinctId = process.env.MCP_USER_ID || fallbackDistinctId();
|
|
34
|
-
log(`[MCP-ANALYTICS] Initialized \u2014 host=${POSTHOG_HOST} distinctId=${distinctId}
|
|
35
|
-
`);
|
|
36
|
-
}
|
|
37
|
-
function fallbackDistinctId() {
|
|
38
|
-
try {
|
|
39
|
-
return userInfo().username;
|
|
40
|
-
} catch {
|
|
41
|
-
return `os-${process.pid}`;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
function trackSessionStarted(workspaceSlug, workspaceId2, serverVersion) {
|
|
45
|
-
if (!client) return;
|
|
46
|
-
client.capture({
|
|
47
|
-
distinctId,
|
|
48
|
-
event: "mcp_session_started",
|
|
49
|
-
properties: {
|
|
50
|
-
workspace_slug: workspaceSlug,
|
|
51
|
-
workspace_id: workspaceId2,
|
|
52
|
-
server_version: serverVersion,
|
|
53
|
-
source: "mcp-server",
|
|
54
|
-
$groups: { workspace: workspaceId2 }
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
function trackToolCall(fn, status, durationMs, workspaceId2, errorMsg) {
|
|
59
|
-
const properties = {
|
|
60
|
-
tool: fn,
|
|
61
|
-
status,
|
|
62
|
-
duration_ms: durationMs,
|
|
63
|
-
workspace_slug: process.env.WORKSPACE_SLUG ?? "unknown",
|
|
64
|
-
source: "mcp-server",
|
|
65
|
-
$groups: { workspace: workspaceId2 }
|
|
66
|
-
};
|
|
67
|
-
if (errorMsg) properties.error = errorMsg;
|
|
68
|
-
if (!client) return;
|
|
69
|
-
client.capture({
|
|
70
|
-
distinctId,
|
|
71
|
-
event: "mcp_tool_called",
|
|
72
|
-
properties
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
async function shutdownAnalytics() {
|
|
76
|
-
await client?.shutdown();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// src/client.ts
|
|
80
|
-
var DEFAULT_CLOUD_URL = "https://trustworthy-kangaroo-277.convex.site";
|
|
81
|
-
var cachedWorkspaceId = null;
|
|
82
|
-
var cloudMode = false;
|
|
83
|
-
var AUDIT_BUFFER_SIZE = 50;
|
|
84
|
-
var auditBuffer = [];
|
|
85
|
-
function bootstrapCloudMode() {
|
|
86
|
-
const pbKey = process.env.PRODUCTBRAIN_API_KEY;
|
|
87
|
-
if (pbKey?.startsWith("pb_sk_")) {
|
|
88
|
-
const cloudUrl = process.env.PRODUCTBRAIN_URL ?? DEFAULT_CLOUD_URL;
|
|
89
|
-
process.env.CONVEX_SITE_URL ??= cloudUrl;
|
|
90
|
-
process.env.MCP_API_KEY ??= pbKey;
|
|
91
|
-
cloudMode = true;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function getEnv(key) {
|
|
95
|
-
const value = process.env[key];
|
|
96
|
-
if (!value) throw new Error(`${key} environment variable is required`);
|
|
97
|
-
return value;
|
|
98
|
-
}
|
|
99
|
-
function shouldLogAudit(status) {
|
|
100
|
-
return status === "error" || process.env.MCP_DEBUG === "1";
|
|
101
|
-
}
|
|
102
|
-
function audit(fn, status, durationMs, errorMsg) {
|
|
103
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
104
|
-
const workspace = cachedWorkspaceId ?? "unresolved";
|
|
105
|
-
const entry = { ts, fn, workspace, status, durationMs };
|
|
106
|
-
if (errorMsg) entry.error = errorMsg;
|
|
107
|
-
auditBuffer.push(entry);
|
|
108
|
-
if (auditBuffer.length > AUDIT_BUFFER_SIZE) auditBuffer.shift();
|
|
109
|
-
trackToolCall(fn, status, durationMs, workspace, errorMsg);
|
|
110
|
-
if (!shouldLogAudit(status)) return;
|
|
111
|
-
const base = `[MCP-AUDIT] ${ts} fn=${fn} workspace=${workspace} status=${status} duration=${durationMs}ms`;
|
|
112
|
-
if (status === "error" && errorMsg) {
|
|
113
|
-
process.stderr.write(`${base} error=${JSON.stringify(errorMsg)}
|
|
114
|
-
`);
|
|
115
|
-
} else {
|
|
116
|
-
process.stderr.write(`${base}
|
|
117
|
-
`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
function getAuditLog() {
|
|
121
|
-
return auditBuffer;
|
|
122
|
-
}
|
|
123
|
-
async function mcpCall(fn, args = {}) {
|
|
124
|
-
const siteUrl = getEnv("CONVEX_SITE_URL").replace(/\/$/, "");
|
|
125
|
-
const apiKey = getEnv("MCP_API_KEY");
|
|
126
|
-
const start = Date.now();
|
|
127
|
-
let res;
|
|
128
|
-
try {
|
|
129
|
-
res = await fetch(`${siteUrl}/api/mcp`, {
|
|
130
|
-
method: "POST",
|
|
131
|
-
headers: {
|
|
132
|
-
"Content-Type": "application/json",
|
|
133
|
-
Authorization: `Bearer ${apiKey}`
|
|
134
|
-
},
|
|
135
|
-
body: JSON.stringify({ fn, args })
|
|
136
|
-
});
|
|
137
|
-
} catch (err) {
|
|
138
|
-
audit(fn, "error", Date.now() - start, err.message);
|
|
139
|
-
throw new Error(`MCP call "${fn}" network error: ${err.message}`);
|
|
140
|
-
}
|
|
141
|
-
const json = await res.json();
|
|
142
|
-
if (!res.ok || json.error) {
|
|
143
|
-
audit(fn, "error", Date.now() - start, json.error);
|
|
144
|
-
throw new Error(`MCP call "${fn}" failed (${res.status}): ${json.error ?? "unknown error"}`);
|
|
145
|
-
}
|
|
146
|
-
audit(fn, "ok", Date.now() - start);
|
|
147
|
-
return json.data;
|
|
148
|
-
}
|
|
149
|
-
async function getWorkspaceId() {
|
|
150
|
-
if (cachedWorkspaceId) return cachedWorkspaceId;
|
|
151
|
-
if (cloudMode) {
|
|
152
|
-
const workspace2 = await mcpCall(
|
|
153
|
-
"resolveWorkspace",
|
|
154
|
-
{ slug: "__cloud__" }
|
|
155
|
-
);
|
|
156
|
-
if (!workspace2) {
|
|
157
|
-
throw new Error("Cloud key is valid but no workspace is associated. Run `npx productbrain setup` again.");
|
|
158
|
-
}
|
|
159
|
-
cachedWorkspaceId = workspace2._id;
|
|
160
|
-
return cachedWorkspaceId;
|
|
161
|
-
}
|
|
162
|
-
const slug = getEnv("WORKSPACE_SLUG");
|
|
163
|
-
const workspace = await mcpCall(
|
|
164
|
-
"resolveWorkspace",
|
|
165
|
-
{ slug }
|
|
166
|
-
);
|
|
167
|
-
if (!workspace) {
|
|
168
|
-
throw new Error(`Workspace with slug "${slug}" not found`);
|
|
169
|
-
}
|
|
170
|
-
cachedWorkspaceId = workspace._id;
|
|
171
|
-
return cachedWorkspaceId;
|
|
172
|
-
}
|
|
173
|
-
async function mcpQuery(fn, args = {}) {
|
|
174
|
-
const workspaceId2 = await getWorkspaceId();
|
|
175
|
-
return mcpCall(fn, { ...args, workspaceId: workspaceId2 });
|
|
176
|
-
}
|
|
177
|
-
async function mcpMutation(fn, args = {}) {
|
|
178
|
-
const workspaceId2 = await getWorkspaceId();
|
|
179
|
-
return mcpCall(fn, { ...args, workspaceId: workspaceId2 });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// src/tools/knowledge.ts
|
|
183
34
|
function extractPreview(data, maxLen) {
|
|
184
35
|
if (!data || typeof data !== "object") return "";
|
|
185
36
|
const raw = data.description ?? data.canonical ?? data.detail ?? "";
|
|
@@ -195,7 +46,7 @@ function registerKnowledgeTools(server2) {
|
|
|
195
46
|
annotations: { readOnlyHint: true }
|
|
196
47
|
},
|
|
197
48
|
async () => {
|
|
198
|
-
const collections = await mcpQuery("
|
|
49
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
199
50
|
if (collections.length === 0) {
|
|
200
51
|
return { content: [{ type: "text", text: "No collections found in this workspace." }] };
|
|
201
52
|
}
|
|
@@ -230,10 +81,10 @@ ${formatted}` }]
|
|
|
230
81
|
async ({ collection, status, tag, label }) => {
|
|
231
82
|
let entries;
|
|
232
83
|
if (label) {
|
|
233
|
-
entries = await mcpQuery("
|
|
84
|
+
entries = await mcpQuery("chain.listEntriesByLabel", { labelSlug: label });
|
|
234
85
|
if (status) entries = entries.filter((e) => e.status === status);
|
|
235
86
|
} else {
|
|
236
|
-
entries = await mcpQuery("
|
|
87
|
+
entries = await mcpQuery("chain.listEntries", {
|
|
237
88
|
collectionSlug: collection,
|
|
238
89
|
status,
|
|
239
90
|
tag
|
|
@@ -267,7 +118,7 @@ ${formatted}` }]
|
|
|
267
118
|
annotations: { readOnlyHint: true }
|
|
268
119
|
},
|
|
269
120
|
async ({ entryId }) => {
|
|
270
|
-
const entry = await mcpQuery("
|
|
121
|
+
const entry = await mcpQuery("chain.getEntry", { entryId });
|
|
271
122
|
if (!entry) {
|
|
272
123
|
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
273
124
|
}
|
|
@@ -324,21 +175,26 @@ ${formatted}` }]
|
|
|
324
175
|
},
|
|
325
176
|
async ({ entryId, name, status, data, order, autoPublish }) => {
|
|
326
177
|
try {
|
|
327
|
-
|
|
178
|
+
requireWriteAccess();
|
|
179
|
+
const id = await mcpMutation("chain.updateEntry", {
|
|
328
180
|
entryId,
|
|
329
181
|
name,
|
|
330
182
|
status,
|
|
331
183
|
data,
|
|
332
184
|
order,
|
|
333
|
-
autoPublish
|
|
185
|
+
autoPublish,
|
|
186
|
+
changedBy: getAgentSessionId() ? `agent:${getAgentSessionId()}` : void 0
|
|
334
187
|
});
|
|
188
|
+
await recordSessionActivity({ entryModified: entryId });
|
|
189
|
+
const wsCtx = await getWorkspaceContext();
|
|
335
190
|
const mode = autoPublish ? "published" : "saved as draft";
|
|
336
191
|
return {
|
|
337
192
|
content: [{ type: "text", text: `# Entry Updated
|
|
338
193
|
|
|
339
194
|
**${entryId}** has been ${mode}.
|
|
340
195
|
|
|
341
|
-
Internal ID: ${id}
|
|
196
|
+
Internal ID: ${id}
|
|
197
|
+
**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})` }]
|
|
342
198
|
};
|
|
343
199
|
} catch (error) {
|
|
344
200
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -380,8 +236,8 @@ Process criteria (TBD): e.g. 3+ users approved, or 7 days without valid concerns
|
|
|
380
236
|
const scope = collection ? ` in \`${collection}\`` : "";
|
|
381
237
|
await server2.sendLoggingMessage({ level: "info", data: `Searching${scope} for "${query}"...`, logger: "product-os" });
|
|
382
238
|
const [results, collections] = await Promise.all([
|
|
383
|
-
mcpQuery("
|
|
384
|
-
mcpQuery("
|
|
239
|
+
mcpQuery("chain.searchEntries", { query, collectionSlug: collection, status }),
|
|
240
|
+
mcpQuery("chain.listCollections")
|
|
385
241
|
]);
|
|
386
242
|
if (results.length === 0) {
|
|
387
243
|
return { content: [{ type: "text", text: `No results for "${query}"${scope}. Try a broader search or check list-collections for available data.` }] };
|
|
@@ -430,7 +286,7 @@ ${footer}` }]
|
|
|
430
286
|
annotations: { readOnlyHint: true }
|
|
431
287
|
},
|
|
432
288
|
async ({ entryId }) => {
|
|
433
|
-
const history = await mcpQuery("
|
|
289
|
+
const history = await mcpQuery("chain.listEntryHistory", { entryId });
|
|
434
290
|
if (history.length === 0) {
|
|
435
291
|
return { content: [{ type: "text", text: `No history found for \`${entryId}\`.` }] };
|
|
436
292
|
}
|
|
@@ -459,18 +315,73 @@ ${formatted}` }]
|
|
|
459
315
|
annotations: { destructiveHint: false }
|
|
460
316
|
},
|
|
461
317
|
async ({ from, to, type }) => {
|
|
462
|
-
|
|
318
|
+
requireWriteAccess();
|
|
319
|
+
await mcpMutation("chain.createEntryRelation", {
|
|
463
320
|
fromEntryId: from,
|
|
464
321
|
toEntryId: to,
|
|
465
322
|
type
|
|
466
323
|
});
|
|
324
|
+
await recordSessionActivity({ relationCreated: true });
|
|
325
|
+
const wsCtx = await getWorkspaceContext();
|
|
467
326
|
return {
|
|
468
327
|
content: [{ type: "text", text: `# Relation Created
|
|
469
328
|
|
|
470
|
-
**${from}** \u2014[${type}]\u2192 **${to}
|
|
329
|
+
**${from}** \u2014[${type}]\u2192 **${to}**
|
|
330
|
+
**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})` }]
|
|
471
331
|
};
|
|
472
332
|
}
|
|
473
333
|
);
|
|
334
|
+
server2.registerTool(
|
|
335
|
+
"batch-relate",
|
|
336
|
+
{
|
|
337
|
+
title: "Batch Link Entries",
|
|
338
|
+
description: "Create multiple relations in one call. Accepts an array of {from, to, type} objects. Use after suggest-links to apply several suggestions at once instead of calling relate-entries repeatedly.\n\nEach relation is created independently \u2014 if one fails, the others still succeed. Returns a summary of created and failed relations.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
relations: z.array(z.object({
|
|
341
|
+
from: z.string().describe("Source entry ID"),
|
|
342
|
+
to: z.string().describe("Target entry ID"),
|
|
343
|
+
type: z.string().describe("Relation type")
|
|
344
|
+
})).min(1).max(20).describe("Array of relations to create")
|
|
345
|
+
},
|
|
346
|
+
annotations: { destructiveHint: false }
|
|
347
|
+
},
|
|
348
|
+
async ({ relations }) => {
|
|
349
|
+
requireWriteAccess();
|
|
350
|
+
const results = [];
|
|
351
|
+
for (const rel of relations) {
|
|
352
|
+
try {
|
|
353
|
+
await mcpMutation("chain.createEntryRelation", {
|
|
354
|
+
fromEntryId: rel.from,
|
|
355
|
+
toEntryId: rel.to,
|
|
356
|
+
type: rel.type
|
|
357
|
+
});
|
|
358
|
+
results.push({ ...rel, ok: true });
|
|
359
|
+
} catch (e) {
|
|
360
|
+
results.push({ ...rel, ok: false, error: e.message || "Unknown error" });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const created = results.filter((r) => r.ok);
|
|
364
|
+
const failed = results.filter((r) => !r.ok);
|
|
365
|
+
const lines = [`# Batch Link Results
|
|
366
|
+
`];
|
|
367
|
+
lines.push(`**${created.length}** created, **${failed.length}** failed out of ${relations.length} total.
|
|
368
|
+
`);
|
|
369
|
+
if (created.length > 0) {
|
|
370
|
+
lines.push("## Created");
|
|
371
|
+
for (const r of created) {
|
|
372
|
+
lines.push(`- **${r.from}** \u2014[${r.type}]\u2192 **${r.to}**`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (failed.length > 0) {
|
|
376
|
+
lines.push("");
|
|
377
|
+
lines.push("## Failed");
|
|
378
|
+
for (const r of failed) {
|
|
379
|
+
lines.push(`- **${r.from}** \u2192 **${r.to}** (${r.type}): _${r.error}_`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
383
|
+
}
|
|
384
|
+
);
|
|
474
385
|
server2.registerTool(
|
|
475
386
|
"find-related",
|
|
476
387
|
{
|
|
@@ -483,11 +394,11 @@ ${formatted}` }]
|
|
|
483
394
|
annotations: { readOnlyHint: true }
|
|
484
395
|
},
|
|
485
396
|
async ({ entryId, direction }) => {
|
|
486
|
-
const relations = await mcpQuery("
|
|
397
|
+
const relations = await mcpQuery("chain.listEntryRelations", { entryId });
|
|
487
398
|
if (relations.length === 0) {
|
|
488
399
|
return { content: [{ type: "text", text: `No relations found for \`${entryId}\`. Use relate-entries to create connections.` }] };
|
|
489
400
|
}
|
|
490
|
-
const sourceEntry = await mcpQuery("
|
|
401
|
+
const sourceEntry = await mcpQuery("chain.getEntry", { entryId });
|
|
491
402
|
if (!sourceEntry) {
|
|
492
403
|
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
493
404
|
}
|
|
@@ -502,12 +413,12 @@ ${formatted}` }]
|
|
|
502
413
|
}
|
|
503
414
|
const otherEntries = /* @__PURE__ */ new Map();
|
|
504
415
|
for (const id of otherIds) {
|
|
505
|
-
const entry = await mcpQuery("
|
|
416
|
+
const entry = await mcpQuery("chain.getEntry", { id });
|
|
506
417
|
if (entry) {
|
|
507
418
|
otherEntries.set(entry._id, { entryId: entry.entryId, name: entry.name, collectionId: entry.collectionId });
|
|
508
419
|
}
|
|
509
420
|
}
|
|
510
|
-
const collections = await mcpQuery("
|
|
421
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
511
422
|
const collMap = /* @__PURE__ */ new Map();
|
|
512
423
|
for (const c of collections) collMap.set(c._id, c.slug);
|
|
513
424
|
const lines = [`# Relations for ${entryId}: ${sourceEntry.name}`, ""];
|
|
@@ -549,19 +460,67 @@ ${formatted}` }]
|
|
|
549
460
|
"gather-context",
|
|
550
461
|
{
|
|
551
462
|
title: "Gather Context",
|
|
552
|
-
description: "Assemble knowledge context in one call.
|
|
463
|
+
description: "Assemble knowledge context in one call. Three modes:\n\n1. **By entry** (entryId): Traverse the knowledge graph around a specific entry. Returns all related entries grouped by collection.\n2. **By task** (task): Auto-load relevant domain knowledge for a natural-language task. Searches the chain, traverses the graph, and returns ranked entries with confidence scores.\n3. **Graph mode** (entryId + mode='graph'): Enhanced graph traversal with provenance \u2014 each entry includes the full path that led to it (via what relations, from what starting point).\n\nUse mode 1/3 when you have a specific entry ID. Use mode 2 at the start of a conversation to ground the agent in domain context before writing code or making recommendations.",
|
|
553
464
|
inputSchema: {
|
|
554
465
|
entryId: z.string().optional().describe("Entry ID for graph traversal, e.g. 'FEAT-001', 'GT-019'"),
|
|
555
466
|
task: z.string().optional().describe("Natural-language task description for auto-loading relevant context"),
|
|
467
|
+
mode: z.enum(["search", "graph"]).default("search").optional().describe("'search' (default, backward-compatible) or 'graph' (enhanced with provenance paths)"),
|
|
556
468
|
maxHops: z.number().min(1).max(3).default(2).describe("How many relation hops to traverse (1=direct only, 2=default, 3=wide net)"),
|
|
557
469
|
maxResults: z.number().min(1).max(25).default(10).optional().describe("Max entries to return in task mode (default 10)")
|
|
558
470
|
},
|
|
559
471
|
annotations: { readOnlyHint: true }
|
|
560
472
|
},
|
|
561
|
-
async ({ entryId, task, maxHops, maxResults }) => {
|
|
473
|
+
async ({ entryId, task, mode, maxHops, maxResults }) => {
|
|
562
474
|
if (!entryId && !task) {
|
|
563
475
|
return { content: [{ type: "text", text: "Provide either `entryId` (graph traversal) or `task` (auto-load context for a task)." }] };
|
|
564
476
|
}
|
|
477
|
+
if (entryId && mode === "graph") {
|
|
478
|
+
const result2 = await mcpQuery("chain.graphGatherContext", {
|
|
479
|
+
entryId,
|
|
480
|
+
maxHops: maxHops ?? 2
|
|
481
|
+
});
|
|
482
|
+
if (!result2?.root) {
|
|
483
|
+
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
484
|
+
}
|
|
485
|
+
if (result2.context.length === 0) {
|
|
486
|
+
return {
|
|
487
|
+
content: [{
|
|
488
|
+
type: "text",
|
|
489
|
+
text: `# Context for ${result2.root.entryId}: ${result2.root.name}
|
|
490
|
+
|
|
491
|
+
_No relations found._ This entry is not yet connected to the knowledge graph.
|
|
492
|
+
|
|
493
|
+
Use \`suggest-links\` to discover potential connections.`
|
|
494
|
+
}]
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const byCollection2 = /* @__PURE__ */ new Map();
|
|
498
|
+
for (const entry of result2.context) {
|
|
499
|
+
const key = entry.collectionName;
|
|
500
|
+
if (!byCollection2.has(key)) byCollection2.set(key, []);
|
|
501
|
+
byCollection2.get(key).push(entry);
|
|
502
|
+
}
|
|
503
|
+
const lines2 = [
|
|
504
|
+
`# Context for ${result2.root.entryId}: ${result2.root.name} (graph mode)`,
|
|
505
|
+
`_${result2.totalFound} related entries across ${byCollection2.size} collections (${result2.hopsTraversed} hops)_`,
|
|
506
|
+
""
|
|
507
|
+
];
|
|
508
|
+
for (const [collName, entries] of byCollection2) {
|
|
509
|
+
lines2.push(`## ${collName} (${entries.length})`);
|
|
510
|
+
for (const e of entries) {
|
|
511
|
+
const arrow = e.relationDirection === "outgoing" ? "\u2192" : "\u2190";
|
|
512
|
+
const id = e.entryId ? `${e.entryId}: ` : "";
|
|
513
|
+
const hopLabel = e.hop > 1 ? ` (hop ${e.hop})` : "";
|
|
514
|
+
const provenancePath = e.provenance && e.provenance.length > 1 ? `
|
|
515
|
+
_Path: ${e.provenance.map((p) => `${p.entryId ?? p.name} [${p.relationType}]`).join(" \u2192 ")}_` : "";
|
|
516
|
+
const preview = e.preview ? `
|
|
517
|
+
${e.preview.substring(0, 120)}` : "";
|
|
518
|
+
lines2.push(`- ${arrow} **${e.relationType}** ${id}${e.name}${hopLabel}${provenancePath}${preview}`);
|
|
519
|
+
}
|
|
520
|
+
lines2.push("");
|
|
521
|
+
}
|
|
522
|
+
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
523
|
+
}
|
|
565
524
|
if (task && !entryId) {
|
|
566
525
|
await server2.sendLoggingMessage({
|
|
567
526
|
level: "info",
|
|
@@ -569,7 +528,7 @@ ${formatted}` }]
|
|
|
569
528
|
logger: "product-brain"
|
|
570
529
|
});
|
|
571
530
|
const searchTerms = task.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 12).join(" ");
|
|
572
|
-
const searchResults = await mcpQuery("
|
|
531
|
+
const searchResults = await mcpQuery("chain.searchEntries", {
|
|
573
532
|
query: searchTerms
|
|
574
533
|
});
|
|
575
534
|
if (!searchResults || searchResults.length === 0) {
|
|
@@ -602,7 +561,7 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
|
|
|
602
561
|
});
|
|
603
562
|
if (hit.entryId) {
|
|
604
563
|
try {
|
|
605
|
-
const graph = await mcpQuery("
|
|
564
|
+
const graph = await mcpQuery("chain.gatherContext", {
|
|
606
565
|
entryId: hit.entryId,
|
|
607
566
|
maxHops: maxHops ?? 2
|
|
608
567
|
});
|
|
@@ -652,7 +611,7 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
|
|
|
652
611
|
lines2.push(`_Use \`get-entry\` for full details on any entry._`);
|
|
653
612
|
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
654
613
|
}
|
|
655
|
-
const result = await mcpQuery("
|
|
614
|
+
const result = await mcpQuery("chain.gatherContext", { entryId, maxHops });
|
|
656
615
|
if (!result?.root) {
|
|
657
616
|
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
658
617
|
}
|
|
@@ -696,61 +655,108 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
|
|
|
696
655
|
"suggest-links",
|
|
697
656
|
{
|
|
698
657
|
title: "Suggest Links",
|
|
699
|
-
description: "Discover potential connections for an entry
|
|
658
|
+
description: "Discover potential connections for an entry using graph-aware intelligence. Accepts an entry ID (e.g. 'FEAT-001') OR an entry name (e.g. 'Chain Intelligence'). Traverses the knowledge graph (2-hop BFS) and scores candidates by graph distance, text similarity, and relation type fit. Also finds text-similar entries not yet in the graph.\n\nReturns ranked suggestions with confidence scores, recommended relation types, and graph-path reasoning explaining WHY each connection matters.\n\nThis is a discovery tool \u2014 review suggestions and use relate-entries to create the ones that make sense.",
|
|
700
659
|
inputSchema: {
|
|
701
|
-
entryId: z.string().describe("Entry ID
|
|
702
|
-
limit: z.number().min(1).max(20).default(10).describe("Max number of suggestions to return")
|
|
660
|
+
entryId: z.string().describe("Entry ID (e.g. 'FEAT-001') or entry name (e.g. 'Chain Intelligence') to find suggestions for"),
|
|
661
|
+
limit: z.number().min(1).max(20).default(10).describe("Max number of suggestions to return"),
|
|
662
|
+
depth: z.number().min(1).max(3).default(2).describe("Graph traversal depth: 1=direct neighbors only, 2=default, 3=wide net")
|
|
703
663
|
},
|
|
704
664
|
annotations: { readOnlyHint: true }
|
|
705
665
|
},
|
|
706
|
-
async ({ entryId, limit }) => {
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (
|
|
713
|
-
|
|
714
|
-
if (entry.data?.rationale) searchTerms.push(entry.data.rationale);
|
|
715
|
-
if (entry.data?.rule) searchTerms.push(entry.data.rule);
|
|
716
|
-
const queryText = searchTerms.join(" ").replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 8).join(" ");
|
|
717
|
-
if (!queryText) {
|
|
718
|
-
return { content: [{ type: "text", text: `Entry \`${entryId}\` has too little text content to generate suggestions.` }] };
|
|
719
|
-
}
|
|
720
|
-
const results = await mcpQuery("kb.searchEntries", { query: queryText });
|
|
721
|
-
if (!results || results.length === 0) {
|
|
722
|
-
return { content: [{ type: "text", text: `No suggestions found for \`${entryId}\`. The chain may need more entries.` }] };
|
|
723
|
-
}
|
|
724
|
-
const existingRelations = await mcpQuery("kb.listEntryRelations", { entryId });
|
|
725
|
-
const relatedIds = new Set(
|
|
726
|
-
existingRelations.flatMap((r) => [r.fromId, r.toId])
|
|
727
|
-
);
|
|
728
|
-
const collections = await mcpQuery("kb.listCollections");
|
|
729
|
-
const collMap = /* @__PURE__ */ new Map();
|
|
730
|
-
for (const c of collections) collMap.set(c._id, c.slug);
|
|
731
|
-
const suggestions = results.filter((r) => r._id !== entry._id && !relatedIds.has(r._id)).slice(0, limit).map((r) => ({
|
|
732
|
-
entryId: r.entryId,
|
|
733
|
-
name: r.name,
|
|
734
|
-
collection: collMap.get(r.collectionId) ?? "unknown",
|
|
735
|
-
preview: extractPreview(r.data, 80)
|
|
736
|
-
}));
|
|
737
|
-
if (suggestions.length === 0) {
|
|
738
|
-
return { content: [{ type: "text", text: `No new link suggestions for \`${entryId}\` \u2014 it may already be well-connected, or no similar entries exist.` }] };
|
|
666
|
+
async ({ entryId, limit, depth }) => {
|
|
667
|
+
const result = await mcpQuery("chain.graphSuggestLinks", {
|
|
668
|
+
entryId,
|
|
669
|
+
maxHops: depth ?? 2,
|
|
670
|
+
limit: limit ?? 10
|
|
671
|
+
});
|
|
672
|
+
if (!result || !result.suggestions || result.suggestions.length === 0) {
|
|
673
|
+
return { content: [{ type: "text", text: `No suggestions found for \`${entryId}\` \u2014 it may already be well-connected, or no similar entries exist.` }] };
|
|
739
674
|
}
|
|
675
|
+
const resolved = result.resolvedEntry;
|
|
676
|
+
const resolvedLabel = resolved ? `\`${resolved.entryId ?? resolved.name}\` (${resolved.name})` : `\`${entryId}\``;
|
|
677
|
+
const suggestions = result.suggestions;
|
|
678
|
+
const graphCount = suggestions.filter((s) => s.graphDistance > 0).length;
|
|
679
|
+
const textCount = suggestions.filter((s) => s.graphDistance === -1).length;
|
|
680
|
+
const sourceId = resolved?.entryId ?? entryId;
|
|
740
681
|
const lines = [
|
|
741
|
-
`# Link Suggestions for ${
|
|
742
|
-
`_${suggestions.length} potential connections
|
|
682
|
+
`# Link Suggestions for ${resolvedLabel}`,
|
|
683
|
+
`_${suggestions.length} potential connections (${graphCount} via graph, ${textCount} via text similarity)._`,
|
|
743
684
|
""
|
|
744
685
|
];
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
686
|
+
const top3 = suggestions.slice(0, 3);
|
|
687
|
+
lines.push("## Quick Wins (top 3)");
|
|
688
|
+
const batchArgs = [];
|
|
689
|
+
for (const s of top3) {
|
|
690
|
+
const tid = s.entryId ?? "(no ID)";
|
|
691
|
+
const relType = s.recommendedRelationType || "related_to";
|
|
692
|
+
lines.push(`- **${tid}**: ${s.name} [${s.collectionSlug}] \u2014 ${s.score}/100 \u2192 \`${relType}\``);
|
|
693
|
+
if (s.entryId) batchArgs.push(`{from:"${sourceId}",to:"${tid}",type:"${relType}"}`);
|
|
749
694
|
}
|
|
750
695
|
lines.push("");
|
|
751
|
-
|
|
696
|
+
if (batchArgs.length > 0) {
|
|
697
|
+
lines.push(`**Link all 3:** \`batch-relate relations=[${batchArgs.join(",")}]\``);
|
|
698
|
+
lines.push("");
|
|
699
|
+
}
|
|
700
|
+
if (suggestions.length > 3) {
|
|
701
|
+
lines.push("## All Suggestions");
|
|
702
|
+
for (let i = 0; i < suggestions.length; i++) {
|
|
703
|
+
const s = suggestions[i];
|
|
704
|
+
const scoreBar = "\u2588".repeat(Math.round(s.score / 10)) + "\u2591".repeat(10 - Math.round(s.score / 10));
|
|
705
|
+
const hopLabel = s.graphDistance > 0 ? `${s.graphDistance}-hop` : "text";
|
|
706
|
+
const recType = s.recommendedRelationType !== "related_to" ? ` \u2192 \`${s.recommendedRelationType}\`` : "";
|
|
707
|
+
lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collectionSlug}] (${hopLabel})`);
|
|
708
|
+
lines.push(` ${scoreBar} ${s.score}/100${recType}`);
|
|
709
|
+
if (s.preview) {
|
|
710
|
+
lines.push(` ${s.preview}`);
|
|
711
|
+
}
|
|
712
|
+
lines.push(` _${s.reasoning}_`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
752
715
|
lines.push("");
|
|
753
|
-
lines.push(
|
|
716
|
+
lines.push(`**To link one:** \`relate-entries from="${sourceId}" to="{target_id}" type="{type}"\``);
|
|
717
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
718
|
+
}
|
|
719
|
+
);
|
|
720
|
+
server2.registerTool(
|
|
721
|
+
"commit-entry",
|
|
722
|
+
{
|
|
723
|
+
title: "Commit Entry to Chain",
|
|
724
|
+
description: "Promote a draft entry to committed status (SSOT on the Chain). Runs a keyword contradiction check against governance entries before committing. Warnings are advisory \u2014 they do not block the commit.\n\nUse after capture + suggest-links + relate-entries to finalize an entry.",
|
|
725
|
+
inputSchema: {
|
|
726
|
+
entryId: z.string().describe("Entry ID to commit, e.g. 'TEN-abc123', 'GT-019'")
|
|
727
|
+
},
|
|
728
|
+
annotations: { destructiveHint: false }
|
|
729
|
+
},
|
|
730
|
+
async ({ entryId }) => {
|
|
731
|
+
requireWriteAccess();
|
|
732
|
+
const { runContradictionCheck } = await import("./smart-capture-QGIC5N47.js");
|
|
733
|
+
const entry = await mcpQuery("chain.getEntry", { entryId });
|
|
734
|
+
if (!entry) {
|
|
735
|
+
return {
|
|
736
|
+
content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }]
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
const descField = entry.data?.description ?? entry.data?.canonical ?? entry.data?.rationale ?? "";
|
|
740
|
+
const warnings = await runContradictionCheck(entry.name, descField);
|
|
741
|
+
if (warnings.length > 0) {
|
|
742
|
+
await recordSessionActivity({ contradictionWarning: true });
|
|
743
|
+
}
|
|
744
|
+
const result = await mcpMutation("chain.commitEntry", { entryId });
|
|
745
|
+
await recordSessionActivity({ entryModified: entryId });
|
|
746
|
+
const wsCtx = await getWorkspaceContext();
|
|
747
|
+
const lines = [
|
|
748
|
+
`# Committed: ${entryId}`,
|
|
749
|
+
`**${entry.name}** promoted to SSOT on the Chain.`,
|
|
750
|
+
`**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
|
|
751
|
+
];
|
|
752
|
+
if (warnings.length > 0) {
|
|
753
|
+
lines.push("");
|
|
754
|
+
lines.push("\u26A0 Contradiction check: proposed entry matched existing governance entries:");
|
|
755
|
+
for (const w of warnings) {
|
|
756
|
+
lines.push(`- ${w.name} (${w.collection}, ${w.entryId}) \u2014 has 'governs' relation to ${w.governsCount} entries`);
|
|
757
|
+
}
|
|
758
|
+
lines.push("Run gather-context on these entries before committing.");
|
|
759
|
+
}
|
|
754
760
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
755
761
|
}
|
|
756
762
|
);
|
|
@@ -778,7 +784,7 @@ function registerLabelTools(server2) {
|
|
|
778
784
|
},
|
|
779
785
|
async ({ action, slug, name, color, description, parentSlug, isGroup, order, entryId }) => {
|
|
780
786
|
if (action === "list") {
|
|
781
|
-
const labels = await mcpQuery("
|
|
787
|
+
const labels = await mcpQuery("chain.listLabels");
|
|
782
788
|
if (labels.length === 0) {
|
|
783
789
|
return { content: [{ type: "text", text: "No labels defined in this workspace yet." }] };
|
|
784
790
|
}
|
|
@@ -813,26 +819,26 @@ function registerLabelTools(server2) {
|
|
|
813
819
|
}
|
|
814
820
|
let parentId;
|
|
815
821
|
if (parentSlug) {
|
|
816
|
-
const labels = await mcpQuery("
|
|
822
|
+
const labels = await mcpQuery("chain.listLabels");
|
|
817
823
|
const parent = labels.find((l) => l.slug === parentSlug);
|
|
818
824
|
if (!parent) {
|
|
819
825
|
return { content: [{ type: "text", text: `Parent label \`${parentSlug}\` not found. Use \`labels action=list\` to see available groups.` }] };
|
|
820
826
|
}
|
|
821
827
|
parentId = parent._id;
|
|
822
828
|
}
|
|
823
|
-
await mcpMutation("
|
|
829
|
+
await mcpMutation("chain.createLabel", { slug, name, color, description, parentId, isGroup, order });
|
|
824
830
|
return { content: [{ type: "text", text: `# Label Created
|
|
825
831
|
|
|
826
832
|
**${name}** (\`${slug}\`)` }] };
|
|
827
833
|
}
|
|
828
834
|
if (action === "update") {
|
|
829
|
-
await mcpMutation("
|
|
835
|
+
await mcpMutation("chain.updateLabel", { slug, name, color, description, isGroup, order });
|
|
830
836
|
return { content: [{ type: "text", text: `# Label Updated
|
|
831
837
|
|
|
832
838
|
\`${slug}\` has been updated.` }] };
|
|
833
839
|
}
|
|
834
840
|
if (action === "delete") {
|
|
835
|
-
await mcpMutation("
|
|
841
|
+
await mcpMutation("chain.deleteLabel", { slug });
|
|
836
842
|
return { content: [{ type: "text", text: `# Label Deleted
|
|
837
843
|
|
|
838
844
|
\`${slug}\` removed from all entries and deleted.` }] };
|
|
@@ -842,10 +848,10 @@ function registerLabelTools(server2) {
|
|
|
842
848
|
return { content: [{ type: "text", text: "An `entryId` is required for apply/remove actions." }] };
|
|
843
849
|
}
|
|
844
850
|
if (action === "apply") {
|
|
845
|
-
await mcpMutation("
|
|
851
|
+
await mcpMutation("chain.applyLabel", { entryId, labelSlug: slug });
|
|
846
852
|
return { content: [{ type: "text", text: `Label \`${slug}\` applied to **${entryId}**.` }] };
|
|
847
853
|
}
|
|
848
|
-
await mcpMutation("
|
|
854
|
+
await mcpMutation("chain.removeLabel", { entryId, labelSlug: slug });
|
|
849
855
|
return { content: [{ type: "text", text: `Label \`${slug}\` removed from **${entryId}**.` }] };
|
|
850
856
|
}
|
|
851
857
|
return { content: [{ type: "text", text: "Unknown action." }] };
|
|
@@ -856,23 +862,23 @@ function registerLabelTools(server2) {
|
|
|
856
862
|
// src/tools/health.ts
|
|
857
863
|
import { z as z3 } from "zod";
|
|
858
864
|
var CALL_CATEGORIES = {
|
|
859
|
-
"
|
|
860
|
-
"
|
|
861
|
-
"
|
|
862
|
-
"
|
|
863
|
-
"
|
|
864
|
-
"
|
|
865
|
-
"
|
|
866
|
-
"
|
|
867
|
-
"
|
|
868
|
-
"
|
|
869
|
-
"
|
|
870
|
-
"
|
|
871
|
-
"
|
|
872
|
-
"
|
|
873
|
-
"
|
|
874
|
-
"
|
|
875
|
-
"
|
|
865
|
+
"chain.getEntry": "read",
|
|
866
|
+
"chain.listEntries": "read",
|
|
867
|
+
"chain.listEntryHistory": "read",
|
|
868
|
+
"chain.listEntryRelations": "read",
|
|
869
|
+
"chain.listEntriesByLabel": "read",
|
|
870
|
+
"chain.searchEntries": "search",
|
|
871
|
+
"chain.createEntry": "write",
|
|
872
|
+
"chain.updateEntry": "write",
|
|
873
|
+
"chain.createEntryRelation": "write",
|
|
874
|
+
"chain.applyLabel": "label",
|
|
875
|
+
"chain.removeLabel": "label",
|
|
876
|
+
"chain.createLabel": "label",
|
|
877
|
+
"chain.updateLabel": "label",
|
|
878
|
+
"chain.deleteLabel": "label",
|
|
879
|
+
"chain.listCollections": "meta",
|
|
880
|
+
"chain.getCollection": "meta",
|
|
881
|
+
"chain.listLabels": "meta",
|
|
876
882
|
"resolveWorkspace": "meta"
|
|
877
883
|
};
|
|
878
884
|
function categorize(fn) {
|
|
@@ -884,23 +890,23 @@ function formatDuration(ms) {
|
|
|
884
890
|
const secs = Math.round(ms % 6e4 / 1e3);
|
|
885
891
|
return `${mins}m ${secs}s`;
|
|
886
892
|
}
|
|
887
|
-
function buildSessionSummary(
|
|
888
|
-
if (
|
|
893
|
+
function buildSessionSummary(log) {
|
|
894
|
+
if (log.length === 0) return "";
|
|
889
895
|
const byCategory = /* @__PURE__ */ new Map();
|
|
890
896
|
let errorCount = 0;
|
|
891
897
|
let writeCreates = 0;
|
|
892
898
|
let writeUpdates = 0;
|
|
893
|
-
for (const entry of
|
|
899
|
+
for (const entry of log) {
|
|
894
900
|
const cat = categorize(entry.fn);
|
|
895
901
|
if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
|
|
896
902
|
const fnCounts = byCategory.get(cat);
|
|
897
903
|
fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
|
|
898
904
|
if (entry.status === "error") errorCount++;
|
|
899
|
-
if (entry.fn === "
|
|
900
|
-
if (entry.fn === "
|
|
905
|
+
if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
|
|
906
|
+
if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
|
|
901
907
|
}
|
|
902
|
-
const firstTs = new Date(
|
|
903
|
-
const lastTs = new Date(
|
|
908
|
+
const firstTs = new Date(log[0].ts).getTime();
|
|
909
|
+
const lastTs = new Date(log[log.length - 1].ts).getTime();
|
|
904
910
|
const duration = formatDuration(lastTs - firstTs);
|
|
905
911
|
const lines = [`# Session Summary (${duration})
|
|
906
912
|
`];
|
|
@@ -915,7 +921,7 @@ function buildSessionSummary(log2) {
|
|
|
915
921
|
const fnCounts = byCategory.get(cat);
|
|
916
922
|
if (!fnCounts || fnCounts.size === 0) continue;
|
|
917
923
|
const total = [...fnCounts.values()].reduce((a, b) => a + b, 0);
|
|
918
|
-
const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("
|
|
924
|
+
const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
|
|
919
925
|
lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
|
|
920
926
|
}
|
|
921
927
|
lines.push(`- **Errors:** ${errorCount}`);
|
|
@@ -938,33 +944,40 @@ function registerHealthTools(server2) {
|
|
|
938
944
|
async () => {
|
|
939
945
|
const start = Date.now();
|
|
940
946
|
const errors = [];
|
|
941
|
-
let
|
|
947
|
+
let workspaceId;
|
|
942
948
|
try {
|
|
943
|
-
|
|
949
|
+
workspaceId = await getWorkspaceId();
|
|
944
950
|
} catch (e) {
|
|
945
951
|
errors.push(`Workspace resolution failed: ${e.message}`);
|
|
946
952
|
}
|
|
947
953
|
let collections = [];
|
|
948
954
|
try {
|
|
949
|
-
collections = await mcpQuery("
|
|
955
|
+
collections = await mcpQuery("chain.listCollections");
|
|
950
956
|
} catch (e) {
|
|
951
957
|
errors.push(`Collection fetch failed: ${e.message}`);
|
|
952
958
|
}
|
|
953
959
|
let totalEntries = 0;
|
|
954
960
|
if (collections.length > 0) {
|
|
955
961
|
try {
|
|
956
|
-
const entries = await mcpQuery("
|
|
962
|
+
const entries = await mcpQuery("chain.listEntries", {});
|
|
957
963
|
totalEntries = entries.length;
|
|
958
964
|
} catch (e) {
|
|
959
965
|
errors.push(`Entry count failed: ${e.message}`);
|
|
960
966
|
}
|
|
961
967
|
}
|
|
968
|
+
let wsCtx = null;
|
|
969
|
+
try {
|
|
970
|
+
wsCtx = await getWorkspaceContext();
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
962
973
|
const durationMs = Date.now() - start;
|
|
963
974
|
const healthy = errors.length === 0;
|
|
964
975
|
const lines = [
|
|
965
976
|
`# ${healthy ? "Healthy" : "Degraded"}`,
|
|
966
977
|
"",
|
|
967
|
-
`**Workspace:** ${
|
|
978
|
+
`**Workspace:** ${workspaceId ?? "unresolved"}`,
|
|
979
|
+
`**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
|
|
980
|
+
`**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
|
|
968
981
|
`**Collections:** ${collections.length}`,
|
|
969
982
|
`**Entries:** ${totalEntries}`,
|
|
970
983
|
`**Latency:** ${durationMs}ms`
|
|
@@ -978,6 +991,224 @@ function registerHealthTools(server2) {
|
|
|
978
991
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
979
992
|
}
|
|
980
993
|
);
|
|
994
|
+
server2.registerTool(
|
|
995
|
+
"whoami",
|
|
996
|
+
{
|
|
997
|
+
title: "Session Identity",
|
|
998
|
+
description: "Returns the current workspace and auth context for this MCP session. Use at the start of a session to confirm you're operating on the right workspace before making writes.",
|
|
999
|
+
annotations: { readOnlyHint: true }
|
|
1000
|
+
},
|
|
1001
|
+
async () => {
|
|
1002
|
+
const ctx = await getWorkspaceContext();
|
|
1003
|
+
const lines = [
|
|
1004
|
+
`# Session Identity`,
|
|
1005
|
+
"",
|
|
1006
|
+
`**Workspace ID:** ${ctx.workspaceId}`,
|
|
1007
|
+
`**Workspace Slug:** ${ctx.workspaceSlug}`,
|
|
1008
|
+
`**Workspace Name:** ${ctx.workspaceName}`
|
|
1009
|
+
];
|
|
1010
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
1013
|
+
server2.registerTool(
|
|
1014
|
+
"workspace-status",
|
|
1015
|
+
{
|
|
1016
|
+
title: "Workspace Status",
|
|
1017
|
+
description: "The 'Monday morning' tool \u2014 returns workspace readiness score, specific gaps with suggested next actions, and workspace stats (entries, relations, orphans, drafts).\n\nUse this to understand how ready the workspace is, what foundational knowledge is missing, and what to work on next. Great for starting a session or planning knowledge work.",
|
|
1018
|
+
annotations: { readOnlyHint: true }
|
|
1019
|
+
},
|
|
1020
|
+
async () => {
|
|
1021
|
+
const result = await mcpQuery("chain.workspaceReadiness");
|
|
1022
|
+
const { score, totalChecks, passedChecks, checks, gaps, stats } = result;
|
|
1023
|
+
const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
|
|
1024
|
+
const lines = [
|
|
1025
|
+
`# Workspace Readiness: ${score}%`,
|
|
1026
|
+
`${scoreBar} ${passedChecks}/${totalChecks} requirements met`,
|
|
1027
|
+
"",
|
|
1028
|
+
"## Stats",
|
|
1029
|
+
`- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
|
|
1030
|
+
`- **Relations:** ${stats.totalRelations}`,
|
|
1031
|
+
`- **Collections:** ${stats.collectionCount}`,
|
|
1032
|
+
`- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
|
|
1033
|
+
""
|
|
1034
|
+
];
|
|
1035
|
+
if (gaps.length > 0) {
|
|
1036
|
+
lines.push("## Gaps (action required)");
|
|
1037
|
+
for (const gap of gaps) {
|
|
1038
|
+
lines.push(`- [ ] **${gap.label}** \u2014 ${gap.description}`);
|
|
1039
|
+
lines.push(` ${gap.current}/${gap.required} | _${gap.guidance}_`);
|
|
1040
|
+
}
|
|
1041
|
+
lines.push("");
|
|
1042
|
+
}
|
|
1043
|
+
const passed = checks.filter((c) => c.passed);
|
|
1044
|
+
if (passed.length > 0) {
|
|
1045
|
+
lines.push("## Passed");
|
|
1046
|
+
for (const check of passed) {
|
|
1047
|
+
lines.push(`- [x] **${check.label}** (${check.current}/${check.required})`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1051
|
+
}
|
|
1052
|
+
);
|
|
1053
|
+
server2.registerTool(
|
|
1054
|
+
"orient",
|
|
1055
|
+
{
|
|
1056
|
+
title: "Orient \u2014 Start Here",
|
|
1057
|
+
description: "The single entry point for starting a session. Returns everything needed in one call: workspace identity, readiness score, prior session summaries, architecture/governance constraints, open tensions, and suggested next actions.\n\nUse this FIRST. One call to orient replaces 3\u20135 individual tool calls.\n\nCompleting orientation unlocks write tools for the active session.",
|
|
1058
|
+
annotations: { readOnlyHint: true }
|
|
1059
|
+
},
|
|
1060
|
+
async () => {
|
|
1061
|
+
const errors = [];
|
|
1062
|
+
const agentSessionId = getAgentSessionId();
|
|
1063
|
+
let wsCtx = null;
|
|
1064
|
+
try {
|
|
1065
|
+
wsCtx = await getWorkspaceContext();
|
|
1066
|
+
} catch (e) {
|
|
1067
|
+
errors.push(`Workspace: ${e.message}`);
|
|
1068
|
+
}
|
|
1069
|
+
let priorSessions = [];
|
|
1070
|
+
let maxSessions = 3;
|
|
1071
|
+
if (wsCtx) {
|
|
1072
|
+
try {
|
|
1073
|
+
priorSessions = await mcpQuery("agent.recentSessions", { limit: maxSessions });
|
|
1074
|
+
} catch {
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
let constraintEntries = [];
|
|
1078
|
+
let maxConstraints = 8;
|
|
1079
|
+
try {
|
|
1080
|
+
const [archEntries, ruleEntries, decisionEntries] = await Promise.all([
|
|
1081
|
+
mcpQuery("chain.listEntries", { collectionSlug: "architecture" }),
|
|
1082
|
+
mcpQuery("chain.listEntries", { collectionSlug: "business-rules" }),
|
|
1083
|
+
mcpQuery("chain.listEntries", { collectionSlug: "decisions" })
|
|
1084
|
+
]);
|
|
1085
|
+
const committed = [
|
|
1086
|
+
...(archEntries ?? []).filter((e) => e.status === "active" || e.status === "healthy"),
|
|
1087
|
+
...(ruleEntries ?? []).filter((e) => e.status === "Active" || e.status === "active"),
|
|
1088
|
+
...(decisionEntries ?? []).filter((e) => e.status === "Decided" || e.status === "active")
|
|
1089
|
+
];
|
|
1090
|
+
constraintEntries = committed.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, maxConstraints);
|
|
1091
|
+
} catch {
|
|
1092
|
+
}
|
|
1093
|
+
let openTensions = [];
|
|
1094
|
+
try {
|
|
1095
|
+
const tensions = await mcpQuery("chain.listEntries", { collectionSlug: "tensions" });
|
|
1096
|
+
openTensions = (tensions ?? []).filter((e) => e.status === "draft");
|
|
1097
|
+
} catch {
|
|
1098
|
+
}
|
|
1099
|
+
let readiness = null;
|
|
1100
|
+
try {
|
|
1101
|
+
readiness = await mcpQuery("chain.workspaceReadiness");
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
errors.push(`Readiness: ${e.message}`);
|
|
1104
|
+
}
|
|
1105
|
+
const TOKEN_LIMIT = 6e3;
|
|
1106
|
+
const CHAR_PER_TOKEN = 4;
|
|
1107
|
+
const CHAR_LIMIT = TOKEN_LIMIT * CHAR_PER_TOKEN;
|
|
1108
|
+
const lines = [];
|
|
1109
|
+
let charCount = 0;
|
|
1110
|
+
function addLine(line) {
|
|
1111
|
+
lines.push(line);
|
|
1112
|
+
charCount += line.length + 1;
|
|
1113
|
+
}
|
|
1114
|
+
function addLines(ls) {
|
|
1115
|
+
for (const l of ls) addLine(l);
|
|
1116
|
+
}
|
|
1117
|
+
if (wsCtx) {
|
|
1118
|
+
addLine(`# ${wsCtx.workspaceName}`);
|
|
1119
|
+
addLine(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
|
|
1120
|
+
} else {
|
|
1121
|
+
addLine("# Workspace");
|
|
1122
|
+
addLine("_Could not resolve workspace._");
|
|
1123
|
+
}
|
|
1124
|
+
addLine("");
|
|
1125
|
+
if (readiness) {
|
|
1126
|
+
const scoreBar = "\u2588".repeat(Math.round(readiness.score / 10)) + "\u2591".repeat(10 - Math.round(readiness.score / 10));
|
|
1127
|
+
addLine(`## Readiness: ${readiness.score}%`);
|
|
1128
|
+
addLine(`${scoreBar} ${readiness.passedChecks}/${readiness.totalChecks} requirements`);
|
|
1129
|
+
addLine("");
|
|
1130
|
+
if (readiness.gaps && readiness.gaps.length > 0) {
|
|
1131
|
+
addLine("### Gaps");
|
|
1132
|
+
for (const gap of readiness.gaps) {
|
|
1133
|
+
addLine(`- [ ] **${gap.label}** (${gap.current}/${gap.required}) \u2014 _${gap.guidance}_`);
|
|
1134
|
+
}
|
|
1135
|
+
addLine("");
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (openTensions.length > 0) {
|
|
1139
|
+
addLine(`## Open Tensions (${openTensions.length})`);
|
|
1140
|
+
for (const t of openTensions) {
|
|
1141
|
+
const id = t.entryId ?? "(no ID)";
|
|
1142
|
+
const prio = t.data?.priority ?? "";
|
|
1143
|
+
addLine(`- \`${id}\` ${t.name}${prio ? ` _(${prio})_` : ""}`);
|
|
1144
|
+
}
|
|
1145
|
+
addLine("");
|
|
1146
|
+
}
|
|
1147
|
+
if (charCount > CHAR_LIMIT * 0.6) {
|
|
1148
|
+
maxSessions = 2;
|
|
1149
|
+
maxConstraints = 5;
|
|
1150
|
+
priorSessions = priorSessions.slice(0, maxSessions);
|
|
1151
|
+
constraintEntries = constraintEntries.slice(0, maxConstraints);
|
|
1152
|
+
}
|
|
1153
|
+
if (priorSessions.length > 0) {
|
|
1154
|
+
addLine(`## Prior Agent Sessions (last ${priorSessions.length})`);
|
|
1155
|
+
for (const s of priorSessions) {
|
|
1156
|
+
const date = new Date(s.startedAt).toISOString().split("T")[0];
|
|
1157
|
+
const created = Array.isArray(s.entriesCreated) ? s.entriesCreated.length : s.entriesCreated ?? 0;
|
|
1158
|
+
const modified = Array.isArray(s.entriesModified) ? s.entriesModified.length : s.entriesModified ?? 0;
|
|
1159
|
+
const stats = `${created} created, ${modified} modified, ${s.relationsCreated ?? 0} relations`;
|
|
1160
|
+
const gates = (s.gateFailures ?? 0) > 0 ? `, ${s.gateFailures} gate failures` : "";
|
|
1161
|
+
const warns = (s.contradictionWarnings ?? 0) > 0 ? `, ${s.contradictionWarnings} contradiction warnings` : "";
|
|
1162
|
+
addLine(`- **${date}** (${s.status}, by ${s.initiatedBy ?? "unknown"}) \u2014 ${stats}${gates}${warns}`);
|
|
1163
|
+
}
|
|
1164
|
+
addLine("");
|
|
1165
|
+
}
|
|
1166
|
+
if (constraintEntries.length > 0) {
|
|
1167
|
+
addLine(`## Active Constraints (${constraintEntries.length})`);
|
|
1168
|
+
addLine("_Architecture, business rules, and decisions that govern this workspace._");
|
|
1169
|
+
addLine("");
|
|
1170
|
+
for (const c of constraintEntries) {
|
|
1171
|
+
const id = c.entryId ?? "(no ID)";
|
|
1172
|
+
const col = c.collectionSlug ?? "";
|
|
1173
|
+
const desc = c.data?.description ?? c.data?.rationale ?? "";
|
|
1174
|
+
const truncated = desc.length > 100 ? desc.slice(0, 100) + "..." : desc;
|
|
1175
|
+
addLine(`- \`${id}\` **${c.name}** [${col}] \u2014 ${truncated}`);
|
|
1176
|
+
}
|
|
1177
|
+
addLine("");
|
|
1178
|
+
}
|
|
1179
|
+
if (errors.length > 0) {
|
|
1180
|
+
addLine("## Errors");
|
|
1181
|
+
for (const err of errors) addLine(`- ${err}`);
|
|
1182
|
+
addLine("");
|
|
1183
|
+
}
|
|
1184
|
+
if (agentSessionId) {
|
|
1185
|
+
try {
|
|
1186
|
+
await mcpCall("agent.markOriented", { sessionId: agentSessionId });
|
|
1187
|
+
setSessionOriented(true);
|
|
1188
|
+
const scope = getApiKeyScope();
|
|
1189
|
+
const sessionInfo = `Session ${agentSessionId}.`;
|
|
1190
|
+
const readinessInfo = readiness ? `${readiness.score}% readiness.` : "";
|
|
1191
|
+
const tensionInfo = openTensions.length > 0 ? `${openTensions.length} open tensions.` : "No open tensions.";
|
|
1192
|
+
let sessionSummary = "";
|
|
1193
|
+
if (priorSessions.length > 0) {
|
|
1194
|
+
const lastSession = priorSessions[0];
|
|
1195
|
+
const created = Array.isArray(lastSession.entriesCreated) ? lastSession.entriesCreated.length : lastSession.entriesCreated ?? 0;
|
|
1196
|
+
const modified = Array.isArray(lastSession.entriesModified) ? lastSession.entriesModified.length : lastSession.entriesModified ?? 0;
|
|
1197
|
+
sessionSummary = `Last session: ${created} created, ${modified} modified.`;
|
|
1198
|
+
}
|
|
1199
|
+
addLine("---");
|
|
1200
|
+
addLine(`Orientation complete. ${sessionInfo} Write tools now available. ${readinessInfo} ${tensionInfo} ${sessionSummary}`);
|
|
1201
|
+
} catch {
|
|
1202
|
+
addLine("---");
|
|
1203
|
+
addLine("_Warning: Could not mark session as oriented. Write tools may be restricted._");
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
addLine("---");
|
|
1207
|
+
addLine("_No active agent session. Call `agent-start` to begin a tracked session._");
|
|
1208
|
+
}
|
|
1209
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1210
|
+
}
|
|
1211
|
+
);
|
|
981
1212
|
server2.registerTool(
|
|
982
1213
|
"mcp-audit",
|
|
983
1214
|
{
|
|
@@ -989,13 +1220,13 @@ function registerHealthTools(server2) {
|
|
|
989
1220
|
annotations: { readOnlyHint: true }
|
|
990
1221
|
},
|
|
991
1222
|
async ({ limit }) => {
|
|
992
|
-
const
|
|
993
|
-
const recent =
|
|
1223
|
+
const log = getAuditLog();
|
|
1224
|
+
const recent = log.slice(-limit);
|
|
994
1225
|
if (recent.length === 0) {
|
|
995
1226
|
return { content: [{ type: "text", text: "No calls recorded yet this session." }] };
|
|
996
1227
|
}
|
|
997
|
-
const summary = buildSessionSummary(
|
|
998
|
-
const logLines = [`# Audit Log (last ${recent.length} of ${
|
|
1228
|
+
const summary = buildSessionSummary(log);
|
|
1229
|
+
const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
|
|
999
1230
|
`];
|
|
1000
1231
|
for (const entry of recent) {
|
|
1001
1232
|
const icon = entry.status === "ok" ? "\u2713" : "\u2717";
|
|
@@ -1188,7 +1419,7 @@ function registerVerifyTools(server2) {
|
|
|
1188
1419
|
data: `Verifying "${collection}" against ${schema.size} schema tables at ${projectRoot}`,
|
|
1189
1420
|
logger: "product-os"
|
|
1190
1421
|
});
|
|
1191
|
-
const scopedEntries = await mcpQuery("
|
|
1422
|
+
const scopedEntries = await mcpQuery("chain.listEntries", { collectionSlug: collection });
|
|
1192
1423
|
if (scopedEntries.length === 0) {
|
|
1193
1424
|
return {
|
|
1194
1425
|
content: [{ type: "text", text: `No entries found in \`${collection}\`. Nothing to verify.` }]
|
|
@@ -1196,7 +1427,7 @@ function registerVerifyTools(server2) {
|
|
|
1196
1427
|
}
|
|
1197
1428
|
let allEntryIds;
|
|
1198
1429
|
try {
|
|
1199
|
-
const allEntries = await mcpQuery("
|
|
1430
|
+
const allEntries = await mcpQuery("chain.listEntries", {});
|
|
1200
1431
|
allEntryIds = new Set(allEntries.map((e) => e.entryId).filter(Boolean));
|
|
1201
1432
|
} catch {
|
|
1202
1433
|
allEntryIds = new Set(scopedEntries.map((e) => e.entryId).filter(Boolean));
|
|
@@ -1261,7 +1492,7 @@ function registerVerifyTools(server2) {
|
|
|
1261
1492
|
const updated = (entry.data?.codeMapping ?? []).map(
|
|
1262
1493
|
(cm) => cm.status === "aligned" && driftedFields.has(cm.field) ? { ...cm, status: "drifted" } : cm
|
|
1263
1494
|
);
|
|
1264
|
-
await mcpMutation("
|
|
1495
|
+
await mcpMutation("chain.updateEntry", {
|
|
1265
1496
|
entryId: entry.entryId,
|
|
1266
1497
|
data: { codeMapping: updated }
|
|
1267
1498
|
});
|
|
@@ -1283,655 +1514,39 @@ function registerVerifyTools(server2) {
|
|
|
1283
1514
|
);
|
|
1284
1515
|
}
|
|
1285
1516
|
|
|
1286
|
-
// src/tools/
|
|
1517
|
+
// src/tools/architecture.ts
|
|
1518
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
1519
|
+
import { resolve as resolve2, relative, dirname, normalize } from "path";
|
|
1287
1520
|
import { z as z5 } from "zod";
|
|
1288
|
-
var
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1521
|
+
var COLLECTION_SLUG = "architecture";
|
|
1522
|
+
var COLLECTION_FIELDS = [
|
|
1523
|
+
{ key: "archType", label: "Architecture Type", type: "select", required: true, options: ["template", "layer", "node", "flow"], searchable: true },
|
|
1524
|
+
{ key: "templateRef", label: "Template Entry ID", type: "text", searchable: false },
|
|
1525
|
+
{ key: "layerRef", label: "Layer Entry ID", type: "text", searchable: false },
|
|
1526
|
+
{ key: "description", label: "Description", type: "text", searchable: true },
|
|
1527
|
+
{ key: "color", label: "Color", type: "text" },
|
|
1528
|
+
{ key: "icon", label: "Icon", type: "text" },
|
|
1529
|
+
{ key: "sourceNode", label: "Source Node (flows)", type: "text" },
|
|
1530
|
+
{ key: "targetNode", label: "Target Node (flows)", type: "text" },
|
|
1531
|
+
{ key: "filePaths", label: "File Paths", type: "text", searchable: true },
|
|
1532
|
+
{ key: "owner", label: "Owner (circle/role)", type: "text", searchable: true },
|
|
1533
|
+
{ key: "layerOrder", label: "Layer Order (for templates)", type: "text" },
|
|
1534
|
+
{ key: "rationale", label: "Why Here? (placement rationale)", type: "text", searchable: true },
|
|
1535
|
+
{ key: "dependsOn", label: "Allowed Dependencies (layers this can import from)", type: "text" }
|
|
1536
|
+
];
|
|
1537
|
+
async function ensureCollection() {
|
|
1538
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
1539
|
+
if (collections.some((c) => c.slug === COLLECTION_SLUG)) return;
|
|
1540
|
+
await mcpMutation("chain.createCollection", {
|
|
1541
|
+
slug: COLLECTION_SLUG,
|
|
1542
|
+
name: "Architecture",
|
|
1543
|
+
icon: "\u{1F3D7}\uFE0F",
|
|
1544
|
+
description: "System architecture map \u2014 templates, layers, nodes, and flows. Visualized in the Architecture Explorer and via MCP architecture tools.",
|
|
1545
|
+
fields: COLLECTION_FIELDS
|
|
1546
|
+
});
|
|
1309
1547
|
}
|
|
1310
|
-
function
|
|
1311
|
-
return
|
|
1312
|
-
}
|
|
1313
|
-
var COMMON_CHECKS = {
|
|
1314
|
-
clearName: {
|
|
1315
|
-
id: "clear-name",
|
|
1316
|
-
label: "Clear, specific name (not vague)",
|
|
1317
|
-
check: (ctx) => ctx.name.length > 10 && !["new tension", "new entry", "untitled", "test"].includes(ctx.name.toLowerCase()),
|
|
1318
|
-
suggestion: () => "Rename to something specific \u2014 describe the actual problem or concept."
|
|
1319
|
-
},
|
|
1320
|
-
hasDescription: {
|
|
1321
|
-
id: "has-description",
|
|
1322
|
-
label: "Description provided (>50 chars)",
|
|
1323
|
-
check: (ctx) => ctx.description.length > 50,
|
|
1324
|
-
suggestion: () => "Add a fuller description explaining context and impact."
|
|
1325
|
-
},
|
|
1326
|
-
hasRelations: {
|
|
1327
|
-
id: "has-relations",
|
|
1328
|
-
label: "At least 1 relation created",
|
|
1329
|
-
check: (ctx) => ctx.linksCreated.length >= 1,
|
|
1330
|
-
suggestion: () => "Use `suggest-links` and `relate-entries` to add more connections."
|
|
1331
|
-
},
|
|
1332
|
-
diverseRelations: {
|
|
1333
|
-
id: "diverse-relations",
|
|
1334
|
-
label: "Relations span multiple collections",
|
|
1335
|
-
check: (ctx) => {
|
|
1336
|
-
const colls = new Set(ctx.linksCreated.map((l) => l.targetCollection));
|
|
1337
|
-
return colls.size >= 2;
|
|
1338
|
-
},
|
|
1339
|
-
suggestion: () => "Try linking to entries in different collections (glossary, business-rules, strategy)."
|
|
1340
|
-
}
|
|
1341
|
-
};
|
|
1342
|
-
var PROFILES = /* @__PURE__ */ new Map([
|
|
1343
|
-
["tensions", {
|
|
1344
|
-
idPrefix: "TEN",
|
|
1345
|
-
governedDraft: false,
|
|
1346
|
-
descriptionField: "description",
|
|
1347
|
-
defaults: [
|
|
1348
|
-
{ key: "priority", value: "medium" },
|
|
1349
|
-
{ key: "date", value: "today" },
|
|
1350
|
-
{ key: "raised", value: "infer" },
|
|
1351
|
-
{ key: "severity", value: "infer" }
|
|
1352
|
-
],
|
|
1353
|
-
recommendedRelationTypes: ["surfaces_tension_in", "references", "belongs_to", "related_to"],
|
|
1354
|
-
inferField: (ctx) => {
|
|
1355
|
-
const fields = {};
|
|
1356
|
-
const text = `${ctx.name} ${ctx.description}`;
|
|
1357
|
-
const area = inferArea(text);
|
|
1358
|
-
if (area) fields.raised = area;
|
|
1359
|
-
if (text.toLowerCase().includes("critical") || text.toLowerCase().includes("blocker")) {
|
|
1360
|
-
fields.severity = "critical";
|
|
1361
|
-
} else if (text.toLowerCase().includes("bottleneck") || text.toLowerCase().includes("scaling") || text.toLowerCase().includes("breaking")) {
|
|
1362
|
-
fields.severity = "high";
|
|
1363
|
-
} else {
|
|
1364
|
-
fields.severity = "medium";
|
|
1365
|
-
}
|
|
1366
|
-
if (area) fields.affectedArea = area;
|
|
1367
|
-
return fields;
|
|
1368
|
-
},
|
|
1369
|
-
qualityChecks: [
|
|
1370
|
-
COMMON_CHECKS.clearName,
|
|
1371
|
-
COMMON_CHECKS.hasDescription,
|
|
1372
|
-
COMMON_CHECKS.hasRelations,
|
|
1373
|
-
{
|
|
1374
|
-
id: "has-severity",
|
|
1375
|
-
label: "Severity specified",
|
|
1376
|
-
check: (ctx) => !!ctx.data.severity && ctx.data.severity !== "",
|
|
1377
|
-
suggestion: (ctx) => {
|
|
1378
|
-
const text = `${ctx.name} ${ctx.description}`.toLowerCase();
|
|
1379
|
-
const inferred = text.includes("critical") ? "critical" : text.includes("bottleneck") ? "high" : "medium";
|
|
1380
|
-
return `Set severity \u2014 suggest: ${inferred} (based on description keywords).`;
|
|
1381
|
-
}
|
|
1382
|
-
},
|
|
1383
|
-
{
|
|
1384
|
-
id: "has-affected-area",
|
|
1385
|
-
label: "Affected area identified",
|
|
1386
|
-
check: (ctx) => !!ctx.data.affectedArea && ctx.data.affectedArea !== "",
|
|
1387
|
-
suggestion: (ctx) => {
|
|
1388
|
-
const area = inferArea(`${ctx.name} ${ctx.description}`);
|
|
1389
|
-
return area ? `Set affectedArea \u2014 suggest: "${area}" (inferred from content).` : "Specify which product area or domain this tension impacts.";
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
]
|
|
1393
|
-
}],
|
|
1394
|
-
["business-rules", {
|
|
1395
|
-
idPrefix: "SOS",
|
|
1396
|
-
governedDraft: true,
|
|
1397
|
-
descriptionField: "description",
|
|
1398
|
-
defaults: [
|
|
1399
|
-
{ key: "severity", value: "medium" },
|
|
1400
|
-
{ key: "domain", value: "infer" }
|
|
1401
|
-
],
|
|
1402
|
-
recommendedRelationTypes: ["governs", "references", "conflicts_with", "related_to"],
|
|
1403
|
-
inferField: (ctx) => {
|
|
1404
|
-
const fields = {};
|
|
1405
|
-
const domain = inferDomain(`${ctx.name} ${ctx.description}`);
|
|
1406
|
-
if (domain) fields.domain = domain;
|
|
1407
|
-
return fields;
|
|
1408
|
-
},
|
|
1409
|
-
qualityChecks: [
|
|
1410
|
-
COMMON_CHECKS.clearName,
|
|
1411
|
-
COMMON_CHECKS.hasDescription,
|
|
1412
|
-
COMMON_CHECKS.hasRelations,
|
|
1413
|
-
{
|
|
1414
|
-
id: "has-rationale",
|
|
1415
|
-
label: "Rationale provided",
|
|
1416
|
-
check: (ctx) => typeof ctx.data.rationale === "string" && ctx.data.rationale.length > 10,
|
|
1417
|
-
suggestion: () => "Add a rationale explaining why this rule exists via `update-entry`."
|
|
1418
|
-
},
|
|
1419
|
-
{
|
|
1420
|
-
id: "has-domain",
|
|
1421
|
-
label: "Domain specified",
|
|
1422
|
-
check: (ctx) => !!ctx.data.domain && ctx.data.domain !== "",
|
|
1423
|
-
suggestion: (ctx) => {
|
|
1424
|
-
const domain = inferDomain(`${ctx.name} ${ctx.description}`);
|
|
1425
|
-
return domain ? `Set domain \u2014 suggest: "${domain}" (inferred from content).` : "Specify the business domain this rule belongs to.";
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
]
|
|
1429
|
-
}],
|
|
1430
|
-
["glossary", {
|
|
1431
|
-
idPrefix: "GT",
|
|
1432
|
-
governedDraft: true,
|
|
1433
|
-
descriptionField: "canonical",
|
|
1434
|
-
defaults: [
|
|
1435
|
-
{ key: "category", value: "infer" }
|
|
1436
|
-
],
|
|
1437
|
-
recommendedRelationTypes: ["defines_term_for", "confused_with", "related_to", "references"],
|
|
1438
|
-
inferField: (ctx) => {
|
|
1439
|
-
const fields = {};
|
|
1440
|
-
const area = inferArea(`${ctx.name} ${ctx.description}`);
|
|
1441
|
-
if (area) {
|
|
1442
|
-
const categoryMap = {
|
|
1443
|
-
"Architecture": "Platform & Architecture",
|
|
1444
|
-
"Chain": "Knowledge Management",
|
|
1445
|
-
"AI & MCP Integration": "AI & Developer Tools",
|
|
1446
|
-
"Developer Experience": "AI & Developer Tools",
|
|
1447
|
-
"Governance & Decision-Making": "Governance & Process",
|
|
1448
|
-
"Analytics & Tracking": "Platform & Architecture",
|
|
1449
|
-
"Security": "Platform & Architecture"
|
|
1450
|
-
};
|
|
1451
|
-
fields.category = categoryMap[area] ?? "";
|
|
1452
|
-
}
|
|
1453
|
-
return fields;
|
|
1454
|
-
},
|
|
1455
|
-
qualityChecks: [
|
|
1456
|
-
COMMON_CHECKS.clearName,
|
|
1457
|
-
{
|
|
1458
|
-
id: "has-canonical",
|
|
1459
|
-
label: "Canonical definition provided (>20 chars)",
|
|
1460
|
-
check: (ctx) => {
|
|
1461
|
-
const canonical = ctx.data.canonical;
|
|
1462
|
-
return typeof canonical === "string" && canonical.length > 20;
|
|
1463
|
-
},
|
|
1464
|
-
suggestion: () => "Add a clear canonical definition \u2014 this is the single source of truth for this term."
|
|
1465
|
-
},
|
|
1466
|
-
COMMON_CHECKS.hasRelations,
|
|
1467
|
-
{
|
|
1468
|
-
id: "has-category",
|
|
1469
|
-
label: "Category assigned",
|
|
1470
|
-
check: (ctx) => !!ctx.data.category && ctx.data.category !== "",
|
|
1471
|
-
suggestion: () => "Assign a category (e.g., 'Platform & Architecture', 'Governance & Process')."
|
|
1472
|
-
}
|
|
1473
|
-
]
|
|
1474
|
-
}],
|
|
1475
|
-
["decisions", {
|
|
1476
|
-
idPrefix: "DEC",
|
|
1477
|
-
governedDraft: false,
|
|
1478
|
-
descriptionField: "rationale",
|
|
1479
|
-
defaults: [
|
|
1480
|
-
{ key: "date", value: "today" },
|
|
1481
|
-
{ key: "decidedBy", value: "infer" }
|
|
1482
|
-
],
|
|
1483
|
-
recommendedRelationTypes: ["informs", "references", "replaces", "related_to"],
|
|
1484
|
-
inferField: (ctx) => {
|
|
1485
|
-
const fields = {};
|
|
1486
|
-
const area = inferArea(`${ctx.name} ${ctx.description}`);
|
|
1487
|
-
if (area) fields.decidedBy = area;
|
|
1488
|
-
return fields;
|
|
1489
|
-
},
|
|
1490
|
-
qualityChecks: [
|
|
1491
|
-
COMMON_CHECKS.clearName,
|
|
1492
|
-
{
|
|
1493
|
-
id: "has-rationale",
|
|
1494
|
-
label: "Rationale provided (>30 chars)",
|
|
1495
|
-
check: (ctx) => {
|
|
1496
|
-
const rationale = ctx.data.rationale;
|
|
1497
|
-
return typeof rationale === "string" && rationale.length > 30;
|
|
1498
|
-
},
|
|
1499
|
-
suggestion: () => "Explain why this decision was made \u2014 what was considered and rejected?"
|
|
1500
|
-
},
|
|
1501
|
-
COMMON_CHECKS.hasRelations,
|
|
1502
|
-
{
|
|
1503
|
-
id: "has-date",
|
|
1504
|
-
label: "Decision date recorded",
|
|
1505
|
-
check: (ctx) => !!ctx.data.date && ctx.data.date !== "",
|
|
1506
|
-
suggestion: () => "Record when this decision was made."
|
|
1507
|
-
}
|
|
1508
|
-
]
|
|
1509
|
-
}],
|
|
1510
|
-
["features", {
|
|
1511
|
-
idPrefix: "FEAT",
|
|
1512
|
-
governedDraft: false,
|
|
1513
|
-
descriptionField: "description",
|
|
1514
|
-
defaults: [],
|
|
1515
|
-
recommendedRelationTypes: ["belongs_to", "depends_on", "surfaces_tension_in", "related_to"],
|
|
1516
|
-
qualityChecks: [
|
|
1517
|
-
COMMON_CHECKS.clearName,
|
|
1518
|
-
COMMON_CHECKS.hasDescription,
|
|
1519
|
-
COMMON_CHECKS.hasRelations,
|
|
1520
|
-
{
|
|
1521
|
-
id: "has-owner",
|
|
1522
|
-
label: "Owner assigned",
|
|
1523
|
-
check: (ctx) => !!ctx.data.owner && ctx.data.owner !== "",
|
|
1524
|
-
suggestion: () => "Assign an owner team or product area."
|
|
1525
|
-
},
|
|
1526
|
-
{
|
|
1527
|
-
id: "has-rationale",
|
|
1528
|
-
label: "Rationale documented",
|
|
1529
|
-
check: (ctx) => !!ctx.data.rationale && String(ctx.data.rationale).length > 20,
|
|
1530
|
-
suggestion: () => "Explain why this feature matters \u2014 what problem does it solve?"
|
|
1531
|
-
}
|
|
1532
|
-
]
|
|
1533
|
-
}]
|
|
1534
|
-
]);
|
|
1535
|
-
var FALLBACK_PROFILE = {
|
|
1536
|
-
idPrefix: "",
|
|
1537
|
-
governedDraft: false,
|
|
1538
|
-
descriptionField: "description",
|
|
1539
|
-
defaults: [],
|
|
1540
|
-
recommendedRelationTypes: ["related_to", "references"],
|
|
1541
|
-
qualityChecks: [
|
|
1542
|
-
COMMON_CHECKS.clearName,
|
|
1543
|
-
COMMON_CHECKS.hasDescription,
|
|
1544
|
-
COMMON_CHECKS.hasRelations
|
|
1545
|
-
]
|
|
1546
|
-
};
|
|
1547
|
-
function generateEntryId(prefix) {
|
|
1548
|
-
if (!prefix) return "";
|
|
1549
|
-
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1550
|
-
let suffix = "";
|
|
1551
|
-
for (let i = 0; i < 6; i++) {
|
|
1552
|
-
suffix += chars[Math.floor(Math.random() * chars.length)];
|
|
1553
|
-
}
|
|
1554
|
-
return `${prefix}-${suffix}`;
|
|
1555
|
-
}
|
|
1556
|
-
function extractSearchTerms(name, description) {
|
|
1557
|
-
const text = `${name} ${description}`;
|
|
1558
|
-
return text.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 8).join(" ");
|
|
1559
|
-
}
|
|
1560
|
-
function computeLinkConfidence(candidate, sourceName, sourceDescription, sourceCollection, candidateCollection) {
|
|
1561
|
-
const text = `${sourceName} ${sourceDescription}`.toLowerCase();
|
|
1562
|
-
const candidateName = candidate.name.toLowerCase();
|
|
1563
|
-
let score = 0;
|
|
1564
|
-
if (text.includes(candidateName) && candidateName.length > 3) {
|
|
1565
|
-
score += 40;
|
|
1566
|
-
}
|
|
1567
|
-
const candidateWords = candidateName.split(/\s+/).filter((w) => w.length > 3);
|
|
1568
|
-
const matchingWords = candidateWords.filter((w) => text.includes(w));
|
|
1569
|
-
score += matchingWords.length / Math.max(candidateWords.length, 1) * 30;
|
|
1570
|
-
const HUB_COLLECTIONS = /* @__PURE__ */ new Set(["strategy", "features"]);
|
|
1571
|
-
if (HUB_COLLECTIONS.has(candidateCollection)) {
|
|
1572
|
-
score += 15;
|
|
1573
|
-
}
|
|
1574
|
-
if (candidateCollection !== sourceCollection) {
|
|
1575
|
-
score += 10;
|
|
1576
|
-
}
|
|
1577
|
-
return Math.min(score, 100);
|
|
1578
|
-
}
|
|
1579
|
-
function inferRelationType(sourceCollection, targetCollection, profile) {
|
|
1580
|
-
const typeMap = {
|
|
1581
|
-
tensions: {
|
|
1582
|
-
glossary: "surfaces_tension_in",
|
|
1583
|
-
"business-rules": "references",
|
|
1584
|
-
strategy: "belongs_to",
|
|
1585
|
-
features: "surfaces_tension_in",
|
|
1586
|
-
decisions: "references"
|
|
1587
|
-
},
|
|
1588
|
-
"business-rules": {
|
|
1589
|
-
glossary: "references",
|
|
1590
|
-
features: "governs",
|
|
1591
|
-
strategy: "belongs_to",
|
|
1592
|
-
tensions: "references"
|
|
1593
|
-
},
|
|
1594
|
-
glossary: {
|
|
1595
|
-
features: "defines_term_for",
|
|
1596
|
-
"business-rules": "references",
|
|
1597
|
-
strategy: "references"
|
|
1598
|
-
},
|
|
1599
|
-
decisions: {
|
|
1600
|
-
features: "informs",
|
|
1601
|
-
"business-rules": "references",
|
|
1602
|
-
strategy: "references",
|
|
1603
|
-
tensions: "references"
|
|
1604
|
-
}
|
|
1605
|
-
};
|
|
1606
|
-
return typeMap[sourceCollection]?.[targetCollection] ?? profile.recommendedRelationTypes[0] ?? "related_to";
|
|
1607
|
-
}
|
|
1608
|
-
function scoreQuality(ctx, profile) {
|
|
1609
|
-
const checks = profile.qualityChecks.map((qc) => {
|
|
1610
|
-
const passed2 = qc.check(ctx);
|
|
1611
|
-
return {
|
|
1612
|
-
id: qc.id,
|
|
1613
|
-
label: qc.label,
|
|
1614
|
-
passed: passed2,
|
|
1615
|
-
suggestion: passed2 ? void 0 : qc.suggestion?.(ctx)
|
|
1616
|
-
};
|
|
1617
|
-
});
|
|
1618
|
-
const passed = checks.filter((c) => c.passed).length;
|
|
1619
|
-
const total = checks.length;
|
|
1620
|
-
const score = total > 0 ? Math.round(passed / total * 10) : 10;
|
|
1621
|
-
return { score, maxScore: 10, checks };
|
|
1622
|
-
}
|
|
1623
|
-
function formatQualityReport(result) {
|
|
1624
|
-
const lines = [`## Quality: ${result.score}/${result.maxScore}`];
|
|
1625
|
-
for (const check of result.checks) {
|
|
1626
|
-
const icon = check.passed ? "[x]" : "[ ]";
|
|
1627
|
-
const suggestion = check.passed ? "" : ` -- ${check.suggestion ?? ""}`;
|
|
1628
|
-
lines.push(`${icon} ${check.label}${suggestion}`);
|
|
1629
|
-
}
|
|
1630
|
-
return lines.join("\n");
|
|
1631
|
-
}
|
|
1632
|
-
async function checkEntryQuality(entryId) {
|
|
1633
|
-
const entry = await mcpQuery("kb.getEntry", { entryId });
|
|
1634
|
-
if (!entry) {
|
|
1635
|
-
return {
|
|
1636
|
-
text: `Entry \`${entryId}\` not found. Try search to find the right ID.`,
|
|
1637
|
-
quality: { score: 0, maxScore: 10, checks: [] }
|
|
1638
|
-
};
|
|
1639
|
-
}
|
|
1640
|
-
const collections = await mcpQuery("kb.listCollections");
|
|
1641
|
-
const collMap = /* @__PURE__ */ new Map();
|
|
1642
|
-
for (const c of collections) collMap.set(c._id, c.slug);
|
|
1643
|
-
const collectionSlug = collMap.get(entry.collectionId) ?? "unknown";
|
|
1644
|
-
const profile = PROFILES.get(collectionSlug) ?? FALLBACK_PROFILE;
|
|
1645
|
-
const relations = await mcpQuery("kb.listEntryRelations", { entryId });
|
|
1646
|
-
const linksCreated = [];
|
|
1647
|
-
for (const r of relations) {
|
|
1648
|
-
const otherId = r.fromId === entry._id ? r.toId : r.fromId;
|
|
1649
|
-
linksCreated.push({
|
|
1650
|
-
targetEntryId: otherId,
|
|
1651
|
-
targetName: "",
|
|
1652
|
-
targetCollection: "",
|
|
1653
|
-
relationType: r.type
|
|
1654
|
-
});
|
|
1655
|
-
}
|
|
1656
|
-
const descField = profile.descriptionField;
|
|
1657
|
-
const description = typeof entry.data?.[descField] === "string" ? entry.data[descField] : "";
|
|
1658
|
-
const ctx = {
|
|
1659
|
-
collection: collectionSlug,
|
|
1660
|
-
name: entry.name,
|
|
1661
|
-
description,
|
|
1662
|
-
data: entry.data ?? {},
|
|
1663
|
-
entryId: entry.entryId ?? "",
|
|
1664
|
-
linksCreated,
|
|
1665
|
-
linksSuggested: [],
|
|
1666
|
-
collectionFields: []
|
|
1667
|
-
};
|
|
1668
|
-
const quality = scoreQuality(ctx, profile);
|
|
1669
|
-
const lines = [
|
|
1670
|
-
`# Quality Check: ${entry.entryId ?? entry.name}`,
|
|
1671
|
-
`**${entry.name}** in \`${collectionSlug}\` [${entry.status}]`,
|
|
1672
|
-
"",
|
|
1673
|
-
formatQualityReport(quality)
|
|
1674
|
-
];
|
|
1675
|
-
if (quality.score < 10) {
|
|
1676
|
-
const failedChecks = quality.checks.filter((c) => !c.passed && c.suggestion);
|
|
1677
|
-
if (failedChecks.length > 0) {
|
|
1678
|
-
lines.push("");
|
|
1679
|
-
lines.push(`_To improve: use \`update-entry\` to fill missing fields, or \`relate-entries\` to add connections._`);
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
return { text: lines.join("\n"), quality };
|
|
1683
|
-
}
|
|
1684
|
-
var GOVERNED_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
1685
|
-
"glossary",
|
|
1686
|
-
"business-rules",
|
|
1687
|
-
"principles",
|
|
1688
|
-
"standards",
|
|
1689
|
-
"strategy",
|
|
1690
|
-
"features"
|
|
1691
|
-
]);
|
|
1692
|
-
var AUTO_LINK_CONFIDENCE_THRESHOLD = 35;
|
|
1693
|
-
var MAX_AUTO_LINKS = 5;
|
|
1694
|
-
var MAX_SUGGESTIONS = 5;
|
|
1695
|
-
function registerSmartCaptureTools(server2) {
|
|
1696
|
-
server2.registerTool(
|
|
1697
|
-
"capture",
|
|
1698
|
-
{
|
|
1699
|
-
title: "Capture",
|
|
1700
|
-
description: "The single tool for creating knowledge entries. Creates an entry, auto-links related entries, and returns a quality scorecard \u2014 all in one call. Provide a collection, name, and description \u2014 everything else is inferred or auto-filled.\n\nSupported collections with smart profiles: tensions, business-rules, glossary, decisions, features.\nAll other collections use sensible defaults.\n\nAlways creates as 'draft' for governed collections. Use `update-entry` for post-creation adjustments.",
|
|
1701
|
-
inputSchema: {
|
|
1702
|
-
collection: z5.string().describe("Collection slug, e.g. 'tensions', 'business-rules', 'glossary', 'decisions'"),
|
|
1703
|
-
name: z5.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
|
|
1704
|
-
description: z5.string().describe("Full context \u2014 what's happening, why it matters, what you observed"),
|
|
1705
|
-
context: z5.string().optional().describe("Optional additional context (e.g. 'Observed during gather-context calls taking 700ms+')"),
|
|
1706
|
-
entryId: z5.string().optional().describe("Optional custom entry ID (e.g. 'TEN-my-id'). Auto-generated if omitted.")
|
|
1707
|
-
},
|
|
1708
|
-
annotations: { destructiveHint: false }
|
|
1709
|
-
},
|
|
1710
|
-
async ({ collection, name, description, context, entryId }) => {
|
|
1711
|
-
const profile = PROFILES.get(collection) ?? FALLBACK_PROFILE;
|
|
1712
|
-
const col = await mcpQuery("kb.getCollection", { slug: collection });
|
|
1713
|
-
if (!col) {
|
|
1714
|
-
return {
|
|
1715
|
-
content: [{ type: "text", text: `Collection \`${collection}\` not found. Use \`list-collections\` to see available collections.` }]
|
|
1716
|
-
};
|
|
1717
|
-
}
|
|
1718
|
-
const data = {};
|
|
1719
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1720
|
-
for (const field of col.fields ?? []) {
|
|
1721
|
-
const key = field.key;
|
|
1722
|
-
if (key === profile.descriptionField) {
|
|
1723
|
-
data[key] = description;
|
|
1724
|
-
} else if (field.type === "array" || field.type === "multi-select") {
|
|
1725
|
-
data[key] = [];
|
|
1726
|
-
} else {
|
|
1727
|
-
data[key] = "";
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
for (const def of profile.defaults) {
|
|
1731
|
-
if (def.value === "today") {
|
|
1732
|
-
data[def.key] = today;
|
|
1733
|
-
} else if (def.value !== "infer") {
|
|
1734
|
-
data[def.key] = def.value;
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
if (profile.inferField) {
|
|
1738
|
-
const inferred = profile.inferField({
|
|
1739
|
-
collection,
|
|
1740
|
-
name,
|
|
1741
|
-
description,
|
|
1742
|
-
context,
|
|
1743
|
-
data,
|
|
1744
|
-
entryId: "",
|
|
1745
|
-
linksCreated: [],
|
|
1746
|
-
linksSuggested: [],
|
|
1747
|
-
collectionFields: col.fields ?? []
|
|
1748
|
-
});
|
|
1749
|
-
for (const [key, val] of Object.entries(inferred)) {
|
|
1750
|
-
if (val !== void 0 && val !== "") {
|
|
1751
|
-
data[key] = val;
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
if (!data[profile.descriptionField] && !data.description && !data.canonical) {
|
|
1756
|
-
data[profile.descriptionField || "description"] = description;
|
|
1757
|
-
}
|
|
1758
|
-
const status = GOVERNED_COLLECTIONS.has(collection) ? "draft" : "draft";
|
|
1759
|
-
const finalEntryId = entryId ?? generateEntryId(profile.idPrefix);
|
|
1760
|
-
let internalId;
|
|
1761
|
-
try {
|
|
1762
|
-
internalId = await mcpMutation("kb.createEntry", {
|
|
1763
|
-
collectionSlug: collection,
|
|
1764
|
-
entryId: finalEntryId || void 0,
|
|
1765
|
-
name,
|
|
1766
|
-
status,
|
|
1767
|
-
data,
|
|
1768
|
-
createdBy: "capture"
|
|
1769
|
-
});
|
|
1770
|
-
} catch (error) {
|
|
1771
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1772
|
-
if (msg.includes("Duplicate") || msg.includes("already exists")) {
|
|
1773
|
-
return {
|
|
1774
|
-
content: [{
|
|
1775
|
-
type: "text",
|
|
1776
|
-
text: `# Cannot Capture \u2014 Duplicate Detected
|
|
1777
|
-
|
|
1778
|
-
${msg}
|
|
1779
|
-
|
|
1780
|
-
Use \`get-entry\` to inspect the existing entry, or \`update-entry\` to modify it.`
|
|
1781
|
-
}]
|
|
1782
|
-
};
|
|
1783
|
-
}
|
|
1784
|
-
throw error;
|
|
1785
|
-
}
|
|
1786
|
-
const linksCreated = [];
|
|
1787
|
-
const linksSuggested = [];
|
|
1788
|
-
const searchQuery = extractSearchTerms(name, description);
|
|
1789
|
-
if (searchQuery) {
|
|
1790
|
-
const [searchResults, allCollections] = await Promise.all([
|
|
1791
|
-
mcpQuery("kb.searchEntries", { query: searchQuery }),
|
|
1792
|
-
mcpQuery("kb.listCollections")
|
|
1793
|
-
]);
|
|
1794
|
-
const collMap = /* @__PURE__ */ new Map();
|
|
1795
|
-
for (const c of allCollections) collMap.set(c._id, c.slug);
|
|
1796
|
-
const candidates = (searchResults ?? []).filter((r) => r.entryId !== finalEntryId && r._id !== internalId).map((r) => ({
|
|
1797
|
-
...r,
|
|
1798
|
-
collSlug: collMap.get(r.collectionId) ?? "unknown",
|
|
1799
|
-
confidence: computeLinkConfidence(r, name, description, collection, collMap.get(r.collectionId) ?? "unknown")
|
|
1800
|
-
})).sort((a, b) => b.confidence - a.confidence);
|
|
1801
|
-
for (const c of candidates) {
|
|
1802
|
-
if (linksCreated.length >= MAX_AUTO_LINKS) break;
|
|
1803
|
-
if (c.confidence < AUTO_LINK_CONFIDENCE_THRESHOLD) break;
|
|
1804
|
-
if (!c.entryId || !finalEntryId) continue;
|
|
1805
|
-
const relationType = inferRelationType(collection, c.collSlug, profile);
|
|
1806
|
-
try {
|
|
1807
|
-
await mcpMutation("kb.createEntryRelation", {
|
|
1808
|
-
fromEntryId: finalEntryId,
|
|
1809
|
-
toEntryId: c.entryId,
|
|
1810
|
-
type: relationType
|
|
1811
|
-
});
|
|
1812
|
-
linksCreated.push({
|
|
1813
|
-
targetEntryId: c.entryId,
|
|
1814
|
-
targetName: c.name,
|
|
1815
|
-
targetCollection: c.collSlug,
|
|
1816
|
-
relationType
|
|
1817
|
-
});
|
|
1818
|
-
} catch {
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
const linkedIds = new Set(linksCreated.map((l) => l.targetEntryId));
|
|
1822
|
-
for (const c of candidates) {
|
|
1823
|
-
if (linksSuggested.length >= MAX_SUGGESTIONS) break;
|
|
1824
|
-
if (linkedIds.has(c.entryId)) continue;
|
|
1825
|
-
if (c.confidence < 10) continue;
|
|
1826
|
-
const preview = extractPreview2(c.data, 80);
|
|
1827
|
-
const reason = c.confidence >= AUTO_LINK_CONFIDENCE_THRESHOLD ? "high relevance (already linked)" : `"${c.name.toLowerCase().split(/\s+/).filter((w) => `${name} ${description}`.toLowerCase().includes(w) && w.length > 3).slice(0, 2).join('", "')}" appears in content`;
|
|
1828
|
-
linksSuggested.push({
|
|
1829
|
-
entryId: c.entryId,
|
|
1830
|
-
name: c.name,
|
|
1831
|
-
collection: c.collSlug,
|
|
1832
|
-
reason,
|
|
1833
|
-
preview
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
const captureCtx = {
|
|
1838
|
-
collection,
|
|
1839
|
-
name,
|
|
1840
|
-
description,
|
|
1841
|
-
context,
|
|
1842
|
-
data,
|
|
1843
|
-
entryId: finalEntryId,
|
|
1844
|
-
linksCreated,
|
|
1845
|
-
linksSuggested,
|
|
1846
|
-
collectionFields: col.fields ?? []
|
|
1847
|
-
};
|
|
1848
|
-
const quality = scoreQuality(captureCtx, profile);
|
|
1849
|
-
const lines = [
|
|
1850
|
-
`# Captured: ${finalEntryId || name}`,
|
|
1851
|
-
`**${name}** added to \`${collection}\` as \`${status}\``
|
|
1852
|
-
];
|
|
1853
|
-
if (linksCreated.length > 0) {
|
|
1854
|
-
lines.push("");
|
|
1855
|
-
lines.push(`## Auto-linked (${linksCreated.length})`);
|
|
1856
|
-
for (const link of linksCreated) {
|
|
1857
|
-
lines.push(`- -> **${link.relationType}** ${link.targetEntryId}: ${link.targetName} [${link.targetCollection}]`);
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
if (linksSuggested.length > 0) {
|
|
1861
|
-
lines.push("");
|
|
1862
|
-
lines.push("## Suggested links (review and use relate-entries)");
|
|
1863
|
-
for (let i = 0; i < linksSuggested.length; i++) {
|
|
1864
|
-
const s = linksSuggested[i];
|
|
1865
|
-
const preview = s.preview ? ` \u2014 ${s.preview}` : "";
|
|
1866
|
-
lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collection}]${preview}`);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
lines.push("");
|
|
1870
|
-
lines.push(formatQualityReport(quality));
|
|
1871
|
-
const failedChecks = quality.checks.filter((c) => !c.passed);
|
|
1872
|
-
if (failedChecks.length > 0) {
|
|
1873
|
-
lines.push("");
|
|
1874
|
-
lines.push(`_To improve: \`update-entry entryId="${finalEntryId}"\` to fill missing fields._`);
|
|
1875
|
-
}
|
|
1876
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1877
|
-
}
|
|
1878
|
-
);
|
|
1879
|
-
server2.registerTool(
|
|
1880
|
-
"quality-check",
|
|
1881
|
-
{
|
|
1882
|
-
title: "Quality Check",
|
|
1883
|
-
description: "Score an existing knowledge entry against collection-specific quality criteria. Returns a scorecard (X/10) with specific, actionable suggestions for improvement. Checks: name clarity, description completeness, relation connectedness, and collection-specific fields.\n\nUse after creating entries to assess their quality, or to audit existing entries.",
|
|
1884
|
-
inputSchema: {
|
|
1885
|
-
entryId: z5.string().describe("Entry ID to check, e.g. 'TEN-graph-db', 'GT-019', 'SOS-006'")
|
|
1886
|
-
},
|
|
1887
|
-
annotations: { readOnlyHint: true }
|
|
1888
|
-
},
|
|
1889
|
-
async ({ entryId }) => {
|
|
1890
|
-
const result = await checkEntryQuality(entryId);
|
|
1891
|
-
return { content: [{ type: "text", text: result.text }] };
|
|
1892
|
-
}
|
|
1893
|
-
);
|
|
1894
|
-
}
|
|
1895
|
-
function extractPreview2(data, maxLen) {
|
|
1896
|
-
if (!data || typeof data !== "object") return "";
|
|
1897
|
-
const raw = data.description ?? data.canonical ?? data.detail ?? data.rule ?? "";
|
|
1898
|
-
if (typeof raw !== "string" || !raw) return "";
|
|
1899
|
-
return raw.length > maxLen ? raw.substring(0, maxLen) + "..." : raw;
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
// src/tools/architecture.ts
|
|
1903
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
1904
|
-
import { resolve as resolve2, relative, dirname, normalize } from "path";
|
|
1905
|
-
import { z as z6 } from "zod";
|
|
1906
|
-
var COLLECTION_SLUG = "architecture";
|
|
1907
|
-
var COLLECTION_FIELDS = [
|
|
1908
|
-
{ key: "archType", label: "Architecture Type", type: "select", required: true, options: ["template", "layer", "node", "flow"], searchable: true },
|
|
1909
|
-
{ key: "templateRef", label: "Template Entry ID", type: "text", searchable: false },
|
|
1910
|
-
{ key: "layerRef", label: "Layer Entry ID", type: "text", searchable: false },
|
|
1911
|
-
{ key: "description", label: "Description", type: "text", searchable: true },
|
|
1912
|
-
{ key: "color", label: "Color", type: "text" },
|
|
1913
|
-
{ key: "icon", label: "Icon", type: "text" },
|
|
1914
|
-
{ key: "sourceNode", label: "Source Node (flows)", type: "text" },
|
|
1915
|
-
{ key: "targetNode", label: "Target Node (flows)", type: "text" },
|
|
1916
|
-
{ key: "filePaths", label: "File Paths", type: "text", searchable: true },
|
|
1917
|
-
{ key: "owner", label: "Owner (circle/role)", type: "text", searchable: true },
|
|
1918
|
-
{ key: "layerOrder", label: "Layer Order (for templates)", type: "text" },
|
|
1919
|
-
{ key: "rationale", label: "Why Here? (placement rationale)", type: "text", searchable: true },
|
|
1920
|
-
{ key: "dependsOn", label: "Allowed Dependencies (layers this can import from)", type: "text" }
|
|
1921
|
-
];
|
|
1922
|
-
async function ensureCollection() {
|
|
1923
|
-
const collections = await mcpQuery("kb.listCollections");
|
|
1924
|
-
if (collections.some((c) => c.slug === COLLECTION_SLUG)) return;
|
|
1925
|
-
await mcpMutation("kb.createCollection", {
|
|
1926
|
-
slug: COLLECTION_SLUG,
|
|
1927
|
-
name: "Architecture",
|
|
1928
|
-
icon: "\u{1F3D7}\uFE0F",
|
|
1929
|
-
description: "System architecture map \u2014 templates, layers, nodes, and flows. Visualized in the Architecture Explorer and via MCP architecture tools.",
|
|
1930
|
-
fields: COLLECTION_FIELDS
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
1933
|
-
async function listArchEntries() {
|
|
1934
|
-
return mcpQuery("kb.listEntries", { collectionSlug: COLLECTION_SLUG });
|
|
1548
|
+
async function listArchEntries() {
|
|
1549
|
+
return mcpQuery("chain.listEntries", { collectionSlug: COLLECTION_SLUG });
|
|
1935
1550
|
}
|
|
1936
1551
|
function byTag(entries, archType) {
|
|
1937
1552
|
return entries.filter((e) => e.tags?.includes(`archType:${archType}`) || e.data?.archType === archType).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
@@ -2056,10 +1671,10 @@ function registerArchitectureTools(server2) {
|
|
|
2056
1671
|
title: "Architecture",
|
|
2057
1672
|
description: "Explore the system architecture \u2014 show the full map, explore a specific layer, or visualize a data flow.\n\nActions:\n- `show`: Render the layered architecture map (Auth \u2192 Infra \u2192 Core \u2192 Features \u2192 Integration)\n- `explore`: Drill into a layer to see nodes, ownership, file paths\n- `flow`: Visualize a data flow path between nodes",
|
|
2058
1673
|
inputSchema: {
|
|
2059
|
-
action:
|
|
2060
|
-
template:
|
|
2061
|
-
layer:
|
|
2062
|
-
flow:
|
|
1674
|
+
action: z5.enum(["show", "explore", "flow"]).describe("Action: show full map, explore a layer, or visualize a flow"),
|
|
1675
|
+
template: z5.string().optional().describe("Template entry ID to filter by (for show)"),
|
|
1676
|
+
layer: z5.string().optional().describe("Layer name or entry ID (for explore), e.g. 'Core' or 'ARCH-layer-core'"),
|
|
1677
|
+
flow: z5.string().optional().describe("Flow name or entry ID (for flow), e.g. 'Smart Capture Flow'")
|
|
2063
1678
|
},
|
|
2064
1679
|
annotations: { readOnlyHint: true }
|
|
2065
1680
|
},
|
|
@@ -2178,7 +1793,7 @@ ${nodeDetail}${flowLines}`
|
|
|
2178
1793
|
title: "Architecture Admin",
|
|
2179
1794
|
description: "Architecture maintenance \u2014 seed the default architecture data or run a dependency health check.\n\nActions:\n- `seed`: Populate the architecture collection with the default Product OS map. Safe to re-run.\n- `check`: Scan the codebase for dependency direction violations against layer rules.",
|
|
2180
1795
|
inputSchema: {
|
|
2181
|
-
action:
|
|
1796
|
+
action: z5.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
|
|
2182
1797
|
}
|
|
2183
1798
|
},
|
|
2184
1799
|
async ({ action }) => {
|
|
@@ -2205,14 +1820,14 @@ ${nodeDetail}${flowLines}`
|
|
|
2205
1820
|
);
|
|
2206
1821
|
if (hasChanges) {
|
|
2207
1822
|
const mergedData = { ...existingData, ...seedData };
|
|
2208
|
-
await mcpMutation("
|
|
1823
|
+
await mcpMutation("chain.updateEntry", { entryId: seed.entryId, data: mergedData });
|
|
2209
1824
|
updated++;
|
|
2210
1825
|
} else {
|
|
2211
1826
|
unchanged++;
|
|
2212
1827
|
}
|
|
2213
1828
|
continue;
|
|
2214
1829
|
}
|
|
2215
|
-
await mcpMutation("
|
|
1830
|
+
await mcpMutation("chain.createEntry", {
|
|
2216
1831
|
collectionSlug: COLLECTION_SLUG,
|
|
2217
1832
|
entryId: seed.entryId,
|
|
2218
1833
|
name: seed.name,
|
|
@@ -2471,7 +2086,7 @@ function formatScanReport(result) {
|
|
|
2471
2086
|
}
|
|
2472
2087
|
|
|
2473
2088
|
// src/tools/workflows.ts
|
|
2474
|
-
import { z as
|
|
2089
|
+
import { z as z6 } from "zod";
|
|
2475
2090
|
|
|
2476
2091
|
// src/workflows/definitions.ts
|
|
2477
2092
|
var RETRO_WORKFLOW = {
|
|
@@ -2700,16 +2315,16 @@ ${cards}`
|
|
|
2700
2315
|
title: "Workflow Checkpoint",
|
|
2701
2316
|
description: "Record the output of a workflow round. Captures the round's data to the Chain as a structured entry. Use this during Facilitator Mode after completing each round to persist progress \u2014 so if the conversation is interrupted, work is not lost.\n\nAt workflow completion, this tool can also generate the final summary entry.",
|
|
2702
2317
|
inputSchema: {
|
|
2703
|
-
workflowId:
|
|
2704
|
-
roundId:
|
|
2705
|
-
output:
|
|
2706
|
-
isFinal:
|
|
2318
|
+
workflowId: z6.string().describe("Workflow ID (e.g., 'retro')"),
|
|
2319
|
+
roundId: z6.string().describe("Round ID (e.g., 'what-went-well')"),
|
|
2320
|
+
output: z6.string().describe("The round's output \u2014 synthesized by the facilitator from the conversation"),
|
|
2321
|
+
isFinal: z6.boolean().optional().describe(
|
|
2707
2322
|
"If true, this is the final checkpoint and triggers the summary chain entry creation"
|
|
2708
2323
|
),
|
|
2709
|
-
summaryName:
|
|
2324
|
+
summaryName: z6.string().optional().describe(
|
|
2710
2325
|
"Name for the final chain entry (required when isFinal=true)"
|
|
2711
2326
|
),
|
|
2712
|
-
summaryDescription:
|
|
2327
|
+
summaryDescription: z6.string().optional().describe(
|
|
2713
2328
|
"Full description/rationale for the final chain entry (required when isFinal=true)"
|
|
2714
2329
|
)
|
|
2715
2330
|
},
|
|
@@ -2745,7 +2360,7 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
|
|
|
2745
2360
|
];
|
|
2746
2361
|
if (isFinal && summaryName && summaryDescription) {
|
|
2747
2362
|
try {
|
|
2748
|
-
const entryId = await mcpMutation("
|
|
2363
|
+
const entryId = await mcpMutation("chain.createEntry", {
|
|
2749
2364
|
collectionSlug: wf.kbOutputCollection,
|
|
2750
2365
|
name: summaryName,
|
|
2751
2366
|
status: "draft",
|
|
@@ -2802,8 +2417,139 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
|
|
|
2802
2417
|
);
|
|
2803
2418
|
}
|
|
2804
2419
|
|
|
2420
|
+
// src/tools/session.ts
|
|
2421
|
+
function registerSessionTools(server2) {
|
|
2422
|
+
server2.registerTool(
|
|
2423
|
+
"agent-start",
|
|
2424
|
+
{
|
|
2425
|
+
title: "Start Agent Session",
|
|
2426
|
+
description: "Start an agent session. Creates a tracked session for this workspace with full attribution. If a session is already active, it gets superseded (graceful handover). Write tools are available after calling orient.",
|
|
2427
|
+
annotations: { readOnlyHint: false }
|
|
2428
|
+
},
|
|
2429
|
+
async () => {
|
|
2430
|
+
try {
|
|
2431
|
+
const result = await startAgentSession();
|
|
2432
|
+
const lines = [];
|
|
2433
|
+
if (result.superseded) {
|
|
2434
|
+
lines.push(
|
|
2435
|
+
`Previous session superseded. Session ${result.superseded.previousSessionId} (started ${result.superseded.startedAt}, initiated by ${result.superseded.initiatedBy}) was closed.`,
|
|
2436
|
+
""
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
lines.push(
|
|
2440
|
+
`Session ${result.sessionId} active. Initiated by ${result.initiatedBy}. Workspace ${result.workspaceName}. Scope: ${result.toolsScope}. Write tools available after orient.`
|
|
2441
|
+
);
|
|
2442
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2443
|
+
} catch (err) {
|
|
2444
|
+
return {
|
|
2445
|
+
content: [{ type: "text", text: `Failed to start agent session: ${err.message}` }],
|
|
2446
|
+
isError: true
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
);
|
|
2451
|
+
server2.registerTool(
|
|
2452
|
+
"agent-close",
|
|
2453
|
+
{
|
|
2454
|
+
title: "Close Agent Session",
|
|
2455
|
+
description: "Close the current agent session. Records structured session data (entries created, modified, relations, gate results). After this, write tools are blocked even if the MCP connection stays open.",
|
|
2456
|
+
annotations: { readOnlyHint: false }
|
|
2457
|
+
},
|
|
2458
|
+
async () => {
|
|
2459
|
+
try {
|
|
2460
|
+
const sessionId = getAgentSessionId();
|
|
2461
|
+
if (!sessionId) {
|
|
2462
|
+
return {
|
|
2463
|
+
content: [{ type: "text", text: "No active agent session to close." }]
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
const session = await mcpCall("agent.getSession", {
|
|
2467
|
+
sessionId
|
|
2468
|
+
});
|
|
2469
|
+
await closeAgentSession();
|
|
2470
|
+
const lines = [
|
|
2471
|
+
`Session ${sessionId} closed.`,
|
|
2472
|
+
""
|
|
2473
|
+
];
|
|
2474
|
+
if (session) {
|
|
2475
|
+
const created = session.entriesCreated?.length ?? 0;
|
|
2476
|
+
const modified = session.entriesModified?.length ?? 0;
|
|
2477
|
+
const relations = session.relationsCreated ?? 0;
|
|
2478
|
+
const gates = session.gateFailures ?? 0;
|
|
2479
|
+
const warnings = session.contradictionWarnings ?? 0;
|
|
2480
|
+
lines.push(
|
|
2481
|
+
`| Metric | Count |`,
|
|
2482
|
+
`|--------|-------|`,
|
|
2483
|
+
`| Entries created | ${created} |`,
|
|
2484
|
+
`| Entries modified | ${modified} |`,
|
|
2485
|
+
`| Relations created | ${relations} |`,
|
|
2486
|
+
`| Gate failures | ${gates} |`,
|
|
2487
|
+
`| Contradiction warnings | ${warnings} |`
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
lines.push("", "Write tools are now blocked. Session data saved for future orientation.");
|
|
2491
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2492
|
+
} catch (err) {
|
|
2493
|
+
return {
|
|
2494
|
+
content: [{ type: "text", text: `Failed to close agent session: ${err.message}` }],
|
|
2495
|
+
isError: true
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
);
|
|
2500
|
+
server2.registerTool(
|
|
2501
|
+
"agent-status",
|
|
2502
|
+
{
|
|
2503
|
+
title: "Agent Session Status",
|
|
2504
|
+
description: "Check the current agent session status \u2014 whether a session is active, whether orientation is complete, and session activity so far.",
|
|
2505
|
+
annotations: { readOnlyHint: true }
|
|
2506
|
+
},
|
|
2507
|
+
async () => {
|
|
2508
|
+
try {
|
|
2509
|
+
const sessionId = getAgentSessionId();
|
|
2510
|
+
if (!sessionId) {
|
|
2511
|
+
return {
|
|
2512
|
+
content: [{ type: "text", text: "No active agent session. Call `agent-start` to begin." }]
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
const session = await mcpCall("agent.getSession", {
|
|
2516
|
+
sessionId
|
|
2517
|
+
});
|
|
2518
|
+
if (!session) {
|
|
2519
|
+
return {
|
|
2520
|
+
content: [{ type: "text", text: "Session ID cached but not found in Convex. Call `agent-start` to create a new session." }]
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
const oriented = isSessionOriented();
|
|
2524
|
+
const created = session.entriesCreated?.length ?? 0;
|
|
2525
|
+
const modified = session.entriesModified?.length ?? 0;
|
|
2526
|
+
const lines = [
|
|
2527
|
+
`Session ${sessionId} ${session.status}.`,
|
|
2528
|
+
`Initiated by ${session.initiatedBy}.`,
|
|
2529
|
+
`Scope: ${session.toolsScope}.`,
|
|
2530
|
+
`Oriented: ${oriented ? "yes" : "no"}.`,
|
|
2531
|
+
"",
|
|
2532
|
+
`| Metric | Value |`,
|
|
2533
|
+
`|--------|-------|`,
|
|
2534
|
+
`| Entries created | ${created} |`,
|
|
2535
|
+
`| Entries modified | ${modified} |`,
|
|
2536
|
+
`| Relations created | ${session.relationsCreated ?? 0} |`,
|
|
2537
|
+
`| Started | ${new Date(session.startedAt).toISOString()} |`,
|
|
2538
|
+
`| Expires | ${new Date(session.expiresAt).toISOString()} |`
|
|
2539
|
+
];
|
|
2540
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
return {
|
|
2543
|
+
content: [{ type: "text", text: `Failed to get session status: ${err.message}` }],
|
|
2544
|
+
isError: true
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2805
2551
|
// src/tools/gitchain.ts
|
|
2806
|
-
import { z as
|
|
2552
|
+
import { z as z7 } from "zod";
|
|
2807
2553
|
function linkSummary(links) {
|
|
2808
2554
|
return Object.entries(links).map(([id, content]) => {
|
|
2809
2555
|
const filled = typeof content === "string" && content.length > 0;
|
|
@@ -2818,15 +2564,15 @@ function registerGitChainTools(server2) {
|
|
|
2818
2564
|
title: "Chain",
|
|
2819
2565
|
description: "Manage processes \u2014 create, get, list, or edit process links. Processes are versioned knowledge artifacts that follow a process template (e.g. Strategy Coherence: Problem \u2192 Insight \u2192 Choice \u2192 Action \u2192 Outcome).",
|
|
2820
2566
|
inputSchema: {
|
|
2821
|
-
action:
|
|
2822
|
-
chainEntryId:
|
|
2823
|
-
title:
|
|
2824
|
-
chainTypeId:
|
|
2825
|
-
description:
|
|
2826
|
-
linkId:
|
|
2827
|
-
content:
|
|
2828
|
-
status:
|
|
2829
|
-
author:
|
|
2567
|
+
action: z7.enum(["create", "get", "list", "edit"]).describe("Action: create a process, get process details, list all processes, or edit a process link"),
|
|
2568
|
+
chainEntryId: z7.string().optional().describe("Chain entry ID (required for get/edit)"),
|
|
2569
|
+
title: z7.string().optional().describe("Process title (required for create)"),
|
|
2570
|
+
chainTypeId: z7.string().optional().default("strategy-coherence").describe("Process template slug for create: 'strategy-coherence', 'idm-proposal', or any custom template slug"),
|
|
2571
|
+
description: z7.string().optional().describe("Description (for create)"),
|
|
2572
|
+
linkId: z7.string().optional().describe("Link to edit (for edit action): problem, insight, choice, action, outcome"),
|
|
2573
|
+
content: z7.string().optional().describe("New content for the link (for edit action)"),
|
|
2574
|
+
status: z7.string().optional().describe("Filter by status for list: 'draft' or 'active'"),
|
|
2575
|
+
author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
2830
2576
|
}
|
|
2831
2577
|
},
|
|
2832
2578
|
async ({ action, chainEntryId, title, chainTypeId, description, linkId, content, status, author }) => {
|
|
@@ -2923,13 +2669,13 @@ Use \`chain action=get\` to see the full chain with updated scores.`
|
|
|
2923
2669
|
title: "Chain Version",
|
|
2924
2670
|
description: "Manage process versions \u2014 commit snapshots, list commits, view history, diff versions, or revert. Commits record all link content, compute coherence scores, and track changes.",
|
|
2925
2671
|
inputSchema: {
|
|
2926
|
-
action:
|
|
2927
|
-
chainEntryId:
|
|
2928
|
-
commitMessage:
|
|
2929
|
-
versionA:
|
|
2930
|
-
versionB:
|
|
2931
|
-
toVersion:
|
|
2932
|
-
author:
|
|
2672
|
+
action: z7.enum(["commit", "list", "diff", "revert", "history"]).describe("Action: commit a snapshot, list commits, diff two versions, revert to a version, or view history"),
|
|
2673
|
+
chainEntryId: z7.string().describe("The chain's entry ID"),
|
|
2674
|
+
commitMessage: z7.string().optional().describe("Commit message (required for commit). Convention: type(link): description"),
|
|
2675
|
+
versionA: z7.number().optional().describe("Earlier version for diff"),
|
|
2676
|
+
versionB: z7.number().optional().describe("Later version for diff"),
|
|
2677
|
+
toVersion: z7.number().optional().describe("Version number to revert to"),
|
|
2678
|
+
author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
2933
2679
|
},
|
|
2934
2680
|
annotations: { readOnlyHint: false }
|
|
2935
2681
|
},
|
|
@@ -3037,11 +2783,11 @@ ${formatted}` }] };
|
|
|
3037
2783
|
title: "Chain Branch",
|
|
3038
2784
|
description: "Manage process branches \u2014 create a branch for isolated editing, list branches, merge a branch back into main, or check for conflicts.",
|
|
3039
2785
|
inputSchema: {
|
|
3040
|
-
action:
|
|
3041
|
-
chainEntryId:
|
|
3042
|
-
branchName:
|
|
3043
|
-
strategy:
|
|
3044
|
-
author:
|
|
2786
|
+
action: z7.enum(["create", "list", "merge", "conflicts"]).describe("Action: create a branch, list branches, merge a branch, or check for conflicts"),
|
|
2787
|
+
chainEntryId: z7.string().describe("The chain's entry ID"),
|
|
2788
|
+
branchName: z7.string().optional().describe("Branch name (required for merge/conflicts, optional for create)"),
|
|
2789
|
+
strategy: z7.enum(["merge_commit", "squash"]).optional().describe("Merge strategy: 'merge_commit' (default) or 'squash'"),
|
|
2790
|
+
author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
3045
2791
|
}
|
|
3046
2792
|
},
|
|
3047
2793
|
async ({ action, chainEntryId, branchName, strategy, author }) => {
|
|
@@ -3125,14 +2871,14 @@ Resolve conflicts before merging.`
|
|
|
3125
2871
|
title: "Chain Review",
|
|
3126
2872
|
description: "Review process quality \u2014 run the coherence gate or manage comments on process versions. The gate checks: coherence score >= 70%, all links filled, commit message convention.",
|
|
3127
2873
|
inputSchema: {
|
|
3128
|
-
action:
|
|
3129
|
-
chainEntryId:
|
|
3130
|
-
commitMessage:
|
|
3131
|
-
versionNumber:
|
|
3132
|
-
linkId:
|
|
3133
|
-
body:
|
|
3134
|
-
commentId:
|
|
3135
|
-
author:
|
|
2874
|
+
action: z7.enum(["gate", "comment", "resolve-comment", "list-comments"]).describe("Action: run coherence gate, add a comment, resolve a comment, or list comments"),
|
|
2875
|
+
chainEntryId: z7.string().describe("The chain's entry ID"),
|
|
2876
|
+
commitMessage: z7.string().optional().describe("Commit message to lint (for gate action)"),
|
|
2877
|
+
versionNumber: z7.number().optional().describe("Version to comment on or list comments for"),
|
|
2878
|
+
linkId: z7.string().optional().describe("Link this comment targets (optional for comment)"),
|
|
2879
|
+
body: z7.string().optional().describe("Comment text (required for comment action)"),
|
|
2880
|
+
commentId: z7.string().optional().describe("Comment ID (required for resolve-comment)"),
|
|
2881
|
+
author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
3136
2882
|
},
|
|
3137
2883
|
annotations: { readOnlyHint: false }
|
|
3138
2884
|
},
|
|
@@ -3211,42 +2957,782 @@ ${formatted}`
|
|
|
3211
2957
|
);
|
|
3212
2958
|
}
|
|
3213
2959
|
|
|
3214
|
-
// src/
|
|
3215
|
-
import {
|
|
3216
|
-
function
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
|
|
2960
|
+
// src/tools/maps.ts
|
|
2961
|
+
import { z as z8 } from "zod";
|
|
2962
|
+
function slotSummary(slots) {
|
|
2963
|
+
return Object.entries(slots).map(([id, refs]) => {
|
|
2964
|
+
if (!refs || refs.length === 0) return ` - **${id}**: (empty)`;
|
|
2965
|
+
const items = refs.map((r) => {
|
|
2966
|
+
const name = r.ingredientName ?? r.label ?? r.entryId;
|
|
2967
|
+
const version = r.pinnedVersion ? ` v${r.pinnedVersion}` : "";
|
|
2968
|
+
const status = r.ingredientStatus ? ` [${r.ingredientStatus}]` : "";
|
|
2969
|
+
return `${name}${version}${status}`;
|
|
2970
|
+
}).join(", ");
|
|
2971
|
+
return ` - **${id}**: ${items}`;
|
|
2972
|
+
}).join("\n");
|
|
3227
2973
|
}
|
|
3228
|
-
function
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
)
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
2974
|
+
function registerMapTools(server2) {
|
|
2975
|
+
server2.registerTool(
|
|
2976
|
+
"map",
|
|
2977
|
+
{
|
|
2978
|
+
title: "Map",
|
|
2979
|
+
description: "Manage composed framework maps \u2014 create, get, or list. Maps assemble ingredient entries into framework slots (e.g. Lean Canvas). Use map-slot to add/remove ingredients, map-version to commit and view history.",
|
|
2980
|
+
inputSchema: {
|
|
2981
|
+
action: z8.enum(["create", "get", "list"]).describe("Action: create a map, get map details, or list all maps"),
|
|
2982
|
+
mapEntryId: z8.string().optional().describe("Map entry ID (for get)"),
|
|
2983
|
+
title: z8.string().optional().describe("Map title (for create)"),
|
|
2984
|
+
templateId: z8.string().optional().default("lean-canvas").describe("Template slug for create: 'lean-canvas' or any composed template"),
|
|
2985
|
+
description: z8.string().optional().describe("Description (for create)"),
|
|
2986
|
+
slotIds: z8.array(z8.string()).optional().describe("Slot IDs to initialize (for create; auto-populated from template if omitted)"),
|
|
2987
|
+
status: z8.string().optional().describe("Filter by status for list")
|
|
2988
|
+
}
|
|
2989
|
+
},
|
|
2990
|
+
async ({ action, mapEntryId, title, templateId, description, slotIds, status }) => {
|
|
2991
|
+
if (action === "create") {
|
|
2992
|
+
if (!title) {
|
|
2993
|
+
return {
|
|
2994
|
+
content: [
|
|
2995
|
+
{ type: "text", text: "A `title` is required to create a map." }
|
|
2996
|
+
]
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
const result = await mcpMutation(
|
|
3000
|
+
"maps.createMap",
|
|
3001
|
+
{ title, templateId, description, slotIds }
|
|
3002
|
+
);
|
|
3003
|
+
const wsCtx = await getWorkspaceContext();
|
|
3004
|
+
return {
|
|
3005
|
+
content: [
|
|
3006
|
+
{
|
|
3007
|
+
type: "text",
|
|
3008
|
+
text: `# Map Created
|
|
3009
|
+
|
|
3010
|
+
- **Entry ID:** \`${result.entryId}\`
|
|
3011
|
+
- **Title:** ${title}
|
|
3012
|
+
- **Template:** ${templateId}
|
|
3013
|
+
- **Status:** draft
|
|
3014
|
+
- **Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})
|
|
3015
|
+
|
|
3016
|
+
Use \`map-slot action=add mapEntryId="${result.entryId}" slotId="problem" ingredientEntryId="..."\` to start filling slots.`
|
|
3017
|
+
}
|
|
3018
|
+
]
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
if (action === "get") {
|
|
3022
|
+
if (!mapEntryId) {
|
|
3023
|
+
return {
|
|
3024
|
+
content: [{ type: "text", text: "A `mapEntryId` is required." }]
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
const map = await mcpQuery("maps.getMap", { mapEntryId });
|
|
3028
|
+
if (!map) {
|
|
3029
|
+
return {
|
|
3030
|
+
content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
return {
|
|
3034
|
+
content: [
|
|
3035
|
+
{
|
|
3036
|
+
type: "text",
|
|
3037
|
+
text: `# ${map.name}
|
|
3038
|
+
|
|
3039
|
+
- **Entry ID:** \`${map.entryId}\`
|
|
3040
|
+
- **Template:** ${map.templateName}
|
|
3041
|
+
- **Status:** ${map.status}
|
|
3042
|
+
- **Version:** ${map.currentVersion}
|
|
3043
|
+
- **Completion:** ${map.completionScore}% (${map.filledSlots}/${map.totalSlots} slots)
|
|
3044
|
+
|
|
3045
|
+
## Slots
|
|
3046
|
+
|
|
3047
|
+
` + slotSummary(map.slots)
|
|
3048
|
+
}
|
|
3049
|
+
]
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
if (action === "list") {
|
|
3053
|
+
const maps = await mcpQuery("maps.listMaps", {
|
|
3054
|
+
templateId: templateId !== "lean-canvas" ? templateId : void 0,
|
|
3055
|
+
status
|
|
3056
|
+
});
|
|
3057
|
+
if (!maps || maps.length === 0) {
|
|
3058
|
+
return {
|
|
3059
|
+
content: [
|
|
3060
|
+
{
|
|
3061
|
+
type: "text",
|
|
3062
|
+
text: 'No maps found. Create one with `map action=create title="My Canvas"`.'
|
|
3063
|
+
}
|
|
3064
|
+
]
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
const lines = maps.map(
|
|
3068
|
+
(m) => `- **${m.name}** (\`${m.entryId}\`) \u2014 ${m.templateId}, ${m.completionScore}% complete, v${m.currentVersion}`
|
|
3069
|
+
);
|
|
3070
|
+
return {
|
|
3071
|
+
content: [
|
|
3072
|
+
{
|
|
3073
|
+
type: "text",
|
|
3074
|
+
text: `# Maps (${maps.length})
|
|
3075
|
+
|
|
3076
|
+
${lines.join("\n")}`
|
|
3077
|
+
}
|
|
3078
|
+
]
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
return {
|
|
3082
|
+
content: [{ type: "text", text: `Unknown action: ${action}` }]
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
);
|
|
3086
|
+
server2.registerTool(
|
|
3087
|
+
"map-slot",
|
|
3088
|
+
{
|
|
3089
|
+
title: "Map Slot",
|
|
3090
|
+
description: "Add, remove, or replace ingredient references in a map's slots. Ingredients are entries from any collection that fill a framework position.",
|
|
3091
|
+
inputSchema: {
|
|
3092
|
+
action: z8.enum(["add", "remove", "replace", "list"]).describe("Action: add/remove/replace an ingredient in a slot, or list slot contents"),
|
|
3093
|
+
mapEntryId: z8.string().describe("Map entry ID"),
|
|
3094
|
+
slotId: z8.string().optional().describe("Slot ID (e.g. 'problem', 'customer-segments')"),
|
|
3095
|
+
ingredientEntryId: z8.string().optional().describe("Ingredient entry ID to add/remove"),
|
|
3096
|
+
newIngredientEntryId: z8.string().optional().describe("New ingredient entry ID (for replace)"),
|
|
3097
|
+
label: z8.string().optional().describe("Display label override"),
|
|
3098
|
+
author: z8.string().optional().describe("Who is performing the action")
|
|
3099
|
+
}
|
|
3100
|
+
},
|
|
3101
|
+
async ({
|
|
3102
|
+
action,
|
|
3103
|
+
mapEntryId,
|
|
3104
|
+
slotId,
|
|
3105
|
+
ingredientEntryId,
|
|
3106
|
+
newIngredientEntryId,
|
|
3107
|
+
label,
|
|
3108
|
+
author
|
|
3109
|
+
}) => {
|
|
3110
|
+
if (action === "list") {
|
|
3111
|
+
const map = await mcpQuery("maps.getMap", { mapEntryId });
|
|
3112
|
+
if (!map) {
|
|
3113
|
+
return {
|
|
3114
|
+
content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
if (slotId && map.slots[slotId]) {
|
|
3118
|
+
return {
|
|
3119
|
+
content: [
|
|
3120
|
+
{
|
|
3121
|
+
type: "text",
|
|
3122
|
+
text: `# Slot: ${slotId}
|
|
3123
|
+
|
|
3124
|
+
` + (map.slots[slotId].length === 0 ? "(empty)" : map.slots[slotId].map(
|
|
3125
|
+
(r) => `- **${r.ingredientName ?? r.entryId}** (\`${r.entryId}\`)${r.pinnedVersion ? ` v${r.pinnedVersion}` : ""}`
|
|
3126
|
+
).join("\n"))
|
|
3127
|
+
}
|
|
3128
|
+
]
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
return {
|
|
3132
|
+
content: [
|
|
3133
|
+
{ type: "text", text: `## All Slots
|
|
3134
|
+
|
|
3135
|
+
${slotSummary(map.slots)}` }
|
|
3136
|
+
]
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
if (action === "add") {
|
|
3140
|
+
if (!slotId || !ingredientEntryId) {
|
|
3141
|
+
return {
|
|
3142
|
+
content: [
|
|
3143
|
+
{
|
|
3144
|
+
type: "text",
|
|
3145
|
+
text: "Both `slotId` and `ingredientEntryId` are required for add."
|
|
3146
|
+
}
|
|
3147
|
+
]
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
await mcpMutation("maps.addToSlot", {
|
|
3151
|
+
mapEntryId,
|
|
3152
|
+
slotId,
|
|
3153
|
+
ingredientEntryId,
|
|
3154
|
+
label,
|
|
3155
|
+
author
|
|
3156
|
+
});
|
|
3157
|
+
return {
|
|
3158
|
+
content: [
|
|
3159
|
+
{
|
|
3160
|
+
type: "text",
|
|
3161
|
+
text: `Added \`${ingredientEntryId}\` to slot "${slotId}" on map \`${mapEntryId}\`.`
|
|
3162
|
+
}
|
|
3163
|
+
]
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
if (action === "remove") {
|
|
3167
|
+
if (!slotId || !ingredientEntryId) {
|
|
3168
|
+
return {
|
|
3169
|
+
content: [
|
|
3170
|
+
{
|
|
3171
|
+
type: "text",
|
|
3172
|
+
text: "Both `slotId` and `ingredientEntryId` are required for remove."
|
|
3173
|
+
}
|
|
3174
|
+
]
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
await mcpMutation("maps.removeFromSlot", {
|
|
3178
|
+
mapEntryId,
|
|
3179
|
+
slotId,
|
|
3180
|
+
ingredientEntryId,
|
|
3181
|
+
author
|
|
3182
|
+
});
|
|
3183
|
+
return {
|
|
3184
|
+
content: [
|
|
3185
|
+
{
|
|
3186
|
+
type: "text",
|
|
3187
|
+
text: `Removed \`${ingredientEntryId}\` from slot "${slotId}" on map \`${mapEntryId}\`.`
|
|
3188
|
+
}
|
|
3189
|
+
]
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
if (action === "replace") {
|
|
3193
|
+
if (!slotId || !ingredientEntryId || !newIngredientEntryId) {
|
|
3194
|
+
return {
|
|
3195
|
+
content: [
|
|
3196
|
+
{
|
|
3197
|
+
type: "text",
|
|
3198
|
+
text: "`slotId`, `ingredientEntryId`, and `newIngredientEntryId` are required for replace."
|
|
3199
|
+
}
|
|
3200
|
+
]
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
await mcpMutation("maps.replaceInSlot", {
|
|
3204
|
+
mapEntryId,
|
|
3205
|
+
slotId,
|
|
3206
|
+
oldIngredientEntryId: ingredientEntryId,
|
|
3207
|
+
newIngredientEntryId,
|
|
3208
|
+
label,
|
|
3209
|
+
author
|
|
3210
|
+
});
|
|
3211
|
+
return {
|
|
3212
|
+
content: [
|
|
3213
|
+
{
|
|
3214
|
+
type: "text",
|
|
3215
|
+
text: `Replaced \`${ingredientEntryId}\` with \`${newIngredientEntryId}\` in slot "${slotId}".`
|
|
3216
|
+
}
|
|
3217
|
+
]
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
return {
|
|
3221
|
+
content: [{ type: "text", text: `Unknown action: ${action}` }]
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
);
|
|
3225
|
+
server2.registerTool(
|
|
3226
|
+
"map-version",
|
|
3227
|
+
{
|
|
3228
|
+
title: "Map Version",
|
|
3229
|
+
description: "Commit a map (pins ingredient versions), list commit history, or view a specific commit.",
|
|
3230
|
+
inputSchema: {
|
|
3231
|
+
action: z8.enum(["commit", "list", "history"]).describe("Action: commit the map, list commits, or view commit history"),
|
|
3232
|
+
mapEntryId: z8.string().describe("Map entry ID"),
|
|
3233
|
+
commitMessage: z8.string().optional().describe("Commit message (for commit action)"),
|
|
3234
|
+
author: z8.string().optional().describe("Who is committing")
|
|
3235
|
+
}
|
|
3236
|
+
},
|
|
3237
|
+
async ({ action, mapEntryId, commitMessage, author }) => {
|
|
3238
|
+
if (action === "commit") {
|
|
3239
|
+
const result = await mcpMutation("maps.commitMap", {
|
|
3240
|
+
mapEntryId,
|
|
3241
|
+
commitMessage: commitMessage ?? "Map committed",
|
|
3242
|
+
author
|
|
3243
|
+
});
|
|
3244
|
+
return {
|
|
3245
|
+
content: [
|
|
3246
|
+
{
|
|
3247
|
+
type: "text",
|
|
3248
|
+
text: `# Map Committed
|
|
3249
|
+
|
|
3250
|
+
- **Version:** ${result.version}
|
|
3251
|
+
- **Completion:** ${result.completionScore}%
|
|
3252
|
+
- **Slots modified:** ${result.slotsModified.length > 0 ? result.slotsModified.join(", ") : "(none)"}
|
|
3253
|
+
|
|
3254
|
+
All ingredient references have been pinned at their current versions.`
|
|
3255
|
+
}
|
|
3256
|
+
]
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
if (action === "list" || action === "history") {
|
|
3260
|
+
const commits = await mcpQuery("maps.listMapCommits", {
|
|
3261
|
+
mapEntryId
|
|
3262
|
+
});
|
|
3263
|
+
if (!commits || commits.length === 0) {
|
|
3264
|
+
return {
|
|
3265
|
+
content: [
|
|
3266
|
+
{
|
|
3267
|
+
type: "text",
|
|
3268
|
+
text: `No commits yet for map \`${mapEntryId}\`. Use \`map-version action=commit\` to create the first snapshot.`
|
|
3269
|
+
}
|
|
3270
|
+
]
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
const lines = commits.map(
|
|
3274
|
+
(c) => `- **v${c.version}** (${new Date(c.createdAt).toLocaleDateString()}) \u2014 ${c.commitMessage ?? "(no message)"} \u2014 by ${c.author}` + (c.slotsModified.length > 0 ? `
|
|
3275
|
+
Slots changed: ${c.slotsModified.join(", ")}` : "")
|
|
3276
|
+
);
|
|
3277
|
+
return {
|
|
3278
|
+
content: [
|
|
3279
|
+
{
|
|
3280
|
+
type: "text",
|
|
3281
|
+
text: `# Map History: \`${mapEntryId}\` (${commits.length} commits)
|
|
3282
|
+
|
|
3283
|
+
${lines.join("\n")}`
|
|
3284
|
+
}
|
|
3285
|
+
]
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
return {
|
|
3289
|
+
content: [{ type: "text", text: `Unknown action: ${action}` }]
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
);
|
|
3293
|
+
server2.registerTool(
|
|
3294
|
+
"map-suggest",
|
|
3295
|
+
{
|
|
3296
|
+
title: "Map Suggest",
|
|
3297
|
+
description: "Given a map with empty slots, search the Chain for ingredients that could fill them. Uses keyword search and collection matching based on the template's suggested collections.",
|
|
3298
|
+
inputSchema: {
|
|
3299
|
+
mapEntryId: z8.string().describe("Map entry ID to suggest ingredients for"),
|
|
3300
|
+
slotId: z8.string().optional().describe("Specific slot to find ingredients for (or all empty slots)"),
|
|
3301
|
+
query: z8.string().optional().describe("Optional search query to narrow ingredient suggestions")
|
|
3302
|
+
}
|
|
3303
|
+
},
|
|
3304
|
+
async ({ mapEntryId, slotId, query }) => {
|
|
3305
|
+
const map = await mcpQuery("maps.getMap", { mapEntryId });
|
|
3306
|
+
if (!map) {
|
|
3307
|
+
return {
|
|
3308
|
+
content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
const emptySlots = slotId ? [slotId].filter((id) => (map.slots[id]?.length ?? 0) === 0) : Object.entries(map.slots).filter(([, refs]) => refs.length === 0).map(([id]) => id);
|
|
3312
|
+
if (emptySlots.length === 0) {
|
|
3313
|
+
return {
|
|
3314
|
+
content: [
|
|
3315
|
+
{
|
|
3316
|
+
type: "text",
|
|
3317
|
+
text: slotId ? `Slot "${slotId}" already has ingredients.` : "All slots have ingredients. Nothing to suggest."
|
|
3318
|
+
}
|
|
3319
|
+
]
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
const slotDefs = map.slotDefs ?? [];
|
|
3323
|
+
const suggestions = [];
|
|
3324
|
+
for (const sid of emptySlots) {
|
|
3325
|
+
const def = slotDefs.find((s) => s.id === sid);
|
|
3326
|
+
const searchQuery = query ?? def?.label ?? sid;
|
|
3327
|
+
const results = await mcpQuery("knowledge.search", {
|
|
3328
|
+
query: searchQuery,
|
|
3329
|
+
limit: 5
|
|
3330
|
+
});
|
|
3331
|
+
if (results && results.length > 0) {
|
|
3332
|
+
const items = results.map(
|
|
3333
|
+
(r) => ` - **${r.name}** (\`${r.entryId}\`, ${r.collectionName ?? "unknown"}) \u2014 ${r.status}`
|
|
3334
|
+
);
|
|
3335
|
+
suggestions.push(
|
|
3336
|
+
`### ${def?.label ?? sid}
|
|
3337
|
+
${def?.description ?? ""}
|
|
3338
|
+
|
|
3339
|
+
${items.join("\n")}`
|
|
3340
|
+
);
|
|
3341
|
+
} else {
|
|
3342
|
+
suggestions.push(
|
|
3343
|
+
`### ${def?.label ?? sid}
|
|
3344
|
+
_No matching entries found. Create ingredients first._`
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
return {
|
|
3349
|
+
content: [
|
|
3350
|
+
{
|
|
3351
|
+
type: "text",
|
|
3352
|
+
text: `# Ingredient Suggestions for "${map.name}"
|
|
3353
|
+
|
|
3354
|
+
` + suggestions.join("\n\n") + `
|
|
3355
|
+
|
|
3356
|
+
Use \`map-slot action=add mapEntryId="${mapEntryId}" slotId="..." ingredientEntryId="..."\` to add ingredients.`
|
|
3357
|
+
}
|
|
3358
|
+
]
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
// src/tools/start.ts
|
|
3365
|
+
import { z as z9 } from "zod";
|
|
3366
|
+
|
|
3367
|
+
// src/presets/collections.ts
|
|
3368
|
+
var COLLECTION_PRESETS = [
|
|
3369
|
+
{
|
|
3370
|
+
id: "software-product",
|
|
3371
|
+
name: "Software Product",
|
|
3372
|
+
description: "For teams building software products \u2014 glossary, features, architecture, tech debt, and API endpoints",
|
|
3373
|
+
collections: [
|
|
3374
|
+
{ slug: "glossary", name: "Glossary", description: "Canonical terminology for the product domain", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "select", options: ["Platform & Architecture", "Knowledge Management", "AI & Developer Tools", "Governance & Process"] }, { key: "confusedWith", label: "Confused With", type: "array" }] },
|
|
3375
|
+
{ slug: "features", name: "Features", description: "Product features and capabilities", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "scope", label: "Scope", type: "string" }, { key: "area", label: "Area", type: "string" }, { key: "status", label: "Status", type: "select", options: ["proposed", "in-progress", "shipped", "deprecated"] }] },
|
|
3376
|
+
{ slug: "architecture", name: "Architecture", description: "System architecture layers and components", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "layer", label: "Layer", type: "string" }, { key: "dependencies", label: "Dependencies", type: "array" }] },
|
|
3377
|
+
{ slug: "tech-debt", name: "Tech Debt", description: "Technical debt items to track and address", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "severity", label: "Severity", type: "select", options: ["low", "medium", "high", "critical"] }, { key: "area", label: "Area", type: "string" }, { key: "effort", label: "Effort", type: "select", options: ["small", "medium", "large"] }] },
|
|
3378
|
+
{ slug: "api-endpoints", name: "API Endpoints", description: "REST/GraphQL endpoints and their contracts", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "method", label: "Method", type: "select", options: ["GET", "POST", "PUT", "PATCH", "DELETE"] }, { key: "path", label: "Path", type: "string" }, { key: "auth", label: "Auth Required", type: "select", options: ["none", "api-key", "bearer", "session"] }] },
|
|
3379
|
+
{ slug: "decisions", name: "Decisions", description: "Significant decisions with rationale and context", fields: [{ key: "rationale", label: "Rationale", type: "string", required: true, searchable: true }, { key: "date", label: "Date", type: "string" }, { key: "decidedBy", label: "Decided By", type: "string" }, { key: "alternatives", label: "Alternatives", type: "string" }] },
|
|
3380
|
+
{ slug: "tensions", name: "Tensions", description: "Friction points, pain points, and unmet needs", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "priority", label: "Priority", type: "select", options: ["low", "medium", "high", "critical"] }, { key: "severity", label: "Severity", type: "select", options: ["low", "medium", "high", "critical"] }] }
|
|
3381
|
+
]
|
|
3382
|
+
},
|
|
3383
|
+
{
|
|
3384
|
+
id: "content-business",
|
|
3385
|
+
name: "Content Business",
|
|
3386
|
+
description: "For content creators, publishers, and media companies \u2014 topics, audience segments, content calendar, and brand voice",
|
|
3387
|
+
collections: [
|
|
3388
|
+
{ slug: "glossary", name: "Glossary", description: "Industry terminology and brand language", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "string" }] },
|
|
3389
|
+
{ slug: "topics", name: "Topics", description: "Content topics and themes", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "pillar", label: "Content Pillar", type: "string" }, { key: "stage", label: "Stage", type: "select", options: ["idea", "researching", "drafting", "published", "evergreen"] }] },
|
|
3390
|
+
{ slug: "audience-segments", name: "Audience Segments", description: "Target reader/viewer personas", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "painPoints", label: "Pain Points", type: "string" }, { key: "channels", label: "Channels", type: "array" }] },
|
|
3391
|
+
{ slug: "brand-voice", name: "Brand Voice", description: "Tone, style, and voice guidelines", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "doThis", label: "Do This", type: "string" }, { key: "notThis", label: "Not This", type: "string" }] },
|
|
3392
|
+
{ slug: "decisions", name: "Decisions", description: "Editorial and strategic decisions", fields: [{ key: "rationale", label: "Rationale", type: "string", required: true, searchable: true }, { key: "date", label: "Date", type: "string" }, { key: "decidedBy", label: "Decided By", type: "string" }] },
|
|
3393
|
+
{ slug: "tensions", name: "Tensions", description: "Content gaps, audience friction, and unmet needs", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "priority", label: "Priority", type: "select", options: ["low", "medium", "high", "critical"] }] }
|
|
3394
|
+
]
|
|
3395
|
+
},
|
|
3396
|
+
{
|
|
3397
|
+
id: "agency",
|
|
3398
|
+
name: "Agency",
|
|
3399
|
+
description: "For agencies managing multiple clients \u2014 client profiles, project scopes, deliverables, and processes",
|
|
3400
|
+
collections: [
|
|
3401
|
+
{ slug: "glossary", name: "Glossary", description: "Agency and client terminology", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "string" }] },
|
|
3402
|
+
{ slug: "clients", name: "Clients", description: "Client profiles and relationship context", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "industry", label: "Industry", type: "string" }, { key: "contact", label: "Primary Contact", type: "string" }] },
|
|
3403
|
+
{ slug: "deliverables", name: "Deliverables", description: "Standard deliverable types and templates", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "type", label: "Type", type: "string" }, { key: "estimatedHours", label: "Estimated Hours", type: "string" }] },
|
|
3404
|
+
{ slug: "processes", name: "Processes", description: "Standard operating procedures and workflows", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "steps", label: "Steps", type: "string" }, { key: "owner", label: "Owner", type: "string" }] },
|
|
3405
|
+
{ slug: "decisions", name: "Decisions", description: "Strategic and operational decisions", fields: [{ key: "rationale", label: "Rationale", type: "string", required: true, searchable: true }, { key: "date", label: "Date", type: "string" }] },
|
|
3406
|
+
{ slug: "tensions", name: "Tensions", description: "Process bottlenecks and client friction", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "priority", label: "Priority", type: "select", options: ["low", "medium", "high", "critical"] }] }
|
|
3407
|
+
]
|
|
3408
|
+
},
|
|
3409
|
+
{
|
|
3410
|
+
id: "saas-api",
|
|
3411
|
+
name: "SaaS API",
|
|
3412
|
+
description: "For API-first SaaS products \u2014 endpoints, schemas, rate limits, changelog, and integration guides",
|
|
3413
|
+
collections: [
|
|
3414
|
+
{ slug: "glossary", name: "Glossary", description: "API and domain terminology", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "string" }] },
|
|
3415
|
+
{ slug: "api-endpoints", name: "API Endpoints", description: "API routes and contracts", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "method", label: "Method", type: "select", options: ["GET", "POST", "PUT", "PATCH", "DELETE"] }, { key: "path", label: "Path", type: "string" }, { key: "auth", label: "Auth", type: "select", options: ["none", "api-key", "bearer", "oauth"] }] },
|
|
3416
|
+
{ slug: "schemas", name: "Schemas", description: "Data models and API schemas", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "format", label: "Format", type: "select", options: ["json", "protobuf", "graphql", "openapi"] }, { key: "version", label: "Version", type: "string" }] },
|
|
3417
|
+
{ slug: "rate-limits", name: "Rate Limits", description: "Rate limiting policies and tiers", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "tier", label: "Tier", type: "string" }, { key: "limit", label: "Limit", type: "string" }] },
|
|
3418
|
+
{ slug: "changelog", name: "Changelog", description: "API version history and breaking changes", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "version", label: "Version", type: "string" }, { key: "date", label: "Date", type: "string" }, { key: "breaking", label: "Breaking", type: "select", options: ["yes", "no"] }] },
|
|
3419
|
+
{ slug: "decisions", name: "Decisions", description: "API design decisions and rationale", fields: [{ key: "rationale", label: "Rationale", type: "string", required: true, searchable: true }, { key: "date", label: "Date", type: "string" }] }
|
|
3420
|
+
]
|
|
3421
|
+
},
|
|
3422
|
+
{
|
|
3423
|
+
id: "general",
|
|
3424
|
+
name: "General",
|
|
3425
|
+
description: "A minimal starter set \u2014 glossary, decisions, and tensions. Add more collections as you need them.",
|
|
3426
|
+
collections: [
|
|
3427
|
+
{ slug: "glossary", name: "Glossary", description: "Canonical terminology", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "string" }] },
|
|
3428
|
+
{ slug: "decisions", name: "Decisions", description: "Decisions with rationale", fields: [{ key: "rationale", label: "Rationale", type: "string", required: true, searchable: true }, { key: "date", label: "Date", type: "string" }, { key: "decidedBy", label: "Decided By", type: "string" }] },
|
|
3429
|
+
{ slug: "tensions", name: "Tensions", description: "Friction points and unmet needs", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "priority", label: "Priority", type: "select", options: ["low", "medium", "high", "critical"] }] }
|
|
3430
|
+
]
|
|
3431
|
+
}
|
|
3432
|
+
];
|
|
3433
|
+
function getPreset(id) {
|
|
3434
|
+
return COLLECTION_PRESETS.find((p) => p.id === id);
|
|
3435
|
+
}
|
|
3436
|
+
function listPresets() {
|
|
3437
|
+
return COLLECTION_PRESETS.map((p) => ({
|
|
3438
|
+
id: p.id,
|
|
3439
|
+
name: p.name,
|
|
3440
|
+
description: p.description,
|
|
3441
|
+
collectionCount: p.collections.length
|
|
3442
|
+
}));
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
// src/tools/start.ts
|
|
3446
|
+
function registerStartTools(server2) {
|
|
3447
|
+
server2.registerTool(
|
|
3448
|
+
"start",
|
|
3449
|
+
{
|
|
3450
|
+
title: "Start Product Brain",
|
|
3451
|
+
description: "The zero-friction entry point. Say 'start PB' to begin.\n\n- **Fresh workspace**: asks what you're building, seeds tailored collections, and gets you ready to capture knowledge immediately.\n- **Existing workspace**: returns readiness score, recent activity, open tensions, and suggested next actions (same as orient).\n\nUse this as your first call. Replaces the need to call orient, workspace-status, or health separately.",
|
|
3452
|
+
inputSchema: {
|
|
3453
|
+
preset: z9.string().optional().describe(
|
|
3454
|
+
"Collection preset ID to seed (e.g. 'software-product', 'content-business', 'agency', 'saas-api', 'general'). Only used for fresh workspaces. If omitted on a fresh workspace, returns the preset menu."
|
|
3455
|
+
)
|
|
3456
|
+
},
|
|
3457
|
+
annotations: { readOnlyHint: false }
|
|
3458
|
+
},
|
|
3459
|
+
async ({ preset }) => {
|
|
3460
|
+
const errors = [];
|
|
3461
|
+
const agentSessionId = getAgentSessionId();
|
|
3462
|
+
let wsCtx = null;
|
|
3463
|
+
try {
|
|
3464
|
+
wsCtx = await getWorkspaceContext();
|
|
3465
|
+
} catch (e) {
|
|
3466
|
+
errors.push(`Workspace: ${e.message}`);
|
|
3467
|
+
}
|
|
3468
|
+
if (!wsCtx) {
|
|
3469
|
+
return {
|
|
3470
|
+
content: [
|
|
3471
|
+
{
|
|
3472
|
+
type: "text",
|
|
3473
|
+
text: "# Could not connect to Product Brain\n\n" + (errors.length > 0 ? errors.map((e) => `- ${e}`).join("\n") : "Check your API key and CONVEX_SITE_URL.")
|
|
3474
|
+
}
|
|
3475
|
+
]
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
const isFresh = await detectFreshWorkspace();
|
|
3479
|
+
if (isFresh && !preset) {
|
|
3480
|
+
return { content: [{ type: "text", text: buildPresetMenu(wsCtx) }] };
|
|
3481
|
+
}
|
|
3482
|
+
if (isFresh && preset) {
|
|
3483
|
+
return { content: [{ type: "text", text: await seedPreset(wsCtx, preset, agentSessionId) }] };
|
|
3484
|
+
}
|
|
3485
|
+
return { content: [{ type: "text", text: await buildOrientResponse(wsCtx, agentSessionId, errors) }] };
|
|
3486
|
+
}
|
|
3487
|
+
);
|
|
3488
|
+
}
|
|
3489
|
+
async function detectFreshWorkspace() {
|
|
3490
|
+
try {
|
|
3491
|
+
const entries = await mcpQuery("chain.listEntries", {});
|
|
3492
|
+
const nonSystem = (entries ?? []).filter((e) => !e.isSystem);
|
|
3493
|
+
return nonSystem.length === 0;
|
|
3494
|
+
} catch {
|
|
3495
|
+
return false;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
function buildPresetMenu(wsCtx) {
|
|
3499
|
+
const presets = listPresets();
|
|
3500
|
+
const lines = [
|
|
3501
|
+
`# Welcome to ${wsCtx.workspaceName}`,
|
|
3502
|
+
"",
|
|
3503
|
+
"Your workspace is fresh \u2014 let's set it up. **What are you building?**",
|
|
3504
|
+
"",
|
|
3505
|
+
"Choose a preset to tailor your collections:",
|
|
3506
|
+
""
|
|
3507
|
+
];
|
|
3508
|
+
for (const p of presets) {
|
|
3509
|
+
lines.push(`- **${p.name}** (\`${p.id}\`) \u2014 ${p.description} (${p.collectionCount} collections)`);
|
|
3510
|
+
}
|
|
3511
|
+
lines.push(
|
|
3512
|
+
"",
|
|
3513
|
+
'Call `start` again with your choice, e.g.: `start preset="software-product"`',
|
|
3514
|
+
"",
|
|
3515
|
+
"_You can always add, remove, or customize collections later._"
|
|
3516
|
+
);
|
|
3517
|
+
return lines.join("\n");
|
|
3518
|
+
}
|
|
3519
|
+
async function seedPreset(wsCtx, presetId, agentSessionId) {
|
|
3520
|
+
const preset = getPreset(presetId);
|
|
3521
|
+
if (!preset) {
|
|
3522
|
+
return `Preset "${presetId}" not found.
|
|
3523
|
+
|
|
3524
|
+
Available presets: ${listPresets().map((p) => `\`${p.id}\``).join(", ")}`;
|
|
3525
|
+
}
|
|
3526
|
+
const seeded = [];
|
|
3527
|
+
const skipped = [];
|
|
3528
|
+
for (const col of preset.collections) {
|
|
3529
|
+
try {
|
|
3530
|
+
await mcpCall("chain.createCollection", {
|
|
3531
|
+
slug: col.slug,
|
|
3532
|
+
name: col.name,
|
|
3533
|
+
description: col.description,
|
|
3534
|
+
icon: col.icon,
|
|
3535
|
+
fields: col.fields,
|
|
3536
|
+
isDefault: false,
|
|
3537
|
+
createdBy: "preset:" + presetId
|
|
3538
|
+
});
|
|
3539
|
+
seeded.push(col.name);
|
|
3540
|
+
} catch {
|
|
3541
|
+
skipped.push(`${col.name} (already exists)`);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
if (agentSessionId) {
|
|
3545
|
+
try {
|
|
3546
|
+
await mcpCall("agent.markOriented", { sessionId: agentSessionId });
|
|
3547
|
+
setSessionOriented(true);
|
|
3548
|
+
} catch {
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
const lines = [
|
|
3552
|
+
`# ${wsCtx.workspaceName} is ready`,
|
|
3553
|
+
"",
|
|
3554
|
+
`Seeded **${preset.name}** preset with ${seeded.length} collection${seeded.length === 1 ? "" : "s"}:`,
|
|
3555
|
+
""
|
|
3556
|
+
];
|
|
3557
|
+
for (const name of seeded) {
|
|
3558
|
+
lines.push(`- ${name}`);
|
|
3559
|
+
}
|
|
3560
|
+
if (skipped.length > 0) {
|
|
3561
|
+
lines.push("", "Skipped (already exist):");
|
|
3562
|
+
for (const name of skipped) {
|
|
3563
|
+
lines.push(`- ${name}`);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
lines.push(
|
|
3567
|
+
"",
|
|
3568
|
+
"## What's next?",
|
|
3569
|
+
"",
|
|
3570
|
+
"Start capturing knowledge. Here are example commands for your collections:",
|
|
3571
|
+
""
|
|
3572
|
+
);
|
|
3573
|
+
for (const col of preset.collections.slice(0, 3)) {
|
|
3574
|
+
lines.push(
|
|
3575
|
+
`- \`capture collection="${col.slug}" name="..." description="..."\` \u2014 add to ${col.name}`
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3578
|
+
lines.push(
|
|
3579
|
+
"",
|
|
3580
|
+
"After capturing a few entries, use `suggest-links` to connect them into a knowledge graph.",
|
|
3581
|
+
"",
|
|
3582
|
+
"---",
|
|
3583
|
+
`Orientation complete. Write tools are available.`
|
|
3584
|
+
);
|
|
3585
|
+
return lines.join("\n");
|
|
3586
|
+
}
|
|
3587
|
+
async function buildOrientResponse(wsCtx, agentSessionId, errors) {
|
|
3588
|
+
let priorSessions = [];
|
|
3589
|
+
try {
|
|
3590
|
+
priorSessions = await mcpQuery("agent.recentSessions", { limit: 3 });
|
|
3591
|
+
} catch {
|
|
3592
|
+
}
|
|
3593
|
+
let constraintEntries = [];
|
|
3594
|
+
try {
|
|
3595
|
+
const [archEntries, ruleEntries, decisionEntries] = await Promise.all([
|
|
3596
|
+
mcpQuery("chain.listEntries", { collectionSlug: "architecture" }),
|
|
3597
|
+
mcpQuery("chain.listEntries", { collectionSlug: "business-rules" }),
|
|
3598
|
+
mcpQuery("chain.listEntries", { collectionSlug: "decisions" })
|
|
3599
|
+
]);
|
|
3600
|
+
const committed = [
|
|
3601
|
+
...(archEntries ?? []).filter((e) => e.status === "active" || e.status === "healthy"),
|
|
3602
|
+
...(ruleEntries ?? []).filter((e) => e.status === "Active" || e.status === "active"),
|
|
3603
|
+
...(decisionEntries ?? []).filter((e) => e.status === "Decided" || e.status === "active")
|
|
3604
|
+
];
|
|
3605
|
+
constraintEntries = committed.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, 8);
|
|
3606
|
+
} catch {
|
|
3607
|
+
}
|
|
3608
|
+
let openTensions = [];
|
|
3609
|
+
try {
|
|
3610
|
+
const tensions = await mcpQuery("chain.listEntries", { collectionSlug: "tensions" });
|
|
3611
|
+
openTensions = (tensions ?? []).filter((e) => e.status === "draft");
|
|
3612
|
+
} catch {
|
|
3613
|
+
}
|
|
3614
|
+
let readiness = null;
|
|
3615
|
+
try {
|
|
3616
|
+
readiness = await mcpQuery("chain.workspaceReadiness");
|
|
3617
|
+
} catch (e) {
|
|
3618
|
+
errors.push(`Readiness: ${e.message}`);
|
|
3619
|
+
}
|
|
3620
|
+
const lines = [];
|
|
3621
|
+
lines.push(`# ${wsCtx.workspaceName}`);
|
|
3622
|
+
lines.push(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
|
|
3623
|
+
lines.push("");
|
|
3624
|
+
if (readiness) {
|
|
3625
|
+
const scoreBar = "\u2588".repeat(Math.round(readiness.score / 10)) + "\u2591".repeat(10 - Math.round(readiness.score / 10));
|
|
3626
|
+
lines.push(`## Readiness: ${readiness.score}%`);
|
|
3627
|
+
lines.push(`${scoreBar} ${readiness.passedChecks}/${readiness.totalChecks} requirements`);
|
|
3628
|
+
lines.push("");
|
|
3629
|
+
if (readiness.gaps?.length > 0) {
|
|
3630
|
+
lines.push("### Gaps");
|
|
3631
|
+
for (const gap of readiness.gaps) {
|
|
3632
|
+
lines.push(`- [ ] **${gap.label}** (${gap.current}/${gap.required}) \u2014 _${gap.guidance}_`);
|
|
3633
|
+
}
|
|
3634
|
+
lines.push("");
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
if (openTensions.length > 0) {
|
|
3638
|
+
lines.push(`## Open Tensions (${openTensions.length})`);
|
|
3639
|
+
for (const t of openTensions) {
|
|
3640
|
+
const id = t.entryId ?? "(no ID)";
|
|
3641
|
+
const prio = t.data?.priority ?? "";
|
|
3642
|
+
lines.push(`- \`${id}\` ${t.name}${prio ? ` _(${prio})_` : ""}`);
|
|
3643
|
+
}
|
|
3644
|
+
lines.push("");
|
|
3645
|
+
}
|
|
3646
|
+
if (priorSessions.length > 0) {
|
|
3647
|
+
lines.push(`## Prior Agent Sessions (last ${priorSessions.length})`);
|
|
3648
|
+
for (const s of priorSessions) {
|
|
3649
|
+
const date = new Date(s.startedAt).toISOString().split("T")[0];
|
|
3650
|
+
const created = Array.isArray(s.entriesCreated) ? s.entriesCreated.length : s.entriesCreated ?? 0;
|
|
3651
|
+
const modified = Array.isArray(s.entriesModified) ? s.entriesModified.length : s.entriesModified ?? 0;
|
|
3652
|
+
const stats = `${created} created, ${modified} modified, ${s.relationsCreated ?? 0} relations`;
|
|
3653
|
+
lines.push(`- **${date}** (${s.status}, by ${s.initiatedBy ?? "unknown"}) \u2014 ${stats}`);
|
|
3654
|
+
}
|
|
3655
|
+
lines.push("");
|
|
3656
|
+
}
|
|
3657
|
+
if (constraintEntries.length > 0) {
|
|
3658
|
+
lines.push(`## Active Constraints (${constraintEntries.length})`);
|
|
3659
|
+
lines.push("_Architecture, business rules, and decisions that govern this workspace._");
|
|
3660
|
+
lines.push("");
|
|
3661
|
+
for (const c of constraintEntries) {
|
|
3662
|
+
const id = c.entryId ?? "(no ID)";
|
|
3663
|
+
const col = c.collectionSlug ?? "";
|
|
3664
|
+
const desc = c.data?.description ?? c.data?.rationale ?? "";
|
|
3665
|
+
const truncated = desc.length > 100 ? desc.slice(0, 100) + "..." : desc;
|
|
3666
|
+
lines.push(`- \`${id}\` **${c.name}** [${col}] \u2014 ${truncated}`);
|
|
3667
|
+
}
|
|
3668
|
+
lines.push("");
|
|
3669
|
+
}
|
|
3670
|
+
if (errors.length > 0) {
|
|
3671
|
+
lines.push("## Errors");
|
|
3672
|
+
for (const err of errors) lines.push(`- ${err}`);
|
|
3673
|
+
lines.push("");
|
|
3674
|
+
}
|
|
3675
|
+
if (agentSessionId) {
|
|
3676
|
+
try {
|
|
3677
|
+
await mcpCall("agent.markOriented", { sessionId: agentSessionId });
|
|
3678
|
+
setSessionOriented(true);
|
|
3679
|
+
const readinessInfo = readiness ? `${readiness.score}% readiness.` : "";
|
|
3680
|
+
const tensionInfo = openTensions.length > 0 ? `${openTensions.length} open tensions.` : "No open tensions.";
|
|
3681
|
+
lines.push("---");
|
|
3682
|
+
lines.push(
|
|
3683
|
+
`Orientation complete. Session ${agentSessionId}. Write tools now available. ${readinessInfo} ${tensionInfo}`
|
|
3684
|
+
);
|
|
3685
|
+
} catch {
|
|
3686
|
+
lines.push("---");
|
|
3687
|
+
lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
|
|
3688
|
+
}
|
|
3689
|
+
} else {
|
|
3690
|
+
lines.push("---");
|
|
3691
|
+
lines.push("_No active agent session. Call `agent-start` to begin a tracked session._");
|
|
3692
|
+
}
|
|
3693
|
+
return lines.join("\n");
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
// src/resources/index.ts
|
|
3697
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3698
|
+
function formatEntryMarkdown(entry) {
|
|
3699
|
+
const id = entry.entryId ? `${entry.entryId}: ` : "";
|
|
3700
|
+
const lines = [`## ${id}${entry.name} [${entry.status}]`];
|
|
3701
|
+
if (entry.data && typeof entry.data === "object") {
|
|
3702
|
+
for (const [key, val] of Object.entries(entry.data)) {
|
|
3703
|
+
if (val && key !== "rawData") {
|
|
3704
|
+
lines.push(`**${key}**: ${typeof val === "string" ? val : JSON.stringify(val)}`);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
return lines.join("\n");
|
|
3709
|
+
}
|
|
3710
|
+
function buildOrientationMarkdown(collections, trackingEvents, standards, businessRules) {
|
|
3711
|
+
const sections = ["# Product Brain \u2014 Orientation"];
|
|
3712
|
+
sections.push(
|
|
3713
|
+
"## Core Product Architecture\nThe Chain is a versioned, connected, compounding knowledge base \u2014 the SSOT for a product team.\nEverything on the Chain is one of **three primitives**:\n\n- **Entry** (atom) \u2014 a discrete knowledge unit: target audience, business rule, glossary term, metric, tension. Lives in a collection.\n- **Process** (authored) \u2014 a narrative chain with named links (Strategy Coherence, IDM Proposal). Content is inline text. Collection: `chains`.\n- **Map** (composed) \u2014 a framework assembled by reference (Lean Canvas, Empathy Map). Slots point to ingredient entries. Collection: `maps`.\n\nEntries are atoms. Processes author narrative from atoms. Maps compose frameworks from atoms.\nAll three share the same versioning, branching, gating, and relation system.\n\nThe Chain compounds: each new relation makes entries discoverable from more starting points \u2192 gather-context returns richer results \u2192 AI processes have more context \u2192 quality scores improve \u2192 next commit is smarter than the last."
|
|
3714
|
+
);
|
|
3715
|
+
sections.push(
|
|
3716
|
+
"## Architecture\n```\nCursor (stdio) \u2192 MCP Server (mcp-server/src/index.ts)\n \u2192 POST /api/mcp with Bearer token\n \u2192 Convex HTTP Action (convex/http.ts)\n \u2192 internalQuery / internalMutation (convex/mcpKnowledge.ts)\n \u2192 Convex DB (workspace-scoped)\n```\nSecurity: API key auth on every request, workspace-scoped data, internal functions blocked from external clients.\nKey files: `packages/mcp-server/src/client.ts` (HTTP client + audit), `convex/schema.ts` (schema)."
|
|
3717
|
+
);
|
|
3718
|
+
if (collections) {
|
|
3719
|
+
const collList = collections.map((c) => {
|
|
3720
|
+
const prefix = c.icon ? `${c.icon} ` : "";
|
|
3721
|
+
return `- ${prefix}**${c.name}** (\`${c.slug}\`) \u2014 ${c.description || "no description"}`;
|
|
3722
|
+
}).join("\n");
|
|
3723
|
+
sections.push(
|
|
3724
|
+
`## Data Model (${collections.length} collections)
|
|
3725
|
+
Unified entries model: collections define field schemas, entries hold data in a flexible \`data\` field.
|
|
3726
|
+
The \`data\` field is polymorphic: plain fields for generic entries, \`ChainData\` (chainTypeId + links) for processes, \`MapData\` (templateId + slots) for maps.
|
|
3727
|
+
Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, versions via \`entryVersions\`, labels via \`labels\` + \`entryLabels\`.
|
|
3728
|
+
|
|
3729
|
+
` + collList + "\n\nUse `list-collections` for field schemas, `get-entry` for full records."
|
|
3730
|
+
);
|
|
3731
|
+
} else {
|
|
3732
|
+
sections.push(
|
|
3733
|
+
"## Data Model\nCould not load collections \u2014 use `list-collections` to browse manually."
|
|
3734
|
+
);
|
|
3735
|
+
}
|
|
3250
3736
|
const rulesCount = businessRules ? `${businessRules.length} entries` : "not loaded \u2014 collection may not exist yet";
|
|
3251
3737
|
sections.push(
|
|
3252
3738
|
`## Business Rules
|
|
@@ -3262,16 +3748,16 @@ Draft a new rule: use the \`draft-rule-from-context\` prompt.`
|
|
|
3262
3748
|
Event catalog: \`tracking-events\` collection (${eventsCount}).
|
|
3263
3749
|
${conventionNote}
|
|
3264
3750
|
Implementation: \`src/lib/analytics.ts\`. Workspace-scoped events MUST use \`withWorkspaceGroup()\`.
|
|
3265
|
-
Browse: \`list-entries collection=tracking-events
|
|
3751
|
+
Browse: \`list-entries collection=tracking-events\`.`
|
|
3266
3752
|
);
|
|
3267
3753
|
sections.push(
|
|
3268
|
-
"## Knowledge Graph\nEntries are connected via typed relations (`entryRelations` table). Relations are bidirectional and collection-agnostic \u2014 any entry can link to any other entry.\n\n**Recommended relation types** (extensible \u2014 any string accepted):\n- `governs` \u2014 a rule constrains behavior of a feature\n- `defines_term_for` \u2014 a glossary term is canonical vocabulary for a feature/area\n- `belongs_to` \u2014 a feature belongs to a product area or parent concept\n- `informs` \u2014 a decision or insight informs a feature\n- `surfaces_tension_in` \u2014 a tension exists within a feature area\n- `related_to`, `depends_on`, `replaces`, `conflicts_with`, `references`, `confused_with`\n\nEach relation type is defined as a glossary entry (prefix `GT-REL-*`) to prevent terminology drift.\n\n**Tools:**\n- `gather-context` \u2014 get the full context around any entry (multi-hop graph traversal)\n- `suggest-links` \u2014 discover potential connections for an entry\n- `relate-entries` \u2014 create a typed link between two entries\n- `find-related` \u2014 list direct relations for an entry\n\n**Convention:** When creating or updating entries in governed collections, always use `suggest-links` to discover and create relevant relations."
|
|
3754
|
+
"## Knowledge Graph\nEntries are connected via typed relations (`entryRelations` table). Relations are bidirectional and collection-agnostic \u2014 any entry can link to any other entry.\n\n**Recommended relation types** (extensible \u2014 any string accepted):\n- `governs` \u2014 a rule constrains behavior of a feature\n- `defines_term_for` \u2014 a glossary term is canonical vocabulary for a feature/area\n- `belongs_to` \u2014 a feature belongs to a product area or parent concept\n- `informs` \u2014 a decision or insight informs a feature\n- `fills_slot` \u2014 an ingredient entry fills a slot in a map\n- `surfaces_tension_in` \u2014 a tension exists within a feature area\n- `related_to`, `depends_on`, `replaces`, `conflicts_with`, `references`, `confused_with`\n\nEach relation type is defined as a glossary entry (prefix `GT-REL-*`) to prevent terminology drift.\n\n**Tools:**\n- `gather-context` \u2014 get the full context around any entry (multi-hop graph traversal)\n- `suggest-links` \u2014 discover potential connections for an entry\n- `relate-entries` \u2014 create a typed link between two entries\n- `find-related` \u2014 list direct relations for an entry\n\n**Convention:** When creating or updating entries in governed collections, always use `suggest-links` to discover and create relevant relations."
|
|
3269
3755
|
);
|
|
3270
3756
|
sections.push(
|
|
3271
|
-
"## Creating Knowledge\
|
|
3757
|
+
"## Creating Knowledge\n**Entries:** Use `capture` as the primary tool for creating new entries. It handles the full workflow in one call:\n1. Creates the entry with collection-aware defaults (auto-fills dates, infers domains, sets priority)\n2. Auto-links related entries from across the chain (up to 5 confident matches)\n3. Returns a quality scorecard (X/10) with actionable improvement suggestions\n\n**Smart profiles** exist for: `tensions`, `business-rules`, `glossary`, `decisions`, `features`, `audiences`, `strategy`, `standards`, `maps`, `chains`, `tracking-events`.\nAll other collections use the `ENT-{random}` fallback profile.\n\n**Processes:** Use `chain action=create` with a template (e.g., `strategy-coherence`, `idm-proposal`). Fill links with `chain action=edit`.\n\n**Maps:** Use `map action=create` with a template (e.g., `lean-canvas`). Fill slots with `map-slot action=add`. Commit with `map-version action=commit`.\nNote: committing a map version creates a snapshot but does NOT change the entry status. To activate: `update-entry entryId=MAP-xxx status=active autoPublish=true`.\nUse `map-suggest` to discover ingredients for empty slots.\n\n**Prompts** (invoke via MCP prompt protocol):\n- `name-check` \u2014 verify a name against glossary conventions\n- `draft-decision-record` \u2014 scaffold a decision entry\n- `review-against-rules` \u2014 check work against business rules for a domain\n- `draft-rule-from-context` \u2014 create a new business rule from context\n- `run-workflow` \u2014 run a structured ceremony (e.g., retrospective)\n\nUse `quality-check` to score existing entries retroactively.\nUse `update-entry` for post-creation adjustments (status changes, field updates, deprecation)."
|
|
3272
3758
|
);
|
|
3273
3759
|
sections.push(
|
|
3274
|
-
"## Where to Go Next\n- **Create entry** \u2192 `capture` tool (auto-links + quality score in one call)\n- **Full context** \u2192 `gather-context` tool (by entry ID or task description)\n- **Discover links** \u2192 `suggest-links` tool\n- **Quality audit** \u2192 `quality-check` tool\n- **Terminology** \u2192 `name-check` prompt or `productbrain://terminology` resource\n- **Schema details** \u2192 `productbrain://collections` resource or `list-collections` tool\n- **Labels** \u2192 `productbrain://labels` resource or `labels` tool\n- **Any collection** \u2192 `productbrain://{slug}/entries` resource\n- **Log a decision** \u2192 `draft-decision-record` prompt\n- **Architecture map** \u2192 `architecture action=show` tool (layered system visualization)\n- **Explore a layer** \u2192 `architecture action=explore` tool (drill into Auth, Core, Features, etc.)\n- **Health check** \u2192 `health` tool\n- **Debug MCP calls** \u2192 `mcp-audit` tool"
|
|
3760
|
+
"## Where to Go Next\n- **Create entry** \u2192 `capture` tool (auto-links + quality score in one call)\n- **Full context** \u2192 `gather-context` tool (by entry ID or task description)\n- **Discover links** \u2192 `suggest-links` tool\n- **Quality audit** \u2192 `quality-check` tool\n- **Terminology** \u2192 `name-check` prompt or `productbrain://terminology` resource\n- **Schema details** \u2192 `productbrain://collections` resource or `list-collections` tool\n- **Labels** \u2192 `productbrain://labels` resource or `labels` tool\n- **Any collection** \u2192 `productbrain://{slug}/entries` resource\n- **Log a decision** \u2192 `draft-decision-record` prompt\n- **Build a map** \u2192 `map action=create` + `map-slot action=add` + `map-version action=commit`\n- **Architecture map** \u2192 `architecture action=show` tool (layered system visualization)\n- **Explore a layer** \u2192 `architecture action=explore` tool (drill into Auth, Core, Features, etc.)\n- **Growth funnel** \u2192 `productbrain://growth-funnel` resource or `list-entries collection=strategy status=draft`\n- **Audiences** \u2192 `productbrain://audiences/entries` resource or `list-entries collection=audiences`\n- **Session identity** \u2192 `whoami` tool (workspace + auth context)\n- **Health check** \u2192 `health` tool\n- **Debug MCP calls** \u2192 `mcp-audit` tool"
|
|
3275
3761
|
);
|
|
3276
3762
|
return sections.join("\n\n---\n\n");
|
|
3277
3763
|
}
|
|
@@ -3281,10 +3767,10 @@ function registerResources(server2) {
|
|
|
3281
3767
|
"productbrain://orientation",
|
|
3282
3768
|
async (uri) => {
|
|
3283
3769
|
const [collectionsResult, eventsResult, standardsResult, rulesResult] = await Promise.allSettled([
|
|
3284
|
-
mcpQuery("
|
|
3285
|
-
mcpQuery("
|
|
3286
|
-
mcpQuery("
|
|
3287
|
-
mcpQuery("
|
|
3770
|
+
mcpQuery("chain.listCollections"),
|
|
3771
|
+
mcpQuery("chain.listEntries", { collectionSlug: "tracking-events" }),
|
|
3772
|
+
mcpQuery("chain.listEntries", { collectionSlug: "standards" }),
|
|
3773
|
+
mcpQuery("chain.listEntries", { collectionSlug: "business-rules" })
|
|
3288
3774
|
]);
|
|
3289
3775
|
const collections = collectionsResult.status === "fulfilled" ? collectionsResult.value : null;
|
|
3290
3776
|
const trackingEvents = eventsResult.status === "fulfilled" ? eventsResult.value : null;
|
|
@@ -3304,8 +3790,8 @@ function registerResources(server2) {
|
|
|
3304
3790
|
"productbrain://terminology",
|
|
3305
3791
|
async (uri) => {
|
|
3306
3792
|
const [glossaryResult, standardsResult] = await Promise.allSettled([
|
|
3307
|
-
mcpQuery("
|
|
3308
|
-
mcpQuery("
|
|
3793
|
+
mcpQuery("chain.listEntries", { collectionSlug: "glossary" }),
|
|
3794
|
+
mcpQuery("chain.listEntries", { collectionSlug: "standards" })
|
|
3309
3795
|
]);
|
|
3310
3796
|
const lines = ["# Product Brain \u2014 Terminology"];
|
|
3311
3797
|
if (glossaryResult.status === "fulfilled") {
|
|
@@ -3341,7 +3827,7 @@ ${stds}`);
|
|
|
3341
3827
|
"chain-collections",
|
|
3342
3828
|
"productbrain://collections",
|
|
3343
3829
|
async (uri) => {
|
|
3344
|
-
const collections = await mcpQuery("
|
|
3830
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
3345
3831
|
if (collections.length === 0) {
|
|
3346
3832
|
return { contents: [{ uri: uri.href, text: "No collections in this workspace.", mimeType: "text/markdown" }] };
|
|
3347
3833
|
}
|
|
@@ -3364,7 +3850,7 @@ ${formatted}`, mimeType: "text/markdown" }]
|
|
|
3364
3850
|
"chain-collection-entries",
|
|
3365
3851
|
new ResourceTemplate("productbrain://{slug}/entries", {
|
|
3366
3852
|
list: async () => {
|
|
3367
|
-
const collections = await mcpQuery("
|
|
3853
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
3368
3854
|
return {
|
|
3369
3855
|
resources: collections.map((c) => ({
|
|
3370
3856
|
uri: `productbrain://${c.slug}/entries`,
|
|
@@ -3374,7 +3860,7 @@ ${formatted}`, mimeType: "text/markdown" }]
|
|
|
3374
3860
|
}
|
|
3375
3861
|
}),
|
|
3376
3862
|
async (uri, { slug }) => {
|
|
3377
|
-
const entries = await mcpQuery("
|
|
3863
|
+
const entries = await mcpQuery("chain.listEntries", { collectionSlug: slug });
|
|
3378
3864
|
const formatted = entries.map(formatEntryMarkdown).join("\n\n---\n\n");
|
|
3379
3865
|
return {
|
|
3380
3866
|
contents: [{
|
|
@@ -3389,7 +3875,7 @@ ${formatted}`, mimeType: "text/markdown" }]
|
|
|
3389
3875
|
"chain-labels",
|
|
3390
3876
|
"productbrain://labels",
|
|
3391
3877
|
async (uri) => {
|
|
3392
|
-
const labels = await mcpQuery("
|
|
3878
|
+
const labels = await mcpQuery("chain.listLabels");
|
|
3393
3879
|
if (labels.length === 0) {
|
|
3394
3880
|
return { contents: [{ uri: uri.href, text: "No labels in this workspace.", mimeType: "text/markdown" }] };
|
|
3395
3881
|
}
|
|
@@ -3419,14 +3905,14 @@ ${lines.join("\n")}`, mimeType: "text/markdown" }]
|
|
|
3419
3905
|
}
|
|
3420
3906
|
|
|
3421
3907
|
// src/prompts/index.ts
|
|
3422
|
-
import { z as
|
|
3908
|
+
import { z as z10 } from "zod";
|
|
3423
3909
|
function registerPrompts(server2) {
|
|
3424
3910
|
server2.prompt(
|
|
3425
3911
|
"review-against-rules",
|
|
3426
3912
|
"Review code or a design decision against all business rules for a given domain. Fetches the rules and asks you to do a structured compliance review.",
|
|
3427
|
-
{ domain:
|
|
3913
|
+
{ domain: z10.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
|
|
3428
3914
|
async ({ domain }) => {
|
|
3429
|
-
const entries = await mcpQuery("
|
|
3915
|
+
const entries = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
|
|
3430
3916
|
const rules = entries.filter((e) => e.data?.domain === domain);
|
|
3431
3917
|
if (rules.length === 0) {
|
|
3432
3918
|
return {
|
|
@@ -3477,9 +3963,9 @@ Provide a structured review with a compliance status for each rule (COMPLIANT /
|
|
|
3477
3963
|
server2.prompt(
|
|
3478
3964
|
"name-check",
|
|
3479
3965
|
"Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
|
|
3480
|
-
{ names:
|
|
3966
|
+
{ names: z10.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
|
|
3481
3967
|
async ({ names }) => {
|
|
3482
|
-
const terms = await mcpQuery("
|
|
3968
|
+
const terms = await mcpQuery("chain.listEntries", { collectionSlug: "glossary" });
|
|
3483
3969
|
const glossaryContext = terms.map(
|
|
3484
3970
|
(t) => `${t.name} (${t.entryId ?? ""}) [${t.status}]: ${t.data?.canonical ?? ""}` + (t.data?.confusedWith?.length > 0 ? ` \u2014 Often confused with: ${t.data.confusedWith.join(", ")}` : "") + (t.data?.codeMapping?.length > 0 ? `
|
|
3485
3971
|
Code mappings: ${t.data.codeMapping.map((m) => `${m.platform}:${m.field}`).join(", ")}` : "")
|
|
@@ -3513,9 +3999,9 @@ Format as a table: Name | Status | Canonical Form | Action Needed`
|
|
|
3513
3999
|
server2.prompt(
|
|
3514
4000
|
"draft-decision-record",
|
|
3515
4001
|
"Draft a structured decision record from a description of what was decided. Includes context from recent decisions and relevant rules.",
|
|
3516
|
-
{ context:
|
|
4002
|
+
{ context: z10.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
|
|
3517
4003
|
async ({ context }) => {
|
|
3518
|
-
const recentDecisions = await mcpQuery("
|
|
4004
|
+
const recentDecisions = await mcpQuery("chain.listEntries", { collectionSlug: "decisions" });
|
|
3519
4005
|
const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
|
|
3520
4006
|
const recentContext = sorted.length > 0 ? sorted.map((d) => `- [${d.status}] ${d.name} (${d.data?.date ?? "no date"})`).join("\n") : "No previous decisions recorded.";
|
|
3521
4007
|
return {
|
|
@@ -3551,11 +4037,11 @@ After drafting, I can log it using the capture tool with collection "decisions".
|
|
|
3551
4037
|
"draft-rule-from-context",
|
|
3552
4038
|
"Draft a new business rule from an observation or discovery made while coding. Fetches existing rules for the domain to ensure consistency.",
|
|
3553
4039
|
{
|
|
3554
|
-
observation:
|
|
3555
|
-
domain:
|
|
4040
|
+
observation: z10.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
|
|
4041
|
+
domain: z10.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
|
|
3556
4042
|
},
|
|
3557
4043
|
async ({ observation, domain }) => {
|
|
3558
|
-
const allRules = await mcpQuery("
|
|
4044
|
+
const allRules = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
|
|
3559
4045
|
const existingRules = allRules.filter((r) => r.data?.domain === domain);
|
|
3560
4046
|
const existingContext = existingRules.length > 0 ? existingRules.map((r) => `${r.entryId ?? ""}: ${r.name} [${r.status}] \u2014 ${r.data?.description ?? ""}`).join("\n") : "No existing rules for this domain.";
|
|
3561
4047
|
const highestRuleNum = allRules.map((r) => parseInt((r.entryId ?? "").replace(/^[A-Z]+-/, ""), 10)).filter((n) => !isNaN(n)).sort((a, b) => b - a)[0] || 0;
|
|
@@ -3597,10 +4083,10 @@ Make sure the rule is consistent with existing rules and doesn't contradict them
|
|
|
3597
4083
|
"run-workflow",
|
|
3598
4084
|
"Launch a Chainwork workflow (retro, shape, IDM) in Facilitator Mode. Returns the full workflow definition, facilitation instructions, and round structure. The agent enters Facilitator Mode and guides the participant through each round.",
|
|
3599
4085
|
{
|
|
3600
|
-
workflow:
|
|
4086
|
+
workflow: z10.string().describe(
|
|
3601
4087
|
"Workflow ID to run. Available: " + listWorkflows().map((w) => `'${w.id}' (${w.name})`).join(", ")
|
|
3602
4088
|
),
|
|
3603
|
-
context:
|
|
4089
|
+
context: z10.string().optional().describe(
|
|
3604
4090
|
"Optional context from the participant (e.g., 'retro on last sprint', 'shape the Chainwork API bet')"
|
|
3605
4091
|
)
|
|
3606
4092
|
},
|
|
@@ -3627,7 +4113,7 @@ Use one of these IDs to run a workflow.`
|
|
|
3627
4113
|
}
|
|
3628
4114
|
let kbContext = "";
|
|
3629
4115
|
try {
|
|
3630
|
-
const recentDecisions = await mcpQuery("
|
|
4116
|
+
const recentDecisions = await mcpQuery("chain.listEntries", {
|
|
3631
4117
|
collectionSlug: wf.kbOutputCollection
|
|
3632
4118
|
});
|
|
3633
4119
|
const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
|
|
@@ -3716,23 +4202,9 @@ if (!process.env.CONVEX_SITE_URL && !process.env.PRODUCTBRAIN_API_KEY) {
|
|
|
3716
4202
|
} catch {
|
|
3717
4203
|
}
|
|
3718
4204
|
}
|
|
3719
|
-
|
|
4205
|
+
bootstrap();
|
|
3720
4206
|
var SERVER_VERSION = "2.0.0";
|
|
3721
4207
|
initAnalytics();
|
|
3722
|
-
var workspaceId;
|
|
3723
|
-
try {
|
|
3724
|
-
workspaceId = await getWorkspaceId();
|
|
3725
|
-
} catch (err) {
|
|
3726
|
-
const hint = !process.env.PRODUCTBRAIN_API_KEY && !process.env.CONVEX_SITE_URL ? "\n[MCP] Hint: Set PRODUCTBRAIN_API_KEY in your MCP config env block, or run `npx productbrain setup`." : "";
|
|
3727
|
-
process.stderr.write(`[MCP] Startup failed: ${err.message}${hint}
|
|
3728
|
-
`);
|
|
3729
|
-
process.exit(1);
|
|
3730
|
-
}
|
|
3731
|
-
trackSessionStarted(
|
|
3732
|
-
process.env.WORKSPACE_SLUG ?? "unknown",
|
|
3733
|
-
workspaceId,
|
|
3734
|
-
SERVER_VERSION
|
|
3735
|
-
);
|
|
3736
4208
|
var server = new McpServer2(
|
|
3737
4209
|
{
|
|
3738
4210
|
name: "Product Brain",
|
|
@@ -3745,28 +4217,27 @@ var server = new McpServer2(
|
|
|
3745
4217
|
"Terminology, standards, and core data all live here \u2014 no need to check external docs.",
|
|
3746
4218
|
"",
|
|
3747
4219
|
"Workflow:",
|
|
3748
|
-
" 1.
|
|
3749
|
-
" 2.
|
|
4220
|
+
" 1. Start: call `agent-start` to begin a tracked session.",
|
|
4221
|
+
" 2. Orient: call `orient` to load workspace context and unlock write tools.",
|
|
3750
4222
|
" 3. Discover: use `search` to find entries, or `list-entries` to browse.",
|
|
3751
4223
|
" 4. Drill in: use `get-entry` for full details \u2014 data, labels, relations, history.",
|
|
3752
4224
|
" 5. Context: use `gather-context` with an entryId or a task description.",
|
|
3753
4225
|
" 6. Capture: use `capture` to create entries \u2014 auto-links and scores in one call.",
|
|
3754
|
-
" 7.
|
|
3755
|
-
" 8.
|
|
3756
|
-
" 9.
|
|
3757
|
-
" 10. Debug: use `mcp-audit` to see what backend calls happened this session.",
|
|
4226
|
+
" 7. Commit: use `commit-entry` to promote drafts to SSOT.",
|
|
4227
|
+
" 8. Connect: use `suggest-links` then `relate-entries` to build the graph.",
|
|
4228
|
+
" 9. Close: call `agent-close` when done \u2014 records session activity.",
|
|
3758
4229
|
"",
|
|
3759
|
-
"
|
|
3760
|
-
"",
|
|
3761
|
-
"
|
|
3762
|
-
"
|
|
3763
|
-
" and analytics. It is the map. Then use tools to drill in."
|
|
4230
|
+
"Write tools (capture, update-entry, relate-entries, commit-entry) require:",
|
|
4231
|
+
" - An active agent session (call agent-start)",
|
|
4232
|
+
" - Completed orientation (call orient)",
|
|
4233
|
+
" - A readwrite API key scope"
|
|
3764
4234
|
].join("\n")
|
|
3765
4235
|
}
|
|
3766
4236
|
);
|
|
3767
4237
|
var enabledModules = new Set(
|
|
3768
|
-
(process.env.PB_MODULES ?? "core,gitchain").split(",").map((m) => m.trim().toLowerCase())
|
|
4238
|
+
(process.env.PB_MODULES ?? "core,gitchain,arch").split(",").map((m) => m.trim().toLowerCase())
|
|
3769
4239
|
);
|
|
4240
|
+
registerSessionTools(server);
|
|
3770
4241
|
registerKnowledgeTools(server);
|
|
3771
4242
|
registerLabelTools(server);
|
|
3772
4243
|
registerHealthTools(server);
|
|
@@ -3774,15 +4245,40 @@ registerVerifyTools(server);
|
|
|
3774
4245
|
registerSmartCaptureTools(server);
|
|
3775
4246
|
registerWorkflowTools(server);
|
|
3776
4247
|
if (enabledModules.has("gitchain")) registerGitChainTools(server);
|
|
4248
|
+
if (enabledModules.has("gitchain")) registerMapTools(server);
|
|
3777
4249
|
if (enabledModules.has("arch")) registerArchitectureTools(server);
|
|
4250
|
+
registerStartTools(server);
|
|
3778
4251
|
registerResources(server);
|
|
3779
4252
|
registerPrompts(server);
|
|
3780
4253
|
var transport = new StdioServerTransport();
|
|
3781
4254
|
await server.connect(transport);
|
|
4255
|
+
getWorkspaceId().then(async (wsId) => {
|
|
4256
|
+
trackSessionStarted(wsId, SERVER_VERSION);
|
|
4257
|
+
try {
|
|
4258
|
+
await startAgentSession();
|
|
4259
|
+
process.stderr.write("[MCP] Agent session started automatically.\n");
|
|
4260
|
+
} catch (err) {
|
|
4261
|
+
process.stderr.write(`[MCP] Auto session start failed: ${err.message}. Call agent-start manually.
|
|
4262
|
+
`);
|
|
4263
|
+
await recoverSessionState();
|
|
4264
|
+
}
|
|
4265
|
+
}).catch(() => {
|
|
4266
|
+
process.stderr.write("[MCP] Workspace resolution deferred \u2014 will retry on first tool call.\n");
|
|
4267
|
+
});
|
|
3782
4268
|
async function gracefulShutdown() {
|
|
4269
|
+
if (getAgentSessionId()) {
|
|
4270
|
+
await orphanAgentSession();
|
|
4271
|
+
}
|
|
3783
4272
|
await shutdownAnalytics();
|
|
3784
4273
|
process.exit(0);
|
|
3785
4274
|
}
|
|
3786
4275
|
process.on("SIGINT", gracefulShutdown);
|
|
3787
4276
|
process.on("SIGTERM", gracefulShutdown);
|
|
4277
|
+
process.stdin.on("end", async () => {
|
|
4278
|
+
if (getAgentSessionId()) {
|
|
4279
|
+
await orphanAgentSession();
|
|
4280
|
+
}
|
|
4281
|
+
await shutdownAnalytics();
|
|
4282
|
+
process.exit(0);
|
|
4283
|
+
});
|
|
3788
4284
|
//# sourceMappingURL=index.js.map
|