@topolo/mcp 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -26
- package/dist/dispatch.js +144 -106
- package/dist/http.js +152 -114
- package/dist/index.js +417 -167
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -26,9 +26,10 @@ Either:
|
|
|
26
26
|
|
|
27
27
|
- **Long-lived**: mint an API key at the Topolo Developers console
|
|
28
28
|
(`TOPOLO_API_KEY=topo_live_...`). Preferred for persistent agent installs.
|
|
29
|
-
- **Short-lived**: `topolo auth login
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
- **Short-lived**: run `topolo auth login`. The stdio MCP reads and refreshes
|
|
30
|
+
the same environment-specific credential store, so no token copying is
|
|
31
|
+
needed. Explicit `TOPOLO_API_KEY` or `TOPOLO_ACCESS_TOKEN` values still take
|
|
32
|
+
precedence for headless installations.
|
|
32
33
|
|
|
33
34
|
## Register with an MCP client
|
|
34
35
|
|
|
@@ -89,9 +90,9 @@ wins.
|
|
|
89
90
|
1. Resolve the credential from env. **Refuse to start** if none is set.
|
|
90
91
|
2. Call `GET /api/me` to load the user's granted scopes + role.
|
|
91
92
|
3. Connect the stdio transport and begin serving MCP requests.
|
|
92
|
-
4.
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
4. Advertise a fixed, bounded discovery/invocation surface. Agents search one
|
|
94
|
+
application at a time and call exact action IDs; the server never expands
|
|
95
|
+
one MCP tool per action or materializes the global catalog.
|
|
95
96
|
|
|
96
97
|
Startup is synchronous and fast (< 500 ms typical). If the credential is
|
|
97
98
|
rejected, the process logs the error to stderr and exits non-zero — host
|
|
@@ -102,44 +103,64 @@ clients surface that as a failed server launch.
|
|
|
102
103
|
| Tool | Required scopes | Destructive hint |
|
|
103
104
|
| ---------------------------- | --------------------- | ---------------- |
|
|
104
105
|
| `topolo_whoami` | (none) | no |
|
|
105
|
-
| `
|
|
106
|
-
| `topolo_list_applications` | (none) | no |
|
|
106
|
+
| `topolo_search_applications` | (none) | no |
|
|
107
107
|
| `topolo_get_application` | (none) | no |
|
|
108
108
|
| `topolo_list_application_requirements` | (none) | no |
|
|
109
109
|
| `topolo_audit_applications` | (none) | no |
|
|
110
|
-
| `
|
|
111
|
-
|
|
|
110
|
+
| `topolo_search_actions` | (credential-scoped) | no |
|
|
111
|
+
| `topolo_get_action` | (credential-scoped) | no |
|
|
112
|
+
| `topolo_read_action` | catalog or target policy | no |
|
|
113
|
+
| `topolo_call_action` | catalog or target policy | yes |
|
|
112
114
|
| `topolo_api_call` | (service-enforced) | yes |
|
|
113
115
|
|
|
114
116
|
Applications publish machine-action definitions through Topolo Developers.
|
|
115
117
|
TopoloAuth projects those into a credential-scoped action catalog using the
|
|
116
118
|
same service permissions already used for API keys and application access. MCP
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
keeps six stable application/action tools regardless of whether the platform
|
|
120
|
+
contains ten actions or one billion: search applications, search actions within
|
|
121
|
+
one app, inspect exact applications/actions, safely invoke automatic reads, and
|
|
122
|
+
invoke confirmation-gated actions.
|
|
123
|
+
|
|
124
|
+
Most actions use `authorizationMode: static` and are filtered by one declared
|
|
125
|
+
application permission before discovery. Operations whose authority depends on
|
|
126
|
+
request input (for example, organization admin OR `grants:write` in the target
|
|
127
|
+
application) use `authorizationMode: runtime`: the catalog exposes the typed
|
|
128
|
+
capability without inventing a global permission, and the target service checks
|
|
129
|
+
the current database-backed policy for the supplied organization/application on
|
|
130
|
+
every call.
|
|
120
131
|
|
|
121
132
|
`topolo_api_call` is the escape hatch for endpoints without typed MCP tools
|
|
122
133
|
yet. GET requests work without confirmation; mutating HTTP methods must pass
|
|
123
134
|
`confirm: true` or the SDK rejects the call. Backing services still enforce
|
|
124
135
|
permissions for every request. The tool schema intentionally does not embed a
|
|
125
|
-
large service enum; agents should call `
|
|
136
|
+
large service enum; agents should call `topolo_search_applications` first, then pass an
|
|
126
137
|
app ID/slug from that credential-scoped result. New services become callable
|
|
127
138
|
without an MCP release once Auth grants the organization and credential the
|
|
128
|
-
relevant permissions, and new actions become
|
|
129
|
-
once the application publishes them.
|
|
139
|
+
relevant permissions, and new actions become discoverable/callable without an
|
|
140
|
+
MCP release once the application publishes them.
|
|
141
|
+
|
|
142
|
+
`topolo_read_action` and `topolo_call_action` accept an optional paired
|
|
143
|
+
`resourceType` / `resourceId` context for app-local workspace boundaries. The
|
|
144
|
+
SDK sends it as resource headers without changing the credential's personal or
|
|
145
|
+
organization context. This lets agents address many workspaces independently;
|
|
146
|
+
providing only one of the pair is rejected before the request is sent.
|
|
130
147
|
|
|
131
148
|
## Safety rails
|
|
132
149
|
|
|
133
150
|
- **No `orgId` parameter** on any tool. The organization is derived entirely
|
|
134
151
|
from the credential by each backend app. There is no path for Org A's agent
|
|
135
152
|
to see Org B's data.
|
|
136
|
-
- **
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
- **Credential-bound context.** OAuth tokens and refresh rotation retain their
|
|
154
|
+
own personal/org context. CLI/MCP context switches do not move the user's web
|
|
155
|
+
session, and browser switches cannot redirect a running agent credential.
|
|
156
|
+
- **Scope-gated tool advertisement.** Static actions are removed unless their
|
|
157
|
+
permission is granted. Input-dependent runtime actions remain discoverable
|
|
158
|
+
and are authorized by the target service for the concrete request.
|
|
159
|
+
- **Credential-scoped catalog discovery.** `topolo_search_applications` uses
|
|
160
|
+
TopoloAuth's live organization/permission
|
|
140
161
|
catalog. They do not expose the global generated platform catalog.
|
|
141
|
-
- **Credential-scoped action discovery.**
|
|
142
|
-
|
|
162
|
+
- **Credential-scoped action discovery.** `topolo_search_actions` reads only one
|
|
163
|
+
app partition and returns at most 100 permission-filtered actions per page.
|
|
143
164
|
- **Audit headers.** Every request sends `X-Topolo-Client: topolo-mcp/<ver>`,
|
|
144
165
|
`X-Topolo-Agent: <label>`, `X-Topolo-Request-Id: <uuid>`.
|
|
145
166
|
- **Write-action confirmation.** The SDK refuses mutating HTTP methods unless
|
|
@@ -180,10 +201,33 @@ npx @modelcontextprotocol/inspector node dist/index.js
|
|
|
180
201
|
|
|
181
202
|
## Release Flow
|
|
182
203
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
204
|
+
Agent packages are released locally from the `staging` branch after its package
|
|
205
|
+
gates pass. Publish in dependency order (`@topolo/sdk`, `@topolo/cli`, then
|
|
206
|
+
`@topolo/mcp`) so every public dependency already exists in the registry.
|
|
207
|
+
|
|
208
|
+
For each package, run its tests, typecheck, build, and tarball verification:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pnpm --dir packages/topolo-mcp test
|
|
212
|
+
pnpm --dir packages/topolo-mcp typecheck
|
|
213
|
+
pnpm --dir packages/topolo-mcp build
|
|
214
|
+
node tooling/verify-npm-package.mjs packages/topolo-mcp
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The npm token is stored in the macOS Keychain under `registry.npmjs.org`. Use a
|
|
218
|
+
temporary npm config so the token is never written into the repository or
|
|
219
|
+
printed:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
printf '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\n' > /tmp/topolo-release.npmrc
|
|
223
|
+
NODE_AUTH_TOKEN="$(security find-generic-password -s registry.npmjs.org -w)" \
|
|
224
|
+
NPM_CONFIG_USERCONFIG=/tmp/topolo-release.npmrc \
|
|
225
|
+
sh -c 'cd packages/topolo-mcp && npm publish --access public'
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
After publishing, install exact versions into an empty temporary directory and
|
|
229
|
+
exercise the SDK exports, CLI version command, MCP exports, and `topolo-mcp`
|
|
230
|
+
executable before updating `topoloPublishedVersion`.
|
|
187
231
|
|
|
188
232
|
## Phase 2 (planned)
|
|
189
233
|
|
package/dist/dispatch.js
CHANGED
|
@@ -53,6 +53,15 @@ function splitService(servicePart) {
|
|
|
53
53
|
// src/tools.ts
|
|
54
54
|
var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
|
|
55
55
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
56
|
+
function optionalResourceContext(args) {
|
|
57
|
+
const resourceType = typeof args["resourceType"] === "string" ? args["resourceType"].trim() : "";
|
|
58
|
+
const resourceId = typeof args["resourceId"] === "string" ? args["resourceId"].trim() : "";
|
|
59
|
+
if (!resourceType && !resourceId) return void 0;
|
|
60
|
+
if (!resourceType || !resourceId) {
|
|
61
|
+
throw new Error("resourceType and resourceId must be provided together.");
|
|
62
|
+
}
|
|
63
|
+
return { resourceType, resourceId };
|
|
64
|
+
}
|
|
56
65
|
var TOOLS = [
|
|
57
66
|
{
|
|
58
67
|
name: "topolo_whoami",
|
|
@@ -68,22 +77,32 @@ var TOOLS = [
|
|
|
68
77
|
handler: async (topolo) => topolo.identity.whoami()
|
|
69
78
|
},
|
|
70
79
|
{
|
|
71
|
-
name: "
|
|
72
|
-
title: "
|
|
73
|
-
description: "
|
|
80
|
+
name: "topolo_search_applications",
|
|
81
|
+
title: "Search Topolo applications the agent can reach",
|
|
82
|
+
description: "Searches the credential-scoped application registry with a bounded page and continuation cursor. Returns actionService as the canonical selector for topolo_search_actions, plus granted permissions, API URL, and launch URL without loading the global platform catalog.",
|
|
74
83
|
requiredScopes: [],
|
|
75
84
|
destructive: false,
|
|
76
85
|
inputSchema: {
|
|
77
86
|
type: "object",
|
|
78
|
-
properties: {
|
|
87
|
+
properties: {
|
|
88
|
+
query: { type: "string" },
|
|
89
|
+
cursor: { type: "string" },
|
|
90
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
|
|
91
|
+
},
|
|
79
92
|
additionalProperties: false
|
|
80
93
|
},
|
|
81
|
-
handler: async (topolo) => {
|
|
82
|
-
const
|
|
94
|
+
handler: async (topolo, args) => {
|
|
95
|
+
const result = await topolo.client.searchServices({
|
|
96
|
+
query: typeof args["query"] === "string" ? args["query"] : void 0,
|
|
97
|
+
cursor: typeof args["cursor"] === "string" ? args["cursor"] : void 0,
|
|
98
|
+
limit: typeof args["limit"] === "number" ? args["limit"] : void 0
|
|
99
|
+
});
|
|
100
|
+
const applications = result.services.map((entry) => {
|
|
83
101
|
const id = displayAppId(entry);
|
|
84
102
|
return {
|
|
85
103
|
id,
|
|
86
104
|
appId: entry.appId,
|
|
105
|
+
actionService: entry.appId,
|
|
87
106
|
slug: entry.slug,
|
|
88
107
|
name: entry.name,
|
|
89
108
|
apiBaseUrl: entry.apiBaseUrl,
|
|
@@ -96,13 +115,13 @@ var TOOLS = [
|
|
|
96
115
|
permissions: [...entry.permissions]
|
|
97
116
|
};
|
|
98
117
|
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
99
|
-
return {
|
|
118
|
+
return { applications, page: result.page };
|
|
100
119
|
}
|
|
101
120
|
},
|
|
102
121
|
{
|
|
103
|
-
name: "
|
|
104
|
-
title: "
|
|
105
|
-
description: "
|
|
122
|
+
name: "topolo_search_actions",
|
|
123
|
+
title: "Search Topolo actions the agent can call",
|
|
124
|
+
description: "Searches one application's credential-scoped actions with a bounded page and continuation cursor.",
|
|
106
125
|
requiredScopes: [],
|
|
107
126
|
destructive: false,
|
|
108
127
|
inputSchema: {
|
|
@@ -110,31 +129,25 @@ var TOOLS = [
|
|
|
110
129
|
properties: {
|
|
111
130
|
service: {
|
|
112
131
|
type: "string",
|
|
113
|
-
description: "
|
|
114
|
-
}
|
|
132
|
+
description: "Required application selector. Prefer actionService from topolo_search_applications; canonical app id, slug, and name are also accepted."
|
|
133
|
+
},
|
|
134
|
+
query: { type: "string" },
|
|
135
|
+
cursor: { type: "string" },
|
|
136
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
|
|
115
137
|
},
|
|
138
|
+
required: ["service"],
|
|
116
139
|
additionalProperties: false
|
|
117
140
|
},
|
|
118
|
-
handler: async (topolo, args) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
requiredScopes: [],
|
|
129
|
-
destructive: false,
|
|
130
|
-
inputSchema: {
|
|
131
|
-
type: "object",
|
|
132
|
-
properties: {},
|
|
133
|
-
additionalProperties: false
|
|
134
|
-
},
|
|
135
|
-
handler: async (topolo) => ({
|
|
136
|
-
applications: await topolo.client.listServices()
|
|
137
|
-
})
|
|
141
|
+
handler: async (topolo, args) => {
|
|
142
|
+
const service = typeof args["service"] === "string" ? args["service"].trim() : "";
|
|
143
|
+
if (!service) throw new Error("service is required for bounded action discovery.");
|
|
144
|
+
return topolo.client.searchActions({
|
|
145
|
+
service,
|
|
146
|
+
query: typeof args["query"] === "string" ? args["query"] : void 0,
|
|
147
|
+
cursor: typeof args["cursor"] === "string" ? args["cursor"] : void 0,
|
|
148
|
+
limit: typeof args["limit"] === "number" ? args["limit"] : void 0
|
|
149
|
+
});
|
|
150
|
+
}
|
|
138
151
|
},
|
|
139
152
|
{
|
|
140
153
|
name: "topolo_get_application",
|
|
@@ -147,7 +160,7 @@ var TOOLS = [
|
|
|
147
160
|
properties: {
|
|
148
161
|
application: {
|
|
149
162
|
type: "string",
|
|
150
|
-
description: "Application identifier: app id, slug, or name from
|
|
163
|
+
description: "Application identifier: app id, slug, or name from topolo_search_applications."
|
|
151
164
|
}
|
|
152
165
|
},
|
|
153
166
|
required: ["application"],
|
|
@@ -158,18 +171,98 @@ var TOOLS = [
|
|
|
158
171
|
if (typeof application !== "string" || !application.trim()) {
|
|
159
172
|
throw new Error("`application` must be a non-empty string.");
|
|
160
173
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
return { application: await topolo.client.getService(application) };
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "topolo_get_action",
|
|
179
|
+
title: "Describe one Topolo action",
|
|
180
|
+
description: "Returns one credential-scoped action by exact action id.",
|
|
181
|
+
requiredScopes: [],
|
|
182
|
+
destructive: false,
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: { actionId: { type: "string", minLength: 1 } },
|
|
186
|
+
required: ["actionId"],
|
|
187
|
+
additionalProperties: false
|
|
188
|
+
},
|
|
189
|
+
handler: async (topolo, args) => {
|
|
190
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
191
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
192
|
+
return { action: await topolo.client.getAction(actionId) };
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "topolo_read_action",
|
|
197
|
+
title: "Call one read-only Topolo action",
|
|
198
|
+
description: "Calls one exact action id only when its published contract is read-only and needs no confirmation.",
|
|
199
|
+
requiredScopes: [],
|
|
200
|
+
destructive: false,
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
actionId: { type: "string", minLength: 1 },
|
|
205
|
+
input: { type: "object", additionalProperties: true },
|
|
206
|
+
resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
|
|
207
|
+
resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." }
|
|
208
|
+
},
|
|
209
|
+
required: ["actionId"],
|
|
210
|
+
additionalProperties: false
|
|
211
|
+
},
|
|
212
|
+
handler: async (topolo, args) => {
|
|
213
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
214
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
215
|
+
const input = args["input"];
|
|
216
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
|
|
217
|
+
throw new Error("input must be an object.");
|
|
218
|
+
}
|
|
219
|
+
const action = await topolo.client.getAction(actionId);
|
|
220
|
+
const agentAccess = action.agentAccess ?? (action.destructive ? "off" : action.readOnly && !action.requiresConfirmation ? "auto" : "confirm");
|
|
221
|
+
if (action.method !== "GET" || !action.readOnly || action.destructive || action.requiresConfirmation || agentAccess !== "auto") {
|
|
222
|
+
throw new Error(`Action "${actionId}" is not an automatic read-only action. Use topolo_call_action with confirmation.`);
|
|
223
|
+
}
|
|
224
|
+
const resource = optionalResourceContext(args);
|
|
225
|
+
return topolo.client.callAction(
|
|
226
|
+
actionId,
|
|
227
|
+
input ?? {},
|
|
228
|
+
resource ? { resource } : {}
|
|
165
229
|
);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: "topolo_call_action",
|
|
234
|
+
title: "Call one Topolo action",
|
|
235
|
+
description: "Calls one exact action id. Mutating actions require confirm:true.",
|
|
236
|
+
requiredScopes: [],
|
|
237
|
+
destructive: true,
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
actionId: { type: "string", minLength: 1 },
|
|
242
|
+
input: { type: "object", additionalProperties: true },
|
|
243
|
+
resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
|
|
244
|
+
resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." },
|
|
245
|
+
confirm: { type: "boolean" }
|
|
246
|
+
},
|
|
247
|
+
required: ["actionId"],
|
|
248
|
+
additionalProperties: false
|
|
249
|
+
},
|
|
250
|
+
handler: async (topolo, args) => {
|
|
251
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
252
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
253
|
+
const input = args["input"];
|
|
254
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
|
|
255
|
+
throw new Error("input must be an object.");
|
|
171
256
|
}
|
|
172
|
-
|
|
257
|
+
const resource = optionalResourceContext(args);
|
|
258
|
+
return topolo.client.callAction(
|
|
259
|
+
actionId,
|
|
260
|
+
input ?? {},
|
|
261
|
+
{
|
|
262
|
+
confirm: args["confirm"] === true,
|
|
263
|
+
...resource ? { resource } : {}
|
|
264
|
+
}
|
|
265
|
+
);
|
|
173
266
|
}
|
|
174
267
|
},
|
|
175
268
|
{
|
|
@@ -183,7 +276,7 @@ var TOOLS = [
|
|
|
183
276
|
properties: {
|
|
184
277
|
application: {
|
|
185
278
|
type: "string",
|
|
186
|
-
description: "Optional application ID from
|
|
279
|
+
description: "Optional application ID from topolo_search_applications."
|
|
187
280
|
}
|
|
188
281
|
},
|
|
189
282
|
additionalProperties: false
|
|
@@ -223,7 +316,7 @@ var TOOLS = [
|
|
|
223
316
|
properties: {
|
|
224
317
|
application: {
|
|
225
318
|
type: "string",
|
|
226
|
-
description: "Optional application ID from
|
|
319
|
+
description: "Optional application ID from topolo_search_applications."
|
|
227
320
|
},
|
|
228
321
|
failOn: {
|
|
229
322
|
type: "string",
|
|
@@ -257,7 +350,7 @@ var TOOLS = [
|
|
|
257
350
|
{
|
|
258
351
|
name: "topolo_api_call",
|
|
259
352
|
title: "Call any Topolo platform service",
|
|
260
|
-
description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call
|
|
353
|
+
description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call topolo_search_applications first to discover callable app IDs; the SDK can also resolve live Auth-catalog app ids/slugs for newly registered API surfaces.",
|
|
261
354
|
requiredScopes: [],
|
|
262
355
|
destructive: true,
|
|
263
356
|
inputSchema: {
|
|
@@ -288,7 +381,7 @@ var TOOLS = [
|
|
|
288
381
|
},
|
|
289
382
|
resourceType: {
|
|
290
383
|
type: "string",
|
|
291
|
-
description: "Optional workspace resource alias from
|
|
384
|
+
description: "Optional workspace resource alias from topolo_search_applications apiKeyResources. Aliases are app-defined, e.g. brand, mailbox, venue, merchant, or any other cataloged alias."
|
|
292
385
|
},
|
|
293
386
|
resourceId: {
|
|
294
387
|
type: "string",
|
|
@@ -306,7 +399,7 @@ var TOOLS = [
|
|
|
306
399
|
const service = args["service"];
|
|
307
400
|
const path = args["path"];
|
|
308
401
|
if (typeof service !== "string" || !service.trim()) {
|
|
309
|
-
throw new Error("service is required. Call
|
|
402
|
+
throw new Error("service is required. Call topolo_search_applications first to discover callable services.");
|
|
310
403
|
}
|
|
311
404
|
if (typeof path !== "string" || !path) {
|
|
312
405
|
throw new Error("path is required");
|
|
@@ -320,12 +413,7 @@ var TOOLS = [
|
|
|
320
413
|
const normalisedQuery = query && typeof query === "object" && !Array.isArray(query) ? Object.fromEntries(
|
|
321
414
|
Object.entries(query).map(([k, v]) => [k, String(v)])
|
|
322
415
|
) : void 0;
|
|
323
|
-
const
|
|
324
|
-
const resourceId = typeof args["resourceId"] === "string" ? args["resourceId"].trim() : "";
|
|
325
|
-
if (resourceType && !resourceId || !resourceType && resourceId) {
|
|
326
|
-
throw new Error("resourceType and resourceId must be provided together.");
|
|
327
|
-
}
|
|
328
|
-
const resource = resourceType && resourceId ? { resourceType, resourceId } : void 0;
|
|
416
|
+
const resource = optionalResourceContext(args);
|
|
329
417
|
return topolo.client.request({
|
|
330
418
|
service: service.trim(),
|
|
331
419
|
method,
|
|
@@ -342,58 +430,8 @@ function filterToolsByScopes(tools, scopes) {
|
|
|
342
430
|
return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
|
|
343
431
|
}
|
|
344
432
|
async function buildTopoloTools(topolo, scopes, tools = TOOLS) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const actionTools = actionsToToolDefs(actions);
|
|
348
|
-
return filterToolsByScopes([...baseTools, ...actionTools], scopes);
|
|
349
|
-
}
|
|
350
|
-
function effectiveAgentAccess(a) {
|
|
351
|
-
if (a.agentAccess) return a.agentAccess;
|
|
352
|
-
if (a.destructive) return "off";
|
|
353
|
-
if (a.readOnly && !a.requiresConfirmation) return "auto";
|
|
354
|
-
return "confirm";
|
|
355
|
-
}
|
|
356
|
-
function actionsToToolDefs(actions) {
|
|
357
|
-
return actions.map((action) => ({ action, access: effectiveAgentAccess(action) })).filter(({ access }) => access !== "off").map(({ action, access }) => ({
|
|
358
|
-
name: action.toolName || toolNameForAction(action),
|
|
359
|
-
title: action.title,
|
|
360
|
-
description: action.description,
|
|
361
|
-
requiredScopes: action.requiredPermission ? [action.requiredPermission] : [],
|
|
362
|
-
destructive: access === "confirm",
|
|
363
|
-
inputSchema: actionInputSchema(action, access),
|
|
364
|
-
handler: async (topolo, args) => {
|
|
365
|
-
const confirm = Boolean(args["confirm"]);
|
|
366
|
-
const input = { ...args };
|
|
367
|
-
delete input["confirm"];
|
|
368
|
-
return topolo.client.callAction(action.actionId, input, { confirm });
|
|
369
|
-
}
|
|
370
|
-
}));
|
|
371
|
-
}
|
|
372
|
-
function toolNameForAction(action) {
|
|
373
|
-
const service = (action.appSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
374
|
-
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
375
|
-
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
376
|
-
}
|
|
377
|
-
function actionInputSchema(action, access = "auto") {
|
|
378
|
-
const schema = {
|
|
379
|
-
type: "object",
|
|
380
|
-
properties: {},
|
|
381
|
-
additionalProperties: false,
|
|
382
|
-
...action.inputSchema
|
|
383
|
-
};
|
|
384
|
-
const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? { ...schema.properties } : {};
|
|
385
|
-
if (access === "confirm") {
|
|
386
|
-
properties.confirm = {
|
|
387
|
-
type: "boolean",
|
|
388
|
-
description: "Required for mutating actions. Explicit human-in-the-loop acknowledgement."
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
return {
|
|
392
|
-
...schema,
|
|
393
|
-
type: "object",
|
|
394
|
-
properties,
|
|
395
|
-
additionalProperties: schema.additionalProperties ?? false
|
|
396
|
-
};
|
|
433
|
+
void topolo;
|
|
434
|
+
return filterToolsByScopes(tools, scopes);
|
|
397
435
|
}
|
|
398
436
|
function evaluateApplicationAuditGate(report, threshold) {
|
|
399
437
|
const counts = {
|