@unified-product-graph/mcp-server 0.9.0 → 0.9.1
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/TOOLS.md +62 -5
- package/dist/index.js +459 -87
- package/dist/index.js.map +1 -1
- package/dist/tools-manifest.json +133 -21
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
7
|
-
import { UPGFileStore } from "@unified-product-graph/sdk";
|
|
5
|
+
import * as fs4 from "fs/promises";
|
|
6
|
+
import * as path7 from "path";
|
|
7
|
+
import { UPGFileStore as UPGFileStore2 } from "@unified-product-graph/sdk";
|
|
8
8
|
|
|
9
9
|
// src/server.ts
|
|
10
10
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
-
import
|
|
12
|
+
import fs3 from "fs";
|
|
13
13
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14
|
-
import * as
|
|
14
|
+
import * as path6 from "path";
|
|
15
15
|
import {
|
|
16
16
|
CallToolRequestSchema,
|
|
17
17
|
ListToolsRequestSchema
|
|
@@ -2741,7 +2741,8 @@ var UPG_CROSS_EDGE_TYPES = [
|
|
|
2741
2741
|
"depends_on_product",
|
|
2742
2742
|
"cannibalises",
|
|
2743
2743
|
"succeeds",
|
|
2744
|
-
"hosts"
|
|
2744
|
+
"hosts",
|
|
2745
|
+
"contributes_to"
|
|
2745
2746
|
];
|
|
2746
2747
|
var TYPES = getTypes();
|
|
2747
2748
|
var TYPES_SET = new Set(TYPES);
|
|
@@ -24646,7 +24647,7 @@ function serializePortfolioWithHeader(doc, opts) {
|
|
|
24646
24647
|
header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
|
|
24647
24648
|
return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
|
|
24648
24649
|
}
|
|
24649
|
-
var UPG_VERSION = "0.9.
|
|
24650
|
+
var UPG_VERSION = "0.9.1";
|
|
24650
24651
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
24651
24652
|
var UPG_TYPES = getTypes();
|
|
24652
24653
|
var UPG_TYPES_SET = new Set(UPG_TYPES);
|
|
@@ -26849,6 +26850,38 @@ import {
|
|
|
26849
26850
|
WorkspaceAlreadyExistsError,
|
|
26850
26851
|
WorkspaceNotInitialisedError
|
|
26851
26852
|
} from "@unified-product-graph/sdk";
|
|
26853
|
+
function isExistingFile(p) {
|
|
26854
|
+
try {
|
|
26855
|
+
return fs.statSync(p).isFile();
|
|
26856
|
+
} catch {
|
|
26857
|
+
return false;
|
|
26858
|
+
}
|
|
26859
|
+
}
|
|
26860
|
+
function findWorkspaceUpgFiles(cwd) {
|
|
26861
|
+
const candidates = [];
|
|
26862
|
+
let topEntries;
|
|
26863
|
+
try {
|
|
26864
|
+
topEntries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
26865
|
+
} catch {
|
|
26866
|
+
return candidates;
|
|
26867
|
+
}
|
|
26868
|
+
for (const entry of topEntries) {
|
|
26869
|
+
if (entry.isFile() && entry.name.endsWith(".upg")) {
|
|
26870
|
+
candidates.push(path3.join(cwd, entry.name));
|
|
26871
|
+
} else if (entry.isDirectory() && (entry.name === ".upg" || !entry.name.startsWith("."))) {
|
|
26872
|
+
try {
|
|
26873
|
+
const subEntries = fs.readdirSync(path3.join(cwd, entry.name), { withFileTypes: true });
|
|
26874
|
+
for (const sub of subEntries) {
|
|
26875
|
+
if (sub.isFile() && sub.name.endsWith(".upg")) {
|
|
26876
|
+
candidates.push(path3.join(cwd, entry.name, sub.name));
|
|
26877
|
+
}
|
|
26878
|
+
}
|
|
26879
|
+
} catch {
|
|
26880
|
+
}
|
|
26881
|
+
}
|
|
26882
|
+
}
|
|
26883
|
+
return candidates;
|
|
26884
|
+
}
|
|
26852
26885
|
var listLocalProducts = (_args, _ctx) => {
|
|
26853
26886
|
const cwd = process.cwd();
|
|
26854
26887
|
const products = [];
|
|
@@ -26871,26 +26904,7 @@ var listLocalProducts = (_args, _ctx) => {
|
|
|
26871
26904
|
}
|
|
26872
26905
|
} catch {
|
|
26873
26906
|
}
|
|
26874
|
-
const candidates =
|
|
26875
|
-
const topEntries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
26876
|
-
for (const entry of topEntries) {
|
|
26877
|
-
if (entry.isFile() && entry.name.endsWith(".upg")) {
|
|
26878
|
-
candidates.push(path3.join(cwd, entry.name));
|
|
26879
|
-
} else if (entry.isDirectory() && (entry.name === ".upg" || !entry.name.startsWith("."))) {
|
|
26880
|
-
try {
|
|
26881
|
-
const subEntries = fs.readdirSync(
|
|
26882
|
-
path3.join(cwd, entry.name),
|
|
26883
|
-
{ withFileTypes: true }
|
|
26884
|
-
);
|
|
26885
|
-
for (const sub of subEntries) {
|
|
26886
|
-
if (sub.isFile() && sub.name.endsWith(".upg")) {
|
|
26887
|
-
candidates.push(path3.join(cwd, entry.name, sub.name));
|
|
26888
|
-
}
|
|
26889
|
-
}
|
|
26890
|
-
} catch {
|
|
26891
|
-
}
|
|
26892
|
-
}
|
|
26893
|
-
}
|
|
26907
|
+
const candidates = findWorkspaceUpgFiles(cwd);
|
|
26894
26908
|
for (const filePath of candidates) {
|
|
26895
26909
|
try {
|
|
26896
26910
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
@@ -26920,21 +26934,19 @@ var switchProduct = async (args, ctx) => {
|
|
|
26920
26934
|
if (typeof fileArg !== "string" || fileArg.length === 0) {
|
|
26921
26935
|
return textError("Missing required parameter: file (alias: product). Pass a .upg path or a bare product name.");
|
|
26922
26936
|
}
|
|
26923
|
-
|
|
26924
|
-
|
|
26925
|
-
|
|
26926
|
-
|
|
26927
|
-
|
|
26928
|
-
|
|
26929
|
-
|
|
26930
|
-
|
|
26931
|
-
|
|
26932
|
-
|
|
26933
|
-
|
|
26934
|
-
|
|
26935
|
-
|
|
26936
|
-
);
|
|
26937
|
-
}
|
|
26937
|
+
const cwd = process.cwd();
|
|
26938
|
+
const direct = path3.resolve(fileArg);
|
|
26939
|
+
const candidates = [
|
|
26940
|
+
path3.join(cwd, ".upg", fileArg),
|
|
26941
|
+
path3.join(cwd, ".upg", `${fileArg}.upg`),
|
|
26942
|
+
direct,
|
|
26943
|
+
`${direct}.upg`
|
|
26944
|
+
];
|
|
26945
|
+
const resolved = candidates.find(isExistingFile);
|
|
26946
|
+
if (!resolved) {
|
|
26947
|
+
return textError(
|
|
26948
|
+
`File not found: ${direct} (also checked .upg/${fileArg} and .upg/${fileArg}.upg). Pass a .upg path or a bare product name from list_local_products.`
|
|
26949
|
+
);
|
|
26938
26950
|
}
|
|
26939
26951
|
try {
|
|
26940
26952
|
await store.flush();
|
|
@@ -27419,6 +27431,312 @@ var batchCreateCrossProductEdges = async (args, _ctx) => {
|
|
|
27419
27431
|
);
|
|
27420
27432
|
};
|
|
27421
27433
|
|
|
27434
|
+
// src/tools/portfolio-read.ts
|
|
27435
|
+
import * as path4 from "path";
|
|
27436
|
+
import * as fs2 from "fs";
|
|
27437
|
+
import { UPGFileStore, computeGraphDigest as computeGraphDigest2 } from "@unified-product-graph/sdk";
|
|
27438
|
+
|
|
27439
|
+
// src/lib/graph-traverse.ts
|
|
27440
|
+
function traverseGraph(reader, params) {
|
|
27441
|
+
const fromType = params.from;
|
|
27442
|
+
const fromId = params.from_id;
|
|
27443
|
+
if (!fromType && !fromId) {
|
|
27444
|
+
return { ok: false, error: 'Provide either "from" (entity type) or "from_id" (node ID)' };
|
|
27445
|
+
}
|
|
27446
|
+
const traverseEdgeTypes = params.traverse;
|
|
27447
|
+
const maxDepth = Math.min(Math.max(params.depth ?? 3, 1), 10);
|
|
27448
|
+
const maxNodes = Math.min(Math.max(params.limit ?? 200, 1), 1e3);
|
|
27449
|
+
const includeFields = new Set(params.include ?? ["title", "status", "type"]);
|
|
27450
|
+
includeFields.add("id");
|
|
27451
|
+
includeFields.add("type");
|
|
27452
|
+
let startNodes;
|
|
27453
|
+
if (fromId) {
|
|
27454
|
+
const node = reader.getNode(fromId);
|
|
27455
|
+
if (!node) return { ok: false, error: `Node not found: ${fromId}` };
|
|
27456
|
+
startNodes = [node];
|
|
27457
|
+
} else {
|
|
27458
|
+
startNodes = reader.getAllNodes().filter((n) => n.type === fromType);
|
|
27459
|
+
}
|
|
27460
|
+
if (startNodes.length === 0) {
|
|
27461
|
+
return { ok: true, result: { nodes: [], edges: [], total_nodes: 0, total_edges: 0, truncated: false } };
|
|
27462
|
+
}
|
|
27463
|
+
const visited = /* @__PURE__ */ new Set();
|
|
27464
|
+
const collectedNodes = [];
|
|
27465
|
+
const collectedEdges = /* @__PURE__ */ new Map();
|
|
27466
|
+
const queue = [];
|
|
27467
|
+
let truncated = false;
|
|
27468
|
+
let maxDepthReached = 0;
|
|
27469
|
+
for (const n of startNodes) {
|
|
27470
|
+
if (collectedNodes.length >= maxNodes) {
|
|
27471
|
+
truncated = true;
|
|
27472
|
+
break;
|
|
27473
|
+
}
|
|
27474
|
+
visited.add(n.id);
|
|
27475
|
+
collectedNodes.push(n);
|
|
27476
|
+
queue.push({ id: n.id, level: 0 });
|
|
27477
|
+
}
|
|
27478
|
+
while (queue.length > 0) {
|
|
27479
|
+
if (collectedNodes.length >= maxNodes) {
|
|
27480
|
+
truncated = true;
|
|
27481
|
+
break;
|
|
27482
|
+
}
|
|
27483
|
+
const { id, level } = queue.shift();
|
|
27484
|
+
if (level > maxDepthReached) maxDepthReached = level;
|
|
27485
|
+
if (level >= maxDepth) continue;
|
|
27486
|
+
const edges = reader.getEdgesForNode(id);
|
|
27487
|
+
for (const edge of edges) {
|
|
27488
|
+
if (edge.source !== id) continue;
|
|
27489
|
+
if (traverseEdgeTypes && traverseEdgeTypes.length > 0) {
|
|
27490
|
+
const edgeTypeForLevel = level < traverseEdgeTypes.length ? traverseEdgeTypes[level] : traverseEdgeTypes[traverseEdgeTypes.length - 1];
|
|
27491
|
+
if (edgeTypeForLevel.startsWith("!")) {
|
|
27492
|
+
if (edge.type === edgeTypeForLevel.slice(1)) continue;
|
|
27493
|
+
} else {
|
|
27494
|
+
if (edge.type !== edgeTypeForLevel) continue;
|
|
27495
|
+
}
|
|
27496
|
+
}
|
|
27497
|
+
collectedEdges.set(edge.id, edge);
|
|
27498
|
+
const neighborId = edge.target;
|
|
27499
|
+
if (!visited.has(neighborId)) {
|
|
27500
|
+
visited.add(neighborId);
|
|
27501
|
+
const neighbor = reader.getNode(neighborId);
|
|
27502
|
+
if (neighbor) {
|
|
27503
|
+
if (collectedNodes.length >= maxNodes) {
|
|
27504
|
+
truncated = true;
|
|
27505
|
+
break;
|
|
27506
|
+
}
|
|
27507
|
+
collectedNodes.push(neighbor);
|
|
27508
|
+
queue.push({ id: neighborId, level: level + 1 });
|
|
27509
|
+
}
|
|
27510
|
+
}
|
|
27511
|
+
}
|
|
27512
|
+
}
|
|
27513
|
+
const propInclude = params.property_include;
|
|
27514
|
+
const propFilter = propInclude && propInclude.length > 0 ? new Set(propInclude) : null;
|
|
27515
|
+
const projectedNodes = collectedNodes.map((n) => {
|
|
27516
|
+
const projected = { id: n.id, type: n.type };
|
|
27517
|
+
if (includeFields.has("title")) projected.title = n.title;
|
|
27518
|
+
if (includeFields.has("status")) projected.status = n.status;
|
|
27519
|
+
if (includeFields.has("tags")) projected.tags = n.tags;
|
|
27520
|
+
if (includeFields.has("description")) projected.description = n.description;
|
|
27521
|
+
if (includeFields.has("properties")) {
|
|
27522
|
+
if (propFilter && n.properties) {
|
|
27523
|
+
const filtered = {};
|
|
27524
|
+
for (const key of propFilter) {
|
|
27525
|
+
if (key in n.properties) filtered[key] = n.properties[key];
|
|
27526
|
+
}
|
|
27527
|
+
projected.properties = filtered;
|
|
27528
|
+
} else {
|
|
27529
|
+
projected.properties = n.properties;
|
|
27530
|
+
}
|
|
27531
|
+
}
|
|
27532
|
+
return projected;
|
|
27533
|
+
});
|
|
27534
|
+
const edgeInclude = params.edge_include;
|
|
27535
|
+
let edgeArray;
|
|
27536
|
+
if (edgeInclude !== void 0 && edgeInclude.length === 0) {
|
|
27537
|
+
edgeArray = [];
|
|
27538
|
+
} else {
|
|
27539
|
+
const edgeFields = edgeInclude ? new Set(edgeInclude) : null;
|
|
27540
|
+
edgeArray = [...collectedEdges.values()].map((e) => {
|
|
27541
|
+
if (!edgeFields) return { id: e.id, type: e.type, source: e.source, target: e.target };
|
|
27542
|
+
const projected = {};
|
|
27543
|
+
if (edgeFields.has("id")) projected.id = e.id;
|
|
27544
|
+
if (edgeFields.has("type")) projected.type = e.type;
|
|
27545
|
+
if (edgeFields.has("source")) projected.source = e.source;
|
|
27546
|
+
if (edgeFields.has("target")) projected.target = e.target;
|
|
27547
|
+
return projected;
|
|
27548
|
+
});
|
|
27549
|
+
}
|
|
27550
|
+
const result = {
|
|
27551
|
+
nodes: projectedNodes,
|
|
27552
|
+
edges: edgeArray,
|
|
27553
|
+
total_nodes: projectedNodes.length,
|
|
27554
|
+
total_edges: edgeArray.length,
|
|
27555
|
+
truncated
|
|
27556
|
+
};
|
|
27557
|
+
if (truncated) result.truncated_at_depth = maxDepthReached;
|
|
27558
|
+
return { ok: true, result };
|
|
27559
|
+
}
|
|
27560
|
+
|
|
27561
|
+
// src/tools/portfolio-read.ts
|
|
27562
|
+
function resolveScopedProducts(cwd, scope) {
|
|
27563
|
+
const all = [];
|
|
27564
|
+
for (const absPath of findWorkspaceUpgFiles(cwd)) {
|
|
27565
|
+
try {
|
|
27566
|
+
const doc = JSON.parse(fs2.readFileSync(absPath, "utf-8"));
|
|
27567
|
+
if (!doc.product) continue;
|
|
27568
|
+
all.push({
|
|
27569
|
+
id: doc.product.id ?? null,
|
|
27570
|
+
title: doc.product.title ?? "(untitled)",
|
|
27571
|
+
file: path4.relative(cwd, absPath),
|
|
27572
|
+
absPath
|
|
27573
|
+
});
|
|
27574
|
+
} catch {
|
|
27575
|
+
}
|
|
27576
|
+
}
|
|
27577
|
+
if (!scope || scope.length === 0) {
|
|
27578
|
+
return { products: all, unmatched: [] };
|
|
27579
|
+
}
|
|
27580
|
+
const matches = (p, want) => p.id === want || p.file === want || path4.basename(p.file) === want || path4.basename(p.file, ".upg") === want;
|
|
27581
|
+
const products = all.filter((p) => scope.some((want) => matches(p, want)));
|
|
27582
|
+
const unmatched = scope.filter((want) => !all.some((p) => matches(p, want)));
|
|
27583
|
+
return { products, unmatched };
|
|
27584
|
+
}
|
|
27585
|
+
async function readerFor(product, activeStore) {
|
|
27586
|
+
const activePath = activeStore.getFilePath();
|
|
27587
|
+
if (activePath && path4.resolve(activePath) === path4.resolve(product.absPath)) {
|
|
27588
|
+
return { reader: activeStore, store: activeStore, active: true };
|
|
27589
|
+
}
|
|
27590
|
+
const store = new UPGFileStore();
|
|
27591
|
+
await store.loadReadOnly(product.absPath);
|
|
27592
|
+
return { reader: store, store, active: false };
|
|
27593
|
+
}
|
|
27594
|
+
var portfolioQuery = async (args, ctx) => {
|
|
27595
|
+
const { store } = ctx;
|
|
27596
|
+
const from = args.from;
|
|
27597
|
+
const fromId = args.from_id;
|
|
27598
|
+
if (!from && !fromId) {
|
|
27599
|
+
return textError('Provide either "from" (entity type) or "from_id" (node ID)');
|
|
27600
|
+
}
|
|
27601
|
+
const scope = args.scope;
|
|
27602
|
+
const cwd = process.cwd();
|
|
27603
|
+
const { products, unmatched } = resolveScopedProducts(cwd, scope);
|
|
27604
|
+
if (products.length === 0) {
|
|
27605
|
+
return text(
|
|
27606
|
+
JSON.stringify(
|
|
27607
|
+
{
|
|
27608
|
+
products: [],
|
|
27609
|
+
products_searched: 0,
|
|
27610
|
+
products_with_matches: 0,
|
|
27611
|
+
empty_products: [],
|
|
27612
|
+
...unmatched.length > 0 ? { unmatched_scope: unmatched } : {},
|
|
27613
|
+
note: scope && scope.length > 0 ? "No workspace products matched the requested scope." : "No products found in the workspace. Run from a directory with a .upg/ workspace."
|
|
27614
|
+
},
|
|
27615
|
+
null,
|
|
27616
|
+
2
|
|
27617
|
+
)
|
|
27618
|
+
);
|
|
27619
|
+
}
|
|
27620
|
+
const perProductLimit = Math.min(Math.max(args.limit ?? 100, 1), 1e3);
|
|
27621
|
+
const params = {
|
|
27622
|
+
from,
|
|
27623
|
+
from_id: fromId,
|
|
27624
|
+
traverse: args.traverse,
|
|
27625
|
+
depth: args.depth,
|
|
27626
|
+
limit: perProductLimit,
|
|
27627
|
+
include: args.include,
|
|
27628
|
+
edge_include: args.edge_include,
|
|
27629
|
+
property_include: args.property_include
|
|
27630
|
+
};
|
|
27631
|
+
const matched = [];
|
|
27632
|
+
const emptyProducts = [];
|
|
27633
|
+
const errored = [];
|
|
27634
|
+
let totalNodes = 0;
|
|
27635
|
+
let totalEdges = 0;
|
|
27636
|
+
for (const product of products) {
|
|
27637
|
+
let reader;
|
|
27638
|
+
try {
|
|
27639
|
+
;
|
|
27640
|
+
({ reader } = await readerFor(product, store));
|
|
27641
|
+
} catch (err) {
|
|
27642
|
+
errored.push({ product_id: product.id, file: product.file, error: err.message });
|
|
27643
|
+
continue;
|
|
27644
|
+
}
|
|
27645
|
+
const outcome = traverseGraph(reader, params);
|
|
27646
|
+
if (!outcome.ok) {
|
|
27647
|
+
emptyProducts.push(product.id ?? product.file);
|
|
27648
|
+
continue;
|
|
27649
|
+
}
|
|
27650
|
+
const r = outcome.result;
|
|
27651
|
+
if (r.total_nodes === 0) {
|
|
27652
|
+
emptyProducts.push(product.id ?? product.file);
|
|
27653
|
+
continue;
|
|
27654
|
+
}
|
|
27655
|
+
totalNodes += r.total_nodes;
|
|
27656
|
+
totalEdges += r.total_edges;
|
|
27657
|
+
matched.push({
|
|
27658
|
+
product_id: product.id,
|
|
27659
|
+
file: product.file,
|
|
27660
|
+
title: product.title,
|
|
27661
|
+
total_nodes: r.total_nodes,
|
|
27662
|
+
total_edges: r.total_edges,
|
|
27663
|
+
nodes: r.nodes,
|
|
27664
|
+
edges: r.edges,
|
|
27665
|
+
...r.truncated ? { truncated: true, truncated_at_depth: r.truncated_at_depth } : {}
|
|
27666
|
+
});
|
|
27667
|
+
}
|
|
27668
|
+
const guard = preflightPayload({
|
|
27669
|
+
toolName: "portfolio_query",
|
|
27670
|
+
nodeCount: totalNodes,
|
|
27671
|
+
edgeCount: totalEdges,
|
|
27672
|
+
compactEdges: true,
|
|
27673
|
+
argsHint: `from=${from ?? fromId}, products=${matched.length}, limit=${perProductLimit}`
|
|
27674
|
+
});
|
|
27675
|
+
if (guard.kind === "refuse") return guard.result;
|
|
27676
|
+
const response = {
|
|
27677
|
+
products: matched,
|
|
27678
|
+
products_searched: products.length,
|
|
27679
|
+
products_with_matches: matched.length,
|
|
27680
|
+
total_nodes: totalNodes,
|
|
27681
|
+
total_edges: totalEdges,
|
|
27682
|
+
empty_products: emptyProducts
|
|
27683
|
+
};
|
|
27684
|
+
if (errored.length > 0) response.errored_products = errored;
|
|
27685
|
+
if (unmatched.length > 0) response.unmatched_scope = unmatched;
|
|
27686
|
+
if (guard.kind === "warn") Object.assign(response, guard.fields);
|
|
27687
|
+
return text(JSON.stringify(response, null, 2));
|
|
27688
|
+
};
|
|
27689
|
+
var portfolioDigest = async (args, ctx) => {
|
|
27690
|
+
const { store } = ctx;
|
|
27691
|
+
const scope = args.scope;
|
|
27692
|
+
const cwd = process.cwd();
|
|
27693
|
+
const { products, unmatched } = resolveScopedProducts(cwd, scope);
|
|
27694
|
+
const summaries = [];
|
|
27695
|
+
const errored = [];
|
|
27696
|
+
const byStage = {};
|
|
27697
|
+
let totalNodes = 0;
|
|
27698
|
+
let totalEdges = 0;
|
|
27699
|
+
for (const product of products) {
|
|
27700
|
+
try {
|
|
27701
|
+
const { store: reader } = await readerFor(product, store);
|
|
27702
|
+
const digest = computeGraphDigest2(reader);
|
|
27703
|
+
const stage = digest.product.stage || "unset";
|
|
27704
|
+
byStage[stage] = (byStage[stage] ?? 0) + 1;
|
|
27705
|
+
totalNodes += digest.counts.total_nodes;
|
|
27706
|
+
totalEdges += digest.counts.total_edges;
|
|
27707
|
+
const topTypes = Object.entries(digest.counts.by_type).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([type, count]) => ({ type, count }));
|
|
27708
|
+
summaries.push({
|
|
27709
|
+
product_id: product.id,
|
|
27710
|
+
file: product.file,
|
|
27711
|
+
title: digest.product.title,
|
|
27712
|
+
stage: digest.product.stage || null,
|
|
27713
|
+
total_nodes: digest.counts.total_nodes,
|
|
27714
|
+
total_edges: digest.counts.total_edges,
|
|
27715
|
+
health: digest.health,
|
|
27716
|
+
coverage_pct: digest.coverage.stage_summary?.overall_pct ?? null,
|
|
27717
|
+
top_types: topTypes
|
|
27718
|
+
});
|
|
27719
|
+
} catch (err) {
|
|
27720
|
+
errored.push({ product_id: product.id, file: product.file, error: err.message });
|
|
27721
|
+
}
|
|
27722
|
+
}
|
|
27723
|
+
const response = {
|
|
27724
|
+
products: summaries,
|
|
27725
|
+
rollup: {
|
|
27726
|
+
products: summaries.length,
|
|
27727
|
+
total_nodes: totalNodes,
|
|
27728
|
+
total_edges: totalEdges,
|
|
27729
|
+
by_stage: byStage
|
|
27730
|
+
}
|
|
27731
|
+
};
|
|
27732
|
+
if (errored.length > 0) response.errored_products = errored;
|
|
27733
|
+
if (unmatched.length > 0) response.unmatched_scope = unmatched;
|
|
27734
|
+
if (products.length === 0) {
|
|
27735
|
+
response.note = scope && scope.length > 0 ? "No workspace products matched the requested scope." : "No products found in the workspace. Run from a directory with a .upg/ workspace.";
|
|
27736
|
+
}
|
|
27737
|
+
return text(JSON.stringify(response, null, 2));
|
|
27738
|
+
};
|
|
27739
|
+
|
|
27422
27740
|
// src/tools/schema.ts
|
|
27423
27741
|
var getEntitySchema = (args, _ctx) => {
|
|
27424
27742
|
const rawType = args.type;
|
|
@@ -28120,24 +28438,24 @@ var prioritise = (args, ctx) => {
|
|
|
28120
28438
|
};
|
|
28121
28439
|
var trace = (args, ctx) => {
|
|
28122
28440
|
const anchor = args.anchor;
|
|
28123
|
-
const
|
|
28441
|
+
const path8 = args.path;
|
|
28124
28442
|
const edgesOverride = args.edges_override;
|
|
28125
28443
|
if (!anchor) {
|
|
28126
28444
|
return textError("Missing required parameter: anchor (entity_id)");
|
|
28127
28445
|
}
|
|
28128
|
-
if (!
|
|
28446
|
+
if (!path8 || !Array.isArray(path8) || path8.length === 0) {
|
|
28129
28447
|
return textError("Missing required parameter: path (UPGEntityType[])");
|
|
28130
28448
|
}
|
|
28131
|
-
if (edgesOverride && edgesOverride.length !==
|
|
28449
|
+
if (edgesOverride && edgesOverride.length !== path8.length) {
|
|
28132
28450
|
return textError(
|
|
28133
|
-
`edges_override length (${edgesOverride.length}) must match path length (${
|
|
28451
|
+
`edges_override length (${edgesOverride.length}) must match path length (${path8.length})`
|
|
28134
28452
|
);
|
|
28135
28453
|
}
|
|
28136
|
-
const result = executeTrace(ctx.store, anchor,
|
|
28454
|
+
const result = executeTrace(ctx.store, anchor, path8, edgesOverride);
|
|
28137
28455
|
const payload = {
|
|
28138
28456
|
params: {
|
|
28139
28457
|
anchor,
|
|
28140
|
-
path:
|
|
28458
|
+
path: path8,
|
|
28141
28459
|
edges_override: edgesOverride ?? null
|
|
28142
28460
|
},
|
|
28143
28461
|
trail: result.trail,
|
|
@@ -28733,7 +29051,7 @@ var migrateStatus = (args, ctx) => {
|
|
|
28733
29051
|
|
|
28734
29052
|
// src/tools/sync.ts
|
|
28735
29053
|
import * as fsp4 from "fs/promises";
|
|
28736
|
-
import * as
|
|
29054
|
+
import * as path5 from "path";
|
|
28737
29055
|
import { nodeId, edgeId as edgeId4 } from "@unified-product-graph/sdk";
|
|
28738
29056
|
var getSyncState = async (_args, ctx) => {
|
|
28739
29057
|
const { store, sync } = ctx;
|
|
@@ -28901,7 +29219,7 @@ var pushToCloud = async (args, ctx) => {
|
|
|
28901
29219
|
const productId = args.product_id;
|
|
28902
29220
|
if (!cloudEndpoint || !apiKey) {
|
|
28903
29221
|
try {
|
|
28904
|
-
const mcpConfigPath =
|
|
29222
|
+
const mcpConfigPath = path5.join(process.cwd(), ".mcp.json");
|
|
28905
29223
|
const mcpRaw = await fsp4.readFile(mcpConfigPath, "utf-8");
|
|
28906
29224
|
const mcpConfig = JSON.parse(mcpRaw);
|
|
28907
29225
|
const upgCloud = mcpConfig.mcpServers?.["upg-cloud"];
|
|
@@ -28986,8 +29304,8 @@ var pushToCloud = async (args, ctx) => {
|
|
|
28986
29304
|
};
|
|
28987
29305
|
|
|
28988
29306
|
// src/tools/skills.ts
|
|
28989
|
-
import { existsSync as existsSync2, lstatSync, readlinkSync, readFileSync as
|
|
28990
|
-
import { join as join5, resolve as
|
|
29307
|
+
import { existsSync as existsSync2, lstatSync, readlinkSync, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync } from "fs";
|
|
29308
|
+
import { join as join5, resolve as resolve3, dirname as dirname3 } from "path";
|
|
28991
29309
|
import { fileURLToPath } from "url";
|
|
28992
29310
|
function repoRoot() {
|
|
28993
29311
|
return process.cwd();
|
|
@@ -29016,7 +29334,7 @@ function resolveBundledSkillsDir() {
|
|
|
29016
29334
|
} catch {
|
|
29017
29335
|
md = process.cwd();
|
|
29018
29336
|
}
|
|
29019
|
-
for (const c of [
|
|
29337
|
+
for (const c of [resolve3(md, "..", "skills"), resolve3(md, "..", "..", "skills"), resolve3(md, "skills")]) {
|
|
29020
29338
|
if (isSkillsDir(c)) return c;
|
|
29021
29339
|
}
|
|
29022
29340
|
let dir = md;
|
|
@@ -29030,12 +29348,12 @@ function resolveBundledSkillsDir() {
|
|
|
29030
29348
|
return null;
|
|
29031
29349
|
}
|
|
29032
29350
|
function sourceSkillsDir() {
|
|
29033
|
-
const cwdPath =
|
|
29351
|
+
const cwdPath = resolve3(repoRoot(), "packages/upg-mcp-server/skills");
|
|
29034
29352
|
if (existsSync2(cwdPath)) return cwdPath;
|
|
29035
29353
|
return resolveBundledSkillsDir() ?? cwdPath;
|
|
29036
29354
|
}
|
|
29037
29355
|
function deployedSkillsDir() {
|
|
29038
|
-
return
|
|
29356
|
+
return resolve3(repoRoot(), ".claude/skills");
|
|
29039
29357
|
}
|
|
29040
29358
|
function parseFrontmatter(body) {
|
|
29041
29359
|
if (!body.startsWith("---\n")) return null;
|
|
@@ -29079,11 +29397,11 @@ function auditOne(name) {
|
|
|
29079
29397
|
let deployedFrontmatter = null;
|
|
29080
29398
|
let deployedFirstHeading = null;
|
|
29081
29399
|
if (deployedExists) {
|
|
29082
|
-
const deployedBody =
|
|
29400
|
+
const deployedBody = readFileSync3(deployedPath, "utf8");
|
|
29083
29401
|
deployedFrontmatter = parseFrontmatter(deployedBody);
|
|
29084
29402
|
deployedFirstHeading = firstHeading(deployedBody);
|
|
29085
29403
|
if (sourceExists) {
|
|
29086
|
-
const sourceBody =
|
|
29404
|
+
const sourceBody = readFileSync3(sourcePath, "utf8");
|
|
29087
29405
|
inSync = deployedBody === sourceBody;
|
|
29088
29406
|
if (!inSync) {
|
|
29089
29407
|
issues.push("Deployed SKILL.md differs from canonical source; symlink is stale or broken");
|
|
@@ -30044,7 +30362,7 @@ var TOOL_DEFINITIONS = [
|
|
|
30044
30362
|
},
|
|
30045
30363
|
{
|
|
30046
30364
|
name: "list_cross_edge_types",
|
|
30047
|
-
description: "List the canonical cross-product edge types from `UPG_CROSS_EDGE_TYPES`: `shares_persona`, `shares_competitor`, `shares_metric`, `depends_on_product`, `cannibalises`, `succeeds`, `hosts`. Portfolio-level relationships across products. Distinct from the within-product `UPG_EDGE_CATALOG`.",
|
|
30365
|
+
description: "List the canonical cross-product edge types from `UPG_CROSS_EDGE_TYPES`: `shares_persona`, `shares_competitor`, `shares_metric`, `depends_on_product`, `cannibalises`, `succeeds`, `hosts`, `contributes_to`. Portfolio-level relationships across products. Distinct from the within-product `UPG_EDGE_CATALOG`.",
|
|
30048
30366
|
inputSchema: { type: "object", properties: {} }
|
|
30049
30367
|
},
|
|
30050
30368
|
{
|
|
@@ -30525,7 +30843,7 @@ var TOOL_DEFINITIONS = [
|
|
|
30525
30843
|
},
|
|
30526
30844
|
{
|
|
30527
30845
|
name: "create_cross_product_edge",
|
|
30528
|
-
description: "Create a cross-product relationship between two entities in different products within a portfolio graph. Types: `shares_persona`, `shares_competitor`, `shares_metric`, `depends_on_product`, `cannibalises`, `succeeds`, `hosts` (host product runs the hosted product inside itself, directed host to hosted).",
|
|
30846
|
+
description: "Create a cross-product relationship between two entities in different products within a portfolio graph. Types: `shares_persona`, `shares_competitor`, `shares_metric`, `depends_on_product`, `cannibalises`, `succeeds`, `hosts` (host product runs the hosted product inside itself, directed host to hosted), `contributes_to` (a product strategy entity rolls up to a higher-level one, e.g. product objective \u2192 company objective, product key_result \u2192 company key_result; directed subordinate to superior).",
|
|
30529
30847
|
inputSchema: {
|
|
30530
30848
|
type: "object",
|
|
30531
30849
|
properties: {
|
|
@@ -30533,7 +30851,7 @@ var TOOL_DEFINITIONS = [
|
|
|
30533
30851
|
target_id: { type: "string", description: "Target node ID" },
|
|
30534
30852
|
type: {
|
|
30535
30853
|
type: "string",
|
|
30536
|
-
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
|
|
30854
|
+
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts", "contributes_to"],
|
|
30537
30855
|
description: "Cross-product relationship type"
|
|
30538
30856
|
},
|
|
30539
30857
|
source_product_id: { type: "string", description: "Product ID of the source node" },
|
|
@@ -30569,7 +30887,7 @@ var TOOL_DEFINITIONS = [
|
|
|
30569
30887
|
target_id: { type: "string", description: "Target node ID (bare or qualified {product_id}/{node_id})" },
|
|
30570
30888
|
type: {
|
|
30571
30889
|
type: "string",
|
|
30572
|
-
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
|
|
30890
|
+
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts", "contributes_to"],
|
|
30573
30891
|
description: "Cross-product relationship type"
|
|
30574
30892
|
},
|
|
30575
30893
|
source_product_id: { type: "string", description: "Product ID of the source node (qualifies a bare source_id)" },
|
|
@@ -30588,6 +30906,58 @@ var TOOL_DEFINITIONS = [
|
|
|
30588
30906
|
description: "List all cross-product edges stored in the portfolio document (`.upg/portfolio.upg`). Empty list when the portfolio document is absent.",
|
|
30589
30907
|
inputSchema: { type: "object", properties: {} }
|
|
30590
30908
|
},
|
|
30909
|
+
{
|
|
30910
|
+
name: "portfolio_query",
|
|
30911
|
+
description: 'Traverse the graph ACROSS products in one call (the multi-product `query`). Runs the same BFS (typed-edge traversal + field projection) against every product in scope and tags each subgraph with its source `product_id`, without `switch_product` (the active product is read live; others are read-only). Use for portfolio-level questions ("every product\'s strategy region", "which products have a persona"). `from_id` only matches in its owning product. Read-only.',
|
|
30912
|
+
inputSchema: {
|
|
30913
|
+
type: "object",
|
|
30914
|
+
properties: {
|
|
30915
|
+
from: { type: "string", description: "Start from all nodes of this type (in each product)" },
|
|
30916
|
+
from_id: { type: "string", description: "Start from a specific node ID. Node IDs are product-local; only the owning product returns results." },
|
|
30917
|
+
traverse: {
|
|
30918
|
+
type: "array",
|
|
30919
|
+
items: { type: "string" },
|
|
30920
|
+
description: "Edge types to follow at each level (in order). If omitted, follows all edges. Prefix with ! to exclude."
|
|
30921
|
+
},
|
|
30922
|
+
depth: { type: "number", description: "Max traversal depth (default 3, max 10)" },
|
|
30923
|
+
include: {
|
|
30924
|
+
type: "array",
|
|
30925
|
+
items: { type: "string" },
|
|
30926
|
+
description: 'Fields per node: "title", "status", "tags", "description", "properties" (default: title, status, type)'
|
|
30927
|
+
},
|
|
30928
|
+
limit: { type: "number", description: "Max nodes per product (default 100, max 1000)" },
|
|
30929
|
+
edge_include: {
|
|
30930
|
+
type: "array",
|
|
30931
|
+
items: { type: "string" },
|
|
30932
|
+
description: 'Edge fields to return: "id", "type", "source", "target". Empty array = no edges. Default: all fields.'
|
|
30933
|
+
},
|
|
30934
|
+
property_include: {
|
|
30935
|
+
type: "array",
|
|
30936
|
+
items: { type: "string" },
|
|
30937
|
+
description: 'When "properties" is in include, only return these property keys.'
|
|
30938
|
+
},
|
|
30939
|
+
scope: {
|
|
30940
|
+
type: "array",
|
|
30941
|
+
items: { type: "string" },
|
|
30942
|
+
description: "Product IDs (or files) to query. Omit to query ALL products in the workspace. Match by product id, relative file, or basename."
|
|
30943
|
+
}
|
|
30944
|
+
}
|
|
30945
|
+
}
|
|
30946
|
+
},
|
|
30947
|
+
{
|
|
30948
|
+
name: "portfolio_digest",
|
|
30949
|
+
description: "Roll up every product's counts, health, and stage-coverage in one call (the multi-product `get_graph_digest`). The strategic-surface read that otherwise required `switch_product` + `get_graph_digest` per graph. Returns per-product summaries plus a portfolio rollup (totals, products-by-stage). Read-only; never mutates active-product state.",
|
|
30950
|
+
inputSchema: {
|
|
30951
|
+
type: "object",
|
|
30952
|
+
properties: {
|
|
30953
|
+
scope: {
|
|
30954
|
+
type: "array",
|
|
30955
|
+
items: { type: "string" },
|
|
30956
|
+
description: "Product IDs (or files) to summarise. Omit to summarise ALL products in the workspace."
|
|
30957
|
+
}
|
|
30958
|
+
}
|
|
30959
|
+
}
|
|
30960
|
+
},
|
|
30591
30961
|
{
|
|
30592
30962
|
name: "migrate_cross_edges",
|
|
30593
30963
|
description: "Migrate inline cross-product edges from the current product's `edges[]` into the portfolio document (`.upg/portfolio.upg`) with qualified IDs. `dry_run: true` (default) previews; `dry_run: false` applies. Requires `source_product_id` to qualify source node IDs.",
|
|
@@ -30774,6 +31144,8 @@ var HANDLERS = {
|
|
|
30774
31144
|
attach_product_to_portfolio: attachProductToPortfolioTool,
|
|
30775
31145
|
detach_product_from_portfolio: detachProductFromPortfolioTool,
|
|
30776
31146
|
list_portfolio_cross_edges: listPortfolioCrossEdges,
|
|
31147
|
+
portfolio_query: portfolioQuery,
|
|
31148
|
+
portfolio_digest: portfolioDigest,
|
|
30777
31149
|
migrate_cross_edges: migrateCrossEdges,
|
|
30778
31150
|
get_sync_state: getSyncState,
|
|
30779
31151
|
apply_pull_changeset: applyPullChangeset,
|
|
@@ -30832,9 +31204,9 @@ var SERVER_INSTRUCTIONS = [
|
|
|
30832
31204
|
].join("\n");
|
|
30833
31205
|
function resolvePackageVersion() {
|
|
30834
31206
|
try {
|
|
30835
|
-
const here =
|
|
30836
|
-
const pkgPath =
|
|
30837
|
-
const raw =
|
|
31207
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
31208
|
+
const pkgPath = path6.resolve(here, "..", "package.json");
|
|
31209
|
+
const raw = fs3.readFileSync(pkgPath, "utf-8");
|
|
30838
31210
|
const pkg = JSON.parse(raw);
|
|
30839
31211
|
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
30840
31212
|
} catch {
|
|
@@ -30876,7 +31248,7 @@ function createServer(store) {
|
|
|
30876
31248
|
const result = handler ? await handler(args, ctx) : textError(`Unknown tool: ${name}`);
|
|
30877
31249
|
if (logFile) {
|
|
30878
31250
|
const entry = JSON.stringify({ ts: t0, tool: name, params: args, result, durationMs: Date.now() - t0 });
|
|
30879
|
-
|
|
31251
|
+
fs3.appendFileSync(logFile, entry + "\n");
|
|
30880
31252
|
}
|
|
30881
31253
|
return result;
|
|
30882
31254
|
});
|
|
@@ -30893,15 +31265,15 @@ import { nanoid } from "nanoid";
|
|
|
30893
31265
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
30894
31266
|
import { realpathSync as realpathSync2 } from "fs";
|
|
30895
31267
|
async function discoverUPGFile(explicitFile) {
|
|
30896
|
-
if (explicitFile) return
|
|
31268
|
+
if (explicitFile) return path7.resolve(explicitFile);
|
|
30897
31269
|
const cwd = process.cwd();
|
|
30898
|
-
const workspacePath =
|
|
31270
|
+
const workspacePath = path7.join(cwd, ".upg", "workspace.json");
|
|
30899
31271
|
try {
|
|
30900
|
-
const raw = await
|
|
31272
|
+
const raw = await fs4.readFile(workspacePath, "utf-8");
|
|
30901
31273
|
const workspace = JSON.parse(raw);
|
|
30902
31274
|
if (workspace.default_product) {
|
|
30903
|
-
const filePath =
|
|
30904
|
-
await
|
|
31275
|
+
const filePath = path7.join(cwd, ".upg", workspace.default_product);
|
|
31276
|
+
await fs4.access(filePath);
|
|
30905
31277
|
const title = workspace.products?.find(
|
|
30906
31278
|
(p) => p.file === workspace.default_product
|
|
30907
31279
|
)?.title ?? workspace.default_product;
|
|
@@ -30912,16 +31284,16 @@ async function discoverUPGFile(explicitFile) {
|
|
|
30912
31284
|
return filePath;
|
|
30913
31285
|
}
|
|
30914
31286
|
} catch {
|
|
30915
|
-
const upgDir =
|
|
31287
|
+
const upgDir = path7.join(cwd, ".upg");
|
|
30916
31288
|
try {
|
|
30917
|
-
const dirEntries = await
|
|
31289
|
+
const dirEntries = await fs4.readdir(upgDir);
|
|
30918
31290
|
const upgFiles = dirEntries.filter((f) => f.endsWith(".upg")).sort();
|
|
30919
31291
|
if (upgFiles.length > 0) {
|
|
30920
31292
|
const products = [];
|
|
30921
31293
|
for (const file of upgFiles) {
|
|
30922
|
-
let title =
|
|
31294
|
+
let title = path7.basename(file, ".upg");
|
|
30923
31295
|
try {
|
|
30924
|
-
const raw = await
|
|
31296
|
+
const raw = await fs4.readFile(path7.join(upgDir, file), "utf-8");
|
|
30925
31297
|
const doc = JSON.parse(raw);
|
|
30926
31298
|
if (doc.product?.title) title = doc.product.title;
|
|
30927
31299
|
} catch {
|
|
@@ -30933,12 +31305,12 @@ async function discoverUPGFile(explicitFile) {
|
|
|
30933
31305
|
default_product: upgFiles[0],
|
|
30934
31306
|
products
|
|
30935
31307
|
};
|
|
30936
|
-
await
|
|
31308
|
+
await fs4.writeFile(workspacePath, JSON.stringify(workspace, null, 2) + "\n", "utf-8");
|
|
30937
31309
|
process.stderr.write(
|
|
30938
31310
|
`UPG workspace: auto-created workspace.json (${upgFiles.length} product${upgFiles.length > 1 ? "s" : ""})
|
|
30939
31311
|
`
|
|
30940
31312
|
);
|
|
30941
|
-
const filePath =
|
|
31313
|
+
const filePath = path7.join(upgDir, upgFiles[0]);
|
|
30942
31314
|
process.stderr.write(`UPG workspace: loading "${products[0].title}"
|
|
30943
31315
|
`);
|
|
30944
31316
|
return filePath;
|
|
@@ -30947,17 +31319,17 @@ async function discoverUPGFile(explicitFile) {
|
|
|
30947
31319
|
}
|
|
30948
31320
|
}
|
|
30949
31321
|
try {
|
|
30950
|
-
const entries = await
|
|
31322
|
+
const entries = await fs4.readdir(cwd);
|
|
30951
31323
|
const upgFiles = entries.filter((f) => f.endsWith(".upg")).sort();
|
|
30952
31324
|
if (upgFiles.length === 1) {
|
|
30953
|
-
return
|
|
31325
|
+
return path7.resolve(upgFiles[0]);
|
|
30954
31326
|
}
|
|
30955
31327
|
if (upgFiles.length > 1) {
|
|
30956
31328
|
process.stderr.write(
|
|
30957
31329
|
`Found ${upgFiles.length} .upg files: loading ${upgFiles[0]}. Use --file to pick a specific one.
|
|
30958
31330
|
`
|
|
30959
31331
|
);
|
|
30960
|
-
return
|
|
31332
|
+
return path7.resolve(upgFiles[0]);
|
|
30961
31333
|
}
|
|
30962
31334
|
} catch {
|
|
30963
31335
|
}
|
|
@@ -30977,7 +31349,7 @@ async function runMcpServer() {
|
|
|
30977
31349
|
});
|
|
30978
31350
|
let resolvedPath = await discoverUPGFile(values.file);
|
|
30979
31351
|
if (!resolvedPath) {
|
|
30980
|
-
const defaultFile =
|
|
31352
|
+
const defaultFile = path7.resolve("product.upg");
|
|
30981
31353
|
const title = values.title ?? "My Product";
|
|
30982
31354
|
const blank = {
|
|
30983
31355
|
upg_version: UPG_VERSION,
|
|
@@ -30993,16 +31365,16 @@ async function runMcpServer() {
|
|
|
30993
31365
|
nodes: [],
|
|
30994
31366
|
edges: []
|
|
30995
31367
|
};
|
|
30996
|
-
await
|
|
30997
|
-
await
|
|
31368
|
+
await fs4.mkdir(path7.dirname(defaultFile), { recursive: true });
|
|
31369
|
+
await fs4.writeFile(defaultFile, serializeCanonical(blank), "utf-8");
|
|
30998
31370
|
process.stderr.write(`Created new UPG file: ${defaultFile}
|
|
30999
31371
|
`);
|
|
31000
31372
|
resolvedPath = defaultFile;
|
|
31001
31373
|
} else {
|
|
31002
31374
|
try {
|
|
31003
|
-
await
|
|
31375
|
+
await fs4.access(resolvedPath);
|
|
31004
31376
|
} catch {
|
|
31005
|
-
const title = values.title ??
|
|
31377
|
+
const title = values.title ?? path7.basename(resolvedPath, ".upg");
|
|
31006
31378
|
const blank = {
|
|
31007
31379
|
upg_version: UPG_VERSION,
|
|
31008
31380
|
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -31017,13 +31389,13 @@ async function runMcpServer() {
|
|
|
31017
31389
|
nodes: [],
|
|
31018
31390
|
edges: []
|
|
31019
31391
|
};
|
|
31020
|
-
await
|
|
31021
|
-
await
|
|
31392
|
+
await fs4.mkdir(path7.dirname(resolvedPath), { recursive: true });
|
|
31393
|
+
await fs4.writeFile(resolvedPath, serializeCanonical(blank), "utf-8");
|
|
31022
31394
|
process.stderr.write(`Created new UPG file: ${resolvedPath}
|
|
31023
31395
|
`);
|
|
31024
31396
|
}
|
|
31025
31397
|
}
|
|
31026
|
-
const store = new
|
|
31398
|
+
const store = new UPGFileStore2();
|
|
31027
31399
|
store.setWriter("upg-mcp-local", SERVER_VERSION);
|
|
31028
31400
|
await store.load(resolvedPath);
|
|
31029
31401
|
const nodes = store.getAllNodes();
|