@unified-product-graph/mcp-server 0.8.15 → 0.8.16
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/README.md +2 -2
- package/TOOLS.md +194 -6
- package/dist/index.js +346 -7
- package/dist/index.js.map +1 -1
- package/dist/tools-manifest.json +345 -23
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -2733,7 +2733,8 @@ var UPG_CROSS_EDGE_TYPES = [
|
|
|
2733
2733
|
"shares_metric",
|
|
2734
2734
|
"depends_on_product",
|
|
2735
2735
|
"cannibalises",
|
|
2736
|
-
"succeeds"
|
|
2736
|
+
"succeeds",
|
|
2737
|
+
"hosts"
|
|
2737
2738
|
];
|
|
2738
2739
|
var TYPES = getTypes();
|
|
2739
2740
|
var TYPES_SET = new Set(TYPES);
|
|
@@ -24372,7 +24373,7 @@ function serializePortfolioWithHeader(doc, opts) {
|
|
|
24372
24373
|
header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
|
|
24373
24374
|
return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
|
|
24374
24375
|
}
|
|
24375
|
-
var UPG_VERSION = "0.8.
|
|
24376
|
+
var UPG_VERSION = "0.8.16";
|
|
24376
24377
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
24377
24378
|
var UPG_TYPES = getTypes();
|
|
24378
24379
|
var UPG_TYPES_SET = new Set(UPG_TYPES);
|
|
@@ -26257,6 +26258,10 @@ import {
|
|
|
26257
26258
|
writePortfolioScopedNode as writePortfolioScopedNode2,
|
|
26258
26259
|
openPortfolioStoreIfExists,
|
|
26259
26260
|
assignProductToArea,
|
|
26261
|
+
updateProductArea,
|
|
26262
|
+
removeProductFromArea,
|
|
26263
|
+
deleteArea,
|
|
26264
|
+
moveProductToArea,
|
|
26260
26265
|
PortfolioRoutingError as PortfolioRoutingError2
|
|
26261
26266
|
} from "@unified-product-graph/sdk";
|
|
26262
26267
|
var listProductAreas = async (_args, _ctx) => {
|
|
@@ -26479,6 +26484,73 @@ var getChanges = (args, ctx) => {
|
|
|
26479
26484
|
)
|
|
26480
26485
|
);
|
|
26481
26486
|
};
|
|
26487
|
+
var updateAreaTool = async (args, _ctx) => {
|
|
26488
|
+
const areaId = args.area_id;
|
|
26489
|
+
if (!areaId) return textError("Missing required parameter: area_id");
|
|
26490
|
+
const hasField = args.title !== void 0 || args.description !== void 0 || args.strategic_priority !== void 0 || args.owner !== void 0 || "parent_area_id" in args;
|
|
26491
|
+
if (!hasField) {
|
|
26492
|
+
return textError(
|
|
26493
|
+
"Nothing to update: pass at least one of: title, description, strategic_priority, owner, parent_area_id."
|
|
26494
|
+
);
|
|
26495
|
+
}
|
|
26496
|
+
try {
|
|
26497
|
+
const result = await updateProductArea(process.cwd(), areaId, {
|
|
26498
|
+
title: args.title,
|
|
26499
|
+
description: args.description,
|
|
26500
|
+
strategic_priority: args.strategic_priority,
|
|
26501
|
+
owner: args.owner,
|
|
26502
|
+
// Tri-state: present (incl. null) re-parents/un-nests; absent leaves unchanged.
|
|
26503
|
+
..."parent_area_id" in args ? { parent_area_id: args.parent_area_id ?? null } : {}
|
|
26504
|
+
});
|
|
26505
|
+
return text(
|
|
26506
|
+
JSON.stringify({ message: `Updated area (${result.updated.join(", ")})`, ...result }, null, 2)
|
|
26507
|
+
);
|
|
26508
|
+
} catch (err) {
|
|
26509
|
+
if (err instanceof PortfolioRoutingError2) return textError(err.message);
|
|
26510
|
+
return textError(err.message);
|
|
26511
|
+
}
|
|
26512
|
+
};
|
|
26513
|
+
var removeProductFromAreaTool = async (args, _ctx) => {
|
|
26514
|
+
const productId = args.product_id;
|
|
26515
|
+
const areaId = args.area_id;
|
|
26516
|
+
if (!productId) return textError("Missing required parameter: product_id");
|
|
26517
|
+
if (!areaId) return textError("Missing required parameter: area_id");
|
|
26518
|
+
try {
|
|
26519
|
+
const result = await removeProductFromArea(process.cwd(), { product_id: productId, area_id: areaId });
|
|
26520
|
+
return text(JSON.stringify(result, null, 2));
|
|
26521
|
+
} catch (err) {
|
|
26522
|
+
if (err instanceof PortfolioRoutingError2) return textError(err.message);
|
|
26523
|
+
return textError(err.message);
|
|
26524
|
+
}
|
|
26525
|
+
};
|
|
26526
|
+
var deleteAreaTool = async (args, _ctx) => {
|
|
26527
|
+
const areaId = args.area_id;
|
|
26528
|
+
if (!areaId) return textError("Missing required parameter: area_id");
|
|
26529
|
+
try {
|
|
26530
|
+
const result = await deleteArea(process.cwd(), areaId, { force: args.force });
|
|
26531
|
+
return text(JSON.stringify({ message: `Deleted area ${areaId}`, ...result }, null, 2));
|
|
26532
|
+
} catch (err) {
|
|
26533
|
+
if (err instanceof PortfolioRoutingError2) return textError(err.message);
|
|
26534
|
+
return textError(err.message);
|
|
26535
|
+
}
|
|
26536
|
+
};
|
|
26537
|
+
var moveProductToAreaTool = async (args, _ctx) => {
|
|
26538
|
+
const productId = args.product_id;
|
|
26539
|
+
const toAreaId = args.to_area_id;
|
|
26540
|
+
if (!productId) return textError("Missing required parameter: product_id");
|
|
26541
|
+
if (!toAreaId) return textError("Missing required parameter: to_area_id");
|
|
26542
|
+
try {
|
|
26543
|
+
const result = await moveProductToArea(process.cwd(), {
|
|
26544
|
+
product_id: productId,
|
|
26545
|
+
to_area_id: toAreaId,
|
|
26546
|
+
from_area_id: args.from_area_id
|
|
26547
|
+
});
|
|
26548
|
+
return text(JSON.stringify(result, null, 2));
|
|
26549
|
+
} catch (err) {
|
|
26550
|
+
if (err instanceof PortfolioRoutingError2) return textError(err.message);
|
|
26551
|
+
return textError(err.message);
|
|
26552
|
+
}
|
|
26553
|
+
};
|
|
26482
26554
|
|
|
26483
26555
|
// src/tools/workspace.ts
|
|
26484
26556
|
import * as fs from "fs";
|
|
@@ -26491,7 +26563,9 @@ import {
|
|
|
26491
26563
|
openPortfolioStoreIfExists as openPortfolioStoreIfExists2,
|
|
26492
26564
|
registerProductOnPortfolio,
|
|
26493
26565
|
findProductFileById,
|
|
26494
|
-
attachProductToPortfolio
|
|
26566
|
+
attachProductToPortfolio,
|
|
26567
|
+
detachProductFromPortfolio,
|
|
26568
|
+
deleteCrossProductEdge
|
|
26495
26569
|
} from "@unified-product-graph/sdk";
|
|
26496
26570
|
import {
|
|
26497
26571
|
createProduct,
|
|
@@ -26505,6 +26579,25 @@ import {
|
|
|
26505
26579
|
var listLocalProducts = (_args, _ctx) => {
|
|
26506
26580
|
const cwd = process.cwd();
|
|
26507
26581
|
const products = [];
|
|
26582
|
+
const membership = /* @__PURE__ */ new Map();
|
|
26583
|
+
try {
|
|
26584
|
+
const pdoc = JSON.parse(fs.readFileSync(path3.join(cwd, ".upg", "portfolio.upg"), "utf-8"));
|
|
26585
|
+
for (const area of pdoc.product_areas ?? []) {
|
|
26586
|
+
for (const pid of area.products ?? []) {
|
|
26587
|
+
const m = membership.get(pid) ?? { areas: [], portfolios: [] };
|
|
26588
|
+
m.areas.push(area.title ?? area.id);
|
|
26589
|
+
membership.set(pid, m);
|
|
26590
|
+
}
|
|
26591
|
+
}
|
|
26592
|
+
for (const pf of pdoc.portfolios ?? []) {
|
|
26593
|
+
for (const pid of pf.products ?? []) {
|
|
26594
|
+
const m = membership.get(pid) ?? { areas: [], portfolios: [] };
|
|
26595
|
+
m.portfolios.push(pf.title ?? pf.id);
|
|
26596
|
+
membership.set(pid, m);
|
|
26597
|
+
}
|
|
26598
|
+
}
|
|
26599
|
+
} catch {
|
|
26600
|
+
}
|
|
26508
26601
|
const candidates = [];
|
|
26509
26602
|
const topEntries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
26510
26603
|
for (const entry of topEntries) {
|
|
@@ -26529,13 +26622,19 @@ var listLocalProducts = (_args, _ctx) => {
|
|
|
26529
26622
|
try {
|
|
26530
26623
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
26531
26624
|
const doc = JSON.parse(raw);
|
|
26625
|
+
if (!doc.product) continue;
|
|
26532
26626
|
const coerced = coerceProductStage(doc.product?.stage);
|
|
26627
|
+
const pid = doc.product?.id ?? null;
|
|
26628
|
+
const m = pid ? membership.get(pid) : void 0;
|
|
26533
26629
|
products.push({
|
|
26630
|
+
id: pid,
|
|
26534
26631
|
file: path3.relative(cwd, filePath),
|
|
26535
26632
|
title: doc.product?.title ?? "(untitled)",
|
|
26536
26633
|
stage: coerced.canonical ?? null,
|
|
26537
26634
|
nodes: Array.isArray(doc.nodes) ? doc.nodes.length : 0,
|
|
26538
|
-
edges: Array.isArray(doc.edges) ? doc.edges.length : 0
|
|
26635
|
+
edges: Array.isArray(doc.edges) ? doc.edges.length : 0,
|
|
26636
|
+
...m && m.areas.length > 0 ? { areas: m.areas } : {},
|
|
26637
|
+
...m && m.portfolios.length > 0 ? { portfolios: m.portfolios } : {}
|
|
26539
26638
|
});
|
|
26540
26639
|
} catch {
|
|
26541
26640
|
}
|
|
@@ -26926,6 +27025,126 @@ var attachProductToPortfolioTool = async (args, _ctx) => {
|
|
|
26926
27025
|
return textError(err.message);
|
|
26927
27026
|
}
|
|
26928
27027
|
};
|
|
27028
|
+
var detachProductFromPortfolioTool = async (args, _ctx) => {
|
|
27029
|
+
const productId = args.product_id;
|
|
27030
|
+
const portfolioId = args.portfolio_id;
|
|
27031
|
+
if (!productId) return textError("Missing required parameter: product_id");
|
|
27032
|
+
if (!portfolioId) return textError("Missing required parameter: portfolio_id");
|
|
27033
|
+
try {
|
|
27034
|
+
const result = await detachProductFromPortfolio(process.cwd(), {
|
|
27035
|
+
product_id: productId,
|
|
27036
|
+
portfolio_id: portfolioId
|
|
27037
|
+
});
|
|
27038
|
+
return text(JSON.stringify(result, null, 2));
|
|
27039
|
+
} catch (err) {
|
|
27040
|
+
return textError(err.message);
|
|
27041
|
+
}
|
|
27042
|
+
};
|
|
27043
|
+
var deleteCrossProductEdgeTool = async (args, _ctx) => {
|
|
27044
|
+
const edgeIdArg = args.edge_id;
|
|
27045
|
+
if (!edgeIdArg) return textError("Missing required parameter: edge_id");
|
|
27046
|
+
try {
|
|
27047
|
+
const result = await deleteCrossProductEdge(process.cwd(), edgeIdArg);
|
|
27048
|
+
return text(JSON.stringify(result, null, 2));
|
|
27049
|
+
} catch (err) {
|
|
27050
|
+
return textError(err.message);
|
|
27051
|
+
}
|
|
27052
|
+
};
|
|
27053
|
+
var batchCreateCrossProductEdges = async (args, _ctx) => {
|
|
27054
|
+
const edgesArg = args.edges;
|
|
27055
|
+
if (!Array.isArray(edgesArg) || edgesArg.length === 0) {
|
|
27056
|
+
return textError("Missing required parameter: edges (a non-empty array).");
|
|
27057
|
+
}
|
|
27058
|
+
if (edgesArg.length > 50) {
|
|
27059
|
+
return textError(`Too many edges: ${edgesArg.length}. Max 50 per batch_create_cross_product_edges call.`);
|
|
27060
|
+
}
|
|
27061
|
+
const cwd = process.cwd();
|
|
27062
|
+
const portfolioPath = resolvePortfolioPath(cwd);
|
|
27063
|
+
if (!portfolioPath) {
|
|
27064
|
+
return textError("No workspace found. Run `init_workspace` first to enable portfolio cross-product edges.");
|
|
27065
|
+
}
|
|
27066
|
+
const autoCreatePortfolio = args.auto_create_portfolio ?? false;
|
|
27067
|
+
const portfolioExisted = fs.existsSync(portfolioPath);
|
|
27068
|
+
if (!portfolioExisted && !autoCreatePortfolio) {
|
|
27069
|
+
return textError(
|
|
27070
|
+
'No portfolio document found at .upg/portfolio.upg. Cross-product edges express portfolio-level relationships and should be anchored to a portfolio that contains the products. Create a portfolio first (`create_node({type: "portfolio", title: "..."})`), or pass `auto_create_portfolio: true`.'
|
|
27071
|
+
);
|
|
27072
|
+
}
|
|
27073
|
+
const prepared = [];
|
|
27074
|
+
for (let i = 0; i < edgesArg.length; i++) {
|
|
27075
|
+
const e = edgesArg[i];
|
|
27076
|
+
const sourceIdArg = e.source_id;
|
|
27077
|
+
const targetIdArg = e.target_id;
|
|
27078
|
+
const edgeTypeArg = e.type;
|
|
27079
|
+
const sourceProductId = e.source_product_id;
|
|
27080
|
+
const targetProductId = e.target_product_id;
|
|
27081
|
+
if (!sourceIdArg) return textError(`edges[${i}]: missing source_id`);
|
|
27082
|
+
if (!targetIdArg) return textError(`edges[${i}]: missing target_id`);
|
|
27083
|
+
if (!edgeTypeArg) return textError(`edges[${i}]: missing type`);
|
|
27084
|
+
if (!UPG_CROSS_EDGE_TYPES.includes(edgeTypeArg)) {
|
|
27085
|
+
return textError(`edges[${i}]: invalid cross-product edge type "${edgeTypeArg}". Valid types: ${UPG_CROSS_EDGE_TYPES.join(", ")}`);
|
|
27086
|
+
}
|
|
27087
|
+
let qualifiedSource;
|
|
27088
|
+
if (sourceIdArg.includes("/")) qualifiedSource = sourceIdArg;
|
|
27089
|
+
else if (sourceProductId) qualifiedSource = `${sourceProductId}/${sourceIdArg}`;
|
|
27090
|
+
else return textError(`edges[${i}]: source_id "${sourceIdArg}" is a bare node id. Supply source_product_id or a qualified {product_id}/{node_id}.`);
|
|
27091
|
+
let qualifiedTarget;
|
|
27092
|
+
if (targetIdArg.includes("/")) qualifiedTarget = targetIdArg;
|
|
27093
|
+
else if (targetProductId) qualifiedTarget = `${targetProductId}/${targetIdArg}`;
|
|
27094
|
+
else return textError(`edges[${i}]: target_id "${targetIdArg}" is a bare node id. Supply target_product_id or a qualified {product_id}/{node_id}.`);
|
|
27095
|
+
prepared.push({
|
|
27096
|
+
id: edgeId3(),
|
|
27097
|
+
source: qualifiedSource,
|
|
27098
|
+
target: qualifiedTarget,
|
|
27099
|
+
type: edgeTypeArg,
|
|
27100
|
+
source_product_id: sourceProductId ?? qualifiedSource.split("/")[0],
|
|
27101
|
+
target_product_id: targetProductId ?? qualifiedTarget.split("/")[0]
|
|
27102
|
+
});
|
|
27103
|
+
}
|
|
27104
|
+
const portfolioStore = new UPGPortfolioStore();
|
|
27105
|
+
try {
|
|
27106
|
+
await portfolioStore.loadOrInit(portfolioPath);
|
|
27107
|
+
} catch (err) {
|
|
27108
|
+
return textError(`Failed to load portfolio document: ${err.message}`);
|
|
27109
|
+
}
|
|
27110
|
+
const registeredProducts = [];
|
|
27111
|
+
const portfolioDoc = portfolioStore.getDocument();
|
|
27112
|
+
if (portfolioDoc) {
|
|
27113
|
+
const productIds = /* @__PURE__ */ new Set();
|
|
27114
|
+
for (const e of prepared) {
|
|
27115
|
+
if (e.source_product_id) productIds.add(e.source_product_id);
|
|
27116
|
+
if (e.target_product_id) productIds.add(e.target_product_id);
|
|
27117
|
+
}
|
|
27118
|
+
for (const pid of productIds) {
|
|
27119
|
+
const lookup = findProductFileById(cwd, pid);
|
|
27120
|
+
const wasNew = registerProductOnPortfolio(portfolioDoc, {
|
|
27121
|
+
id: pid,
|
|
27122
|
+
...lookup ? { file_path: lookup.file_path, title: lookup.title } : {}
|
|
27123
|
+
});
|
|
27124
|
+
if (wasNew) registeredProducts.push({ id: pid, ...lookup ? { file_path: lookup.file_path, title: lookup.title } : {} });
|
|
27125
|
+
}
|
|
27126
|
+
if (registeredProducts.length > 0) portfolioStore.markDirty();
|
|
27127
|
+
}
|
|
27128
|
+
try {
|
|
27129
|
+
for (const e of prepared) portfolioStore.addCrossEdge(e);
|
|
27130
|
+
await portfolioStore.flush();
|
|
27131
|
+
} catch (err) {
|
|
27132
|
+
return textError(`Failed to write cross-product edges: ${err.message}`);
|
|
27133
|
+
}
|
|
27134
|
+
return text(
|
|
27135
|
+
JSON.stringify(
|
|
27136
|
+
{
|
|
27137
|
+
message: `Created ${prepared.length} cross-product edge(s)`,
|
|
27138
|
+
created: prepared,
|
|
27139
|
+
count: prepared.length,
|
|
27140
|
+
portfolio_file: path3.relative(cwd, portfolioPath),
|
|
27141
|
+
...registeredProducts.length > 0 ? { registered_products: registeredProducts } : {}
|
|
27142
|
+
},
|
|
27143
|
+
null,
|
|
27144
|
+
2
|
|
27145
|
+
)
|
|
27146
|
+
);
|
|
27147
|
+
};
|
|
26929
27148
|
|
|
26930
27149
|
// src/tools/schema.ts
|
|
26931
27150
|
var getEntitySchema = (args, _ctx) => {
|
|
@@ -29552,7 +29771,7 @@ var TOOL_DEFINITIONS = [
|
|
|
29552
29771
|
},
|
|
29553
29772
|
{
|
|
29554
29773
|
name: "list_cross_edge_types",
|
|
29555
|
-
description: "List the canonical cross-product edge types from `UPG_CROSS_EDGE_TYPES`: `shares_persona`, `shares_competitor`, `shares_metric`, `depends_on_product`, `cannibalises`, `succeeds`. Portfolio-level relationships across products. Distinct from the within-product `UPG_EDGE_CATALOG`.",
|
|
29774
|
+
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`.",
|
|
29556
29775
|
inputSchema: { type: "object", properties: {} }
|
|
29557
29776
|
},
|
|
29558
29777
|
{
|
|
@@ -29937,6 +30156,66 @@ var TOOL_DEFINITIONS = [
|
|
|
29937
30156
|
required: ["product_id", "area_id"]
|
|
29938
30157
|
}
|
|
29939
30158
|
},
|
|
30159
|
+
{
|
|
30160
|
+
name: "update_area",
|
|
30161
|
+
description: "Edit a product area in `.upg/portfolio.upg` (title, description, strategic_priority, owner) and/or re-parent it via `parent_area_id`. The mirror of `update_product` for the organisational axis. `parent_area_id` is tri-state: omit to leave unchanged, pass null to un-nest (top-level), or pass an area id to re-parent (rejected if it would create a cycle).",
|
|
30162
|
+
inputSchema: {
|
|
30163
|
+
type: "object",
|
|
30164
|
+
properties: {
|
|
30165
|
+
area_id: { type: "string", description: "Product area id to edit (from list_product_areas)" },
|
|
30166
|
+
title: { type: "string", description: "New area title" },
|
|
30167
|
+
description: { type: "string", description: "New area description" },
|
|
30168
|
+
strategic_priority: {
|
|
30169
|
+
type: "string",
|
|
30170
|
+
enum: ["urgent", "high", "medium", "low", "none"],
|
|
30171
|
+
description: "Strategic priority (canonical Priority scale)"
|
|
30172
|
+
},
|
|
30173
|
+
parent_area_id: {
|
|
30174
|
+
type: ["string", "null"],
|
|
30175
|
+
description: "Re-parent under this area id; null un-nests (top-level); omit to leave unchanged"
|
|
30176
|
+
},
|
|
30177
|
+
owner: { type: "string", description: "Person or team that owns this area" }
|
|
30178
|
+
},
|
|
30179
|
+
required: ["area_id"]
|
|
30180
|
+
}
|
|
30181
|
+
},
|
|
30182
|
+
{
|
|
30183
|
+
name: "remove_product_from_area",
|
|
30184
|
+
description: "Remove a product from a product area's `products[]` in `.upg/portfolio.upg` (the product stays registered on the portfolio and in any other container). The inverse of `assign_product_to_area`.",
|
|
30185
|
+
inputSchema: {
|
|
30186
|
+
type: "object",
|
|
30187
|
+
properties: {
|
|
30188
|
+
product_id: { type: "string", description: "Product id (from list_local_products)" },
|
|
30189
|
+
area_id: { type: "string", description: "Product area id (from list_product_areas)" }
|
|
30190
|
+
},
|
|
30191
|
+
required: ["product_id", "area_id"]
|
|
30192
|
+
}
|
|
30193
|
+
},
|
|
30194
|
+
{
|
|
30195
|
+
name: "delete_area",
|
|
30196
|
+
description: "Delete a product area from `.upg/portfolio.upg`. Guarded: refuses while the area still has products unless `force: true`. Child areas are un-nested (their parent link is cleared) so no parent reference dangles.",
|
|
30197
|
+
inputSchema: {
|
|
30198
|
+
type: "object",
|
|
30199
|
+
properties: {
|
|
30200
|
+
area_id: { type: "string", description: "Product area id to delete (from list_product_areas)" },
|
|
30201
|
+
force: { type: "boolean", description: "Delete even if the area still has products (default false)" }
|
|
30202
|
+
},
|
|
30203
|
+
required: ["area_id"]
|
|
30204
|
+
}
|
|
30205
|
+
},
|
|
30206
|
+
{
|
|
30207
|
+
name: "move_product_to_area",
|
|
30208
|
+
description: "Move a product to a different product area: remove it from `from_area_id` (or, when omitted, from every area it currently sits in) and add it to `to_area_id`. Convenience over remove_product_from_area + assign_product_to_area.",
|
|
30209
|
+
inputSchema: {
|
|
30210
|
+
type: "object",
|
|
30211
|
+
properties: {
|
|
30212
|
+
product_id: { type: "string", description: "Product id (from list_local_products)" },
|
|
30213
|
+
to_area_id: { type: "string", description: "Destination product area id (from list_product_areas)" },
|
|
30214
|
+
from_area_id: { type: "string", description: "Source area id to remove from; omit to remove from all areas" }
|
|
30215
|
+
},
|
|
30216
|
+
required: ["product_id", "to_area_id"]
|
|
30217
|
+
}
|
|
30218
|
+
},
|
|
29940
30219
|
{
|
|
29941
30220
|
name: "attach_product_to_portfolio",
|
|
29942
30221
|
description: "Place an existing product under a portfolio (adds it to the portfolio's `products[]` in `.upg/portfolio.upg`). Resolves the portfolio against the portfolio document and auto-registers the product on the portfolio registry. Use after `create_product`, or pass `portfolio_id` to `create_product` directly.",
|
|
@@ -29949,6 +30228,18 @@ var TOOL_DEFINITIONS = [
|
|
|
29949
30228
|
required: ["product_id", "portfolio_id"]
|
|
29950
30229
|
}
|
|
29951
30230
|
},
|
|
30231
|
+
{
|
|
30232
|
+
name: "detach_product_from_portfolio",
|
|
30233
|
+
description: "Remove a product from a portfolio's `products[]` in `.upg/portfolio.upg` (the product stays registered and in any other container). The inverse of `attach_product_to_portfolio`.",
|
|
30234
|
+
inputSchema: {
|
|
30235
|
+
type: "object",
|
|
30236
|
+
properties: {
|
|
30237
|
+
product_id: { type: "string", description: "Product id (from list_local_products)" },
|
|
30238
|
+
portfolio_id: { type: "string", description: "Portfolio id (from list_portfolios)" }
|
|
30239
|
+
},
|
|
30240
|
+
required: ["product_id", "portfolio_id"]
|
|
30241
|
+
}
|
|
30242
|
+
},
|
|
29952
30243
|
{
|
|
29953
30244
|
name: "list_portfolios",
|
|
29954
30245
|
description: "List portfolios from the portfolio document (`.upg/portfolio.upg`). Portfolios represent the strategic axis (where we invest). Returns an empty list when no portfolio document exists yet.",
|
|
@@ -29961,7 +30252,7 @@ var TOOL_DEFINITIONS = [
|
|
|
29961
30252
|
},
|
|
29962
30253
|
{
|
|
29963
30254
|
name: "create_cross_product_edge",
|
|
29964
|
-
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
|
|
30255
|
+
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).",
|
|
29965
30256
|
inputSchema: {
|
|
29966
30257
|
type: "object",
|
|
29967
30258
|
properties: {
|
|
@@ -29969,7 +30260,7 @@ var TOOL_DEFINITIONS = [
|
|
|
29969
30260
|
target_id: { type: "string", description: "Target node ID" },
|
|
29970
30261
|
type: {
|
|
29971
30262
|
type: "string",
|
|
29972
|
-
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds"],
|
|
30263
|
+
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
|
|
29973
30264
|
description: "Cross-product relationship type"
|
|
29974
30265
|
},
|
|
29975
30266
|
source_product_id: { type: "string", description: "Product ID of the source node" },
|
|
@@ -29978,6 +30269,47 @@ var TOOL_DEFINITIONS = [
|
|
|
29978
30269
|
required: ["source_id", "target_id", "type"]
|
|
29979
30270
|
}
|
|
29980
30271
|
},
|
|
30272
|
+
{
|
|
30273
|
+
name: "delete_cross_product_edge",
|
|
30274
|
+
description: "Delete a cross-product edge from `.upg/portfolio.upg` by id. The inverse of `create_cross_product_edge`. Returns `deleted: false` (not an error) when no edge with that id exists.",
|
|
30275
|
+
inputSchema: {
|
|
30276
|
+
type: "object",
|
|
30277
|
+
properties: {
|
|
30278
|
+
edge_id: { type: "string", description: "Cross-product edge id (from list_portfolio_cross_edges)" }
|
|
30279
|
+
},
|
|
30280
|
+
required: ["edge_id"]
|
|
30281
|
+
}
|
|
30282
|
+
},
|
|
30283
|
+
{
|
|
30284
|
+
name: "batch_create_cross_product_edges",
|
|
30285
|
+
description: "Create up to 50 cross-product edges in one atomic write (the portfolio-tier mirror of batch_create_edges). Every edge is validated and qualified before anything is written; if any is invalid the whole batch is rejected. Referenced products are auto-registered.",
|
|
30286
|
+
inputSchema: {
|
|
30287
|
+
type: "object",
|
|
30288
|
+
properties: {
|
|
30289
|
+
edges: {
|
|
30290
|
+
type: "array",
|
|
30291
|
+
description: "Cross-product edges to create (max 50). Each: { source_id, target_id, type, source_product_id?, target_product_id? }.",
|
|
30292
|
+
items: {
|
|
30293
|
+
type: "object",
|
|
30294
|
+
properties: {
|
|
30295
|
+
source_id: { type: "string", description: "Source node ID (bare or qualified {product_id}/{node_id})" },
|
|
30296
|
+
target_id: { type: "string", description: "Target node ID (bare or qualified {product_id}/{node_id})" },
|
|
30297
|
+
type: {
|
|
30298
|
+
type: "string",
|
|
30299
|
+
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
|
|
30300
|
+
description: "Cross-product relationship type"
|
|
30301
|
+
},
|
|
30302
|
+
source_product_id: { type: "string", description: "Product ID of the source node (qualifies a bare source_id)" },
|
|
30303
|
+
target_product_id: { type: "string", description: "Product ID of the target node (qualifies a bare target_id)" }
|
|
30304
|
+
},
|
|
30305
|
+
required: ["source_id", "target_id", "type"]
|
|
30306
|
+
}
|
|
30307
|
+
},
|
|
30308
|
+
auto_create_portfolio: { type: "boolean", description: "Create an empty portfolio document if none exists (default false)" }
|
|
30309
|
+
},
|
|
30310
|
+
required: ["edges"]
|
|
30311
|
+
}
|
|
30312
|
+
},
|
|
29981
30313
|
{
|
|
29982
30314
|
name: "list_portfolio_cross_edges",
|
|
29983
30315
|
description: "List all cross-product edges stored in the portfolio document (`.upg/portfolio.upg`). Empty list when the portfolio document is absent.",
|
|
@@ -30157,10 +30489,17 @@ var HANDLERS = {
|
|
|
30157
30489
|
get_area_context: getAreaContext,
|
|
30158
30490
|
create_area: createArea,
|
|
30159
30491
|
assign_product_to_area: assignProductToAreaTool,
|
|
30492
|
+
update_area: updateAreaTool,
|
|
30493
|
+
remove_product_from_area: removeProductFromAreaTool,
|
|
30494
|
+
delete_area: deleteAreaTool,
|
|
30495
|
+
move_product_to_area: moveProductToAreaTool,
|
|
30160
30496
|
list_portfolios: listPortfolios,
|
|
30161
30497
|
get_organization: getOrganization,
|
|
30162
30498
|
create_cross_product_edge: createCrossProductEdge,
|
|
30499
|
+
delete_cross_product_edge: deleteCrossProductEdgeTool,
|
|
30500
|
+
batch_create_cross_product_edges: batchCreateCrossProductEdges,
|
|
30163
30501
|
attach_product_to_portfolio: attachProductToPortfolioTool,
|
|
30502
|
+
detach_product_from_portfolio: detachProductFromPortfolioTool,
|
|
30164
30503
|
list_portfolio_cross_edges: listPortfolioCrossEdges,
|
|
30165
30504
|
migrate_cross_edges: migrateCrossEdges,
|
|
30166
30505
|
get_sync_state: getSyncState,
|