@topolo/mcp 0.7.2 → 0.9.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
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
 
6
- // ../topolo-sdk/dist/index.js
6
+ // ../../../topolo-platform/packages/topolo-sdk/dist/index.js
7
7
  function applyAuthHeaders(headers, credential) {
8
8
  if (credential.kind === "api_key") {
9
9
  headers.set("X-Api-Key", credential.apiKey);
@@ -2092,6 +2092,262 @@ function firstString(...values) {
2092
2092
  }
2093
2093
  return null;
2094
2094
  }
2095
+ var SUPPORTED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
2096
+ "$schema",
2097
+ "$id",
2098
+ "$ref",
2099
+ "$defs",
2100
+ "definitions",
2101
+ "title",
2102
+ "description",
2103
+ "default",
2104
+ "examples",
2105
+ "deprecated",
2106
+ "readOnly",
2107
+ "writeOnly",
2108
+ "type",
2109
+ "nullable",
2110
+ "const",
2111
+ "enum",
2112
+ "allOf",
2113
+ "anyOf",
2114
+ "oneOf",
2115
+ "properties",
2116
+ "required",
2117
+ "additionalProperties",
2118
+ "propertyNames",
2119
+ "minProperties",
2120
+ "maxProperties",
2121
+ "items",
2122
+ "minItems",
2123
+ "maxItems",
2124
+ "uniqueItems",
2125
+ "minLength",
2126
+ "maxLength",
2127
+ "pattern",
2128
+ "format",
2129
+ "minimum",
2130
+ "maximum",
2131
+ "exclusiveMinimum",
2132
+ "exclusiveMaximum",
2133
+ "multipleOf"
2134
+ ]);
2135
+ function validateTopoloActionInput(schema, input) {
2136
+ const schemaIssue = inspectSchema(schema, "$", schema, 0);
2137
+ if (schemaIssue) return { valid: false, errors: [schemaIssue] };
2138
+ const errors = [];
2139
+ validateNode(schema, input, "$", schema, errors, 0);
2140
+ return { valid: errors.length === 0, errors };
2141
+ }
2142
+ function inspectSchema(schema, path, root, depth) {
2143
+ if (depth > 128) return { path, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
2144
+ const unsupportedKeyword = Object.keys(schema).find((keyword) => !SUPPORTED_SCHEMA_KEYWORDS.has(keyword));
2145
+ if (unsupportedKeyword) {
2146
+ return { path, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
2147
+ }
2148
+ if (typeof schema.format === "string" && !["uri", "email", "date-time"].includes(schema.format)) {
2149
+ return { path, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
2150
+ }
2151
+ if (typeof schema.$ref === "string" && !resolveLocalReference(root, schema.$ref)) {
2152
+ return { path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
2153
+ }
2154
+ const children = [];
2155
+ if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
2156
+ if (isRecord(schema.$defs)) children.push(...Object.entries(schema.$defs));
2157
+ if (isRecord(schema.definitions)) children.push(...Object.entries(schema.definitions));
2158
+ children.push(
2159
+ ["additionalProperties", schema.additionalProperties],
2160
+ ["propertyNames", schema.propertyNames],
2161
+ ["items", schema.items],
2162
+ ...arraySchemaEntries("allOf", schema.allOf),
2163
+ ...arraySchemaEntries("anyOf", schema.anyOf),
2164
+ ...arraySchemaEntries("oneOf", schema.oneOf)
2165
+ );
2166
+ for (const [key, child] of children) {
2167
+ if (!isRecord(child)) continue;
2168
+ const issue = inspectSchema(child, `${path}.${key}`, root, depth + 1);
2169
+ if (issue) return issue;
2170
+ }
2171
+ return null;
2172
+ }
2173
+ function arraySchemaEntries(keyword, value) {
2174
+ return Array.isArray(value) ? value.map((candidate, index) => [`${keyword}[${index}]`, candidate]) : [];
2175
+ }
2176
+ function validateNode(schema, value, path, root, errors, depth) {
2177
+ if (depth > 128) {
2178
+ errors.push({ path, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
2179
+ return;
2180
+ }
2181
+ if (typeof schema.$ref === "string") {
2182
+ const resolved = resolveLocalReference(root, schema.$ref);
2183
+ if (!resolved) {
2184
+ errors.push({ path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
2185
+ return;
2186
+ }
2187
+ validateNode(resolved, value, path, root, errors, depth + 1);
2188
+ }
2189
+ if (Array.isArray(schema.allOf)) {
2190
+ for (const candidate of schema.allOf) {
2191
+ if (isRecord(candidate)) validateNode(candidate, value, path, root, errors, depth + 1);
2192
+ }
2193
+ }
2194
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
2195
+ errors.push({ path, code: "any_of", message: "Value does not match any allowed schema." });
2196
+ return;
2197
+ }
2198
+ if (Array.isArray(schema.oneOf)) {
2199
+ const matches = schema.oneOf.filter((candidate) => candidateMatches(candidate, value, root, depth)).length;
2200
+ if (matches !== 1) {
2201
+ errors.push({ path, code: "one_of", message: "Value must match exactly one allowed schema." });
2202
+ return;
2203
+ }
2204
+ }
2205
+ if (value === null && schema.nullable === true) return;
2206
+ if (Object.prototype.hasOwnProperty.call(schema, "const") && !sameJsonValue(value, schema.const)) {
2207
+ errors.push({ path, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
2208
+ return;
2209
+ }
2210
+ if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => sameJsonValue(value, candidate))) {
2211
+ errors.push({ path, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
2212
+ return;
2213
+ }
2214
+ if (!matchesType(schema.type, value)) {
2215
+ errors.push({ path, code: "type", message: `Expected ${String(schema.type)}.` });
2216
+ return;
2217
+ }
2218
+ if (typeof value === "string") validateString(schema, value, path, errors);
2219
+ if (typeof value === "number") validateNumber(schema, value, path, errors);
2220
+ if (Array.isArray(value)) validateArray(schema, value, path, root, errors, depth);
2221
+ if (isRecord(value)) validateObject(schema, value, path, root, errors, depth);
2222
+ }
2223
+ function validateObject(schema, value, path, root, errors, depth) {
2224
+ if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
2225
+ errors.push({ path, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
2226
+ }
2227
+ if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
2228
+ errors.push({ path, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
2229
+ }
2230
+ const properties = isRecord(schema.properties) ? schema.properties : {};
2231
+ const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
2232
+ for (const key of required) {
2233
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
2234
+ errors.push({ path: `${path}.${key}`, code: "required", message: "Required property is missing." });
2235
+ }
2236
+ }
2237
+ if (isRecord(schema.propertyNames)) {
2238
+ for (const key of Object.keys(value)) {
2239
+ validateNode(schema.propertyNames, key, `${path}.${key}`, root, errors, depth + 1);
2240
+ }
2241
+ }
2242
+ for (const [key, child] of Object.entries(value)) {
2243
+ const childSchema = properties[key];
2244
+ if (isRecord(childSchema)) {
2245
+ validateNode(childSchema, child, `${path}.${key}`, root, errors, depth + 1);
2246
+ } else if (schema.additionalProperties === false) {
2247
+ errors.push({ path: `${path}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
2248
+ } else if (isRecord(schema.additionalProperties)) {
2249
+ validateNode(schema.additionalProperties, child, `${path}.${key}`, root, errors, depth + 1);
2250
+ }
2251
+ }
2252
+ }
2253
+ function validateArray(schema, value, path, root, errors, depth) {
2254
+ if (typeof schema.minItems === "number" && value.length < schema.minItems) {
2255
+ errors.push({ path, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
2256
+ }
2257
+ if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
2258
+ errors.push({ path, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
2259
+ }
2260
+ if (schema.uniqueItems === true) {
2261
+ const unique = new Set(value.map((item) => JSON.stringify(item)));
2262
+ if (unique.size !== value.length) errors.push({ path, code: "unique_items", message: "Expected every item to be unique." });
2263
+ }
2264
+ if (isRecord(schema.items)) {
2265
+ value.forEach((item, index) => validateNode(schema.items, item, `${path}[${index}]`, root, errors, depth + 1));
2266
+ }
2267
+ }
2268
+ function validateString(schema, value, path, errors) {
2269
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) {
2270
+ errors.push({ path, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
2271
+ }
2272
+ if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
2273
+ errors.push({ path, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
2274
+ }
2275
+ if (typeof schema.pattern === "string") {
2276
+ try {
2277
+ if (!new RegExp(schema.pattern).test(value)) errors.push({ path, code: "pattern", message: "Value does not match the required pattern." });
2278
+ } catch {
2279
+ errors.push({ path, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
2280
+ }
2281
+ }
2282
+ if (schema.format === "uri" && !isUrl(value)) errors.push({ path, code: "format_uri", message: "Expected an absolute URL." });
2283
+ if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path, code: "format_email", message: "Expected an email address." });
2284
+ if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
2285
+ }
2286
+ function validateNumber(schema, value, path, errors) {
2287
+ if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
2288
+ if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
2289
+ const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum === true && typeof schema.minimum === "number" ? schema.minimum : null;
2290
+ const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum === true && typeof schema.maximum === "number" ? schema.maximum : null;
2291
+ if (exclusiveMinimum !== null && value <= exclusiveMinimum) {
2292
+ errors.push({ path, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
2293
+ }
2294
+ if (exclusiveMaximum !== null && value >= exclusiveMaximum) {
2295
+ errors.push({ path, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
2296
+ }
2297
+ if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
2298
+ errors.push({ path, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
2299
+ }
2300
+ }
2301
+ function candidateMatches(candidate, value, root, depth) {
2302
+ if (!isRecord(candidate)) return false;
2303
+ const issues = [];
2304
+ validateNode(candidate, value, "$", root, issues, depth + 1);
2305
+ return issues.length === 0;
2306
+ }
2307
+ function resolveLocalReference(root, reference) {
2308
+ if (!reference.startsWith("#/")) return null;
2309
+ let current = root;
2310
+ for (const token of reference.slice(2).split("/").map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))) {
2311
+ if (Array.isArray(current)) {
2312
+ if (!/^\d+$/.test(token) || Number(token) >= current.length) return null;
2313
+ current = current[Number(token)];
2314
+ continue;
2315
+ }
2316
+ if (!isRecord(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
2317
+ current = current[token];
2318
+ }
2319
+ return isRecord(current) ? current : null;
2320
+ }
2321
+ function matchesType(type, value) {
2322
+ if (type === void 0) return true;
2323
+ if (Array.isArray(type)) return type.some((candidate) => matchesType(candidate, value));
2324
+ if (type === "null") return value === null;
2325
+ if (type === "array") return Array.isArray(value);
2326
+ if (type === "object") return isRecord(value);
2327
+ if (type === "integer") return typeof value === "number" && Number.isInteger(value);
2328
+ return typeof value === type;
2329
+ }
2330
+ function sameJsonValue(left, right) {
2331
+ return JSON.stringify(left) === JSON.stringify(right);
2332
+ }
2333
+ function isRecord(value) {
2334
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2335
+ }
2336
+ function isUrl(value) {
2337
+ try {
2338
+ return Boolean(new URL(value).protocol);
2339
+ } catch {
2340
+ return false;
2341
+ }
2342
+ }
2343
+ function isDateTime(value) {
2344
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value));
2345
+ }
2346
+ function isMultipleOf(value, divisor) {
2347
+ if (!(divisor > 0)) return false;
2348
+ const quotient = value / divisor;
2349
+ return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
2350
+ }
2095
2351
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2096
2352
  var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
2097
2353
  var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
@@ -2311,7 +2567,10 @@ var TopoloClient = class _TopoloClient {
2311
2567
  serviceKey,
2312
2568
  overrides: this.serviceUrls
2313
2569
  });
2314
- return baseUrl ? { serviceKey, baseUrl } : null;
2570
+ if (baseUrl) return { serviceKey, baseUrl };
2571
+ const actionPage = await this.searchActions({ service: match.appId, limit: 1 }).catch(() => null);
2572
+ const actionBaseUrl = actionPage?.actions.find((action) => action.apiBaseUrl)?.apiBaseUrl ?? null;
2573
+ return actionBaseUrl ? { serviceKey, baseUrl: requireHttpOrigin(actionBaseUrl) } : null;
2315
2574
  }
2316
2575
  /**
2317
2576
  * Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
@@ -2396,7 +2655,7 @@ var TopoloClient = class _TopoloClient {
2396
2655
  }
2397
2656
  const entries = [];
2398
2657
  for (const item of raw) {
2399
- if (!isRecord(item)) continue;
2658
+ if (!isRecord2(item)) continue;
2400
2659
  const s = item;
2401
2660
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2402
2661
  if (!appId) continue;
@@ -2446,6 +2705,44 @@ var TopoloClient = class _TopoloClient {
2446
2705
  }
2447
2706
  return entry;
2448
2707
  }
2708
+ async listBindableResources(application) {
2709
+ const service = await this.getService(application);
2710
+ const identity = await this.introspect();
2711
+ if (identity.apiKey) {
2712
+ return {
2713
+ application: { appId: service.appId, slug: service.slug, name: service.name },
2714
+ organizationId: identity.organization?.id ?? null,
2715
+ resources: identity.apiKey.resourceBindings.map((binding) => ({
2716
+ resourceType: binding.resourceType,
2717
+ resourceId: binding.resourceId,
2718
+ label: null,
2719
+ slug: null,
2720
+ status: "active",
2721
+ organizationId: identity.organization?.id ?? null,
2722
+ metadata: {}
2723
+ })),
2724
+ resourceTypes: service.apiKeyResources,
2725
+ syncError: null,
2726
+ source: "api_key_binding"
2727
+ };
2728
+ }
2729
+ const response = await this.readAction(
2730
+ "app_topolo_auth.service_api_key_resources.list",
2731
+ { appId: service.appId }
2732
+ );
2733
+ const payload = isRecord2(response) && isRecord2(response.data) ? response.data : response;
2734
+ if (!isRecord2(payload) || !Array.isArray(payload.resources)) {
2735
+ throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
2736
+ }
2737
+ return {
2738
+ application: { appId: service.appId, slug: service.slug, name: service.name },
2739
+ organizationId: readOptionalString(payload.organizationId ?? payload.organization_id),
2740
+ resources: payload.resources.map(normalizeBindableResource).filter((resource) => Boolean(resource)),
2741
+ resourceTypes: normalizeApiKeyResourceMetadataList(payload.resourceTypes ?? payload.resource_types),
2742
+ syncError: readOptionalString(payload.syncError ?? payload.sync_error),
2743
+ source: "auth_catalog"
2744
+ };
2745
+ }
2449
2746
  /**
2450
2747
  * List callable actions from the live registry-canonical catalog (definitions
2451
2748
  * from the Catalog registry, scoping from Auth). The response is scoped to the
@@ -2540,10 +2837,44 @@ var TopoloClient = class _TopoloClient {
2540
2837
  const entry = await this.getAction(action);
2541
2838
  return this.executeResolvedAction(entry, input, options);
2542
2839
  }
2840
+ async validateActionInput(action, input = {}) {
2841
+ const entry = await this.getAction(action);
2842
+ return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input) };
2843
+ }
2844
+ async planAction(action, input = {}, options = {}) {
2845
+ const entry = await this.getAction(action);
2846
+ const validation = validateTopoloActionInput(entry.inputSchema, input);
2847
+ const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2848
+ const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
2849
+ let request = null;
2850
+ if (validation.valid) {
2851
+ const mapped = mapActionInput(entry, input);
2852
+ request = {
2853
+ method: entry.method,
2854
+ service: entry.appSlug || entry.appId,
2855
+ apiBaseUrl: entry.apiBaseUrl,
2856
+ path: mapped.path,
2857
+ ...mapped.query ? { query: mapped.query } : {},
2858
+ ...mapped.body !== void 0 ? { body: mapped.body } : {},
2859
+ ...options.resource ? { resource: options.resource } : {}
2860
+ };
2861
+ }
2862
+ return {
2863
+ action: entry,
2864
+ validation,
2865
+ confirmationRequired,
2866
+ confirmationProvided: options.confirm === true,
2867
+ executable: validation.valid && agentAccess !== "off" && (!confirmationRequired || options.confirm === true),
2868
+ request,
2869
+ verification: stringArray(entry.agentContract?.verify),
2870
+ rollbackActionId: pickActionString(entry.agentContract?.rollbackActionId),
2871
+ nextActionIds: stringArray(entry.agentContract?.nextActionIds)
2872
+ };
2873
+ }
2543
2874
  async readAction(action, input = {}, options = {}) {
2544
2875
  const entry = await this.getAction(action);
2545
2876
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2546
- if (entry.method !== "GET" || !entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
2877
+ if (!entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
2547
2878
  throw new TopoloPermissionError(
2548
2879
  `Topolo action "${entry.actionId}" is not an automatic read-only action.`,
2549
2880
  entry.requiredPermission ? [entry.requiredPermission] : []
@@ -2564,6 +2895,13 @@ var TopoloClient = class _TopoloClient {
2564
2895
  `Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
2565
2896
  );
2566
2897
  }
2898
+ const validation = validateTopoloActionInput(entry.inputSchema, input);
2899
+ if (!validation.valid) {
2900
+ throw new TopoloSdkError(
2901
+ "invalid_action_input",
2902
+ `Topolo action "${entry.actionId}" input is invalid: ${validation.errors.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`
2903
+ );
2904
+ }
2567
2905
  const mapped = mapActionInput(entry, input);
2568
2906
  const service = entry.appSlug || entry.appId;
2569
2907
  return this.request({
@@ -2578,7 +2916,7 @@ var TopoloClient = class _TopoloClient {
2578
2916
  [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2579
2917
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
2580
2918
  },
2581
- confirm: options.confirm === true
2919
+ confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
2582
2920
  });
2583
2921
  }
2584
2922
  /**
@@ -2635,7 +2973,14 @@ var TopoloClient = class _TopoloClient {
2635
2973
  id: organization.id,
2636
2974
  slug: organization.slug,
2637
2975
  name: organization.name ?? organization.slug
2638
- } : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null
2976
+ } : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null,
2977
+ apiKey: kind === "api_key" ? {
2978
+ id: readOptionalString(payload.apiKey?.id),
2979
+ name: readOptionalString(payload.apiKey?.name),
2980
+ appId: readOptionalString(payload.apiKey?.appId),
2981
+ scopes: stringArray(payload.apiKey?.scopes),
2982
+ resourceBindings: normalizeResourceBindings(payload.apiKey?.resourceBindings)
2983
+ } : null
2639
2984
  };
2640
2985
  }
2641
2986
  };
@@ -2650,7 +2995,7 @@ function resolveConfiguredAppId(slug, appIds) {
2650
2995
  return null;
2651
2996
  }
2652
2997
  function normalizeApiKeyResourceMetadata(input) {
2653
- if (!isRecord(input)) return null;
2998
+ if (!isRecord2(input)) return null;
2654
2999
  const resourceType = readString(input.resourceType ?? input.resource_type);
2655
3000
  const label = readString(input.label);
2656
3001
  const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
@@ -2683,10 +3028,38 @@ function normalizeApiKeyResourceMetadataList(input) {
2683
3028
  }
2684
3029
  return resources;
2685
3030
  }
3031
+ function normalizeBindableResource(input) {
3032
+ if (!isRecord2(input)) return null;
3033
+ const resourceType = readString(input.resourceType ?? input.resource_type);
3034
+ const resourceId = readString(input.resourceId ?? input.resource_id);
3035
+ if (!resourceType || !resourceId) return null;
3036
+ return {
3037
+ resourceType,
3038
+ resourceId,
3039
+ label: readOptionalString(input.label),
3040
+ slug: readOptionalString(input.slug),
3041
+ status: readOptionalString(input.status),
3042
+ organizationId: readOptionalString(input.organizationId ?? input.organization_id),
3043
+ metadata: isRecord2(input.metadata) ? input.metadata : {}
3044
+ };
3045
+ }
3046
+ function normalizeResourceBindings(input) {
3047
+ if (!Array.isArray(input)) return [];
3048
+ return input.flatMap((binding) => {
3049
+ if (!isRecord2(binding)) return [];
3050
+ const resourceType = readString(binding.resourceType ?? binding.resource_type);
3051
+ const resourceId = readString(binding.resourceId ?? binding.resource_id);
3052
+ return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
3053
+ });
3054
+ }
2686
3055
  function readString(value) {
2687
3056
  return typeof value === "string" ? value.trim() : "";
2688
3057
  }
2689
- function isRecord(value) {
3058
+ function readOptionalString(value) {
3059
+ const normalized = readString(value);
3060
+ return normalized || null;
3061
+ }
3062
+ function isRecord2(value) {
2690
3063
  return typeof value === "object" && value !== null && !Array.isArray(value);
2691
3064
  }
2692
3065
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
@@ -2758,14 +3131,14 @@ function catalogSearchQuery(options) {
2758
3131
  return query;
2759
3132
  }
2760
3133
  function normalizeCatalogPage(value) {
2761
- if (!isRecord(value)) return { nextCursor: null, hasMore: false };
3134
+ if (!isRecord2(value)) return { nextCursor: null, hasMore: false };
2762
3135
  return {
2763
3136
  nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
2764
3137
  hasMore: value.hasMore === true
2765
3138
  };
2766
3139
  }
2767
3140
  function normalizeServiceCatalogEntry(item) {
2768
- if (!isRecord(item)) return null;
3141
+ if (!isRecord2(item)) return null;
2769
3142
  const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
2770
3143
  if (!appId) return null;
2771
3144
  const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
@@ -2820,6 +3193,7 @@ function normalizeActionCatalogEntry(item) {
2820
3193
  authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
2821
3194
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
2822
3195
  outputSchema: plainObject(a.output_schema ?? a.outputSchema),
3196
+ agentContract: plainObject(a.agent_contract ?? a.agentContract),
2823
3197
  readOnly: Boolean(a.read_only ?? a.readOnly),
2824
3198
  destructive: Boolean(a.destructive),
2825
3199
  requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
@@ -2843,6 +3217,9 @@ function pickAuthorizationMode(value) {
2843
3217
  function plainObject(value) {
2844
3218
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2845
3219
  }
3220
+ function stringArray(value) {
3221
+ return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
3222
+ }
2846
3223
  function mapActionInput(action, input) {
2847
3224
  const remaining = { ...input || {} };
2848
3225
  const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
@@ -2992,7 +3369,7 @@ function createTopolo(options) {
2992
3369
  };
2993
3370
  }
2994
3371
 
2995
- // ../topolo-sdk/dist/node/credential-store.js
3372
+ // ../../../topolo-platform/packages/topolo-sdk/dist/node/credential-store.js
2996
3373
  import { mkdir, readFile, writeFile, chmod } from "fs/promises";
2997
3374
  import { existsSync } from "fs";
2998
3375
  import { homedir, platform } from "os";
@@ -3322,6 +3699,46 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
3322
3699
  return cred;
3323
3700
  }
3324
3701
  }
3702
+ async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
3703
+ const config = await readConfig();
3704
+ const profile = profileForEnvironment(config, environment);
3705
+ const context = profile.contexts?.[key];
3706
+ const credential = context?.credential;
3707
+ if (!credential || credential.kind !== "access_token") return credential ?? null;
3708
+ const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
3709
+ if (!credential.refreshToken || !credential.expiresAt || !oauthClientId) return credential;
3710
+ const now = Math.floor(Date.now() / 1e3);
3711
+ if (!options.force && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
3712
+ try {
3713
+ const oauth = new TopoloOAuth({
3714
+ serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
3715
+ });
3716
+ const tokens = await oauth.refreshToken({
3717
+ clientId: oauthClientId,
3718
+ refreshToken: credential.refreshToken
3719
+ });
3720
+ const refreshed = {
3721
+ kind: "access_token",
3722
+ accessToken: tokens.access_token,
3723
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
3724
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
3725
+ };
3726
+ await writeConfig(writeProfileConfig(config, environment, {
3727
+ ...profile.activeContext === key ? { credential: refreshed } : {},
3728
+ contexts: {
3729
+ ...profile.contexts ?? {},
3730
+ [key]: {
3731
+ ...context,
3732
+ credential: refreshed,
3733
+ oauthClientId
3734
+ }
3735
+ }
3736
+ }));
3737
+ return refreshed;
3738
+ } catch {
3739
+ return credential;
3740
+ }
3741
+ }
3325
3742
  function resolveAgentName(configAgentName) {
3326
3743
  return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
3327
3744
  }
@@ -3344,15 +3761,6 @@ function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
3344
3761
  return Object.keys(merged).length > 0 ? merged : void 0;
3345
3762
  }
3346
3763
 
3347
- // src/auth.ts
3348
- function resolveCredentialFromEnv() {
3349
- const apiKey = process.env.TOPOLO_API_KEY;
3350
- if (apiKey) return { kind: "api_key", apiKey };
3351
- const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
3352
- if (accessToken) return { kind: "access_token", accessToken };
3353
- return null;
3354
- }
3355
-
3356
3764
  // src/server.ts
3357
3765
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3358
3766
  import {
@@ -3607,6 +4015,148 @@ var TOOLS = [
3607
4015
  );
3608
4016
  }
3609
4017
  },
4018
+ {
4019
+ name: "topolo_validate_action",
4020
+ title: "Validate Topolo action input",
4021
+ description: "Validates input locally against the exact JSON Schema published for one credential-scoped action. Does not invoke the action.",
4022
+ requiredScopes: [],
4023
+ destructive: false,
4024
+ inputSchema: {
4025
+ type: "object",
4026
+ properties: {
4027
+ actionId: { type: "string", minLength: 1 },
4028
+ input: { type: "object", additionalProperties: true }
4029
+ },
4030
+ required: ["actionId"],
4031
+ additionalProperties: false
4032
+ },
4033
+ handler: async (topolo, args) => {
4034
+ const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
4035
+ if (!actionId) throw new Error("actionId is required.");
4036
+ const input = args["input"];
4037
+ if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
4038
+ throw new Error("input must be an object.");
4039
+ }
4040
+ return topolo.client.validateActionInput(actionId, input ?? {});
4041
+ }
4042
+ },
4043
+ {
4044
+ name: "topolo_plan_action",
4045
+ title: "Plan one Topolo action",
4046
+ description: "Validates one credential-scoped action and returns its exact request, confirmation state, verification steps, rollback action, and next actions without executing it.",
4047
+ requiredScopes: [],
4048
+ destructive: false,
4049
+ inputSchema: {
4050
+ type: "object",
4051
+ properties: {
4052
+ actionId: { type: "string", minLength: 1 },
4053
+ input: { type: "object", additionalProperties: true },
4054
+ resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
4055
+ resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." },
4056
+ confirm: { type: "boolean", description: "Include confirmation in the plan without executing the action." }
4057
+ },
4058
+ required: ["actionId"],
4059
+ additionalProperties: false
4060
+ },
4061
+ handler: async (topolo, args) => {
4062
+ const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
4063
+ if (!actionId) throw new Error("actionId is required.");
4064
+ const input = args["input"];
4065
+ if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
4066
+ throw new Error("input must be an object.");
4067
+ }
4068
+ const resource = optionalResourceContext(args);
4069
+ return topolo.client.planAction(
4070
+ actionId,
4071
+ input ?? {},
4072
+ {
4073
+ confirm: args["confirm"] === true,
4074
+ ...resource ? { resource } : {}
4075
+ }
4076
+ );
4077
+ }
4078
+ },
4079
+ {
4080
+ name: "topolo_get_action_examples",
4081
+ title: "Get Topolo action examples and documentation",
4082
+ description: "Returns the public documentation URL and inline examples published for one action.",
4083
+ requiredScopes: [],
4084
+ destructive: false,
4085
+ inputSchema: {
4086
+ type: "object",
4087
+ properties: { actionId: { type: "string", minLength: 1 } },
4088
+ required: ["actionId"],
4089
+ additionalProperties: false
4090
+ },
4091
+ handler: async (topolo, args) => {
4092
+ const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
4093
+ if (!actionId) throw new Error("actionId is required.");
4094
+ const action = await topolo.client.getAction(actionId);
4095
+ return {
4096
+ actionId: action.actionId,
4097
+ docsUrl: action.agentContract?.docsUrl ?? null,
4098
+ examples: action.agentContract?.examples ?? []
4099
+ };
4100
+ }
4101
+ },
4102
+ {
4103
+ name: "topolo_discover_capabilities",
4104
+ title: "Discover one Topolo application capability contract",
4105
+ description: "Discovers capability and schema actions published by one application. Use the actionService value returned by topolo_search_applications as service.",
4106
+ requiredScopes: [],
4107
+ destructive: false,
4108
+ inputSchema: {
4109
+ type: "object",
4110
+ properties: { service: { type: "string", minLength: 1 } },
4111
+ required: ["service"],
4112
+ additionalProperties: false
4113
+ },
4114
+ handler: async (topolo, args) => {
4115
+ const service = typeof args["service"] === "string" ? args["service"].trim() : "";
4116
+ if (!service) throw new Error("service is required.");
4117
+ return topolo.client.searchActions({ service, query: "capabilities", limit: 100 });
4118
+ }
4119
+ },
4120
+ {
4121
+ name: "topolo_get_resource_types",
4122
+ title: "Get application resource-scoping types",
4123
+ description: "Returns the canonical resource-scoping metadata published for one application, including aliases accepted by resource-scoped API keys and action calls.",
4124
+ requiredScopes: [],
4125
+ destructive: false,
4126
+ inputSchema: {
4127
+ type: "object",
4128
+ properties: { application: { type: "string", minLength: 1 } },
4129
+ required: ["application"],
4130
+ additionalProperties: false
4131
+ },
4132
+ handler: async (topolo, args) => {
4133
+ const application = typeof args["application"] === "string" ? args["application"].trim() : "";
4134
+ if (!application) throw new Error("application is required.");
4135
+ const entry = await topolo.client.getService(application);
4136
+ return {
4137
+ application: { appId: entry.appId, slug: entry.slug, name: entry.name },
4138
+ resourceTypes: entry.apiKeyResources
4139
+ };
4140
+ }
4141
+ },
4142
+ {
4143
+ name: "topolo_list_resources",
4144
+ title: "List bindable Topolo resource ids",
4145
+ description: "Returns credential-scoped resource ids for one application. OAuth credentials use the published Auth resource catalog; API keys return only their own bound resources.",
4146
+ requiredScopes: [],
4147
+ destructive: false,
4148
+ inputSchema: {
4149
+ type: "object",
4150
+ properties: { application: { type: "string", minLength: 1 } },
4151
+ required: ["application"],
4152
+ additionalProperties: false
4153
+ },
4154
+ handler: async (topolo, args) => {
4155
+ const application = typeof args["application"] === "string" ? args["application"].trim() : "";
4156
+ if (!application) throw new Error("application is required.");
4157
+ return topolo.client.listBindableResources(application);
4158
+ }
4159
+ },
3610
4160
  {
3611
4161
  name: "topolo_list_application_requirements",
3612
4162
  title: "List Topolo application build requirements",
@@ -3925,14 +4475,79 @@ function toolError(message) {
3925
4475
  };
3926
4476
  }
3927
4477
 
4478
+ // src/auth.ts
4479
+ function resolveCredentialFromEnv() {
4480
+ const apiKey = process.env.TOPOLO_API_KEY;
4481
+ if (apiKey) return { kind: "api_key", apiKey };
4482
+ const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
4483
+ if (accessToken) return { kind: "access_token", accessToken };
4484
+ return null;
4485
+ }
4486
+
4487
+ // src/context-credential.ts
4488
+ var DEFAULT_DEPENDENCIES = {
4489
+ readConfig,
4490
+ refreshContextCredentialIfNeeded,
4491
+ refreshCredentialIfNeeded,
4492
+ resolveCredentialFromEnv
4493
+ };
4494
+ function resolvePinnedContextKey(profile, selector) {
4495
+ const normalized = selector.trim().toLowerCase();
4496
+ if (!normalized) {
4497
+ throw new Error("TOPOLO_CONTEXT must not be empty");
4498
+ }
4499
+ const contexts = Object.entries(profile.contexts ?? {});
4500
+ const exact = contexts.find(([key]) => key.toLowerCase() === normalized);
4501
+ if (exact) return exact[0];
4502
+ const matches = contexts.filter(([, context]) => [
4503
+ context.organizationId,
4504
+ context.organizationSlug,
4505
+ context.organizationName
4506
+ ].some((value) => String(value || "").trim().toLowerCase() === normalized));
4507
+ if (matches.length === 1) return matches[0][0];
4508
+ if (matches.length > 1) {
4509
+ throw new Error(`TOPOLO_CONTEXT "${selector}" is ambiguous; use an exact context key`);
4510
+ }
4511
+ throw new Error(`TOPOLO_CONTEXT "${selector}" is not signed in for this environment`);
4512
+ }
4513
+ async function resolveMcpCredential(environment, selector = process.env.TOPOLO_CONTEXT, dependencies = DEFAULT_DEPENDENCIES) {
4514
+ const environmentCredential = dependencies.resolveCredentialFromEnv();
4515
+ const requestedSelector = selector?.trim();
4516
+ if (environmentCredential) {
4517
+ if (requestedSelector) {
4518
+ throw new Error("TOPOLO_CONTEXT cannot be combined with an environment credential");
4519
+ }
4520
+ return { credential: environmentCredential, contextKey: null };
4521
+ }
4522
+ if (requestedSelector) {
4523
+ const config = await dependencies.readConfig();
4524
+ const contextKey = resolvePinnedContextKey(
4525
+ profileForEnvironment(config, environment),
4526
+ requestedSelector
4527
+ );
4528
+ const credential = await dependencies.refreshContextCredentialIfNeeded(environment, contextKey);
4529
+ return {
4530
+ credential,
4531
+ contextKey,
4532
+ refreshCredential: () => dependencies.refreshContextCredentialIfNeeded(
4533
+ environment,
4534
+ contextKey,
4535
+ { force: true }
4536
+ )
4537
+ };
4538
+ }
4539
+ return {
4540
+ credential: await dependencies.refreshCredentialIfNeeded(environment),
4541
+ contextKey: null,
4542
+ refreshCredential: () => dependencies.refreshCredentialIfNeeded(environment, { force: true })
4543
+ };
4544
+ }
4545
+
3928
4546
  // src/index.ts
3929
4547
  async function main() {
3930
4548
  const environment = normalizeEnvironment(process.env.TOPOLO_ENV);
3931
- const environmentCredential = resolveCredentialFromEnv();
3932
- let credential = environmentCredential;
3933
- if (!credential) {
3934
- credential = await refreshCredentialIfNeeded(environment);
3935
- }
4549
+ const resolvedCredential = await resolveMcpCredential(environment);
4550
+ const credential = resolvedCredential.credential;
3936
4551
  if (!credential) {
3937
4552
  process.stderr.write(
3938
4553
  "TopoloMCP: no Topolo credential. Run `topolo auth login`, or set TOPOLO_API_KEY / TOPOLO_ACCESS_TOKEN.\n"
@@ -3952,7 +4567,7 @@ async function main() {
3952
4567
  },
3953
4568
  serviceUrls,
3954
4569
  requireConfirmForWrites: true,
3955
- ...!environmentCredential ? { refreshCredential: () => refreshCredentialIfNeeded(environment, { force: true }) } : {}
4570
+ ...resolvedCredential.refreshCredential ? { refreshCredential: resolvedCredential.refreshCredential } : {}
3956
4571
  });
3957
4572
  const scopes = await loadScopes(topolo);
3958
4573
  const server = buildServer(topolo, scopes);
@@ -3960,7 +4575,7 @@ async function main() {
3960
4575
  await server.connect(transport);
3961
4576
  const toolNames = await advertisedToolNames(topolo, scopes);
3962
4577
  process.stderr.write(
3963
- `TopoloMCP v${MCP_VERSION} connected (${environment}). Tools available: ${toolNames.join(", ") || "(none)"}
4578
+ `TopoloMCP v${MCP_VERSION} connected (${environment}${resolvedCredential.contextKey ? `, ${resolvedCredential.contextKey}` : ""}). Tools available: ${toolNames.join(", ") || "(none)"}
3964
4579
  `
3965
4580
  );
3966
4581
  }