@topolo/mcp 0.4.1 → 0.4.2
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 +6 -2
- package/dist/dispatch.js +37 -7
- package/dist/index.js +96 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,8 +114,9 @@ yet. GET requests work without confirmation; mutating HTTP methods must pass
|
|
|
114
114
|
`confirm: true` or the SDK rejects the call. Backing services still enforce
|
|
115
115
|
permissions for every request. The tool schema intentionally does not embed a
|
|
116
116
|
large service enum; agents should call `topolo_list_services` first, then pass a
|
|
117
|
-
service ID/slug
|
|
118
|
-
|
|
117
|
+
service ID/slug from that credential-scoped result. New services become callable
|
|
118
|
+
without an MCP release once Auth grants the organization and credential the
|
|
119
|
+
relevant permissions.
|
|
119
120
|
|
|
120
121
|
## Safety rails
|
|
121
122
|
|
|
@@ -124,6 +125,9 @@ surfaces for newly registered services before the next MCP package release.
|
|
|
124
125
|
to see Org B's data.
|
|
125
126
|
- **Scope-gated tool advertisement.** Agents never see tools they can't use —
|
|
126
127
|
fewer wasted attempts and less prompt noise.
|
|
128
|
+
- **Credential-scoped catalog discovery.** `topolo_list_services` and
|
|
129
|
+
`topolo_list_applications` use TopoloAuth's live organization/permission
|
|
130
|
+
catalog. They do not expose the global generated platform catalog.
|
|
127
131
|
- **Audit headers.** Every request sends `X-Topolo-Client: topolo-mcp/<ver>`,
|
|
128
132
|
`X-Topolo-Agent: <label>`, `X-Topolo-Request-Id: <uuid>`.
|
|
129
133
|
- **Write-action confirmation.** The SDK refuses mutating HTTP methods unless
|
package/dist/dispatch.js
CHANGED
|
@@ -70,7 +70,7 @@ var TOOLS = [
|
|
|
70
70
|
{
|
|
71
71
|
name: "topolo_list_services",
|
|
72
72
|
title: "List every Topolo service the agent can reach",
|
|
73
|
-
description: "Returns the service registry
|
|
73
|
+
description: "Returns the credential-scoped service registry: one entry per platform service the current credential can use in its organization, with granted permissions and base URL. Use this to discover what `topolo_api_call` can address. This is not the global platform catalog.",
|
|
74
74
|
requiredScopes: [],
|
|
75
75
|
destructive: false,
|
|
76
76
|
inputSchema: {
|
|
@@ -78,12 +78,21 @@ var TOOLS = [
|
|
|
78
78
|
properties: {},
|
|
79
79
|
additionalProperties: false
|
|
80
80
|
},
|
|
81
|
-
handler: async () => {
|
|
82
|
-
const services =
|
|
83
|
-
id
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
handler: async (topolo) => {
|
|
82
|
+
const services = (await topolo.client.listServices()).map((entry) => {
|
|
83
|
+
const id = displayServiceId(entry);
|
|
84
|
+
return {
|
|
85
|
+
id,
|
|
86
|
+
serviceId: entry.serviceId,
|
|
87
|
+
slug: entry.slug,
|
|
88
|
+
name: entry.name,
|
|
89
|
+
baseUrl: entry.baseUrl,
|
|
90
|
+
effectiveUrl: resolveServiceUrl(id) || entry.baseUrl,
|
|
91
|
+
surfaceKind: entry.surfaceKind,
|
|
92
|
+
launchable: entry.launchable,
|
|
93
|
+
permissions: [...entry.permissions]
|
|
94
|
+
};
|
|
95
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
87
96
|
return { services };
|
|
88
97
|
}
|
|
89
98
|
},
|
|
@@ -375,6 +384,27 @@ function statusesAtOrWorse(threshold) {
|
|
|
375
384
|
if (threshold === "needs_review") return ["missing", "needs_review"];
|
|
376
385
|
return ["missing", "needs_review", "partial"];
|
|
377
386
|
}
|
|
387
|
+
function displayServiceId(entry) {
|
|
388
|
+
for (const value of [entry.slug, entry.name, entry.serviceId]) {
|
|
389
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
390
|
+
for (const candidate of serviceIdCandidates(value)) {
|
|
391
|
+
if (Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, candidate)) {
|
|
392
|
+
return candidate;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return serviceIdCandidates(entry.slug || entry.serviceId)[0] || entry.serviceId;
|
|
397
|
+
}
|
|
398
|
+
function serviceIdCandidates(value) {
|
|
399
|
+
const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
400
|
+
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
401
|
+
return [.../* @__PURE__ */ new Set([
|
|
402
|
+
normalized,
|
|
403
|
+
normalized.replace(/-/g, "_"),
|
|
404
|
+
withoutTopolo,
|
|
405
|
+
withoutTopolo.replace(/-/g, "_")
|
|
406
|
+
])].filter(Boolean);
|
|
407
|
+
}
|
|
378
408
|
function formatKnownValues(values, label) {
|
|
379
409
|
const sorted = [...values].sort();
|
|
380
410
|
const preview = sorted.slice(0, 25).join(", ");
|
package/dist/index.js
CHANGED
|
@@ -2197,12 +2197,16 @@ var TopoloClient = class _TopoloClient {
|
|
|
2197
2197
|
if (!serviceKey) {
|
|
2198
2198
|
throw new TopoloSdkError("unknown_service", "service is required");
|
|
2199
2199
|
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2200
|
+
if (serviceKey === "auth") {
|
|
2201
|
+
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
2202
|
+
if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
|
|
2203
|
+
return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS.auth };
|
|
2204
|
+
}
|
|
2204
2205
|
const live = await this.resolveLiveCallableService(serviceKey);
|
|
2205
|
-
if (live)
|
|
2206
|
+
if (live) {
|
|
2207
|
+
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
2208
|
+
return overrideUrl ? { ...live, baseUrl: overrideUrl } : live;
|
|
2209
|
+
}
|
|
2206
2210
|
throw new TopoloSdkError(
|
|
2207
2211
|
"unknown_service",
|
|
2208
2212
|
`Unknown Topolo service "${service}". Run service discovery first and pass a callable service id or slug.`
|
|
@@ -2212,10 +2216,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2212
2216
|
const catalog = await this.listServices();
|
|
2213
2217
|
const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
|
|
2214
2218
|
if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
|
|
2215
|
-
return {
|
|
2216
|
-
serviceKey: normalizeLookupKey(match.slug || match.name || match.serviceId) || serviceKey,
|
|
2217
|
-
baseUrl: match.baseUrl
|
|
2218
|
-
};
|
|
2219
|
+
return { serviceKey, baseUrl: match.baseUrl };
|
|
2219
2220
|
}
|
|
2220
2221
|
/**
|
|
2221
2222
|
* Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
|
|
@@ -2277,53 +2278,55 @@ var TopoloClient = class _TopoloClient {
|
|
|
2277
2278
|
/**
|
|
2278
2279
|
* List the platform service catalog from the live registry
|
|
2279
2280
|
* (`GET /api/services/catalog` on TopoloAuth). Credential-aware: the
|
|
2280
|
-
* caller's credential is attached, so
|
|
2281
|
-
*
|
|
2282
|
-
* permits; an unauthenticated/agent caller receives `active` only.
|
|
2281
|
+
* caller's credential is attached, so the result only includes services the
|
|
2282
|
+
* credential's organization and effective permissions can use.
|
|
2283
2283
|
*
|
|
2284
|
-
*
|
|
2285
|
-
*
|
|
2286
|
-
*
|
|
2287
|
-
* authoritative; the snapshot is a floor, not a cap.
|
|
2284
|
+
* Security boundary: this never falls back to the generated global snapshot.
|
|
2285
|
+
* The live Auth catalog is the authority because it is scoped to the caller's
|
|
2286
|
+
* organization and effective permissions.
|
|
2288
2287
|
*/
|
|
2289
2288
|
async listServices() {
|
|
2290
2289
|
const now = Date.now();
|
|
2291
2290
|
if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
|
|
2292
2291
|
return this.serviceCatalogCache.entries;
|
|
2293
2292
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
return entries;
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
} catch {
|
|
2293
|
+
const res = await this.request({ service: "auth", method: "GET", path: "/api/services/catalog" });
|
|
2294
|
+
const raw = res.data?.services ?? res.services;
|
|
2295
|
+
if (!Array.isArray(raw)) {
|
|
2296
|
+
throw new TopoloSdkError(
|
|
2297
|
+
"service_catalog_unavailable",
|
|
2298
|
+
"TopoloAuth did not return a service catalog for the current credential."
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
const entries = [];
|
|
2302
|
+
for (const item of raw) {
|
|
2303
|
+
const s = item;
|
|
2304
|
+
const serviceId = typeof s.service_id === "string" ? s.service_id : typeof s.id === "string" ? s.id : null;
|
|
2305
|
+
if (!serviceId) continue;
|
|
2306
|
+
const permissions = Array.isArray(s.granted_permissions) ? s.granted_permissions.filter((permission) => typeof permission === "string" && permission.trim().length > 0) : [];
|
|
2307
|
+
entries.push({
|
|
2308
|
+
serviceId,
|
|
2309
|
+
slug: typeof s.app_slug === "string" ? s.app_slug : null,
|
|
2310
|
+
name: typeof s.name === "string" ? s.name : null,
|
|
2311
|
+
description: typeof s.description === "string" ? s.description : null,
|
|
2312
|
+
baseUrl: typeof s.base_url === "string" ? s.base_url : null,
|
|
2313
|
+
status: typeof s.status === "string" ? s.status : null,
|
|
2314
|
+
surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
|
|
2315
|
+
launchable: s.launchable === true,
|
|
2316
|
+
permissions
|
|
2317
|
+
});
|
|
2323
2318
|
}
|
|
2324
|
-
|
|
2319
|
+
this.serviceCatalogCache = {
|
|
2320
|
+
entries,
|
|
2321
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS
|
|
2322
|
+
};
|
|
2323
|
+
return entries;
|
|
2325
2324
|
}
|
|
2326
|
-
|
|
2325
|
+
/**
|
|
2326
|
+
* Bundled generated registry for tooling that audits platform packaging, not
|
|
2327
|
+
* for deciding what the current credential may use.
|
|
2328
|
+
*/
|
|
2329
|
+
listBundledServices() {
|
|
2327
2330
|
const apps = Object.values(APPLICATIONS);
|
|
2328
2331
|
return apps.map((app) => ({
|
|
2329
2332
|
serviceId: app.id,
|
|
@@ -2331,9 +2334,10 @@ var TopoloClient = class _TopoloClient {
|
|
|
2331
2334
|
name: app.name ?? app.id,
|
|
2332
2335
|
description: null,
|
|
2333
2336
|
baseUrl: app.productionUrl ?? null,
|
|
2334
|
-
status: "
|
|
2337
|
+
status: "bundled",
|
|
2335
2338
|
surfaceKind: "application",
|
|
2336
|
-
launchable: Boolean(app.productionUrl)
|
|
2339
|
+
launchable: Boolean(app.productionUrl),
|
|
2340
|
+
permissions: []
|
|
2337
2341
|
}));
|
|
2338
2342
|
}
|
|
2339
2343
|
/**
|
|
@@ -2403,7 +2407,7 @@ function serviceKeyCandidates(slug) {
|
|
|
2403
2407
|
])];
|
|
2404
2408
|
}
|
|
2405
2409
|
function normalizeLookupKey(value) {
|
|
2406
|
-
return String(value || "").trim().toLowerCase();
|
|
2410
|
+
return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
|
|
2407
2411
|
}
|
|
2408
2412
|
function catalogEntryMatches(entry, needle) {
|
|
2409
2413
|
return catalogEntryLookupKeys(entry).includes(needle);
|
|
@@ -2414,12 +2418,8 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2414
2418
|
for (const value of values) {
|
|
2415
2419
|
const normalized = normalizeLookupKey(value);
|
|
2416
2420
|
if (!normalized) continue;
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
if (normalized.startsWith("topolo-")) {
|
|
2420
|
-
const withoutTopolo = normalized.slice("topolo-".length);
|
|
2421
|
-
keys.add(withoutTopolo);
|
|
2422
|
-
keys.add(withoutTopolo.replace(/-/g, "_"));
|
|
2421
|
+
for (const candidate of serviceKeyCandidates(normalized)) {
|
|
2422
|
+
keys.add(candidate);
|
|
2423
2423
|
}
|
|
2424
2424
|
}
|
|
2425
2425
|
return [...keys];
|
|
@@ -2427,7 +2427,9 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2427
2427
|
function isCallableCatalogEntry(entry) {
|
|
2428
2428
|
const kind = normalizeLookupKey(entry.surfaceKind || "");
|
|
2429
2429
|
if (kind === "api" || kind === "technical_service" || kind === "service") return true;
|
|
2430
|
-
return Boolean(
|
|
2430
|
+
return Boolean(
|
|
2431
|
+
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2432
|
+
);
|
|
2431
2433
|
}
|
|
2432
2434
|
function describeError(body, fallback) {
|
|
2433
2435
|
if (body && typeof body === "object") {
|
|
@@ -2612,7 +2614,7 @@ var TOOLS = [
|
|
|
2612
2614
|
{
|
|
2613
2615
|
name: "topolo_list_services",
|
|
2614
2616
|
title: "List every Topolo service the agent can reach",
|
|
2615
|
-
description: "Returns the service registry
|
|
2617
|
+
description: "Returns the credential-scoped service registry: one entry per platform service the current credential can use in its organization, with granted permissions and base URL. Use this to discover what `topolo_api_call` can address. This is not the global platform catalog.",
|
|
2616
2618
|
requiredScopes: [],
|
|
2617
2619
|
destructive: false,
|
|
2618
2620
|
inputSchema: {
|
|
@@ -2620,12 +2622,21 @@ var TOOLS = [
|
|
|
2620
2622
|
properties: {},
|
|
2621
2623
|
additionalProperties: false
|
|
2622
2624
|
},
|
|
2623
|
-
handler: async () => {
|
|
2624
|
-
const services =
|
|
2625
|
-
id
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2625
|
+
handler: async (topolo) => {
|
|
2626
|
+
const services = (await topolo.client.listServices()).map((entry) => {
|
|
2627
|
+
const id = displayServiceId(entry);
|
|
2628
|
+
return {
|
|
2629
|
+
id,
|
|
2630
|
+
serviceId: entry.serviceId,
|
|
2631
|
+
slug: entry.slug,
|
|
2632
|
+
name: entry.name,
|
|
2633
|
+
baseUrl: entry.baseUrl,
|
|
2634
|
+
effectiveUrl: resolveServiceUrl(id) || entry.baseUrl,
|
|
2635
|
+
surfaceKind: entry.surfaceKind,
|
|
2636
|
+
launchable: entry.launchable,
|
|
2637
|
+
permissions: [...entry.permissions]
|
|
2638
|
+
};
|
|
2639
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
2629
2640
|
return { services };
|
|
2630
2641
|
}
|
|
2631
2642
|
},
|
|
@@ -2917,6 +2928,27 @@ function statusesAtOrWorse(threshold) {
|
|
|
2917
2928
|
if (threshold === "needs_review") return ["missing", "needs_review"];
|
|
2918
2929
|
return ["missing", "needs_review", "partial"];
|
|
2919
2930
|
}
|
|
2931
|
+
function displayServiceId(entry) {
|
|
2932
|
+
for (const value of [entry.slug, entry.name, entry.serviceId]) {
|
|
2933
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
2934
|
+
for (const candidate of serviceIdCandidates(value)) {
|
|
2935
|
+
if (Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, candidate)) {
|
|
2936
|
+
return candidate;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
return serviceIdCandidates(entry.slug || entry.serviceId)[0] || entry.serviceId;
|
|
2941
|
+
}
|
|
2942
|
+
function serviceIdCandidates(value) {
|
|
2943
|
+
const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
2944
|
+
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
2945
|
+
return [.../* @__PURE__ */ new Set([
|
|
2946
|
+
normalized,
|
|
2947
|
+
normalized.replace(/-/g, "_"),
|
|
2948
|
+
withoutTopolo,
|
|
2949
|
+
withoutTopolo.replace(/-/g, "_")
|
|
2950
|
+
])].filter(Boolean);
|
|
2951
|
+
}
|
|
2920
2952
|
function formatKnownValues(values, label) {
|
|
2921
2953
|
const sorted = [...values].sort();
|
|
2922
2954
|
const preview = sorted.slice(0, 25).join(", ");
|
package/package.json
CHANGED