@nestr/mcp 0.1.46 → 0.1.48

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,68 @@ 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
115
+ const [path, queryString] = hint.url.split("?");
116
+ const searchParams = new URLSearchParams(queryString || "");
117
+ for (const { pattern, tool, params } of HINT_URL_PATTERNS) {
118
+ const match = path.match(pattern);
119
+ if (match) {
120
+ return { ...hint, toolCall: { tool, params: params(match, searchParams, workspaceId) } };
121
+ }
122
+ }
123
+ // Log unrecognized hint URLs so we can add mappings when the API adds new patterns
124
+ console.error(`[nestr-mcp] Unrecognized hint URL pattern: "${hint.url}" (hint type: ${hint.type})`);
125
+ return hint;
126
+ });
127
+ }
128
+ return data;
129
+ }
68
130
  // Tool input schemas using Zod
69
131
  export const schemas = {
70
132
  listWorkspaces: z.object({
@@ -94,11 +156,13 @@ export const schemas = {
94
156
  getNest: z.object({
95
157
  nestId: z.string().describe("Nest ID. Supports comma-separated IDs to fetch multiple nests in one call (e.g., 'id1,id2,id3') — returns an array instead of a single object. Keep total URL under 2000 chars to avoid HTTP limits."),
96
158
  fieldsMetaData: z.boolean().optional().describe("Set to true to include field schema metadata (e.g., available options for project.status)"),
159
+ hints: z.boolean().optional().describe("Include contextual hints on each nest (default: true). Hints surface actionable signals like unassigned roles, stale projects, or unread comments. Set to false for bulk lookups where you only need structural data, not contextual guidance."),
97
160
  }),
98
161
  getNestChildren: z.object({
99
162
  nestId: z.string().describe("Parent nest ID"),
100
163
  limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
101
164
  page: z.number().optional().describe("Page number for pagination"),
165
+ hints: z.boolean().optional().describe("Include contextual hints on each child nest (default: true). Set to false for large result sets or bulk operations where contextual signals aren't needed."),
102
166
  _listTitle: z.string().optional().describe("Short descriptive title for the list UI (e.g., \"Tasks for Website Redesign\"). Omit for default."),
103
167
  }),
104
168
  createNest: z.object({
@@ -133,11 +197,11 @@ export const schemas = {
133
197
  }),
134
198
  addComment: z.object({
135
199
  nestId: z.string().describe("Nest ID to comment on"),
136
- body: z.string().describe("Comment text (supports HTML and @mentions)"),
200
+ body: z.string().describe("Comment text (supports HTML and @mentions: @{userId}, @{email}, @{circle})"),
137
201
  }),
138
202
  updateComment: z.object({
139
203
  commentId: z.string().describe("Comment ID to update"),
140
- body: z.string().describe("Updated comment text (supports HTML and @mentions)"),
204
+ body: z.string().describe("Updated comment text (supports HTML and @mentions: @{userId}, @{email}, @{circle})"),
141
205
  }),
142
206
  deleteComment: z.object({
143
207
  commentId: z.string().describe("Comment ID to delete"),
@@ -491,7 +555,27 @@ export const toolDefinitions = [
491
555
  },
492
556
  {
493
557
  name: "nestr_search",
494
- 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.",
558
+ description: `Search for nests within a workspace. IMPORTANT: Use completed:false when searching for work to exclude old completed items.
559
+
560
+ Operators (combine with spaces for AND; commas within operator for OR; ! prefix for negation):
561
+ - label:role / label:!project — Filter/exclude by label
562
+ - parent-label:circle — Parent has this label
563
+ - assignee:me / assignee:userId / assignee:none / assignee:!userId — Filter by assignee
564
+ - completed:false / completed:true / completed:past_7_days / completed:this_month / completed:YYYY-MM-DD_YYYY-MM-DD
565
+ - in:nestId — Scope to descendants of a specific nest
566
+ - depth:1 / depth:2 — Limit depth (1=direct children, 2=children+grandchildren)
567
+ - mindepth:N — Minimum depth from context
568
+ - has:due / has:pastdue / has:children / has:incompletechildren (supports ! prefix)
569
+ - project->status:Current,Future / project->status:!Done — Field value search (label->field:value)
570
+ - fields.label.property:value — Search any field value (supports partial match)
571
+ - data.property:value — Search data properties
572
+ - updated-date:past_7_days / updated-date:!past_30_days — Filter by update recency
573
+ - sort:title / sort:due / sort:updatedAt / sort:createdAt + sort-order:asc/desc
574
+ - createdby:me / admin:me / type:comment / limit:N / template:id
575
+
576
+ 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
577
+
578
+ 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.`,
495
579
  inputSchema: {
496
580
  type: "object",
497
581
  properties: {
@@ -504,7 +588,8 @@ export const toolDefinitions = [
504
588
  },
505
589
  required: ["workspaceId", "query"],
506
590
  },
507
- _meta: completableListUi,
591
+ // No _meta: completableListUi — search returns all types of nests (roles, circles, etc.).
592
+ // The completable list app should only be used when results are confirmed to be completable items.
508
593
  ...readOnly,
509
594
  },
510
595
  {
@@ -515,6 +600,7 @@ export const toolDefinitions = [
515
600
  properties: {
516
601
  nestId: { type: "string", description: "Nest ID, or comma-separated IDs to fetch multiple nests at once (e.g., 'id1,id2,id3'). Keep total URL under 2000 chars." },
517
602
  fieldsMetaData: { type: "boolean", description: "Set to true to include field schema metadata (available options, field types)" },
603
+ hints: { type: "boolean", description: "Include contextual hints (default: true). Set to false for bulk lookups where you only need structural data." },
518
604
  stripDescription: { type: "boolean", description: "Set true to strip description fields from response, significantly reducing size." },
519
605
  },
520
606
  required: ["nestId"],
@@ -530,17 +616,19 @@ export const toolDefinitions = [
530
616
  nestId: { type: "string", description: "Parent nest ID" },
531
617
  limit: { type: "number", description: "Omit on first call to see meta.total count" },
532
618
  page: { type: "number", description: "Page number (1-indexed)" },
619
+ hints: { type: "boolean", description: "Include contextual hints (default: true). Set to false for large result sets or bulk operations." },
533
620
  stripDescription: { type: "boolean", description: "Set true to strip description fields from response, significantly reducing size. Ideal for bulk/index operations." },
534
621
  _listTitle: { type: "string", description: "Short descriptive title for the list UI header (e.g., \"Tasks for Website Redesign\", \"API project sub-tasks\"). Include the parent name for context." },
535
622
  },
536
623
  required: ["nestId"],
537
624
  },
538
- _meta: completableListUi,
625
+ // No _meta: completableListUi — children can be any type (roles, accountabilities, etc.).
626
+ // The completable list app should only be used when results are confirmed to be completable items.
539
627
  ...readOnly,
540
628
  },
541
629
  {
542
630
  name: "nestr_create_nest",
543
- 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.",
631
+ 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.",
544
632
  inputSchema: {
545
633
  type: "object",
546
634
  properties: {
@@ -579,7 +667,7 @@ export const toolDefinitions = [
579
667
  },
580
668
  {
581
669
  name: "nestr_update_nest",
582
- 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.",
670
+ 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).",
583
671
  inputSchema: {
584
672
  type: "object",
585
673
  properties: {
@@ -635,7 +723,7 @@ export const toolDefinitions = [
635
723
  },
636
724
  {
637
725
  name: "nestr_delete_nest",
638
- description: "Delete a nest (use with caution)",
726
+ 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.",
639
727
  inputSchema: {
640
728
  type: "object",
641
729
  properties: {
@@ -647,12 +735,12 @@ export const toolDefinitions = [
647
735
  },
648
736
  {
649
737
  name: "nestr_add_comment",
650
- description: "Add a comment to a nest. Use @username to mention someone.",
738
+ 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.",
651
739
  inputSchema: {
652
740
  type: "object",
653
741
  properties: {
654
742
  nestId: { type: "string", description: "Nest ID to comment on" },
655
- body: { type: "string", description: "Comment text (supports HTML and @mentions)" },
743
+ body: { type: "string", description: "Comment text (supports HTML and @mentions: @{userId}, @{email}, @{circle})" },
656
744
  },
657
745
  required: ["nestId", "body"],
658
746
  },
@@ -660,12 +748,12 @@ export const toolDefinitions = [
660
748
  },
661
749
  {
662
750
  name: "nestr_update_comment",
663
- description: "Update an existing comment's text.",
751
+ 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.",
664
752
  inputSchema: {
665
753
  type: "object",
666
754
  properties: {
667
755
  commentId: { type: "string", description: "Comment ID to update" },
668
- body: { type: "string", description: "Updated comment text (supports HTML and @mentions)" },
756
+ body: { type: "string", description: "Updated comment text (supports HTML and @mentions: @{userId}, @{email}, @{circle})" },
669
757
  },
670
758
  required: ["commentId", "body"],
671
759
  },
@@ -1542,8 +1630,9 @@ async function _handleToolCall(client, name, args) {
1542
1630
  const nest = await client.getNest(parsed.nestId, {
1543
1631
  cleanText: true,
1544
1632
  fieldsMetaData: parsed.fieldsMetaData,
1633
+ hints: parsed.hints !== false,
1545
1634
  });
1546
- return formatResult(nest);
1635
+ return formatResult(enrichHints(nest));
1547
1636
  }
1548
1637
  case "nestr_get_nest_children": {
1549
1638
  const parsed = schemas.getNestChildren.parse(args);
@@ -1551,8 +1640,9 @@ async function _handleToolCall(client, name, args) {
1551
1640
  limit: parsed.limit,
1552
1641
  page: parsed.page,
1553
1642
  cleanText: true,
1643
+ hints: parsed.hints !== false,
1554
1644
  });
1555
- return formatResult(completableResponse(compactResponse(children), "children", parsed._listTitle || "Sub-items"));
1645
+ return formatResult(completableResponse(compactResponse(enrichHints(children)), "children", parsed._listTitle || "Sub-items"));
1556
1646
  }
1557
1647
  case "nestr_create_nest": {
1558
1648
  const parsed = schemas.createNest.parse(args);
@@ -1860,17 +1950,30 @@ async function _handleToolCall(client, name, args) {
1860
1950
  case "nestr_get_me": {
1861
1951
  const parsed = schemas.getMe.parse(args);
1862
1952
  try {
1863
- const user = await client.getCurrentUser({
1953
+ let user = await client.getCurrentUser({
1864
1954
  fullWorkspaces: parsed.fullWorkspaces,
1865
1955
  });
1866
- return formatResult({
1956
+ // Guard against oversized responses (e.g. many workspaces with large adminUsers arrays).
1957
+ // If the serialized user exceeds 50KB, retry without fullWorkspaces to avoid
1958
+ // blowing past MCP client token limits.
1959
+ const MAX_USER_RESPONSE_BYTES = 50_000;
1960
+ let droppedFullWorkspaces = false;
1961
+ if (parsed.fullWorkspaces && JSON.stringify(user).length > MAX_USER_RESPONSE_BYTES) {
1962
+ user = await client.getCurrentUser({ fullWorkspaces: false });
1963
+ droppedFullWorkspaces = true;
1964
+ }
1965
+ const result = {
1867
1966
  authMode: "oauth",
1868
1967
  user,
1869
1968
  mode: user.bot ? "role-filler" : "assistant",
1870
1969
  hint: user.bot
1871
1970
  ? "You are a bot energizing roles. You have no authority as an agent — only through the roles you fill. Act autonomously within your roles' accountabilities. Process tensions proactively."
1872
1971
  : "You are assisting a human who energizes roles. Defer to them for decisions. Help them articulate tensions and navigate governance.",
1873
- });
1972
+ };
1973
+ if (droppedFullWorkspaces) {
1974
+ result.warning = "fullWorkspaces was dropped because the response exceeded the size limit. Use nestr_list_workspaces to browse workspaces individually.";
1975
+ }
1976
+ return formatResult(result);
1874
1977
  }
1875
1978
  catch (err) {
1876
1979
  // If the error is from the tokenProvider (expired OAuth session),