@nestr/mcp 0.1.99 → 0.1.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/api/client.d.ts +198 -15
- package/build/api/client.d.ts.map +1 -1
- package/build/api/client.js +126 -1
- package/build/api/client.js.map +1 -1
- package/build/help/topics.d.ts.map +1 -1
- package/build/help/topics.js +72 -4
- package/build/help/topics.js.map +1 -1
- package/build/http.d.ts +1 -0
- package/build/http.d.ts.map +1 -1
- package/build/http.js +83 -9
- package/build/http.js.map +1 -1
- package/build/oauth/store.d.ts +2 -0
- package/build/oauth/store.d.ts.map +1 -1
- package/build/oauth/store.js.map +1 -1
- package/build/server.d.ts +8 -0
- package/build/server.d.ts.map +1 -1
- package/build/server.js +7 -3
- package/build/server.js.map +1 -1
- package/build/skills/doing-work.d.ts.map +1 -1
- package/build/skills/doing-work.js +25 -0
- package/build/skills/doing-work.js.map +1 -1
- package/build/skills/tension-processing.d.ts.map +1 -1
- package/build/skills/tension-processing.js +2 -0
- package/build/skills/tension-processing.js.map +1 -1
- package/build/tools/index.d.ts +1874 -184
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +463 -32
- package/build/tools/index.js.map +1 -1
- package/package.json +1 -1
package/build/tools/index.js
CHANGED
|
@@ -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
|
-
|
|
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("
|
|
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("
|
|
1074
|
-
authStrategy: z.enum(["secret", "oauth2"]).optional().describe("How a principal connects: 'secret' (one-time
|
|
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(["
|
|
1080
|
-
ownerId: z.string().describe("Owner 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: {
|
|
@@ -2277,6 +2383,7 @@ export const toolDefinitions = [
|
|
|
2277
2383
|
},
|
|
2278
2384
|
required: ["nestId", "tensionId", "partId"],
|
|
2279
2385
|
},
|
|
2386
|
+
...readOnly,
|
|
2280
2387
|
},
|
|
2281
2388
|
{
|
|
2282
2389
|
name: "nestr_create_tension_part_child",
|
|
@@ -2420,19 +2527,38 @@ export const toolDefinitions = [
|
|
|
2420
2527
|
},
|
|
2421
2528
|
...readOnly,
|
|
2422
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
|
+
},
|
|
2423
2542
|
{
|
|
2424
2543
|
name: "nestr_register_connector",
|
|
2425
|
-
description: "Register a connector in the workspace catalog: a reusable mcp/cli/api template
|
|
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.",
|
|
2426
2545
|
inputSchema: {
|
|
2427
2546
|
type: "object",
|
|
2428
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
|
+
},
|
|
2429
2552
|
workspaceId: { type: "string", description: "Workspace ID to register the connector in" },
|
|
2430
2553
|
type: {
|
|
2431
2554
|
type: "string",
|
|
2432
2555
|
enum: ["mcp", "cli", "api"],
|
|
2433
|
-
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.",
|
|
2434
2561
|
},
|
|
2435
|
-
name: { type: "string", description: "Unique connector name within the workspace catalog" },
|
|
2436
2562
|
config: {
|
|
2437
2563
|
type: "object",
|
|
2438
2564
|
description: "Transport config, no secret. mcp/api need a url, cli a command. Optional non-secret headers go under headers.",
|
|
@@ -2451,13 +2577,44 @@ export const toolDefinitions = [
|
|
|
2451
2577
|
description: "How a principal connects: 'secret' (one-time, via the Connect button) or 'oauth2'. The agent never sees it.",
|
|
2452
2578
|
},
|
|
2453
2579
|
},
|
|
2454
|
-
|
|
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"],
|
|
2455
2612
|
},
|
|
2456
2613
|
...mutating,
|
|
2457
2614
|
},
|
|
2458
2615
|
{
|
|
2459
2616
|
name: "nestr_bind_connector",
|
|
2460
|
-
description: "Bind a registered connector to an owner so that owner can use it. Owner types: '
|
|
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.",
|
|
2461
2618
|
inputSchema: {
|
|
2462
2619
|
type: "object",
|
|
2463
2620
|
properties: {
|
|
@@ -2465,18 +2622,131 @@ export const toolDefinitions = [
|
|
|
2465
2622
|
connectorId: { type: "string", description: "ID of an enabled connector from nestr_list_connectors" },
|
|
2466
2623
|
ownerType: {
|
|
2467
2624
|
type: "string",
|
|
2468
|
-
enum: ["
|
|
2469
|
-
description: "Owner type. 'role-domain'
|
|
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.",
|
|
2470
2627
|
},
|
|
2471
2628
|
ownerId: {
|
|
2472
2629
|
type: "string",
|
|
2473
|
-
description: "Owner 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.",
|
|
2474
2631
|
},
|
|
2475
2632
|
},
|
|
2476
2633
|
required: ["workspaceId", "connectorId", "ownerType", "ownerId"],
|
|
2477
2634
|
},
|
|
2478
2635
|
...mutating,
|
|
2479
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
|
+
},
|
|
2480
2750
|
{
|
|
2481
2751
|
name: "nestr_get_nest_files",
|
|
2482
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.",
|
|
@@ -2532,6 +2802,13 @@ export const toolDefinitions = [
|
|
|
2532
2802
|
...destructive,
|
|
2533
2803
|
},
|
|
2534
2804
|
];
|
|
2805
|
+
// The read-only surface (/mcp/readonly) serves an authenticated bearer but only
|
|
2806
|
+
// tools that cannot change anything. Derived from the annotations rather than a
|
|
2807
|
+
// hand-kept list, so a new tool is denied until someone marks it readOnly: the
|
|
2808
|
+
// failure mode of forgetting is a missing capability, not a silent write.
|
|
2809
|
+
export const READONLY_TOOL_NAMES = new Set(toolDefinitions
|
|
2810
|
+
.filter((t) => t.annotations?.readOnlyHint === true)
|
|
2811
|
+
.map((t) => t.name));
|
|
2535
2812
|
// Strip description fields from nest objects in response data
|
|
2536
2813
|
export function stripDescriptionFields(data) {
|
|
2537
2814
|
if (Array.isArray(data)) {
|
|
@@ -2657,6 +2934,18 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2657
2934
|
return formatResult(PUBLIC_GUEST_ME);
|
|
2658
2935
|
}
|
|
2659
2936
|
}
|
|
2937
|
+
// READ-ONLY surface gate (defense in depth — the readonly route also filters
|
|
2938
|
+
// the advertised tool list). Anything not marked readOnlyHint is refused,
|
|
2939
|
+
// including tools carrying no annotation at all.
|
|
2940
|
+
if (context?.isReadOnly && !READONLY_TOOL_NAMES.has(name)) {
|
|
2941
|
+
return formatError({
|
|
2942
|
+
error: true,
|
|
2943
|
+
code: "AUTH_SCOPE_INSUFFICIENT",
|
|
2944
|
+
message: `Tool '${name}' is not available on the read-only Nestr MCP. This workspace is out of AI credit, so the agent can read but not change anything.`,
|
|
2945
|
+
retryable: false,
|
|
2946
|
+
hint: "Add AI credit, or raise the monthly ceiling if that is what was reached, to restore the full toolset.",
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2660
2949
|
switch (name) {
|
|
2661
2950
|
case "nestr_help": {
|
|
2662
2951
|
const parsed = schemas.help.parse(args);
|
|
@@ -2937,6 +3226,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2937
3226
|
labels: parsed.labels,
|
|
2938
3227
|
fields: parsed.fields,
|
|
2939
3228
|
users: parsed.users,
|
|
3229
|
+
due: parsed.due,
|
|
2940
3230
|
});
|
|
2941
3231
|
return formatResult({ message: "Nest created successfully", nest });
|
|
2942
3232
|
}
|
|
@@ -3069,6 +3359,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3069
3359
|
const parsed = schemas.listUsers.parse(args);
|
|
3070
3360
|
const users = await client.listUsers(parsed.workspaceId, {
|
|
3071
3361
|
search: parsed.search,
|
|
3362
|
+
agents: parsed.agents,
|
|
3072
3363
|
limit: parsed.limit,
|
|
3073
3364
|
page: parsed.page,
|
|
3074
3365
|
});
|
|
@@ -3607,9 +3898,49 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3607
3898
|
const connectors = await client.listConnectors(parsed.workspaceId);
|
|
3608
3899
|
return formatResult(connectors);
|
|
3609
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
|
+
}
|
|
3610
3920
|
case "nestr_register_connector": {
|
|
3611
3921
|
const parsed = schemas.registerConnector.parse(args);
|
|
3612
|
-
|
|
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, {
|
|
3613
3944
|
type: parsed.type,
|
|
3614
3945
|
name: parsed.name,
|
|
3615
3946
|
config: parsed.config,
|
|
@@ -3617,10 +3948,34 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3617
3948
|
exposure: parsed.exposure,
|
|
3618
3949
|
authStrategy: parsed.authStrategy,
|
|
3619
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
|
+
});
|
|
3620
3958
|
return formatResult({
|
|
3621
|
-
message: "Connector registered.
|
|
3622
|
-
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,
|
|
3623
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 });
|
|
3624
3979
|
}
|
|
3625
3980
|
case "nestr_bind_connector": {
|
|
3626
3981
|
const parsed = schemas.bindConnector.parse(args);
|
|
@@ -3628,14 +3983,90 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
3628
3983
|
connectorId: parsed.connectorId,
|
|
3629
3984
|
owner: { type: parsed.ownerType, id: parsed.ownerId },
|
|
3630
3985
|
});
|
|
3631
|
-
//
|
|
3632
|
-
//
|
|
3633
|
-
//
|
|
3634
|
-
const message = parsed.ownerType === "role-domain"
|
|
3635
|
-
? "
|
|
3636
|
-
: "
|
|
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.";
|
|
3637
3992
|
return formatResult({ message, connection });
|
|
3638
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
|
+
}
|
|
3639
4070
|
case "nestr_get_nest_files": {
|
|
3640
4071
|
const parsed = schemas.getNestFiles.parse(args);
|
|
3641
4072
|
const files = await client.getNestFiles(parsed.nestId);
|