@unified-product-graph/mcp-server 0.8.2 → 0.8.5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to `@unified-product-graph/mcp-server` are documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
4
 
5
+ ## [0.8.5] - 2026-06-02
6
+
7
+ Field-report fast-follow (tester report on 0.8.4).
8
+
9
+ ### Fixed
10
+ - `skill_audit` no longer false-reports skills as out of sync on npm/npx installs. It resolved the canonical source from `process.cwd()/packages/upg-mcp-server/skills` — a monorepo-only path absent in a user's project — so every deployed skill came back unverifiable. It now resolves the skills bundled in the installed package (relative to the module), and treats a symlink to a byte-identical bundle, or a matching copy, as healthy: content match, not deployment method, is the signal.
11
+
12
+ ### Changed
13
+ - The `prioritise` `type_mismatch` hint now points to the framework_exercise escape hatch (`apply_framework` / `upg apply`, then prioritise with `exercise_id`), so scoring a non-target entity type is discoverable.
14
+
15
+ ## [0.8.4] - 2026-06-02
16
+
17
+ Framework exercises, with the 0.8.3 launch fix folded in.
18
+
19
+ ### Added
20
+ - `apply_framework` and `score_entity` tools: create a `framework_exercise` and record per-entity results on the includes edge (94 to 96 tools).
21
+ - `create_edge` accepts gated `properties` (only edge types that opt in). `prioritise` accepts an optional `exercise_id` that sources scoring inputs from the includes edges and scores across any entity type.
22
+
23
+ ### Fixed
24
+ - The server no longer crashes with `ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL` when launched via `upg mcp run` — `parseArgs` now tolerates stray positionals. The standalone `upg-mcp-server` bin is unaffected.
25
+
5
26
  ## [0.8.2] - 2026-06-02
6
27
 
7
28
  Co-version with the @unified-product-graph/* 0.8.2 release train.
package/TOOLS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # UPG MCP Server: Tool Reference
2
2
 
3
- Reference for the 94 tools exposed by `@unified-product-graph/mcp-server`. Generated from JSDoc on `src/tools/*.ts` (do not edit by hand).
3
+ Reference for the 96 tools exposed by `@unified-product-graph/mcp-server`. Generated from JSDoc on `src/tools/*.ts` (do not edit by hand).
4
4
 
5
5
  ## Contents
6
6
 
@@ -10,7 +10,7 @@ Reference for the 94 tools exposed by `@unified-product-graph/mcp-server`. Gener
10
10
  - [Areas & Change Log](#areas-change-log): 5 tools
11
11
  - [Workspace & Portfolios](#workspace-portfolios): 10 tools
12
12
  - [Schema](#schema): 1 tool
13
- - [Spec Introspection](#spec-introspection): 43 tools
13
+ - [Spec Introspection](#spec-introspection): 45 tools
14
14
  - [Cloud Sync](#cloud-sync): 3 tools
15
15
  - [Validation](#validation): 3 tools
16
16
 
@@ -767,6 +767,7 @@ Create one edge between two nodes. Edge type auto-infers when omitted. Target ac
767
767
 
768
768
  | Name | Type | Required | Description |
769
769
  | ---- | ---- | -------- | ----------- |
770
+ | `properties` | object | | Edge-scoped properties. Only permitted on edge types that opt in (currently framework_exercise_includes_node); rejected on plain semantic edges. |
770
771
  | `source_id` | string | ✓ | Source node ID |
771
772
  | `target_id` | string | | Target node ID |
772
773
  | `target_title` | string | | Target node title (alternative to target_id; requires target_type). |
@@ -1349,6 +1350,7 @@ edges_in, phases?, initial_phase?, terminal_phases?, domain_guide? }`.
1349
1350
 
1350
1351
  _Canonical playbooks, approaches, domain guides, frameworks, edge catalogue, regions, lenses, type labels, hierarchy, version, cross-edges, entity meta, anti-patterns, benchmarks, bare-verb approach handlers, migrations, lifecycles, scales, framework categories/patterns, and domain rings. All from `@unified-product-graph/core`._
1351
1352
 
1353
+ - [`apply_framework`](#apply-framework)
1352
1354
  - [`get_anti_pattern`](#get-anti-pattern)
1353
1355
  - [`get_approach`](#get-approach)
1354
1356
  - [`get_domain_guide`](#get-domain-guide)
@@ -1391,8 +1393,35 @@ _Canonical playbooks, approaches, domain guides, frameworks, edge catalogue, reg
1391
1393
  - [`prioritise`](#prioritise)
1392
1394
  - [`reflect`](#reflect)
1393
1395
  - [`resolve_edge_for_pair`](#resolve-edge-for-pair)
1396
+ - [`score_entity`](#score-entity)
1394
1397
  - [`trace`](#trace)
1395
1398
 
1399
+ ### `apply_framework`
1400
+
1401
+ Apply a framework (MoSCoW, RICE, Kano, ...) to a set of entities: creates a framework_exercise node and an `includes` edge to each entity. The per-entity result is recorded on the edge via score_entity, never on the entity node, so the same entity can sit in many exercises and any entity type can be scored. Returns { exercise_id, exercise, included, warnings }.
1402
+
1403
+ **Atomicity:** `atomic.`
1404
+
1405
+ **Arguments:**
1406
+
1407
+ | Name | Type | Required | Description |
1408
+ | ---- | ---- | -------- | ----------- |
1409
+ | `entity_ids` | array | | Entities to pull into the exercise (any type). |
1410
+ | `framework_id` | string | ✓ | Required. UPGFramework.id (e.g. "moscow", "rice-scoring"). |
1411
+ | `status` | string | | Lifecycle phase: draft \| active \| archived (default draft). |
1412
+ | `title` | string | | Human label for the exercise (default "<Framework> exercise"). |
1413
+
1414
+ **Returns:**
1415
+
1416
+ JSON: `{ exercise_id, exercise, included: [{ edge_id, entity_id }], warnings }`.
1417
+
1418
+ **Throws:**
1419
+
1420
+ - textError on a missing/unknown framework_id.
1421
+
1422
+ **See also:** `score_entity`
1423
+
1424
+
1396
1425
  ### `get_anti_pattern`
1397
1426
 
1398
1427
  Return one curated anti-pattern by id (kebab-case slug, e.g. "features-without-hypotheses", "personas-without-jobs"). Includes structured condition, why-it-matters, remediation, applicable stages, severity, optional source citation. IDs are stable URL fragments.
@@ -2235,7 +2264,8 @@ execution_mode: "execution_v0_4_0" }`.
2235
2264
 
2236
2265
  | Name | Type | Required | Description |
2237
2266
  | ---- | ---- | -------- | ----------- |
2238
- | `candidates` | array || Required. entity_id[] to rank. |
2267
+ | `candidates` | array | | entity_id[] to rank. Optional when exercise_id is given (the exercise supplies them). |
2268
+ | `exercise_id` | string | | Optional (0.8.4). A framework_exercise id: reads each candidate's scoring inputs from its includes-edge properties instead of node.properties, and bypasses the target-type guard so any entity type can be scored. |
2239
2269
  | `framework_id` | string | ✓ | Required. UPGFramework.id of the scoring lens (e.g. "rice-scoring", "ice-scoring", "kano-model", "cost-of-delay", "wsjf"). |
2240
2270
 
2241
2271
  **Returns:**
@@ -2310,6 +2340,33 @@ not synthesise a non-canonical key.
2310
2340
  **See also:** `list_edge_types`, `get_edge_type`, `create_edge`, `trace`
2311
2341
 
2312
2342
 
2343
+ ### `score_entity`
2344
+
2345
+ Record a framework's result for one entity on the exercise's includes edge (a MoSCoW bucket, a RICE score, a canvas slot). Auto-includes the entity if not already in scope. Merges into existing edge properties unless replace is set. Returns { edge, warnings }.
2346
+
2347
+ **Atomicity:** `atomic.`
2348
+
2349
+ **Arguments:**
2350
+
2351
+ | Name | Type | Required | Description |
2352
+ | ---- | ---- | -------- | ----------- |
2353
+ | `entity_id` | string | ✓ | Required. The entity being scored. |
2354
+ | `exercise_id` | string | ✓ | Required. The framework_exercise id. |
2355
+ | `replace` | boolean | | Replace the edge properties instead of merging (default false). |
2356
+ | `values` | object | ✓ | Required. The result as { input: value }, e.g. { "moscow": "must" } or { "reach": 800, "impact": 3 }. |
2357
+
2358
+ **Returns:**
2359
+
2360
+ JSON: `{ edge, warnings }`.
2361
+
2362
+ **Throws:**
2363
+
2364
+ - textError when the exercise/entity is missing or the node is not a
2365
+ framework_exercise.
2366
+
2367
+ **See also:** `apply_framework`
2368
+
2369
+
2313
2370
  ### `trace`
2314
2371
 
2315
2372
  [LLM-mediated] This tool returns a routing envelope, not computed results. For user-facing tracing, invoke the /upg-trace skill instead of calling this tool directly. Trace approach: path of arrival to "walk a meaningful path through existing graph". Returns the Trace record + invocation params in the family-resemblance envelope. The LLM uses `anchor` + `path` to compose `query()` calls and emits `{ trail: [{ depth, entity_id, edge_type_in }], reached: entity_id[] }`. `path` is type-shorthand: `["persona","job","feature"]` walks persona→job→feature using the canonical edge per pair (via `resolve_edge_for_pair`). Optional `edges_override` selects non-canonical edges per hop; `null` per element means "use canonical".
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { UPGFileStore } from "@unified-product-graph/sdk";
10
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
12
  import fs2 from "fs";
13
- import { fileURLToPath } from "url";
13
+ import { fileURLToPath as fileURLToPath2 } from "url";
14
14
  import * as path5 from "path";
15
15
  import {
16
16
  CallToolRequestSchema,
@@ -511,8 +511,8 @@ var UPG_DOMAINS = [
511
511
  {
512
512
  id: "workspace",
513
513
  label: "Workspace",
514
- description: "Spatial thinking spaces for arranging entities, debating decisions, and committing to the graph. Workspaces are transient canvases that sit alongside all other domains, letting you compose and explore relationships before they become permanent graph structure.",
515
- types: ["workspace"]
514
+ description: "Spatial thinking spaces for arranging entities, debating decisions, and committing to the graph. Workspaces are transient canvases that sit alongside all other domains, letting you compose and explore relationships before they become permanent graph structure. A framework exercise is a structured workspace: one run of a framework (MoSCoW, RICE, Kano, \u2026) applied to a chosen set of entities, with each entity's result recorded on the exercise-to-entity edge rather than the entity itself.",
515
+ types: ["workspace", "framework_exercise"]
516
516
  }
517
517
  ];
518
518
  var UPG_ENTITY_TO_DOMAIN = Object.freeze(
@@ -933,7 +933,8 @@ var UPG_ENTITY_META = [
933
933
  { name: "integration_partner", type_id: "ent_311", maturity: "stable", since: "0.1.0" },
934
934
  { name: "partner_revenue_share", type_id: "ent_312", maturity: "stable", since: "0.1.0" },
935
935
  // ── Workspace ──
936
- { name: "workspace", type_id: "ent_328", maturity: "proposed", since: "0.2.0" }
936
+ { name: "workspace", type_id: "ent_328", maturity: "proposed", since: "0.2.0" },
937
+ { name: "framework_exercise", type_id: "ent_350", maturity: "proposed", since: "0.8.4" }
937
938
  ];
938
939
  var UPG_ENTITY_META_BY_NAME = new Map(
939
940
  UPG_ENTITY_META.map((m) => [m.name, m])
@@ -2073,6 +2074,18 @@ var UPG_EDGE_CATALOG = {
2073
2074
  // universal across the DDD type family. Verbs follow DDD literature:
2074
2075
  // aggregate belongs_to context, context contains aggregates.
2075
2076
  node_belongs_to_bounded_context: { forward_verb: "belongs_to", reverse_verb: "contains", classification: "cross-domain", source_type: "node", target_type: "bounded_context" },
2077
+ // ── Cross-domain: Framework exercises ────────────────────────────────────────
2078
+ // A framework_exercise is one run of a framework (MoSCoW, RICE, Kano, …) over a
2079
+ // set of entities. It `includes` each entity it touches; the framework's
2080
+ // per-entity result — a MoSCoW bucket, a RICE score, a canvas slot, a funnel
2081
+ // stage — rides on this edge's `properties`, NOT on the entity node. A value
2082
+ // that exists only within a specific exercise is a fact about the relationship,
2083
+ // not the entity, so it lives on the edge (same principle as owner-as-edge).
2084
+ // Polymorphic (`target_type: 'node'`): an exercise can include any entity type,
2085
+ // which closes the feature-only limitation structurally. `carries_properties`
2086
+ // opts this edge into the gated edge-property model. See ADR
2087
+ // 2026-06-02-framework-exercises.
2088
+ framework_exercise_includes_node: { forward_verb: "includes", reverse_verb: "included_in", classification: "cross-domain", source_type: "framework_exercise", target_type: "node", carries_properties: true },
2076
2089
  // ── New edges replacing deleted string properties ────────────────────
2077
2090
  // Marketing
2078
2091
  marketing_strategy_pursues_outcome: { forward_verb: "pursues", reverse_verb: "pursued_by", classification: "cross-domain", source_type: "marketing_strategy", target_type: "outcome" },
@@ -2702,7 +2715,9 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
2702
2715
  "node_owned_by_department",
2703
2716
  "node_owned_by_person",
2704
2717
  // Universal architecture references
2705
- "node_belongs_to_bounded_context"
2718
+ "node_belongs_to_bounded_context",
2719
+ // Framework exercises (an exercise can include any entity type)
2720
+ "framework_exercise_includes_node"
2706
2721
  ];
2707
2722
  var _POLY_KEY_SET = new Set(UPG_POLYMORPHIC_EDGE_KEYS);
2708
2723
  var LEGACY_PRODUCT_STAGES = Object.freeze({
@@ -5625,6 +5640,31 @@ var TEMPLATE_LIFECYCLES = [
5625
5640
  // ── Phase B: SALES_DEAL (qualified → proposal → negotiation → won/lost) ─────
5626
5641
  fromTemplate("deal", SALES_DEAL_TEMPLATE)
5627
5642
  ];
5643
+ var FRAMEWORK_EXERCISE_LIFECYCLE = {
5644
+ entity_type: "framework_exercise",
5645
+ initial_phase: "draft",
5646
+ terminal_phases: ["archived"],
5647
+ phases: [
5648
+ {
5649
+ id: "draft",
5650
+ label: "Draft",
5651
+ description: "The exercise has been created and entities pulled into scope, but the framework's inputs have not all been filled in yet.",
5652
+ transitions_to: ["active", "archived"]
5653
+ },
5654
+ {
5655
+ id: "active",
5656
+ label: "Active",
5657
+ description: "The current, authoritative run of its framework. Its include edges carry the live results consumers read, rank, and render by.",
5658
+ transitions_to: ["archived"]
5659
+ },
5660
+ {
5661
+ id: "archived",
5662
+ label: "Archived",
5663
+ description: "A past run, retained for provenance. Still queryable but superseded by a newer exercise and hidden from default views. Can be revived to active.",
5664
+ transitions_to: ["active"]
5665
+ }
5666
+ ]
5667
+ };
5628
5668
  var UPG_LIFECYCLES = [
5629
5669
  // Product (root)
5630
5670
  PRODUCT_LIFECYCLE,
@@ -5726,6 +5766,8 @@ var UPG_LIFECYCLES = [
5726
5766
  // Product & Growth
5727
5767
  VARIANT_LIFECYCLE,
5728
5768
  GROWTH_CAMPAIGN_LIFECYCLE,
5769
+ // Workspace
5770
+ FRAMEWORK_EXERCISE_LIFECYCLE,
5729
5771
  // ── Template-generated lifecycles ─────────────────────────────────
5730
5772
  ...TEMPLATE_LIFECYCLES
5731
5773
  ];
@@ -9203,6 +9245,11 @@ var UPG_PROPERTY_SCHEMA = {
9203
9245
  },
9204
9246
  methodology: { type: "string", description: "Forecasting methodology used" }
9205
9247
  },
9248
+ // FrameworkExerciseProperties: Framework exercise: one run of a framework over a set of entities.
9249
+ framework_exercise: {
9250
+ framework_id: { type: "string", description: "Which framework this exercise runs: a framework id (e.g. 'moscow', 'rice-scoring', 'kano-model'). Resolves against the framework catalog." },
9251
+ inputs_snapshot: { type: "object", description: "Optional frozen copy of the framework's input spec at apply time, so a historical exercise still renders correctly if the framework definition later evolves (inputs added, removed, or rescaled)." }
9252
+ },
9206
9253
  // FunnelProperties: Funnel entity.
9207
9254
  funnel: {
9208
9255
  funnel_type: { type: "string", enum: ["acquisition", "activation", "retention", "revenue", "referral", "custom"], description: "Which stage of the customer lifecycle this funnel measures" },
@@ -18764,7 +18811,7 @@ var PORTFOLIO_GUIDE = {
18764
18811
  var WORKSPACE_GUIDE = {
18765
18812
  domain_id: "workspace",
18766
18813
  anchor_entity: "workspace",
18767
- creation_sequence: ["workspace"],
18814
+ creation_sequence: ["workspace", "framework_exercise"],
18768
18815
  patterns: [],
18769
18816
  required_bridges: [],
18770
18817
  anti_patterns: [
@@ -23564,7 +23611,7 @@ var NODE_KEY_ORDER = [
23564
23611
  "sort_order",
23565
23612
  "properties"
23566
23613
  ];
23567
- var EDGE_KEY_ORDER = ["id", "source", "target", "type", "mapping_confidence"];
23614
+ var EDGE_KEY_ORDER = ["id", "source", "target", "type", "mapping_confidence", "properties"];
23568
23615
  var CROSS_EDGE_KEY_ORDER = ["id", "source", "target", "type", "source_product_id", "target_product_id", "mapping_confidence"];
23569
23616
  var PRODUCT_KEY_ORDER = ["id", "title", "description", "stage", "properties"];
23570
23617
  function canonicalNode(node) {
@@ -23576,7 +23623,8 @@ function canonicalNode(node) {
23576
23623
  }
23577
23624
  function canonicalEdge(edge) {
23578
23625
  return orderedObject(edge, EDGE_KEY_ORDER, {
23579
- forceKeys: ["id", "source", "target", "type"]
23626
+ forceKeys: ["id", "source", "target", "type"],
23627
+ openKeys: ["properties"]
23580
23628
  });
23581
23629
  }
23582
23630
  function canonicalCrossEdge(edge) {
@@ -23680,7 +23728,7 @@ function serializePortfolioWithHeader(doc, opts) {
23680
23728
  header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
23681
23729
  return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
23682
23730
  }
23683
- var UPG_VERSION = "0.8.2";
23731
+ var UPG_VERSION = "0.8.4";
23684
23732
  var MARKDOWN_FORMAT_VERSION = "0.1";
23685
23733
  var UPG_TYPES = getTypes();
23686
23734
  var UPG_TYPES_SET = new Set(UPG_TYPES);
@@ -25286,7 +25334,8 @@ var createEdge = (args, ctx) => {
25286
25334
  target_id: args.target_id,
25287
25335
  target_title: args.target_title,
25288
25336
  target_type: args.target_type,
25289
- type: args.type
25337
+ type: args.type,
25338
+ properties: args.properties
25290
25339
  });
25291
25340
  if ("error" in result) {
25292
25341
  if (result.no_canonical_edge_for) {
@@ -26185,6 +26234,62 @@ var getEntitySchema = (args, _ctx) => {
26185
26234
  }
26186
26235
  };
26187
26236
 
26237
+ // src/tools/frameworks.ts
26238
+ import {
26239
+ applyFramework as applyFrameworkLib,
26240
+ scoreEntity as scoreEntityLib
26241
+ } from "@unified-product-graph/sdk";
26242
+ var applyFramework = (args, ctx) => {
26243
+ const { store } = ctx;
26244
+ const frameworkId = args.framework_id;
26245
+ if (!frameworkId) {
26246
+ return textError('Missing required parameter: framework_id (e.g. "moscow", "rice-scoring")');
26247
+ }
26248
+ try {
26249
+ const result = applyFrameworkLib(store, {
26250
+ framework_id: frameworkId,
26251
+ title: args.title,
26252
+ entity_ids: args.entity_ids ?? [],
26253
+ status: args.status
26254
+ });
26255
+ return text(
26256
+ JSON.stringify(
26257
+ {
26258
+ exercise_id: result.exercise.id,
26259
+ exercise: result.exercise,
26260
+ included: result.edges.map((e) => ({ edge_id: e.id, entity_id: e.target })),
26261
+ warnings: result.warnings
26262
+ },
26263
+ null,
26264
+ 2
26265
+ )
26266
+ );
26267
+ } catch (err) {
26268
+ return textError(err.message);
26269
+ }
26270
+ };
26271
+ var scoreEntity = (args, ctx) => {
26272
+ const { store } = ctx;
26273
+ const exerciseId = args.exercise_id;
26274
+ const entityId = args.entity_id;
26275
+ const values = args.values;
26276
+ if (!exerciseId) return textError("Missing required parameter: exercise_id");
26277
+ if (!entityId) return textError("Missing required parameter: entity_id");
26278
+ if (!values || typeof values !== "object" || Array.isArray(values)) {
26279
+ return textError(
26280
+ 'Missing required parameter: values (object of input \u2192 value, e.g. { "moscow": "must" })'
26281
+ );
26282
+ }
26283
+ const result = scoreEntityLib(store, {
26284
+ exercise_id: exerciseId,
26285
+ entity_id: entityId,
26286
+ values,
26287
+ replace: args.replace
26288
+ });
26289
+ if ("error" in result) return textError(result.error);
26290
+ return text(JSON.stringify({ edge: result.edge, warnings: result.warnings }, null, 2));
26291
+ };
26292
+
26188
26293
  // src/tools/spec.ts
26189
26294
  import { buildResolverHints as buildResolverHints2 } from "@unified-product-graph/sdk";
26190
26295
  import {
@@ -26779,34 +26884,38 @@ var inspect = async (args, ctx) => {
26779
26884
  });
26780
26885
  };
26781
26886
  var prioritise = (args, ctx) => {
26782
- const candidates = args.candidates;
26887
+ const candidates = args.candidates ?? [];
26783
26888
  const frameworkId = args.framework_id;
26784
- if (!candidates || !Array.isArray(candidates) || candidates.length === 0) {
26785
- return textError("Missing required parameter: candidates (entity_id[])");
26786
- }
26889
+ const exerciseId = args.exercise_id;
26787
26890
  if (!frameworkId) {
26788
26891
  return textError(
26789
26892
  'Missing required parameter: framework_id (e.g. "rice-scoring", "ice-scoring", "wsjf")'
26790
26893
  );
26791
26894
  }
26895
+ if (!exerciseId && (!Array.isArray(candidates) || candidates.length === 0)) {
26896
+ return textError(
26897
+ "Provide candidates (entity_id[]), or an exercise_id whose includes edges supply the candidates and their scoring inputs."
26898
+ );
26899
+ }
26792
26900
  const framework = UPG_FRAMEWORKS_BY_ID[frameworkId];
26793
26901
  if (!framework) {
26794
26902
  return textError(
26795
26903
  `Unknown framework_id: "${frameworkId}". See list_frameworks for valid ids.`
26796
26904
  );
26797
26905
  }
26798
- const execResult = executePrioritise(framework, candidates, ctx.store);
26906
+ const execResult = executePrioritise(framework, candidates, ctx.store, { exerciseId });
26907
+ const params = { candidates, framework_id: frameworkId, ...exerciseId ? { exercise_id: exerciseId } : {} };
26799
26908
  if (execResult.kind === "execution") {
26800
26909
  return approachEnvelope("prioritise", candidates, {
26801
- params: { candidates, framework_id: frameworkId },
26910
+ params,
26802
26911
  framework_resolved: execResult.framework_used,
26803
26912
  ranked: execResult.ranked,
26804
26913
  required_properties: execResult.required_properties,
26805
- execution_mode: "execution_v0_4_0"
26914
+ execution_mode: exerciseId ? "exercise_v0_8_4" : "execution_v0_4_0"
26806
26915
  });
26807
26916
  }
26808
26917
  return approachEnvelope("prioritise", candidates, {
26809
- params: { candidates, framework_id: frameworkId },
26918
+ params,
26810
26919
  framework_resolved: execResult.framework_used,
26811
26920
  hint: execResult.hint,
26812
26921
  execution_mode: "definition_lookup_v0_4_0"
@@ -27669,7 +27778,8 @@ var pushToCloud = async (args, ctx) => {
27669
27778
 
27670
27779
  // src/tools/skills.ts
27671
27780
  import { existsSync as existsSync2, lstatSync, readlinkSync, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync } from "fs";
27672
- import { join as join5, resolve as resolve2 } from "path";
27781
+ import { join as join5, resolve as resolve2, dirname as dirname3 } from "path";
27782
+ import { fileURLToPath } from "url";
27673
27783
  function repoRoot() {
27674
27784
  return process.cwd();
27675
27785
  }
@@ -27680,8 +27790,40 @@ function canonicalisePath(p) {
27680
27790
  return p;
27681
27791
  }
27682
27792
  }
27793
+ function isSkillsDir(candidate) {
27794
+ try {
27795
+ if (!existsSync2(candidate)) return false;
27796
+ return readdirSync2(candidate, { withFileTypes: true }).some(
27797
+ (e) => (e.isDirectory() || e.isSymbolicLink()) && existsSync2(join5(candidate, e.name, "SKILL.md"))
27798
+ );
27799
+ } catch {
27800
+ return false;
27801
+ }
27802
+ }
27803
+ function resolveBundledSkillsDir() {
27804
+ let md;
27805
+ try {
27806
+ md = dirname3(fileURLToPath(import.meta.url));
27807
+ } catch {
27808
+ md = process.cwd();
27809
+ }
27810
+ for (const c of [resolve2(md, "..", "skills"), resolve2(md, "..", "..", "skills"), resolve2(md, "skills")]) {
27811
+ if (isSkillsDir(c)) return c;
27812
+ }
27813
+ let dir = md;
27814
+ for (let i = 0; i < 12; i++) {
27815
+ const mono = join5(dir, "packages", "upg-mcp-server", "skills");
27816
+ if (isSkillsDir(mono)) return mono;
27817
+ const parent = dirname3(dir);
27818
+ if (parent === dir) break;
27819
+ dir = parent;
27820
+ }
27821
+ return null;
27822
+ }
27683
27823
  function sourceSkillsDir() {
27684
- return resolve2(repoRoot(), "packages/upg-mcp-server/skills");
27824
+ const cwdPath = resolve2(repoRoot(), "packages/upg-mcp-server/skills");
27825
+ if (existsSync2(cwdPath)) return cwdPath;
27826
+ return resolveBundledSkillsDir() ?? cwdPath;
27685
27827
  }
27686
27828
  function deployedSkillsDir() {
27687
27829
  return resolve2(repoRoot(), ".claude/skills");
@@ -27724,6 +27866,21 @@ function auditOne(name) {
27724
27866
  const deployedExists = existsSync2(deployedPath);
27725
27867
  if (!sourceExists) issues.push("Canonical source SKILL.md is missing");
27726
27868
  if (!deployedExists) issues.push("Deployed SKILL.md is missing; run ./scripts/link-skills.sh");
27869
+ let inSync = false;
27870
+ let deployedFrontmatter = null;
27871
+ let deployedFirstHeading = null;
27872
+ if (deployedExists) {
27873
+ const deployedBody = readFileSync2(deployedPath, "utf8");
27874
+ deployedFrontmatter = parseFrontmatter(deployedBody);
27875
+ deployedFirstHeading = firstHeading(deployedBody);
27876
+ if (sourceExists) {
27877
+ const sourceBody = readFileSync2(sourcePath, "utf8");
27878
+ inSync = deployedBody === sourceBody;
27879
+ if (!inSync) {
27880
+ issues.push("Deployed SKILL.md differs from canonical source; symlink is stale or broken");
27881
+ }
27882
+ }
27883
+ }
27727
27884
  let isSymlink = false;
27728
27885
  let symlinkTarget = null;
27729
27886
  if (existsSync2(deployedDir)) {
@@ -27731,32 +27888,17 @@ function auditOne(name) {
27731
27888
  isSymlink = stat.isSymbolicLink();
27732
27889
  if (isSymlink) {
27733
27890
  symlinkTarget = readlinkSync(deployedDir);
27734
- const targetReal = canonicalisePath(symlinkTarget);
27735
- const expectedReal = canonicalisePath(sourceDir);
27736
- if (targetReal !== expectedReal) {
27737
- issues.push(`Symlink points to ${symlinkTarget}, expected ${sourceDir}`);
27891
+ if (!inSync && sourceExists) {
27892
+ const targetReal = canonicalisePath(symlinkTarget);
27893
+ const expectedReal = canonicalisePath(sourceDir);
27894
+ if (targetReal !== expectedReal) {
27895
+ issues.push(`Symlink points to ${symlinkTarget}, expected ${sourceDir}`);
27896
+ }
27738
27897
  }
27739
- } else if (deployedExists) {
27898
+ } else if (deployedExists && !inSync) {
27740
27899
  issues.push("Deployed entry is a real directory, not a symlink; stale copy will not pick up source updates; run ./scripts/link-skills.sh");
27741
27900
  }
27742
27901
  }
27743
- let inSync = false;
27744
- let deployedFrontmatter = null;
27745
- let deployedFirstHeading = null;
27746
- if (deployedExists && sourceExists) {
27747
- const deployedBody = readFileSync2(deployedPath, "utf8");
27748
- const sourceBody = readFileSync2(sourcePath, "utf8");
27749
- inSync = deployedBody === sourceBody;
27750
- deployedFrontmatter = parseFrontmatter(deployedBody);
27751
- deployedFirstHeading = firstHeading(deployedBody);
27752
- if (!inSync) {
27753
- issues.push("Deployed SKILL.md differs from canonical source; symlink is stale or broken");
27754
- }
27755
- } else if (deployedExists) {
27756
- const deployedBody = readFileSync2(deployedPath, "utf8");
27757
- deployedFrontmatter = parseFrontmatter(deployedBody);
27758
- deployedFirstHeading = firstHeading(deployedBody);
27759
- }
27760
27902
  return {
27761
27903
  name,
27762
27904
  deployed_path: deployedPath,
@@ -28181,6 +28323,10 @@ var TOOL_DEFINITIONS = [
28181
28323
  type: {
28182
28324
  type: "string",
28183
28325
  description: "Edge type. Auto-inferred if omitted."
28326
+ },
28327
+ properties: {
28328
+ type: "object",
28329
+ description: "Edge-scoped properties. Only permitted on edge types that opt in (currently framework_exercise_includes_node); rejected on plain semantic edges."
28184
28330
  }
28185
28331
  },
28186
28332
  required: ["source_id"]
@@ -28482,10 +28628,39 @@ var TOOL_DEFINITIONS = [
28482
28628
  inputSchema: {
28483
28629
  type: "object",
28484
28630
  properties: {
28485
- candidates: { type: "array", items: { type: "string" }, description: "Required. entity_id[] to rank." },
28486
- framework_id: { type: "string", description: 'Required. UPGFramework.id of the scoring lens (e.g. "rice-scoring", "ice-scoring", "kano-model", "cost-of-delay", "wsjf").' }
28631
+ candidates: { type: "array", items: { type: "string" }, description: "entity_id[] to rank. Optional when exercise_id is given (the exercise supplies them)." },
28632
+ framework_id: { type: "string", description: 'Required. UPGFramework.id of the scoring lens (e.g. "rice-scoring", "ice-scoring", "kano-model", "cost-of-delay", "wsjf").' },
28633
+ exercise_id: { type: "string", description: "Optional (0.8.4). A framework_exercise id: reads each candidate's scoring inputs from its includes-edge properties instead of node.properties, and bypasses the target-type guard so any entity type can be scored." }
28634
+ },
28635
+ required: ["framework_id"]
28636
+ }
28637
+ },
28638
+ {
28639
+ name: "apply_framework",
28640
+ description: "Apply a framework (MoSCoW, RICE, Kano, ...) to a set of entities: creates a framework_exercise node and an `includes` edge to each entity. The per-entity result is recorded on the edge via score_entity, never on the entity node, so the same entity can sit in many exercises and any entity type can be scored. Returns { exercise_id, exercise, included, warnings }.",
28641
+ inputSchema: {
28642
+ type: "object",
28643
+ properties: {
28644
+ framework_id: { type: "string", description: 'Required. UPGFramework.id (e.g. "moscow", "rice-scoring").' },
28645
+ title: { type: "string", description: 'Human label for the exercise (default "<Framework> exercise").' },
28646
+ entity_ids: { type: "array", items: { type: "string" }, description: "Entities to pull into the exercise (any type)." },
28647
+ status: { type: "string", description: "Lifecycle phase: draft | active | archived (default draft)." }
28648
+ },
28649
+ required: ["framework_id"]
28650
+ }
28651
+ },
28652
+ {
28653
+ name: "score_entity",
28654
+ description: "Record a framework's result for one entity on the exercise's includes edge (a MoSCoW bucket, a RICE score, a canvas slot). Auto-includes the entity if not already in scope. Merges into existing edge properties unless replace is set. Returns { edge, warnings }.",
28655
+ inputSchema: {
28656
+ type: "object",
28657
+ properties: {
28658
+ exercise_id: { type: "string", description: "Required. The framework_exercise id." },
28659
+ entity_id: { type: "string", description: "Required. The entity being scored." },
28660
+ values: { type: "object", description: 'Required. The result as { input: value }, e.g. { "moscow": "must" } or { "reach": 800, "impact": 3 }.' },
28661
+ replace: { type: "boolean", description: "Replace the edge properties instead of merging (default false)." }
28487
28662
  },
28488
- required: ["candidates", "framework_id"]
28663
+ required: ["exercise_id", "entity_id", "values"]
28489
28664
  }
28490
28665
  },
28491
28666
  {
@@ -29141,6 +29316,8 @@ var HANDLERS = {
29141
29316
  batch_delete_nodes: batchDeleteNodes,
29142
29317
  batch_create_edges: batchCreateEdges,
29143
29318
  batch_delete_edges: batchDeleteEdges,
29319
+ apply_framework: applyFramework,
29320
+ score_entity: scoreEntity,
29144
29321
  repair_dangling_edges: repairDanglingEdges,
29145
29322
  export_edges: exportEdges,
29146
29323
  rename_edge_type: renameEdgeType,
@@ -29273,7 +29450,7 @@ var SERVER_INSTRUCTIONS = [
29273
29450
  ].join("\n");
29274
29451
  function resolvePackageVersion() {
29275
29452
  try {
29276
- const here = path5.dirname(fileURLToPath(import.meta.url));
29453
+ const here = path5.dirname(fileURLToPath2(import.meta.url));
29277
29454
  const pkgPath = path5.resolve(here, "..", "package.json");
29278
29455
  const raw = fs2.readFileSync(pkgPath, "utf-8");
29279
29456
  const pkg = JSON.parse(raw);
@@ -29331,7 +29508,7 @@ function createServer(store) {
29331
29508
 
29332
29509
  // src/index.ts
29333
29510
  import { nanoid } from "nanoid";
29334
- import { fileURLToPath as fileURLToPath2 } from "url";
29511
+ import { fileURLToPath as fileURLToPath3 } from "url";
29335
29512
  import { realpathSync as realpathSync2 } from "fs";
29336
29513
  async function discoverUPGFile(explicitFile) {
29337
29514
  if (explicitFile) return path6.resolve(explicitFile);
@@ -29409,7 +29586,12 @@ async function runMcpServer() {
29409
29586
  options: {
29410
29587
  file: { type: "string", short: "f" },
29411
29588
  title: { type: "string", short: "t" }
29412
- }
29589
+ },
29590
+ // Tolerate stray positionals. When launched via `upg mcp run`, argv carries
29591
+ // the `mcp run` subcommand tokens; without this, parseArgs throws
29592
+ // ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL and the server never starts. The
29593
+ // standalone bin (`upg-mcp-server`) passes none, so this is harmless there.
29594
+ allowPositionals: true
29413
29595
  });
29414
29596
  let resolvedPath = await discoverUPGFile(values.file);
29415
29597
  if (!resolvedPath) {
@@ -29500,7 +29682,7 @@ ${lines.join("\n")}
29500
29682
  function isEntrypoint() {
29501
29683
  if (!process.argv[1]) return false;
29502
29684
  try {
29503
- return realpathSync2(process.argv[1]) === realpathSync2(fileURLToPath2(import.meta.url));
29685
+ return realpathSync2(process.argv[1]) === realpathSync2(fileURLToPath3(import.meta.url));
29504
29686
  } catch {
29505
29687
  return false;
29506
29688
  }