@unified-product-graph/mcp-server 0.9.0 → 0.9.2
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 +546 -94
- 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
|
|
@@ -1303,8 +1303,24 @@ var UPG_EDGE_CATALOG = {
|
|
|
1303
1303
|
journey_step_realised_by_feature: { forward_verb: "realised_by", reverse_verb: "realises", classification: "cross-domain", source_type: "journey_step", target_type: "feature" },
|
|
1304
1304
|
opportunity_improves_user_journey: { forward_verb: "improves", reverse_verb: "improved_by", classification: "cross-domain", source_type: "opportunity", target_type: "user_journey" },
|
|
1305
1305
|
user_journey_passes_through_journey_phase: { forward_verb: "passes_through", reverse_verb: "is_phase_of", classification: "hierarchy", source_type: "user_journey", target_type: "journey_phase" },
|
|
1306
|
-
|
|
1306
|
+
// (since v0.9.2, UPG-663) A journey_phase is a temporal BAND over the
|
|
1307
|
+
// journey's single step timeline, not a container that owns steps. Steps
|
|
1308
|
+
// belong to the journey via `user_journey_contains_journey_step` (the stable
|
|
1309
|
+
// 0.1.0 spine); a phase merely SPANS a range of them. Mirrors the marketing
|
|
1310
|
+
// precedent `customer_journey_stage_spans_journey_step`. Renamed from the
|
|
1311
|
+
// owning `journey_phase_has_step` (hierarchy) so a step has exactly one
|
|
1312
|
+
// containment parent. See UPG_EDGE_MIGRATIONS['0.9.2'].
|
|
1313
|
+
journey_phase_spans_journey_step: { forward_verb: "spans", reverse_verb: "spanned_by", classification: "cross-domain", source_type: "journey_phase", target_type: "journey_step" },
|
|
1307
1314
|
journey_step_has_action: { forward_verb: "has_action", reverse_verb: "is_action_in", classification: "hierarchy", source_type: "journey_step", target_type: "journey_action" },
|
|
1315
|
+
// (since v0.9.2, UPG-663) journey_action outbound edges. Fixes the
|
|
1316
|
+
// discovery dead-end (D2). The finest blueprint layer carries pain_score /
|
|
1317
|
+
// opportunity_score "to drive opportunity discovery" but previously had zero
|
|
1318
|
+
// outbound edges. Opportunity discovery routes through `need` (mirroring
|
|
1319
|
+
// `journey_step_reveals_need`), which already reaches `opportunity` via the
|
|
1320
|
+
// user chain; the action does not link an opportunity directly. The feature
|
|
1321
|
+
// edge mirrors `journey_step_realised_by_feature` one level deeper.
|
|
1322
|
+
journey_action_surfaces_need: { forward_verb: "surfaces", reverse_verb: "surfaced_in", classification: "cross-domain", source_type: "journey_action", target_type: "need" },
|
|
1323
|
+
journey_action_realised_by_feature: { forward_verb: "realised_by", reverse_verb: "realises", classification: "cross-domain", source_type: "journey_action", target_type: "feature" },
|
|
1308
1324
|
// 2.5 UI System Domain
|
|
1309
1325
|
product_systematised_in_design_system: { forward_verb: "systematised_in", reverse_verb: "systematises", classification: "hierarchy", source_type: "product", target_type: "design_system" },
|
|
1310
1326
|
product_built_with_design_component: { forward_verb: "built_with", reverse_verb: "built_for", classification: "hierarchy", source_type: "product", target_type: "design_component" },
|
|
@@ -2741,7 +2757,8 @@ var UPG_CROSS_EDGE_TYPES = [
|
|
|
2741
2757
|
"depends_on_product",
|
|
2742
2758
|
"cannibalises",
|
|
2743
2759
|
"succeeds",
|
|
2744
|
-
"hosts"
|
|
2760
|
+
"hosts",
|
|
2761
|
+
"contributes_to"
|
|
2745
2762
|
];
|
|
2746
2763
|
var TYPES = getTypes();
|
|
2747
2764
|
var TYPES_SET = new Set(TYPES);
|
|
@@ -3028,8 +3045,13 @@ var UPG_VALID_CHILDREN = {
|
|
|
3028
3045
|
// parallel to `observation: ['quote']`. Research-synthesis canon.
|
|
3029
3046
|
// Pairs with `insight_evidenced_by_quote` in the edge catalog.
|
|
3030
3047
|
// ── Design hierarchy ────────────────────────────────────────────────────────
|
|
3048
|
+
// (UPG-663, v0.9.2) A user_journey owns its steps (the stable 0.1.0 spine)
|
|
3049
|
+
// and carries phases as a non-owning band overlay. A journey_phase is NO
|
|
3050
|
+
// LONGER a containment parent of journey_step: it SPANS steps via the
|
|
3051
|
+
// `journey_phase_spans_journey_step` edge (mirroring the marketing
|
|
3052
|
+
// `customer_journey_stage_spans_journey_step` precedent), so each step has
|
|
3053
|
+
// exactly one containment parent and the journey has one canonical step list.
|
|
3031
3054
|
user_journey: ["journey_step", "journey_phase"],
|
|
3032
|
-
journey_phase: ["journey_step"],
|
|
3033
3055
|
journey_step: ["journey_action"],
|
|
3034
3056
|
// v0.5.8 (UPG-528 Part 2d): insights own their evidencing quotes;
|
|
3035
3057
|
// parallel to `observation: ['quote']`. Research-synthesis canon
|
|
@@ -7126,6 +7148,20 @@ var UPG_SPLIT_MIGRATIONS = {
|
|
|
7126
7148
|
]
|
|
7127
7149
|
};
|
|
7128
7150
|
var UPG_EDGE_MIGRATIONS = {
|
|
7151
|
+
"0.9.2": [
|
|
7152
|
+
// (since v0.9.2, UPG-663) Journey-model disambiguation. A journey_phase
|
|
7153
|
+
// is a temporal BAND over the journey's single step timeline, not a
|
|
7154
|
+
// container that owns steps. The owning hierarchy edge
|
|
7155
|
+
// `journey_phase_has_step` is renamed to the non-owning
|
|
7156
|
+
// `journey_phase_spans_journey_step` (mirroring the marketing precedent
|
|
7157
|
+
// `customer_journey_stage_spans_journey_step`). Steps stay owned by the
|
|
7158
|
+
// journey via `user_journey_contains_journey_step` (the stable 0.1.0
|
|
7159
|
+
// spine), so each step keeps a single containment parent and the journey
|
|
7160
|
+
// renders one canonical step list. Endpoints are unchanged
|
|
7161
|
+
// (journey_phase → journey_step); only the edge key and its grammar
|
|
7162
|
+
// (verb has_step → spans, classification hierarchy → cross-domain) change.
|
|
7163
|
+
{ kind: "rename", from: "journey_phase_has_step", to: "journey_phase_spans_journey_step", requires_source_type: "journey_phase", requires_target_type: "journey_step", reason: "Journey-model disambiguation (UPG-663): a phase spans steps, it does not own them. The phase to step edge becomes non-owning (verb has_step to spans), so the step keeps a single containment parent (the journey) and the journey has one canonical step list." }
|
|
7164
|
+
],
|
|
7129
7165
|
"0.9.0": [
|
|
7130
7166
|
// (since v0.9.0, UPG-660) theme → roadmap_theme. The four canonical edges that
|
|
7131
7167
|
// touch the roadmap theme are renamed to the roadmap_theme form. Endpoint guards
|
|
@@ -9544,6 +9580,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
9544
9580
|
},
|
|
9545
9581
|
// JourneyActionProperties: Discrete action at a journey step, classified by service layer.
|
|
9546
9582
|
journey_action: {
|
|
9583
|
+
action_order: { type: "number", description: "Display order of this action within its step (0-indexed). The scalar ordering convention shared with `journey_phase.phase_order` and `journey_step.step_order` (UPG-663). Orders the service-blueprint rows within a single moment." },
|
|
9547
9584
|
layer: { type: "string", enum: ["user", "frontstage", "backstage", "support"], description: "Service layer" },
|
|
9548
9585
|
action_description: { type: "string", description: "Plain-language description. Primary content of the action." },
|
|
9549
9586
|
channel: { type: "string", enum: ["in-app", "email", "web", "mobile", "phone", "in-person", "sms", "social", "other"], description: "Channel or surface. Keeps service-blueprint columns consistent across the journey." },
|
|
@@ -9575,7 +9612,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
9575
9612
|
system: { type: "string", description: "Performing system or service" },
|
|
9576
9613
|
notes: { type: "string", description: "Free-text notes, observations, or follow-up questions" }
|
|
9577
9614
|
},
|
|
9578
|
-
// JourneyPhaseProperties: Phase within a user journey.
|
|
9615
|
+
// JourneyPhaseProperties: Phase within a user journey. A temporal BAND over the journey's step
|
|
9579
9616
|
journey_phase: {
|
|
9580
9617
|
phase_order: { type: "number", description: "Display order within the journey (0-indexed)" },
|
|
9581
9618
|
label: { type: "string", description: 'Short human-readable name. @example "Discovery", "Onboarding", "Activation"' },
|
|
@@ -9586,8 +9623,9 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
9586
9623
|
key_questions: { type: "string[]", description: "Open questions the user asks themselves. Fuel for design and content priorities." },
|
|
9587
9624
|
timeframe: { type: "string", description: 'Typical time window. @example "first 30 seconds", "days 1\u20137", "onboarding week"' }
|
|
9588
9625
|
},
|
|
9589
|
-
// JourneyStepProperties: Single step within a user journey.
|
|
9626
|
+
// JourneyStepProperties: Single step within a user journey. A user-moment on the journey's single
|
|
9590
9627
|
journey_step: {
|
|
9628
|
+
step_order: { type: "number", description: "Display order within the journey's step timeline (0-indexed). The scalar ordering convention shared with `journey_phase.phase_order` and `journey_action.action_order` (UPG-663). For branching journeys, the explicit `journey_step_precedes_journey_step` edge captures the chain." },
|
|
9591
9629
|
touchpoint: { type: "string", description: "Interaction touchpoint" },
|
|
9592
9630
|
channel: { type: "string", description: 'Channel (e.g. "web", "email", "in-store")' },
|
|
9593
9631
|
emotion_score: {
|
|
@@ -17072,8 +17110,13 @@ var STANDARD_LABELS = {
|
|
|
17072
17110
|
interview_guide: { alt_labels: ["discussion guide", "interview script", "moderator guide"] },
|
|
17073
17111
|
survey_response: { alt_labels: ["survey answer", "questionnaire response"] },
|
|
17074
17112
|
// Design layer
|
|
17075
|
-
|
|
17076
|
-
|
|
17113
|
+
// (UPG-663) alt_labels must not collide with other entity types' canonical
|
|
17114
|
+
// names or alt_labels, or the string->type resolver is ambiguous.
|
|
17115
|
+
// Dropped 'customer journey' (collides with customer_journey_stage),
|
|
17116
|
+
// 'touchpoint' and 'journey phase' (both distinct entity surfaces: a
|
|
17117
|
+
// touchpoint is the journey_step.touchpoint property, a phase is journey_phase).
|
|
17118
|
+
user_journey: { alt_labels: ["journey map", "experience map", "journey"] },
|
|
17119
|
+
journey_step: { alt_labels: ["journey moment", "journey stage"] },
|
|
17077
17120
|
design_component: { alt_labels: ["component", "ui component", "design element"] },
|
|
17078
17121
|
design_token: { alt_labels: ["token", "style token", "css variable"] },
|
|
17079
17122
|
wireframe: { alt_labels: ["wireflow", "lo-fi mockup", "skeleton"] },
|
|
@@ -19085,7 +19128,11 @@ var PRODUCT_SPEC_GUIDE = {
|
|
|
19085
19128
|
var UX_DESIGN_GUIDE = {
|
|
19086
19129
|
domain_id: "ux_design",
|
|
19087
19130
|
anchor_entity: "user_journey",
|
|
19088
|
-
|
|
19131
|
+
// (UPG-663) creation order follows containment: a journey_action is a child
|
|
19132
|
+
// of a journey_step, so the step must precede the action. The journey is the
|
|
19133
|
+
// anchor; phases are the band overlay; steps are the timeline; actions
|
|
19134
|
+
// decompose a step into service-blueprint rows.
|
|
19135
|
+
creation_sequence: ["user_journey", "journey_phase", "journey_step", "journey_action", "screen", "screen_state", "user_flow", "wireframe", "prototype", "design_question", "design_concept"],
|
|
19089
19136
|
patterns: [
|
|
19090
19137
|
{
|
|
19091
19138
|
name: "Journey to Screen Flow",
|
|
@@ -22357,6 +22404,40 @@ var UPG_ANTI_PATTERNS = [
|
|
|
22357
22404
|
stages: ["concept", "validation", "build", "beta", "launch", "growth", "mature"],
|
|
22358
22405
|
severity: "medium",
|
|
22359
22406
|
source: { kind: "fundamental" }
|
|
22407
|
+
},
|
|
22408
|
+
// ── Experience-design layer ─────────────────────────────────────────────
|
|
22409
|
+
{
|
|
22410
|
+
id: "journey-phases-without-canonical-steps",
|
|
22411
|
+
name: "Journey phases without a step spine",
|
|
22412
|
+
description: 'The graph has journey phases spanning steps (`journey_phase_spans_journey_step`), but no journey owns its steps via `user_journey_contains_journey_step`. A phase is a band over a step timeline, not a container. When the timeline itself is missing there is no canonical answer to "what are the steps of this journey?". The phase overlay points at steps the journey does not own.',
|
|
22413
|
+
structured_condition: {
|
|
22414
|
+
operator: "and",
|
|
22415
|
+
checks: [
|
|
22416
|
+
{
|
|
22417
|
+
check: {
|
|
22418
|
+
type: "relationship",
|
|
22419
|
+
source_type: "journey_phase",
|
|
22420
|
+
edge_type: "journey_phase_spans_journey_step",
|
|
22421
|
+
target_type: "journey_step",
|
|
22422
|
+
comparison: "exists"
|
|
22423
|
+
}
|
|
22424
|
+
},
|
|
22425
|
+
{
|
|
22426
|
+
check: {
|
|
22427
|
+
type: "relationship",
|
|
22428
|
+
source_type: "user_journey",
|
|
22429
|
+
edge_type: "user_journey_contains_journey_step",
|
|
22430
|
+
target_type: "journey_step",
|
|
22431
|
+
comparison: "not_exists"
|
|
22432
|
+
}
|
|
22433
|
+
}
|
|
22434
|
+
]
|
|
22435
|
+
},
|
|
22436
|
+
why_it_matters: "Steps owned by no journey render a different step list per consumer: the phase overlay sees them, a journey-direct walk does not. The journey has no deterministic step spine to traverse, score, or map to screens.",
|
|
22437
|
+
remediation: "Own every step under its journey with `user_journey_contains_journey_step`, then let phases span ranges of that single timeline via `journey_phase_spans_journey_step`. The phase is a non-owning band overlay, not the step container.",
|
|
22438
|
+
stages: ["concept", "validation", "build", "beta", "launch", "growth"],
|
|
22439
|
+
severity: "high",
|
|
22440
|
+
source: { kind: "fundamental" }
|
|
22360
22441
|
}
|
|
22361
22442
|
];
|
|
22362
22443
|
var SEVERITY_ORDER = {
|
|
@@ -24646,7 +24727,7 @@ function serializePortfolioWithHeader(doc, opts) {
|
|
|
24646
24727
|
header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
|
|
24647
24728
|
return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
|
|
24648
24729
|
}
|
|
24649
|
-
var UPG_VERSION = "0.9.
|
|
24730
|
+
var UPG_VERSION = "0.9.2";
|
|
24650
24731
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
24651
24732
|
var UPG_TYPES = getTypes();
|
|
24652
24733
|
var UPG_TYPES_SET = new Set(UPG_TYPES);
|
|
@@ -26849,6 +26930,38 @@ import {
|
|
|
26849
26930
|
WorkspaceAlreadyExistsError,
|
|
26850
26931
|
WorkspaceNotInitialisedError
|
|
26851
26932
|
} from "@unified-product-graph/sdk";
|
|
26933
|
+
function isExistingFile(p) {
|
|
26934
|
+
try {
|
|
26935
|
+
return fs.statSync(p).isFile();
|
|
26936
|
+
} catch {
|
|
26937
|
+
return false;
|
|
26938
|
+
}
|
|
26939
|
+
}
|
|
26940
|
+
function findWorkspaceUpgFiles(cwd) {
|
|
26941
|
+
const candidates = [];
|
|
26942
|
+
let topEntries;
|
|
26943
|
+
try {
|
|
26944
|
+
topEntries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
26945
|
+
} catch {
|
|
26946
|
+
return candidates;
|
|
26947
|
+
}
|
|
26948
|
+
for (const entry of topEntries) {
|
|
26949
|
+
if (entry.isFile() && entry.name.endsWith(".upg")) {
|
|
26950
|
+
candidates.push(path3.join(cwd, entry.name));
|
|
26951
|
+
} else if (entry.isDirectory() && (entry.name === ".upg" || !entry.name.startsWith("."))) {
|
|
26952
|
+
try {
|
|
26953
|
+
const subEntries = fs.readdirSync(path3.join(cwd, entry.name), { withFileTypes: true });
|
|
26954
|
+
for (const sub of subEntries) {
|
|
26955
|
+
if (sub.isFile() && sub.name.endsWith(".upg")) {
|
|
26956
|
+
candidates.push(path3.join(cwd, entry.name, sub.name));
|
|
26957
|
+
}
|
|
26958
|
+
}
|
|
26959
|
+
} catch {
|
|
26960
|
+
}
|
|
26961
|
+
}
|
|
26962
|
+
}
|
|
26963
|
+
return candidates;
|
|
26964
|
+
}
|
|
26852
26965
|
var listLocalProducts = (_args, _ctx) => {
|
|
26853
26966
|
const cwd = process.cwd();
|
|
26854
26967
|
const products = [];
|
|
@@ -26871,26 +26984,7 @@ var listLocalProducts = (_args, _ctx) => {
|
|
|
26871
26984
|
}
|
|
26872
26985
|
} catch {
|
|
26873
26986
|
}
|
|
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
|
-
}
|
|
26987
|
+
const candidates = findWorkspaceUpgFiles(cwd);
|
|
26894
26988
|
for (const filePath of candidates) {
|
|
26895
26989
|
try {
|
|
26896
26990
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
@@ -26920,21 +27014,19 @@ var switchProduct = async (args, ctx) => {
|
|
|
26920
27014
|
if (typeof fileArg !== "string" || fileArg.length === 0) {
|
|
26921
27015
|
return textError("Missing required parameter: file (alias: product). Pass a .upg path or a bare product name.");
|
|
26922
27016
|
}
|
|
26923
|
-
|
|
26924
|
-
|
|
26925
|
-
|
|
26926
|
-
|
|
26927
|
-
|
|
26928
|
-
|
|
26929
|
-
|
|
26930
|
-
|
|
26931
|
-
|
|
26932
|
-
|
|
26933
|
-
|
|
26934
|
-
|
|
26935
|
-
|
|
26936
|
-
);
|
|
26937
|
-
}
|
|
27017
|
+
const cwd = process.cwd();
|
|
27018
|
+
const direct = path3.resolve(fileArg);
|
|
27019
|
+
const candidates = [
|
|
27020
|
+
path3.join(cwd, ".upg", fileArg),
|
|
27021
|
+
path3.join(cwd, ".upg", `${fileArg}.upg`),
|
|
27022
|
+
direct,
|
|
27023
|
+
`${direct}.upg`
|
|
27024
|
+
];
|
|
27025
|
+
const resolved = candidates.find(isExistingFile);
|
|
27026
|
+
if (!resolved) {
|
|
27027
|
+
return textError(
|
|
27028
|
+
`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.`
|
|
27029
|
+
);
|
|
26938
27030
|
}
|
|
26939
27031
|
try {
|
|
26940
27032
|
await store.flush();
|
|
@@ -27419,6 +27511,312 @@ var batchCreateCrossProductEdges = async (args, _ctx) => {
|
|
|
27419
27511
|
);
|
|
27420
27512
|
};
|
|
27421
27513
|
|
|
27514
|
+
// src/tools/portfolio-read.ts
|
|
27515
|
+
import * as path4 from "path";
|
|
27516
|
+
import * as fs2 from "fs";
|
|
27517
|
+
import { UPGFileStore, computeGraphDigest as computeGraphDigest2 } from "@unified-product-graph/sdk";
|
|
27518
|
+
|
|
27519
|
+
// src/lib/graph-traverse.ts
|
|
27520
|
+
function traverseGraph(reader, params) {
|
|
27521
|
+
const fromType = params.from;
|
|
27522
|
+
const fromId = params.from_id;
|
|
27523
|
+
if (!fromType && !fromId) {
|
|
27524
|
+
return { ok: false, error: 'Provide either "from" (entity type) or "from_id" (node ID)' };
|
|
27525
|
+
}
|
|
27526
|
+
const traverseEdgeTypes = params.traverse;
|
|
27527
|
+
const maxDepth = Math.min(Math.max(params.depth ?? 3, 1), 10);
|
|
27528
|
+
const maxNodes = Math.min(Math.max(params.limit ?? 200, 1), 1e3);
|
|
27529
|
+
const includeFields = new Set(params.include ?? ["title", "status", "type"]);
|
|
27530
|
+
includeFields.add("id");
|
|
27531
|
+
includeFields.add("type");
|
|
27532
|
+
let startNodes;
|
|
27533
|
+
if (fromId) {
|
|
27534
|
+
const node = reader.getNode(fromId);
|
|
27535
|
+
if (!node) return { ok: false, error: `Node not found: ${fromId}` };
|
|
27536
|
+
startNodes = [node];
|
|
27537
|
+
} else {
|
|
27538
|
+
startNodes = reader.getAllNodes().filter((n) => n.type === fromType);
|
|
27539
|
+
}
|
|
27540
|
+
if (startNodes.length === 0) {
|
|
27541
|
+
return { ok: true, result: { nodes: [], edges: [], total_nodes: 0, total_edges: 0, truncated: false } };
|
|
27542
|
+
}
|
|
27543
|
+
const visited = /* @__PURE__ */ new Set();
|
|
27544
|
+
const collectedNodes = [];
|
|
27545
|
+
const collectedEdges = /* @__PURE__ */ new Map();
|
|
27546
|
+
const queue = [];
|
|
27547
|
+
let truncated = false;
|
|
27548
|
+
let maxDepthReached = 0;
|
|
27549
|
+
for (const n of startNodes) {
|
|
27550
|
+
if (collectedNodes.length >= maxNodes) {
|
|
27551
|
+
truncated = true;
|
|
27552
|
+
break;
|
|
27553
|
+
}
|
|
27554
|
+
visited.add(n.id);
|
|
27555
|
+
collectedNodes.push(n);
|
|
27556
|
+
queue.push({ id: n.id, level: 0 });
|
|
27557
|
+
}
|
|
27558
|
+
while (queue.length > 0) {
|
|
27559
|
+
if (collectedNodes.length >= maxNodes) {
|
|
27560
|
+
truncated = true;
|
|
27561
|
+
break;
|
|
27562
|
+
}
|
|
27563
|
+
const { id, level } = queue.shift();
|
|
27564
|
+
if (level > maxDepthReached) maxDepthReached = level;
|
|
27565
|
+
if (level >= maxDepth) continue;
|
|
27566
|
+
const edges = reader.getEdgesForNode(id);
|
|
27567
|
+
for (const edge of edges) {
|
|
27568
|
+
if (edge.source !== id) continue;
|
|
27569
|
+
if (traverseEdgeTypes && traverseEdgeTypes.length > 0) {
|
|
27570
|
+
const edgeTypeForLevel = level < traverseEdgeTypes.length ? traverseEdgeTypes[level] : traverseEdgeTypes[traverseEdgeTypes.length - 1];
|
|
27571
|
+
if (edgeTypeForLevel.startsWith("!")) {
|
|
27572
|
+
if (edge.type === edgeTypeForLevel.slice(1)) continue;
|
|
27573
|
+
} else {
|
|
27574
|
+
if (edge.type !== edgeTypeForLevel) continue;
|
|
27575
|
+
}
|
|
27576
|
+
}
|
|
27577
|
+
collectedEdges.set(edge.id, edge);
|
|
27578
|
+
const neighborId = edge.target;
|
|
27579
|
+
if (!visited.has(neighborId)) {
|
|
27580
|
+
visited.add(neighborId);
|
|
27581
|
+
const neighbor = reader.getNode(neighborId);
|
|
27582
|
+
if (neighbor) {
|
|
27583
|
+
if (collectedNodes.length >= maxNodes) {
|
|
27584
|
+
truncated = true;
|
|
27585
|
+
break;
|
|
27586
|
+
}
|
|
27587
|
+
collectedNodes.push(neighbor);
|
|
27588
|
+
queue.push({ id: neighborId, level: level + 1 });
|
|
27589
|
+
}
|
|
27590
|
+
}
|
|
27591
|
+
}
|
|
27592
|
+
}
|
|
27593
|
+
const propInclude = params.property_include;
|
|
27594
|
+
const propFilter = propInclude && propInclude.length > 0 ? new Set(propInclude) : null;
|
|
27595
|
+
const projectedNodes = collectedNodes.map((n) => {
|
|
27596
|
+
const projected = { id: n.id, type: n.type };
|
|
27597
|
+
if (includeFields.has("title")) projected.title = n.title;
|
|
27598
|
+
if (includeFields.has("status")) projected.status = n.status;
|
|
27599
|
+
if (includeFields.has("tags")) projected.tags = n.tags;
|
|
27600
|
+
if (includeFields.has("description")) projected.description = n.description;
|
|
27601
|
+
if (includeFields.has("properties")) {
|
|
27602
|
+
if (propFilter && n.properties) {
|
|
27603
|
+
const filtered = {};
|
|
27604
|
+
for (const key of propFilter) {
|
|
27605
|
+
if (key in n.properties) filtered[key] = n.properties[key];
|
|
27606
|
+
}
|
|
27607
|
+
projected.properties = filtered;
|
|
27608
|
+
} else {
|
|
27609
|
+
projected.properties = n.properties;
|
|
27610
|
+
}
|
|
27611
|
+
}
|
|
27612
|
+
return projected;
|
|
27613
|
+
});
|
|
27614
|
+
const edgeInclude = params.edge_include;
|
|
27615
|
+
let edgeArray;
|
|
27616
|
+
if (edgeInclude !== void 0 && edgeInclude.length === 0) {
|
|
27617
|
+
edgeArray = [];
|
|
27618
|
+
} else {
|
|
27619
|
+
const edgeFields = edgeInclude ? new Set(edgeInclude) : null;
|
|
27620
|
+
edgeArray = [...collectedEdges.values()].map((e) => {
|
|
27621
|
+
if (!edgeFields) return { id: e.id, type: e.type, source: e.source, target: e.target };
|
|
27622
|
+
const projected = {};
|
|
27623
|
+
if (edgeFields.has("id")) projected.id = e.id;
|
|
27624
|
+
if (edgeFields.has("type")) projected.type = e.type;
|
|
27625
|
+
if (edgeFields.has("source")) projected.source = e.source;
|
|
27626
|
+
if (edgeFields.has("target")) projected.target = e.target;
|
|
27627
|
+
return projected;
|
|
27628
|
+
});
|
|
27629
|
+
}
|
|
27630
|
+
const result = {
|
|
27631
|
+
nodes: projectedNodes,
|
|
27632
|
+
edges: edgeArray,
|
|
27633
|
+
total_nodes: projectedNodes.length,
|
|
27634
|
+
total_edges: edgeArray.length,
|
|
27635
|
+
truncated
|
|
27636
|
+
};
|
|
27637
|
+
if (truncated) result.truncated_at_depth = maxDepthReached;
|
|
27638
|
+
return { ok: true, result };
|
|
27639
|
+
}
|
|
27640
|
+
|
|
27641
|
+
// src/tools/portfolio-read.ts
|
|
27642
|
+
function resolveScopedProducts(cwd, scope) {
|
|
27643
|
+
const all = [];
|
|
27644
|
+
for (const absPath of findWorkspaceUpgFiles(cwd)) {
|
|
27645
|
+
try {
|
|
27646
|
+
const doc = JSON.parse(fs2.readFileSync(absPath, "utf-8"));
|
|
27647
|
+
if (!doc.product) continue;
|
|
27648
|
+
all.push({
|
|
27649
|
+
id: doc.product.id ?? null,
|
|
27650
|
+
title: doc.product.title ?? "(untitled)",
|
|
27651
|
+
file: path4.relative(cwd, absPath),
|
|
27652
|
+
absPath
|
|
27653
|
+
});
|
|
27654
|
+
} catch {
|
|
27655
|
+
}
|
|
27656
|
+
}
|
|
27657
|
+
if (!scope || scope.length === 0) {
|
|
27658
|
+
return { products: all, unmatched: [] };
|
|
27659
|
+
}
|
|
27660
|
+
const matches = (p, want) => p.id === want || p.file === want || path4.basename(p.file) === want || path4.basename(p.file, ".upg") === want;
|
|
27661
|
+
const products = all.filter((p) => scope.some((want) => matches(p, want)));
|
|
27662
|
+
const unmatched = scope.filter((want) => !all.some((p) => matches(p, want)));
|
|
27663
|
+
return { products, unmatched };
|
|
27664
|
+
}
|
|
27665
|
+
async function readerFor(product, activeStore) {
|
|
27666
|
+
const activePath = activeStore.getFilePath();
|
|
27667
|
+
if (activePath && path4.resolve(activePath) === path4.resolve(product.absPath)) {
|
|
27668
|
+
return { reader: activeStore, store: activeStore, active: true };
|
|
27669
|
+
}
|
|
27670
|
+
const store = new UPGFileStore();
|
|
27671
|
+
await store.loadReadOnly(product.absPath);
|
|
27672
|
+
return { reader: store, store, active: false };
|
|
27673
|
+
}
|
|
27674
|
+
var portfolioQuery = async (args, ctx) => {
|
|
27675
|
+
const { store } = ctx;
|
|
27676
|
+
const from = args.from;
|
|
27677
|
+
const fromId = args.from_id;
|
|
27678
|
+
if (!from && !fromId) {
|
|
27679
|
+
return textError('Provide either "from" (entity type) or "from_id" (node ID)');
|
|
27680
|
+
}
|
|
27681
|
+
const scope = args.scope;
|
|
27682
|
+
const cwd = process.cwd();
|
|
27683
|
+
const { products, unmatched } = resolveScopedProducts(cwd, scope);
|
|
27684
|
+
if (products.length === 0) {
|
|
27685
|
+
return text(
|
|
27686
|
+
JSON.stringify(
|
|
27687
|
+
{
|
|
27688
|
+
products: [],
|
|
27689
|
+
products_searched: 0,
|
|
27690
|
+
products_with_matches: 0,
|
|
27691
|
+
empty_products: [],
|
|
27692
|
+
...unmatched.length > 0 ? { unmatched_scope: unmatched } : {},
|
|
27693
|
+
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."
|
|
27694
|
+
},
|
|
27695
|
+
null,
|
|
27696
|
+
2
|
|
27697
|
+
)
|
|
27698
|
+
);
|
|
27699
|
+
}
|
|
27700
|
+
const perProductLimit = Math.min(Math.max(args.limit ?? 100, 1), 1e3);
|
|
27701
|
+
const params = {
|
|
27702
|
+
from,
|
|
27703
|
+
from_id: fromId,
|
|
27704
|
+
traverse: args.traverse,
|
|
27705
|
+
depth: args.depth,
|
|
27706
|
+
limit: perProductLimit,
|
|
27707
|
+
include: args.include,
|
|
27708
|
+
edge_include: args.edge_include,
|
|
27709
|
+
property_include: args.property_include
|
|
27710
|
+
};
|
|
27711
|
+
const matched = [];
|
|
27712
|
+
const emptyProducts = [];
|
|
27713
|
+
const errored = [];
|
|
27714
|
+
let totalNodes = 0;
|
|
27715
|
+
let totalEdges = 0;
|
|
27716
|
+
for (const product of products) {
|
|
27717
|
+
let reader;
|
|
27718
|
+
try {
|
|
27719
|
+
;
|
|
27720
|
+
({ reader } = await readerFor(product, store));
|
|
27721
|
+
} catch (err) {
|
|
27722
|
+
errored.push({ product_id: product.id, file: product.file, error: err.message });
|
|
27723
|
+
continue;
|
|
27724
|
+
}
|
|
27725
|
+
const outcome = traverseGraph(reader, params);
|
|
27726
|
+
if (!outcome.ok) {
|
|
27727
|
+
emptyProducts.push(product.id ?? product.file);
|
|
27728
|
+
continue;
|
|
27729
|
+
}
|
|
27730
|
+
const r = outcome.result;
|
|
27731
|
+
if (r.total_nodes === 0) {
|
|
27732
|
+
emptyProducts.push(product.id ?? product.file);
|
|
27733
|
+
continue;
|
|
27734
|
+
}
|
|
27735
|
+
totalNodes += r.total_nodes;
|
|
27736
|
+
totalEdges += r.total_edges;
|
|
27737
|
+
matched.push({
|
|
27738
|
+
product_id: product.id,
|
|
27739
|
+
file: product.file,
|
|
27740
|
+
title: product.title,
|
|
27741
|
+
total_nodes: r.total_nodes,
|
|
27742
|
+
total_edges: r.total_edges,
|
|
27743
|
+
nodes: r.nodes,
|
|
27744
|
+
edges: r.edges,
|
|
27745
|
+
...r.truncated ? { truncated: true, truncated_at_depth: r.truncated_at_depth } : {}
|
|
27746
|
+
});
|
|
27747
|
+
}
|
|
27748
|
+
const guard = preflightPayload({
|
|
27749
|
+
toolName: "portfolio_query",
|
|
27750
|
+
nodeCount: totalNodes,
|
|
27751
|
+
edgeCount: totalEdges,
|
|
27752
|
+
compactEdges: true,
|
|
27753
|
+
argsHint: `from=${from ?? fromId}, products=${matched.length}, limit=${perProductLimit}`
|
|
27754
|
+
});
|
|
27755
|
+
if (guard.kind === "refuse") return guard.result;
|
|
27756
|
+
const response = {
|
|
27757
|
+
products: matched,
|
|
27758
|
+
products_searched: products.length,
|
|
27759
|
+
products_with_matches: matched.length,
|
|
27760
|
+
total_nodes: totalNodes,
|
|
27761
|
+
total_edges: totalEdges,
|
|
27762
|
+
empty_products: emptyProducts
|
|
27763
|
+
};
|
|
27764
|
+
if (errored.length > 0) response.errored_products = errored;
|
|
27765
|
+
if (unmatched.length > 0) response.unmatched_scope = unmatched;
|
|
27766
|
+
if (guard.kind === "warn") Object.assign(response, guard.fields);
|
|
27767
|
+
return text(JSON.stringify(response, null, 2));
|
|
27768
|
+
};
|
|
27769
|
+
var portfolioDigest = async (args, ctx) => {
|
|
27770
|
+
const { store } = ctx;
|
|
27771
|
+
const scope = args.scope;
|
|
27772
|
+
const cwd = process.cwd();
|
|
27773
|
+
const { products, unmatched } = resolveScopedProducts(cwd, scope);
|
|
27774
|
+
const summaries = [];
|
|
27775
|
+
const errored = [];
|
|
27776
|
+
const byStage = {};
|
|
27777
|
+
let totalNodes = 0;
|
|
27778
|
+
let totalEdges = 0;
|
|
27779
|
+
for (const product of products) {
|
|
27780
|
+
try {
|
|
27781
|
+
const { store: reader } = await readerFor(product, store);
|
|
27782
|
+
const digest = computeGraphDigest2(reader);
|
|
27783
|
+
const stage = digest.product.stage || "unset";
|
|
27784
|
+
byStage[stage] = (byStage[stage] ?? 0) + 1;
|
|
27785
|
+
totalNodes += digest.counts.total_nodes;
|
|
27786
|
+
totalEdges += digest.counts.total_edges;
|
|
27787
|
+
const topTypes = Object.entries(digest.counts.by_type).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([type, count]) => ({ type, count }));
|
|
27788
|
+
summaries.push({
|
|
27789
|
+
product_id: product.id,
|
|
27790
|
+
file: product.file,
|
|
27791
|
+
title: digest.product.title,
|
|
27792
|
+
stage: digest.product.stage || null,
|
|
27793
|
+
total_nodes: digest.counts.total_nodes,
|
|
27794
|
+
total_edges: digest.counts.total_edges,
|
|
27795
|
+
health: digest.health,
|
|
27796
|
+
coverage_pct: digest.coverage.stage_summary?.overall_pct ?? null,
|
|
27797
|
+
top_types: topTypes
|
|
27798
|
+
});
|
|
27799
|
+
} catch (err) {
|
|
27800
|
+
errored.push({ product_id: product.id, file: product.file, error: err.message });
|
|
27801
|
+
}
|
|
27802
|
+
}
|
|
27803
|
+
const response = {
|
|
27804
|
+
products: summaries,
|
|
27805
|
+
rollup: {
|
|
27806
|
+
products: summaries.length,
|
|
27807
|
+
total_nodes: totalNodes,
|
|
27808
|
+
total_edges: totalEdges,
|
|
27809
|
+
by_stage: byStage
|
|
27810
|
+
}
|
|
27811
|
+
};
|
|
27812
|
+
if (errored.length > 0) response.errored_products = errored;
|
|
27813
|
+
if (unmatched.length > 0) response.unmatched_scope = unmatched;
|
|
27814
|
+
if (products.length === 0) {
|
|
27815
|
+
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.";
|
|
27816
|
+
}
|
|
27817
|
+
return text(JSON.stringify(response, null, 2));
|
|
27818
|
+
};
|
|
27819
|
+
|
|
27422
27820
|
// src/tools/schema.ts
|
|
27423
27821
|
var getEntitySchema = (args, _ctx) => {
|
|
27424
27822
|
const rawType = args.type;
|
|
@@ -28120,24 +28518,24 @@ var prioritise = (args, ctx) => {
|
|
|
28120
28518
|
};
|
|
28121
28519
|
var trace = (args, ctx) => {
|
|
28122
28520
|
const anchor = args.anchor;
|
|
28123
|
-
const
|
|
28521
|
+
const path8 = args.path;
|
|
28124
28522
|
const edgesOverride = args.edges_override;
|
|
28125
28523
|
if (!anchor) {
|
|
28126
28524
|
return textError("Missing required parameter: anchor (entity_id)");
|
|
28127
28525
|
}
|
|
28128
|
-
if (!
|
|
28526
|
+
if (!path8 || !Array.isArray(path8) || path8.length === 0) {
|
|
28129
28527
|
return textError("Missing required parameter: path (UPGEntityType[])");
|
|
28130
28528
|
}
|
|
28131
|
-
if (edgesOverride && edgesOverride.length !==
|
|
28529
|
+
if (edgesOverride && edgesOverride.length !== path8.length) {
|
|
28132
28530
|
return textError(
|
|
28133
|
-
`edges_override length (${edgesOverride.length}) must match path length (${
|
|
28531
|
+
`edges_override length (${edgesOverride.length}) must match path length (${path8.length})`
|
|
28134
28532
|
);
|
|
28135
28533
|
}
|
|
28136
|
-
const result = executeTrace(ctx.store, anchor,
|
|
28534
|
+
const result = executeTrace(ctx.store, anchor, path8, edgesOverride);
|
|
28137
28535
|
const payload = {
|
|
28138
28536
|
params: {
|
|
28139
28537
|
anchor,
|
|
28140
|
-
path:
|
|
28538
|
+
path: path8,
|
|
28141
28539
|
edges_override: edgesOverride ?? null
|
|
28142
28540
|
},
|
|
28143
28541
|
trail: result.trail,
|
|
@@ -28733,7 +29131,7 @@ var migrateStatus = (args, ctx) => {
|
|
|
28733
29131
|
|
|
28734
29132
|
// src/tools/sync.ts
|
|
28735
29133
|
import * as fsp4 from "fs/promises";
|
|
28736
|
-
import * as
|
|
29134
|
+
import * as path5 from "path";
|
|
28737
29135
|
import { nodeId, edgeId as edgeId4 } from "@unified-product-graph/sdk";
|
|
28738
29136
|
var getSyncState = async (_args, ctx) => {
|
|
28739
29137
|
const { store, sync } = ctx;
|
|
@@ -28901,7 +29299,7 @@ var pushToCloud = async (args, ctx) => {
|
|
|
28901
29299
|
const productId = args.product_id;
|
|
28902
29300
|
if (!cloudEndpoint || !apiKey) {
|
|
28903
29301
|
try {
|
|
28904
|
-
const mcpConfigPath =
|
|
29302
|
+
const mcpConfigPath = path5.join(process.cwd(), ".mcp.json");
|
|
28905
29303
|
const mcpRaw = await fsp4.readFile(mcpConfigPath, "utf-8");
|
|
28906
29304
|
const mcpConfig = JSON.parse(mcpRaw);
|
|
28907
29305
|
const upgCloud = mcpConfig.mcpServers?.["upg-cloud"];
|
|
@@ -28986,8 +29384,8 @@ var pushToCloud = async (args, ctx) => {
|
|
|
28986
29384
|
};
|
|
28987
29385
|
|
|
28988
29386
|
// src/tools/skills.ts
|
|
28989
|
-
import { existsSync as existsSync2, lstatSync, readlinkSync, readFileSync as
|
|
28990
|
-
import { join as join5, resolve as
|
|
29387
|
+
import { existsSync as existsSync2, lstatSync, readlinkSync, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync } from "fs";
|
|
29388
|
+
import { join as join5, resolve as resolve3, dirname as dirname3 } from "path";
|
|
28991
29389
|
import { fileURLToPath } from "url";
|
|
28992
29390
|
function repoRoot() {
|
|
28993
29391
|
return process.cwd();
|
|
@@ -29016,7 +29414,7 @@ function resolveBundledSkillsDir() {
|
|
|
29016
29414
|
} catch {
|
|
29017
29415
|
md = process.cwd();
|
|
29018
29416
|
}
|
|
29019
|
-
for (const c of [
|
|
29417
|
+
for (const c of [resolve3(md, "..", "skills"), resolve3(md, "..", "..", "skills"), resolve3(md, "skills")]) {
|
|
29020
29418
|
if (isSkillsDir(c)) return c;
|
|
29021
29419
|
}
|
|
29022
29420
|
let dir = md;
|
|
@@ -29030,12 +29428,12 @@ function resolveBundledSkillsDir() {
|
|
|
29030
29428
|
return null;
|
|
29031
29429
|
}
|
|
29032
29430
|
function sourceSkillsDir() {
|
|
29033
|
-
const cwdPath =
|
|
29431
|
+
const cwdPath = resolve3(repoRoot(), "packages/upg-mcp-server/skills");
|
|
29034
29432
|
if (existsSync2(cwdPath)) return cwdPath;
|
|
29035
29433
|
return resolveBundledSkillsDir() ?? cwdPath;
|
|
29036
29434
|
}
|
|
29037
29435
|
function deployedSkillsDir() {
|
|
29038
|
-
return
|
|
29436
|
+
return resolve3(repoRoot(), ".claude/skills");
|
|
29039
29437
|
}
|
|
29040
29438
|
function parseFrontmatter(body) {
|
|
29041
29439
|
if (!body.startsWith("---\n")) return null;
|
|
@@ -29079,11 +29477,11 @@ function auditOne(name) {
|
|
|
29079
29477
|
let deployedFrontmatter = null;
|
|
29080
29478
|
let deployedFirstHeading = null;
|
|
29081
29479
|
if (deployedExists) {
|
|
29082
|
-
const deployedBody =
|
|
29480
|
+
const deployedBody = readFileSync3(deployedPath, "utf8");
|
|
29083
29481
|
deployedFrontmatter = parseFrontmatter(deployedBody);
|
|
29084
29482
|
deployedFirstHeading = firstHeading(deployedBody);
|
|
29085
29483
|
if (sourceExists) {
|
|
29086
|
-
const sourceBody =
|
|
29484
|
+
const sourceBody = readFileSync3(sourcePath, "utf8");
|
|
29087
29485
|
inSync = deployedBody === sourceBody;
|
|
29088
29486
|
if (!inSync) {
|
|
29089
29487
|
issues.push("Deployed SKILL.md differs from canonical source; symlink is stale or broken");
|
|
@@ -30044,7 +30442,7 @@ var TOOL_DEFINITIONS = [
|
|
|
30044
30442
|
},
|
|
30045
30443
|
{
|
|
30046
30444
|
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`.",
|
|
30445
|
+
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
30446
|
inputSchema: { type: "object", properties: {} }
|
|
30049
30447
|
},
|
|
30050
30448
|
{
|
|
@@ -30525,7 +30923,7 @@ var TOOL_DEFINITIONS = [
|
|
|
30525
30923
|
},
|
|
30526
30924
|
{
|
|
30527
30925
|
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).",
|
|
30926
|
+
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
30927
|
inputSchema: {
|
|
30530
30928
|
type: "object",
|
|
30531
30929
|
properties: {
|
|
@@ -30533,7 +30931,7 @@ var TOOL_DEFINITIONS = [
|
|
|
30533
30931
|
target_id: { type: "string", description: "Target node ID" },
|
|
30534
30932
|
type: {
|
|
30535
30933
|
type: "string",
|
|
30536
|
-
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
|
|
30934
|
+
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts", "contributes_to"],
|
|
30537
30935
|
description: "Cross-product relationship type"
|
|
30538
30936
|
},
|
|
30539
30937
|
source_product_id: { type: "string", description: "Product ID of the source node" },
|
|
@@ -30569,7 +30967,7 @@ var TOOL_DEFINITIONS = [
|
|
|
30569
30967
|
target_id: { type: "string", description: "Target node ID (bare or qualified {product_id}/{node_id})" },
|
|
30570
30968
|
type: {
|
|
30571
30969
|
type: "string",
|
|
30572
|
-
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
|
|
30970
|
+
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts", "contributes_to"],
|
|
30573
30971
|
description: "Cross-product relationship type"
|
|
30574
30972
|
},
|
|
30575
30973
|
source_product_id: { type: "string", description: "Product ID of the source node (qualifies a bare source_id)" },
|
|
@@ -30588,6 +30986,58 @@ var TOOL_DEFINITIONS = [
|
|
|
30588
30986
|
description: "List all cross-product edges stored in the portfolio document (`.upg/portfolio.upg`). Empty list when the portfolio document is absent.",
|
|
30589
30987
|
inputSchema: { type: "object", properties: {} }
|
|
30590
30988
|
},
|
|
30989
|
+
{
|
|
30990
|
+
name: "portfolio_query",
|
|
30991
|
+
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.',
|
|
30992
|
+
inputSchema: {
|
|
30993
|
+
type: "object",
|
|
30994
|
+
properties: {
|
|
30995
|
+
from: { type: "string", description: "Start from all nodes of this type (in each product)" },
|
|
30996
|
+
from_id: { type: "string", description: "Start from a specific node ID. Node IDs are product-local; only the owning product returns results." },
|
|
30997
|
+
traverse: {
|
|
30998
|
+
type: "array",
|
|
30999
|
+
items: { type: "string" },
|
|
31000
|
+
description: "Edge types to follow at each level (in order). If omitted, follows all edges. Prefix with ! to exclude."
|
|
31001
|
+
},
|
|
31002
|
+
depth: { type: "number", description: "Max traversal depth (default 3, max 10)" },
|
|
31003
|
+
include: {
|
|
31004
|
+
type: "array",
|
|
31005
|
+
items: { type: "string" },
|
|
31006
|
+
description: 'Fields per node: "title", "status", "tags", "description", "properties" (default: title, status, type)'
|
|
31007
|
+
},
|
|
31008
|
+
limit: { type: "number", description: "Max nodes per product (default 100, max 1000)" },
|
|
31009
|
+
edge_include: {
|
|
31010
|
+
type: "array",
|
|
31011
|
+
items: { type: "string" },
|
|
31012
|
+
description: 'Edge fields to return: "id", "type", "source", "target". Empty array = no edges. Default: all fields.'
|
|
31013
|
+
},
|
|
31014
|
+
property_include: {
|
|
31015
|
+
type: "array",
|
|
31016
|
+
items: { type: "string" },
|
|
31017
|
+
description: 'When "properties" is in include, only return these property keys.'
|
|
31018
|
+
},
|
|
31019
|
+
scope: {
|
|
31020
|
+
type: "array",
|
|
31021
|
+
items: { type: "string" },
|
|
31022
|
+
description: "Product IDs (or files) to query. Omit to query ALL products in the workspace. Match by product id, relative file, or basename."
|
|
31023
|
+
}
|
|
31024
|
+
}
|
|
31025
|
+
}
|
|
31026
|
+
},
|
|
31027
|
+
{
|
|
31028
|
+
name: "portfolio_digest",
|
|
31029
|
+
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.",
|
|
31030
|
+
inputSchema: {
|
|
31031
|
+
type: "object",
|
|
31032
|
+
properties: {
|
|
31033
|
+
scope: {
|
|
31034
|
+
type: "array",
|
|
31035
|
+
items: { type: "string" },
|
|
31036
|
+
description: "Product IDs (or files) to summarise. Omit to summarise ALL products in the workspace."
|
|
31037
|
+
}
|
|
31038
|
+
}
|
|
31039
|
+
}
|
|
31040
|
+
},
|
|
30591
31041
|
{
|
|
30592
31042
|
name: "migrate_cross_edges",
|
|
30593
31043
|
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 +31224,8 @@ var HANDLERS = {
|
|
|
30774
31224
|
attach_product_to_portfolio: attachProductToPortfolioTool,
|
|
30775
31225
|
detach_product_from_portfolio: detachProductFromPortfolioTool,
|
|
30776
31226
|
list_portfolio_cross_edges: listPortfolioCrossEdges,
|
|
31227
|
+
portfolio_query: portfolioQuery,
|
|
31228
|
+
portfolio_digest: portfolioDigest,
|
|
30777
31229
|
migrate_cross_edges: migrateCrossEdges,
|
|
30778
31230
|
get_sync_state: getSyncState,
|
|
30779
31231
|
apply_pull_changeset: applyPullChangeset,
|
|
@@ -30832,9 +31284,9 @@ var SERVER_INSTRUCTIONS = [
|
|
|
30832
31284
|
].join("\n");
|
|
30833
31285
|
function resolvePackageVersion() {
|
|
30834
31286
|
try {
|
|
30835
|
-
const here =
|
|
30836
|
-
const pkgPath =
|
|
30837
|
-
const raw =
|
|
31287
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
31288
|
+
const pkgPath = path6.resolve(here, "..", "package.json");
|
|
31289
|
+
const raw = fs3.readFileSync(pkgPath, "utf-8");
|
|
30838
31290
|
const pkg = JSON.parse(raw);
|
|
30839
31291
|
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
30840
31292
|
} catch {
|
|
@@ -30876,7 +31328,7 @@ function createServer(store) {
|
|
|
30876
31328
|
const result = handler ? await handler(args, ctx) : textError(`Unknown tool: ${name}`);
|
|
30877
31329
|
if (logFile) {
|
|
30878
31330
|
const entry = JSON.stringify({ ts: t0, tool: name, params: args, result, durationMs: Date.now() - t0 });
|
|
30879
|
-
|
|
31331
|
+
fs3.appendFileSync(logFile, entry + "\n");
|
|
30880
31332
|
}
|
|
30881
31333
|
return result;
|
|
30882
31334
|
});
|
|
@@ -30893,15 +31345,15 @@ import { nanoid } from "nanoid";
|
|
|
30893
31345
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
30894
31346
|
import { realpathSync as realpathSync2 } from "fs";
|
|
30895
31347
|
async function discoverUPGFile(explicitFile) {
|
|
30896
|
-
if (explicitFile) return
|
|
31348
|
+
if (explicitFile) return path7.resolve(explicitFile);
|
|
30897
31349
|
const cwd = process.cwd();
|
|
30898
|
-
const workspacePath =
|
|
31350
|
+
const workspacePath = path7.join(cwd, ".upg", "workspace.json");
|
|
30899
31351
|
try {
|
|
30900
|
-
const raw = await
|
|
31352
|
+
const raw = await fs4.readFile(workspacePath, "utf-8");
|
|
30901
31353
|
const workspace = JSON.parse(raw);
|
|
30902
31354
|
if (workspace.default_product) {
|
|
30903
|
-
const filePath =
|
|
30904
|
-
await
|
|
31355
|
+
const filePath = path7.join(cwd, ".upg", workspace.default_product);
|
|
31356
|
+
await fs4.access(filePath);
|
|
30905
31357
|
const title = workspace.products?.find(
|
|
30906
31358
|
(p) => p.file === workspace.default_product
|
|
30907
31359
|
)?.title ?? workspace.default_product;
|
|
@@ -30912,16 +31364,16 @@ async function discoverUPGFile(explicitFile) {
|
|
|
30912
31364
|
return filePath;
|
|
30913
31365
|
}
|
|
30914
31366
|
} catch {
|
|
30915
|
-
const upgDir =
|
|
31367
|
+
const upgDir = path7.join(cwd, ".upg");
|
|
30916
31368
|
try {
|
|
30917
|
-
const dirEntries = await
|
|
31369
|
+
const dirEntries = await fs4.readdir(upgDir);
|
|
30918
31370
|
const upgFiles = dirEntries.filter((f) => f.endsWith(".upg")).sort();
|
|
30919
31371
|
if (upgFiles.length > 0) {
|
|
30920
31372
|
const products = [];
|
|
30921
31373
|
for (const file of upgFiles) {
|
|
30922
|
-
let title =
|
|
31374
|
+
let title = path7.basename(file, ".upg");
|
|
30923
31375
|
try {
|
|
30924
|
-
const raw = await
|
|
31376
|
+
const raw = await fs4.readFile(path7.join(upgDir, file), "utf-8");
|
|
30925
31377
|
const doc = JSON.parse(raw);
|
|
30926
31378
|
if (doc.product?.title) title = doc.product.title;
|
|
30927
31379
|
} catch {
|
|
@@ -30933,12 +31385,12 @@ async function discoverUPGFile(explicitFile) {
|
|
|
30933
31385
|
default_product: upgFiles[0],
|
|
30934
31386
|
products
|
|
30935
31387
|
};
|
|
30936
|
-
await
|
|
31388
|
+
await fs4.writeFile(workspacePath, JSON.stringify(workspace, null, 2) + "\n", "utf-8");
|
|
30937
31389
|
process.stderr.write(
|
|
30938
31390
|
`UPG workspace: auto-created workspace.json (${upgFiles.length} product${upgFiles.length > 1 ? "s" : ""})
|
|
30939
31391
|
`
|
|
30940
31392
|
);
|
|
30941
|
-
const filePath =
|
|
31393
|
+
const filePath = path7.join(upgDir, upgFiles[0]);
|
|
30942
31394
|
process.stderr.write(`UPG workspace: loading "${products[0].title}"
|
|
30943
31395
|
`);
|
|
30944
31396
|
return filePath;
|
|
@@ -30947,17 +31399,17 @@ async function discoverUPGFile(explicitFile) {
|
|
|
30947
31399
|
}
|
|
30948
31400
|
}
|
|
30949
31401
|
try {
|
|
30950
|
-
const entries = await
|
|
31402
|
+
const entries = await fs4.readdir(cwd);
|
|
30951
31403
|
const upgFiles = entries.filter((f) => f.endsWith(".upg")).sort();
|
|
30952
31404
|
if (upgFiles.length === 1) {
|
|
30953
|
-
return
|
|
31405
|
+
return path7.resolve(upgFiles[0]);
|
|
30954
31406
|
}
|
|
30955
31407
|
if (upgFiles.length > 1) {
|
|
30956
31408
|
process.stderr.write(
|
|
30957
31409
|
`Found ${upgFiles.length} .upg files: loading ${upgFiles[0]}. Use --file to pick a specific one.
|
|
30958
31410
|
`
|
|
30959
31411
|
);
|
|
30960
|
-
return
|
|
31412
|
+
return path7.resolve(upgFiles[0]);
|
|
30961
31413
|
}
|
|
30962
31414
|
} catch {
|
|
30963
31415
|
}
|
|
@@ -30977,7 +31429,7 @@ async function runMcpServer() {
|
|
|
30977
31429
|
});
|
|
30978
31430
|
let resolvedPath = await discoverUPGFile(values.file);
|
|
30979
31431
|
if (!resolvedPath) {
|
|
30980
|
-
const defaultFile =
|
|
31432
|
+
const defaultFile = path7.resolve("product.upg");
|
|
30981
31433
|
const title = values.title ?? "My Product";
|
|
30982
31434
|
const blank = {
|
|
30983
31435
|
upg_version: UPG_VERSION,
|
|
@@ -30993,16 +31445,16 @@ async function runMcpServer() {
|
|
|
30993
31445
|
nodes: [],
|
|
30994
31446
|
edges: []
|
|
30995
31447
|
};
|
|
30996
|
-
await
|
|
30997
|
-
await
|
|
31448
|
+
await fs4.mkdir(path7.dirname(defaultFile), { recursive: true });
|
|
31449
|
+
await fs4.writeFile(defaultFile, serializeCanonical(blank), "utf-8");
|
|
30998
31450
|
process.stderr.write(`Created new UPG file: ${defaultFile}
|
|
30999
31451
|
`);
|
|
31000
31452
|
resolvedPath = defaultFile;
|
|
31001
31453
|
} else {
|
|
31002
31454
|
try {
|
|
31003
|
-
await
|
|
31455
|
+
await fs4.access(resolvedPath);
|
|
31004
31456
|
} catch {
|
|
31005
|
-
const title = values.title ??
|
|
31457
|
+
const title = values.title ?? path7.basename(resolvedPath, ".upg");
|
|
31006
31458
|
const blank = {
|
|
31007
31459
|
upg_version: UPG_VERSION,
|
|
31008
31460
|
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -31017,13 +31469,13 @@ async function runMcpServer() {
|
|
|
31017
31469
|
nodes: [],
|
|
31018
31470
|
edges: []
|
|
31019
31471
|
};
|
|
31020
|
-
await
|
|
31021
|
-
await
|
|
31472
|
+
await fs4.mkdir(path7.dirname(resolvedPath), { recursive: true });
|
|
31473
|
+
await fs4.writeFile(resolvedPath, serializeCanonical(blank), "utf-8");
|
|
31022
31474
|
process.stderr.write(`Created new UPG file: ${resolvedPath}
|
|
31023
31475
|
`);
|
|
31024
31476
|
}
|
|
31025
31477
|
}
|
|
31026
|
-
const store = new
|
|
31478
|
+
const store = new UPGFileStore2();
|
|
31027
31479
|
store.setWriter("upg-mcp-local", SERVER_VERSION);
|
|
31028
31480
|
await store.load(resolvedPath);
|
|
31029
31481
|
const nodes = store.getAllNodes();
|