@transloadit/mcp-server 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -80
- package/dist/cli.js +48 -2
- package/dist/cli.js.map +1 -1
- package/dist/express.d.ts.map +1 -1
- package/dist/express.js +25 -12
- package/dist/express.js.map +1 -1
- package/dist/http-helpers.d.ts +88 -0
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +193 -3
- package/dist/http-helpers.js.map +1 -1
- package/dist/http-request-handler.d.ts +1 -0
- package/dist/http-request-handler.d.ts.map +1 -1
- package/dist/http-request-handler.js +9 -19
- package/dist/http-request-handler.js.map +1 -1
- package/dist/http.d.ts +2 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +25 -49
- package/dist/http.js.map +1 -1
- package/dist/json.d.ts +5 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +14 -0
- package/dist/json.js.map +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -1
- package/dist/logger.js.map +1 -1
- package/dist/options.d.ts +10 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +26 -0
- package/dist/options.js.map +1 -0
- package/dist/request-transport.d.ts +21 -0
- package/dist/request-transport.d.ts.map +1 -0
- package/dist/request-transport.js +129 -0
- package/dist/request-transport.js.map +1 -0
- package/dist/server-card.d.ts +20 -7
- package/dist/server-card.d.ts.map +1 -1
- package/dist/server-card.js +41 -16
- package/dist/server-card.js.map +1 -1
- package/dist/server.d.ts +35 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +419 -81
- package/dist/server.js.map +1 -1
- package/dist/tool-list.d.ts +9 -0
- package/dist/tool-list.d.ts.map +1 -0
- package/dist/tool-list.js +44 -0
- package/dist/tool-list.js.map +1 -0
- package/dist/tool-metadata.d.ts +53 -0
- package/dist/tool-metadata.d.ts.map +1 -0
- package/dist/tool-metadata.js +147 -0
- package/dist/tool-metadata.js.map +1 -0
- package/dist/ui/assembly-result-widget.d.ts +38 -0
- package/dist/ui/assembly-result-widget.d.ts.map +1 -0
- package/dist/ui/assembly-result-widget.js +415 -0
- package/dist/ui/assembly-result-widget.js.map +1 -0
- package/dist/upload-instructions.d.ts +29 -0
- package/dist/upload-instructions.d.ts.map +1 -0
- package/dist/upload-instructions.js +60 -0
- package/dist/upload-instructions.js.map +1 -0
- package/package.json +3 -2
package/dist/server.js
CHANGED
|
@@ -2,8 +2,21 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { ApiError, extractFieldNamesFromTemplate, getRobotHelp, isKnownRobot, listRobots, mergeTemplateContent, prepareInputFiles, Transloadit, } from '@transloadit/node';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import packageJson from '../package.json' with { type: 'json' };
|
|
5
|
-
import { extractBearerToken } from "./http-helpers.js";
|
|
5
|
+
import { buildBearerChallenge, extractBearerToken } from "./http-helpers.js";
|
|
6
|
+
import { assertServerOptions } from "./options.js";
|
|
7
|
+
import { mirrorSecuritySchemes } from "./tool-list.js";
|
|
8
|
+
import { buildToolMeta, resolveSecuritySchemes, resolveToolAuthMode, toolMetadata, } from "./tool-metadata.js";
|
|
9
|
+
import { registerAssemblyResultWidget, widgetContextMetaKey } from "./ui/assembly-result-widget.js";
|
|
10
|
+
import { buildUploadInstructions, uploadInstructionSchema } from "./upload-instructions.js";
|
|
11
|
+
const defaultConsoleUrl = 'https://transloadit.com';
|
|
12
|
+
/** Signature algorithms API2 accepts for Auth Key signatures. */
|
|
13
|
+
export const signatureAlgorithmSchema = z.enum(['sha1', 'sha256', 'sha384']);
|
|
14
|
+
/** Header that carries `upstreamSecret` on API2 calls made with a forwarded bearer token. */
|
|
15
|
+
export const upstreamSecretHeader = 'Transloadit-Mcp-Upstream';
|
|
6
16
|
const maxBase64Bytes = 512_000;
|
|
17
|
+
/** URL inputs beyond these limits are not downloaded to this server's disk (per call). */
|
|
18
|
+
const defaultMaxUrlDownloadBytes = 1024 * 1024 * 1024;
|
|
19
|
+
const defaultUrlDownloadTimeoutMs = 10 * 60 * 1000;
|
|
7
20
|
const lintIssueSchema = z.object({
|
|
8
21
|
path: z.string(),
|
|
9
22
|
message: z.string(),
|
|
@@ -82,16 +95,28 @@ const inputFileSchema = z.discriminatedUnion('kind', [
|
|
|
82
95
|
contentType: z.string().optional(),
|
|
83
96
|
}),
|
|
84
97
|
]);
|
|
98
|
+
// Exactly the file object ChatGPT hydrates for `_meta["openai/fileParams"]`: the two ids are
|
|
99
|
+
// required, the descriptive fields optional, and nothing else is allowed.
|
|
100
|
+
const hostFileSchema = z
|
|
101
|
+
.object({
|
|
102
|
+
download_url: z.string(),
|
|
103
|
+
file_id: z.string(),
|
|
104
|
+
mime_type: z.string().optional(),
|
|
105
|
+
file_name: z.string().optional(),
|
|
106
|
+
})
|
|
107
|
+
.strict();
|
|
85
108
|
const createAssemblyInputSchema = z.object({
|
|
86
109
|
instructions: z.unknown().optional(),
|
|
87
110
|
files: z.array(inputFileSchema).optional(),
|
|
111
|
+
attachments: z.array(hostFileSchema).optional(),
|
|
88
112
|
fields: z.record(z.string(), z.unknown()).optional(),
|
|
89
113
|
wait_for_completion: z.boolean().optional(),
|
|
90
114
|
wait_timeout_ms: z.number().int().positive().optional(),
|
|
91
115
|
upload_concurrency: z.number().int().positive().optional(),
|
|
92
116
|
upload_chunk_size: z.number().int().positive().optional(),
|
|
93
117
|
upload_behavior: z.enum(['await', 'background', 'none']).optional(),
|
|
94
|
-
|
|
118
|
+
// Each expected upload becomes an instruction in the response; keep that response bounded.
|
|
119
|
+
expected_uploads: z.number().int().positive().max(100).optional(),
|
|
95
120
|
assembly_url: z.string().optional(),
|
|
96
121
|
});
|
|
97
122
|
const createAssemblyOutputSchema = z.object({
|
|
@@ -105,6 +130,7 @@ const createAssemblyOutputSchema = z.object({
|
|
|
105
130
|
upload_urls: z.record(z.string(), z.string()).optional(),
|
|
106
131
|
})
|
|
107
132
|
.optional(),
|
|
133
|
+
upload_instructions: z.array(uploadInstructionSchema).optional(),
|
|
108
134
|
next_steps: z.array(z.string()).optional(),
|
|
109
135
|
errors: z.array(toolMessageSchema).optional(),
|
|
110
136
|
warnings: z.array(toolMessageSchema).optional(),
|
|
@@ -166,6 +192,15 @@ const lintAssemblyOutputSchema = z.object({
|
|
|
166
192
|
linting_issues: z.array(lintIssueSchema),
|
|
167
193
|
normalized_instructions: z.unknown().optional(),
|
|
168
194
|
});
|
|
195
|
+
const getProfileInputSchema = z.object({}).strict();
|
|
196
|
+
// Shape ChatGPT expects from a profile tool: an opaque stable id plus optional display fields.
|
|
197
|
+
const getProfileOutputSchema = z
|
|
198
|
+
.object({
|
|
199
|
+
id: z.string().min(1).regex(/\S/),
|
|
200
|
+
name: z.string().optional(),
|
|
201
|
+
nickname: z.string().optional(),
|
|
202
|
+
})
|
|
203
|
+
.strict();
|
|
169
204
|
const toLintIssues = (issues) => issues.map((issue) => ({
|
|
170
205
|
path: issue.stepName ? `steps.${issue.stepName}` : 'instructions',
|
|
171
206
|
message: issue.summary,
|
|
@@ -180,7 +215,7 @@ const safeJsonParse = (value) => {
|
|
|
180
215
|
return value;
|
|
181
216
|
}
|
|
182
217
|
};
|
|
183
|
-
const buildToolResponse = (payload) => {
|
|
218
|
+
const buildToolResponse = (payload, extras = {}) => {
|
|
184
219
|
const content = {
|
|
185
220
|
type: 'text',
|
|
186
221
|
text: JSON.stringify(payload),
|
|
@@ -188,8 +223,52 @@ const buildToolResponse = (payload) => {
|
|
|
188
223
|
return {
|
|
189
224
|
content: [content],
|
|
190
225
|
structuredContent: payload,
|
|
226
|
+
...(extras.isError ? { isError: true } : {}),
|
|
227
|
+
...(extras.meta ? { _meta: extras.meta } : {}),
|
|
191
228
|
};
|
|
192
229
|
};
|
|
230
|
+
/**
|
|
231
|
+
* The SDK client validates `structuredContent` against a tool's output schema even on `isError`
|
|
232
|
+
* results, so tools whose schema cannot describe an error (Template lists, the strict profile)
|
|
233
|
+
* return errors as text only. The text still carries the code and hint as JSON.
|
|
234
|
+
*/
|
|
235
|
+
const withoutStructuredContent = (result) => {
|
|
236
|
+
const { structuredContent: _structuredContent, ...rest } = result;
|
|
237
|
+
return rest;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Tool-level auth failure. `_meta["mcp/www_authenticate"]` mirrors the HTTP challenge so ChatGPT
|
|
241
|
+
* and Claude open their account-linking UI; the text content keeps the reason readable.
|
|
242
|
+
*/
|
|
243
|
+
const buildAuthError = (options, input) => buildToolResponse({
|
|
244
|
+
status: 'error',
|
|
245
|
+
errors: [{ code: input.code, message: input.message, hint: input.hint }],
|
|
246
|
+
}, {
|
|
247
|
+
isError: true,
|
|
248
|
+
meta: {
|
|
249
|
+
'mcp/www_authenticate': [
|
|
250
|
+
buildBearerChallenge({
|
|
251
|
+
resourceMetadataUrl: options.resourceMetadataUrl,
|
|
252
|
+
error: { code: input.oauthError, description: input.message },
|
|
253
|
+
scopes: input.scopes,
|
|
254
|
+
}),
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
const buildMissingAuthError = (options) =>
|
|
259
|
+
// Only a hosted server has an authorization server a host could link an account with.
|
|
260
|
+
options.resourceMetadataUrl
|
|
261
|
+
? buildAuthError(options, {
|
|
262
|
+
code: 'mcp_missing_auth',
|
|
263
|
+
oauthError: 'insufficient_scope',
|
|
264
|
+
message: 'Sign in to Transloadit to use this tool.',
|
|
265
|
+
hint: 'Connect your Transloadit account through OAuth, then retry.',
|
|
266
|
+
})
|
|
267
|
+
: buildCredentialError({
|
|
268
|
+
code: 'mcp_missing_auth',
|
|
269
|
+
message: 'This server has no Transloadit credentials for this tool.',
|
|
270
|
+
hint: 'Set TRANSLOADIT_KEY/TRANSLOADIT_SECRET or send an Authorization: Bearer token.',
|
|
271
|
+
});
|
|
193
272
|
const buildToolError = (code, message, options = {}) => buildToolResponse({
|
|
194
273
|
status: 'error',
|
|
195
274
|
errors: [
|
|
@@ -234,27 +313,32 @@ const createLiveClient = (options, extra) => {
|
|
|
234
313
|
const authToken = token && token !== options.mcpToken ? token : undefined;
|
|
235
314
|
if (authToken) {
|
|
236
315
|
return {
|
|
316
|
+
credentials: 'bearer',
|
|
237
317
|
client: new Transloadit({
|
|
238
318
|
authToken,
|
|
239
319
|
authKey: options.authKey,
|
|
240
320
|
authSecret: options.authSecret,
|
|
241
321
|
endpoint: options.endpoint,
|
|
242
322
|
clientName: getClientName(options),
|
|
323
|
+
signatureAlgorithm: options.signatureAlgorithm,
|
|
243
324
|
followRedirects: false,
|
|
325
|
+
extraHeaders: options.upstreamSecret
|
|
326
|
+
? { [upstreamSecretHeader]: options.upstreamSecret }
|
|
327
|
+
: undefined,
|
|
244
328
|
}),
|
|
245
329
|
};
|
|
246
330
|
}
|
|
247
331
|
if (!options.authKey || !options.authSecret) {
|
|
248
|
-
return {
|
|
249
|
-
error: buildToolError('mcp_missing_auth', 'Missing TRANSLOADIT_KEY/TRANSLOADIT_SECRET or Authorization: Bearer token for live API calls.'),
|
|
250
|
-
};
|
|
332
|
+
return { error: buildMissingAuthError(options) };
|
|
251
333
|
}
|
|
252
334
|
return {
|
|
335
|
+
credentials: 'auth-key',
|
|
253
336
|
client: new Transloadit({
|
|
254
337
|
authKey: options.authKey,
|
|
255
338
|
authSecret: options.authSecret,
|
|
256
339
|
endpoint: options.endpoint,
|
|
257
340
|
clientName: getClientName(options),
|
|
341
|
+
signatureAlgorithm: options.signatureAlgorithm,
|
|
258
342
|
followRedirects: false,
|
|
259
343
|
}),
|
|
260
344
|
};
|
|
@@ -284,6 +368,134 @@ const getHttpStatusCode = (error) => {
|
|
|
284
368
|
return undefined;
|
|
285
369
|
};
|
|
286
370
|
const isErrnoException = (value) => isRecord(value) && typeof value.code === 'string';
|
|
371
|
+
/**
|
|
372
|
+
* Maps API2 rejections of the forwarded credentials to a tool auth error. Other failures are
|
|
373
|
+
* left to the caller so they keep surfacing as ordinary tool errors.
|
|
374
|
+
*/
|
|
375
|
+
const toAuthRejection = (options, error, credentials, scopes) => {
|
|
376
|
+
const status = getHttpStatusCode(error);
|
|
377
|
+
// The token or key is valid but names a different Auth Key than the instructions'
|
|
378
|
+
// `auth.key`; reconnecting cannot fix that, so it is reported like any other bad argument.
|
|
379
|
+
if (error instanceof ApiError && error.code === 'BEARER_TOKEN_AUTH_KEY_MISMATCH') {
|
|
380
|
+
return buildCredentialError({
|
|
381
|
+
code: 'mcp_auth_key_mismatch',
|
|
382
|
+
message: 'The instructions name a different Auth Key than the connected credentials.',
|
|
383
|
+
hint: 'Remove auth.key from the instructions; the connected credentials supply it.',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
// INSUFFICIENT_AUTH_SCOPE is API2's only scope rejection.
|
|
387
|
+
const scopeRejected = status === 403 && error instanceof ApiError && error.code === 'INSUFFICIENT_AUTH_SCOPE';
|
|
388
|
+
// Only forwarded tokens can be renewed by the host's account linking; an Auth Key configured
|
|
389
|
+
// on the server needs an operator, so it gets no OAuth challenge.
|
|
390
|
+
if (credentials === 'auth-key') {
|
|
391
|
+
if (status === 401) {
|
|
392
|
+
return buildCredentialError({
|
|
393
|
+
code: 'mcp_credentials_rejected',
|
|
394
|
+
message: 'Transloadit rejected the Auth Key this server is configured with.',
|
|
395
|
+
hint: 'Check that TRANSLOADIT_KEY and TRANSLOADIT_SECRET belong to an active Auth Key.',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (scopeRejected) {
|
|
399
|
+
return buildCredentialError({
|
|
400
|
+
code: 'mcp_insufficient_scope',
|
|
401
|
+
message: 'The configured Auth Key lacks the scope this tool needs.',
|
|
402
|
+
hint: 'Grant the Auth Key the scopes this tool declares, or configure another key.',
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else if (status === 401) {
|
|
407
|
+
return buildAuthError(options, {
|
|
408
|
+
code: 'mcp_auth_rejected',
|
|
409
|
+
oauthError: 'invalid_token',
|
|
410
|
+
message: 'Transloadit rejected the credentials; the token may have expired.',
|
|
411
|
+
hint: 'Reconnect your Transloadit account and retry.',
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
else if (scopeRejected) {
|
|
415
|
+
return buildAuthError(options, {
|
|
416
|
+
code: 'mcp_insufficient_scope',
|
|
417
|
+
oauthError: 'insufficient_scope',
|
|
418
|
+
message: 'The connected credentials lack the scope this tool needs.',
|
|
419
|
+
hint: 'Reconnect your Transloadit account and grant the requested access.',
|
|
420
|
+
scopes,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (error instanceof ApiError && error.code === 'INVALID_SIGNATURE') {
|
|
424
|
+
return buildSignatureError(error);
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
};
|
|
428
|
+
/** The Assembly exists, but its status could not be read with the caller's credentials. */
|
|
429
|
+
const buildCreatedAssemblyUnavailable = (options, assemblyId) => {
|
|
430
|
+
// Matches resolveAssemblyReference: the SDK's default when the endpoint is empty.
|
|
431
|
+
const assemblyUrl = `${options.endpoint || 'https://api2.transloadit.com'}/assemblies/${assemblyId}`;
|
|
432
|
+
return buildToolResponse({
|
|
433
|
+
status: 'error',
|
|
434
|
+
assembly: { assembly_id: assemblyId, assembly_ssl_url: assemblyUrl },
|
|
435
|
+
errors: [
|
|
436
|
+
{
|
|
437
|
+
code: 'mcp_assembly_status_unavailable',
|
|
438
|
+
message: 'The Assembly was created, but Transloadit rejected the credentials while reading its status.',
|
|
439
|
+
hint: `Reconnect if needed, then call transloadit_get_assembly_status with assembly_url ${assemblyUrl} instead of creating it again.`,
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
}, { isError: true });
|
|
443
|
+
};
|
|
444
|
+
/** A failed call caused by server-side credentials; an error result so strict outputs still pass. */
|
|
445
|
+
const buildCredentialError = (error) => buildToolResponse({ status: 'error', errors: [error] }, { isError: true });
|
|
446
|
+
/**
|
|
447
|
+
* Key/secret signatures fail when the configured algorithm differs from the Auth Key's
|
|
448
|
+
* `signature_algo`; API2 names the required one, so the hint can say exactly what to set.
|
|
449
|
+
*/
|
|
450
|
+
const buildSignatureError = (error) => {
|
|
451
|
+
const required = signatureAlgorithmSchema.safeParse(/requires (sha\d+)/.exec(error.rawMessage ?? '')?.[1]);
|
|
452
|
+
return buildCredentialError({
|
|
453
|
+
code: 'mcp_invalid_signature',
|
|
454
|
+
message: 'Transloadit rejected the request signature for this Auth Key.',
|
|
455
|
+
hint: required.success
|
|
456
|
+
? `This Auth Key signs with ${required.data}: set TRANSLOADIT_SIGNATURE_ALGORITHM=${required.data} (or the signatureAlgorithm option) and restart the MCP server.`
|
|
457
|
+
: 'Check that TRANSLOADIT_SECRET and TRANSLOADIT_SIGNATURE_ALGORITHM match the Auth Key.',
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
/** The Console origin and base path without a trailing slash, or `undefined` when unusable. */
|
|
461
|
+
const parseConsoleUrl = (value) => {
|
|
462
|
+
if (!URL.canParse(value))
|
|
463
|
+
return undefined;
|
|
464
|
+
const url = new URL(value);
|
|
465
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
466
|
+
return undefined;
|
|
467
|
+
return `${url.origin}${url.pathname}`.replace(/\/$/, '');
|
|
468
|
+
};
|
|
469
|
+
/** Widget-only context: whether the caller is signed in and where the Console deep links go. */
|
|
470
|
+
const buildWidgetContext = (options, assembly) => {
|
|
471
|
+
// Links are a convenience: a malformed consoleUrl must not turn an Assembly that was already
|
|
472
|
+
// created into a failed call, which would invite a retry that processes the files twice.
|
|
473
|
+
const consoleUrl = parseConsoleUrl(options.consoleUrl || defaultConsoleUrl);
|
|
474
|
+
const slug = isNonEmptyString(assembly.account_slug) ? assembly.account_slug : undefined;
|
|
475
|
+
const assemblyId = isNonEmptyString(assembly.assembly_id) ? assembly.assembly_id : undefined;
|
|
476
|
+
const templateId = isNonEmptyString(assembly.template_id) ? assembly.template_id : undefined;
|
|
477
|
+
if (!slug || !consoleUrl)
|
|
478
|
+
return { authenticated: true };
|
|
479
|
+
const workspaceUrl = `${consoleUrl}/c/${encodeURIComponent(slug)}`;
|
|
480
|
+
const newTemplateUrl = new URL(`${workspaceUrl}/templates/new`);
|
|
481
|
+
// The Console seeds the editor from `fromAssembly` since transloadit/content#6207, which ships
|
|
482
|
+
// together with this release: it loads the Assembly's effective instructions, overrides
|
|
483
|
+
// included. `duplicateFrom` stays as the seed for Consoles that predate it. Hiding or relabeling
|
|
484
|
+
// the action was considered and rejected in review of transloadit/node-sdk#529.
|
|
485
|
+
if (assemblyId) {
|
|
486
|
+
newTemplateUrl.searchParams.set('fromAssembly', assemblyId);
|
|
487
|
+
}
|
|
488
|
+
if (templateId && !isBuiltinTemplateId(templateId)) {
|
|
489
|
+
newTemplateUrl.searchParams.set('duplicateFrom', templateId);
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
authenticated: true,
|
|
493
|
+
assembly_console_url: assemblyId
|
|
494
|
+
? `${workspaceUrl}/assemblies/${encodeURIComponent(assemblyId)}`
|
|
495
|
+
: undefined,
|
|
496
|
+
new_template_url: newTemplateUrl.toString(),
|
|
497
|
+
};
|
|
498
|
+
};
|
|
287
499
|
const isHttpImportStep = (value) => isRecord(value) && value.robot === '/http/import';
|
|
288
500
|
const listHttpImportStepNames = (steps) => Object.entries(steps)
|
|
289
501
|
.filter(([, step]) => isHttpImportStep(step))
|
|
@@ -348,6 +560,8 @@ const mergeFieldValues = (templateFields, argFields) => {
|
|
|
348
560
|
};
|
|
349
561
|
const assemblyIdSchema = z.string().regex(/^[a-f\d]{32}$/i);
|
|
350
562
|
const assemblyUrlSchema = z.url();
|
|
563
|
+
/** Production (`.com`) and development or tunnel (`.dev`) Transloadit hosts. */
|
|
564
|
+
const isTransloaditHost = (hostname) => ['transloadit.com', 'transloadit.dev'].some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
351
565
|
const resolveAssemblyReference = (options, args) => {
|
|
352
566
|
if (args.assembly_url === undefined && args.assembly_id === undefined) {
|
|
353
567
|
return { error: buildToolError('mcp_missing_args', 'Provide assembly_url or assembly_id.') };
|
|
@@ -361,20 +575,29 @@ const resolveAssemblyReference = (options, args) => {
|
|
|
361
575
|
// Match the SDK's default when an environment variable supplies an empty endpoint.
|
|
362
576
|
const endpoint = options.endpoint || 'https://api2.transloadit.com';
|
|
363
577
|
let assemblyId = args.assembly_id;
|
|
364
|
-
|
|
578
|
+
// The message promises "URL or ID", and models pass a bare ID in either field.
|
|
579
|
+
if (args.assembly_url !== undefined && assemblyIdSchema.safeParse(args.assembly_url).success) {
|
|
580
|
+
assemblyId = args.assembly_url;
|
|
581
|
+
}
|
|
582
|
+
else if (args.assembly_url !== undefined) {
|
|
365
583
|
const parsed = assemblyUrlSchema.safeParse(args.assembly_url);
|
|
366
584
|
if (!parsed.success)
|
|
367
585
|
return invalidReference;
|
|
368
586
|
const url = new URL(parsed.data);
|
|
369
587
|
const apiEndpoint = new URL(endpoint);
|
|
370
588
|
const usesConfiguredOrigin = url.origin === apiEndpoint.origin;
|
|
371
|
-
|
|
589
|
+
// A hosted server may answer on a public origin (a tunnel or proxy) other than its API
|
|
590
|
+
// endpoint; Assembly URLs it hands out carry that origin.
|
|
591
|
+
const usesPublicOrigin = options.resourceMetadataUrl !== undefined &&
|
|
592
|
+
URL.canParse(options.resourceMetadataUrl) &&
|
|
593
|
+
url.origin === new URL(options.resourceMetadataUrl).origin;
|
|
594
|
+
const usesTransloaditOrigin = isTransloaditHost(url.hostname) && url.port === '';
|
|
372
595
|
if ((url.protocol !== 'http:' && url.protocol !== 'https:') ||
|
|
373
596
|
url.username ||
|
|
374
597
|
url.password ||
|
|
375
598
|
url.search ||
|
|
376
599
|
url.hash ||
|
|
377
|
-
(!usesConfiguredOrigin && !usesTransloaditOrigin)) {
|
|
600
|
+
(!usesConfiguredOrigin && !usesPublicOrigin && !usesTransloaditOrigin)) {
|
|
378
601
|
return invalidReference;
|
|
379
602
|
}
|
|
380
603
|
const prefix = usesConfiguredOrigin ? apiEndpoint.pathname.replace(/\/$/, '') : '';
|
|
@@ -402,6 +625,7 @@ const resolveAssemblyAccess = (options, extra, args) => {
|
|
|
402
625
|
return reference;
|
|
403
626
|
return {
|
|
404
627
|
client: liveClient.client,
|
|
628
|
+
credentials: liveClient.credentials,
|
|
405
629
|
...reference,
|
|
406
630
|
};
|
|
407
631
|
};
|
|
@@ -412,6 +636,7 @@ const apiTemplateSchema = z
|
|
|
412
636
|
description: z.string().optional(),
|
|
413
637
|
builtin_version: z.string().optional(),
|
|
414
638
|
content: z.unknown().optional(),
|
|
639
|
+
account_id: z.string().nullable().optional(),
|
|
415
640
|
})
|
|
416
641
|
.passthrough();
|
|
417
642
|
const listTemplatesResponseSchema = z
|
|
@@ -449,6 +674,42 @@ const loadTemplateSteps = async (client, template) => {
|
|
|
449
674
|
const full = await client.getTemplate(templateId);
|
|
450
675
|
return extractTemplateSteps(full.content);
|
|
451
676
|
};
|
|
677
|
+
/**
|
|
678
|
+
* API2 has no userinfo endpoint yet, so the Workspace behind the credentials is read from the
|
|
679
|
+
* caller's own Assemblies (id, name and slug) or, failing that, from an owned Template (id only).
|
|
680
|
+
*/
|
|
681
|
+
const resolveWorkspaceProfile = async (client) => {
|
|
682
|
+
const assemblies = await client.listAssemblies({ pagesize: 1 });
|
|
683
|
+
const latest = assemblies.items[0];
|
|
684
|
+
if (latest?.id) {
|
|
685
|
+
// The details only add the Workspace name; an expired or unreadable Assembly must not hide
|
|
686
|
+
// the id the list already returned, nor skip the Template fallback.
|
|
687
|
+
const status = await client.getAssembly(latest.id).catch(() => ({}));
|
|
688
|
+
const id = isNonEmptyString(status.account_id) ? status.account_id : latest.account_id;
|
|
689
|
+
if (isNonEmptyString(id)) {
|
|
690
|
+
return {
|
|
691
|
+
id,
|
|
692
|
+
name: isNonEmptyString(status.account_name) ? status.account_name : undefined,
|
|
693
|
+
nickname: isNonEmptyString(status.account_slug) ? status.account_slug : undefined,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// API2's default Template list fields omit account_id, so request it explicitly.
|
|
698
|
+
const templates = listTemplatesResponseSchema.safeParse(await client.listTemplates({ pagesize: 1, fields: ['id', 'account_id'] }));
|
|
699
|
+
const template = templates.success ? templates.data.items?.[0] : undefined;
|
|
700
|
+
if (template && isNonEmptyString(template.account_id)) {
|
|
701
|
+
return { id: template.account_id };
|
|
702
|
+
}
|
|
703
|
+
return undefined;
|
|
704
|
+
};
|
|
705
|
+
/** Host-attached files become URL inputs; `file_id` is opaque, so the field name is positional. */
|
|
706
|
+
const toAttachmentInputs = (attachments) => attachments.map((attachment, index) => ({
|
|
707
|
+
kind: 'url',
|
|
708
|
+
field: `attachment_${index + 1}`,
|
|
709
|
+
url: attachment.download_url,
|
|
710
|
+
filename: attachment.file_name,
|
|
711
|
+
contentType: attachment.mime_type,
|
|
712
|
+
}));
|
|
452
713
|
const looksLikeAssemblyParams = (input) => {
|
|
453
714
|
return ('steps' in input ||
|
|
454
715
|
'template_id' in input ||
|
|
@@ -480,17 +741,25 @@ const toAssemblyInstructionsInput = (params) => {
|
|
|
480
741
|
return rest;
|
|
481
742
|
};
|
|
482
743
|
export const createTransloaditMcpServer = (options = {}) => {
|
|
744
|
+
assertServerOptions(options);
|
|
483
745
|
const server = new McpServer({
|
|
484
746
|
name: options.serverName ?? 'Transloadit MCP',
|
|
485
747
|
version: options.serverVersion ?? packageJson.version,
|
|
486
748
|
});
|
|
749
|
+
const authMode = resolveToolAuthMode(options);
|
|
750
|
+
const register = (name, inputSchema, outputSchema, callback) => {
|
|
751
|
+
const metadata = toolMetadata[name];
|
|
752
|
+
server.registerTool(name, {
|
|
753
|
+
title: metadata.title,
|
|
754
|
+
description: metadata.description,
|
|
755
|
+
inputSchema,
|
|
756
|
+
outputSchema,
|
|
757
|
+
annotations: metadata.annotations,
|
|
758
|
+
_meta: buildToolMeta(metadata, resolveSecuritySchemes(metadata, authMode)),
|
|
759
|
+
}, callback);
|
|
760
|
+
};
|
|
487
761
|
// Builtin templates supersede the old golden template tool; no legacy alias by design.
|
|
488
|
-
|
|
489
|
-
title: 'Lint Assembly Instructions',
|
|
490
|
-
description: 'Lint Assembly Instructions without creating an Assembly. Returns structured issues.',
|
|
491
|
-
inputSchema: lintAssemblyInputSchema,
|
|
492
|
-
outputSchema: lintAssemblyOutputSchema,
|
|
493
|
-
}, async ({ instructions, strict, return_fixed }) => {
|
|
762
|
+
register('transloadit_lint_assembly_instructions', lintAssemblyInputSchema, lintAssemblyOutputSchema, async ({ instructions, strict, return_fixed }) => {
|
|
494
763
|
const client = createLintClient(options);
|
|
495
764
|
const assemblyInstructions = instructions;
|
|
496
765
|
const result = await client.lintAssemblyInstructions({
|
|
@@ -507,16 +776,11 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
507
776
|
}
|
|
508
777
|
return buildToolResponse(payload);
|
|
509
778
|
});
|
|
510
|
-
|
|
511
|
-
title: 'Create or resume an Assembly',
|
|
512
|
-
description: 'Create or resume an Assembly, optionally uploading files and waiting for completion.',
|
|
513
|
-
inputSchema: createAssemblyInputSchema,
|
|
514
|
-
outputSchema: createAssemblyOutputSchema,
|
|
515
|
-
}, async ({ instructions, files, fields, wait_for_completion, wait_timeout_ms, upload_concurrency, upload_chunk_size, upload_behavior, expected_uploads, assembly_url, }, extra) => {
|
|
779
|
+
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) => {
|
|
516
780
|
const liveClient = createLiveClient(options, extra);
|
|
517
781
|
if ('error' in liveClient)
|
|
518
782
|
return liveClient.error;
|
|
519
|
-
const { client } = liveClient;
|
|
783
|
+
const { client, credentials } = liveClient;
|
|
520
784
|
const reference = assembly_url === undefined ? undefined : resolveAssemblyReference(options, { assembly_url });
|
|
521
785
|
if (reference && 'error' in reference)
|
|
522
786
|
return reference.error;
|
|
@@ -524,7 +788,7 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
524
788
|
const warnings = [];
|
|
525
789
|
let templatePathHint;
|
|
526
790
|
try {
|
|
527
|
-
const fileInputs = files ?? [];
|
|
791
|
+
const fileInputs = [...(files ?? []), ...toAttachmentInputs(attachments ?? [])];
|
|
528
792
|
const urlInputs = fileInputs.filter((file) => file.kind === 'url');
|
|
529
793
|
const hasUrlInputs = urlInputs.length > 0;
|
|
530
794
|
let inputFilesForPrep = fileInputs;
|
|
@@ -618,6 +882,14 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
618
882
|
});
|
|
619
883
|
}
|
|
620
884
|
}
|
|
885
|
+
// The hosted gate only checks that a bearer is present. Before a URL input is downloaded
|
|
886
|
+
// to this server's disk (not for /http/import), API2 vouches for the token once
|
|
887
|
+
// (templates:read is declared for this tool).
|
|
888
|
+
let tokenCheck;
|
|
889
|
+
const verifyTokenBeforeDownload = async () => {
|
|
890
|
+
tokenCheck ??= client.listTemplates({ pagesize: 1 });
|
|
891
|
+
await tokenCheck;
|
|
892
|
+
};
|
|
621
893
|
const prep = await prepareInputFiles({
|
|
622
894
|
inputFiles: inputFilesForPrep,
|
|
623
895
|
params,
|
|
@@ -626,7 +898,13 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
626
898
|
urlStrategy: reference ? 'download' : 'import-if-present',
|
|
627
899
|
allowPrivateUrls: false,
|
|
628
900
|
maxBase64Bytes,
|
|
901
|
+
maxUrlDownloadBytes: options.maxUrlDownloadBytes ?? defaultMaxUrlDownloadBytes,
|
|
902
|
+
urlDownloadTimeoutMs: options.urlDownloadTimeoutMs ?? defaultUrlDownloadTimeoutMs,
|
|
903
|
+
beforeUrlDownload: credentials === 'bearer' ? verifyTokenBeforeDownload : undefined,
|
|
629
904
|
}).catch((error) => {
|
|
905
|
+
// API rejections (the token check) are not input mistakes; the outer handler maps them.
|
|
906
|
+
if (error instanceof ApiError)
|
|
907
|
+
throw error;
|
|
630
908
|
const message = error instanceof Error ? error.message : 'Invalid file input.';
|
|
631
909
|
if (message.startsWith('Duplicate file field')) {
|
|
632
910
|
return buildToolError('mcp_duplicate_field', message, { path: 'files' });
|
|
@@ -651,14 +929,26 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
651
929
|
total_files: totalFiles,
|
|
652
930
|
};
|
|
653
931
|
const timeout = wait_timeout_ms;
|
|
654
|
-
|
|
932
|
+
// Files the caller uploads itself (from a sandbox) can only start once this call returns,
|
|
933
|
+
// so waiting here would just run into the timeout.
|
|
934
|
+
const outOfBandUploads = reference ? 0 : Math.max((expected_uploads ?? 0) - totalFiles, 0);
|
|
935
|
+
const waitForCompletion = (wait_for_completion ?? false) && outOfBandUploads === 0;
|
|
936
|
+
if (wait_for_completion && outOfBandUploads > 0) {
|
|
937
|
+
warnings.push({
|
|
938
|
+
code: 'mcp_wait_skipped_for_uploads',
|
|
939
|
+
message: 'Did not wait for completion because the Assembly waits for the expected uploads. Run upload_instructions, then call transloadit_wait_for_assembly.',
|
|
940
|
+
});
|
|
941
|
+
}
|
|
655
942
|
const uploadBehavior = upload_behavior ?? (waitForCompletion ? 'await' : 'background');
|
|
656
943
|
const uploadConcurrency = upload_concurrency;
|
|
657
944
|
const chunkSize = upload_chunk_size;
|
|
658
945
|
let assembly;
|
|
946
|
+
// Set by the SDK only after API2 accepted the creation request, so a rejected request
|
|
947
|
+
// (including an error body with HTTP 200) is never mistaken for an existing Assembly.
|
|
948
|
+
let createdAssemblyId;
|
|
659
949
|
try {
|
|
660
|
-
|
|
661
|
-
|
|
950
|
+
if (reference) {
|
|
951
|
+
assembly = await client.resumeAssemblyUploads({
|
|
662
952
|
assemblyUrl: reference.assemblyUrl,
|
|
663
953
|
files: filesMap,
|
|
664
954
|
uploads: uploadsMap,
|
|
@@ -667,8 +957,10 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
667
957
|
uploadConcurrency,
|
|
668
958
|
chunkSize,
|
|
669
959
|
uploadBehavior,
|
|
670
|
-
})
|
|
671
|
-
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
const creation = client.createAssembly({
|
|
672
964
|
params,
|
|
673
965
|
files: filesMap,
|
|
674
966
|
uploads: uploadsMap,
|
|
@@ -678,9 +970,20 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
678
970
|
chunkSize,
|
|
679
971
|
uploadBehavior,
|
|
680
972
|
expectedUploads: expected_uploads,
|
|
973
|
+
onAssemblyCreated: (created) => {
|
|
974
|
+
createdAssemblyId = created.assembly_id ?? creation.assemblyId;
|
|
975
|
+
},
|
|
681
976
|
});
|
|
977
|
+
assembly = await creation;
|
|
978
|
+
}
|
|
682
979
|
}
|
|
683
980
|
catch (error) {
|
|
981
|
+
// Once API2 accepted the creation request, an auth failure (a token expiring while
|
|
982
|
+
// uploads or polling run) must not reach the client as a challenge: it would refresh and
|
|
983
|
+
// replay the call, creating and billing a second Assembly.
|
|
984
|
+
if (createdAssemblyId && toAuthRejection(options, error, credentials, [])) {
|
|
985
|
+
return buildCreatedAssemblyUnavailable(options, createdAssemblyId);
|
|
986
|
+
}
|
|
684
987
|
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
685
988
|
return buildToolError('mcp_file_not_found', 'A prepared upload is no longer available.', {
|
|
686
989
|
hint: 'Retry with base64 or a public URL, or upload via `npx -y @transloadit/node upload`.',
|
|
@@ -703,64 +1006,85 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
703
1006
|
if (isRecord(assembly.upload_urls)) {
|
|
704
1007
|
uploadSummary.upload_urls = assembly.upload_urls;
|
|
705
1008
|
}
|
|
706
|
-
const
|
|
707
|
-
? []
|
|
708
|
-
:
|
|
1009
|
+
const uploadInstructions = outOfBandUploads > 0
|
|
1010
|
+
? buildUploadInstructions(assembly, outOfBandUploads, new Set([...Object.keys(filesMap), ...Object.keys(uploadsMap)]))
|
|
1011
|
+
: undefined;
|
|
1012
|
+
if (outOfBandUploads > 0 && !uploadInstructions) {
|
|
1013
|
+
warnings.push({
|
|
1014
|
+
code: 'mcp_upload_instructions_unavailable',
|
|
1015
|
+
message: 'The Assembly status has no tus_url or assembly_ssl_url to upload to.',
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
const nextSteps = uploadInstructions
|
|
1019
|
+
? ['transloadit_wait_for_assembly']
|
|
1020
|
+
: waitForCompletion
|
|
1021
|
+
? []
|
|
1022
|
+
: ['transloadit_wait_for_assembly', 'transloadit_get_assembly_status'];
|
|
709
1023
|
return buildToolResponse({
|
|
710
1024
|
status: 'ok',
|
|
711
1025
|
assembly,
|
|
712
1026
|
upload: uploadSummary,
|
|
1027
|
+
upload_instructions: uploadInstructions,
|
|
713
1028
|
next_steps: nextSteps,
|
|
714
1029
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
715
|
-
});
|
|
1030
|
+
}, { meta: { [widgetContextMetaKey]: buildWidgetContext(options, assembly) } });
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
const rejection = toAuthRejection(options, error, credentials, toolMetadata.transloadit_create_assembly.scopes);
|
|
1034
|
+
if (rejection)
|
|
1035
|
+
return rejection;
|
|
1036
|
+
throw error;
|
|
716
1037
|
}
|
|
717
1038
|
finally {
|
|
718
1039
|
await Promise.all(tempCleanups.map((cleanup) => cleanup()));
|
|
719
1040
|
}
|
|
720
1041
|
});
|
|
721
|
-
|
|
722
|
-
title: 'Get Assembly status',
|
|
723
|
-
description: 'Fetch the latest Assembly status by URL or ID.',
|
|
724
|
-
inputSchema: getAssemblyStatusInputSchema,
|
|
725
|
-
outputSchema: getAssemblyStatusOutputSchema,
|
|
726
|
-
}, async ({ assembly_url, assembly_id }, extra) => {
|
|
1042
|
+
register('transloadit_get_assembly_status', getAssemblyStatusInputSchema, getAssemblyStatusOutputSchema, async ({ assembly_url, assembly_id }, extra) => {
|
|
727
1043
|
const access = resolveAssemblyAccess(options, extra, { assembly_url, assembly_id });
|
|
728
1044
|
if ('error' in access)
|
|
729
1045
|
return access.error;
|
|
730
|
-
|
|
1046
|
+
let assembly;
|
|
1047
|
+
try {
|
|
1048
|
+
assembly = await access.client.getAssembly(access.assemblyId);
|
|
1049
|
+
}
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
const rejection = toAuthRejection(options, error, access.credentials, toolMetadata.transloadit_get_assembly_status.scopes);
|
|
1052
|
+
if (rejection)
|
|
1053
|
+
return rejection;
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
731
1056
|
return buildToolResponse({
|
|
732
1057
|
status: 'ok',
|
|
733
1058
|
assembly,
|
|
734
1059
|
});
|
|
735
1060
|
});
|
|
736
|
-
|
|
737
|
-
title: 'Wait for Assembly completion',
|
|
738
|
-
description: 'Polls until the Assembly completes or timeout is reached.',
|
|
739
|
-
inputSchema: waitForAssemblyInputSchema,
|
|
740
|
-
outputSchema: waitForAssemblyOutputSchema,
|
|
741
|
-
}, async ({ assembly_url, assembly_id, timeout_ms, poll_interval_ms }, extra) => {
|
|
1061
|
+
register('transloadit_wait_for_assembly', waitForAssemblyInputSchema, waitForAssemblyOutputSchema, async ({ assembly_url, assembly_id, timeout_ms, poll_interval_ms }, extra) => {
|
|
742
1062
|
const access = resolveAssemblyAccess(options, extra, { assembly_url, assembly_id });
|
|
743
1063
|
if ('error' in access)
|
|
744
1064
|
return access.error;
|
|
745
1065
|
const start = Date.now();
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1066
|
+
let assembly;
|
|
1067
|
+
try {
|
|
1068
|
+
assembly = await access.client.awaitAssemblyCompletion(access.assemblyId, {
|
|
1069
|
+
timeout: timeout_ms,
|
|
1070
|
+
interval: poll_interval_ms,
|
|
1071
|
+
assemblyUrl: access.assemblyUrl,
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
const rejection = toAuthRejection(options, error, access.credentials, toolMetadata.transloadit_wait_for_assembly.scopes);
|
|
1076
|
+
if (rejection)
|
|
1077
|
+
return rejection;
|
|
1078
|
+
throw error;
|
|
1079
|
+
}
|
|
751
1080
|
const waited_ms = Date.now() - start;
|
|
752
1081
|
return buildToolResponse({
|
|
753
1082
|
status: 'ok',
|
|
754
1083
|
assembly,
|
|
755
1084
|
waited_ms,
|
|
756
|
-
});
|
|
1085
|
+
}, { meta: { [widgetContextMetaKey]: buildWidgetContext(options, assembly) } });
|
|
757
1086
|
});
|
|
758
|
-
|
|
759
|
-
title: 'List Transloadit robots',
|
|
760
|
-
description: 'Returns a filtered list of robots with short summaries.',
|
|
761
|
-
inputSchema: listRobotsInputSchema,
|
|
762
|
-
outputSchema: listRobotsOutputSchema,
|
|
763
|
-
}, ({ category, search, limit, cursor }) => {
|
|
1087
|
+
register('transloadit_list_robots', listRobotsInputSchema, listRobotsOutputSchema, ({ category, search, limit, cursor }) => {
|
|
764
1088
|
const result = listRobots({ category, search, limit, cursor });
|
|
765
1089
|
return buildToolResponse({
|
|
766
1090
|
status: 'ok',
|
|
@@ -768,12 +1092,7 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
768
1092
|
next_cursor: result.nextCursor,
|
|
769
1093
|
});
|
|
770
1094
|
});
|
|
771
|
-
|
|
772
|
-
title: 'Get robot parameter help',
|
|
773
|
-
description: 'Returns a robot summary and parameter details.',
|
|
774
|
-
inputSchema: getRobotHelpInputSchema,
|
|
775
|
-
outputSchema: getRobotHelpOutputSchema,
|
|
776
|
-
}, ({ robot_name, robot_names }) => {
|
|
1095
|
+
register('transloadit_get_robot_help', getRobotHelpInputSchema, getRobotHelpOutputSchema, ({ robot_name, robot_names }) => {
|
|
777
1096
|
const splitComma = (value) => value
|
|
778
1097
|
.split(',')
|
|
779
1098
|
.map((part) => part.trim())
|
|
@@ -819,25 +1138,10 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
819
1138
|
not_found: notFound.length > 0 ? notFound : undefined,
|
|
820
1139
|
});
|
|
821
1140
|
});
|
|
822
|
-
|
|
823
|
-
title: 'List templates',
|
|
824
|
-
description: 'List Assembly Templates (owned and/or builtin). Tip: pass include_builtin: "exclusively-latest" to list builtins only.',
|
|
825
|
-
inputSchema: listTemplatesInputSchema,
|
|
826
|
-
outputSchema: listTemplatesOutputSchema,
|
|
827
|
-
}, async ({ page, page_size, sort, order, keywords, include_builtin, include_content }, extra) => {
|
|
1141
|
+
register('transloadit_list_templates', listTemplatesInputSchema, listTemplatesOutputSchema, async ({ page, page_size, sort, order, keywords, include_builtin, include_content }, extra) => {
|
|
828
1142
|
const liveClient = createLiveClient(options, extra);
|
|
829
|
-
if ('error' in liveClient)
|
|
830
|
-
return
|
|
831
|
-
status: 'error',
|
|
832
|
-
templates: [],
|
|
833
|
-
errors: [
|
|
834
|
-
{
|
|
835
|
-
code: 'mcp_missing_auth',
|
|
836
|
-
message: 'Missing TRANSLOADIT_KEY/TRANSLOADIT_SECRET or Authorization: Bearer token for live API calls.',
|
|
837
|
-
},
|
|
838
|
-
],
|
|
839
|
-
});
|
|
840
|
-
}
|
|
1143
|
+
if ('error' in liveClient)
|
|
1144
|
+
return withoutStructuredContent(liveClient.error);
|
|
841
1145
|
try {
|
|
842
1146
|
const response = await liveClient.client.listTemplates({
|
|
843
1147
|
page,
|
|
@@ -869,6 +1173,9 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
869
1173
|
});
|
|
870
1174
|
}
|
|
871
1175
|
catch (error) {
|
|
1176
|
+
const rejection = toAuthRejection(options, error, liveClient.credentials, toolMetadata.transloadit_list_templates.scopes);
|
|
1177
|
+
if (rejection)
|
|
1178
|
+
return withoutStructuredContent(rejection);
|
|
872
1179
|
const message = error instanceof Error ? error.message : 'Failed to list templates.';
|
|
873
1180
|
return buildToolResponse({
|
|
874
1181
|
status: 'error',
|
|
@@ -882,6 +1189,37 @@ export const createTransloaditMcpServer = (options = {}) => {
|
|
|
882
1189
|
});
|
|
883
1190
|
}
|
|
884
1191
|
});
|
|
1192
|
+
register('transloadit_get_profile', getProfileInputSchema, getProfileOutputSchema, async (_args, extra) => {
|
|
1193
|
+
const liveClient = createLiveClient(options, extra);
|
|
1194
|
+
if ('error' in liveClient)
|
|
1195
|
+
return withoutStructuredContent(liveClient.error);
|
|
1196
|
+
let profile;
|
|
1197
|
+
try {
|
|
1198
|
+
profile = await resolveWorkspaceProfile(liveClient.client);
|
|
1199
|
+
}
|
|
1200
|
+
catch (error) {
|
|
1201
|
+
const rejection = toAuthRejection(options, error, liveClient.credentials, toolMetadata.transloadit_get_profile.scopes);
|
|
1202
|
+
if (rejection)
|
|
1203
|
+
return withoutStructuredContent(rejection);
|
|
1204
|
+
throw error;
|
|
1205
|
+
}
|
|
1206
|
+
if (!profile) {
|
|
1207
|
+
// The strict profile output schema has no error shape, so this must be an error result.
|
|
1208
|
+
return withoutStructuredContent(buildToolResponse({
|
|
1209
|
+
status: 'error',
|
|
1210
|
+
errors: [
|
|
1211
|
+
{
|
|
1212
|
+
code: 'mcp_profile_unavailable',
|
|
1213
|
+
message: 'Could not determine the Workspace behind these credentials yet.',
|
|
1214
|
+
hint: 'Create an Assembly or a Template first, then call this tool again.',
|
|
1215
|
+
},
|
|
1216
|
+
],
|
|
1217
|
+
}, { isError: true }));
|
|
1218
|
+
}
|
|
1219
|
+
return buildToolResponse(profile);
|
|
1220
|
+
});
|
|
1221
|
+
registerAssemblyResultWidget(server, { resultDomains: options.resultDomains });
|
|
1222
|
+
mirrorSecuritySchemes(server);
|
|
885
1223
|
return server;
|
|
886
1224
|
};
|
|
887
1225
|
//# sourceMappingURL=server.js.map
|