@nestr/mcp 0.1.104 → 0.1.106
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 +23 -1
- package/SECURITY.md +97 -0
- package/build/api/client.d.ts +82 -29
- package/build/api/client.d.ts.map +1 -1
- package/build/api/client.js +90 -34
- package/build/api/client.js.map +1 -1
- package/build/api/readonly-bearer.d.ts +13 -0
- package/build/api/readonly-bearer.d.ts.map +1 -0
- package/build/api/readonly-bearer.js +18 -0
- package/build/api/readonly-bearer.js.map +1 -0
- package/build/help/articles.d.ts.map +1 -1
- package/build/help/articles.js +88 -4
- 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 -1
- package/build/help/cross-links.js.map +1 -1
- package/build/help/topics.d.ts.map +1 -1
- package/build/help/topics.js +356 -22
- package/build/help/topics.js.map +1 -1
- package/build/http.d.ts.map +1 -1
- package/build/http.js +6 -1
- package/build/http.js.map +1 -1
- package/build/server.d.ts.map +1 -1
- package/build/server.js +2 -1
- package/build/server.js.map +1 -1
- package/build/skills/tension-processing.d.ts.map +1 -1
- package/build/skills/tension-processing.js +3 -2
- package/build/skills/tension-processing.js.map +1 -1
- package/build/tools/index.d.ts +1110 -13
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +277 -24
- package/build/tools/index.js.map +1 -1
- package/package.json +4 -3
package/build/tools/index.js
CHANGED
|
@@ -356,6 +356,7 @@ const HINT_ENDPOINT_TOOL_MAPPINGS = [
|
|
|
356
356
|
bodyParams: new Set([
|
|
357
357
|
"_id", "title", "labels", "description", "purpose",
|
|
358
358
|
"parentId", "users", "due", "accountabilities", "domains",
|
|
359
|
+
"role", // operational output: the role the work is asked of
|
|
359
360
|
"roleId", // election mode
|
|
360
361
|
]),
|
|
361
362
|
},
|
|
@@ -369,6 +370,7 @@ const HINT_ENDPOINT_TOOL_MAPPINGS = [
|
|
|
369
370
|
bodyParams: new Set([
|
|
370
371
|
"_id", "title", "labels", "description", "purpose",
|
|
371
372
|
"parentId", "users", "due", "accountabilities", "domains",
|
|
373
|
+
"role",
|
|
372
374
|
]),
|
|
373
375
|
},
|
|
374
376
|
// DELETE /parts (body has _id) — propose deletion of an existing item.
|
|
@@ -490,6 +492,93 @@ export function nestrWebBase(apiBase) {
|
|
|
490
492
|
return base.replace(/\/api\/?$/, "").replace(/\/+$/, "") || "https://app.nestr.io";
|
|
491
493
|
}
|
|
492
494
|
const NESTR_WEB_BASE = nestrWebBase(process.env.NESTR_API_BASE);
|
|
495
|
+
/**
|
|
496
|
+
* Turn the tool-level `hints` argument into what the API should be sent.
|
|
497
|
+
*
|
|
498
|
+
* `false` means none. A level passes through. `undefined` and `true` both take the
|
|
499
|
+
* caller-appropriate default, which differs by call shape on purpose: a single read
|
|
500
|
+
* wants the teaching prose, a listing wants it once from nestr_help rather than once
|
|
501
|
+
* per row.
|
|
502
|
+
*/
|
|
503
|
+
export function resolveHintLevel(value, fallback) {
|
|
504
|
+
if (value === false)
|
|
505
|
+
return false;
|
|
506
|
+
if (value === "full" || value === "summary")
|
|
507
|
+
return value;
|
|
508
|
+
return fallback;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Append `strict:true` unless the caller already asked for it.
|
|
512
|
+
*
|
|
513
|
+
* Without it an unrecognised operator, label or field filter is silently dropped and the
|
|
514
|
+
* search returns a broader result that looks like a real answer. That is fine for
|
|
515
|
+
* browsing and wrong for counting, so the tool exposes it as a flag rather than making
|
|
516
|
+
* every caller remember the operator.
|
|
517
|
+
*/
|
|
518
|
+
export function withStrict(query, strict) {
|
|
519
|
+
if (!strict)
|
|
520
|
+
return query;
|
|
521
|
+
if (/(^|\s)strict:true(\s|$)/i.test(query))
|
|
522
|
+
return query;
|
|
523
|
+
return `${query} strict:true`;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Reduce an OpenAPI document to something a model can read.
|
|
527
|
+
*
|
|
528
|
+
* The whole document is far too large to return, and returning nothing useful is how a
|
|
529
|
+
* caller ends up guessing whether an endpoint exists. So: an operation index by default,
|
|
530
|
+
* one operation in full when asked for by path, and a keyword filter in between. The
|
|
531
|
+
* counts matter as much as the rows — "0 of 94 operations match 'duration'" is the
|
|
532
|
+
* answer to "does the API store meeting duration", and it is a real negative rather than
|
|
533
|
+
* a failed search.
|
|
534
|
+
*/
|
|
535
|
+
export function summariseApiSpec(spec, filter = {}) {
|
|
536
|
+
const paths = (spec?.paths || {});
|
|
537
|
+
const allPaths = Object.keys(paths);
|
|
538
|
+
if (filter.path) {
|
|
539
|
+
const match = allPaths.find((p) => { return p === filter.path; })
|
|
540
|
+
|| allPaths.find((p) => { return p.toLowerCase() === filter.path.toLowerCase(); });
|
|
541
|
+
if (!match) {
|
|
542
|
+
return {
|
|
543
|
+
found: false,
|
|
544
|
+
requested: filter.path,
|
|
545
|
+
totalPaths: allPaths.length,
|
|
546
|
+
note: "No such path in this deployment's spec. This is a definitive negative, not a failed lookup.",
|
|
547
|
+
// Intentionally broad rather than precise: the first path segment, so
|
|
548
|
+
// `/nests/{id}/nonsense` suggests everything under `/nests/`. A caller who got the
|
|
549
|
+
// path wrong usually has the resource right, and a wide list they can scan beats a
|
|
550
|
+
// narrow one that misses the route they meant. Capped so it stays readable.
|
|
551
|
+
didYouMean: allPaths.filter((p) => { return p.includes(filter.path.split("/")[1] || ""); }).slice(0, 10),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return { found: true, path: match, operations: paths[match] };
|
|
555
|
+
}
|
|
556
|
+
const rows = [];
|
|
557
|
+
for (const p of allPaths) {
|
|
558
|
+
for (const [method, op] of Object.entries(paths[p] || {})) {
|
|
559
|
+
if (typeof op !== "object" || op === null)
|
|
560
|
+
continue;
|
|
561
|
+
const summary = (op.summary || op.description || "").split("\n")[0].slice(0, 160);
|
|
562
|
+
rows.push({ method: method.toUpperCase(), path: p, summary });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const needle = filter.search?.toLowerCase();
|
|
566
|
+
const matched = needle
|
|
567
|
+
? rows.filter((r) => {
|
|
568
|
+
return r.path.toLowerCase().includes(needle) || r.summary.toLowerCase().includes(needle);
|
|
569
|
+
})
|
|
570
|
+
: rows;
|
|
571
|
+
return {
|
|
572
|
+
totalOperations: rows.length,
|
|
573
|
+
matchedOperations: matched.length,
|
|
574
|
+
...(needle ? { query: filter.search } : {}),
|
|
575
|
+
...(needle && matched.length === 0
|
|
576
|
+
? { note: `No operation mentions "${filter.search}". This deployment does not serve one, which is a definitive answer rather than a failed search.` }
|
|
577
|
+
: {}),
|
|
578
|
+
operations: matched.slice(0, 200),
|
|
579
|
+
...(matched.length > 200 ? { truncated: true } : {}),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
493
582
|
export function enrichHints(data) {
|
|
494
583
|
if (!data || typeof data !== "object")
|
|
495
584
|
return data;
|
|
@@ -563,9 +652,78 @@ export function enrichHints(data) {
|
|
|
563
652
|
// `data` would discard both the enriched payload and the enriched envelope hints.
|
|
564
653
|
return subject;
|
|
565
654
|
}
|
|
566
|
-
|
|
655
|
+
// Which of the PARENT's tabs holds a child carrying this label, keyed by the
|
|
656
|
+
// API-facing label name (the API strips `circleplus-` and renames
|
|
657
|
+
// prepared-tension to tension, so these are the names that actually arrive).
|
|
658
|
+
//
|
|
659
|
+
// This is the reverse of the `labels: [...]` declarations the tab definitions
|
|
660
|
+
// already carry in slashme-online `packages/nestr_circleplus/lib/tabs.js`: the
|
|
661
|
+
// Projects tab declares it holds project / individual-action / sprint / epic,
|
|
662
|
+
// the Roles tab declares role / circle, and so on. A few tabs (Goals, Todos,
|
|
663
|
+
// Metrics on a circle) express the same containment as a `searchTerm` rather
|
|
664
|
+
// than a labels array, so those entries are read off the search and written
|
|
665
|
+
// here by hand.
|
|
666
|
+
//
|
|
667
|
+
// Why a static map rather than asking the server which tab contains this nest:
|
|
668
|
+
// the precise answer needs the PARENT's resolved tab set, and getTabs() on the
|
|
669
|
+
// server evaluates the circleplus tab callback (a walk through getWorkspace,
|
|
670
|
+
// getData and TAPi18n). translateNest runs per row on every search and children
|
|
671
|
+
// response, so that is a per-row cost on the hot path for a link most rows never
|
|
672
|
+
// need. A wrong guess costs nothing: listview_lists falls back to the nest's
|
|
673
|
+
// default tab when the hash names no available tab, which is exactly the
|
|
674
|
+
// hashless behaviour we have today.
|
|
675
|
+
const LABEL_CONTAINING_TAB = {
|
|
676
|
+
project: "projects",
|
|
677
|
+
"individual-action": "projects",
|
|
678
|
+
sprint: "projects",
|
|
679
|
+
epic: "projects",
|
|
680
|
+
task: "tasks",
|
|
681
|
+
role: "roles",
|
|
682
|
+
circle: "roles",
|
|
683
|
+
"anchor-circle": "roles",
|
|
684
|
+
domain: "policies",
|
|
685
|
+
policy: "policies",
|
|
686
|
+
meeting: "meetings",
|
|
687
|
+
governance: "meetings",
|
|
688
|
+
tactical: "meetings",
|
|
689
|
+
tension: "meetings",
|
|
690
|
+
metric: "metrics",
|
|
691
|
+
checklist: "checklists",
|
|
692
|
+
goal: "goals",
|
|
693
|
+
result: "goals",
|
|
694
|
+
skill: "skills",
|
|
695
|
+
feedback: "feedback",
|
|
696
|
+
note: "notes",
|
|
697
|
+
};
|
|
698
|
+
// The tab on the parent that a person opens to SEE this nest in its list.
|
|
699
|
+
// First label wins, so a scrum story labelled ["project", "userstory"] resolves
|
|
700
|
+
// through `project` and lands on Projects.
|
|
701
|
+
export function containingTabHash(labels) {
|
|
702
|
+
if (!Array.isArray(labels))
|
|
703
|
+
return undefined;
|
|
704
|
+
for (const label of labels) {
|
|
705
|
+
if (typeof label !== "string")
|
|
706
|
+
continue;
|
|
707
|
+
const tab = LABEL_CONTAINING_TAB[label];
|
|
708
|
+
if (tab)
|
|
709
|
+
return tab;
|
|
710
|
+
}
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
// A nest URL without a `#` is NOT a stable link. listview_lists reads the tab
|
|
714
|
+
// from localStorage `<nestId>_preferred_header`, so the same link opens whichever
|
|
715
|
+
// tab that particular person last used on that particular nest, and a first-time
|
|
716
|
+
// visitor gets the container's first tab — which for a circle is Structure >
|
|
717
|
+
// About, not the work they were sent to look at. Carrying the hash is what makes
|
|
718
|
+
// the link mean the same thing to everyone.
|
|
719
|
+
//
|
|
720
|
+
// The hash belongs to the LEFT pane, which in the two-id form is the parent, so
|
|
721
|
+
// it is only appended there. On the bare `/n/{id}` form the hash would select a
|
|
722
|
+
// tab on the nest ITSELF, and `#projects` on a project means nothing.
|
|
723
|
+
function buildNestUrl(id, parentId, labels) {
|
|
567
724
|
if (parentId && parentId.toLowerCase() !== "inbox") {
|
|
568
|
-
|
|
725
|
+
const tab = containingTabHash(labels);
|
|
726
|
+
return `${NESTR_WEB_BASE}/n/${parentId}/${id}${tab ? `#${tab}` : ""}`;
|
|
569
727
|
}
|
|
570
728
|
return `${NESTR_WEB_BASE}/n/${id}`;
|
|
571
729
|
}
|
|
@@ -597,7 +755,7 @@ export function addNestUrls(data) {
|
|
|
597
755
|
const record = data;
|
|
598
756
|
const out = { ...record };
|
|
599
757
|
if (looksLikeNest(record) && typeof out.url !== "string") {
|
|
600
|
-
out.url = buildNestUrl(record._id, record.parentId);
|
|
758
|
+
out.url = buildNestUrl(record._id, record.parentId, record.labels);
|
|
601
759
|
}
|
|
602
760
|
for (const [key, value] of Object.entries(out)) {
|
|
603
761
|
if (value && typeof value === "object") {
|
|
@@ -652,9 +810,27 @@ const MENTION_DESC = "Supports HTML and @mentions. Mentions MUST use literal cur
|
|
|
652
810
|
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.";
|
|
653
811
|
const PURPOSE_DESC = "Only for workspaces, circles and roles: a short aspirational statement. Details belong in description, not here. Supports HTML.";
|
|
654
812
|
const CONTENT_DESC = "The primary content field: details, context, acceptance criteria. Structured data goes in fields, progress in comments. Supports Markdown and HTML.";
|
|
813
|
+
const HINTS_DESC = "Contextual hints. 'summary' keeps the per-nest signal (type, severity, count, url, and the "
|
|
814
|
+
+ "`query` that finds every other nest with the same problem) and drops the fixed teaching prose "
|
|
815
|
+
+ "and endpoint list. 'full' is the whole payload. false for none.";
|
|
655
816
|
const STRIP_DESCRIPTION = "Strip description fields to shrink the response. Use for bulk or index reads.";
|
|
656
817
|
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).";
|
|
657
818
|
// Tool input schemas using Zod
|
|
819
|
+
/**
|
|
820
|
+
* `hints` is a level, not a flag. `summary` keeps what varies per nest and drops the
|
|
821
|
+
* teaching prose and endpoint list, which are identical for every nest of a type; `full`
|
|
822
|
+
* is the whole payload. Booleans still work: the API reads a bare `true` on a listing as
|
|
823
|
+
* `summary`, which is the difference between one paragraph and fifty copies of it.
|
|
824
|
+
*/
|
|
825
|
+
const hintLevelSchema = z.union([z.boolean(), z.enum(["full", "summary"])]).optional();
|
|
826
|
+
const hintFilterSchemas = {
|
|
827
|
+
hintTypes: coerceFromJson(z.array(z.string())).optional()
|
|
828
|
+
.describe("Keep only these hint types, e.g. ['project_waiting_no_reason','unassigned_role']."),
|
|
829
|
+
minSeverity: z.enum(["info", "suggestion", "warning", "alert"]).optional()
|
|
830
|
+
.describe("Drop hints below this severity. 'warning' is the useful floor when sweeping."),
|
|
831
|
+
};
|
|
832
|
+
const linkedUsersSchema = z.boolean().optional()
|
|
833
|
+
.describe("Resolve every user id in the results to a full user, returned once in linked.users.");
|
|
658
834
|
export const schemas = {
|
|
659
835
|
listWorkspaces: z.object({
|
|
660
836
|
search: z.string().optional().describe("Search query to filter workspaces"),
|
|
@@ -719,6 +895,8 @@ export const schemas = {
|
|
|
719
895
|
search: z.object({
|
|
720
896
|
workspaceId: z.string().describe("Workspace ID to search in"),
|
|
721
897
|
query: z.string().describe("Search query"),
|
|
898
|
+
strict: z.boolean().optional().describe("Reject the search if any operator, label or field filter in it was not recognised, instead of silently returning a broader result. Use it whenever you are counting rather than browsing: without it a typo'd filter reads as a real, larger answer."),
|
|
899
|
+
linkedUsers: linkedUsersSchema,
|
|
722
900
|
sort: z.string().optional().describe(`${SORT_DESCRIPTION} Takes precedence over sort:/sort-order: operators in the query.`),
|
|
723
901
|
limit: z.number().optional().describe("Max results per page. Omit on first call to see meta.total count."),
|
|
724
902
|
page: z.number().optional().describe("Page number (1-indexed) for pagination"),
|
|
@@ -727,7 +905,8 @@ export const schemas = {
|
|
|
727
905
|
getNest: z.object({
|
|
728
906
|
nestId: z.string().describe("Nest ID. Supports comma-separated IDs to fetch multiple nests in one call (e.g., 'id1,id2,id3') — returns an array instead of a single object. Keep total URL under 2000 chars to avoid HTTP limits."),
|
|
729
907
|
fieldsMetaData: z.boolean().optional().describe("Set to true to include field schema metadata (e.g., available options for project.status)"),
|
|
730
|
-
hints:
|
|
908
|
+
hints: hintLevelSchema.describe("Contextual hints: 'full' (default on a single read), 'summary', or false. 'summary' keeps type, severity, count, url and the sibling query, and drops the teaching prose and endpoints. Hints surface signals like unassigned roles, stale projects or unread comments."),
|
|
909
|
+
...hintFilterSchemas,
|
|
731
910
|
provenance: z.boolean().optional().describe("Single-nest only. Include field/property provenance: which label (and circle context) defines each field and property."),
|
|
732
911
|
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."),
|
|
733
912
|
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."),
|
|
@@ -740,12 +919,25 @@ export const schemas = {
|
|
|
740
919
|
}),
|
|
741
920
|
getNestChildren: z.object({
|
|
742
921
|
nestId: z.string().describe("Parent nest ID"),
|
|
922
|
+
search: z.string().optional().describe("Search scoped to this nest, full operator syntax. Ask for the subset you want rather than fetching every child and filtering: 'label:role' for a circle's roles, 'label:project fields.project.status:Waiting' for its waiting projects. depth:1 is applied when you set no depth, and the response says so in appliedDefaults."),
|
|
743
923
|
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
744
924
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
745
925
|
page: z.number().optional().describe("Page number for pagination"),
|
|
746
|
-
hints:
|
|
926
|
+
hints: hintLevelSchema.describe("Contextual hints: 'summary' (default on a listing), 'full', or false. A listing at 'full' repeats the same teaching prose once per row."),
|
|
927
|
+
...hintFilterSchemas,
|
|
928
|
+
linkedUsers: linkedUsersSchema,
|
|
747
929
|
_listTitle: z.string().optional().describe("Short descriptive title for the list UI (e.g., \"Tasks for Website Redesign\"). Omit for default."),
|
|
748
930
|
}),
|
|
931
|
+
hintsRollup: z.object({
|
|
932
|
+
nestId: z.string().describe("Nest to roll up. A circle or workspace root is the useful scope."),
|
|
933
|
+
hintTypes: coerceFromJson(z.array(z.string())).optional().describe("Count only these hint types."),
|
|
934
|
+
minSeverity: z.enum(["info", "suggestion", "warning", "alert"]).optional().describe("Drop rules below this severity."),
|
|
935
|
+
sampleSize: z.number().optional().describe("Example nests per type. Default 10, max 100, 0 for counts only."),
|
|
936
|
+
}),
|
|
937
|
+
apiSpec: z.object({
|
|
938
|
+
search: z.string().optional().describe("Filter operations by keyword against path and summary, e.g. 'meeting' or 'tension'."),
|
|
939
|
+
path: z.string().optional().describe("Return the full schema for one path, e.g. '/nests/{id}/children'."),
|
|
940
|
+
}),
|
|
749
941
|
createNest: z.object({
|
|
750
942
|
parentId: z.string().describe("Parent nest ID (workspace, circle, or project)"),
|
|
751
943
|
title: z.string().describe("Title of the new nest (plain text, HTML stripped)"),
|
|
@@ -809,6 +1001,7 @@ export const schemas = {
|
|
|
809
1001
|
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
810
1002
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
811
1003
|
page: z.number().optional().describe("Page number for pagination"),
|
|
1004
|
+
linkedUsers: linkedUsersSchema,
|
|
812
1005
|
}),
|
|
813
1006
|
listUserRoles: z.object({
|
|
814
1007
|
userId: z.string().optional().describe("User ID to look up. Omit for yourself. Requires workspaceId when set."),
|
|
@@ -843,6 +1036,7 @@ export const schemas = {
|
|
|
843
1036
|
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
844
1037
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
845
1038
|
page: z.number().optional().describe("Page number for pagination"),
|
|
1039
|
+
linkedUsers: linkedUsersSchema,
|
|
846
1040
|
_listTitle: z.string().optional().describe("Short descriptive title for the list UI (e.g., \"Engineering projects\"). Omit for default."),
|
|
847
1041
|
}),
|
|
848
1042
|
getComments: z.object({
|
|
@@ -987,7 +1181,6 @@ export const schemas = {
|
|
|
987
1181
|
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
988
1182
|
limit: z.number().optional().describe("Max results to return"),
|
|
989
1183
|
page: z.number().optional().describe("Page number for pagination"),
|
|
990
|
-
order: z.string().optional().describe("Deprecated alias of sort"),
|
|
991
1184
|
}),
|
|
992
1185
|
updateTension: z.object({
|
|
993
1186
|
nestId: z.string().describe("ID of the circle or role the tension belongs to"),
|
|
@@ -1018,6 +1211,7 @@ export const schemas = {
|
|
|
1018
1211
|
due: z.string().optional().describe("Due or re-election date, ISO. For an election, the term end; omit for no term."),
|
|
1019
1212
|
accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Accountability titles on a role (replaces all; children tools for individual edits)"),
|
|
1020
1213
|
domains: coerceFromJson(z.array(z.string())).optional().describe("Domain titles on a role (replaces all; children tools for individual edits)"),
|
|
1214
|
+
role: z.string().optional().describe("The role this output belongs to, for an operational output (pathway 3/4). Pair with users: users is WHO does it, role is the role it is asked of, and the work request names both (\"Ada as Systems: ...\"). A role id, not a title. Distinct from roleId, which is election mode."),
|
|
1021
1215
|
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."),
|
|
1022
1216
|
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."),
|
|
1023
1217
|
}).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." }),
|
|
@@ -1034,6 +1228,7 @@ export const schemas = {
|
|
|
1034
1228
|
due: z.string().optional().describe("Updated due date (ISO format)"),
|
|
1035
1229
|
accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Updated accountabilities (replaces all; children tools for individual edits)"),
|
|
1036
1230
|
domains: coerceFromJson(z.array(z.string())).optional().describe("Updated domains (replaces all; children tools for individual edits)"),
|
|
1231
|
+
role: z.string().optional().describe("Updated role for an operational output. A role id; pass an empty string to clear it."),
|
|
1037
1232
|
}),
|
|
1038
1233
|
removeTensionPart: z.object({
|
|
1039
1234
|
nestId: z.string().describe("ID of the circle or role the tension belongs to"),
|
|
@@ -1206,7 +1401,7 @@ const destructive = { annotations: { readOnlyHint: false, destructiveHint: true
|
|
|
1206
1401
|
export const toolDefinitions = [
|
|
1207
1402
|
{
|
|
1208
1403
|
name: "nestr_help",
|
|
1209
|
-
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.",
|
|
1404
|
+
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, **in English whatever language the conversation is in** — the corpus is English and the index scores slugs and keywords, so a query in another language usually returns nothing; 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.",
|
|
1210
1405
|
inputSchema: {
|
|
1211
1406
|
type: "object",
|
|
1212
1407
|
properties: {
|
|
@@ -1296,12 +1491,14 @@ export const toolDefinitions = [
|
|
|
1296
1491
|
},
|
|
1297
1492
|
{
|
|
1298
1493
|
name: "nestr_search",
|
|
1299
|
-
description: "Search nests in a workspace. Supports operators like label:, assignee:, createdby:, completed:, in:, sort:. createdby: accepts me, an email address, or a user id. Always use completed:false for active work. See nestr_help('search') for full syntax. Results carry user ids; when presenting to a person, show names and/or emails
|
|
1494
|
+
description: "Search nests in a workspace. Supports operators like label:, assignee:, createdby:, completed:, in:, sort:. createdby: accepts me, an email address, or a user id. Always use completed:false for active work. See nestr_help('search') for full syntax. Results carry user ids; when presenting to a person, show names and/or emails. Pass `linkedUsers: true` to get them all resolved in the same request rather than calling nestr_get_user per id.",
|
|
1300
1495
|
inputSchema: {
|
|
1301
1496
|
type: "object",
|
|
1302
1497
|
properties: {
|
|
1303
1498
|
workspaceId: { type: "string", description: "Workspace ID to search in" },
|
|
1304
1499
|
query: { type: "string", description: "Search query with optional operators (e.g., 'label:role', 'assignee:me completed:false')" },
|
|
1500
|
+
strict: { type: "boolean", description: "Reject the search when any operator, label or field filter in it was not recognised, rather than silently returning a broader result. Use it whenever you are counting rather than browsing: without it, a mistyped filter comes back as a real and larger answer with nothing to say the filter was dropped." },
|
|
1501
|
+
linkedUsers: { type: "boolean", description: "Resolve every user id in the results to a full user, returned once in `linked.users`. Prefer this over calling nestr_get_user per id." },
|
|
1305
1502
|
sort: { type: "string", description: `${SORT_DESCRIPTION} Takes precedence over sort:/sort-order: operators in the query.` },
|
|
1306
1503
|
limit: { type: "number", description: "Max results per page. Omit on the first call so meta.total shows the match count." },
|
|
1307
1504
|
page: { type: "number", description: "Page number (1-indexed) for fetching additional pages" },
|
|
@@ -1322,7 +1519,9 @@ export const toolDefinitions = [
|
|
|
1322
1519
|
properties: {
|
|
1323
1520
|
nestId: { type: "string", description: "Nest ID, or comma-separated IDs for a batch (e.g. 'id1,id2'). Keep the URL under 2000 chars." },
|
|
1324
1521
|
fieldsMetaData: { type: "boolean", description: "Set to true to include field schema metadata (available options, field types)" },
|
|
1325
|
-
hints: { type: "boolean",
|
|
1522
|
+
hints: { type: ["string", "boolean"], enum: ["summary", "full", true, false], description: `${HINTS_DESC} Defaults to 'full' on a single read, which is what you want when you asked about one nest.` },
|
|
1523
|
+
hintTypes: { type: "array", items: { type: "string" }, description: "Keep only these hint types." },
|
|
1524
|
+
minSeverity: { type: "string", enum: ["info", "suggestion", "warning", "alert"], description: "Drop hints below this severity." },
|
|
1326
1525
|
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1327
1526
|
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." },
|
|
1328
1527
|
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." },
|
|
@@ -1349,15 +1548,19 @@ export const toolDefinitions = [
|
|
|
1349
1548
|
},
|
|
1350
1549
|
{
|
|
1351
1550
|
name: "nestr_get_nest_children",
|
|
1352
|
-
description: "Get children of a nest.
|
|
1551
|
+
description: "Get children of a nest, or just the ones you want. Pass `search` with the full operator syntax to ask for a subset rather than fetching everything and filtering: `search: 'label:role'` returns a circle's roles in one call. Paginated at 50 per page; read meta.total for the match count.",
|
|
1353
1552
|
inputSchema: {
|
|
1354
1553
|
type: "object",
|
|
1355
1554
|
properties: {
|
|
1356
1555
|
nestId: { type: "string", description: "Parent nest ID" },
|
|
1556
|
+
search: { type: "string", description: "Search scoped to this nest, full operator syntax. Ask for the subset you want instead of fetching every child and filtering: `label:role` for a circle's roles, `label:project fields.project.status:Waiting` for its waiting projects. `depth:1` is applied when you set no depth and the response says so in `appliedDefaults`; pass `depth:2` or higher to reach further down." },
|
|
1357
1557
|
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
1358
1558
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1359
1559
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1360
|
-
hints: { type: "boolean",
|
|
1560
|
+
hints: { type: ["string", "boolean"], enum: ["summary", "full", true, false], description: `${HINTS_DESC} Defaults to 'summary' here: a listing at 'full' repeats the same paragraph once per row.` },
|
|
1561
|
+
hintTypes: { type: "array", items: { type: "string" }, description: "Keep only these hint types." },
|
|
1562
|
+
minSeverity: { type: "string", enum: ["info", "suggestion", "warning", "alert"], description: "Drop hints below this severity." },
|
|
1563
|
+
linkedUsers: { type: "boolean", description: "Resolve every user id in the results to a full user, returned once in `linked.users`. One request instead of one per id." },
|
|
1361
1564
|
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1362
1565
|
_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." },
|
|
1363
1566
|
},
|
|
@@ -1367,9 +1570,36 @@ export const toolDefinitions = [
|
|
|
1367
1570
|
// The completable list app should only be used when results are confirmed to be completable items.
|
|
1368
1571
|
...readOnly,
|
|
1369
1572
|
},
|
|
1573
|
+
{
|
|
1574
|
+
name: "nestr_hints_rollup",
|
|
1575
|
+
description: "Count hints across a whole circle or workspace in one call. A hint on a single nest says \"this one has this problem\"; this says how many have it, which is the question you actually ask of a circle. Answers \"how healthy is this circle\", \"how many projects are waiting with no reason\", \"how many roles have nobody in them\" without reading every nest. Each result carries type, severity, count and a small sample to open. Read `notComputed`: it names every hint type this cannot count in one query, so a type listed there is unknown rather than zero. Do not sum counts across types, one nest can carry several; ask for a single hintTypes when you want a number that adds up.",
|
|
1576
|
+
inputSchema: {
|
|
1577
|
+
type: "object",
|
|
1578
|
+
properties: {
|
|
1579
|
+
nestId: { type: "string", description: "Nest to roll up. A circle or the workspace root is the useful scope." },
|
|
1580
|
+
hintTypes: { type: "array", items: { type: "string" }, description: "Count only these hint types. Omit for every type this can compute." },
|
|
1581
|
+
minSeverity: { type: "string", enum: ["info", "suggestion", "warning", "alert"], description: "Drop rules below this severity. 'warning' is the useful floor for \"what needs attention\"." },
|
|
1582
|
+
sampleSize: { type: "number", description: "Example nests per type. Default 10, max 100, 0 for counts only." },
|
|
1583
|
+
},
|
|
1584
|
+
required: ["nestId"],
|
|
1585
|
+
},
|
|
1586
|
+
...readOnly,
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
name: "nestr_api_spec",
|
|
1590
|
+
description: "The deployment's own OpenAPI document, so you can check whether the API has something rather than concluding \"I did not find it\" and guessing. Call with no arguments for the operation index (method, path, one-line summary). `search` filters by keyword against path and summary. `path` returns the full schema for one operation, including its parameters. Use this to answer \"is there an endpoint for X\" with certainty, and note that a negative here is a real negative: if it is not in the spec, this deployment does not serve it. For the search query language rather than the HTTP surface, use nestr_help('search'); for a filter you are unsure of, `strict: true` on nestr_search tells you whether it applied.",
|
|
1591
|
+
inputSchema: {
|
|
1592
|
+
type: "object",
|
|
1593
|
+
properties: {
|
|
1594
|
+
search: { type: "string", description: "Filter operations by keyword against path and summary, e.g. 'meeting', 'tension', 'duration'." },
|
|
1595
|
+
path: { type: "string", description: "Return the full schema for one path, e.g. '/nests/{id}/children'." },
|
|
1596
|
+
},
|
|
1597
|
+
},
|
|
1598
|
+
...readOnly,
|
|
1599
|
+
},
|
|
1370
1600
|
{
|
|
1371
1601
|
name: "nestr_create_nest",
|
|
1372
|
-
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').`,
|
|
1602
|
+
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. Meetings attach to a circle that ALREADY exists — the workspace anchor circle counts, and is the right parent when no sub-circle fits. Never create a circle to hold a meeting. Use \`['meeting','circle-meeting']\` for a tactical meeting, \`['meeting','governance']\` for a governance meeting, and set \`due\` to the start time. See nestr_help('labels') and nestr_help('meetings').`,
|
|
1373
1603
|
inputSchema: {
|
|
1374
1604
|
type: "object",
|
|
1375
1605
|
properties: {
|
|
@@ -1586,6 +1816,7 @@ export const toolDefinitions = [
|
|
|
1586
1816
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1587
1817
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1588
1818
|
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1819
|
+
linkedUsers: { type: "boolean", description: "Resolve every user id in the results to a full user, returned once in `linked.users`. One request instead of one per id." },
|
|
1589
1820
|
},
|
|
1590
1821
|
required: ["workspaceId"],
|
|
1591
1822
|
},
|
|
@@ -1801,6 +2032,7 @@ export const toolDefinitions = [
|
|
|
1801
2032
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1802
2033
|
stripDescription: { type: "boolean", description: STRIP_DESCRIPTION },
|
|
1803
2034
|
_listTitle: { type: "string", description: "Short descriptive title for the list UI header (e.g., \"Engineering projects\", \"All projects\"). Omit for default." },
|
|
2035
|
+
linkedUsers: { type: "boolean", description: "Resolve every user id in the results to a full user, returned once in `linked.users`. One request instead of one per id." },
|
|
1804
2036
|
},
|
|
1805
2037
|
required: ["workspaceId"],
|
|
1806
2038
|
},
|
|
@@ -1818,6 +2050,7 @@ export const toolDefinitions = [
|
|
|
1818
2050
|
oneOf: [{ type: "number" }, { type: "string", enum: ["all"] }],
|
|
1819
2051
|
description: "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.",
|
|
1820
2052
|
},
|
|
2053
|
+
unread: { type: "boolean", description: "true for comments you have not read, false for the ones you have. Omit for all. Combine with depth to sweep a circle for what you have missed." },
|
|
1821
2054
|
},
|
|
1822
2055
|
required: ["nestId"],
|
|
1823
2056
|
},
|
|
@@ -1905,7 +2138,7 @@ export const toolDefinitions = [
|
|
|
1905
2138
|
},
|
|
1906
2139
|
{
|
|
1907
2140
|
name: "nestr_get_workspace_apps",
|
|
1908
|
-
description: "List
|
|
2141
|
+
description: "List a workspace's apps/features. Returns the FULL catalogue, each entry `{ _id, title, enabled }` — a disabled app is present with `enabled: false`, never absent, so test `enabled` and not presence. Note the field is `_id`, not `id`. Check before using features that require specific apps (e.g., Insights, Scrum, Meetings).",
|
|
1909
2142
|
inputSchema: {
|
|
1910
2143
|
type: "object",
|
|
1911
2144
|
properties: {
|
|
@@ -2258,9 +2491,6 @@ export const toolDefinitions = [
|
|
|
2258
2491
|
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
2259
2492
|
limit: { type: "number", description: "Max results to return" },
|
|
2260
2493
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
2261
|
-
// The legacy `order` alias is deliberately not advertised — the Zod
|
|
2262
|
-
// schema still accepts it so existing callers keep working, but new
|
|
2263
|
-
// clients should only learn the canonical `sort` param.
|
|
2264
2494
|
},
|
|
2265
2495
|
required: ["nestId"],
|
|
2266
2496
|
},
|
|
@@ -2311,7 +2541,7 @@ export const toolDefinitions = [
|
|
|
2311
2541
|
},
|
|
2312
2542
|
{
|
|
2313
2543
|
name: "nestr_add_tension_part",
|
|
2314
|
-
description: "Add a
|
|
2544
|
+
description: "Add a part to a tension. A part is either the operational work the tension asks for or a governance change it proposes. Modes: OPERATIONAL OUTPUT (title, description, users, role, and no governance label) is what a work request is made of, and is the common case; new governance item (omit _id, give title plus a governance label such as ['role'] or ['policy']); 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). The part you get back has its own _id and a sourceId, the output nest underneath it; nestr_modify_tension_part takes the part _id, not the sourceId. See nestr_help('tension-processing').",
|
|
2315
2545
|
inputSchema: {
|
|
2316
2546
|
type: "object",
|
|
2317
2547
|
properties: {
|
|
@@ -2327,6 +2557,7 @@ export const toolDefinitions = [
|
|
|
2327
2557
|
due: { type: "string", description: "Due or re-election date, ISO. For an election, the term end; omit for no term." },
|
|
2328
2558
|
accountabilities: { type: "array", items: { type: "string" }, description: "Accountability titles on a role (replaces all; children tools for individual edits)" },
|
|
2329
2559
|
domains: { type: "array", items: { type: "string" }, description: "Domain titles on a role (replaces all; children tools for individual edits)" },
|
|
2560
|
+
role: { type: "string", description: "The role this output belongs to, for an operational output (pathway 3/4). Pair with users: users is WHO does it, role is the role it is asked of, and the work request names both (\"Ada as Systems: ...\"). A role id, not a title. Distinct from roleId, which is election mode." },
|
|
2330
2561
|
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." },
|
|
2331
2562
|
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." },
|
|
2332
2563
|
},
|
|
@@ -2350,6 +2581,7 @@ export const toolDefinitions = [
|
|
|
2350
2581
|
parentId: { type: "string", description: "Updated parent ID" },
|
|
2351
2582
|
users: { type: "array", items: { type: "string" }, description: "Updated user assignments" },
|
|
2352
2583
|
due: { type: "string", description: "Updated due date (ISO format)" },
|
|
2584
|
+
role: { type: "string", description: "Updated role for an operational output. A role id; pass an empty string to clear it." },
|
|
2353
2585
|
accountabilities: { type: "array", items: { type: "string" }, description: "Updated accountabilities (replaces all; children tools for individual edits)" },
|
|
2354
2586
|
domains: { type: "array", items: { type: "string" }, description: "Updated domains (replaces all; children tools for individual edits)" },
|
|
2355
2587
|
},
|
|
@@ -2960,7 +3192,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2960
3192
|
const entries = await loadArticleIndex();
|
|
2961
3193
|
const hits = searchArticleIndex(entries, parsed.search, 8);
|
|
2962
3194
|
if (hits.length === 0) {
|
|
2963
|
-
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.` }] };
|
|
3195
|
+
return { content: [{ type: "text", text: `_Resolved as: help-article search._\n\nNo help articles matched "${parsed.search}". **The corpus and the index are English. If you searched in another language, translate the query and search again before concluding anything** — that is the single most common reason for an empty result, and one retry usually fixes it. Beyond that: 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. Never fill the gap from memory on anything a customer could check — prices, limits and plan names above all.` }] };
|
|
2964
3196
|
}
|
|
2965
3197
|
// Enrich the top hits with a title + one-line summary so the caller
|
|
2966
3198
|
// can pick the right article without a blind fetch. Best-effort:
|
|
@@ -3144,11 +3376,12 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3144
3376
|
case "nestr_search": {
|
|
3145
3377
|
const parsed = schemas.search.parse(args);
|
|
3146
3378
|
const directives = extractSearchDirectives(parsed.query);
|
|
3147
|
-
const results = await client.searchWorkspace(parsed.workspaceId, parsed.query, {
|
|
3379
|
+
const results = await client.searchWorkspace(parsed.workspaceId, withStrict(parsed.query, parsed.strict), {
|
|
3148
3380
|
sort: parsed.sort ?? directives.sort,
|
|
3149
3381
|
limit: parsed.limit ?? directives.limit,
|
|
3150
3382
|
page: parsed.page,
|
|
3151
3383
|
cleanText: true,
|
|
3384
|
+
linkedUsers: parsed.linkedUsers,
|
|
3152
3385
|
});
|
|
3153
3386
|
return formatResult(completableResponse(compactResponse(results), "search", parsed._listTitle || `Search: ${parsed.query}`));
|
|
3154
3387
|
}
|
|
@@ -3157,7 +3390,9 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3157
3390
|
const nest = await client.getNest(parsed.nestId, {
|
|
3158
3391
|
cleanText: true,
|
|
3159
3392
|
fieldsMetaData: parsed.fieldsMetaData,
|
|
3160
|
-
hints: parsed.hints
|
|
3393
|
+
hints: resolveHintLevel(parsed.hints, "full"),
|
|
3394
|
+
hintTypes: parsed.hintTypes,
|
|
3395
|
+
minSeverity: parsed.minSeverity,
|
|
3161
3396
|
provenance: parsed.provenance,
|
|
3162
3397
|
rights: parsed.rights,
|
|
3163
3398
|
forUser: parsed.forUser,
|
|
@@ -3181,14 +3416,32 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3181
3416
|
case "nestr_get_nest_children": {
|
|
3182
3417
|
const parsed = schemas.getNestChildren.parse(args);
|
|
3183
3418
|
const children = await client.getNestChildren(parsed.nestId, {
|
|
3419
|
+
search: parsed.search,
|
|
3184
3420
|
sort: parsed.sort,
|
|
3185
3421
|
limit: parsed.limit,
|
|
3186
3422
|
page: parsed.page,
|
|
3187
3423
|
cleanText: true,
|
|
3188
|
-
hints: parsed.hints
|
|
3424
|
+
hints: resolveHintLevel(parsed.hints, "summary"),
|
|
3425
|
+
hintTypes: parsed.hintTypes,
|
|
3426
|
+
minSeverity: parsed.minSeverity,
|
|
3427
|
+
linkedUsers: parsed.linkedUsers,
|
|
3189
3428
|
});
|
|
3190
3429
|
return formatResult(completableResponse(compactResponse(enrichHints(children)), "children", parsed._listTitle || "Sub-items"));
|
|
3191
3430
|
}
|
|
3431
|
+
case "nestr_hints_rollup": {
|
|
3432
|
+
const parsed = schemas.hintsRollup.parse(args);
|
|
3433
|
+
const rollup = await client.getHintsRollup(parsed.nestId, {
|
|
3434
|
+
hintTypes: parsed.hintTypes,
|
|
3435
|
+
minSeverity: parsed.minSeverity,
|
|
3436
|
+
sampleSize: parsed.sampleSize,
|
|
3437
|
+
});
|
|
3438
|
+
return formatResult(rollup);
|
|
3439
|
+
}
|
|
3440
|
+
case "nestr_api_spec": {
|
|
3441
|
+
const parsed = schemas.apiSpec.parse(args);
|
|
3442
|
+
const spec = await client.getApiSpec();
|
|
3443
|
+
return formatResult(summariseApiSpec(spec, parsed));
|
|
3444
|
+
}
|
|
3192
3445
|
case "nestr_create_nest": {
|
|
3193
3446
|
const parsed = schemas.createNest.parse(args);
|
|
3194
3447
|
validatePrimeLabels(parsed.labels);
|
|
@@ -3325,6 +3578,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3325
3578
|
limit: parsed.limit,
|
|
3326
3579
|
page: parsed.page,
|
|
3327
3580
|
cleanText: true,
|
|
3581
|
+
linkedUsers: parsed.linkedUsers,
|
|
3328
3582
|
});
|
|
3329
3583
|
return formatResult(compactResponse(roles, "role"));
|
|
3330
3584
|
}
|
|
@@ -3381,6 +3635,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3381
3635
|
limit: parsed.limit,
|
|
3382
3636
|
page: parsed.page,
|
|
3383
3637
|
cleanText: true,
|
|
3638
|
+
linkedUsers: parsed.linkedUsers,
|
|
3384
3639
|
});
|
|
3385
3640
|
return formatResult(completableResponse(compactResponse(projects), "projects", parsed._listTitle || "Projects"));
|
|
3386
3641
|
}
|
|
@@ -3763,9 +4018,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3763
4018
|
case "nestr_list_tensions": {
|
|
3764
4019
|
const parsed = schemas.listTensions.parse(args);
|
|
3765
4020
|
const tensions = await client.listTensions(parsed.nestId, parsed.search, {
|
|
3766
|
-
|
|
3767
|
-
// by the API (which reads `sort`), so route both through sort.
|
|
3768
|
-
sort: parsed.sort ?? parsed.order,
|
|
4021
|
+
sort: parsed.sort,
|
|
3769
4022
|
limit: parsed.limit,
|
|
3770
4023
|
page: parsed.page,
|
|
3771
4024
|
cleanText: true,
|