@topolo/mcp 0.6.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/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;
@@ -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
  ],
@@ -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 TopoloAuthError(
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
- res = await this.fetchImpl(urlStr, {
2159
- method,
2160
- headers,
2161
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2162
- signal
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) {
@@ -2238,9 +2272,8 @@ var TopoloClient = class _TopoloClient {
2238
2272
  );
2239
2273
  }
2240
2274
  async resolveLiveCallableService(serviceKey) {
2241
- const catalog = await this.listServices();
2242
- const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
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 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" });
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
- this.serviceCatalogCache = {
2357
- entries,
2358
- expiresAt: now + CATALOG_CACHE_TTL_MS
2359
- };
2360
- return entries;
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 listActions(options = {}) {
2369
- const now = Date.now();
2370
- if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
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
- ...options.service ? { query: { service: options.service } } : {}
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
- if (!options.service) {
2388
- this.actionCatalogCache = {
2389
- entries,
2390
- expiresAt: now + CATALOG_CACHE_TTL_MS
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");
2392
2451
  }
2393
- return entries;
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.");
2465
+ }
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 needle = normalizeActionLookupKey(action);
2397
- const actions = await this.listActions();
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
- `Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
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
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,6 +2756,8 @@ 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,
@@ -2601,6 +2769,7 @@ function normalizeActionCatalogEntry(item) {
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) => {
@@ -3058,7 +3223,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
3058
3223
  return profileForEnvironment(config, environment).credential ?? null;
3059
3224
  }
3060
3225
  var REFRESH_SKEW_SECONDS = 60;
3061
- async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
3226
+ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
3062
3227
  const prefix = envVarPrefix(environment);
3063
3228
  if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
3064
3229
  return resolveCredential(environment);
@@ -3069,7 +3234,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
3069
3234
  if (!cred || cred.kind !== "access_token") return cred ?? null;
3070
3235
  if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
3071
3236
  const now = Math.floor(Date.now() / 1e3);
3072
- if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
3237
+ if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
3073
3238
  try {
3074
3239
  const oauth = new TopoloOAuth({
3075
3240
  serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
@@ -3084,9 +3249,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
3084
3249
  ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
3085
3250
  expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
3086
3251
  };
3087
- await writeConfig(
3088
- environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed })
3089
- );
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);
3090
3266
  return refreshed;
3091
3267
  } catch {
3092
3268
  return cred;
@@ -3170,6 +3346,15 @@ function splitService(servicePart) {
3170
3346
  // src/tools.ts
3171
3347
  var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
3172
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
+ }
3173
3358
  var TOOLS = [
3174
3359
  {
3175
3360
  name: "topolo_whoami",
@@ -3185,18 +3370,27 @@ var TOOLS = [
3185
3370
  handler: async (topolo) => topolo.identity.whoami()
3186
3371
  },
3187
3372
  {
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.",
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.",
3191
3376
  requiredScopes: [],
3192
3377
  destructive: false,
3193
3378
  inputSchema: {
3194
3379
  type: "object",
3195
- properties: {},
3380
+ properties: {
3381
+ query: { type: "string" },
3382
+ cursor: { type: "string" },
3383
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
3384
+ },
3196
3385
  additionalProperties: false
3197
3386
  },
3198
- handler: async (topolo) => {
3199
- const services = (await topolo.client.listServices()).map((entry) => {
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) => {
3200
3394
  const id = displayAppId(entry);
3201
3395
  return {
3202
3396
  id,
@@ -3213,13 +3407,13 @@ var TOOLS = [
3213
3407
  permissions: [...entry.permissions]
3214
3408
  };
3215
3409
  }).sort((left, right) => left.id.localeCompare(right.id));
3216
- return { services };
3410
+ return { applications, page: result.page };
3217
3411
  }
3218
3412
  },
3219
3413
  {
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.",
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.",
3223
3417
  requiredScopes: [],
3224
3418
  destructive: false,
3225
3419
  inputSchema: {
@@ -3227,31 +3421,25 @@ var TOOLS = [
3227
3421
  properties: {
3228
3422
  service: {
3229
3423
  type: "string",
3230
- description: "Optional app id, slug, or name filter."
3231
- }
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 }
3232
3429
  },
3430
+ required: ["service"],
3233
3431
  additionalProperties: false
3234
3432
  },
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
- })
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
+ }
3255
3443
  },
3256
3444
  {
3257
3445
  name: "topolo_get_application",
@@ -3264,7 +3452,7 @@ var TOOLS = [
3264
3452
  properties: {
3265
3453
  application: {
3266
3454
  type: "string",
3267
- description: "Application identifier: app id, slug, or name from topolo_list_applications."
3455
+ description: "Application identifier: app id, slug, or name from topolo_search_applications."
3268
3456
  }
3269
3457
  },
3270
3458
  required: ["application"],
@@ -3275,18 +3463,98 @@ var TOOLS = [
3275
3463
  if (typeof application !== "string" || !application.trim()) {
3276
3464
  throw new Error("`application` must be a non-empty string.");
3277
3465
  }
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
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
+ }
3282
3557
  );
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
3558
  }
3291
3559
  },
3292
3560
  {
@@ -3300,7 +3568,7 @@ var TOOLS = [
3300
3568
  properties: {
3301
3569
  application: {
3302
3570
  type: "string",
3303
- description: "Optional application ID from topolo_list_applications."
3571
+ description: "Optional application ID from topolo_search_applications."
3304
3572
  }
3305
3573
  },
3306
3574
  additionalProperties: false
@@ -3340,7 +3608,7 @@ var TOOLS = [
3340
3608
  properties: {
3341
3609
  application: {
3342
3610
  type: "string",
3343
- description: "Optional application ID from topolo_list_applications."
3611
+ description: "Optional application ID from topolo_search_applications."
3344
3612
  },
3345
3613
  failOn: {
3346
3614
  type: "string",
@@ -3374,7 +3642,7 @@ var TOOLS = [
3374
3642
  {
3375
3643
  name: "topolo_api_call",
3376
3644
  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.",
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.",
3378
3646
  requiredScopes: [],
3379
3647
  destructive: true,
3380
3648
  inputSchema: {
@@ -3405,7 +3673,7 @@ var TOOLS = [
3405
3673
  },
3406
3674
  resourceType: {
3407
3675
  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."
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."
3409
3677
  },
3410
3678
  resourceId: {
3411
3679
  type: "string",
@@ -3423,7 +3691,7 @@ var TOOLS = [
3423
3691
  const service = args["service"];
3424
3692
  const path = args["path"];
3425
3693
  if (typeof service !== "string" || !service.trim()) {
3426
- throw new Error("service is required. Call topolo_list_services first to discover callable services.");
3694
+ throw new Error("service is required. Call topolo_search_applications first to discover callable services.");
3427
3695
  }
3428
3696
  if (typeof path !== "string" || !path) {
3429
3697
  throw new Error("path is required");
@@ -3437,12 +3705,7 @@ var TOOLS = [
3437
3705
  const normalisedQuery = query && typeof query === "object" && !Array.isArray(query) ? Object.fromEntries(
3438
3706
  Object.entries(query).map(([k, v]) => [k, String(v)])
3439
3707
  ) : 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;
3708
+ const resource = optionalResourceContext(args);
3446
3709
  return topolo.client.request({
3447
3710
  service: service.trim(),
3448
3711
  method,
@@ -3459,58 +3722,8 @@ function filterToolsByScopes(tools, scopes) {
3459
3722
  return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
3460
3723
  }
3461
3724
  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
- };
3725
+ void topolo;
3726
+ return filterToolsByScopes(tools, scopes);
3514
3727
  }
3515
3728
  function evaluateApplicationAuditGate(report, threshold) {
3516
3729
  const counts = {
@@ -3665,7 +3878,8 @@ function toolError(message) {
3665
3878
  // src/index.ts
3666
3879
  async function main() {
3667
3880
  const environment = normalizeEnvironment(process.env.TOPOLO_ENV);
3668
- let credential = resolveCredentialFromEnv();
3881
+ const environmentCredential = resolveCredentialFromEnv();
3882
+ let credential = environmentCredential;
3669
3883
  if (!credential) {
3670
3884
  credential = await refreshCredentialIfNeeded(environment);
3671
3885
  }
@@ -3687,7 +3901,8 @@ async function main() {
3687
3901
  ...agentName ? { agentName } : {}
3688
3902
  },
3689
3903
  serviceUrls,
3690
- requireConfirmForWrites: true
3904
+ requireConfirmForWrites: true,
3905
+ ...!environmentCredential ? { refreshCredential: () => refreshCredentialIfNeeded(environment, { force: true }) } : {}
3691
3906
  });
3692
3907
  const scopes = await loadScopes(topolo);
3693
3908
  const server = buildServer(topolo, scopes);