@nestr/mcp 0.1.100 → 0.1.103

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.
@@ -55,8 +55,11 @@ const COMPACT_FIELDS = {
55
55
  base: ["_id", "title", "purpose", "completed", "labels", "path", "parentId", "ancestors", "description", "due", "users", "hints"],
56
56
  // Additional fields for roles
57
57
  role: ["accountabilities", "domains"],
58
- // Additional fields for users
59
- user: ["_id", "username", "profile"],
58
+ // Additional fields for users. `bot` and `assistant` are not optional trim:
59
+ // stripping them makes an agent indistinguishable from a person in a list, and
60
+ // "which of these can act on my own credentials for me" then has no answer in
61
+ // the data at all. Two booleans per row.
62
+ user: ["_id", "username", "profile", "bot", "assistant"],
60
63
  // Additional fields for labels
61
64
  label: ["_id", "title"],
62
65
  };
@@ -169,6 +172,46 @@ const HINT_URL_PATTERNS = [
169
172
  { pattern: /^\/nests\/([^/]+)$/, tool: "nestr_get_nest", params: (m) => ({ nestId: m[1] }) },
170
173
  ];
171
174
  const HINT_TYPE_TOOL_CALLS = {
175
+ // Raised by POST /connectors when a hand-written url points at a vendor the
176
+ // deployment ships a template for. The follow-up is to look at the template,
177
+ // not to re-register blindly: the connector just created may be exactly what
178
+ // the caller wanted, and only they can say. The hint carries its own ids, so
179
+ // this works on a catalog entry rather than needing nest ancestors.
180
+ connector_template_available(record) {
181
+ const hints = record.hints || [];
182
+ const hint = hints.find((h) => { return h.type === "connector_template_available"; });
183
+ const workspaceId = (hint && hint.workspaceId) || record.workspaceId;
184
+ if (!workspaceId)
185
+ return null;
186
+ return {
187
+ tool: "nestr_list_connector_templates",
188
+ params: { workspaceId },
189
+ };
190
+ },
191
+ // Raised by GET /connector-templates on the response that LISTS them, because
192
+ // listing turned out not to be enough. A caller that could see the Xero
193
+ // template still hand-built a connector from what the template told it, and a
194
+ // hand-built copy carries no settingsKey, no OAuth client and whatever
195
+ // endpoint the model believed, so it authorises and then fails at first use.
196
+ // The follow-up here is the register call itself, pre-filled with the template
197
+ // id, so using the template is one call rather than a thing to remember.
198
+ connector_template_create(record) {
199
+ const hints = record.hints || [];
200
+ const hint = hints.find((h) => { return h.type === "connector_template_create"; });
201
+ if (!hint)
202
+ return null;
203
+ const workspaceId = hint.workspaceId || record.workspaceId;
204
+ if (!workspaceId)
205
+ return null;
206
+ const ids = hint.templateIds || [];
207
+ return {
208
+ tool: "nestr_register_connector",
209
+ params: {
210
+ workspaceId,
211
+ templateId: ids.length === 1 ? ids[0] : "<id of the template you want, from this list>",
212
+ },
213
+ };
214
+ },
172
215
  no_strategy(nest) {
173
216
  const nestId = nest._id;
174
217
  if (!nestId)
@@ -605,7 +648,7 @@ const coerceIntArray = (schema) => z.preprocess((val) => {
605
648
  // Shared description for the sort parameter on list/fetch tools. All of these
606
649
  // endpoints honor a `sort` query param server-side (field name, '-' prefix for
607
650
  // descending) — the same fields the search `sort:` operator uses.
608
- const MENTION_DESC = "Supports HTML and @mentions. Mentions MUST use literal curly braces: `@{userId:roleId}`, NOT `@userId`. Without braces nothing is linked and nobody is notified. Forms: `@{userId:roleId}` (preferred, names the role), `@{userId}`, `@{email}`, `@{circle}` (all fillers in the nearest ancestor circle).";
651
+ const MENTION_DESC = "Supports HTML and @mentions. Mentions MUST use literal curly braces: `@{userId:roleId}`, NOT `@userId`. Without braces nothing is linked and nobody is notified. Forms: `@{userId:roleId}` (preferred, names the role), `@{userId}`, `@{email}`, `@{circle}` (all fillers in the nearest ancestor circle). 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. When you do not know which role the person is acting in, use `@{userId}` rather than substituting the nest you happen to be working on.";
609
652
  const PRIME_LABEL_RULE = "At most ONE prime label per nest (project, tension, role, circle, anchor-circle, meeting, metric, goal, result, checklist, feedback, userstory, sprint, epic, milestone): they are the nest's identity and cannot coexist. Only exception: userstory may pair with project.";
610
653
  const PURPOSE_DESC = "Only for workspaces, circles and roles: a short aspirational statement. Details belong in description, not here. Supports HTML.";
611
654
  const CONTENT_DESC = "The primary content field: details, context, acceptance criteria. Structured data goes in fields, progress in comments. Supports Markdown and HTML.";
@@ -711,6 +754,7 @@ export const schemas = {
711
754
  labels: coerceFromJson(z.array(z.string())).optional().describe("Label IDs to apply"),
712
755
  fields: coerceFromJson(z.record(z.unknown())).optional().describe("Structured field values to set on creation (e.g., { 'project.status': 'Current' }, { 'skill.type': 'process' }). Same shape as nestr_update_nest fields — saves a follow-up update call."),
713
756
  users: coerceFromJson(z.array(z.string())).optional().describe("User IDs to assign (required for tasks/projects to associate with a person)"),
757
+ due: z.string().optional().describe("Due date (ISO 8601). SET THIS whenever the item is meant to happen at a time. It is a field, not a title: the sweep that fires dated work reads `due` and nothing else, so a task called \"Daily digest, 28 Aug\" with no due is invisible to it and simply never runs, with nothing anywhere saying so. For projects/tasks: deadline. For meetings: start time."),
714
758
  accountabilities: coerceFromJson(z.array(z.string())).optional().describe("Accountability titles for roles/circles. Only used when labels include 'role' or 'circle'. Each string becomes an accountability child nest."),
715
759
  domains: coerceFromJson(z.array(z.string())).optional().describe("Domain titles for roles/circles. Only used when labels include 'role' or 'circle'. Each string becomes a domain child nest."),
716
760
  workspaceId: z.string().optional().describe("Workspace ID. Required when creating roles/circles with accountabilities or domains (used to route to the self-organization API)."),
@@ -784,6 +828,7 @@ export const schemas = {
784
828
  listUsers: z.object({
785
829
  workspaceId: z.string().describe("Workspace ID"),
786
830
  search: z.string().optional().describe("Search by name or email"),
831
+ agents: z.enum(["only", "exclude"]).optional().describe("'only' for this workspace's agents, 'exclude' for its people. Omit for both."),
787
832
  limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
788
833
  page: z.number().optional().describe("Page number for pagination"),
789
834
  }),
@@ -1064,20 +1109,72 @@ export const schemas = {
1064
1109
  listConnectors: z.object({
1065
1110
  workspaceId: z.string().describe("Workspace ID whose connector catalog to list"),
1066
1111
  }),
1112
+ listConnectorTemplates: z.object({
1113
+ workspaceId: z.string().describe("Workspace ID whose available connector templates to list"),
1114
+ }),
1067
1115
  registerConnector: z.object({
1116
+ 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."),
1068
1117
  workspaceId: z.string().describe("Workspace ID to register the connector in"),
1069
- type: z.enum(["mcp", "cli", "api"]).describe("Transport: 'mcp' (MCP server over a url), 'api' (REST endpoint over a url), or 'cli' (a command)"),
1070
- name: z.string().describe("Unique connector name within the workspace catalog"),
1071
- config: coerceFromJson(z.record(z.unknown())).optional().describe("Transport config, no secret. mcp/api need a url, cli a command. Optional non-secret headers go under headers."),
1072
- capabilities: coerceFromJson(z.record(z.unknown())).optional().describe("{ discover: boolean, tools: [{ name, description, inputSchema }] }. discover:true lets the connector self-describe its tools at runtime."),
1073
- exposure: coerceFromJson(z.record(z.unknown())).optional().describe("Which owners may bind: { userAgent: boolean, domainGated: boolean }. domainGated:true allows binding to a role's domain."),
1074
- authStrategy: z.enum(["secret", "oauth2"]).optional().describe("How a principal connects: 'secret' (one-time, via the Connect button) or 'oauth2'. The agent never sees it."),
1118
+ 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."),
1119
+ 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."),
1120
+ 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."),
1121
+ 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."),
1122
+ 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."),
1123
+ 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."),
1124
+ }),
1125
+ createAgent: z.object({
1126
+ workspaceId: z.string().describe("Workspace ID to create the agent in"),
1127
+ 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."),
1128
+ description: z.string().optional().describe("What the agent is LIKE: its character and what it is careful about. Not its instructions, which belong in a skill on the role."),
1129
+ roleAssignable: z.boolean().optional().describe("Defaults to true. Pass false to create an ASSISTANT: an agent that can never be assigned to a role, so it can never stop being able to act with the authority of the person it helps."),
1130
+ 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."),
1075
1131
  }),
1076
1132
  bindConnector: z.object({
1077
1133
  workspaceId: z.string().describe("Workspace ID the connector and owner belong to"),
1078
1134
  connectorId: z.string().describe("ID of an enabled connector from nestr_list_connectors"),
1079
- ownerType: z.enum(["user", "agent", "workspace", "role-domain"]).describe("Owner type. 'role-domain' binds to a role's domain, materialising a credentials field there."),
1080
- ownerId: z.string().describe("Owner ID. user/agent: the user ID. workspace: the workspace ID. role-domain: the domain nest ID."),
1135
+ ownerType: z.enum(["role", "role-domain", "workspace", "user", "agent"]).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. 'user' is one person's own account and 'agent' is one bot's own: both are personal, both need a workspace admin, and both are the wrong answer unless the thing really does belong to that one principal. See the tool description."),
1136
+ ownerId: z.string().describe("Owner ID. role: the role nest ID. role-domain: the domain nest ID. workspace: the workspace ID. user: the person's user ID. agent: the bot's user ID."),
1137
+ }),
1138
+ updateConnector: z.object({
1139
+ workspaceId: z.string().describe("Workspace ID the connector belongs to"),
1140
+ connectorId: z.string().describe("ID of the connector to update"),
1141
+ type: z.enum(["mcp", "cli", "api"]).optional().describe("Transport"),
1142
+ name: z.string().optional().describe("Unique connector name within the workspace catalog"),
1143
+ config: coerceFromJson(z.record(z.unknown())).optional().describe("Per-type transport config, no secret"),
1144
+ capabilities: coerceFromJson(z.record(z.unknown())).optional().describe("Capability descriptor"),
1145
+ exposure: coerceFromJson(z.record(z.unknown())).optional().describe("Exposure policy: { userAgent, domainGated }"),
1146
+ authStrategy: z.enum(["secret", "oauth2"]).optional().describe("How a principal connects"),
1147
+ enabled: z.boolean().optional().describe("Switch the connector on or off in this workspace"),
1148
+ }),
1149
+ removeConnector: z.object({
1150
+ workspaceId: z.string().describe("Workspace ID the connector belongs to"),
1151
+ connectorId: z.string().describe("ID of the connector to remove from the catalog"),
1152
+ }),
1153
+ listConnections: z.object({
1154
+ workspaceId: z.string().describe("Workspace ID whose bindings to list"),
1155
+ includeDisabled: z.boolean().optional().describe("Include removed (disabled) bindings. Default false."),
1156
+ }),
1157
+ removeConnection: z.object({
1158
+ workspaceId: z.string().describe("Workspace ID the binding belongs to"),
1159
+ connectionId: z.string().describe("ID of the binding to remove, from nestr_list_connections"),
1160
+ }),
1161
+ getConnectLink: z.object({
1162
+ workspaceId: z.string().describe("Workspace ID the binding belongs to"),
1163
+ connectionId: z.string().describe("ID of the binding to connect, from nestr_list_connections"),
1164
+ }),
1165
+ revokeConnectionCredential: z.object({
1166
+ workspaceId: z.string().describe("Workspace ID the binding belongs to"),
1167
+ connectionId: z.string().describe("ID of the binding whose credential to revoke"),
1168
+ }),
1169
+ getAgentConnectorReach: z.object({
1170
+ workspaceId: z.string().describe("Workspace ID the agent belongs to"),
1171
+ agentUserId: z.string().describe("The agent's bot user ID"),
1172
+ }),
1173
+ runAgent: z.object({
1174
+ workspaceId: z.string().describe("Workspace ID the agent belongs to"),
1175
+ agentUserId: z.string().describe("The agent's bot user ID"),
1176
+ nestId: z.string().describe("The nest the run is pinned to: a role, a project, a task"),
1177
+ message: z.string().optional().describe("What this run is for. Omit for a plain 'advance this item' run."),
1081
1178
  }),
1082
1179
  // File attachments (a comment id works as the nestId — files are keyed by nestId)
1083
1180
  getNestFiles: z.object({
@@ -1294,6 +1391,10 @@ export const toolDefinitions = [
1294
1391
  items: { type: "string" },
1295
1392
  description: "User IDs to assign. ALWAYS set this for projects and tasks — use the role filler's user ID. Placing a nest under a role does NOT auto-assign it.",
1296
1393
  },
1394
+ due: {
1395
+ type: "string",
1396
+ description: "Due date (ISO 8601). SET THIS whenever the item is meant to happen at a time. It is a field, not a title: the sweep that fires dated work reads `due` and nothing else, so a task called \"Daily digest, 28 Aug\" with no due is invisible to it and simply never runs, with nothing anywhere saying so. For projects/tasks: deadline. For meetings: start time.",
1397
+ },
1297
1398
  accountabilities: {
1298
1399
  type: "array",
1299
1400
  items: { type: "string" },
@@ -1510,7 +1611,7 @@ export const toolDefinitions = [
1510
1611
  },
1511
1612
  {
1512
1613
  name: "nestr_start_dm_thread",
1513
- description: "Start a new direct-message thread with someone. Use it for a new subject rather than reopening an old thread. You must share a workspace with them, or already have a conversation with them.",
1614
+ 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. A DM needs at least one PERSON in it: agents cannot hold a private conversation with each other, and the server refuses one. That is the wrong channel rather than a missing permission, so do not ask anyone to widen anything. To reach another agent, comment on the work itself and mention them there, or raise a tension to the role that owns it, which keeps the exchange where the people accountable for the work can read it.",
1514
1615
  inputSchema: {
1515
1616
  type: "object",
1516
1617
  properties: {
@@ -1655,12 +1756,17 @@ export const toolDefinitions = [
1655
1756
  },
1656
1757
  {
1657
1758
  name: "nestr_list_users",
1658
- description: "List members of a workspace. Response includes meta.total showing total matching count. Also the tool to resolve user ids in bulk: when presenting users to a person, show names and/or emails, never bare ids.",
1759
+ description: "List members of a workspace: its people, its agents, or both. Response includes meta.total showing total matching count. Also the tool to resolve user ids in bulk: when presenting users to a person, show names and/or emails, never bare ids. With agents:'only' this is how you find out which other agents exist and what each is for: an agent carries bot:true, its purpose in profile.agentDescription, and assistant:true when it may never fill a role. An assistant is the one to hand work to that needs a person's OWN credentials, which a role-filling agent cannot reach.",
1659
1760
  inputSchema: {
1660
1761
  type: "object",
1661
1762
  properties: {
1662
1763
  workspaceId: { type: "string", description: "Workspace ID" },
1663
1764
  search: { type: "string", description: "Search by name or email" },
1765
+ agents: {
1766
+ type: "string",
1767
+ enum: ["only", "exclude"],
1768
+ description: "'only' for this workspace's agents, 'exclude' for its people. Omit for both.",
1769
+ },
1664
1770
  limit: { type: "number", description: "Omit on first call to see meta.total count" },
1665
1771
  page: { type: "number", description: "Page number (1-indexed)" },
1666
1772
  },
@@ -1670,7 +1776,7 @@ export const toolDefinitions = [
1670
1776
  },
1671
1777
  {
1672
1778
  name: "nestr_list_labels",
1673
- description: "List available labels in a workspace. Response includes meta.total showing total matching count.",
1779
+ description: "List available labels in a workspace. Response includes meta.total showing total matching count. The list does not carry autoComplete, so it mixes labels a person can pick with internal machinery they cannot. Call nestr_get_label before offering a label as a choice to somebody.",
1674
1780
  inputSchema: {
1675
1781
  type: "object",
1676
1782
  properties: {
@@ -1770,7 +1876,7 @@ export const toolDefinitions = [
1770
1876
  },
1771
1877
  {
1772
1878
  name: "nestr_get_label",
1773
- description: "Get details of a specific label.",
1879
+ description: "Get details of a specific label, including fields, properties, group and autoComplete. `autoComplete: false` marks a system label: it is withheld from the label picker, and an already-applied one is hidden from the label tags on the nest too, so the person cannot see it in either place. Do not suggest such a label to a user, apply it as if they had chosen it, or tell them to click it on an item, because there is nothing there to click. It is not unreachable though: typing the id verbatim in the add/remove label modal still matches it, and a `label:` search on the id still finds the items carrying it, so offer those two routes rather than saying it cannot be done. Every check tests for an explicit false, so a label that omits the property is shown normally: treat missing as visible, not as unknown. Only this single-label read returns autoComplete; nestr_list_labels does not.",
1774
1880
  inputSchema: {
1775
1881
  type: "object",
1776
1882
  properties: {
@@ -2421,19 +2527,38 @@ export const toolDefinitions = [
2421
2527
  },
2422
2528
  ...readOnly,
2423
2529
  },
2530
+ {
2531
+ name: "nestr_list_connector_templates",
2532
+ 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.",
2533
+ inputSchema: {
2534
+ type: "object",
2535
+ properties: {
2536
+ workspaceId: { type: "string", description: "Workspace ID whose available connector templates to list" },
2537
+ },
2538
+ required: ["workspaceId"],
2539
+ },
2540
+ ...readOnly,
2541
+ },
2424
2542
  {
2425
2543
  name: "nestr_register_connector",
2426
- description: "Register a connector in the workspace catalog: a reusable mcp/cli/api template holding no secret. Workspace-admin only; a non-admin gets AUTH_SCOPE_INSUFFICIENT (call nestr_diagnose on any auth error). Give type ('mcp' and 'api' need a url in config, 'cli' a command) and a unique name; optionally capabilities, exposure, authStrategy. This creates the template only. Flow: register, bind to a role's domain with nestr_bind_connector, then a human or agent connects the account via the credentials field's Connect button. The secret is captured out-of-band, never by the agent.",
2544
+ 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.",
2427
2545
  inputSchema: {
2428
2546
  type: "object",
2429
2547
  properties: {
2548
+ templateId: {
2549
+ type: "string",
2550
+ 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.",
2551
+ },
2430
2552
  workspaceId: { type: "string", description: "Workspace ID to register the connector in" },
2431
2553
  type: {
2432
2554
  type: "string",
2433
2555
  enum: ["mcp", "cli", "api"],
2434
- description: "Transport: 'mcp' (MCP server over a url), 'api' (REST endpoint over a url), or 'cli' (a command)",
2556
+ description: "Transport: 'mcp' (MCP server over a url), 'api' (REST endpoint over a url), or 'cli' (a command). Required unless templateId is given.",
2557
+ },
2558
+ name: {
2559
+ type: "string",
2560
+ description: "Unique connector name within the workspace catalog. Required unless templateId is given, where it defaults to the template's own name.",
2435
2561
  },
2436
- name: { type: "string", description: "Unique connector name within the workspace catalog" },
2437
2562
  config: {
2438
2563
  type: "object",
2439
2564
  description: "Transport config, no secret. mcp/api need a url, cli a command. Optional non-secret headers go under headers.",
@@ -2452,13 +2577,44 @@ export const toolDefinitions = [
2452
2577
  description: "How a principal connects: 'secret' (one-time, via the Connect button) or 'oauth2'. The agent never sees it.",
2453
2578
  },
2454
2579
  },
2455
- required: ["workspaceId", "type", "name"],
2580
+ // workspaceId only: with a templateId the transport comes from the template,
2581
+ // and demanding type and name here is what made the whole template path
2582
+ // unreachable from a client that reads the schema.
2583
+ required: ["workspaceId"],
2584
+ },
2585
+ ...mutating,
2586
+ },
2587
+ {
2588
+ name: "nestr_create_agent",
2589
+ 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).\n\nWhat decides how an agent behaves at run time is not this flag: it is whether the agent FILLS ANY ROLE in the workspace at that moment. Filling one and assisting are exclusive, and the test is not 'does it fill THIS role' but 'does it fill any', so one role anywhere makes every one of its runs a role-filler run, acting with that role's authority and reaching only the role's connectors. An agent that fills none assists whoever engages it, acting with THAT person's authority and reaching what they reach, their own connectors included.\n\nroleAssignable decides whether that can ever change. Default (true) is an ordinary agent: it assists while it holds no role, and becomes a filler the moment anyone assigns it to one. Pass false for an ASSISTANT: it can never be assigned to a role, so it can never stop being able to act for a person. Prefer false whenever the agent exists to help people with work that follows them, because otherwise a single well-meant role assignment silently ends that.\n\nEither way, work that follows a PERSON (their mailbox, their drive, their queue) puts the agent on the TASK beside them and never in the role's users. Assigning it to the role is the specific mistake: it then cannot reach anything of theirs, reports that it holds no external tool sources, and suggests binding their account to the role, which would hand it to whoever fills that role next.\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. 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.",
2590
+ inputSchema: {
2591
+ type: "object",
2592
+ properties: {
2593
+ workspaceId: { type: "string", description: "Workspace ID to create the agent in" },
2594
+ name: {
2595
+ type: "string",
2596
+ 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.",
2597
+ },
2598
+ description: {
2599
+ type: "string",
2600
+ description: "What the agent is LIKE: its character and what it is careful about. Not its instructions, which belong in a skill on the role.",
2601
+ },
2602
+ roleAssignable: {
2603
+ type: "boolean",
2604
+ description: "Defaults to true, meaning it MAY be assigned to a role. It still assists while it holds none; assigning it to one is what ends that. Pass false for an ASSISTANT, which can never be assigned and so can never stop acting for a person. See the tool description.",
2605
+ },
2606
+ agentConfig: {
2607
+ type: "object",
2608
+ 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.",
2609
+ },
2610
+ },
2611
+ required: ["workspaceId", "name"],
2456
2612
  },
2457
2613
  ...mutating,
2458
2614
  },
2459
2615
  {
2460
2616
  name: "nestr_bind_connector",
2461
- description: "Bind a registered connector to an owner so that owner can use it. Owner types: 'user' or 'agent' (ownerId is the user ID), 'workspace' (the workspace ID), 'role-domain' (the domain nest ID). A role-domain binding also materialises a credentials field on the domain nest, so the Connect button renders there and the response carries credentialsField { domainId, fieldId, fieldCode }. A human or agent then connects the account through that button; the secret is captured out-of-band, never seen by the agent. Workspace-admin only, and the connector must already be registered and enabled.",
2617
+ 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), 'workspace' (ownerId is the workspace ID), 'user' (ownerId is a person's user ID, for their own account), or 'agent' (ownerId is a bot's user ID, for its own). 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 carries domainId (and domainCreated when the bind created it). 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\n'user' is for what is genuinely one person's: their own mailbox, their own drive. Binding that to a role would hand it to whoever fills the role next, which is the opposite of what they asked for, so this is the one case where a role binding is wrong. It needs a workspace admin and the connector has to be open to user owners in the catalog; if either is missing, say which and what the admin has to do rather than falling back to a role binding. The work on a personal connection is then done by an agent that fills NO role, because filling a role and assisting are exclusive and only an assistant can act with a person's own access.\n\n'agent' is the same shape for a bot's own account, used where a provider gives the agent its own login rather than borrowing a person's. It is the rarest of the four: prefer 'role', so the access belongs to the work and survives the filler changing. Both personal types are workspace-admin only and both need the connector open to user/agent owners, so a caller who is not an admin gets AUTH_SCOPE_INSUFFICIENT and should say which of the two is missing rather than binding to a role instead. Be careful arranging one agent's credential from another agent's run: it is a decision about what that agent may reach, so name what you are about to do and why before doing it.",
2462
2618
  inputSchema: {
2463
2619
  type: "object",
2464
2620
  properties: {
@@ -2466,18 +2622,131 @@ export const toolDefinitions = [
2466
2622
  connectorId: { type: "string", description: "ID of an enabled connector from nestr_list_connectors" },
2467
2623
  ownerType: {
2468
2624
  type: "string",
2469
- enum: ["user", "agent", "workspace", "role-domain"],
2470
- description: "Owner type. 'role-domain' binds to a role's domain, materialising a credentials field there.",
2625
+ enum: ["role", "workspace", "role-domain", "user", "agent"],
2626
+ 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. 'user' is one person's own account and 'agent' is one bot's own: both are personal, both are workspace-admin only, and both are wrong unless the thing really belongs to that single principal.",
2471
2627
  },
2472
2628
  ownerId: {
2473
2629
  type: "string",
2474
- description: "Owner ID. user/agent: the user ID. workspace: the workspace ID. role-domain: the domain nest ID.",
2630
+ description: "Owner ID. role: the role nest ID. role-domain: the domain nest ID. workspace: the workspace ID. user: the person's user ID. agent: the bot's user ID.",
2475
2631
  },
2476
2632
  },
2477
2633
  required: ["workspaceId", "connectorId", "ownerType", "ownerId"],
2478
2634
  },
2479
2635
  ...mutating,
2480
2636
  },
2637
+ {
2638
+ name: "nestr_update_connector",
2639
+ 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.",
2640
+ inputSchema: {
2641
+ type: "object",
2642
+ properties: {
2643
+ workspaceId: { type: "string", description: "Workspace ID the connector belongs to" },
2644
+ connectorId: { type: "string", description: "ID of the connector to update" },
2645
+ type: { type: "string", enum: ["mcp", "cli", "api"], description: "Transport" },
2646
+ name: { type: "string", description: "Unique connector name within the workspace catalog" },
2647
+ config: { type: "object", description: "Per-type transport config, no secret" },
2648
+ capabilities: { type: "object", description: "Capability descriptor" },
2649
+ exposure: { type: "object", description: "Exposure policy: { userAgent, domainGated }" },
2650
+ authStrategy: { type: "string", enum: ["secret", "oauth2"], description: "How a principal connects" },
2651
+ enabled: { type: "boolean", description: "Switch the connector on or off in this workspace" },
2652
+ },
2653
+ required: ["workspaceId", "connectorId"],
2654
+ },
2655
+ ...mutating,
2656
+ },
2657
+ {
2658
+ name: "nestr_remove_connector",
2659
+ 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.",
2660
+ inputSchema: {
2661
+ type: "object",
2662
+ properties: {
2663
+ workspaceId: { type: "string", description: "Workspace ID the connector belongs to" },
2664
+ connectorId: { type: "string", description: "ID of the connector to remove" },
2665
+ },
2666
+ required: ["workspaceId", "connectorId"],
2667
+ },
2668
+ ...destructive,
2669
+ },
2670
+ {
2671
+ name: "nestr_list_connections",
2672
+ ...readOnly,
2673
+ 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.",
2674
+ inputSchema: {
2675
+ type: "object",
2676
+ properties: {
2677
+ workspaceId: { type: "string", description: "Workspace ID whose bindings to list" },
2678
+ includeDisabled: { type: "boolean", description: "Include removed bindings. Default false." },
2679
+ },
2680
+ required: ["workspaceId"],
2681
+ },
2682
+ },
2683
+ {
2684
+ name: "nestr_remove_connection",
2685
+ 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.",
2686
+ inputSchema: {
2687
+ type: "object",
2688
+ properties: {
2689
+ workspaceId: { type: "string", description: "Workspace ID the binding belongs to" },
2690
+ connectionId: { type: "string", description: "Binding ID from nestr_list_connections" },
2691
+ },
2692
+ required: ["workspaceId", "connectionId"],
2693
+ },
2694
+ ...destructive,
2695
+ },
2696
+ {
2697
+ name: "nestr_get_connect_link",
2698
+ 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.",
2699
+ inputSchema: {
2700
+ type: "object",
2701
+ properties: {
2702
+ workspaceId: { type: "string", description: "Workspace ID the binding belongs to" },
2703
+ connectionId: { type: "string", description: "Binding ID from nestr_list_connections" },
2704
+ },
2705
+ required: ["workspaceId", "connectionId"],
2706
+ },
2707
+ ...mutating,
2708
+ },
2709
+ {
2710
+ name: "nestr_revoke_connection_credential",
2711
+ 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.",
2712
+ inputSchema: {
2713
+ type: "object",
2714
+ properties: {
2715
+ workspaceId: { type: "string", description: "Workspace ID the binding belongs to" },
2716
+ connectionId: { type: "string", description: "Binding ID from nestr_list_connections" },
2717
+ },
2718
+ required: ["workspaceId", "connectionId"],
2719
+ },
2720
+ ...destructive,
2721
+ },
2722
+ {
2723
+ name: "nestr_get_agent_connectors",
2724
+ ...readOnly,
2725
+ 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.",
2726
+ inputSchema: {
2727
+ type: "object",
2728
+ properties: {
2729
+ workspaceId: { type: "string", description: "Workspace ID the agent belongs to" },
2730
+ agentUserId: { type: "string", description: "The agent's bot user ID" },
2731
+ },
2732
+ required: ["workspaceId", "agentUserId"],
2733
+ },
2734
+ },
2735
+ {
2736
+ name: "nestr_run_agent",
2737
+ 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.",
2738
+ inputSchema: {
2739
+ type: "object",
2740
+ properties: {
2741
+ workspaceId: { type: "string", description: "Workspace ID the agent belongs to" },
2742
+ agentUserId: { type: "string", description: "The agent's bot user ID" },
2743
+ nestId: { type: "string", description: "The nest the run is pinned to: a role, a project, a task" },
2744
+ message: { type: "string", description: "What this run is for. Omit for a plain 'advance this item' run." },
2745
+ },
2746
+ required: ["workspaceId", "agentUserId", "nestId"],
2747
+ },
2748
+ ...mutating,
2749
+ },
2481
2750
  {
2482
2751
  name: "nestr_get_nest_files",
2483
2752
  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.",
@@ -2957,6 +3226,7 @@ async function _handleToolCall(client, name, args, context) {
2957
3226
  labels: parsed.labels,
2958
3227
  fields: parsed.fields,
2959
3228
  users: parsed.users,
3229
+ due: parsed.due,
2960
3230
  });
2961
3231
  return formatResult({ message: "Nest created successfully", nest });
2962
3232
  }
@@ -3089,6 +3359,7 @@ async function _handleToolCall(client, name, args, context) {
3089
3359
  const parsed = schemas.listUsers.parse(args);
3090
3360
  const users = await client.listUsers(parsed.workspaceId, {
3091
3361
  search: parsed.search,
3362
+ agents: parsed.agents,
3092
3363
  limit: parsed.limit,
3093
3364
  page: parsed.page,
3094
3365
  });
@@ -3627,9 +3898,49 @@ async function _handleToolCall(client, name, args, context) {
3627
3898
  const connectors = await client.listConnectors(parsed.workspaceId);
3628
3899
  return formatResult(connectors);
3629
3900
  }
3901
+ case "nestr_list_connector_templates": {
3902
+ const parsed = schemas.listConnectorTemplates.parse(args);
3903
+ const listed = await client.listConnectorTemplates(parsed.workspaceId);
3904
+ const templates = listed.templates;
3905
+ if (!Array.isArray(templates) || templates.length === 0) {
3906
+ return { content: [{ type: "text", text: "This deployment offers no connector templates." }] };
3907
+ }
3908
+ // Enriched so the hint arrives as the register call itself, pre-filled
3909
+ // with the template id, the same treatment every other hint gets.
3910
+ const enrichedTemplates = enrichHints({
3911
+ workspaceId: parsed.workspaceId,
3912
+ ...(listed.hints ? { hints: listed.hints } : {}),
3913
+ });
3914
+ return formatResult({
3915
+ 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.",
3916
+ templates,
3917
+ ...(enrichedTemplates.hints ? { hints: enrichedTemplates.hints } : {}),
3918
+ });
3919
+ }
3630
3920
  case "nestr_register_connector": {
3631
3921
  const parsed = schemas.registerConnector.parse(args);
3632
- const connector = await client.registerConnector(parsed.workspaceId, {
3922
+ if (parsed.templateId) {
3923
+ const fromTemplate = await client.registerConnector(parsed.workspaceId, {
3924
+ templateId: parsed.templateId,
3925
+ ...(parsed.name ? { name: parsed.name } : {}),
3926
+ });
3927
+ return formatResult({
3928
+ 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.",
3929
+ connector: fromTemplate.connector,
3930
+ // The route's hints say what to do next (a Connect link, a missing
3931
+ // OAuth client); dropping them cost the model its follow-up.
3932
+ ...(fromTemplate.hints ? { hints: fromTemplate.hints } : {}),
3933
+ });
3934
+ }
3935
+ if (!parsed.type || !parsed.name) {
3936
+ return formatError({
3937
+ error: true,
3938
+ code: "VALIDATION",
3939
+ message: "Without templateId, both type and name are required. Call nestr_list_connector_templates first: a known vendor almost always has one.",
3940
+ retryable: false,
3941
+ });
3942
+ }
3943
+ const registered = await client.registerConnector(parsed.workspaceId, {
3633
3944
  type: parsed.type,
3634
3945
  name: parsed.name,
3635
3946
  config: parsed.config,
@@ -3637,10 +3948,34 @@ async function _handleToolCall(client, name, args, context) {
3637
3948
  exposure: parsed.exposure,
3638
3949
  authStrategy: parsed.authStrategy,
3639
3950
  });
3951
+ // Enriched so a template hint arrives as a tool call the model can make,
3952
+ // the same treatment every other hint gets. workspaceId is on the entry,
3953
+ // which is what lets enrichHints work on something that is not a nest.
3954
+ const enriched = enrichHints({
3955
+ ...registered.connector,
3956
+ ...(registered.hints ? { hints: registered.hints } : {}),
3957
+ });
3640
3958
  return formatResult({
3641
- message: "Connector registered. Next, bind it to an owner with nestr_bind_connector (e.g. a role's domain), then a human or agent connects the account via the credentials field's Connect button.",
3642
- connector,
3959
+ 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.",
3960
+ connector: enriched,
3961
+ });
3962
+ }
3963
+ case "nestr_create_agent": {
3964
+ const parsed = schemas.createAgent.parse(args);
3965
+ const agent = await client.createAgent(parsed.workspaceId, {
3966
+ name: parsed.name,
3967
+ description: parsed.description,
3968
+ roleAssignable: parsed.roleAssignable,
3969
+ agentConfig: parsed.agentConfig,
3643
3970
  });
3971
+ // Two different next steps, because the two kinds of agent are put to
3972
+ // work in different places and saying "assign it to the role" to
3973
+ // someone who just made an assistant is the mistake this tool exists to
3974
+ // stop.
3975
+ const nextStep = parsed.roleAssignable === false
3976
+ ? "Assistant created: it can never be assigned to a role, so it will always act with the authority of whoever it is helping. Put it on the TASK beside that person (nestr_update_nest users on the task, naming both), and leave the role's users to them alone. Its instructions belong in a skill nest under the role."
3977
+ : "Agent created, and assignable to roles. It assists while it holds none; assigning it to a role is what makes it act as that role instead. To have it fill work: create or find a role named for the WORK (not for the agent) with nestr_create_nest, then assign it with nestr_update_nest users. To have it help a person with work that follows THEM, leave it out of every role and put it on the task beside them. Its instructions belong in a skill nest under the role, not on the agent.";
3978
+ return formatResult({ message: nextStep, agent });
3644
3979
  }
3645
3980
  case "nestr_bind_connector": {
3646
3981
  const parsed = schemas.bindConnector.parse(args);
@@ -3648,14 +3983,90 @@ async function _handleToolCall(client, name, args, context) {
3648
3983
  connectorId: parsed.connectorId,
3649
3984
  owner: { type: parsed.ownerType, id: parsed.ownerId },
3650
3985
  });
3651
- // For a role-domain binding the API materialises a credentials field on
3652
- // the domain; surface that explicitly so the caller knows the Connect
3653
- // button now renders there and the secret is captured out-of-band.
3654
- const message = parsed.ownerType === "role-domain"
3655
- ? "Connector bound to the role's domain. A credentials field was materialised on the domain (see credentialsField) so the role can use the connector and the Connect button renders. A human or agent now connects the account via that button; the secret is captured out-of-band and never by the agent."
3656
- : "Connector bound to the owner. The owner now connects the account out-of-band; the secret is never seen by the agent.";
3986
+ // A role binding creates the connector's domain when there isn't one, so
3987
+ // say where the access landed. The credential is always a separate,
3988
+ // out-of-band step: hand the human a link from nestr_get_connect_link.
3989
+ const message = parsed.ownerType === "role" || parsed.ownerType === "role-domain"
3990
+ ? "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."
3991
+ : "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.";
3657
3992
  return formatResult({ message, connection });
3658
3993
  }
3994
+ case "nestr_update_connector": {
3995
+ const parsed = schemas.updateConnector.parse(args);
3996
+ const { workspaceId, connectorId, ...updates } = parsed;
3997
+ if (Object.keys(updates).length === 0) {
3998
+ return formatError({
3999
+ error: true,
4000
+ code: "VALIDATION",
4001
+ message: "Nothing to update: pass at least one field to change.",
4002
+ retryable: false,
4003
+ });
4004
+ }
4005
+ const connector = await client.updateConnector(workspaceId, connectorId, updates);
4006
+ return formatResult({ message: "Connector updated.", connector });
4007
+ }
4008
+ case "nestr_remove_connector": {
4009
+ const parsed = schemas.removeConnector.parse(args);
4010
+ await client.removeConnector(parsed.workspaceId, parsed.connectorId);
4011
+ return formatResult({
4012
+ message: "Connector removed from the catalog. Bindings that named it stop resolving.",
4013
+ connectorId: parsed.connectorId,
4014
+ });
4015
+ }
4016
+ case "nestr_list_connections": {
4017
+ const parsed = schemas.listConnections.parse(args);
4018
+ const connections = await client.listConnections(parsed.workspaceId, {
4019
+ includeDisabled: parsed.includeDisabled,
4020
+ });
4021
+ return formatResult(connections);
4022
+ }
4023
+ case "nestr_remove_connection": {
4024
+ const parsed = schemas.removeConnection.parse(args);
4025
+ const result = await client.removeConnection(parsed.workspaceId, parsed.connectionId);
4026
+ return formatResult({
4027
+ message: `Access removed. ${result.revokedCount} credential(s) revoked.`,
4028
+ connectionId: parsed.connectionId,
4029
+ });
4030
+ }
4031
+ case "nestr_get_connect_link": {
4032
+ const parsed = schemas.getConnectLink.parse(args);
4033
+ const link = await client.getConnectLink(parsed.workspaceId, parsed.connectionId);
4034
+ return formatResult({
4035
+ 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.",
4036
+ ...link,
4037
+ });
4038
+ }
4039
+ case "nestr_revoke_connection_credential": {
4040
+ const parsed = schemas.revokeConnectionCredential.parse(args);
4041
+ await client.revokeConnectionCredential(parsed.workspaceId, parsed.connectionId);
4042
+ return formatResult({
4043
+ message: "Credential revoked. The binding stays; connect again to restore access.",
4044
+ connectionId: parsed.connectionId,
4045
+ });
4046
+ }
4047
+ case "nestr_get_agent_connectors": {
4048
+ const parsed = schemas.getAgentConnectorReach.parse(args);
4049
+ const reach = await client.getAgentConnectorReach(parsed.workspaceId, parsed.agentUserId);
4050
+ return formatResult(reach);
4051
+ }
4052
+ case "nestr_run_agent": {
4053
+ const parsed = schemas.runAgent.parse(args);
4054
+ const result = await client.runAgent(parsed.workspaceId, parsed.agentUserId, {
4055
+ nestId: parsed.nestId,
4056
+ message: parsed.message,
4057
+ });
4058
+ // A run somebody else asked for is invisible to them until someone says
4059
+ // where it is happening, and this tool's caller is the only party in a
4060
+ // position to. Naming the handover, not just the url, because a link
4061
+ // that reaches the model and not the person is a link nobody sees.
4062
+ const watching = result.watchUrl
4063
+ ? ` Tell whoever asked for this that they can watch it at ${result.watchUrl}, where the agent reports back as it works.`
4064
+ : " It reports back on the item it was run on.";
4065
+ return formatResult({
4066
+ message: `The agent was dispatched.${watching}`,
4067
+ ...result,
4068
+ });
4069
+ }
3659
4070
  case "nestr_get_nest_files": {
3660
4071
  const parsed = schemas.getNestFiles.parse(args);
3661
4072
  const files = await client.getNestFiles(parsed.nestId);