@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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schema_version": "2",
3
3
  "package": "@unified-product-graph/mcp-server",
4
- "package_version": "0.8.14",
5
- "tool_count": 96,
4
+ "package_version": "0.8.16",
5
+ "tool_count": 106,
6
6
  "domains": [
7
7
  "context",
8
8
  "nodes",
@@ -1523,6 +1523,42 @@
1523
1523
  ],
1524
1524
  "atomicity": "atomic-with-rollback. Classification runs against the live\ndocument before any mutation; with `dry_run: false`, the drop set is\ncomputed up-front and applied in a single index rebuild."
1525
1525
  },
1526
+ {
1527
+ "name": "assign_product_to_area",
1528
+ "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.",
1529
+ "domain": "areas",
1530
+ "inputSchema": {
1531
+ "type": "object",
1532
+ "properties": {
1533
+ "product_id": {
1534
+ "type": "string",
1535
+ "description": "Product id (from create_product / list_local_products)"
1536
+ },
1537
+ "area_id": {
1538
+ "type": "string",
1539
+ "description": "Product area id (from list_product_areas)"
1540
+ }
1541
+ },
1542
+ "required": [
1543
+ "product_id",
1544
+ "area_id"
1545
+ ]
1546
+ },
1547
+ "throws": [
1548
+ "textError on a missing workspace, an unknown product, or an unknown\narea id (the message points at list_product_areas / list_local_products)."
1549
+ ],
1550
+ "examples": [],
1551
+ "warnings": [],
1552
+ "see": [
1553
+ "attach_product_to_portfolio",
1554
+ "create_product"
1555
+ ],
1556
+ "source": "src/tools/areas.ts:71",
1557
+ "symbol": "assignProductToAreaTool",
1558
+ "returns": "JSON: `{ product_id, container_id, container_kind: \"product_area\",\ncontainer_title?, already_member, registered }`.",
1559
+ "return_shape": "{ product_id, container_id, container_kind: \"product_area\", container_title?, already_member, registered }",
1560
+ "atomicity": "atomic (single portfolio.upg flush)."
1561
+ },
1526
1562
  {
1527
1563
  "name": "create_area",
1528
1564
  "description": "Create a product area entity in the portfolio document (`.upg/portfolio.upg`). Product areas represent the organisational axis (who owns what). Supports nesting via `parent_area_id`. The portfolio document is created on demand.",
@@ -1545,12 +1581,13 @@
1545
1581
  "strategic_priority": {
1546
1582
  "type": "string",
1547
1583
  "enum": [
1548
- "critical",
1584
+ "urgent",
1549
1585
  "high",
1550
1586
  "medium",
1551
- "low"
1587
+ "low",
1588
+ "none"
1552
1589
  ],
1553
- "description": "Strategic priority of this area"
1590
+ "description": "Strategic priority of this area (canonical Priority scale)"
1554
1591
  },
1555
1592
  "owner": {
1556
1593
  "type": "string",
@@ -1575,7 +1612,7 @@
1575
1612
  "see": [
1576
1613
  "list_product_areas"
1577
1614
  ],
1578
- "source": "src/tools/areas.ts:261",
1615
+ "source": "src/tools/areas.ts:294",
1579
1616
  "symbol": "createArea",
1580
1617
  "returns": "JSON: `{ node, portfolio_file, written_to }`. `node` is the typed\n`UPGProductArea` record persisted to `portfolio_areas[]`.",
1581
1618
  "return_shape": "{ node, portfolio_file, written_to }",
@@ -1584,6 +1621,41 @@
1584
1621
  ],
1585
1622
  "atomicity": "atomic per write; the portfolio file is read, mutated, and\nflushed in one pass."
1586
1623
  },
1624
+ {
1625
+ "name": "delete_area",
1626
+ "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.",
1627
+ "domain": "areas",
1628
+ "inputSchema": {
1629
+ "type": "object",
1630
+ "properties": {
1631
+ "area_id": {
1632
+ "type": "string",
1633
+ "description": "Product area id to delete (from list_product_areas)"
1634
+ },
1635
+ "force": {
1636
+ "type": "boolean",
1637
+ "description": "Delete even if the area still has products (default false)"
1638
+ }
1639
+ },
1640
+ "required": [
1641
+ "area_id"
1642
+ ]
1643
+ },
1644
+ "throws": [
1645
+ "textError on a missing workspace, unknown area, or a non-empty area without\n`force`."
1646
+ ],
1647
+ "examples": [],
1648
+ "warnings": [],
1649
+ "see": [
1650
+ "create_area",
1651
+ "remove_product_from_area"
1652
+ ],
1653
+ "source": "src/tools/areas.ts:441",
1654
+ "symbol": "deleteAreaTool",
1655
+ "returns": "JSON: `{ message, area_id, deleted, unnested_children: string[] }`.",
1656
+ "return_shape": "{ message, area_id, deleted, unnested_children: string[] }",
1657
+ "atomicity": "atomic (single portfolio.upg flush)."
1658
+ },
1587
1659
  {
1588
1660
  "name": "get_area_context",
1589
1661
  "description": "Check whether the current working directory has a `.upg-area.json` that scopes work to a specific product area.",
@@ -1602,7 +1674,7 @@
1602
1674
  ],
1603
1675
  "warnings": [],
1604
1676
  "see": [],
1605
- "source": "src/tools/areas.ts:211",
1677
+ "source": "src/tools/areas.ts:244",
1606
1678
  "symbol": "getAreaContext",
1607
1679
  "returns": "JSON: `{ has_area_context: false }` or\n`{ has_area_context: true, area_id, area_name, found_at }`.",
1608
1680
  "return_shape": "{ has_area_context: false }",
@@ -1642,7 +1714,7 @@
1642
1714
  "see": [
1643
1715
  "list_product_areas"
1644
1716
  ],
1645
- "source": "src/tools/areas.ts:69",
1717
+ "source": "src/tools/areas.ts:102",
1646
1718
  "symbol": "getAreaGraph",
1647
1719
  "returns": "JSON: `{ area, nodes, edges, node_count, edge_count }`. May\ninclude a `degraded` block when the response was auto-trimmed.",
1648
1720
  "return_shape": "{ area, nodes, edges, node_count, edge_count }",
@@ -1674,7 +1746,7 @@
1674
1746
  ],
1675
1747
  "warnings": [],
1676
1748
  "see": [],
1677
- "source": "src/tools/areas.ts:300",
1749
+ "source": "src/tools/areas.ts:333",
1678
1750
  "symbol": "getChanges",
1679
1751
  "returns": "JSON: `{ changes, summary: { create, update, delete }, total }`.\n`since` filters to ISO 8601 timestamps after the cutoff.",
1680
1752
  "return_shape": "{ changes, summary: { create, update, delete }, total }",
@@ -1704,15 +1776,265 @@
1704
1776
  "create_area",
1705
1777
  "get_area_graph"
1706
1778
  ],
1707
- "source": "src/tools/areas.ts:33",
1779
+ "source": "src/tools/areas.ts:38",
1708
1780
  "symbol": "listProductAreas",
1709
1781
  "returns": "JSON: `{ areas: Array<{ id, title, strategic_priority?,\nparent_area_id?, products? }>, total }`.",
1710
1782
  "return_shape": "{ areas: Array<{ id, title, strategic_priority?, parent_area_id?, products? }>, total }",
1711
1783
  "atomicity": "atomic (read-only)"
1712
1784
  },
1785
+ {
1786
+ "name": "move_product_to_area",
1787
+ "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.",
1788
+ "domain": "areas",
1789
+ "inputSchema": {
1790
+ "type": "object",
1791
+ "properties": {
1792
+ "product_id": {
1793
+ "type": "string",
1794
+ "description": "Product id (from list_local_products)"
1795
+ },
1796
+ "to_area_id": {
1797
+ "type": "string",
1798
+ "description": "Destination product area id (from list_product_areas)"
1799
+ },
1800
+ "from_area_id": {
1801
+ "type": "string",
1802
+ "description": "Source area id to remove from; omit to remove from all areas"
1803
+ }
1804
+ },
1805
+ "required": [
1806
+ "product_id",
1807
+ "to_area_id"
1808
+ ]
1809
+ },
1810
+ "throws": [
1811
+ "textError on a missing workspace, unknown product, or unknown target area."
1812
+ ],
1813
+ "examples": [],
1814
+ "warnings": [],
1815
+ "see": [
1816
+ "assign_product_to_area",
1817
+ "remove_product_from_area"
1818
+ ],
1819
+ "source": "src/tools/areas.ts:464",
1820
+ "symbol": "moveProductToAreaTool",
1821
+ "returns": "JSON: `{ product_id, to_area_id, to_area_title?, removed_from: string[], added }`.",
1822
+ "return_shape": "{ product_id, to_area_id, to_area_title?, removed_from: string[], added }",
1823
+ "atomicity": "atomic (single portfolio.upg flush)."
1824
+ },
1825
+ {
1826
+ "name": "remove_product_from_area",
1827
+ "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`.",
1828
+ "domain": "areas",
1829
+ "inputSchema": {
1830
+ "type": "object",
1831
+ "properties": {
1832
+ "product_id": {
1833
+ "type": "string",
1834
+ "description": "Product id (from list_local_products)"
1835
+ },
1836
+ "area_id": {
1837
+ "type": "string",
1838
+ "description": "Product area id (from list_product_areas)"
1839
+ }
1840
+ },
1841
+ "required": [
1842
+ "product_id",
1843
+ "area_id"
1844
+ ]
1845
+ },
1846
+ "throws": [
1847
+ "textError on a missing workspace or an unknown area id."
1848
+ ],
1849
+ "examples": [],
1850
+ "warnings": [],
1851
+ "see": [
1852
+ "assign_product_to_area",
1853
+ "move_product_to_area"
1854
+ ],
1855
+ "source": "src/tools/areas.ts:414",
1856
+ "symbol": "removeProductFromAreaTool",
1857
+ "returns": "JSON: `{ product_id, container_id, container_kind: \"product_area\",\ncontainer_title?, removed }`. `removed: false` (not an error) when the product\nwas not a member, so retries are idempotent.",
1858
+ "return_shape": "{ product_id, container_id, container_kind: \"product_area\", container_title?, removed }",
1859
+ "return_notes": [
1860
+ "`removed: false` (not an error) when the product was not a member, so retries are idempotent."
1861
+ ],
1862
+ "atomicity": "atomic (single portfolio.upg flush)."
1863
+ },
1864
+ {
1865
+ "name": "update_area",
1866
+ "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).",
1867
+ "domain": "areas",
1868
+ "inputSchema": {
1869
+ "type": "object",
1870
+ "properties": {
1871
+ "area_id": {
1872
+ "type": "string",
1873
+ "description": "Product area id to edit (from list_product_areas)"
1874
+ },
1875
+ "title": {
1876
+ "type": "string",
1877
+ "description": "New area title"
1878
+ },
1879
+ "description": {
1880
+ "type": "string",
1881
+ "description": "New area description"
1882
+ },
1883
+ "strategic_priority": {
1884
+ "type": "string",
1885
+ "enum": [
1886
+ "urgent",
1887
+ "high",
1888
+ "medium",
1889
+ "low",
1890
+ "none"
1891
+ ],
1892
+ "description": "Strategic priority (canonical Priority scale)"
1893
+ },
1894
+ "parent_area_id": {
1895
+ "type": [
1896
+ "string",
1897
+ "null"
1898
+ ],
1899
+ "description": "Re-parent under this area id; null un-nests (top-level); omit to leave unchanged"
1900
+ },
1901
+ "owner": {
1902
+ "type": "string",
1903
+ "description": "Person or team that owns this area"
1904
+ }
1905
+ },
1906
+ "required": [
1907
+ "area_id"
1908
+ ]
1909
+ },
1910
+ "throws": [
1911
+ "textError on a missing workspace, unknown area/parent, a re-parent cycle, or\nwhen no editable field is supplied."
1912
+ ],
1913
+ "examples": [],
1914
+ "warnings": [],
1915
+ "see": [
1916
+ "create_area",
1917
+ "list_product_areas"
1918
+ ],
1919
+ "source": "src/tools/areas.ts:367",
1920
+ "symbol": "updateAreaTool",
1921
+ "returns": "JSON: `{ message, area, updated: string[] }`.",
1922
+ "return_shape": "{ message, area, updated: string[] }",
1923
+ "atomicity": "atomic (single portfolio.upg flush)."
1924
+ },
1925
+ {
1926
+ "name": "attach_product_to_portfolio",
1927
+ "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.",
1928
+ "domain": "workspace",
1929
+ "inputSchema": {
1930
+ "type": "object",
1931
+ "properties": {
1932
+ "product_id": {
1933
+ "type": "string",
1934
+ "description": "Product id (from create_product / list_local_products)"
1935
+ },
1936
+ "portfolio_id": {
1937
+ "type": "string",
1938
+ "description": "Portfolio id (from list_portfolios)"
1939
+ }
1940
+ },
1941
+ "required": [
1942
+ "product_id",
1943
+ "portfolio_id"
1944
+ ]
1945
+ },
1946
+ "throws": [
1947
+ "textError on a missing workspace, an unknown product, or an unknown\nportfolio id (the message points at list_portfolios / list_local_products)."
1948
+ ],
1949
+ "examples": [],
1950
+ "warnings": [],
1951
+ "see": [
1952
+ "assign_product_to_area",
1953
+ "create_product"
1954
+ ],
1955
+ "source": "src/tools/workspace.ts:777",
1956
+ "symbol": "attachProductToPortfolioTool",
1957
+ "returns": "JSON: `{ product_id, container_id, container_kind: \"portfolio\",\ncontainer_title?, already_member, registered }`.",
1958
+ "return_shape": "{ product_id, container_id, container_kind: \"portfolio\", container_title?, already_member, registered }",
1959
+ "atomicity": "atomic (single portfolio.upg flush)."
1960
+ },
1961
+ {
1962
+ "name": "batch_create_cross_product_edges",
1963
+ "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.",
1964
+ "domain": "workspace",
1965
+ "inputSchema": {
1966
+ "type": "object",
1967
+ "properties": {
1968
+ "edges": {
1969
+ "type": "array",
1970
+ "description": "Cross-product edges to create (max 50). Each: { source_id, target_id, type, source_product_id?, target_product_id? }.",
1971
+ "items": {
1972
+ "type": "object",
1973
+ "properties": {
1974
+ "source_id": {
1975
+ "type": "string",
1976
+ "description": "Source node ID (bare or qualified {product_id}/{node_id})"
1977
+ },
1978
+ "target_id": {
1979
+ "type": "string",
1980
+ "description": "Target node ID (bare or qualified {product_id}/{node_id})"
1981
+ },
1982
+ "type": {
1983
+ "type": "string",
1984
+ "enum": [
1985
+ "shares_persona",
1986
+ "shares_competitor",
1987
+ "shares_metric",
1988
+ "depends_on_product",
1989
+ "cannibalises",
1990
+ "succeeds",
1991
+ "hosts"
1992
+ ],
1993
+ "description": "Cross-product relationship type"
1994
+ },
1995
+ "source_product_id": {
1996
+ "type": "string",
1997
+ "description": "Product ID of the source node (qualifies a bare source_id)"
1998
+ },
1999
+ "target_product_id": {
2000
+ "type": "string",
2001
+ "description": "Product ID of the target node (qualifies a bare target_id)"
2002
+ }
2003
+ },
2004
+ "required": [
2005
+ "source_id",
2006
+ "target_id",
2007
+ "type"
2008
+ ]
2009
+ }
2010
+ },
2011
+ "auto_create_portfolio": {
2012
+ "type": "boolean",
2013
+ "description": "Create an empty portfolio document if none exists (default false)"
2014
+ }
2015
+ },
2016
+ "required": [
2017
+ "edges"
2018
+ ]
2019
+ },
2020
+ "throws": [
2021
+ "textError when `edges` is missing/empty/oversized, when any edge is invalid,\nor when no portfolio document exists (pass `auto_create_portfolio: true` to mint one)."
2022
+ ],
2023
+ "examples": [],
2024
+ "warnings": [],
2025
+ "see": [
2026
+ "create_cross_product_edge",
2027
+ "list_cross_edge_types"
2028
+ ],
2029
+ "source": "src/tools/workspace.ts:860",
2030
+ "symbol": "batchCreateCrossProductEdges",
2031
+ "returns": "JSON: `{ message, created: UPGCrossEdge[], count, portfolio_file,\nregistered_products? }`.",
2032
+ "return_shape": "{ message, created: UPGCrossEdge[], count, portfolio_file, registered_products? }",
2033
+ "atomicity": "atomic. All edges validated first, then a single portfolio.upg flush."
2034
+ },
1713
2035
  {
1714
2036
  "name": "create_cross_product_edge",
1715
- "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`.",
2037
+ "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).",
1716
2038
  "domain": "workspace",
1717
2039
  "inputSchema": {
1718
2040
  "type": "object",
@@ -1733,7 +2055,8 @@
1733
2055
  "shares_metric",
1734
2056
  "depends_on_product",
1735
2057
  "cannibalises",
1736
- "succeeds"
2058
+ "succeeds",
2059
+ "hosts"
1737
2060
  ],
1738
2061
  "description": "Cross-product relationship type"
1739
2062
  },
@@ -1762,7 +2085,7 @@
1762
2085
  "list_portfolio_cross_edges",
1763
2086
  "migrate_cross_edges"
1764
2087
  ],
1765
- "source": "src/tools/workspace.ts:413",
2088
+ "source": "src/tools/workspace.ts:494",
1766
2089
  "symbol": "createCrossProductEdge",
1767
2090
  "returns": "JSON: `{ edge, portfolio_file }`.",
1768
2091
  "return_shape": "{ edge, portfolio_file }",
@@ -1793,7 +2116,11 @@
1793
2116
  },
1794
2117
  "portfolio_id": {
1795
2118
  "type": "string",
1796
- "description": "Optional portfolio node id in the current store. When provided, a `portfolio_contains_product` edge is created in the current graph."
2119
+ "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)."
2120
+ },
2121
+ "area_id": {
2122
+ "type": "string",
2123
+ "description": "Optional product_area id (resolved against portfolio.upg) to place the new product under."
1797
2124
  }
1798
2125
  },
1799
2126
  "required": [
@@ -1814,7 +2141,7 @@
1814
2141
  "see": [
1815
2142
  "init_workspace"
1816
2143
  ],
1817
- "source": "src/tools/workspace.ts:293",
2144
+ "source": "src/tools/workspace.ts:335",
1818
2145
  "symbol": "createProductTool",
1819
2146
  "returns": "JSON: `{ message, ...result }`. `result` carries `id`, `title`,\n`slug`, `file_path`, and the optional portfolio edge.",
1820
2147
  "return_shape": "{ message,...result }",
@@ -1823,6 +2150,78 @@
1823
2150
  ],
1824
2151
  "atomicity": "non-atomic. File write + workspace.json patch + optional\nportfolio edge are separate mutations."
1825
2152
  },
2153
+ {
2154
+ "name": "delete_cross_product_edge",
2155
+ "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.",
2156
+ "domain": "workspace",
2157
+ "inputSchema": {
2158
+ "type": "object",
2159
+ "properties": {
2160
+ "edge_id": {
2161
+ "type": "string",
2162
+ "description": "Cross-product edge id (from list_portfolio_cross_edges)"
2163
+ }
2164
+ },
2165
+ "required": [
2166
+ "edge_id"
2167
+ ]
2168
+ },
2169
+ "throws": [
2170
+ "textError on a missing workspace."
2171
+ ],
2172
+ "examples": [],
2173
+ "warnings": [],
2174
+ "see": [
2175
+ "create_cross_product_edge",
2176
+ "list_portfolio_cross_edges"
2177
+ ],
2178
+ "source": "src/tools/workspace.ts:831",
2179
+ "symbol": "deleteCrossProductEdgeTool",
2180
+ "returns": "JSON: `{ edge_id, deleted, edge? }`. `deleted: false` (not an error) when\nno edge with that id exists, so retries are idempotent.",
2181
+ "return_shape": "{ edge_id, deleted, edge? }",
2182
+ "return_notes": [
2183
+ "`deleted: false` (not an error) when no edge with that id exists, so retries are idempotent."
2184
+ ],
2185
+ "atomicity": "atomic (single portfolio.upg flush)."
2186
+ },
2187
+ {
2188
+ "name": "detach_product_from_portfolio",
2189
+ "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`.",
2190
+ "domain": "workspace",
2191
+ "inputSchema": {
2192
+ "type": "object",
2193
+ "properties": {
2194
+ "product_id": {
2195
+ "type": "string",
2196
+ "description": "Product id (from list_local_products)"
2197
+ },
2198
+ "portfolio_id": {
2199
+ "type": "string",
2200
+ "description": "Portfolio id (from list_portfolios)"
2201
+ }
2202
+ },
2203
+ "required": [
2204
+ "product_id",
2205
+ "portfolio_id"
2206
+ ]
2207
+ },
2208
+ "throws": [
2209
+ "textError on a missing workspace or an unknown portfolio id."
2210
+ ],
2211
+ "examples": [],
2212
+ "warnings": [],
2213
+ "see": [
2214
+ "attach_product_to_portfolio"
2215
+ ],
2216
+ "source": "src/tools/workspace.ts:804",
2217
+ "symbol": "detachProductFromPortfolioTool",
2218
+ "returns": "JSON: `{ product_id, container_id, container_kind: \"portfolio\",\ncontainer_title?, removed }`. `removed: false` (not an error) when the product was\nnot a member, so retries are idempotent.",
2219
+ "return_shape": "{ product_id, container_id, container_kind: \"portfolio\", container_title?, removed }",
2220
+ "return_notes": [
2221
+ "`removed: false` (not an error) when the product was not a member, so retries are idempotent."
2222
+ ],
2223
+ "atomicity": "atomic (single portfolio.upg flush)."
2224
+ },
1826
2225
  {
1827
2226
  "name": "get_organization",
1828
2227
  "description": "Get the organisation that owns the current workspace's portfolio. Reads the singleton `portfolio.upg.organization`. Returns `{ organization: null }` when no portfolio document exists yet.",
@@ -1843,7 +2242,7 @@
1843
2242
  "see": [
1844
2243
  "list_portfolios"
1845
2244
  ],
1846
- "source": "src/tools/workspace.ts:362",
2245
+ "source": "src/tools/workspace.ts:443",
1847
2246
  "symbol": "getOrganization",
1848
2247
  "returns": "JSON: `{ organization: UPGOrganization | null, portfolio_file? }`.\nReturns `{ organization: null }` when no portfolio document exists yet.",
1849
2248
  "return_shape": "{ organization: UPGOrganization | null, portfolio_file? }",
@@ -1872,7 +2271,7 @@
1872
2271
  "see": [
1873
2272
  "init_workspace"
1874
2273
  ],
1875
- "source": "src/tools/workspace.ts:181",
2274
+ "source": "src/tools/workspace.ts:223",
1876
2275
  "symbol": "getWorkspaceInfo",
1877
2276
  "returns": "JSON: `{ mode, workspace_path?, current_product?, current_file?,\nproducts }`. The shape depends on whether `.upg/workspace.json` exists.",
1878
2277
  "return_shape": "{ mode, workspace_path?, current_product?, current_file?, products }",
@@ -1912,7 +2311,7 @@
1912
2311
  "switch_product",
1913
2312
  "get_workspace_info"
1914
2313
  ],
1915
- "source": "src/tools/workspace.ts:257",
2314
+ "source": "src/tools/workspace.ts:299",
1916
2315
  "symbol": "initWorkspaceTool",
1917
2316
  "returns": "JSON: `{ message, ...result }`. `result` carries the workspace\npath and the moved file's new location.",
1918
2317
  "return_shape": "{ message,...result }",
@@ -1942,7 +2341,7 @@
1942
2341
  "switch_product",
1943
2342
  "get_workspace_info"
1944
2343
  ],
1945
- "source": "src/tools/workspace.ts:44",
2344
+ "source": "src/tools/workspace.ts:48",
1946
2345
  "symbol": "listLocalProducts",
1947
2346
  "returns": "JSON: `{ products: Array<{ file, title, stage, nodes, edges }> }`.\n`stage` is the CANONICAL UPGProductStage (legacy values like `idea` are\ncoerced to `concept`), or `null` when unset — matching what\n`get_product_context` reports for the same product (UPG-611 / DT-MCP-3).",
1948
2347
  "return_shape": "{ products: Array<{ file, title, stage, nodes, edges }> }",
@@ -1965,7 +2364,7 @@
1965
2364
  "see": [
1966
2365
  "create_cross_product_edge"
1967
2366
  ],
1968
- "source": "src/tools/workspace.ts:552",
2367
+ "source": "src/tools/workspace.ts:633",
1969
2368
  "symbol": "listPortfolioCrossEdges",
1970
2369
  "returns": "JSON: `{ cross_edges: UPGCrossEdge[], total, portfolio_file? }`.",
1971
2370
  "return_shape": "{ cross_edges: UPGCrossEdge[], total, portfolio_file? }",
@@ -1992,7 +2391,7 @@
1992
2391
  "create_cross_product_edge",
1993
2392
  "get_organization"
1994
2393
  ],
1995
- "source": "src/tools/workspace.ts:335",
2394
+ "source": "src/tools/workspace.ts:416",
1996
2395
  "symbol": "listPortfolios",
1997
2396
  "returns": "JSON: `{ portfolios: Array<{ id, title, description?,\nparent_portfolio_id?, hierarchy_model?, products? }>, total }`.",
1998
2397
  "return_shape": "{ portfolios: Array<{ id, title, description?, parent_portfolio_id?, hierarchy_model?, products? }>, total }",
@@ -2035,7 +2434,7 @@
2035
2434
  "list_cross_edge_types",
2036
2435
  "init_workspace"
2037
2436
  ],
2038
- "source": "src/tools/workspace.ts:621",
2437
+ "source": "src/tools/workspace.ts:702",
2039
2438
  "symbol": "migrateCrossEdges",
2040
2439
  "returns": "JSON: `{ migrated, skipped, dry_run, portfolio_file? }`.",
2041
2440
  "return_shape": "{ migrated, skipped, dry_run, portfolio_file? }",
@@ -2073,12 +2472,58 @@
2073
2472
  "list_local_products",
2074
2473
  "init_workspace"
2075
2474
  ],
2076
- "source": "src/tools/workspace.ts:118",
2475
+ "source": "src/tools/workspace.ts:160",
2077
2476
  "symbol": "switchProduct",
2078
2477
  "returns": "JSON: `{ message, file, product: { title, stage }, entities }`.",
2079
2478
  "return_shape": "{ message, file, product: { title, stage }, entities }",
2080
2479
  "atomicity": "non-atomic. Flushes the current store, stops watching, and\nloads the new file as separate filesystem operations."
2081
2480
  },
2481
+ {
2482
+ "name": "update_product",
2483
+ "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.",
2484
+ "domain": "workspace",
2485
+ "inputSchema": {
2486
+ "type": "object",
2487
+ "properties": {
2488
+ "stage": {
2489
+ "type": "string",
2490
+ "description": "Product lifecycle stage (canonical UPGProductStage)."
2491
+ },
2492
+ "title": {
2493
+ "type": "string",
2494
+ "description": "Product display title."
2495
+ },
2496
+ "description": {
2497
+ "type": "string",
2498
+ "description": "Product description."
2499
+ },
2500
+ "health_status": {
2501
+ "type": "string",
2502
+ "description": "Product health (free-form, e.g. on_track / at_risk)."
2503
+ },
2504
+ "url": {
2505
+ "type": "string",
2506
+ "description": "Product URL."
2507
+ }
2508
+ }
2509
+ },
2510
+ "throws": [
2511
+ "textError when no field is supplied, when there is no product header,\nor when `stage` is non-canonical (same strict validation as create_product)."
2512
+ ],
2513
+ "examples": [],
2514
+ "warnings": [],
2515
+ "see": [
2516
+ "create_product"
2517
+ ],
2518
+ "source": "src/tools/workspace.ts:376",
2519
+ "symbol": "updateProductTool",
2520
+ "returns": "JSON: `{ product, updated: string[] }` (the fields changed).",
2521
+ "return_shape": "{ product, updated: string[] }",
2522
+ "return_notes": [
2523
+ "(the fields changed)."
2524
+ ],
2525
+ "atomicity": "atomic (single flush)."
2526
+ },
2082
2527
  {
2083
2528
  "name": "get_entity_schema",
2084
2529
  "description": "Return expected properties, valid statuses, valid edge types, and domain for an entity type. Lets agents construct valid entities without skill prompts.",
@@ -3019,7 +3464,7 @@
3019
3464
  },
3020
3465
  {
3021
3466
  "name": "list_cross_edge_types",
3022
- "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`.",
3467
+ "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`.",
3023
3468
  "domain": "spec",
3024
3469
  "inputSchema": {
3025
3470
  "type": "object",