@topolo/mcp 0.4.3 → 0.4.5
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 +21 -9
- package/dist/dispatch.d.ts +3 -1
- package/dist/dispatch.js +87 -49
- package/dist/index.js +338 -75
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ ready-made agent guide that covers both the CLI and this MCP.
|
|
|
79
79
|
| `TOPOLO_API_KEY` | Platform API key (preferred) |
|
|
80
80
|
| `TOPOLO_ACCESS_TOKEN` | Short-lived JWT (dev/testing) |
|
|
81
81
|
| `TOPOLO_AGENT_NAME` | Human-readable agent label for audit logs |
|
|
82
|
-
| `TOPOLO_SERVICE_URL_<ID>` | Override a service base URL (e.g. `_AUTH`, `
|
|
82
|
+
| `TOPOLO_SERVICE_URL_<ID>` | Override a service base URL (e.g. `_AUTH`, `_MAIL`) |
|
|
83
83
|
|
|
84
84
|
Exactly one credential var must be set. If both are present, `TOPOLO_API_KEY`
|
|
85
85
|
wins.
|
|
@@ -88,8 +88,10 @@ wins.
|
|
|
88
88
|
|
|
89
89
|
1. Resolve the credential from env. **Refuse to start** if none is set.
|
|
90
90
|
2. Call `GET /api/me` to load the user's granted scopes + role.
|
|
91
|
-
3.
|
|
92
|
-
4.
|
|
91
|
+
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
95
|
|
|
94
96
|
Startup is synchronous and fast (< 500 ms typical). If the credential is
|
|
95
97
|
rejected, the process logs the error to stderr and exits non-zero — host
|
|
@@ -105,10 +107,17 @@ clients surface that as a failed server launch.
|
|
|
105
107
|
| `topolo_get_application` | (none) | no |
|
|
106
108
|
| `topolo_list_application_requirements` | (none) | no |
|
|
107
109
|
| `topolo_audit_applications` | (none) | no |
|
|
108
|
-
| `
|
|
109
|
-
|
|
|
110
|
+
| `topolo_list_actions` | (credential-scoped) | no |
|
|
111
|
+
| Dynamic action tools | action permission | action-defined |
|
|
110
112
|
| `topolo_api_call` | (service-enforced) | yes |
|
|
111
113
|
|
|
114
|
+
Applications publish machine-action definitions through Topolo Developers.
|
|
115
|
+
TopoloAuth projects those into a credential-scoped action catalog using the
|
|
116
|
+
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.
|
|
120
|
+
|
|
112
121
|
`topolo_api_call` is the escape hatch for endpoints without typed MCP tools
|
|
113
122
|
yet. GET requests work without confirmation; mutating HTTP methods must pass
|
|
114
123
|
`confirm: true` or the SDK rejects the call. Backing services still enforce
|
|
@@ -116,7 +125,8 @@ permissions for every request. The tool schema intentionally does not embed a
|
|
|
116
125
|
large service enum; agents should call `topolo_list_services` first, then pass a
|
|
117
126
|
service ID/slug from that credential-scoped result. New services become callable
|
|
118
127
|
without an MCP release once Auth grants the organization and credential the
|
|
119
|
-
relevant permissions
|
|
128
|
+
relevant permissions, and new actions become typed tools without an MCP release
|
|
129
|
+
once the application publishes them.
|
|
120
130
|
|
|
121
131
|
## Safety rails
|
|
122
132
|
|
|
@@ -128,11 +138,13 @@ relevant permissions.
|
|
|
128
138
|
- **Credential-scoped catalog discovery.** `topolo_list_services` and
|
|
129
139
|
`topolo_list_applications` use TopoloAuth's live organization/permission
|
|
130
140
|
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.
|
|
131
143
|
- **Audit headers.** Every request sends `X-Topolo-Client: topolo-mcp/<ver>`,
|
|
132
144
|
`X-Topolo-Agent: <label>`, `X-Topolo-Request-Id: <uuid>`.
|
|
133
145
|
- **Write-action confirmation.** The SDK refuses mutating HTTP methods unless
|
|
134
|
-
the call explicitly passes `confirm: true`. Mutating tools
|
|
135
|
-
|
|
146
|
+
the call explicitly passes `confirm: true`. Mutating action tools require the
|
|
147
|
+
host client to approve before invocation.
|
|
136
148
|
|
|
137
149
|
## Troubleshooting
|
|
138
150
|
|
|
@@ -175,6 +187,6 @@ after running the same package checks and `npm pack --dry-run`.
|
|
|
175
187
|
|
|
176
188
|
## Phase 2 (planned)
|
|
177
189
|
|
|
178
|
-
-
|
|
190
|
+
- Hosted action catalog administration in Developers UI.
|
|
179
191
|
- HTTP transport in addition to stdio, for hosted (non-subprocess) deployments.
|
|
180
192
|
- Optional `--provenance` on npm publish once the repo is public.
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ interface ToolDef {
|
|
|
43
43
|
}
|
|
44
44
|
declare const TOOLS: ToolDef[];
|
|
45
45
|
declare function filterToolsByScopes(tools: ToolDef[], scopes: ScopeSet): ToolDef[];
|
|
46
|
+
declare function buildTopoloTools(topolo: Topolo, scopes: ScopeSet, tools?: ToolDef[]): Promise<ToolDef[]>;
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Transport-agnostic Topolo tool dispatch.
|
|
@@ -84,6 +85,7 @@ interface ToolAdvertisement {
|
|
|
84
85
|
inputSchema: Record<string, unknown>;
|
|
85
86
|
}
|
|
86
87
|
declare function listAvailableTopoloTools(scopes: ScopeSet, tools?: ToolDef[]): ToolAdvertisement[];
|
|
88
|
+
declare function listAvailableDynamicTopoloTools(topolo: Topolo, scopes: ScopeSet, tools?: ToolDef[]): Promise<ToolAdvertisement[]>;
|
|
87
89
|
declare function dispatchTopoloTool(input: ToolDispatchInput): Promise<ToolDispatchResult>;
|
|
88
90
|
|
|
89
|
-
export { type ScopeSet, TOOLS, type ToolAdvertisement, type ToolDef, type ToolDispatchFailureReason, type ToolDispatchInput, type ToolDispatchResult, dispatchTopoloTool, filterToolsByScopes, hasAnyScope, hasScope, listAvailableTopoloTools };
|
|
91
|
+
export { type ScopeSet, TOOLS, type ToolAdvertisement, type ToolDef, type ToolDispatchFailureReason, type ToolDispatchInput, type ToolDispatchResult, buildTopoloTools, dispatchTopoloTool, filterToolsByScopes, hasAnyScope, hasScope, listAvailableDynamicTopoloTools, listAvailableTopoloTools };
|
package/dist/dispatch.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
auditAllApplicationRequirements,
|
|
11
11
|
applicationRequirementScopes,
|
|
12
12
|
requirementsForApplication,
|
|
13
|
+
resolveCatalogServiceUrl,
|
|
13
14
|
resolveServiceUrl
|
|
14
15
|
} from "@topolo/sdk";
|
|
15
16
|
|
|
@@ -87,7 +88,9 @@ var TOOLS = [
|
|
|
87
88
|
slug: entry.slug,
|
|
88
89
|
name: entry.name,
|
|
89
90
|
baseUrl: entry.baseUrl,
|
|
90
|
-
|
|
91
|
+
apiBaseUrl: entry.apiBaseUrl,
|
|
92
|
+
launchUrl: entry.launchUrl,
|
|
93
|
+
effectiveUrl: resolveCatalogServiceUrl(entry, { serviceKey: id }) || resolveServiceUrl(id),
|
|
91
94
|
surfaceKind: entry.surfaceKind,
|
|
92
95
|
launchable: entry.launchable,
|
|
93
96
|
permissions: [...entry.permissions]
|
|
@@ -96,6 +99,28 @@ var TOOLS = [
|
|
|
96
99
|
return { services };
|
|
97
100
|
}
|
|
98
101
|
},
|
|
102
|
+
{
|
|
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.",
|
|
106
|
+
requiredScopes: [],
|
|
107
|
+
destructive: false,
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
service: {
|
|
112
|
+
type: "string",
|
|
113
|
+
description: "Optional service id, slug, or name filter."
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
additionalProperties: false
|
|
117
|
+
},
|
|
118
|
+
handler: async (topolo, args) => ({
|
|
119
|
+
actions: await topolo.client.listActions({
|
|
120
|
+
service: typeof args["service"] === "string" ? args["service"] : void 0
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
},
|
|
99
124
|
{
|
|
100
125
|
name: "topolo_list_applications",
|
|
101
126
|
title: "List every Topolo platform application",
|
|
@@ -229,29 +254,6 @@ var TOOLS = [
|
|
|
229
254
|
return gate ? { ...report, conformanceGate: evaluateApplicationAuditGate(report, gate) } : report;
|
|
230
255
|
}
|
|
231
256
|
},
|
|
232
|
-
{
|
|
233
|
-
name: "topolo_crm_list_contacts",
|
|
234
|
-
title: "List CRM contacts",
|
|
235
|
-
description: "List contacts in the authenticated organization from TopoloCRM. Supports full-text search and pagination. All results are scoped to the caller's organization \u2014 there is no way to query contacts from another org.",
|
|
236
|
-
requiredScopes: ["crm.contacts:read"],
|
|
237
|
-
destructive: false,
|
|
238
|
-
inputSchema: {
|
|
239
|
-
type: "object",
|
|
240
|
-
properties: {
|
|
241
|
-
query: { type: "string", description: "Full-text search string" },
|
|
242
|
-
page: { type: "integer", minimum: 1, default: 1 },
|
|
243
|
-
pageSize: { type: "integer", minimum: 1, maximum: 200, default: 25 }
|
|
244
|
-
},
|
|
245
|
-
additionalProperties: false
|
|
246
|
-
},
|
|
247
|
-
handler: async (topolo, args) => {
|
|
248
|
-
return topolo.crm.listContacts({
|
|
249
|
-
q: typeof args["query"] === "string" ? args["query"] : void 0,
|
|
250
|
-
page: typeof args["page"] === "number" ? args["page"] : void 0,
|
|
251
|
-
pageSize: typeof args["pageSize"] === "number" ? args["pageSize"] : void 0
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
257
|
{
|
|
256
258
|
name: "topolo_api_call",
|
|
257
259
|
title: "Call any Topolo platform service",
|
|
@@ -319,35 +321,59 @@ var TOOLS = [
|
|
|
319
321
|
confirm: Boolean(args["confirm"])
|
|
320
322
|
});
|
|
321
323
|
}
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
name: "topolo_crm_get_contact",
|
|
325
|
-
title: "Fetch a single CRM contact",
|
|
326
|
-
description: "Fetch a single contact by id from TopoloCRM, scoped to the caller's organization.",
|
|
327
|
-
requiredScopes: ["crm.contacts:read"],
|
|
328
|
-
destructive: false,
|
|
329
|
-
inputSchema: {
|
|
330
|
-
type: "object",
|
|
331
|
-
properties: {
|
|
332
|
-
contactId: { type: "string", minLength: 1 }
|
|
333
|
-
},
|
|
334
|
-
required: ["contactId"],
|
|
335
|
-
additionalProperties: false
|
|
336
|
-
},
|
|
337
|
-
handler: async (topolo, args) => {
|
|
338
|
-
const contactId = args["contactId"];
|
|
339
|
-
if (typeof contactId !== "string" || !contactId) {
|
|
340
|
-
throw new Error("contactId is required");
|
|
341
|
-
}
|
|
342
|
-
const contact = await topolo.crm.getContact(contactId);
|
|
343
|
-
if (!contact) throw new Error(`Contact not found: ${contactId}`);
|
|
344
|
-
return contact;
|
|
345
|
-
}
|
|
346
324
|
}
|
|
347
325
|
];
|
|
348
326
|
function filterToolsByScopes(tools, scopes) {
|
|
349
327
|
return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
|
|
350
328
|
}
|
|
329
|
+
async function buildTopoloTools(topolo, scopes, tools = TOOLS) {
|
|
330
|
+
const baseTools = filterToolsByScopes(tools, scopes);
|
|
331
|
+
const actions = await topolo.client.listActions().catch(() => []);
|
|
332
|
+
const actionTools = actionsToToolDefs(actions);
|
|
333
|
+
return filterToolsByScopes([...baseTools, ...actionTools], scopes);
|
|
334
|
+
}
|
|
335
|
+
function actionsToToolDefs(actions) {
|
|
336
|
+
return actions.map((action) => ({
|
|
337
|
+
name: action.toolName || toolNameForAction(action),
|
|
338
|
+
title: action.title,
|
|
339
|
+
description: action.description,
|
|
340
|
+
requiredScopes: action.requiredPermission ? [action.requiredPermission] : [],
|
|
341
|
+
destructive: action.destructive || action.requiresConfirmation || action.method !== "GET",
|
|
342
|
+
inputSchema: actionInputSchema(action),
|
|
343
|
+
handler: async (topolo, args) => {
|
|
344
|
+
const confirm = Boolean(args["confirm"]);
|
|
345
|
+
const input = { ...args };
|
|
346
|
+
delete input["confirm"];
|
|
347
|
+
return topolo.client.callAction(action.actionId, input, { confirm });
|
|
348
|
+
}
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
function toolNameForAction(action) {
|
|
352
|
+
const service = (action.serviceSlug || action.serviceId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
353
|
+
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
354
|
+
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
355
|
+
}
|
|
356
|
+
function actionInputSchema(action) {
|
|
357
|
+
const schema = {
|
|
358
|
+
type: "object",
|
|
359
|
+
properties: {},
|
|
360
|
+
additionalProperties: false,
|
|
361
|
+
...action.inputSchema
|
|
362
|
+
};
|
|
363
|
+
const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? { ...schema.properties } : {};
|
|
364
|
+
if (action.requiresConfirmation || action.method !== "GET") {
|
|
365
|
+
properties.confirm = {
|
|
366
|
+
type: "boolean",
|
|
367
|
+
description: "Required for mutating actions. Explicit human-in-the-loop acknowledgement."
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
...schema,
|
|
372
|
+
type: "object",
|
|
373
|
+
properties,
|
|
374
|
+
additionalProperties: schema.additionalProperties ?? false
|
|
375
|
+
};
|
|
376
|
+
}
|
|
351
377
|
function evaluateApplicationAuditGate(report, threshold) {
|
|
352
378
|
const counts = {
|
|
353
379
|
met: 0,
|
|
@@ -423,9 +449,19 @@ function listAvailableTopoloTools(scopes, tools = TOOLS) {
|
|
|
423
449
|
inputSchema: t.inputSchema
|
|
424
450
|
}));
|
|
425
451
|
}
|
|
452
|
+
async function listAvailableDynamicTopoloTools(topolo, scopes, tools = TOOLS) {
|
|
453
|
+
return (await buildTopoloTools(topolo, scopes, tools)).map((t) => ({
|
|
454
|
+
name: t.name,
|
|
455
|
+
title: t.title,
|
|
456
|
+
description: t.description,
|
|
457
|
+
requiredScopes: t.requiredScopes,
|
|
458
|
+
destructive: t.destructive,
|
|
459
|
+
inputSchema: t.inputSchema
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
426
462
|
async function dispatchTopoloTool(input) {
|
|
427
463
|
const started = Date.now();
|
|
428
|
-
const tools = input.tools ?? TOOLS;
|
|
464
|
+
const tools = await buildTopoloTools(input.topolo, input.scopes, input.tools ?? TOOLS);
|
|
429
465
|
const available = filterToolsByScopes(tools, input.scopes);
|
|
430
466
|
const tool = available.find((t) => t.name === input.toolName);
|
|
431
467
|
if (!tool) {
|
|
@@ -476,9 +512,11 @@ async function dispatchTopoloTool(input) {
|
|
|
476
512
|
}
|
|
477
513
|
export {
|
|
478
514
|
TOOLS,
|
|
515
|
+
buildTopoloTools,
|
|
479
516
|
dispatchTopoloTool,
|
|
480
517
|
filterToolsByScopes,
|
|
481
518
|
hasAnyScope,
|
|
482
519
|
hasScope,
|
|
520
|
+
listAvailableDynamicTopoloTools,
|
|
483
521
|
listAvailableTopoloTools
|
|
484
522
|
};
|
package/dist/index.js
CHANGED
|
@@ -2040,15 +2040,88 @@ function statusRank(status) {
|
|
|
2040
2040
|
return 2;
|
|
2041
2041
|
}
|
|
2042
2042
|
var DEFAULT_SERVICE_URLS = SERVICES;
|
|
2043
|
+
function resolveCatalogServiceUrl(entry, options = {}) {
|
|
2044
|
+
const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
|
|
2045
|
+
const override = resolveServiceUrlOverride(keys, options.overrides);
|
|
2046
|
+
if (override) return override;
|
|
2047
|
+
const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
|
|
2048
|
+
if (apiBaseUrl) return apiBaseUrl;
|
|
2049
|
+
const bundledUrl = firstBundledServiceUrl(keys);
|
|
2050
|
+
const liveUrl = firstString(entry.baseUrl);
|
|
2051
|
+
if (bundledUrl && (!liveUrl || sameUrlEnvironment(bundledUrl, liveUrl))) return bundledUrl;
|
|
2052
|
+
return liveUrl ?? bundledUrl ?? null;
|
|
2053
|
+
}
|
|
2043
2054
|
function resolveServiceUrl(service, overrides) {
|
|
2044
|
-
const override =
|
|
2055
|
+
const override = resolveServiceUrlOverride([service], overrides);
|
|
2045
2056
|
if (override) return override;
|
|
2046
|
-
const envKey = `TOPOLO_SERVICE_URL_${service.replace(/\./g, "_").toUpperCase()}`;
|
|
2047
|
-
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2048
|
-
if (envValue) return envValue;
|
|
2049
2057
|
return DEFAULT_SERVICE_URLS[service];
|
|
2050
2058
|
}
|
|
2059
|
+
function catalogServiceKeyCandidates(entry, preferred) {
|
|
2060
|
+
const values = [preferred, entry.slug, entry.name, entry.serviceId].filter(
|
|
2061
|
+
(value) => typeof value === "string" && value.trim().length > 0
|
|
2062
|
+
);
|
|
2063
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2064
|
+
for (const value of values) {
|
|
2065
|
+
for (const candidate of serviceKeyCandidates(value)) keys.add(candidate);
|
|
2066
|
+
}
|
|
2067
|
+
return [...keys];
|
|
2068
|
+
}
|
|
2069
|
+
function serviceKeyCandidates(value) {
|
|
2070
|
+
const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
2071
|
+
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
2072
|
+
return [.../* @__PURE__ */ new Set([
|
|
2073
|
+
normalized,
|
|
2074
|
+
normalized.replace(/-/g, "_"),
|
|
2075
|
+
withoutTopolo,
|
|
2076
|
+
withoutTopolo.replace(/-/g, "_")
|
|
2077
|
+
])].filter(Boolean);
|
|
2078
|
+
}
|
|
2079
|
+
function resolveServiceUrlOverride(keys, overrides) {
|
|
2080
|
+
for (const key of keys) {
|
|
2081
|
+
const direct = overrides?.[key];
|
|
2082
|
+
if (direct) return direct;
|
|
2083
|
+
const envStyleKey = key.replace(/\./g, "_");
|
|
2084
|
+
const envStyle = overrides?.[envStyleKey];
|
|
2085
|
+
if (envStyle) return envStyle;
|
|
2086
|
+
}
|
|
2087
|
+
for (const key of keys) {
|
|
2088
|
+
const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, "_").toUpperCase()}`;
|
|
2089
|
+
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2090
|
+
if (envValue) return envValue;
|
|
2091
|
+
}
|
|
2092
|
+
return null;
|
|
2093
|
+
}
|
|
2094
|
+
function firstBundledServiceUrl(keys) {
|
|
2095
|
+
for (const key of keys) {
|
|
2096
|
+
const url = DEFAULT_SERVICE_URLS[key];
|
|
2097
|
+
if (url) return url;
|
|
2098
|
+
}
|
|
2099
|
+
return null;
|
|
2100
|
+
}
|
|
2101
|
+
function firstString(...values) {
|
|
2102
|
+
for (const value of values) {
|
|
2103
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2104
|
+
}
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
function sameUrlEnvironment(left, right) {
|
|
2108
|
+
return urlEnvironment(left) === urlEnvironment(right);
|
|
2109
|
+
}
|
|
2110
|
+
function urlEnvironment(value) {
|
|
2111
|
+
try {
|
|
2112
|
+
const url = new URL(value);
|
|
2113
|
+
const host = url.hostname.toLowerCase();
|
|
2114
|
+
if (host === "localhost" || host === "127.0.0.1") return "local";
|
|
2115
|
+
if (host.endsWith(".stg.topolo.us") || host.includes("topolo-staging")) return "staging";
|
|
2116
|
+
if (host.endsWith(".topolo.app") || host.endsWith(".topolo.us")) return "production";
|
|
2117
|
+
return "external";
|
|
2118
|
+
} catch {
|
|
2119
|
+
return "external";
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2051
2122
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
2123
|
+
var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
|
|
2124
|
+
var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
|
|
2052
2125
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2053
2126
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
2054
2127
|
var CATALOG_CACHE_TTL_MS = 60 * 1e3;
|
|
@@ -2062,6 +2135,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2062
2135
|
fetchImpl;
|
|
2063
2136
|
debug;
|
|
2064
2137
|
serviceCatalogCache = null;
|
|
2138
|
+
actionCatalogCache = null;
|
|
2065
2139
|
/**
|
|
2066
2140
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
2067
2141
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2215,8 +2289,12 @@ var TopoloClient = class _TopoloClient {
|
|
|
2215
2289
|
async resolveLiveCallableService(serviceKey) {
|
|
2216
2290
|
const catalog = await this.listServices();
|
|
2217
2291
|
const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
|
|
2218
|
-
if (!match
|
|
2219
|
-
|
|
2292
|
+
if (!match || !isCallableCatalogEntry(match)) return null;
|
|
2293
|
+
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
2294
|
+
serviceKey,
|
|
2295
|
+
overrides: this.serviceUrls
|
|
2296
|
+
});
|
|
2297
|
+
return baseUrl ? { serviceKey, baseUrl } : null;
|
|
2220
2298
|
}
|
|
2221
2299
|
/**
|
|
2222
2300
|
* Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
|
|
@@ -2310,6 +2388,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2310
2388
|
name: typeof s.name === "string" ? s.name : null,
|
|
2311
2389
|
description: typeof s.description === "string" ? s.description : null,
|
|
2312
2390
|
baseUrl: typeof s.base_url === "string" ? s.base_url : null,
|
|
2391
|
+
apiBaseUrl: typeof s.api_base_url === "string" ? s.api_base_url : typeof s.apiBaseUrl === "string" ? s.apiBaseUrl : null,
|
|
2392
|
+
launchUrl: typeof s.launch_url === "string" ? s.launch_url : typeof s.launchUrl === "string" ? s.launchUrl : null,
|
|
2313
2393
|
status: typeof s.status === "string" ? s.status : null,
|
|
2314
2394
|
surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
|
|
2315
2395
|
launchable: s.launchable === true,
|
|
@@ -2322,6 +2402,67 @@ var TopoloClient = class _TopoloClient {
|
|
|
2322
2402
|
};
|
|
2323
2403
|
return entries;
|
|
2324
2404
|
}
|
|
2405
|
+
/**
|
|
2406
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
2407
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
2408
|
+
* action list is used as an authorization source.
|
|
2409
|
+
*/
|
|
2410
|
+
async listActions(options = {}) {
|
|
2411
|
+
const now = Date.now();
|
|
2412
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
2413
|
+
return this.actionCatalogCache.entries;
|
|
2414
|
+
}
|
|
2415
|
+
const res = await this.request({
|
|
2416
|
+
service: "auth",
|
|
2417
|
+
method: "GET",
|
|
2418
|
+
path: "/api/actions/catalog",
|
|
2419
|
+
...options.service ? { query: { service: options.service } } : {}
|
|
2420
|
+
});
|
|
2421
|
+
const raw = res.data?.actions ?? res.actions;
|
|
2422
|
+
if (!Array.isArray(raw)) {
|
|
2423
|
+
throw new TopoloSdkError(
|
|
2424
|
+
"action_catalog_unavailable",
|
|
2425
|
+
"TopoloAuth did not return an action catalog for the current credential."
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2429
|
+
if (!options.service) {
|
|
2430
|
+
this.actionCatalogCache = {
|
|
2431
|
+
entries,
|
|
2432
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
return entries;
|
|
2436
|
+
}
|
|
2437
|
+
async getAction(action) {
|
|
2438
|
+
const needle = normalizeActionLookupKey(action);
|
|
2439
|
+
const actions = await this.listActions();
|
|
2440
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2441
|
+
if (!match) {
|
|
2442
|
+
throw new TopoloSdkError(
|
|
2443
|
+
"unknown_action",
|
|
2444
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
return match;
|
|
2448
|
+
}
|
|
2449
|
+
async callAction(action, input = {}, options = {}) {
|
|
2450
|
+
const entry = await this.getAction(action);
|
|
2451
|
+
const mapped = mapActionInput(entry, input);
|
|
2452
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
2453
|
+
return this.request({
|
|
2454
|
+
service,
|
|
2455
|
+
method: entry.method,
|
|
2456
|
+
path: mapped.path,
|
|
2457
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2458
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2459
|
+
headers: {
|
|
2460
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2461
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2462
|
+
},
|
|
2463
|
+
confirm: options.confirm === true
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2325
2466
|
/**
|
|
2326
2467
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
2327
2468
|
* for deciding what the current credential may use.
|
|
@@ -2334,6 +2475,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2334
2475
|
name: app.name ?? app.id,
|
|
2335
2476
|
description: null,
|
|
2336
2477
|
baseUrl: app.productionUrl ?? null,
|
|
2478
|
+
apiBaseUrl: null,
|
|
2479
|
+
launchUrl: app.productionUrl ?? null,
|
|
2337
2480
|
status: "bundled",
|
|
2338
2481
|
surfaceKind: "application",
|
|
2339
2482
|
launchable: Boolean(app.productionUrl),
|
|
@@ -2377,7 +2520,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2377
2520
|
}
|
|
2378
2521
|
};
|
|
2379
2522
|
function resolveConfiguredServiceId(slug, serviceIds) {
|
|
2380
|
-
for (const serviceKey of
|
|
2523
|
+
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
2381
2524
|
const configured = serviceIds?.[serviceKey];
|
|
2382
2525
|
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
2383
2526
|
const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
|
|
@@ -2387,7 +2530,7 @@ function resolveConfiguredServiceId(slug, serviceIds) {
|
|
|
2387
2530
|
return null;
|
|
2388
2531
|
}
|
|
2389
2532
|
function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
2390
|
-
for (const serviceKey of
|
|
2533
|
+
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
2391
2534
|
const configured = serviceUrls?.[serviceKey];
|
|
2392
2535
|
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
2393
2536
|
const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
|
|
@@ -2396,7 +2539,7 @@ function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
|
2396
2539
|
}
|
|
2397
2540
|
return null;
|
|
2398
2541
|
}
|
|
2399
|
-
function
|
|
2542
|
+
function serviceKeyCandidates2(slug) {
|
|
2400
2543
|
const normalized = normalizeLookupKey(slug);
|
|
2401
2544
|
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
2402
2545
|
return [.../* @__PURE__ */ new Set([
|
|
@@ -2418,7 +2561,7 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2418
2561
|
for (const value of values) {
|
|
2419
2562
|
const normalized = normalizeLookupKey(value);
|
|
2420
2563
|
if (!normalized) continue;
|
|
2421
|
-
for (const candidate of
|
|
2564
|
+
for (const candidate of serviceKeyCandidates2(normalized)) {
|
|
2422
2565
|
keys.add(candidate);
|
|
2423
2566
|
}
|
|
2424
2567
|
}
|
|
@@ -2428,9 +2571,101 @@ function isCallableCatalogEntry(entry) {
|
|
|
2428
2571
|
const kind = normalizeLookupKey(entry.surfaceKind || "");
|
|
2429
2572
|
if (kind === "api" || kind === "technical_service" || kind === "service") return true;
|
|
2430
2573
|
return Boolean(
|
|
2431
|
-
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2574
|
+
(entry.baseUrl || entry.apiBaseUrl) && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2432
2575
|
);
|
|
2433
2576
|
}
|
|
2577
|
+
function normalizeActionCatalogEntry(item) {
|
|
2578
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2579
|
+
const a = item;
|
|
2580
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
2581
|
+
const name = pickActionString(a.name);
|
|
2582
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
2583
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
2584
|
+
const path = pickActionString(a.path);
|
|
2585
|
+
if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
|
|
2586
|
+
return null;
|
|
2587
|
+
}
|
|
2588
|
+
return {
|
|
2589
|
+
actionId,
|
|
2590
|
+
name,
|
|
2591
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2592
|
+
title: pickActionString(a.title) ?? name,
|
|
2593
|
+
description: pickActionString(a.description) ?? "",
|
|
2594
|
+
serviceId,
|
|
2595
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
2596
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
2597
|
+
method,
|
|
2598
|
+
path,
|
|
2599
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2600
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2601
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2602
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2603
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
2604
|
+
destructive: Boolean(a.destructive),
|
|
2605
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
2606
|
+
status: pickActionString(a.status),
|
|
2607
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision)
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
function isHttpMethod(value) {
|
|
2611
|
+
return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
|
|
2612
|
+
}
|
|
2613
|
+
function pickActionString(value) {
|
|
2614
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2615
|
+
}
|
|
2616
|
+
function plainObject(value) {
|
|
2617
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2618
|
+
}
|
|
2619
|
+
function normalizeActionLookupKey(value) {
|
|
2620
|
+
return String(value || "").trim().toLowerCase();
|
|
2621
|
+
}
|
|
2622
|
+
function actionEntryMatches(entry, needle) {
|
|
2623
|
+
if (!needle) return false;
|
|
2624
|
+
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2625
|
+
}
|
|
2626
|
+
function mapActionInput(action, input) {
|
|
2627
|
+
const remaining = { ...input || {} };
|
|
2628
|
+
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
2629
|
+
const value = remaining[key];
|
|
2630
|
+
if (value === void 0 || value === null || value === "") {
|
|
2631
|
+
throw new TopoloSdkError(
|
|
2632
|
+
"invalid_action_input",
|
|
2633
|
+
`Action "${action.name}" requires path parameter "${key}".`
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
delete remaining[key];
|
|
2637
|
+
return encodeURIComponent(String(value));
|
|
2638
|
+
});
|
|
2639
|
+
const explicitQuery = plainObject(remaining.query);
|
|
2640
|
+
delete remaining.query;
|
|
2641
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
|
|
2642
|
+
delete remaining.body;
|
|
2643
|
+
if (action.method === "GET") {
|
|
2644
|
+
return {
|
|
2645
|
+
path,
|
|
2646
|
+
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
const query = objectToQuery(explicitQuery);
|
|
2650
|
+
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
2651
|
+
return {
|
|
2652
|
+
path,
|
|
2653
|
+
...query ? { query } : {},
|
|
2654
|
+
body
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
function objectToQuery(value) {
|
|
2658
|
+
const query = {};
|
|
2659
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
2660
|
+
if (raw === void 0 || raw === null) continue;
|
|
2661
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
2662
|
+
query[key] = raw;
|
|
2663
|
+
} else {
|
|
2664
|
+
query[key] = JSON.stringify(raw);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
return Object.keys(query).length > 0 ? query : void 0;
|
|
2668
|
+
}
|
|
2434
2669
|
function describeError(body, fallback) {
|
|
2435
2670
|
if (body && typeof body === "object") {
|
|
2436
2671
|
const maybe = body;
|
|
@@ -2631,7 +2866,9 @@ var TOOLS = [
|
|
|
2631
2866
|
slug: entry.slug,
|
|
2632
2867
|
name: entry.name,
|
|
2633
2868
|
baseUrl: entry.baseUrl,
|
|
2634
|
-
|
|
2869
|
+
apiBaseUrl: entry.apiBaseUrl,
|
|
2870
|
+
launchUrl: entry.launchUrl,
|
|
2871
|
+
effectiveUrl: resolveCatalogServiceUrl(entry, { serviceKey: id }) || resolveServiceUrl(id),
|
|
2635
2872
|
surfaceKind: entry.surfaceKind,
|
|
2636
2873
|
launchable: entry.launchable,
|
|
2637
2874
|
permissions: [...entry.permissions]
|
|
@@ -2640,6 +2877,28 @@ var TOOLS = [
|
|
|
2640
2877
|
return { services };
|
|
2641
2878
|
}
|
|
2642
2879
|
},
|
|
2880
|
+
{
|
|
2881
|
+
name: "topolo_list_actions",
|
|
2882
|
+
title: "List every Topolo action the agent can call",
|
|
2883
|
+
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.",
|
|
2884
|
+
requiredScopes: [],
|
|
2885
|
+
destructive: false,
|
|
2886
|
+
inputSchema: {
|
|
2887
|
+
type: "object",
|
|
2888
|
+
properties: {
|
|
2889
|
+
service: {
|
|
2890
|
+
type: "string",
|
|
2891
|
+
description: "Optional service id, slug, or name filter."
|
|
2892
|
+
}
|
|
2893
|
+
},
|
|
2894
|
+
additionalProperties: false
|
|
2895
|
+
},
|
|
2896
|
+
handler: async (topolo, args) => ({
|
|
2897
|
+
actions: await topolo.client.listActions({
|
|
2898
|
+
service: typeof args["service"] === "string" ? args["service"] : void 0
|
|
2899
|
+
})
|
|
2900
|
+
})
|
|
2901
|
+
},
|
|
2643
2902
|
{
|
|
2644
2903
|
name: "topolo_list_applications",
|
|
2645
2904
|
title: "List every Topolo platform application",
|
|
@@ -2773,29 +3032,6 @@ var TOOLS = [
|
|
|
2773
3032
|
return gate ? { ...report, conformanceGate: evaluateApplicationAuditGate(report, gate) } : report;
|
|
2774
3033
|
}
|
|
2775
3034
|
},
|
|
2776
|
-
{
|
|
2777
|
-
name: "topolo_crm_list_contacts",
|
|
2778
|
-
title: "List CRM contacts",
|
|
2779
|
-
description: "List contacts in the authenticated organization from TopoloCRM. Supports full-text search and pagination. All results are scoped to the caller's organization \u2014 there is no way to query contacts from another org.",
|
|
2780
|
-
requiredScopes: ["crm.contacts:read"],
|
|
2781
|
-
destructive: false,
|
|
2782
|
-
inputSchema: {
|
|
2783
|
-
type: "object",
|
|
2784
|
-
properties: {
|
|
2785
|
-
query: { type: "string", description: "Full-text search string" },
|
|
2786
|
-
page: { type: "integer", minimum: 1, default: 1 },
|
|
2787
|
-
pageSize: { type: "integer", minimum: 1, maximum: 200, default: 25 }
|
|
2788
|
-
},
|
|
2789
|
-
additionalProperties: false
|
|
2790
|
-
},
|
|
2791
|
-
handler: async (topolo, args) => {
|
|
2792
|
-
return topolo.crm.listContacts({
|
|
2793
|
-
q: typeof args["query"] === "string" ? args["query"] : void 0,
|
|
2794
|
-
page: typeof args["page"] === "number" ? args["page"] : void 0,
|
|
2795
|
-
pageSize: typeof args["pageSize"] === "number" ? args["pageSize"] : void 0
|
|
2796
|
-
});
|
|
2797
|
-
}
|
|
2798
|
-
},
|
|
2799
3035
|
{
|
|
2800
3036
|
name: "topolo_api_call",
|
|
2801
3037
|
title: "Call any Topolo platform service",
|
|
@@ -2863,35 +3099,59 @@ var TOOLS = [
|
|
|
2863
3099
|
confirm: Boolean(args["confirm"])
|
|
2864
3100
|
});
|
|
2865
3101
|
}
|
|
2866
|
-
},
|
|
2867
|
-
{
|
|
2868
|
-
name: "topolo_crm_get_contact",
|
|
2869
|
-
title: "Fetch a single CRM contact",
|
|
2870
|
-
description: "Fetch a single contact by id from TopoloCRM, scoped to the caller's organization.",
|
|
2871
|
-
requiredScopes: ["crm.contacts:read"],
|
|
2872
|
-
destructive: false,
|
|
2873
|
-
inputSchema: {
|
|
2874
|
-
type: "object",
|
|
2875
|
-
properties: {
|
|
2876
|
-
contactId: { type: "string", minLength: 1 }
|
|
2877
|
-
},
|
|
2878
|
-
required: ["contactId"],
|
|
2879
|
-
additionalProperties: false
|
|
2880
|
-
},
|
|
2881
|
-
handler: async (topolo, args) => {
|
|
2882
|
-
const contactId = args["contactId"];
|
|
2883
|
-
if (typeof contactId !== "string" || !contactId) {
|
|
2884
|
-
throw new Error("contactId is required");
|
|
2885
|
-
}
|
|
2886
|
-
const contact = await topolo.crm.getContact(contactId);
|
|
2887
|
-
if (!contact) throw new Error(`Contact not found: ${contactId}`);
|
|
2888
|
-
return contact;
|
|
2889
|
-
}
|
|
2890
3102
|
}
|
|
2891
3103
|
];
|
|
2892
3104
|
function filterToolsByScopes(tools, scopes) {
|
|
2893
3105
|
return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
|
|
2894
3106
|
}
|
|
3107
|
+
async function buildTopoloTools(topolo, scopes, tools = TOOLS) {
|
|
3108
|
+
const baseTools = filterToolsByScopes(tools, scopes);
|
|
3109
|
+
const actions = await topolo.client.listActions().catch(() => []);
|
|
3110
|
+
const actionTools = actionsToToolDefs(actions);
|
|
3111
|
+
return filterToolsByScopes([...baseTools, ...actionTools], scopes);
|
|
3112
|
+
}
|
|
3113
|
+
function actionsToToolDefs(actions) {
|
|
3114
|
+
return actions.map((action) => ({
|
|
3115
|
+
name: action.toolName || toolNameForAction(action),
|
|
3116
|
+
title: action.title,
|
|
3117
|
+
description: action.description,
|
|
3118
|
+
requiredScopes: action.requiredPermission ? [action.requiredPermission] : [],
|
|
3119
|
+
destructive: action.destructive || action.requiresConfirmation || action.method !== "GET",
|
|
3120
|
+
inputSchema: actionInputSchema(action),
|
|
3121
|
+
handler: async (topolo, args) => {
|
|
3122
|
+
const confirm = Boolean(args["confirm"]);
|
|
3123
|
+
const input = { ...args };
|
|
3124
|
+
delete input["confirm"];
|
|
3125
|
+
return topolo.client.callAction(action.actionId, input, { confirm });
|
|
3126
|
+
}
|
|
3127
|
+
}));
|
|
3128
|
+
}
|
|
3129
|
+
function toolNameForAction(action) {
|
|
3130
|
+
const service = (action.serviceSlug || action.serviceId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3131
|
+
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3132
|
+
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
3133
|
+
}
|
|
3134
|
+
function actionInputSchema(action) {
|
|
3135
|
+
const schema = {
|
|
3136
|
+
type: "object",
|
|
3137
|
+
properties: {},
|
|
3138
|
+
additionalProperties: false,
|
|
3139
|
+
...action.inputSchema
|
|
3140
|
+
};
|
|
3141
|
+
const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? { ...schema.properties } : {};
|
|
3142
|
+
if (action.requiresConfirmation || action.method !== "GET") {
|
|
3143
|
+
properties.confirm = {
|
|
3144
|
+
type: "boolean",
|
|
3145
|
+
description: "Required for mutating actions. Explicit human-in-the-loop acknowledgement."
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
return {
|
|
3149
|
+
...schema,
|
|
3150
|
+
type: "object",
|
|
3151
|
+
properties,
|
|
3152
|
+
additionalProperties: schema.additionalProperties ?? false
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
2895
3155
|
function evaluateApplicationAuditGate(report, threshold) {
|
|
2896
3156
|
const counts = {
|
|
2897
3157
|
met: 0,
|
|
@@ -2961,25 +3221,28 @@ var MCP_VERSION = "0.4.1";
|
|
|
2961
3221
|
|
|
2962
3222
|
// src/server.ts
|
|
2963
3223
|
function buildServer(topolo, scopes, tools = TOOLS) {
|
|
2964
|
-
const availableTools = filterToolsByScopes(tools, scopes);
|
|
2965
3224
|
const server = new Server(
|
|
2966
3225
|
{ name: "topolo", version: MCP_VERSION },
|
|
2967
3226
|
{ capabilities: { tools: {} } }
|
|
2968
3227
|
);
|
|
2969
|
-
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3228
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
3229
|
+
const availableTools = await buildTopoloTools(topolo, scopes, tools);
|
|
3230
|
+
return {
|
|
3231
|
+
tools: availableTools.map((t) => ({
|
|
3232
|
+
name: t.name,
|
|
3233
|
+
title: t.title,
|
|
3234
|
+
description: t.description,
|
|
3235
|
+
inputSchema: t.inputSchema,
|
|
3236
|
+
annotations: {
|
|
3237
|
+
destructiveHint: t.destructive,
|
|
3238
|
+
readOnlyHint: !t.destructive
|
|
3239
|
+
}
|
|
3240
|
+
}))
|
|
3241
|
+
};
|
|
3242
|
+
});
|
|
2981
3243
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2982
3244
|
const params = request.params;
|
|
3245
|
+
const availableTools = await buildTopoloTools(topolo, scopes, tools);
|
|
2983
3246
|
const tool = availableTools.find((t) => t.name === params.name);
|
|
2984
3247
|
if (!tool) {
|
|
2985
3248
|
return toolError(
|
|
@@ -3008,8 +3271,8 @@ function buildServer(topolo, scopes, tools = TOOLS) {
|
|
|
3008
3271
|
});
|
|
3009
3272
|
return server;
|
|
3010
3273
|
}
|
|
3011
|
-
function advertisedToolNames(scopes, tools = TOOLS) {
|
|
3012
|
-
return
|
|
3274
|
+
async function advertisedToolNames(topolo, scopes, tools = TOOLS) {
|
|
3275
|
+
return (await buildTopoloTools(topolo, scopes, tools)).map((t) => t.name);
|
|
3013
3276
|
}
|
|
3014
3277
|
function toolError(message) {
|
|
3015
3278
|
return {
|
|
@@ -3043,7 +3306,7 @@ async function main() {
|
|
|
3043
3306
|
const server = buildServer(topolo, scopes);
|
|
3044
3307
|
const transport = new StdioServerTransport();
|
|
3045
3308
|
await server.connect(transport);
|
|
3046
|
-
const toolNames = advertisedToolNames(scopes);
|
|
3309
|
+
const toolNames = await advertisedToolNames(topolo, scopes);
|
|
3047
3310
|
process.stderr.write(
|
|
3048
3311
|
`TopoloMCP v${MCP_VERSION} connected. Tools available: ${toolNames.join(", ") || "(none)"}
|
|
3049
3312
|
`
|
package/package.json
CHANGED