umtri-mcp 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/core/tools.js +25 -7
  2. package/package.json +1 -1
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); }
@@ -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 ? patch.metadata : current.metadata;
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
- const updated = await api.put(`/api/projects/${encodeURIComponent(slug)}/nodes/${encodeURIComponent(id)}`, patch);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umtri-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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": [