@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 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` with the CLI, then copy the access token
30
- from `~/.config/topolo/config.json`. Useful for dev and testing. MCP does
31
- not read the CLI config directly; pass the token in `TOPOLO_ACCESS_TOKEN`.
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. On tool listing and tool calls, filter static tools by those scopes and load
93
- the live action catalog for the credential. Agents never see an action tool
94
- they cannot use.
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
- | `topolo_list_services` | (none) | no |
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
- | `topolo_list_actions` | (credential-scoped) | no |
111
- | Dynamic action tools | action permission | action-defined |
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
- turns each visible catalog entry into a tool named by the action catalog, such
118
- as `topolo_mail_messages_send`, with the action's input schema and destructive
119
- hint.
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 `topolo_list_services` first, then pass a
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 typed tools without an MCP release
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
- - **Scope-gated tool advertisement.** Agents never see tools they can't use —
137
- fewer wasted attempts and less prompt noise.
138
- - **Credential-scoped catalog discovery.** `topolo_list_services` and
139
- `topolo_list_applications` use TopoloAuth's live organization/permission
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.** Dynamic action tools are loaded from
142
- the live Auth action catalog and filtered by existing service permissions.
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
- Preferred flow: push a `v<x.y.z>` tag to `main`; GitHub Actions typechecks,
184
- tests, builds, verifies the tag matches `package.json`, and publishes to npm via
185
- the `NPM_TOKEN` secret. Operators may publish directly with an npm token only
186
- after running the same package checks and `npm pack --dry-run`.
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: "topolo_list_services",
72
- title: "List every Topolo service the agent can reach",
73
- description: "Returns the credential-scoped service registry: one entry per platform service the current credential can use in its organization, with granted permissions, API URL, and launch URL. Use this to discover what `topolo_api_call` can address. This is not the global platform catalog.",
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 services = (await topolo.client.listServices()).map((entry) => {
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 { services };
118
+ return { applications, page: result.page };
100
119
  }
101
120
  },
102
121
  {
103
- name: "topolo_list_actions",
104
- title: "List every Topolo action the agent can call",
105
- description: "Returns the credential-scoped callable action catalog. Actions are published by applications through TopoloDevelopers and filtered by TopoloAuth against the current organization and effective permissions.",
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: "Optional app id, slug, or name filter."
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
- actions: await topolo.client.listActions({
120
- service: typeof args["service"] === "string" ? args["service"] : void 0
121
- })
122
- })
123
- },
124
- {
125
- name: "topolo_list_applications",
126
- title: "List every Topolo platform application",
127
- description: "Returns the live Topolo application catalog from the platform registry. Reflects apps as they are registered/removed with no client rebuild. Authenticated callers in an entitled org also see the developer_only apps their role permits. Falls back to a bundled snapshot only if the registry is unreachable.",
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 topolo_list_applications."
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
- const needle = application.trim().toLowerCase();
162
- const catalog = await topolo.client.listServices();
163
- const match = catalog.find(
164
- (e) => e.appId.toLowerCase() === needle || e.slug && e.slug.toLowerCase() === needle || e.name && e.name.toLowerCase() === needle
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
- if (!match) {
167
- const known = catalog.map((e) => e.slug || e.appId).sort().filter((value) => typeof value === "string" && value.length > 0);
168
- throw new Error(
169
- `Unknown application "${application}". ${formatKnownValues(known, "applications")}`
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
- return { application: match };
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 topolo_list_applications."
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 topolo_list_applications."
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 topolo_list_services first to discover callable app IDs; the SDK can also resolve live Auth-catalog app ids/slugs for newly registered API surfaces.",
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 topolo_list_services apiKeyResources. Aliases are app-defined, e.g. brand, mailbox, venue, merchant, or any other cataloged alias."
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 topolo_list_services first to discover callable services.");
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 resourceType = typeof args["resourceType"] === "string" ? args["resourceType"].trim() : "";
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
- const baseTools = filterToolsByScopes(tools, scopes);
346
- const actions = await topolo.client.listActions().catch(() => []);
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 = {