@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 +57 -0
- package/dist/http.js +80 -3
- package/dist/index.js +430 -29
- package/package.json +3 -3
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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(
|
|
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 (!
|
|
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 =
|
|
2832
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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:
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
3496
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
39
|
+
"@topolo/sdk": "^0.9.8"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@topolo/sdk": "file:../topolo-sdk",
|