@topolo/mcp 0.5.0 → 0.7.0
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 +44 -23
- package/dist/dispatch.js +146 -109
- package/dist/http.js +147 -110
- package/dist/index.js +757 -203
- package/package.json +9 -2
package/dist/index.js
CHANGED
|
@@ -44,6 +44,12 @@ var TopoloPermissionError = class extends TopoloSdkError {
|
|
|
44
44
|
this.required = required;
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
+
var TopoloConfirmationRequiredError = class extends TopoloSdkError {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super("confirmation_required", message);
|
|
50
|
+
this.name = "TopoloConfirmationRequiredError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
47
53
|
var TopoloHttpError = class extends TopoloSdkError {
|
|
48
54
|
status;
|
|
49
55
|
body;
|
|
@@ -58,7 +64,7 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
58
64
|
this.path = path;
|
|
59
65
|
}
|
|
60
66
|
};
|
|
61
|
-
var
|
|
67
|
+
var APP_API_URLS = {
|
|
62
68
|
agent: "https://agent.topolo.app",
|
|
63
69
|
auth: "https://auth.topolo.app",
|
|
64
70
|
backup: "https://backup.topolo.app",
|
|
@@ -1106,6 +1112,7 @@ var APPLICATIONS = {
|
|
|
1106
1112
|
"topolo.io"
|
|
1107
1113
|
],
|
|
1108
1114
|
"routes": [
|
|
1115
|
+
"one.topolo.io",
|
|
1109
1116
|
"topolo.io",
|
|
1110
1117
|
"www.topolo.io"
|
|
1111
1118
|
],
|
|
@@ -1645,12 +1652,12 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
1645
1652
|
{
|
|
1646
1653
|
id: "shared-auth-boundary",
|
|
1647
1654
|
title: "Use the shared Topolo auth boundary",
|
|
1648
|
-
summary: "Applications must derive identity, organization, scopes, and
|
|
1655
|
+
summary: "Applications must derive identity, organization, scopes, and app entitlement from TopoloAuth rather than inventing parallel auth state.",
|
|
1649
1656
|
appliesTo: ["browser", "api", "tooling", "agent_surface"],
|
|
1650
1657
|
evidence: [
|
|
1651
1658
|
"@topolo/auth-client or @topolo-io/worker-runtime is used where applicable",
|
|
1652
1659
|
"No public command, tool, or SDK method accepts orgId",
|
|
1653
|
-
"
|
|
1660
|
+
"App slug and Auth app registration are documented for protected APIs"
|
|
1654
1661
|
],
|
|
1655
1662
|
implementation: [
|
|
1656
1663
|
"Use shared auth/runtime packages instead of copying token parsing, gateway, or browser storage flows",
|
|
@@ -1683,20 +1690,20 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
1683
1690
|
]
|
|
1684
1691
|
},
|
|
1685
1692
|
{
|
|
1686
|
-
id: "
|
|
1687
|
-
title: "Register protected APIs as platform
|
|
1688
|
-
summary: "Callable app APIs need a stable
|
|
1693
|
+
id: "app-registration-and-scopes",
|
|
1694
|
+
title: "Register protected APIs as platform apps",
|
|
1695
|
+
summary: "Callable app APIs need a stable app ID, app slug, permissions, role bundles, and API-key scopes before agents or users rely on them.",
|
|
1689
1696
|
appliesTo: ["api"],
|
|
1690
1697
|
evidence: [
|
|
1691
|
-
"Auth
|
|
1692
|
-
"Permissions, role bundles, and API-key scopes exist for the
|
|
1693
|
-
"SDK, CLI, and MCP
|
|
1694
|
-
"CloudControl production HTTP targets classify agent reachability with
|
|
1698
|
+
"Auth app catalog contains the app ID and slug",
|
|
1699
|
+
"Permissions, role bundles, and API-key scopes exist for the app",
|
|
1700
|
+
"SDK, CLI, and MCP app discovery include the callable production API",
|
|
1701
|
+
"CloudControl production HTTP targets classify agent reachability with sdk_app_id or agent_callable:false"
|
|
1695
1702
|
],
|
|
1696
1703
|
implementation: [
|
|
1697
|
-
"Add production Worker api_url metadata and
|
|
1704
|
+
"Add production Worker api_url metadata and sdk_app_id for every agent-callable API target",
|
|
1698
1705
|
"Mark browser, documentation, and runtime-only Worker targets with agent_callable:false",
|
|
1699
|
-
"Expose stable
|
|
1706
|
+
"Expose stable app IDs through generated DEFAULT_APP_URLS and live catalog discovery",
|
|
1700
1707
|
"Keep typed SDK/CLI/MCP surfaces in sync when an API contract stabilizes"
|
|
1701
1708
|
]
|
|
1702
1709
|
},
|
|
@@ -1902,7 +1909,7 @@ function evaluateRequirement(application, requirement) {
|
|
|
1902
1909
|
"Catalog metadata cannot verify shared shell, launcher, and account menu adoption.",
|
|
1903
1910
|
evidence
|
|
1904
1911
|
);
|
|
1905
|
-
case "
|
|
1912
|
+
case "app-registration-and-scopes": {
|
|
1906
1913
|
if (application.services.length > 0) {
|
|
1907
1914
|
evidence.push(`services=${application.services.join(",")}`);
|
|
1908
1915
|
return finding(
|
|
@@ -1912,7 +1919,7 @@ function evaluateRequirement(application, requirement) {
|
|
|
1912
1919
|
evidence
|
|
1913
1920
|
);
|
|
1914
1921
|
}
|
|
1915
|
-
return finding(requirement, "missing", "No SDK
|
|
1922
|
+
return finding(requirement, "missing", "No SDK app API id is registered for this callable API surface.", evidence);
|
|
1916
1923
|
}
|
|
1917
1924
|
case "organization-scoped-data":
|
|
1918
1925
|
return finding(
|
|
@@ -1997,7 +2004,7 @@ function statusRank(status) {
|
|
|
1997
2004
|
if (status === "partial") return 1;
|
|
1998
2005
|
return 2;
|
|
1999
2006
|
}
|
|
2000
|
-
var
|
|
2007
|
+
var DEFAULT_APP_URLS = APP_API_URLS;
|
|
2001
2008
|
function resolveCatalogServiceUrl(entry, options = {}) {
|
|
2002
2009
|
const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
|
|
2003
2010
|
const override = resolveServiceUrlOverride(keys, options.overrides);
|
|
@@ -2009,7 +2016,7 @@ function resolveCatalogServiceUrl(entry, options = {}) {
|
|
|
2009
2016
|
function resolveServiceUrl(service, overrides) {
|
|
2010
2017
|
const override = resolveServiceUrlOverride([service], overrides);
|
|
2011
2018
|
if (override) return override;
|
|
2012
|
-
return
|
|
2019
|
+
return DEFAULT_APP_URLS[service];
|
|
2013
2020
|
}
|
|
2014
2021
|
function catalogServiceKeyCandidates(entry, preferred) {
|
|
2015
2022
|
const values = [preferred, entry.slug, entry.name, entry.appId].filter(
|
|
@@ -2040,7 +2047,7 @@ function resolveServiceUrlOverride(keys, overrides) {
|
|
|
2040
2047
|
if (envStyle) return envStyle;
|
|
2041
2048
|
}
|
|
2042
2049
|
for (const key of keys) {
|
|
2043
|
-
const envKey = `
|
|
2050
|
+
const envKey = `TOPOLO_APP_URL_${key.replace(/\./g, "_").toUpperCase()}`;
|
|
2044
2051
|
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2045
2052
|
if (envValue) return envValue;
|
|
2046
2053
|
}
|
|
@@ -2059,7 +2066,6 @@ var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
|
|
|
2059
2066
|
var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
|
|
2060
2067
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2061
2068
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
2062
|
-
var CATALOG_CACHE_TTL_MS = 60 * 1e3;
|
|
2063
2069
|
var TopoloClient = class _TopoloClient {
|
|
2064
2070
|
credential;
|
|
2065
2071
|
agent;
|
|
@@ -2068,9 +2074,9 @@ var TopoloClient = class _TopoloClient {
|
|
|
2068
2074
|
requireConfirmForWrites;
|
|
2069
2075
|
timeoutMs;
|
|
2070
2076
|
fetchImpl;
|
|
2077
|
+
refreshCredential;
|
|
2078
|
+
refreshInFlight = null;
|
|
2071
2079
|
debug;
|
|
2072
|
-
serviceCatalogCache = null;
|
|
2073
|
-
actionCatalogCache = null;
|
|
2074
2080
|
/**
|
|
2075
2081
|
* Process-wide cache for slug → app-id lookups. Keyed by
|
|
2076
2082
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2095,6 +2101,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2095
2101
|
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
2096
2102
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
2097
2103
|
this.fetchImpl = options.fetch ?? fetch;
|
|
2104
|
+
this.refreshCredential = options.refreshCredential;
|
|
2098
2105
|
this.debug = options.debug;
|
|
2099
2106
|
}
|
|
2100
2107
|
emit(event) {
|
|
@@ -2112,11 +2119,11 @@ var TopoloClient = class _TopoloClient {
|
|
|
2112
2119
|
async request(opts) {
|
|
2113
2120
|
const method = opts.method ?? "GET";
|
|
2114
2121
|
if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
|
|
2115
|
-
throw new
|
|
2122
|
+
throw new TopoloConfirmationRequiredError(
|
|
2116
2123
|
`Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
|
|
2117
2124
|
);
|
|
2118
2125
|
}
|
|
2119
|
-
const resolvedService = await this.resolveRequestService(opts.service);
|
|
2126
|
+
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
|
|
2120
2127
|
const baseUrl = resolvedService.baseUrl;
|
|
2121
2128
|
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
2122
2129
|
if (opts.query) {
|
|
@@ -2147,20 +2154,36 @@ var TopoloClient = class _TopoloClient {
|
|
|
2147
2154
|
headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
|
|
2148
2155
|
headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
|
|
2149
2156
|
}
|
|
2150
|
-
const controller = new AbortController();
|
|
2151
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2152
|
-
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
2153
2157
|
const urlStr = url.toString();
|
|
2154
2158
|
const startedAt = Date.now();
|
|
2155
2159
|
this.emit({ phase: "request", method, service: resolvedService.serviceKey, path: opts.path, url: urlStr, requestId });
|
|
2160
|
+
const fetchOnce = async () => {
|
|
2161
|
+
const controller = new AbortController();
|
|
2162
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2163
|
+
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
2164
|
+
try {
|
|
2165
|
+
return await this.fetchImpl(urlStr, {
|
|
2166
|
+
method,
|
|
2167
|
+
headers: new Headers(headers),
|
|
2168
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
|
|
2169
|
+
signal
|
|
2170
|
+
});
|
|
2171
|
+
} finally {
|
|
2172
|
+
clearTimeout(timeoutId);
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2156
2175
|
let res;
|
|
2157
2176
|
try {
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2177
|
+
const attemptedCredential = this.credential;
|
|
2178
|
+
res = await fetchOnce();
|
|
2179
|
+
if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
|
|
2180
|
+
await res.body?.cancel().catch(() => void 0);
|
|
2181
|
+
const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
|
|
2182
|
+
if (refreshed) {
|
|
2183
|
+
applyAuthHeaders(headers, refreshed);
|
|
2184
|
+
res = await fetchOnce();
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2164
2187
|
} catch (err) {
|
|
2165
2188
|
this.emit({
|
|
2166
2189
|
phase: "error",
|
|
@@ -2173,8 +2196,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
2173
2196
|
error: err instanceof Error ? err.message : String(err)
|
|
2174
2197
|
});
|
|
2175
2198
|
throw err;
|
|
2176
|
-
} finally {
|
|
2177
|
-
clearTimeout(timeoutId);
|
|
2178
2199
|
}
|
|
2179
2200
|
const contentType = res.headers.get("Content-Type") ?? "";
|
|
2180
2201
|
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
@@ -2213,6 +2234,19 @@ var TopoloClient = class _TopoloClient {
|
|
|
2213
2234
|
});
|
|
2214
2235
|
return parsed;
|
|
2215
2236
|
}
|
|
2237
|
+
async refreshRejectedAccessToken(attemptedAccessToken) {
|
|
2238
|
+
if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
|
|
2239
|
+
return this.credential;
|
|
2240
|
+
}
|
|
2241
|
+
if (!this.refreshCredential) return null;
|
|
2242
|
+
this.refreshInFlight ??= this.refreshCredential().then((credential) => {
|
|
2243
|
+
if (credential) this.credential = credential;
|
|
2244
|
+
return credential;
|
|
2245
|
+
}).catch(() => null).finally(() => {
|
|
2246
|
+
this.refreshInFlight = null;
|
|
2247
|
+
});
|
|
2248
|
+
return this.refreshInFlight;
|
|
2249
|
+
}
|
|
2216
2250
|
async resolveRequestService(service) {
|
|
2217
2251
|
const serviceKey = normalizeLookupKey(service);
|
|
2218
2252
|
if (!serviceKey) {
|
|
@@ -2225,7 +2259,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2225
2259
|
const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
|
|
2226
2260
|
if (derived) return { serviceKey, baseUrl: derived };
|
|
2227
2261
|
}
|
|
2228
|
-
return { serviceKey, baseUrl:
|
|
2262
|
+
return { serviceKey, baseUrl: DEFAULT_APP_URLS[serviceKey] };
|
|
2229
2263
|
}
|
|
2230
2264
|
const live = await this.resolveLiveCallableService(serviceKey);
|
|
2231
2265
|
if (live) {
|
|
@@ -2234,13 +2268,12 @@ var TopoloClient = class _TopoloClient {
|
|
|
2234
2268
|
}
|
|
2235
2269
|
throw new TopoloSdkError(
|
|
2236
2270
|
"unknown_service",
|
|
2237
|
-
`Unknown Topolo
|
|
2271
|
+
`Unknown Topolo app API "${service}". Run app discovery first and pass a callable app id or slug.`
|
|
2238
2272
|
);
|
|
2239
2273
|
}
|
|
2240
2274
|
async resolveLiveCallableService(serviceKey) {
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
if (!match) return null;
|
|
2275
|
+
const match = await this.getService(serviceKey).catch(() => null);
|
|
2276
|
+
if (!match || !catalogEntryMatches(match, serviceKey)) return null;
|
|
2244
2277
|
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
2245
2278
|
serviceKey,
|
|
2246
2279
|
overrides: this.serviceUrls
|
|
@@ -2314,12 +2347,13 @@ var TopoloClient = class _TopoloClient {
|
|
|
2314
2347
|
* The live Auth catalog is the authority because it is scoped to the caller's
|
|
2315
2348
|
* organization and effective permissions.
|
|
2316
2349
|
*/
|
|
2317
|
-
async
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2350
|
+
async searchServices(options = {}) {
|
|
2351
|
+
const res = await this.request({
|
|
2352
|
+
service: "developers",
|
|
2353
|
+
method: "GET",
|
|
2354
|
+
path: "/api/services/catalog",
|
|
2355
|
+
query: catalogSearchQuery(options)
|
|
2356
|
+
});
|
|
2323
2357
|
const raw = res.data?.services ?? res.services;
|
|
2324
2358
|
if (!Array.isArray(raw)) {
|
|
2325
2359
|
throw new TopoloSdkError(
|
|
@@ -2329,6 +2363,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2329
2363
|
}
|
|
2330
2364
|
const entries = [];
|
|
2331
2365
|
for (const item of raw) {
|
|
2366
|
+
if (!isRecord(item)) continue;
|
|
2332
2367
|
const s = item;
|
|
2333
2368
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
2334
2369
|
if (!appId) continue;
|
|
@@ -2353,11 +2388,30 @@ var TopoloClient = class _TopoloClient {
|
|
|
2353
2388
|
apiKeyResources
|
|
2354
2389
|
});
|
|
2355
2390
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
};
|
|
2360
|
-
|
|
2391
|
+
const page = normalizeCatalogPage(
|
|
2392
|
+
res.data?.page ?? res.page
|
|
2393
|
+
);
|
|
2394
|
+
return { services: entries, page };
|
|
2395
|
+
}
|
|
2396
|
+
/** @deprecated Prefer searchServices() so callers retain the continuation cursor. */
|
|
2397
|
+
async listServices(options = {}) {
|
|
2398
|
+
return (await this.searchServices(options)).services;
|
|
2399
|
+
}
|
|
2400
|
+
async getService(application) {
|
|
2401
|
+
const needle = application.trim();
|
|
2402
|
+
if (!needle) throw new TopoloSdkError("unknown_service", "application is required");
|
|
2403
|
+
const res = await this.request({
|
|
2404
|
+
service: "developers",
|
|
2405
|
+
method: "GET",
|
|
2406
|
+
path: "/api/services/catalog",
|
|
2407
|
+
query: { application: needle }
|
|
2408
|
+
});
|
|
2409
|
+
const raw = res.data?.application ?? res.application;
|
|
2410
|
+
const entry = normalizeServiceCatalogEntry(raw);
|
|
2411
|
+
if (!entry) {
|
|
2412
|
+
throw new TopoloSdkError("unknown_service", `Unknown Topolo application "${application}".`);
|
|
2413
|
+
}
|
|
2414
|
+
return entry;
|
|
2361
2415
|
}
|
|
2362
2416
|
/**
|
|
2363
2417
|
* List callable actions from the live registry-canonical catalog (definitions
|
|
@@ -2365,16 +2419,15 @@ var TopoloClient = class _TopoloClient {
|
|
|
2365
2419
|
* caller's organization and effective permissions; no bundled/static app
|
|
2366
2420
|
* action list is used as an authorization source.
|
|
2367
2421
|
*/
|
|
2368
|
-
async
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
return this.actionCatalogCache.entries;
|
|
2422
|
+
async searchActions(options) {
|
|
2423
|
+
if (!options.service?.trim()) {
|
|
2424
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2372
2425
|
}
|
|
2373
2426
|
const res = await this.request({
|
|
2374
2427
|
service: "developers",
|
|
2375
2428
|
method: "GET",
|
|
2376
2429
|
path: "/api/actions/catalog",
|
|
2377
|
-
|
|
2430
|
+
query: { service: options.service, ...catalogSearchQuery(options) }
|
|
2378
2431
|
});
|
|
2379
2432
|
const raw = res.data?.actions ?? res.actions;
|
|
2380
2433
|
if (!Array.isArray(raw)) {
|
|
@@ -2384,36 +2437,96 @@ var TopoloClient = class _TopoloClient {
|
|
|
2384
2437
|
);
|
|
2385
2438
|
}
|
|
2386
2439
|
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2440
|
+
return {
|
|
2441
|
+
actions: entries,
|
|
2442
|
+
page: normalizeCatalogPage(
|
|
2443
|
+
res.data?.page ?? res.page
|
|
2444
|
+
)
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
async getActionCatalogSummary(service) {
|
|
2448
|
+
const boundedService = service.trim();
|
|
2449
|
+
if (!boundedService) {
|
|
2450
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2451
|
+
}
|
|
2452
|
+
const res = await this.request({
|
|
2453
|
+
service: "developers",
|
|
2454
|
+
method: "GET",
|
|
2455
|
+
path: "/api/actions/catalog",
|
|
2456
|
+
query: { service: boundedService, summary: "true" }
|
|
2457
|
+
});
|
|
2458
|
+
const raw = res.data?.summary ?? res.summary;
|
|
2459
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2460
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
|
|
2461
|
+
}
|
|
2462
|
+
const record = raw;
|
|
2463
|
+
if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
|
|
2464
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
|
|
2392
2465
|
}
|
|
2393
|
-
return
|
|
2466
|
+
return {
|
|
2467
|
+
algorithm: "sha256-v1",
|
|
2468
|
+
digest: record.digest,
|
|
2469
|
+
actionCount: Number(record.actionCount)
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
/** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
|
|
2473
|
+
async listActions(options) {
|
|
2474
|
+
if (!options) {
|
|
2475
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2476
|
+
}
|
|
2477
|
+
return (await this.searchActions(options)).actions;
|
|
2394
2478
|
}
|
|
2395
2479
|
async getAction(action) {
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2399
|
-
if (!match) {
|
|
2480
|
+
const actionId = action.trim();
|
|
2481
|
+
if (!actionId) {
|
|
2400
2482
|
throw new TopoloSdkError(
|
|
2401
2483
|
"unknown_action",
|
|
2402
|
-
|
|
2484
|
+
"action id is required"
|
|
2403
2485
|
);
|
|
2404
2486
|
}
|
|
2487
|
+
let res;
|
|
2488
|
+
try {
|
|
2489
|
+
res = await this.request({
|
|
2490
|
+
service: "developers",
|
|
2491
|
+
method: "GET",
|
|
2492
|
+
path: "/api/actions/catalog",
|
|
2493
|
+
query: { actionId }
|
|
2494
|
+
});
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
if (error instanceof TopoloHttpError && error.status === 404) {
|
|
2497
|
+
throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2498
|
+
}
|
|
2499
|
+
throw error;
|
|
2500
|
+
}
|
|
2501
|
+
const raw = res.data?.action ?? res.action;
|
|
2502
|
+
const match = normalizeActionCatalogEntry(raw);
|
|
2503
|
+
if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2405
2504
|
return match;
|
|
2406
2505
|
}
|
|
2407
2506
|
async callAction(action, input = {}, options = {}) {
|
|
2408
2507
|
const entry = await this.getAction(action);
|
|
2508
|
+
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2509
|
+
if (agentAccess === "off") {
|
|
2510
|
+
throw new TopoloPermissionError(
|
|
2511
|
+
`Topolo action "${entry.actionId}" is not available for agent invocation.`,
|
|
2512
|
+
entry.requiredPermission ? [entry.requiredPermission] : []
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
|
|
2516
|
+
throw new TopoloConfirmationRequiredError(
|
|
2517
|
+
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2409
2520
|
const mapped = mapActionInput(entry, input);
|
|
2410
|
-
const service = entry.
|
|
2521
|
+
const service = entry.appSlug || entry.appId;
|
|
2411
2522
|
return this.request({
|
|
2412
2523
|
service,
|
|
2524
|
+
...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
|
|
2413
2525
|
method: entry.method,
|
|
2414
2526
|
path: mapped.path,
|
|
2415
2527
|
...mapped.query ? { query: mapped.query } : {},
|
|
2416
2528
|
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2529
|
+
...options.resource ? { resource: options.resource } : {},
|
|
2417
2530
|
headers: {
|
|
2418
2531
|
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2419
2532
|
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
@@ -2551,6 +2664,18 @@ function serviceKeyCandidates2(slug) {
|
|
|
2551
2664
|
function normalizeLookupKey(value) {
|
|
2552
2665
|
return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
|
|
2553
2666
|
}
|
|
2667
|
+
function requireHttpOrigin(value) {
|
|
2668
|
+
let url;
|
|
2669
|
+
try {
|
|
2670
|
+
url = new URL(value);
|
|
2671
|
+
} catch {
|
|
2672
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2673
|
+
}
|
|
2674
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
|
|
2675
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2676
|
+
}
|
|
2677
|
+
return url.origin;
|
|
2678
|
+
}
|
|
2554
2679
|
function deriveDevelopersFromAuth(authUrl) {
|
|
2555
2680
|
if (!authUrl) return null;
|
|
2556
2681
|
try {
|
|
@@ -2577,6 +2702,47 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2577
2702
|
}
|
|
2578
2703
|
return [...keys];
|
|
2579
2704
|
}
|
|
2705
|
+
function catalogSearchQuery(options) {
|
|
2706
|
+
const query = {};
|
|
2707
|
+
if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
|
|
2708
|
+
if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
|
|
2709
|
+
if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
|
|
2710
|
+
return query;
|
|
2711
|
+
}
|
|
2712
|
+
function normalizeCatalogPage(value) {
|
|
2713
|
+
if (!isRecord(value)) return { nextCursor: null, hasMore: false };
|
|
2714
|
+
return {
|
|
2715
|
+
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2716
|
+
hasMore: value.hasMore === true
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
function normalizeServiceCatalogEntry(item) {
|
|
2720
|
+
if (!isRecord(item)) return null;
|
|
2721
|
+
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2722
|
+
if (!appId) return null;
|
|
2723
|
+
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
2724
|
+
(permission) => typeof permission === "string" && permission.trim().length > 0
|
|
2725
|
+
) : [];
|
|
2726
|
+
const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
|
|
2727
|
+
item.api_key_resources ?? item.apiKeyResources
|
|
2728
|
+
);
|
|
2729
|
+
const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
|
|
2730
|
+
const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
|
|
2731
|
+
return {
|
|
2732
|
+
appId,
|
|
2733
|
+
slug: typeof item.app_slug === "string" ? item.app_slug : null,
|
|
2734
|
+
name: typeof item.name === "string" ? item.name : null,
|
|
2735
|
+
description: typeof item.description === "string" ? item.description : null,
|
|
2736
|
+
apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
|
|
2737
|
+
launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
|
|
2738
|
+
status: typeof item.status === "string" ? item.status : null,
|
|
2739
|
+
surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
|
|
2740
|
+
launchable: item.launchable === true,
|
|
2741
|
+
permissions,
|
|
2742
|
+
apiKeyResource,
|
|
2743
|
+
apiKeyResources
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2580
2746
|
function normalizeActionCatalogEntry(item) {
|
|
2581
2747
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2582
2748
|
const a = item;
|
|
@@ -2590,17 +2756,20 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2590
2756
|
}
|
|
2591
2757
|
return {
|
|
2592
2758
|
actionId,
|
|
2759
|
+
target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
|
|
2760
|
+
apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
|
|
2593
2761
|
name,
|
|
2594
2762
|
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2595
2763
|
title: pickActionString(a.title) ?? name,
|
|
2596
2764
|
description: pickActionString(a.description) ?? "",
|
|
2597
2765
|
appId,
|
|
2598
|
-
|
|
2599
|
-
|
|
2766
|
+
appSlug: pickActionString(a.app_slug) ?? pickActionString(a.appSlug) ?? null,
|
|
2767
|
+
appName: pickActionString(a.app_name) ?? pickActionString(a.appName) ?? null,
|
|
2600
2768
|
method,
|
|
2601
2769
|
path,
|
|
2602
2770
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2603
2771
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2772
|
+
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
2604
2773
|
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2605
2774
|
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2606
2775
|
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
@@ -2620,16 +2789,12 @@ function pickActionString(value) {
|
|
|
2620
2789
|
function pickAgentAccess(value) {
|
|
2621
2790
|
return value === "auto" || value === "confirm" || value === "off" ? value : null;
|
|
2622
2791
|
}
|
|
2792
|
+
function pickAuthorizationMode(value) {
|
|
2793
|
+
return value === "runtime" ? "runtime" : "static";
|
|
2794
|
+
}
|
|
2623
2795
|
function plainObject(value) {
|
|
2624
2796
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2625
2797
|
}
|
|
2626
|
-
function normalizeActionLookupKey(value) {
|
|
2627
|
-
return String(value || "").trim().toLowerCase();
|
|
2628
|
-
}
|
|
2629
|
-
function actionEntryMatches(entry, needle) {
|
|
2630
|
-
if (!needle) return false;
|
|
2631
|
-
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2632
|
-
}
|
|
2633
2798
|
function mapActionInput(action, input) {
|
|
2634
2799
|
const remaining = { ...input || {} };
|
|
2635
2800
|
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
@@ -2711,6 +2876,7 @@ var CrmModule = class {
|
|
|
2711
2876
|
constructor(client) {
|
|
2712
2877
|
this.client = client;
|
|
2713
2878
|
}
|
|
2879
|
+
client;
|
|
2714
2880
|
async listContacts(options = {}) {
|
|
2715
2881
|
const res = await this.client.request({
|
|
2716
2882
|
service: "crm",
|
|
@@ -2756,6 +2922,7 @@ var IdentityModule = class {
|
|
|
2756
2922
|
constructor(client) {
|
|
2757
2923
|
this.client = client;
|
|
2758
2924
|
}
|
|
2925
|
+
client;
|
|
2759
2926
|
/** Returns the resolved user, organization, and permission set for the
|
|
2760
2927
|
* current credential. Equivalent to `TopoloClient.introspect()` — exposed
|
|
2761
2928
|
* on this module for symmetry with the other domain modules. */
|
|
@@ -2772,22 +2939,364 @@ function createTopolo(options) {
|
|
|
2772
2939
|
};
|
|
2773
2940
|
}
|
|
2774
2941
|
|
|
2775
|
-
//
|
|
2776
|
-
|
|
2777
|
-
|
|
2942
|
+
// ../topolo-sdk/dist/node/credential-store.js
|
|
2943
|
+
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
2944
|
+
import { existsSync } from "fs";
|
|
2945
|
+
import { homedir, platform } from "os";
|
|
2946
|
+
import { dirname, join } from "path";
|
|
2947
|
+
var TopoloSdkError2 = class extends Error {
|
|
2948
|
+
code;
|
|
2949
|
+
constructor(code, message) {
|
|
2950
|
+
super(message);
|
|
2951
|
+
this.name = "TopoloSdkError";
|
|
2952
|
+
this.code = code;
|
|
2953
|
+
}
|
|
2954
|
+
};
|
|
2955
|
+
var TopoloAuthError2 = class extends TopoloSdkError2 {
|
|
2956
|
+
constructor(message, code = "auth_error") {
|
|
2957
|
+
super(code, message);
|
|
2958
|
+
this.name = "TopoloAuthError";
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
2962
|
+
status;
|
|
2963
|
+
body;
|
|
2964
|
+
service;
|
|
2965
|
+
path;
|
|
2966
|
+
constructor(service, path, status, body, message) {
|
|
2967
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path}`);
|
|
2968
|
+
this.name = "TopoloHttpError";
|
|
2969
|
+
this.status = status;
|
|
2970
|
+
this.body = body;
|
|
2971
|
+
this.service = service;
|
|
2972
|
+
this.path = path;
|
|
2973
|
+
}
|
|
2974
|
+
};
|
|
2975
|
+
var APP_API_URLS2 = {
|
|
2976
|
+
agent: "https://agent.topolo.app",
|
|
2977
|
+
auth: "https://auth.topolo.app",
|
|
2978
|
+
backup: "https://backup.topolo.app",
|
|
2979
|
+
books: "https://books.topolo.app",
|
|
2980
|
+
bugfix: "https://bugfix.topolo.app",
|
|
2981
|
+
bytes: "https://bytes.topolo.app",
|
|
2982
|
+
calendar: "https://calendar.topolo.app",
|
|
2983
|
+
capacity: "https://capacity.topolo.app",
|
|
2984
|
+
chat: "https://chat.topolo.app",
|
|
2985
|
+
commerce: "https://commerce.topolo.app",
|
|
2986
|
+
compose: "https://compose.topolo.app",
|
|
2987
|
+
consent: "https://consent.topolo.app",
|
|
2988
|
+
crm: "https://crm.topolo.app",
|
|
2989
|
+
developers: "https://developers.topolo.app",
|
|
2990
|
+
director: "https://director.topolo.app",
|
|
2991
|
+
feed: "https://feed.topolo.app",
|
|
2992
|
+
flow: "https://flow.topolo.app",
|
|
2993
|
+
forecast: "https://forecast.topolo.app",
|
|
2994
|
+
forms: "https://forms.topolo.app",
|
|
2995
|
+
home: "https://home.topolo.app",
|
|
2996
|
+
insights: "https://insights.topolo.app",
|
|
2997
|
+
inventory: "https://inventory.topolo.app",
|
|
2998
|
+
learn: "https://learn.topolo.app",
|
|
2999
|
+
localize: "https://localize.topolo.app",
|
|
3000
|
+
mail: "https://mail.topolo.app",
|
|
3001
|
+
mdm: "https://mdm.topolo.app",
|
|
3002
|
+
messages: "https://messages.topolo.app",
|
|
3003
|
+
nexus: "https://nexus.topolo.app",
|
|
3004
|
+
notify: "https://notify.topolo.app",
|
|
3005
|
+
observability_watch: "https://observability-watch.topolo.app",
|
|
3006
|
+
one: "https://www.topolo.app",
|
|
3007
|
+
p2p: "https://p2p.topolo.app",
|
|
3008
|
+
pay: "https://pay.topolo.app",
|
|
3009
|
+
people: "https://people.topolo.app",
|
|
3010
|
+
"quro.api": "https://api.ol0.me",
|
|
3011
|
+
"quro.redirect": "https://ol0.me",
|
|
3012
|
+
roadmapper: "https://roadmapper.topolo.app",
|
|
3013
|
+
sign: "https://sign.topolo.app",
|
|
3014
|
+
social_studio: "https://studio.topolo.app",
|
|
3015
|
+
socialize: "https://socialize.topolo.app",
|
|
3016
|
+
spaces: "https://spaces.topolo.app",
|
|
3017
|
+
status: "https://status.topolo.app",
|
|
3018
|
+
success: "https://success.topolo.app",
|
|
3019
|
+
support: "https://support.topolo.app",
|
|
3020
|
+
survey: "https://survey.topolo.app",
|
|
3021
|
+
voice: "https://voice.topolo.app",
|
|
3022
|
+
web: "https://web.topolo.app"
|
|
3023
|
+
};
|
|
3024
|
+
var DEFAULT_APP_URLS2 = APP_API_URLS2;
|
|
3025
|
+
function resolveServiceUrl2(service, overrides) {
|
|
3026
|
+
const override = resolveServiceUrlOverride2([service], overrides);
|
|
3027
|
+
if (override) return override;
|
|
3028
|
+
return DEFAULT_APP_URLS2[service];
|
|
3029
|
+
}
|
|
3030
|
+
function resolveServiceUrlOverride2(keys, overrides) {
|
|
3031
|
+
for (const key of keys) {
|
|
3032
|
+
const direct = overrides?.[key];
|
|
3033
|
+
if (direct) return direct;
|
|
3034
|
+
const envStyleKey = key.replace(/\./g, "_");
|
|
3035
|
+
const envStyle = overrides?.[envStyleKey];
|
|
3036
|
+
if (envStyle) return envStyle;
|
|
3037
|
+
}
|
|
3038
|
+
for (const key of keys) {
|
|
3039
|
+
const envKey = `TOPOLO_APP_URL_${key.replace(/\./g, "_").toUpperCase()}`;
|
|
3040
|
+
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
3041
|
+
if (envValue) return envValue;
|
|
3042
|
+
}
|
|
3043
|
+
return null;
|
|
3044
|
+
}
|
|
3045
|
+
var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
3046
|
+
var TopoloOAuth = class {
|
|
3047
|
+
baseUrl;
|
|
3048
|
+
fetchImpl;
|
|
3049
|
+
constructor(options = {}) {
|
|
3050
|
+
this.baseUrl = resolveServiceUrl2("auth", options.serviceUrls);
|
|
3051
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* RFC 8628 device authorization grant — starts the flow by asking the server
|
|
3055
|
+
* for a device_code + user_code. The caller prints the user_code +
|
|
3056
|
+
* verification_uri and then polls `pollDeviceToken` until approval.
|
|
3057
|
+
*/
|
|
3058
|
+
async requestDeviceCode(params) {
|
|
3059
|
+
const body = new URLSearchParams();
|
|
3060
|
+
body.set("client_id", params.clientId);
|
|
3061
|
+
if (params.scope) {
|
|
3062
|
+
body.set("scope", Array.isArray(params.scope) ? params.scope.join(" ") : params.scope);
|
|
3063
|
+
}
|
|
3064
|
+
return this.#postForm("/api/developer-oauth/device_authorization", body);
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* Polls the token endpoint once. Returns the token pair on success, or
|
|
3068
|
+
* throws a `TopoloAuthError` whose `code` is one of the RFC 8628 poll
|
|
3069
|
+
* states: `authorization_pending`, `slow_down`, `access_denied`,
|
|
3070
|
+
* `expired_token`. Callers should back off on `slow_down`, wait at least
|
|
3071
|
+
* one `interval` on `authorization_pending`, and give up on the rest.
|
|
3072
|
+
*/
|
|
3073
|
+
async pollDeviceToken(params) {
|
|
3074
|
+
const body = new URLSearchParams();
|
|
3075
|
+
body.set("grant_type", DEVICE_GRANT);
|
|
3076
|
+
body.set("client_id", params.clientId);
|
|
3077
|
+
body.set("device_code", params.deviceCode);
|
|
3078
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3079
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
3080
|
+
}
|
|
3081
|
+
/** Exchange an authorization code (with PKCE) for tokens. */
|
|
3082
|
+
async exchangeAuthorizationCode(params) {
|
|
3083
|
+
const body = new URLSearchParams();
|
|
3084
|
+
body.set("grant_type", "authorization_code");
|
|
3085
|
+
body.set("client_id", params.clientId);
|
|
3086
|
+
body.set("code", params.code);
|
|
3087
|
+
body.set("redirect_uri", params.redirectUri);
|
|
3088
|
+
if (params.codeVerifier) body.set("code_verifier", params.codeVerifier);
|
|
3089
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3090
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
3091
|
+
}
|
|
3092
|
+
/** Rotate a refresh token for a new token pair. */
|
|
3093
|
+
async refreshToken(params) {
|
|
3094
|
+
const body = new URLSearchParams();
|
|
3095
|
+
body.set("grant_type", "refresh_token");
|
|
3096
|
+
body.set("client_id", params.clientId);
|
|
3097
|
+
body.set("refresh_token", params.refreshToken);
|
|
3098
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3099
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
3100
|
+
}
|
|
3101
|
+
async #postForm(path, body) {
|
|
3102
|
+
const url = new URL(path, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
3103
|
+
const res = await this.fetchImpl(url.toString(), {
|
|
3104
|
+
method: "POST",
|
|
3105
|
+
headers: {
|
|
3106
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3107
|
+
Accept: "application/json"
|
|
3108
|
+
},
|
|
3109
|
+
body: body.toString()
|
|
3110
|
+
});
|
|
3111
|
+
const contentType = res.headers.get("Content-Type") ?? "";
|
|
3112
|
+
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
3113
|
+
if (!res.ok) {
|
|
3114
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
3115
|
+
const err = parsed;
|
|
3116
|
+
throw new TopoloAuthError2(err.error_description || err.error, err.error);
|
|
3117
|
+
}
|
|
3118
|
+
throw new TopoloHttpError2("auth", path, res.status, parsed);
|
|
3119
|
+
}
|
|
3120
|
+
return parsed;
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
|
|
3124
|
+
production: {
|
|
3125
|
+
developers: "https://developers.topolo.app"
|
|
3126
|
+
},
|
|
3127
|
+
staging: {
|
|
3128
|
+
auth: "https://auth.stg.topolo.us",
|
|
3129
|
+
developers: "https://developers.stg.topolo.us",
|
|
3130
|
+
seed: "https://seed.stg.topolo.us"
|
|
3131
|
+
},
|
|
3132
|
+
development: {
|
|
3133
|
+
auth: "https://auth.topolo.dev",
|
|
3134
|
+
developers: "https://developers.topolo.dev"
|
|
3135
|
+
},
|
|
3136
|
+
local: {}
|
|
3137
|
+
};
|
|
3138
|
+
function configPath() {
|
|
3139
|
+
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
3140
|
+
if (override) return override;
|
|
3141
|
+
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
|
|
3142
|
+
return join(base, "topolo", "config.json");
|
|
3143
|
+
}
|
|
3144
|
+
async function readConfig() {
|
|
3145
|
+
const path = configPath();
|
|
3146
|
+
if (!existsSync(path)) return {};
|
|
3147
|
+
try {
|
|
3148
|
+
const raw = await readFile(path, "utf8");
|
|
3149
|
+
return JSON.parse(raw);
|
|
3150
|
+
} catch {
|
|
3151
|
+
return {};
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
async function writeConfig(config) {
|
|
3155
|
+
const path = configPath();
|
|
3156
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3157
|
+
await writeFile(path, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3158
|
+
if (platform() !== "win32") {
|
|
3159
|
+
try {
|
|
3160
|
+
await chmod(path, 384);
|
|
3161
|
+
} catch {
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
function normalizeEnvironment(value) {
|
|
3166
|
+
const normalized = (value ?? "production").trim().toLowerCase();
|
|
3167
|
+
if (normalized === "" || normalized === "production" || normalized === "prod") {
|
|
3168
|
+
return "production";
|
|
3169
|
+
}
|
|
3170
|
+
if (normalized === "staging" || normalized === "stage" || normalized === "stg") {
|
|
3171
|
+
return "staging";
|
|
3172
|
+
}
|
|
3173
|
+
if (normalized === "development" || normalized === "dev") {
|
|
3174
|
+
return "development";
|
|
3175
|
+
}
|
|
3176
|
+
if (normalized === "local" || normalized === "localhost") {
|
|
3177
|
+
return "local";
|
|
3178
|
+
}
|
|
3179
|
+
throw new Error(
|
|
3180
|
+
`Unknown Topolo environment "${value}". Expected production, staging, development, or local.`
|
|
3181
|
+
);
|
|
3182
|
+
}
|
|
3183
|
+
function selectedEnvironment() {
|
|
3184
|
+
return normalizeEnvironment(process.env.TOPOLO_ENV);
|
|
3185
|
+
}
|
|
3186
|
+
function envVarPrefix(environment) {
|
|
3187
|
+
if (environment === "production") return "TOPOLO_PRODUCTION";
|
|
3188
|
+
if (environment === "staging") return "TOPOLO_STAGING";
|
|
3189
|
+
if (environment === "development") return "TOPOLO_DEVELOPMENT";
|
|
3190
|
+
return "TOPOLO_LOCAL";
|
|
3191
|
+
}
|
|
3192
|
+
function profileForEnvironment(config, environment = selectedEnvironment()) {
|
|
3193
|
+
const profile = config.profiles?.[environment] ?? {};
|
|
3194
|
+
if (environment === "production") {
|
|
3195
|
+
return {
|
|
3196
|
+
credential: config.credential,
|
|
3197
|
+
oauthClientId: config.oauthClientId,
|
|
3198
|
+
...profile,
|
|
3199
|
+
serviceUrls: {
|
|
3200
|
+
...config.serviceUrls ?? {},
|
|
3201
|
+
...profile.serviceUrls ?? {}
|
|
3202
|
+
},
|
|
3203
|
+
agentName: profile.agentName ?? config.agentName
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
return profile;
|
|
3207
|
+
}
|
|
3208
|
+
function writeProfileConfig(config, environment, profile) {
|
|
3209
|
+
const profiles = { ...config.profiles ?? {} };
|
|
3210
|
+
profiles[environment] = {
|
|
3211
|
+
...profiles[environment] ?? {},
|
|
3212
|
+
...profile
|
|
3213
|
+
};
|
|
3214
|
+
return { ...config, profiles };
|
|
3215
|
+
}
|
|
3216
|
+
async function resolveCredential(environment = selectedEnvironment()) {
|
|
3217
|
+
const prefix = envVarPrefix(environment);
|
|
3218
|
+
const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
|
|
2778
3219
|
if (apiKey) return { kind: "api_key", apiKey };
|
|
2779
|
-
const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
|
|
3220
|
+
const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
|
|
2780
3221
|
if (accessToken) return { kind: "access_token", accessToken };
|
|
2781
|
-
|
|
3222
|
+
const config = await readConfig();
|
|
3223
|
+
return profileForEnvironment(config, environment).credential ?? null;
|
|
3224
|
+
}
|
|
3225
|
+
var REFRESH_SKEW_SECONDS = 60;
|
|
3226
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
3227
|
+
const prefix = envVarPrefix(environment);
|
|
3228
|
+
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
3229
|
+
return resolveCredential(environment);
|
|
3230
|
+
}
|
|
3231
|
+
const config = await readConfig();
|
|
3232
|
+
const profile = profileForEnvironment(config, environment);
|
|
3233
|
+
const cred = profile.credential;
|
|
3234
|
+
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
3235
|
+
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
3236
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3237
|
+
if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
3238
|
+
try {
|
|
3239
|
+
const oauth = new TopoloOAuth({
|
|
3240
|
+
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
3241
|
+
});
|
|
3242
|
+
const tokens = await oauth.refreshToken({
|
|
3243
|
+
clientId: profile.oauthClientId,
|
|
3244
|
+
refreshToken: cred.refreshToken
|
|
3245
|
+
});
|
|
3246
|
+
const refreshed = {
|
|
3247
|
+
kind: "access_token",
|
|
3248
|
+
accessToken: tokens.access_token,
|
|
3249
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
3250
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
3251
|
+
};
|
|
3252
|
+
const activeContext = profile.activeContext;
|
|
3253
|
+
const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
|
|
3254
|
+
const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
|
|
3255
|
+
credential: refreshed,
|
|
3256
|
+
contexts: {
|
|
3257
|
+
...profile.contexts ?? {},
|
|
3258
|
+
[activeContext]: {
|
|
3259
|
+
...activeEntry,
|
|
3260
|
+
credential: refreshed,
|
|
3261
|
+
oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
}) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
|
|
3265
|
+
await writeConfig(updated);
|
|
3266
|
+
return refreshed;
|
|
3267
|
+
} catch {
|
|
3268
|
+
return cred;
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
function resolveAgentName(configAgentName) {
|
|
3272
|
+
return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
|
|
2782
3273
|
}
|
|
2783
|
-
function
|
|
2784
|
-
const
|
|
3274
|
+
function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
|
|
3275
|
+
const merged = {
|
|
3276
|
+
...ENVIRONMENT_DEFAULT_SERVICE_URLS[environment],
|
|
3277
|
+
...configUrls ?? {}
|
|
3278
|
+
};
|
|
3279
|
+
const prefix = `${envVarPrefix(environment)}_SERVICE_URL_`;
|
|
3280
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
3281
|
+
if (k.startsWith(prefix) && v) {
|
|
3282
|
+
merged[k.slice(prefix.length).toLowerCase()] = v;
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
2785
3285
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2786
3286
|
if (k.startsWith("TOPOLO_SERVICE_URL_") && v) {
|
|
2787
|
-
|
|
3287
|
+
merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
|
|
2788
3288
|
}
|
|
2789
3289
|
}
|
|
2790
|
-
return Object.keys(
|
|
3290
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// src/auth.ts
|
|
3294
|
+
function resolveCredentialFromEnv() {
|
|
3295
|
+
const apiKey = process.env.TOPOLO_API_KEY;
|
|
3296
|
+
if (apiKey) return { kind: "api_key", apiKey };
|
|
3297
|
+
const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
|
|
3298
|
+
if (accessToken) return { kind: "access_token", accessToken };
|
|
3299
|
+
return null;
|
|
2791
3300
|
}
|
|
2792
3301
|
|
|
2793
3302
|
// src/server.ts
|
|
@@ -2837,6 +3346,15 @@ function splitService(servicePart) {
|
|
|
2837
3346
|
// src/tools.ts
|
|
2838
3347
|
var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
|
|
2839
3348
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
3349
|
+
function optionalResourceContext(args) {
|
|
3350
|
+
const resourceType = typeof args["resourceType"] === "string" ? args["resourceType"].trim() : "";
|
|
3351
|
+
const resourceId = typeof args["resourceId"] === "string" ? args["resourceId"].trim() : "";
|
|
3352
|
+
if (!resourceType && !resourceId) return void 0;
|
|
3353
|
+
if (!resourceType || !resourceId) {
|
|
3354
|
+
throw new Error("resourceType and resourceId must be provided together.");
|
|
3355
|
+
}
|
|
3356
|
+
return { resourceType, resourceId };
|
|
3357
|
+
}
|
|
2840
3358
|
var TOOLS = [
|
|
2841
3359
|
{
|
|
2842
3360
|
name: "topolo_whoami",
|
|
@@ -2852,18 +3370,27 @@ var TOOLS = [
|
|
|
2852
3370
|
handler: async (topolo) => topolo.identity.whoami()
|
|
2853
3371
|
},
|
|
2854
3372
|
{
|
|
2855
|
-
name: "
|
|
2856
|
-
title: "
|
|
2857
|
-
description: "
|
|
3373
|
+
name: "topolo_search_applications",
|
|
3374
|
+
title: "Search Topolo applications the agent can reach",
|
|
3375
|
+
description: "Searches the credential-scoped application registry with a bounded page and continuation cursor. Returns granted permissions, API URL, and launch URL without loading the global platform catalog.",
|
|
2858
3376
|
requiredScopes: [],
|
|
2859
3377
|
destructive: false,
|
|
2860
3378
|
inputSchema: {
|
|
2861
3379
|
type: "object",
|
|
2862
|
-
properties: {
|
|
3380
|
+
properties: {
|
|
3381
|
+
query: { type: "string" },
|
|
3382
|
+
cursor: { type: "string" },
|
|
3383
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
|
|
3384
|
+
},
|
|
2863
3385
|
additionalProperties: false
|
|
2864
3386
|
},
|
|
2865
|
-
handler: async (topolo) => {
|
|
2866
|
-
const
|
|
3387
|
+
handler: async (topolo, args) => {
|
|
3388
|
+
const result = await topolo.client.searchServices({
|
|
3389
|
+
query: typeof args["query"] === "string" ? args["query"] : void 0,
|
|
3390
|
+
cursor: typeof args["cursor"] === "string" ? args["cursor"] : void 0,
|
|
3391
|
+
limit: typeof args["limit"] === "number" ? args["limit"] : void 0
|
|
3392
|
+
});
|
|
3393
|
+
const applications = result.services.map((entry) => {
|
|
2867
3394
|
const id = displayAppId(entry);
|
|
2868
3395
|
return {
|
|
2869
3396
|
id,
|
|
@@ -2880,13 +3407,13 @@ var TOOLS = [
|
|
|
2880
3407
|
permissions: [...entry.permissions]
|
|
2881
3408
|
};
|
|
2882
3409
|
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
2883
|
-
return {
|
|
3410
|
+
return { applications, page: result.page };
|
|
2884
3411
|
}
|
|
2885
3412
|
},
|
|
2886
3413
|
{
|
|
2887
|
-
name: "
|
|
2888
|
-
title: "
|
|
2889
|
-
description: "
|
|
3414
|
+
name: "topolo_search_actions",
|
|
3415
|
+
title: "Search Topolo actions the agent can call",
|
|
3416
|
+
description: "Searches one application's credential-scoped actions with a bounded page and continuation cursor.",
|
|
2890
3417
|
requiredScopes: [],
|
|
2891
3418
|
destructive: false,
|
|
2892
3419
|
inputSchema: {
|
|
@@ -2894,31 +3421,25 @@ var TOOLS = [
|
|
|
2894
3421
|
properties: {
|
|
2895
3422
|
service: {
|
|
2896
3423
|
type: "string",
|
|
2897
|
-
description: "
|
|
2898
|
-
}
|
|
3424
|
+
description: "Required app id, slug, or name."
|
|
3425
|
+
},
|
|
3426
|
+
query: { type: "string" },
|
|
3427
|
+
cursor: { type: "string" },
|
|
3428
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
|
|
2899
3429
|
},
|
|
3430
|
+
required: ["service"],
|
|
2900
3431
|
additionalProperties: false
|
|
2901
3432
|
},
|
|
2902
|
-
handler: async (topolo, args) =>
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
requiredScopes: [],
|
|
2913
|
-
destructive: false,
|
|
2914
|
-
inputSchema: {
|
|
2915
|
-
type: "object",
|
|
2916
|
-
properties: {},
|
|
2917
|
-
additionalProperties: false
|
|
2918
|
-
},
|
|
2919
|
-
handler: async (topolo) => ({
|
|
2920
|
-
applications: await topolo.client.listServices()
|
|
2921
|
-
})
|
|
3433
|
+
handler: async (topolo, args) => {
|
|
3434
|
+
const service = typeof args["service"] === "string" ? args["service"].trim() : "";
|
|
3435
|
+
if (!service) throw new Error("service is required for bounded action discovery.");
|
|
3436
|
+
return topolo.client.searchActions({
|
|
3437
|
+
service,
|
|
3438
|
+
query: typeof args["query"] === "string" ? args["query"] : void 0,
|
|
3439
|
+
cursor: typeof args["cursor"] === "string" ? args["cursor"] : void 0,
|
|
3440
|
+
limit: typeof args["limit"] === "number" ? args["limit"] : void 0
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
2922
3443
|
},
|
|
2923
3444
|
{
|
|
2924
3445
|
name: "topolo_get_application",
|
|
@@ -2931,7 +3452,7 @@ var TOOLS = [
|
|
|
2931
3452
|
properties: {
|
|
2932
3453
|
application: {
|
|
2933
3454
|
type: "string",
|
|
2934
|
-
description: "Application identifier: app id, slug, or name from
|
|
3455
|
+
description: "Application identifier: app id, slug, or name from topolo_search_applications."
|
|
2935
3456
|
}
|
|
2936
3457
|
},
|
|
2937
3458
|
required: ["application"],
|
|
@@ -2942,18 +3463,98 @@ var TOOLS = [
|
|
|
2942
3463
|
if (typeof application !== "string" || !application.trim()) {
|
|
2943
3464
|
throw new Error("`application` must be a non-empty string.");
|
|
2944
3465
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
3466
|
+
return { application: await topolo.client.getService(application) };
|
|
3467
|
+
}
|
|
3468
|
+
},
|
|
3469
|
+
{
|
|
3470
|
+
name: "topolo_get_action",
|
|
3471
|
+
title: "Describe one Topolo action",
|
|
3472
|
+
description: "Returns one credential-scoped action by exact action id.",
|
|
3473
|
+
requiredScopes: [],
|
|
3474
|
+
destructive: false,
|
|
3475
|
+
inputSchema: {
|
|
3476
|
+
type: "object",
|
|
3477
|
+
properties: { actionId: { type: "string", minLength: 1 } },
|
|
3478
|
+
required: ["actionId"],
|
|
3479
|
+
additionalProperties: false
|
|
3480
|
+
},
|
|
3481
|
+
handler: async (topolo, args) => {
|
|
3482
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
3483
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
3484
|
+
return { action: await topolo.client.getAction(actionId) };
|
|
3485
|
+
}
|
|
3486
|
+
},
|
|
3487
|
+
{
|
|
3488
|
+
name: "topolo_read_action",
|
|
3489
|
+
title: "Call one read-only Topolo action",
|
|
3490
|
+
description: "Calls one exact action id only when its published contract is read-only and needs no confirmation.",
|
|
3491
|
+
requiredScopes: [],
|
|
3492
|
+
destructive: false,
|
|
3493
|
+
inputSchema: {
|
|
3494
|
+
type: "object",
|
|
3495
|
+
properties: {
|
|
3496
|
+
actionId: { type: "string", minLength: 1 },
|
|
3497
|
+
input: { type: "object", additionalProperties: true },
|
|
3498
|
+
resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
|
|
3499
|
+
resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." }
|
|
3500
|
+
},
|
|
3501
|
+
required: ["actionId"],
|
|
3502
|
+
additionalProperties: false
|
|
3503
|
+
},
|
|
3504
|
+
handler: async (topolo, args) => {
|
|
3505
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
3506
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
3507
|
+
const input = args["input"];
|
|
3508
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
|
|
3509
|
+
throw new Error("input must be an object.");
|
|
3510
|
+
}
|
|
3511
|
+
const action = await topolo.client.getAction(actionId);
|
|
3512
|
+
const agentAccess = action.agentAccess ?? (action.destructive ? "off" : action.readOnly && !action.requiresConfirmation ? "auto" : "confirm");
|
|
3513
|
+
if (action.method !== "GET" || !action.readOnly || action.destructive || action.requiresConfirmation || agentAccess !== "auto") {
|
|
3514
|
+
throw new Error(`Action "${actionId}" is not an automatic read-only action. Use topolo_call_action with confirmation.`);
|
|
3515
|
+
}
|
|
3516
|
+
const resource = optionalResourceContext(args);
|
|
3517
|
+
return topolo.client.callAction(
|
|
3518
|
+
actionId,
|
|
3519
|
+
input ?? {},
|
|
3520
|
+
resource ? { resource } : {}
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
},
|
|
3524
|
+
{
|
|
3525
|
+
name: "topolo_call_action",
|
|
3526
|
+
title: "Call one Topolo action",
|
|
3527
|
+
description: "Calls one exact action id. Mutating actions require confirm:true.",
|
|
3528
|
+
requiredScopes: [],
|
|
3529
|
+
destructive: true,
|
|
3530
|
+
inputSchema: {
|
|
3531
|
+
type: "object",
|
|
3532
|
+
properties: {
|
|
3533
|
+
actionId: { type: "string", minLength: 1 },
|
|
3534
|
+
input: { type: "object", additionalProperties: true },
|
|
3535
|
+
resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
|
|
3536
|
+
resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." },
|
|
3537
|
+
confirm: { type: "boolean" }
|
|
3538
|
+
},
|
|
3539
|
+
required: ["actionId"],
|
|
3540
|
+
additionalProperties: false
|
|
3541
|
+
},
|
|
3542
|
+
handler: async (topolo, args) => {
|
|
3543
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
3544
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
3545
|
+
const input = args["input"];
|
|
3546
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
|
|
3547
|
+
throw new Error("input must be an object.");
|
|
3548
|
+
}
|
|
3549
|
+
const resource = optionalResourceContext(args);
|
|
3550
|
+
return topolo.client.callAction(
|
|
3551
|
+
actionId,
|
|
3552
|
+
input ?? {},
|
|
3553
|
+
{
|
|
3554
|
+
confirm: args["confirm"] === true,
|
|
3555
|
+
...resource ? { resource } : {}
|
|
3556
|
+
}
|
|
2949
3557
|
);
|
|
2950
|
-
if (!match) {
|
|
2951
|
-
const known = catalog.map((e) => e.slug || e.appId).sort().filter((value) => typeof value === "string" && value.length > 0);
|
|
2952
|
-
throw new Error(
|
|
2953
|
-
`Unknown application "${application}". ${formatKnownValues(known, "applications")}`
|
|
2954
|
-
);
|
|
2955
|
-
}
|
|
2956
|
-
return { application: match };
|
|
2957
3558
|
}
|
|
2958
3559
|
},
|
|
2959
3560
|
{
|
|
@@ -2967,7 +3568,7 @@ var TOOLS = [
|
|
|
2967
3568
|
properties: {
|
|
2968
3569
|
application: {
|
|
2969
3570
|
type: "string",
|
|
2970
|
-
description: "Optional application ID from
|
|
3571
|
+
description: "Optional application ID from topolo_search_applications."
|
|
2971
3572
|
}
|
|
2972
3573
|
},
|
|
2973
3574
|
additionalProperties: false
|
|
@@ -3007,7 +3608,7 @@ var TOOLS = [
|
|
|
3007
3608
|
properties: {
|
|
3008
3609
|
application: {
|
|
3009
3610
|
type: "string",
|
|
3010
|
-
description: "Optional application ID from
|
|
3611
|
+
description: "Optional application ID from topolo_search_applications."
|
|
3011
3612
|
},
|
|
3012
3613
|
failOn: {
|
|
3013
3614
|
type: "string",
|
|
@@ -3041,7 +3642,7 @@ var TOOLS = [
|
|
|
3041
3642
|
{
|
|
3042
3643
|
name: "topolo_api_call",
|
|
3043
3644
|
title: "Call any Topolo platform service",
|
|
3044
|
-
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
|
|
3645
|
+
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.",
|
|
3045
3646
|
requiredScopes: [],
|
|
3046
3647
|
destructive: true,
|
|
3047
3648
|
inputSchema: {
|
|
@@ -3049,7 +3650,7 @@ var TOOLS = [
|
|
|
3049
3650
|
properties: {
|
|
3050
3651
|
service: {
|
|
3051
3652
|
type: "string",
|
|
3052
|
-
description: "Target
|
|
3653
|
+
description: "Target app ID from the platform registry."
|
|
3053
3654
|
},
|
|
3054
3655
|
method: {
|
|
3055
3656
|
type: "string",
|
|
@@ -3072,7 +3673,7 @@ var TOOLS = [
|
|
|
3072
3673
|
},
|
|
3073
3674
|
resourceType: {
|
|
3074
3675
|
type: "string",
|
|
3075
|
-
description: "Optional workspace resource alias from
|
|
3676
|
+
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."
|
|
3076
3677
|
},
|
|
3077
3678
|
resourceId: {
|
|
3078
3679
|
type: "string",
|
|
@@ -3090,7 +3691,7 @@ var TOOLS = [
|
|
|
3090
3691
|
const service = args["service"];
|
|
3091
3692
|
const path = args["path"];
|
|
3092
3693
|
if (typeof service !== "string" || !service.trim()) {
|
|
3093
|
-
throw new Error("service is required. Call
|
|
3694
|
+
throw new Error("service is required. Call topolo_search_applications first to discover callable services.");
|
|
3094
3695
|
}
|
|
3095
3696
|
if (typeof path !== "string" || !path) {
|
|
3096
3697
|
throw new Error("path is required");
|
|
@@ -3104,12 +3705,7 @@ var TOOLS = [
|
|
|
3104
3705
|
const normalisedQuery = query && typeof query === "object" && !Array.isArray(query) ? Object.fromEntries(
|
|
3105
3706
|
Object.entries(query).map(([k, v]) => [k, String(v)])
|
|
3106
3707
|
) : void 0;
|
|
3107
|
-
const
|
|
3108
|
-
const resourceId = typeof args["resourceId"] === "string" ? args["resourceId"].trim() : "";
|
|
3109
|
-
if (resourceType && !resourceId || !resourceType && resourceId) {
|
|
3110
|
-
throw new Error("resourceType and resourceId must be provided together.");
|
|
3111
|
-
}
|
|
3112
|
-
const resource = resourceType && resourceId ? { resourceType, resourceId } : void 0;
|
|
3708
|
+
const resource = optionalResourceContext(args);
|
|
3113
3709
|
return topolo.client.request({
|
|
3114
3710
|
service: service.trim(),
|
|
3115
3711
|
method,
|
|
@@ -3126,58 +3722,8 @@ function filterToolsByScopes(tools, scopes) {
|
|
|
3126
3722
|
return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
|
|
3127
3723
|
}
|
|
3128
3724
|
async function buildTopoloTools(topolo, scopes, tools = TOOLS) {
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
const actionTools = actionsToToolDefs(actions);
|
|
3132
|
-
return filterToolsByScopes([...baseTools, ...actionTools], scopes);
|
|
3133
|
-
}
|
|
3134
|
-
function effectiveAgentAccess(a) {
|
|
3135
|
-
if (a.agentAccess) return a.agentAccess;
|
|
3136
|
-
if (a.destructive) return "off";
|
|
3137
|
-
if (a.readOnly && !a.requiresConfirmation) return "auto";
|
|
3138
|
-
return "confirm";
|
|
3139
|
-
}
|
|
3140
|
-
function actionsToToolDefs(actions) {
|
|
3141
|
-
return actions.map((action) => ({ action, access: effectiveAgentAccess(action) })).filter(({ access }) => access !== "off").map(({ action, access }) => ({
|
|
3142
|
-
name: action.toolName || toolNameForAction(action),
|
|
3143
|
-
title: action.title,
|
|
3144
|
-
description: action.description,
|
|
3145
|
-
requiredScopes: action.requiredPermission ? [action.requiredPermission] : [],
|
|
3146
|
-
destructive: access === "confirm",
|
|
3147
|
-
inputSchema: actionInputSchema(action, access),
|
|
3148
|
-
handler: async (topolo, args) => {
|
|
3149
|
-
const confirm = Boolean(args["confirm"]);
|
|
3150
|
-
const input = { ...args };
|
|
3151
|
-
delete input["confirm"];
|
|
3152
|
-
return topolo.client.callAction(action.actionId, input, { confirm });
|
|
3153
|
-
}
|
|
3154
|
-
}));
|
|
3155
|
-
}
|
|
3156
|
-
function toolNameForAction(action) {
|
|
3157
|
-
const service = (action.serviceSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3158
|
-
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3159
|
-
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
3160
|
-
}
|
|
3161
|
-
function actionInputSchema(action, access = "auto") {
|
|
3162
|
-
const schema = {
|
|
3163
|
-
type: "object",
|
|
3164
|
-
properties: {},
|
|
3165
|
-
additionalProperties: false,
|
|
3166
|
-
...action.inputSchema
|
|
3167
|
-
};
|
|
3168
|
-
const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? { ...schema.properties } : {};
|
|
3169
|
-
if (access === "confirm") {
|
|
3170
|
-
properties.confirm = {
|
|
3171
|
-
type: "boolean",
|
|
3172
|
-
description: "Required for mutating actions. Explicit human-in-the-loop acknowledgement."
|
|
3173
|
-
};
|
|
3174
|
-
}
|
|
3175
|
-
return {
|
|
3176
|
-
...schema,
|
|
3177
|
-
type: "object",
|
|
3178
|
-
properties,
|
|
3179
|
-
additionalProperties: schema.additionalProperties ?? false
|
|
3180
|
-
};
|
|
3725
|
+
void topolo;
|
|
3726
|
+
return filterToolsByScopes(tools, scopes);
|
|
3181
3727
|
}
|
|
3182
3728
|
function evaluateApplicationAuditGate(report, threshold) {
|
|
3183
3729
|
const counts = {
|
|
@@ -3219,7 +3765,7 @@ function displayAppId(entry) {
|
|
|
3219
3765
|
for (const value of [entry.slug, entry.name, entry.appId]) {
|
|
3220
3766
|
if (typeof value !== "string" || !value.trim()) continue;
|
|
3221
3767
|
for (const candidate of appIdCandidates(value)) {
|
|
3222
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
3768
|
+
if (Object.prototype.hasOwnProperty.call(DEFAULT_APP_URLS, candidate)) {
|
|
3223
3769
|
return candidate;
|
|
3224
3770
|
}
|
|
3225
3771
|
}
|
|
@@ -3245,14 +3791,14 @@ function formatKnownValues(values, label) {
|
|
|
3245
3791
|
|
|
3246
3792
|
// src/version.ts
|
|
3247
3793
|
import { readFileSync } from "fs";
|
|
3248
|
-
import { dirname, join } from "path";
|
|
3794
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
3249
3795
|
import { fileURLToPath } from "url";
|
|
3250
3796
|
function readMcpVersion() {
|
|
3251
3797
|
try {
|
|
3252
|
-
const here =
|
|
3798
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
3253
3799
|
const candidates = [
|
|
3254
|
-
|
|
3255
|
-
|
|
3800
|
+
join2(here, "package.json"),
|
|
3801
|
+
join2(here, "..", "package.json")
|
|
3256
3802
|
];
|
|
3257
3803
|
for (const path of candidates) {
|
|
3258
3804
|
try {
|
|
@@ -3331,15 +3877,22 @@ function toolError(message) {
|
|
|
3331
3877
|
|
|
3332
3878
|
// src/index.ts
|
|
3333
3879
|
async function main() {
|
|
3334
|
-
const
|
|
3880
|
+
const environment = normalizeEnvironment(process.env.TOPOLO_ENV);
|
|
3881
|
+
const environmentCredential = resolveCredentialFromEnv();
|
|
3882
|
+
let credential = environmentCredential;
|
|
3883
|
+
if (!credential) {
|
|
3884
|
+
credential = await refreshCredentialIfNeeded(environment);
|
|
3885
|
+
}
|
|
3335
3886
|
if (!credential) {
|
|
3336
3887
|
process.stderr.write(
|
|
3337
|
-
"TopoloMCP:
|
|
3888
|
+
"TopoloMCP: no Topolo credential. Run `topolo auth login`, or set TOPOLO_API_KEY / TOPOLO_ACCESS_TOKEN.\n"
|
|
3338
3889
|
);
|
|
3339
3890
|
process.exit(1);
|
|
3340
3891
|
}
|
|
3341
|
-
const
|
|
3342
|
-
const
|
|
3892
|
+
const config = await readConfig();
|
|
3893
|
+
const profile = profileForEnvironment(config, environment);
|
|
3894
|
+
const agentName = resolveAgentName(profile.agentName);
|
|
3895
|
+
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
3343
3896
|
const topolo = createTopolo({
|
|
3344
3897
|
credential,
|
|
3345
3898
|
agent: {
|
|
@@ -3348,7 +3901,8 @@ async function main() {
|
|
|
3348
3901
|
...agentName ? { agentName } : {}
|
|
3349
3902
|
},
|
|
3350
3903
|
serviceUrls,
|
|
3351
|
-
requireConfirmForWrites: true
|
|
3904
|
+
requireConfirmForWrites: true,
|
|
3905
|
+
...!environmentCredential ? { refreshCredential: () => refreshCredentialIfNeeded(environment, { force: true }) } : {}
|
|
3352
3906
|
});
|
|
3353
3907
|
const scopes = await loadScopes(topolo);
|
|
3354
3908
|
const server = buildServer(topolo, scopes);
|
|
@@ -3356,7 +3910,7 @@ async function main() {
|
|
|
3356
3910
|
await server.connect(transport);
|
|
3357
3911
|
const toolNames = await advertisedToolNames(topolo, scopes);
|
|
3358
3912
|
process.stderr.write(
|
|
3359
|
-
`TopoloMCP v${MCP_VERSION} connected. Tools available: ${toolNames.join(", ") || "(none)"}
|
|
3913
|
+
`TopoloMCP v${MCP_VERSION} connected (${environment}). Tools available: ${toolNames.join(", ") || "(none)"}
|
|
3360
3914
|
`
|
|
3361
3915
|
);
|
|
3362
3916
|
}
|