@topolo/mcp 0.4.2 → 0.4.4
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 +84 -49
- package/dist/index.js +240 -64
- 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
|
@@ -87,7 +87,7 @@ var TOOLS = [
|
|
|
87
87
|
slug: entry.slug,
|
|
88
88
|
name: entry.name,
|
|
89
89
|
baseUrl: entry.baseUrl,
|
|
90
|
-
effectiveUrl: resolveServiceUrl(id)
|
|
90
|
+
effectiveUrl: entry.baseUrl || resolveServiceUrl(id),
|
|
91
91
|
surfaceKind: entry.surfaceKind,
|
|
92
92
|
launchable: entry.launchable,
|
|
93
93
|
permissions: [...entry.permissions]
|
|
@@ -96,6 +96,28 @@ var TOOLS = [
|
|
|
96
96
|
return { services };
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
name: "topolo_list_actions",
|
|
101
|
+
title: "List every Topolo action the agent can call",
|
|
102
|
+
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.",
|
|
103
|
+
requiredScopes: [],
|
|
104
|
+
destructive: false,
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
service: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description: "Optional service id, slug, or name filter."
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
additionalProperties: false
|
|
114
|
+
},
|
|
115
|
+
handler: async (topolo, args) => ({
|
|
116
|
+
actions: await topolo.client.listActions({
|
|
117
|
+
service: typeof args["service"] === "string" ? args["service"] : void 0
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
},
|
|
99
121
|
{
|
|
100
122
|
name: "topolo_list_applications",
|
|
101
123
|
title: "List every Topolo platform application",
|
|
@@ -229,29 +251,6 @@ var TOOLS = [
|
|
|
229
251
|
return gate ? { ...report, conformanceGate: evaluateApplicationAuditGate(report, gate) } : report;
|
|
230
252
|
}
|
|
231
253
|
},
|
|
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
254
|
{
|
|
256
255
|
name: "topolo_api_call",
|
|
257
256
|
title: "Call any Topolo platform service",
|
|
@@ -319,35 +318,59 @@ var TOOLS = [
|
|
|
319
318
|
confirm: Boolean(args["confirm"])
|
|
320
319
|
});
|
|
321
320
|
}
|
|
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
321
|
}
|
|
347
322
|
];
|
|
348
323
|
function filterToolsByScopes(tools, scopes) {
|
|
349
324
|
return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
|
|
350
325
|
}
|
|
326
|
+
async function buildTopoloTools(topolo, scopes, tools = TOOLS) {
|
|
327
|
+
const baseTools = filterToolsByScopes(tools, scopes);
|
|
328
|
+
const actions = await topolo.client.listActions().catch(() => []);
|
|
329
|
+
const actionTools = actionsToToolDefs(actions);
|
|
330
|
+
return filterToolsByScopes([...baseTools, ...actionTools], scopes);
|
|
331
|
+
}
|
|
332
|
+
function actionsToToolDefs(actions) {
|
|
333
|
+
return actions.map((action) => ({
|
|
334
|
+
name: action.toolName || toolNameForAction(action),
|
|
335
|
+
title: action.title,
|
|
336
|
+
description: action.description,
|
|
337
|
+
requiredScopes: action.requiredPermission ? [action.requiredPermission] : [],
|
|
338
|
+
destructive: action.destructive || action.requiresConfirmation || action.method !== "GET",
|
|
339
|
+
inputSchema: actionInputSchema(action),
|
|
340
|
+
handler: async (topolo, args) => {
|
|
341
|
+
const confirm = Boolean(args["confirm"]);
|
|
342
|
+
const input = { ...args };
|
|
343
|
+
delete input["confirm"];
|
|
344
|
+
return topolo.client.callAction(action.actionId, input, { confirm });
|
|
345
|
+
}
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
function toolNameForAction(action) {
|
|
349
|
+
const service = (action.serviceSlug || action.serviceId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
350
|
+
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
351
|
+
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
352
|
+
}
|
|
353
|
+
function actionInputSchema(action) {
|
|
354
|
+
const schema = {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {},
|
|
357
|
+
additionalProperties: false,
|
|
358
|
+
...action.inputSchema
|
|
359
|
+
};
|
|
360
|
+
const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? { ...schema.properties } : {};
|
|
361
|
+
if (action.requiresConfirmation || action.method !== "GET") {
|
|
362
|
+
properties.confirm = {
|
|
363
|
+
type: "boolean",
|
|
364
|
+
description: "Required for mutating actions. Explicit human-in-the-loop acknowledgement."
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
...schema,
|
|
369
|
+
type: "object",
|
|
370
|
+
properties,
|
|
371
|
+
additionalProperties: schema.additionalProperties ?? false
|
|
372
|
+
};
|
|
373
|
+
}
|
|
351
374
|
function evaluateApplicationAuditGate(report, threshold) {
|
|
352
375
|
const counts = {
|
|
353
376
|
met: 0,
|
|
@@ -423,9 +446,19 @@ function listAvailableTopoloTools(scopes, tools = TOOLS) {
|
|
|
423
446
|
inputSchema: t.inputSchema
|
|
424
447
|
}));
|
|
425
448
|
}
|
|
449
|
+
async function listAvailableDynamicTopoloTools(topolo, scopes, tools = TOOLS) {
|
|
450
|
+
return (await buildTopoloTools(topolo, scopes, tools)).map((t) => ({
|
|
451
|
+
name: t.name,
|
|
452
|
+
title: t.title,
|
|
453
|
+
description: t.description,
|
|
454
|
+
requiredScopes: t.requiredScopes,
|
|
455
|
+
destructive: t.destructive,
|
|
456
|
+
inputSchema: t.inputSchema
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
426
459
|
async function dispatchTopoloTool(input) {
|
|
427
460
|
const started = Date.now();
|
|
428
|
-
const tools = input.tools ?? TOOLS;
|
|
461
|
+
const tools = await buildTopoloTools(input.topolo, input.scopes, input.tools ?? TOOLS);
|
|
429
462
|
const available = filterToolsByScopes(tools, input.scopes);
|
|
430
463
|
const tool = available.find((t) => t.name === input.toolName);
|
|
431
464
|
if (!tool) {
|
|
@@ -476,9 +509,11 @@ async function dispatchTopoloTool(input) {
|
|
|
476
509
|
}
|
|
477
510
|
export {
|
|
478
511
|
TOOLS,
|
|
512
|
+
buildTopoloTools,
|
|
479
513
|
dispatchTopoloTool,
|
|
480
514
|
filterToolsByScopes,
|
|
481
515
|
hasAnyScope,
|
|
482
516
|
hasScope,
|
|
517
|
+
listAvailableDynamicTopoloTools,
|
|
483
518
|
listAvailableTopoloTools
|
|
484
519
|
};
|
package/dist/index.js
CHANGED
|
@@ -2062,6 +2062,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2062
2062
|
fetchImpl;
|
|
2063
2063
|
debug;
|
|
2064
2064
|
serviceCatalogCache = null;
|
|
2065
|
+
actionCatalogCache = null;
|
|
2065
2066
|
/**
|
|
2066
2067
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
2067
2068
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2322,6 +2323,63 @@ var TopoloClient = class _TopoloClient {
|
|
|
2322
2323
|
};
|
|
2323
2324
|
return entries;
|
|
2324
2325
|
}
|
|
2326
|
+
/**
|
|
2327
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
2328
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
2329
|
+
* action list is used as an authorization source.
|
|
2330
|
+
*/
|
|
2331
|
+
async listActions(options = {}) {
|
|
2332
|
+
const now = Date.now();
|
|
2333
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
2334
|
+
return this.actionCatalogCache.entries;
|
|
2335
|
+
}
|
|
2336
|
+
const res = await this.request({
|
|
2337
|
+
service: "auth",
|
|
2338
|
+
method: "GET",
|
|
2339
|
+
path: "/api/actions/catalog",
|
|
2340
|
+
...options.service ? { query: { service: options.service } } : {}
|
|
2341
|
+
});
|
|
2342
|
+
const raw = res.data?.actions ?? res.actions;
|
|
2343
|
+
if (!Array.isArray(raw)) {
|
|
2344
|
+
throw new TopoloSdkError(
|
|
2345
|
+
"action_catalog_unavailable",
|
|
2346
|
+
"TopoloAuth did not return an action catalog for the current credential."
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2350
|
+
if (!options.service) {
|
|
2351
|
+
this.actionCatalogCache = {
|
|
2352
|
+
entries,
|
|
2353
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
return entries;
|
|
2357
|
+
}
|
|
2358
|
+
async getAction(action) {
|
|
2359
|
+
const needle = normalizeActionLookupKey(action);
|
|
2360
|
+
const actions = await this.listActions();
|
|
2361
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2362
|
+
if (!match) {
|
|
2363
|
+
throw new TopoloSdkError(
|
|
2364
|
+
"unknown_action",
|
|
2365
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
return match;
|
|
2369
|
+
}
|
|
2370
|
+
async callAction(action, input = {}, options = {}) {
|
|
2371
|
+
const entry = await this.getAction(action);
|
|
2372
|
+
const mapped = mapActionInput(entry, input);
|
|
2373
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
2374
|
+
return this.request({
|
|
2375
|
+
service,
|
|
2376
|
+
method: entry.method,
|
|
2377
|
+
path: mapped.path,
|
|
2378
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2379
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2380
|
+
confirm: options.confirm === true
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2325
2383
|
/**
|
|
2326
2384
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
2327
2385
|
* for deciding what the current credential may use.
|
|
@@ -2431,6 +2489,98 @@ function isCallableCatalogEntry(entry) {
|
|
|
2431
2489
|
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2432
2490
|
);
|
|
2433
2491
|
}
|
|
2492
|
+
function normalizeActionCatalogEntry(item) {
|
|
2493
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2494
|
+
const a = item;
|
|
2495
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
2496
|
+
const name = pickActionString(a.name);
|
|
2497
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
2498
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
2499
|
+
const path = pickActionString(a.path);
|
|
2500
|
+
if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
|
|
2501
|
+
return null;
|
|
2502
|
+
}
|
|
2503
|
+
return {
|
|
2504
|
+
actionId,
|
|
2505
|
+
name,
|
|
2506
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2507
|
+
title: pickActionString(a.title) ?? name,
|
|
2508
|
+
description: pickActionString(a.description) ?? "",
|
|
2509
|
+
serviceId,
|
|
2510
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
2511
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
2512
|
+
method,
|
|
2513
|
+
path,
|
|
2514
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2515
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2516
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2517
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2518
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
2519
|
+
destructive: Boolean(a.destructive),
|
|
2520
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
2521
|
+
status: pickActionString(a.status),
|
|
2522
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision)
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
function isHttpMethod(value) {
|
|
2526
|
+
return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
|
|
2527
|
+
}
|
|
2528
|
+
function pickActionString(value) {
|
|
2529
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2530
|
+
}
|
|
2531
|
+
function plainObject(value) {
|
|
2532
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2533
|
+
}
|
|
2534
|
+
function normalizeActionLookupKey(value) {
|
|
2535
|
+
return String(value || "").trim().toLowerCase();
|
|
2536
|
+
}
|
|
2537
|
+
function actionEntryMatches(entry, needle) {
|
|
2538
|
+
if (!needle) return false;
|
|
2539
|
+
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2540
|
+
}
|
|
2541
|
+
function mapActionInput(action, input) {
|
|
2542
|
+
const remaining = { ...input || {} };
|
|
2543
|
+
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
2544
|
+
const value = remaining[key];
|
|
2545
|
+
if (value === void 0 || value === null || value === "") {
|
|
2546
|
+
throw new TopoloSdkError(
|
|
2547
|
+
"invalid_action_input",
|
|
2548
|
+
`Action "${action.name}" requires path parameter "${key}".`
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
delete remaining[key];
|
|
2552
|
+
return encodeURIComponent(String(value));
|
|
2553
|
+
});
|
|
2554
|
+
const explicitQuery = plainObject(remaining.query);
|
|
2555
|
+
delete remaining.query;
|
|
2556
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
|
|
2557
|
+
delete remaining.body;
|
|
2558
|
+
if (action.method === "GET") {
|
|
2559
|
+
return {
|
|
2560
|
+
path,
|
|
2561
|
+
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
const query = objectToQuery(explicitQuery);
|
|
2565
|
+
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
2566
|
+
return {
|
|
2567
|
+
path,
|
|
2568
|
+
...query ? { query } : {},
|
|
2569
|
+
body
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
function objectToQuery(value) {
|
|
2573
|
+
const query = {};
|
|
2574
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
2575
|
+
if (raw === void 0 || raw === null) continue;
|
|
2576
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
2577
|
+
query[key] = raw;
|
|
2578
|
+
} else {
|
|
2579
|
+
query[key] = JSON.stringify(raw);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
return Object.keys(query).length > 0 ? query : void 0;
|
|
2583
|
+
}
|
|
2434
2584
|
function describeError(body, fallback) {
|
|
2435
2585
|
if (body && typeof body === "object") {
|
|
2436
2586
|
const maybe = body;
|
|
@@ -2631,7 +2781,7 @@ var TOOLS = [
|
|
|
2631
2781
|
slug: entry.slug,
|
|
2632
2782
|
name: entry.name,
|
|
2633
2783
|
baseUrl: entry.baseUrl,
|
|
2634
|
-
effectiveUrl: resolveServiceUrl(id)
|
|
2784
|
+
effectiveUrl: entry.baseUrl || resolveServiceUrl(id),
|
|
2635
2785
|
surfaceKind: entry.surfaceKind,
|
|
2636
2786
|
launchable: entry.launchable,
|
|
2637
2787
|
permissions: [...entry.permissions]
|
|
@@ -2640,6 +2790,28 @@ var TOOLS = [
|
|
|
2640
2790
|
return { services };
|
|
2641
2791
|
}
|
|
2642
2792
|
},
|
|
2793
|
+
{
|
|
2794
|
+
name: "topolo_list_actions",
|
|
2795
|
+
title: "List every Topolo action the agent can call",
|
|
2796
|
+
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.",
|
|
2797
|
+
requiredScopes: [],
|
|
2798
|
+
destructive: false,
|
|
2799
|
+
inputSchema: {
|
|
2800
|
+
type: "object",
|
|
2801
|
+
properties: {
|
|
2802
|
+
service: {
|
|
2803
|
+
type: "string",
|
|
2804
|
+
description: "Optional service id, slug, or name filter."
|
|
2805
|
+
}
|
|
2806
|
+
},
|
|
2807
|
+
additionalProperties: false
|
|
2808
|
+
},
|
|
2809
|
+
handler: async (topolo, args) => ({
|
|
2810
|
+
actions: await topolo.client.listActions({
|
|
2811
|
+
service: typeof args["service"] === "string" ? args["service"] : void 0
|
|
2812
|
+
})
|
|
2813
|
+
})
|
|
2814
|
+
},
|
|
2643
2815
|
{
|
|
2644
2816
|
name: "topolo_list_applications",
|
|
2645
2817
|
title: "List every Topolo platform application",
|
|
@@ -2773,29 +2945,6 @@ var TOOLS = [
|
|
|
2773
2945
|
return gate ? { ...report, conformanceGate: evaluateApplicationAuditGate(report, gate) } : report;
|
|
2774
2946
|
}
|
|
2775
2947
|
},
|
|
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
2948
|
{
|
|
2800
2949
|
name: "topolo_api_call",
|
|
2801
2950
|
title: "Call any Topolo platform service",
|
|
@@ -2863,35 +3012,59 @@ var TOOLS = [
|
|
|
2863
3012
|
confirm: Boolean(args["confirm"])
|
|
2864
3013
|
});
|
|
2865
3014
|
}
|
|
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
3015
|
}
|
|
2891
3016
|
];
|
|
2892
3017
|
function filterToolsByScopes(tools, scopes) {
|
|
2893
3018
|
return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
|
|
2894
3019
|
}
|
|
3020
|
+
async function buildTopoloTools(topolo, scopes, tools = TOOLS) {
|
|
3021
|
+
const baseTools = filterToolsByScopes(tools, scopes);
|
|
3022
|
+
const actions = await topolo.client.listActions().catch(() => []);
|
|
3023
|
+
const actionTools = actionsToToolDefs(actions);
|
|
3024
|
+
return filterToolsByScopes([...baseTools, ...actionTools], scopes);
|
|
3025
|
+
}
|
|
3026
|
+
function actionsToToolDefs(actions) {
|
|
3027
|
+
return actions.map((action) => ({
|
|
3028
|
+
name: action.toolName || toolNameForAction(action),
|
|
3029
|
+
title: action.title,
|
|
3030
|
+
description: action.description,
|
|
3031
|
+
requiredScopes: action.requiredPermission ? [action.requiredPermission] : [],
|
|
3032
|
+
destructive: action.destructive || action.requiresConfirmation || action.method !== "GET",
|
|
3033
|
+
inputSchema: actionInputSchema(action),
|
|
3034
|
+
handler: async (topolo, args) => {
|
|
3035
|
+
const confirm = Boolean(args["confirm"]);
|
|
3036
|
+
const input = { ...args };
|
|
3037
|
+
delete input["confirm"];
|
|
3038
|
+
return topolo.client.callAction(action.actionId, input, { confirm });
|
|
3039
|
+
}
|
|
3040
|
+
}));
|
|
3041
|
+
}
|
|
3042
|
+
function toolNameForAction(action) {
|
|
3043
|
+
const service = (action.serviceSlug || action.serviceId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3044
|
+
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3045
|
+
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
3046
|
+
}
|
|
3047
|
+
function actionInputSchema(action) {
|
|
3048
|
+
const schema = {
|
|
3049
|
+
type: "object",
|
|
3050
|
+
properties: {},
|
|
3051
|
+
additionalProperties: false,
|
|
3052
|
+
...action.inputSchema
|
|
3053
|
+
};
|
|
3054
|
+
const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? { ...schema.properties } : {};
|
|
3055
|
+
if (action.requiresConfirmation || action.method !== "GET") {
|
|
3056
|
+
properties.confirm = {
|
|
3057
|
+
type: "boolean",
|
|
3058
|
+
description: "Required for mutating actions. Explicit human-in-the-loop acknowledgement."
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
return {
|
|
3062
|
+
...schema,
|
|
3063
|
+
type: "object",
|
|
3064
|
+
properties,
|
|
3065
|
+
additionalProperties: schema.additionalProperties ?? false
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
2895
3068
|
function evaluateApplicationAuditGate(report, threshold) {
|
|
2896
3069
|
const counts = {
|
|
2897
3070
|
met: 0,
|
|
@@ -2961,25 +3134,28 @@ var MCP_VERSION = "0.4.1";
|
|
|
2961
3134
|
|
|
2962
3135
|
// src/server.ts
|
|
2963
3136
|
function buildServer(topolo, scopes, tools = TOOLS) {
|
|
2964
|
-
const availableTools = filterToolsByScopes(tools, scopes);
|
|
2965
3137
|
const server = new Server(
|
|
2966
3138
|
{ name: "topolo", version: MCP_VERSION },
|
|
2967
3139
|
{ capabilities: { tools: {} } }
|
|
2968
3140
|
);
|
|
2969
|
-
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3141
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
3142
|
+
const availableTools = await buildTopoloTools(topolo, scopes, tools);
|
|
3143
|
+
return {
|
|
3144
|
+
tools: availableTools.map((t) => ({
|
|
3145
|
+
name: t.name,
|
|
3146
|
+
title: t.title,
|
|
3147
|
+
description: t.description,
|
|
3148
|
+
inputSchema: t.inputSchema,
|
|
3149
|
+
annotations: {
|
|
3150
|
+
destructiveHint: t.destructive,
|
|
3151
|
+
readOnlyHint: !t.destructive
|
|
3152
|
+
}
|
|
3153
|
+
}))
|
|
3154
|
+
};
|
|
3155
|
+
});
|
|
2981
3156
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2982
3157
|
const params = request.params;
|
|
3158
|
+
const availableTools = await buildTopoloTools(topolo, scopes, tools);
|
|
2983
3159
|
const tool = availableTools.find((t) => t.name === params.name);
|
|
2984
3160
|
if (!tool) {
|
|
2985
3161
|
return toolError(
|
|
@@ -3008,8 +3184,8 @@ function buildServer(topolo, scopes, tools = TOOLS) {
|
|
|
3008
3184
|
});
|
|
3009
3185
|
return server;
|
|
3010
3186
|
}
|
|
3011
|
-
function advertisedToolNames(scopes, tools = TOOLS) {
|
|
3012
|
-
return
|
|
3187
|
+
async function advertisedToolNames(topolo, scopes, tools = TOOLS) {
|
|
3188
|
+
return (await buildTopoloTools(topolo, scopes, tools)).map((t) => t.name);
|
|
3013
3189
|
}
|
|
3014
3190
|
function toolError(message) {
|
|
3015
3191
|
return {
|
|
@@ -3043,7 +3219,7 @@ async function main() {
|
|
|
3043
3219
|
const server = buildServer(topolo, scopes);
|
|
3044
3220
|
const transport = new StdioServerTransport();
|
|
3045
3221
|
await server.connect(transport);
|
|
3046
|
-
const toolNames = advertisedToolNames(scopes);
|
|
3222
|
+
const toolNames = await advertisedToolNames(topolo, scopes);
|
|
3047
3223
|
process.stderr.write(
|
|
3048
3224
|
`TopoloMCP v${MCP_VERSION} connected. Tools available: ${toolNames.join(", ") || "(none)"}
|
|
3049
3225
|
`
|
package/package.json
CHANGED