@transloadit/mcp-server 0.7.4 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -3
- package/dist/builtin-shortcuts.d.ts +17 -3
- package/dist/builtin-shortcuts.d.ts.map +1 -1
- package/dist/builtin-shortcuts.js +51 -4
- package/dist/builtin-shortcuts.js.map +1 -1
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/http-helpers.d.ts +1 -0
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js.map +1 -1
- package/dist/logger.d.ts +2 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -1
- package/dist/options.d.ts +1 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +7 -0
- package/dist/options.js.map +1 -1
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +445 -90
- package/dist/server.js.map +1 -1
- package/dist/tool-metadata.d.ts +6 -0
- package/dist/tool-metadata.d.ts.map +1 -1
- package/dist/tool-metadata.js +20 -7
- package/dist/tool-metadata.js.map +1 -1
- package/dist/ui/assembly-result-widget.d.ts.map +1 -1
- package/dist/ui/assembly-result-widget.js +3 -1
- package/dist/ui/assembly-result-widget.js.map +1 -1
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -3,11 +3,12 @@ import { ApiError, extractFieldNamesFromTemplate, getRobotHelp, isKnownRobot, li
|
|
|
3
3
|
import { builtinTemplateFieldsSchema } from '@transloadit/zod/v4/builtinTemplates';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import packageJson from '../package.json' with { type: 'json' };
|
|
6
|
-
import { applyBuiltinCatalog, BuiltinFieldValidationError, builtinShortcuts, loadBuiltinShortcuts, validateBuiltinFields, } from "./builtin-shortcuts.js";
|
|
6
|
+
import { applyBuiltinCatalog, BuiltinFieldValidationError, builtinShortcuts, loadBuiltinShortcuts, selectBuiltinInputUrls, validateBuiltinFields, } from "./builtin-shortcuts.js";
|
|
7
7
|
import { buildBearerChallenge, extractBearerToken } from "./http-helpers.js";
|
|
8
|
+
import { buildRedactor, getDiagnosticsLogger } from "./logger.js";
|
|
8
9
|
import { assertServerOptions } from "./options.js";
|
|
9
10
|
import { mirrorSecuritySchemes } from "./tool-list.js";
|
|
10
|
-
import { buildToolMeta, resolveSecuritySchemes, resolveToolAuthMode, toolMetadata, } from "./tool-metadata.js";
|
|
11
|
+
import { buildToolMeta, resolveSecuritySchemes, resolveToolAuthMode, serverInstructions, toolMetadata, } from "./tool-metadata.js";
|
|
11
12
|
import { describeAssemblySteps, registerAssemblyResultWidget, widgetContextMetaKey, } from "./ui/assembly-result-widget.js";
|
|
12
13
|
import { buildUploadInstructions, uploadInstructionSchema } from "./upload-instructions.js";
|
|
13
14
|
const defaultConsoleUrl = 'https://transloadit.com';
|
|
@@ -19,6 +20,16 @@ const maxBase64Bytes = 512_000;
|
|
|
19
20
|
/** URL inputs beyond these limits are not downloaded to this server's disk (per call). */
|
|
20
21
|
const defaultMaxUrlDownloadBytes = 1024 * 1024 * 1024;
|
|
21
22
|
const defaultUrlDownloadTimeoutMs = 10 * 60 * 1000;
|
|
23
|
+
/**
|
|
24
|
+
* Hosts abandon tool calls that run too long (ChatGPT after about a minute) and may retry them,
|
|
25
|
+
* creating a second Assembly. Calls that create or follow an Assembly therefore return its
|
|
26
|
+
* current state once this budget is spent; transloadit_wait_for_assembly follows it from there.
|
|
27
|
+
*/
|
|
28
|
+
const defaultMaxWaitMs = 45_000;
|
|
29
|
+
/** Lowest request timeout handed to the SDK, so a nearly spent budget cannot fail a request. */
|
|
30
|
+
const minimumSdkTimeoutMs = 10_000;
|
|
31
|
+
/** Longest a status read after an early return may take. */
|
|
32
|
+
const statusReadTimeoutMs = 5_000;
|
|
22
33
|
const lintIssueSchema = z.object({
|
|
23
34
|
path: z.string(),
|
|
24
35
|
message: z.string(),
|
|
@@ -126,6 +137,7 @@ const createAssemblyInputSchema = z.object({
|
|
|
126
137
|
const createAssemblyOutputSchema = z.object({
|
|
127
138
|
status: z.enum(['ok', 'error']),
|
|
128
139
|
assembly: z.unknown().optional(),
|
|
140
|
+
running: z.boolean().optional(),
|
|
129
141
|
upload: z
|
|
130
142
|
.object({
|
|
131
143
|
status: z.enum(['none', 'uploading', 'complete']),
|
|
@@ -158,6 +170,7 @@ const waitForAssemblyInputSchema = z.object({
|
|
|
158
170
|
const waitForAssemblyOutputSchema = z.object({
|
|
159
171
|
status: z.enum(['ok', 'error']),
|
|
160
172
|
assembly: z.unknown().optional(),
|
|
173
|
+
running: z.boolean().optional(),
|
|
161
174
|
waited_ms: z.number().int().nonnegative().optional(),
|
|
162
175
|
errors: z.array(toolMessageSchema).optional(),
|
|
163
176
|
warnings: z.array(toolMessageSchema).optional(),
|
|
@@ -289,6 +302,65 @@ const buildShortcutError = (...args) => ({
|
|
|
289
302
|
...buildToolError(...args),
|
|
290
303
|
isError: true,
|
|
291
304
|
});
|
|
305
|
+
/** Maps an input preparation failure to a tool error; API rejections are left to the caller. */
|
|
306
|
+
const buildInputError = (error) => {
|
|
307
|
+
// API rejections (the token check) are not input mistakes; the outer handler maps them.
|
|
308
|
+
if (error instanceof ApiError)
|
|
309
|
+
throw error;
|
|
310
|
+
const message = error instanceof Error ? error.message : 'Invalid file input.';
|
|
311
|
+
if (message.startsWith('Duplicate file field')) {
|
|
312
|
+
return buildToolError('mcp_duplicate_field', message, { path: 'files' });
|
|
313
|
+
}
|
|
314
|
+
if (message.startsWith('Base64 payload exceeds')) {
|
|
315
|
+
return buildToolError('mcp_base64_too_large', message, {
|
|
316
|
+
hint: 'Use a public URL import or upload from your own machine instead.',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return buildToolError('mcp_invalid_args', message);
|
|
320
|
+
};
|
|
321
|
+
const deadlineReached = Symbol('deadlineReached');
|
|
322
|
+
/** Stops input preparation from starting another download once the wait budget is spent. */
|
|
323
|
+
class PreparationBudgetSpentError extends Error {
|
|
324
|
+
name = 'PreparationBudgetSpentError';
|
|
325
|
+
}
|
|
326
|
+
/** Settles like `promise`, or with `deadlineReached` when it is still pending after `ms`. */
|
|
327
|
+
const settleWithin = async (promise, ms) => {
|
|
328
|
+
let timer;
|
|
329
|
+
const deadline = new Promise((resolve) => {
|
|
330
|
+
timer = setTimeout(resolve, ms, deadlineReached);
|
|
331
|
+
});
|
|
332
|
+
try {
|
|
333
|
+
return await Promise.race([promise, deadline]);
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
/** The SDK does not export PollingTimeoutError, so it is recognized by name. */
|
|
340
|
+
const isPollingTimeout = (error) => error instanceof Error && error.name === 'PollingTimeoutError';
|
|
341
|
+
/** The same states the SDK's awaitAssemblyCompletion keeps polling on. */
|
|
342
|
+
const runningAssemblyStates = new Set([
|
|
343
|
+
'ASSEMBLY_UPLOADING',
|
|
344
|
+
'ASSEMBLY_EXECUTING',
|
|
345
|
+
'ASSEMBLY_REPLAYING',
|
|
346
|
+
]);
|
|
347
|
+
const isAssemblyRunning = (assembly) => !assembly.error && typeof assembly.ok === 'string' && runningAssemblyStates.has(assembly.ok);
|
|
348
|
+
/** Tells the model that the call returned early and how to follow the Assembly instead of retrying. */
|
|
349
|
+
const buildStillRunningNotice = (assembly, fallbackUrl) => {
|
|
350
|
+
const assemblyUrl = isNonEmptyString(assembly.assembly_ssl_url)
|
|
351
|
+
? assembly.assembly_ssl_url
|
|
352
|
+
: isNonEmptyString(assembly.assembly_url)
|
|
353
|
+
? assembly.assembly_url
|
|
354
|
+
: fallbackUrl;
|
|
355
|
+
const follow = 'Follow it with transloadit_wait_for_assembly or transloadit_get_assembly_status';
|
|
356
|
+
return {
|
|
357
|
+
code: 'mcp_assembly_still_running',
|
|
358
|
+
message: 'Returned before the Assembly finished, within the time this call may wait. The Assembly keeps running.',
|
|
359
|
+
hint: assemblyUrl
|
|
360
|
+
? `${follow} using assembly_url ${assemblyUrl}; do not create it again.`
|
|
361
|
+
: `${follow}; do not create it again.`,
|
|
362
|
+
};
|
|
363
|
+
};
|
|
292
364
|
const getClientName = (options) => {
|
|
293
365
|
const base = options.clientName ?? `mcp-server:${packageJson.version}`;
|
|
294
366
|
const suffix = options.clientSuffix;
|
|
@@ -436,22 +508,53 @@ const toAuthRejection = (options, error, credentials, scopes) => {
|
|
|
436
508
|
}
|
|
437
509
|
return undefined;
|
|
438
510
|
};
|
|
511
|
+
/** Matches resolveAssemblyReference: the SDK's default when the endpoint is empty. */
|
|
512
|
+
const defaultAssemblyUrl = (options, assemblyId) => `${options.endpoint || 'https://api2.transloadit.com'}/assemblies/${assemblyId}`;
|
|
439
513
|
/** The Assembly exists, but its status could not be read with the caller's credentials. */
|
|
440
|
-
const buildCreatedAssemblyUnavailable = (options, assemblyId) => {
|
|
441
|
-
|
|
442
|
-
|
|
514
|
+
const buildCreatedAssemblyUnavailable = (options, assemblyId, describe = (assemblyUrl) => ({
|
|
515
|
+
code: 'mcp_assembly_status_unavailable',
|
|
516
|
+
message: 'The Assembly was created, but Transloadit rejected the credentials while reading its status.',
|
|
517
|
+
hint: `Reconnect if needed, then call transloadit_get_assembly_status with assembly_url ${assemblyUrl} instead of creating it again.`,
|
|
518
|
+
})) => {
|
|
519
|
+
const assemblyUrl = defaultAssemblyUrl(options, assemblyId);
|
|
443
520
|
return buildToolResponse({
|
|
444
521
|
status: 'error',
|
|
445
522
|
assembly: { assembly_id: assemblyId, assembly_ssl_url: assemblyUrl },
|
|
446
|
-
errors: [
|
|
447
|
-
{
|
|
448
|
-
code: 'mcp_assembly_status_unavailable',
|
|
449
|
-
message: 'The Assembly was created, but Transloadit rejected the credentials while reading its status.',
|
|
450
|
-
hint: `Reconnect if needed, then call transloadit_get_assembly_status with assembly_url ${assemblyUrl} instead of creating it again.`,
|
|
451
|
-
},
|
|
452
|
-
],
|
|
523
|
+
errors: [describe(assemblyUrl)],
|
|
453
524
|
}, { isError: true });
|
|
454
525
|
};
|
|
526
|
+
/** The creation request is still in flight after the budget; its ID is chosen by this server. */
|
|
527
|
+
const buildCreationPending = (options, assemblyId, serverFiles,
|
|
528
|
+
/** Set when some of them are uploaded from the caller's sandbox. */
|
|
529
|
+
expectedUploads) => buildCreatedAssemblyUnavailable(options, assemblyId, (assemblyUrl) => {
|
|
530
|
+
// An Assembly that API2 accepted only after the request was stopped never receives this
|
|
531
|
+
// call's files, so it is resumed rather than created again: another Assembly could repeat
|
|
532
|
+
// Steps that already ran, such as imports. API2 could in theory still be handling a request
|
|
533
|
+
// it received before the abort, so the hint asks for a pause first. Strict idempotency would
|
|
534
|
+
// need an API2 creation key, which does not exist; this path only runs after a request
|
|
535
|
+
// already took longer than the budget.
|
|
536
|
+
const resumeArguments = [
|
|
537
|
+
`assembly_url ${assemblyUrl}`,
|
|
538
|
+
...(serverFiles > 0 ? ['the same files'] : []),
|
|
539
|
+
...(expectedUploads ? [`expected_uploads ${expectedUploads}`] : []),
|
|
540
|
+
];
|
|
541
|
+
const resume = serverFiles > 0 || expectedUploads
|
|
542
|
+
? `. If it is still waiting for uploads (ASSEMBLY_UPLOADING), its files were never sent: resume it by calling transloadit_create_assembly with ${resumeArguments.join(' and ')}${expectedUploads ? ', which hands out new upload_instructions' : ''}`
|
|
543
|
+
: '';
|
|
544
|
+
return {
|
|
545
|
+
code: 'mcp_assembly_creation_pending',
|
|
546
|
+
message: 'Transloadit did not confirm the Assembly in time, so this server stopped sending the creation request. Transloadit may still have received it.',
|
|
547
|
+
hint: `Wait about 30 seconds, then call transloadit_get_assembly_status with assembly_url ${assemblyUrl}. Create it again only if several checks over a few minutes report that the Assembly does not exist${resume}.`,
|
|
548
|
+
};
|
|
549
|
+
});
|
|
550
|
+
/** Reserved upload URLs that an early return could not include yet. */
|
|
551
|
+
const buildUploadUrlsPending = (assemblyUrl) => ({
|
|
552
|
+
code: 'mcp_upload_urls_pending',
|
|
553
|
+
message: 'The upload URLs were not reserved before this call returned.',
|
|
554
|
+
// The reservations this call started keep running; asking at once could reserve the files
|
|
555
|
+
// a second time instead of returning the URLs those reservations produce.
|
|
556
|
+
hint: `Wait about 30 seconds, then call transloadit_create_assembly with assembly_url ${assemblyUrl ?? 'from this result'}, the same files and upload_behavior "none" to get them.`,
|
|
557
|
+
});
|
|
455
558
|
/** A failed call caused by server-side credentials; an error result so strict outputs still pass. */
|
|
456
559
|
const buildCredentialError = (error) => buildToolResponse({ status: 'error', errors: [error] }, { isError: true });
|
|
457
560
|
/**
|
|
@@ -573,6 +676,25 @@ const mergeFieldValues = (templateFields, argFields) => {
|
|
|
573
676
|
};
|
|
574
677
|
const assemblyIdSchema = z.string().regex(/^[a-f\d]{32}$/i);
|
|
575
678
|
const assemblyUrlSchema = z.url();
|
|
679
|
+
/**
|
|
680
|
+
* Reads an Assembly's current status once, without outlasting the host's patience. The SDK's
|
|
681
|
+
* rate-limit backoff does not observe the abort signal, so the read is also raced against the
|
|
682
|
+
* clock; without a status in time, it returns undefined.
|
|
683
|
+
*/
|
|
684
|
+
const readAssemblyStatus = async (client, assemblyId) => {
|
|
685
|
+
const status = await settleWithin(
|
|
686
|
+
// Aborts after the race below gave up, so the abort never becomes the call's error.
|
|
687
|
+
client.getAssembly(assemblyId, { signal: AbortSignal.timeout(statusReadTimeoutMs + 1_000) }), statusReadTimeoutMs);
|
|
688
|
+
return status === deadlineReached ? undefined : status;
|
|
689
|
+
};
|
|
690
|
+
/** Answers a call that returned early when not even the Assembly's status arrived in time. */
|
|
691
|
+
const buildStatusPendingResponse = (reference, fields = {}, warnings = []) => buildToolResponse({
|
|
692
|
+
status: 'ok',
|
|
693
|
+
assembly: { assembly_id: reference.assemblyId, assembly_ssl_url: reference.assemblyUrl },
|
|
694
|
+
running: true,
|
|
695
|
+
...fields,
|
|
696
|
+
warnings: [buildStillRunningNotice({}, reference.assemblyUrl), ...warnings],
|
|
697
|
+
});
|
|
576
698
|
/** Production (`.com`) and development or tunnel (`.dev`) Transloadit hosts. */
|
|
577
699
|
const isTransloaditHost = (hostname) => ['transloadit.com', 'transloadit.dev'].some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
578
700
|
const resolveAssemblyReference = (options, args) => {
|
|
@@ -768,10 +890,12 @@ export const listOwnTools = (server) => {
|
|
|
768
890
|
};
|
|
769
891
|
export const createTransloaditMcpServer = (options = {}) => {
|
|
770
892
|
assertServerOptions(options);
|
|
893
|
+
const logger = (options.logger ?? getDiagnosticsLogger()).nest('assemblies');
|
|
894
|
+
const redactError = buildRedactor([options.authSecret, options.upstreamSecret, options.mcpToken]);
|
|
771
895
|
const server = new McpServer({
|
|
772
896
|
name: options.serverName ?? 'Transloadit MCP',
|
|
773
897
|
version: options.serverVersion ?? packageJson.version,
|
|
774
|
-
});
|
|
898
|
+
}, { instructions: serverInstructions });
|
|
775
899
|
const authMode = resolveToolAuthMode(options);
|
|
776
900
|
const ownTools = new Map();
|
|
777
901
|
ownToolsByServer.set(server, ownTools);
|
|
@@ -807,7 +931,9 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
807
931
|
}
|
|
808
932
|
return buildToolResponse(payload);
|
|
809
933
|
});
|
|
810
|
-
const createAssembly = register('transloadit_create_assembly', createAssemblyInputSchema, createAssemblyOutputSchema, async ({ instructions, files, attachments, fields, wait_for_completion, wait_timeout_ms, upload_concurrency, upload_chunk_size, upload_behavior, expected_uploads, assembly_url, }, extra, fieldContractValidated = false
|
|
934
|
+
const createAssembly = register('transloadit_create_assembly', createAssemblyInputSchema, createAssemblyOutputSchema, async ({ instructions, files, attachments, fields, wait_for_completion, wait_timeout_ms, upload_concurrency, upload_chunk_size, upload_behavior, expected_uploads, assembly_url, }, extra, fieldContractValidated = false,
|
|
935
|
+
// Shortcuts start the clock before loading their catalog, which counts toward the budget.
|
|
936
|
+
startedAt = performance.now()) => {
|
|
811
937
|
const liveClient = createLiveClient(options, extra);
|
|
812
938
|
if ('error' in liveClient)
|
|
813
939
|
return liveClient.error;
|
|
@@ -816,8 +942,19 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
816
942
|
if (reference && 'error' in reference)
|
|
817
943
|
return reference.error;
|
|
818
944
|
const tempCleanups = [];
|
|
945
|
+
// Uploads or polling that continue after the call returned early.
|
|
946
|
+
let backgroundWork;
|
|
819
947
|
const warnings = [];
|
|
820
948
|
let templatePathHint;
|
|
949
|
+
const maxWaitMs = options.maxWaitMs ?? defaultMaxWaitMs;
|
|
950
|
+
// Preparation (Template lookup, file inputs) runs before any Assembly exists, so failing
|
|
951
|
+
// once it used up the budget is safe to retry, unlike a host timeout after creation.
|
|
952
|
+
const preparationRemainingMs = () => Math.max(startedAt + maxWaitMs - performance.now(), 0);
|
|
953
|
+
const preparationTimeout = (reason) => buildToolError('mcp_preparation_timeout', reference
|
|
954
|
+
? 'Preparing the files took too long, so none were added to the Assembly.'
|
|
955
|
+
: 'Preparing the Assembly took too long, so none was created.', {
|
|
956
|
+
hint: reason ? `Retrying this call is safe. ${reason}` : 'Retrying this call is safe.',
|
|
957
|
+
});
|
|
821
958
|
try {
|
|
822
959
|
const fileInputs = [...(files ?? []), ...toAttachmentInputs(attachments ?? [])];
|
|
823
960
|
const urlInputs = fileInputs.filter((file) => file.kind === 'url');
|
|
@@ -833,7 +970,10 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
833
970
|
templatePathHint = templatePathHint ?? 'instructions.template_id';
|
|
834
971
|
let template;
|
|
835
972
|
try {
|
|
836
|
-
|
|
973
|
+
const lookup = await settleWithin(client.getTemplate(params.template_id), preparationRemainingMs());
|
|
974
|
+
if (lookup === deadlineReached)
|
|
975
|
+
return preparationTimeout();
|
|
976
|
+
template = lookup;
|
|
837
977
|
}
|
|
838
978
|
catch (error) {
|
|
839
979
|
const isTemplateNotFound = (error instanceof ApiError && error.code === 'TEMPLATE_NOT_FOUND') ||
|
|
@@ -924,7 +1064,7 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
924
1064
|
tokenCheck ??= client.listTemplates({ pagesize: 1 });
|
|
925
1065
|
await tokenCheck;
|
|
926
1066
|
};
|
|
927
|
-
const
|
|
1067
|
+
const preparation = prepareInputFiles({
|
|
928
1068
|
inputFiles: inputFilesForPrep,
|
|
929
1069
|
params,
|
|
930
1070
|
fields,
|
|
@@ -933,23 +1073,37 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
933
1073
|
allowPrivateUrls: false,
|
|
934
1074
|
maxBase64Bytes,
|
|
935
1075
|
maxUrlDownloadBytes: options.maxUrlDownloadBytes ?? defaultMaxUrlDownloadBytes,
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
//
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
return buildToolError('mcp_invalid_args', message);
|
|
1076
|
+
// prepareInputFiles() takes no abort signal, so its download timeout is what stops a
|
|
1077
|
+
// download once the budget is spent; a retry would otherwise run next to it. The extra
|
|
1078
|
+
// second lets the budget check below answer first, with mcp_preparation_timeout. Each
|
|
1079
|
+
// download gets this timeout from its own start, but beforeUrlDownload starts none
|
|
1080
|
+
// after the budget, so an abandoned download ends at most one timeout after the budget
|
|
1081
|
+
// (twice the budget at worst). A tighter bound needs an abort signal in the SDK, which
|
|
1082
|
+
// the URL import work in API2 makes unnecessary.
|
|
1083
|
+
urlDownloadTimeoutMs: Math.min(options.urlDownloadTimeoutMs ?? defaultUrlDownloadTimeoutMs, preparationRemainingMs() + 1_000),
|
|
1084
|
+
// Runs before each download, so an abandoned preparation starts no further one.
|
|
1085
|
+
beforeUrlDownload: async () => {
|
|
1086
|
+
if (credentials === 'bearer')
|
|
1087
|
+
await verifyTokenBeforeDownload();
|
|
1088
|
+
if (preparationRemainingMs() === 0)
|
|
1089
|
+
throw new PreparationBudgetSpentError();
|
|
1090
|
+
},
|
|
952
1091
|
});
|
|
1092
|
+
const prep = await settleWithin(preparation, preparationRemainingMs()).catch((error) =>
|
|
1093
|
+
// The budget check can reject just before the deadline timer fires.
|
|
1094
|
+
error instanceof PreparationBudgetSpentError ? deadlineReached : buildInputError(error));
|
|
1095
|
+
if (prep === deadlineReached) {
|
|
1096
|
+
// The download stops at its timeout and prepareInputFiles() removes its files; one that
|
|
1097
|
+
// still finishes first returns files that nothing uploads.
|
|
1098
|
+
void preparation
|
|
1099
|
+
.then((late) => Promise.all(late.cleanup.map((cleanupFile) => cleanupFile())), () => undefined)
|
|
1100
|
+
.catch((error) => {
|
|
1101
|
+
logger.warn('Removing prepared files failed: %s', redactError(error));
|
|
1102
|
+
});
|
|
1103
|
+
return preparationTimeout(inputFilesForPrep.some((file) => file.kind === 'url')
|
|
1104
|
+
? 'A retry downloads the URL inputs again from the start. To have Transloadit fetch large files itself, use transloadit_create_assembly with Steps that contain an /http/import Step.'
|
|
1105
|
+
: undefined);
|
|
1106
|
+
}
|
|
953
1107
|
if ('content' in prep) {
|
|
954
1108
|
return prep;
|
|
955
1109
|
}
|
|
@@ -962,68 +1116,173 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
962
1116
|
status: totalFiles > 0 ? 'complete' : 'none',
|
|
963
1117
|
total_files: totalFiles,
|
|
964
1118
|
};
|
|
965
|
-
const timeout = wait_timeout_ms;
|
|
966
1119
|
// Files the caller uploads itself (from a sandbox) can only start once this call returns,
|
|
967
|
-
// so waiting here would just run into the timeout.
|
|
968
|
-
|
|
1120
|
+
// so waiting here would just run into the timeout. A resumed Assembly hands them out
|
|
1121
|
+
// again, e.g. after a creation request that was stopped. Whether its uploads already
|
|
1122
|
+
// arrived shows only in its status, so a resume with expected_uploads does not wait
|
|
1123
|
+
// either; next_steps then point to transloadit_wait_for_assembly.
|
|
1124
|
+
const outOfBandUploads = Math.max((expected_uploads ?? 0) - totalFiles, 0);
|
|
969
1125
|
const waitForCompletion = (wait_for_completion ?? false) && outOfBandUploads === 0;
|
|
970
|
-
if (wait_for_completion && outOfBandUploads > 0) {
|
|
971
|
-
warnings.push({
|
|
972
|
-
code: 'mcp_wait_skipped_for_uploads',
|
|
973
|
-
message: 'Did not wait for completion because the Assembly waits for the expected uploads. Run upload_instructions, then call transloadit_wait_for_assembly.',
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
1126
|
const uploadBehavior = upload_behavior ?? (waitForCompletion ? 'await' : 'background');
|
|
977
1127
|
const uploadConcurrency = upload_concurrency;
|
|
978
1128
|
const chunkSize = upload_chunk_size;
|
|
1129
|
+
const deadline = startedAt +
|
|
1130
|
+
(waitForCompletion ? Math.min(wait_timeout_ms ?? maxWaitMs, maxWaitMs) : maxWaitMs);
|
|
1131
|
+
const remainingMs = () => Math.max(deadline - performance.now(), 0);
|
|
1132
|
+
// The SDK also applies this timeout to the creation request. It outlasts the deadline and
|
|
1133
|
+
// the creation grace period after it, so a request timeout never fails a creation that
|
|
1134
|
+
// API2 may have accepted; the deadline below bounds the call itself. Polling after an
|
|
1135
|
+
// early return is not stopped separately: it ends with this timeout, a few seconds later,
|
|
1136
|
+
// and the signal that could stop it would also stop uploads the Assembly still needs.
|
|
1137
|
+
const timeout = Math.max(remainingMs() + statusReadTimeoutMs + 1_000, minimumSdkTimeoutMs);
|
|
1138
|
+
// Every status the SDK reports, so a call that returns early still has the latest state.
|
|
1139
|
+
let latest;
|
|
1140
|
+
const trackProgress = (status) => {
|
|
1141
|
+
latest = status;
|
|
1142
|
+
};
|
|
979
1143
|
let assembly;
|
|
980
1144
|
// Set by the SDK only after API2 accepted the creation request, so a rejected request
|
|
981
1145
|
// (including an error body with HTTP 200) is never mistaken for an existing Assembly.
|
|
982
1146
|
let createdAssemblyId;
|
|
1147
|
+
let resolveCreated = () => undefined;
|
|
1148
|
+
const created = new Promise((resolve) => {
|
|
1149
|
+
resolveCreated = resolve;
|
|
1150
|
+
});
|
|
1151
|
+
let work;
|
|
1152
|
+
// Chosen by the SDK before the creation request is sent.
|
|
1153
|
+
let creationAssemblyId;
|
|
1154
|
+
// Stops a creation request (and its rate-limit retries) that outlasted the call.
|
|
1155
|
+
const creationAbort = new AbortController();
|
|
1156
|
+
if (reference) {
|
|
1157
|
+
work = client.resumeAssemblyUploads({
|
|
1158
|
+
assemblyUrl: reference.assemblyUrl,
|
|
1159
|
+
files: filesMap,
|
|
1160
|
+
uploads: uploadsMap,
|
|
1161
|
+
waitForCompletion,
|
|
1162
|
+
timeout,
|
|
1163
|
+
uploadConcurrency,
|
|
1164
|
+
chunkSize,
|
|
1165
|
+
uploadBehavior,
|
|
1166
|
+
onAssemblyProgress: trackProgress,
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
const creation = client.createAssembly({
|
|
1171
|
+
params,
|
|
1172
|
+
files: filesMap,
|
|
1173
|
+
uploads: uploadsMap,
|
|
1174
|
+
waitForCompletion,
|
|
1175
|
+
timeout,
|
|
1176
|
+
uploadConcurrency,
|
|
1177
|
+
chunkSize,
|
|
1178
|
+
uploadBehavior,
|
|
1179
|
+
expectedUploads: expected_uploads,
|
|
1180
|
+
onAssemblyCreated: (status) => {
|
|
1181
|
+
createdAssemblyId = status.assembly_id ?? creation.assemblyId;
|
|
1182
|
+
latest = status;
|
|
1183
|
+
resolveCreated(status);
|
|
1184
|
+
},
|
|
1185
|
+
onAssemblyProgress: trackProgress,
|
|
1186
|
+
signal: creationAbort.signal,
|
|
1187
|
+
});
|
|
1188
|
+
creationAssemblyId = creation.assemblyId;
|
|
1189
|
+
work = creation;
|
|
1190
|
+
}
|
|
1191
|
+
let workSettled = true;
|
|
1192
|
+
let returnedEarly = false;
|
|
983
1193
|
try {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
files: filesMap,
|
|
988
|
-
uploads: uploadsMap,
|
|
989
|
-
waitForCompletion,
|
|
990
|
-
timeout,
|
|
991
|
-
uploadConcurrency,
|
|
992
|
-
chunkSize,
|
|
993
|
-
uploadBehavior,
|
|
994
|
-
});
|
|
1194
|
+
const outcome = await settleWithin(work, remainingMs());
|
|
1195
|
+
if (outcome !== deadlineReached) {
|
|
1196
|
+
assembly = outcome;
|
|
995
1197
|
}
|
|
996
1198
|
else {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
},
|
|
1199
|
+
// Hosts abandon calls that outlast their deadline and retry them, which creates a
|
|
1200
|
+
// second Assembly. Return what is known instead; the Assembly keeps running.
|
|
1201
|
+
workSettled = false;
|
|
1202
|
+
returnedEarly = true;
|
|
1203
|
+
backgroundWork = work.then(() => undefined, (error) => {
|
|
1204
|
+
if (isPollingTimeout(error) || creationAbort.signal.aborted)
|
|
1205
|
+
return;
|
|
1206
|
+
// A creation failure within the grace period below also reaches the caller; the
|
|
1207
|
+
// extra log line is accepted to keep this handler attached before that wait.
|
|
1208
|
+
// Hosted instances keep no per-Assembly state to report this later; follow-up
|
|
1209
|
+
// status calls show the Assembly itself.
|
|
1210
|
+
logger.warn('Assembly work after an early return failed: %s', redactError(error));
|
|
1010
1211
|
});
|
|
1011
|
-
|
|
1212
|
+
if (latest) {
|
|
1213
|
+
assembly = latest;
|
|
1214
|
+
}
|
|
1215
|
+
else if (reference) {
|
|
1216
|
+
// The uploads keep running, so even a rejected read must not invite a replay.
|
|
1217
|
+
const status = await readAssemblyStatus(client, reference.assemblyId).catch(() => undefined);
|
|
1218
|
+
if (!status) {
|
|
1219
|
+
// Nothing sends reserved files or sandbox files by itself; say how to get the
|
|
1220
|
+
// URLs or commands for them.
|
|
1221
|
+
const recovery = [
|
|
1222
|
+
...(uploadBehavior === 'none' && totalFiles > 0
|
|
1223
|
+
? [buildUploadUrlsPending(reference.assemblyUrl)]
|
|
1224
|
+
: []),
|
|
1225
|
+
...(outOfBandUploads > 0
|
|
1226
|
+
? [
|
|
1227
|
+
{
|
|
1228
|
+
code: 'mcp_upload_instructions_pending',
|
|
1229
|
+
message: 'The upload instructions were not ready before this call returned.',
|
|
1230
|
+
// Uploads this call started still reserve their fields; with the same
|
|
1231
|
+
// files, the next call reuses them instead of handing out commands.
|
|
1232
|
+
hint: `Wait about 30 seconds, then call transloadit_create_assembly with assembly_url ${reference.assemblyUrl}${totalFiles > 0 ? ', the same files' : ''} and expected_uploads ${expected_uploads} to get them.`,
|
|
1233
|
+
},
|
|
1234
|
+
]
|
|
1235
|
+
: []),
|
|
1236
|
+
];
|
|
1237
|
+
return buildStatusPendingResponse(reference, {
|
|
1238
|
+
upload: {
|
|
1239
|
+
...uploadSummary,
|
|
1240
|
+
status: uploadBehavior === 'none' || totalFiles === 0 ? 'none' : 'uploading',
|
|
1241
|
+
resumed: true,
|
|
1242
|
+
},
|
|
1243
|
+
next_steps: [
|
|
1244
|
+
'transloadit_wait_for_assembly',
|
|
1245
|
+
'transloadit_get_assembly_status',
|
|
1246
|
+
],
|
|
1247
|
+
}, recovery);
|
|
1248
|
+
}
|
|
1249
|
+
assembly = status;
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
// The creation request itself is slow (or retried); give it as long as a status read.
|
|
1253
|
+
const known = await settleWithin(Promise.race([created, work]), statusReadTimeoutMs);
|
|
1254
|
+
if (known === deadlineReached) {
|
|
1255
|
+
// Without the request, nothing can create this Assembly later, so a status check
|
|
1256
|
+
// that finds no Assembly makes creating it again safe.
|
|
1257
|
+
creationAbort.abort();
|
|
1258
|
+
return creationAssemblyId
|
|
1259
|
+
? buildCreationPending(options, creationAssemblyId, totalFiles, outOfBandUploads > 0 ? expected_uploads : undefined)
|
|
1260
|
+
: buildToolError('mcp_assembly_creation_pending', 'Creating the Assembly is taking too long.');
|
|
1261
|
+
}
|
|
1262
|
+
assembly = known;
|
|
1263
|
+
}
|
|
1012
1264
|
}
|
|
1013
1265
|
}
|
|
1014
1266
|
catch (error) {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
return buildCreatedAssemblyUnavailable(options, createdAssemblyId);
|
|
1267
|
+
if (isPollingTimeout(error) && latest) {
|
|
1268
|
+
// The SDK's own polling ran out of time; the Assembly is still known and running.
|
|
1269
|
+
assembly = latest;
|
|
1270
|
+
returnedEarly = true;
|
|
1020
1271
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1272
|
+
else {
|
|
1273
|
+
// Once API2 accepted the creation request, an auth failure (a token expiring while
|
|
1274
|
+
// uploads or polling run) must not reach the client as a challenge: it would refresh
|
|
1275
|
+
// and replay the call, creating and billing a second Assembly.
|
|
1276
|
+
if (createdAssemblyId && toAuthRejection(options, error, credentials, [])) {
|
|
1277
|
+
return buildCreatedAssemblyUnavailable(options, createdAssemblyId);
|
|
1278
|
+
}
|
|
1279
|
+
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
1280
|
+
return buildToolError('mcp_file_not_found', 'A prepared upload is no longer available.', {
|
|
1281
|
+
hint: 'Retry with base64 or a public URL, or upload via `npx -y @transloadit/node upload`.',
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
throw error;
|
|
1025
1285
|
}
|
|
1026
|
-
throw error;
|
|
1027
1286
|
}
|
|
1028
1287
|
if (assembly_url) {
|
|
1029
1288
|
uploadSummary.resumed = true;
|
|
@@ -1034,29 +1293,63 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
1034
1293
|
else if (uploadBehavior === 'none') {
|
|
1035
1294
|
uploadSummary.status = 'none';
|
|
1036
1295
|
}
|
|
1037
|
-
else if (uploadBehavior === 'background'
|
|
1296
|
+
else if (uploadBehavior === 'background' ||
|
|
1297
|
+
(!workSettled && assembly.ok === 'ASSEMBLY_UPLOADING')) {
|
|
1038
1298
|
uploadSummary.status = 'uploading';
|
|
1039
1299
|
}
|
|
1040
1300
|
if (isRecord(assembly.upload_urls)) {
|
|
1041
1301
|
uploadSummary.upload_urls = assembly.upload_urls;
|
|
1042
1302
|
}
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1303
|
+
const ownFields = new Set([...Object.keys(filesMap), ...Object.keys(uploadsMap)]);
|
|
1304
|
+
// A resumed Assembly may have received some uploads already, and takes more only while
|
|
1305
|
+
// it waits for them; expected_uploads counts all of its uploads, as at creation.
|
|
1306
|
+
// Finished uploads are listed under uploads, running ones under tus_uploads.
|
|
1307
|
+
const receivedFields = new Set([
|
|
1308
|
+
...(assembly.uploads ?? []).map((upload) => upload.field),
|
|
1309
|
+
...(assembly.tus_uploads ?? []).map((upload) => upload.fieldname),
|
|
1310
|
+
].filter((field) => isNonEmptyString(field) && !ownFields.has(field)));
|
|
1311
|
+
const pendingUploads = !reference
|
|
1312
|
+
? outOfBandUploads
|
|
1313
|
+
: assembly.ok === 'ASSEMBLY_UPLOADING'
|
|
1314
|
+
? Math.max(outOfBandUploads - receivedFields.size, 0)
|
|
1315
|
+
: 0;
|
|
1316
|
+
const uploadInstructions = pendingUploads > 0
|
|
1317
|
+
? buildUploadInstructions(assembly, pendingUploads, new Set([...ownFields, ...receivedFields]))
|
|
1045
1318
|
: undefined;
|
|
1046
|
-
if (
|
|
1319
|
+
if (wait_for_completion && uploadInstructions) {
|
|
1320
|
+
warnings.push({
|
|
1321
|
+
code: 'mcp_wait_skipped_for_uploads',
|
|
1322
|
+
message: 'Did not wait for completion because the Assembly waits for the expected uploads. Run upload_instructions, then call transloadit_wait_for_assembly.',
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
if (pendingUploads > 0 && !uploadInstructions) {
|
|
1047
1326
|
warnings.push({
|
|
1048
1327
|
code: 'mcp_upload_instructions_unavailable',
|
|
1049
1328
|
message: 'The Assembly status has no tus_url or assembly_ssl_url to upload to.',
|
|
1050
1329
|
});
|
|
1051
1330
|
}
|
|
1331
|
+
const running = isAssemblyRunning(assembly);
|
|
1332
|
+
if (returnedEarly && running) {
|
|
1333
|
+
warnings.push(buildStillRunningNotice(assembly, reference?.assemblyUrl));
|
|
1334
|
+
}
|
|
1335
|
+
if (returnedEarly &&
|
|
1336
|
+
uploadBehavior === 'none' &&
|
|
1337
|
+
totalFiles > 0 &&
|
|
1338
|
+
!uploadSummary.upload_urls) {
|
|
1339
|
+
// Nothing uploads these files later, so the caller needs their reserved upload URLs.
|
|
1340
|
+
const assemblyUrl = reference?.assemblyUrl ??
|
|
1341
|
+
(isNonEmptyString(assembly.assembly_ssl_url) ? assembly.assembly_ssl_url : undefined);
|
|
1342
|
+
warnings.push(buildUploadUrlsPending(assemblyUrl));
|
|
1343
|
+
}
|
|
1052
1344
|
const nextSteps = uploadInstructions
|
|
1053
1345
|
? ['transloadit_wait_for_assembly']
|
|
1054
|
-
: waitForCompletion
|
|
1346
|
+
: waitForCompletion && !running
|
|
1055
1347
|
? []
|
|
1056
1348
|
: ['transloadit_wait_for_assembly', 'transloadit_get_assembly_status'];
|
|
1057
1349
|
return buildToolResponse({
|
|
1058
1350
|
status: 'ok',
|
|
1059
1351
|
assembly,
|
|
1352
|
+
running,
|
|
1060
1353
|
upload: uploadSummary,
|
|
1061
1354
|
upload_instructions: uploadInstructions,
|
|
1062
1355
|
next_steps: nextSteps,
|
|
@@ -1070,7 +1363,19 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
1070
1363
|
throw error;
|
|
1071
1364
|
}
|
|
1072
1365
|
finally {
|
|
1073
|
-
|
|
1366
|
+
const cleanup = async () => {
|
|
1367
|
+
await Promise.all(tempCleanups.map((cleanupFile) => cleanupFile()));
|
|
1368
|
+
};
|
|
1369
|
+
// Uploads that continue after an early return still read the prepared files. This
|
|
1370
|
+
// promise never rejects.
|
|
1371
|
+
if (backgroundWork) {
|
|
1372
|
+
void backgroundWork.then(cleanup).catch((error) => {
|
|
1373
|
+
logger.warn('Removing prepared files failed: %s', redactError(error));
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
await cleanup();
|
|
1378
|
+
}
|
|
1074
1379
|
}
|
|
1075
1380
|
});
|
|
1076
1381
|
// Keep the generic execution path responsible for auth, uploads, waiting and widget results.
|
|
@@ -1080,6 +1385,7 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
1080
1385
|
.strict();
|
|
1081
1386
|
for (const { id, name } of builtinShortcuts) {
|
|
1082
1387
|
register(name, shortcutInputSchema, createAssemblyOutputSchema, async (args, extra) => {
|
|
1388
|
+
const startedAt = performance.now();
|
|
1083
1389
|
const live = createLiveClient(options, extra, 0);
|
|
1084
1390
|
if ('error' in live)
|
|
1085
1391
|
return live.error;
|
|
@@ -1107,7 +1413,22 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
1107
1413
|
}
|
|
1108
1414
|
return buildShortcutError('mcp_invalid_builtin_fields', error.message, { path: 'fields' });
|
|
1109
1415
|
}
|
|
1110
|
-
|
|
1416
|
+
// upload_behavior none asks for reserved upload URLs, which only the upload path hands out.
|
|
1417
|
+
const inputUrls = args.upload_behavior === 'none'
|
|
1418
|
+
? undefined
|
|
1419
|
+
: selectBuiltinInputUrls(template.id, [...(args.files ?? []), ...toAttachmentInputs(args.attachments ?? [])], args.expected_uploads);
|
|
1420
|
+
// API2 imports eligible URLs itself (api2#10292), so the Assembly exists at once and nothing
|
|
1421
|
+
// is downloaded here. A refusal is API2's to explain, so it surfaces like any other error.
|
|
1422
|
+
const result = await createAssembly(inputUrls
|
|
1423
|
+
? {
|
|
1424
|
+
...args,
|
|
1425
|
+
files: undefined,
|
|
1426
|
+
attachments: undefined,
|
|
1427
|
+
expected_uploads: undefined,
|
|
1428
|
+
fields,
|
|
1429
|
+
instructions: { template_id: template.id, input_url: inputUrls },
|
|
1430
|
+
}
|
|
1431
|
+
: { ...args, fields, instructions: { template_id: template.id } }, extra, true, startedAt);
|
|
1111
1432
|
// Keep the generic tool's historical error shape while making new shortcut failures
|
|
1112
1433
|
// recognizable to MCP clients that rely on isError instead of structuredContent.status.
|
|
1113
1434
|
return result.structuredContent?.status === 'error' ? { ...result, isError: true } : result;
|
|
@@ -1137,25 +1458,59 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
1137
1458
|
if ('error' in access)
|
|
1138
1459
|
return access.error;
|
|
1139
1460
|
const start = Date.now();
|
|
1461
|
+
const maxWaitMs = options.maxWaitMs ?? defaultMaxWaitMs;
|
|
1462
|
+
const budgetMs = Math.min(timeout_ms ?? maxWaitMs, maxWaitMs);
|
|
1463
|
+
// Polling only reads status, so it can be stopped as soon as the budget is spent.
|
|
1464
|
+
const polling = new AbortController();
|
|
1465
|
+
let latest;
|
|
1140
1466
|
let assembly;
|
|
1467
|
+
let returnedEarly = false;
|
|
1141
1468
|
try {
|
|
1142
|
-
|
|
1143
|
-
timeout:
|
|
1469
|
+
const outcome = await settleWithin(access.client.awaitAssemblyCompletion(access.assemblyId, {
|
|
1470
|
+
timeout: budgetMs,
|
|
1144
1471
|
interval: poll_interval_ms,
|
|
1145
1472
|
assemblyUrl: access.assemblyUrl,
|
|
1146
|
-
|
|
1473
|
+
signal: polling.signal,
|
|
1474
|
+
onAssemblyProgress: (status) => {
|
|
1475
|
+
latest = status;
|
|
1476
|
+
},
|
|
1477
|
+
}), budgetMs);
|
|
1478
|
+
if (outcome === deadlineReached) {
|
|
1479
|
+
polling.abort();
|
|
1480
|
+
returnedEarly = true;
|
|
1481
|
+
const status = latest ?? (await readAssemblyStatus(access.client, access.assemblyId));
|
|
1482
|
+
if (!status) {
|
|
1483
|
+
return buildStatusPendingResponse({
|
|
1484
|
+
assemblyId: access.assemblyId,
|
|
1485
|
+
assemblyUrl: access.assemblyUrl ?? defaultAssemblyUrl(options, access.assemblyId),
|
|
1486
|
+
}, { waited_ms: Date.now() - start });
|
|
1487
|
+
}
|
|
1488
|
+
assembly = status;
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
assembly = outcome;
|
|
1492
|
+
}
|
|
1147
1493
|
}
|
|
1148
1494
|
catch (error) {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1495
|
+
if (!(isPollingTimeout(error) && latest)) {
|
|
1496
|
+
const rejection = toAuthRejection(options, error, access.credentials, toolMetadata.transloadit_wait_for_assembly.scopes);
|
|
1497
|
+
if (rejection)
|
|
1498
|
+
return rejection;
|
|
1499
|
+
throw error;
|
|
1500
|
+
}
|
|
1501
|
+
assembly = latest;
|
|
1502
|
+
returnedEarly = true;
|
|
1153
1503
|
}
|
|
1154
1504
|
const waited_ms = Date.now() - start;
|
|
1505
|
+
const running = isAssemblyRunning(assembly);
|
|
1155
1506
|
return buildToolResponse({
|
|
1156
1507
|
status: 'ok',
|
|
1157
1508
|
assembly,
|
|
1509
|
+
running,
|
|
1158
1510
|
waited_ms,
|
|
1511
|
+
warnings: returnedEarly && running
|
|
1512
|
+
? [buildStillRunningNotice(assembly, access.assemblyUrl)]
|
|
1513
|
+
: undefined,
|
|
1159
1514
|
}, { meta: { [widgetContextMetaKey]: buildWidgetContext(options, assembly) } });
|
|
1160
1515
|
});
|
|
1161
1516
|
register('transloadit_list_robots', listRobotsInputSchema, listRobotsOutputSchema, ({ category, search, limit, cursor }) => {
|