@nestr/mcp 0.1.47 → 0.1.49

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.
@@ -27,7 +27,7 @@ function completableResponse(data, source, title) {
27
27
  // Fields to keep for compact list responses (reduces token usage)
28
28
  const COMPACT_FIELDS = {
29
29
  // Common fields for all nests (includes fields needed by the completable list app)
30
- base: ["_id", "title", "purpose", "completed", "labels", "path", "parentId", "ancestors", "description", "due"],
30
+ base: ["_id", "title", "purpose", "completed", "labels", "path", "parentId", "ancestors", "description", "due", "hints"],
31
31
  // Additional fields for roles
32
32
  role: ["accountabilities", "domains"],
33
33
  // Additional fields for users
@@ -65,6 +65,85 @@ function compactResponse(data, type = "nest") {
65
65
  return compact;
66
66
  });
67
67
  }
68
+ // URL-to-tool mapping for hint enrichment.
69
+ // The Nestr API returns hints with relative URLs (e.g., "/nests/abc123/children?search=...").
70
+ // This maps those URL patterns to MCP tool calls so models can act on hints directly.
71
+ // Note: patterns are tried in order — more specific patterns must come before catch-alls.
72
+ const HINT_URL_PATTERNS = [
73
+ // /nests/{id}/children?search=... → nestr_search with in:{id} scoped query
74
+ {
75
+ pattern: /^\/nests\/([^/]+)\/children$/,
76
+ tool: "nestr_search",
77
+ params: (m, sp, workspaceId) => {
78
+ const search = sp.get("search") || "";
79
+ const result = { query: `in:${m[1]} ${search}`.trim() };
80
+ if (workspaceId)
81
+ result.workspaceId = workspaceId;
82
+ return result;
83
+ },
84
+ },
85
+ // /nests/{id}/posts → nestr_get_comments
86
+ { pattern: /^\/nests\/([^/]+)\/posts$/, tool: "nestr_get_comments", params: (m) => ({ nestId: m[1] }) },
87
+ // /nests/{id}/tensions → nestr_list_tensions
88
+ { pattern: /^\/nests\/([^/]+)\/tensions$/, tool: "nestr_list_tensions", params: (m) => ({ nestId: m[1] }) },
89
+ // /nests/{id} → nestr_get_nest (must be last — catches all /nests/{id} patterns)
90
+ { pattern: /^\/nests\/([^/]+)$/, tool: "nestr_get_nest", params: (m) => ({ nestId: m[1] }) },
91
+ ];
92
+ // Enrich hints with tool call parameters so models can act on hints directly.
93
+ // Extracts workspaceId from nest ancestors (last element) for search-based hints.
94
+ function enrichHints(data) {
95
+ if (!data || typeof data !== "object")
96
+ return data;
97
+ // Handle arrays (e.g., from getNestChildren)
98
+ if (Array.isArray(data)) {
99
+ return data.map((item) => enrichHints(item));
100
+ }
101
+ // Handle wrapped responses { data: [...] }
102
+ if ("data" in data && Array.isArray(data.data)) {
103
+ return { ...data, data: enrichHints(data.data) };
104
+ }
105
+ // Enrich hints on this nest
106
+ const record = data;
107
+ if (Array.isArray(record.hints)) {
108
+ // Extract workspaceId from ancestors (last element is always the workspace)
109
+ const ancestors = record.ancestors;
110
+ const workspaceId = ancestors?.length ? ancestors[ancestors.length - 1] : undefined;
111
+ record.hints = record.hints.map((hint) => {
112
+ if (!hint.url)
113
+ return hint;
114
+ // Parse URL and query params — strip absolute URL prefix if present
115
+ let rawUrl = hint.url;
116
+ const apiPrefixMatch = rawUrl.match(/^https?:\/\/[^/]+\/api(\/.*)/);
117
+ if (apiPrefixMatch)
118
+ rawUrl = apiPrefixMatch[1];
119
+ const [path, queryString] = rawUrl.split("?");
120
+ const searchParams = new URLSearchParams(queryString || "");
121
+ for (const { pattern, tool, params } of HINT_URL_PATTERNS) {
122
+ const match = path.match(pattern);
123
+ if (match) {
124
+ return { ...hint, toolCall: { tool, params: params(match, searchParams, workspaceId) } };
125
+ }
126
+ }
127
+ // Log unrecognized hint URLs so we can add mappings when the API adds new patterns
128
+ console.error(`[nestr-mcp] Unrecognized hint URL pattern: "${hint.url}" (hint type: ${hint.type})`);
129
+ return hint;
130
+ });
131
+ }
132
+ return data;
133
+ }
134
+ // Coerce JSON-stringified arrays/objects before Zod validation.
135
+ // Some MCP clients send array/object params as JSON strings (e.g., "[\"project\"]" instead of ["project"]).
136
+ const coerceFromJson = (schema) => z.preprocess((val) => {
137
+ if (typeof val === 'string') {
138
+ try {
139
+ return JSON.parse(val);
140
+ }
141
+ catch {
142
+ return val;
143
+ }
144
+ }
145
+ return val;
146
+ }, schema);
68
147
  // Tool input schemas using Zod
69
148
  export const schemas = {
70
149
  listWorkspaces: z.object({
@@ -81,7 +160,7 @@ export const schemas = {
81
160
  type: z.enum(['personal', 'collaborative']).optional().describe("'personal' for individual use (free forever), 'collaborative' for team use (free trial, then paid). Defaults to 'collaborative'."),
82
161
  governance: z.enum(['holacracy', 'sociocracy', 'roles_circles']).optional().describe("Self-organization model. Defaults to 'roles_circles' (generic role-based)."),
83
162
  plan: z.enum(['starter', 'pro']).optional().describe("Subscription plan for collaborative workspaces. Defaults to 'pro' (17-day trial)."),
84
- apps: z.array(z.enum(['okr', 'feedback', 'insights'])).optional().describe("Apps to enable (e.g., ['okr', 'feedback']). 'insights' requires pro plan."),
163
+ apps: coerceFromJson(z.array(z.enum(['okr', 'feedback', 'insights']))).optional().describe("Apps to enable (e.g., ['okr', 'feedback']). 'insights' requires pro plan."),
85
164
  layout: z.enum(['board', 'list']).optional().describe("Layout style for personal workspaces. 'board' creates kanban columns (Todo, Doing, Done)."),
86
165
  }),
87
166
  search: z.object({
@@ -108,10 +187,10 @@ export const schemas = {
108
187
  title: z.string().describe("Title of the new nest (plain text, HTML stripped)"),
109
188
  purpose: z.string().optional().describe("Purpose — the aspirational future state this nest is working towards. Most important for workspaces, circles, and roles where it defines the north star and context boundary. For other nests, prefer description or fields for detailed information — but purpose can be set if meaningful. Supports HTML."),
110
189
  description: z.string().optional().describe("Detailed description — the primary field for storing information about a nest. Use for project details, task context, acceptance criteria, Definition of Done, etc. Supports HTML: <b>, <i>, <code>, <ul>, <li>, <a>."),
111
- labels: z.array(z.string()).optional().describe("Label IDs to apply"),
112
- users: z.array(z.string()).optional().describe("User IDs to assign (required for tasks/projects to associate with a person)"),
113
- accountabilities: z.array(z.string()).optional().describe("Accountability titles for roles/circles. Only used when labels include 'role' or 'circle'. Each string becomes an accountability child nest."),
114
- domains: z.array(z.string()).optional().describe("Domain titles for roles/circles. Only used when labels include 'role' or 'circle'. Each string becomes a domain child nest."),
190
+ labels: coerceFromJson(z.array(z.string())).optional().describe("Label IDs to apply"),
191
+ users: coerceFromJson(z.array(z.string())).optional().describe("User IDs to assign (required for tasks/projects to associate with a person)"),
192
+ accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Accountability titles for roles/circles. Only used when labels include 'role' or 'circle'. Each string becomes an accountability child nest."),
193
+ domains: coerceFromJson(z.array(z.string())).optional().describe("Domain titles for roles/circles. Only used when labels include 'role' or 'circle'. Each string becomes a domain child nest."),
115
194
  workspaceId: z.string().optional().describe("Workspace ID. Required when creating roles/circles with accountabilities or domains (used to route to the self-organization API)."),
116
195
  }),
117
196
  updateNest: z.object({
@@ -120,14 +199,14 @@ export const schemas = {
120
199
  purpose: z.string().optional().describe("New purpose — the aspirational future state. Most important for workspaces, circles, and roles. For other nests, prefer description or fields — but purpose can be set if meaningful. Supports HTML."),
121
200
  description: z.string().optional().describe("New description — the primary field for detailed information. Use for project details, task context, acceptance criteria, etc. Supports HTML."),
122
201
  parentId: z.string().optional().describe("New parent ID (move nest to different location, e.g., move inbox item to a role or project)"),
123
- labels: z.array(z.string()).optional().describe("Label IDs to set (e.g., ['project'] to convert an item into a project)"),
124
- fields: z.record(z.unknown()).optional().describe("Field updates (e.g., { 'project.status': 'Current' })"),
125
- users: z.array(z.string()).optional().describe("User IDs to assign"),
126
- data: z.record(z.unknown()).optional().describe("Key-value data store shared with Nestr internals — never overwrite existing keys. Namespace your own data under 'mcp.' (e.g., { 'mcp.lastSync': '...' }). For AI knowledge persistence, use skills instead."),
202
+ labels: coerceFromJson(z.array(z.string())).optional().describe("Label IDs to set (e.g., ['project'] to convert an item into a project)"),
203
+ fields: coerceFromJson(z.record(z.unknown())).optional().describe("Field updates (e.g., { 'project.status': 'Current' })"),
204
+ users: coerceFromJson(z.array(z.string())).optional().describe("User IDs to assign"),
205
+ data: coerceFromJson(z.record(z.unknown())).optional().describe("Key-value data store shared with Nestr internals — never overwrite existing keys. Namespace your own data under 'mcp.' (e.g., { 'mcp.lastSync': '...' }). For AI knowledge persistence, use skills instead."),
127
206
  due: z.string().optional().describe("Due date (ISO format). For projects/tasks: deadline. For roles: re-election date. For meetings: start time."),
128
207
  completed: z.boolean().optional().describe("Mark task as completed (root-level field, not in fields). Note: Projects use fields['project.status'] = 'Done' instead."),
129
- accountabilities: z.array(z.string()).optional().describe("Accountability titles for roles/circles (replaces existing). Only used when updating a role or circle. Requires workspaceId."),
130
- domains: z.array(z.string()).optional().describe("Domain titles for roles/circles (replaces existing). Only used when updating a role or circle. Requires workspaceId."),
208
+ accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Accountability titles for roles/circles (replaces existing). Only used when updating a role or circle. Requires workspaceId."),
209
+ domains: coerceFromJson(z.array(z.string())).optional().describe("Domain titles for roles/circles (replaces existing). Only used when updating a role or circle. Requires workspaceId."),
131
210
  workspaceId: z.string().optional().describe("Workspace ID. Required when updating accountabilities or domains on roles/circles."),
132
211
  }),
133
212
  deleteNest: z.object({
@@ -135,11 +214,11 @@ export const schemas = {
135
214
  }),
136
215
  addComment: z.object({
137
216
  nestId: z.string().describe("Nest ID to comment on"),
138
- body: z.string().describe("Comment text (supports HTML and @mentions)"),
217
+ body: z.string().describe("Comment text (supports HTML and @mentions: @{userId}, @{email}, @{circle})"),
139
218
  }),
140
219
  updateComment: z.object({
141
220
  commentId: z.string().describe("Comment ID to update"),
142
- body: z.string().describe("Updated comment text (supports HTML and @mentions)"),
221
+ body: z.string().describe("Updated comment text (supports HTML and @mentions: @{userId}, @{email}, @{circle})"),
143
222
  }),
144
223
  deleteComment: z.object({
145
224
  commentId: z.string().describe("Comment ID to delete"),
@@ -216,7 +295,7 @@ export const schemas = {
216
295
  }),
217
296
  // Inbox tools (require OAuth token)
218
297
  listInbox: z.object({
219
- completedAfter: z.string().optional().describe("Include completed items from this date (ISO format). If omitted, only non-completed items are returned."),
298
+ completedAfter: z.string().optional().describe("Include completed items from this date (ISO format). If omitted, only non-completed items are returned. For reordering, this default is usually sufficient — nestr_reorder_inbox only requires the IDs of items you want to reposition."),
220
299
  }),
221
300
  createInboxItem: z.object({
222
301
  title: z.string().describe("Title of the inbox item (plain text, HTML stripped)"),
@@ -230,10 +309,10 @@ export const schemas = {
230
309
  title: z.string().optional().describe("Updated title (plain text, HTML stripped)"),
231
310
  description: z.string().optional().describe("Updated description (supports HTML)"),
232
311
  completed: z.boolean().optional().describe("Mark as completed (processed)"),
233
- data: z.record(z.unknown()).optional().describe("Custom data storage"),
312
+ data: coerceFromJson(z.record(z.unknown())).optional().describe("Custom data storage"),
234
313
  }),
235
314
  reorderInbox: z.object({
236
- nestIds: z.array(z.string()).describe("Array of inbox item IDs in the desired order"),
315
+ nestIds: coerceFromJson(z.array(z.string())).describe("Array of inbox item IDs in the desired order"),
237
316
  }),
238
317
  reorderInboxItem: z.object({
239
318
  nestId: z.string().describe("ID of the inbox item to reorder"),
@@ -250,10 +329,10 @@ export const schemas = {
250
329
  labelId: z.string().describe("Label ID to remove"),
251
330
  }),
252
331
  addToDailyPlan: z.object({
253
- nestIds: z.array(z.string()).describe("Array of nest IDs to add to the daily plan"),
332
+ nestIds: coerceFromJson(z.array(z.string())).describe("Array of nest IDs to add to the daily plan"),
254
333
  }),
255
334
  removeFromDailyPlan: z.object({
256
- nestIds: z.array(z.string()).describe("Array of nest IDs to remove from the daily plan"),
335
+ nestIds: coerceFromJson(z.array(z.string())).describe("Array of nest IDs to remove from the daily plan"),
257
336
  }),
258
337
  // Personal labels (require OAuth token)
259
338
  listPersonalLabels: z.object({}),
@@ -271,7 +350,7 @@ export const schemas = {
271
350
  }),
272
351
  bulkReorder: z.object({
273
352
  workspaceId: z.string().describe("Workspace ID"),
274
- nestIds: z.array(z.string()).describe("Array of nest IDs in the desired order"),
353
+ nestIds: coerceFromJson(z.array(z.string())).describe("Array of nest IDs in the desired order"),
275
354
  }),
276
355
  // Daily plan (requires OAuth token)
277
356
  getDailyPlan: z.object({}),
@@ -334,14 +413,14 @@ export const schemas = {
334
413
  tensionId: z.string().describe("Tension ID"),
335
414
  _id: z.string().optional().describe("ID of an existing governance item to change or remove. Omit to propose a new item."),
336
415
  title: z.string().optional().describe("Title for the governance item"),
337
- labels: z.array(z.string()).optional().describe("Labels defining the item type (e.g., ['role'], ['circle'], ['policy'], ['accountability'], ['domain'])"),
416
+ labels: coerceFromJson(z.array(z.string())).optional().describe("Labels defining the item type (e.g., ['role'], ['circle'], ['policy'], ['accountability'], ['domain'])"),
338
417
  purpose: z.string().optional().describe("Purpose — aspirational future state. Most important for roles/circles where it defines the north star. Supports HTML."),
339
418
  description: z.string().optional().describe("Description — detailed information about the item. Supports HTML."),
340
419
  parentId: z.string().optional().describe("Parent ID — use to move/restructure items (e.g., move role to different circle)"),
341
- users: z.array(z.string()).optional().describe("User IDs to assign (e.g., for role elections: assign the elected user to the role)"),
420
+ users: coerceFromJson(z.array(z.string())).optional().describe("User IDs to assign (e.g., for role elections: assign the elected user to the role)"),
342
421
  due: z.string().optional().describe("Due date / re-election date (ISO format)"),
343
- accountabilities: z.array(z.string()).optional().describe("Accountability titles to set on a role (replaces all — use children endpoint for individual management)"),
344
- domains: z.array(z.string()).optional().describe("Domain titles to set on a role (replaces all — use children endpoint for individual management)"),
422
+ accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Accountability titles to set on a role (replaces all — use children endpoint for individual management)"),
423
+ domains: coerceFromJson(z.array(z.string())).optional().describe("Domain titles to set on a role (replaces all — use children endpoint for individual management)"),
345
424
  }),
346
425
  modifyTensionPart: z.object({
347
426
  nestId: z.string().describe("ID of the circle or role the tension belongs to"),
@@ -350,12 +429,12 @@ export const schemas = {
350
429
  title: z.string().optional().describe("Updated title"),
351
430
  purpose: z.string().optional().describe("Updated purpose — aspirational future state. Most important for roles/circles. Supports HTML."),
352
431
  description: z.string().optional().describe("Updated description — detailed information. Supports HTML."),
353
- labels: z.array(z.string()).optional().describe("Updated labels"),
432
+ labels: coerceFromJson(z.array(z.string())).optional().describe("Updated labels"),
354
433
  parentId: z.string().optional().describe("Updated parent ID"),
355
- users: z.array(z.string()).optional().describe("Updated user assignments"),
434
+ users: coerceFromJson(z.array(z.string())).optional().describe("Updated user assignments"),
356
435
  due: z.string().optional().describe("Updated due date (ISO format)"),
357
- accountabilities: z.array(z.string()).optional().describe("Updated accountabilities (replaces all — use children endpoint for individual management)"),
358
- domains: z.array(z.string()).optional().describe("Updated domains (replaces all — use children endpoint for individual management)"),
436
+ accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Updated accountabilities (replaces all — use children endpoint for individual management)"),
437
+ domains: coerceFromJson(z.array(z.string())).optional().describe("Updated domains (replaces all — use children endpoint for individual management)"),
359
438
  }),
360
439
  removeTensionPart: z.object({
361
440
  nestId: z.string().describe("ID of the circle or role the tension belongs to"),
@@ -372,7 +451,7 @@ export const schemas = {
372
451
  tensionId: z.string().describe("Tension ID"),
373
452
  partId: z.string().describe("Part ID"),
374
453
  title: z.string().describe("Title for the new accountability or domain"),
375
- labels: z.array(z.string()).describe("Labels defining the type: ['accountability'] or ['domain']"),
454
+ labels: coerceFromJson(z.array(z.string())).describe("Labels defining the type: ['accountability'] or ['domain']"),
376
455
  }),
377
456
  updateTensionPartChild: z.object({
378
457
  nestId: z.string().describe("ID of the circle or role the tension belongs to"),
@@ -493,7 +572,27 @@ export const toolDefinitions = [
493
572
  },
494
573
  {
495
574
  name: "nestr_search",
496
- description: "Search for nests within a workspace. Supports operators: label:, parent-label:, assignee: (me/userId/!userId/none), admin:, createdby:, completed:, type:, has: (due/pastdue/children/incompletechildren), depth:, mindepth:, in:, updated-date:, limit:, template:, data.property:, fields.{label}.{property}: to search any value in a nest's fields object (supports partial match, e.g., fields.project.status:Current — use nestr_get_nest with fieldsMetaData=true to discover available fields), label->field:value. Use ! prefix for negation. IMPORTANT: Use completed:false when searching for work to exclude old completed items. Response includes meta.total showing total matching count. IMPORTANT UI RULE: The completable list app must ONLY be used when results contain completable items (tasks, projects, todos) AND there are results to show. When searching for roles, circles, metrics, policies, or any non-completable type, you MUST omit the _listTitle parameter and respond in plain text instead — never render these in the app. Also never render empty results in the app.",
575
+ description: `Search for nests within a workspace. IMPORTANT: Use completed:false when searching for work to exclude old completed items.
576
+
577
+ Operators (combine with spaces for AND; commas within operator for OR; ! prefix for negation):
578
+ - label:role / label:!project — Filter/exclude by label
579
+ - parent-label:circle — Parent has this label
580
+ - assignee:me / assignee:userId / assignee:none / assignee:!userId — Filter by assignee
581
+ - completed:false / completed:true / completed:past_7_days / completed:this_month / completed:YYYY-MM-DD_YYYY-MM-DD
582
+ - in:nestId — Scope to descendants of a specific nest
583
+ - depth:1 / depth:2 — Limit depth (1=direct children, 2=children+grandchildren)
584
+ - mindepth:N — Minimum depth from context
585
+ - has:due / has:pastdue / has:children / has:incompletechildren (supports ! prefix)
586
+ - project->status:Current,Future / project->status:!Done — Field value search (label->field:value)
587
+ - fields.label.property:value — Search any field value (supports partial match)
588
+ - data.property:value — Search data properties
589
+ - updated-date:past_7_days / updated-date:!past_30_days — Filter by update recency
590
+ - sort:title / sort:due / sort:updatedAt / sort:createdAt + sort-order:asc/desc
591
+ - createdby:me / admin:me / type:comment / limit:N / template:id
592
+
593
+ Examples: label:role → all roles | assignee:me completed:false → my active work | in:circleId label:role depth:1 → roles in circle | label:project project->status:Current → active projects | in:roleId label:project → role's projects | has:pastdue completed:false → overdue items | label:accountability customer → accountabilities matching keyword
594
+
595
+ Response includes meta.total showing total matching count. IMPORTANT UI RULE: The completable list app must ONLY be used when results are confirmed to contain completable items (tasks, projects, todos) AND there are results to show. When searching for roles, circles, metrics, policies, or any non-completable type, you MUST omit the _listTitle parameter and respond in plain text instead. Never render empty results in the app.`,
497
596
  inputSchema: {
498
597
  type: "object",
499
598
  properties: {
@@ -506,7 +605,8 @@ export const toolDefinitions = [
506
605
  },
507
606
  required: ["workspaceId", "query"],
508
607
  },
509
- _meta: completableListUi,
608
+ // No _meta: completableListUi — search returns all types of nests (roles, circles, etc.).
609
+ // The completable list app should only be used when results are confirmed to be completable items.
510
610
  ...readOnly,
511
611
  },
512
612
  {
@@ -539,12 +639,13 @@ export const toolDefinitions = [
539
639
  },
540
640
  required: ["nestId"],
541
641
  },
542
- _meta: completableListUi,
642
+ // No _meta: completableListUi — children can be any type (roles, accountabilities, etc.).
643
+ // The completable list app should only be used when results are confirmed to be completable items.
543
644
  ...readOnly,
544
645
  },
545
646
  {
546
647
  name: "nestr_create_nest",
547
- description: "Create a new nest (task, project, role, circle, etc.) under a parent. Set users to assign to people - placing under a role does NOT auto-assign. For roles and circles: include accountabilities and domains arrays to create them inline (requires workspaceId). The API auto-routes to the self-organization endpoint when governance labels are detected with accountabilities/domains.",
648
+ description: "Create a new nest (task, project, role, circle, etc.) under a parent. Set users to assign to people - placing under a role does NOT auto-assign. For roles and circles: include accountabilities and domains arrays to create them inline (requires workspaceId). The API auto-routes to the self-organization endpoint when governance labels are detected with accountabilities/domains. GOVERNANCE RULE: Only create roles, circles, accountabilities, domains, or policies directly during workspace/circle setup mode (new or sparsely populated workspace). In established workspaces with multiple users, governance changes MUST go through the tension/proposal flow (nestr_create_tension + nestr_add_tension_part) so circle members can consent.",
548
649
  inputSchema: {
549
650
  type: "object",
550
651
  properties: {
@@ -583,7 +684,7 @@ export const toolDefinitions = [
583
684
  },
584
685
  {
585
686
  name: "nestr_update_nest",
586
- description: "Update properties of an existing nest. Use parentId to move a nest (e.g., inbox item to a project). For roles and circles: include accountabilities and domains arrays to update them inline (requires workspaceId). For AI knowledge persistence, create skill-labeled nests under roles/circles instead of using data fields.",
687
+ description: "Update properties of an existing nest. Use parentId to move a nest (e.g., inbox item to a project). For roles and circles: include accountabilities and domains arrays to update them inline (requires workspaceId). For AI knowledge persistence, create skill-labeled nests under roles/circles instead of using data fields. GOVERNANCE RULE: Only modify governance items (roles, circles, accountabilities, domains, policies) directly during setup mode. In established workspaces, governance changes MUST go through tensions (nestr_create_tension + nestr_add_tension_part).",
587
688
  inputSchema: {
588
689
  type: "object",
589
690
  properties: {
@@ -639,7 +740,7 @@ export const toolDefinitions = [
639
740
  },
640
741
  {
641
742
  name: "nestr_delete_nest",
642
- description: "Delete a nest (use with caution)",
743
+ description: "Delete a nest (use with caution). GOVERNANCE RULE: To remove governance items (roles, accountabilities, domains, policies) in established workspaces, use nestr_remove_tension_part to propose deletion through the consent process instead.",
643
744
  inputSchema: {
644
745
  type: "object",
645
746
  properties: {
@@ -651,12 +752,12 @@ export const toolDefinitions = [
651
752
  },
652
753
  {
653
754
  name: "nestr_add_comment",
654
- description: "Add a comment to a nest. Use @username to mention someone.",
755
+ description: "Add a comment to a nest. Supports @mentions using the format @{userId|email|circle|everyone}: @{userId} mentions by user ID, @{email} mentions by any email the user is registered with in Nestr, @{circle} notifies all role fillers in the nearest ancestor circle, @{everyone} is available in the UI but not yet via the API.",
655
756
  inputSchema: {
656
757
  type: "object",
657
758
  properties: {
658
759
  nestId: { type: "string", description: "Nest ID to comment on" },
659
- body: { type: "string", description: "Comment text (supports HTML and @mentions)" },
760
+ body: { type: "string", description: "Comment text (supports HTML and @mentions: @{userId}, @{email}, @{circle})" },
660
761
  },
661
762
  required: ["nestId", "body"],
662
763
  },
@@ -664,12 +765,12 @@ export const toolDefinitions = [
664
765
  },
665
766
  {
666
767
  name: "nestr_update_comment",
667
- description: "Update an existing comment's text.",
768
+ description: "Update an existing comment's text. Supports @mentions using the format @{userId|email|circle|everyone}: @{userId} mentions by user ID, @{email} mentions by any email the user is registered with in Nestr, @{circle} notifies all role fillers in the nearest ancestor circle, @{everyone} is available in the UI but not yet via the API.",
668
769
  inputSchema: {
669
770
  type: "object",
670
771
  properties: {
671
772
  commentId: { type: "string", description: "Comment ID to update" },
672
- body: { type: "string", description: "Updated comment text (supports HTML and @mentions)" },
773
+ body: { type: "string", description: "Updated comment text (supports HTML and @mentions: @{userId}, @{email}, @{circle})" },
673
774
  },
674
775
  required: ["commentId", "body"],
675
776
  },
@@ -901,7 +1002,7 @@ export const toolDefinitions = [
901
1002
  inputSchema: {
902
1003
  type: "object",
903
1004
  properties: {
904
- completedAfter: { type: "string", description: "Include completed items from this date (ISO format). If omitted, only non-completed items are returned." },
1005
+ completedAfter: { type: "string", description: "Include completed items from this date (ISO format). If omitted, only non-completed items are returned. For reordering, this default is usually sufficient — nestr_reorder_inbox only requires the IDs of items you want to reposition." },
905
1006
  stripDescription: { type: "boolean", description: "Set true to strip description fields from response, significantly reducing size." },
906
1007
  },
907
1008
  },
@@ -952,7 +1053,7 @@ export const toolDefinitions = [
952
1053
  },
953
1054
  {
954
1055
  name: "nestr_reorder_inbox",
955
- description: "Reorder inbox items by providing an array of item IDs in the desired order. You can provide a subset of items - they will be placed at the top in the given order, with remaining items unchanged below. Requires OAuth token.",
1056
+ description: "Reorder inbox items by providing an array of item IDs in the desired order. You can provide a subset of items they will be placed at the top in the given order, while all other items retain their existing order below them. To move non-completed items to the top, simply pass their IDs — there is no need to fetch completed items. Requires OAuth token.",
956
1057
  inputSchema: {
957
1058
  type: "object",
958
1059
  properties: {
@@ -1548,7 +1649,7 @@ async function _handleToolCall(client, name, args) {
1548
1649
  fieldsMetaData: parsed.fieldsMetaData,
1549
1650
  hints: parsed.hints !== false,
1550
1651
  });
1551
- return formatResult(nest);
1652
+ return formatResult(enrichHints(nest));
1552
1653
  }
1553
1654
  case "nestr_get_nest_children": {
1554
1655
  const parsed = schemas.getNestChildren.parse(args);
@@ -1558,7 +1659,7 @@ async function _handleToolCall(client, name, args) {
1558
1659
  cleanText: true,
1559
1660
  hints: parsed.hints !== false,
1560
1661
  });
1561
- return formatResult(completableResponse(compactResponse(children), "children", parsed._listTitle || "Sub-items"));
1662
+ return formatResult(completableResponse(compactResponse(enrichHints(children)), "children", parsed._listTitle || "Sub-items"));
1562
1663
  }
1563
1664
  case "nestr_create_nest": {
1564
1665
  const parsed = schemas.createNest.parse(args);