@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 +21 -0
- package/TOOLS.md +60 -3
- package/dist/index.js +231 -49
- package/dist/index.js.map +1 -1
- package/dist/tools-manifest.json +145 -49
- package/package.json +1 -1
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
|
|
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):
|
|
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 |
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
27735
|
-
|
|
27736
|
-
|
|
27737
|
-
|
|
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: "
|
|
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: ["
|
|
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(
|
|
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
|
|
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(
|
|
29685
|
+
return realpathSync2(process.argv[1]) === realpathSync2(fileURLToPath3(import.meta.url));
|
|
29504
29686
|
} catch {
|
|
29505
29687
|
return false;
|
|
29506
29688
|
}
|