@unified-product-graph/mcp-server 0.8.14 → 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/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);
@@ -10131,7 +10132,8 @@ var UPG_PROPERTY_SCHEMA = {
10131
10132
  // ProductAreaProperties: ProductArea entity.
10132
10133
  product_area: {
10133
10134
  strategic_priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Strategic priority assigned to this area" },
10134
- description: { type: "string", description: "Narrative description of what this area covers" }
10135
+ description: { type: "string", description: "Narrative description of what this area covers" },
10136
+ owner: { type: "string", description: "Person or team that owns this area" }
10135
10137
  },
10136
10138
  // ProgramProperties: Program.
10137
10139
  program: {
@@ -24371,7 +24373,7 @@ function serializePortfolioWithHeader(doc, opts) {
24371
24373
  header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
24372
24374
  return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
24373
24375
  }
24374
- var UPG_VERSION = "0.8.14";
24376
+ var UPG_VERSION = "0.8.16";
24375
24377
  var MARKDOWN_FORMAT_VERSION = "0.1";
24376
24378
  var UPG_TYPES = getTypes();
24377
24379
  var UPG_TYPES_SET = new Set(UPG_TYPES);
@@ -26255,6 +26257,11 @@ import * as path2 from "path";
26255
26257
  import {
26256
26258
  writePortfolioScopedNode as writePortfolioScopedNode2,
26257
26259
  openPortfolioStoreIfExists,
26260
+ assignProductToArea,
26261
+ updateProductArea,
26262
+ removeProductFromArea,
26263
+ deleteArea,
26264
+ moveProductToArea,
26258
26265
  PortfolioRoutingError as PortfolioRoutingError2
26259
26266
  } from "@unified-product-graph/sdk";
26260
26267
  var listProductAreas = async (_args, _ctx) => {
@@ -26269,11 +26276,24 @@ var listProductAreas = async (_args, _ctx) => {
26269
26276
  if (area.description) row.description = area.description;
26270
26277
  if (area.parent_area_id !== void 0) row.parent_area_id = area.parent_area_id;
26271
26278
  if (area.strategic_priority) row.strategic_priority = area.strategic_priority;
26279
+ if (area.owner) row.owner = area.owner;
26272
26280
  if (area.products) row.products = area.products;
26273
26281
  return row;
26274
26282
  });
26275
26283
  return text(JSON.stringify({ areas: result, total: result.length }, null, 2));
26276
26284
  };
26285
+ var assignProductToAreaTool = async (args, _ctx) => {
26286
+ const productId = args.product_id;
26287
+ const areaId = args.area_id;
26288
+ if (!productId) return textError("Missing required parameter: product_id");
26289
+ if (!areaId) return textError("Missing required parameter: area_id");
26290
+ try {
26291
+ const result = await assignProductToArea(process.cwd(), { product_id: productId, area_id: areaId });
26292
+ return text(JSON.stringify(result, null, 2));
26293
+ } catch (err) {
26294
+ return textError(err.message);
26295
+ }
26296
+ };
26277
26297
  var getAreaGraph = (args, ctx) => {
26278
26298
  const { store } = ctx;
26279
26299
  const areaId = args.area_id;
@@ -26428,6 +26448,7 @@ var createArea = async (args, _ctx) => {
26428
26448
  const properties = {};
26429
26449
  if (args.strategic_priority) properties.strategic_priority = args.strategic_priority;
26430
26450
  if (args.parent_area_id) properties.parent_area_id = args.parent_area_id;
26451
+ if (args.owner) properties.owner = args.owner;
26431
26452
  try {
26432
26453
  const result = await writePortfolioScopedNode2(process.cwd(), {
26433
26454
  type: "product_area",
@@ -26463,6 +26484,73 @@ var getChanges = (args, ctx) => {
26463
26484
  )
26464
26485
  );
26465
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
+ };
26466
26554
 
26467
26555
  // src/tools/workspace.ts
26468
26556
  import * as fs from "fs";
@@ -26474,10 +26562,14 @@ import {
26474
26562
  resolvePortfolioPath,
26475
26563
  openPortfolioStoreIfExists as openPortfolioStoreIfExists2,
26476
26564
  registerProductOnPortfolio,
26477
- findProductFileById
26565
+ findProductFileById,
26566
+ attachProductToPortfolio,
26567
+ detachProductFromPortfolio,
26568
+ deleteCrossProductEdge
26478
26569
  } from "@unified-product-graph/sdk";
26479
26570
  import {
26480
26571
  createProduct,
26572
+ updateProduct,
26481
26573
  initWorkspace,
26482
26574
  InvalidProductNameError,
26483
26575
  InvalidProductStageError,
@@ -26487,6 +26579,25 @@ import {
26487
26579
  var listLocalProducts = (_args, _ctx) => {
26488
26580
  const cwd = process.cwd();
26489
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
+ }
26490
26601
  const candidates = [];
26491
26602
  const topEntries = fs.readdirSync(cwd, { withFileTypes: true });
26492
26603
  for (const entry of topEntries) {
@@ -26511,13 +26622,19 @@ var listLocalProducts = (_args, _ctx) => {
26511
26622
  try {
26512
26623
  const raw = fs.readFileSync(filePath, "utf-8");
26513
26624
  const doc = JSON.parse(raw);
26625
+ if (!doc.product) continue;
26514
26626
  const coerced = coerceProductStage(doc.product?.stage);
26627
+ const pid = doc.product?.id ?? null;
26628
+ const m = pid ? membership.get(pid) : void 0;
26515
26629
  products.push({
26630
+ id: pid,
26516
26631
  file: path3.relative(cwd, filePath),
26517
26632
  title: doc.product?.title ?? "(untitled)",
26518
26633
  stage: coerced.canonical ?? null,
26519
26634
  nodes: Array.isArray(doc.nodes) ? doc.nodes.length : 0,
26520
- 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 } : {}
26521
26638
  });
26522
26639
  } catch {
26523
26640
  }
@@ -26645,7 +26762,8 @@ var createProductTool = async (args, ctx) => {
26645
26762
  slug: args.slug,
26646
26763
  description: args.description,
26647
26764
  stage: args.stage,
26648
- portfolio_id: args.portfolio_id
26765
+ portfolio_id: args.portfolio_id,
26766
+ area_id: args.area_id
26649
26767
  });
26650
26768
  return text(
26651
26769
  JSON.stringify({ message: `Created product: ${result.title}`, ...result }, null, 2)
@@ -26658,6 +26776,31 @@ var createProductTool = async (args, ctx) => {
26658
26776
  return textError(`create_product failed: ${err.message}`);
26659
26777
  }
26660
26778
  };
26779
+ var updateProductTool = async (args, ctx) => {
26780
+ const { store } = ctx;
26781
+ try {
26782
+ const result = updateProduct({
26783
+ store,
26784
+ stage: args.stage,
26785
+ title: args.title,
26786
+ description: args.description,
26787
+ health_status: args.health_status,
26788
+ url: args.url
26789
+ });
26790
+ if (result.updated.length === 0) {
26791
+ return textError(
26792
+ "Nothing to update: pass at least one of: stage, title, description, health_status, url."
26793
+ );
26794
+ }
26795
+ await store.flush();
26796
+ return text(
26797
+ JSON.stringify({ message: `Updated product (${result.updated.join(", ")})`, ...result }, null, 2)
26798
+ );
26799
+ } catch (err) {
26800
+ if (err instanceof InvalidProductStageError) return textError(err.message);
26801
+ return textError(`update_product failed: ${err.message}`);
26802
+ }
26803
+ };
26661
26804
  var listPortfolios = async (_args, _ctx) => {
26662
26805
  const portfolioStore = await openPortfolioStoreIfExists2(process.cwd());
26663
26806
  if (!portfolioStore) {
@@ -26867,6 +27010,141 @@ var migrateCrossEdges = async (args, ctx) => {
26867
27010
  )
26868
27011
  );
26869
27012
  };
27013
+ var attachProductToPortfolioTool = async (args, _ctx) => {
27014
+ const productId = args.product_id;
27015
+ const portfolioId = args.portfolio_id;
27016
+ if (!productId) return textError("Missing required parameter: product_id");
27017
+ if (!portfolioId) return textError("Missing required parameter: portfolio_id");
27018
+ try {
27019
+ const result = await attachProductToPortfolio(process.cwd(), {
27020
+ product_id: productId,
27021
+ portfolio_id: portfolioId
27022
+ });
27023
+ return text(JSON.stringify(result, null, 2));
27024
+ } catch (err) {
27025
+ return textError(err.message);
27026
+ }
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
+ };
26870
27148
 
26871
27149
  // src/tools/schema.ts
26872
27150
  var getEntitySchema = (args, _ctx) => {
@@ -29138,12 +29416,30 @@ var TOOL_DEFINITIONS = [
29138
29416
  },
29139
29417
  portfolio_id: {
29140
29418
  type: "string",
29141
- description: "Optional portfolio node id in the current store. When provided, a `portfolio_contains_product` edge is created in the current graph."
29419
+ description: "Optional portfolio id (resolved against portfolio.upg) to place the new product under. A portfolio id that resolves only in the active graph still attaches via an in-graph edge (DEPRECATED; prefer attach_product_to_portfolio)."
29420
+ },
29421
+ area_id: {
29422
+ type: "string",
29423
+ description: "Optional product_area id (resolved against portfolio.upg) to place the new product under."
29142
29424
  }
29143
29425
  },
29144
29426
  required: ["name"]
29145
29427
  }
29146
29428
  },
29429
+ {
29430
+ name: "update_product",
29431
+ description: "Update the product header (`$upg.product`): stage, title, description, health_status, url. The supported way to advance a product's lifecycle stage; it writes the value get_graph_digest reads, without hand-editing the .upg file.",
29432
+ inputSchema: {
29433
+ type: "object",
29434
+ properties: {
29435
+ stage: { type: "string", description: "Product lifecycle stage (canonical UPGProductStage)." },
29436
+ title: { type: "string", description: "Product display title." },
29437
+ description: { type: "string", description: "Product description." },
29438
+ health_status: { type: "string", description: "Product health (free-form, e.g. on_track / at_risk)." },
29439
+ url: { type: "string", description: "Product URL." }
29440
+ }
29441
+ }
29442
+ },
29147
29443
  {
29148
29444
  name: "migrate_type",
29149
29445
  description: "Migrate every entity of one type to another, applying defaults from `UPG_MIGRATIONS`. Three passes commit as one write: (1) node rename, (2) edges through `UPG_EDGE_MIGRATIONS` (catalog-aware renames, direction flips, drops; endpoint guards check post-migration types; uncatalogued edges surface as `unmapped_legacy_edges`), (3) every node through `UPG_PROPERTY_MIGRATIONS` (top-level renames, lifts, drops, self-referential cleanup). Type-specific property rules see the post-rename type.",
@@ -29475,7 +29771,7 @@ var TOOL_DEFINITIONS = [
29475
29771
  },
29476
29772
  {
29477
29773
  name: "list_cross_edge_types",
29478
- 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`.",
29479
29775
  inputSchema: { type: "object", properties: {} }
29480
29776
  },
29481
29777
  {
@@ -29840,14 +30136,110 @@ var TOOL_DEFINITIONS = [
29840
30136
  },
29841
30137
  strategic_priority: {
29842
30138
  type: "string",
29843
- enum: ["critical", "high", "medium", "low"],
29844
- description: "Strategic priority of this area"
30139
+ enum: ["urgent", "high", "medium", "low", "none"],
30140
+ description: "Strategic priority of this area (canonical Priority scale)"
29845
30141
  },
29846
30142
  owner: { type: "string", description: "Person or team that owns this area" }
29847
30143
  },
29848
30144
  required: ["title"]
29849
30145
  }
29850
30146
  },
30147
+ {
30148
+ name: "assign_product_to_area",
30149
+ description: "Place an existing product under a product area (adds it to the area's `products[]` in `.upg/portfolio.upg`). Resolves the area against the portfolio document and auto-registers the product on the portfolio registry. Use after `create_product`, or pass `area_id` to `create_product` directly.",
30150
+ inputSchema: {
30151
+ type: "object",
30152
+ properties: {
30153
+ product_id: { type: "string", description: "Product id (from create_product / list_local_products)" },
30154
+ area_id: { type: "string", description: "Product area id (from list_product_areas)" }
30155
+ },
30156
+ required: ["product_id", "area_id"]
30157
+ }
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
+ },
30219
+ {
30220
+ name: "attach_product_to_portfolio",
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.",
30222
+ inputSchema: {
30223
+ type: "object",
30224
+ properties: {
30225
+ product_id: { type: "string", description: "Product id (from create_product / list_local_products)" },
30226
+ portfolio_id: { type: "string", description: "Portfolio id (from list_portfolios)" }
30227
+ },
30228
+ required: ["product_id", "portfolio_id"]
30229
+ }
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
+ },
29851
30243
  {
29852
30244
  name: "list_portfolios",
29853
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.",
@@ -29860,7 +30252,7 @@ var TOOL_DEFINITIONS = [
29860
30252
  },
29861
30253
  {
29862
30254
  name: "create_cross_product_edge",
29863
- 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).",
29864
30256
  inputSchema: {
29865
30257
  type: "object",
29866
30258
  properties: {
@@ -29868,7 +30260,7 @@ var TOOL_DEFINITIONS = [
29868
30260
  target_id: { type: "string", description: "Target node ID" },
29869
30261
  type: {
29870
30262
  type: "string",
29871
- 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"],
29872
30264
  description: "Cross-product relationship type"
29873
30265
  },
29874
30266
  source_product_id: { type: "string", description: "Product ID of the source node" },
@@ -29877,6 +30269,47 @@ var TOOL_DEFINITIONS = [
29877
30269
  required: ["source_id", "target_id", "type"]
29878
30270
  }
29879
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
+ },
29880
30313
  {
29881
30314
  name: "list_portfolio_cross_edges",
29882
30315
  description: "List all cross-product edges stored in the portfolio document (`.upg/portfolio.upg`). Empty list when the portfolio document is absent.",
@@ -29999,6 +30432,7 @@ var HANDLERS = {
29999
30432
  get_workspace_info: getWorkspaceInfo,
30000
30433
  init_workspace: initWorkspaceTool,
30001
30434
  create_product: createProductTool,
30435
+ update_product: updateProductTool,
30002
30436
  migrate_type: migrateType,
30003
30437
  migrate_properties: migrateProperties,
30004
30438
  migrate_status: migrateStatus,
@@ -30054,9 +30488,18 @@ var HANDLERS = {
30054
30488
  skill_audit: skillAudit,
30055
30489
  get_area_context: getAreaContext,
30056
30490
  create_area: createArea,
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,
30057
30496
  list_portfolios: listPortfolios,
30058
30497
  get_organization: getOrganization,
30059
30498
  create_cross_product_edge: createCrossProductEdge,
30499
+ delete_cross_product_edge: deleteCrossProductEdgeTool,
30500
+ batch_create_cross_product_edges: batchCreateCrossProductEdges,
30501
+ attach_product_to_portfolio: attachProductToPortfolioTool,
30502
+ detach_product_from_portfolio: detachProductFromPortfolioTool,
30060
30503
  list_portfolio_cross_edges: listPortfolioCrossEdges,
30061
30504
  migrate_cross_edges: migrateCrossEdges,
30062
30505
  get_sync_state: getSyncState,