@productbrain/mcp 0.0.1-beta.5 → 0.0.1-beta.7
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-6ZSCQINU.js +1202 -0
- package/dist/chunk-6ZSCQINU.js.map +1 -0
- package/dist/index.js +1655 -1033
- package/dist/index.js.map +1 -1
- package/dist/smart-capture-SEINMTTR.js +13 -0
- package/dist/smart-capture-SEINMTTR.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
bootstrap,
|
|
4
|
+
closeAgentSession,
|
|
5
|
+
getAgentSessionId,
|
|
6
|
+
getAuditLog,
|
|
7
|
+
getWorkspaceContext,
|
|
8
|
+
getWorkspaceId,
|
|
9
|
+
initAnalytics,
|
|
10
|
+
isSessionOriented,
|
|
11
|
+
mcpCall,
|
|
12
|
+
mcpMutation,
|
|
13
|
+
mcpQuery,
|
|
14
|
+
orphanAgentSession,
|
|
15
|
+
recordSessionActivity,
|
|
16
|
+
recoverSessionState,
|
|
17
|
+
registerSmartCaptureTools,
|
|
18
|
+
requireWriteAccess,
|
|
19
|
+
setSessionOriented,
|
|
20
|
+
shutdownAnalytics,
|
|
21
|
+
startAgentSession,
|
|
22
|
+
trackSessionStarted
|
|
23
|
+
} from "./chunk-6ZSCQINU.js";
|
|
24
|
+
|
|
2
25
|
// src/index.ts
|
|
3
26
|
import { readFileSync as readFileSync3 } from "fs";
|
|
4
27
|
import { resolve as resolve3 } from "path";
|
|
@@ -7,179 +30,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
30
|
|
|
8
31
|
// src/tools/knowledge.ts
|
|
9
32
|
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
33
|
function extractPreview(data, maxLen) {
|
|
184
34
|
if (!data || typeof data !== "object") return "";
|
|
185
35
|
const raw = data.description ?? data.canonical ?? data.detail ?? "";
|
|
@@ -195,7 +45,7 @@ function registerKnowledgeTools(server2) {
|
|
|
195
45
|
annotations: { readOnlyHint: true }
|
|
196
46
|
},
|
|
197
47
|
async () => {
|
|
198
|
-
const collections = await mcpQuery("
|
|
48
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
199
49
|
if (collections.length === 0) {
|
|
200
50
|
return { content: [{ type: "text", text: "No collections found in this workspace." }] };
|
|
201
51
|
}
|
|
@@ -230,10 +80,10 @@ ${formatted}` }]
|
|
|
230
80
|
async ({ collection, status, tag, label }) => {
|
|
231
81
|
let entries;
|
|
232
82
|
if (label) {
|
|
233
|
-
entries = await mcpQuery("
|
|
83
|
+
entries = await mcpQuery("chain.listEntriesByLabel", { labelSlug: label });
|
|
234
84
|
if (status) entries = entries.filter((e) => e.status === status);
|
|
235
85
|
} else {
|
|
236
|
-
entries = await mcpQuery("
|
|
86
|
+
entries = await mcpQuery("chain.listEntries", {
|
|
237
87
|
collectionSlug: collection,
|
|
238
88
|
status,
|
|
239
89
|
tag
|
|
@@ -267,7 +117,7 @@ ${formatted}` }]
|
|
|
267
117
|
annotations: { readOnlyHint: true }
|
|
268
118
|
},
|
|
269
119
|
async ({ entryId }) => {
|
|
270
|
-
const entry = await mcpQuery("
|
|
120
|
+
const entry = await mcpQuery("chain.getEntry", { entryId });
|
|
271
121
|
if (!entry) {
|
|
272
122
|
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
273
123
|
}
|
|
@@ -324,21 +174,26 @@ ${formatted}` }]
|
|
|
324
174
|
},
|
|
325
175
|
async ({ entryId, name, status, data, order, autoPublish }) => {
|
|
326
176
|
try {
|
|
327
|
-
|
|
177
|
+
requireWriteAccess();
|
|
178
|
+
const id = await mcpMutation("chain.updateEntry", {
|
|
328
179
|
entryId,
|
|
329
180
|
name,
|
|
330
181
|
status,
|
|
331
182
|
data,
|
|
332
183
|
order,
|
|
333
|
-
autoPublish
|
|
184
|
+
autoPublish,
|
|
185
|
+
changedBy: getAgentSessionId() ? `agent:${getAgentSessionId()}` : void 0
|
|
334
186
|
});
|
|
187
|
+
await recordSessionActivity({ entryModified: entryId });
|
|
188
|
+
const wsCtx = await getWorkspaceContext();
|
|
335
189
|
const mode = autoPublish ? "published" : "saved as draft";
|
|
336
190
|
return {
|
|
337
191
|
content: [{ type: "text", text: `# Entry Updated
|
|
338
192
|
|
|
339
193
|
**${entryId}** has been ${mode}.
|
|
340
194
|
|
|
341
|
-
Internal ID: ${id}
|
|
195
|
+
Internal ID: ${id}
|
|
196
|
+
**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})` }]
|
|
342
197
|
};
|
|
343
198
|
} catch (error) {
|
|
344
199
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -380,8 +235,8 @@ Process criteria (TBD): e.g. 3+ users approved, or 7 days without valid concerns
|
|
|
380
235
|
const scope = collection ? ` in \`${collection}\`` : "";
|
|
381
236
|
await server2.sendLoggingMessage({ level: "info", data: `Searching${scope} for "${query}"...`, logger: "product-os" });
|
|
382
237
|
const [results, collections] = await Promise.all([
|
|
383
|
-
mcpQuery("
|
|
384
|
-
mcpQuery("
|
|
238
|
+
mcpQuery("chain.searchEntries", { query, collectionSlug: collection, status }),
|
|
239
|
+
mcpQuery("chain.listCollections")
|
|
385
240
|
]);
|
|
386
241
|
if (results.length === 0) {
|
|
387
242
|
return { content: [{ type: "text", text: `No results for "${query}"${scope}. Try a broader search or check list-collections for available data.` }] };
|
|
@@ -430,7 +285,7 @@ ${footer}` }]
|
|
|
430
285
|
annotations: { readOnlyHint: true }
|
|
431
286
|
},
|
|
432
287
|
async ({ entryId }) => {
|
|
433
|
-
const history = await mcpQuery("
|
|
288
|
+
const history = await mcpQuery("chain.listEntryHistory", { entryId });
|
|
434
289
|
if (history.length === 0) {
|
|
435
290
|
return { content: [{ type: "text", text: `No history found for \`${entryId}\`.` }] };
|
|
436
291
|
}
|
|
@@ -459,18 +314,73 @@ ${formatted}` }]
|
|
|
459
314
|
annotations: { destructiveHint: false }
|
|
460
315
|
},
|
|
461
316
|
async ({ from, to, type }) => {
|
|
462
|
-
|
|
317
|
+
requireWriteAccess();
|
|
318
|
+
await mcpMutation("chain.createEntryRelation", {
|
|
463
319
|
fromEntryId: from,
|
|
464
320
|
toEntryId: to,
|
|
465
321
|
type
|
|
466
322
|
});
|
|
323
|
+
await recordSessionActivity({ relationCreated: true });
|
|
324
|
+
const wsCtx = await getWorkspaceContext();
|
|
467
325
|
return {
|
|
468
326
|
content: [{ type: "text", text: `# Relation Created
|
|
469
327
|
|
|
470
|
-
**${from}** \u2014[${type}]\u2192 **${to}
|
|
328
|
+
**${from}** \u2014[${type}]\u2192 **${to}**
|
|
329
|
+
**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})` }]
|
|
471
330
|
};
|
|
472
331
|
}
|
|
473
332
|
);
|
|
333
|
+
server2.registerTool(
|
|
334
|
+
"batch-relate",
|
|
335
|
+
{
|
|
336
|
+
title: "Batch Link Entries",
|
|
337
|
+
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.",
|
|
338
|
+
inputSchema: {
|
|
339
|
+
relations: z.array(z.object({
|
|
340
|
+
from: z.string().describe("Source entry ID"),
|
|
341
|
+
to: z.string().describe("Target entry ID"),
|
|
342
|
+
type: z.string().describe("Relation type")
|
|
343
|
+
})).min(1).max(20).describe("Array of relations to create")
|
|
344
|
+
},
|
|
345
|
+
annotations: { destructiveHint: false }
|
|
346
|
+
},
|
|
347
|
+
async ({ relations }) => {
|
|
348
|
+
requireWriteAccess();
|
|
349
|
+
const results = [];
|
|
350
|
+
for (const rel of relations) {
|
|
351
|
+
try {
|
|
352
|
+
await mcpMutation("chain.createEntryRelation", {
|
|
353
|
+
fromEntryId: rel.from,
|
|
354
|
+
toEntryId: rel.to,
|
|
355
|
+
type: rel.type
|
|
356
|
+
});
|
|
357
|
+
results.push({ ...rel, ok: true });
|
|
358
|
+
} catch (e) {
|
|
359
|
+
results.push({ ...rel, ok: false, error: e.message || "Unknown error" });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const created = results.filter((r) => r.ok);
|
|
363
|
+
const failed = results.filter((r) => !r.ok);
|
|
364
|
+
const lines = [`# Batch Link Results
|
|
365
|
+
`];
|
|
366
|
+
lines.push(`**${created.length}** created, **${failed.length}** failed out of ${relations.length} total.
|
|
367
|
+
`);
|
|
368
|
+
if (created.length > 0) {
|
|
369
|
+
lines.push("## Created");
|
|
370
|
+
for (const r of created) {
|
|
371
|
+
lines.push(`- **${r.from}** \u2014[${r.type}]\u2192 **${r.to}**`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (failed.length > 0) {
|
|
375
|
+
lines.push("");
|
|
376
|
+
lines.push("## Failed");
|
|
377
|
+
for (const r of failed) {
|
|
378
|
+
lines.push(`- **${r.from}** \u2192 **${r.to}** (${r.type}): _${r.error}_`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
382
|
+
}
|
|
383
|
+
);
|
|
474
384
|
server2.registerTool(
|
|
475
385
|
"find-related",
|
|
476
386
|
{
|
|
@@ -483,11 +393,11 @@ ${formatted}` }]
|
|
|
483
393
|
annotations: { readOnlyHint: true }
|
|
484
394
|
},
|
|
485
395
|
async ({ entryId, direction }) => {
|
|
486
|
-
const relations = await mcpQuery("
|
|
396
|
+
const relations = await mcpQuery("chain.listEntryRelations", { entryId });
|
|
487
397
|
if (relations.length === 0) {
|
|
488
398
|
return { content: [{ type: "text", text: `No relations found for \`${entryId}\`. Use relate-entries to create connections.` }] };
|
|
489
399
|
}
|
|
490
|
-
const sourceEntry = await mcpQuery("
|
|
400
|
+
const sourceEntry = await mcpQuery("chain.getEntry", { entryId });
|
|
491
401
|
if (!sourceEntry) {
|
|
492
402
|
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
493
403
|
}
|
|
@@ -502,12 +412,12 @@ ${formatted}` }]
|
|
|
502
412
|
}
|
|
503
413
|
const otherEntries = /* @__PURE__ */ new Map();
|
|
504
414
|
for (const id of otherIds) {
|
|
505
|
-
const entry = await mcpQuery("
|
|
415
|
+
const entry = await mcpQuery("chain.getEntry", { id });
|
|
506
416
|
if (entry) {
|
|
507
417
|
otherEntries.set(entry._id, { entryId: entry.entryId, name: entry.name, collectionId: entry.collectionId });
|
|
508
418
|
}
|
|
509
419
|
}
|
|
510
|
-
const collections = await mcpQuery("
|
|
420
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
511
421
|
const collMap = /* @__PURE__ */ new Map();
|
|
512
422
|
for (const c of collections) collMap.set(c._id, c.slug);
|
|
513
423
|
const lines = [`# Relations for ${entryId}: ${sourceEntry.name}`, ""];
|
|
@@ -549,19 +459,67 @@ ${formatted}` }]
|
|
|
549
459
|
"gather-context",
|
|
550
460
|
{
|
|
551
461
|
title: "Gather Context",
|
|
552
|
-
description: "Assemble knowledge context in one call.
|
|
462
|
+
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
463
|
inputSchema: {
|
|
554
464
|
entryId: z.string().optional().describe("Entry ID for graph traversal, e.g. 'FEAT-001', 'GT-019'"),
|
|
555
465
|
task: z.string().optional().describe("Natural-language task description for auto-loading relevant context"),
|
|
466
|
+
mode: z.enum(["search", "graph"]).default("search").optional().describe("'search' (default, backward-compatible) or 'graph' (enhanced with provenance paths)"),
|
|
556
467
|
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
468
|
maxResults: z.number().min(1).max(25).default(10).optional().describe("Max entries to return in task mode (default 10)")
|
|
558
469
|
},
|
|
559
470
|
annotations: { readOnlyHint: true }
|
|
560
471
|
},
|
|
561
|
-
async ({ entryId, task, maxHops, maxResults }) => {
|
|
472
|
+
async ({ entryId, task, mode, maxHops, maxResults }) => {
|
|
562
473
|
if (!entryId && !task) {
|
|
563
474
|
return { content: [{ type: "text", text: "Provide either `entryId` (graph traversal) or `task` (auto-load context for a task)." }] };
|
|
564
475
|
}
|
|
476
|
+
if (entryId && mode === "graph") {
|
|
477
|
+
const result2 = await mcpQuery("chain.graphGatherContext", {
|
|
478
|
+
entryId,
|
|
479
|
+
maxHops: maxHops ?? 2
|
|
480
|
+
});
|
|
481
|
+
if (!result2?.root) {
|
|
482
|
+
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
483
|
+
}
|
|
484
|
+
if (result2.context.length === 0) {
|
|
485
|
+
return {
|
|
486
|
+
content: [{
|
|
487
|
+
type: "text",
|
|
488
|
+
text: `# Context for ${result2.root.entryId}: ${result2.root.name}
|
|
489
|
+
|
|
490
|
+
_No relations found._ This entry is not yet connected to the knowledge graph.
|
|
491
|
+
|
|
492
|
+
Use \`suggest-links\` to discover potential connections.`
|
|
493
|
+
}]
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const byCollection2 = /* @__PURE__ */ new Map();
|
|
497
|
+
for (const entry of result2.context) {
|
|
498
|
+
const key = entry.collectionName;
|
|
499
|
+
if (!byCollection2.has(key)) byCollection2.set(key, []);
|
|
500
|
+
byCollection2.get(key).push(entry);
|
|
501
|
+
}
|
|
502
|
+
const lines2 = [
|
|
503
|
+
`# Context for ${result2.root.entryId}: ${result2.root.name} (graph mode)`,
|
|
504
|
+
`_${result2.totalFound} related entries across ${byCollection2.size} collections (${result2.hopsTraversed} hops)_`,
|
|
505
|
+
""
|
|
506
|
+
];
|
|
507
|
+
for (const [collName, entries] of byCollection2) {
|
|
508
|
+
lines2.push(`## ${collName} (${entries.length})`);
|
|
509
|
+
for (const e of entries) {
|
|
510
|
+
const arrow = e.relationDirection === "outgoing" ? "\u2192" : "\u2190";
|
|
511
|
+
const id = e.entryId ? `${e.entryId}: ` : "";
|
|
512
|
+
const hopLabel = e.hop > 1 ? ` (hop ${e.hop})` : "";
|
|
513
|
+
const provenancePath = e.provenance && e.provenance.length > 1 ? `
|
|
514
|
+
_Path: ${e.provenance.map((p) => `${p.entryId ?? p.name} [${p.relationType}]`).join(" \u2192 ")}_` : "";
|
|
515
|
+
const preview = e.preview ? `
|
|
516
|
+
${e.preview.substring(0, 120)}` : "";
|
|
517
|
+
lines2.push(`- ${arrow} **${e.relationType}** ${id}${e.name}${hopLabel}${provenancePath}${preview}`);
|
|
518
|
+
}
|
|
519
|
+
lines2.push("");
|
|
520
|
+
}
|
|
521
|
+
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
522
|
+
}
|
|
565
523
|
if (task && !entryId) {
|
|
566
524
|
await server2.sendLoggingMessage({
|
|
567
525
|
level: "info",
|
|
@@ -569,7 +527,7 @@ ${formatted}` }]
|
|
|
569
527
|
logger: "product-brain"
|
|
570
528
|
});
|
|
571
529
|
const searchTerms = task.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 12).join(" ");
|
|
572
|
-
const searchResults = await mcpQuery("
|
|
530
|
+
const searchResults = await mcpQuery("chain.searchEntries", {
|
|
573
531
|
query: searchTerms
|
|
574
532
|
});
|
|
575
533
|
if (!searchResults || searchResults.length === 0) {
|
|
@@ -602,7 +560,7 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
|
|
|
602
560
|
});
|
|
603
561
|
if (hit.entryId) {
|
|
604
562
|
try {
|
|
605
|
-
const graph = await mcpQuery("
|
|
563
|
+
const graph = await mcpQuery("chain.gatherContext", {
|
|
606
564
|
entryId: hit.entryId,
|
|
607
565
|
maxHops: maxHops ?? 2
|
|
608
566
|
});
|
|
@@ -652,7 +610,7 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
|
|
|
652
610
|
lines2.push(`_Use \`get-entry\` for full details on any entry._`);
|
|
653
611
|
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
654
612
|
}
|
|
655
|
-
const result = await mcpQuery("
|
|
613
|
+
const result = await mcpQuery("chain.gatherContext", { entryId, maxHops });
|
|
656
614
|
if (!result?.root) {
|
|
657
615
|
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
658
616
|
}
|
|
@@ -696,61 +654,213 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
|
|
|
696
654
|
"suggest-links",
|
|
697
655
|
{
|
|
698
656
|
title: "Suggest Links",
|
|
699
|
-
description: "Discover potential connections for an entry
|
|
657
|
+
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
658
|
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")
|
|
659
|
+
entryId: z.string().describe("Entry ID (e.g. 'FEAT-001') or entry name (e.g. 'Chain Intelligence') to find suggestions for"),
|
|
660
|
+
limit: z.number().min(1).max(20).default(10).describe("Max number of suggestions to return"),
|
|
661
|
+
depth: z.number().min(1).max(3).default(2).describe("Graph traversal depth: 1=direct neighbors only, 2=default, 3=wide net")
|
|
703
662
|
},
|
|
704
663
|
annotations: { readOnlyHint: true }
|
|
705
664
|
},
|
|
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.` }] };
|
|
665
|
+
async ({ entryId, limit, depth }) => {
|
|
666
|
+
const result = await mcpQuery("chain.graphSuggestLinks", {
|
|
667
|
+
entryId,
|
|
668
|
+
maxHops: depth ?? 2,
|
|
669
|
+
limit: limit ?? 10
|
|
670
|
+
});
|
|
671
|
+
if (!result || !result.suggestions || result.suggestions.length === 0) {
|
|
672
|
+
return { content: [{ type: "text", text: `No suggestions found for \`${entryId}\` \u2014 it may already be well-connected, or no similar entries exist.` }] };
|
|
739
673
|
}
|
|
674
|
+
const resolved = result.resolvedEntry;
|
|
675
|
+
const resolvedLabel = resolved ? `\`${resolved.entryId ?? resolved.name}\` (${resolved.name})` : `\`${entryId}\``;
|
|
676
|
+
const suggestions = result.suggestions;
|
|
677
|
+
const graphCount = suggestions.filter((s) => s.graphDistance > 0).length;
|
|
678
|
+
const textCount = suggestions.filter((s) => s.graphDistance === -1).length;
|
|
679
|
+
const sourceId = resolved?.entryId ?? entryId;
|
|
740
680
|
const lines = [
|
|
741
|
-
`# Link Suggestions for ${
|
|
742
|
-
`_${suggestions.length} potential connections
|
|
681
|
+
`# Link Suggestions for ${resolvedLabel}`,
|
|
682
|
+
`_${suggestions.length} potential connections (${graphCount} via graph, ${textCount} via text similarity)._`,
|
|
743
683
|
""
|
|
744
684
|
];
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
685
|
+
const top3 = suggestions.slice(0, 3);
|
|
686
|
+
lines.push("## Quick Wins (top 3)");
|
|
687
|
+
const batchArgs = [];
|
|
688
|
+
for (const s of top3) {
|
|
689
|
+
const tid = s.entryId ?? "(no ID)";
|
|
690
|
+
const relType = s.recommendedRelationType || "related_to";
|
|
691
|
+
lines.push(`- **${tid}**: ${s.name} [${s.collectionSlug}] \u2014 ${s.score}/100 \u2192 \`${relType}\``);
|
|
692
|
+
if (s.entryId) batchArgs.push(`{from:"${sourceId}",to:"${tid}",type:"${relType}"}`);
|
|
749
693
|
}
|
|
750
694
|
lines.push("");
|
|
751
|
-
|
|
695
|
+
if (batchArgs.length > 0) {
|
|
696
|
+
lines.push(`**Link all 3:** \`batch-relate relations=[${batchArgs.join(",")}]\``);
|
|
697
|
+
lines.push("");
|
|
698
|
+
}
|
|
699
|
+
if (suggestions.length > 3) {
|
|
700
|
+
lines.push("## All Suggestions");
|
|
701
|
+
for (let i = 0; i < suggestions.length; i++) {
|
|
702
|
+
const s = suggestions[i];
|
|
703
|
+
const scoreBar = "\u2588".repeat(Math.round(s.score / 10)) + "\u2591".repeat(10 - Math.round(s.score / 10));
|
|
704
|
+
const hopLabel = s.graphDistance > 0 ? `${s.graphDistance}-hop` : "text";
|
|
705
|
+
const recType = s.recommendedRelationType !== "related_to" ? ` \u2192 \`${s.recommendedRelationType}\`` : "";
|
|
706
|
+
lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collectionSlug}] (${hopLabel})`);
|
|
707
|
+
lines.push(` ${scoreBar} ${s.score}/100${recType}`);
|
|
708
|
+
if (s.preview) {
|
|
709
|
+
lines.push(` ${s.preview}`);
|
|
710
|
+
}
|
|
711
|
+
lines.push(` _${s.reasoning}_`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
752
714
|
lines.push("");
|
|
753
|
-
lines.push(
|
|
715
|
+
lines.push(`**To link one:** \`relate-entries from="${sourceId}" to="{target_id}" type="{type}"\``);
|
|
716
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
717
|
+
}
|
|
718
|
+
);
|
|
719
|
+
server2.registerTool(
|
|
720
|
+
"create-collection",
|
|
721
|
+
{
|
|
722
|
+
title: "Create Collection",
|
|
723
|
+
description: "Create a new knowledge collection in the workspace. Collections define the structure for entries (like Notion databases or Capacity boards). Provide a slug, name, and field schema.\n\nUse this when setting up a workspace or when the user wants to track a new type of knowledge. Use `list-collections` first to see what already exists.",
|
|
724
|
+
inputSchema: {
|
|
725
|
+
slug: z.string().describe("URL-safe identifier, e.g. 'glossary', 'tech-debt', 'api-endpoints'"),
|
|
726
|
+
name: z.string().describe("Display name, e.g. 'Glossary', 'Tech Debt', 'API Endpoints'"),
|
|
727
|
+
description: z.string().optional().describe("What this collection is for"),
|
|
728
|
+
icon: z.string().optional().describe("Emoji icon for the collection"),
|
|
729
|
+
fields: z.array(z.object({
|
|
730
|
+
key: z.string().describe("Field key, e.g. 'description', 'severity', 'status'"),
|
|
731
|
+
label: z.string().describe("Display label, e.g. 'Description', 'Severity'"),
|
|
732
|
+
type: z.string().describe("Field type: 'string', 'select', 'array', 'number', 'boolean'"),
|
|
733
|
+
required: z.boolean().optional().describe("Whether this field is required"),
|
|
734
|
+
options: z.array(z.string()).optional().describe("Options for 'select' type fields"),
|
|
735
|
+
searchable: z.boolean().optional().describe("Whether this field is included in full-text search")
|
|
736
|
+
})).describe("Field definitions for the collection schema")
|
|
737
|
+
},
|
|
738
|
+
annotations: { destructiveHint: false }
|
|
739
|
+
},
|
|
740
|
+
async ({ slug, name, description, icon, fields }) => {
|
|
741
|
+
requireWriteAccess();
|
|
742
|
+
try {
|
|
743
|
+
await mcpMutation("chain.createCollection", {
|
|
744
|
+
slug,
|
|
745
|
+
name,
|
|
746
|
+
description,
|
|
747
|
+
icon,
|
|
748
|
+
fields,
|
|
749
|
+
isDefault: false,
|
|
750
|
+
createdBy: getAgentSessionId() ? `agent:${getAgentSessionId()}` : "mcp"
|
|
751
|
+
});
|
|
752
|
+
const fieldList = fields.map((f) => ` - \`${f.key}\` (${f.type}${f.required ? ", required" : ""}${f.searchable ? ", searchable" : ""})`).join("\n");
|
|
753
|
+
return {
|
|
754
|
+
content: [{
|
|
755
|
+
type: "text",
|
|
756
|
+
text: `# Collection Created: ${name}
|
|
757
|
+
|
|
758
|
+
**Slug:** \`${slug}\`
|
|
759
|
+
` + (description ? `**Description:** ${description}
|
|
760
|
+
` : "") + `
|
|
761
|
+
**Fields:**
|
|
762
|
+
${fieldList}
|
|
763
|
+
|
|
764
|
+
You can now capture entries: \`capture collection="${slug}" name="..." description="..."\``
|
|
765
|
+
}]
|
|
766
|
+
};
|
|
767
|
+
} catch (error) {
|
|
768
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
769
|
+
if (msg.includes("already exists")) {
|
|
770
|
+
return {
|
|
771
|
+
content: [{
|
|
772
|
+
type: "text",
|
|
773
|
+
text: `Collection \`${slug}\` already exists. Use \`update-collection\` to modify it, or choose a different slug.`
|
|
774
|
+
}]
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
);
|
|
781
|
+
server2.registerTool(
|
|
782
|
+
"update-collection",
|
|
783
|
+
{
|
|
784
|
+
title: "Update Collection",
|
|
785
|
+
description: "Update an existing collection's name, description, icon, or field schema. Only provide the fields you want to change. Use `list-collections` to see current state.",
|
|
786
|
+
inputSchema: {
|
|
787
|
+
slug: z.string().describe("Collection slug to update, e.g. 'glossary', 'tech-debt'"),
|
|
788
|
+
name: z.string().optional().describe("New display name"),
|
|
789
|
+
description: z.string().optional().describe("New description"),
|
|
790
|
+
icon: z.string().optional().describe("New emoji icon"),
|
|
791
|
+
fields: z.array(z.object({
|
|
792
|
+
key: z.string(),
|
|
793
|
+
label: z.string(),
|
|
794
|
+
type: z.string(),
|
|
795
|
+
required: z.boolean().optional(),
|
|
796
|
+
options: z.array(z.string()).optional(),
|
|
797
|
+
searchable: z.boolean().optional()
|
|
798
|
+
})).optional().describe("Replacement field schema (replaces all fields \u2014 include existing fields you want to keep)")
|
|
799
|
+
},
|
|
800
|
+
annotations: { destructiveHint: false }
|
|
801
|
+
},
|
|
802
|
+
async ({ slug, name, description, icon, fields }) => {
|
|
803
|
+
requireWriteAccess();
|
|
804
|
+
await mcpMutation("chain.updateCollection", {
|
|
805
|
+
slug,
|
|
806
|
+
...name !== void 0 && { name },
|
|
807
|
+
...description !== void 0 && { description },
|
|
808
|
+
...icon !== void 0 && { icon },
|
|
809
|
+
...fields !== void 0 && { fields }
|
|
810
|
+
});
|
|
811
|
+
const changes = [name && "name", description && "description", icon && "icon", fields && "fields"].filter(Boolean).join(", ");
|
|
812
|
+
return {
|
|
813
|
+
content: [{
|
|
814
|
+
type: "text",
|
|
815
|
+
text: `# Collection Updated: \`${slug}\`
|
|
816
|
+
|
|
817
|
+
Changed: ${changes || "no changes"}.
|
|
818
|
+
|
|
819
|
+
Use \`list-collections\` to verify the result.`
|
|
820
|
+
}]
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
);
|
|
824
|
+
server2.registerTool(
|
|
825
|
+
"commit-entry",
|
|
826
|
+
{
|
|
827
|
+
title: "Commit Entry to Chain",
|
|
828
|
+
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.",
|
|
829
|
+
inputSchema: {
|
|
830
|
+
entryId: z.string().describe("Entry ID to commit, e.g. 'TEN-abc123', 'GT-019'")
|
|
831
|
+
},
|
|
832
|
+
annotations: { destructiveHint: false }
|
|
833
|
+
},
|
|
834
|
+
async ({ entryId }) => {
|
|
835
|
+
requireWriteAccess();
|
|
836
|
+
const { runContradictionCheck } = await import("./smart-capture-SEINMTTR.js");
|
|
837
|
+
const entry = await mcpQuery("chain.getEntry", { entryId });
|
|
838
|
+
if (!entry) {
|
|
839
|
+
return {
|
|
840
|
+
content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }]
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
const descField = entry.data?.description ?? entry.data?.canonical ?? entry.data?.rationale ?? "";
|
|
844
|
+
const warnings = await runContradictionCheck(entry.name, descField);
|
|
845
|
+
if (warnings.length > 0) {
|
|
846
|
+
await recordSessionActivity({ contradictionWarning: true });
|
|
847
|
+
}
|
|
848
|
+
const result = await mcpMutation("chain.commitEntry", { entryId });
|
|
849
|
+
await recordSessionActivity({ entryModified: entryId });
|
|
850
|
+
const wsCtx = await getWorkspaceContext();
|
|
851
|
+
const lines = [
|
|
852
|
+
`# Committed: ${entryId}`,
|
|
853
|
+
`**${entry.name}** promoted to SSOT on the Chain.`,
|
|
854
|
+
`**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
|
|
855
|
+
];
|
|
856
|
+
if (warnings.length > 0) {
|
|
857
|
+
lines.push("");
|
|
858
|
+
lines.push("\u26A0 Contradiction check: proposed entry matched existing governance entries:");
|
|
859
|
+
for (const w of warnings) {
|
|
860
|
+
lines.push(`- ${w.name} (${w.collection}, ${w.entryId}) \u2014 has 'governs' relation to ${w.governsCount} entries`);
|
|
861
|
+
}
|
|
862
|
+
lines.push("Run gather-context on these entries before committing.");
|
|
863
|
+
}
|
|
754
864
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
755
865
|
}
|
|
756
866
|
);
|
|
@@ -778,7 +888,7 @@ function registerLabelTools(server2) {
|
|
|
778
888
|
},
|
|
779
889
|
async ({ action, slug, name, color, description, parentSlug, isGroup, order, entryId }) => {
|
|
780
890
|
if (action === "list") {
|
|
781
|
-
const labels = await mcpQuery("
|
|
891
|
+
const labels = await mcpQuery("chain.listLabels");
|
|
782
892
|
if (labels.length === 0) {
|
|
783
893
|
return { content: [{ type: "text", text: "No labels defined in this workspace yet." }] };
|
|
784
894
|
}
|
|
@@ -813,26 +923,26 @@ function registerLabelTools(server2) {
|
|
|
813
923
|
}
|
|
814
924
|
let parentId;
|
|
815
925
|
if (parentSlug) {
|
|
816
|
-
const labels = await mcpQuery("
|
|
926
|
+
const labels = await mcpQuery("chain.listLabels");
|
|
817
927
|
const parent = labels.find((l) => l.slug === parentSlug);
|
|
818
928
|
if (!parent) {
|
|
819
929
|
return { content: [{ type: "text", text: `Parent label \`${parentSlug}\` not found. Use \`labels action=list\` to see available groups.` }] };
|
|
820
930
|
}
|
|
821
931
|
parentId = parent._id;
|
|
822
932
|
}
|
|
823
|
-
await mcpMutation("
|
|
933
|
+
await mcpMutation("chain.createLabel", { slug, name, color, description, parentId, isGroup, order });
|
|
824
934
|
return { content: [{ type: "text", text: `# Label Created
|
|
825
935
|
|
|
826
936
|
**${name}** (\`${slug}\`)` }] };
|
|
827
937
|
}
|
|
828
938
|
if (action === "update") {
|
|
829
|
-
await mcpMutation("
|
|
939
|
+
await mcpMutation("chain.updateLabel", { slug, name, color, description, isGroup, order });
|
|
830
940
|
return { content: [{ type: "text", text: `# Label Updated
|
|
831
941
|
|
|
832
942
|
\`${slug}\` has been updated.` }] };
|
|
833
943
|
}
|
|
834
944
|
if (action === "delete") {
|
|
835
|
-
await mcpMutation("
|
|
945
|
+
await mcpMutation("chain.deleteLabel", { slug });
|
|
836
946
|
return { content: [{ type: "text", text: `# Label Deleted
|
|
837
947
|
|
|
838
948
|
\`${slug}\` removed from all entries and deleted.` }] };
|
|
@@ -842,10 +952,10 @@ function registerLabelTools(server2) {
|
|
|
842
952
|
return { content: [{ type: "text", text: "An `entryId` is required for apply/remove actions." }] };
|
|
843
953
|
}
|
|
844
954
|
if (action === "apply") {
|
|
845
|
-
await mcpMutation("
|
|
955
|
+
await mcpMutation("chain.applyLabel", { entryId, labelSlug: slug });
|
|
846
956
|
return { content: [{ type: "text", text: `Label \`${slug}\` applied to **${entryId}**.` }] };
|
|
847
957
|
}
|
|
848
|
-
await mcpMutation("
|
|
958
|
+
await mcpMutation("chain.removeLabel", { entryId, labelSlug: slug });
|
|
849
959
|
return { content: [{ type: "text", text: `Label \`${slug}\` removed from **${entryId}**.` }] };
|
|
850
960
|
}
|
|
851
961
|
return { content: [{ type: "text", text: "Unknown action." }] };
|
|
@@ -856,23 +966,25 @@ function registerLabelTools(server2) {
|
|
|
856
966
|
// src/tools/health.ts
|
|
857
967
|
import { z as z3 } from "zod";
|
|
858
968
|
var CALL_CATEGORIES = {
|
|
859
|
-
"
|
|
860
|
-
"
|
|
861
|
-
"
|
|
862
|
-
"
|
|
863
|
-
"
|
|
864
|
-
"
|
|
865
|
-
"
|
|
866
|
-
"
|
|
867
|
-
"
|
|
868
|
-
"
|
|
869
|
-
"
|
|
870
|
-
"
|
|
871
|
-
"
|
|
872
|
-
"
|
|
873
|
-
"
|
|
874
|
-
"
|
|
875
|
-
"
|
|
969
|
+
"chain.getEntry": "read",
|
|
970
|
+
"chain.listEntries": "read",
|
|
971
|
+
"chain.listEntryHistory": "read",
|
|
972
|
+
"chain.listEntryRelations": "read",
|
|
973
|
+
"chain.listEntriesByLabel": "read",
|
|
974
|
+
"chain.searchEntries": "search",
|
|
975
|
+
"chain.createEntry": "write",
|
|
976
|
+
"chain.updateEntry": "write",
|
|
977
|
+
"chain.createEntryRelation": "write",
|
|
978
|
+
"chain.applyLabel": "label",
|
|
979
|
+
"chain.removeLabel": "label",
|
|
980
|
+
"chain.createLabel": "label",
|
|
981
|
+
"chain.updateLabel": "label",
|
|
982
|
+
"chain.deleteLabel": "label",
|
|
983
|
+
"chain.createCollection": "write",
|
|
984
|
+
"chain.updateCollection": "write",
|
|
985
|
+
"chain.listCollections": "meta",
|
|
986
|
+
"chain.getCollection": "meta",
|
|
987
|
+
"chain.listLabels": "meta",
|
|
876
988
|
"resolveWorkspace": "meta"
|
|
877
989
|
};
|
|
878
990
|
function categorize(fn) {
|
|
@@ -884,23 +996,23 @@ function formatDuration(ms) {
|
|
|
884
996
|
const secs = Math.round(ms % 6e4 / 1e3);
|
|
885
997
|
return `${mins}m ${secs}s`;
|
|
886
998
|
}
|
|
887
|
-
function buildSessionSummary(
|
|
888
|
-
if (
|
|
999
|
+
function buildSessionSummary(log) {
|
|
1000
|
+
if (log.length === 0) return "";
|
|
889
1001
|
const byCategory = /* @__PURE__ */ new Map();
|
|
890
1002
|
let errorCount = 0;
|
|
891
1003
|
let writeCreates = 0;
|
|
892
1004
|
let writeUpdates = 0;
|
|
893
|
-
for (const entry of
|
|
1005
|
+
for (const entry of log) {
|
|
894
1006
|
const cat = categorize(entry.fn);
|
|
895
1007
|
if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
|
|
896
1008
|
const fnCounts = byCategory.get(cat);
|
|
897
1009
|
fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
|
|
898
1010
|
if (entry.status === "error") errorCount++;
|
|
899
|
-
if (entry.fn === "
|
|
900
|
-
if (entry.fn === "
|
|
1011
|
+
if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
|
|
1012
|
+
if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
|
|
901
1013
|
}
|
|
902
|
-
const firstTs = new Date(
|
|
903
|
-
const lastTs = new Date(
|
|
1014
|
+
const firstTs = new Date(log[0].ts).getTime();
|
|
1015
|
+
const lastTs = new Date(log[log.length - 1].ts).getTime();
|
|
904
1016
|
const duration = formatDuration(lastTs - firstTs);
|
|
905
1017
|
const lines = [`# Session Summary (${duration})
|
|
906
1018
|
`];
|
|
@@ -915,7 +1027,7 @@ function buildSessionSummary(log2) {
|
|
|
915
1027
|
const fnCounts = byCategory.get(cat);
|
|
916
1028
|
if (!fnCounts || fnCounts.size === 0) continue;
|
|
917
1029
|
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("
|
|
1030
|
+
const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
|
|
919
1031
|
lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
|
|
920
1032
|
}
|
|
921
1033
|
lines.push(`- **Errors:** ${errorCount}`);
|
|
@@ -938,33 +1050,40 @@ function registerHealthTools(server2) {
|
|
|
938
1050
|
async () => {
|
|
939
1051
|
const start = Date.now();
|
|
940
1052
|
const errors = [];
|
|
941
|
-
let
|
|
1053
|
+
let workspaceId;
|
|
942
1054
|
try {
|
|
943
|
-
|
|
1055
|
+
workspaceId = await getWorkspaceId();
|
|
944
1056
|
} catch (e) {
|
|
945
1057
|
errors.push(`Workspace resolution failed: ${e.message}`);
|
|
946
1058
|
}
|
|
947
1059
|
let collections = [];
|
|
948
1060
|
try {
|
|
949
|
-
collections = await mcpQuery("
|
|
1061
|
+
collections = await mcpQuery("chain.listCollections");
|
|
950
1062
|
} catch (e) {
|
|
951
1063
|
errors.push(`Collection fetch failed: ${e.message}`);
|
|
952
1064
|
}
|
|
953
1065
|
let totalEntries = 0;
|
|
954
1066
|
if (collections.length > 0) {
|
|
955
1067
|
try {
|
|
956
|
-
const entries = await mcpQuery("
|
|
1068
|
+
const entries = await mcpQuery("chain.listEntries", {});
|
|
957
1069
|
totalEntries = entries.length;
|
|
958
1070
|
} catch (e) {
|
|
959
1071
|
errors.push(`Entry count failed: ${e.message}`);
|
|
960
1072
|
}
|
|
961
1073
|
}
|
|
1074
|
+
let wsCtx = null;
|
|
1075
|
+
try {
|
|
1076
|
+
wsCtx = await getWorkspaceContext();
|
|
1077
|
+
} catch {
|
|
1078
|
+
}
|
|
962
1079
|
const durationMs = Date.now() - start;
|
|
963
1080
|
const healthy = errors.length === 0;
|
|
964
1081
|
const lines = [
|
|
965
1082
|
`# ${healthy ? "Healthy" : "Degraded"}`,
|
|
966
1083
|
"",
|
|
967
|
-
`**Workspace:** ${
|
|
1084
|
+
`**Workspace:** ${workspaceId ?? "unresolved"}`,
|
|
1085
|
+
`**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
|
|
1086
|
+
`**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
|
|
968
1087
|
`**Collections:** ${collections.length}`,
|
|
969
1088
|
`**Entries:** ${totalEntries}`,
|
|
970
1089
|
`**Latency:** ${durationMs}ms`
|
|
@@ -978,6 +1097,213 @@ function registerHealthTools(server2) {
|
|
|
978
1097
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
979
1098
|
}
|
|
980
1099
|
);
|
|
1100
|
+
server2.registerTool(
|
|
1101
|
+
"whoami",
|
|
1102
|
+
{
|
|
1103
|
+
title: "Session Identity",
|
|
1104
|
+
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.",
|
|
1105
|
+
annotations: { readOnlyHint: true }
|
|
1106
|
+
},
|
|
1107
|
+
async () => {
|
|
1108
|
+
const ctx = await getWorkspaceContext();
|
|
1109
|
+
const lines = [
|
|
1110
|
+
`# Session Identity`,
|
|
1111
|
+
"",
|
|
1112
|
+
`**Workspace ID:** ${ctx.workspaceId}`,
|
|
1113
|
+
`**Workspace Slug:** ${ctx.workspaceSlug}`,
|
|
1114
|
+
`**Workspace Name:** ${ctx.workspaceName}`
|
|
1115
|
+
];
|
|
1116
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1117
|
+
}
|
|
1118
|
+
);
|
|
1119
|
+
server2.registerTool(
|
|
1120
|
+
"workspace-status",
|
|
1121
|
+
{
|
|
1122
|
+
title: "Workspace Status",
|
|
1123
|
+
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.",
|
|
1124
|
+
annotations: { readOnlyHint: true }
|
|
1125
|
+
},
|
|
1126
|
+
async () => {
|
|
1127
|
+
const result = await mcpQuery("chain.workspaceReadiness");
|
|
1128
|
+
const { score, totalChecks, passedChecks, checks, gaps, stats } = result;
|
|
1129
|
+
const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
|
|
1130
|
+
const lines = [
|
|
1131
|
+
`# Workspace Readiness: ${score}%`,
|
|
1132
|
+
`${scoreBar} ${passedChecks}/${totalChecks} requirements met`,
|
|
1133
|
+
"",
|
|
1134
|
+
"## Stats",
|
|
1135
|
+
`- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
|
|
1136
|
+
`- **Relations:** ${stats.totalRelations}`,
|
|
1137
|
+
`- **Collections:** ${stats.collectionCount}`,
|
|
1138
|
+
`- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
|
|
1139
|
+
""
|
|
1140
|
+
];
|
|
1141
|
+
if (gaps.length > 0) {
|
|
1142
|
+
lines.push("## Gaps (action required)");
|
|
1143
|
+
for (const gap of gaps) {
|
|
1144
|
+
lines.push(`- [ ] **${gap.label}** \u2014 ${gap.description}`);
|
|
1145
|
+
lines.push(` ${gap.current}/${gap.required} | _${gap.guidance}_`);
|
|
1146
|
+
}
|
|
1147
|
+
lines.push("");
|
|
1148
|
+
}
|
|
1149
|
+
const passed = checks.filter((c) => c.passed);
|
|
1150
|
+
if (passed.length > 0) {
|
|
1151
|
+
lines.push("## Passed");
|
|
1152
|
+
for (const check of passed) {
|
|
1153
|
+
lines.push(`- [x] **${check.label}** (${check.current}/${check.required})`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1157
|
+
}
|
|
1158
|
+
);
|
|
1159
|
+
server2.registerTool(
|
|
1160
|
+
"orient",
|
|
1161
|
+
{
|
|
1162
|
+
title: "Orient \u2014 Start Here",
|
|
1163
|
+
description: "The single entry point for starting a session. Returns workspace context with a single recommended next action for low-readiness workspaces, or a standup-style briefing for established workspaces.\n\nUse this FIRST. One call to orient replaces 3\u20135 individual tool calls.\n\nCompleting orientation unlocks write tools for the active session.",
|
|
1164
|
+
annotations: { readOnlyHint: true }
|
|
1165
|
+
},
|
|
1166
|
+
async () => {
|
|
1167
|
+
const errors = [];
|
|
1168
|
+
const agentSessionId = getAgentSessionId();
|
|
1169
|
+
let wsCtx = null;
|
|
1170
|
+
try {
|
|
1171
|
+
wsCtx = await getWorkspaceContext();
|
|
1172
|
+
} catch (e) {
|
|
1173
|
+
errors.push(`Workspace: ${e.message}`);
|
|
1174
|
+
}
|
|
1175
|
+
let priorSessions = [];
|
|
1176
|
+
if (wsCtx) {
|
|
1177
|
+
try {
|
|
1178
|
+
priorSessions = await mcpQuery("agent.recentSessions", { limit: 3 });
|
|
1179
|
+
} catch {
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
let constraintEntries = [];
|
|
1183
|
+
try {
|
|
1184
|
+
const [archEntries, ruleEntries, decisionEntries] = await Promise.all([
|
|
1185
|
+
mcpQuery("chain.listEntries", { collectionSlug: "architecture" }),
|
|
1186
|
+
mcpQuery("chain.listEntries", { collectionSlug: "business-rules" }),
|
|
1187
|
+
mcpQuery("chain.listEntries", { collectionSlug: "decisions" })
|
|
1188
|
+
]);
|
|
1189
|
+
const committed = [
|
|
1190
|
+
...(archEntries ?? []).filter((e) => e.status === "active" || e.status === "healthy"),
|
|
1191
|
+
...(ruleEntries ?? []).filter((e) => e.status === "Active" || e.status === "active"),
|
|
1192
|
+
...(decisionEntries ?? []).filter((e) => e.status === "Decided" || e.status === "active")
|
|
1193
|
+
];
|
|
1194
|
+
constraintEntries = committed.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, 8);
|
|
1195
|
+
} catch {
|
|
1196
|
+
}
|
|
1197
|
+
let openTensions = [];
|
|
1198
|
+
try {
|
|
1199
|
+
const tensions = await mcpQuery("chain.listEntries", { collectionSlug: "tensions" });
|
|
1200
|
+
openTensions = (tensions ?? []).filter((e) => e.status === "draft");
|
|
1201
|
+
} catch {
|
|
1202
|
+
}
|
|
1203
|
+
let readiness = null;
|
|
1204
|
+
try {
|
|
1205
|
+
readiness = await mcpQuery("chain.workspaceReadiness");
|
|
1206
|
+
} catch (e) {
|
|
1207
|
+
errors.push(`Readiness: ${e.message}`);
|
|
1208
|
+
}
|
|
1209
|
+
const lines = [];
|
|
1210
|
+
const isLowReadiness = readiness && readiness.score < 50;
|
|
1211
|
+
if (wsCtx) {
|
|
1212
|
+
lines.push(`# ${wsCtx.workspaceName}`);
|
|
1213
|
+
lines.push(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
|
|
1214
|
+
} else {
|
|
1215
|
+
lines.push("# Workspace");
|
|
1216
|
+
lines.push("_Could not resolve workspace._");
|
|
1217
|
+
}
|
|
1218
|
+
lines.push("");
|
|
1219
|
+
if (isLowReadiness && wsCtx?.createdAt) {
|
|
1220
|
+
const ageDays = Math.floor((Date.now() - wsCtx.createdAt) / (1e3 * 60 * 60 * 24));
|
|
1221
|
+
if (ageDays >= 30) {
|
|
1222
|
+
lines.push(`Your workspace has been around for ${ageDays} days but is only ${readiness.score}% ready.`);
|
|
1223
|
+
lines.push("Let's close the gaps \u2014 or if the current structure doesn't fit, we can reshape it.");
|
|
1224
|
+
lines.push("");
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (isLowReadiness) {
|
|
1228
|
+
lines.push(`Readiness: ${readiness.score}% (${readiness.passedChecks}/${readiness.totalChecks}).`);
|
|
1229
|
+
lines.push("");
|
|
1230
|
+
const gaps = readiness.gaps ?? [];
|
|
1231
|
+
if (gaps.length > 0) {
|
|
1232
|
+
const gap = gaps[0];
|
|
1233
|
+
const ctaMap = {
|
|
1234
|
+
"strategy-vision": "Tell me what you're building \u2014 your vision, mission, and north star \u2014 and I'll capture it.",
|
|
1235
|
+
"architecture-layers": "Describe your architecture in a few sentences and I'll capture it.",
|
|
1236
|
+
"glossary-foundation": "What are the key terms your team uses? Tell me a few and I'll add them to the glossary.",
|
|
1237
|
+
"decisions-documented": "What's a recent significant decision your team made? I'll document it with the rationale.",
|
|
1238
|
+
"tensions-tracked": "What's a friction point or pain point you're dealing with? I'll capture it as a tension."
|
|
1239
|
+
};
|
|
1240
|
+
const cta = ctaMap[gap.id] ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
|
|
1241
|
+
lines.push("## Recommended next step");
|
|
1242
|
+
lines.push(`**${gap.label}** (${gap.current}/${gap.required})`);
|
|
1243
|
+
lines.push("");
|
|
1244
|
+
lines.push(cta);
|
|
1245
|
+
lines.push("");
|
|
1246
|
+
lines.push('_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._');
|
|
1247
|
+
lines.push("");
|
|
1248
|
+
const remainingGaps = gaps.length - 1;
|
|
1249
|
+
if (remainingGaps > 0 || openTensions.length > 0) {
|
|
1250
|
+
lines.push(`_${remainingGaps > 0 ? `${remainingGaps} more gap${remainingGaps === 1 ? "" : "s"}` : ""}${remainingGaps > 0 && openTensions.length > 0 ? " and " : ""}${openTensions.length > 0 ? `${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}` : ""} \u2014 ask "show full status" for details._`);
|
|
1251
|
+
lines.push("");
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
} else if (readiness) {
|
|
1255
|
+
lines.push(`Readiness: ${readiness.score}% (${readiness.passedChecks}/${readiness.totalChecks}).`);
|
|
1256
|
+
lines.push("");
|
|
1257
|
+
const briefingItems = [];
|
|
1258
|
+
if (openTensions.length > 0) {
|
|
1259
|
+
const topTension = openTensions[0];
|
|
1260
|
+
briefingItems.push(`**${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}** \u2014 top: ${topTension.name}`);
|
|
1261
|
+
}
|
|
1262
|
+
if (priorSessions.length > 0) {
|
|
1263
|
+
const last = priorSessions[0];
|
|
1264
|
+
const date = new Date(last.startedAt).toISOString().split("T")[0];
|
|
1265
|
+
const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
|
|
1266
|
+
const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
|
|
1267
|
+
briefingItems.push(`**Last session** (${date}): ${created} created, ${modified} modified`);
|
|
1268
|
+
}
|
|
1269
|
+
if (readiness.gaps?.length > 0) {
|
|
1270
|
+
briefingItems.push(`**${readiness.gaps.length} gap${readiness.gaps.length === 1 ? "" : "s"}** remaining`);
|
|
1271
|
+
}
|
|
1272
|
+
if (constraintEntries.length > 0) {
|
|
1273
|
+
briefingItems.push(`**${constraintEntries.length} active constraint${constraintEntries.length === 1 ? "" : "s"}** (architecture, rules, decisions)`);
|
|
1274
|
+
}
|
|
1275
|
+
if (briefingItems.length > 0) {
|
|
1276
|
+
lines.push("## Briefing");
|
|
1277
|
+
for (const item of briefingItems) {
|
|
1278
|
+
lines.push(`- ${item}`);
|
|
1279
|
+
}
|
|
1280
|
+
lines.push("");
|
|
1281
|
+
}
|
|
1282
|
+
lines.push("What would you like to work on?");
|
|
1283
|
+
lines.push("");
|
|
1284
|
+
}
|
|
1285
|
+
if (errors.length > 0) {
|
|
1286
|
+
lines.push("## Errors");
|
|
1287
|
+
for (const err of errors) lines.push(`- ${err}`);
|
|
1288
|
+
lines.push("");
|
|
1289
|
+
}
|
|
1290
|
+
if (agentSessionId) {
|
|
1291
|
+
try {
|
|
1292
|
+
await mcpCall("agent.markOriented", { sessionId: agentSessionId });
|
|
1293
|
+
setSessionOriented(true);
|
|
1294
|
+
lines.push("---");
|
|
1295
|
+
lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
|
|
1296
|
+
} catch {
|
|
1297
|
+
lines.push("---");
|
|
1298
|
+
lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
|
|
1299
|
+
}
|
|
1300
|
+
} else {
|
|
1301
|
+
lines.push("---");
|
|
1302
|
+
lines.push("_No active agent session. Call `agent-start` to begin a tracked session._");
|
|
1303
|
+
}
|
|
1304
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1305
|
+
}
|
|
1306
|
+
);
|
|
981
1307
|
server2.registerTool(
|
|
982
1308
|
"mcp-audit",
|
|
983
1309
|
{
|
|
@@ -989,13 +1315,13 @@ function registerHealthTools(server2) {
|
|
|
989
1315
|
annotations: { readOnlyHint: true }
|
|
990
1316
|
},
|
|
991
1317
|
async ({ limit }) => {
|
|
992
|
-
const
|
|
993
|
-
const recent =
|
|
1318
|
+
const log = getAuditLog();
|
|
1319
|
+
const recent = log.slice(-limit);
|
|
994
1320
|
if (recent.length === 0) {
|
|
995
1321
|
return { content: [{ type: "text", text: "No calls recorded yet this session." }] };
|
|
996
1322
|
}
|
|
997
|
-
const summary = buildSessionSummary(
|
|
998
|
-
const logLines = [`# Audit Log (last ${recent.length} of ${
|
|
1323
|
+
const summary = buildSessionSummary(log);
|
|
1324
|
+
const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
|
|
999
1325
|
`];
|
|
1000
1326
|
for (const entry of recent) {
|
|
1001
1327
|
const icon = entry.status === "ok" ? "\u2713" : "\u2717";
|
|
@@ -1188,7 +1514,7 @@ function registerVerifyTools(server2) {
|
|
|
1188
1514
|
data: `Verifying "${collection}" against ${schema.size} schema tables at ${projectRoot}`,
|
|
1189
1515
|
logger: "product-os"
|
|
1190
1516
|
});
|
|
1191
|
-
const scopedEntries = await mcpQuery("
|
|
1517
|
+
const scopedEntries = await mcpQuery("chain.listEntries", { collectionSlug: collection });
|
|
1192
1518
|
if (scopedEntries.length === 0) {
|
|
1193
1519
|
return {
|
|
1194
1520
|
content: [{ type: "text", text: `No entries found in \`${collection}\`. Nothing to verify.` }]
|
|
@@ -1196,7 +1522,7 @@ function registerVerifyTools(server2) {
|
|
|
1196
1522
|
}
|
|
1197
1523
|
let allEntryIds;
|
|
1198
1524
|
try {
|
|
1199
|
-
const allEntries = await mcpQuery("
|
|
1525
|
+
const allEntries = await mcpQuery("chain.listEntries", {});
|
|
1200
1526
|
allEntryIds = new Set(allEntries.map((e) => e.entryId).filter(Boolean));
|
|
1201
1527
|
} catch {
|
|
1202
1528
|
allEntryIds = new Set(scopedEntries.map((e) => e.entryId).filter(Boolean));
|
|
@@ -1261,7 +1587,7 @@ function registerVerifyTools(server2) {
|
|
|
1261
1587
|
const updated = (entry.data?.codeMapping ?? []).map(
|
|
1262
1588
|
(cm) => cm.status === "aligned" && driftedFields.has(cm.field) ? { ...cm, status: "drifted" } : cm
|
|
1263
1589
|
);
|
|
1264
|
-
await mcpMutation("
|
|
1590
|
+
await mcpMutation("chain.updateEntry", {
|
|
1265
1591
|
entryId: entry.entryId,
|
|
1266
1592
|
data: { codeMapping: updated }
|
|
1267
1593
|
});
|
|
@@ -1283,626 +1609,10 @@ function registerVerifyTools(server2) {
|
|
|
1283
1609
|
);
|
|
1284
1610
|
}
|
|
1285
1611
|
|
|
1286
|
-
// src/tools/smart-capture.ts
|
|
1287
|
-
import { z as z5 } from "zod";
|
|
1288
|
-
var AREA_KEYWORDS = {
|
|
1289
|
-
"Architecture": ["convex", "schema", "database", "migration", "api", "backend", "infrastructure", "scaling", "performance"],
|
|
1290
|
-
"Chain": ["knowledge", "glossary", "entry", "collection", "terminology", "drift", "graph", "chain", "commit"],
|
|
1291
|
-
"AI & MCP Integration": ["mcp", "ai", "cursor", "agent", "tool", "llm", "prompt", "context"],
|
|
1292
|
-
"Developer Experience": ["dx", "developer", "ide", "workflow", "friction", "ceremony"],
|
|
1293
|
-
"Governance & Decision-Making": ["governance", "decision", "rule", "policy", "compliance", "approval"],
|
|
1294
|
-
"Analytics & Tracking": ["analytics", "posthog", "tracking", "event", "metric", "funnel"],
|
|
1295
|
-
"Security": ["security", "auth", "api key", "permission", "access", "token"]
|
|
1296
|
-
};
|
|
1297
|
-
function inferArea(text) {
|
|
1298
|
-
const lower = text.toLowerCase();
|
|
1299
|
-
let bestArea = "";
|
|
1300
|
-
let bestScore = 0;
|
|
1301
|
-
for (const [area, keywords] of Object.entries(AREA_KEYWORDS)) {
|
|
1302
|
-
const score = keywords.filter((kw) => lower.includes(kw)).length;
|
|
1303
|
-
if (score > bestScore) {
|
|
1304
|
-
bestScore = score;
|
|
1305
|
-
bestArea = area;
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
return bestArea;
|
|
1309
|
-
}
|
|
1310
|
-
function inferDomain(text) {
|
|
1311
|
-
return inferArea(text) || "";
|
|
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
1612
|
// src/tools/architecture.ts
|
|
1903
1613
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
1904
1614
|
import { resolve as resolve2, relative, dirname, normalize } from "path";
|
|
1905
|
-
import { z as
|
|
1615
|
+
import { z as z5 } from "zod";
|
|
1906
1616
|
var COLLECTION_SLUG = "architecture";
|
|
1907
1617
|
var COLLECTION_FIELDS = [
|
|
1908
1618
|
{ key: "archType", label: "Architecture Type", type: "select", required: true, options: ["template", "layer", "node", "flow"], searchable: true },
|
|
@@ -1920,9 +1630,9 @@ var COLLECTION_FIELDS = [
|
|
|
1920
1630
|
{ key: "dependsOn", label: "Allowed Dependencies (layers this can import from)", type: "text" }
|
|
1921
1631
|
];
|
|
1922
1632
|
async function ensureCollection() {
|
|
1923
|
-
const collections = await mcpQuery("
|
|
1633
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
1924
1634
|
if (collections.some((c) => c.slug === COLLECTION_SLUG)) return;
|
|
1925
|
-
await mcpMutation("
|
|
1635
|
+
await mcpMutation("chain.createCollection", {
|
|
1926
1636
|
slug: COLLECTION_SLUG,
|
|
1927
1637
|
name: "Architecture",
|
|
1928
1638
|
icon: "\u{1F3D7}\uFE0F",
|
|
@@ -1931,7 +1641,7 @@ async function ensureCollection() {
|
|
|
1931
1641
|
});
|
|
1932
1642
|
}
|
|
1933
1643
|
async function listArchEntries() {
|
|
1934
|
-
return mcpQuery("
|
|
1644
|
+
return mcpQuery("chain.listEntries", { collectionSlug: COLLECTION_SLUG });
|
|
1935
1645
|
}
|
|
1936
1646
|
function byTag(entries, archType) {
|
|
1937
1647
|
return entries.filter((e) => e.tags?.includes(`archType:${archType}`) || e.data?.archType === archType).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
@@ -2056,10 +1766,10 @@ function registerArchitectureTools(server2) {
|
|
|
2056
1766
|
title: "Architecture",
|
|
2057
1767
|
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
1768
|
inputSchema: {
|
|
2059
|
-
action:
|
|
2060
|
-
template:
|
|
2061
|
-
layer:
|
|
2062
|
-
flow:
|
|
1769
|
+
action: z5.enum(["show", "explore", "flow"]).describe("Action: show full map, explore a layer, or visualize a flow"),
|
|
1770
|
+
template: z5.string().optional().describe("Template entry ID to filter by (for show)"),
|
|
1771
|
+
layer: z5.string().optional().describe("Layer name or entry ID (for explore), e.g. 'Core' or 'ARCH-layer-core'"),
|
|
1772
|
+
flow: z5.string().optional().describe("Flow name or entry ID (for flow), e.g. 'Smart Capture Flow'")
|
|
2063
1773
|
},
|
|
2064
1774
|
annotations: { readOnlyHint: true }
|
|
2065
1775
|
},
|
|
@@ -2178,7 +1888,7 @@ ${nodeDetail}${flowLines}`
|
|
|
2178
1888
|
title: "Architecture Admin",
|
|
2179
1889
|
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
1890
|
inputSchema: {
|
|
2181
|
-
action:
|
|
1891
|
+
action: z5.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
|
|
2182
1892
|
}
|
|
2183
1893
|
},
|
|
2184
1894
|
async ({ action }) => {
|
|
@@ -2205,14 +1915,14 @@ ${nodeDetail}${flowLines}`
|
|
|
2205
1915
|
);
|
|
2206
1916
|
if (hasChanges) {
|
|
2207
1917
|
const mergedData = { ...existingData, ...seedData };
|
|
2208
|
-
await mcpMutation("
|
|
1918
|
+
await mcpMutation("chain.updateEntry", { entryId: seed.entryId, data: mergedData });
|
|
2209
1919
|
updated++;
|
|
2210
1920
|
} else {
|
|
2211
1921
|
unchanged++;
|
|
2212
1922
|
}
|
|
2213
1923
|
continue;
|
|
2214
1924
|
}
|
|
2215
|
-
await mcpMutation("
|
|
1925
|
+
await mcpMutation("chain.createEntry", {
|
|
2216
1926
|
collectionSlug: COLLECTION_SLUG,
|
|
2217
1927
|
entryId: seed.entryId,
|
|
2218
1928
|
name: seed.name,
|
|
@@ -2471,7 +2181,7 @@ function formatScanReport(result) {
|
|
|
2471
2181
|
}
|
|
2472
2182
|
|
|
2473
2183
|
// src/tools/workflows.ts
|
|
2474
|
-
import { z as
|
|
2184
|
+
import { z as z6 } from "zod";
|
|
2475
2185
|
|
|
2476
2186
|
// src/workflows/definitions.ts
|
|
2477
2187
|
var RETRO_WORKFLOW = {
|
|
@@ -2700,16 +2410,16 @@ ${cards}`
|
|
|
2700
2410
|
title: "Workflow Checkpoint",
|
|
2701
2411
|
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
2412
|
inputSchema: {
|
|
2703
|
-
workflowId:
|
|
2704
|
-
roundId:
|
|
2705
|
-
output:
|
|
2706
|
-
isFinal:
|
|
2413
|
+
workflowId: z6.string().describe("Workflow ID (e.g., 'retro')"),
|
|
2414
|
+
roundId: z6.string().describe("Round ID (e.g., 'what-went-well')"),
|
|
2415
|
+
output: z6.string().describe("The round's output \u2014 synthesized by the facilitator from the conversation"),
|
|
2416
|
+
isFinal: z6.boolean().optional().describe(
|
|
2707
2417
|
"If true, this is the final checkpoint and triggers the summary chain entry creation"
|
|
2708
2418
|
),
|
|
2709
|
-
summaryName:
|
|
2419
|
+
summaryName: z6.string().optional().describe(
|
|
2710
2420
|
"Name for the final chain entry (required when isFinal=true)"
|
|
2711
2421
|
),
|
|
2712
|
-
summaryDescription:
|
|
2422
|
+
summaryDescription: z6.string().optional().describe(
|
|
2713
2423
|
"Full description/rationale for the final chain entry (required when isFinal=true)"
|
|
2714
2424
|
)
|
|
2715
2425
|
},
|
|
@@ -2745,7 +2455,7 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
|
|
|
2745
2455
|
];
|
|
2746
2456
|
if (isFinal && summaryName && summaryDescription) {
|
|
2747
2457
|
try {
|
|
2748
|
-
const entryId = await mcpMutation("
|
|
2458
|
+
const entryId = await mcpMutation("chain.createEntry", {
|
|
2749
2459
|
collectionSlug: wf.kbOutputCollection,
|
|
2750
2460
|
name: summaryName,
|
|
2751
2461
|
status: "draft",
|
|
@@ -2802,8 +2512,139 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
|
|
|
2802
2512
|
);
|
|
2803
2513
|
}
|
|
2804
2514
|
|
|
2515
|
+
// src/tools/session.ts
|
|
2516
|
+
function registerSessionTools(server2) {
|
|
2517
|
+
server2.registerTool(
|
|
2518
|
+
"agent-start",
|
|
2519
|
+
{
|
|
2520
|
+
title: "Start Agent Session",
|
|
2521
|
+
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.",
|
|
2522
|
+
annotations: { readOnlyHint: false }
|
|
2523
|
+
},
|
|
2524
|
+
async () => {
|
|
2525
|
+
try {
|
|
2526
|
+
const result = await startAgentSession();
|
|
2527
|
+
const lines = [];
|
|
2528
|
+
if (result.superseded) {
|
|
2529
|
+
lines.push(
|
|
2530
|
+
`Previous session superseded. Session ${result.superseded.previousSessionId} (started ${result.superseded.startedAt}, initiated by ${result.superseded.initiatedBy}) was closed.`,
|
|
2531
|
+
""
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
lines.push(
|
|
2535
|
+
`Session ${result.sessionId} active. Initiated by ${result.initiatedBy}. Workspace ${result.workspaceName}. Scope: ${result.toolsScope}. Write tools available after orient.`
|
|
2536
|
+
);
|
|
2537
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2538
|
+
} catch (err) {
|
|
2539
|
+
return {
|
|
2540
|
+
content: [{ type: "text", text: `Failed to start agent session: ${err.message}` }],
|
|
2541
|
+
isError: true
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
);
|
|
2546
|
+
server2.registerTool(
|
|
2547
|
+
"agent-close",
|
|
2548
|
+
{
|
|
2549
|
+
title: "Close Agent Session",
|
|
2550
|
+
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.",
|
|
2551
|
+
annotations: { readOnlyHint: false }
|
|
2552
|
+
},
|
|
2553
|
+
async () => {
|
|
2554
|
+
try {
|
|
2555
|
+
const sessionId = getAgentSessionId();
|
|
2556
|
+
if (!sessionId) {
|
|
2557
|
+
return {
|
|
2558
|
+
content: [{ type: "text", text: "No active agent session to close." }]
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
const session = await mcpCall("agent.getSession", {
|
|
2562
|
+
sessionId
|
|
2563
|
+
});
|
|
2564
|
+
await closeAgentSession();
|
|
2565
|
+
const lines = [
|
|
2566
|
+
`Session ${sessionId} closed.`,
|
|
2567
|
+
""
|
|
2568
|
+
];
|
|
2569
|
+
if (session) {
|
|
2570
|
+
const created = session.entriesCreated?.length ?? 0;
|
|
2571
|
+
const modified = session.entriesModified?.length ?? 0;
|
|
2572
|
+
const relations = session.relationsCreated ?? 0;
|
|
2573
|
+
const gates = session.gateFailures ?? 0;
|
|
2574
|
+
const warnings = session.contradictionWarnings ?? 0;
|
|
2575
|
+
lines.push(
|
|
2576
|
+
`| Metric | Count |`,
|
|
2577
|
+
`|--------|-------|`,
|
|
2578
|
+
`| Entries created | ${created} |`,
|
|
2579
|
+
`| Entries modified | ${modified} |`,
|
|
2580
|
+
`| Relations created | ${relations} |`,
|
|
2581
|
+
`| Gate failures | ${gates} |`,
|
|
2582
|
+
`| Contradiction warnings | ${warnings} |`
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
lines.push("", "Write tools are now blocked. Session data saved for future orientation.");
|
|
2586
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2587
|
+
} catch (err) {
|
|
2588
|
+
return {
|
|
2589
|
+
content: [{ type: "text", text: `Failed to close agent session: ${err.message}` }],
|
|
2590
|
+
isError: true
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
);
|
|
2595
|
+
server2.registerTool(
|
|
2596
|
+
"agent-status",
|
|
2597
|
+
{
|
|
2598
|
+
title: "Agent Session Status",
|
|
2599
|
+
description: "Check the current agent session status \u2014 whether a session is active, whether orientation is complete, and session activity so far.",
|
|
2600
|
+
annotations: { readOnlyHint: true }
|
|
2601
|
+
},
|
|
2602
|
+
async () => {
|
|
2603
|
+
try {
|
|
2604
|
+
const sessionId = getAgentSessionId();
|
|
2605
|
+
if (!sessionId) {
|
|
2606
|
+
return {
|
|
2607
|
+
content: [{ type: "text", text: "No active agent session. Call `agent-start` to begin." }]
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
const session = await mcpCall("agent.getSession", {
|
|
2611
|
+
sessionId
|
|
2612
|
+
});
|
|
2613
|
+
if (!session) {
|
|
2614
|
+
return {
|
|
2615
|
+
content: [{ type: "text", text: "Session ID cached but not found in Convex. Call `agent-start` to create a new session." }]
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
const oriented = isSessionOriented();
|
|
2619
|
+
const created = session.entriesCreated?.length ?? 0;
|
|
2620
|
+
const modified = session.entriesModified?.length ?? 0;
|
|
2621
|
+
const lines = [
|
|
2622
|
+
`Session ${sessionId} ${session.status}.`,
|
|
2623
|
+
`Initiated by ${session.initiatedBy}.`,
|
|
2624
|
+
`Scope: ${session.toolsScope}.`,
|
|
2625
|
+
`Oriented: ${oriented ? "yes" : "no"}.`,
|
|
2626
|
+
"",
|
|
2627
|
+
`| Metric | Value |`,
|
|
2628
|
+
`|--------|-------|`,
|
|
2629
|
+
`| Entries created | ${created} |`,
|
|
2630
|
+
`| Entries modified | ${modified} |`,
|
|
2631
|
+
`| Relations created | ${session.relationsCreated ?? 0} |`,
|
|
2632
|
+
`| Started | ${new Date(session.startedAt).toISOString()} |`,
|
|
2633
|
+
`| Expires | ${new Date(session.expiresAt).toISOString()} |`
|
|
2634
|
+
];
|
|
2635
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2636
|
+
} catch (err) {
|
|
2637
|
+
return {
|
|
2638
|
+
content: [{ type: "text", text: `Failed to get session status: ${err.message}` }],
|
|
2639
|
+
isError: true
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2805
2646
|
// src/tools/gitchain.ts
|
|
2806
|
-
import { z as
|
|
2647
|
+
import { z as z7 } from "zod";
|
|
2807
2648
|
function linkSummary(links) {
|
|
2808
2649
|
return Object.entries(links).map(([id, content]) => {
|
|
2809
2650
|
const filled = typeof content === "string" && content.length > 0;
|
|
@@ -2818,15 +2659,15 @@ function registerGitChainTools(server2) {
|
|
|
2818
2659
|
title: "Chain",
|
|
2819
2660
|
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
2661
|
inputSchema: {
|
|
2821
|
-
action:
|
|
2822
|
-
chainEntryId:
|
|
2823
|
-
title:
|
|
2824
|
-
chainTypeId:
|
|
2825
|
-
description:
|
|
2826
|
-
linkId:
|
|
2827
|
-
content:
|
|
2828
|
-
status:
|
|
2829
|
-
author:
|
|
2662
|
+
action: z7.enum(["create", "get", "list", "edit"]).describe("Action: create a process, get process details, list all processes, or edit a process link"),
|
|
2663
|
+
chainEntryId: z7.string().optional().describe("Chain entry ID (required for get/edit)"),
|
|
2664
|
+
title: z7.string().optional().describe("Process title (required for create)"),
|
|
2665
|
+
chainTypeId: z7.string().optional().default("strategy-coherence").describe("Process template slug for create: 'strategy-coherence', 'idm-proposal', or any custom template slug"),
|
|
2666
|
+
description: z7.string().optional().describe("Description (for create)"),
|
|
2667
|
+
linkId: z7.string().optional().describe("Link to edit (for edit action): problem, insight, choice, action, outcome"),
|
|
2668
|
+
content: z7.string().optional().describe("New content for the link (for edit action)"),
|
|
2669
|
+
status: z7.string().optional().describe("Filter by status for list: 'draft' or 'active'"),
|
|
2670
|
+
author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
2830
2671
|
}
|
|
2831
2672
|
},
|
|
2832
2673
|
async ({ action, chainEntryId, title, chainTypeId, description, linkId, content, status, author }) => {
|
|
@@ -2923,13 +2764,13 @@ Use \`chain action=get\` to see the full chain with updated scores.`
|
|
|
2923
2764
|
title: "Chain Version",
|
|
2924
2765
|
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
2766
|
inputSchema: {
|
|
2926
|
-
action:
|
|
2927
|
-
chainEntryId:
|
|
2928
|
-
commitMessage:
|
|
2929
|
-
versionA:
|
|
2930
|
-
versionB:
|
|
2931
|
-
toVersion:
|
|
2932
|
-
author:
|
|
2767
|
+
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"),
|
|
2768
|
+
chainEntryId: z7.string().describe("The chain's entry ID"),
|
|
2769
|
+
commitMessage: z7.string().optional().describe("Commit message (required for commit). Convention: type(link): description"),
|
|
2770
|
+
versionA: z7.number().optional().describe("Earlier version for diff"),
|
|
2771
|
+
versionB: z7.number().optional().describe("Later version for diff"),
|
|
2772
|
+
toVersion: z7.number().optional().describe("Version number to revert to"),
|
|
2773
|
+
author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
2933
2774
|
},
|
|
2934
2775
|
annotations: { readOnlyHint: false }
|
|
2935
2776
|
},
|
|
@@ -3037,11 +2878,11 @@ ${formatted}` }] };
|
|
|
3037
2878
|
title: "Chain Branch",
|
|
3038
2879
|
description: "Manage process branches \u2014 create a branch for isolated editing, list branches, merge a branch back into main, or check for conflicts.",
|
|
3039
2880
|
inputSchema: {
|
|
3040
|
-
action:
|
|
3041
|
-
chainEntryId:
|
|
3042
|
-
branchName:
|
|
3043
|
-
strategy:
|
|
3044
|
-
author:
|
|
2881
|
+
action: z7.enum(["create", "list", "merge", "conflicts"]).describe("Action: create a branch, list branches, merge a branch, or check for conflicts"),
|
|
2882
|
+
chainEntryId: z7.string().describe("The chain's entry ID"),
|
|
2883
|
+
branchName: z7.string().optional().describe("Branch name (required for merge/conflicts, optional for create)"),
|
|
2884
|
+
strategy: z7.enum(["merge_commit", "squash"]).optional().describe("Merge strategy: 'merge_commit' (default) or 'squash'"),
|
|
2885
|
+
author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
3045
2886
|
}
|
|
3046
2887
|
},
|
|
3047
2888
|
async ({ action, chainEntryId, branchName, strategy, author }) => {
|
|
@@ -3125,14 +2966,14 @@ Resolve conflicts before merging.`
|
|
|
3125
2966
|
title: "Chain Review",
|
|
3126
2967
|
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
2968
|
inputSchema: {
|
|
3128
|
-
action:
|
|
3129
|
-
chainEntryId:
|
|
3130
|
-
commitMessage:
|
|
3131
|
-
versionNumber:
|
|
3132
|
-
linkId:
|
|
3133
|
-
body:
|
|
3134
|
-
commentId:
|
|
3135
|
-
author:
|
|
2969
|
+
action: z7.enum(["gate", "comment", "resolve-comment", "list-comments"]).describe("Action: run coherence gate, add a comment, resolve a comment, or list comments"),
|
|
2970
|
+
chainEntryId: z7.string().describe("The chain's entry ID"),
|
|
2971
|
+
commitMessage: z7.string().optional().describe("Commit message to lint (for gate action)"),
|
|
2972
|
+
versionNumber: z7.number().optional().describe("Version to comment on or list comments for"),
|
|
2973
|
+
linkId: z7.string().optional().describe("Link this comment targets (optional for comment)"),
|
|
2974
|
+
body: z7.string().optional().describe("Comment text (required for comment action)"),
|
|
2975
|
+
commentId: z7.string().optional().describe("Comment ID (required for resolve-comment)"),
|
|
2976
|
+
author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
3136
2977
|
},
|
|
3137
2978
|
annotations: { readOnlyHint: false }
|
|
3138
2979
|
},
|
|
@@ -3211,40 +3052,799 @@ ${formatted}`
|
|
|
3211
3052
|
);
|
|
3212
3053
|
}
|
|
3213
3054
|
|
|
3214
|
-
// src/
|
|
3215
|
-
import {
|
|
3216
|
-
function
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
|
|
3055
|
+
// src/tools/maps.ts
|
|
3056
|
+
import { z as z8 } from "zod";
|
|
3057
|
+
function slotSummary(slots) {
|
|
3058
|
+
return Object.entries(slots).map(([id, refs]) => {
|
|
3059
|
+
if (!refs || refs.length === 0) return ` - **${id}**: (empty)`;
|
|
3060
|
+
const items = refs.map((r) => {
|
|
3061
|
+
const name = r.ingredientName ?? r.label ?? r.entryId;
|
|
3062
|
+
const version = r.pinnedVersion ? ` v${r.pinnedVersion}` : "";
|
|
3063
|
+
const status = r.ingredientStatus ? ` [${r.ingredientStatus}]` : "";
|
|
3064
|
+
return `${name}${version}${status}`;
|
|
3065
|
+
}).join(", ");
|
|
3066
|
+
return ` - **${id}**: ${items}`;
|
|
3067
|
+
}).join("\n");
|
|
3227
3068
|
}
|
|
3228
|
-
function
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
)
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3069
|
+
function registerMapTools(server2) {
|
|
3070
|
+
server2.registerTool(
|
|
3071
|
+
"map",
|
|
3072
|
+
{
|
|
3073
|
+
title: "Map",
|
|
3074
|
+
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.",
|
|
3075
|
+
inputSchema: {
|
|
3076
|
+
action: z8.enum(["create", "get", "list"]).describe("Action: create a map, get map details, or list all maps"),
|
|
3077
|
+
mapEntryId: z8.string().optional().describe("Map entry ID (for get)"),
|
|
3078
|
+
title: z8.string().optional().describe("Map title (for create)"),
|
|
3079
|
+
templateId: z8.string().optional().default("lean-canvas").describe("Template slug for create: 'lean-canvas' or any composed template"),
|
|
3080
|
+
description: z8.string().optional().describe("Description (for create)"),
|
|
3081
|
+
slotIds: z8.array(z8.string()).optional().describe("Slot IDs to initialize (for create; auto-populated from template if omitted)"),
|
|
3082
|
+
status: z8.string().optional().describe("Filter by status for list")
|
|
3083
|
+
}
|
|
3084
|
+
},
|
|
3085
|
+
async ({ action, mapEntryId, title, templateId, description, slotIds, status }) => {
|
|
3086
|
+
if (action === "create") {
|
|
3087
|
+
if (!title) {
|
|
3088
|
+
return {
|
|
3089
|
+
content: [
|
|
3090
|
+
{ type: "text", text: "A `title` is required to create a map." }
|
|
3091
|
+
]
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
const result = await mcpMutation(
|
|
3095
|
+
"maps.createMap",
|
|
3096
|
+
{ title, templateId, description, slotIds }
|
|
3097
|
+
);
|
|
3098
|
+
const wsCtx = await getWorkspaceContext();
|
|
3099
|
+
return {
|
|
3100
|
+
content: [
|
|
3101
|
+
{
|
|
3102
|
+
type: "text",
|
|
3103
|
+
text: `# Map Created
|
|
3104
|
+
|
|
3105
|
+
- **Entry ID:** \`${result.entryId}\`
|
|
3106
|
+
- **Title:** ${title}
|
|
3107
|
+
- **Template:** ${templateId}
|
|
3108
|
+
- **Status:** draft
|
|
3109
|
+
- **Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})
|
|
3110
|
+
|
|
3111
|
+
Use \`map-slot action=add mapEntryId="${result.entryId}" slotId="problem" ingredientEntryId="..."\` to start filling slots.`
|
|
3112
|
+
}
|
|
3113
|
+
]
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
if (action === "get") {
|
|
3117
|
+
if (!mapEntryId) {
|
|
3118
|
+
return {
|
|
3119
|
+
content: [{ type: "text", text: "A `mapEntryId` is required." }]
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
const map = await mcpQuery("maps.getMap", { mapEntryId });
|
|
3123
|
+
if (!map) {
|
|
3124
|
+
return {
|
|
3125
|
+
content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
return {
|
|
3129
|
+
content: [
|
|
3130
|
+
{
|
|
3131
|
+
type: "text",
|
|
3132
|
+
text: `# ${map.name}
|
|
3133
|
+
|
|
3134
|
+
- **Entry ID:** \`${map.entryId}\`
|
|
3135
|
+
- **Template:** ${map.templateName}
|
|
3136
|
+
- **Status:** ${map.status}
|
|
3137
|
+
- **Version:** ${map.currentVersion}
|
|
3138
|
+
- **Completion:** ${map.completionScore}% (${map.filledSlots}/${map.totalSlots} slots)
|
|
3139
|
+
|
|
3140
|
+
## Slots
|
|
3141
|
+
|
|
3142
|
+
` + slotSummary(map.slots)
|
|
3143
|
+
}
|
|
3144
|
+
]
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3147
|
+
if (action === "list") {
|
|
3148
|
+
const maps = await mcpQuery("maps.listMaps", {
|
|
3149
|
+
templateId: templateId !== "lean-canvas" ? templateId : void 0,
|
|
3150
|
+
status
|
|
3151
|
+
});
|
|
3152
|
+
if (!maps || maps.length === 0) {
|
|
3153
|
+
return {
|
|
3154
|
+
content: [
|
|
3155
|
+
{
|
|
3156
|
+
type: "text",
|
|
3157
|
+
text: 'No maps found. Create one with `map action=create title="My Canvas"`.'
|
|
3158
|
+
}
|
|
3159
|
+
]
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
const lines = maps.map(
|
|
3163
|
+
(m) => `- **${m.name}** (\`${m.entryId}\`) \u2014 ${m.templateId}, ${m.completionScore}% complete, v${m.currentVersion}`
|
|
3164
|
+
);
|
|
3165
|
+
return {
|
|
3166
|
+
content: [
|
|
3167
|
+
{
|
|
3168
|
+
type: "text",
|
|
3169
|
+
text: `# Maps (${maps.length})
|
|
3170
|
+
|
|
3171
|
+
${lines.join("\n")}`
|
|
3172
|
+
}
|
|
3173
|
+
]
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
return {
|
|
3177
|
+
content: [{ type: "text", text: `Unknown action: ${action}` }]
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
);
|
|
3181
|
+
server2.registerTool(
|
|
3182
|
+
"map-slot",
|
|
3183
|
+
{
|
|
3184
|
+
title: "Map Slot",
|
|
3185
|
+
description: "Add, remove, or replace ingredient references in a map's slots. Ingredients are entries from any collection that fill a framework position.",
|
|
3186
|
+
inputSchema: {
|
|
3187
|
+
action: z8.enum(["add", "remove", "replace", "list"]).describe("Action: add/remove/replace an ingredient in a slot, or list slot contents"),
|
|
3188
|
+
mapEntryId: z8.string().describe("Map entry ID"),
|
|
3189
|
+
slotId: z8.string().optional().describe("Slot ID (e.g. 'problem', 'customer-segments')"),
|
|
3190
|
+
ingredientEntryId: z8.string().optional().describe("Ingredient entry ID to add/remove"),
|
|
3191
|
+
newIngredientEntryId: z8.string().optional().describe("New ingredient entry ID (for replace)"),
|
|
3192
|
+
label: z8.string().optional().describe("Display label override"),
|
|
3193
|
+
author: z8.string().optional().describe("Who is performing the action")
|
|
3194
|
+
}
|
|
3195
|
+
},
|
|
3196
|
+
async ({
|
|
3197
|
+
action,
|
|
3198
|
+
mapEntryId,
|
|
3199
|
+
slotId,
|
|
3200
|
+
ingredientEntryId,
|
|
3201
|
+
newIngredientEntryId,
|
|
3202
|
+
label,
|
|
3203
|
+
author
|
|
3204
|
+
}) => {
|
|
3205
|
+
if (action === "list") {
|
|
3206
|
+
const map = await mcpQuery("maps.getMap", { mapEntryId });
|
|
3207
|
+
if (!map) {
|
|
3208
|
+
return {
|
|
3209
|
+
content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
if (slotId && map.slots[slotId]) {
|
|
3213
|
+
return {
|
|
3214
|
+
content: [
|
|
3215
|
+
{
|
|
3216
|
+
type: "text",
|
|
3217
|
+
text: `# Slot: ${slotId}
|
|
3218
|
+
|
|
3219
|
+
` + (map.slots[slotId].length === 0 ? "(empty)" : map.slots[slotId].map(
|
|
3220
|
+
(r) => `- **${r.ingredientName ?? r.entryId}** (\`${r.entryId}\`)${r.pinnedVersion ? ` v${r.pinnedVersion}` : ""}`
|
|
3221
|
+
).join("\n"))
|
|
3222
|
+
}
|
|
3223
|
+
]
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
return {
|
|
3227
|
+
content: [
|
|
3228
|
+
{ type: "text", text: `## All Slots
|
|
3229
|
+
|
|
3230
|
+
${slotSummary(map.slots)}` }
|
|
3231
|
+
]
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
if (action === "add") {
|
|
3235
|
+
if (!slotId || !ingredientEntryId) {
|
|
3236
|
+
return {
|
|
3237
|
+
content: [
|
|
3238
|
+
{
|
|
3239
|
+
type: "text",
|
|
3240
|
+
text: "Both `slotId` and `ingredientEntryId` are required for add."
|
|
3241
|
+
}
|
|
3242
|
+
]
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
await mcpMutation("maps.addToSlot", {
|
|
3246
|
+
mapEntryId,
|
|
3247
|
+
slotId,
|
|
3248
|
+
ingredientEntryId,
|
|
3249
|
+
label,
|
|
3250
|
+
author
|
|
3251
|
+
});
|
|
3252
|
+
return {
|
|
3253
|
+
content: [
|
|
3254
|
+
{
|
|
3255
|
+
type: "text",
|
|
3256
|
+
text: `Added \`${ingredientEntryId}\` to slot "${slotId}" on map \`${mapEntryId}\`.`
|
|
3257
|
+
}
|
|
3258
|
+
]
|
|
3259
|
+
};
|
|
3260
|
+
}
|
|
3261
|
+
if (action === "remove") {
|
|
3262
|
+
if (!slotId || !ingredientEntryId) {
|
|
3263
|
+
return {
|
|
3264
|
+
content: [
|
|
3265
|
+
{
|
|
3266
|
+
type: "text",
|
|
3267
|
+
text: "Both `slotId` and `ingredientEntryId` are required for remove."
|
|
3268
|
+
}
|
|
3269
|
+
]
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
await mcpMutation("maps.removeFromSlot", {
|
|
3273
|
+
mapEntryId,
|
|
3274
|
+
slotId,
|
|
3275
|
+
ingredientEntryId,
|
|
3276
|
+
author
|
|
3277
|
+
});
|
|
3278
|
+
return {
|
|
3279
|
+
content: [
|
|
3280
|
+
{
|
|
3281
|
+
type: "text",
|
|
3282
|
+
text: `Removed \`${ingredientEntryId}\` from slot "${slotId}" on map \`${mapEntryId}\`.`
|
|
3283
|
+
}
|
|
3284
|
+
]
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
if (action === "replace") {
|
|
3288
|
+
if (!slotId || !ingredientEntryId || !newIngredientEntryId) {
|
|
3289
|
+
return {
|
|
3290
|
+
content: [
|
|
3291
|
+
{
|
|
3292
|
+
type: "text",
|
|
3293
|
+
text: "`slotId`, `ingredientEntryId`, and `newIngredientEntryId` are required for replace."
|
|
3294
|
+
}
|
|
3295
|
+
]
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
await mcpMutation("maps.replaceInSlot", {
|
|
3299
|
+
mapEntryId,
|
|
3300
|
+
slotId,
|
|
3301
|
+
oldIngredientEntryId: ingredientEntryId,
|
|
3302
|
+
newIngredientEntryId,
|
|
3303
|
+
label,
|
|
3304
|
+
author
|
|
3305
|
+
});
|
|
3306
|
+
return {
|
|
3307
|
+
content: [
|
|
3308
|
+
{
|
|
3309
|
+
type: "text",
|
|
3310
|
+
text: `Replaced \`${ingredientEntryId}\` with \`${newIngredientEntryId}\` in slot "${slotId}".`
|
|
3311
|
+
}
|
|
3312
|
+
]
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
return {
|
|
3316
|
+
content: [{ type: "text", text: `Unknown action: ${action}` }]
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
);
|
|
3320
|
+
server2.registerTool(
|
|
3321
|
+
"map-version",
|
|
3322
|
+
{
|
|
3323
|
+
title: "Map Version",
|
|
3324
|
+
description: "Commit a map (pins ingredient versions), list commit history, or view a specific commit.",
|
|
3325
|
+
inputSchema: {
|
|
3326
|
+
action: z8.enum(["commit", "list", "history"]).describe("Action: commit the map, list commits, or view commit history"),
|
|
3327
|
+
mapEntryId: z8.string().describe("Map entry ID"),
|
|
3328
|
+
commitMessage: z8.string().optional().describe("Commit message (for commit action)"),
|
|
3329
|
+
author: z8.string().optional().describe("Who is committing")
|
|
3330
|
+
}
|
|
3331
|
+
},
|
|
3332
|
+
async ({ action, mapEntryId, commitMessage, author }) => {
|
|
3333
|
+
if (action === "commit") {
|
|
3334
|
+
const result = await mcpMutation("maps.commitMap", {
|
|
3335
|
+
mapEntryId,
|
|
3336
|
+
commitMessage: commitMessage ?? "Map committed",
|
|
3337
|
+
author
|
|
3338
|
+
});
|
|
3339
|
+
return {
|
|
3340
|
+
content: [
|
|
3341
|
+
{
|
|
3342
|
+
type: "text",
|
|
3343
|
+
text: `# Map Committed
|
|
3344
|
+
|
|
3345
|
+
- **Version:** ${result.version}
|
|
3346
|
+
- **Completion:** ${result.completionScore}%
|
|
3347
|
+
- **Slots modified:** ${result.slotsModified.length > 0 ? result.slotsModified.join(", ") : "(none)"}
|
|
3348
|
+
|
|
3349
|
+
All ingredient references have been pinned at their current versions.`
|
|
3350
|
+
}
|
|
3351
|
+
]
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
if (action === "list" || action === "history") {
|
|
3355
|
+
const commits = await mcpQuery("maps.listMapCommits", {
|
|
3356
|
+
mapEntryId
|
|
3357
|
+
});
|
|
3358
|
+
if (!commits || commits.length === 0) {
|
|
3359
|
+
return {
|
|
3360
|
+
content: [
|
|
3361
|
+
{
|
|
3362
|
+
type: "text",
|
|
3363
|
+
text: `No commits yet for map \`${mapEntryId}\`. Use \`map-version action=commit\` to create the first snapshot.`
|
|
3364
|
+
}
|
|
3365
|
+
]
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
const lines = commits.map(
|
|
3369
|
+
(c) => `- **v${c.version}** (${new Date(c.createdAt).toLocaleDateString()}) \u2014 ${c.commitMessage ?? "(no message)"} \u2014 by ${c.author}` + (c.slotsModified.length > 0 ? `
|
|
3370
|
+
Slots changed: ${c.slotsModified.join(", ")}` : "")
|
|
3371
|
+
);
|
|
3372
|
+
return {
|
|
3373
|
+
content: [
|
|
3374
|
+
{
|
|
3375
|
+
type: "text",
|
|
3376
|
+
text: `# Map History: \`${mapEntryId}\` (${commits.length} commits)
|
|
3377
|
+
|
|
3378
|
+
${lines.join("\n")}`
|
|
3379
|
+
}
|
|
3380
|
+
]
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
return {
|
|
3384
|
+
content: [{ type: "text", text: `Unknown action: ${action}` }]
|
|
3385
|
+
};
|
|
3386
|
+
}
|
|
3387
|
+
);
|
|
3388
|
+
server2.registerTool(
|
|
3389
|
+
"map-suggest",
|
|
3390
|
+
{
|
|
3391
|
+
title: "Map Suggest",
|
|
3392
|
+
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.",
|
|
3393
|
+
inputSchema: {
|
|
3394
|
+
mapEntryId: z8.string().describe("Map entry ID to suggest ingredients for"),
|
|
3395
|
+
slotId: z8.string().optional().describe("Specific slot to find ingredients for (or all empty slots)"),
|
|
3396
|
+
query: z8.string().optional().describe("Optional search query to narrow ingredient suggestions")
|
|
3397
|
+
}
|
|
3398
|
+
},
|
|
3399
|
+
async ({ mapEntryId, slotId, query }) => {
|
|
3400
|
+
const map = await mcpQuery("maps.getMap", { mapEntryId });
|
|
3401
|
+
if (!map) {
|
|
3402
|
+
return {
|
|
3403
|
+
content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
const emptySlots = slotId ? [slotId].filter((id) => (map.slots[id]?.length ?? 0) === 0) : Object.entries(map.slots).filter(([, refs]) => refs.length === 0).map(([id]) => id);
|
|
3407
|
+
if (emptySlots.length === 0) {
|
|
3408
|
+
return {
|
|
3409
|
+
content: [
|
|
3410
|
+
{
|
|
3411
|
+
type: "text",
|
|
3412
|
+
text: slotId ? `Slot "${slotId}" already has ingredients.` : "All slots have ingredients. Nothing to suggest."
|
|
3413
|
+
}
|
|
3414
|
+
]
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
const slotDefs = map.slotDefs ?? [];
|
|
3418
|
+
const suggestions = [];
|
|
3419
|
+
for (const sid of emptySlots) {
|
|
3420
|
+
const def = slotDefs.find((s) => s.id === sid);
|
|
3421
|
+
const searchQuery = query ?? def?.label ?? sid;
|
|
3422
|
+
const results = await mcpQuery("knowledge.search", {
|
|
3423
|
+
query: searchQuery,
|
|
3424
|
+
limit: 5
|
|
3425
|
+
});
|
|
3426
|
+
if (results && results.length > 0) {
|
|
3427
|
+
const items = results.map(
|
|
3428
|
+
(r) => ` - **${r.name}** (\`${r.entryId}\`, ${r.collectionName ?? "unknown"}) \u2014 ${r.status}`
|
|
3429
|
+
);
|
|
3430
|
+
suggestions.push(
|
|
3431
|
+
`### ${def?.label ?? sid}
|
|
3432
|
+
${def?.description ?? ""}
|
|
3433
|
+
|
|
3434
|
+
${items.join("\n")}`
|
|
3435
|
+
);
|
|
3436
|
+
} else {
|
|
3437
|
+
suggestions.push(
|
|
3438
|
+
`### ${def?.label ?? sid}
|
|
3439
|
+
_No matching entries found. Create ingredients first._`
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return {
|
|
3444
|
+
content: [
|
|
3445
|
+
{
|
|
3446
|
+
type: "text",
|
|
3447
|
+
text: `# Ingredient Suggestions for "${map.name}"
|
|
3448
|
+
|
|
3449
|
+
` + suggestions.join("\n\n") + `
|
|
3450
|
+
|
|
3451
|
+
Use \`map-slot action=add mapEntryId="${mapEntryId}" slotId="..." ingredientEntryId="..."\` to add ingredients.`
|
|
3452
|
+
}
|
|
3453
|
+
]
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
// src/tools/start.ts
|
|
3460
|
+
import { z as z9 } from "zod";
|
|
3461
|
+
|
|
3462
|
+
// src/presets/collections.ts
|
|
3463
|
+
var COLLECTION_PRESETS = [
|
|
3464
|
+
{
|
|
3465
|
+
id: "software-product",
|
|
3466
|
+
name: "Software Product",
|
|
3467
|
+
description: "For teams building software products \u2014 glossary, features, architecture, tech debt, and API endpoints",
|
|
3468
|
+
collections: [
|
|
3469
|
+
{ 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" }] },
|
|
3470
|
+
{ 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"] }] },
|
|
3471
|
+
{ 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" }] },
|
|
3472
|
+
{ 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"] }] },
|
|
3473
|
+
{ 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"] }] },
|
|
3474
|
+
{ 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" }] },
|
|
3475
|
+
{ 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"] }] }
|
|
3476
|
+
]
|
|
3477
|
+
},
|
|
3478
|
+
{
|
|
3479
|
+
id: "content-business",
|
|
3480
|
+
name: "Content Business",
|
|
3481
|
+
description: "For content creators, publishers, and media companies \u2014 topics, audience segments, content calendar, and brand voice",
|
|
3482
|
+
collections: [
|
|
3483
|
+
{ 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" }] },
|
|
3484
|
+
{ 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"] }] },
|
|
3485
|
+
{ 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" }] },
|
|
3486
|
+
{ 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" }] },
|
|
3487
|
+
{ 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" }] },
|
|
3488
|
+
{ 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"] }] }
|
|
3489
|
+
]
|
|
3490
|
+
},
|
|
3491
|
+
{
|
|
3492
|
+
id: "agency",
|
|
3493
|
+
name: "Agency",
|
|
3494
|
+
description: "For agencies managing multiple clients \u2014 client profiles, project scopes, deliverables, and processes",
|
|
3495
|
+
collections: [
|
|
3496
|
+
{ 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" }] },
|
|
3497
|
+
{ 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" }] },
|
|
3498
|
+
{ 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" }] },
|
|
3499
|
+
{ 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" }] },
|
|
3500
|
+
{ 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" }] },
|
|
3501
|
+
{ 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"] }] }
|
|
3502
|
+
]
|
|
3503
|
+
},
|
|
3504
|
+
{
|
|
3505
|
+
id: "saas-api",
|
|
3506
|
+
name: "SaaS API",
|
|
3507
|
+
description: "For API-first SaaS products \u2014 endpoints, schemas, rate limits, changelog, and integration guides",
|
|
3508
|
+
collections: [
|
|
3509
|
+
{ 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" }] },
|
|
3510
|
+
{ 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"] }] },
|
|
3511
|
+
{ 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" }] },
|
|
3512
|
+
{ 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" }] },
|
|
3513
|
+
{ 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"] }] },
|
|
3514
|
+
{ 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" }] }
|
|
3515
|
+
]
|
|
3516
|
+
},
|
|
3517
|
+
{
|
|
3518
|
+
id: "general",
|
|
3519
|
+
name: "General",
|
|
3520
|
+
description: "A minimal starter set \u2014 glossary, decisions, and tensions. Add more collections as you need them.",
|
|
3521
|
+
collections: [
|
|
3522
|
+
{ slug: "glossary", name: "Glossary", description: "Canonical terminology", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "string" }] },
|
|
3523
|
+
{ 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" }] },
|
|
3524
|
+
{ 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"] }] }
|
|
3525
|
+
]
|
|
3526
|
+
}
|
|
3527
|
+
];
|
|
3528
|
+
function getPreset(id) {
|
|
3529
|
+
return COLLECTION_PRESETS.find((p) => p.id === id);
|
|
3530
|
+
}
|
|
3531
|
+
function listPresets() {
|
|
3532
|
+
return COLLECTION_PRESETS.map((p) => ({
|
|
3533
|
+
id: p.id,
|
|
3534
|
+
name: p.name,
|
|
3535
|
+
description: p.description,
|
|
3536
|
+
collectionCount: p.collections.length
|
|
3537
|
+
}));
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
// src/tools/start.ts
|
|
3541
|
+
function registerStartTools(server2) {
|
|
3542
|
+
server2.registerTool(
|
|
3543
|
+
"start",
|
|
3544
|
+
{
|
|
3545
|
+
title: "Start Product Brain",
|
|
3546
|
+
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.",
|
|
3547
|
+
inputSchema: {
|
|
3548
|
+
preset: z9.string().optional().describe(
|
|
3549
|
+
"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."
|
|
3550
|
+
)
|
|
3551
|
+
},
|
|
3552
|
+
annotations: { readOnlyHint: false }
|
|
3553
|
+
},
|
|
3554
|
+
async ({ preset }) => {
|
|
3555
|
+
const errors = [];
|
|
3556
|
+
const agentSessionId = getAgentSessionId();
|
|
3557
|
+
let wsCtx = null;
|
|
3558
|
+
try {
|
|
3559
|
+
wsCtx = await getWorkspaceContext();
|
|
3560
|
+
} catch (e) {
|
|
3561
|
+
errors.push(`Workspace: ${e.message}`);
|
|
3562
|
+
}
|
|
3563
|
+
if (!wsCtx) {
|
|
3564
|
+
return {
|
|
3565
|
+
content: [
|
|
3566
|
+
{
|
|
3567
|
+
type: "text",
|
|
3568
|
+
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.")
|
|
3569
|
+
}
|
|
3570
|
+
]
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3573
|
+
const isFresh = await detectFreshWorkspace();
|
|
3574
|
+
if (isFresh && !preset) {
|
|
3575
|
+
return { content: [{ type: "text", text: buildPresetMenu(wsCtx) }] };
|
|
3576
|
+
}
|
|
3577
|
+
if (isFresh && preset) {
|
|
3578
|
+
return { content: [{ type: "text", text: await seedPreset(wsCtx, preset, agentSessionId) }] };
|
|
3579
|
+
}
|
|
3580
|
+
return { content: [{ type: "text", text: await buildOrientResponse(wsCtx, agentSessionId, errors) }] };
|
|
3581
|
+
}
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3584
|
+
async function detectFreshWorkspace() {
|
|
3585
|
+
try {
|
|
3586
|
+
const entries = await mcpQuery("chain.listEntries", {});
|
|
3587
|
+
const nonSystem = (entries ?? []).filter((e) => !e.isSystem);
|
|
3588
|
+
return nonSystem.length === 0;
|
|
3589
|
+
} catch {
|
|
3590
|
+
return false;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
function buildPresetMenu(wsCtx) {
|
|
3594
|
+
const presets = listPresets();
|
|
3595
|
+
const lines = [
|
|
3596
|
+
`# Welcome to ${wsCtx.workspaceName}`,
|
|
3597
|
+
"",
|
|
3598
|
+
"Your workspace is fresh \u2014 let's get it set up together.",
|
|
3599
|
+
"",
|
|
3600
|
+
"**Tell me: what are you building?** Describe it in a sentence or two and I'll help you pick the right structure. Or choose a preset to start from:",
|
|
3601
|
+
""
|
|
3602
|
+
];
|
|
3603
|
+
for (const p of presets) {
|
|
3604
|
+
lines.push(`- **${p.name}** (\`${p.id}\`) \u2014 ${p.description} (${p.collectionCount} collections)`);
|
|
3605
|
+
}
|
|
3606
|
+
lines.push(
|
|
3607
|
+
"",
|
|
3608
|
+
'Call `start` again with your choice, e.g.: `start preset="software-product"`',
|
|
3609
|
+
"",
|
|
3610
|
+
"_These are starting points. You can add, remove, or customize collections at any time using `create-collection` and `update-collection`._"
|
|
3611
|
+
);
|
|
3612
|
+
return lines.join("\n");
|
|
3613
|
+
}
|
|
3614
|
+
async function seedPreset(wsCtx, presetId, agentSessionId) {
|
|
3615
|
+
const preset = getPreset(presetId);
|
|
3616
|
+
if (!preset) {
|
|
3617
|
+
return `Preset "${presetId}" not found.
|
|
3618
|
+
|
|
3619
|
+
Available presets: ${listPresets().map((p) => `\`${p.id}\``).join(", ")}`;
|
|
3620
|
+
}
|
|
3621
|
+
const seeded = [];
|
|
3622
|
+
const skipped = [];
|
|
3623
|
+
for (const col of preset.collections) {
|
|
3624
|
+
try {
|
|
3625
|
+
await mcpCall("chain.createCollection", {
|
|
3626
|
+
slug: col.slug,
|
|
3627
|
+
name: col.name,
|
|
3628
|
+
description: col.description,
|
|
3629
|
+
icon: col.icon,
|
|
3630
|
+
fields: col.fields,
|
|
3631
|
+
isDefault: false,
|
|
3632
|
+
createdBy: "preset:" + presetId
|
|
3633
|
+
});
|
|
3634
|
+
seeded.push(col.name);
|
|
3635
|
+
} catch {
|
|
3636
|
+
skipped.push(`${col.name} (already exists)`);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
if (agentSessionId) {
|
|
3640
|
+
try {
|
|
3641
|
+
await mcpCall("agent.markOriented", { sessionId: agentSessionId });
|
|
3642
|
+
setSessionOriented(true);
|
|
3643
|
+
} catch {
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
const lines = [
|
|
3647
|
+
`# ${wsCtx.workspaceName} is ready`,
|
|
3648
|
+
"",
|
|
3649
|
+
`Seeded **${preset.name}** preset with ${seeded.length} collection${seeded.length === 1 ? "" : "s"}:`,
|
|
3650
|
+
""
|
|
3651
|
+
];
|
|
3652
|
+
for (const name of seeded) {
|
|
3653
|
+
lines.push(`- ${name}`);
|
|
3654
|
+
}
|
|
3655
|
+
if (skipped.length > 0) {
|
|
3656
|
+
lines.push("", "Skipped (already exist):");
|
|
3657
|
+
for (const name of skipped) {
|
|
3658
|
+
lines.push(`- ${name}`);
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
lines.push(
|
|
3662
|
+
"",
|
|
3663
|
+
"## Let's activate your workspace",
|
|
3664
|
+
"",
|
|
3665
|
+
"I'll help you load knowledge step by step. Everything stays as a draft until you confirm it.",
|
|
3666
|
+
"",
|
|
3667
|
+
"**First:** Tell me what you're building in one or two sentences. I'll capture it as your product vision.",
|
|
3668
|
+
"",
|
|
3669
|
+
`_After that, we'll add a few key terms to your glossary and any important decisions. Each entry is a draft \u2014 say "commit" or "looks good" when you're happy with it, and I'll promote it to the Chain (SSOT)._`,
|
|
3670
|
+
"",
|
|
3671
|
+
"You can also customize your structure anytime: `create-collection`, `update-collection`, or `list-collections`.",
|
|
3672
|
+
"",
|
|
3673
|
+
"---",
|
|
3674
|
+
"Orientation complete. Write tools are available."
|
|
3675
|
+
);
|
|
3676
|
+
return lines.join("\n");
|
|
3677
|
+
}
|
|
3678
|
+
function computeWorkspaceAge(createdAt) {
|
|
3679
|
+
if (!createdAt) return { ageDays: 0, isNeglected: false };
|
|
3680
|
+
const ageDays = Math.floor((Date.now() - createdAt) / (1e3 * 60 * 60 * 24));
|
|
3681
|
+
return { ageDays, isNeglected: ageDays >= 30 };
|
|
3682
|
+
}
|
|
3683
|
+
function pickNextAction(gaps, openTensions, priorSessions) {
|
|
3684
|
+
if (gaps.length === 0 && openTensions.length === 0) return null;
|
|
3685
|
+
if (gaps.length > 0) {
|
|
3686
|
+
const gap = gaps[0];
|
|
3687
|
+
const ctaMap = {
|
|
3688
|
+
"strategy-vision": "Tell me what you're building \u2014 your vision, mission, and north star \u2014 and I'll capture it.",
|
|
3689
|
+
"architecture-layers": "Describe your architecture in a few sentences and I'll capture it.",
|
|
3690
|
+
"glossary-foundation": "What are the key terms your team uses? Tell me a few and I'll add them to the glossary.",
|
|
3691
|
+
"decisions-documented": "What's a recent significant decision your team made? I'll document it with the rationale.",
|
|
3692
|
+
"tensions-tracked": "What's a friction point or pain point you're dealing with? I'll capture it as a tension."
|
|
3693
|
+
};
|
|
3694
|
+
const cta = ctaMap[gap.id] ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
|
|
3695
|
+
return { action: gap.label, cta };
|
|
3696
|
+
}
|
|
3697
|
+
if (openTensions.length > 0) {
|
|
3698
|
+
const t = openTensions[0];
|
|
3699
|
+
return {
|
|
3700
|
+
action: `Open tension: ${t.name}`,
|
|
3701
|
+
cta: "Want to discuss this tension or capture a decision about it?"
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
return null;
|
|
3705
|
+
}
|
|
3706
|
+
async function buildOrientResponse(wsCtx, agentSessionId, errors) {
|
|
3707
|
+
const wsFullCtx = await getWorkspaceContext();
|
|
3708
|
+
const { ageDays, isNeglected } = computeWorkspaceAge(wsFullCtx.createdAt);
|
|
3709
|
+
let priorSessions = [];
|
|
3710
|
+
try {
|
|
3711
|
+
priorSessions = await mcpQuery("agent.recentSessions", { limit: 3 });
|
|
3712
|
+
} catch {
|
|
3713
|
+
}
|
|
3714
|
+
let openTensions = [];
|
|
3715
|
+
try {
|
|
3716
|
+
const tensions = await mcpQuery("chain.listEntries", { collectionSlug: "tensions" });
|
|
3717
|
+
openTensions = (tensions ?? []).filter((e) => e.status === "draft");
|
|
3718
|
+
} catch {
|
|
3719
|
+
}
|
|
3720
|
+
let readiness = null;
|
|
3721
|
+
try {
|
|
3722
|
+
readiness = await mcpQuery("chain.workspaceReadiness");
|
|
3723
|
+
} catch (e) {
|
|
3724
|
+
errors.push(`Readiness: ${e.message}`);
|
|
3725
|
+
}
|
|
3726
|
+
const lines = [];
|
|
3727
|
+
const isLowReadiness = readiness && readiness.score < 50;
|
|
3728
|
+
const isHighReadiness = readiness && readiness.score >= 50;
|
|
3729
|
+
lines.push(`# ${wsCtx.workspaceName}`);
|
|
3730
|
+
lines.push(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
|
|
3731
|
+
lines.push("");
|
|
3732
|
+
if (isLowReadiness && isNeglected) {
|
|
3733
|
+
lines.push(`Your workspace has been around for ${ageDays} days but is only ${readiness.score}% ready.`);
|
|
3734
|
+
lines.push("Let's close the gaps \u2014 or if the current structure doesn't fit, we can reshape it.");
|
|
3735
|
+
lines.push("");
|
|
3736
|
+
} else if (isLowReadiness) {
|
|
3737
|
+
lines.push(`Readiness: ${readiness.score}%. Let's get your workspace active.`);
|
|
3738
|
+
lines.push("");
|
|
3739
|
+
}
|
|
3740
|
+
if (isLowReadiness) {
|
|
3741
|
+
const nextAction = pickNextAction(readiness.gaps ?? [], openTensions, priorSessions);
|
|
3742
|
+
if (nextAction) {
|
|
3743
|
+
lines.push("## Recommended next step");
|
|
3744
|
+
lines.push(`**${nextAction.action}**`);
|
|
3745
|
+
lines.push("");
|
|
3746
|
+
lines.push(nextAction.cta);
|
|
3747
|
+
lines.push("");
|
|
3748
|
+
lines.push('_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._');
|
|
3749
|
+
lines.push("");
|
|
3750
|
+
const remainingGaps = (readiness.gaps ?? []).length - 1;
|
|
3751
|
+
if (remainingGaps > 0 || openTensions.length > 0) {
|
|
3752
|
+
lines.push(`_${remainingGaps > 0 ? `${remainingGaps} more gap${remainingGaps === 1 ? "" : "s"}` : ""}${remainingGaps > 0 && openTensions.length > 0 ? " and " : ""}${openTensions.length > 0 ? `${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}` : ""} \u2014 ask "show full status" for details._`);
|
|
3753
|
+
lines.push("");
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
} else if (isHighReadiness) {
|
|
3757
|
+
if (readiness) {
|
|
3758
|
+
lines.push(`Readiness: ${readiness.score}% (${readiness.passedChecks}/${readiness.totalChecks}).`);
|
|
3759
|
+
}
|
|
3760
|
+
const briefingItems = [];
|
|
3761
|
+
if (openTensions.length > 0) {
|
|
3762
|
+
const topTension = openTensions[0];
|
|
3763
|
+
briefingItems.push(`**${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}** \u2014 top: ${topTension.name}`);
|
|
3764
|
+
}
|
|
3765
|
+
if (priorSessions.length > 0) {
|
|
3766
|
+
const last = priorSessions[0];
|
|
3767
|
+
const date = new Date(last.startedAt).toISOString().split("T")[0];
|
|
3768
|
+
const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
|
|
3769
|
+
const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
|
|
3770
|
+
briefingItems.push(`**Last session** (${date}): ${created} created, ${modified} modified`);
|
|
3771
|
+
}
|
|
3772
|
+
if (readiness?.gaps?.length > 0) {
|
|
3773
|
+
briefingItems.push(`**${readiness.gaps.length} gap${readiness.gaps.length === 1 ? "" : "s"}** remaining`);
|
|
3774
|
+
}
|
|
3775
|
+
if (briefingItems.length > 0) {
|
|
3776
|
+
lines.push("");
|
|
3777
|
+
lines.push("## Briefing");
|
|
3778
|
+
for (const item of briefingItems) {
|
|
3779
|
+
lines.push(`- ${item}`);
|
|
3780
|
+
}
|
|
3781
|
+
lines.push("");
|
|
3782
|
+
}
|
|
3783
|
+
lines.push("What would you like to work on?");
|
|
3784
|
+
lines.push("");
|
|
3785
|
+
}
|
|
3786
|
+
if (errors.length > 0) {
|
|
3787
|
+
lines.push("## Errors");
|
|
3788
|
+
for (const err of errors) lines.push(`- ${err}`);
|
|
3789
|
+
lines.push("");
|
|
3790
|
+
}
|
|
3791
|
+
if (agentSessionId) {
|
|
3792
|
+
try {
|
|
3793
|
+
await mcpCall("agent.markOriented", { sessionId: agentSessionId });
|
|
3794
|
+
setSessionOriented(true);
|
|
3795
|
+
lines.push("---");
|
|
3796
|
+
lines.push(
|
|
3797
|
+
`Orientation complete. Session ${agentSessionId}. Write tools available.`
|
|
3798
|
+
);
|
|
3799
|
+
} catch {
|
|
3800
|
+
lines.push("---");
|
|
3801
|
+
lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
|
|
3802
|
+
}
|
|
3803
|
+
} else {
|
|
3804
|
+
lines.push("---");
|
|
3805
|
+
lines.push("_No active agent session. Call `agent-start` to begin a tracked session._");
|
|
3806
|
+
}
|
|
3807
|
+
return lines.join("\n");
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
// src/resources/index.ts
|
|
3811
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3812
|
+
function formatEntryMarkdown(entry) {
|
|
3813
|
+
const id = entry.entryId ? `${entry.entryId}: ` : "";
|
|
3814
|
+
const lines = [`## ${id}${entry.name} [${entry.status}]`];
|
|
3815
|
+
if (entry.data && typeof entry.data === "object") {
|
|
3816
|
+
for (const [key, val] of Object.entries(entry.data)) {
|
|
3817
|
+
if (val && key !== "rawData") {
|
|
3818
|
+
lines.push(`**${key}**: ${typeof val === "string" ? val : JSON.stringify(val)}`);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
return lines.join("\n");
|
|
3823
|
+
}
|
|
3824
|
+
function buildOrientationMarkdown(collections, trackingEvents, standards, businessRules) {
|
|
3825
|
+
const sections = ["# Product Brain \u2014 Orientation"];
|
|
3826
|
+
sections.push(
|
|
3827
|
+
"## 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."
|
|
3828
|
+
);
|
|
3829
|
+
sections.push(
|
|
3830
|
+
"## 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)."
|
|
3831
|
+
);
|
|
3832
|
+
if (collections) {
|
|
3833
|
+
const collList = collections.map((c) => {
|
|
3834
|
+
const prefix = c.icon ? `${c.icon} ` : "";
|
|
3835
|
+
return `- ${prefix}**${c.name}** (\`${c.slug}\`) \u2014 ${c.description || "no description"}`;
|
|
3836
|
+
}).join("\n");
|
|
3837
|
+
sections.push(
|
|
3838
|
+
`## Data Model (${collections.length} collections)
|
|
3839
|
+
Unified entries model: collections define field schemas, entries hold data in a flexible \`data\` field.
|
|
3840
|
+
The \`data\` field is polymorphic: plain fields for generic entries, \`ChainData\` (chainTypeId + links) for processes, \`MapData\` (templateId + slots) for maps.
|
|
3841
|
+
Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, versions via \`entryVersions\`, labels via \`labels\` + \`entryLabels\`.
|
|
3842
|
+
|
|
3843
|
+
` + collList + "\n\nUse `list-collections` for field schemas, `get-entry` for full records."
|
|
3844
|
+
);
|
|
3845
|
+
} else {
|
|
3846
|
+
sections.push(
|
|
3847
|
+
"## Data Model\nCould not load collections \u2014 use `list-collections` to browse manually."
|
|
3248
3848
|
);
|
|
3249
3849
|
}
|
|
3250
3850
|
const rulesCount = businessRules ? `${businessRules.length} entries` : "not loaded \u2014 collection may not exist yet";
|
|
@@ -3262,16 +3862,16 @@ Draft a new rule: use the \`draft-rule-from-context\` prompt.`
|
|
|
3262
3862
|
Event catalog: \`tracking-events\` collection (${eventsCount}).
|
|
3263
3863
|
${conventionNote}
|
|
3264
3864
|
Implementation: \`src/lib/analytics.ts\`. Workspace-scoped events MUST use \`withWorkspaceGroup()\`.
|
|
3265
|
-
Browse: \`list-entries collection=tracking-events
|
|
3865
|
+
Browse: \`list-entries collection=tracking-events\`.`
|
|
3266
3866
|
);
|
|
3267
3867
|
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."
|
|
3868
|
+
"## 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
3869
|
);
|
|
3270
3870
|
sections.push(
|
|
3271
|
-
"## Creating Knowledge\
|
|
3871
|
+
"## 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
3872
|
);
|
|
3273
3873
|
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"
|
|
3874
|
+
"## 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
3875
|
);
|
|
3276
3876
|
return sections.join("\n\n---\n\n");
|
|
3277
3877
|
}
|
|
@@ -3281,10 +3881,10 @@ function registerResources(server2) {
|
|
|
3281
3881
|
"productbrain://orientation",
|
|
3282
3882
|
async (uri) => {
|
|
3283
3883
|
const [collectionsResult, eventsResult, standardsResult, rulesResult] = await Promise.allSettled([
|
|
3284
|
-
mcpQuery("
|
|
3285
|
-
mcpQuery("
|
|
3286
|
-
mcpQuery("
|
|
3287
|
-
mcpQuery("
|
|
3884
|
+
mcpQuery("chain.listCollections"),
|
|
3885
|
+
mcpQuery("chain.listEntries", { collectionSlug: "tracking-events" }),
|
|
3886
|
+
mcpQuery("chain.listEntries", { collectionSlug: "standards" }),
|
|
3887
|
+
mcpQuery("chain.listEntries", { collectionSlug: "business-rules" })
|
|
3288
3888
|
]);
|
|
3289
3889
|
const collections = collectionsResult.status === "fulfilled" ? collectionsResult.value : null;
|
|
3290
3890
|
const trackingEvents = eventsResult.status === "fulfilled" ? eventsResult.value : null;
|
|
@@ -3304,8 +3904,8 @@ function registerResources(server2) {
|
|
|
3304
3904
|
"productbrain://terminology",
|
|
3305
3905
|
async (uri) => {
|
|
3306
3906
|
const [glossaryResult, standardsResult] = await Promise.allSettled([
|
|
3307
|
-
mcpQuery("
|
|
3308
|
-
mcpQuery("
|
|
3907
|
+
mcpQuery("chain.listEntries", { collectionSlug: "glossary" }),
|
|
3908
|
+
mcpQuery("chain.listEntries", { collectionSlug: "standards" })
|
|
3309
3909
|
]);
|
|
3310
3910
|
const lines = ["# Product Brain \u2014 Terminology"];
|
|
3311
3911
|
if (glossaryResult.status === "fulfilled") {
|
|
@@ -3341,7 +3941,7 @@ ${stds}`);
|
|
|
3341
3941
|
"chain-collections",
|
|
3342
3942
|
"productbrain://collections",
|
|
3343
3943
|
async (uri) => {
|
|
3344
|
-
const collections = await mcpQuery("
|
|
3944
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
3345
3945
|
if (collections.length === 0) {
|
|
3346
3946
|
return { contents: [{ uri: uri.href, text: "No collections in this workspace.", mimeType: "text/markdown" }] };
|
|
3347
3947
|
}
|
|
@@ -3364,7 +3964,7 @@ ${formatted}`, mimeType: "text/markdown" }]
|
|
|
3364
3964
|
"chain-collection-entries",
|
|
3365
3965
|
new ResourceTemplate("productbrain://{slug}/entries", {
|
|
3366
3966
|
list: async () => {
|
|
3367
|
-
const collections = await mcpQuery("
|
|
3967
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
3368
3968
|
return {
|
|
3369
3969
|
resources: collections.map((c) => ({
|
|
3370
3970
|
uri: `productbrain://${c.slug}/entries`,
|
|
@@ -3374,7 +3974,7 @@ ${formatted}`, mimeType: "text/markdown" }]
|
|
|
3374
3974
|
}
|
|
3375
3975
|
}),
|
|
3376
3976
|
async (uri, { slug }) => {
|
|
3377
|
-
const entries = await mcpQuery("
|
|
3977
|
+
const entries = await mcpQuery("chain.listEntries", { collectionSlug: slug });
|
|
3378
3978
|
const formatted = entries.map(formatEntryMarkdown).join("\n\n---\n\n");
|
|
3379
3979
|
return {
|
|
3380
3980
|
contents: [{
|
|
@@ -3389,7 +3989,7 @@ ${formatted}`, mimeType: "text/markdown" }]
|
|
|
3389
3989
|
"chain-labels",
|
|
3390
3990
|
"productbrain://labels",
|
|
3391
3991
|
async (uri) => {
|
|
3392
|
-
const labels = await mcpQuery("
|
|
3992
|
+
const labels = await mcpQuery("chain.listLabels");
|
|
3393
3993
|
if (labels.length === 0) {
|
|
3394
3994
|
return { contents: [{ uri: uri.href, text: "No labels in this workspace.", mimeType: "text/markdown" }] };
|
|
3395
3995
|
}
|
|
@@ -3419,14 +4019,14 @@ ${lines.join("\n")}`, mimeType: "text/markdown" }]
|
|
|
3419
4019
|
}
|
|
3420
4020
|
|
|
3421
4021
|
// src/prompts/index.ts
|
|
3422
|
-
import { z as
|
|
4022
|
+
import { z as z10 } from "zod";
|
|
3423
4023
|
function registerPrompts(server2) {
|
|
3424
4024
|
server2.prompt(
|
|
3425
4025
|
"review-against-rules",
|
|
3426
4026
|
"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:
|
|
4027
|
+
{ domain: z10.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
|
|
3428
4028
|
async ({ domain }) => {
|
|
3429
|
-
const entries = await mcpQuery("
|
|
4029
|
+
const entries = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
|
|
3430
4030
|
const rules = entries.filter((e) => e.data?.domain === domain);
|
|
3431
4031
|
if (rules.length === 0) {
|
|
3432
4032
|
return {
|
|
@@ -3477,9 +4077,9 @@ Provide a structured review with a compliance status for each rule (COMPLIANT /
|
|
|
3477
4077
|
server2.prompt(
|
|
3478
4078
|
"name-check",
|
|
3479
4079
|
"Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
|
|
3480
|
-
{ names:
|
|
4080
|
+
{ names: z10.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
|
|
3481
4081
|
async ({ names }) => {
|
|
3482
|
-
const terms = await mcpQuery("
|
|
4082
|
+
const terms = await mcpQuery("chain.listEntries", { collectionSlug: "glossary" });
|
|
3483
4083
|
const glossaryContext = terms.map(
|
|
3484
4084
|
(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
4085
|
Code mappings: ${t.data.codeMapping.map((m) => `${m.platform}:${m.field}`).join(", ")}` : "")
|
|
@@ -3513,9 +4113,9 @@ Format as a table: Name | Status | Canonical Form | Action Needed`
|
|
|
3513
4113
|
server2.prompt(
|
|
3514
4114
|
"draft-decision-record",
|
|
3515
4115
|
"Draft a structured decision record from a description of what was decided. Includes context from recent decisions and relevant rules.",
|
|
3516
|
-
{ context:
|
|
4116
|
+
{ context: z10.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
|
|
3517
4117
|
async ({ context }) => {
|
|
3518
|
-
const recentDecisions = await mcpQuery("
|
|
4118
|
+
const recentDecisions = await mcpQuery("chain.listEntries", { collectionSlug: "decisions" });
|
|
3519
4119
|
const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
|
|
3520
4120
|
const recentContext = sorted.length > 0 ? sorted.map((d) => `- [${d.status}] ${d.name} (${d.data?.date ?? "no date"})`).join("\n") : "No previous decisions recorded.";
|
|
3521
4121
|
return {
|
|
@@ -3551,11 +4151,11 @@ After drafting, I can log it using the capture tool with collection "decisions".
|
|
|
3551
4151
|
"draft-rule-from-context",
|
|
3552
4152
|
"Draft a new business rule from an observation or discovery made while coding. Fetches existing rules for the domain to ensure consistency.",
|
|
3553
4153
|
{
|
|
3554
|
-
observation:
|
|
3555
|
-
domain:
|
|
4154
|
+
observation: z10.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
|
|
4155
|
+
domain: z10.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
|
|
3556
4156
|
},
|
|
3557
4157
|
async ({ observation, domain }) => {
|
|
3558
|
-
const allRules = await mcpQuery("
|
|
4158
|
+
const allRules = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
|
|
3559
4159
|
const existingRules = allRules.filter((r) => r.data?.domain === domain);
|
|
3560
4160
|
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
4161
|
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 +4197,10 @@ Make sure the rule is consistent with existing rules and doesn't contradict them
|
|
|
3597
4197
|
"run-workflow",
|
|
3598
4198
|
"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
4199
|
{
|
|
3600
|
-
workflow:
|
|
4200
|
+
workflow: z10.string().describe(
|
|
3601
4201
|
"Workflow ID to run. Available: " + listWorkflows().map((w) => `'${w.id}' (${w.name})`).join(", ")
|
|
3602
4202
|
),
|
|
3603
|
-
context:
|
|
4203
|
+
context: z10.string().optional().describe(
|
|
3604
4204
|
"Optional context from the participant (e.g., 'retro on last sprint', 'shape the Chainwork API bet')"
|
|
3605
4205
|
)
|
|
3606
4206
|
},
|
|
@@ -3627,7 +4227,7 @@ Use one of these IDs to run a workflow.`
|
|
|
3627
4227
|
}
|
|
3628
4228
|
let kbContext = "";
|
|
3629
4229
|
try {
|
|
3630
|
-
const recentDecisions = await mcpQuery("
|
|
4230
|
+
const recentDecisions = await mcpQuery("chain.listEntries", {
|
|
3631
4231
|
collectionSlug: wf.kbOutputCollection
|
|
3632
4232
|
});
|
|
3633
4233
|
const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
|
|
@@ -3716,23 +4316,9 @@ if (!process.env.CONVEX_SITE_URL && !process.env.PRODUCTBRAIN_API_KEY) {
|
|
|
3716
4316
|
} catch {
|
|
3717
4317
|
}
|
|
3718
4318
|
}
|
|
3719
|
-
|
|
4319
|
+
bootstrap();
|
|
3720
4320
|
var SERVER_VERSION = "2.0.0";
|
|
3721
4321
|
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
4322
|
var server = new McpServer2(
|
|
3737
4323
|
{
|
|
3738
4324
|
name: "Product Brain",
|
|
@@ -3745,28 +4331,39 @@ var server = new McpServer2(
|
|
|
3745
4331
|
"Terminology, standards, and core data all live here \u2014 no need to check external docs.",
|
|
3746
4332
|
"",
|
|
3747
4333
|
"Workflow:",
|
|
3748
|
-
" 1.
|
|
3749
|
-
" 2.
|
|
4334
|
+
" 1. Start: call `agent-start` to begin a tracked session.",
|
|
4335
|
+
" 2. Orient: call `orient` to load workspace context and unlock write tools.",
|
|
3750
4336
|
" 3. Discover: use `search` to find entries, or `list-entries` to browse.",
|
|
3751
4337
|
" 4. Drill in: use `get-entry` for full details \u2014 data, labels, relations, history.",
|
|
3752
4338
|
" 5. Context: use `gather-context` with an entryId or a task description.",
|
|
3753
4339
|
" 6. Capture: use `capture` to create entries \u2014 auto-links and scores in one call.",
|
|
3754
|
-
" 7.
|
|
3755
|
-
" 8.
|
|
3756
|
-
" 9.
|
|
3757
|
-
"
|
|
4340
|
+
" 7. Commit: use `commit-entry` to promote drafts to SSOT \u2014 only when the user confirms.",
|
|
4341
|
+
" 8. Connect: use `suggest-links` then `relate-entries` to build the graph.",
|
|
4342
|
+
" 9. Close: call `agent-close` when done \u2014 records session activity.",
|
|
4343
|
+
"",
|
|
4344
|
+
"Write tools (capture, update-entry, relate-entries, commit-entry) require:",
|
|
4345
|
+
" - An active agent session (call agent-start)",
|
|
4346
|
+
" - Completed orientation (call orient)",
|
|
4347
|
+
" - A readwrite API key scope",
|
|
3758
4348
|
"",
|
|
3759
|
-
"
|
|
4349
|
+
"Commit-on-confirm: always capture as draft first and show the user what was captured.",
|
|
4350
|
+
"Only call `commit-entry` when the user explicitly confirms (e.g. 'commit', 'looks good', 'yes').",
|
|
4351
|
+
"This builds trust \u2014 the Chain (main) is SSOT; nothing goes there without user consent.",
|
|
3760
4352
|
"",
|
|
3761
|
-
"
|
|
3762
|
-
"
|
|
3763
|
-
"
|
|
4353
|
+
"Workspace setup: use `create-collection` and `update-collection` to shape the workspace",
|
|
4354
|
+
"structure with the user. Ask what they need to track; presets are starting points, not fixed.",
|
|
4355
|
+
"",
|
|
4356
|
+
"Personalization: if you have context about the user from memory (prior work, recent",
|
|
4357
|
+
"conversations, team context), use it to personalize recommendations. For example,",
|
|
4358
|
+
"'Based on your recent pitch reviews, the gap most likely to matter is X.'",
|
|
4359
|
+
"The orient/start output gives you the workspace state; your memory fills in the human context."
|
|
3764
4360
|
].join("\n")
|
|
3765
4361
|
}
|
|
3766
4362
|
);
|
|
3767
4363
|
var enabledModules = new Set(
|
|
3768
|
-
(process.env.PB_MODULES ?? "core,gitchain").split(",").map((m) => m.trim().toLowerCase())
|
|
4364
|
+
(process.env.PB_MODULES ?? "core,gitchain,arch").split(",").map((m) => m.trim().toLowerCase())
|
|
3769
4365
|
);
|
|
4366
|
+
registerSessionTools(server);
|
|
3770
4367
|
registerKnowledgeTools(server);
|
|
3771
4368
|
registerLabelTools(server);
|
|
3772
4369
|
registerHealthTools(server);
|
|
@@ -3774,15 +4371,40 @@ registerVerifyTools(server);
|
|
|
3774
4371
|
registerSmartCaptureTools(server);
|
|
3775
4372
|
registerWorkflowTools(server);
|
|
3776
4373
|
if (enabledModules.has("gitchain")) registerGitChainTools(server);
|
|
4374
|
+
if (enabledModules.has("gitchain")) registerMapTools(server);
|
|
3777
4375
|
if (enabledModules.has("arch")) registerArchitectureTools(server);
|
|
4376
|
+
registerStartTools(server);
|
|
3778
4377
|
registerResources(server);
|
|
3779
4378
|
registerPrompts(server);
|
|
3780
4379
|
var transport = new StdioServerTransport();
|
|
3781
4380
|
await server.connect(transport);
|
|
4381
|
+
getWorkspaceId().then(async (wsId) => {
|
|
4382
|
+
trackSessionStarted(wsId, SERVER_VERSION);
|
|
4383
|
+
try {
|
|
4384
|
+
await startAgentSession();
|
|
4385
|
+
process.stderr.write("[MCP] Agent session started automatically.\n");
|
|
4386
|
+
} catch (err) {
|
|
4387
|
+
process.stderr.write(`[MCP] Auto session start failed: ${err.message}. Call agent-start manually.
|
|
4388
|
+
`);
|
|
4389
|
+
await recoverSessionState();
|
|
4390
|
+
}
|
|
4391
|
+
}).catch(() => {
|
|
4392
|
+
process.stderr.write("[MCP] Workspace resolution deferred \u2014 will retry on first tool call.\n");
|
|
4393
|
+
});
|
|
3782
4394
|
async function gracefulShutdown() {
|
|
4395
|
+
if (getAgentSessionId()) {
|
|
4396
|
+
await orphanAgentSession();
|
|
4397
|
+
}
|
|
3783
4398
|
await shutdownAnalytics();
|
|
3784
4399
|
process.exit(0);
|
|
3785
4400
|
}
|
|
3786
4401
|
process.on("SIGINT", gracefulShutdown);
|
|
3787
4402
|
process.on("SIGTERM", gracefulShutdown);
|
|
4403
|
+
process.stdin.on("end", async () => {
|
|
4404
|
+
if (getAgentSessionId()) {
|
|
4405
|
+
await orphanAgentSession();
|
|
4406
|
+
}
|
|
4407
|
+
await shutdownAnalytics();
|
|
4408
|
+
process.exit(0);
|
|
4409
|
+
});
|
|
3788
4410
|
//# sourceMappingURL=index.js.map
|