@nestr/mcp 0.1.96 → 0.1.98
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/build/api/client.d.ts +225 -16
- package/build/api/client.d.ts.map +1 -1
- package/build/api/client.js +226 -1
- 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 +84 -1
- package/build/help/topics.js.map +1 -1
- package/build/skills/doing-work.d.ts.map +1 -1
- package/build/skills/doing-work.js +25 -0
- package/build/skills/doing-work.js.map +1 -1
- package/build/skills/tension-processing.d.ts.map +1 -1
- package/build/skills/tension-processing.js +2 -0
- package/build/skills/tension-processing.js.map +1 -1
- package/build/tools/index.d.ts +3668 -609
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +851 -48
- 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
|
|
@@ -135,6 +169,46 @@ const HINT_URL_PATTERNS = [
|
|
|
135
169
|
{ pattern: /^\/nests\/([^/]+)$/, tool: "nestr_get_nest", params: (m) => ({ nestId: m[1] }) },
|
|
136
170
|
];
|
|
137
171
|
const HINT_TYPE_TOOL_CALLS = {
|
|
172
|
+
// Raised by POST /connectors when a hand-written url points at a vendor the
|
|
173
|
+
// deployment ships a template for. The follow-up is to look at the template,
|
|
174
|
+
// not to re-register blindly: the connector just created may be exactly what
|
|
175
|
+
// the caller wanted, and only they can say. The hint carries its own ids, so
|
|
176
|
+
// this works on a catalog entry rather than needing nest ancestors.
|
|
177
|
+
connector_template_available(record) {
|
|
178
|
+
const hints = record.hints || [];
|
|
179
|
+
const hint = hints.find((h) => { return h.type === "connector_template_available"; });
|
|
180
|
+
const workspaceId = (hint && hint.workspaceId) || record.workspaceId;
|
|
181
|
+
if (!workspaceId)
|
|
182
|
+
return null;
|
|
183
|
+
return {
|
|
184
|
+
tool: "nestr_list_connector_templates",
|
|
185
|
+
params: { workspaceId },
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
// Raised by GET /connector-templates on the response that LISTS them, because
|
|
189
|
+
// listing turned out not to be enough. A caller that could see the Xero
|
|
190
|
+
// template still hand-built a connector from what the template told it, and a
|
|
191
|
+
// hand-built copy carries no settingsKey, no OAuth client and whatever
|
|
192
|
+
// endpoint the model believed, so it authorises and then fails at first use.
|
|
193
|
+
// The follow-up here is the register call itself, pre-filled with the template
|
|
194
|
+
// id, so using the template is one call rather than a thing to remember.
|
|
195
|
+
connector_template_create(record) {
|
|
196
|
+
const hints = record.hints || [];
|
|
197
|
+
const hint = hints.find((h) => { return h.type === "connector_template_create"; });
|
|
198
|
+
if (!hint)
|
|
199
|
+
return null;
|
|
200
|
+
const workspaceId = hint.workspaceId || record.workspaceId;
|
|
201
|
+
if (!workspaceId)
|
|
202
|
+
return null;
|
|
203
|
+
const ids = hint.templateIds || [];
|
|
204
|
+
return {
|
|
205
|
+
tool: "nestr_register_connector",
|
|
206
|
+
params: {
|
|
207
|
+
workspaceId,
|
|
208
|
+
templateId: ids.length === 1 ? ids[0] : "<id of the template you want, from this list>",
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
},
|
|
138
212
|
no_strategy(nest) {
|
|
139
213
|
const nestId = nest._id;
|
|
140
214
|
if (!nestId)
|
|
@@ -151,8 +225,109 @@ const HINT_TYPE_TOOL_CALLS = {
|
|
|
151
225
|
},
|
|
152
226
|
};
|
|
153
227
|
},
|
|
228
|
+
// `purpose` is a first-class nest field, so there is no per-label field key to pick;
|
|
229
|
+
// only the example text differs between a role and a circle.
|
|
230
|
+
no_purpose(nest) {
|
|
231
|
+
const nestId = nest._id;
|
|
232
|
+
if (!nestId)
|
|
233
|
+
return null;
|
|
234
|
+
const labels = nest.labels || [];
|
|
235
|
+
const isRole = labels.includes("circleplus-role") || labels.includes("role");
|
|
236
|
+
return {
|
|
237
|
+
tool: "nestr_update_nest",
|
|
238
|
+
params: {
|
|
239
|
+
nestId,
|
|
240
|
+
purpose: isRole
|
|
241
|
+
? "<purpose statement: why this role exists and the future state it works towards>"
|
|
242
|
+
: "<purpose statement: the north star every role and project here traces back to>",
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
},
|
|
154
246
|
};
|
|
155
247
|
const HINT_ENDPOINT_TOOL_MAPPINGS = [
|
|
248
|
+
// Direct messages. The unread hints on a container and a thread each carry the endpoint
|
|
249
|
+
// that answers them, so these turn "3 threads you have not read" into the one call that
|
|
250
|
+
// lists them. Deeper routes first: the patterns are tried in order.
|
|
251
|
+
// Support queues. Sibling of the DM routes, so these sit alongside them.
|
|
252
|
+
{
|
|
253
|
+
method: "GET",
|
|
254
|
+
pattern: /^\/users\/me\/queues\/([^/]+)\/threads\/?$/,
|
|
255
|
+
tool: "nestr_list_queue_threads",
|
|
256
|
+
pathParamNames: ["key"],
|
|
257
|
+
bodyParams: new Set([]),
|
|
258
|
+
queryParams: { unread: "unread" },
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
method: "GET",
|
|
262
|
+
pattern: /^\/users\/me\/queues\/?$/,
|
|
263
|
+
tool: "nestr_list_queues",
|
|
264
|
+
pathParamNames: [],
|
|
265
|
+
bodyParams: new Set([]),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
method: "GET",
|
|
269
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/posts\/?$/,
|
|
270
|
+
tool: "nestr_get_dm_posts",
|
|
271
|
+
pathParamNames: ["threadId"],
|
|
272
|
+
bodyParams: new Set([]),
|
|
273
|
+
queryParams: { unread: "unread", depth: "depth" },
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
method: "POST",
|
|
277
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/posts\/?$/,
|
|
278
|
+
tool: "nestr_post_dm_message",
|
|
279
|
+
pathParamNames: ["threadId"],
|
|
280
|
+
bodyParams: new Set(["body"]),
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
method: "POST",
|
|
284
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/escalate\/?$/,
|
|
285
|
+
tool: "nestr_escalate_to_support",
|
|
286
|
+
pathParamNames: ["threadId"],
|
|
287
|
+
bodyParams: new Set(["reason"]),
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
method: "GET",
|
|
291
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/?$/,
|
|
292
|
+
tool: "nestr_get_dm_thread",
|
|
293
|
+
pathParamNames: ["threadId"],
|
|
294
|
+
bodyParams: new Set([]),
|
|
295
|
+
queryParams: { unread: "unread" },
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
method: "PATCH",
|
|
299
|
+
pattern: /^\/users\/me\/dm\/([^/]+)\/?$/,
|
|
300
|
+
tool: "nestr_update_dm_thread",
|
|
301
|
+
pathParamNames: ["threadId"],
|
|
302
|
+
bodyParams: new Set(["title", "completed", "users"]),
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
method: "POST",
|
|
306
|
+
pattern: /^\/users\/me\/dm\/?$/,
|
|
307
|
+
tool: "nestr_start_dm_thread",
|
|
308
|
+
pathParamNames: [],
|
|
309
|
+
bodyParams: new Set(["user", "title"]),
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
method: "GET",
|
|
313
|
+
pattern: /^\/users\/me\/dm\/?$/,
|
|
314
|
+
tool: "nestr_list_dms",
|
|
315
|
+
pathParamNames: [],
|
|
316
|
+
bodyParams: new Set([]),
|
|
317
|
+
// The route spells the filter ?user=, the tool calls it withUser. unread now belongs
|
|
318
|
+
// here too: the listing is the threads themselves, so "what have I not read" is
|
|
319
|
+
// answered by this call rather than by a container's own threads route.
|
|
320
|
+
queryParams: { user: "withUser", unread: "unread" },
|
|
321
|
+
},
|
|
322
|
+
// Carried by the unread_posts hint on nests/{id}/posts, so acknowledging what you just
|
|
323
|
+
// read is one call. Works for any post, not only a DM.
|
|
324
|
+
{
|
|
325
|
+
method: "POST",
|
|
326
|
+
pattern: /^\/posts\/([^/]+)\/read\/?$/,
|
|
327
|
+
tool: "nestr_mark_post_read",
|
|
328
|
+
pathParamNames: ["postId"],
|
|
329
|
+
bodyParams: new Set([]),
|
|
330
|
+
},
|
|
156
331
|
{
|
|
157
332
|
method: "POST",
|
|
158
333
|
pattern: /^\/nests\/?$/,
|
|
@@ -211,10 +386,18 @@ const HINT_ENDPOINT_TOOL_MAPPINGS = [
|
|
|
211
386
|
bodyParams: new Set([]),
|
|
212
387
|
},
|
|
213
388
|
];
|
|
214
|
-
/**
|
|
389
|
+
/**
|
|
390
|
+
* Strip optional host + /api prefix so we match against canonical routes, and split the
|
|
391
|
+
* query off: the patterns describe paths, so a trailing `?unread=true` would stop every
|
|
392
|
+
* one of them matching.
|
|
393
|
+
*/
|
|
215
394
|
function normalizeEndpointPath(path) {
|
|
216
395
|
const hostStripped = path.replace(/^https?:\/\/[^/]+/, "");
|
|
217
|
-
|
|
396
|
+
const [rawPath, queryString] = hostStripped.split("?");
|
|
397
|
+
return {
|
|
398
|
+
path: rawPath.replace(/^\/api(?=\/)/, ""),
|
|
399
|
+
search: new URLSearchParams(queryString || ""),
|
|
400
|
+
};
|
|
218
401
|
}
|
|
219
402
|
/**
|
|
220
403
|
* Translate one API hint endpoint into an MCP tool-call suggestion.
|
|
@@ -226,7 +409,7 @@ export function translateEndpoint(endpoint) {
|
|
|
226
409
|
const method = (endpoint.method || "").toUpperCase();
|
|
227
410
|
if (!method)
|
|
228
411
|
return null;
|
|
229
|
-
const path = normalizeEndpointPath(endpoint.path || "");
|
|
412
|
+
const { path, search } = normalizeEndpointPath(endpoint.path || "");
|
|
230
413
|
for (const mapping of HINT_ENDPOINT_TOOL_MAPPINGS) {
|
|
231
414
|
if (mapping.method !== method)
|
|
232
415
|
continue;
|
|
@@ -237,6 +420,17 @@ export function translateEndpoint(endpoint) {
|
|
|
237
420
|
mapping.pathParamNames.forEach((name, i) => {
|
|
238
421
|
parametersExample[name] = match[i + 1];
|
|
239
422
|
});
|
|
423
|
+
// "true"/"false" become booleans: every tool that takes one of these declares it as a
|
|
424
|
+
// boolean, and a string would fail schema validation on the suggested call.
|
|
425
|
+
if (mapping.queryParams) {
|
|
426
|
+
for (const [key, value] of search.entries()) {
|
|
427
|
+
const paramName = mapping.queryParams[key];
|
|
428
|
+
if (!paramName)
|
|
429
|
+
continue;
|
|
430
|
+
parametersExample[paramName] =
|
|
431
|
+
value === "true" || value === "false" ? value === "true" : value;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
240
434
|
if (mapping.extraParams)
|
|
241
435
|
Object.assign(parametersExample, mapping.extraParams);
|
|
242
436
|
const droppedFields = [];
|
|
@@ -278,6 +472,21 @@ export function commentPlacementNote(requestedNestId, post) {
|
|
|
278
472
|
}
|
|
279
473
|
// Enrich hints with tool call parameters so models can act on hints directly.
|
|
280
474
|
// Extracts workspaceId from nest ancestors (last element) for search-based hints.
|
|
475
|
+
// Canonical web URL for a nest in the Nestr app.
|
|
476
|
+
// Pattern: /n/{parentId}/{id} when a parent context is known, /n/{id} otherwise.
|
|
477
|
+
// Parent 'inbox' is treated as no parent — inbox is not a navigable container.
|
|
478
|
+
//
|
|
479
|
+
// The host comes from the API base this server was pointed at, because these URLs are
|
|
480
|
+
// handed to a person and have to open on the Nestr they are using. Hardcoding the
|
|
481
|
+
// production host meant a self-hosted or local deployment answered with app.nestr.io
|
|
482
|
+
// links for nests that only exist on their own server — a wrong link, confidently given,
|
|
483
|
+
// which is the failure this whole area keeps producing. Hint URLs do not need this:
|
|
484
|
+
// Nestr sends those absolute already. This is for the URLs this server mints itself.
|
|
485
|
+
export function nestrWebBase(apiBase) {
|
|
486
|
+
const base = apiBase || "https://app.nestr.io/api";
|
|
487
|
+
return base.replace(/\/api\/?$/, "").replace(/\/+$/, "") || "https://app.nestr.io";
|
|
488
|
+
}
|
|
489
|
+
const NESTR_WEB_BASE = nestrWebBase(process.env.NESTR_API_BASE);
|
|
281
490
|
export function enrichHints(data) {
|
|
282
491
|
if (!data || typeof data !== "object")
|
|
283
492
|
return data;
|
|
@@ -285,12 +494,17 @@ export function enrichHints(data) {
|
|
|
285
494
|
if (Array.isArray(data)) {
|
|
286
495
|
return data.map((item) => enrichHints(item));
|
|
287
496
|
}
|
|
288
|
-
// Handle wrapped responses { data: [...] }
|
|
289
|
-
|
|
290
|
-
|
|
497
|
+
// Handle wrapped responses { data: [...] }. Enrich the payload, then fall through so
|
|
498
|
+
// the envelope's OWN hints are enriched too: a posts response carries unread_posts
|
|
499
|
+
// beside its data, and returning here left that hint as a bare URL.
|
|
500
|
+
let subject = data;
|
|
501
|
+
if ("data" in subject && Array.isArray(subject.data)) {
|
|
502
|
+
subject = { ...subject, data: enrichHints(subject.data) };
|
|
503
|
+
if (!Array.isArray(subject.hints))
|
|
504
|
+
return subject;
|
|
291
505
|
}
|
|
292
506
|
// Enrich hints on this nest
|
|
293
|
-
const record =
|
|
507
|
+
const record = subject;
|
|
294
508
|
if (Array.isArray(record.hints)) {
|
|
295
509
|
// Extract workspaceId from ancestors (last element is always the workspace)
|
|
296
510
|
const ancestors = record.ancestors;
|
|
@@ -308,10 +522,12 @@ export function enrichHints(data) {
|
|
|
308
522
|
else if (hint.url) {
|
|
309
523
|
// Legacy: single URL → toolCall. Kept for backwards compatibility with
|
|
310
524
|
// hints that pre-date the endpoints[] payload.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
525
|
+
// Strip an optional host, then an optional /api prefix. Previously only the
|
|
526
|
+
// host-qualified form was handled, so a bare "/api/..." hint matched no pattern
|
|
527
|
+
// and was reported as unrecognized. normalizeEndpointPath does both for the
|
|
528
|
+
// endpoints[] payload; this is the same rule for the legacy url field.
|
|
529
|
+
let rawUrl = hint.url.replace(/^https?:\/\/[^/]+/, "");
|
|
530
|
+
rawUrl = rawUrl.replace(/^\/api(?=\/)/, "");
|
|
315
531
|
const [path, queryString] = rawUrl.split("?");
|
|
316
532
|
const searchParams = new URLSearchParams(queryString || "");
|
|
317
533
|
let matched = false;
|
|
@@ -340,12 +556,10 @@ export function enrichHints(data) {
|
|
|
340
556
|
return enriched;
|
|
341
557
|
});
|
|
342
558
|
}
|
|
343
|
-
|
|
559
|
+
// subject, not data: the wrapped-response branch above works on a copy, so returning
|
|
560
|
+
// `data` would discard both the enriched payload and the enriched envelope hints.
|
|
561
|
+
return subject;
|
|
344
562
|
}
|
|
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
563
|
function buildNestUrl(id, parentId) {
|
|
350
564
|
if (parentId && parentId.toLowerCase() !== "inbox") {
|
|
351
565
|
return `${NESTR_WEB_BASE}/n/${parentId}/${id}`;
|
|
@@ -440,6 +654,48 @@ export const schemas = {
|
|
|
440
654
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
441
655
|
page: z.number().optional().describe("Page number (1-indexed) for pagination"),
|
|
442
656
|
}),
|
|
657
|
+
listDMs: z.object({
|
|
658
|
+
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."),
|
|
659
|
+
unread: z.boolean().optional().describe("true returns only threads with messages you have not read"),
|
|
660
|
+
includeCompleted: z.boolean().optional().describe("true also returns closed conversations. They are left out by default."),
|
|
661
|
+
limit: z.number().optional().describe("Threads per page (default 50, max 200)"),
|
|
662
|
+
page: z.number().optional().describe("Page number, 1-based"),
|
|
663
|
+
}),
|
|
664
|
+
startDMThread: z.object({
|
|
665
|
+
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."),
|
|
666
|
+
title: z.string().optional().describe("Optional thread title. Defaults to a dated one, as the app uses."),
|
|
667
|
+
}),
|
|
668
|
+
listQueues: z.object({}),
|
|
669
|
+
listQueueThreads: z.object({
|
|
670
|
+
key: z.string().describe("Queue key, e.g. 'support'. From nestr_list_queues."),
|
|
671
|
+
unread: z.boolean().optional().describe("true returns only threads you have not read"),
|
|
672
|
+
}),
|
|
673
|
+
getDMThread: z.object({
|
|
674
|
+
threadId: z.string().describe("Thread id"),
|
|
675
|
+
unread: z.boolean().optional().describe("true embeds the posts you have not read, false the ones you have. Omit for the thread alone."),
|
|
676
|
+
}),
|
|
677
|
+
updateDMThread: z.object({
|
|
678
|
+
threadId: z.string().describe("Thread id"),
|
|
679
|
+
title: z.string().optional().describe("New thread title"),
|
|
680
|
+
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."),
|
|
681
|
+
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."),
|
|
682
|
+
}),
|
|
683
|
+
getDMPosts: z.object({
|
|
684
|
+
threadId: z.string().describe("Thread id"),
|
|
685
|
+
unread: z.boolean().optional().describe("true for posts you have not read, false for the ones you have. Omit for all."),
|
|
686
|
+
depth: z.union([z.number(), z.literal("all")]).optional().describe("Include posts on descendant nests"),
|
|
687
|
+
}),
|
|
688
|
+
createDMPost: z.object({
|
|
689
|
+
threadId: z.string().describe("Thread id"),
|
|
690
|
+
body: z.string().describe("Message text. Supports HTML and Markdown."),
|
|
691
|
+
}),
|
|
692
|
+
markPostRead: z.object({
|
|
693
|
+
postId: z.string().describe("Post to mark read up to. Everything up to and including it becomes read."),
|
|
694
|
+
}),
|
|
695
|
+
escalateToSupport: z.object({
|
|
696
|
+
threadId: z.string().describe("Thread id to escalate. It must be a conversation Nestradamus is in."),
|
|
697
|
+
reason: z.string().describe("One or two sentences for whoever picks this up: what is needed and what has been tried."),
|
|
698
|
+
}),
|
|
443
699
|
getWorkspace: z.object({
|
|
444
700
|
workspaceId: z.string().describe("Workspace ID"),
|
|
445
701
|
}),
|
|
@@ -515,12 +771,12 @@ export const schemas = {
|
|
|
515
771
|
}),
|
|
516
772
|
addComment: z.object({
|
|
517
773
|
nestId: z.string().describe("Nest ID to comment on"),
|
|
518
|
-
body: z.string().describe("Comment text. Supports HTML and @mentions. **Mentions MUST be wrapped in literal curly braces** — write `@{aBcD1234eFgH5678i:roleNestId}`, NOT `@aBcD1234eFgH5678i`. Without the braces the platform will not link the mention or notify the user. Forms: `@{userId:roleId}` (preferred — addresses the user in a specific role/circle), `@{userId}`
|
|
774
|
+
body: z.string().describe("Comment text. Supports HTML and @mentions. **Mentions MUST be wrapped in literal curly braces** — write `@{aBcD1234eFgH5678i:roleNestId}`, NOT `@aBcD1234eFgH5678i`. Without the braces the platform will not link the mention or notify the user. Forms: `@{userId:roleId}` (preferred — addresses the user in a specific role/circle). The second id MUST be a ROLE or CIRCLE nest. Never the project, task or tension you are commenting on: the mention renders that nest's title where the role name belongs, so a project id produces 'Henk as Write a weekly blog post', which reads as though the project were his role. If you do not know which role the person is acting in, use `@{userId}` rather than substituting the nest you happen to be working on. Other forms: `@{userId}` (no role context), `@{email}`, `@{circle}` (all role fillers in nearest ancestor circle)."),
|
|
519
775
|
labels: z.array(z.string()).optional().describe("Optional label IDs to attach to the comment at creation time (e.g., 'decision', 'question', or a custom label ID). Personal labels are auto-scoped to the authenticated user. Use nestr_list_labels / nestr_list_personal_labels to discover IDs."),
|
|
520
776
|
}),
|
|
521
777
|
updateComment: z.object({
|
|
522
778
|
commentId: z.string().describe("Comment ID to update"),
|
|
523
|
-
body: z.string().describe("Updated comment text. Supports HTML and @mentions. **Mentions MUST be wrapped in literal curly braces** — write `@{aBcD1234eFgH5678i:roleNestId}`, NOT `@aBcD1234eFgH5678i`. Without the braces the platform will not link the mention or notify the user. Forms: `@{userId:roleId}` (preferred — addresses the user in a specific role/circle), `@{userId}`
|
|
779
|
+
body: z.string().describe("Updated comment text. Supports HTML and @mentions. **Mentions MUST be wrapped in literal curly braces** — write `@{aBcD1234eFgH5678i:roleNestId}`, NOT `@aBcD1234eFgH5678i`. Without the braces the platform will not link the mention or notify the user. Forms: `@{userId:roleId}` (preferred — addresses the user in a specific role/circle). The second id MUST be a ROLE or CIRCLE nest. Never the project, task or tension you are commenting on: the mention renders that nest's title where the role name belongs, so a project id produces 'Henk as Write a weekly blog post', which reads as though the project were his role. If you do not know which role the person is acting in, use `@{userId}` rather than substituting the nest you happen to be working on. Other forms: `@{userId}` (no role context), `@{email}`, `@{circle}` (all role fillers in nearest ancestor circle)."),
|
|
524
780
|
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
781
|
}),
|
|
526
782
|
deleteComment: z.object({
|
|
@@ -582,6 +838,7 @@ export const schemas = {
|
|
|
582
838
|
getComments: z.object({
|
|
583
839
|
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
840
|
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."),
|
|
841
|
+
unread: z.boolean().optional().describe("true for comments you have not read, false for the ones you have. Omit for all."),
|
|
585
842
|
}),
|
|
586
843
|
getCircle: z.object({
|
|
587
844
|
workspaceId: z.string().describe("Workspace ID"),
|
|
@@ -842,20 +1099,70 @@ export const schemas = {
|
|
|
842
1099
|
listConnectors: z.object({
|
|
843
1100
|
workspaceId: z.string().describe("Workspace ID whose connector catalog to list"),
|
|
844
1101
|
}),
|
|
1102
|
+
listConnectorTemplates: z.object({
|
|
1103
|
+
workspaceId: z.string().describe("Workspace ID whose available connector templates to list"),
|
|
1104
|
+
}),
|
|
845
1105
|
registerConnector: z.object({
|
|
1106
|
+
templateId: z.string().optional().describe("Id of a template from nestr_list_connector_templates. Given this, everything else is filled in from the template and you should omit type/config/capabilities/exposure/authStrategy. ALWAYS prefer this over hand-registering a vendor the deployment already knows."),
|
|
846
1107
|
workspaceId: z.string().describe("Workspace ID to register the connector in"),
|
|
847
|
-
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
|
-
name: z.string().describe("Unique connector name within the workspace catalog"),
|
|
1108
|
+
type: z.enum(["mcp", "cli", "api"]).optional().describe("Transport: 'mcp' (MCP server over a url), 'api' (REST endpoint over a url), or 'cli' (a command). Required unless templateId is given."),
|
|
1109
|
+
name: z.string().optional().describe("Unique connector name within the workspace catalog. Required unless templateId is given, where it defaults to the template's own name."),
|
|
849
1110
|
config: coerceFromJson(z.record(z.unknown())).optional().describe("Per-type transport config, no secret. mcp/api need a url (e.g., { url: 'https://...' }); cli needs a command (e.g., { command: 'some-cli' }). Optional non-secret headers go under headers."),
|
|
850
1111
|
capabilities: coerceFromJson(z.record(z.unknown())).optional().describe("Capability descriptor: { discover: boolean, tools: [{ name, description, inputSchema }] }. discover:true lets the connector self-describe its tools at runtime."),
|
|
851
1112
|
exposure: coerceFromJson(z.record(z.unknown())).optional().describe("Exposure policy deciding which owners may bind: { userAgent: boolean, domainGated: boolean }. Set domainGated:true to allow binding to a role's domain."),
|
|
852
1113
|
authStrategy: z.enum(["secret", "oauth2"]).optional().describe("How a principal connects: 'secret' (a one-time secret captured via the Connect button) or 'oauth2'. The agent never sees the secret."),
|
|
853
1114
|
}),
|
|
1115
|
+
createAgent: z.object({
|
|
1116
|
+
workspaceId: z.string().describe("Workspace ID to create the agent in"),
|
|
1117
|
+
name: z.string().describe("The AGENT's own name, as its identity calls it (e.g. 'Collab'). Not the name of the work: that belongs to the role this agent will fill."),
|
|
1118
|
+
agentConfig: coerceFromJson(z.record(z.unknown())).optional().describe("Runtime wiring, not persona: { runtimeCallbackUrl (https, or http to a *.svc.cluster.local service), tokenTtlSeconds (30-1800) }. Omit for an agent that runs on Nestr's own runtime."),
|
|
1119
|
+
}),
|
|
854
1120
|
bindConnector: z.object({
|
|
855
1121
|
workspaceId: z.string().describe("Workspace ID the connector and owner belong to"),
|
|
856
1122
|
connectorId: z.string().describe("ID of an enabled connector from nestr_list_connectors"),
|
|
857
|
-
ownerType: z.enum(["
|
|
858
|
-
ownerId: z.string().describe("Owner ID.
|
|
1123
|
+
ownerType: z.enum(["role", "role-domain", "workspace"]).describe("Who gets access. 'role' is usually what you want: pass a role nest ID and the connector's domain is found or created under it. 'role-domain' targets an existing domain directly. 'workspace' gives everyone. Personal owners ('user', 'agent') are deliberately not available here: see the tool description."),
|
|
1124
|
+
ownerId: z.string().describe("Owner ID. role: the role nest ID. role-domain: the domain nest ID. workspace: the workspace ID."),
|
|
1125
|
+
}),
|
|
1126
|
+
updateConnector: z.object({
|
|
1127
|
+
workspaceId: z.string().describe("Workspace ID the connector belongs to"),
|
|
1128
|
+
connectorId: z.string().describe("ID of the connector to update"),
|
|
1129
|
+
type: z.enum(["mcp", "cli", "api"]).optional().describe("Transport"),
|
|
1130
|
+
name: z.string().optional().describe("Unique connector name within the workspace catalog"),
|
|
1131
|
+
config: coerceFromJson(z.record(z.unknown())).optional().describe("Per-type transport config, no secret"),
|
|
1132
|
+
capabilities: coerceFromJson(z.record(z.unknown())).optional().describe("Capability descriptor"),
|
|
1133
|
+
exposure: coerceFromJson(z.record(z.unknown())).optional().describe("Exposure policy: { userAgent, domainGated }"),
|
|
1134
|
+
authStrategy: z.enum(["secret", "oauth2"]).optional().describe("How a principal connects"),
|
|
1135
|
+
enabled: z.boolean().optional().describe("Switch the connector on or off in this workspace"),
|
|
1136
|
+
}),
|
|
1137
|
+
removeConnector: z.object({
|
|
1138
|
+
workspaceId: z.string().describe("Workspace ID the connector belongs to"),
|
|
1139
|
+
connectorId: z.string().describe("ID of the connector to remove from the catalog"),
|
|
1140
|
+
}),
|
|
1141
|
+
listConnections: z.object({
|
|
1142
|
+
workspaceId: z.string().describe("Workspace ID whose bindings to list"),
|
|
1143
|
+
includeDisabled: z.boolean().optional().describe("Include removed (disabled) bindings. Default false."),
|
|
1144
|
+
}),
|
|
1145
|
+
removeConnection: z.object({
|
|
1146
|
+
workspaceId: z.string().describe("Workspace ID the binding belongs to"),
|
|
1147
|
+
connectionId: z.string().describe("ID of the binding to remove, from nestr_list_connections"),
|
|
1148
|
+
}),
|
|
1149
|
+
getConnectLink: z.object({
|
|
1150
|
+
workspaceId: z.string().describe("Workspace ID the binding belongs to"),
|
|
1151
|
+
connectionId: z.string().describe("ID of the binding to connect, from nestr_list_connections"),
|
|
1152
|
+
}),
|
|
1153
|
+
revokeConnectionCredential: z.object({
|
|
1154
|
+
workspaceId: z.string().describe("Workspace ID the binding belongs to"),
|
|
1155
|
+
connectionId: z.string().describe("ID of the binding whose credential to revoke"),
|
|
1156
|
+
}),
|
|
1157
|
+
getAgentConnectorReach: z.object({
|
|
1158
|
+
workspaceId: z.string().describe("Workspace ID the agent belongs to"),
|
|
1159
|
+
agentUserId: z.string().describe("The agent's bot user ID"),
|
|
1160
|
+
}),
|
|
1161
|
+
runAgent: z.object({
|
|
1162
|
+
workspaceId: z.string().describe("Workspace ID the agent belongs to"),
|
|
1163
|
+
agentUserId: z.string().describe("The agent's bot user ID"),
|
|
1164
|
+
nestId: z.string().describe("The nest the run is pinned to: a role, a project, a task"),
|
|
1165
|
+
message: z.string().optional().describe("What this run is for. Omit for a plain 'advance this item' run."),
|
|
859
1166
|
}),
|
|
860
1167
|
// File attachments (a comment id works as the nestId — files are keyed by nestId)
|
|
861
1168
|
getNestFiles: z.object({
|
|
@@ -1161,12 +1468,12 @@ export const toolDefinitions = [
|
|
|
1161
1468
|
},
|
|
1162
1469
|
{
|
|
1163
1470
|
name: "nestr_add_comment",
|
|
1164
|
-
description: "Add a comment to a nest. Supports HTML and @mentions — **mentions MUST be wrapped in literal curly braces** (e.g. `@{aBcD1234eFgH5678i:roleNestId}`, NOT `@aBcD1234eFgH5678i`); without the braces the user is not notified. Prefer `@{userId:roleId}` so the recipient knows which role they're being addressed in. Use for progress updates and discussion. Optionally attach labels at creation time via the `labels` parameter.",
|
|
1471
|
+
description: "Add a comment to a nest. Supports HTML and @mentions — **mentions MUST be wrapped in literal curly braces** (e.g. `@{aBcD1234eFgH5678i:roleNestId}`, NOT `@aBcD1234eFgH5678i`); without the braces the user is not notified. Prefer `@{userId:roleId}` so the recipient knows which role they're being addressed in; the second id must be a role or circle nest, never the project or task being commented on. Use for progress updates and discussion. Optionally attach labels at creation time via the `labels` parameter.",
|
|
1165
1472
|
inputSchema: {
|
|
1166
1473
|
type: "object",
|
|
1167
1474
|
properties: {
|
|
1168
1475
|
nestId: { type: "string", description: "ID of the nest or conversation the comment belongs to. Passing a comment ID instead replies to that comment, inside its thread. A direct-message conversation is flat and holds no threads, so a message ID there is moved onto the conversation and the response says where the comment landed." },
|
|
1169
|
-
body: { type: "string", description: "Comment text. Supports HTML and @mentions. **Mentions MUST be wrapped in literal curly braces** — write `@{aBcD1234eFgH5678i:roleNestId}`, NOT `@aBcD1234eFgH5678i`. Without the braces the platform will not link the mention or notify the user. Forms: `@{userId:roleId}` (preferred — addresses the user in a specific role/circle), `@{userId}`
|
|
1476
|
+
body: { type: "string", description: "Comment text. Supports HTML and @mentions. **Mentions MUST be wrapped in literal curly braces** — write `@{aBcD1234eFgH5678i:roleNestId}`, NOT `@aBcD1234eFgH5678i`. Without the braces the platform will not link the mention or notify the user. Forms: `@{userId:roleId}` (preferred — addresses the user in a specific role/circle). The second id MUST be a ROLE or CIRCLE nest. Never the project, task or tension you are commenting on: the mention renders that nest's title where the role name belongs, so a project id produces 'Henk as Write a weekly blog post', which reads as though the project were his role. If you do not know which role the person is acting in, use `@{userId}` rather than substituting the nest you happen to be working on. Other forms: `@{userId}` (no role context), `@{email}`, `@{circle}` (all role fillers in nearest ancestor circle)." },
|
|
1170
1477
|
labels: {
|
|
1171
1478
|
type: "array",
|
|
1172
1479
|
items: { type: "string" },
|
|
@@ -1184,7 +1491,7 @@ export const toolDefinitions = [
|
|
|
1184
1491
|
type: "object",
|
|
1185
1492
|
properties: {
|
|
1186
1493
|
commentId: { type: "string", description: "Comment ID to update" },
|
|
1187
|
-
body: { type: "string", description: "Updated comment text. Supports HTML and @mentions. **Mentions MUST be wrapped in literal curly braces** — write `@{aBcD1234eFgH5678i:roleNestId}`, NOT `@aBcD1234eFgH5678i`. Without the braces the platform will not link the mention or notify the user. Forms: `@{userId:roleId}` (preferred — addresses the user in a specific role/circle), `@{userId}`
|
|
1494
|
+
body: { type: "string", description: "Updated comment text. Supports HTML and @mentions. **Mentions MUST be wrapped in literal curly braces** — write `@{aBcD1234eFgH5678i:roleNestId}`, NOT `@aBcD1234eFgH5678i`. Without the braces the platform will not link the mention or notify the user. Forms: `@{userId:roleId}` (preferred — addresses the user in a specific role/circle). The second id MUST be a ROLE or CIRCLE nest. Never the project, task or tension you are commenting on: the mention renders that nest's title where the role name belongs, so a project id produces 'Henk as Write a weekly blog post', which reads as though the project were his role. If you do not know which role the person is acting in, use `@{userId}` rather than substituting the nest you happen to be working on. Other forms: `@{userId}` (no role context), `@{email}`, `@{circle}` (all role fillers in nearest ancestor circle)." },
|
|
1188
1495
|
labels: {
|
|
1189
1496
|
type: "array",
|
|
1190
1497
|
items: { type: "string" },
|
|
@@ -1268,6 +1575,140 @@ export const toolDefinitions = [
|
|
|
1268
1575
|
},
|
|
1269
1576
|
...readOnly,
|
|
1270
1577
|
},
|
|
1578
|
+
// ---- Direct messages ----
|
|
1579
|
+
// A thread is the unit and its id is the whole address. There is no container to fetch
|
|
1580
|
+
// first: start from nestr_list_dms, optionally narrowed to one person with withUser.
|
|
1581
|
+
{
|
|
1582
|
+
name: "nestr_list_dms",
|
|
1583
|
+
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.",
|
|
1584
|
+
inputSchema: {
|
|
1585
|
+
type: "object",
|
|
1586
|
+
properties: {
|
|
1587
|
+
withUser: { type: "string", description: "Only threads with this person: user id, username or email" },
|
|
1588
|
+
unread: { type: "boolean", description: "Only threads with messages you have not read" },
|
|
1589
|
+
includeCompleted: { type: "boolean", description: "Also return closed conversations (left out by default)" },
|
|
1590
|
+
limit: { type: "number", description: "Threads per page (default 50, max 200)" },
|
|
1591
|
+
page: { type: "number", description: "Page number, 1-based" },
|
|
1592
|
+
},
|
|
1593
|
+
},
|
|
1594
|
+
...readOnly,
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
name: "nestr_start_dm_thread",
|
|
1598
|
+
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.",
|
|
1599
|
+
inputSchema: {
|
|
1600
|
+
type: "object",
|
|
1601
|
+
properties: {
|
|
1602
|
+
user: { type: "string", description: "Who to message: user id, username or email" },
|
|
1603
|
+
title: { type: "string", description: "Optional title. Defaults to a dated one, as the app uses." },
|
|
1604
|
+
},
|
|
1605
|
+
required: ["user"],
|
|
1606
|
+
},
|
|
1607
|
+
...mutating,
|
|
1608
|
+
},
|
|
1609
|
+
// ---- Support queues ----
|
|
1610
|
+
// A queue is a label on threads across many DM spaces, not a space itself. It hands
|
|
1611
|
+
// back thread ids, and a thread id is the whole address: nestr_get_dm_thread /
|
|
1612
|
+
// nestr_get_dm_posts take it directly.
|
|
1613
|
+
{
|
|
1614
|
+
name: "nestr_list_queues",
|
|
1615
|
+
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.",
|
|
1616
|
+
inputSchema: { type: "object", properties: {} },
|
|
1617
|
+
...readOnly,
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
name: "nestr_list_queue_threads",
|
|
1621
|
+
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.",
|
|
1622
|
+
inputSchema: {
|
|
1623
|
+
type: "object",
|
|
1624
|
+
properties: {
|
|
1625
|
+
key: { type: "string", description: "Queue key, e.g. 'support'" },
|
|
1626
|
+
unread: { type: "boolean", description: "Only threads you have not read" },
|
|
1627
|
+
},
|
|
1628
|
+
required: ["key"],
|
|
1629
|
+
},
|
|
1630
|
+
...readOnly,
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
name: "nestr_get_dm_thread",
|
|
1634
|
+
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.",
|
|
1635
|
+
inputSchema: {
|
|
1636
|
+
type: "object",
|
|
1637
|
+
properties: {
|
|
1638
|
+
threadId: { type: "string", description: "Thread id" },
|
|
1639
|
+
unread: { type: "boolean", description: "true embeds unread posts, false embeds read ones" },
|
|
1640
|
+
},
|
|
1641
|
+
required: ["threadId"],
|
|
1642
|
+
},
|
|
1643
|
+
...readOnly,
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
name: "nestr_update_dm_thread",
|
|
1647
|
+
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.",
|
|
1648
|
+
inputSchema: {
|
|
1649
|
+
type: "object",
|
|
1650
|
+
properties: {
|
|
1651
|
+
threadId: { type: "string", description: "Thread id" },
|
|
1652
|
+
title: { type: "string", description: "New thread title" },
|
|
1653
|
+
completed: { type: ["boolean", "null"], description: "true closes the conversation, null reopens it" },
|
|
1654
|
+
users: { type: "array", items: { type: "string" }, description: "The participant list you want, replacing the current one" },
|
|
1655
|
+
},
|
|
1656
|
+
required: ["threadId"],
|
|
1657
|
+
},
|
|
1658
|
+
...mutating,
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
name: "nestr_get_dm_posts",
|
|
1662
|
+
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.",
|
|
1663
|
+
inputSchema: {
|
|
1664
|
+
type: "object",
|
|
1665
|
+
properties: {
|
|
1666
|
+
threadId: { type: "string", description: "Thread id" },
|
|
1667
|
+
unread: { type: "boolean", description: "true for unread posts, false for read ones. Omit for all." },
|
|
1668
|
+
depth: { type: ["number", "string"], description: "Include posts on descendant nests, or 'all'" },
|
|
1669
|
+
},
|
|
1670
|
+
required: ["threadId"],
|
|
1671
|
+
},
|
|
1672
|
+
...readOnly,
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
name: "nestr_post_dm_message",
|
|
1676
|
+
description: "Post a message into a direct-message thread.",
|
|
1677
|
+
inputSchema: {
|
|
1678
|
+
type: "object",
|
|
1679
|
+
properties: {
|
|
1680
|
+
threadId: { type: "string", description: "Thread id" },
|
|
1681
|
+
body: { type: "string", description: "Message text. Supports HTML and Markdown." },
|
|
1682
|
+
},
|
|
1683
|
+
required: ["threadId", "body"],
|
|
1684
|
+
},
|
|
1685
|
+
...mutating,
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
name: "nestr_mark_post_read",
|
|
1689
|
+
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.",
|
|
1690
|
+
inputSchema: {
|
|
1691
|
+
type: "object",
|
|
1692
|
+
properties: {
|
|
1693
|
+
postId: { type: "string", description: "Post to mark read up to" },
|
|
1694
|
+
},
|
|
1695
|
+
required: ["postId"],
|
|
1696
|
+
},
|
|
1697
|
+
...mutating,
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
name: "nestr_escalate_to_support",
|
|
1701
|
+
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.",
|
|
1702
|
+
inputSchema: {
|
|
1703
|
+
type: "object",
|
|
1704
|
+
properties: {
|
|
1705
|
+
threadId: { type: "string", description: "Thread id to escalate" },
|
|
1706
|
+
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." },
|
|
1707
|
+
},
|
|
1708
|
+
required: ["threadId", "reason"],
|
|
1709
|
+
},
|
|
1710
|
+
...mutating,
|
|
1711
|
+
},
|
|
1271
1712
|
{
|
|
1272
1713
|
name: "nestr_get_insights",
|
|
1273
1714
|
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').",
|
|
@@ -1347,7 +1788,7 @@ export const toolDefinitions = [
|
|
|
1347
1788
|
},
|
|
1348
1789
|
{
|
|
1349
1790
|
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.",
|
|
1791
|
+
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
1792
|
inputSchema: {
|
|
1352
1793
|
type: "object",
|
|
1353
1794
|
properties: {
|
|
@@ -1390,7 +1831,10 @@ export const toolDefinitions = [
|
|
|
1390
1831
|
},
|
|
1391
1832
|
{
|
|
1392
1833
|
name: "nestr_add_workspace_user",
|
|
1393
|
-
description: "Add a user to a workspace by email. Creates account if
|
|
1834
|
+
description: "Add a user to a workspace by email. Creates the account if it does not exist, adds them, "
|
|
1835
|
+
+ "and SENDS THEM AN INVITE EMAIL, so confirm with the person asking before you call it: a real "
|
|
1836
|
+
+ "person receives that mail. This is the action behind requests about seats, membership, "
|
|
1837
|
+
+ "extending a plan by a person, or getting a colleague in.",
|
|
1394
1838
|
inputSchema: {
|
|
1395
1839
|
type: "object",
|
|
1396
1840
|
properties: {
|
|
@@ -2055,19 +2499,38 @@ export const toolDefinitions = [
|
|
|
2055
2499
|
},
|
|
2056
2500
|
...readOnly,
|
|
2057
2501
|
},
|
|
2502
|
+
{
|
|
2503
|
+
name: "nestr_list_connector_templates",
|
|
2504
|
+
description: "The connector templates this deployment can add in one click, filtered to the ones it can actually offer. Each carries the vendor's real endpoint, transport, auth strategy and the deployment's OAuth client.\n\nCALL THIS FIRST, before nestr_register_connector, whenever the tool is a known vendor (Xero, HubSpot, Slack, Stripe, GitHub, Notion, Linear and so on). Hand-registering means guessing an endpoint, and a wrong guess authorises cleanly and then fails every call: a Xero connector registered against api.xero.com instead of the template's mcp.xero.com looked healthy in every record and returned 403 forever. Pass the id you find here as templateId to nestr_register_connector. Workspace-admin only.",
|
|
2505
|
+
inputSchema: {
|
|
2506
|
+
type: "object",
|
|
2507
|
+
properties: {
|
|
2508
|
+
workspaceId: { type: "string", description: "Workspace ID whose available connector templates to list" },
|
|
2509
|
+
},
|
|
2510
|
+
required: ["workspaceId"],
|
|
2511
|
+
},
|
|
2512
|
+
...readOnly,
|
|
2513
|
+
},
|
|
2058
2514
|
{
|
|
2059
2515
|
name: "nestr_register_connector",
|
|
2060
|
-
description: "Register a connector in the workspace catalog: a reusable mcp / cli / api template that holds no secret. Workspace-admin only. A non-admin caller gets AUTH_SCOPE_INSUFFICIENT (call nestr_diagnose on any auth error). Provide type ('mcp' or 'api' need a url in config; 'cli' needs a command) and a unique name; optionally capabilities, exposure ({ userAgent, domainGated }), and authStrategy ('secret' or 'oauth2'). This only creates the template. Typical flow: register here, then bind it to a role's
|
|
2516
|
+
description: "Register a connector in the workspace catalog. PREFER A TEMPLATE: call nestr_list_connector_templates first and pass its id as templateId, which fills in the vendor's real endpoint, transport, auth strategy and this deployment's OAuth client. Hand-registering a known vendor means guessing an endpoint, and a wrong guess authorises cleanly and then fails every call. Only describe the transport yourself for something the deployment has no template for. A reusable mcp / cli / api template that holds no secret. Workspace-admin only. A non-admin caller gets AUTH_SCOPE_INSUFFICIENT (call nestr_diagnose on any auth error). Provide type ('mcp' or 'api' need a url in config; 'cli' needs a command) and a unique name; optionally capabilities, exposure ({ userAgent, domainGated }), and authStrategy ('secret' or 'oauth2'). This only creates the template. Typical flow: register here, then bind it to a role's DOMAIN with nestr_bind_connector (create one under the role with nestr_create_nest and labels ['circleplus-domain'] if the role has none yet: the bind refuses a role id), then a human or agent connects the account via the credentials field's Connect button. The secret is captured out-of-band through that button, never by the agent.",
|
|
2061
2517
|
inputSchema: {
|
|
2062
2518
|
type: "object",
|
|
2063
2519
|
properties: {
|
|
2520
|
+
templateId: {
|
|
2521
|
+
type: "string",
|
|
2522
|
+
description: "Id of a template from nestr_list_connector_templates. Given this, everything else is filled in from the template and you should omit type/config/capabilities/exposure/authStrategy. ALWAYS prefer this over hand-registering a vendor the deployment already knows.",
|
|
2523
|
+
},
|
|
2064
2524
|
workspaceId: { type: "string", description: "Workspace ID to register the connector in" },
|
|
2065
2525
|
type: {
|
|
2066
2526
|
type: "string",
|
|
2067
2527
|
enum: ["mcp", "cli", "api"],
|
|
2068
|
-
description: "Transport: 'mcp' (MCP server over a url), 'api' (REST endpoint over a url), or 'cli' (a command)",
|
|
2528
|
+
description: "Transport: 'mcp' (MCP server over a url), 'api' (REST endpoint over a url), or 'cli' (a command). Required unless templateId is given.",
|
|
2529
|
+
},
|
|
2530
|
+
name: {
|
|
2531
|
+
type: "string",
|
|
2532
|
+
description: "Unique connector name within the workspace catalog. Required unless templateId is given, where it defaults to the template's own name.",
|
|
2069
2533
|
},
|
|
2070
|
-
name: { type: "string", description: "Unique connector name within the workspace catalog" },
|
|
2071
2534
|
config: {
|
|
2072
2535
|
type: "object",
|
|
2073
2536
|
description: "Per-type transport config, no secret. mcp/api need a url (e.g., { url: 'https://...' }); cli needs a command (e.g., { command: 'some-cli' }). Optional non-secret headers go under headers.",
|
|
@@ -2086,13 +2549,36 @@ export const toolDefinitions = [
|
|
|
2086
2549
|
description: "How a principal connects: 'secret' (a one-time secret captured via the Connect button) or 'oauth2'. The agent never sees the secret.",
|
|
2087
2550
|
},
|
|
2088
2551
|
},
|
|
2089
|
-
|
|
2552
|
+
// workspaceId only: with a templateId the transport comes from the template,
|
|
2553
|
+
// and demanding type and name here is what made the whole template path
|
|
2554
|
+
// unreachable from a client that reads the schema.
|
|
2555
|
+
required: ["workspaceId"],
|
|
2556
|
+
},
|
|
2557
|
+
...mutating,
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
name: "nestr_create_agent",
|
|
2561
|
+
description: "Create an agent user in the workspace. Workspace-admin only; a non-admin caller gets AUTH_SCOPE_INSUFFICIENT (call nestr_diagnose on any auth error). The agent is added to the workspace and can then fill roles like a person does.\n\nAn agent and the role it fills are two different things, named differently: the agent carries its own name (Collab), the role is named for the WORK (Marketing). Do not name the role after the agent. Create the agent here, create or find the role with nestr_create_nest, then assign the agent to the role with nestr_update_nest users. Keeping them apart is what lets the agent be replaced without the role losing its purpose, accountabilities and history, and lets one agent fill several roles.\n\nThe agent's instructions do not go here: they belong in a skill nest under the role, which loads whenever the role acts. agentConfig is runtime wiring only.",
|
|
2562
|
+
inputSchema: {
|
|
2563
|
+
type: "object",
|
|
2564
|
+
properties: {
|
|
2565
|
+
workspaceId: { type: "string", description: "Workspace ID to create the agent in" },
|
|
2566
|
+
name: {
|
|
2567
|
+
type: "string",
|
|
2568
|
+
description: "The AGENT's own name, as its identity calls it (e.g. 'Collab'). Not the name of the work: that belongs to the role this agent will fill.",
|
|
2569
|
+
},
|
|
2570
|
+
agentConfig: {
|
|
2571
|
+
type: "object",
|
|
2572
|
+
description: "Runtime wiring, not persona: { runtimeCallbackUrl (https, or http to a *.svc.cluster.local service), tokenTtlSeconds (30-1800) }. Omit for an agent that runs on Nestr's own runtime.",
|
|
2573
|
+
},
|
|
2574
|
+
},
|
|
2575
|
+
required: ["workspaceId", "name"],
|
|
2090
2576
|
},
|
|
2091
2577
|
...mutating,
|
|
2092
2578
|
},
|
|
2093
2579
|
{
|
|
2094
2580
|
name: "nestr_bind_connector",
|
|
2095
|
-
description: "Bind a registered connector to an owner so that owner can use it. Owner types: '
|
|
2581
|
+
description: "Bind a registered connector to an owner so that owner can use it. Owner types: 'role' (ownerId is the role nest ID — the server finds or creates the connector's domain under it), 'role-domain' (ownerId is an existing domain nest ID), or 'workspace' (ownerId is the workspace ID). A 'role' or 'role-domain' owner materialises a credentials field on the domain nest, so the role can use the connector and the Connect button renders there; the response then includes credentialsField { domainId, fieldId, fieldCode }. After binding, a human or agent connects the account via that Connect button. The secret is captured out-of-band and is never seen by the agent. Workspace-admin only: a non-admin caller gets AUTH_SCOPE_INSUFFICIENT. The connector must already be registered (nestr_register_connector) and enabled.\n\nBIND TO THE ROLE, not to whoever fills it. A role binding is the governance act: the access belongs to the work, survives the filler changing, and is visible to the circle. Reach for 'role' by default — it is the usual onboarding path: pass the role nest ID and the server does the domain lookup. Use 'role-domain' only when you already have the domain nest ID and want to target it directly.\n\nPersonal owners ('user' and 'agent') are deliberately unavailable through this tool. They are for things that are genuinely one person's or one bot's, an individual mailbox being the usual case, and handing one agent the power to attach a credential to ANOTHER agent is not a decision to make from a tool call. When a personal binding is really what is wanted, say so and let a workspace admin set it up in the agent's own panel in the UI.",
|
|
2096
2582
|
inputSchema: {
|
|
2097
2583
|
type: "object",
|
|
2098
2584
|
properties: {
|
|
@@ -2100,21 +2586,132 @@ export const toolDefinitions = [
|
|
|
2100
2586
|
connectorId: { type: "string", description: "ID of an enabled connector from nestr_list_connectors" },
|
|
2101
2587
|
ownerType: {
|
|
2102
2588
|
type: "string",
|
|
2103
|
-
enum: ["
|
|
2104
|
-
description: "Owner type. 'role
|
|
2589
|
+
enum: ["role", "workspace", "role-domain"],
|
|
2590
|
+
description: "Owner type. 'role' is the usual path: pass the role nest ID and the server finds or creates the connector's domain under it. 'role-domain' targets an existing domain directly. 'workspace' gives everyone. Personal owners ('user', 'agent') are deliberately not available here.",
|
|
2105
2591
|
},
|
|
2106
2592
|
ownerId: {
|
|
2107
2593
|
type: "string",
|
|
2108
|
-
description: "Owner ID.
|
|
2594
|
+
description: "Owner ID. role: the role nest ID. role-domain: the domain nest ID. workspace: the workspace ID.",
|
|
2109
2595
|
},
|
|
2110
2596
|
},
|
|
2111
2597
|
required: ["workspaceId", "connectorId", "ownerType", "ownerId"],
|
|
2112
2598
|
},
|
|
2113
2599
|
...mutating,
|
|
2114
2600
|
},
|
|
2601
|
+
{
|
|
2602
|
+
name: "nestr_update_connector",
|
|
2603
|
+
description: "Update a connector in the workspace catalog, or switch it on and off with `enabled`. Workspace-admin only. Switching it off, or narrowing its exposure, takes effect immediately everywhere it is used: the policy is re-read every time a credential is handed out, not only when access was given.",
|
|
2604
|
+
inputSchema: {
|
|
2605
|
+
type: "object",
|
|
2606
|
+
properties: {
|
|
2607
|
+
workspaceId: { type: "string", description: "Workspace ID the connector belongs to" },
|
|
2608
|
+
connectorId: { type: "string", description: "ID of the connector to update" },
|
|
2609
|
+
type: { type: "string", enum: ["mcp", "cli", "api"], description: "Transport" },
|
|
2610
|
+
name: { type: "string", description: "Unique connector name within the workspace catalog" },
|
|
2611
|
+
config: { type: "object", description: "Per-type transport config, no secret" },
|
|
2612
|
+
capabilities: { type: "object", description: "Capability descriptor" },
|
|
2613
|
+
exposure: { type: "object", description: "Exposure policy: { userAgent, domainGated }" },
|
|
2614
|
+
authStrategy: { type: "string", enum: ["secret", "oauth2"], description: "How a principal connects" },
|
|
2615
|
+
enabled: { type: "boolean", description: "Switch the connector on or off in this workspace" },
|
|
2616
|
+
},
|
|
2617
|
+
required: ["workspaceId", "connectorId"],
|
|
2618
|
+
},
|
|
2619
|
+
...mutating,
|
|
2620
|
+
},
|
|
2621
|
+
{
|
|
2622
|
+
name: "nestr_remove_connector",
|
|
2623
|
+
description: "Remove a connector from the workspace catalog. Workspace-admin only. Bindings that named it stop resolving, so prefer nestr_update_connector with enabled:false when you only want to pause it.",
|
|
2624
|
+
inputSchema: {
|
|
2625
|
+
type: "object",
|
|
2626
|
+
properties: {
|
|
2627
|
+
workspaceId: { type: "string", description: "Workspace ID the connector belongs to" },
|
|
2628
|
+
connectorId: { type: "string", description: "ID of the connector to remove" },
|
|
2629
|
+
},
|
|
2630
|
+
required: ["workspaceId", "connectorId"],
|
|
2631
|
+
},
|
|
2632
|
+
...destructive,
|
|
2633
|
+
},
|
|
2634
|
+
{
|
|
2635
|
+
name: "nestr_list_connections",
|
|
2636
|
+
description: "List who has access to what in this workspace: each binding's connector, its owner (a role's domain, a person, an agent, or the whole workspace), and who holds a credential on it. Shows when an agent is using a person's account, and never returns a secret. Use it to check whether access already exists before giving more, and to find the connectionId for nestr_get_connect_link.",
|
|
2637
|
+
inputSchema: {
|
|
2638
|
+
type: "object",
|
|
2639
|
+
properties: {
|
|
2640
|
+
workspaceId: { type: "string", description: "Workspace ID whose bindings to list" },
|
|
2641
|
+
includeDisabled: { type: "boolean", description: "Include removed bindings. Default false." },
|
|
2642
|
+
},
|
|
2643
|
+
required: ["workspaceId"],
|
|
2644
|
+
},
|
|
2645
|
+
},
|
|
2646
|
+
{
|
|
2647
|
+
name: "nestr_remove_connection",
|
|
2648
|
+
description: "Take a connector off an owner: the binding is removed and every credential on it revoked. Workspace-admin only. A domain left holding nothing goes back to being an ordinary descriptive domain.",
|
|
2649
|
+
inputSchema: {
|
|
2650
|
+
type: "object",
|
|
2651
|
+
properties: {
|
|
2652
|
+
workspaceId: { type: "string", description: "Workspace ID the binding belongs to" },
|
|
2653
|
+
connectionId: { type: "string", description: "Binding ID from nestr_list_connections" },
|
|
2654
|
+
},
|
|
2655
|
+
required: ["workspaceId", "connectionId"],
|
|
2656
|
+
},
|
|
2657
|
+
...destructive,
|
|
2658
|
+
},
|
|
2659
|
+
{
|
|
2660
|
+
name: "nestr_get_connect_link",
|
|
2661
|
+
description: "Get a link a PERSON opens to connect an account for a binding. This is how you finish setting up access: you can register a connector and give a role access, but you must never handle a raw token, so the sign-in or key entry happens behind this link. The link carries no authority — whoever opens it is checked then. Give it to the user in your reply.",
|
|
2662
|
+
inputSchema: {
|
|
2663
|
+
type: "object",
|
|
2664
|
+
properties: {
|
|
2665
|
+
workspaceId: { type: "string", description: "Workspace ID the binding belongs to" },
|
|
2666
|
+
connectionId: { type: "string", description: "Binding ID from nestr_list_connections" },
|
|
2667
|
+
},
|
|
2668
|
+
required: ["workspaceId", "connectionId"],
|
|
2669
|
+
},
|
|
2670
|
+
...mutating,
|
|
2671
|
+
},
|
|
2672
|
+
{
|
|
2673
|
+
name: "nestr_revoke_connection_credential",
|
|
2674
|
+
description: "Revoke the calling user's credential on a binding. The binding stays, so access can be restored by connecting again. Use nestr_remove_connection to remove the access entirely.",
|
|
2675
|
+
inputSchema: {
|
|
2676
|
+
type: "object",
|
|
2677
|
+
properties: {
|
|
2678
|
+
workspaceId: { type: "string", description: "Workspace ID the binding belongs to" },
|
|
2679
|
+
connectionId: { type: "string", description: "Binding ID from nestr_list_connections" },
|
|
2680
|
+
},
|
|
2681
|
+
required: ["workspaceId", "connectionId"],
|
|
2682
|
+
},
|
|
2683
|
+
...destructive,
|
|
2684
|
+
},
|
|
2685
|
+
{
|
|
2686
|
+
name: "nestr_get_agent_connectors",
|
|
2687
|
+
description: "What an agent can and cannot use, and why. Groups each connector by where the grant comes from (its own binding, the workspace, or a role it fills) and, when unavailable, names the reason: no credential yet, the connector is disabled, it has no usable tools, or it no longer allows this kind of access. Reach for this when an agent seems to be missing something it should have.",
|
|
2688
|
+
inputSchema: {
|
|
2689
|
+
type: "object",
|
|
2690
|
+
properties: {
|
|
2691
|
+
workspaceId: { type: "string", description: "Workspace ID the agent belongs to" },
|
|
2692
|
+
agentUserId: { type: "string", description: "The agent's bot user ID" },
|
|
2693
|
+
},
|
|
2694
|
+
required: ["workspaceId", "agentUserId"],
|
|
2695
|
+
},
|
|
2696
|
+
},
|
|
2697
|
+
{
|
|
2698
|
+
name: "nestr_run_agent",
|
|
2699
|
+
description: "Run an agent now on a nest, optionally saying what the run is for. This is how one agent asks another to do something. The run is pinned to the nest you name and reports back there. You need assign rights on that nest and the agent must fill or be assigned to it, so this cannot run an agent anywhere in the workspace.",
|
|
2700
|
+
inputSchema: {
|
|
2701
|
+
type: "object",
|
|
2702
|
+
properties: {
|
|
2703
|
+
workspaceId: { type: "string", description: "Workspace ID the agent belongs to" },
|
|
2704
|
+
agentUserId: { type: "string", description: "The agent's bot user ID" },
|
|
2705
|
+
nestId: { type: "string", description: "The nest the run is pinned to: a role, a project, a task" },
|
|
2706
|
+
message: { type: "string", description: "What this run is for. Omit for a plain 'advance this item' run." },
|
|
2707
|
+
},
|
|
2708
|
+
required: ["workspaceId", "agentUserId", "nestId"],
|
|
2709
|
+
},
|
|
2710
|
+
...mutating,
|
|
2711
|
+
},
|
|
2115
2712
|
{
|
|
2116
2713
|
name: "nestr_get_nest_files",
|
|
2117
|
-
description: "List a nest's file attachments. A comment ID works too — files are keyed by nestId, so pass a comment ID to see files attached to that comment. Returns each file's id, name, contentType and size. Use nestr_read_file with a returned id to read one (images come back as viewable image content). Auth: any valid token with access to the nest.",
|
|
2714
|
+
description: "List a nest's file attachments. Images pasted into the nest's text are deliberately excluded — they belong to the text that references them; the inline_images hint counts those and their ids come from the references in the content. A comment ID works too — files are keyed by nestId, so pass a comment ID to see files attached to that comment. Returns each file's id, name, contentType and size. Use nestr_read_file with a returned id to read one (images come back as viewable image content). Auth: any valid token with access to the nest.",
|
|
2118
2715
|
inputSchema: {
|
|
2119
2716
|
type: "object",
|
|
2120
2717
|
properties: {
|
|
@@ -2126,7 +2723,7 @@ export const toolDefinitions = [
|
|
|
2126
2723
|
},
|
|
2127
2724
|
{
|
|
2128
2725
|
name: "nestr_read_file",
|
|
2129
|
-
description: "Read a single file attachment on a nest (or comment). Branches on contentType: images (image/*) return as viewable image content so you can see them (very large images return metadata only); JSON and text (application/json, text/*) return as decoded UTF-8 text (large text is truncated); PDFs and other types return their metadata only (cannot be inlined yet). Get file ids from nestr_get_nest_files. A comment ID works as the nestId. Auth: any valid token with access to the nest.",
|
|
2726
|
+
description: "Read a single file attachment on a nest (or comment). Branches on contentType: images (image/*) return as viewable image content so you can see them (very large images return metadata only); JSON and text (application/json, text/*) return as decoded UTF-8 text (large text is truncated); PDFs and other types return their metadata only (cannot be inlined yet). Get file ids from nestr_get_nest_files, or, for an image pasted into a nest's text, from the  reference in its content — those are not listed by nestr_get_nest_files but are readable here. A comment ID works as the nestId. Auth: any valid token with access to the nest.",
|
|
2130
2727
|
inputSchema: {
|
|
2131
2728
|
type: "object",
|
|
2132
2729
|
properties: {
|
|
@@ -2306,7 +2903,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2306
2903
|
const entries = await loadArticleIndex();
|
|
2307
2904
|
const hits = searchArticleIndex(entries, parsed.search, 8);
|
|
2308
2905
|
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.` }] };
|
|
2906
|
+
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
2907
|
}
|
|
2311
2908
|
// Enrich the top hits with a title + one-line summary so the caller
|
|
2312
2909
|
// can pick the right article without a blind fetch. Best-effort:
|
|
@@ -2732,8 +3329,12 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2732
3329
|
const parsed = schemas.getComments.parse(args);
|
|
2733
3330
|
const comments = await client.getNestPosts(parsed.nestId, {
|
|
2734
3331
|
depth: parsed.depth,
|
|
3332
|
+
unread: parsed.unread,
|
|
2735
3333
|
});
|
|
2736
|
-
|
|
3334
|
+
// enrichHints turns the unread_posts hint's endpoint into a nestr_mark_post_read
|
|
3335
|
+
// call. Without it the hint still arrives, but as a raw URL the model has to
|
|
3336
|
+
// recognise and hand-assemble.
|
|
3337
|
+
return formatResult(enrichHints(comments));
|
|
2737
3338
|
}
|
|
2738
3339
|
case "nestr_get_circle": {
|
|
2739
3340
|
const parsed = schemas.getCircle.parse(args);
|
|
@@ -2830,6 +3431,92 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2830
3431
|
});
|
|
2831
3432
|
return formatResult({ message: "Personal label created successfully", label });
|
|
2832
3433
|
}
|
|
3434
|
+
// Direct messages
|
|
3435
|
+
case "nestr_list_dms": {
|
|
3436
|
+
const parsed = schemas.listDMs.parse(args);
|
|
3437
|
+
const result = await client.listDMs({
|
|
3438
|
+
withUser: parsed.withUser,
|
|
3439
|
+
unread: parsed.unread,
|
|
3440
|
+
includeCompleted: parsed.includeCompleted,
|
|
3441
|
+
limit: parsed.limit,
|
|
3442
|
+
page: parsed.page,
|
|
3443
|
+
});
|
|
3444
|
+
return formatResult({ threads: result });
|
|
3445
|
+
}
|
|
3446
|
+
case "nestr_start_dm_thread": {
|
|
3447
|
+
const parsed = schemas.startDMThread.parse(args);
|
|
3448
|
+
const result = await client.createDMThread(parsed.user, parsed.title);
|
|
3449
|
+
return formatResult({ message: "Thread started", thread: result });
|
|
3450
|
+
}
|
|
3451
|
+
case "nestr_list_queues": {
|
|
3452
|
+
schemas.listQueues.parse(args ?? {});
|
|
3453
|
+
const result = await client.listQueues();
|
|
3454
|
+
return formatResult({ queues: result });
|
|
3455
|
+
}
|
|
3456
|
+
case "nestr_list_queue_threads": {
|
|
3457
|
+
const parsed = schemas.listQueueThreads.parse(args);
|
|
3458
|
+
const result = await client.listQueueThreads(parsed.key, { unread: parsed.unread });
|
|
3459
|
+
return formatResult(enrichHints(result));
|
|
3460
|
+
}
|
|
3461
|
+
case "nestr_get_dm_thread": {
|
|
3462
|
+
const parsed = schemas.getDMThread.parse(args);
|
|
3463
|
+
const result = await client.getDMThread(parsed.threadId, { unread: parsed.unread });
|
|
3464
|
+
return formatResult(enrichHints(result));
|
|
3465
|
+
}
|
|
3466
|
+
case "nestr_update_dm_thread": {
|
|
3467
|
+
const parsed = schemas.updateDMThread.parse(args);
|
|
3468
|
+
// `completed` is meaningful as null, so presence is the test rather than truth.
|
|
3469
|
+
const setsCompleted = args !== null
|
|
3470
|
+
&& typeof args === "object"
|
|
3471
|
+
&& Object.prototype.hasOwnProperty.call(args, "completed");
|
|
3472
|
+
if (parsed.title === undefined && !setsCompleted && parsed.users === undefined) {
|
|
3473
|
+
throw new Error("Pass at least one of title, completed or users.");
|
|
3474
|
+
}
|
|
3475
|
+
const result = await client.updateDMThread(parsed.threadId, {
|
|
3476
|
+
...(parsed.title !== undefined ? { title: parsed.title } : {}),
|
|
3477
|
+
...(setsCompleted ? { completed: parsed.completed ?? null } : {}),
|
|
3478
|
+
...(parsed.users !== undefined ? { users: parsed.users } : {}),
|
|
3479
|
+
});
|
|
3480
|
+
return formatResult({
|
|
3481
|
+
message: setsCompleted && parsed.completed
|
|
3482
|
+
? "Conversation closed"
|
|
3483
|
+
: "Thread updated",
|
|
3484
|
+
thread: result,
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
case "nestr_get_dm_posts": {
|
|
3488
|
+
const parsed = schemas.getDMPosts.parse(args);
|
|
3489
|
+
const result = await client.getDMPosts(parsed.threadId, {
|
|
3490
|
+
unread: parsed.unread,
|
|
3491
|
+
depth: parsed.depth,
|
|
3492
|
+
});
|
|
3493
|
+
return formatResult(enrichHints(result));
|
|
3494
|
+
}
|
|
3495
|
+
case "nestr_post_dm_message": {
|
|
3496
|
+
const parsed = schemas.createDMPost.parse(args);
|
|
3497
|
+
const result = await client.createDMPost(parsed.threadId, parsed.body);
|
|
3498
|
+
return formatResult({ message: "Message posted", post: result });
|
|
3499
|
+
}
|
|
3500
|
+
case "nestr_mark_post_read": {
|
|
3501
|
+
const parsed = schemas.markPostRead.parse(args);
|
|
3502
|
+
const result = await client.markPostRead(parsed.postId);
|
|
3503
|
+
return formatResult({ message: "Marked read", read: result });
|
|
3504
|
+
}
|
|
3505
|
+
case "nestr_escalate_to_support": {
|
|
3506
|
+
const parsed = schemas.escalateToSupport.parse(args);
|
|
3507
|
+
const result = await client.escalateDMThread(parsed.threadId, parsed.reason);
|
|
3508
|
+
const { alreadyQueued, statusMessagePosted } = result;
|
|
3509
|
+
let message = "A human has been brought in. Tell them so, and keep helping in the meantime.";
|
|
3510
|
+
if (alreadyQueued) {
|
|
3511
|
+
message = "Already with a human; nothing more to do.";
|
|
3512
|
+
}
|
|
3513
|
+
else if (statusMessagePosted) {
|
|
3514
|
+
// Nestr posted its own confirmation into the thread, so saying it again is the
|
|
3515
|
+
// double message this flag exists to avoid.
|
|
3516
|
+
message = "A human has been brought in and the thread already says so. Do not repeat it; carry on helping.";
|
|
3517
|
+
}
|
|
3518
|
+
return formatResult({ message, escalation: result });
|
|
3519
|
+
}
|
|
2833
3520
|
// Reorder tools
|
|
2834
3521
|
case "nestr_reorder_nest": {
|
|
2835
3522
|
const parsed = schemas.reorderNest.parse(args);
|
|
@@ -3152,9 +3839,46 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3152
3839
|
const connectors = await client.listConnectors(parsed.workspaceId);
|
|
3153
3840
|
return formatResult(connectors);
|
|
3154
3841
|
}
|
|
3842
|
+
case "nestr_list_connector_templates": {
|
|
3843
|
+
const parsed = schemas.listConnectorTemplates.parse(args);
|
|
3844
|
+
const listed = await client.listConnectorTemplates(parsed.workspaceId);
|
|
3845
|
+
const templates = listed.templates;
|
|
3846
|
+
if (!Array.isArray(templates) || templates.length === 0) {
|
|
3847
|
+
return { content: [{ type: "text", text: "This deployment offers no connector templates." }] };
|
|
3848
|
+
}
|
|
3849
|
+
// Enriched so the hint arrives as the register call itself, pre-filled
|
|
3850
|
+
// with the template id, the same treatment every other hint gets.
|
|
3851
|
+
const enrichedTemplates = enrichHints({
|
|
3852
|
+
workspaceId: parsed.workspaceId,
|
|
3853
|
+
...(listed.hints ? { hints: listed.hints } : {}),
|
|
3854
|
+
});
|
|
3855
|
+
return formatResult({
|
|
3856
|
+
message: "Pass the id of the one you want as templateId to nestr_register_connector. It carries the vendor's endpoint, transport and auth strategy, so nothing has to be guessed. Do not rebuild one of these by hand: a hand-built copy has no OAuth client and fails at first use.",
|
|
3857
|
+
templates,
|
|
3858
|
+
...(enrichedTemplates.hints ? { hints: enrichedTemplates.hints } : {}),
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3155
3861
|
case "nestr_register_connector": {
|
|
3156
3862
|
const parsed = schemas.registerConnector.parse(args);
|
|
3157
|
-
|
|
3863
|
+
if (parsed.templateId) {
|
|
3864
|
+
const fromTemplate = await client.registerConnector(parsed.workspaceId, {
|
|
3865
|
+
templateId: parsed.templateId,
|
|
3866
|
+
...(parsed.name ? { name: parsed.name } : {}),
|
|
3867
|
+
});
|
|
3868
|
+
return formatResult({
|
|
3869
|
+
message: "Connector registered from a template, so its endpoint and auth strategy are the vendor's own. Next, bind it to a role's DOMAIN with nestr_bind_connector, then a human connects the account via the Connect button.",
|
|
3870
|
+
connector: fromTemplate.connector,
|
|
3871
|
+
});
|
|
3872
|
+
}
|
|
3873
|
+
if (!parsed.type || !parsed.name) {
|
|
3874
|
+
return formatError({
|
|
3875
|
+
error: true,
|
|
3876
|
+
code: "VALIDATION",
|
|
3877
|
+
message: "Without templateId, both type and name are required. Call nestr_list_connector_templates first: a known vendor almost always has one.",
|
|
3878
|
+
retryable: false,
|
|
3879
|
+
});
|
|
3880
|
+
}
|
|
3881
|
+
const registered = await client.registerConnector(parsed.workspaceId, {
|
|
3158
3882
|
type: parsed.type,
|
|
3159
3883
|
name: parsed.name,
|
|
3160
3884
|
config: parsed.config,
|
|
@@ -3162,9 +3886,27 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3162
3886
|
exposure: parsed.exposure,
|
|
3163
3887
|
authStrategy: parsed.authStrategy,
|
|
3164
3888
|
});
|
|
3889
|
+
// Enriched so a template hint arrives as a tool call the model can make,
|
|
3890
|
+
// the same treatment every other hint gets. workspaceId is on the entry,
|
|
3891
|
+
// which is what lets enrichHints work on something that is not a nest.
|
|
3892
|
+
const enriched = enrichHints({
|
|
3893
|
+
...registered.connector,
|
|
3894
|
+
...(registered.hints ? { hints: registered.hints } : {}),
|
|
3895
|
+
});
|
|
3896
|
+
return formatResult({
|
|
3897
|
+
message: "Connector registered. It does nothing yet: nobody has access to it. Give a ROLE access with nestr_bind_connector { ownerType: 'role', ownerId: <role nest id> } and its domain is created under that role, which is the usual onboarding path. Then get a link with nestr_get_connect_link and give it to a person to open, since the credential must never pass through you.",
|
|
3898
|
+
connector: enriched,
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
case "nestr_create_agent": {
|
|
3902
|
+
const parsed = schemas.createAgent.parse(args);
|
|
3903
|
+
const agent = await client.createAgent(parsed.workspaceId, {
|
|
3904
|
+
name: parsed.name,
|
|
3905
|
+
agentConfig: parsed.agentConfig,
|
|
3906
|
+
});
|
|
3165
3907
|
return formatResult({
|
|
3166
|
-
message: "
|
|
3167
|
-
|
|
3908
|
+
message: "Agent created. Next, give it work to fill: create or find a role named for the WORK (not for the agent) with nestr_create_nest, then assign this agent to it with nestr_update_nest users. Its instructions belong in a skill nest under that role, not on the agent.",
|
|
3909
|
+
agent,
|
|
3168
3910
|
});
|
|
3169
3911
|
}
|
|
3170
3912
|
case "nestr_bind_connector": {
|
|
@@ -3173,14 +3915,75 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3173
3915
|
connectorId: parsed.connectorId,
|
|
3174
3916
|
owner: { type: parsed.ownerType, id: parsed.ownerId },
|
|
3175
3917
|
});
|
|
3176
|
-
//
|
|
3177
|
-
//
|
|
3178
|
-
//
|
|
3179
|
-
const message = parsed.ownerType === "role-domain"
|
|
3180
|
-
? "
|
|
3181
|
-
: "
|
|
3918
|
+
// A role binding creates the connector's domain when there isn't one, so
|
|
3919
|
+
// say where the access landed. The credential is always a separate,
|
|
3920
|
+
// out-of-band step: hand the human a link from nestr_get_connect_link.
|
|
3921
|
+
const message = parsed.ownerType === "role" || parsed.ownerType === "role-domain"
|
|
3922
|
+
? "Access given to the role's domain. Nobody can use it until an account is connected: get a link with nestr_get_connect_link and give it to a person to open. The secret is captured out-of-band and never by the agent."
|
|
3923
|
+
: "Access given to the owner. Nobody can use it until an account is connected: get a link with nestr_get_connect_link and give it to a person to open. The secret is never seen by the agent.";
|
|
3182
3924
|
return formatResult({ message, connection });
|
|
3183
3925
|
}
|
|
3926
|
+
case "nestr_update_connector": {
|
|
3927
|
+
const parsed = schemas.updateConnector.parse(args);
|
|
3928
|
+
const { workspaceId, connectorId, ...updates } = parsed;
|
|
3929
|
+
const connector = await client.updateConnector(workspaceId, connectorId, updates);
|
|
3930
|
+
return formatResult({ message: "Connector updated.", connector });
|
|
3931
|
+
}
|
|
3932
|
+
case "nestr_remove_connector": {
|
|
3933
|
+
const parsed = schemas.removeConnector.parse(args);
|
|
3934
|
+
await client.removeConnector(parsed.workspaceId, parsed.connectorId);
|
|
3935
|
+
return formatResult({
|
|
3936
|
+
message: "Connector removed from the catalog. Bindings that named it stop resolving.",
|
|
3937
|
+
connectorId: parsed.connectorId,
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3940
|
+
case "nestr_list_connections": {
|
|
3941
|
+
const parsed = schemas.listConnections.parse(args);
|
|
3942
|
+
const connections = await client.listConnections(parsed.workspaceId, {
|
|
3943
|
+
includeDisabled: parsed.includeDisabled,
|
|
3944
|
+
});
|
|
3945
|
+
return formatResult(connections);
|
|
3946
|
+
}
|
|
3947
|
+
case "nestr_remove_connection": {
|
|
3948
|
+
const parsed = schemas.removeConnection.parse(args);
|
|
3949
|
+
const result = await client.removeConnection(parsed.workspaceId, parsed.connectionId);
|
|
3950
|
+
return formatResult({
|
|
3951
|
+
message: `Access removed. ${result.revokedCount} credential(s) revoked.`,
|
|
3952
|
+
connectionId: parsed.connectionId,
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
case "nestr_get_connect_link": {
|
|
3956
|
+
const parsed = schemas.getConnectLink.parse(args);
|
|
3957
|
+
const link = await client.getConnectLink(parsed.workspaceId, parsed.connectionId);
|
|
3958
|
+
return formatResult({
|
|
3959
|
+
message: "Give this link to a person to open. They complete the sign-in or paste the key there, so the secret never passes through you.",
|
|
3960
|
+
...link,
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
case "nestr_revoke_connection_credential": {
|
|
3964
|
+
const parsed = schemas.revokeConnectionCredential.parse(args);
|
|
3965
|
+
await client.revokeConnectionCredential(parsed.workspaceId, parsed.connectionId);
|
|
3966
|
+
return formatResult({
|
|
3967
|
+
message: "Credential revoked. The binding stays; connect again to restore access.",
|
|
3968
|
+
connectionId: parsed.connectionId,
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
case "nestr_get_agent_connectors": {
|
|
3972
|
+
const parsed = schemas.getAgentConnectorReach.parse(args);
|
|
3973
|
+
const reach = await client.getAgentConnectorReach(parsed.workspaceId, parsed.agentUserId);
|
|
3974
|
+
return formatResult(reach);
|
|
3975
|
+
}
|
|
3976
|
+
case "nestr_run_agent": {
|
|
3977
|
+
const parsed = schemas.runAgent.parse(args);
|
|
3978
|
+
const result = await client.runAgent(parsed.workspaceId, parsed.agentUserId, {
|
|
3979
|
+
nestId: parsed.nestId,
|
|
3980
|
+
message: parsed.message,
|
|
3981
|
+
});
|
|
3982
|
+
return formatResult({
|
|
3983
|
+
message: "The agent was dispatched. It reports back on the item it was run on.",
|
|
3984
|
+
...result,
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3184
3987
|
case "nestr_get_nest_files": {
|
|
3185
3988
|
const parsed = schemas.getNestFiles.parse(args);
|
|
3186
3989
|
const files = await client.getNestFiles(parsed.nestId);
|