@topolo/mcp 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,7 +123,7 @@ yet. GET requests work without confirmation; mutating HTTP methods must pass
123
123
  `confirm: true` or the SDK rejects the call. Backing services still enforce
124
124
  permissions for every request. The tool schema intentionally does not embed a
125
125
  large service enum; agents should call `topolo_list_services` first, then pass a
126
- service ID/slug from that credential-scoped result. New services become callable
126
+ app ID/slug from that credential-scoped result. New services become callable
127
127
  without an MCP release once Auth grants the organization and credential the
128
128
  relevant permissions, and new actions become typed tools without an MCP release
129
129
  once the application publishes them.
package/dist/dispatch.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  APPLICATION_REQUIREMENTS,
7
7
  APPLICATION_REQUIREMENTS_VERSION,
8
8
  APPLICATIONS,
9
- DEFAULT_SERVICE_URLS,
9
+ DEFAULT_APP_URLS,
10
10
  auditAllApplicationRequirements,
11
11
  applicationRequirementScopes,
12
12
  requirementsForApplication,
@@ -257,7 +257,7 @@ var TOOLS = [
257
257
  {
258
258
  name: "topolo_api_call",
259
259
  title: "Call any Topolo platform service",
260
- 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.",
260
+ 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.",
261
261
  requiredScopes: [],
262
262
  destructive: true,
263
263
  inputSchema: {
@@ -265,7 +265,7 @@ var TOOLS = [
265
265
  properties: {
266
266
  service: {
267
267
  type: "string",
268
- description: "Target service ID from the platform registry."
268
+ description: "Target app ID from the platform registry."
269
269
  },
270
270
  method: {
271
271
  type: "string",
@@ -370,7 +370,7 @@ function actionsToToolDefs(actions) {
370
370
  }));
371
371
  }
372
372
  function toolNameForAction(action) {
373
- const service = (action.serviceSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
373
+ const service = (action.appSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
374
374
  const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
375
375
  return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
376
376
  }
@@ -435,7 +435,7 @@ function displayAppId(entry) {
435
435
  for (const value of [entry.slug, entry.name, entry.appId]) {
436
436
  if (typeof value !== "string" || !value.trim()) continue;
437
437
  for (const candidate of appIdCandidates(value)) {
438
- if (Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, candidate)) {
438
+ if (Object.prototype.hasOwnProperty.call(DEFAULT_APP_URLS, candidate)) {
439
439
  return candidate;
440
440
  }
441
441
  }
package/dist/http.js CHANGED
@@ -15814,7 +15814,7 @@ import {
15814
15814
  APPLICATION_REQUIREMENTS,
15815
15815
  APPLICATION_REQUIREMENTS_VERSION,
15816
15816
  APPLICATIONS,
15817
- DEFAULT_SERVICE_URLS,
15817
+ DEFAULT_APP_URLS,
15818
15818
  auditAllApplicationRequirements,
15819
15819
  applicationRequirementScopes,
15820
15820
  requirementsForApplication,
@@ -16065,7 +16065,7 @@ var TOOLS = [
16065
16065
  {
16066
16066
  name: "topolo_api_call",
16067
16067
  title: "Call any Topolo platform service",
16068
- 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.",
16068
+ 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.",
16069
16069
  requiredScopes: [],
16070
16070
  destructive: true,
16071
16071
  inputSchema: {
@@ -16073,7 +16073,7 @@ var TOOLS = [
16073
16073
  properties: {
16074
16074
  service: {
16075
16075
  type: "string",
16076
- description: "Target service ID from the platform registry."
16076
+ description: "Target app ID from the platform registry."
16077
16077
  },
16078
16078
  method: {
16079
16079
  type: "string",
@@ -16178,7 +16178,7 @@ function actionsToToolDefs(actions) {
16178
16178
  }));
16179
16179
  }
16180
16180
  function toolNameForAction(action) {
16181
- const service = (action.serviceSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
16181
+ const service = (action.appSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
16182
16182
  const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
16183
16183
  return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
16184
16184
  }
@@ -16243,7 +16243,7 @@ function displayAppId(entry) {
16243
16243
  for (const value of [entry.slug, entry.name, entry.appId]) {
16244
16244
  if (typeof value !== "string" || !value.trim()) continue;
16245
16245
  for (const candidate of appIdCandidates(value)) {
16246
- if (Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, candidate)) {
16246
+ if (Object.prototype.hasOwnProperty.call(DEFAULT_APP_URLS, candidate)) {
16247
16247
  return candidate;
16248
16248
  }
16249
16249
  }
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ var TopoloHttpError = class extends TopoloSdkError {
58
58
  this.path = path;
59
59
  }
60
60
  };
61
- var SERVICES = {
61
+ var APP_API_URLS = {
62
62
  agent: "https://agent.topolo.app",
63
63
  auth: "https://auth.topolo.app",
64
64
  backup: "https://backup.topolo.app",
@@ -1645,12 +1645,12 @@ var APPLICATION_REQUIREMENTS = [
1645
1645
  {
1646
1646
  id: "shared-auth-boundary",
1647
1647
  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.",
1648
+ summary: "Applications must derive identity, organization, scopes, and app entitlement from TopoloAuth rather than inventing parallel auth state.",
1649
1649
  appliesTo: ["browser", "api", "tooling", "agent_surface"],
1650
1650
  evidence: [
1651
1651
  "@topolo/auth-client or @topolo-io/worker-runtime is used where applicable",
1652
1652
  "No public command, tool, or SDK method accepts orgId",
1653
- "Service slug and Auth service registration are documented for protected services"
1653
+ "App slug and Auth app registration are documented for protected APIs"
1654
1654
  ],
1655
1655
  implementation: [
1656
1656
  "Use shared auth/runtime packages instead of copying token parsing, gateway, or browser storage flows",
@@ -1683,20 +1683,20 @@ var APPLICATION_REQUIREMENTS = [
1683
1683
  ]
1684
1684
  },
1685
1685
  {
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.",
1686
+ id: "app-registration-and-scopes",
1687
+ title: "Register protected APIs as platform apps",
1688
+ 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
1689
  appliesTo: ["api"],
1690
1690
  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"
1691
+ "Auth app catalog contains the app ID and slug",
1692
+ "Permissions, role bundles, and API-key scopes exist for the app",
1693
+ "SDK, CLI, and MCP app discovery include the callable production API",
1694
+ "CloudControl production HTTP targets classify agent reachability with sdk_app_id or agent_callable:false"
1695
1695
  ],
1696
1696
  implementation: [
1697
- "Add production Worker api_url metadata and sdk_service_id for every agent-callable API target",
1697
+ "Add production Worker api_url metadata and sdk_app_id for every agent-callable API target",
1698
1698
  "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",
1699
+ "Expose stable app IDs through generated DEFAULT_APP_URLS and live catalog discovery",
1700
1700
  "Keep typed SDK/CLI/MCP surfaces in sync when an API contract stabilizes"
1701
1701
  ]
1702
1702
  },
@@ -1902,7 +1902,7 @@ function evaluateRequirement(application, requirement) {
1902
1902
  "Catalog metadata cannot verify shared shell, launcher, and account menu adoption.",
1903
1903
  evidence
1904
1904
  );
1905
- case "service-registration-and-scopes": {
1905
+ case "app-registration-and-scopes": {
1906
1906
  if (application.services.length > 0) {
1907
1907
  evidence.push(`services=${application.services.join(",")}`);
1908
1908
  return finding(
@@ -1912,7 +1912,7 @@ function evaluateRequirement(application, requirement) {
1912
1912
  evidence
1913
1913
  );
1914
1914
  }
1915
- return finding(requirement, "missing", "No SDK service ID is registered for this callable API surface.", evidence);
1915
+ return finding(requirement, "missing", "No SDK app API id is registered for this callable API surface.", evidence);
1916
1916
  }
1917
1917
  case "organization-scoped-data":
1918
1918
  return finding(
@@ -1997,7 +1997,7 @@ function statusRank(status) {
1997
1997
  if (status === "partial") return 1;
1998
1998
  return 2;
1999
1999
  }
2000
- var DEFAULT_SERVICE_URLS = SERVICES;
2000
+ var DEFAULT_APP_URLS = APP_API_URLS;
2001
2001
  function resolveCatalogServiceUrl(entry, options = {}) {
2002
2002
  const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
2003
2003
  const override = resolveServiceUrlOverride(keys, options.overrides);
@@ -2009,7 +2009,7 @@ function resolveCatalogServiceUrl(entry, options = {}) {
2009
2009
  function resolveServiceUrl(service, overrides) {
2010
2010
  const override = resolveServiceUrlOverride([service], overrides);
2011
2011
  if (override) return override;
2012
- return DEFAULT_SERVICE_URLS[service];
2012
+ return DEFAULT_APP_URLS[service];
2013
2013
  }
2014
2014
  function catalogServiceKeyCandidates(entry, preferred) {
2015
2015
  const values = [preferred, entry.slug, entry.name, entry.appId].filter(
@@ -2040,7 +2040,7 @@ function resolveServiceUrlOverride(keys, overrides) {
2040
2040
  if (envStyle) return envStyle;
2041
2041
  }
2042
2042
  for (const key of keys) {
2043
- const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, "_").toUpperCase()}`;
2043
+ const envKey = `TOPOLO_APP_URL_${key.replace(/\./g, "_").toUpperCase()}`;
2044
2044
  const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2045
2045
  if (envValue) return envValue;
2046
2046
  }
@@ -2225,7 +2225,7 @@ var TopoloClient = class _TopoloClient {
2225
2225
  const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
2226
2226
  if (derived) return { serviceKey, baseUrl: derived };
2227
2227
  }
2228
- return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS[serviceKey] };
2228
+ return { serviceKey, baseUrl: DEFAULT_APP_URLS[serviceKey] };
2229
2229
  }
2230
2230
  const live = await this.resolveLiveCallableService(serviceKey);
2231
2231
  if (live) {
@@ -2234,7 +2234,7 @@ var TopoloClient = class _TopoloClient {
2234
2234
  }
2235
2235
  throw new TopoloSdkError(
2236
2236
  "unknown_service",
2237
- `Unknown Topolo service "${service}". Run service discovery first and pass a callable service id or slug.`
2237
+ `Unknown Topolo app API "${service}". Run app discovery first and pass a callable app id or slug.`
2238
2238
  );
2239
2239
  }
2240
2240
  async resolveLiveCallableService(serviceKey) {
@@ -2407,7 +2407,7 @@ var TopoloClient = class _TopoloClient {
2407
2407
  async callAction(action, input = {}, options = {}) {
2408
2408
  const entry = await this.getAction(action);
2409
2409
  const mapped = mapActionInput(entry, input);
2410
- const service = entry.serviceSlug || entry.appId;
2410
+ const service = entry.appSlug || entry.appId;
2411
2411
  return this.request({
2412
2412
  service,
2413
2413
  method: entry.method,
@@ -2595,8 +2595,8 @@ function normalizeActionCatalogEntry(item) {
2595
2595
  title: pickActionString(a.title) ?? name,
2596
2596
  description: pickActionString(a.description) ?? "",
2597
2597
  appId,
2598
- serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
2599
- serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
2598
+ appSlug: pickActionString(a.app_slug) ?? pickActionString(a.appSlug) ?? null,
2599
+ appName: pickActionString(a.app_name) ?? pickActionString(a.appName) ?? null,
2600
2600
  method,
2601
2601
  path,
2602
2602
  permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
@@ -2711,6 +2711,7 @@ var CrmModule = class {
2711
2711
  constructor(client) {
2712
2712
  this.client = client;
2713
2713
  }
2714
+ client;
2714
2715
  async listContacts(options = {}) {
2715
2716
  const res = await this.client.request({
2716
2717
  service: "crm",
@@ -2756,6 +2757,7 @@ var IdentityModule = class {
2756
2757
  constructor(client) {
2757
2758
  this.client = client;
2758
2759
  }
2760
+ client;
2759
2761
  /** Returns the resolved user, organization, and permission set for the
2760
2762
  * current credential. Equivalent to `TopoloClient.introspect()` — exposed
2761
2763
  * on this module for symmetry with the other domain modules. */
@@ -2772,22 +2774,353 @@ function createTopolo(options) {
2772
2774
  };
2773
2775
  }
2774
2776
 
2775
- // src/auth.ts
2776
- function resolveCredentialFromEnv() {
2777
- const apiKey = process.env.TOPOLO_API_KEY;
2777
+ // ../topolo-sdk/dist/node/credential-store.js
2778
+ import { mkdir, readFile, writeFile, chmod } from "fs/promises";
2779
+ import { existsSync } from "fs";
2780
+ import { homedir, platform } from "os";
2781
+ import { dirname, join } from "path";
2782
+ var TopoloSdkError2 = class extends Error {
2783
+ code;
2784
+ constructor(code, message) {
2785
+ super(message);
2786
+ this.name = "TopoloSdkError";
2787
+ this.code = code;
2788
+ }
2789
+ };
2790
+ var TopoloAuthError2 = class extends TopoloSdkError2 {
2791
+ constructor(message, code = "auth_error") {
2792
+ super(code, message);
2793
+ this.name = "TopoloAuthError";
2794
+ }
2795
+ };
2796
+ var TopoloHttpError2 = class extends TopoloSdkError2 {
2797
+ status;
2798
+ body;
2799
+ service;
2800
+ path;
2801
+ constructor(service, path, status, body, message) {
2802
+ super("http_error", message ?? `HTTP ${status} from ${service}${path}`);
2803
+ this.name = "TopoloHttpError";
2804
+ this.status = status;
2805
+ this.body = body;
2806
+ this.service = service;
2807
+ this.path = path;
2808
+ }
2809
+ };
2810
+ var APP_API_URLS2 = {
2811
+ agent: "https://agent.topolo.app",
2812
+ auth: "https://auth.topolo.app",
2813
+ backup: "https://backup.topolo.app",
2814
+ books: "https://books.topolo.app",
2815
+ bugfix: "https://bugfix.topolo.app",
2816
+ bytes: "https://bytes.topolo.app",
2817
+ calendar: "https://calendar.topolo.app",
2818
+ capacity: "https://capacity.topolo.app",
2819
+ chat: "https://chat.topolo.app",
2820
+ commerce: "https://commerce.topolo.app",
2821
+ compose: "https://compose.topolo.app",
2822
+ consent: "https://consent.topolo.app",
2823
+ crm: "https://crm.topolo.app",
2824
+ developers: "https://developers.topolo.app",
2825
+ director: "https://director.topolo.app",
2826
+ feed: "https://feed.topolo.app",
2827
+ flow: "https://flow.topolo.app",
2828
+ forecast: "https://forecast.topolo.app",
2829
+ forms: "https://forms.topolo.app",
2830
+ home: "https://home.topolo.app",
2831
+ insights: "https://insights.topolo.app",
2832
+ inventory: "https://inventory.topolo.app",
2833
+ learn: "https://learn.topolo.app",
2834
+ localize: "https://localize.topolo.app",
2835
+ mail: "https://mail.topolo.app",
2836
+ mdm: "https://mdm.topolo.app",
2837
+ messages: "https://messages.topolo.app",
2838
+ nexus: "https://nexus.topolo.app",
2839
+ notify: "https://notify.topolo.app",
2840
+ observability_watch: "https://observability-watch.topolo.app",
2841
+ one: "https://www.topolo.app",
2842
+ p2p: "https://p2p.topolo.app",
2843
+ pay: "https://pay.topolo.app",
2844
+ people: "https://people.topolo.app",
2845
+ "quro.api": "https://api.ol0.me",
2846
+ "quro.redirect": "https://ol0.me",
2847
+ roadmapper: "https://roadmapper.topolo.app",
2848
+ sign: "https://sign.topolo.app",
2849
+ social_studio: "https://studio.topolo.app",
2850
+ socialize: "https://socialize.topolo.app",
2851
+ spaces: "https://spaces.topolo.app",
2852
+ status: "https://status.topolo.app",
2853
+ success: "https://success.topolo.app",
2854
+ support: "https://support.topolo.app",
2855
+ survey: "https://survey.topolo.app",
2856
+ voice: "https://voice.topolo.app",
2857
+ web: "https://web.topolo.app"
2858
+ };
2859
+ var DEFAULT_APP_URLS2 = APP_API_URLS2;
2860
+ function resolveServiceUrl2(service, overrides) {
2861
+ const override = resolveServiceUrlOverride2([service], overrides);
2862
+ if (override) return override;
2863
+ return DEFAULT_APP_URLS2[service];
2864
+ }
2865
+ function resolveServiceUrlOverride2(keys, overrides) {
2866
+ for (const key of keys) {
2867
+ const direct = overrides?.[key];
2868
+ if (direct) return direct;
2869
+ const envStyleKey = key.replace(/\./g, "_");
2870
+ const envStyle = overrides?.[envStyleKey];
2871
+ if (envStyle) return envStyle;
2872
+ }
2873
+ for (const key of keys) {
2874
+ const envKey = `TOPOLO_APP_URL_${key.replace(/\./g, "_").toUpperCase()}`;
2875
+ const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2876
+ if (envValue) return envValue;
2877
+ }
2878
+ return null;
2879
+ }
2880
+ var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
2881
+ var TopoloOAuth = class {
2882
+ baseUrl;
2883
+ fetchImpl;
2884
+ constructor(options = {}) {
2885
+ this.baseUrl = resolveServiceUrl2("auth", options.serviceUrls);
2886
+ this.fetchImpl = options.fetch ?? fetch;
2887
+ }
2888
+ /**
2889
+ * RFC 8628 device authorization grant — starts the flow by asking the server
2890
+ * for a device_code + user_code. The caller prints the user_code +
2891
+ * verification_uri and then polls `pollDeviceToken` until approval.
2892
+ */
2893
+ async requestDeviceCode(params) {
2894
+ const body = new URLSearchParams();
2895
+ body.set("client_id", params.clientId);
2896
+ if (params.scope) {
2897
+ body.set("scope", Array.isArray(params.scope) ? params.scope.join(" ") : params.scope);
2898
+ }
2899
+ return this.#postForm("/api/developer-oauth/device_authorization", body);
2900
+ }
2901
+ /**
2902
+ * Polls the token endpoint once. Returns the token pair on success, or
2903
+ * throws a `TopoloAuthError` whose `code` is one of the RFC 8628 poll
2904
+ * states: `authorization_pending`, `slow_down`, `access_denied`,
2905
+ * `expired_token`. Callers should back off on `slow_down`, wait at least
2906
+ * one `interval` on `authorization_pending`, and give up on the rest.
2907
+ */
2908
+ async pollDeviceToken(params) {
2909
+ const body = new URLSearchParams();
2910
+ body.set("grant_type", DEVICE_GRANT);
2911
+ body.set("client_id", params.clientId);
2912
+ body.set("device_code", params.deviceCode);
2913
+ if (params.clientSecret) body.set("client_secret", params.clientSecret);
2914
+ return this.#postForm("/api/developer-oauth/token", body);
2915
+ }
2916
+ /** Exchange an authorization code (with PKCE) for tokens. */
2917
+ async exchangeAuthorizationCode(params) {
2918
+ const body = new URLSearchParams();
2919
+ body.set("grant_type", "authorization_code");
2920
+ body.set("client_id", params.clientId);
2921
+ body.set("code", params.code);
2922
+ body.set("redirect_uri", params.redirectUri);
2923
+ if (params.codeVerifier) body.set("code_verifier", params.codeVerifier);
2924
+ if (params.clientSecret) body.set("client_secret", params.clientSecret);
2925
+ return this.#postForm("/api/developer-oauth/token", body);
2926
+ }
2927
+ /** Rotate a refresh token for a new token pair. */
2928
+ async refreshToken(params) {
2929
+ const body = new URLSearchParams();
2930
+ body.set("grant_type", "refresh_token");
2931
+ body.set("client_id", params.clientId);
2932
+ body.set("refresh_token", params.refreshToken);
2933
+ if (params.clientSecret) body.set("client_secret", params.clientSecret);
2934
+ return this.#postForm("/api/developer-oauth/token", body);
2935
+ }
2936
+ async #postForm(path, body) {
2937
+ const url = new URL(path, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
2938
+ const res = await this.fetchImpl(url.toString(), {
2939
+ method: "POST",
2940
+ headers: {
2941
+ "Content-Type": "application/x-www-form-urlencoded",
2942
+ Accept: "application/json"
2943
+ },
2944
+ body: body.toString()
2945
+ });
2946
+ const contentType = res.headers.get("Content-Type") ?? "";
2947
+ const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
2948
+ if (!res.ok) {
2949
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
2950
+ const err = parsed;
2951
+ throw new TopoloAuthError2(err.error_description || err.error, err.error);
2952
+ }
2953
+ throw new TopoloHttpError2("auth", path, res.status, parsed);
2954
+ }
2955
+ return parsed;
2956
+ }
2957
+ };
2958
+ var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
2959
+ production: {
2960
+ developers: "https://developers.topolo.app"
2961
+ },
2962
+ staging: {
2963
+ auth: "https://auth.stg.topolo.us",
2964
+ developers: "https://developers.stg.topolo.us",
2965
+ seed: "https://seed.stg.topolo.us"
2966
+ },
2967
+ development: {
2968
+ auth: "https://auth.topolo.dev",
2969
+ developers: "https://developers.topolo.dev"
2970
+ },
2971
+ local: {}
2972
+ };
2973
+ function configPath() {
2974
+ const override = process.env.TOPOLO_CONFIG_PATH;
2975
+ if (override) return override;
2976
+ const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
2977
+ return join(base, "topolo", "config.json");
2978
+ }
2979
+ async function readConfig() {
2980
+ const path = configPath();
2981
+ if (!existsSync(path)) return {};
2982
+ try {
2983
+ const raw = await readFile(path, "utf8");
2984
+ return JSON.parse(raw);
2985
+ } catch {
2986
+ return {};
2987
+ }
2988
+ }
2989
+ async function writeConfig(config) {
2990
+ const path = configPath();
2991
+ await mkdir(dirname(path), { recursive: true });
2992
+ await writeFile(path, JSON.stringify(config, null, 2) + "\n", "utf8");
2993
+ if (platform() !== "win32") {
2994
+ try {
2995
+ await chmod(path, 384);
2996
+ } catch {
2997
+ }
2998
+ }
2999
+ }
3000
+ function normalizeEnvironment(value) {
3001
+ const normalized = (value ?? "production").trim().toLowerCase();
3002
+ if (normalized === "" || normalized === "production" || normalized === "prod") {
3003
+ return "production";
3004
+ }
3005
+ if (normalized === "staging" || normalized === "stage" || normalized === "stg") {
3006
+ return "staging";
3007
+ }
3008
+ if (normalized === "development" || normalized === "dev") {
3009
+ return "development";
3010
+ }
3011
+ if (normalized === "local" || normalized === "localhost") {
3012
+ return "local";
3013
+ }
3014
+ throw new Error(
3015
+ `Unknown Topolo environment "${value}". Expected production, staging, development, or local.`
3016
+ );
3017
+ }
3018
+ function selectedEnvironment() {
3019
+ return normalizeEnvironment(process.env.TOPOLO_ENV);
3020
+ }
3021
+ function envVarPrefix(environment) {
3022
+ if (environment === "production") return "TOPOLO_PRODUCTION";
3023
+ if (environment === "staging") return "TOPOLO_STAGING";
3024
+ if (environment === "development") return "TOPOLO_DEVELOPMENT";
3025
+ return "TOPOLO_LOCAL";
3026
+ }
3027
+ function profileForEnvironment(config, environment = selectedEnvironment()) {
3028
+ const profile = config.profiles?.[environment] ?? {};
3029
+ if (environment === "production") {
3030
+ return {
3031
+ credential: config.credential,
3032
+ oauthClientId: config.oauthClientId,
3033
+ ...profile,
3034
+ serviceUrls: {
3035
+ ...config.serviceUrls ?? {},
3036
+ ...profile.serviceUrls ?? {}
3037
+ },
3038
+ agentName: profile.agentName ?? config.agentName
3039
+ };
3040
+ }
3041
+ return profile;
3042
+ }
3043
+ function writeProfileConfig(config, environment, profile) {
3044
+ const profiles = { ...config.profiles ?? {} };
3045
+ profiles[environment] = {
3046
+ ...profiles[environment] ?? {},
3047
+ ...profile
3048
+ };
3049
+ return { ...config, profiles };
3050
+ }
3051
+ async function resolveCredential(environment = selectedEnvironment()) {
3052
+ const prefix = envVarPrefix(environment);
3053
+ const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
2778
3054
  if (apiKey) return { kind: "api_key", apiKey };
2779
- const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
3055
+ const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
2780
3056
  if (accessToken) return { kind: "access_token", accessToken };
2781
- return null;
3057
+ const config = await readConfig();
3058
+ return profileForEnvironment(config, environment).credential ?? null;
3059
+ }
3060
+ var REFRESH_SKEW_SECONDS = 60;
3061
+ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
3062
+ const prefix = envVarPrefix(environment);
3063
+ if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
3064
+ return resolveCredential(environment);
3065
+ }
3066
+ const config = await readConfig();
3067
+ const profile = profileForEnvironment(config, environment);
3068
+ const cred = profile.credential;
3069
+ if (!cred || cred.kind !== "access_token") return cred ?? null;
3070
+ if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
3071
+ const now = Math.floor(Date.now() / 1e3);
3072
+ if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
3073
+ try {
3074
+ const oauth = new TopoloOAuth({
3075
+ serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
3076
+ });
3077
+ const tokens = await oauth.refreshToken({
3078
+ clientId: profile.oauthClientId,
3079
+ refreshToken: cred.refreshToken
3080
+ });
3081
+ const refreshed = {
3082
+ kind: "access_token",
3083
+ accessToken: tokens.access_token,
3084
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
3085
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
3086
+ };
3087
+ await writeConfig(
3088
+ environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed })
3089
+ );
3090
+ return refreshed;
3091
+ } catch {
3092
+ return cred;
3093
+ }
3094
+ }
3095
+ function resolveAgentName(configAgentName) {
3096
+ return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
2782
3097
  }
2783
- function resolveServiceUrlsFromEnv() {
2784
- const out = {};
3098
+ function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
3099
+ const merged = {
3100
+ ...ENVIRONMENT_DEFAULT_SERVICE_URLS[environment],
3101
+ ...configUrls ?? {}
3102
+ };
3103
+ const prefix = `${envVarPrefix(environment)}_SERVICE_URL_`;
3104
+ for (const [k, v] of Object.entries(process.env)) {
3105
+ if (k.startsWith(prefix) && v) {
3106
+ merged[k.slice(prefix.length).toLowerCase()] = v;
3107
+ }
3108
+ }
2785
3109
  for (const [k, v] of Object.entries(process.env)) {
2786
3110
  if (k.startsWith("TOPOLO_SERVICE_URL_") && v) {
2787
- out[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
3111
+ merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
2788
3112
  }
2789
3113
  }
2790
- return Object.keys(out).length > 0 ? out : void 0;
3114
+ return Object.keys(merged).length > 0 ? merged : void 0;
3115
+ }
3116
+
3117
+ // src/auth.ts
3118
+ function resolveCredentialFromEnv() {
3119
+ const apiKey = process.env.TOPOLO_API_KEY;
3120
+ if (apiKey) return { kind: "api_key", apiKey };
3121
+ const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
3122
+ if (accessToken) return { kind: "access_token", accessToken };
3123
+ return null;
2791
3124
  }
2792
3125
 
2793
3126
  // src/server.ts
@@ -3041,7 +3374,7 @@ var TOOLS = [
3041
3374
  {
3042
3375
  name: "topolo_api_call",
3043
3376
  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.",
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.",
3045
3378
  requiredScopes: [],
3046
3379
  destructive: true,
3047
3380
  inputSchema: {
@@ -3049,7 +3382,7 @@ var TOOLS = [
3049
3382
  properties: {
3050
3383
  service: {
3051
3384
  type: "string",
3052
- description: "Target service ID from the platform registry."
3385
+ description: "Target app ID from the platform registry."
3053
3386
  },
3054
3387
  method: {
3055
3388
  type: "string",
@@ -3154,7 +3487,7 @@ function actionsToToolDefs(actions) {
3154
3487
  }));
3155
3488
  }
3156
3489
  function toolNameForAction(action) {
3157
- const service = (action.serviceSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
3490
+ const service = (action.appSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
3158
3491
  const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
3159
3492
  return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
3160
3493
  }
@@ -3219,7 +3552,7 @@ function displayAppId(entry) {
3219
3552
  for (const value of [entry.slug, entry.name, entry.appId]) {
3220
3553
  if (typeof value !== "string" || !value.trim()) continue;
3221
3554
  for (const candidate of appIdCandidates(value)) {
3222
- if (Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, candidate)) {
3555
+ if (Object.prototype.hasOwnProperty.call(DEFAULT_APP_URLS, candidate)) {
3223
3556
  return candidate;
3224
3557
  }
3225
3558
  }
@@ -3245,14 +3578,14 @@ function formatKnownValues(values, label) {
3245
3578
 
3246
3579
  // src/version.ts
3247
3580
  import { readFileSync } from "fs";
3248
- import { dirname, join } from "path";
3581
+ import { dirname as dirname2, join as join2 } from "path";
3249
3582
  import { fileURLToPath } from "url";
3250
3583
  function readMcpVersion() {
3251
3584
  try {
3252
- const here = dirname(fileURLToPath(import.meta.url));
3585
+ const here = dirname2(fileURLToPath(import.meta.url));
3253
3586
  const candidates = [
3254
- join(here, "package.json"),
3255
- join(here, "..", "package.json")
3587
+ join2(here, "package.json"),
3588
+ join2(here, "..", "package.json")
3256
3589
  ];
3257
3590
  for (const path of candidates) {
3258
3591
  try {
@@ -3331,15 +3664,21 @@ function toolError(message) {
3331
3664
 
3332
3665
  // src/index.ts
3333
3666
  async function main() {
3334
- const credential = resolveCredentialFromEnv();
3667
+ const environment = normalizeEnvironment(process.env.TOPOLO_ENV);
3668
+ let credential = resolveCredentialFromEnv();
3669
+ if (!credential) {
3670
+ credential = await refreshCredentialIfNeeded(environment);
3671
+ }
3335
3672
  if (!credential) {
3336
3673
  process.stderr.write(
3337
- "TopoloMCP: TOPOLO_API_KEY or TOPOLO_ACCESS_TOKEN must be set in the environment.\n"
3674
+ "TopoloMCP: no Topolo credential. Run `topolo auth login`, or set TOPOLO_API_KEY / TOPOLO_ACCESS_TOKEN.\n"
3338
3675
  );
3339
3676
  process.exit(1);
3340
3677
  }
3341
- const agentName = process.env.TOPOLO_AGENT_NAME;
3342
- const serviceUrls = resolveServiceUrlsFromEnv();
3678
+ const config = await readConfig();
3679
+ const profile = profileForEnvironment(config, environment);
3680
+ const agentName = resolveAgentName(profile.agentName);
3681
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
3343
3682
  const topolo = createTopolo({
3344
3683
  credential,
3345
3684
  agent: {
@@ -3356,7 +3695,7 @@ async function main() {
3356
3695
  await server.connect(transport);
3357
3696
  const toolNames = await advertisedToolNames(topolo, scopes);
3358
3697
  process.stderr.write(
3359
- `TopoloMCP v${MCP_VERSION} connected. Tools available: ${toolNames.join(", ") || "(none)"}
3698
+ `TopoloMCP v${MCP_VERSION} connected (${environment}). Tools available: ${toolNames.join(", ") || "(none)"}
3360
3699
  `
3361
3700
  );
3362
3701
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Model Context Protocol server for the Topolo platform. Exposes scope-gated tools that third-party agents (Claude, Codex, etc.) can call natively.",
5
5
  "type": "module",
6
6
  "bin": {