@productbrain/mcp 0.0.1-beta.4 → 0.0.1-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1 -1
- package/dist/index.js +637 -1094
- package/dist/index.js.map +1 -1
- package/dist/{setup-R6GG7752.js → setup-MJLPTQBL.js} +15 -5
- package/dist/setup-MJLPTQBL.js.map +1 -0
- package/package.json +1 -1
- package/dist/setup-R6GG7752.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -191,7 +191,7 @@ function registerKnowledgeTools(server2) {
|
|
|
191
191
|
"list-collections",
|
|
192
192
|
{
|
|
193
193
|
title: "Browse Collections",
|
|
194
|
-
description: "List every knowledge collection in the workspace \u2014 glossary, business rules, tracking events, standards, etc. Returns each collection's slug, name, description, and field schema. Start here before
|
|
194
|
+
description: "List every knowledge collection in the workspace \u2014 glossary, business rules, tracking events, standards, etc. Returns each collection's slug, name, description, and field schema. Start here before capture so you know which collections exist and what fields they expect.",
|
|
195
195
|
annotations: { readOnlyHint: true }
|
|
196
196
|
},
|
|
197
197
|
async () => {
|
|
@@ -260,7 +260,7 @@ ${formatted}` }]
|
|
|
260
260
|
"get-entry",
|
|
261
261
|
{
|
|
262
262
|
title: "Look Up Entry",
|
|
263
|
-
description: "Retrieve a single knowledge entry by its human-readable ID (e.g. 'T-SUPPLIER', 'BR-001', 'EVT-workspace_created'). Returns the full record: all data fields, labels, relations, and change history. Use
|
|
263
|
+
description: "Retrieve a single knowledge entry by its human-readable ID (e.g. 'T-SUPPLIER', 'BR-001', 'EVT-workspace_created'). Returns the full record: all data fields, labels, relations, and change history. Use search or list-entries first to discover entry IDs.",
|
|
264
264
|
inputSchema: {
|
|
265
265
|
entryId: z.string().describe("Entry ID, e.g. 'T-SUPPLIER', 'BR-001', 'EVT-workspace_created'")
|
|
266
266
|
},
|
|
@@ -269,7 +269,7 @@ ${formatted}` }]
|
|
|
269
269
|
async ({ entryId }) => {
|
|
270
270
|
const entry = await mcpQuery("kb.getEntry", { entryId });
|
|
271
271
|
if (!entry) {
|
|
272
|
-
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try
|
|
272
|
+
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
273
273
|
}
|
|
274
274
|
const lines = [
|
|
275
275
|
`# ${entry.entryId ? `${entry.entryId}: ` : ""}${entry.name}`,
|
|
@@ -307,81 +307,6 @@ ${formatted}` }]
|
|
|
307
307
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
308
308
|
}
|
|
309
309
|
);
|
|
310
|
-
const governedCollections = /* @__PURE__ */ new Set([
|
|
311
|
-
"glossary",
|
|
312
|
-
"business-rules",
|
|
313
|
-
"principles",
|
|
314
|
-
"standards",
|
|
315
|
-
"strategy"
|
|
316
|
-
]);
|
|
317
|
-
server2.registerTool(
|
|
318
|
-
"create-entry",
|
|
319
|
-
{
|
|
320
|
-
title: "Create Entry",
|
|
321
|
-
description: "Create a new knowledge entry. Provide the collection slug, a display name, status, and data matching the collection's field schema. Call list-collections first to see available collections and their field definitions. Governed collections (glossary, business-rules, principles, standards, strategy) require status 'draft' \u2014 to promote to 'active' or 'verified', raise a tension or use update-entry after approval.",
|
|
322
|
-
inputSchema: {
|
|
323
|
-
collection: z.string().describe("Collection slug, e.g. 'tracking-events', 'standards', 'glossary'"),
|
|
324
|
-
entryId: z.string().optional().describe("Human-readable ID, e.g. 'EVT-workspace_created', 'STD-posthog-events'"),
|
|
325
|
-
name: z.string().describe("Display name"),
|
|
326
|
-
status: z.string().default("draft").describe("Lifecycle status: draft | active | verified | deprecated"),
|
|
327
|
-
data: z.record(z.unknown()).describe("Data object \u2014 keys must match the collection's field definitions"),
|
|
328
|
-
order: z.number().optional().describe("Manual sort order within the collection")
|
|
329
|
-
},
|
|
330
|
-
annotations: { destructiveHint: false }
|
|
331
|
-
},
|
|
332
|
-
async ({ collection, entryId, name, status, data, order }) => {
|
|
333
|
-
if (governedCollections.has(collection) && status !== "draft" && status !== "deprecated") {
|
|
334
|
-
return {
|
|
335
|
-
content: [{
|
|
336
|
-
type: "text",
|
|
337
|
-
text: `# Governance Required
|
|
338
|
-
|
|
339
|
-
The \`${collection}\` collection is governed. New entries must be created with status \`draft\`.
|
|
340
|
-
|
|
341
|
-
**How to proceed:**
|
|
342
|
-
1. Create the entry with status \`draft\` (treated as a proposal)
|
|
343
|
-
2. Raise a tension in the \`tensions\` collection to request promotion
|
|
344
|
-
3. After approval, use \`update-entry\` to change status to \`active\` or \`verified\``
|
|
345
|
-
}]
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
try {
|
|
349
|
-
const id = await mcpMutation("kb.createEntry", {
|
|
350
|
-
collectionSlug: collection,
|
|
351
|
-
entryId,
|
|
352
|
-
name,
|
|
353
|
-
status,
|
|
354
|
-
data,
|
|
355
|
-
order
|
|
356
|
-
});
|
|
357
|
-
return {
|
|
358
|
-
content: [{ type: "text", text: `# Entry Created
|
|
359
|
-
|
|
360
|
-
**${entryId ?? name}** added to \`${collection}\` as \`${status}\`.
|
|
361
|
-
|
|
362
|
-
Internal ID: ${id}` }]
|
|
363
|
-
};
|
|
364
|
-
} catch (error) {
|
|
365
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
366
|
-
if (msg.includes("Duplicate entry") || msg.includes("already exists")) {
|
|
367
|
-
return {
|
|
368
|
-
content: [{
|
|
369
|
-
type: "text",
|
|
370
|
-
text: `# Cannot Create \u2014 Duplicate Detected
|
|
371
|
-
|
|
372
|
-
${msg}
|
|
373
|
-
|
|
374
|
-
**What to do:**
|
|
375
|
-
- Use \`get-entry\` to inspect the existing entry
|
|
376
|
-
- Use \`update-entry\` to modify it
|
|
377
|
-
- If a genuinely new entry is needed, raise a tension to propose it`
|
|
378
|
-
}]
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
throw error;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
);
|
|
385
310
|
server2.registerTool(
|
|
386
311
|
"update-entry",
|
|
387
312
|
{
|
|
@@ -426,7 +351,7 @@ Internal ID: ${id}` }]
|
|
|
426
351
|
Tension status (open, in-progress, closed) must be changed through the defined process, not via MCP.
|
|
427
352
|
|
|
428
353
|
**What you can do:**
|
|
429
|
-
- Create tensions: \`
|
|
354
|
+
- Create tensions: \`capture collection=tensions name="..." description="..."\`
|
|
430
355
|
- List tensions: \`list-entries collection=tensions\`
|
|
431
356
|
- Update non-status fields (raised, date, priority, description) via \`update-entry\`
|
|
432
357
|
- After process approval, a human uses the Product Brain UI to change status
|
|
@@ -440,10 +365,10 @@ Process criteria (TBD): e.g. 3+ users approved, or 7 days without valid concerns
|
|
|
440
365
|
}
|
|
441
366
|
);
|
|
442
367
|
server2.registerTool(
|
|
443
|
-
"
|
|
368
|
+
"search",
|
|
444
369
|
{
|
|
445
|
-
title: "Search
|
|
446
|
-
description: "Full-text search across all
|
|
370
|
+
title: "Search the Chain",
|
|
371
|
+
description: "Full-text search across all entries on the Chain. Returns entry names, collection, status, and a description preview. Scope results to a specific collection (e.g. collection='business-rules') or filter by status (e.g. status='active'). Use this to discover entries before calling get-entry for full details.",
|
|
447
372
|
inputSchema: {
|
|
448
373
|
query: z.string().describe("Search text (min 2 characters)"),
|
|
449
374
|
collection: z.string().optional().describe("Scope to a collection slug, e.g. 'business-rules', 'glossary', 'tracking-events'"),
|
|
@@ -564,7 +489,7 @@ ${formatted}` }]
|
|
|
564
489
|
}
|
|
565
490
|
const sourceEntry = await mcpQuery("kb.getEntry", { entryId });
|
|
566
491
|
if (!sourceEntry) {
|
|
567
|
-
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try
|
|
492
|
+
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
568
493
|
}
|
|
569
494
|
const sourceInternalId = sourceEntry._id;
|
|
570
495
|
const MAX_RELATIONS = 25;
|
|
@@ -623,18 +548,113 @@ ${formatted}` }]
|
|
|
623
548
|
server2.registerTool(
|
|
624
549
|
"gather-context",
|
|
625
550
|
{
|
|
626
|
-
title: "Gather
|
|
627
|
-
description: "Assemble
|
|
551
|
+
title: "Gather Context",
|
|
552
|
+
description: "Assemble knowledge context in one call. Two 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.\n\nUse mode 1 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.",
|
|
628
553
|
inputSchema: {
|
|
629
|
-
entryId: z.string().describe("Entry ID, e.g. 'FEAT-001', 'GT-019'
|
|
630
|
-
|
|
554
|
+
entryId: z.string().optional().describe("Entry ID for graph traversal, e.g. 'FEAT-001', 'GT-019'"),
|
|
555
|
+
task: z.string().optional().describe("Natural-language task description for auto-loading relevant context"),
|
|
556
|
+
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
|
+
maxResults: z.number().min(1).max(25).default(10).optional().describe("Max entries to return in task mode (default 10)")
|
|
631
558
|
},
|
|
632
559
|
annotations: { readOnlyHint: true }
|
|
633
560
|
},
|
|
634
|
-
async ({ entryId, maxHops }) => {
|
|
561
|
+
async ({ entryId, task, maxHops, maxResults }) => {
|
|
562
|
+
if (!entryId && !task) {
|
|
563
|
+
return { content: [{ type: "text", text: "Provide either `entryId` (graph traversal) or `task` (auto-load context for a task)." }] };
|
|
564
|
+
}
|
|
565
|
+
if (task && !entryId) {
|
|
566
|
+
await server2.sendLoggingMessage({
|
|
567
|
+
level: "info",
|
|
568
|
+
data: `Loading context for task: "${task.substring(0, 80)}..."`,
|
|
569
|
+
logger: "product-brain"
|
|
570
|
+
});
|
|
571
|
+
const searchTerms = task.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 12).join(" ");
|
|
572
|
+
const searchResults = await mcpQuery("kb.searchEntries", {
|
|
573
|
+
query: searchTerms
|
|
574
|
+
});
|
|
575
|
+
if (!searchResults || searchResults.length === 0) {
|
|
576
|
+
return {
|
|
577
|
+
content: [{
|
|
578
|
+
type: "text",
|
|
579
|
+
text: `# Context Loaded
|
|
580
|
+
|
|
581
|
+
**Confidence:** None
|
|
582
|
+
|
|
583
|
+
No context found for this task. The chain may not cover this area yet.
|
|
584
|
+
|
|
585
|
+
_Consider capturing domain knowledge discovered during this task via \`capture\`._`
|
|
586
|
+
}]
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const limit = maxResults ?? 10;
|
|
590
|
+
const topHits = searchResults.slice(0, Math.min(limit, 5));
|
|
591
|
+
const allEntries = [];
|
|
592
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
593
|
+
for (const hit of topHits) {
|
|
594
|
+
const hitId = hit.entryId ?? hit._id;
|
|
595
|
+
if (!hitId || seenIds.has(hitId)) continue;
|
|
596
|
+
seenIds.add(hitId);
|
|
597
|
+
allEntries.push({
|
|
598
|
+
entryId: hit.entryId,
|
|
599
|
+
name: hit.name ?? "Untitled",
|
|
600
|
+
collectionName: hit.collectionName ?? hit.collectionSlug ?? "unknown",
|
|
601
|
+
hop: 0
|
|
602
|
+
});
|
|
603
|
+
if (hit.entryId) {
|
|
604
|
+
try {
|
|
605
|
+
const graph = await mcpQuery("kb.gatherContext", {
|
|
606
|
+
entryId: hit.entryId,
|
|
607
|
+
maxHops: maxHops ?? 2
|
|
608
|
+
});
|
|
609
|
+
if (graph?.related) {
|
|
610
|
+
for (const rel of graph.related) {
|
|
611
|
+
const relId = rel.entryId ?? rel._id;
|
|
612
|
+
if (relId && !seenIds.has(relId)) {
|
|
613
|
+
seenIds.add(relId);
|
|
614
|
+
allEntries.push({
|
|
615
|
+
entryId: rel.entryId,
|
|
616
|
+
name: rel.name ?? "Untitled",
|
|
617
|
+
collectionName: rel.collectionName ?? "unknown",
|
|
618
|
+
hop: rel.hop ?? 1,
|
|
619
|
+
relationType: rel.relationType
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (allEntries.length >= limit) break;
|
|
628
|
+
}
|
|
629
|
+
const trimmed = allEntries.slice(0, limit);
|
|
630
|
+
const confidence = trimmed.length >= 5 ? "high" : trimmed.length >= 2 ? "medium" : "low";
|
|
631
|
+
const byCollection2 = /* @__PURE__ */ new Map();
|
|
632
|
+
for (const entry of trimmed) {
|
|
633
|
+
const key = entry.collectionName;
|
|
634
|
+
if (!byCollection2.has(key)) byCollection2.set(key, []);
|
|
635
|
+
byCollection2.get(key).push(entry);
|
|
636
|
+
}
|
|
637
|
+
const lines2 = [
|
|
638
|
+
`# Context Loaded`,
|
|
639
|
+
`**Confidence:** ${confidence.charAt(0).toUpperCase() + confidence.slice(1)}`,
|
|
640
|
+
`**Matched:** ${trimmed.length} entries across ${byCollection2.size} collection${byCollection2.size === 1 ? "" : "s"}`,
|
|
641
|
+
""
|
|
642
|
+
];
|
|
643
|
+
for (const [collName, entries] of byCollection2) {
|
|
644
|
+
lines2.push(`### ${collName} (${entries.length})`);
|
|
645
|
+
for (const e of entries) {
|
|
646
|
+
const id = e.entryId ? `**${e.entryId}:** ` : "";
|
|
647
|
+
const hopLabel = e.hop > 0 ? ` _(hop ${e.hop}${e.relationType ? `, ${e.relationType}` : ""})_` : "";
|
|
648
|
+
lines2.push(`- ${id}${e.name}${hopLabel}`);
|
|
649
|
+
}
|
|
650
|
+
lines2.push("");
|
|
651
|
+
}
|
|
652
|
+
lines2.push(`_Use \`get-entry\` for full details on any entry._`);
|
|
653
|
+
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
654
|
+
}
|
|
635
655
|
const result = await mcpQuery("kb.gatherContext", { entryId, maxHops });
|
|
636
656
|
if (!result?.root) {
|
|
637
|
-
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try
|
|
657
|
+
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
638
658
|
}
|
|
639
659
|
if (result.related.length === 0) {
|
|
640
660
|
return {
|
|
@@ -676,7 +696,7 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
|
|
|
676
696
|
"suggest-links",
|
|
677
697
|
{
|
|
678
698
|
title: "Suggest Links",
|
|
679
|
-
description: "Discover potential connections for an entry by scanning the
|
|
699
|
+
description: "Discover potential connections for an entry by scanning the chain for related content. Returns ranked suggestions based on text similarity \u2014 review them and use relate-entries to create the ones that make sense.\n\nThis is a discovery tool, not auto-linking. Always review suggestions before linking.",
|
|
680
700
|
inputSchema: {
|
|
681
701
|
entryId: z.string().describe("Entry ID to find suggestions for, e.g. 'FEAT-001'"),
|
|
682
702
|
limit: z.number().min(1).max(20).default(10).describe("Max number of suggestions to return")
|
|
@@ -686,7 +706,7 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
|
|
|
686
706
|
async ({ entryId, limit }) => {
|
|
687
707
|
const entry = await mcpQuery("kb.getEntry", { entryId });
|
|
688
708
|
if (!entry) {
|
|
689
|
-
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try
|
|
709
|
+
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
|
|
690
710
|
}
|
|
691
711
|
const searchTerms = [entry.name];
|
|
692
712
|
if (entry.data?.description) searchTerms.push(entry.data.description);
|
|
@@ -699,7 +719,7 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
|
|
|
699
719
|
}
|
|
700
720
|
const results = await mcpQuery("kb.searchEntries", { query: queryText });
|
|
701
721
|
if (!results || results.length === 0) {
|
|
702
|
-
return { content: [{ type: "text", text: `No suggestions found for \`${entryId}\`. The
|
|
722
|
+
return { content: [{ type: "text", text: `No suggestions found for \`${entryId}\`. The chain may need more entries.` }] };
|
|
703
723
|
}
|
|
704
724
|
const existingRelations = await mcpQuery("kb.listEntryRelations", { entryId });
|
|
705
725
|
const relatedIds = new Set(
|
|
@@ -734,273 +754,59 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
|
|
|
734
754
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
735
755
|
}
|
|
736
756
|
);
|
|
737
|
-
server2.registerTool(
|
|
738
|
-
"quick-capture",
|
|
739
|
-
{
|
|
740
|
-
title: "Quick Capture",
|
|
741
|
-
description: "Quickly capture a knowledge entry with just a name and description \u2014 no need to look up the full field schema first. Always creates as 'draft' status with sensible defaults for remaining fields. Use update-entry later to fill in details. Embodies 'Capture Now, Curate Later' (PRI-81cbdq).",
|
|
742
|
-
inputSchema: {
|
|
743
|
-
collection: z.string().describe("Collection slug, e.g. 'business-rules', 'glossary', 'tensions', 'decisions'"),
|
|
744
|
-
name: z.string().describe("Display name for the entry"),
|
|
745
|
-
description: z.string().describe("Short description \u2014 the essential context to capture now"),
|
|
746
|
-
entryId: z.string().optional().describe("Optional human-readable ID (e.g. 'SOS-020', 'GT-031')")
|
|
747
|
-
},
|
|
748
|
-
annotations: { destructiveHint: false }
|
|
749
|
-
},
|
|
750
|
-
async ({ collection, name, description, entryId }) => {
|
|
751
|
-
const col = await mcpQuery("kb.getCollection", { slug: collection });
|
|
752
|
-
if (!col) {
|
|
753
|
-
return { content: [{ type: "text", text: `Collection \`${collection}\` not found. Use list-collections to see available collections.` }] };
|
|
754
|
-
}
|
|
755
|
-
const data = {};
|
|
756
|
-
const emptyFields = [];
|
|
757
|
-
for (const field of col.fields ?? []) {
|
|
758
|
-
const key = field.key;
|
|
759
|
-
if (key === "description" || key === "canonical" || key === "detail") {
|
|
760
|
-
data[key] = description;
|
|
761
|
-
} else if (field.type === "array" || field.type === "multi-select") {
|
|
762
|
-
data[key] = [];
|
|
763
|
-
emptyFields.push(key);
|
|
764
|
-
} else if (field.type === "select") {
|
|
765
|
-
data[key] = field.options?.[0] ?? "";
|
|
766
|
-
emptyFields.push(key);
|
|
767
|
-
} else {
|
|
768
|
-
data[key] = "";
|
|
769
|
-
emptyFields.push(key);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
if (!data.description && !data.canonical && !data.detail) {
|
|
773
|
-
data.description = description;
|
|
774
|
-
}
|
|
775
|
-
try {
|
|
776
|
-
const id = await mcpMutation("kb.createEntry", {
|
|
777
|
-
collectionSlug: collection,
|
|
778
|
-
entryId,
|
|
779
|
-
name,
|
|
780
|
-
status: "draft",
|
|
781
|
-
data
|
|
782
|
-
});
|
|
783
|
-
const emptyNote = emptyFields.length > 0 ? `
|
|
784
|
-
|
|
785
|
-
**Fields to fill later** (via \`update-entry\`):
|
|
786
|
-
${emptyFields.map((f) => `- \`${f}\``).join("\n")}` : "";
|
|
787
|
-
return {
|
|
788
|
-
content: [{
|
|
789
|
-
type: "text",
|
|
790
|
-
text: `# Quick Capture \u2014 Done
|
|
791
|
-
|
|
792
|
-
**${entryId ?? name}** added to \`${collection}\` as \`draft\`.
|
|
793
|
-
|
|
794
|
-
Internal ID: ${id}${emptyNote}
|
|
795
|
-
|
|
796
|
-
_Use \`update-entry\` to fill in details when ready. Use \`get-entry\` to review._`
|
|
797
|
-
}]
|
|
798
|
-
};
|
|
799
|
-
} catch (error) {
|
|
800
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
801
|
-
if (msg.includes("Duplicate") || msg.includes("already exists")) {
|
|
802
|
-
return {
|
|
803
|
-
content: [{
|
|
804
|
-
type: "text",
|
|
805
|
-
text: `# Cannot Capture \u2014 Duplicate Detected
|
|
806
|
-
|
|
807
|
-
${msg}
|
|
808
|
-
|
|
809
|
-
Use \`get-entry\` to inspect the existing entry, or \`update-entry\` to modify it.`
|
|
810
|
-
}]
|
|
811
|
-
};
|
|
812
|
-
}
|
|
813
|
-
throw error;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
);
|
|
817
|
-
server2.registerTool(
|
|
818
|
-
"load-context-for-task",
|
|
819
|
-
{
|
|
820
|
-
title: "Load Context for Task",
|
|
821
|
-
description: "Auto-load relevant domain knowledge for a task in a single call. Pass a natural-language task description; the tool searches the KB, traverses the knowledge graph, and returns a ranked set of entries (business rules, glossary terms, decisions, features, etc.) grouped by collection with a confidence score.\n\nUse this at the start of a conversation to ground the agent in domain context before writing code or making recommendations.\n\nConfidence levels:\n- high: 3+ direct KB matches \u2014 strong domain coverage\n- medium: 1-2 direct matches \u2014 partial coverage, may want to drill deeper\n- low: no direct matches but related entries found via graph traversal\n- none: no relevant entries found \u2014 KB may not cover this area yet",
|
|
822
|
-
inputSchema: {
|
|
823
|
-
taskDescription: z.string().describe("Natural-language description of the task or user message"),
|
|
824
|
-
maxResults: z.number().min(1).max(25).default(10).optional().describe("Max entries to return (default 10)"),
|
|
825
|
-
maxHops: z.number().min(1).max(3).default(2).optional().describe("Graph traversal depth from each search hit (default 2)")
|
|
826
|
-
},
|
|
827
|
-
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
828
|
-
},
|
|
829
|
-
async ({ taskDescription, maxResults, maxHops }) => {
|
|
830
|
-
await server2.sendLoggingMessage({
|
|
831
|
-
level: "info",
|
|
832
|
-
data: `Loading context for task: "${taskDescription.substring(0, 80)}..."`,
|
|
833
|
-
logger: "product-brain"
|
|
834
|
-
});
|
|
835
|
-
const result = await mcpQuery("kb.loadContextForTask", {
|
|
836
|
-
taskDescription,
|
|
837
|
-
maxResults: maxResults ?? 10,
|
|
838
|
-
maxHops: maxHops ?? 2
|
|
839
|
-
});
|
|
840
|
-
if (result.confidence === "none" || result.entries.length === 0) {
|
|
841
|
-
return {
|
|
842
|
-
content: [{
|
|
843
|
-
type: "text",
|
|
844
|
-
text: `# Context Loaded
|
|
845
|
-
|
|
846
|
-
**Confidence:** None
|
|
847
|
-
|
|
848
|
-
No KB context found for this task. The knowledge base may not cover this area yet.
|
|
849
|
-
|
|
850
|
-
_Consider capturing domain knowledge discovered during this task via \`smart-capture\`._`
|
|
851
|
-
}]
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
const byCollection = /* @__PURE__ */ new Map();
|
|
855
|
-
for (const entry of result.entries) {
|
|
856
|
-
const key = entry.collectionName;
|
|
857
|
-
if (!byCollection.has(key)) byCollection.set(key, []);
|
|
858
|
-
byCollection.get(key).push(entry);
|
|
859
|
-
}
|
|
860
|
-
const lines = [
|
|
861
|
-
`# Context Loaded`,
|
|
862
|
-
`**Confidence:** ${result.confidence.charAt(0).toUpperCase() + result.confidence.slice(1)}`,
|
|
863
|
-
`**Matched:** ${result.entries.length} entries across ${byCollection.size} collection${byCollection.size === 1 ? "" : "s"}`,
|
|
864
|
-
""
|
|
865
|
-
];
|
|
866
|
-
for (const [collName, entries] of byCollection) {
|
|
867
|
-
lines.push(`### ${collName} (${entries.length})`);
|
|
868
|
-
for (const e of entries) {
|
|
869
|
-
const id = e.entryId ? `**${e.entryId}:** ` : "";
|
|
870
|
-
const hopLabel = e.hop > 0 ? ` _(hop ${e.hop}${e.relationType ? `, ${e.relationType}` : ""})_` : "";
|
|
871
|
-
const preview = e.descriptionPreview ? `
|
|
872
|
-
${e.descriptionPreview}` : "";
|
|
873
|
-
const codePaths = e.codePaths.length > 0 ? `
|
|
874
|
-
Code: ${e.codePaths.join(", ")}` : "";
|
|
875
|
-
lines.push(`- ${id}${e.name}${hopLabel}${preview}${codePaths}`);
|
|
876
|
-
}
|
|
877
|
-
lines.push("");
|
|
878
|
-
}
|
|
879
|
-
lines.push(`_Use \`get-entry\` for full details on any entry._`);
|
|
880
|
-
return {
|
|
881
|
-
content: [{ type: "text", text: lines.join("\n") }]
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
);
|
|
885
|
-
server2.registerTool(
|
|
886
|
-
"review-rules",
|
|
887
|
-
{
|
|
888
|
-
title: "Review Business Rules",
|
|
889
|
-
description: "Surface all active business rules for a domain, formatted for compliance review. Use when reviewing code, designs, or decisions against Product Brain governance. Optionally provide context (what you're building or reviewing) to help focus the review. This is the tool form of the review-against-rules prompt.",
|
|
890
|
-
inputSchema: {
|
|
891
|
-
domain: z.string().describe("Business rule domain, e.g. 'AI & MCP Integration', 'Governance & Decision-Making'"),
|
|
892
|
-
context: z.string().optional().describe("What you're reviewing \u2014 code change, design decision, file path, etc.")
|
|
893
|
-
},
|
|
894
|
-
annotations: { readOnlyHint: true }
|
|
895
|
-
},
|
|
896
|
-
async ({ domain, context }) => {
|
|
897
|
-
const entries = await mcpQuery("kb.listEntries", { collectionSlug: "business-rules" });
|
|
898
|
-
const domainLower = domain.toLowerCase();
|
|
899
|
-
const rules = entries.filter((e) => {
|
|
900
|
-
const ruleDomain = e.data?.domain ?? "";
|
|
901
|
-
return ruleDomain.toLowerCase() === domainLower || ruleDomain.toLowerCase().includes(domainLower);
|
|
902
|
-
});
|
|
903
|
-
if (rules.length === 0) {
|
|
904
|
-
const allDomains = [...new Set(entries.map((e) => e.data?.domain).filter(Boolean))];
|
|
905
|
-
return {
|
|
906
|
-
content: [{
|
|
907
|
-
type: "text",
|
|
908
|
-
text: `# No Rules Found for "${domain}"
|
|
909
|
-
|
|
910
|
-
Available domains:
|
|
911
|
-
${allDomains.map((d) => `- ${d}`).join("\n")}
|
|
912
|
-
|
|
913
|
-
_Try one of the domains above, or use kb-search to find rules by keyword._`
|
|
914
|
-
}]
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
const header = context ? `# Business Rules: ${domain}
|
|
918
|
-
|
|
919
|
-
**Review context:** ${context}
|
|
920
|
-
|
|
921
|
-
For each rule, assess: compliant, at risk, violation, or not applicable.
|
|
922
|
-
` : `# Business Rules: ${domain}
|
|
923
|
-
`;
|
|
924
|
-
const formatted = rules.map((r) => {
|
|
925
|
-
const id = r.entryId ? `**${r.entryId}:** ` : "";
|
|
926
|
-
const severity = r.data?.severity ? ` | Severity: ${r.data.severity}` : "";
|
|
927
|
-
const desc = r.data?.description ?? "";
|
|
928
|
-
const impact = r.data?.dataImpact ? `
|
|
929
|
-
Data impact: ${r.data.dataImpact}` : "";
|
|
930
|
-
const related = (r.data?.relatedRules ?? []).length > 0 ? `
|
|
931
|
-
Related: ${r.data.relatedRules.join(", ")}` : "";
|
|
932
|
-
return `### ${id}${r.name} \`${r.status}\`${severity}
|
|
933
|
-
|
|
934
|
-
${desc}${impact}${related}`;
|
|
935
|
-
}).join("\n\n---\n\n");
|
|
936
|
-
const footer = `
|
|
937
|
-
_${rules.length} rule${rules.length === 1 ? "" : "s"} in this domain. Use \`get-entry\` for full details. Use the \`draft-rule-from-context\` prompt to propose new rules._`;
|
|
938
|
-
return {
|
|
939
|
-
content: [{ type: "text", text: `${header}
|
|
940
|
-
${formatted}
|
|
941
|
-
${footer}` }]
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
);
|
|
945
757
|
}
|
|
946
758
|
|
|
947
759
|
// src/tools/labels.ts
|
|
948
760
|
import { z as z2 } from "zod";
|
|
949
761
|
function registerLabelTools(server2) {
|
|
950
762
|
server2.registerTool(
|
|
951
|
-
"
|
|
952
|
-
{
|
|
953
|
-
title: "Browse Labels",
|
|
954
|
-
description: "List all workspace labels with their groups and hierarchy. Labels can be applied to any entry across any collection for cross-domain filtering. Similar to labels in Linear or GitHub.",
|
|
955
|
-
annotations: { readOnlyHint: true }
|
|
956
|
-
},
|
|
957
|
-
async () => {
|
|
958
|
-
const labels = await mcpQuery("kb.listLabels");
|
|
959
|
-
if (labels.length === 0) {
|
|
960
|
-
return { content: [{ type: "text", text: "No labels defined in this workspace yet." }] };
|
|
961
|
-
}
|
|
962
|
-
const groups = labels.filter((l) => l.isGroup);
|
|
963
|
-
const ungrouped = labels.filter((l) => !l.isGroup && !l.parentId);
|
|
964
|
-
const children = (parentId) => labels.filter((l) => l.parentId === parentId);
|
|
965
|
-
const lines = ["# Workspace Labels"];
|
|
966
|
-
for (const group of groups) {
|
|
967
|
-
lines.push(`
|
|
968
|
-
## ${group.name}`);
|
|
969
|
-
if (group.description) lines.push(`_${group.description}_`);
|
|
970
|
-
for (const child of children(group._id)) {
|
|
971
|
-
const color = child.color ? ` ${child.color}` : "";
|
|
972
|
-
lines.push(` - \`${child.slug}\` ${child.name}${color}`);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
if (ungrouped.length > 0) {
|
|
976
|
-
lines.push("\n## Ungrouped");
|
|
977
|
-
for (const label of ungrouped) {
|
|
978
|
-
const color = label.color ? ` ${label.color}` : "";
|
|
979
|
-
lines.push(`- \`${label.slug}\` ${label.name}${color}${label.description ? ` \u2014 _${label.description}_` : ""}`);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
return {
|
|
983
|
-
content: [{ type: "text", text: lines.join("\n") }]
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
);
|
|
987
|
-
server2.registerTool(
|
|
988
|
-
"manage-labels",
|
|
763
|
+
"labels",
|
|
989
764
|
{
|
|
990
|
-
title: "
|
|
991
|
-
description: "
|
|
765
|
+
title: "Labels",
|
|
766
|
+
description: "Manage workspace labels \u2014 list, create, update, delete, apply to entries, or remove from entries. Labels can be applied to any entry across any collection for cross-domain filtering. Similar to labels in Linear or GitHub. Labels support hierarchy (groups with children).",
|
|
992
767
|
inputSchema: {
|
|
993
|
-
action: z2.enum(["create", "update", "delete"]).describe("
|
|
994
|
-
slug: z2.string().describe("Label slug (
|
|
768
|
+
action: z2.enum(["list", "create", "update", "delete", "apply", "remove"]).describe("Action: list all labels, create/update/delete a label, or apply/remove a label on an entry"),
|
|
769
|
+
slug: z2.string().optional().describe("Label slug (required for create/update/delete/apply/remove)"),
|
|
995
770
|
name: z2.string().optional().describe("Display name (required for create)"),
|
|
996
771
|
color: z2.string().optional().describe("Hex color, e.g. '#ef4444'"),
|
|
997
772
|
description: z2.string().optional().describe("What this label means"),
|
|
998
773
|
parentSlug: z2.string().optional().describe("Parent group slug for label hierarchy"),
|
|
999
774
|
isGroup: z2.boolean().optional().describe("True if this is a group container, not a taggable label"),
|
|
1000
|
-
order: z2.number().optional().describe("Sort order within its group")
|
|
775
|
+
order: z2.number().optional().describe("Sort order within its group"),
|
|
776
|
+
entryId: z2.string().optional().describe("Entry ID for apply/remove actions")
|
|
1001
777
|
}
|
|
1002
778
|
},
|
|
1003
|
-
async ({ action, slug, name, color, description, parentSlug, isGroup, order }) => {
|
|
779
|
+
async ({ action, slug, name, color, description, parentSlug, isGroup, order, entryId }) => {
|
|
780
|
+
if (action === "list") {
|
|
781
|
+
const labels = await mcpQuery("kb.listLabels");
|
|
782
|
+
if (labels.length === 0) {
|
|
783
|
+
return { content: [{ type: "text", text: "No labels defined in this workspace yet." }] };
|
|
784
|
+
}
|
|
785
|
+
const groups = labels.filter((l) => l.isGroup);
|
|
786
|
+
const ungrouped = labels.filter((l) => !l.isGroup && !l.parentId);
|
|
787
|
+
const children = (parentId) => labels.filter((l) => l.parentId === parentId);
|
|
788
|
+
const lines = ["# Workspace Labels"];
|
|
789
|
+
for (const group of groups) {
|
|
790
|
+
lines.push(`
|
|
791
|
+
## ${group.name}`);
|
|
792
|
+
if (group.description) lines.push(`_${group.description}_`);
|
|
793
|
+
for (const child of children(group._id)) {
|
|
794
|
+
const c = child.color ? ` ${child.color}` : "";
|
|
795
|
+
lines.push(` - \`${child.slug}\` ${child.name}${c}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (ungrouped.length > 0) {
|
|
799
|
+
lines.push("\n## Ungrouped");
|
|
800
|
+
for (const label of ungrouped) {
|
|
801
|
+
const c = label.color ? ` ${label.color}` : "";
|
|
802
|
+
lines.push(`- \`${label.slug}\` ${label.name}${c}${label.description ? ` \u2014 _${label.description}_` : ""}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
806
|
+
}
|
|
807
|
+
if (!slug) {
|
|
808
|
+
return { content: [{ type: "text", text: "A `slug` is required for this action." }] };
|
|
809
|
+
}
|
|
1004
810
|
if (action === "create") {
|
|
1005
811
|
if (!name) {
|
|
1006
812
|
return { content: [{ type: "text", text: "Cannot create a label without a name." }] };
|
|
@@ -1010,7 +816,7 @@ function registerLabelTools(server2) {
|
|
|
1010
816
|
const labels = await mcpQuery("kb.listLabels");
|
|
1011
817
|
const parent = labels.find((l) => l.slug === parentSlug);
|
|
1012
818
|
if (!parent) {
|
|
1013
|
-
return { content: [{ type: "text", text: `Parent label \`${parentSlug}\` not found. Use
|
|
819
|
+
return { content: [{ type: "text", text: `Parent label \`${parentSlug}\` not found. Use \`labels action=list\` to see available groups.` }] };
|
|
1014
820
|
}
|
|
1015
821
|
parentId = parent._id;
|
|
1016
822
|
}
|
|
@@ -1031,27 +837,18 @@ function registerLabelTools(server2) {
|
|
|
1031
837
|
|
|
1032
838
|
\`${slug}\` removed from all entries and deleted.` }] };
|
|
1033
839
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
entryId: z2.string().describe("Entry ID, e.g. 'T-SUPPLIER', 'EVT-workspace_created'"),
|
|
1045
|
-
label: z2.string().describe("Label slug to apply/remove")
|
|
1046
|
-
}
|
|
1047
|
-
},
|
|
1048
|
-
async ({ action, entryId, label }) => {
|
|
1049
|
-
if (action === "apply") {
|
|
1050
|
-
await mcpMutation("kb.applyLabel", { entryId, labelSlug: label });
|
|
1051
|
-
return { content: [{ type: "text", text: `Label \`${label}\` applied to **${entryId}**.` }] };
|
|
840
|
+
if (action === "apply" || action === "remove") {
|
|
841
|
+
if (!entryId) {
|
|
842
|
+
return { content: [{ type: "text", text: "An `entryId` is required for apply/remove actions." }] };
|
|
843
|
+
}
|
|
844
|
+
if (action === "apply") {
|
|
845
|
+
await mcpMutation("kb.applyLabel", { entryId, labelSlug: slug });
|
|
846
|
+
return { content: [{ type: "text", text: `Label \`${slug}\` applied to **${entryId}**.` }] };
|
|
847
|
+
}
|
|
848
|
+
await mcpMutation("kb.removeLabel", { entryId, labelSlug: slug });
|
|
849
|
+
return { content: [{ type: "text", text: `Label \`${slug}\` removed from **${entryId}**.` }] };
|
|
1052
850
|
}
|
|
1053
|
-
|
|
1054
|
-
return { content: [{ type: "text", text: `Label \`${label}\` removed from **${entryId}**.` }] };
|
|
851
|
+
return { content: [{ type: "text", text: "Unknown action." }] };
|
|
1055
852
|
}
|
|
1056
853
|
);
|
|
1057
854
|
}
|
|
@@ -1185,7 +982,7 @@ function registerHealthTools(server2) {
|
|
|
1185
982
|
"mcp-audit",
|
|
1186
983
|
{
|
|
1187
984
|
title: "Session Audit Log",
|
|
1188
|
-
description: "Show a session summary (reads, writes, searches, contributions) and the last N backend calls. Useful for debugging, tracing tool behavior, and seeing what you contributed to the
|
|
985
|
+
description: "Show a session summary (reads, writes, searches, contributions) and the last N backend calls. Useful for debugging, tracing tool behavior, and seeing what you contributed to the chain.",
|
|
1189
986
|
inputSchema: {
|
|
1190
987
|
limit: z3.number().min(1).max(50).default(20).describe("How many recent calls to show (max 50)")
|
|
1191
988
|
},
|
|
@@ -1367,7 +1164,7 @@ function registerVerifyTools(server2) {
|
|
|
1367
1164
|
server2.registerTool(
|
|
1368
1165
|
"verify",
|
|
1369
1166
|
{
|
|
1370
|
-
title: "Verify
|
|
1167
|
+
title: "Verify the Chain",
|
|
1371
1168
|
description: "Verify knowledge entries against the actual codebase. Checks glossary code mappings (do referenced files and schema fields still exist?) and validates cross-references (do relatedRules point to real entries?). Produces a trust report with a trust score. Use mode='fix' to auto-update drifted codeMapping statuses.",
|
|
1372
1169
|
inputSchema: {
|
|
1373
1170
|
collection: z4.string().default("glossary").describe("Collection slug to verify (default: glossary)"),
|
|
@@ -1490,7 +1287,7 @@ function registerVerifyTools(server2) {
|
|
|
1490
1287
|
import { z as z5 } from "zod";
|
|
1491
1288
|
var AREA_KEYWORDS = {
|
|
1492
1289
|
"Architecture": ["convex", "schema", "database", "migration", "api", "backend", "infrastructure", "scaling", "performance"],
|
|
1493
|
-
"
|
|
1290
|
+
"Chain": ["knowledge", "glossary", "entry", "collection", "terminology", "drift", "graph", "chain", "commit"],
|
|
1494
1291
|
"AI & MCP Integration": ["mcp", "ai", "cursor", "agent", "tool", "llm", "prompt", "context"],
|
|
1495
1292
|
"Developer Experience": ["dx", "developer", "ide", "workflow", "friction", "ceremony"],
|
|
1496
1293
|
"Governance & Decision-Making": ["governance", "decision", "rule", "policy", "compliance", "approval"],
|
|
@@ -1530,7 +1327,7 @@ var COMMON_CHECKS = {
|
|
|
1530
1327
|
id: "has-relations",
|
|
1531
1328
|
label: "At least 1 relation created",
|
|
1532
1329
|
check: (ctx) => ctx.linksCreated.length >= 1,
|
|
1533
|
-
suggestion: () => "Use `suggest-links` and `relate-entries` to
|
|
1330
|
+
suggestion: () => "Use `suggest-links` and `relate-entries` to add more connections."
|
|
1534
1331
|
},
|
|
1535
1332
|
diverseRelations: {
|
|
1536
1333
|
id: "diverse-relations",
|
|
@@ -1644,7 +1441,7 @@ var PROFILES = /* @__PURE__ */ new Map([
|
|
|
1644
1441
|
if (area) {
|
|
1645
1442
|
const categoryMap = {
|
|
1646
1443
|
"Architecture": "Platform & Architecture",
|
|
1647
|
-
"
|
|
1444
|
+
"Chain": "Knowledge Management",
|
|
1648
1445
|
"AI & MCP Integration": "AI & Developer Tools",
|
|
1649
1446
|
"Developer Experience": "AI & Developer Tools",
|
|
1650
1447
|
"Governance & Decision-Making": "Governance & Process",
|
|
@@ -1836,7 +1633,7 @@ async function checkEntryQuality(entryId) {
|
|
|
1836
1633
|
const entry = await mcpQuery("kb.getEntry", { entryId });
|
|
1837
1634
|
if (!entry) {
|
|
1838
1635
|
return {
|
|
1839
|
-
text: `Entry \`${entryId}\` not found. Try
|
|
1636
|
+
text: `Entry \`${entryId}\` not found. Try search to find the right ID.`,
|
|
1840
1637
|
quality: { score: 0, maxScore: 10, checks: [] }
|
|
1841
1638
|
};
|
|
1842
1639
|
}
|
|
@@ -1897,10 +1694,10 @@ var MAX_AUTO_LINKS = 5;
|
|
|
1897
1694
|
var MAX_SUGGESTIONS = 5;
|
|
1898
1695
|
function registerSmartCaptureTools(server2) {
|
|
1899
1696
|
server2.registerTool(
|
|
1900
|
-
"
|
|
1697
|
+
"capture",
|
|
1901
1698
|
{
|
|
1902
|
-
title: "
|
|
1903
|
-
description: "
|
|
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.",
|
|
1904
1701
|
inputSchema: {
|
|
1905
1702
|
collection: z5.string().describe("Collection slug, e.g. 'tensions', 'business-rules', 'glossary', 'decisions'"),
|
|
1906
1703
|
name: z5.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
|
|
@@ -1968,7 +1765,7 @@ function registerSmartCaptureTools(server2) {
|
|
|
1968
1765
|
name,
|
|
1969
1766
|
status,
|
|
1970
1767
|
data,
|
|
1971
|
-
createdBy: "
|
|
1768
|
+
createdBy: "capture"
|
|
1972
1769
|
});
|
|
1973
1770
|
} catch (error) {
|
|
1974
1771
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -2228,7 +2025,7 @@ var SEED_NODES = [
|
|
|
2228
2025
|
{ entryId: "ARCH-node-posthog", name: "PostHog", order: 1, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u{1F4CA}", description: "Product analytics \u2014 workspace-scoped events, feature flags, session replay", filePaths: "src/lib/analytics.ts, src/lib/components/PostHogWorkspaceSync.svelte", owner: "Platform", rationale: "Infrastructure because PostHog is analytics plumbing \u2014 event collection and aggregation. It has no knowledge of business domains." } },
|
|
2229
2026
|
{ entryId: "ARCH-node-openrouter", name: "OpenRouter", order: 2, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u{1F916}", description: "AI model routing for ChainWork artifact generation \u2014 streaming responses with format-aware prompts", filePaths: "src/routes/api/chainwork/generate/+server.ts", owner: "ChainWork", rationale: "Infrastructure because OpenRouter is an AI model gateway \u2014 it routes prompts to models. The strategy logic lives in ChainWork Engine (Core); this is just the pipe." } },
|
|
2230
2027
|
// Core layer
|
|
2231
|
-
{ entryId: "ARCH-node-mcp", name: "MCP Server", order: 0, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F527}", description: "
|
|
2028
|
+
{ entryId: "ARCH-node-mcp", name: "MCP Server", order: 0, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F527}", description: "~19 modular tools exposing the knowledge graph as AI-consumable operations \u2014 capture, context assembly, verification, quality checks", filePaths: "packages/mcp-server/src/index.ts, packages/mcp-server/src/tools/", owner: "AI DX", rationale: "Core layer because the MCP server encodes business operations \u2014 capture with auto-linking, quality scoring, governance rules. It depends on Infra (Convex) but never touches UI routes. Why not Infrastructure? Because it has domain opinions. Why not Features? Because it has no UI \u2014 any client (Cursor, CLI, API) can call it." } },
|
|
2232
2029
|
{ entryId: "ARCH-node-knowledge-graph", name: "Knowledge Graph", order: 1, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F578}\uFE0F", description: "20 collections, 170+ entries with typed cross-collection relations. Smart capture, auto-linking, quality scoring", filePaths: "convex/mcpKnowledge.ts, convex/entries.ts", owner: "Knowledge", rationale: "Core layer because the knowledge graph IS the domain model \u2014 collections, entries, relations, versioning, quality scoring. Glossary DATA, business rules DATA, tension DATA all live here. Why not Features? Because the data model exists independently of any page. The Glossary page in Features is just one way to visualize terms that Core owns. Think: Core owns the dictionary, Features owns the dictionary app." } },
|
|
2233
2030
|
{ entryId: "ARCH-node-governance", name: "Governance Engine", order: 2, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u2696\uFE0F", description: "Circles, roles, consent-based decision-making, tension processing with IDM-inspired async workflows", filePaths: "convex/versioning.ts, src/lib/components/versioning/", owner: "Governance", rationale: "Core layer because governance logic (draft\u2192publish workflows, consent-based decisions, tension status rules) is business process that multiple UIs consume. Why not Features? Because the versioning system and proposal flow are reusable engines \u2014 the Governance Pages in Features are just one rendering of these rules." } },
|
|
2234
2031
|
{ entryId: "ARCH-node-chainwork-engine", name: "ChainWork Engine", order: 3, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u26D3", description: "Guided strategy creation through 5-step coherence chain \u2014 AI-generated artifacts with scoring and achievements", filePaths: "src/lib/components/chainwork/config.ts, src/lib/components/chainwork/scoring.ts", owner: "ChainWork", rationale: "Core layer because the coherence chain logic, scoring algorithm, and quality gates are business rules. config.ts defines chain steps, scoring.ts computes quality \u2014 these could power a CLI or API. Why not Features? Because the ChainWork UI wizard in Features is just one skin over this engine." } },
|
|
@@ -2246,7 +2043,7 @@ var SEED_NODES = [
|
|
|
2246
2043
|
{ entryId: "ARCH-node-linear", name: "Linear (planned)", order: 2, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F4D0}", description: "Issue tracking, roadmap sync, tension-to-issue pipeline (planned integration)", owner: "Platform", rationale: "Integration layer because Linear is an external issue tracker. Why not Infrastructure? Because Infra is generic plumbing we run. Linear is a third-party tool we connect to. The planned pipeline bridges tensions (Core) to Linear issues \u2014 a classic outward integration pattern." } }
|
|
2247
2044
|
];
|
|
2248
2045
|
var SEED_FLOWS = [
|
|
2249
|
-
{ entryId: "ARCH-flow-smart-capture", name: "
|
|
2046
|
+
{ entryId: "ARCH-flow-smart-capture", name: "Capture Flow", order: 0, data: { archType: "flow", sourceNode: "ARCH-node-cursor", targetNode: "ARCH-node-knowledge-graph", description: "Developer/AI calls capture via MCP \u2192 entry created with auto-linking and quality score \u2192 stored in Knowledge Graph", color: "#8b5cf6" } },
|
|
2250
2047
|
{ entryId: "ARCH-flow-governance", name: "Governance Flow", order: 1, data: { archType: "flow", sourceNode: "ARCH-node-tensions", targetNode: "ARCH-node-governance", description: "Tension raised \u2192 appears in Command Center \u2192 triaged \u2192 processed via IDM \u2192 decision logged", color: "#6366f1" } },
|
|
2251
2048
|
{ entryId: "ARCH-flow-chainwork", name: "ChainWork Strategy Flow", order: 2, data: { archType: "flow", sourceNode: "ARCH-node-chainwork-ui", targetNode: "ARCH-node-artifacts", description: "Leader opens ChainWork \u2192 walks coherence chain \u2192 AI generates artifact \u2192 scored and published to knowledge graph", color: "#f59e0b" } },
|
|
2252
2049
|
{ entryId: "ARCH-flow-knowledge-trust", name: "Knowledge Trust Flow", order: 3, data: { archType: "flow", sourceNode: "ARCH-node-mcp", targetNode: "ARCH-node-glossary", description: "MCP verify tool checks entries against codebase \u2192 file existence, schema references validated \u2192 trust scores updated", color: "#22c55e" } },
|
|
@@ -2254,247 +2051,208 @@ var SEED_FLOWS = [
|
|
|
2254
2051
|
];
|
|
2255
2052
|
function registerArchitectureTools(server2) {
|
|
2256
2053
|
server2.registerTool(
|
|
2257
|
-
"
|
|
2054
|
+
"architecture",
|
|
2258
2055
|
{
|
|
2259
|
-
title: "
|
|
2260
|
-
description: "
|
|
2056
|
+
title: "Architecture",
|
|
2057
|
+
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",
|
|
2261
2058
|
inputSchema: {
|
|
2262
|
-
|
|
2059
|
+
action: z6.enum(["show", "explore", "flow"]).describe("Action: show full map, explore a layer, or visualize a flow"),
|
|
2060
|
+
template: z6.string().optional().describe("Template entry ID to filter by (for show)"),
|
|
2061
|
+
layer: z6.string().optional().describe("Layer name or entry ID (for explore), e.g. 'Core' or 'ARCH-layer-core'"),
|
|
2062
|
+
flow: z6.string().optional().describe("Flow name or entry ID (for flow), e.g. 'Smart Capture Flow'")
|
|
2263
2063
|
},
|
|
2264
2064
|
annotations: { readOnlyHint: true }
|
|
2265
2065
|
},
|
|
2266
|
-
async ({ template }) => {
|
|
2066
|
+
async ({ action, template, layer, flow }) => {
|
|
2267
2067
|
await ensureCollection();
|
|
2268
2068
|
const all = await listArchEntries();
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2069
|
+
if (action === "show") {
|
|
2070
|
+
const templates = byTag(all, "template");
|
|
2071
|
+
const activeTemplate = template ? templates.find((t) => t.entryId === template) : templates[0];
|
|
2072
|
+
const templateName = activeTemplate?.name ?? "System Architecture";
|
|
2073
|
+
const templateId = activeTemplate?.entryId;
|
|
2074
|
+
const layers = byTag(all, "layer").filter((l) => !templateId || l.data?.templateRef === templateId);
|
|
2075
|
+
const nodes = byTag(all, "node");
|
|
2076
|
+
const flows = byTag(all, "flow");
|
|
2077
|
+
if (layers.length === 0) {
|
|
2078
|
+
return {
|
|
2079
|
+
content: [{
|
|
2080
|
+
type: "text",
|
|
2081
|
+
text: "# Architecture Explorer\n\nNo architecture data found. Use `architecture-admin action=seed` to populate the default architecture."
|
|
2082
|
+
}]
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
const textLayers = layers.map((l) => formatLayerText(l, nodes)).join("\n\n");
|
|
2086
|
+
const textFlows = flows.length > 0 ? "\n\n---\n\n## Data Flows\n\n" + flows.map(
|
|
2087
|
+
(f) => `- **${f.name}**: ${f.data?.description ?? ""}`
|
|
2088
|
+
).join("\n") : "";
|
|
2089
|
+
const text = `# ${templateName}
|
|
2289
2090
|
|
|
2290
2091
|
${textLayers}${textFlows}`;
|
|
2291
|
-
|
|
2292
|
-
return {
|
|
2293
|
-
content: [
|
|
2294
|
-
{ type: "text", text },
|
|
2295
|
-
{ type: "resource", resource: { uri: `ui://product-os/architecture`, mimeType: "text/html", text: html } }
|
|
2296
|
-
]
|
|
2297
|
-
};
|
|
2298
|
-
}
|
|
2299
|
-
);
|
|
2300
|
-
server2.registerTool(
|
|
2301
|
-
"explore-layer",
|
|
2302
|
-
{
|
|
2303
|
-
title: "Explore Architecture Layer",
|
|
2304
|
-
description: "Drill into a specific architecture layer to see all its component nodes with descriptions, team ownership, file paths, and linked entry counts.",
|
|
2305
|
-
inputSchema: {
|
|
2306
|
-
layer: z6.string().describe("Layer name or entry ID, e.g. 'Core' or 'ARCH-layer-core'")
|
|
2307
|
-
},
|
|
2308
|
-
annotations: { readOnlyHint: true }
|
|
2309
|
-
},
|
|
2310
|
-
async ({ layer }) => {
|
|
2311
|
-
await ensureCollection();
|
|
2312
|
-
const all = await listArchEntries();
|
|
2313
|
-
const layers = byTag(all, "layer");
|
|
2314
|
-
const target = layers.find(
|
|
2315
|
-
(l) => l.name.toLowerCase() === layer.toLowerCase() || l.entryId === layer
|
|
2316
|
-
);
|
|
2317
|
-
if (!target) {
|
|
2318
|
-
const available = layers.map((l) => `\`${l.name}\``).join(", ");
|
|
2092
|
+
const html = renderArchitectureHtml(layers, nodes, flows, templateName);
|
|
2319
2093
|
return {
|
|
2320
|
-
content: [
|
|
2321
|
-
type: "text",
|
|
2322
|
-
|
|
2323
|
-
|
|
2094
|
+
content: [
|
|
2095
|
+
{ type: "text", text },
|
|
2096
|
+
{ type: "resource", resource: { uri: `ui://product-os/architecture`, mimeType: "text/html", text: html } }
|
|
2097
|
+
]
|
|
2324
2098
|
};
|
|
2325
2099
|
}
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2100
|
+
if (action === "explore") {
|
|
2101
|
+
if (!layer) return { content: [{ type: "text", text: "A `layer` is required for explore." }] };
|
|
2102
|
+
const layers = byTag(all, "layer");
|
|
2103
|
+
const target = layers.find(
|
|
2104
|
+
(l) => l.name.toLowerCase() === layer.toLowerCase() || l.entryId === layer
|
|
2105
|
+
);
|
|
2106
|
+
if (!target) {
|
|
2107
|
+
const available = layers.map((l) => `\`${l.name}\``).join(", ");
|
|
2108
|
+
return { content: [{ type: "text", text: `Layer "${layer}" not found. Available layers: ${available}` }] };
|
|
2109
|
+
}
|
|
2110
|
+
const nodes = byTag(all, "node").filter((n) => n.data?.layerRef === target.entryId);
|
|
2111
|
+
const flows = byTag(all, "flow").filter(
|
|
2112
|
+
(f) => nodes.some((n) => n.entryId === f.data?.sourceNode || n.entryId === f.data?.targetNode)
|
|
2113
|
+
);
|
|
2114
|
+
const depRule = target.data?.dependsOn ? `
|
|
2331
2115
|
**Dependency rule:** May import from ${target.data.dependsOn === "none" ? "nothing (foundation layer)" : target.data.dependsOn}.
|
|
2332
2116
|
` : "";
|
|
2333
|
-
|
|
2117
|
+
const layerRationale = target.data?.rationale ? `
|
|
2334
2118
|
> ${target.data.rationale}
|
|
2335
2119
|
` : "";
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2120
|
+
const nodeDetail = nodes.map((n) => {
|
|
2121
|
+
const lines = [`#### ${n.data?.icon ?? "\u25FB"} ${n.name}`];
|
|
2122
|
+
if (n.data?.description) lines.push(String(n.data.description));
|
|
2123
|
+
if (n.data?.owner) lines.push(`**Owner:** ${n.data.owner}`);
|
|
2124
|
+
if (n.data?.filePaths) lines.push(`**Files:** \`${n.data.filePaths}\``);
|
|
2125
|
+
if (n.data?.rationale) lines.push(`
|
|
2342
2126
|
**Why here?** ${n.data.rationale}`);
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2127
|
+
return lines.join("\n");
|
|
2128
|
+
}).join("\n\n");
|
|
2129
|
+
const flowLines = flows.length > 0 ? "\n\n### Connected Flows\n\n" + flows.map((f) => `- ${f.name}: ${f.data?.description ?? ""}`).join("\n") : "";
|
|
2130
|
+
return {
|
|
2131
|
+
content: [{
|
|
2132
|
+
type: "text",
|
|
2133
|
+
text: `# ${target.data?.icon ?? ""} ${target.name} Layer
|
|
2350
2134
|
|
|
2351
2135
|
${target.data?.description ?? ""}${depRule}${layerRationale}
|
|
2352
2136
|
**${nodes.length} components**
|
|
2353
2137
|
|
|
2354
2138
|
${nodeDetail}${flowLines}`
|
|
2355
|
-
}]
|
|
2356
|
-
};
|
|
2357
|
-
}
|
|
2358
|
-
);
|
|
2359
|
-
server2.registerTool(
|
|
2360
|
-
"show-flow",
|
|
2361
|
-
{
|
|
2362
|
-
title: "Show Architecture Flow",
|
|
2363
|
-
description: "Visualize a specific data flow path between architecture nodes \u2014 shows source, target, and description.",
|
|
2364
|
-
inputSchema: {
|
|
2365
|
-
flow: z6.string().describe("Flow name or entry ID, e.g. 'Smart Capture Flow' or 'ARCH-flow-smart-capture'")
|
|
2366
|
-
},
|
|
2367
|
-
annotations: { readOnlyHint: true }
|
|
2368
|
-
},
|
|
2369
|
-
async ({ flow }) => {
|
|
2370
|
-
await ensureCollection();
|
|
2371
|
-
const all = await listArchEntries();
|
|
2372
|
-
const flows = byTag(all, "flow");
|
|
2373
|
-
const target = flows.find(
|
|
2374
|
-
(f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
|
|
2375
|
-
);
|
|
2376
|
-
if (!target) {
|
|
2377
|
-
const available = flows.map((f) => `\`${f.name}\``).join(", ");
|
|
2378
|
-
return {
|
|
2379
|
-
content: [{
|
|
2380
|
-
type: "text",
|
|
2381
|
-
text: `Flow "${flow}" not found. Available flows: ${available}`
|
|
2382
2139
|
}]
|
|
2383
2140
|
};
|
|
2384
2141
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2142
|
+
if (action === "flow") {
|
|
2143
|
+
if (!flow) return { content: [{ type: "text", text: "A `flow` name or entry ID is required." }] };
|
|
2144
|
+
const flows = byTag(all, "flow");
|
|
2145
|
+
const target = flows.find(
|
|
2146
|
+
(f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
|
|
2147
|
+
);
|
|
2148
|
+
if (!target) {
|
|
2149
|
+
const available = flows.map((f) => `\`${f.name}\``).join(", ");
|
|
2150
|
+
return { content: [{ type: "text", text: `Flow "${flow}" not found. Available flows: ${available}` }] };
|
|
2151
|
+
}
|
|
2152
|
+
const nodes = byTag(all, "node");
|
|
2153
|
+
const source = nodes.find((n) => n.entryId === target.data?.sourceNode);
|
|
2154
|
+
const dest = nodes.find((n) => n.entryId === target.data?.targetNode);
|
|
2155
|
+
const lines = [
|
|
2156
|
+
`# ${target.name}`,
|
|
2157
|
+
"",
|
|
2158
|
+
`**${source?.data?.icon ?? "?"} ${source?.name ?? "Unknown"}** \u2192 **${dest?.data?.icon ?? "?"} ${dest?.name ?? "Unknown"}**`,
|
|
2159
|
+
"",
|
|
2160
|
+
String(target.data?.description ?? "")
|
|
2161
|
+
];
|
|
2162
|
+
if (source) {
|
|
2163
|
+
lines.push("", `### Source: ${source.name}`, String(source.data?.description ?? ""));
|
|
2164
|
+
if (source.data?.filePaths) lines.push(`Files: \`${source.data.filePaths}\``);
|
|
2165
|
+
}
|
|
2166
|
+
if (dest) {
|
|
2167
|
+
lines.push("", `### Target: ${dest.name}`, String(dest.data?.description ?? ""));
|
|
2168
|
+
if (dest.data?.filePaths) lines.push(`Files: \`${dest.data.filePaths}\``);
|
|
2169
|
+
}
|
|
2170
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2402
2171
|
}
|
|
2403
|
-
return { content: [{ type: "text", text:
|
|
2172
|
+
return { content: [{ type: "text", text: "Unknown action." }] };
|
|
2404
2173
|
}
|
|
2405
2174
|
);
|
|
2406
2175
|
server2.registerTool(
|
|
2407
|
-
"
|
|
2176
|
+
"architecture-admin",
|
|
2408
2177
|
{
|
|
2409
|
-
title: "
|
|
2410
|
-
description: "Populate the architecture collection with the default Product OS
|
|
2411
|
-
|
|
2178
|
+
title: "Architecture Admin",
|
|
2179
|
+
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
|
+
inputSchema: {
|
|
2181
|
+
action: z6.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
|
|
2182
|
+
}
|
|
2412
2183
|
},
|
|
2413
|
-
async () => {
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
entryId: seed.entryId,
|
|
2438
|
-
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2184
|
+
async ({ action }) => {
|
|
2185
|
+
if (action === "seed") {
|
|
2186
|
+
await ensureCollection();
|
|
2187
|
+
const existing = await listArchEntries();
|
|
2188
|
+
const existingIds = new Set(existing.map((e) => e.entryId));
|
|
2189
|
+
let created = 0;
|
|
2190
|
+
let updated = 0;
|
|
2191
|
+
let unchanged = 0;
|
|
2192
|
+
const allSeeds = [
|
|
2193
|
+
{ ...SEED_TEMPLATE, order: 0, status: "active" },
|
|
2194
|
+
...SEED_LAYERS.map((l) => ({ ...l, status: "active" })),
|
|
2195
|
+
...SEED_NODES.map((n) => ({ ...n, status: "active" })),
|
|
2196
|
+
...SEED_FLOWS.map((f) => ({ ...f, status: "active" }))
|
|
2197
|
+
];
|
|
2198
|
+
for (const seed of allSeeds) {
|
|
2199
|
+
if (existingIds.has(seed.entryId)) {
|
|
2200
|
+
const existingEntry = existing.find((e) => e.entryId === seed.entryId);
|
|
2201
|
+
const existingData = existingEntry?.data ?? {};
|
|
2202
|
+
const seedData = seed.data;
|
|
2203
|
+
const hasChanges = Object.keys(seedData).some(
|
|
2204
|
+
(k) => seedData[k] !== void 0 && existingData[k] !== seedData[k]
|
|
2205
|
+
);
|
|
2206
|
+
if (hasChanges) {
|
|
2207
|
+
const mergedData = { ...existingData, ...seedData };
|
|
2208
|
+
await mcpMutation("kb.updateEntry", { entryId: seed.entryId, data: mergedData });
|
|
2209
|
+
updated++;
|
|
2210
|
+
} else {
|
|
2211
|
+
unchanged++;
|
|
2212
|
+
}
|
|
2213
|
+
continue;
|
|
2443
2214
|
}
|
|
2444
|
-
|
|
2215
|
+
await mcpMutation("kb.createEntry", {
|
|
2216
|
+
collectionSlug: COLLECTION_SLUG,
|
|
2217
|
+
entryId: seed.entryId,
|
|
2218
|
+
name: seed.name,
|
|
2219
|
+
status: seed.status,
|
|
2220
|
+
data: seed.data,
|
|
2221
|
+
order: seed.order ?? 0
|
|
2222
|
+
});
|
|
2223
|
+
created++;
|
|
2445
2224
|
}
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
status: seed.status,
|
|
2451
|
-
data: seed.data,
|
|
2452
|
-
order: seed.order ?? 0
|
|
2453
|
-
});
|
|
2454
|
-
created++;
|
|
2455
|
-
}
|
|
2456
|
-
return {
|
|
2457
|
-
content: [{
|
|
2458
|
-
type: "text",
|
|
2459
|
-
text: `# Architecture Seeded
|
|
2225
|
+
return {
|
|
2226
|
+
content: [{
|
|
2227
|
+
type: "text",
|
|
2228
|
+
text: `# Architecture Seeded
|
|
2460
2229
|
|
|
2461
2230
|
**Created:** ${created} entries
|
|
2462
2231
|
**Updated:** ${updated} (merged new fields)
|
|
2463
2232
|
**Unchanged:** ${unchanged}
|
|
2464
2233
|
|
|
2465
|
-
Use \`show
|
|
2466
|
-
}]
|
|
2467
|
-
};
|
|
2468
|
-
}
|
|
2469
|
-
);
|
|
2470
|
-
server2.registerTool(
|
|
2471
|
-
"check-architecture",
|
|
2472
|
-
{
|
|
2473
|
-
title: "Check Architecture Health",
|
|
2474
|
-
description: "Scan the codebase for dependency direction violations. Reads architecture layers, nodes, and their file paths from the knowledge base, then parses TypeScript/Svelte imports and checks them against the layer dependency rules (Auth \u2190 Infra \u2190 Core \u2190 Features; Integration \u2192 Core only). Returns a structured violation report.",
|
|
2475
|
-
annotations: { readOnlyHint: true }
|
|
2476
|
-
},
|
|
2477
|
-
async () => {
|
|
2478
|
-
const projectRoot = resolveProjectRoot2();
|
|
2479
|
-
if (!projectRoot) {
|
|
2480
|
-
return {
|
|
2481
|
-
content: [{
|
|
2482
|
-
type: "text",
|
|
2483
|
-
text: "# Scan Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent). Set `WORKSPACE_PATH` env var to the absolute path of the Product OS project root."
|
|
2234
|
+
Use \`architecture action=show\` to view the map.`
|
|
2484
2235
|
}]
|
|
2485
2236
|
};
|
|
2486
2237
|
}
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2238
|
+
if (action === "check") {
|
|
2239
|
+
const projectRoot = resolveProjectRoot2();
|
|
2240
|
+
if (!projectRoot) {
|
|
2241
|
+
return {
|
|
2242
|
+
content: [{
|
|
2243
|
+
type: "text",
|
|
2244
|
+
text: "# Scan Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent). Set `WORKSPACE_PATH` env var to the absolute path of the Product OS project root."
|
|
2245
|
+
}]
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
await ensureCollection();
|
|
2249
|
+
const all = await listArchEntries();
|
|
2250
|
+
const layers = byTag(all, "layer");
|
|
2251
|
+
const nodes = byTag(all, "node");
|
|
2252
|
+
const result = scanDependencies(projectRoot, layers, nodes);
|
|
2253
|
+
return { content: [{ type: "text", text: formatScanReport(result) }] };
|
|
2254
|
+
}
|
|
2255
|
+
return { content: [{ type: "text", text: "Unknown action." }] };
|
|
2498
2256
|
}
|
|
2499
2257
|
);
|
|
2500
2258
|
}
|
|
@@ -2719,7 +2477,7 @@ import { z as z7 } from "zod";
|
|
|
2719
2477
|
var RETRO_WORKFLOW = {
|
|
2720
2478
|
id: "retro",
|
|
2721
2479
|
name: "Retrospective",
|
|
2722
|
-
shortDescription: "Structured team retrospective \u2014 reflect, surface patterns, commit to actions.
|
|
2480
|
+
shortDescription: "Structured team retrospective \u2014 reflect, surface patterns, commit to actions. Commits a decision entry to the Chain.",
|
|
2723
2481
|
icon: "\u25CE",
|
|
2724
2482
|
facilitatorPreamble: `You are now in **Facilitator Mode**. You are not a coding assistant \u2014 you are a facilitator running a structured retrospective.
|
|
2725
2483
|
|
|
@@ -2731,7 +2489,7 @@ var RETRO_WORKFLOW = {
|
|
|
2731
2489
|
4. **Create a Plan** at the start showing all rounds as tasks. Update it as you progress.
|
|
2732
2490
|
5. **Synthesize between rounds.** After collecting input, reflect back what you heard before moving on.
|
|
2733
2491
|
6. **Never go silent.** If something fails, say what happened and what to do next.
|
|
2734
|
-
7. **
|
|
2492
|
+
7. **Commit to the Chain** at the end using the Product OS capture tool.
|
|
2735
2493
|
8. **Match the energy.** Be warm but structured. This is a ceremony, not a checklist.
|
|
2736
2494
|
|
|
2737
2495
|
## Communication Style
|
|
@@ -2855,7 +2613,7 @@ Create a Cursor Plan with these rounds as tasks. Mark each in_progress as you en
|
|
|
2855
2613
|
label: "Close & Capture",
|
|
2856
2614
|
type: "close",
|
|
2857
2615
|
instruction: "One last thing \u2014 in one sentence, what's the single most important thing you're taking away from this retro?",
|
|
2858
|
-
facilitatorGuidance: "Keep it brief. One sentence reflection. Then summarize the entire retro: scope, key wins, key pain points, patterns identified, actions committed. Ask if they want to
|
|
2616
|
+
facilitatorGuidance: "Keep it brief. One sentence reflection. Then summarize the entire retro: scope, key wins, key pain points, patterns identified, actions committed. Ask if they want to commit this to the Chain. If yes, use capture to create a decision/tension entry. Thank them for the retro.",
|
|
2859
2617
|
outputSchema: {
|
|
2860
2618
|
field: "takeaway",
|
|
2861
2619
|
description: "Single-sentence takeaway",
|
|
@@ -2872,11 +2630,11 @@ Create a Cursor Plan with these rounds as tasks. Mark each in_progress as you en
|
|
|
2872
2630
|
},
|
|
2873
2631
|
errorRecovery: `If anything goes wrong during the retro:
|
|
2874
2632
|
|
|
2875
|
-
1. **MCP tool failure**: Skip the
|
|
2633
|
+
1. **MCP tool failure**: Skip the chain commit step. Summarize everything in the conversation instead. Suggest the participant runs \`capture\` manually later.
|
|
2876
2634
|
2. **AskQuestion not available**: Fall back to numbered options in plain text. "Reply with 1, 2, or 3."
|
|
2877
2635
|
3. **Plan creation fails**: Continue without the Plan. The conversation IS the record.
|
|
2878
2636
|
4. **Participant goes off-topic**: Gently redirect: "That's valuable \u2014 let's capture it. For now, let's stay with [current round]."
|
|
2879
|
-
5. **Participant wants to stop**: Respect it. Summarize what you have so far. Offer to
|
|
2637
|
+
5. **Participant wants to stop**: Respect it. Summarize what you have so far. Offer to commit partial results to the Chain.
|
|
2880
2638
|
|
|
2881
2639
|
The retro must never fail silently. Always communicate state.`
|
|
2882
2640
|
};
|
|
@@ -2940,19 +2698,19 @@ ${cards}`
|
|
|
2940
2698
|
"workflow-checkpoint",
|
|
2941
2699
|
{
|
|
2942
2700
|
title: "Workflow Checkpoint",
|
|
2943
|
-
description: "Record the output of a workflow round. Captures the round's data to the
|
|
2701
|
+
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.",
|
|
2944
2702
|
inputSchema: {
|
|
2945
2703
|
workflowId: z7.string().describe("Workflow ID (e.g., 'retro')"),
|
|
2946
2704
|
roundId: z7.string().describe("Round ID (e.g., 'what-went-well')"),
|
|
2947
2705
|
output: z7.string().describe("The round's output \u2014 synthesized by the facilitator from the conversation"),
|
|
2948
2706
|
isFinal: z7.boolean().optional().describe(
|
|
2949
|
-
"If true, this is the final checkpoint and triggers the summary
|
|
2707
|
+
"If true, this is the final checkpoint and triggers the summary chain entry creation"
|
|
2950
2708
|
),
|
|
2951
2709
|
summaryName: z7.string().optional().describe(
|
|
2952
|
-
"Name for the final
|
|
2710
|
+
"Name for the final chain entry (required when isFinal=true)"
|
|
2953
2711
|
),
|
|
2954
2712
|
summaryDescription: z7.string().optional().describe(
|
|
2955
|
-
"Full description/rationale for the final
|
|
2713
|
+
"Full description/rationale for the final chain entry (required when isFinal=true)"
|
|
2956
2714
|
)
|
|
2957
2715
|
},
|
|
2958
2716
|
annotations: { destructiveHint: false }
|
|
@@ -3000,20 +2758,20 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
|
|
|
3000
2758
|
createdBy: `workflow:${wf.id}`
|
|
3001
2759
|
});
|
|
3002
2760
|
lines.push(
|
|
3003
|
-
`**
|
|
2761
|
+
`**Entry Committed**: \`${entryId}\``,
|
|
3004
2762
|
`Collection: \`${wf.kbOutputCollection}\``,
|
|
3005
2763
|
`Name: ${summaryName}`,
|
|
3006
2764
|
"",
|
|
3007
|
-
`The retro is now
|
|
2765
|
+
`The retro is now committed to the Chain. `,
|
|
3008
2766
|
`Use \`suggest-links\` on this entry to connect it to related knowledge.`
|
|
3009
2767
|
);
|
|
3010
2768
|
} catch (err) {
|
|
3011
2769
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3012
2770
|
lines.push(
|
|
3013
|
-
`**
|
|
2771
|
+
`**Chain commit failed**: ${msg}`,
|
|
3014
2772
|
"",
|
|
3015
2773
|
`The retro output is preserved in this conversation. `,
|
|
3016
|
-
`You can manually create the entry later using \`
|
|
2774
|
+
`You can manually create the entry later using \`capture\` with:`,
|
|
3017
2775
|
`- Collection: \`${wf.kbOutputCollection}\``,
|
|
3018
2776
|
`- Name: ${summaryName}`,
|
|
3019
2777
|
`- Description: (copy from the conversation summary above)`
|
|
@@ -3035,7 +2793,7 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
|
|
|
3035
2793
|
} else {
|
|
3036
2794
|
lines.push(
|
|
3037
2795
|
"",
|
|
3038
|
-
`**All rounds complete.** Call this tool again with \`isFinal: true\` to
|
|
2796
|
+
`**All rounds complete.** Call this tool again with \`isFinal: true\` to commit to the Chain.`
|
|
3039
2797
|
);
|
|
3040
2798
|
}
|
|
3041
2799
|
}
|
|
@@ -3055,68 +2813,57 @@ function linkSummary(links) {
|
|
|
3055
2813
|
}
|
|
3056
2814
|
function registerGitChainTools(server2) {
|
|
3057
2815
|
server2.registerTool(
|
|
3058
|
-
"chain
|
|
2816
|
+
"chain",
|
|
3059
2817
|
{
|
|
3060
|
-
title: "
|
|
3061
|
-
description: "
|
|
2818
|
+
title: "Chain",
|
|
2819
|
+
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).",
|
|
3062
2820
|
inputSchema: {
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
),
|
|
3067
|
-
description: z8.string().optional().describe("
|
|
3068
|
-
|
|
2821
|
+
action: z8.enum(["create", "get", "list", "edit"]).describe("Action: create a process, get process details, list all processes, or edit a process link"),
|
|
2822
|
+
chainEntryId: z8.string().optional().describe("Chain entry ID (required for get/edit)"),
|
|
2823
|
+
title: z8.string().optional().describe("Process title (required for create)"),
|
|
2824
|
+
chainTypeId: z8.string().optional().default("strategy-coherence").describe("Process template slug for create: 'strategy-coherence', 'idm-proposal', or any custom template slug"),
|
|
2825
|
+
description: z8.string().optional().describe("Description (for create)"),
|
|
2826
|
+
linkId: z8.string().optional().describe("Link to edit (for edit action): problem, insight, choice, action, outcome"),
|
|
2827
|
+
content: z8.string().optional().describe("New content for the link (for edit action)"),
|
|
2828
|
+
status: z8.string().optional().describe("Filter by status for list: 'draft' or 'active'"),
|
|
2829
|
+
author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
3069
2830
|
}
|
|
3070
2831
|
},
|
|
3071
|
-
async ({ title, chainTypeId, description, author }) => {
|
|
3072
|
-
|
|
3073
|
-
"
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
2832
|
+
async ({ action, chainEntryId, title, chainTypeId, description, linkId, content, status, author }) => {
|
|
2833
|
+
if (action === "create") {
|
|
2834
|
+
if (!title) return { content: [{ type: "text", text: "A `title` is required to create a process." }] };
|
|
2835
|
+
const result = await mcpMutation(
|
|
2836
|
+
"gitchain.createChain",
|
|
2837
|
+
{ title, chainTypeId, description, author }
|
|
2838
|
+
);
|
|
2839
|
+
return {
|
|
2840
|
+
content: [{
|
|
3079
2841
|
type: "text",
|
|
3080
|
-
text: `#
|
|
2842
|
+
text: `# Process Created
|
|
3081
2843
|
|
|
3082
2844
|
- **Entry ID:** \`${result.entryId}\`
|
|
3083
2845
|
- **Title:** ${title}
|
|
3084
2846
|
- **Type:** ${chainTypeId}
|
|
3085
2847
|
- **Status:** draft
|
|
3086
2848
|
|
|
3087
|
-
Use \`chain
|
|
3088
|
-
}
|
|
3089
|
-
]
|
|
3090
|
-
};
|
|
3091
|
-
}
|
|
3092
|
-
);
|
|
3093
|
-
server2.registerTool(
|
|
3094
|
-
"chain-get",
|
|
3095
|
-
{
|
|
3096
|
-
title: "Get Chain",
|
|
3097
|
-
description: "Retrieve a chain by its entry ID. Returns the full chain content, coherence scores, link fill status, and version info. Use chain-list first to discover chain entry IDs.",
|
|
3098
|
-
inputSchema: {
|
|
3099
|
-
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'")
|
|
3100
|
-
},
|
|
3101
|
-
annotations: { readOnlyHint: true }
|
|
3102
|
-
},
|
|
3103
|
-
async ({ chainEntryId }) => {
|
|
3104
|
-
const chain = await mcpQuery("gitchain.getChain", { chainEntryId });
|
|
3105
|
-
if (!chain) {
|
|
3106
|
-
return {
|
|
3107
|
-
content: [
|
|
3108
|
-
{
|
|
3109
|
-
type: "text",
|
|
3110
|
-
text: `Chain "${chainEntryId}" not found.`
|
|
3111
|
-
}
|
|
3112
|
-
]
|
|
2849
|
+
Use \`chain action=edit chainEntryId="${result.entryId}" linkId="problem" content="..."\` to start filling in links.`
|
|
2850
|
+
}]
|
|
3113
2851
|
};
|
|
3114
2852
|
}
|
|
3115
|
-
|
|
2853
|
+
if (action === "get") {
|
|
2854
|
+
if (!chainEntryId) return { content: [{ type: "text", text: "A `chainEntryId` is required." }] };
|
|
2855
|
+
const chain = await mcpQuery("gitchain.getChain", { chainEntryId });
|
|
2856
|
+
if (!chain) {
|
|
2857
|
+
return { content: [{ type: "text", text: `Process "${chainEntryId}" not found.` }] };
|
|
2858
|
+
}
|
|
2859
|
+
const scoreSection = chain.scores ? `
|
|
3116
2860
|
## Coherence: ${chain.coherenceScore}%
|
|
3117
2861
|
|
|
3118
2862
|
` + chain.scores.sections.map((s) => `- **${s.key}**: ${s.power}/100 (${"\u2605".repeat(Math.min(s.stars ?? 0, 5))}${"\u2606".repeat(Math.max(0, 5 - (s.stars ?? 0)))})`).join("\n") : "";
|
|
3119
|
-
|
|
2863
|
+
return {
|
|
2864
|
+
content: [{
|
|
2865
|
+
type: "text",
|
|
2866
|
+
text: `# ${chain.name}
|
|
3120
2867
|
|
|
3121
2868
|
- **Entry ID:** \`${chain.entryId}\`
|
|
3122
2869
|
- **Type:** ${chain.chainTypeName}
|
|
@@ -3129,29 +2876,32 @@ Use \`chain-edit\` with chainEntryId=\`${result.entryId}\` to start filling in l
|
|
|
3129
2876
|
|
|
3130
2877
|
## Links
|
|
3131
2878
|
|
|
3132
|
-
` + linkSummary(chain.links)
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
);
|
|
3136
|
-
server2.registerTool(
|
|
3137
|
-
"chain-edit",
|
|
3138
|
-
{
|
|
3139
|
-
title: "Edit Chain Link",
|
|
3140
|
-
description: "Edit a specific link in a chain. Each chain has named links (e.g. for strategy-coherence: problem, insight, choice, action, outcome). The link content is replaced entirely \u2014 pass the full new text.",
|
|
3141
|
-
inputSchema: {
|
|
3142
|
-
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
|
|
3143
|
-
linkId: z8.string().describe(
|
|
3144
|
-
"Which link to edit. For strategy-coherence: problem, insight, choice, action, outcome. For idm-proposal: tension, proposal, objections, integration, decision."
|
|
3145
|
-
),
|
|
3146
|
-
content: z8.string().describe("The full new content for this link"),
|
|
3147
|
-
author: z8.string().optional().describe("Who is making this edit. Defaults to 'mcp'.")
|
|
2879
|
+
` + linkSummary(chain.links)
|
|
2880
|
+
}]
|
|
2881
|
+
};
|
|
3148
2882
|
}
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
2883
|
+
if (action === "list") {
|
|
2884
|
+
const chains = await mcpQuery("gitchain.listChains", {
|
|
2885
|
+
chainTypeId,
|
|
2886
|
+
status
|
|
2887
|
+
});
|
|
2888
|
+
if (chains.length === 0) {
|
|
2889
|
+
return { content: [{ type: "text", text: "No processes found. Use `chain action=create` to create one." }] };
|
|
2890
|
+
}
|
|
2891
|
+
const formatted = chains.map(
|
|
2892
|
+
(c) => `- **\`${c.entryId}\`** ${c.name} \u2014 ${c.chainTypeId} \xB7 ${c.filledCount}/${c.totalCount} links \xB7 coherence: ${c.coherenceScore}% \xB7 status: ${c.status}`
|
|
2893
|
+
).join("\n");
|
|
2894
|
+
return { content: [{ type: "text", text: `# Chains (${chains.length})
|
|
2895
|
+
|
|
2896
|
+
${formatted}` }] };
|
|
2897
|
+
}
|
|
2898
|
+
if (action === "edit") {
|
|
2899
|
+
if (!chainEntryId) return { content: [{ type: "text", text: "A `chainEntryId` is required." }] };
|
|
2900
|
+
if (!linkId) return { content: [{ type: "text", text: "A `linkId` is required (e.g. problem, insight, choice, action, outcome)." }] };
|
|
2901
|
+
if (!content) return { content: [{ type: "text", text: "The `content` for the link is required." }] };
|
|
2902
|
+
const result = await mcpMutation("gitchain.editLink", { chainEntryId, linkId, content, author });
|
|
2903
|
+
return {
|
|
2904
|
+
content: [{
|
|
3155
2905
|
type: "text",
|
|
3156
2906
|
text: `# Link Updated
|
|
3157
2907
|
|
|
@@ -3160,114 +2910,38 @@ Use \`chain-edit\` with chainEntryId=\`${result.entryId}\` to start filling in l
|
|
|
3160
2910
|
- **Chain status:** ${result.status}
|
|
3161
2911
|
- **Content length:** ${content.length} chars
|
|
3162
2912
|
|
|
3163
|
-
Use \`chain
|
|
3164
|
-
}
|
|
3165
|
-
]
|
|
3166
|
-
};
|
|
3167
|
-
}
|
|
3168
|
-
);
|
|
3169
|
-
server2.registerTool(
|
|
3170
|
-
"chain-list",
|
|
3171
|
-
{
|
|
3172
|
-
title: "List Chains",
|
|
3173
|
-
description: "List all chains in the workspace, optionally filtered by chain type or status. Returns entry IDs, titles, link fill progress, and coherence scores.",
|
|
3174
|
-
inputSchema: {
|
|
3175
|
-
chainTypeId: z8.string().optional().describe("Filter by chain type: 'strategy-coherence' or 'idm-proposal'"),
|
|
3176
|
-
status: z8.string().optional().describe("Filter by status: 'draft' or 'active'")
|
|
3177
|
-
},
|
|
3178
|
-
annotations: { readOnlyHint: true }
|
|
3179
|
-
},
|
|
3180
|
-
async ({ chainTypeId, status }) => {
|
|
3181
|
-
const chains = await mcpQuery("gitchain.listChains", {
|
|
3182
|
-
chainTypeId,
|
|
3183
|
-
status
|
|
3184
|
-
});
|
|
3185
|
-
if (chains.length === 0) {
|
|
3186
|
-
return {
|
|
3187
|
-
content: [
|
|
3188
|
-
{
|
|
3189
|
-
type: "text",
|
|
3190
|
-
text: "No chains found. Use `chain-create` to create one."
|
|
3191
|
-
}
|
|
3192
|
-
]
|
|
2913
|
+
Use \`chain action=get\` to see the full chain with updated scores.`
|
|
2914
|
+
}]
|
|
3193
2915
|
};
|
|
3194
2916
|
}
|
|
3195
|
-
|
|
3196
|
-
(c) => `- **\`${c.entryId}\`** ${c.name} \u2014 ${c.chainTypeId} \xB7 ${c.filledCount}/${c.totalCount} links \xB7 coherence: ${c.coherenceScore}% \xB7 status: ${c.status}`
|
|
3197
|
-
).join("\n");
|
|
3198
|
-
return {
|
|
3199
|
-
content: [
|
|
3200
|
-
{
|
|
3201
|
-
type: "text",
|
|
3202
|
-
text: `# Chains (${chains.length})
|
|
3203
|
-
|
|
3204
|
-
${formatted}`
|
|
3205
|
-
}
|
|
3206
|
-
]
|
|
3207
|
-
};
|
|
2917
|
+
return { content: [{ type: "text", text: "Unknown action." }] };
|
|
3208
2918
|
}
|
|
3209
2919
|
);
|
|
3210
2920
|
server2.registerTool(
|
|
3211
|
-
"chain-
|
|
2921
|
+
"chain-version",
|
|
3212
2922
|
{
|
|
3213
|
-
title: "Chain
|
|
3214
|
-
description: "
|
|
2923
|
+
title: "Chain Version",
|
|
2924
|
+
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.",
|
|
3215
2925
|
inputSchema: {
|
|
3216
|
-
|
|
2926
|
+
action: z8.enum(["commit", "list", "diff", "revert", "history"]).describe("Action: commit a snapshot, list commits, diff two versions, revert to a version, or view history"),
|
|
2927
|
+
chainEntryId: z8.string().describe("The chain's entry ID"),
|
|
2928
|
+
commitMessage: z8.string().optional().describe("Commit message (required for commit). Convention: type(link): description"),
|
|
2929
|
+
versionA: z8.number().optional().describe("Earlier version for diff"),
|
|
2930
|
+
versionB: z8.number().optional().describe("Later version for diff"),
|
|
2931
|
+
toVersion: z8.number().optional().describe("Version number to revert to"),
|
|
2932
|
+
author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
3217
2933
|
},
|
|
3218
|
-
annotations: { readOnlyHint:
|
|
3219
|
-
},
|
|
3220
|
-
async ({ chainEntryId }) => {
|
|
3221
|
-
const history = await mcpQuery("gitchain.getHistory", {
|
|
3222
|
-
chainEntryId
|
|
3223
|
-
});
|
|
3224
|
-
if (history.length === 0) {
|
|
3225
|
-
return {
|
|
3226
|
-
content: [
|
|
3227
|
-
{
|
|
3228
|
-
type: "text",
|
|
3229
|
-
text: `No history found for chain "${chainEntryId}".`
|
|
3230
|
-
}
|
|
3231
|
-
]
|
|
3232
|
-
};
|
|
3233
|
-
}
|
|
3234
|
-
const formatted = history.sort((a, b) => b.timestamp - a.timestamp).map((h) => {
|
|
3235
|
-
const date = new Date(h.timestamp).toISOString().replace("T", " ").substring(0, 19);
|
|
3236
|
-
return `- **${date}** [${h.event}] by ${h.changedBy ?? "unknown"} \u2014 ${h.note ?? ""}`;
|
|
3237
|
-
}).join("\n");
|
|
3238
|
-
return {
|
|
3239
|
-
content: [
|
|
3240
|
-
{
|
|
3241
|
-
type: "text",
|
|
3242
|
-
text: `# History for ${chainEntryId} (${history.length} events)
|
|
3243
|
-
|
|
3244
|
-
${formatted}`
|
|
3245
|
-
}
|
|
3246
|
-
]
|
|
3247
|
-
};
|
|
3248
|
-
}
|
|
3249
|
-
);
|
|
3250
|
-
server2.registerTool(
|
|
3251
|
-
"chain-commit",
|
|
3252
|
-
{
|
|
3253
|
-
title: "Commit Chain",
|
|
3254
|
-
description: "Create a version snapshot (commit) of the current chain state. Records all link content, computes coherence score, and tracks which links changed. Commit messages should follow: type(link): description. Types: edit, refine, rewrite, integrate, revert.",
|
|
3255
|
-
inputSchema: {
|
|
3256
|
-
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
|
|
3257
|
-
commitMessage: z8.string().describe(
|
|
3258
|
-
"Commit message following convention: type(link): description. Example: 'edit(outcome): Add Q1 revenue target'"
|
|
3259
|
-
),
|
|
3260
|
-
author: z8.string().optional().describe("Who is committing. Defaults to 'mcp'.")
|
|
3261
|
-
}
|
|
2934
|
+
annotations: { readOnlyHint: false }
|
|
3262
2935
|
},
|
|
3263
|
-
async ({ chainEntryId, commitMessage, author }) => {
|
|
3264
|
-
|
|
3265
|
-
|
|
2936
|
+
async ({ action, chainEntryId, commitMessage, versionA, versionB, toVersion, author }) => {
|
|
2937
|
+
if (action === "commit") {
|
|
2938
|
+
if (!commitMessage) return { content: [{ type: "text", text: "A `commitMessage` is required." }] };
|
|
2939
|
+
const result = await mcpMutation("gitchain.commitChain", { chainEntryId, commitMessage, author });
|
|
2940
|
+
const warning = result.commitLintWarning ? `
|
|
3266
2941
|
|
|
3267
2942
|
> **Lint warning:** ${result.commitLintWarning}` : "";
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
{
|
|
2943
|
+
return {
|
|
2944
|
+
content: [{
|
|
3271
2945
|
type: "text",
|
|
3272
2946
|
text: `# Committed v${result.version}
|
|
3273
2947
|
|
|
@@ -3277,229 +2951,160 @@ ${formatted}`
|
|
|
3277
2951
|
- **Coherence:** ${result.coherenceScore}%
|
|
3278
2952
|
- **Links modified:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
|
|
3279
2953
|
` + warning
|
|
3280
|
-
}
|
|
3281
|
-
]
|
|
3282
|
-
};
|
|
3283
|
-
}
|
|
3284
|
-
);
|
|
3285
|
-
server2.registerTool(
|
|
3286
|
-
"chain-commits",
|
|
3287
|
-
{
|
|
3288
|
-
title: "List Chain Commits",
|
|
3289
|
-
description: "List all version snapshots (commits) for a chain, newest first. Shows version number, commit message, author, which links were modified, and status.",
|
|
3290
|
-
inputSchema: {
|
|
3291
|
-
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'")
|
|
3292
|
-
},
|
|
3293
|
-
annotations: { readOnlyHint: true }
|
|
3294
|
-
},
|
|
3295
|
-
async ({ chainEntryId }) => {
|
|
3296
|
-
const commits = await mcpQuery("gitchain.listCommits", {
|
|
3297
|
-
chainEntryId
|
|
3298
|
-
});
|
|
3299
|
-
if (commits.length === 0) {
|
|
3300
|
-
return {
|
|
3301
|
-
content: [
|
|
3302
|
-
{
|
|
3303
|
-
type: "text",
|
|
3304
|
-
text: `No commits found for chain "${chainEntryId}". Use \`chain-commit\` to create the first snapshot.`
|
|
3305
|
-
}
|
|
3306
|
-
]
|
|
2954
|
+
}]
|
|
3307
2955
|
};
|
|
3308
2956
|
}
|
|
3309
|
-
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
{
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
title: "Diff Chain Versions",
|
|
3331
|
-
description: "Compare two versions of a chain. Shows which links changed, word-level diffs for modified links, and the coherence score delta. Use chain-commits first to see available version numbers.",
|
|
3332
|
-
inputSchema: {
|
|
3333
|
-
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
|
|
3334
|
-
versionA: z8.number().describe("The earlier version number"),
|
|
3335
|
-
versionB: z8.number().describe("The later version number")
|
|
3336
|
-
},
|
|
3337
|
-
annotations: { readOnlyHint: true }
|
|
3338
|
-
},
|
|
3339
|
-
async ({ chainEntryId, versionA, versionB }) => {
|
|
3340
|
-
const diff = await mcpMutation("gitchain.diffVersions", {
|
|
3341
|
-
chainEntryId,
|
|
3342
|
-
versionA,
|
|
3343
|
-
versionB
|
|
3344
|
-
});
|
|
3345
|
-
let text = `# Diff: v${versionA} \u2192 v${versionB}
|
|
2957
|
+
if (action === "list") {
|
|
2958
|
+
const commits = await mcpQuery("gitchain.listCommits", { chainEntryId });
|
|
2959
|
+
if (commits.length === 0) {
|
|
2960
|
+
return { content: [{ type: "text", text: `No commits found for chain "${chainEntryId}". Use \`chain-version action=commit\` to create the first snapshot.` }] };
|
|
2961
|
+
}
|
|
2962
|
+
const formatted = commits.map((c) => {
|
|
2963
|
+
const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
|
|
2964
|
+
const msg = c.commitMessage ?? c.changeNote ?? "(no message)";
|
|
2965
|
+
const links = c.linksModified?.length > 0 ? ` [${c.linksModified.join(", ")}]` : "";
|
|
2966
|
+
return `- **v${c.version}** ${date} by ${c.author} \u2014 ${msg}${links} (${c.versionStatus})`;
|
|
2967
|
+
}).join("\n");
|
|
2968
|
+
return { content: [{ type: "text", text: `# Commits for ${chainEntryId} (${commits.length})
|
|
2969
|
+
|
|
2970
|
+
${formatted}` }] };
|
|
2971
|
+
}
|
|
2972
|
+
if (action === "diff") {
|
|
2973
|
+
if (versionA == null || versionB == null) {
|
|
2974
|
+
return { content: [{ type: "text", text: "Both `versionA` and `versionB` are required for diff." }] };
|
|
2975
|
+
}
|
|
2976
|
+
const diff = await mcpMutation("gitchain.diffVersions", { chainEntryId, versionA, versionB });
|
|
2977
|
+
let text = `# Diff: v${versionA} \u2192 v${versionB}
|
|
3346
2978
|
|
|
3347
2979
|
- **Chain:** \`${diff.chainEntryId}\`
|
|
3348
2980
|
- **Coherence:** ${diff.coherenceBefore}% \u2192 ${diff.coherenceAfter}% (${diff.coherenceDelta >= 0 ? "+" : ""}${diff.coherenceDelta})
|
|
3349
2981
|
- **Links changed:** ${diff.linksChanged.length > 0 ? diff.linksChanged.join(", ") : "none"}
|
|
3350
2982
|
`;
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
2983
|
+
for (const ld of diff.linkDiffs) {
|
|
2984
|
+
if (ld.status === "unchanged") continue;
|
|
2985
|
+
text += `
|
|
3354
2986
|
## ${ld.linkId} (${ld.status})
|
|
3355
2987
|
|
|
3356
2988
|
`;
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
text +=
|
|
3362
|
-
|
|
3363
|
-
text += `**${w.value.substring(0, 200)}**`;
|
|
3364
|
-
} else {
|
|
3365
|
-
text += w.value.substring(0, 200);
|
|
2989
|
+
const wordDiff = diff.wordDiffs[ld.linkId];
|
|
2990
|
+
if (wordDiff?.length > 0) {
|
|
2991
|
+
for (const w of wordDiff) {
|
|
2992
|
+
if (w.type === "delete") text += `~~${w.value.substring(0, 200)}~~`;
|
|
2993
|
+
else if (w.type === "insert") text += `**${w.value.substring(0, 200)}**`;
|
|
2994
|
+
else text += w.value.substring(0, 200);
|
|
3366
2995
|
}
|
|
2996
|
+
text += "\n";
|
|
3367
2997
|
}
|
|
3368
|
-
text += "\n";
|
|
3369
2998
|
}
|
|
2999
|
+
return { content: [{ type: "text", text }] };
|
|
3370
3000
|
}
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
{
|
|
3377
|
-
title: "Chain Coherence Gate",
|
|
3378
|
-
description: "Run the coherence gate on a chain. Checks: coherence score >= 70%, all links filled, commit message follows convention. Returns pass/fail with detailed check results. This is the same gate that blocks publishing.",
|
|
3379
|
-
inputSchema: {
|
|
3380
|
-
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
|
|
3381
|
-
commitMessage: z8.string().optional().describe("Optional commit message to lint")
|
|
3382
|
-
},
|
|
3383
|
-
annotations: { readOnlyHint: true }
|
|
3384
|
-
},
|
|
3385
|
-
async ({ chainEntryId, commitMessage }) => {
|
|
3386
|
-
const gate = await mcpQuery("gitchain.runGate", {
|
|
3387
|
-
chainEntryId,
|
|
3388
|
-
commitMessage
|
|
3389
|
-
});
|
|
3390
|
-
const checkLines = gate.checks.map(
|
|
3391
|
-
(c) => `- ${c.pass ? "PASS" : "FAIL"} **${c.name}**: ${c.detail}`
|
|
3392
|
-
).join("\n");
|
|
3393
|
-
const icon = gate.pass ? "PASS" : "BLOCKED";
|
|
3394
|
-
return {
|
|
3395
|
-
content: [
|
|
3396
|
-
{
|
|
3001
|
+
if (action === "revert") {
|
|
3002
|
+
if (toVersion == null) return { content: [{ type: "text", text: "A `toVersion` is required for revert." }] };
|
|
3003
|
+
const result = await mcpMutation("gitchain.revertChain", { chainEntryId, toVersion, author });
|
|
3004
|
+
return {
|
|
3005
|
+
content: [{
|
|
3397
3006
|
type: "text",
|
|
3398
|
-
text: `#
|
|
3007
|
+
text: `# Reverted
|
|
3399
3008
|
|
|
3400
|
-
- **
|
|
3401
|
-
- **
|
|
3009
|
+
- **Chain:** \`${result.entryId}\`
|
|
3010
|
+
- **Reverted to:** v${result.revertedTo}
|
|
3011
|
+
- **New version:** v${result.newVersion}
|
|
3012
|
+
- **Links affected:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
|
|
3402
3013
|
|
|
3403
|
-
|
|
3014
|
+
History is preserved \u2014 this created a new version, not a destructive reset.`
|
|
3015
|
+
}]
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
if (action === "history") {
|
|
3019
|
+
const history = await mcpQuery("gitchain.getHistory", { chainEntryId });
|
|
3020
|
+
if (history.length === 0) {
|
|
3021
|
+
return { content: [{ type: "text", text: `No history found for chain "${chainEntryId}".` }] };
|
|
3022
|
+
}
|
|
3023
|
+
const formatted = history.sort((a, b) => b.timestamp - a.timestamp).map((h) => {
|
|
3024
|
+
const date = new Date(h.timestamp).toISOString().replace("T", " ").substring(0, 19);
|
|
3025
|
+
return `- **${date}** [${h.event}] by ${h.changedBy ?? "unknown"} \u2014 ${h.note ?? ""}`;
|
|
3026
|
+
}).join("\n");
|
|
3027
|
+
return { content: [{ type: "text", text: `# History for ${chainEntryId} (${history.length} events)
|
|
3404
3028
|
|
|
3405
|
-
${
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
};
|
|
3029
|
+
${formatted}` }] };
|
|
3030
|
+
}
|
|
3031
|
+
return { content: [{ type: "text", text: "Unknown action." }] };
|
|
3409
3032
|
}
|
|
3410
3033
|
);
|
|
3411
3034
|
server2.registerTool(
|
|
3412
3035
|
"chain-branch",
|
|
3413
3036
|
{
|
|
3414
|
-
title: "
|
|
3415
|
-
description: "
|
|
3037
|
+
title: "Chain Branch",
|
|
3038
|
+
description: "Manage process branches \u2014 create a branch for isolated editing, list branches, merge a branch back into main, or check for conflicts.",
|
|
3416
3039
|
inputSchema: {
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3040
|
+
action: z8.enum(["create", "list", "merge", "conflicts"]).describe("Action: create a branch, list branches, merge a branch, or check for conflicts"),
|
|
3041
|
+
chainEntryId: z8.string().describe("The chain's entry ID"),
|
|
3042
|
+
branchName: z8.string().optional().describe("Branch name (required for merge/conflicts, optional for create)"),
|
|
3043
|
+
strategy: z8.enum(["merge_commit", "squash"]).optional().describe("Merge strategy: 'merge_commit' (default) or 'squash'"),
|
|
3044
|
+
author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
3421
3045
|
}
|
|
3422
3046
|
},
|
|
3423
|
-
async ({ chainEntryId,
|
|
3047
|
+
async ({ action, chainEntryId, branchName, strategy, author }) => {
|
|
3424
3048
|
if (action === "create") {
|
|
3425
|
-
const result = await mcpMutation("gitchain.createBranch", { chainEntryId, name, author });
|
|
3049
|
+
const result = await mcpMutation("gitchain.createBranch", { chainEntryId, name: branchName, author });
|
|
3426
3050
|
return {
|
|
3427
|
-
content: [
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
text: `# Branch Created
|
|
3051
|
+
content: [{
|
|
3052
|
+
type: "text",
|
|
3053
|
+
text: `# Branch Created
|
|
3431
3054
|
|
|
3432
3055
|
- **Name:** ${result.name}
|
|
3433
3056
|
- **Based on:** v${result.baseVersion}
|
|
3434
3057
|
- **Chain:** \`${chainEntryId}\`
|
|
3435
3058
|
|
|
3436
|
-
Edit links and commit on this branch, then use \`chain-merge\` to land changes.`
|
|
3437
|
-
|
|
3438
|
-
]
|
|
3059
|
+
Edit links and commit on this branch, then use \`chain-branch action=merge\` to land changes.`
|
|
3060
|
+
}]
|
|
3439
3061
|
};
|
|
3440
3062
|
}
|
|
3441
|
-
|
|
3442
|
-
chainEntryId
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
}
|
|
3451
|
-
]
|
|
3452
|
-
};
|
|
3063
|
+
if (action === "list") {
|
|
3064
|
+
const branches = await mcpMutation("gitchain.listBranches", { chainEntryId });
|
|
3065
|
+
if (branches.length === 0) {
|
|
3066
|
+
return { content: [{ type: "text", text: `No branches found for chain "${chainEntryId}".` }] };
|
|
3067
|
+
}
|
|
3068
|
+
const formatted = branches.map((b) => `- **${b.name}** (${b.status}) \u2014 based on v${b.baseVersion}, by ${b.createdBy}`).join("\n");
|
|
3069
|
+
return { content: [{ type: "text", text: `# Branches for ${chainEntryId} (${branches.length})
|
|
3070
|
+
|
|
3071
|
+
${formatted}` }] };
|
|
3453
3072
|
}
|
|
3454
|
-
|
|
3455
|
-
(
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
{
|
|
3073
|
+
if (action === "merge") {
|
|
3074
|
+
if (!branchName) return { content: [{ type: "text", text: "A `branchName` is required for merge." }] };
|
|
3075
|
+
const result = await mcpMutation("gitchain.mergeBranch", { chainEntryId, branchName, strategy, author });
|
|
3076
|
+
return {
|
|
3077
|
+
content: [{
|
|
3460
3078
|
type: "text",
|
|
3461
|
-
text: `#
|
|
3079
|
+
text: `# Branch Merged
|
|
3462
3080
|
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
}
|
|
3468
|
-
);
|
|
3469
|
-
server2.registerTool(
|
|
3470
|
-
"chain-conflicts",
|
|
3471
|
-
{
|
|
3472
|
-
title: "Check Branch Conflicts",
|
|
3473
|
-
description: "Check if a branch has conflicts with other active branches. Conflicts occur when two branches modify the same chain link. Always check before merging.",
|
|
3474
|
-
inputSchema: {
|
|
3475
|
-
chainEntryId: z8.string().describe("The chain's entry ID"),
|
|
3476
|
-
branchName: z8.string().describe("The branch name to check for conflicts")
|
|
3477
|
-
},
|
|
3478
|
-
annotations: { readOnlyHint: true }
|
|
3479
|
-
},
|
|
3480
|
-
async ({ chainEntryId, branchName }) => {
|
|
3481
|
-
const result = await mcpMutation("gitchain.checkConflicts", {
|
|
3482
|
-
chainEntryId,
|
|
3483
|
-
branchName
|
|
3484
|
-
});
|
|
3485
|
-
if (!result.hasConflicts) {
|
|
3486
|
-
return {
|
|
3487
|
-
content: [
|
|
3488
|
-
{
|
|
3489
|
-
type: "text",
|
|
3490
|
-
text: `# No Conflicts
|
|
3081
|
+
- **Chain:** \`${result.entryId}\`
|
|
3082
|
+
- **Branch:** ${result.branchName} (now closed)
|
|
3083
|
+
- **Version:** v${result.version}
|
|
3084
|
+
- **Strategy:** ${result.strategy}
|
|
3491
3085
|
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
]
|
|
3086
|
+
Main is now at v${result.version}. The branch has been closed.`
|
|
3087
|
+
}]
|
|
3495
3088
|
};
|
|
3496
3089
|
}
|
|
3497
|
-
|
|
3498
|
-
(
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3090
|
+
if (action === "conflicts") {
|
|
3091
|
+
if (!branchName) return { content: [{ type: "text", text: "A `branchName` is required for conflict check." }] };
|
|
3092
|
+
const result = await mcpMutation("gitchain.checkConflicts", { chainEntryId, branchName });
|
|
3093
|
+
if (!result.hasConflicts) {
|
|
3094
|
+
return {
|
|
3095
|
+
content: [{
|
|
3096
|
+
type: "text",
|
|
3097
|
+
text: `# No Conflicts
|
|
3098
|
+
|
|
3099
|
+
Branch "${branchName}" on \`${chainEntryId}\` has no conflicts. Safe to merge.`
|
|
3100
|
+
}]
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
const conflictLines = result.conflicts.map(
|
|
3104
|
+
(c) => `- **${c.linkId}** \u2014 modified by: ${c.branches.map((b) => `${b.branchName} (${b.author})`).join(", ")}`
|
|
3105
|
+
).join("\n");
|
|
3106
|
+
return {
|
|
3107
|
+
content: [{
|
|
3503
3108
|
type: "text",
|
|
3504
3109
|
text: `# Conflicts Detected
|
|
3505
3110
|
|
|
@@ -3507,67 +3112,52 @@ Branch "${branchName}" conflicts with other branches on these links:
|
|
|
3507
3112
|
|
|
3508
3113
|
` + conflictLines + `
|
|
3509
3114
|
|
|
3510
|
-
Resolve conflicts before merging
|
|
3511
|
-
}
|
|
3512
|
-
|
|
3513
|
-
}
|
|
3115
|
+
Resolve conflicts before merging.`
|
|
3116
|
+
}]
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
return { content: [{ type: "text", text: "Unknown action." }] };
|
|
3514
3120
|
}
|
|
3515
3121
|
);
|
|
3516
3122
|
server2.registerTool(
|
|
3517
|
-
"chain-
|
|
3123
|
+
"chain-review",
|
|
3518
3124
|
{
|
|
3519
|
-
title: "
|
|
3520
|
-
description: "
|
|
3125
|
+
title: "Chain Review",
|
|
3126
|
+
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.",
|
|
3521
3127
|
inputSchema: {
|
|
3128
|
+
action: z8.enum(["gate", "comment", "resolve-comment", "list-comments"]).describe("Action: run coherence gate, add a comment, resolve a comment, or list comments"),
|
|
3522
3129
|
chainEntryId: z8.string().describe("The chain's entry ID"),
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3130
|
+
commitMessage: z8.string().optional().describe("Commit message to lint (for gate action)"),
|
|
3131
|
+
versionNumber: z8.number().optional().describe("Version to comment on or list comments for"),
|
|
3132
|
+
linkId: z8.string().optional().describe("Link this comment targets (optional for comment)"),
|
|
3133
|
+
body: z8.string().optional().describe("Comment text (required for comment action)"),
|
|
3134
|
+
commentId: z8.string().optional().describe("Comment ID (required for resolve-comment)"),
|
|
3135
|
+
author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
|
|
3136
|
+
},
|
|
3137
|
+
annotations: { readOnlyHint: false }
|
|
3527
3138
|
},
|
|
3528
|
-
async ({ chainEntryId,
|
|
3529
|
-
|
|
3530
|
-
chainEntryId,
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
return {
|
|
3536
|
-
content: [
|
|
3537
|
-
{
|
|
3139
|
+
async ({ action, chainEntryId, commitMessage, versionNumber, linkId, body, commentId, author }) => {
|
|
3140
|
+
if (action === "gate") {
|
|
3141
|
+
const gate = await mcpQuery("gitchain.runGate", { chainEntryId, commitMessage });
|
|
3142
|
+
const checkLines = gate.checks.map((c) => `- ${c.pass ? "PASS" : "FAIL"} **${c.name}**: ${c.detail}`).join("\n");
|
|
3143
|
+
const icon = gate.pass ? "PASS" : "BLOCKED";
|
|
3144
|
+
return {
|
|
3145
|
+
content: [{
|
|
3538
3146
|
type: "text",
|
|
3539
|
-
text: `#
|
|
3147
|
+
text: `# Gate: ${icon}
|
|
3540
3148
|
|
|
3541
|
-
- **
|
|
3542
|
-
- **
|
|
3543
|
-
- **Version:** v${result.version}
|
|
3544
|
-
- **Strategy:** ${result.strategy}
|
|
3149
|
+
- **Score:** ${gate.score}%
|
|
3150
|
+
- **Threshold:** ${gate.threshold}%
|
|
3545
3151
|
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
);
|
|
3552
|
-
server2.registerTool(
|
|
3553
|
-
"chain-comment",
|
|
3554
|
-
{
|
|
3555
|
-
title: "Comment on Chain Version",
|
|
3556
|
-
description: "Add a threaded comment on a specific chain version, optionally targeting a specific link. Comments support threading via parentId. Use chain-comments to see existing comments.",
|
|
3557
|
-
inputSchema: {
|
|
3558
|
-
chainEntryId: z8.string().describe("The chain's entry ID"),
|
|
3559
|
-
action: z8.enum(["add", "resolve", "list"]).describe("'add' a comment, 'resolve' a comment, or 'list' all comments"),
|
|
3560
|
-
versionNumber: z8.number().optional().describe("Version number to comment on (required for 'add')"),
|
|
3561
|
-
linkId: z8.string().optional().describe("Which link this comment targets (optional)"),
|
|
3562
|
-
body: z8.string().optional().describe("Comment text (required for 'add')"),
|
|
3563
|
-
commentId: z8.string().optional().describe("Comment ID to resolve (required for 'resolve')"),
|
|
3564
|
-
author: z8.string().optional().describe("Who is commenting. Defaults to 'mcp'.")
|
|
3152
|
+
## Checks
|
|
3153
|
+
|
|
3154
|
+
${checkLines}`
|
|
3155
|
+
}]
|
|
3156
|
+
};
|
|
3565
3157
|
}
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
if (!versionNumber) throw new Error("versionNumber is required for 'add'");
|
|
3570
|
-
if (!body) throw new Error("body is required for 'add'");
|
|
3158
|
+
if (action === "comment") {
|
|
3159
|
+
if (!versionNumber) return { content: [{ type: "text", text: "A `versionNumber` is required." }] };
|
|
3160
|
+
if (!body) return { content: [{ type: "text", text: "A `body` is required." }] };
|
|
3571
3161
|
const result = await mcpMutation("gitchain.addComment", {
|
|
3572
3162
|
chainEntryId,
|
|
3573
3163
|
versionNumber,
|
|
@@ -3576,92 +3166,47 @@ Main is now at v${result.version}. The branch has been closed.`
|
|
|
3576
3166
|
author
|
|
3577
3167
|
});
|
|
3578
3168
|
return {
|
|
3579
|
-
content: [
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
text: `# Comment Added
|
|
3169
|
+
content: [{
|
|
3170
|
+
type: "text",
|
|
3171
|
+
text: `# Comment Added
|
|
3583
3172
|
|
|
3584
3173
|
- **Chain:** \`${result.chainEntryId}\`
|
|
3585
3174
|
- **Version:** v${result.versionNumber}
|
|
3586
3175
|
` + (result.linkId ? `- **Link:** ${result.linkId}
|
|
3587
|
-
` : "") + `- **Body:** ${body
|
|
3588
|
-
|
|
3589
|
-
]
|
|
3176
|
+
` : "") + `- **Body:** ${body.substring(0, 200)}`
|
|
3177
|
+
}]
|
|
3590
3178
|
};
|
|
3591
3179
|
}
|
|
3592
|
-
if (action === "resolve") {
|
|
3593
|
-
if (!commentId)
|
|
3180
|
+
if (action === "resolve-comment") {
|
|
3181
|
+
if (!commentId) return { content: [{ type: "text", text: "A `commentId` is required." }] };
|
|
3594
3182
|
await mcpMutation("gitchain.resolveComment", { commentId });
|
|
3595
|
-
return {
|
|
3596
|
-
content: [
|
|
3597
|
-
{
|
|
3598
|
-
type: "text",
|
|
3599
|
-
text: `Comment resolved.`
|
|
3600
|
-
}
|
|
3601
|
-
]
|
|
3602
|
-
};
|
|
3183
|
+
return { content: [{ type: "text", text: `Comment resolved.` }] };
|
|
3603
3184
|
}
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3185
|
+
if (action === "list-comments") {
|
|
3186
|
+
const comments = await mcpMutation("gitchain.listComments", {
|
|
3187
|
+
chainEntryId,
|
|
3188
|
+
versionNumber
|
|
3189
|
+
});
|
|
3190
|
+
if (comments.length === 0) {
|
|
3191
|
+
return { content: [{ type: "text", text: `No comments found for chain "${chainEntryId}"${versionNumber ? ` v${versionNumber}` : ""}.` }] };
|
|
3192
|
+
}
|
|
3193
|
+
const formatted = comments.map((c) => {
|
|
3194
|
+
const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
|
|
3195
|
+
const resolved = c.resolved ? " (RESOLVED)" : "";
|
|
3196
|
+
const link = c.linkId ? ` [${c.linkId}]` : "";
|
|
3197
|
+
return `- **v${c.version}${link}** ${date} by ${c.author}${resolved}: ${c.body.substring(0, 150)}`;
|
|
3198
|
+
}).join("\n");
|
|
3199
|
+
const unresolvedCount = comments.filter((c) => !c.resolved).length;
|
|
3609
3200
|
return {
|
|
3610
|
-
content: [
|
|
3611
|
-
{
|
|
3612
|
-
type: "text",
|
|
3613
|
-
text: `No comments found for chain "${chainEntryId}"${versionNumber ? ` v${versionNumber}` : ""}.`
|
|
3614
|
-
}
|
|
3615
|
-
]
|
|
3616
|
-
};
|
|
3617
|
-
}
|
|
3618
|
-
const formatted = comments.map((c) => {
|
|
3619
|
-
const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
|
|
3620
|
-
const resolved = c.resolved ? " (RESOLVED)" : "";
|
|
3621
|
-
const link = c.linkId ? ` [${c.linkId}]` : "";
|
|
3622
|
-
return `- **v${c.version}${link}** ${date} by ${c.author}${resolved}: ${c.body.substring(0, 150)}`;
|
|
3623
|
-
}).join("\n");
|
|
3624
|
-
const unresolvedCount = comments.filter((c) => !c.resolved).length;
|
|
3625
|
-
return {
|
|
3626
|
-
content: [
|
|
3627
|
-
{
|
|
3201
|
+
content: [{
|
|
3628
3202
|
type: "text",
|
|
3629
3203
|
text: `# Comments for ${chainEntryId} (${comments.length}, ${unresolvedCount} unresolved)
|
|
3630
3204
|
|
|
3631
3205
|
${formatted}`
|
|
3632
|
-
}
|
|
3633
|
-
|
|
3634
|
-
};
|
|
3635
|
-
}
|
|
3636
|
-
);
|
|
3637
|
-
server2.registerTool(
|
|
3638
|
-
"chain-revert",
|
|
3639
|
-
{
|
|
3640
|
-
title: "Revert Chain",
|
|
3641
|
-
description: "Safely revert a chain to a previous version. Creates a NEW version that restores the content of the target version \u2014 history is preserved, nothing is destroyed. Use chain-commits to see available versions.",
|
|
3642
|
-
inputSchema: {
|
|
3643
|
-
chainEntryId: z8.string().describe("The chain's entry ID"),
|
|
3644
|
-
toVersion: z8.number().describe("The version number to revert to"),
|
|
3645
|
-
author: z8.string().optional().describe("Who is reverting. Defaults to 'mcp'.")
|
|
3206
|
+
}]
|
|
3207
|
+
};
|
|
3646
3208
|
}
|
|
3647
|
-
|
|
3648
|
-
async ({ chainEntryId, toVersion, author }) => {
|
|
3649
|
-
const result = await mcpMutation("gitchain.revertChain", { chainEntryId, toVersion, author });
|
|
3650
|
-
return {
|
|
3651
|
-
content: [
|
|
3652
|
-
{
|
|
3653
|
-
type: "text",
|
|
3654
|
-
text: `# Reverted
|
|
3655
|
-
|
|
3656
|
-
- **Chain:** \`${result.entryId}\`
|
|
3657
|
-
- **Reverted to:** v${result.revertedTo}
|
|
3658
|
-
- **New version:** v${result.newVersion}
|
|
3659
|
-
- **Links affected:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
|
|
3660
|
-
|
|
3661
|
-
History is preserved \u2014 this created a new version, not a destructive reset.`
|
|
3662
|
-
}
|
|
3663
|
-
]
|
|
3664
|
-
};
|
|
3209
|
+
return { content: [{ type: "text", text: "Unknown action." }] };
|
|
3665
3210
|
}
|
|
3666
3211
|
);
|
|
3667
3212
|
}
|
|
@@ -3706,7 +3251,7 @@ Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, h
|
|
|
3706
3251
|
sections.push(
|
|
3707
3252
|
`## Business Rules
|
|
3708
3253
|
Collection: \`business-rules\` (${rulesCount}).
|
|
3709
|
-
Find rules: \`
|
|
3254
|
+
Find rules: \`search\` for text search, \`list-entries collection=business-rules\` to browse.
|
|
3710
3255
|
Check compliance: use the \`review-against-rules\` prompt (pass a domain).
|
|
3711
3256
|
Draft a new rule: use the \`draft-rule-from-context\` prompt.`
|
|
3712
3257
|
);
|
|
@@ -3723,16 +3268,16 @@ Browse: \`list-entries collection=tracking-events\`. Full setup: \`docs/posthog-
|
|
|
3723
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."
|
|
3724
3269
|
);
|
|
3725
3270
|
sections.push(
|
|
3726
|
-
"## Creating Knowledge\nUse `
|
|
3271
|
+
"## Creating Knowledge\nUse `capture` as the only 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`.\nAll other collections use sensible defaults.\n\nExample: `capture collection='tensions' name='...' description='...'`\n\nUse `quality-check` to score existing entries retroactively.\nUse `update-entry` for post-creation adjustments when you need full field control."
|
|
3727
3272
|
);
|
|
3728
3273
|
sections.push(
|
|
3729
|
-
"## Where to Go Next\n- **Create entry** \u2192 `
|
|
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"
|
|
3730
3275
|
);
|
|
3731
3276
|
return sections.join("\n\n---\n\n");
|
|
3732
3277
|
}
|
|
3733
3278
|
function registerResources(server2) {
|
|
3734
3279
|
server2.resource(
|
|
3735
|
-
"
|
|
3280
|
+
"chain-orientation",
|
|
3736
3281
|
"productbrain://orientation",
|
|
3737
3282
|
async (uri) => {
|
|
3738
3283
|
const [collectionsResult, eventsResult, standardsResult, rulesResult] = await Promise.allSettled([
|
|
@@ -3755,7 +3300,7 @@ function registerResources(server2) {
|
|
|
3755
3300
|
}
|
|
3756
3301
|
);
|
|
3757
3302
|
server2.resource(
|
|
3758
|
-
"
|
|
3303
|
+
"chain-terminology",
|
|
3759
3304
|
"productbrain://terminology",
|
|
3760
3305
|
async (uri) => {
|
|
3761
3306
|
const [glossaryResult, standardsResult] = await Promise.allSettled([
|
|
@@ -3770,7 +3315,7 @@ function registerResources(server2) {
|
|
|
3770
3315
|
|
|
3771
3316
|
${terms}`);
|
|
3772
3317
|
} else {
|
|
3773
|
-
lines.push("## Glossary\n\nNo glossary terms yet. Use `
|
|
3318
|
+
lines.push("## Glossary\n\nNo glossary terms yet. Use `capture` with collection `glossary` to add terms.");
|
|
3774
3319
|
}
|
|
3775
3320
|
} else {
|
|
3776
3321
|
lines.push("## Glossary\n\nCould not load glossary \u2014 use `list-entries collection=glossary` to browse manually.");
|
|
@@ -3782,7 +3327,7 @@ ${terms}`);
|
|
|
3782
3327
|
|
|
3783
3328
|
${stds}`);
|
|
3784
3329
|
} else {
|
|
3785
|
-
lines.push("## Standards\n\nNo standards yet. Use `
|
|
3330
|
+
lines.push("## Standards\n\nNo standards yet. Use `capture` with collection `standards` to add standards.");
|
|
3786
3331
|
}
|
|
3787
3332
|
} else {
|
|
3788
3333
|
lines.push("## Standards\n\nCould not load standards \u2014 use `list-entries collection=standards` to browse manually.");
|
|
@@ -3793,7 +3338,7 @@ ${stds}`);
|
|
|
3793
3338
|
}
|
|
3794
3339
|
);
|
|
3795
3340
|
server2.resource(
|
|
3796
|
-
"
|
|
3341
|
+
"chain-collections",
|
|
3797
3342
|
"productbrain://collections",
|
|
3798
3343
|
async (uri) => {
|
|
3799
3344
|
const collections = await mcpQuery("kb.listCollections");
|
|
@@ -3816,7 +3361,7 @@ ${formatted}`, mimeType: "text/markdown" }]
|
|
|
3816
3361
|
}
|
|
3817
3362
|
);
|
|
3818
3363
|
server2.resource(
|
|
3819
|
-
"
|
|
3364
|
+
"chain-collection-entries",
|
|
3820
3365
|
new ResourceTemplate("productbrain://{slug}/entries", {
|
|
3821
3366
|
list: async () => {
|
|
3822
3367
|
const collections = await mcpQuery("kb.listCollections");
|
|
@@ -3841,7 +3386,7 @@ ${formatted}`, mimeType: "text/markdown" }]
|
|
|
3841
3386
|
}
|
|
3842
3387
|
);
|
|
3843
3388
|
server2.resource(
|
|
3844
|
-
"
|
|
3389
|
+
"chain-labels",
|
|
3845
3390
|
"productbrain://labels",
|
|
3846
3391
|
async (uri) => {
|
|
3847
3392
|
const labels = await mcpQuery("kb.listLabels");
|
|
@@ -3995,7 +3540,7 @@ Structure the decision record with:
|
|
|
3995
3540
|
6. **Alternatives considered**: What else was on the table
|
|
3996
3541
|
7. **Related rules or tensions**: Any business rules or tensions this connects to
|
|
3997
3542
|
|
|
3998
|
-
After drafting, I can log it using the
|
|
3543
|
+
After drafting, I can log it using the capture tool with collection "decisions".`
|
|
3999
3544
|
}
|
|
4000
3545
|
}
|
|
4001
3546
|
]
|
|
@@ -4041,7 +3586,7 @@ Draft the rule with these fields:
|
|
|
4041
3586
|
7. **data.platforms**: Which platforms are affected
|
|
4042
3587
|
8. **data.relatedRules**: Any related existing rules
|
|
4043
3588
|
|
|
4044
|
-
Make sure the rule is consistent with existing rules and doesn't contradict them. After drafting, I can create it using the
|
|
3589
|
+
Make sure the rule is consistent with existing rules and doesn't contradict them. After drafting, I can create it using the capture tool with collection "business-rules".`
|
|
4045
3590
|
}
|
|
4046
3591
|
}
|
|
4047
3592
|
]
|
|
@@ -4092,7 +3637,7 @@ Use one of these IDs to run a workflow.`
|
|
|
4092
3637
|
` + sorted.map((d) => `- ${d.entryId ?? ""}: ${d.name} [${d.status}]`).join("\n");
|
|
4093
3638
|
}
|
|
4094
3639
|
} catch {
|
|
4095
|
-
kbContext = "\n_Could not load
|
|
3640
|
+
kbContext = "\n_Could not load chain context \u2014 proceed without it._";
|
|
4096
3641
|
}
|
|
4097
3642
|
const roundsPlan = wf.rounds.map(
|
|
4098
3643
|
(r) => `### Round ${r.num}: ${r.label}
|
|
@@ -4139,9 +3684,9 @@ ${wf.errorRecovery}
|
|
|
4139
3684
|
|
|
4140
3685
|
---
|
|
4141
3686
|
|
|
4142
|
-
##
|
|
3687
|
+
## Chain Output
|
|
4143
3688
|
|
|
4144
|
-
When complete, use \`
|
|
3689
|
+
When complete, use \`capture\` to create a \`${wf.kbOutputCollection}\` entry.
|
|
4145
3690
|
Name template: ${wf.kbOutputTemplate.nameTemplate}
|
|
4146
3691
|
Description field: ${wf.kbOutputTemplate.descriptionField}
|
|
4147
3692
|
` + kbContext + `
|
|
@@ -4172,7 +3717,7 @@ if (!process.env.CONVEX_SITE_URL && !process.env.PRODUCTBRAIN_API_KEY) {
|
|
|
4172
3717
|
}
|
|
4173
3718
|
}
|
|
4174
3719
|
bootstrapCloudMode();
|
|
4175
|
-
var SERVER_VERSION = "
|
|
3720
|
+
var SERVER_VERSION = "2.0.0";
|
|
4176
3721
|
initAnalytics();
|
|
4177
3722
|
var workspaceId;
|
|
4178
3723
|
try {
|
|
@@ -4199,39 +3744,37 @@ var server = new McpServer2(
|
|
|
4199
3744
|
"Product Brain \u2014 the single source of truth for product knowledge.",
|
|
4200
3745
|
"Terminology, standards, and core data all live here \u2014 no need to check external docs.",
|
|
4201
3746
|
"",
|
|
4202
|
-
"Terminology & naming: For 'what is X?' or naming questions, fetch `productbrain://terminology`",
|
|
4203
|
-
"or use the `name-check` prompt to validate names against the glossary.",
|
|
4204
|
-
"",
|
|
4205
3747
|
"Workflow:",
|
|
4206
3748
|
" 1. Verify: call `health` to confirm connectivity.",
|
|
4207
|
-
" 2. Terminology: fetch `productbrain://terminology` or use `name-check` prompt
|
|
4208
|
-
" 3. Discover: use `
|
|
3749
|
+
" 2. Terminology: fetch `productbrain://terminology` or use `name-check` prompt.",
|
|
3750
|
+
" 3. Discover: use `search` to find entries, or `list-entries` to browse.",
|
|
4209
3751
|
" 4. Drill in: use `get-entry` for full details \u2014 data, labels, relations, history.",
|
|
4210
|
-
" 5.
|
|
4211
|
-
"
|
|
4212
|
-
"
|
|
4213
|
-
"
|
|
4214
|
-
"
|
|
4215
|
-
"
|
|
3752
|
+
" 5. Context: use `gather-context` with an entryId or a task description.",
|
|
3753
|
+
" 6. Capture: use `capture` to create entries \u2014 auto-links and scores in one call.",
|
|
3754
|
+
" 7. Connect: use `suggest-links` then `relate-entries` to build the graph.",
|
|
3755
|
+
" 8. Quality: use `quality-check` to assess entry completeness.",
|
|
3756
|
+
" 9. Labels: use `labels` to list, create, or apply labels.",
|
|
3757
|
+
" 10. Debug: use `mcp-audit` to see what backend calls happened this session.",
|
|
4216
3758
|
"",
|
|
4217
|
-
"Always
|
|
4218
|
-
"Always prefer kb-search or list-entries before get-entry \u2014 discover, then drill in.",
|
|
3759
|
+
"Always discover before drilling in (search or list-entries before get-entry).",
|
|
4219
3760
|
"",
|
|
4220
3761
|
"Orientation:",
|
|
4221
|
-
"
|
|
4222
|
-
"
|
|
4223
|
-
" It gives you the map. Then use the appropriate tool to drill in."
|
|
3762
|
+
" Fetch `productbrain://orientation` to understand architecture, data model, rules,",
|
|
3763
|
+
" and analytics. It is the map. Then use tools to drill in."
|
|
4224
3764
|
].join("\n")
|
|
4225
3765
|
}
|
|
4226
3766
|
);
|
|
3767
|
+
var enabledModules = new Set(
|
|
3768
|
+
(process.env.PB_MODULES ?? "core,gitchain").split(",").map((m) => m.trim().toLowerCase())
|
|
3769
|
+
);
|
|
4227
3770
|
registerKnowledgeTools(server);
|
|
4228
3771
|
registerLabelTools(server);
|
|
4229
3772
|
registerHealthTools(server);
|
|
4230
3773
|
registerVerifyTools(server);
|
|
4231
3774
|
registerSmartCaptureTools(server);
|
|
4232
|
-
registerArchitectureTools(server);
|
|
4233
3775
|
registerWorkflowTools(server);
|
|
4234
|
-
registerGitChainTools(server);
|
|
3776
|
+
if (enabledModules.has("gitchain")) registerGitChainTools(server);
|
|
3777
|
+
if (enabledModules.has("arch")) registerArchitectureTools(server);
|
|
4235
3778
|
registerResources(server);
|
|
4236
3779
|
registerPrompts(server);
|
|
4237
3780
|
var transport = new StdioServerTransport();
|