@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/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 SERVICES = {
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 service entitlement from TopoloAuth rather than inventing parallel auth state.",
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
- "Service slug and Auth service registration are documented for protected services"
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: "service-registration-and-scopes",
1687
- title: "Register protected APIs as platform services",
1688
- summary: "Callable app APIs need a stable service ID, service slug, permissions, role bundles, and API-key scopes before agents or users rely on them.",
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 service catalog contains the service ID and slug",
1692
- "Permissions, role bundles, and API-key scopes exist for the service",
1693
- "SDK, CLI, and MCP service discovery include the callable production API",
1694
- "CloudControl production HTTP targets classify agent reachability with sdk_service_id or agent_callable:false"
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 sdk_service_id for every agent-callable API target",
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 service IDs through generated DEFAULT_SERVICE_URLS and live catalog discovery",
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 "service-registration-and-scopes": {
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 service ID is registered for this callable API surface.", evidence);
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 DEFAULT_SERVICE_URLS = SERVICES;
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 DEFAULT_SERVICE_URLS[service];
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 = `TOPOLO_SERVICE_URL_${key.replace(/\./g, "_").toUpperCase()}`;
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 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) {
@@ -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: DEFAULT_SERVICE_URLS[serviceKey] };
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 service "${service}". Run service discovery first and pass a callable service id or slug.`
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 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");
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 entries;
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
- const service = entry.serviceSlug || entry.appId;
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
- serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
2599
- serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
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
- // src/auth.ts
2776
- function resolveCredentialFromEnv() {
2777
- const apiKey = process.env.TOPOLO_API_KEY;
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
- return null;
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 resolveServiceUrlsFromEnv() {
2784
- const out = {};
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
- out[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
3287
+ merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
2788
3288
  }
2789
3289
  }
2790
- return Object.keys(out).length > 0 ? out : void 0;
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: "topolo_list_services",
2856
- title: "List every Topolo service the agent can reach",
2857
- 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.",
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 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) => {
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 { services };
3410
+ return { applications, page: result.page };
2884
3411
  }
2885
3412
  },
2886
3413
  {
2887
- name: "topolo_list_actions",
2888
- title: "List every Topolo action the agent can call",
2889
- 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.",
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: "Optional app id, slug, or name filter."
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
- actions: await topolo.client.listActions({
2904
- service: typeof args["service"] === "string" ? args["service"] : void 0
2905
- })
2906
- })
2907
- },
2908
- {
2909
- name: "topolo_list_applications",
2910
- title: "List every Topolo platform application",
2911
- 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.",
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 topolo_list_applications."
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
- const needle = application.trim().toLowerCase();
2946
- const catalog = await topolo.client.listServices();
2947
- const match = catalog.find(
2948
- (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
+ }
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 topolo_list_applications."
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 topolo_list_applications."
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 topolo_list_services first to discover callable service IDs; the SDK can also resolve live Auth-catalog service 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.",
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 service ID from the platform registry."
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 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."
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 topolo_list_services first to discover callable services.");
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 resourceType = typeof args["resourceType"] === "string" ? args["resourceType"].trim() : "";
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
- const baseTools = filterToolsByScopes(tools, scopes);
3130
- const actions = await topolo.client.listActions().catch(() => []);
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(DEFAULT_SERVICE_URLS, candidate)) {
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 = dirname(fileURLToPath(import.meta.url));
3798
+ const here = dirname2(fileURLToPath(import.meta.url));
3253
3799
  const candidates = [
3254
- join(here, "package.json"),
3255
- join(here, "..", "package.json")
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 credential = resolveCredentialFromEnv();
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: TOPOLO_API_KEY or TOPOLO_ACCESS_TOKEN must be set in the environment.\n"
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 agentName = process.env.TOPOLO_AGENT_NAME;
3342
- const serviceUrls = resolveServiceUrlsFromEnv();
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
  }