@topolo/mcp 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -66,6 +72,7 @@ var APP_API_URLS = {
66
72
  bugfix: "https://bugfix.topolo.app",
67
73
  bytes: "https://bytes.topolo.app",
68
74
  calendar: "https://calendar.topolo.app",
75
+ campaigns: "https://campaigns.topolo.app",
69
76
  capacity: "https://capacity.topolo.app",
70
77
  chat: "https://chat.topolo.app",
71
78
  commerce: "https://commerce.topolo.app",
@@ -324,6 +331,38 @@ var APPLICATIONS = {
324
331
  }
325
332
  ]
326
333
  },
334
+ "campaigns": {
335
+ "id": "campaigns",
336
+ "appDir": "TopoloCampaigns",
337
+ "name": "topolo-campaigns",
338
+ "packageName": "topolo-campaigns",
339
+ "productionUrl": "https://campaigns.topolo.app",
340
+ "services": [
341
+ "campaigns"
342
+ ],
343
+ "deployTargets": [
344
+ {
345
+ "name": "topolo-campaigns",
346
+ "kind": "worker",
347
+ "apiUrl": "https://campaigns.topolo.app",
348
+ "domains": [
349
+ "campaigns.topolo.app"
350
+ ],
351
+ "routes": [
352
+ "campaigns.topolo.app"
353
+ ],
354
+ "appId": "campaigns"
355
+ },
356
+ {
357
+ "name": "topolo-campaigns-api",
358
+ "kind": "worker",
359
+ "apiUrl": null,
360
+ "domains": [],
361
+ "routes": [],
362
+ "appId": null
363
+ }
364
+ ]
365
+ },
327
366
  "capacity": {
328
367
  "id": "capacity",
329
368
  "appDir": "TopoloCapacity",
@@ -1106,6 +1145,7 @@ var APPLICATIONS = {
1106
1145
  "topolo.io"
1107
1146
  ],
1108
1147
  "routes": [
1148
+ "one.topolo.io",
1109
1149
  "topolo.io",
1110
1150
  "www.topolo.io"
1111
1151
  ],
@@ -2059,7 +2099,6 @@ var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
2059
2099
  var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
2060
2100
  var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
2061
2101
  var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
2062
- var CATALOG_CACHE_TTL_MS = 60 * 1e3;
2063
2102
  var TopoloClient = class _TopoloClient {
2064
2103
  credential;
2065
2104
  agent;
@@ -2068,9 +2107,9 @@ var TopoloClient = class _TopoloClient {
2068
2107
  requireConfirmForWrites;
2069
2108
  timeoutMs;
2070
2109
  fetchImpl;
2110
+ refreshCredential;
2111
+ refreshInFlight = null;
2071
2112
  debug;
2072
- serviceCatalogCache = null;
2073
- actionCatalogCache = null;
2074
2113
  /**
2075
2114
  * Process-wide cache for slug → app-id lookups. Keyed by
2076
2115
  * `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
@@ -2095,6 +2134,7 @@ var TopoloClient = class _TopoloClient {
2095
2134
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
2096
2135
  this.timeoutMs = options.timeoutMs ?? 3e4;
2097
2136
  this.fetchImpl = options.fetch ?? fetch;
2137
+ this.refreshCredential = options.refreshCredential;
2098
2138
  this.debug = options.debug;
2099
2139
  }
2100
2140
  emit(event) {
@@ -2112,11 +2152,11 @@ var TopoloClient = class _TopoloClient {
2112
2152
  async request(opts) {
2113
2153
  const method = opts.method ?? "GET";
2114
2154
  if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
2115
- throw new TopoloAuthError(
2155
+ throw new TopoloConfirmationRequiredError(
2116
2156
  `Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
2117
2157
  );
2118
2158
  }
2119
- const resolvedService = await this.resolveRequestService(opts.service);
2159
+ const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
2120
2160
  const baseUrl = resolvedService.baseUrl;
2121
2161
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
2122
2162
  if (opts.query) {
@@ -2147,20 +2187,36 @@ var TopoloClient = class _TopoloClient {
2147
2187
  headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
2148
2188
  headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
2149
2189
  }
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
2190
  const urlStr = url.toString();
2154
2191
  const startedAt = Date.now();
2155
2192
  this.emit({ phase: "request", method, service: resolvedService.serviceKey, path: opts.path, url: urlStr, requestId });
2193
+ const fetchOnce = async () => {
2194
+ const controller = new AbortController();
2195
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2196
+ const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
2197
+ try {
2198
+ return await this.fetchImpl(urlStr, {
2199
+ method,
2200
+ headers: new Headers(headers),
2201
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2202
+ signal
2203
+ });
2204
+ } finally {
2205
+ clearTimeout(timeoutId);
2206
+ }
2207
+ };
2156
2208
  let res;
2157
2209
  try {
2158
- res = await this.fetchImpl(urlStr, {
2159
- method,
2160
- headers,
2161
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2162
- signal
2163
- });
2210
+ const attemptedCredential = this.credential;
2211
+ res = await fetchOnce();
2212
+ if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
2213
+ await res.body?.cancel().catch(() => void 0);
2214
+ const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
2215
+ if (refreshed) {
2216
+ applyAuthHeaders(headers, refreshed);
2217
+ res = await fetchOnce();
2218
+ }
2219
+ }
2164
2220
  } catch (err) {
2165
2221
  this.emit({
2166
2222
  phase: "error",
@@ -2173,8 +2229,6 @@ var TopoloClient = class _TopoloClient {
2173
2229
  error: err instanceof Error ? err.message : String(err)
2174
2230
  });
2175
2231
  throw err;
2176
- } finally {
2177
- clearTimeout(timeoutId);
2178
2232
  }
2179
2233
  const contentType = res.headers.get("Content-Type") ?? "";
2180
2234
  const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
@@ -2213,6 +2267,19 @@ var TopoloClient = class _TopoloClient {
2213
2267
  });
2214
2268
  return parsed;
2215
2269
  }
2270
+ async refreshRejectedAccessToken(attemptedAccessToken) {
2271
+ if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
2272
+ return this.credential;
2273
+ }
2274
+ if (!this.refreshCredential) return null;
2275
+ this.refreshInFlight ??= this.refreshCredential().then((credential) => {
2276
+ if (credential) this.credential = credential;
2277
+ return credential;
2278
+ }).catch(() => null).finally(() => {
2279
+ this.refreshInFlight = null;
2280
+ });
2281
+ return this.refreshInFlight;
2282
+ }
2216
2283
  async resolveRequestService(service) {
2217
2284
  const serviceKey = normalizeLookupKey(service);
2218
2285
  if (!serviceKey) {
@@ -2238,9 +2305,8 @@ var TopoloClient = class _TopoloClient {
2238
2305
  );
2239
2306
  }
2240
2307
  async resolveLiveCallableService(serviceKey) {
2241
- const catalog = await this.listServices();
2242
- const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2243
- if (!match) return null;
2308
+ const match = await this.getService(serviceKey).catch(() => null);
2309
+ if (!match || !catalogEntryMatches(match, serviceKey)) return null;
2244
2310
  const baseUrl = resolveCatalogServiceUrl(match, {
2245
2311
  serviceKey,
2246
2312
  overrides: this.serviceUrls
@@ -2314,12 +2380,13 @@ var TopoloClient = class _TopoloClient {
2314
2380
  * The live Auth catalog is the authority because it is scoped to the caller's
2315
2381
  * organization and effective permissions.
2316
2382
  */
2317
- async listServices() {
2318
- const now = Date.now();
2319
- if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
2320
- return this.serviceCatalogCache.entries;
2321
- }
2322
- const res = await this.request({ service: "developers", method: "GET", path: "/api/services/catalog" });
2383
+ async searchServices(options = {}) {
2384
+ const res = await this.request({
2385
+ service: "developers",
2386
+ method: "GET",
2387
+ path: "/api/services/catalog",
2388
+ query: catalogSearchQuery(options)
2389
+ });
2323
2390
  const raw = res.data?.services ?? res.services;
2324
2391
  if (!Array.isArray(raw)) {
2325
2392
  throw new TopoloSdkError(
@@ -2329,6 +2396,7 @@ var TopoloClient = class _TopoloClient {
2329
2396
  }
2330
2397
  const entries = [];
2331
2398
  for (const item of raw) {
2399
+ if (!isRecord(item)) continue;
2332
2400
  const s = item;
2333
2401
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2334
2402
  if (!appId) continue;
@@ -2353,11 +2421,30 @@ var TopoloClient = class _TopoloClient {
2353
2421
  apiKeyResources
2354
2422
  });
2355
2423
  }
2356
- this.serviceCatalogCache = {
2357
- entries,
2358
- expiresAt: now + CATALOG_CACHE_TTL_MS
2359
- };
2360
- return entries;
2424
+ const page = normalizeCatalogPage(
2425
+ res.data?.page ?? res.page
2426
+ );
2427
+ return { services: entries, page };
2428
+ }
2429
+ /** @deprecated Prefer searchServices() so callers retain the continuation cursor. */
2430
+ async listServices(options = {}) {
2431
+ return (await this.searchServices(options)).services;
2432
+ }
2433
+ async getService(application) {
2434
+ const needle = application.trim();
2435
+ if (!needle) throw new TopoloSdkError("unknown_service", "application is required");
2436
+ const res = await this.request({
2437
+ service: "developers",
2438
+ method: "GET",
2439
+ path: "/api/services/catalog",
2440
+ query: { application: needle }
2441
+ });
2442
+ const raw = res.data?.application ?? res.application;
2443
+ const entry = normalizeServiceCatalogEntry(raw);
2444
+ if (!entry) {
2445
+ throw new TopoloSdkError("unknown_service", `Unknown Topolo application "${application}".`);
2446
+ }
2447
+ return entry;
2361
2448
  }
2362
2449
  /**
2363
2450
  * List callable actions from the live registry-canonical catalog (definitions
@@ -2365,16 +2452,15 @@ var TopoloClient = class _TopoloClient {
2365
2452
  * caller's organization and effective permissions; no bundled/static app
2366
2453
  * action list is used as an authorization source.
2367
2454
  */
2368
- async listActions(options = {}) {
2369
- const now = Date.now();
2370
- if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
2371
- return this.actionCatalogCache.entries;
2455
+ async searchActions(options) {
2456
+ if (!options.service?.trim()) {
2457
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2372
2458
  }
2373
2459
  const res = await this.request({
2374
2460
  service: "developers",
2375
2461
  method: "GET",
2376
2462
  path: "/api/actions/catalog",
2377
- ...options.service ? { query: { service: options.service } } : {}
2463
+ query: { service: options.service, ...catalogSearchQuery(options) }
2378
2464
  });
2379
2465
  const raw = res.data?.actions ?? res.actions;
2380
2466
  if (!Array.isArray(raw)) {
@@ -2384,36 +2470,96 @@ var TopoloClient = class _TopoloClient {
2384
2470
  );
2385
2471
  }
2386
2472
  const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
2387
- if (!options.service) {
2388
- this.actionCatalogCache = {
2389
- entries,
2390
- expiresAt: now + CATALOG_CACHE_TTL_MS
2391
- };
2473
+ return {
2474
+ actions: entries,
2475
+ page: normalizeCatalogPage(
2476
+ res.data?.page ?? res.page
2477
+ )
2478
+ };
2479
+ }
2480
+ async getActionCatalogSummary(service) {
2481
+ const boundedService = service.trim();
2482
+ if (!boundedService) {
2483
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2484
+ }
2485
+ const res = await this.request({
2486
+ service: "developers",
2487
+ method: "GET",
2488
+ path: "/api/actions/catalog",
2489
+ query: { service: boundedService, summary: "true" }
2490
+ });
2491
+ const raw = res.data?.summary ?? res.summary;
2492
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2493
+ throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
2494
+ }
2495
+ const record = raw;
2496
+ if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
2497
+ throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
2498
+ }
2499
+ return {
2500
+ algorithm: "sha256-v1",
2501
+ digest: record.digest,
2502
+ actionCount: Number(record.actionCount)
2503
+ };
2504
+ }
2505
+ /** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
2506
+ async listActions(options) {
2507
+ if (!options) {
2508
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2392
2509
  }
2393
- return entries;
2510
+ return (await this.searchActions(options)).actions;
2394
2511
  }
2395
2512
  async getAction(action) {
2396
- const needle = normalizeActionLookupKey(action);
2397
- const actions = await this.listActions();
2398
- const match = actions.find((entry) => actionEntryMatches(entry, needle));
2399
- if (!match) {
2513
+ const actionId = action.trim();
2514
+ if (!actionId) {
2400
2515
  throw new TopoloSdkError(
2401
2516
  "unknown_action",
2402
- `Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
2517
+ "action id is required"
2403
2518
  );
2404
2519
  }
2520
+ let res;
2521
+ try {
2522
+ res = await this.request({
2523
+ service: "developers",
2524
+ method: "GET",
2525
+ path: "/api/actions/catalog",
2526
+ query: { actionId }
2527
+ });
2528
+ } catch (error) {
2529
+ if (error instanceof TopoloHttpError && error.status === 404) {
2530
+ throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
2531
+ }
2532
+ throw error;
2533
+ }
2534
+ const raw = res.data?.action ?? res.action;
2535
+ const match = normalizeActionCatalogEntry(raw);
2536
+ if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
2405
2537
  return match;
2406
2538
  }
2407
2539
  async callAction(action, input = {}, options = {}) {
2408
2540
  const entry = await this.getAction(action);
2541
+ const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2542
+ if (agentAccess === "off") {
2543
+ throw new TopoloPermissionError(
2544
+ `Topolo action "${entry.actionId}" is not available for agent invocation.`,
2545
+ entry.requiredPermission ? [entry.requiredPermission] : []
2546
+ );
2547
+ }
2548
+ if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
2549
+ throw new TopoloConfirmationRequiredError(
2550
+ `Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
2551
+ );
2552
+ }
2409
2553
  const mapped = mapActionInput(entry, input);
2410
2554
  const service = entry.appSlug || entry.appId;
2411
2555
  return this.request({
2412
2556
  service,
2557
+ ...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
2413
2558
  method: entry.method,
2414
2559
  path: mapped.path,
2415
2560
  ...mapped.query ? { query: mapped.query } : {},
2416
2561
  ...mapped.body !== void 0 ? { body: mapped.body } : {},
2562
+ ...options.resource ? { resource: options.resource } : {},
2417
2563
  headers: {
2418
2564
  [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2419
2565
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
@@ -2551,6 +2697,18 @@ function serviceKeyCandidates2(slug) {
2551
2697
  function normalizeLookupKey(value) {
2552
2698
  return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
2553
2699
  }
2700
+ function requireHttpOrigin(value) {
2701
+ let url;
2702
+ try {
2703
+ url = new URL(value);
2704
+ } catch {
2705
+ throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
2706
+ }
2707
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
2708
+ throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
2709
+ }
2710
+ return url.origin;
2711
+ }
2554
2712
  function deriveDevelopersFromAuth(authUrl) {
2555
2713
  if (!authUrl) return null;
2556
2714
  try {
@@ -2577,6 +2735,47 @@ function catalogEntryLookupKeys(entry) {
2577
2735
  }
2578
2736
  return [...keys];
2579
2737
  }
2738
+ function catalogSearchQuery(options) {
2739
+ const query = {};
2740
+ if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
2741
+ if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
2742
+ if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
2743
+ return query;
2744
+ }
2745
+ function normalizeCatalogPage(value) {
2746
+ if (!isRecord(value)) return { nextCursor: null, hasMore: false };
2747
+ return {
2748
+ nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
2749
+ hasMore: value.hasMore === true
2750
+ };
2751
+ }
2752
+ function normalizeServiceCatalogEntry(item) {
2753
+ if (!isRecord(item)) return null;
2754
+ const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
2755
+ if (!appId) return null;
2756
+ const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
2757
+ (permission) => typeof permission === "string" && permission.trim().length > 0
2758
+ ) : [];
2759
+ const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
2760
+ item.api_key_resources ?? item.apiKeyResources
2761
+ );
2762
+ const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
2763
+ const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
2764
+ return {
2765
+ appId,
2766
+ slug: typeof item.app_slug === "string" ? item.app_slug : null,
2767
+ name: typeof item.name === "string" ? item.name : null,
2768
+ description: typeof item.description === "string" ? item.description : null,
2769
+ apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
2770
+ launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
2771
+ status: typeof item.status === "string" ? item.status : null,
2772
+ surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
2773
+ launchable: item.launchable === true,
2774
+ permissions,
2775
+ apiKeyResource,
2776
+ apiKeyResources
2777
+ };
2778
+ }
2580
2779
  function normalizeActionCatalogEntry(item) {
2581
2780
  if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2582
2781
  const a = item;
@@ -2590,6 +2789,8 @@ function normalizeActionCatalogEntry(item) {
2590
2789
  }
2591
2790
  return {
2592
2791
  actionId,
2792
+ target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
2793
+ apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
2593
2794
  name,
2594
2795
  toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
2595
2796
  title: pickActionString(a.title) ?? name,
@@ -2601,6 +2802,7 @@ function normalizeActionCatalogEntry(item) {
2601
2802
  path,
2602
2803
  permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
2603
2804
  requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
2805
+ authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
2604
2806
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
2605
2807
  outputSchema: plainObject(a.output_schema ?? a.outputSchema),
2606
2808
  readOnly: Boolean(a.read_only ?? a.readOnly),
@@ -2620,16 +2822,12 @@ function pickActionString(value) {
2620
2822
  function pickAgentAccess(value) {
2621
2823
  return value === "auto" || value === "confirm" || value === "off" ? value : null;
2622
2824
  }
2825
+ function pickAuthorizationMode(value) {
2826
+ return value === "runtime" ? "runtime" : "static";
2827
+ }
2623
2828
  function plainObject(value) {
2624
2829
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2625
2830
  }
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
2831
  function mapActionInput(action, input) {
2634
2832
  const remaining = { ...input || {} };
2635
2833
  const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
@@ -2815,6 +3013,7 @@ var APP_API_URLS2 = {
2815
3013
  bugfix: "https://bugfix.topolo.app",
2816
3014
  bytes: "https://bytes.topolo.app",
2817
3015
  calendar: "https://calendar.topolo.app",
3016
+ campaigns: "https://campaigns.topolo.app",
2818
3017
  capacity: "https://capacity.topolo.app",
2819
3018
  chat: "https://chat.topolo.app",
2820
3019
  commerce: "https://commerce.topolo.app",
@@ -3058,7 +3257,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
3058
3257
  return profileForEnvironment(config, environment).credential ?? null;
3059
3258
  }
3060
3259
  var REFRESH_SKEW_SECONDS = 60;
3061
- async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
3260
+ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
3062
3261
  const prefix = envVarPrefix(environment);
3063
3262
  if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
3064
3263
  return resolveCredential(environment);
@@ -3069,7 +3268,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
3069
3268
  if (!cred || cred.kind !== "access_token") return cred ?? null;
3070
3269
  if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
3071
3270
  const now = Math.floor(Date.now() / 1e3);
3072
- if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
3271
+ if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
3073
3272
  try {
3074
3273
  const oauth = new TopoloOAuth({
3075
3274
  serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
@@ -3084,9 +3283,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
3084
3283
  ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
3085
3284
  expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
3086
3285
  };
3087
- await writeConfig(
3088
- environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed })
3089
- );
3286
+ const activeContext = profile.activeContext;
3287
+ const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
3288
+ const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
3289
+ credential: refreshed,
3290
+ contexts: {
3291
+ ...profile.contexts ?? {},
3292
+ [activeContext]: {
3293
+ ...activeEntry,
3294
+ credential: refreshed,
3295
+ oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
3296
+ }
3297
+ }
3298
+ }) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
3299
+ await writeConfig(updated);
3090
3300
  return refreshed;
3091
3301
  } catch {
3092
3302
  return cred;
@@ -3170,6 +3380,15 @@ function splitService(servicePart) {
3170
3380
  // src/tools.ts
3171
3381
  var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
3172
3382
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
3383
+ function optionalResourceContext(args) {
3384
+ const resourceType = typeof args["resourceType"] === "string" ? args["resourceType"].trim() : "";
3385
+ const resourceId = typeof args["resourceId"] === "string" ? args["resourceId"].trim() : "";
3386
+ if (!resourceType && !resourceId) return void 0;
3387
+ if (!resourceType || !resourceId) {
3388
+ throw new Error("resourceType and resourceId must be provided together.");
3389
+ }
3390
+ return { resourceType, resourceId };
3391
+ }
3173
3392
  var TOOLS = [
3174
3393
  {
3175
3394
  name: "topolo_whoami",
@@ -3185,22 +3404,32 @@ var TOOLS = [
3185
3404
  handler: async (topolo) => topolo.identity.whoami()
3186
3405
  },
3187
3406
  {
3188
- name: "topolo_list_services",
3189
- title: "List every Topolo service the agent can reach",
3190
- description: "Returns the credential-scoped service registry: one entry per platform service the current credential can use in its organization, with granted permissions, API URL, and launch URL. Use this to discover what `topolo_api_call` can address. This is not the global platform catalog.",
3407
+ name: "topolo_search_applications",
3408
+ title: "Search Topolo applications the agent can reach",
3409
+ description: "Searches the credential-scoped application registry with a bounded page and continuation cursor. Returns actionService as the canonical selector for topolo_search_actions, plus granted permissions, API URL, and launch URL without loading the global platform catalog.",
3191
3410
  requiredScopes: [],
3192
3411
  destructive: false,
3193
3412
  inputSchema: {
3194
3413
  type: "object",
3195
- properties: {},
3414
+ properties: {
3415
+ query: { type: "string" },
3416
+ cursor: { type: "string" },
3417
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
3418
+ },
3196
3419
  additionalProperties: false
3197
3420
  },
3198
- handler: async (topolo) => {
3199
- const services = (await topolo.client.listServices()).map((entry) => {
3421
+ handler: async (topolo, args) => {
3422
+ const result = await topolo.client.searchServices({
3423
+ query: typeof args["query"] === "string" ? args["query"] : void 0,
3424
+ cursor: typeof args["cursor"] === "string" ? args["cursor"] : void 0,
3425
+ limit: typeof args["limit"] === "number" ? args["limit"] : void 0
3426
+ });
3427
+ const applications = result.services.map((entry) => {
3200
3428
  const id = displayAppId(entry);
3201
3429
  return {
3202
3430
  id,
3203
3431
  appId: entry.appId,
3432
+ actionService: entry.appId,
3204
3433
  slug: entry.slug,
3205
3434
  name: entry.name,
3206
3435
  apiBaseUrl: entry.apiBaseUrl,
@@ -3213,13 +3442,13 @@ var TOOLS = [
3213
3442
  permissions: [...entry.permissions]
3214
3443
  };
3215
3444
  }).sort((left, right) => left.id.localeCompare(right.id));
3216
- return { services };
3445
+ return { applications, page: result.page };
3217
3446
  }
3218
3447
  },
3219
3448
  {
3220
- name: "topolo_list_actions",
3221
- title: "List every Topolo action the agent can call",
3222
- 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.",
3449
+ name: "topolo_search_actions",
3450
+ title: "Search Topolo actions the agent can call",
3451
+ description: "Searches one application's credential-scoped actions with a bounded page and continuation cursor.",
3223
3452
  requiredScopes: [],
3224
3453
  destructive: false,
3225
3454
  inputSchema: {
@@ -3227,31 +3456,25 @@ var TOOLS = [
3227
3456
  properties: {
3228
3457
  service: {
3229
3458
  type: "string",
3230
- description: "Optional app id, slug, or name filter."
3231
- }
3459
+ description: "Required application selector. Prefer actionService from topolo_search_applications; canonical app id, slug, and name are also accepted."
3460
+ },
3461
+ query: { type: "string" },
3462
+ cursor: { type: "string" },
3463
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
3232
3464
  },
3465
+ required: ["service"],
3233
3466
  additionalProperties: false
3234
3467
  },
3235
- handler: async (topolo, args) => ({
3236
- actions: await topolo.client.listActions({
3237
- service: typeof args["service"] === "string" ? args["service"] : void 0
3238
- })
3239
- })
3240
- },
3241
- {
3242
- name: "topolo_list_applications",
3243
- title: "List every Topolo platform application",
3244
- description: "Returns the live Topolo application catalog from the platform registry. Reflects apps as they are registered/removed with no client rebuild. Authenticated callers in an entitled org also see the developer_only apps their role permits. Falls back to a bundled snapshot only if the registry is unreachable.",
3245
- requiredScopes: [],
3246
- destructive: false,
3247
- inputSchema: {
3248
- type: "object",
3249
- properties: {},
3250
- additionalProperties: false
3251
- },
3252
- handler: async (topolo) => ({
3253
- applications: await topolo.client.listServices()
3254
- })
3468
+ handler: async (topolo, args) => {
3469
+ const service = typeof args["service"] === "string" ? args["service"].trim() : "";
3470
+ if (!service) throw new Error("service is required for bounded action discovery.");
3471
+ return topolo.client.searchActions({
3472
+ service,
3473
+ query: typeof args["query"] === "string" ? args["query"] : void 0,
3474
+ cursor: typeof args["cursor"] === "string" ? args["cursor"] : void 0,
3475
+ limit: typeof args["limit"] === "number" ? args["limit"] : void 0
3476
+ });
3477
+ }
3255
3478
  },
3256
3479
  {
3257
3480
  name: "topolo_get_application",
@@ -3264,7 +3487,7 @@ var TOOLS = [
3264
3487
  properties: {
3265
3488
  application: {
3266
3489
  type: "string",
3267
- description: "Application identifier: app id, slug, or name from topolo_list_applications."
3490
+ description: "Application identifier: app id, slug, or name from topolo_search_applications."
3268
3491
  }
3269
3492
  },
3270
3493
  required: ["application"],
@@ -3275,18 +3498,98 @@ var TOOLS = [
3275
3498
  if (typeof application !== "string" || !application.trim()) {
3276
3499
  throw new Error("`application` must be a non-empty string.");
3277
3500
  }
3278
- const needle = application.trim().toLowerCase();
3279
- const catalog = await topolo.client.listServices();
3280
- const match = catalog.find(
3281
- (e) => e.appId.toLowerCase() === needle || e.slug && e.slug.toLowerCase() === needle || e.name && e.name.toLowerCase() === needle
3501
+ return { application: await topolo.client.getService(application) };
3502
+ }
3503
+ },
3504
+ {
3505
+ name: "topolo_get_action",
3506
+ title: "Describe one Topolo action",
3507
+ description: "Returns one credential-scoped action by exact action id.",
3508
+ requiredScopes: [],
3509
+ destructive: false,
3510
+ inputSchema: {
3511
+ type: "object",
3512
+ properties: { actionId: { type: "string", minLength: 1 } },
3513
+ required: ["actionId"],
3514
+ additionalProperties: false
3515
+ },
3516
+ handler: async (topolo, args) => {
3517
+ const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
3518
+ if (!actionId) throw new Error("actionId is required.");
3519
+ return { action: await topolo.client.getAction(actionId) };
3520
+ }
3521
+ },
3522
+ {
3523
+ name: "topolo_read_action",
3524
+ title: "Call one read-only Topolo action",
3525
+ description: "Calls one exact action id only when its published contract is read-only and needs no confirmation.",
3526
+ requiredScopes: [],
3527
+ destructive: false,
3528
+ inputSchema: {
3529
+ type: "object",
3530
+ properties: {
3531
+ actionId: { type: "string", minLength: 1 },
3532
+ input: { type: "object", additionalProperties: true },
3533
+ resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
3534
+ resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." }
3535
+ },
3536
+ required: ["actionId"],
3537
+ additionalProperties: false
3538
+ },
3539
+ handler: async (topolo, args) => {
3540
+ const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
3541
+ if (!actionId) throw new Error("actionId is required.");
3542
+ const input = args["input"];
3543
+ if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
3544
+ throw new Error("input must be an object.");
3545
+ }
3546
+ const action = await topolo.client.getAction(actionId);
3547
+ const agentAccess = action.agentAccess ?? (action.destructive ? "off" : action.readOnly && !action.requiresConfirmation ? "auto" : "confirm");
3548
+ if (action.method !== "GET" || !action.readOnly || action.destructive || action.requiresConfirmation || agentAccess !== "auto") {
3549
+ throw new Error(`Action "${actionId}" is not an automatic read-only action. Use topolo_call_action with confirmation.`);
3550
+ }
3551
+ const resource = optionalResourceContext(args);
3552
+ return topolo.client.callAction(
3553
+ actionId,
3554
+ input ?? {},
3555
+ resource ? { resource } : {}
3556
+ );
3557
+ }
3558
+ },
3559
+ {
3560
+ name: "topolo_call_action",
3561
+ title: "Call one Topolo action",
3562
+ description: "Calls one exact action id. Mutating actions require confirm:true.",
3563
+ requiredScopes: [],
3564
+ destructive: true,
3565
+ inputSchema: {
3566
+ type: "object",
3567
+ properties: {
3568
+ actionId: { type: "string", minLength: 1 },
3569
+ input: { type: "object", additionalProperties: true },
3570
+ resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
3571
+ resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." },
3572
+ confirm: { type: "boolean" }
3573
+ },
3574
+ required: ["actionId"],
3575
+ additionalProperties: false
3576
+ },
3577
+ handler: async (topolo, args) => {
3578
+ const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
3579
+ if (!actionId) throw new Error("actionId is required.");
3580
+ const input = args["input"];
3581
+ if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
3582
+ throw new Error("input must be an object.");
3583
+ }
3584
+ const resource = optionalResourceContext(args);
3585
+ return topolo.client.callAction(
3586
+ actionId,
3587
+ input ?? {},
3588
+ {
3589
+ confirm: args["confirm"] === true,
3590
+ ...resource ? { resource } : {}
3591
+ }
3282
3592
  );
3283
- if (!match) {
3284
- const known = catalog.map((e) => e.slug || e.appId).sort().filter((value) => typeof value === "string" && value.length > 0);
3285
- throw new Error(
3286
- `Unknown application "${application}". ${formatKnownValues(known, "applications")}`
3287
- );
3288
- }
3289
- return { application: match };
3290
3593
  }
3291
3594
  },
3292
3595
  {
@@ -3300,7 +3603,7 @@ var TOOLS = [
3300
3603
  properties: {
3301
3604
  application: {
3302
3605
  type: "string",
3303
- description: "Optional application ID from topolo_list_applications."
3606
+ description: "Optional application ID from topolo_search_applications."
3304
3607
  }
3305
3608
  },
3306
3609
  additionalProperties: false
@@ -3340,7 +3643,7 @@ var TOOLS = [
3340
3643
  properties: {
3341
3644
  application: {
3342
3645
  type: "string",
3343
- description: "Optional application ID from topolo_list_applications."
3646
+ description: "Optional application ID from topolo_search_applications."
3344
3647
  },
3345
3648
  failOn: {
3346
3649
  type: "string",
@@ -3374,7 +3677,7 @@ var TOOLS = [
3374
3677
  {
3375
3678
  name: "topolo_api_call",
3376
3679
  title: "Call any Topolo platform service",
3377
- description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call topolo_list_services first to discover callable app IDs; the SDK can also resolve live Auth-catalog app ids/slugs for newly registered API surfaces.",
3680
+ 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.",
3378
3681
  requiredScopes: [],
3379
3682
  destructive: true,
3380
3683
  inputSchema: {
@@ -3405,7 +3708,7 @@ var TOOLS = [
3405
3708
  },
3406
3709
  resourceType: {
3407
3710
  type: "string",
3408
- description: "Optional workspace resource alias from topolo_list_services apiKeyResources. Aliases are app-defined, e.g. brand, mailbox, venue, merchant, or any other cataloged alias."
3711
+ 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."
3409
3712
  },
3410
3713
  resourceId: {
3411
3714
  type: "string",
@@ -3423,7 +3726,7 @@ var TOOLS = [
3423
3726
  const service = args["service"];
3424
3727
  const path = args["path"];
3425
3728
  if (typeof service !== "string" || !service.trim()) {
3426
- throw new Error("service is required. Call topolo_list_services first to discover callable services.");
3729
+ throw new Error("service is required. Call topolo_search_applications first to discover callable services.");
3427
3730
  }
3428
3731
  if (typeof path !== "string" || !path) {
3429
3732
  throw new Error("path is required");
@@ -3437,12 +3740,7 @@ var TOOLS = [
3437
3740
  const normalisedQuery = query && typeof query === "object" && !Array.isArray(query) ? Object.fromEntries(
3438
3741
  Object.entries(query).map(([k, v]) => [k, String(v)])
3439
3742
  ) : void 0;
3440
- const resourceType = typeof args["resourceType"] === "string" ? args["resourceType"].trim() : "";
3441
- const resourceId = typeof args["resourceId"] === "string" ? args["resourceId"].trim() : "";
3442
- if (resourceType && !resourceId || !resourceType && resourceId) {
3443
- throw new Error("resourceType and resourceId must be provided together.");
3444
- }
3445
- const resource = resourceType && resourceId ? { resourceType, resourceId } : void 0;
3743
+ const resource = optionalResourceContext(args);
3446
3744
  return topolo.client.request({
3447
3745
  service: service.trim(),
3448
3746
  method,
@@ -3459,58 +3757,8 @@ function filterToolsByScopes(tools, scopes) {
3459
3757
  return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
3460
3758
  }
3461
3759
  async function buildTopoloTools(topolo, scopes, tools = TOOLS) {
3462
- const baseTools = filterToolsByScopes(tools, scopes);
3463
- const actions = await topolo.client.listActions().catch(() => []);
3464
- const actionTools = actionsToToolDefs(actions);
3465
- return filterToolsByScopes([...baseTools, ...actionTools], scopes);
3466
- }
3467
- function effectiveAgentAccess(a) {
3468
- if (a.agentAccess) return a.agentAccess;
3469
- if (a.destructive) return "off";
3470
- if (a.readOnly && !a.requiresConfirmation) return "auto";
3471
- return "confirm";
3472
- }
3473
- function actionsToToolDefs(actions) {
3474
- return actions.map((action) => ({ action, access: effectiveAgentAccess(action) })).filter(({ access }) => access !== "off").map(({ action, access }) => ({
3475
- name: action.toolName || toolNameForAction(action),
3476
- title: action.title,
3477
- description: action.description,
3478
- requiredScopes: action.requiredPermission ? [action.requiredPermission] : [],
3479
- destructive: access === "confirm",
3480
- inputSchema: actionInputSchema(action, access),
3481
- handler: async (topolo, args) => {
3482
- const confirm = Boolean(args["confirm"]);
3483
- const input = { ...args };
3484
- delete input["confirm"];
3485
- return topolo.client.callAction(action.actionId, input, { confirm });
3486
- }
3487
- }));
3488
- }
3489
- function toolNameForAction(action) {
3490
- const service = (action.appSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
3491
- const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
3492
- return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
3493
- }
3494
- function actionInputSchema(action, access = "auto") {
3495
- const schema = {
3496
- type: "object",
3497
- properties: {},
3498
- additionalProperties: false,
3499
- ...action.inputSchema
3500
- };
3501
- const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? { ...schema.properties } : {};
3502
- if (access === "confirm") {
3503
- properties.confirm = {
3504
- type: "boolean",
3505
- description: "Required for mutating actions. Explicit human-in-the-loop acknowledgement."
3506
- };
3507
- }
3508
- return {
3509
- ...schema,
3510
- type: "object",
3511
- properties,
3512
- additionalProperties: schema.additionalProperties ?? false
3513
- };
3760
+ void topolo;
3761
+ return filterToolsByScopes(tools, scopes);
3514
3762
  }
3515
3763
  function evaluateApplicationAuditGate(report, threshold) {
3516
3764
  const counts = {
@@ -3665,7 +3913,8 @@ function toolError(message) {
3665
3913
  // src/index.ts
3666
3914
  async function main() {
3667
3915
  const environment = normalizeEnvironment(process.env.TOPOLO_ENV);
3668
- let credential = resolveCredentialFromEnv();
3916
+ const environmentCredential = resolveCredentialFromEnv();
3917
+ let credential = environmentCredential;
3669
3918
  if (!credential) {
3670
3919
  credential = await refreshCredentialIfNeeded(environment);
3671
3920
  }
@@ -3687,7 +3936,8 @@ async function main() {
3687
3936
  ...agentName ? { agentName } : {}
3688
3937
  },
3689
3938
  serviceUrls,
3690
- requireConfirmForWrites: true
3939
+ requireConfirmForWrites: true,
3940
+ ...!environmentCredential ? { refreshCredential: () => refreshCredentialIfNeeded(environment, { force: true }) } : {}
3691
3941
  });
3692
3942
  const scopes = await loadScopes(topolo);
3693
3943
  const server = buildServer(topolo, scopes);