@topolo/mcp 0.9.7 → 0.9.8

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/dispatch.js CHANGED
@@ -271,6 +271,60 @@ var TOOLS = [
271
271
  );
272
272
  }
273
273
  },
274
+ {
275
+ name: "topolo_upload_action",
276
+ title: "Upload content through one Topolo action",
277
+ description: "Uploads base64 content through an exact action id using its published upload contract. Mutating uploads require confirm:true.",
278
+ requiredScopes: [],
279
+ destructive: true,
280
+ inputSchema: {
281
+ type: "object",
282
+ properties: {
283
+ actionId: { type: "string", minLength: 1 },
284
+ contentBase64: { type: "string", minLength: 4, description: "Canonical base64 content without a data URL prefix." },
285
+ contentType: { type: "string", minLength: 1 },
286
+ fileName: { type: "string", minLength: 1 },
287
+ input: { type: "object", additionalProperties: true },
288
+ completionInput: { type: "object", additionalProperties: true },
289
+ resourceType: { type: "string" },
290
+ resourceId: { type: "string" },
291
+ confirm: { type: "boolean" }
292
+ },
293
+ required: ["actionId", "contentBase64", "contentType", "fileName"],
294
+ additionalProperties: false
295
+ },
296
+ handler: async (topolo, args) => {
297
+ const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
298
+ const contentBase64 = typeof args["contentBase64"] === "string" ? args["contentBase64"] : "";
299
+ const contentType = typeof args["contentType"] === "string" ? args["contentType"].trim() : "";
300
+ const fileName = typeof args["fileName"] === "string" ? args["fileName"].trim() : "";
301
+ if (!actionId) throw new TopoloMcpPublicError("actionId is required.");
302
+ if (!isCanonicalBase64(contentBase64)) throw new TopoloMcpPublicError("contentBase64 must be canonical base64.");
303
+ if (!contentType) throw new TopoloMcpPublicError("contentType is required.");
304
+ if (!fileName) throw new TopoloMcpPublicError("fileName is required.");
305
+ const input = args["input"];
306
+ if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
307
+ throw new TopoloMcpPublicError("input must be an object.");
308
+ }
309
+ const completionInput = args["completionInput"];
310
+ if (completionInput !== void 0 && (!completionInput || typeof completionInput !== "object" || Array.isArray(completionInput))) {
311
+ throw new TopoloMcpPublicError("completionInput must be an object.");
312
+ }
313
+ const resource = optionalResourceContext(args);
314
+ return topolo.client.uploadActionBase64(
315
+ actionId,
316
+ contentBase64,
317
+ input ?? {},
318
+ {
319
+ fileName,
320
+ contentType,
321
+ completionInput: completionInput ?? {},
322
+ confirm: args["confirm"] === true,
323
+ ...resource ? { resource } : {}
324
+ }
325
+ );
326
+ }
327
+ },
274
328
  {
275
329
  name: "topolo_validate_action",
276
330
  title: "Validate Topolo action input",
@@ -574,6 +628,9 @@ var TOOLS = [
574
628
  }
575
629
  }
576
630
  ];
631
+ function isCanonicalBase64(value) {
632
+ return value.length > 0 && value.length % 4 === 0 && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
633
+ }
577
634
  function filterToolsByScopes(tools, scopes) {
578
635
  return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
579
636
  }
package/dist/http.js CHANGED
@@ -15824,7 +15824,7 @@ var Server = class extends Protocol {
15824
15824
  };
15825
15825
 
15826
15826
  // src/server.ts
15827
- import { TopoloAuthError, TopoloPermissionError } from "@topolo/sdk";
15827
+ import { TopoloAuthError, TopoloHttpError, TopoloPermissionError } from "@topolo/sdk";
15828
15828
 
15829
15829
  // src/tools.ts
15830
15830
  import {
@@ -16085,6 +16085,60 @@ var TOOLS = [
16085
16085
  );
16086
16086
  }
16087
16087
  },
16088
+ {
16089
+ name: "topolo_upload_action",
16090
+ title: "Upload content through one Topolo action",
16091
+ description: "Uploads base64 content through an exact action id using its published upload contract. Mutating uploads require confirm:true.",
16092
+ requiredScopes: [],
16093
+ destructive: true,
16094
+ inputSchema: {
16095
+ type: "object",
16096
+ properties: {
16097
+ actionId: { type: "string", minLength: 1 },
16098
+ contentBase64: { type: "string", minLength: 4, description: "Canonical base64 content without a data URL prefix." },
16099
+ contentType: { type: "string", minLength: 1 },
16100
+ fileName: { type: "string", minLength: 1 },
16101
+ input: { type: "object", additionalProperties: true },
16102
+ completionInput: { type: "object", additionalProperties: true },
16103
+ resourceType: { type: "string" },
16104
+ resourceId: { type: "string" },
16105
+ confirm: { type: "boolean" }
16106
+ },
16107
+ required: ["actionId", "contentBase64", "contentType", "fileName"],
16108
+ additionalProperties: false
16109
+ },
16110
+ handler: async (topolo, args) => {
16111
+ const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
16112
+ const contentBase64 = typeof args["contentBase64"] === "string" ? args["contentBase64"] : "";
16113
+ const contentType = typeof args["contentType"] === "string" ? args["contentType"].trim() : "";
16114
+ const fileName = typeof args["fileName"] === "string" ? args["fileName"].trim() : "";
16115
+ if (!actionId) throw new TopoloMcpPublicError("actionId is required.");
16116
+ if (!isCanonicalBase64(contentBase64)) throw new TopoloMcpPublicError("contentBase64 must be canonical base64.");
16117
+ if (!contentType) throw new TopoloMcpPublicError("contentType is required.");
16118
+ if (!fileName) throw new TopoloMcpPublicError("fileName is required.");
16119
+ const input = args["input"];
16120
+ if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
16121
+ throw new TopoloMcpPublicError("input must be an object.");
16122
+ }
16123
+ const completionInput = args["completionInput"];
16124
+ if (completionInput !== void 0 && (!completionInput || typeof completionInput !== "object" || Array.isArray(completionInput))) {
16125
+ throw new TopoloMcpPublicError("completionInput must be an object.");
16126
+ }
16127
+ const resource = optionalResourceContext(args);
16128
+ return topolo.client.uploadActionBase64(
16129
+ actionId,
16130
+ contentBase64,
16131
+ input ?? {},
16132
+ {
16133
+ fileName,
16134
+ contentType,
16135
+ completionInput: completionInput ?? {},
16136
+ confirm: args["confirm"] === true,
16137
+ ...resource ? { resource } : {}
16138
+ }
16139
+ );
16140
+ }
16141
+ },
16088
16142
  {
16089
16143
  name: "topolo_validate_action",
16090
16144
  title: "Validate Topolo action input",
@@ -16388,6 +16442,9 @@ var TOOLS = [
16388
16442
  }
16389
16443
  }
16390
16444
  ];
16445
+ function isCanonicalBase64(value) {
16446
+ return value.length > 0 && value.length % 4 === 0 && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
16447
+ }
16391
16448
  function filterToolsByScopes(tools, scopes) {
16392
16449
  return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
16393
16450
  }
@@ -16525,16 +16582,36 @@ function buildServer(topolo, scopes, tools = TOOLS) {
16525
16582
  };
16526
16583
  } catch (err) {
16527
16584
  if (err instanceof TopoloPermissionError) {
16528
- return toolError(`Permission denied. Required: ${err.required.join(", ") || "(unknown)"}`);
16585
+ return toolStructuredError(err, `Permission denied. Required: ${err.required.join(", ") || "(unknown)"}`, {
16586
+ required: err.required
16587
+ });
16529
16588
  }
16530
16589
  if (err instanceof TopoloAuthError) {
16531
- return toolError("Authentication failed.");
16590
+ return toolStructuredError(err, "Authentication failed.");
16591
+ }
16592
+ if (err instanceof TopoloHttpError) {
16593
+ return toolStructuredError(err, err.message);
16532
16594
  }
16533
16595
  return toolError(publicMcpErrorMessage(err, "Tool execution failed."));
16534
16596
  }
16535
16597
  });
16536
16598
  return server;
16537
16599
  }
16600
+ function toolStructuredError(error2, message, extra = {}) {
16601
+ return toolError(JSON.stringify({
16602
+ error: {
16603
+ code: error2.code,
16604
+ status: error2.status,
16605
+ message,
16606
+ service: error2.service,
16607
+ path: error2.path,
16608
+ requestId: error2.requestId,
16609
+ retryAfter: error2.retryAfter,
16610
+ details: error2.body,
16611
+ ...extra
16612
+ }
16613
+ }, null, 2));
16614
+ }
16538
16615
  function toolError(message) {
16539
16616
  return {
16540
16617
  isError: true,
package/dist/index.js CHANGED
@@ -31,19 +31,54 @@ var TopoloSdkError = class extends Error {
31
31
  }
32
32
  };
33
33
  var TopoloAuthError = class extends TopoloSdkError {
34
- constructor(message, code = "auth_error") {
34
+ status;
35
+ body;
36
+ service;
37
+ path;
38
+ requestId;
39
+ retryAfter;
40
+ constructor(message, code = "auth_error", options = {}) {
35
41
  super(code, message);
36
42
  this.name = "TopoloAuthError";
43
+ this.status = options.status ?? 401;
44
+ this.body = options.body;
45
+ this.service = options.service;
46
+ this.path = options.path;
47
+ this.requestId = options.requestId;
48
+ this.retryAfter = options.retryAfter;
37
49
  }
38
50
  };
39
51
  var TopoloPermissionError = class extends TopoloSdkError {
40
52
  required;
41
- constructor(message, required) {
53
+ status;
54
+ body;
55
+ service;
56
+ path;
57
+ requestId;
58
+ retryAfter;
59
+ constructor(message, required, options = {}) {
42
60
  super("permission_denied", message);
43
61
  this.name = "TopoloPermissionError";
44
62
  this.required = required;
63
+ this.status = options.status ?? 403;
64
+ this.body = options.body;
65
+ this.service = options.service;
66
+ this.path = options.path;
67
+ this.requestId = options.requestId;
68
+ this.retryAfter = options.retryAfter;
45
69
  }
46
70
  };
71
+ function extractHttpErrorCode(body) {
72
+ if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
73
+ const record = body;
74
+ if (typeof record.code === "string" && record.code.trim()) return record.code.trim();
75
+ const nested = record.error;
76
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
77
+ const code = nested.code;
78
+ if (typeof code === "string" && code.trim()) return code.trim();
79
+ }
80
+ return "http_error";
81
+ }
47
82
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
48
83
  constructor(message) {
49
84
  super("confirmation_required", message);
@@ -55,13 +90,17 @@ var TopoloHttpError = class extends TopoloSdkError {
55
90
  body;
56
91
  service;
57
92
  path;
58
- constructor(service, path, status, body, message) {
59
- super("http_error", message ?? `HTTP ${status} from ${service}${path}`);
93
+ requestId;
94
+ retryAfter;
95
+ constructor(service, path, status, body, message, options = {}) {
96
+ super(extractHttpErrorCode(body), message ?? `HTTP ${status} from ${service}${path}`);
60
97
  this.name = "TopoloHttpError";
61
98
  this.status = status;
62
99
  this.body = body;
63
100
  this.service = service;
64
101
  this.path = path;
102
+ this.requestId = options.requestId;
103
+ this.retryAfter = options.retryAfter;
65
104
  }
66
105
  };
67
106
  var APP_API_URLS = {
@@ -271,7 +310,9 @@ var APPLICATIONS = {
271
310
  "domains": [
272
311
  "brand.topolo.app"
273
312
  ],
274
- "routes": [],
313
+ "routes": [
314
+ "brand.topolo.app"
315
+ ],
275
316
  "appId": "brand"
276
317
  },
277
318
  {
@@ -443,7 +484,9 @@ var APPLICATIONS = {
443
484
  "domains": [
444
485
  "chat.topolo.app"
445
486
  ],
446
- "routes": [],
487
+ "routes": [
488
+ "chat.topolo.app"
489
+ ],
447
490
  "appId": "chat"
448
491
  },
449
492
  {
@@ -825,7 +868,9 @@ var APPLICATIONS = {
825
868
  "domains": [
826
869
  "forms.topolo.app"
827
870
  ],
828
- "routes": [],
871
+ "routes": [
872
+ "forms.topolo.app"
873
+ ],
829
874
  "appId": "forms"
830
875
  },
831
876
  {
@@ -1003,7 +1048,9 @@ var APPLICATIONS = {
1003
1048
  "domains": [
1004
1049
  "mail.topolo.app"
1005
1050
  ],
1006
- "routes": [],
1051
+ "routes": [
1052
+ "mail.topolo.app"
1053
+ ],
1007
1054
  "appId": "mail"
1008
1055
  },
1009
1056
  {
@@ -1354,7 +1401,9 @@ var APPLICATIONS = {
1354
1401
  "domains": [
1355
1402
  "quro.topolo.app"
1356
1403
  ],
1357
- "routes": [],
1404
+ "routes": [
1405
+ "quro.topolo.app"
1406
+ ],
1358
1407
  "appId": null
1359
1408
  }
1360
1409
  ]
@@ -1638,7 +1687,9 @@ var APPLICATIONS = {
1638
1687
  "domains": [
1639
1688
  "survey.topolo.app"
1640
1689
  ],
1641
- "routes": [],
1690
+ "routes": [
1691
+ "survey.topolo.app"
1692
+ ],
1642
1693
  "appId": "survey"
1643
1694
  },
1644
1695
  {
@@ -2429,6 +2480,166 @@ function isMultipleOf(value, divisor) {
2429
2480
  const quotient = value / divisor;
2430
2481
  return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
2431
2482
  }
2483
+ function parseTopoloActionUploadContract(value) {
2484
+ if (!isRecord2(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
2485
+ throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
2486
+ }
2487
+ const contentTypeField = requiredField(value.contentTypeField, "contentTypeField");
2488
+ const fileNameField = requiredField(value.fileNameField, "fileNameField");
2489
+ const maxBytes = Number(value.maxBytes);
2490
+ const acceptedContentTypes = Array.isArray(value.acceptedContentTypes) ? value.acceptedContentTypes.map((item) => String(item).trim().toLowerCase()).filter(Boolean) : [];
2491
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || acceptedContentTypes.length === 0) {
2492
+ throw new TopoloSdkError("invalid_upload_contract", "The action upload contract must declare maxBytes and acceptedContentTypes.");
2493
+ }
2494
+ if (value.profile === "inline-base64") {
2495
+ return {
2496
+ profile: "inline-base64",
2497
+ contentField: requiredField(value.contentField, "contentField"),
2498
+ contentTypeField,
2499
+ fileNameField,
2500
+ maxBytes,
2501
+ acceptedContentTypes
2502
+ };
2503
+ }
2504
+ const uploadMethod = value.uploadMethod === "PUT" || value.uploadMethod === "POST" ? value.uploadMethod : null;
2505
+ const uploadUrlPath = requiredPath(value.uploadUrlPath, "uploadUrlPath");
2506
+ const complete = value.complete === void 0 ? void 0 : parseCompleteContract(value.complete);
2507
+ if (!uploadMethod) throw new TopoloSdkError("invalid_upload_contract", "The action upload contract has an invalid uploadMethod.");
2508
+ return {
2509
+ profile: "url-session",
2510
+ fileNameField,
2511
+ contentTypeField,
2512
+ sizeField: requiredField(value.sizeField, "sizeField"),
2513
+ maxBytes,
2514
+ acceptedContentTypes,
2515
+ uploadUrlPath,
2516
+ uploadMethod,
2517
+ ...complete ? { complete } : {}
2518
+ };
2519
+ }
2520
+ function prepareTopoloActionUploadInput(contractValue, content, input, options) {
2521
+ const contract = parseTopoloActionUploadContract(contractValue);
2522
+ if (contract.profile !== "inline-base64") throw unsupportedInlineProfile();
2523
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
2524
+ if (bytes.byteLength === 0) {
2525
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2526
+ }
2527
+ if (bytes.byteLength > contract.maxBytes) {
2528
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2529
+ }
2530
+ return mapUploadInput(contract, bytesToBase64(bytes), input, options);
2531
+ }
2532
+ function prepareTopoloActionBase64UploadInput(contractValue, contentBase64, input, options) {
2533
+ const contract = parseTopoloActionUploadContract(contractValue);
2534
+ if (contract.profile !== "inline-base64") throw unsupportedInlineProfile();
2535
+ if (!isCanonicalBase64(contentBase64)) {
2536
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must be canonical base64.");
2537
+ }
2538
+ const padding = contentBase64.endsWith("==") ? 2 : contentBase64.endsWith("=") ? 1 : 0;
2539
+ const byteLength = contentBase64.length / 4 * 3 - padding;
2540
+ if (byteLength === 0) throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2541
+ if (byteLength > contract.maxBytes) {
2542
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2543
+ }
2544
+ return mapUploadInput(contract, contentBase64, input, options);
2545
+ }
2546
+ function prepareTopoloUrlSessionUploadInput(contractValue, byteLength, input, options) {
2547
+ const contract = parseTopoloActionUploadContract(contractValue);
2548
+ if (contract.profile !== "url-session") {
2549
+ throw new TopoloSdkError("invalid_upload_contract", "This action does not publish a URL-session upload contract.");
2550
+ }
2551
+ validateUploadMetadata(contract, byteLength, options);
2552
+ return {
2553
+ contract,
2554
+ input: {
2555
+ ...input,
2556
+ [contract.fileNameField]: options.fileName.trim(),
2557
+ [contract.contentTypeField]: options.contentType.trim().toLowerCase(),
2558
+ [contract.sizeField]: byteLength
2559
+ }
2560
+ };
2561
+ }
2562
+ function topoloUploadResponseValue(response, path) {
2563
+ return path.split(".").reduce((value, segment) => isRecord2(value) ? value[segment] : void 0, response);
2564
+ }
2565
+ function decodeTopoloActionBase64(contentBase64) {
2566
+ if (!isCanonicalBase64(contentBase64)) {
2567
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must be canonical base64.");
2568
+ }
2569
+ const binary = atob(contentBase64);
2570
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
2571
+ }
2572
+ function mapUploadInput(contract, contentBase64, input, options) {
2573
+ validateUploadMetadata(contract, Math.max(1, decodedBase64Size(contentBase64)), options);
2574
+ const fileName = options.fileName.trim();
2575
+ const contentType = options.contentType.trim().toLowerCase();
2576
+ return {
2577
+ ...input,
2578
+ [contract.contentField]: contentBase64,
2579
+ [contract.contentTypeField]: contentType,
2580
+ [contract.fileNameField]: fileName
2581
+ };
2582
+ }
2583
+ function validateUploadMetadata(contract, byteLength, options) {
2584
+ const fileName = options.fileName.trim();
2585
+ const contentType = options.contentType.trim().toLowerCase();
2586
+ if (!fileName) throw new TopoloSdkError("invalid_upload_file_name", "Upload fileName must not be empty.");
2587
+ if (!Number.isSafeInteger(byteLength) || byteLength <= 0) {
2588
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2589
+ }
2590
+ if (byteLength > contract.maxBytes) {
2591
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2592
+ }
2593
+ if (!matchesContentType(contentType, contract.acceptedContentTypes)) {
2594
+ throw new TopoloSdkError("unsupported_upload_content_type", `Content type "${contentType}" is not accepted by this action.`);
2595
+ }
2596
+ }
2597
+ function decodedBase64Size(contentBase64) {
2598
+ const padding = contentBase64.endsWith("==") ? 2 : contentBase64.endsWith("=") ? 1 : 0;
2599
+ return contentBase64.length / 4 * 3 - padding;
2600
+ }
2601
+ function parseCompleteContract(value) {
2602
+ if (!isRecord2(value)) throw new TopoloSdkError("invalid_upload_contract", "The action upload completion contract is invalid.");
2603
+ return {
2604
+ actionId: requiredPath(value.actionId, "complete.actionId"),
2605
+ inputField: requiredField(value.inputField, "complete.inputField"),
2606
+ responsePath: requiredPath(value.responsePath, "complete.responsePath")
2607
+ };
2608
+ }
2609
+ function requiredPath(value, name) {
2610
+ const path = typeof value === "string" ? value.trim() : "";
2611
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(path)) {
2612
+ throw new TopoloSdkError("invalid_upload_contract", `The action upload contract has an invalid ${name}.`);
2613
+ }
2614
+ return path;
2615
+ }
2616
+ function unsupportedInlineProfile() {
2617
+ return new TopoloSdkError("invalid_upload_contract", "This action does not publish an inline-base64 upload contract.");
2618
+ }
2619
+ function isCanonicalBase64(value) {
2620
+ return value.length > 0 && value.length % 4 === 0 && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
2621
+ }
2622
+ function requiredField(value, name) {
2623
+ const field = typeof value === "string" ? value.trim() : "";
2624
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(field)) {
2625
+ throw new TopoloSdkError("invalid_upload_contract", `The action upload contract has an invalid ${name}.`);
2626
+ }
2627
+ return field;
2628
+ }
2629
+ function matchesContentType(contentType, accepted) {
2630
+ return accepted.some((candidate) => candidate === "*/*" || candidate === contentType || candidate.endsWith("/*") && contentType.startsWith(candidate.slice(0, -1)));
2631
+ }
2632
+ function bytesToBase64(bytes) {
2633
+ let binary = "";
2634
+ const chunkSize = 32768;
2635
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
2636
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
2637
+ }
2638
+ return btoa(binary);
2639
+ }
2640
+ function isRecord2(value) {
2641
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2642
+ }
2432
2643
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2433
2644
  var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
2434
2645
  var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
@@ -2505,6 +2716,9 @@ var TopoloClient = class _TopoloClient {
2505
2716
  if (v !== void 0) url.searchParams.set(k, String(v));
2506
2717
  }
2507
2718
  }
2719
+ if (opts.body !== void 0 && opts.rawBody !== void 0) {
2720
+ throw new TopoloSdkError("invalid_request_body", "body and rawBody are mutually exclusive.");
2721
+ }
2508
2722
  const headers = new Headers();
2509
2723
  headers.set("Accept", "application/json");
2510
2724
  if (opts.body !== void 0) headers.set("Content-Type", "application/json");
@@ -2539,7 +2753,7 @@ var TopoloClient = class _TopoloClient {
2539
2753
  return await this.fetchImpl(urlStr, {
2540
2754
  method,
2541
2755
  headers: new Headers(headers),
2542
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2756
+ body: opts.rawBody ?? (opts.body !== void 0 ? JSON.stringify(opts.body) : null),
2543
2757
  signal
2544
2758
  });
2545
2759
  } finally {
@@ -2587,14 +2801,32 @@ var TopoloClient = class _TopoloClient {
2587
2801
  error: message,
2588
2802
  status: res.status
2589
2803
  });
2590
- if (res.status === 401) throw new TopoloAuthError(describeError(parsed, "Unauthorized"));
2804
+ const errorContext = {
2805
+ status: res.status,
2806
+ body: parsed,
2807
+ service: resolvedService.serviceKey,
2808
+ path: opts.path,
2809
+ requestId: res.headers.get("X-Topolo-Request-Id") || requestId,
2810
+ retryAfter: res.headers.get("Retry-After") || void 0
2811
+ };
2812
+ if (res.status === 401) {
2813
+ throw new TopoloAuthError(describeError(parsed, "Unauthorized"), "auth_error", errorContext);
2814
+ }
2591
2815
  if (res.status === 403) {
2592
2816
  throw new TopoloPermissionError(
2593
2817
  describeError(parsed, "Permission denied"),
2594
- extractRequired(parsed)
2818
+ extractRequired(parsed),
2819
+ errorContext
2595
2820
  );
2596
2821
  }
2597
- throw new TopoloHttpError(resolvedService.serviceKey, opts.path, res.status, parsed);
2822
+ throw new TopoloHttpError(
2823
+ resolvedService.serviceKey,
2824
+ opts.path,
2825
+ res.status,
2826
+ parsed,
2827
+ message,
2828
+ errorContext
2829
+ );
2598
2830
  }
2599
2831
  this.emit({
2600
2832
  phase: "response",
@@ -2753,7 +2985,7 @@ var TopoloClient = class _TopoloClient {
2753
2985
  }
2754
2986
  const entries = [];
2755
2987
  for (const item of raw) {
2756
- if (!isRecord2(item)) continue;
2988
+ if (!isRecord3(item)) continue;
2757
2989
  const s = item;
2758
2990
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2759
2991
  if (!appId) continue;
@@ -2828,8 +3060,8 @@ var TopoloClient = class _TopoloClient {
2828
3060
  "app_topolo_auth.service_api_key_resources.list",
2829
3061
  { appId: service.appId }
2830
3062
  );
2831
- const payload = isRecord2(response) && isRecord2(response.data) ? response.data : response;
2832
- if (!isRecord2(payload) || !Array.isArray(payload.resources)) {
3063
+ const payload = isRecord3(response) && isRecord3(response.data) ? response.data : response;
3064
+ if (!isRecord3(payload) || !Array.isArray(payload.resources)) {
2833
3065
  throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
2834
3066
  }
2835
3067
  return {
@@ -2935,6 +3167,71 @@ var TopoloClient = class _TopoloClient {
2935
3167
  const entry = await this.getAction(action);
2936
3168
  return this.executeResolvedAction(entry, input, options);
2937
3169
  }
3170
+ async uploadAction(action, content, input, options) {
3171
+ const entry = await this.getAction(action);
3172
+ const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
3173
+ if (contract.profile === "url-session") {
3174
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
3175
+ return this.executeUrlSessionUpload(entry, contract, bytes, input, options);
3176
+ }
3177
+ const uploadInput = prepareTopoloActionUploadInput(entry.agentContract?.upload, content, input, options);
3178
+ return this.executeResolvedAction(entry, uploadInput, options);
3179
+ }
3180
+ async uploadActionBase64(action, contentBase64, input, options) {
3181
+ const entry = await this.getAction(action);
3182
+ const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
3183
+ if (contract.profile === "url-session") {
3184
+ return this.executeUrlSessionUpload(entry, contract, decodeTopoloActionBase64(contentBase64), input, options);
3185
+ }
3186
+ const uploadInput = prepareTopoloActionBase64UploadInput(entry.agentContract?.upload, contentBase64, input, options);
3187
+ return this.executeResolvedAction(entry, uploadInput, options);
3188
+ }
3189
+ async executeUrlSessionUpload(entry, contract, bytes, input, options) {
3190
+ const prepared = prepareTopoloUrlSessionUploadInput(contract, bytes.byteLength, input, options);
3191
+ const initiation = await this.executeResolvedAction(entry, prepared.input, options);
3192
+ const uploadUrl = topoloUploadResponseValue(initiation, contract.uploadUrlPath);
3193
+ if (typeof uploadUrl !== "string" || !uploadUrl.trim()) {
3194
+ throw new TopoloSdkError("invalid_upload_response", `Upload action response is missing ${contract.uploadUrlPath}.`);
3195
+ }
3196
+ await this.putUrlSessionContent(entry, uploadUrl, contract.uploadMethod, bytes, options);
3197
+ if (!contract.complete) return initiation;
3198
+ const completionValue = topoloUploadResponseValue(initiation, contract.complete.responsePath);
3199
+ if (typeof completionValue !== "string" || !completionValue.trim()) {
3200
+ throw new TopoloSdkError("invalid_upload_response", `Upload action response is missing ${contract.complete.responsePath}.`);
3201
+ }
3202
+ const completion = await this.getAction(contract.complete.actionId);
3203
+ return this.executeResolvedAction(completion, {
3204
+ ...options.completionInput ?? {},
3205
+ [contract.complete.inputField]: completionValue
3206
+ }, options);
3207
+ }
3208
+ async putUrlSessionContent(entry, uploadUrl, method, bytes, options) {
3209
+ const service = entry.appSlug || entry.appId;
3210
+ const resolved = entry.apiBaseUrl ? { serviceKey: normalizeLookupKey(service), baseUrl: requireHttpOrigin(entry.apiBaseUrl) } : await this.resolveRequestService(service);
3211
+ const target = new URL(uploadUrl, resolved.baseUrl.endsWith("/") ? resolved.baseUrl : `${resolved.baseUrl}/`);
3212
+ const body = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
3213
+ if (target.origin === new URL(resolved.baseUrl).origin) {
3214
+ await this.request({
3215
+ service,
3216
+ baseUrl: resolved.baseUrl,
3217
+ path: target.toString(),
3218
+ method,
3219
+ rawBody: body,
3220
+ headers: { "Content-Type": options.contentType.trim().toLowerCase() },
3221
+ resource: options.resource,
3222
+ confirm: options.confirm
3223
+ });
3224
+ return;
3225
+ }
3226
+ const response = await this.fetchImpl(target, {
3227
+ method,
3228
+ headers: { "Content-Type": options.contentType.trim().toLowerCase() },
3229
+ body
3230
+ });
3231
+ if (!response.ok) {
3232
+ throw new TopoloHttpError(service, target.pathname, response.status, await response.text(), `Upload transport failed with HTTP ${response.status}.`);
3233
+ }
3234
+ }
2938
3235
  async validateActionInput(action, input = {}) {
2939
3236
  const entry = await this.getAction(action);
2940
3237
  return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input) };
@@ -3093,7 +3390,7 @@ function resolveConfiguredAppId(slug, appIds) {
3093
3390
  return null;
3094
3391
  }
3095
3392
  function normalizeApiKeyResourceMetadata(input) {
3096
- if (!isRecord2(input)) return null;
3393
+ if (!isRecord3(input)) return null;
3097
3394
  const resourceType = readString(input.resourceType ?? input.resource_type);
3098
3395
  const label = readString(input.label);
3099
3396
  const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
@@ -3127,7 +3424,7 @@ function normalizeApiKeyResourceMetadataList(input) {
3127
3424
  return resources;
3128
3425
  }
3129
3426
  function normalizeBindableResource(input) {
3130
- if (!isRecord2(input)) return null;
3427
+ if (!isRecord3(input)) return null;
3131
3428
  const resourceType = readString(input.resourceType ?? input.resource_type);
3132
3429
  const resourceId = readString(input.resourceId ?? input.resource_id);
3133
3430
  if (!resourceType || !resourceId) return null;
@@ -3138,13 +3435,13 @@ function normalizeBindableResource(input) {
3138
3435
  slug: readOptionalString(input.slug),
3139
3436
  status: readOptionalString(input.status),
3140
3437
  organizationId: readOptionalString(input.organizationId ?? input.organization_id),
3141
- metadata: isRecord2(input.metadata) ? input.metadata : {}
3438
+ metadata: isRecord3(input.metadata) ? input.metadata : {}
3142
3439
  };
3143
3440
  }
3144
3441
  function normalizeResourceBindings(input) {
3145
3442
  if (!Array.isArray(input)) return [];
3146
3443
  return input.flatMap((binding) => {
3147
- if (!isRecord2(binding)) return [];
3444
+ if (!isRecord3(binding)) return [];
3148
3445
  const resourceType = readString(binding.resourceType ?? binding.resource_type);
3149
3446
  const resourceId = readString(binding.resourceId ?? binding.resource_id);
3150
3447
  return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
@@ -3157,7 +3454,7 @@ function readOptionalString(value) {
3157
3454
  const normalized = readString(value);
3158
3455
  return normalized || null;
3159
3456
  }
3160
- function isRecord2(value) {
3457
+ function isRecord3(value) {
3161
3458
  return typeof value === "object" && value !== null && !Array.isArray(value);
3162
3459
  }
3163
3460
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
@@ -3229,14 +3526,14 @@ function catalogSearchQuery(options) {
3229
3526
  return query;
3230
3527
  }
3231
3528
  function normalizeCatalogPage(value) {
3232
- if (!isRecord2(value)) return { nextCursor: null, hasMore: false };
3529
+ if (!isRecord3(value)) return { nextCursor: null, hasMore: false };
3233
3530
  return {
3234
3531
  nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
3235
3532
  hasMore: value.hasMore === true
3236
3533
  };
3237
3534
  }
3238
3535
  function normalizeServiceCatalogEntry(item) {
3239
- if (!isRecord2(item)) return null;
3536
+ if (!isRecord3(item)) return null;
3240
3537
  const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
3241
3538
  if (!appId) return null;
3242
3539
  const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
@@ -3482,23 +3779,50 @@ var TopoloSdkError2 = class extends Error {
3482
3779
  }
3483
3780
  };
3484
3781
  var TopoloAuthError2 = class extends TopoloSdkError2 {
3485
- constructor(message, code = "auth_error") {
3782
+ status;
3783
+ body;
3784
+ service;
3785
+ path;
3786
+ requestId;
3787
+ retryAfter;
3788
+ constructor(message, code = "auth_error", options = {}) {
3486
3789
  super(code, message);
3487
3790
  this.name = "TopoloAuthError";
3791
+ this.status = options.status ?? 401;
3792
+ this.body = options.body;
3793
+ this.service = options.service;
3794
+ this.path = options.path;
3795
+ this.requestId = options.requestId;
3796
+ this.retryAfter = options.retryAfter;
3488
3797
  }
3489
3798
  };
3799
+ function extractHttpErrorCode2(body) {
3800
+ if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
3801
+ const record = body;
3802
+ if (typeof record.code === "string" && record.code.trim()) return record.code.trim();
3803
+ const nested = record.error;
3804
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
3805
+ const code = nested.code;
3806
+ if (typeof code === "string" && code.trim()) return code.trim();
3807
+ }
3808
+ return "http_error";
3809
+ }
3490
3810
  var TopoloHttpError2 = class extends TopoloSdkError2 {
3491
3811
  status;
3492
3812
  body;
3493
3813
  service;
3494
3814
  path;
3495
- constructor(service, path, status, body, message) {
3496
- super("http_error", message ?? `HTTP ${status} from ${service}${path}`);
3815
+ requestId;
3816
+ retryAfter;
3817
+ constructor(service, path, status, body, message, options = {}) {
3818
+ super(extractHttpErrorCode2(body), message ?? `HTTP ${status} from ${service}${path}`);
3497
3819
  this.name = "TopoloHttpError";
3498
3820
  this.status = status;
3499
3821
  this.body = body;
3500
3822
  this.service = service;
3501
3823
  this.path = path;
3824
+ this.requestId = options.requestId;
3825
+ this.retryAfter = options.retryAfter;
3502
3826
  }
3503
3827
  };
3504
3828
  var APP_API_URLS2 = {
@@ -4126,6 +4450,60 @@ var TOOLS = [
4126
4450
  );
4127
4451
  }
4128
4452
  },
4453
+ {
4454
+ name: "topolo_upload_action",
4455
+ title: "Upload content through one Topolo action",
4456
+ description: "Uploads base64 content through an exact action id using its published upload contract. Mutating uploads require confirm:true.",
4457
+ requiredScopes: [],
4458
+ destructive: true,
4459
+ inputSchema: {
4460
+ type: "object",
4461
+ properties: {
4462
+ actionId: { type: "string", minLength: 1 },
4463
+ contentBase64: { type: "string", minLength: 4, description: "Canonical base64 content without a data URL prefix." },
4464
+ contentType: { type: "string", minLength: 1 },
4465
+ fileName: { type: "string", minLength: 1 },
4466
+ input: { type: "object", additionalProperties: true },
4467
+ completionInput: { type: "object", additionalProperties: true },
4468
+ resourceType: { type: "string" },
4469
+ resourceId: { type: "string" },
4470
+ confirm: { type: "boolean" }
4471
+ },
4472
+ required: ["actionId", "contentBase64", "contentType", "fileName"],
4473
+ additionalProperties: false
4474
+ },
4475
+ handler: async (topolo, args) => {
4476
+ const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
4477
+ const contentBase64 = typeof args["contentBase64"] === "string" ? args["contentBase64"] : "";
4478
+ const contentType = typeof args["contentType"] === "string" ? args["contentType"].trim() : "";
4479
+ const fileName = typeof args["fileName"] === "string" ? args["fileName"].trim() : "";
4480
+ if (!actionId) throw new TopoloMcpPublicError("actionId is required.");
4481
+ if (!isCanonicalBase642(contentBase64)) throw new TopoloMcpPublicError("contentBase64 must be canonical base64.");
4482
+ if (!contentType) throw new TopoloMcpPublicError("contentType is required.");
4483
+ if (!fileName) throw new TopoloMcpPublicError("fileName is required.");
4484
+ const input = args["input"];
4485
+ if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
4486
+ throw new TopoloMcpPublicError("input must be an object.");
4487
+ }
4488
+ const completionInput = args["completionInput"];
4489
+ if (completionInput !== void 0 && (!completionInput || typeof completionInput !== "object" || Array.isArray(completionInput))) {
4490
+ throw new TopoloMcpPublicError("completionInput must be an object.");
4491
+ }
4492
+ const resource = optionalResourceContext(args);
4493
+ return topolo.client.uploadActionBase64(
4494
+ actionId,
4495
+ contentBase64,
4496
+ input ?? {},
4497
+ {
4498
+ fileName,
4499
+ contentType,
4500
+ completionInput: completionInput ?? {},
4501
+ confirm: args["confirm"] === true,
4502
+ ...resource ? { resource } : {}
4503
+ }
4504
+ );
4505
+ }
4506
+ },
4129
4507
  {
4130
4508
  name: "topolo_validate_action",
4131
4509
  title: "Validate Topolo action input",
@@ -4429,6 +4807,9 @@ var TOOLS = [
4429
4807
  }
4430
4808
  }
4431
4809
  ];
4810
+ function isCanonicalBase642(value) {
4811
+ return value.length > 0 && value.length % 4 === 0 && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
4812
+ }
4432
4813
  function filterToolsByScopes(tools, scopes) {
4433
4814
  return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
4434
4815
  }
@@ -4566,16 +4947,36 @@ function buildServer(topolo, scopes, tools = TOOLS) {
4566
4947
  };
4567
4948
  } catch (err) {
4568
4949
  if (err instanceof TopoloPermissionError) {
4569
- return toolError(`Permission denied. Required: ${err.required.join(", ") || "(unknown)"}`);
4950
+ return toolStructuredError(err, `Permission denied. Required: ${err.required.join(", ") || "(unknown)"}`, {
4951
+ required: err.required
4952
+ });
4570
4953
  }
4571
4954
  if (err instanceof TopoloAuthError) {
4572
- return toolError("Authentication failed.");
4955
+ return toolStructuredError(err, "Authentication failed.");
4956
+ }
4957
+ if (err instanceof TopoloHttpError) {
4958
+ return toolStructuredError(err, err.message);
4573
4959
  }
4574
4960
  return toolError(publicMcpErrorMessage(err, "Tool execution failed."));
4575
4961
  }
4576
4962
  });
4577
4963
  return server;
4578
4964
  }
4965
+ function toolStructuredError(error, message, extra = {}) {
4966
+ return toolError(JSON.stringify({
4967
+ error: {
4968
+ code: error.code,
4969
+ status: error.status,
4970
+ message,
4971
+ service: error.service,
4972
+ path: error.path,
4973
+ requestId: error.requestId,
4974
+ retryAfter: error.retryAfter,
4975
+ details: error.body,
4976
+ ...extra
4977
+ }
4978
+ }, null, 2));
4979
+ }
4579
4980
  async function advertisedToolNames(topolo, scopes, tools = TOOLS) {
4580
4981
  return (await buildTopoloTools(topolo, scopes, tools)).map((t) => t.name);
4581
4982
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/mcp",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
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": {
@@ -20,7 +20,7 @@
20
20
  "dist"
21
21
  ],
22
22
  "scripts": {
23
- "build:sdk": "pnpm --dir ../topolo-sdk build",
23
+ "build:sdk": "python3 ../../tooling/with-resource-lock.py ../topolo-sdk -- pnpm --dir ../topolo-sdk build",
24
24
  "prebuild": "pnpm run build:sdk",
25
25
  "build": "tsup",
26
26
  "dev": "tsup --watch",
@@ -36,7 +36,7 @@
36
36
  "@modelcontextprotocol/sdk": "^1.0.0"
37
37
  },
38
38
  "peerDependencies": {
39
- "@topolo/sdk": "^0.9.6"
39
+ "@topolo/sdk": "^0.9.8"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@topolo/sdk": "file:../topolo-sdk",