umtri-mcp 1.4.1 → 1.5.0

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.
@@ -15,6 +15,6 @@ export const SERVER_INSTRUCTIONS = `Umtri maps a project's structure as a tree (
15
15
 
16
16
  Umtri never reads git. A commit reaches the ground only because something called create_node / update_node / record_commit — so the tree drifts behind the code unless the repo has a habit that says otherwise. If this repo's own rules file (CLAUDE.md, AGENTS.md, …) has no commit-sync policy, read umtri://rules/commit-sync and offer the human a short one to paste in.
17
17
 
18
- Before creating or reclassifying nodes, read umtri://rules/vocabulary. Opening a season seals the current one, so create_season only acts after the user confirms (umtri://rules/seasons).
18
+ Before creating or reclassifying nodes, read umtri://rules/vocabulary; before creating a trunk or a limb under one, also umtri://rules/trunk-kinds. Opening a season seals the current one, so create_season only acts after the user confirms (umtri://rules/seasons).
19
19
 
20
20
  If Umtri itself gets in your way — a call rejected for a reason that looks wrong, a capability that is missing, a tool description that misled you — tell the human what happened and offer to file it with send_feedback. Never file it on your own initiative.`;
package/core/policy.js CHANGED
@@ -24,6 +24,26 @@ const VERB_ENDINGS = ['수정', '적용', '마이그레이션', '분리', '등
24
24
  // Trunk naming pattern: <name>-app / <name>-server / single lowercase word (db, www).
25
25
  const TRUNK_PATTERN = /^[a-z][a-z0-9-]*(-app|-server|-service|-job|-cdn)$|^[a-z]{2,8}$/;
26
26
 
27
+ // Trunk kinds — umtri://rules/trunk-kinds. Recorded in trunk metadata.kind.
28
+ // A trunk that fits none of these keeps kind unset; the agent asks the human whether to
29
+ // suggest a new kind via send_feedback (never files it on its own).
30
+ export const TRUNK_KINDS = ['front', 'server', 'database', 'library', 'device'];
31
+
32
+ // Suffixes that repeat what the trunk kind already says ("예약 API", "Reservation tables").
33
+ // Matched at the end of a limb label directly under a trunk.
34
+ const KIND_SUFFIX_PATTERN = /(\s|·)?(api|apis|응답|테이블|table|tables|관리|admin|management|endpoints?|엔드포인트)$/i;
35
+
36
+ // Technical-layer limb names — how code is arranged, not what the system is made of.
37
+ // Whole-label match (after trimming) so "상태 관리 화면" or "Store locator" don't trip it.
38
+ const LAYER_LIMB_LABELS = [
39
+ 'state', 'state management', 'store', 'stores', 'utils', 'util', 'utilities', 'helpers',
40
+ 'hooks', 'routes', 'routing', 'router', 'middleware', 'middlewares', 'models',
41
+ 'services', 'controllers', 'components', 'handlers', 'lib', 'core', 'http routes',
42
+ 'data store', 'app bootstrap', 'bootstrap',
43
+ '상태 관리', '상태', '유틸', '유틸리티', '헬퍼', '훅', '라우트', '라우팅', '미들웨어',
44
+ '모델', '서비스', '컨트롤러', '컴포넌트', '핸들러',
45
+ ];
46
+
27
47
  // Promotion signals — when a twig's description contains these, it might belong at limb level.
28
48
  // Derived from sidebar/auth/ui-system limb promotions on 2026-05-21.
29
49
  const PROMOTION_KEYWORDS = [
@@ -100,6 +120,71 @@ export function trunkNamingCheck(type, label) {
100
120
  };
101
121
  }
102
122
 
123
+ // trunk에 kind가 없거나 다섯 유형 밖이면 알린다. 거부하지 않는다 — 기존 나무가 대부분 kind 없이 있다.
124
+ export function trunkKindCheck(type, metadata) {
125
+ if (type !== 'trunk') return null;
126
+ const kind = metadata?.kind;
127
+ const fallback = 'If none of the five fits, leave kind unset, tell the human which trunk did not fit and why, and offer to suggest a new kind with send_feedback (kind "improvement") — do not file it on your own. See umtri://rules/trunk-kinds.';
128
+ if (kind == null || kind === '') {
129
+ return {
130
+ rule: 'trunk-kind-missing',
131
+ severity: 'info',
132
+ message: `Trunk has no metadata.kind. Set one of ${TRUNK_KINDS.join(' / ')} — the kind decides how its limbs split. ${fallback}`,
133
+ };
134
+ }
135
+ if (!TRUNK_KINDS.includes(kind)) {
136
+ return {
137
+ rule: 'trunk-kind-unknown',
138
+ severity: 'info',
139
+ message: `metadata.kind "${kind}" is not one of ${TRUNK_KINDS.join(' / ')}. ${fallback}`,
140
+ };
141
+ }
142
+ return null;
143
+ }
144
+
145
+ // trunk 바로 아래 limb의 이름 규칙 — 접미사 금지, 레이어 이름 금지, 공통 limb는 하나.
146
+ // parentKind: 부모 trunk의 metadata.kind(없으면 null). siblingCommonCount: 같은 trunk 아래
147
+ // 이 노드를 뺀 metadata.common=true limb 수.
148
+ export function limbUnderTrunkChecks({ type, label, parentType, parentKind, metadata, siblingCommonCount = 0 }) {
149
+ const out = [];
150
+ if (type !== 'limb' || parentType !== 'trunk' || !label) return out;
151
+ const trimmed = label.trim();
152
+
153
+ const isLayer = LAYER_LIMB_LABELS.includes(trimmed.toLowerCase());
154
+ // 레이어 이름("상태 관리")은 접미사 문제가 아니라 축 문제라 layer 경고 하나만 낸다.
155
+ if (!isLayer && KIND_SUFFIX_PATTERN.test(trimmed) && trimmed.replace(KIND_SUFFIX_PATTERN, '').trim()) {
156
+ out.push({
157
+ rule: 'limb-domain-suffix',
158
+ severity: 'info',
159
+ message: `Limb "${label}" ends in a kind suffix. Name the domain alone ("${trimmed.replace(KIND_SUFFIX_PATTERN, '').trim()}") and reuse the exact label other trunks use for it — the trunk already says what kind of thing it is. See umtri://rules/trunk-kinds.`,
160
+ });
161
+ }
162
+
163
+ if (isLayer) {
164
+ const axis = {
165
+ front: 'user-facing features or flows (or pages for a public site)',
166
+ server: 'domains / resources',
167
+ database: 'domains, named like the server',
168
+ library: 'capabilities or concepts',
169
+ device: 'physical parts or screen regions',
170
+ }[parentKind] ?? "the trunk kind's axis (umtri://rules/trunk-kinds)";
171
+ out.push({
172
+ rule: 'limb-layer-axis',
173
+ severity: 'warn',
174
+ message: `Limb "${label}" names a technical layer. Split this trunk by ${axis}; put what this would hold into the feature/domain limb it serves, or into the trunk's one common limb.`,
175
+ });
176
+ }
177
+
178
+ if (metadata?.common === true && siblingCommonCount > 0) {
179
+ out.push({
180
+ rule: 'common-limb-duplicate',
181
+ severity: 'warn',
182
+ message: 'This trunk already has a common limb (metadata.common=true). Keep one per trunk — make this a twig of that limb, or a real feature/domain limb.',
183
+ });
184
+ }
185
+ return out;
186
+ }
187
+
103
188
  export function leafVsVeinCheck(type, label) {
104
189
  if (!label) return null;
105
190
  if (type !== 'leaf' && type !== 'vein') return null;
@@ -356,7 +441,7 @@ export function hierarchyCheck(parentType, childType) {
356
441
  // phase: 'create' enables dormancy hint (irrelevant on update where children may already exist).
357
442
  // siblingLeafCount: # of active leaf siblings already under the resolved parent (for parallel-leaves rule).
358
443
  // metadata: this node's metadata (for missing-implements rule).
359
- export function validateNode({ type, label, parentType, description, metadata, siblingLeafCount, siblingLeafLabels, phase = 'create' }) {
444
+ export function validateNode({ type, label, parentType, parentKind = null, siblingCommonCount = 0, description, metadata, siblingLeafCount, siblingLeafLabels, phase = 'create' }) {
360
445
  const warnings = [];
361
446
  let rejectReason = null;
362
447
 
@@ -366,12 +451,26 @@ export function validateNode({ type, label, parentType, description, metadata, s
366
451
  else warnings.push(h);
367
452
  }
368
453
 
369
- const r = reservedDomainCheck(label);
454
+ let r = reservedDomainCheck(label);
455
+ // 유형이 정해진 trunk 바로 아래 limb는 도메인 이름이다 — Umtri 자신의 server·db에는 "버그" 도메인이
456
+ // 있다. 그 코드 영역을 가리키는 거라면 정당하니 info로 낮추고 그 뜻을 말해 준다.
457
+ if (r?.rule === 'reserved-domain' && type === 'limb' && parentType === 'trunk' && TRUNK_KINDS.includes(parentKind)) {
458
+ r = {
459
+ rule: 'reserved-domain',
460
+ severity: 'info',
461
+ message: `"${label}" is also a first-class record (bugs/apis/seasons/edges). As a limb under a ${parentKind} trunk it is fine if it names the code that handles that domain — not individual records.`,
462
+ };
463
+ }
370
464
  if (r) warnings.push(r);
371
465
 
372
466
  const t = trunkNamingCheck(type, label);
373
467
  if (t) warnings.push(t);
374
468
 
469
+ const tk = trunkKindCheck(type, metadata);
470
+ if (tk) warnings.push(tk);
471
+
472
+ warnings.push(...limbUnderTrunkChecks({ type, label, parentType, parentKind, metadata, siblingCommonCount }));
473
+
375
474
  const l = leafVsVeinCheck(type, label);
376
475
  if (l) warnings.push(l);
377
476
 
@@ -79,6 +79,10 @@ users* vs *read by us*.
79
79
  - `trunk` = a big domain (e.g. `www`, `server`, `db`). Pick stable,
80
80
  long-lived names — usually matching a top-level code area.
81
81
  - `limb` = a category inside a trunk (e.g. `graph`, `table`, `dashboard`).
82
+ How a trunk splits into limbs depends on its kind (`metadata.kind`): features
83
+ for a front, domains for a server and its database (same names, no suffix),
84
+ capabilities for a library, parts for a device — one common limb per trunk.
85
+ See `umtri://rules/trunk-kinds`.
82
86
  - Do **not** create one limb per time period. Time is expressed through
83
87
  `season` and `created_at`, not by carving the tree into temporal slices.
84
88
  - Infrastructure (servers, DNS) is *not* a trunk — it is the environment the
@@ -106,6 +110,8 @@ questions about a ground.
106
110
  | `role_change_at` | type-changed nodes | `string` — ISO date |
107
111
  | `role_change_reason` | type-changed nodes | `string` — short prose explaining the shift |
108
112
  | `placeholder` | intentionally-empty structure | `true` — opts out of dormant cleanup suggestions |
113
+ | `kind` | trunks | `front` \| `server` \| `database` \| `library` \| `device` |
114
+ | `common` | the one shared limb of a trunk | `true` — see `umtri://rules/trunk-kinds` |
109
115
 
110
116
  ## One file, multiple nodes
111
117
 
@@ -0,0 +1,92 @@
1
+ # Trunk Kinds — How Each Kind of Trunk Branches
2
+
3
+ Every trunk is one of five kinds, and each kind has its own rule for what its
4
+ limbs are. Recording the kind lets the same domain line up across trunks —
5
+ `Reservations` as a screen area, as endpoints, and as tables — instead of each
6
+ trunk being carved along a different axis.
7
+
8
+ ## Record the kind
9
+
10
+ Set `metadata.kind` on the trunk. Do not leave it to be guessed from the
11
+ label: the kind is what the MCP reads to check the limbs under it.
12
+
13
+ | kind | What it is | Limbs split by | The common limb holds | Leaves are |
14
+ | --- | --- | --- | --- | --- |
15
+ | `front` | Something people use on a screen — app, web app, admin, static site | User-facing feature or flow (Reservations, My page). A public site splits by page | App shell (layout, routing, boot), design system, shared components | Screens, components |
16
+ | `server` | A service that takes requests — API, webhooks, jobs | Domain / resource. Non-domain consumers (external integrations, inbound webhooks) get their own limb | Request handling — authentication, authorization, errors, boot | Endpoint groups (`metadata.endpoints`) |
17
+ | `database` | A data store — relational DB, document DB, cache | Domain, **with the same names as the server** | Tables shared by many domains (sessions, audit log) | Tables (`metadata.columns`), labelled by their logical name |
18
+ | `library` | Shared code or a package that other trunks import | Capability or concept (Money, Signal pipeline) | Shared types and constants | Modules |
19
+ | `device` | Physical hardware, an embedded screen, an enclosure | Physical part or screen region | Device-wide parts (power, communication) | Parts, screen elements |
20
+
21
+ Static sites and admin panels are `front`. A monorepo package that apps import
22
+ is `library`.
23
+
24
+ ### When none of the five fits
25
+
26
+ Do not force it. Leave `metadata.kind` unset, tell the human which trunk did
27
+ not fit and why, and **offer** to report it with `send_feedback` (kind
28
+ `improvement`) so the kind can be added. Never file that feedback on your own
29
+ initiative — the human decides.
30
+
31
+ ## One common limb per trunk
32
+
33
+ Each trunk has **at most one** common limb, marked `metadata.common: true`.
34
+ Its name is free (`Common`, `Shell`, `공통`, `요청 처리` …); what it holds is
35
+ defined by the kind (table above). Anything that belongs to no single feature
36
+ or domain goes there — as twigs if it grows.
37
+
38
+ If a second common limb seems necessary, it is either a real feature/domain
39
+ limb or a twig of the existing common limb.
40
+
41
+ **Never split a trunk by technical layer.** `State`, `Utils`, `Hooks`,
42
+ `Routes`, `Middleware`, `Store`, `Models`, `Services`, `Components`
43
+ (`상태 관리`, `유틸`, `훅`, `라우트`, `미들웨어`, `모델` …) are how the code is
44
+ arranged, not what the system is made of. Their contents belong in the feature
45
+ or domain limb they serve, or in the common limb.
46
+
47
+ ## Same domain, same name — no suffixes
48
+
49
+ A domain is named the same in every trunk, without a suffix:
50
+
51
+ | ✅ | ❌ |
52
+ | --- | --- |
53
+ | `Reservations` (front) · `Reservations` (server) · `Reservations` (database) | `Reservations API`, `Reservation tables`, `Reservation admin` |
54
+ | `예약` · `예약` · `예약` | `예약 API`, `예약 응답`, `예약 테이블`, `예약 관리` |
55
+
56
+ The trunk already says what kind of thing it is, so a suffix repeats it — and
57
+ suffixes drift (`API` in one ground, `응답` in another), which breaks the
58
+ alignment the shared name is for. An admin `front` names its limbs by the
59
+ resource too: `Members`, not `Member management`.
60
+
61
+ When adding a limb, look at the other trunks first. If the domain already
62
+ exists there, reuse its exact label.
63
+
64
+ ## Connections between kinds
65
+
66
+ | From → to | Record as |
67
+ | --- | --- |
68
+ | front screen → server endpoint | api (`create_api`, caller → callee) |
69
+ | server endpoint → database table | edge `data_flow` (writes) or `dependency` (reads) |
70
+ | table → table (foreign key) | edge `dependency` (referencing → referenced) |
71
+ | any trunk → library module | edge `dependency` |
72
+
73
+ ## Warnings
74
+
75
+ `create_node` and `update_node` never reject on these — they come back in
76
+ `warnings[]`:
77
+
78
+ | Rule | Meaning |
79
+ | --- | --- |
80
+ | `trunk-kind-missing` | A trunk has no `metadata.kind`. Pick one of the five, or follow "When none of the five fits". |
81
+ | `trunk-kind-unknown` | `metadata.kind` is not one of the five. Same advice. |
82
+ | `limb-domain-suffix` | A limb directly under a trunk ends in a kind suffix (`API`, `응답`, `테이블`, `관리`, `Table(s)`, `Admin`, `Management`). Drop it and reuse the domain name. |
83
+ | `limb-layer-axis` | A limb is named after a technical layer. Split by the trunk kind's axis instead. |
84
+ | `common-limb-duplicate` | The trunk already has a limb with `metadata.common: true`. |
85
+
86
+ ## Reshaping an existing tree
87
+
88
+ Grounds built before these rules exist. Bring them in line one node at a
89
+ time: set each trunk's `kind`, then rename or regroup limbs with the label
90
+ change visible, recording `reparented_*` when nodes move (see
91
+ `umtri://rules/system-structure`). Ask the human before a large regrouping —
92
+ the shape of their tree is theirs to approve.
@@ -32,6 +32,10 @@ Domains or feature areas (auth, payments, reservations, dashboard, …) are
32
32
  that spans the user-app and admin trunks" — fine. "Authentication is a
33
33
  trunk" — no.
34
34
 
35
+ Record which of the five kinds the trunk is in `metadata.kind` (`front`,
36
+ `server`, `database`, `library`, `device`). The kind decides how its limbs
37
+ split — see `umtri://rules/trunk-kinds`.
38
+
35
39
  Naming convention (recommended, not enforced):
36
40
  - Apps: `<name>-app` (e.g. `user-app`, `kiosk-app`)
37
41
  - Servers: `<name>-server` (e.g. `api-server`, `iot-server`)
@@ -24,8 +24,8 @@ guideline; see `umtri://rules/system-structure`.
24
24
 
25
25
  | Type | Role | What it represents |
26
26
  | ------- | --------- | ------------------------------------------------------------------ |
27
- | `trunk` | structure | A deployable system or independent service (app, server, DB). |
28
- | `limb` | structure | A domain or feature area inside a trunk (auth, payments, dashboard).|
27
+ | `trunk` | structure | A deployable system or independent service (app, server, DB). Carries `metadata.kind` — see `umtri://rules/trunk-kinds`. |
28
+ | `limb` | structure | A domain or feature area inside a trunk (auth, payments, dashboard) — the axis depends on the trunk's kind. |
29
29
  | `twig` | structure | A cluster of related leaves (a module, a feature group). |
30
30
  | `leaf` | object | The thing itself — class, component, screen, entity. Noun phrase. |
31
31
  | `vein` | action | A capability the object performs — a method/function that exists now. Verb phrase. NOT a task or a change that was made. |
@@ -172,9 +172,14 @@ If you see one of these in the response, read it before proceeding:
172
172
  | `structure-without-children` | New limb/twig will render dormant until a leaf/vein appears. |
173
173
  | `twig-promotion-candidate` | Twig description suggests it should be a sibling limb. |
174
174
  | `redundant-child` | A twig and its only leaf duplicate the same concept. |
175
+ | `trunk-kind-missing` / `trunk-kind-unknown` | Trunk has no / an unknown `metadata.kind` (front, server, database, library, device). |
176
+ | `limb-domain-suffix` | Limb under a trunk ends in `API`/`응답`/`테이블`/`관리`… — name the domain alone. |
177
+ | `limb-layer-axis` | Limb is a technical layer (State, Utils, Routes, Middleware…). |
178
+ | `common-limb-duplicate` | The trunk already has its one common limb (`metadata.common`). |
175
179
 
176
180
  ## Further reading (read only when relevant)
177
181
 
182
+ - `umtri://rules/trunk-kinds` — the five trunk kinds and how each splits into limbs
178
183
  - `umtri://rules/system-structure` — metadata key conventions, file→leaf mapping
179
184
  - `umtri://rules/vocabulary-detailed` — promotion signals, reclassification
180
185
  policy, twig-vs-leaf decision matrix, redundant-child antipattern details
package/core/resources.js CHANGED
@@ -22,6 +22,13 @@ const RESOURCES = [
22
22
  title: 'Umtri vocabulary — detailed guidance',
23
23
  description: 'Reclassification policy, twig vs leaf decision matrix, twig promotion signals, redundant-child antipattern, Korean label heuristics. Read only when restructuring an existing tree or wrestling with a borderline classification.',
24
24
  },
25
+ {
26
+ name: 'trunk-kinds',
27
+ uri: 'umtri://rules/trunk-kinds',
28
+ file: 'trunk-kinds.md',
29
+ title: 'Trunk kinds — how each kind of trunk branches',
30
+ description: 'Five trunk kinds (front, server, database, library, device) recorded in metadata.kind, the limb axis for each, one common limb per trunk (metadata.common), the same domain name across trunks with no suffix, and how kinds connect. Read before creating a trunk or a limb directly under one, and when restructuring.',
31
+ },
25
32
  {
26
33
  name: 'seasons',
27
34
  uri: 'umtri://rules/seasons',
package/core/tools.js CHANGED
@@ -592,6 +592,8 @@ export function registerTools(server, { api }) {
592
592
  },
593
593
  );
594
594
 
595
+ // visibility 인자는 두지 않는다. 배지를 밖에 보이는 경로가 없어 받아도 버려졌고(#70), 트리는
596
+ // 경로·연동·버그까지 담은 지도라 공개 여부는 에이전트가 아니라 사람이 정할 일이다.
595
597
  server.registerTool(
596
598
  'create_project',
597
599
  {
@@ -601,17 +603,15 @@ export function registerTools(server, { api }) {
601
603
  slug: z.string().regex(/^[a-z0-9][a-z0-9-]{0,49}$/).describe('URL slug. Lowercase letters, digits, hyphens; 1–50 chars.'),
602
604
  name: z.string().min(1).max(200).describe('Display name.'),
603
605
  description: z.string().optional(),
604
- visibility: z.enum(['private', 'unlisted', 'public']).optional().describe('Defaults to private.'),
605
606
  org: z.string().min(1).optional().describe('Team slug to create the ground in. Omit for your personal space. You must own the team.'),
606
607
  }),
607
608
  },
608
- async ({ slug, name, description, visibility, org }) => {
609
+ async ({ slug, name, description, org }) => {
609
610
  try {
610
611
  const markupWarnings = toolMarkupCheck({ description });
611
612
  const body = { slug, name };
612
613
  if (org !== undefined) body.org = org;
613
614
  if (description !== undefined) body.description = description;
614
- if (visibility !== undefined) body.visibility = visibility;
615
615
  const created = await api.post('/api/projects', body);
616
616
  return jsonResult(markupWarnings.length ? { ...created, warnings: markupWarnings } : created);
617
617
  } catch (e) { return errorResult(e); }
@@ -666,6 +666,7 @@ export function registerTools(server, { api }) {
666
666
  'parent ∈ existing node id; omit to create a root-level node.',
667
667
  'season ∈ existing season id; omit to use the active "now" season. Past seasons are normally rejected, but allowed while the ground is transplanting (project.transplanting=true) — nodes added then are auto-stamped metadata.transplanted=true for audit. See umtri://rules/transplant.',
668
668
  'The tool validates against protocol policies. Hierarchy violations are rejected. Soft issues (reserved-domain labels, leaf↔vein heuristic, trunk naming) come back as warnings in the response — reconsider before continuing if warnings appear.',
669
+ 'A trunk carries metadata.kind ∈ front · server · database · library · device, and its limbs follow that kind\'s axis: same domain name across trunks with no suffix ("예약", not "예약 API"), no technical-layer limbs, and at most one common limb (metadata.common=true). Read umtri://rules/trunk-kinds before creating a trunk or a limb under one.',
669
670
  'After creating a leaf/vein, consider its connections: if it calls/feeds another node add an api (create_api), if it depends on/is built on another add an edge (create_edge). The response carries a connectionCheck reminder. See umtri://rules/system-structure (Connections).',
670
671
  'Do not open a new season just because none fits — attach to the now-season, or ask the user. create_season needs their confirmation (umtri://rules/seasons).',
671
672
  'When realizing a human-drawn plan brief, any detail nodes you add should carry metadata.plan=true and the realized node needs metadata.implements — see umtri://rules/plan. A trunk is never plan: metadata.plan on a trunk is rejected.',
@@ -685,19 +686,25 @@ export function registerTools(server, { api }) {
685
686
  async ({ slug, type, label, parent, season, description, tags, metadata, sproutedAt }) => {
686
687
  try {
687
688
  let parentType = null;
689
+ let parentKind = null;
690
+ let siblingCommonCount = 0;
688
691
  let siblingLeafLabels;
689
692
  if (parent) {
690
693
  const graph = await api.get(`/api/projects/${encodeURIComponent(slug)}/graph`);
691
694
  const parentNode = (graph.nodes || []).find(n => n.id === parent);
692
695
  if (!parentNode) throw new Error(`Parent node "${parent}" not found in ground "${slug}".`);
693
696
  parentType = parentNode.type;
697
+ // trunk 유형별 limb 규칙(umtri://rules/trunk-kinds)에 쓰는 부모 kind와 공통 limb 수.
698
+ parentKind = parentNode.metadata?.kind ?? null;
699
+ siblingCommonCount = (graph.nodes || [])
700
+ .filter(n => n.parent === parent && n.type === 'limb' && n.metadata?.common === true).length;
694
701
  siblingLeafLabels = (graph.nodes || [])
695
702
  .filter(n => n.parent === parent && n.type === 'leaf')
696
703
  .map(n => n.label);
697
704
  }
698
705
 
699
706
  const { ok, rejectReason, warnings } = validateNode({
700
- type, label, parentType, description, metadata, siblingLeafLabels,
707
+ type, label, parentType, parentKind, siblingCommonCount, description, metadata, siblingLeafLabels,
701
708
  });
702
709
  if (!ok) throw new Error(`Rejected by protocol: ${rejectReason}`);
703
710
 
@@ -749,7 +756,9 @@ export function registerTools(server, { api }) {
749
756
  warnings = warnings.concat(patchResult.warnings);
750
757
  warnings = warnings.concat(toolMarkupCheck({ description: patch.description }));
751
758
 
752
- if (patch.type !== undefined || patch.parent !== undefined || patch.label !== undefined) {
759
+ // metadata.kind·common이 바뀌면 trunk 유형 경고도 다시 본다.
760
+ const kindTouched = patch.metadata && ('kind' in patch.metadata || 'common' in patch.metadata);
761
+ if (patch.type !== undefined || patch.parent !== undefined || patch.label !== undefined || kindTouched) {
753
762
  const graph = await api.get(`/api/projects/${encodeURIComponent(slug)}/graph`);
754
763
  const current = (graph.nodes || []).find(n => n.id === id);
755
764
  if (!current) throw new Error(`Node "${id}" not found in ground "${slug}".`);
@@ -761,14 +770,20 @@ export function registerTools(server, { api }) {
761
770
  const newMetadata = patch.metadata !== undefined
762
771
  ? Object.fromEntries(Object.entries({ ...(current.metadata || {}), ...patch.metadata }).filter(([, v]) => v !== null))
763
772
  : current.metadata;
764
- const parentType = newParentId
765
- ? (graph.nodes || []).find(n => n.id === newParentId)?.type ?? null
766
- : null;
773
+ const parentNode = newParentId ? (graph.nodes || []).find(n => n.id === newParentId) : null;
774
+ const parentType = parentNode?.type ?? null;
775
+ const parentKind = parentNode?.metadata?.kind ?? null;
776
+ const siblingCommonCount = newParentId
777
+ ? (graph.nodes || []).filter(n => n.parent === newParentId && n.id !== id
778
+ && n.type === 'limb' && n.metadata?.common === true).length
779
+ : 0;
767
780
 
768
781
  const result = validateNode({
769
782
  type: newType,
770
783
  label: newLabel,
771
784
  parentType,
785
+ parentKind,
786
+ siblingCommonCount,
772
787
  description: newDescription,
773
788
  metadata: newMetadata,
774
789
  phase: 'update',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umtri-mcp",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "MCP server for Umtri — give your AI coding agent a persistent map of your project's structure, dependencies, and bugs.",
5
5
  "mcpName": "io.umtri/umtri",
6
6
  "keywords": [