umtri-mcp 1.4.0 → 1.4.1
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/core/tools.js +22 -4
- package/package.json +1 -1
package/core/tools.js
CHANGED
|
@@ -725,7 +725,7 @@ export function registerTools(server, { api }) {
|
|
|
725
725
|
'update_node',
|
|
726
726
|
{
|
|
727
727
|
title: 'Update a node',
|
|
728
|
-
description: 'Partial update of a node. Patchable fields: label, type, parent, season, description, tags, metadata, sproutedAt. Type change reclassifies role; parent change recomputes the ltree path automatically. On a grown (past-season) node, only the tree\'s shape and timeline are locked — parent, season, type and sproutedAt are rejected. Content fields (label, description, metadata, tags) stay editable, so you can keep metadata.implements current when code moves without reopening transplanting. Moving any node into a `past` season is rejected. Both restrictions lift while the ground is transplanting (project.transplanting=true), so historical structure can be reconstructed (see umtri://rules/transplant). Same protocol validation as create_node — reject on hierarchy violations, warn on soft issues.',
|
|
728
|
+
description: 'Partial update of a node. Patchable fields: label, type, parent, season, description, tags, metadata, sproutedAt. metadata is merged key by key: only the keys you send change, keys you leave out (commits that record_commit accumulated, transplanted, and so on) are kept, and a key sent as null is removed. So to change implements, send just { metadata: { implements: [...] } }. Type change reclassifies role; parent change recomputes the ltree path automatically. On a grown (past-season) node, only the tree\'s shape and timeline are locked — parent, season, type and sproutedAt are rejected. Content fields (label, description, metadata, tags) stay editable, so you can keep metadata.implements current when code moves without reopening transplanting. Moving any node into a `past` season is rejected. Both restrictions lift while the ground is transplanting (project.transplanting=true), so historical structure can be reconstructed (see umtri://rules/transplant). Same protocol validation as create_node — reject on hierarchy violations, warn on soft issues.',
|
|
729
729
|
inputSchema: z.object({
|
|
730
730
|
slug: z.string().min(1).describe('Ground slug.'),
|
|
731
731
|
id: z.string().min(1).describe('Node id.'),
|
|
@@ -736,7 +736,7 @@ export function registerTools(server, { api }) {
|
|
|
736
736
|
season: z.string().nullable().optional().describe('New season id, or null to detach from any season. Past seasons are rejected.'),
|
|
737
737
|
description: z.string().nullable().optional(),
|
|
738
738
|
tags: z.array(z.string()).optional(),
|
|
739
|
-
metadata: z.record(z.any()).optional(),
|
|
739
|
+
metadata: z.record(z.any()).optional().describe('Keys to change — merged into the existing metadata. null removes a key. Keys you omit are kept.'),
|
|
740
740
|
sproutedAt: z.string().datetime({ offset: true }).nullable().optional().describe('Effective creation time (ISO 8601 with offset). Set to null to clear and fall back to the system timestamp. Use only for back-filling historical projects.'),
|
|
741
741
|
}).describe('Only the fields you want to change.'),
|
|
742
742
|
}),
|
|
@@ -758,7 +758,9 @@ export function registerTools(server, { api }) {
|
|
|
758
758
|
const newLabel = patch.label ?? current.label;
|
|
759
759
|
const newParentId = patch.parent !== undefined ? patch.parent : current.parent;
|
|
760
760
|
const newDescription = patch.description !== undefined ? patch.description : current.description;
|
|
761
|
-
const newMetadata = patch.metadata !== undefined
|
|
761
|
+
const newMetadata = patch.metadata !== undefined
|
|
762
|
+
? Object.fromEntries(Object.entries({ ...(current.metadata || {}), ...patch.metadata }).filter(([, v]) => v !== null))
|
|
763
|
+
: current.metadata;
|
|
762
764
|
const parentType = newParentId
|
|
763
765
|
? (graph.nodes || []).find(n => n.id === newParentId)?.type ?? null
|
|
764
766
|
: null;
|
|
@@ -775,7 +777,23 @@ export function registerTools(server, { api }) {
|
|
|
775
777
|
warnings = warnings.concat(result.warnings);
|
|
776
778
|
}
|
|
777
779
|
|
|
778
|
-
|
|
780
|
+
// metadata는 키 단위로 병합해서 보낸다. REST는 metadata를 통째로 교체하는데(앱의 plan 커밋이
|
|
781
|
+
// 그 동작에 기댄다), 에이전트가 implements만 보내면 record_commit이 쌓은 commits까지 조용히
|
|
782
|
+
// 지워졌다(건의 #2). 그래서 이 도구에서 현재 값을 읽어 합친 전체를 보낸다. null은 키 삭제.
|
|
783
|
+
let body = patch;
|
|
784
|
+
if (patch.metadata !== undefined && patch.metadata !== null) {
|
|
785
|
+
const g = await api.get(`/api/projects/${encodeURIComponent(slug)}/graph?root=${encodeURIComponent(id)}&depth=0`);
|
|
786
|
+
const current = (g.nodes || []).find(n => n.id === id);
|
|
787
|
+
if (!current) throw new Error(`Node "${id}" not found in ground "${slug}".`);
|
|
788
|
+
const merged = { ...(current.metadata || {}) };
|
|
789
|
+
for (const [k, v] of Object.entries(patch.metadata)) {
|
|
790
|
+
if (v === null) delete merged[k];
|
|
791
|
+
else merged[k] = v;
|
|
792
|
+
}
|
|
793
|
+
body = { ...patch, metadata: merged };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const updated = await api.put(`/api/projects/${encodeURIComponent(slug)}/nodes/${encodeURIComponent(id)}`, body);
|
|
779
797
|
return jsonResult(warnings.length ? { ...updated, warnings } : updated);
|
|
780
798
|
} catch (e) { return errorResult(e); }
|
|
781
799
|
},
|
package/package.json
CHANGED