@nestr/mcp 0.1.97 → 0.1.99
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/README.md +1 -1
- package/build/api/client.d.ts +46 -1
- package/build/api/client.d.ts.map +1 -1
- package/build/api/client.js +102 -0
- package/build/api/client.js.map +1 -1
- package/build/help/articles.d.ts.map +1 -1
- package/build/help/articles.js +10 -3
- package/build/help/articles.js.map +1 -1
- package/build/help/cross-links.d.ts.map +1 -1
- package/build/help/cross-links.js +4 -0
- package/build/help/cross-links.js.map +1 -1
- package/build/help/topics.d.ts.map +1 -1
- package/build/help/topics.js +73 -0
- package/build/help/topics.js.map +1 -1
- package/build/tools/index.d.ts +1737 -165
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +551 -96
- package/build/tools/index.js.map +1 -1
- package/package.json +1 -1
package/build/tools/index.js
CHANGED
|
@@ -125,6 +125,40 @@ const HINT_URL_PATTERNS = [
|
|
|
125
125
|
tool: "nestr_search",
|
|
126
126
|
params: (m, sp) => ({ workspaceId: m[1], query: sp.get("search") || "" }),
|
|
127
127
|
},
|
|
128
|
+
// Direct-message hints. The unread hint on a thread carries the endpoint that answers
|
|
129
|
+
// it, so these turn "3 posts you have not read" into the one call that lists them
|
|
130
|
+
// rather than a URL the model has to hand-assemble.
|
|
131
|
+
// /users/me/dm/{t}/posts → nestr_get_dm_posts (before the thread pattern)
|
|
132
|
+
{
|
|
133
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/posts$/,
|
|
134
|
+
tool: "nestr_get_dm_posts",
|
|
135
|
+
params: (m, sp) => {
|
|
136
|
+
const result = { threadId: m[1] };
|
|
137
|
+
const unread = sp.get("unread");
|
|
138
|
+
if (unread)
|
|
139
|
+
result.unread = unread === "true";
|
|
140
|
+
return result;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
// /users/me/dm/{t} → nestr_get_dm_thread (after the deeper pattern above)
|
|
144
|
+
{
|
|
145
|
+
pattern: /^\/users\/me\/dm\/([^/]+)$/,
|
|
146
|
+
tool: "nestr_get_dm_thread",
|
|
147
|
+
params: (m, sp) => {
|
|
148
|
+
const result = { threadId: m[1] };
|
|
149
|
+
const unread = sp.get("unread");
|
|
150
|
+
if (unread)
|
|
151
|
+
result.unread = unread === "true";
|
|
152
|
+
return result;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
// /posts/{id}/read → nestr_mark_post_read. Carried by the unread_posts hint that
|
|
156
|
+
// nests/{id}/posts returns, so acknowledging what you just read is one call.
|
|
157
|
+
{
|
|
158
|
+
pattern: /^\/posts\/([^/]+)\/read$/,
|
|
159
|
+
tool: "nestr_mark_post_read",
|
|
160
|
+
params: (m) => ({ postId: m[1] }),
|
|
161
|
+
},
|
|
128
162
|
// /nests/{id}/posts → nestr_get_comments
|
|
129
163
|
{ pattern: /^\/nests\/([^/]+)\/posts$/, tool: "nestr_get_comments", params: (m) => ({ nestId: m[1] }) },
|
|
130
164
|
// /nests/{id}/files → nestr_get_nest_files
|
|
@@ -151,8 +185,109 @@ const HINT_TYPE_TOOL_CALLS = {
|
|
|
151
185
|
},
|
|
152
186
|
};
|
|
153
187
|
},
|
|
188
|
+
// `purpose` is a first-class nest field, so there is no per-label field key to pick;
|
|
189
|
+
// only the example text differs between a role and a circle.
|
|
190
|
+
no_purpose(nest) {
|
|
191
|
+
const nestId = nest._id;
|
|
192
|
+
if (!nestId)
|
|
193
|
+
return null;
|
|
194
|
+
const labels = nest.labels || [];
|
|
195
|
+
const isRole = labels.includes("circleplus-role") || labels.includes("role");
|
|
196
|
+
return {
|
|
197
|
+
tool: "nestr_update_nest",
|
|
198
|
+
params: {
|
|
199
|
+
nestId,
|
|
200
|
+
purpose: isRole
|
|
201
|
+
? "<purpose statement: why this role exists and the future state it works towards>"
|
|
202
|
+
: "<purpose statement: the north star every role and project here traces back to>",
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
},
|
|
154
206
|
};
|
|
155
207
|
const HINT_ENDPOINT_TOOL_MAPPINGS = [
|
|
208
|
+
// Direct messages. The unread hints on a container and a thread each carry the endpoint
|
|
209
|
+
// that answers them, so these turn "3 threads you have not read" into the one call that
|
|
210
|
+
// lists them. Deeper routes first: the patterns are tried in order.
|
|
211
|
+
// Support queues. Sibling of the DM routes, so these sit alongside them.
|
|
212
|
+
{
|
|
213
|
+
method: "GET",
|
|
214
|
+
pattern: /^\/users\/me\/queues\/([^/]+)\/threads\/?$/,
|
|
215
|
+
tool: "nestr_list_queue_threads",
|
|
216
|
+
pathParamNames: ["key"],
|
|
217
|
+
bodyParams: new Set([]),
|
|
218
|
+
queryParams: { unread: "unread" },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
method: "GET",
|
|
222
|
+
pattern: /^\/users\/me\/queues\/?$/,
|
|
223
|
+
tool: "nestr_list_queues",
|
|
224
|
+
pathParamNames: [],
|
|
225
|
+
bodyParams: new Set([]),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
method: "GET",
|
|
229
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/posts\/?$/,
|
|
230
|
+
tool: "nestr_get_dm_posts",
|
|
231
|
+
pathParamNames: ["threadId"],
|
|
232
|
+
bodyParams: new Set([]),
|
|
233
|
+
queryParams: { unread: "unread", depth: "depth" },
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
method: "POST",
|
|
237
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/posts\/?$/,
|
|
238
|
+
tool: "nestr_post_dm_message",
|
|
239
|
+
pathParamNames: ["threadId"],
|
|
240
|
+
bodyParams: new Set(["body"]),
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
method: "POST",
|
|
244
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/escalate\/?$/,
|
|
245
|
+
tool: "nestr_escalate_to_support",
|
|
246
|
+
pathParamNames: ["threadId"],
|
|
247
|
+
bodyParams: new Set(["reason"]),
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
method: "GET",
|
|
251
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/?$/,
|
|
252
|
+
tool: "nestr_get_dm_thread",
|
|
253
|
+
pathParamNames: ["threadId"],
|
|
254
|
+
bodyParams: new Set([]),
|
|
255
|
+
queryParams: { unread: "unread" },
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
method: "PATCH",
|
|
259
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/?$/,
|
|
260
|
+
tool: "nestr_update_dm_thread",
|
|
261
|
+
pathParamNames: ["threadId"],
|
|
262
|
+
bodyParams: new Set(["title", "completed", "users"]),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
method: "POST",
|
|
266
|
+
pattern: /^\/users\/me\/dm\/?$/,
|
|
267
|
+
tool: "nestr_start_dm_thread",
|
|
268
|
+
pathParamNames: [],
|
|
269
|
+
bodyParams: new Set(["user", "title"]),
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
method: "GET",
|
|
273
|
+
pattern: /^\/users\/me\/dm\/?$/,
|
|
274
|
+
tool: "nestr_list_dms",
|
|
275
|
+
pathParamNames: [],
|
|
276
|
+
bodyParams: new Set([]),
|
|
277
|
+
// The route spells the filter ?user=, the tool calls it withUser. unread now belongs
|
|
278
|
+
// here too: the listing is the threads themselves, so "what have I not read" is
|
|
279
|
+
// answered by this call rather than by a container's own threads route.
|
|
280
|
+
queryParams: { user: "withUser", unread: "unread" },
|
|
281
|
+
},
|
|
282
|
+
// Carried by the unread_posts hint on nests/{id}/posts, so acknowledging what you just
|
|
283
|
+
// read is one call. Works for any post, not only a DM.
|
|
284
|
+
{
|
|
285
|
+
method: "POST",
|
|
286
|
+
pattern: /^\/posts\/([^/]+)\/read\/?$/,
|
|
287
|
+
tool: "nestr_mark_post_read",
|
|
288
|
+
pathParamNames: ["postId"],
|
|
289
|
+
bodyParams: new Set([]),
|
|
290
|
+
},
|
|
156
291
|
{
|
|
157
292
|
method: "POST",
|
|
158
293
|
pattern: /^\/nests\/?$/,
|
|
@@ -211,10 +346,18 @@ const HINT_ENDPOINT_TOOL_MAPPINGS = [
|
|
|
211
346
|
bodyParams: new Set([]),
|
|
212
347
|
},
|
|
213
348
|
];
|
|
214
|
-
/**
|
|
349
|
+
/**
|
|
350
|
+
* Strip optional host + /api prefix so we match against canonical routes, and split the
|
|
351
|
+
* query off: the patterns describe paths, so a trailing `?unread=true` would stop every
|
|
352
|
+
* one of them matching.
|
|
353
|
+
*/
|
|
215
354
|
function normalizeEndpointPath(path) {
|
|
216
355
|
const hostStripped = path.replace(/^https?:\/\/[^/]+/, "");
|
|
217
|
-
|
|
356
|
+
const [rawPath, queryString] = hostStripped.split("?");
|
|
357
|
+
return {
|
|
358
|
+
path: rawPath.replace(/^\/api(?=\/)/, ""),
|
|
359
|
+
search: new URLSearchParams(queryString || ""),
|
|
360
|
+
};
|
|
218
361
|
}
|
|
219
362
|
/**
|
|
220
363
|
* Translate one API hint endpoint into an MCP tool-call suggestion.
|
|
@@ -226,7 +369,7 @@ export function translateEndpoint(endpoint) {
|
|
|
226
369
|
const method = (endpoint.method || "").toUpperCase();
|
|
227
370
|
if (!method)
|
|
228
371
|
return null;
|
|
229
|
-
const path = normalizeEndpointPath(endpoint.path || "");
|
|
372
|
+
const { path, search } = normalizeEndpointPath(endpoint.path || "");
|
|
230
373
|
for (const mapping of HINT_ENDPOINT_TOOL_MAPPINGS) {
|
|
231
374
|
if (mapping.method !== method)
|
|
232
375
|
continue;
|
|
@@ -237,6 +380,17 @@ export function translateEndpoint(endpoint) {
|
|
|
237
380
|
mapping.pathParamNames.forEach((name, i) => {
|
|
238
381
|
parametersExample[name] = match[i + 1];
|
|
239
382
|
});
|
|
383
|
+
// "true"/"false" become booleans: every tool that takes one of these declares it as a
|
|
384
|
+
// boolean, and a string would fail schema validation on the suggested call.
|
|
385
|
+
if (mapping.queryParams) {
|
|
386
|
+
for (const [key, value] of search.entries()) {
|
|
387
|
+
const paramName = mapping.queryParams[key];
|
|
388
|
+
if (!paramName)
|
|
389
|
+
continue;
|
|
390
|
+
parametersExample[paramName] =
|
|
391
|
+
value === "true" || value === "false" ? value === "true" : value;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
240
394
|
if (mapping.extraParams)
|
|
241
395
|
Object.assign(parametersExample, mapping.extraParams);
|
|
242
396
|
const droppedFields = [];
|
|
@@ -278,6 +432,21 @@ export function commentPlacementNote(requestedNestId, post) {
|
|
|
278
432
|
}
|
|
279
433
|
// Enrich hints with tool call parameters so models can act on hints directly.
|
|
280
434
|
// Extracts workspaceId from nest ancestors (last element) for search-based hints.
|
|
435
|
+
// Canonical web URL for a nest in the Nestr app.
|
|
436
|
+
// Pattern: /n/{parentId}/{id} when a parent context is known, /n/{id} otherwise.
|
|
437
|
+
// Parent 'inbox' is treated as no parent — inbox is not a navigable container.
|
|
438
|
+
//
|
|
439
|
+
// The host comes from the API base this server was pointed at, because these URLs are
|
|
440
|
+
// handed to a person and have to open on the Nestr they are using. Hardcoding the
|
|
441
|
+
// production host meant a self-hosted or local deployment answered with app.nestr.io
|
|
442
|
+
// links for nests that only exist on their own server — a wrong link, confidently given,
|
|
443
|
+
// which is the failure this whole area keeps producing. Hint URLs do not need this:
|
|
444
|
+
// Nestr sends those absolute already. This is for the URLs this server mints itself.
|
|
445
|
+
export function nestrWebBase(apiBase) {
|
|
446
|
+
const base = apiBase || "https://app.nestr.io/api";
|
|
447
|
+
return base.replace(/\/api\/?$/, "").replace(/\/+$/, "") || "https://app.nestr.io";
|
|
448
|
+
}
|
|
449
|
+
const NESTR_WEB_BASE = nestrWebBase(process.env.NESTR_API_BASE);
|
|
281
450
|
export function enrichHints(data) {
|
|
282
451
|
if (!data || typeof data !== "object")
|
|
283
452
|
return data;
|
|
@@ -285,12 +454,17 @@ export function enrichHints(data) {
|
|
|
285
454
|
if (Array.isArray(data)) {
|
|
286
455
|
return data.map((item) => enrichHints(item));
|
|
287
456
|
}
|
|
288
|
-
// Handle wrapped responses { data: [...] }
|
|
289
|
-
|
|
290
|
-
|
|
457
|
+
// Handle wrapped responses { data: [...] }. Enrich the payload, then fall through so
|
|
458
|
+
// the envelope's OWN hints are enriched too: a posts response carries unread_posts
|
|
459
|
+
// beside its data, and returning here left that hint as a bare URL.
|
|
460
|
+
let subject = data;
|
|
461
|
+
if ("data" in subject && Array.isArray(subject.data)) {
|
|
462
|
+
subject = { ...subject, data: enrichHints(subject.data) };
|
|
463
|
+
if (!Array.isArray(subject.hints))
|
|
464
|
+
return subject;
|
|
291
465
|
}
|
|
292
466
|
// Enrich hints on this nest
|
|
293
|
-
const record =
|
|
467
|
+
const record = subject;
|
|
294
468
|
if (Array.isArray(record.hints)) {
|
|
295
469
|
// Extract workspaceId from ancestors (last element is always the workspace)
|
|
296
470
|
const ancestors = record.ancestors;
|
|
@@ -308,10 +482,12 @@ export function enrichHints(data) {
|
|
|
308
482
|
else if (hint.url) {
|
|
309
483
|
// Legacy: single URL → toolCall. Kept for backwards compatibility with
|
|
310
484
|
// hints that pre-date the endpoints[] payload.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
485
|
+
// Strip an optional host, then an optional /api prefix. Previously only the
|
|
486
|
+
// host-qualified form was handled, so a bare "/api/..." hint matched no pattern
|
|
487
|
+
// and was reported as unrecognized. normalizeEndpointPath does both for the
|
|
488
|
+
// endpoints[] payload; this is the same rule for the legacy url field.
|
|
489
|
+
let rawUrl = hint.url.replace(/^https?:\/\/[^/]+/, "");
|
|
490
|
+
rawUrl = rawUrl.replace(/^\/api(?=\/)/, "");
|
|
315
491
|
const [path, queryString] = rawUrl.split("?");
|
|
316
492
|
const searchParams = new URLSearchParams(queryString || "");
|
|
317
493
|
let matched = false;
|
|
@@ -340,12 +516,10 @@ export function enrichHints(data) {
|
|
|
340
516
|
return enriched;
|
|
341
517
|
});
|
|
342
518
|
}
|
|
343
|
-
|
|
519
|
+
// subject, not data: the wrapped-response branch above works on a copy, so returning
|
|
520
|
+
// `data` would discard both the enriched payload and the enriched envelope hints.
|
|
521
|
+
return subject;
|
|
344
522
|
}
|
|
345
|
-
// Canonical web URL for a nest in the Nestr app.
|
|
346
|
-
// Pattern: /n/{parentId}/{id} when a parent context is known, /n/{id} otherwise.
|
|
347
|
-
// Parent 'inbox' is treated as no parent — inbox is not a navigable container.
|
|
348
|
-
const NESTR_WEB_BASE = "https://app.nestr.io";
|
|
349
523
|
function buildNestUrl(id, parentId) {
|
|
350
524
|
if (parentId && parentId.toLowerCase() !== "inbox") {
|
|
351
525
|
return `${NESTR_WEB_BASE}/n/${parentId}/${id}`;
|
|
@@ -431,7 +605,12 @@ const coerceIntArray = (schema) => z.preprocess((val) => {
|
|
|
431
605
|
// Shared description for the sort parameter on list/fetch tools. All of these
|
|
432
606
|
// endpoints honor a `sort` query param server-side (field name, '-' prefix for
|
|
433
607
|
// descending) — the same fields the search `sort:` operator uses.
|
|
434
|
-
const
|
|
608
|
+
const MENTION_DESC = "Supports HTML and @mentions. Mentions MUST use literal curly braces: `@{userId:roleId}`, NOT `@userId`. Without braces nothing is linked and nobody is notified. Forms: `@{userId:roleId}` (preferred, names the role), `@{userId}`, `@{email}`, `@{circle}` (all fillers in the nearest ancestor circle).";
|
|
609
|
+
const PRIME_LABEL_RULE = "At most ONE prime label per nest (project, tension, role, circle, anchor-circle, meeting, metric, goal, result, checklist, feedback, userstory, sprint, epic, milestone): they are the nest's identity and cannot coexist. Only exception: userstory may pair with project.";
|
|
610
|
+
const PURPOSE_DESC = "Only for workspaces, circles and roles: a short aspirational statement. Details belong in description, not here. Supports HTML.";
|
|
611
|
+
const CONTENT_DESC = "The primary content field: details, context, acceptance criteria. Structured data goes in fields, progress in comments. Supports Markdown and HTML.";
|
|
612
|
+
const STRIP_DESCRIPTION = "Strip description fields to shrink the response. Use for bulk or index reads.";
|
|
613
|
+
const SORT_DESCRIPTION = "Sort field: title, createdAt, updatedAt, due, activityAt, order. Prefix '-' to reverse. For 'recently active' use '-activityAt' (includes children), not '-updatedAt' (own edits only).";
|
|
435
614
|
// Tool input schemas using Zod
|
|
436
615
|
export const schemas = {
|
|
437
616
|
listWorkspaces: z.object({
|
|
@@ -440,6 +619,48 @@ export const schemas = {
|
|
|
440
619
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
441
620
|
page: z.number().optional().describe("Page number (1-indexed) for pagination"),
|
|
442
621
|
}),
|
|
622
|
+
listDMs: z.object({
|
|
623
|
+
withUser: z.string().optional().describe("Only threads with this person: their user id, username or email. Use 'nestr_support' for your Nestradamus conversation. Errors if you cannot message them."),
|
|
624
|
+
unread: z.boolean().optional().describe("true returns only threads with messages you have not read"),
|
|
625
|
+
includeCompleted: z.boolean().optional().describe("true also returns closed conversations. They are left out by default."),
|
|
626
|
+
limit: z.number().optional().describe("Threads per page (default 50, max 200)"),
|
|
627
|
+
page: z.number().optional().describe("Page number, 1-based"),
|
|
628
|
+
}),
|
|
629
|
+
startDMThread: z.object({
|
|
630
|
+
user: z.string().describe("Who to message: their user id, username or email. Must be someone you share a workspace with, or already have a conversation with."),
|
|
631
|
+
title: z.string().optional().describe("Optional thread title. Defaults to a dated one, as the app uses."),
|
|
632
|
+
}),
|
|
633
|
+
listQueues: z.object({}),
|
|
634
|
+
listQueueThreads: z.object({
|
|
635
|
+
key: z.string().describe("Queue key, e.g. 'support'. From nestr_list_queues."),
|
|
636
|
+
unread: z.boolean().optional().describe("true returns only threads you have not read"),
|
|
637
|
+
}),
|
|
638
|
+
getDMThread: z.object({
|
|
639
|
+
threadId: z.string().describe("Thread id"),
|
|
640
|
+
unread: z.boolean().optional().describe("true embeds the posts you have not read, false the ones you have. Omit for the thread alone."),
|
|
641
|
+
}),
|
|
642
|
+
updateDMThread: z.object({
|
|
643
|
+
threadId: z.string().describe("Thread id"),
|
|
644
|
+
title: z.string().optional().describe("New thread title"),
|
|
645
|
+
completed: z.boolean().nullable().optional().describe("true closes the conversation, null reopens it. A closed one drops out of nestr_list_dms unless includeCompleted is set, and stays readable and postable by id. Repeating a state changes nothing."),
|
|
646
|
+
users: z.array(z.string()).optional().describe("The participant list you want, replacing the current one — read it from nestr_get_dm_thread first. Anyone you add sees the whole thread and must be someone you share a workspace with; the bot and the person who raised the thread cannot be removed. Leave a conversation by sending the list without yourself."),
|
|
647
|
+
}),
|
|
648
|
+
getDMPosts: z.object({
|
|
649
|
+
threadId: z.string().describe("Thread id"),
|
|
650
|
+
unread: z.boolean().optional().describe("true for posts you have not read, false for the ones you have. Omit for all."),
|
|
651
|
+
depth: z.union([z.number(), z.literal("all")]).optional().describe("Include posts on descendant nests"),
|
|
652
|
+
}),
|
|
653
|
+
createDMPost: z.object({
|
|
654
|
+
threadId: z.string().describe("Thread id"),
|
|
655
|
+
body: z.string().describe("Message text. Supports HTML and Markdown."),
|
|
656
|
+
}),
|
|
657
|
+
markPostRead: z.object({
|
|
658
|
+
postId: z.string().describe("Post to mark read up to. Everything up to and including it becomes read."),
|
|
659
|
+
}),
|
|
660
|
+
escalateToSupport: z.object({
|
|
661
|
+
threadId: z.string().describe("Thread id to escalate. It must be a conversation Nestradamus is in."),
|
|
662
|
+
reason: z.string().describe("One or two sentences for whoever picks this up: what is needed and what has been tried."),
|
|
663
|
+
}),
|
|
443
664
|
getWorkspace: z.object({
|
|
444
665
|
workspaceId: z.string().describe("Workspace ID"),
|
|
445
666
|
}),
|
|
@@ -466,7 +687,7 @@ export const schemas = {
|
|
|
466
687
|
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."),
|
|
467
688
|
provenance: z.boolean().optional().describe("Single-nest only. Include field/property provenance: which label (and circle context) defines each field and property."),
|
|
468
689
|
rights: z.boolean().optional().describe("Single-nest only. Include the caller's composed rights on the nest plus a deny trace naming the profiles that block each op."),
|
|
469
|
-
forUser: z.string().optional().describe("Single
|
|
690
|
+
forUser: z.string().optional().describe("Single nest, with rights=true. Rights for this user id instead of the caller. Caller must be a nest admin."),
|
|
470
691
|
whoCan: z.string().optional().describe("Single-nest only. Comma-separated ops (read,update,delete,create) to list who can perform each on this nest."),
|
|
471
692
|
}),
|
|
472
693
|
explainNest: z.object({
|
|
@@ -486,7 +707,7 @@ export const schemas = {
|
|
|
486
707
|
parentId: z.string().describe("Parent nest ID (workspace, circle, or project)"),
|
|
487
708
|
title: z.string().describe("Title of the new nest (plain text, HTML stripped)"),
|
|
488
709
|
description: z.string().optional().describe("The primary content field — use for project details, task context, acceptance criteria, Definition of Done, and any detailed information. Supports Markdown and HTML."),
|
|
489
|
-
purpose: z.string().optional().describe(
|
|
710
|
+
purpose: z.string().optional().describe(PURPOSE_DESC),
|
|
490
711
|
labels: coerceFromJson(z.array(z.string())).optional().describe("Label IDs to apply"),
|
|
491
712
|
fields: coerceFromJson(z.record(z.unknown())).optional().describe("Structured field values to set on creation (e.g., { 'project.status': 'Current' }, { 'skill.type': 'process' }). Same shape as nestr_update_nest fields — saves a follow-up update call."),
|
|
492
713
|
users: coerceFromJson(z.array(z.string())).optional().describe("User IDs to assign (required for tasks/projects to associate with a person)"),
|
|
@@ -498,7 +719,7 @@ export const schemas = {
|
|
|
498
719
|
nestId: z.string().describe("Nest ID to update"),
|
|
499
720
|
title: z.string().optional().describe("New title (plain text, HTML stripped)"),
|
|
500
721
|
description: z.string().optional().describe("The primary content field — use for project details, task context, acceptance criteria, and any detailed information. Supports Markdown and HTML."),
|
|
501
|
-
purpose: z.string().optional().describe(
|
|
722
|
+
purpose: z.string().optional().describe(PURPOSE_DESC),
|
|
502
723
|
parentId: z.string().optional().describe("New parent ID (move nest to different location, e.g., move inbox item to a role or project)"),
|
|
503
724
|
labels: coerceFromJson(z.array(z.string())).optional().describe("Label IDs to set (e.g., ['project'] to convert an item into a project)"),
|
|
504
725
|
fields: coerceFromJson(z.record(z.unknown())).optional().describe("Field updates (e.g., { 'project.status': 'Current' })"),
|
|
@@ -515,12 +736,12 @@ export const schemas = {
|
|
|
515
736
|
}),
|
|
516
737
|
addComment: z.object({
|
|
517
738
|
nestId: z.string().describe("Nest ID to comment on"),
|
|
518
|
-
body: z.string().describe(
|
|
519
|
-
labels: z.array(z.string()).optional().describe("Optional label IDs to attach
|
|
739
|
+
body: z.string().describe(`Comment text. ${MENTION_DESC}`),
|
|
740
|
+
labels: z.array(z.string()).optional().describe("Optional label IDs to attach at creation (e.g. 'decision', 'question', or a custom ID). Personal labels are auto-scoped to the caller. Discover IDs via nestr_list_labels / nestr_list_personal_labels."),
|
|
520
741
|
}),
|
|
521
742
|
updateComment: z.object({
|
|
522
743
|
commentId: z.string().describe("Comment ID to update"),
|
|
523
|
-
body: z.string().describe(
|
|
744
|
+
body: z.string().describe(`Updated comment text. ${MENTION_DESC}`),
|
|
524
745
|
labels: z.array(z.string()).optional().describe("Optional full set of label IDs for the comment. When provided, this REPLACES the comment's existing labels. To incrementally add or remove a single label without replacing the rest, use nestr_add_label / nestr_remove_label with the commentId as the nestId."),
|
|
525
746
|
}),
|
|
526
747
|
deleteComment: z.object({
|
|
@@ -582,6 +803,7 @@ export const schemas = {
|
|
|
582
803
|
getComments: z.object({
|
|
583
804
|
nestId: z.string().describe("Nest ID to get comments from. Pass a workspace ID to gather communication across the whole workspace (combine with depth='all')."),
|
|
584
805
|
depth: z.union([z.number(), z.literal("all")]).optional().describe("How deep below the context nest to look for comments. 0 (default) returns only comments directly on this nest; N includes comments on descendants up to N levels deep; 'all' includes comments on this nest and every descendant. Use 'all' on a workspace or circle nest to analyse large sets of communication in one call."),
|
|
806
|
+
unread: z.boolean().optional().describe("true for comments you have not read, false for the ones you have. Omit for all."),
|
|
585
807
|
}),
|
|
586
808
|
getCircle: z.object({
|
|
587
809
|
workspaceId: z.string().describe("Workspace ID"),
|
|
@@ -743,14 +965,14 @@ export const schemas = {
|
|
|
743
965
|
tensionId: z.string().describe("Tension ID"),
|
|
744
966
|
_id: z.string().optional().describe("ID of an existing governance item to change or remove. Omit to propose a new item."),
|
|
745
967
|
title: z.string().optional().describe("Title for the governance item"),
|
|
746
|
-
labels: coerceFromJson(z.array(z.string())).optional().describe("
|
|
968
|
+
labels: coerceFromJson(z.array(z.string())).optional().describe("Item type, e.g. ['role'], ['circle'], ['policy'], ['accountability'], ['domain']"),
|
|
747
969
|
description: z.string().optional().describe("The primary content field — detailed information about the item. Supports Markdown and HTML."),
|
|
748
|
-
purpose: z.string().optional().describe(
|
|
970
|
+
purpose: z.string().optional().describe(PURPOSE_DESC),
|
|
749
971
|
parentId: z.string().optional().describe("Parent ID — use to move/restructure items (e.g., move role to different circle)"),
|
|
750
|
-
users: coerceFromJson(z.array(z.string())).optional().describe("User IDs to assign. For an election (with roleId), the
|
|
751
|
-
due: z.string().optional().describe("Due
|
|
752
|
-
accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Accountability titles
|
|
753
|
-
domains: coerceFromJson(z.array(z.string())).optional().describe("Domain titles
|
|
972
|
+
users: coerceFromJson(z.array(z.string())).optional().describe("User IDs to assign. For an election (with roleId), the one user being elected."),
|
|
973
|
+
due: z.string().optional().describe("Due or re-election date, ISO. For an election, the term end; omit for no term."),
|
|
974
|
+
accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Accountability titles on a role (replaces all; children tools for individual edits)"),
|
|
975
|
+
domains: coerceFromJson(z.array(z.string())).optional().describe("Domain titles on a role (replaces all; children tools for individual edits)"),
|
|
754
976
|
roleId: z.string().optional().describe("Hold an ELECTION: the electable role to fill (Facilitator/Secretary/Rep Link or any electable role). Assigns or reconfirms the role's filler for a term WITHOUT changing its accountabilities/domains — provide users:[userId] (one person) and optional due (term). Do not combine with _id."),
|
|
755
977
|
removeNest: z.boolean().optional().describe("Set true with _id to propose deletion of the referenced governance item (when the proposal is accepted, the item is removed). Distinct from nestr_remove_tension_part, which undoes a proposal part you already added. Requires _id; other body fields are ignored."),
|
|
756
978
|
}).refine((data) => !data.removeNest || !!data._id, { message: "removeNest:true requires _id to identify which item to propose for deletion" }).refine((data) => !data.roleId || (Array.isArray(data.users) && data.users.length === 1), { message: "An election (roleId) requires exactly one user to elect — users: [userId]." }).refine((data) => !(data.roleId && data._id), { message: "Provide either roleId (to hold an election) or _id (to change/delete an existing item), not both." }),
|
|
@@ -760,13 +982,13 @@ export const schemas = {
|
|
|
760
982
|
partId: z.string().describe("Part ID to modify"),
|
|
761
983
|
title: z.string().optional().describe("Updated title"),
|
|
762
984
|
description: z.string().optional().describe("Updated description — the primary content field. Supports Markdown and HTML."),
|
|
763
|
-
purpose: z.string().optional().describe(
|
|
985
|
+
purpose: z.string().optional().describe(PURPOSE_DESC),
|
|
764
986
|
labels: coerceFromJson(z.array(z.string())).optional().describe("Updated labels"),
|
|
765
987
|
parentId: z.string().optional().describe("Updated parent ID"),
|
|
766
988
|
users: coerceFromJson(z.array(z.string())).optional().describe("Updated user assignments"),
|
|
767
989
|
due: z.string().optional().describe("Updated due date (ISO format)"),
|
|
768
|
-
accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Updated accountabilities (replaces all
|
|
769
|
-
domains: coerceFromJson(z.array(z.string())).optional().describe("Updated domains (replaces all
|
|
990
|
+
accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Updated accountabilities (replaces all; children tools for individual edits)"),
|
|
991
|
+
domains: coerceFromJson(z.array(z.string())).optional().describe("Updated domains (replaces all; children tools for individual edits)"),
|
|
770
992
|
}),
|
|
771
993
|
removeTensionPart: z.object({
|
|
772
994
|
nestId: z.string().describe("ID of the circle or role the tension belongs to"),
|
|
@@ -846,15 +1068,15 @@ export const schemas = {
|
|
|
846
1068
|
workspaceId: z.string().describe("Workspace ID to register the connector in"),
|
|
847
1069
|
type: z.enum(["mcp", "cli", "api"]).describe("Transport: 'mcp' (MCP server over a url), 'api' (REST endpoint over a url), or 'cli' (a command)"),
|
|
848
1070
|
name: z.string().describe("Unique connector name within the workspace catalog"),
|
|
849
|
-
config: coerceFromJson(z.record(z.unknown())).optional().describe("
|
|
850
|
-
capabilities: coerceFromJson(z.record(z.unknown())).optional().describe("
|
|
851
|
-
exposure: coerceFromJson(z.record(z.unknown())).optional().describe("
|
|
852
|
-
authStrategy: z.enum(["secret", "oauth2"]).optional().describe("How a principal connects: 'secret' (
|
|
1071
|
+
config: coerceFromJson(z.record(z.unknown())).optional().describe("Transport config, no secret. mcp/api need a url, cli a command. Optional non-secret headers go under headers."),
|
|
1072
|
+
capabilities: coerceFromJson(z.record(z.unknown())).optional().describe("{ discover: boolean, tools: [{ name, description, inputSchema }] }. discover:true lets the connector self-describe its tools at runtime."),
|
|
1073
|
+
exposure: coerceFromJson(z.record(z.unknown())).optional().describe("Which owners may bind: { userAgent: boolean, domainGated: boolean }. domainGated:true allows binding to a role's domain."),
|
|
1074
|
+
authStrategy: z.enum(["secret", "oauth2"]).optional().describe("How a principal connects: 'secret' (one-time, via the Connect button) or 'oauth2'. The agent never sees it."),
|
|
853
1075
|
}),
|
|
854
1076
|
bindConnector: z.object({
|
|
855
1077
|
workspaceId: z.string().describe("Workspace ID the connector and owner belong to"),
|
|
856
1078
|
connectorId: z.string().describe("ID of an enabled connector from nestr_list_connectors"),
|
|
857
|
-
ownerType: z.enum(["user", "agent", "workspace", "role-domain"]).describe("Owner type. 'role-domain' binds
|
|
1079
|
+
ownerType: z.enum(["user", "agent", "workspace", "role-domain"]).describe("Owner type. 'role-domain' binds to a role's domain, materialising a credentials field there."),
|
|
858
1080
|
ownerId: z.string().describe("Owner ID. user/agent: the user ID. workspace: the workspace ID. role-domain: the domain nest ID."),
|
|
859
1081
|
}),
|
|
860
1082
|
// File attachments (a comment id works as the nestId — files are keyed by nestId)
|
|
@@ -887,15 +1109,15 @@ const destructive = { annotations: { readOnlyHint: false, destructiveHint: true
|
|
|
887
1109
|
export const toolDefinitions = [
|
|
888
1110
|
{
|
|
889
1111
|
name: "nestr_help",
|
|
890
|
-
description: "
|
|
1112
|
+
description: "Nestr documentation, three modes. (1) Internal topic: `topic` with a curated key (search, labels, nest-model, inbox, daily-plan, notifications, insights, tension-processing, skills, mcp-apps, authentication, scrum, okr, ...); 'topics' lists them all. (2) Help article: `topic` with a slug from nestr.io/help/articles/<slug>; returns markdown plus a numbered list of its images. Images are never attached by default: includeImages:true takes the first maxImages screenshots, imageIndexes:[..] takes chosen ones. Attach when the user wants to see how something looks. (3) Search: `search` with free text; returns ranked matches, each a title and one-line summary, tolerant of typos and synonyms (kanban/sprint to scrum). A topic is tried internally first, then as an article. Every response opens with 'Resolved as:' naming the mode, and topics and articles cross-link. Call before unfamiliar operations. No auth.",
|
|
891
1113
|
inputSchema: {
|
|
892
1114
|
type: "object",
|
|
893
1115
|
properties: {
|
|
894
1116
|
topic: { type: "string", description: "Internal topic key or help-article slug. Use 'topics' for the full list of internal topics." },
|
|
895
1117
|
search: { type: "string", description: "Free-text query against the public help articles. Returns slugs to fetch via `topic`." },
|
|
896
|
-
includeImages: { type: "boolean", description: "
|
|
897
|
-
imageIndexes: { type: "array", items: { type: "integer", minimum: 0 }, description: "
|
|
898
|
-
maxImages: { type: "integer", minimum: 1, description: "
|
|
1118
|
+
includeImages: { type: "boolean", description: "Article mode. Default false (markdown plus a numbered image-URL list). True attaches the first maxImages content screenshots inline, downscaled. Header, thumbnail and uncaptioned images are never auto-attached; reach those with imageIndexes." },
|
|
1119
|
+
imageIndexes: { type: "array", items: { type: "integer", minimum: 0 }, description: "Article mode. Attach screenshots by [index] from the numbered list in a prior response, e.g. [4,5,6]. Overrides the default selection and the maxImages cap, attaching exactly these in order." },
|
|
1120
|
+
maxImages: { type: "integer", minimum: 1, description: "Article mode. Caps the default selection (first N content images). Default 3, max 6. Ignored when imageIndexes is set." },
|
|
899
1121
|
},
|
|
900
1122
|
},
|
|
901
1123
|
...readOnly,
|
|
@@ -919,7 +1141,7 @@ export const toolDefinitions = [
|
|
|
919
1141
|
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
920
1142
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
921
1143
|
page: { type: "number", description: "Page number (1-indexed) for pagination" },
|
|
922
|
-
stripDescription: { type: "boolean", description:
|
|
1144
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
923
1145
|
},
|
|
924
1146
|
},
|
|
925
1147
|
...readOnly,
|
|
@@ -931,7 +1153,7 @@ export const toolDefinitions = [
|
|
|
931
1153
|
type: "object",
|
|
932
1154
|
properties: {
|
|
933
1155
|
workspaceId: { type: "string", description: "Workspace ID" },
|
|
934
|
-
stripDescription: { type: "boolean", description:
|
|
1156
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
935
1157
|
},
|
|
936
1158
|
required: ["workspaceId"],
|
|
937
1159
|
},
|
|
@@ -984,10 +1206,10 @@ export const toolDefinitions = [
|
|
|
984
1206
|
workspaceId: { type: "string", description: "Workspace ID to search in" },
|
|
985
1207
|
query: { type: "string", description: "Search query with optional operators (e.g., 'label:role', 'assignee:me completed:false')" },
|
|
986
1208
|
sort: { type: "string", description: `${SORT_DESCRIPTION} Takes precedence over sort:/sort-order: operators in the query.` },
|
|
987
|
-
limit: { type: "number", description: "Max results per page.
|
|
1209
|
+
limit: { type: "number", description: "Max results per page. Omit on the first call so meta.total shows the match count." },
|
|
988
1210
|
page: { type: "number", description: "Page number (1-indexed) for fetching additional pages" },
|
|
989
|
-
stripDescription: { type: "boolean", description:
|
|
990
|
-
_listTitle: { type: "string", description: "Short
|
|
1211
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1212
|
+
_listTitle: { type: "string", description: "Short title for the list UI header, 2-4 words, e.g. Marketing projects. Say what is shown, not the query." },
|
|
991
1213
|
},
|
|
992
1214
|
required: ["workspaceId", "query"],
|
|
993
1215
|
},
|
|
@@ -1001,14 +1223,14 @@ export const toolDefinitions = [
|
|
|
1001
1223
|
inputSchema: {
|
|
1002
1224
|
type: "object",
|
|
1003
1225
|
properties: {
|
|
1004
|
-
nestId: { type: "string", description: "Nest ID, or comma-separated IDs
|
|
1226
|
+
nestId: { type: "string", description: "Nest ID, or comma-separated IDs for a batch (e.g. 'id1,id2'). Keep the URL under 2000 chars." },
|
|
1005
1227
|
fieldsMetaData: { type: "boolean", description: "Set to true to include field schema metadata (available options, field types)" },
|
|
1006
|
-
hints: { type: "boolean", description: "
|
|
1007
|
-
stripDescription: { type: "boolean", description:
|
|
1008
|
-
provenance: { type: "boolean", description: "Single
|
|
1009
|
-
rights: { type: "boolean", description: "Single
|
|
1010
|
-
forUser: { type: "string", description: "Single
|
|
1011
|
-
whoCan: { type: "string", description: "Single
|
|
1228
|
+
hints: { type: "boolean", description: "Contextual hints, default true. False for bulk lookups needing only structure." },
|
|
1229
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1230
|
+
provenance: { type: "boolean", description: "Single nest. Which label and circle context defines each field and property, e.g. why a role has a given icon." },
|
|
1231
|
+
rights: { type: "boolean", description: "Single nest. The caller's composed rights (self read/update/delete) plus a deny trace naming what blocks each op, and why." },
|
|
1232
|
+
forUser: { type: "string", description: "Single nest, with rights=true. Rights for this user id instead of the caller. Caller must be a nest admin." },
|
|
1233
|
+
whoCan: { type: "string", description: "Single nest. Comma-separated ops (read,update,delete,create): who can perform each (admins + role-holders), with contact for admin callers." },
|
|
1012
1234
|
},
|
|
1013
1235
|
required: ["nestId"],
|
|
1014
1236
|
},
|
|
@@ -1039,7 +1261,7 @@ export const toolDefinitions = [
|
|
|
1039
1261
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1040
1262
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1041
1263
|
hints: { type: "boolean", description: "Include contextual hints (default: true). Set to false for large result sets or bulk operations." },
|
|
1042
|
-
stripDescription: { type: "boolean", description:
|
|
1264
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1043
1265
|
_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." },
|
|
1044
1266
|
},
|
|
1045
1267
|
required: ["nestId"],
|
|
@@ -1050,14 +1272,14 @@ export const toolDefinitions = [
|
|
|
1050
1272
|
},
|
|
1051
1273
|
{
|
|
1052
1274
|
name: "nestr_create_nest",
|
|
1053
|
-
description:
|
|
1275
|
+
description: `Create a nest under a parent. Labels define the type, e.g. ['project'], ['role']. ${PRIME_LABEL_RULE} Sprint/epic/milestone never pair; stories link to those via graph relations. In established workspaces prefer the tension flow for governance. See nestr_help('labels').`,
|
|
1054
1276
|
inputSchema: {
|
|
1055
1277
|
type: "object",
|
|
1056
1278
|
properties: {
|
|
1057
1279
|
parentId: { type: "string", description: "Parent nest ID (workspace, circle, or project)" },
|
|
1058
1280
|
title: { type: "string", description: "Title of the new nest (plain text, HTML tags stripped)" },
|
|
1059
|
-
description: { type: "string", description:
|
|
1060
|
-
purpose: { type: "string", description:
|
|
1281
|
+
description: { type: "string", description: CONTENT_DESC },
|
|
1282
|
+
purpose: { type: "string", description: PURPOSE_DESC },
|
|
1061
1283
|
labels: {
|
|
1062
1284
|
type: "array",
|
|
1063
1285
|
items: { type: "string" },
|
|
@@ -1093,14 +1315,14 @@ export const toolDefinitions = [
|
|
|
1093
1315
|
},
|
|
1094
1316
|
{
|
|
1095
1317
|
name: "nestr_update_nest",
|
|
1096
|
-
description:
|
|
1318
|
+
description: `Update nest properties. Set parentId to move. Send only what changes. When replacing labels: ${PRIME_LABEL_RULE} Prefer tensions for governance. See nestr_help('nest-model') for fields and data namespacing.`,
|
|
1097
1319
|
inputSchema: {
|
|
1098
1320
|
type: "object",
|
|
1099
1321
|
properties: {
|
|
1100
1322
|
nestId: { type: "string", description: "Nest ID to update" },
|
|
1101
1323
|
title: { type: "string", description: "New title (plain text, HTML tags stripped)" },
|
|
1102
|
-
description: { type: "string", description:
|
|
1103
|
-
purpose: { type: "string", description:
|
|
1324
|
+
description: { type: "string", description: CONTENT_DESC },
|
|
1325
|
+
purpose: { type: "string", description: PURPOSE_DESC },
|
|
1104
1326
|
parentId: { type: "string", description: "New parent ID (move nest to different location)" },
|
|
1105
1327
|
labels: {
|
|
1106
1328
|
type: "array",
|
|
@@ -1161,16 +1383,16 @@ export const toolDefinitions = [
|
|
|
1161
1383
|
},
|
|
1162
1384
|
{
|
|
1163
1385
|
name: "nestr_add_comment",
|
|
1164
|
-
description: "Add a comment to a nest
|
|
1386
|
+
description: "Add a comment to a nest, for progress updates and discussion. Mentions need literal curly braces, see `body`. Optionally attach labels at creation.",
|
|
1165
1387
|
inputSchema: {
|
|
1166
1388
|
type: "object",
|
|
1167
1389
|
properties: {
|
|
1168
|
-
nestId: { type: "string", description: "
|
|
1169
|
-
body: { type: "string", description:
|
|
1390
|
+
nestId: { type: "string", description: "Nest or conversation the comment belongs to. A comment ID instead replies inside that thread. Direct-message conversations are flat, so a message ID there lands on the conversation and the response says where." },
|
|
1391
|
+
body: { type: "string", description: `Comment text. ${MENTION_DESC}` },
|
|
1170
1392
|
labels: {
|
|
1171
1393
|
type: "array",
|
|
1172
1394
|
items: { type: "string" },
|
|
1173
|
-
description: "Optional label IDs to attach
|
|
1395
|
+
description: "Optional label IDs to attach at creation (e.g. 'decision', 'question', or a custom ID). Personal labels are auto-scoped to the caller. Discover IDs via nestr_list_labels / nestr_list_personal_labels.",
|
|
1174
1396
|
},
|
|
1175
1397
|
},
|
|
1176
1398
|
required: ["nestId", "body"],
|
|
@@ -1184,7 +1406,7 @@ export const toolDefinitions = [
|
|
|
1184
1406
|
type: "object",
|
|
1185
1407
|
properties: {
|
|
1186
1408
|
commentId: { type: "string", description: "Comment ID to update" },
|
|
1187
|
-
body: { type: "string", description:
|
|
1409
|
+
body: { type: "string", description: `Updated comment text. ${MENTION_DESC}` },
|
|
1188
1410
|
labels: {
|
|
1189
1411
|
type: "array",
|
|
1190
1412
|
items: { type: "string" },
|
|
@@ -1217,7 +1439,7 @@ export const toolDefinitions = [
|
|
|
1217
1439
|
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
1218
1440
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1219
1441
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1220
|
-
stripDescription: { type: "boolean", description:
|
|
1442
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1221
1443
|
},
|
|
1222
1444
|
required: ["workspaceId"],
|
|
1223
1445
|
},
|
|
@@ -1234,7 +1456,7 @@ export const toolDefinitions = [
|
|
|
1234
1456
|
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
1235
1457
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1236
1458
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1237
|
-
stripDescription: { type: "boolean", description:
|
|
1459
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1238
1460
|
},
|
|
1239
1461
|
required: ["workspaceId", "circleId"],
|
|
1240
1462
|
},
|
|
@@ -1262,12 +1484,146 @@ export const toolDefinitions = [
|
|
|
1262
1484
|
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
1263
1485
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1264
1486
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1265
|
-
stripDescription: { type: "boolean", description:
|
|
1487
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1266
1488
|
},
|
|
1267
1489
|
required: ["workspaceId"],
|
|
1268
1490
|
},
|
|
1269
1491
|
...readOnly,
|
|
1270
1492
|
},
|
|
1493
|
+
// ---- Direct messages ----
|
|
1494
|
+
// A thread is the unit and its id is the whole address. There is no container to fetch
|
|
1495
|
+
// first: start from nestr_list_dms, optionally narrowed to one person with withUser.
|
|
1496
|
+
{
|
|
1497
|
+
name: "nestr_list_dms",
|
|
1498
|
+
description: "List your open direct-message threads, most recently posted first. Closed ones are left out unless includeCompleted is set. Pass withUser to see only the ones with a particular person; withUser:'nestr_support' is your Nestradamus conversation. Each thread carries participants, so a flat list still tells you who you are talking to.",
|
|
1499
|
+
inputSchema: {
|
|
1500
|
+
type: "object",
|
|
1501
|
+
properties: {
|
|
1502
|
+
withUser: { type: "string", description: "Only threads with this person: user id, username or email" },
|
|
1503
|
+
unread: { type: "boolean", description: "Only threads with messages you have not read" },
|
|
1504
|
+
includeCompleted: { type: "boolean", description: "Also return closed conversations (left out by default)" },
|
|
1505
|
+
limit: { type: "number", description: "Threads per page (default 50, max 200)" },
|
|
1506
|
+
page: { type: "number", description: "Page number, 1-based" },
|
|
1507
|
+
},
|
|
1508
|
+
},
|
|
1509
|
+
...readOnly,
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
name: "nestr_start_dm_thread",
|
|
1513
|
+
description: "Start a new direct-message thread with someone. Use it for a new subject rather than reopening an old thread. You must share a workspace with them, or already have a conversation with them.",
|
|
1514
|
+
inputSchema: {
|
|
1515
|
+
type: "object",
|
|
1516
|
+
properties: {
|
|
1517
|
+
user: { type: "string", description: "Who to message: user id, username or email" },
|
|
1518
|
+
title: { type: "string", description: "Optional title. Defaults to a dated one, as the app uses." },
|
|
1519
|
+
},
|
|
1520
|
+
required: ["user"],
|
|
1521
|
+
},
|
|
1522
|
+
...mutating,
|
|
1523
|
+
},
|
|
1524
|
+
// ---- Support queues ----
|
|
1525
|
+
// A queue is a label on threads across many DM spaces, not a space itself. It hands
|
|
1526
|
+
// back thread ids, and a thread id is the whole address: nestr_get_dm_thread /
|
|
1527
|
+
// nestr_get_dm_posts take it directly.
|
|
1528
|
+
{
|
|
1529
|
+
name: "nestr_list_queues",
|
|
1530
|
+
description: "List the support queues you can see: the ones you monitor, plus any you have raised a thread in. Each carries `subscribed`, which decides what nestr_list_queue_threads returns for you.",
|
|
1531
|
+
inputSchema: { type: "object", properties: {} },
|
|
1532
|
+
...readOnly,
|
|
1533
|
+
},
|
|
1534
|
+
{
|
|
1535
|
+
name: "nestr_list_queue_threads",
|
|
1536
|
+
description: "List threads in a support queue, most recently posted first. If you subscribe to the queue you get every thread in it; otherwise you get only the ones you raised, which is how you find your own open support tickets. Pass unread:true for just what has moved. Read one with nestr_get_dm_thread using the id you get back.",
|
|
1537
|
+
inputSchema: {
|
|
1538
|
+
type: "object",
|
|
1539
|
+
properties: {
|
|
1540
|
+
key: { type: "string", description: "Queue key, e.g. 'support'" },
|
|
1541
|
+
unread: { type: "boolean", description: "Only threads you have not read" },
|
|
1542
|
+
},
|
|
1543
|
+
required: ["key"],
|
|
1544
|
+
},
|
|
1545
|
+
...readOnly,
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
name: "nestr_get_dm_thread",
|
|
1549
|
+
description: "Get a direct-message thread as a nest, with hints. Pass unread:true to embed the posts you have not read in the same call, which is usually what you want when picking a thread back up.",
|
|
1550
|
+
inputSchema: {
|
|
1551
|
+
type: "object",
|
|
1552
|
+
properties: {
|
|
1553
|
+
threadId: { type: "string", description: "Thread id" },
|
|
1554
|
+
unread: { type: "boolean", description: "true embeds unread posts, false embeds read ones" },
|
|
1555
|
+
},
|
|
1556
|
+
required: ["threadId"],
|
|
1557
|
+
},
|
|
1558
|
+
...readOnly,
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
name: "nestr_update_dm_thread",
|
|
1562
|
+
description: "Update a direct-message thread: rename it, close or reopen it, or change who is in it. Send only the keys you want changed, as with nestr_update_nest. completed:true closes a conversation once it is dealt with, which takes it out of nestr_list_dms without losing it; completed:null reopens. `users` is the participant list you want, so read the thread first and send the list with someone added or removed; the bot and the person who raised the thread cannot be removed. Answers with the updated thread.",
|
|
1563
|
+
inputSchema: {
|
|
1564
|
+
type: "object",
|
|
1565
|
+
properties: {
|
|
1566
|
+
threadId: { type: "string", description: "Thread id" },
|
|
1567
|
+
title: { type: "string", description: "New thread title" },
|
|
1568
|
+
completed: { type: ["boolean", "null"], description: "true closes the conversation, null reopens it" },
|
|
1569
|
+
users: { type: "array", items: { type: "string" }, description: "The participant list you want, replacing the current one" },
|
|
1570
|
+
},
|
|
1571
|
+
required: ["threadId"],
|
|
1572
|
+
},
|
|
1573
|
+
...mutating,
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
name: "nestr_get_dm_posts",
|
|
1577
|
+
description: "Read the posts in a direct-message thread, oldest first, each with its nested replies. Pass unread:true for just what is new, false for the rest.",
|
|
1578
|
+
inputSchema: {
|
|
1579
|
+
type: "object",
|
|
1580
|
+
properties: {
|
|
1581
|
+
threadId: { type: "string", description: "Thread id" },
|
|
1582
|
+
unread: { type: "boolean", description: "true for unread posts, false for read ones. Omit for all." },
|
|
1583
|
+
depth: { type: ["number", "string"], description: "Include posts on descendant nests, or 'all'" },
|
|
1584
|
+
},
|
|
1585
|
+
required: ["threadId"],
|
|
1586
|
+
},
|
|
1587
|
+
...readOnly,
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
name: "nestr_post_dm_message",
|
|
1591
|
+
description: "Post a message into a direct-message thread.",
|
|
1592
|
+
inputSchema: {
|
|
1593
|
+
type: "object",
|
|
1594
|
+
properties: {
|
|
1595
|
+
threadId: { type: "string", description: "Thread id" },
|
|
1596
|
+
body: { type: "string", description: "Message text. Supports HTML and Markdown." },
|
|
1597
|
+
},
|
|
1598
|
+
required: ["threadId", "body"],
|
|
1599
|
+
},
|
|
1600
|
+
...mutating,
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
name: "nestr_mark_post_read",
|
|
1604
|
+
description: "Mark a conversation read up to and including this post. Works for any post, not only direct messages. The marker never moves backwards, so calling it on an older post is harmless.",
|
|
1605
|
+
inputSchema: {
|
|
1606
|
+
type: "object",
|
|
1607
|
+
properties: {
|
|
1608
|
+
postId: { type: "string", description: "Post to mark read up to" },
|
|
1609
|
+
},
|
|
1610
|
+
required: ["postId"],
|
|
1611
|
+
},
|
|
1612
|
+
...mutating,
|
|
1613
|
+
},
|
|
1614
|
+
{
|
|
1615
|
+
name: "nestr_escalate_to_support",
|
|
1616
|
+
description: "Bring a human from Nestr support into a Nestradamus conversation. Use it when the person asks for a human, when you have answered the wrong question more than once, or when something needs Nestr staff to look at their account. Find the thread with nestr_list_dms({withUser:'nestr_support'}). Safe to call twice; a thread already waiting stays as it is. Only works on a conversation Nestradamus is in.",
|
|
1617
|
+
inputSchema: {
|
|
1618
|
+
type: "object",
|
|
1619
|
+
properties: {
|
|
1620
|
+
threadId: { type: "string", description: "Thread id to escalate" },
|
|
1621
|
+
reason: { type: "string", description: "One or two sentences for whoever picks this up: what is needed and what has been tried. They can read the thread, so do not summarise it." },
|
|
1622
|
+
},
|
|
1623
|
+
required: ["threadId", "reason"],
|
|
1624
|
+
},
|
|
1625
|
+
...mutating,
|
|
1626
|
+
},
|
|
1271
1627
|
{
|
|
1272
1628
|
name: "nestr_get_insights",
|
|
1273
1629
|
description: "Get organizational health metrics and trends. Each metric has currentValue and compareValue for direction. Pro plan: filter by circle (nestId) or user (userId). Requires Insights app. See nestr_help('insights').",
|
|
@@ -1337,7 +1693,7 @@ export const toolDefinitions = [
|
|
|
1337
1693
|
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
1338
1694
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1339
1695
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1340
|
-
stripDescription: { type: "boolean", description:
|
|
1696
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1341
1697
|
_listTitle: { type: "string", description: "Short descriptive title for the list UI header (e.g., \"Engineering projects\", \"All projects\"). Omit for default." },
|
|
1342
1698
|
},
|
|
1343
1699
|
required: ["workspaceId"],
|
|
@@ -1347,7 +1703,7 @@ export const toolDefinitions = [
|
|
|
1347
1703
|
},
|
|
1348
1704
|
{
|
|
1349
1705
|
name: "nestr_get_comments",
|
|
1350
|
-
description: "Get comments and discussion history on a nest, including full nested reply threads. By default returns only comments posted directly on the given nest. Widen with depth to also include comments on descendant nests, or pass a workspace/circle nest ID with depth='all' to gather large sets of communication for analysis.",
|
|
1706
|
+
description: "Get comments and discussion history on a nest, including full nested reply threads. By default returns only comments posted directly on the given nest. Widen with depth to also include comments on descendant nests, or pass a workspace/circle nest ID with depth='all' to gather large sets of communication for analysis. Carries an unread_posts hint when you have not read everything; nestr_mark_post_read acknowledges up to a given post, on any nest, not just direct messages.",
|
|
1351
1707
|
inputSchema: {
|
|
1352
1708
|
type: "object",
|
|
1353
1709
|
properties: {
|
|
@@ -1369,7 +1725,7 @@ export const toolDefinitions = [
|
|
|
1369
1725
|
properties: {
|
|
1370
1726
|
workspaceId: { type: "string", description: "Workspace ID" },
|
|
1371
1727
|
circleId: { type: "string", description: "Circle ID" },
|
|
1372
|
-
stripDescription: { type: "boolean", description:
|
|
1728
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1373
1729
|
},
|
|
1374
1730
|
required: ["workspaceId", "circleId"],
|
|
1375
1731
|
},
|
|
@@ -1390,7 +1746,16 @@ export const toolDefinitions = [
|
|
|
1390
1746
|
},
|
|
1391
1747
|
{
|
|
1392
1748
|
name: "nestr_add_workspace_user",
|
|
1393
|
-
description: "
|
|
1749
|
+
description: "Adds a user to a workspace by email, creating the account if needed. It SENDS A REAL INVITE "
|
|
1750
|
+
+ "EMAIL, so confirm with the requester first. Covers seats, membership, plan headcount, adding "
|
|
1751
|
+
+ "a colleague. It provisions only domains the workspace added and Nestr verified: personal "
|
|
1752
|
+
+ "providers (gmail.com) are always refused, and an added domain stays refused until verified. "
|
|
1753
|
+
+ "That limits this tool, not the workspace. The in-app invite (Workspace settings, Users, "
|
|
1754
|
+
+ "\"Invite users\") accepts any address with no domain check, so offer it first on a refusal. "
|
|
1755
|
+
+ "NEVER suggest clearing the workspace domain list: it keeps the requirement, kills the only "
|
|
1756
|
+
+ "way to meet it, and breaks auto-join. Suggest adding a domain only when they control that "
|
|
1757
|
+
+ "company domain. Nestr review "
|
|
1758
|
+
+ "takes up to 24 hours.",
|
|
1394
1759
|
inputSchema: {
|
|
1395
1760
|
type: "object",
|
|
1396
1761
|
properties: {
|
|
@@ -1452,7 +1817,7 @@ export const toolDefinitions = [
|
|
|
1452
1817
|
type: "object",
|
|
1453
1818
|
properties: {
|
|
1454
1819
|
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." },
|
|
1455
|
-
stripDescription: { type: "boolean", description:
|
|
1820
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1456
1821
|
},
|
|
1457
1822
|
},
|
|
1458
1823
|
_meta: completableListUi,
|
|
@@ -1478,7 +1843,7 @@ export const toolDefinitions = [
|
|
|
1478
1843
|
type: "object",
|
|
1479
1844
|
properties: {
|
|
1480
1845
|
nestId: { type: "string", description: "Inbox item ID" },
|
|
1481
|
-
stripDescription: { type: "boolean", description:
|
|
1846
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1482
1847
|
},
|
|
1483
1848
|
required: ["nestId"],
|
|
1484
1849
|
},
|
|
@@ -1594,7 +1959,7 @@ export const toolDefinitions = [
|
|
|
1594
1959
|
inputSchema: {
|
|
1595
1960
|
type: "object",
|
|
1596
1961
|
properties: {
|
|
1597
|
-
stripDescription: { type: "boolean", description:
|
|
1962
|
+
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1598
1963
|
},
|
|
1599
1964
|
},
|
|
1600
1965
|
_meta: completableListUi,
|
|
@@ -1840,7 +2205,7 @@ export const toolDefinitions = [
|
|
|
1840
2205
|
},
|
|
1841
2206
|
{
|
|
1842
2207
|
name: "nestr_add_tension_part",
|
|
1843
|
-
description: "Add a governance proposal part to a tension.
|
|
2208
|
+
description: "Add a governance proposal part to a tension. Modes: new item (omit _id, give title/labels); change one (_id plus the changed fields; editing a role copies its accountabilities/domains in, so it reads as a full role edit); delete one (_id plus removeNest:true); election (roleId plus users:[userId], optional due, which assigns or reconfirms the filler and leaves accountabilities/domains untouched). See nestr_help('tension-processing').",
|
|
1844
2209
|
inputSchema: {
|
|
1845
2210
|
type: "object",
|
|
1846
2211
|
properties: {
|
|
@@ -1848,16 +2213,16 @@ export const toolDefinitions = [
|
|
|
1848
2213
|
tensionId: { type: "string", description: "Tension ID" },
|
|
1849
2214
|
_id: { type: "string", description: "ID of an existing governance item to change or remove. Omit to propose a new item." },
|
|
1850
2215
|
title: { type: "string", description: "Title for the governance item" },
|
|
1851
|
-
labels: { type: "array", items: { type: "string" }, description: "
|
|
2216
|
+
labels: { type: "array", items: { type: "string" }, description: "Item type, e.g. ['role'], ['circle'], ['policy'], ['accountability'], ['domain']" },
|
|
1852
2217
|
description: { type: "string", description: "The primary content field — detailed information about the item. Supports Markdown and HTML." },
|
|
1853
|
-
purpose: { type: "string", description:
|
|
2218
|
+
purpose: { type: "string", description: PURPOSE_DESC },
|
|
1854
2219
|
parentId: { type: "string", description: "Parent ID — use to move/restructure items (e.g., move role to different circle)" },
|
|
1855
|
-
users: { type: "array", items: { type: "string" }, description: "User IDs to assign. For an election (with roleId), the
|
|
1856
|
-
due: { type: "string", description: "Due
|
|
1857
|
-
accountabilities: { type: "array", items: { type: "string" }, description: "Accountability titles
|
|
1858
|
-
domains: { type: "array", items: { type: "string" }, description: "Domain titles
|
|
1859
|
-
roleId: { type: "string", description: "
|
|
1860
|
-
removeNest: { type: "boolean", description: "
|
|
2220
|
+
users: { type: "array", items: { type: "string" }, description: "User IDs to assign. For an election (with roleId), the one user being elected." },
|
|
2221
|
+
due: { type: "string", description: "Due or re-election date, ISO. For an election, the term end; omit for no term." },
|
|
2222
|
+
accountabilities: { type: "array", items: { type: "string" }, description: "Accountability titles on a role (replaces all; children tools for individual edits)" },
|
|
2223
|
+
domains: { type: "array", items: { type: "string" }, description: "Domain titles on a role (replaces all; children tools for individual edits)" },
|
|
2224
|
+
roleId: { type: "string", description: "ELECTION mode: the electable role to fill (Facilitator, Secretary, Rep Link or any electable role). Pair with users:[oneUserId] and optional due. Never with _id." },
|
|
2225
|
+
removeNest: { type: "boolean", description: "With _id, propose deleting that governance item; it goes when the proposal is accepted. Not nestr_remove_tension_part, which undoes a part you already added." },
|
|
1861
2226
|
},
|
|
1862
2227
|
required: ["nestId", "tensionId"],
|
|
1863
2228
|
},
|
|
@@ -1874,13 +2239,13 @@ export const toolDefinitions = [
|
|
|
1874
2239
|
partId: { type: "string", description: "Part ID to modify" },
|
|
1875
2240
|
title: { type: "string", description: "Updated title" },
|
|
1876
2241
|
description: { type: "string", description: "Updated description — the primary content field. Supports Markdown and HTML." },
|
|
1877
|
-
purpose: { type: "string", description:
|
|
2242
|
+
purpose: { type: "string", description: PURPOSE_DESC },
|
|
1878
2243
|
labels: { type: "array", items: { type: "string" }, description: "Updated labels" },
|
|
1879
2244
|
parentId: { type: "string", description: "Updated parent ID" },
|
|
1880
2245
|
users: { type: "array", items: { type: "string" }, description: "Updated user assignments" },
|
|
1881
2246
|
due: { type: "string", description: "Updated due date (ISO format)" },
|
|
1882
|
-
accountabilities: { type: "array", items: { type: "string" }, description: "Updated accountabilities (replaces all
|
|
1883
|
-
domains: { type: "array", items: { type: "string" }, description: "Updated domains (replaces all
|
|
2247
|
+
accountabilities: { type: "array", items: { type: "string" }, description: "Updated accountabilities (replaces all; children tools for individual edits)" },
|
|
2248
|
+
domains: { type: "array", items: { type: "string" }, description: "Updated domains (replaces all; children tools for individual edits)" },
|
|
1884
2249
|
},
|
|
1885
2250
|
required: ["nestId", "tensionId", "partId"],
|
|
1886
2251
|
},
|
|
@@ -2057,7 +2422,7 @@ export const toolDefinitions = [
|
|
|
2057
2422
|
},
|
|
2058
2423
|
{
|
|
2059
2424
|
name: "nestr_register_connector",
|
|
2060
|
-
description: "Register a connector in the workspace catalog: a reusable mcp
|
|
2425
|
+
description: "Register a connector in the workspace catalog: a reusable mcp/cli/api template holding no secret. Workspace-admin only; a non-admin gets AUTH_SCOPE_INSUFFICIENT (call nestr_diagnose on any auth error). Give type ('mcp' and 'api' need a url in config, 'cli' a command) and a unique name; optionally capabilities, exposure, authStrategy. This creates the template only. Flow: register, bind to a role's domain with nestr_bind_connector, then a human or agent connects the account via the credentials field's Connect button. The secret is captured out-of-band, never by the agent.",
|
|
2061
2426
|
inputSchema: {
|
|
2062
2427
|
type: "object",
|
|
2063
2428
|
properties: {
|
|
@@ -2070,20 +2435,20 @@ export const toolDefinitions = [
|
|
|
2070
2435
|
name: { type: "string", description: "Unique connector name within the workspace catalog" },
|
|
2071
2436
|
config: {
|
|
2072
2437
|
type: "object",
|
|
2073
|
-
description: "
|
|
2438
|
+
description: "Transport config, no secret. mcp/api need a url, cli a command. Optional non-secret headers go under headers.",
|
|
2074
2439
|
},
|
|
2075
2440
|
capabilities: {
|
|
2076
2441
|
type: "object",
|
|
2077
|
-
description: "
|
|
2442
|
+
description: "{ discover: boolean, tools: [{ name, description, inputSchema }] }. discover:true lets the connector self-describe its tools at runtime.",
|
|
2078
2443
|
},
|
|
2079
2444
|
exposure: {
|
|
2080
2445
|
type: "object",
|
|
2081
|
-
description: "
|
|
2446
|
+
description: "Which owners may bind: { userAgent: boolean, domainGated: boolean }. domainGated:true allows binding to a role's domain.",
|
|
2082
2447
|
},
|
|
2083
2448
|
authStrategy: {
|
|
2084
2449
|
type: "string",
|
|
2085
2450
|
enum: ["secret", "oauth2"],
|
|
2086
|
-
description: "How a principal connects: 'secret' (
|
|
2451
|
+
description: "How a principal connects: 'secret' (one-time, via the Connect button) or 'oauth2'. The agent never sees it.",
|
|
2087
2452
|
},
|
|
2088
2453
|
},
|
|
2089
2454
|
required: ["workspaceId", "type", "name"],
|
|
@@ -2092,7 +2457,7 @@ export const toolDefinitions = [
|
|
|
2092
2457
|
},
|
|
2093
2458
|
{
|
|
2094
2459
|
name: "nestr_bind_connector",
|
|
2095
|
-
description: "Bind a registered connector to an owner so that owner can use it. Owner types: 'user' or 'agent' (ownerId is the user ID), 'workspace' (
|
|
2460
|
+
description: "Bind a registered connector to an owner so that owner can use it. Owner types: 'user' or 'agent' (ownerId is the user ID), 'workspace' (the workspace ID), 'role-domain' (the domain nest ID). A role-domain binding also materialises a credentials field on the domain nest, so the Connect button renders there and the response carries credentialsField { domainId, fieldId, fieldCode }. A human or agent then connects the account through that button; the secret is captured out-of-band, never seen by the agent. Workspace-admin only, and the connector must already be registered and enabled.",
|
|
2096
2461
|
inputSchema: {
|
|
2097
2462
|
type: "object",
|
|
2098
2463
|
properties: {
|
|
@@ -2101,7 +2466,7 @@ export const toolDefinitions = [
|
|
|
2101
2466
|
ownerType: {
|
|
2102
2467
|
type: "string",
|
|
2103
2468
|
enum: ["user", "agent", "workspace", "role-domain"],
|
|
2104
|
-
description: "Owner type. 'role-domain' binds
|
|
2469
|
+
description: "Owner type. 'role-domain' binds to a role's domain, materialising a credentials field there.",
|
|
2105
2470
|
},
|
|
2106
2471
|
ownerId: {
|
|
2107
2472
|
type: "string",
|
|
@@ -2306,7 +2671,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2306
2671
|
const entries = await loadArticleIndex();
|
|
2307
2672
|
const hits = searchArticleIndex(entries, parsed.search, 8);
|
|
2308
2673
|
if (hits.length === 0) {
|
|
2309
|
-
return { content: [{ type: "text", text: `_Resolved as: help-article search._\n\nNo help articles matched "${parsed.search}". Try broader terms or a synonym, or call nestr_help({ topic: "topics" }) for internal MCP topics.` }] };
|
|
2674
|
+
return { content: [{ type: "text", text: `_Resolved as: help-article search._\n\nNo help articles matched "${parsed.search}". The index scores article slugs and curated keywords, not article bodies, so an exact feature, operator or field name often misses even when the docs cover it. Try broader terms or a synonym, or call nestr_help({ topic: "topics" }) for internal MCP topics. An empty result is not evidence the thing does not exist: say you could not find it documented, never that it is unsupported.` }] };
|
|
2310
2675
|
}
|
|
2311
2676
|
// Enrich the top hits with a title + one-line summary so the caller
|
|
2312
2677
|
// can pick the right article without a blind fetch. Best-effort:
|
|
@@ -2732,8 +3097,12 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2732
3097
|
const parsed = schemas.getComments.parse(args);
|
|
2733
3098
|
const comments = await client.getNestPosts(parsed.nestId, {
|
|
2734
3099
|
depth: parsed.depth,
|
|
3100
|
+
unread: parsed.unread,
|
|
2735
3101
|
});
|
|
2736
|
-
|
|
3102
|
+
// enrichHints turns the unread_posts hint's endpoint into a nestr_mark_post_read
|
|
3103
|
+
// call. Without it the hint still arrives, but as a raw URL the model has to
|
|
3104
|
+
// recognise and hand-assemble.
|
|
3105
|
+
return formatResult(enrichHints(comments));
|
|
2737
3106
|
}
|
|
2738
3107
|
case "nestr_get_circle": {
|
|
2739
3108
|
const parsed = schemas.getCircle.parse(args);
|
|
@@ -2830,6 +3199,92 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2830
3199
|
});
|
|
2831
3200
|
return formatResult({ message: "Personal label created successfully", label });
|
|
2832
3201
|
}
|
|
3202
|
+
// Direct messages
|
|
3203
|
+
case "nestr_list_dms": {
|
|
3204
|
+
const parsed = schemas.listDMs.parse(args);
|
|
3205
|
+
const result = await client.listDMs({
|
|
3206
|
+
withUser: parsed.withUser,
|
|
3207
|
+
unread: parsed.unread,
|
|
3208
|
+
includeCompleted: parsed.includeCompleted,
|
|
3209
|
+
limit: parsed.limit,
|
|
3210
|
+
page: parsed.page,
|
|
3211
|
+
});
|
|
3212
|
+
return formatResult({ threads: result });
|
|
3213
|
+
}
|
|
3214
|
+
case "nestr_start_dm_thread": {
|
|
3215
|
+
const parsed = schemas.startDMThread.parse(args);
|
|
3216
|
+
const result = await client.createDMThread(parsed.user, parsed.title);
|
|
3217
|
+
return formatResult({ message: "Thread started", thread: result });
|
|
3218
|
+
}
|
|
3219
|
+
case "nestr_list_queues": {
|
|
3220
|
+
schemas.listQueues.parse(args ?? {});
|
|
3221
|
+
const result = await client.listQueues();
|
|
3222
|
+
return formatResult({ queues: result });
|
|
3223
|
+
}
|
|
3224
|
+
case "nestr_list_queue_threads": {
|
|
3225
|
+
const parsed = schemas.listQueueThreads.parse(args);
|
|
3226
|
+
const result = await client.listQueueThreads(parsed.key, { unread: parsed.unread });
|
|
3227
|
+
return formatResult(enrichHints(result));
|
|
3228
|
+
}
|
|
3229
|
+
case "nestr_get_dm_thread": {
|
|
3230
|
+
const parsed = schemas.getDMThread.parse(args);
|
|
3231
|
+
const result = await client.getDMThread(parsed.threadId, { unread: parsed.unread });
|
|
3232
|
+
return formatResult(enrichHints(result));
|
|
3233
|
+
}
|
|
3234
|
+
case "nestr_update_dm_thread": {
|
|
3235
|
+
const parsed = schemas.updateDMThread.parse(args);
|
|
3236
|
+
// `completed` is meaningful as null, so presence is the test rather than truth.
|
|
3237
|
+
const setsCompleted = args !== null
|
|
3238
|
+
&& typeof args === "object"
|
|
3239
|
+
&& Object.prototype.hasOwnProperty.call(args, "completed");
|
|
3240
|
+
if (parsed.title === undefined && !setsCompleted && parsed.users === undefined) {
|
|
3241
|
+
throw new Error("Pass at least one of title, completed or users.");
|
|
3242
|
+
}
|
|
3243
|
+
const result = await client.updateDMThread(parsed.threadId, {
|
|
3244
|
+
...(parsed.title !== undefined ? { title: parsed.title } : {}),
|
|
3245
|
+
...(setsCompleted ? { completed: parsed.completed ?? null } : {}),
|
|
3246
|
+
...(parsed.users !== undefined ? { users: parsed.users } : {}),
|
|
3247
|
+
});
|
|
3248
|
+
return formatResult({
|
|
3249
|
+
message: setsCompleted && parsed.completed
|
|
3250
|
+
? "Conversation closed"
|
|
3251
|
+
: "Thread updated",
|
|
3252
|
+
thread: result,
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
case "nestr_get_dm_posts": {
|
|
3256
|
+
const parsed = schemas.getDMPosts.parse(args);
|
|
3257
|
+
const result = await client.getDMPosts(parsed.threadId, {
|
|
3258
|
+
unread: parsed.unread,
|
|
3259
|
+
depth: parsed.depth,
|
|
3260
|
+
});
|
|
3261
|
+
return formatResult(enrichHints(result));
|
|
3262
|
+
}
|
|
3263
|
+
case "nestr_post_dm_message": {
|
|
3264
|
+
const parsed = schemas.createDMPost.parse(args);
|
|
3265
|
+
const result = await client.createDMPost(parsed.threadId, parsed.body);
|
|
3266
|
+
return formatResult({ message: "Message posted", post: result });
|
|
3267
|
+
}
|
|
3268
|
+
case "nestr_mark_post_read": {
|
|
3269
|
+
const parsed = schemas.markPostRead.parse(args);
|
|
3270
|
+
const result = await client.markPostRead(parsed.postId);
|
|
3271
|
+
return formatResult({ message: "Marked read", read: result });
|
|
3272
|
+
}
|
|
3273
|
+
case "nestr_escalate_to_support": {
|
|
3274
|
+
const parsed = schemas.escalateToSupport.parse(args);
|
|
3275
|
+
const result = await client.escalateDMThread(parsed.threadId, parsed.reason);
|
|
3276
|
+
const { alreadyQueued, statusMessagePosted } = result;
|
|
3277
|
+
let message = "A human has been brought in. Tell them so, and keep helping in the meantime.";
|
|
3278
|
+
if (alreadyQueued) {
|
|
3279
|
+
message = "Already with a human; nothing more to do.";
|
|
3280
|
+
}
|
|
3281
|
+
else if (statusMessagePosted) {
|
|
3282
|
+
// Nestr posted its own confirmation into the thread, so saying it again is the
|
|
3283
|
+
// double message this flag exists to avoid.
|
|
3284
|
+
message = "A human has been brought in and the thread already says so. Do not repeat it; carry on helping.";
|
|
3285
|
+
}
|
|
3286
|
+
return formatResult({ message, escalation: result });
|
|
3287
|
+
}
|
|
2833
3288
|
// Reorder tools
|
|
2834
3289
|
case "nestr_reorder_nest": {
|
|
2835
3290
|
const parsed = schemas.reorderNest.parse(args);
|