@umbraco-cms/mcp-dev 17.4.1 → 17.4.2

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.
@@ -18711,7 +18711,7 @@ async function validateFilePath(filePath, allowedPaths) {
18711
18711
  try {
18712
18712
  const realAllowedPath = fs.realpathSync(allowedPath);
18713
18713
  return normalizedPath.startsWith(realAllowedPath);
18714
- } catch (e) {
18714
+ } catch (e2) {
18715
18715
  return false;
18716
18716
  }
18717
18717
  });
@@ -18729,7 +18729,7 @@ async function validateFilePath(filePath, allowedPaths) {
18729
18729
  try {
18730
18730
  const realAllowedPath = fs.realpathSync(allowedPath);
18731
18731
  return realPath.startsWith(realAllowedPath);
18732
- } catch (e2) {
18732
+ } catch (e3) {
18733
18733
  return false;
18734
18734
  }
18735
18735
  });
@@ -18756,6 +18756,7 @@ async function validateFilePath(filePath, allowedPaths) {
18756
18756
 
18757
18757
 
18758
18758
 
18759
+ var BASE64_MAX_BYTES = 10 * 1024;
18759
18760
  function getExtensionFromMimeType(mimeType) {
18760
18761
  if (!mimeType) return void 0;
18761
18762
  const baseMimeType = mimeType.split(";")[0].trim();
@@ -18877,6 +18878,13 @@ async function createFilePayload(sourceType, filePath, fileUrl, fileAsBase64, fi
18877
18878
  if (!fileAsBase64) {
18878
18879
  throw new Error("fileAsBase64 is required when sourceType is 'base64'");
18879
18880
  }
18881
+ const padding = fileAsBase64.endsWith("==") ? 2 : fileAsBase64.endsWith("=") ? 1 : 0;
18882
+ const estimatedBytes = Math.floor(fileAsBase64.length * 3 / 4) - padding;
18883
+ if (estimatedBytes > BASE64_MAX_BYTES) {
18884
+ throw new Error(
18885
+ `base64 upload rejected: decoded payload is ~${estimatedBytes.toLocaleString()} bytes (limit is ${BASE64_MAX_BYTES.toLocaleString()} bytes). Use sourceType="url" with a public direct-download URL, or sourceType="file" if the file is attached to the chat.`
18886
+ );
18887
+ }
18880
18888
  const buffer = Buffer.from(fileAsBase64, "base64");
18881
18889
  let filename = fileName;
18882
18890
  if (!fileName.includes(".")) {
@@ -18958,6 +18966,174 @@ function buildSourceTypeSection(allowFilePath, otherSources) {
18958
18966
  return sources.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
18959
18967
  }
18960
18968
 
18969
+ // src/umb-management-api/tools/media/post/helpers/streaming-upload.ts
18970
+
18971
+
18972
+ var _mcphosted = require('@umbraco-cms/mcp-hosted');
18973
+
18974
+
18975
+
18976
+
18977
+
18978
+
18979
+ var authContext = null;
18980
+ function isStreamingAuthContextConfigured() {
18981
+ return authContext !== null;
18982
+ }
18983
+ async function resolveAuth() {
18984
+ if (!authContext) {
18985
+ throw new Error(
18986
+ "Streaming auth context not configured. setStreamingAuthContext() must be called in worker init()."
18987
+ );
18988
+ }
18989
+ const entry = await _mcphosted.getStoredUmbracoToken.call(void 0, authContext.env.OAUTH_KV, authContext.tokenKey);
18990
+ if (!_optionalChain([entry, 'optionalAccess', _48 => _48.tokens, 'optionalAccess', _49 => _49.access_token])) {
18991
+ throw new Error("No Umbraco access token in KV. Reconnect the MCP connector.");
18992
+ }
18993
+ const baseUrl = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_optionalChain([entry, 'access', _50 => _50.site, 'optionalAccess', _51 => _51.serverUrl]), () => ( _optionalChain([entry, 'access', _52 => _52.site, 'optionalAccess', _53 => _53.baseUrl]))), () => ( authContext.env.UMBRACO_SERVER_URL)), () => ( authContext.env.UMBRACO_BASE_URL));
18994
+ if (!baseUrl) {
18995
+ throw new Error("No Umbraco base URL resolvable from site or env.");
18996
+ }
18997
+ return { accessToken: entry.tokens.access_token, baseUrl: _mcpserversdk.normalizeBaseUrl.call(void 0, baseUrl) };
18998
+ }
18999
+ function ensureExtension(name, contentType, urlPath) {
19000
+ if (name.includes(".")) return name;
19001
+ const fromUrl = _optionalChain([urlPath, 'access', _54 => _54.match, 'call', _55 => _55(/\.([a-z0-9]{1,8})$/i), 'optionalAccess', _56 => _56[0]]);
19002
+ if (fromUrl) return `${name}${fromUrl}`;
19003
+ if (contentType) {
19004
+ const subtype = contentType.split(";")[0].split("/")[1];
19005
+ if (subtype && /^[a-z0-9]{1,8}$/i.test(subtype)) {
19006
+ return `${name}.${subtype === "jpeg" ? "jpg" : subtype}`;
19007
+ }
19008
+ }
19009
+ return `${name}.bin`;
19010
+ }
19011
+ async function streamingUploadFromUrl(params) {
19012
+ const client = UmbracoManagementClient3.getClient();
19013
+ const validatedMediaTypeName = validateMediaTypeForSvg(
19014
+ void 0,
19015
+ params.sourceUrl,
19016
+ params.name,
19017
+ params.mediaTypeName
19018
+ );
19019
+ const [mediaTypeId, auth] = await Promise.all([
19020
+ fetchMediaTypeId(client, validatedMediaTypeName),
19021
+ (async () => {
19022
+ await client.getTemporaryFileConfiguration().catch(() => void 0);
19023
+ return resolveAuth();
19024
+ })()
19025
+ ]);
19026
+ const { accessToken, baseUrl } = auth;
19027
+ const ctl = new AbortController();
19028
+ const overallTimeout = setTimeout(() => ctl.abort(), 6e4);
19029
+ try {
19030
+ const sourceResp = await fetch(params.sourceUrl, { signal: ctl.signal });
19031
+ if (!sourceResp.ok || !sourceResp.body) {
19032
+ throw new Error(
19033
+ `Failed to fetch source URL: HTTP ${sourceResp.status} ${sourceResp.statusText}`
19034
+ );
19035
+ }
19036
+ const sourceContentType = _nullishCoalesce(sourceResp.headers.get("content-type"), () => ( "application/octet-stream"));
19037
+ const sourceUrl = new URL(params.sourceUrl);
19038
+ const filename = ensureExtension(params.name, sourceContentType, sourceUrl.pathname);
19039
+ const temporaryFileId = crypto.randomUUID();
19040
+ const boundary = `----mcp-stream-${crypto.randomUUID()}`;
19041
+ const encoder = new TextEncoder();
19042
+ const prefix = encoder.encode(
19043
+ `--${boundary}\r
19044
+ Content-Disposition: form-data; name="Id"\r
19045
+ \r
19046
+ ${temporaryFileId}\r
19047
+ --${boundary}\r
19048
+ Content-Disposition: form-data; name="File"; filename="${filename}"\r
19049
+ Content-Type: ${sourceContentType}\r
19050
+ \r
19051
+ `
19052
+ );
19053
+ const suffix = encoder.encode(`\r
19054
+ --${boundary}--\r
19055
+ `);
19056
+ const sourceBody = sourceResp.body;
19057
+ const multipartBody = new ReadableStream({
19058
+ async start(controller) {
19059
+ const reader = sourceBody.getReader();
19060
+ try {
19061
+ controller.enqueue(prefix);
19062
+ while (true) {
19063
+ const { done, value } = await reader.read();
19064
+ if (done) break;
19065
+ if (value) controller.enqueue(value);
19066
+ }
19067
+ controller.enqueue(suffix);
19068
+ controller.close();
19069
+ } catch (e) {
19070
+ controller.error(e);
19071
+ } finally {
19072
+ reader.releaseLock();
19073
+ }
19074
+ },
19075
+ cancel(reason) {
19076
+ sourceBody.cancel(reason).catch(() => void 0);
19077
+ }
19078
+ });
19079
+ const tempResp = await fetch(`${baseUrl}/umbraco/management/api/v1/temporary-file`, {
19080
+ method: "POST",
19081
+ body: multipartBody,
19082
+ // @ts-expect-error — `duplex` is required by Workers/undici for streaming bodies
19083
+ duplex: "half",
19084
+ signal: ctl.signal,
19085
+ headers: {
19086
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
19087
+ Authorization: `Bearer ${accessToken}`,
19088
+ Accept: "application/json"
19089
+ }
19090
+ });
19091
+ if (!tempResp.ok) {
19092
+ const errBody = await tempResp.text().catch(() => "");
19093
+ throw new Error(
19094
+ `Umbraco temporary-file POST failed: HTTP ${tempResp.status} ${tempResp.statusText} - ${errBody}`
19095
+ );
19096
+ }
19097
+ const valueStructure = buildValueStructure(validatedMediaTypeName, temporaryFileId);
19098
+ const response = await client.postMedia(
19099
+ {
19100
+ mediaType: { id: mediaTypeId },
19101
+ variants: [{ culture: null, segment: null, name: params.name }],
19102
+ values: [valueStructure],
19103
+ parent: params.parentId ? { id: params.parentId } : null
19104
+ },
19105
+ _mcpserversdk.CAPTURE_RAW_HTTP_RESPONSE
19106
+ );
19107
+ if (response.status < 200 || response.status >= 300) {
19108
+ throw new Error(`postMedia failed with status ${response.status}`);
19109
+ }
19110
+ const locationHeader = _nullishCoalesce(_optionalChain([response, 'access', _57 => _57.headers, 'optionalAccess', _58 => _58.location]), () => ( _optionalChain([response, 'access', _59 => _59.headers, 'optionalAccess', _60 => _60.Location])));
19111
+ const idMatch = _optionalChain([locationHeader, 'optionalAccess', _61 => _61.match, 'call', _62 => _62(/\/([a-f0-9-]{36})$/i)]);
19112
+ if (!idMatch) {
19113
+ throw new Error(
19114
+ `Could not extract media id from Location header: ${_nullishCoalesce(locationHeader, () => ( "(missing)"))}`
19115
+ );
19116
+ }
19117
+ return { name: params.name, id: idMatch[1] };
19118
+ } finally {
19119
+ clearTimeout(overallTimeout);
19120
+ }
19121
+ }
19122
+ async function streamingUploadToToolResult(params) {
19123
+ try {
19124
+ const { name, id } = await streamingUploadFromUrl(params);
19125
+ return _mcpserversdk.createToolResult.call(void 0, {
19126
+ message: `Media "${name}" created successfully`,
19127
+ name,
19128
+ id
19129
+ });
19130
+ } catch (error) {
19131
+ return _mcpserversdk.createToolResultError.call(void 0, {
19132
+ detail: `Error creating media: ${error.message}`
19133
+ });
19134
+ }
19135
+ }
19136
+
18961
19137
  // src/umb-management-api/tools/media/post/create-media.ts
18962
19138
 
18963
19139
 
@@ -18970,31 +19146,48 @@ function buildSourceTypeSection(allowFilePath, otherSources) {
18970
19146
 
18971
19147
 
18972
19148
 
19149
+ var fileObjectSchema = _zod.z.object({
19150
+ download_url: _zod.z.string().describe("Temporary URL the host provides to fetch the file bytes"),
19151
+ file_id: _zod.z.string().describe("Persistent file identifier from the host"),
19152
+ mime_type: _zod.z.string().optional().describe("MIME type if the host knows it"),
19153
+ file_name: _zod.z.string().optional().describe("Original file name if the host knows it")
19154
+ });
18973
19155
  var createMediaOutputSchema = _zod.z.object({
18974
19156
  message: _zod.z.string(),
18975
19157
  name: _zod.z.string(),
18976
19158
  id: _zod.z.string().guid()
18977
19159
  });
18978
19160
  function createCreateMediaTool(options) {
18979
- const sourceTypeValues = options.allowFilePath ? ["filePath", "url", "base64"] : ["url", "base64"];
18980
- const sourceTypeDescription = options.allowFilePath ? "Media source type: 'filePath' for local files (most efficient), 'url' for web files, 'base64' for embedded data (small files only)" : "Media source type: 'url' for web files, 'base64' for embedded data (small files only)";
19161
+ const sourceTypeValues = options.allowFilePath ? ["filePath", "url", "file", "base64"] : ["url", "file", "base64"];
19162
+ const base64Kib = BASE64_MAX_BYTES / 1024;
19163
+ const filePathPrefix = options.allowFilePath ? "'filePath' for local files (Node stdio only), " : "";
19164
+ const sourceTypeDescription = `Media source type: ${filePathPrefix}'url' for public direct-download URLs (streamed; preferred for everything not already attached to the chat), 'file' for host-injected attachments \u2014 the connector populates the 'file' object automatically when the user attached a file or you generated one in this chat, 'base64' for TINY inline payloads only \u2014 the server rejects base64 above ${base64Kib} KiB decoded to stop LLM-truncated or thumbnail-preview base64 from persisting as corrupt files`;
18981
19165
  const schema = _zod.z.object({
18982
19166
  sourceType: _zod.z.enum(sourceTypeValues).describe(sourceTypeDescription),
18983
19167
  name: _zod.z.string().describe("The name of the media item"),
18984
19168
  mediaTypeName: _zod.z.string().describe(`Media type: '${_mcpserversdk.MEDIA_TYPE_IMAGE}', '${_mcpserversdk.MEDIA_TYPE_ARTICLE}', '${_mcpserversdk.MEDIA_TYPE_AUDIO}', '${_mcpserversdk.MEDIA_TYPE_VIDEO}', '${_mcpserversdk.MEDIA_TYPE_VECTOR_GRAPHICS}', '${_mcpserversdk.MEDIA_TYPE_FILE}', or custom media type name`),
18985
19169
  filePath: _zod.z.string().optional().describe("Absolute path to the file (required if sourceType is 'filePath')"),
18986
- fileUrl: _zod.z.string().url().optional().describe("[raw] URL to fetch the file from (required if sourceType is 'url')"),
18987
- fileAsBase64: _zod.z.string().optional().describe("Base64 encoded file data (required if sourceType is 'base64')"),
19170
+ fileUrl: _zod.z.string().url().optional().describe("[raw] Public, direct-download URL to fetch the file from (required if sourceType is 'url'). Must be reachable without authentication. Share/viewer links (e.g. drive.google.com/file/d/<id>/view, Dropbox ?dl=0, OneDrive view URLs) must be converted to their direct-download equivalent first \u2014 Google Drive: drive.google.com/uc?export=download&id=<id>. Uploads are streamed, so multi-MB files round-trip without timing out."),
19171
+ file: fileObjectSchema.optional().describe(
19172
+ "[raw] Host-injected file object (required if sourceType is 'file'). ChatGPT's connector populates this automatically when the user attached a file or you generated one in this chat \u2014 leave it for the host to fill, do not synthesise it yourself."
19173
+ ),
19174
+ fileAsBase64: _zod.z.string().optional().describe(`Base64-encoded file data (required if sourceType is 'base64'). HARD LIMIT: decoded payload must be \u2264${base64Kib} KiB; the server rejects anything larger because LLMs reliably truncate big base64 strings or substitute thumbnail previews, both producing corrupt files. Use sourceType='url' or 'file' for everything bigger.`),
18988
19175
  parentId: _zod.z.string().uuid().optional().describe("Parent folder ID (defaults to root)")
18989
19176
  });
18990
19177
  const filePathSection = buildSourceTypeSection(options.allowFilePath, [
18991
- "url - Fetch from web URL",
18992
- "base64 - Only for small files (<10KB) due to token usage"
19178
+ `url - Stream from any public direct-download URL (Drive / Dropbox / etc.). Use this for anything above ${base64Kib} KiB that isn't already attached to the chat.`,
19179
+ `file - Stream a host-attached file. The connector injects { download_url, file_id, mime_type, file_name } on the 'file' field automatically when the user attached a file or you generated one in this chat \u2014 prefer this over url for chat-bound files.`,
19180
+ `base64 - ONLY for tiny inline payloads; server hard-rejects decoded base64 over ${base64Kib} KiB to prevent LLM-truncated corrupt files.`
18993
19181
  ]);
18994
19182
  const tool = {
18995
19183
  name: "create-media",
18996
19184
  description: `Upload any media file to Umbraco (images, documents, audio, video, SVG, or custom types).
18997
19185
 
19186
+ Pick the sourceType that matches where the file lives:
19187
+ - If the file is attached to this chat or you just generated it \u2192 sourceType="file" (the host fills in the file reference automatically).
19188
+ - If you have a public direct-download URL \u2192 sourceType="url".
19189
+ - For tiny inline payloads (\u2264${base64Kib} KiB decoded) \u2192 sourceType="base64".
19190
+
18998
19191
  Media Types:
18999
19192
  - ${_mcpserversdk.MEDIA_TYPE_IMAGE}: jpg, png, gif, webp, etc. (supports cropping)
19000
19193
  - ${_mcpserversdk.MEDIA_TYPE_ARTICLE}: pdf, docx, doc (documents)
@@ -19015,16 +19208,43 @@ ${filePathSection}
19015
19208
  inputSchema: schema.shape,
19016
19209
  outputSchema: createMediaOutputSchema.shape,
19017
19210
  slices: ["create"],
19211
+ _meta: { "openai/fileParams": ["file"] },
19018
19212
  handler: (async (model) => {
19213
+ let effectiveSourceType;
19214
+ let effectiveFileUrl = model.fileUrl;
19215
+ if (model.sourceType === "file") {
19216
+ if (!_optionalChain([model, 'access', _63 => _63.file, 'optionalAccess', _64 => _64.download_url])) {
19217
+ return _mcpserversdk.createToolResultError.call(void 0, {
19218
+ detail: "Error creating media: sourceType is 'file' but no file object was provided. ChatGPT's connector should inject this automatically when a file is attached \u2014 if it didn't, the user has nothing attached or your client doesn't support openai/fileParams."
19219
+ });
19220
+ }
19221
+ effectiveSourceType = "url";
19222
+ effectiveFileUrl = model.file.download_url;
19223
+ } else {
19224
+ effectiveSourceType = model.sourceType;
19225
+ }
19226
+ if (effectiveSourceType === "url" && isStreamingAuthContextConfigured()) {
19227
+ if (!effectiveFileUrl) {
19228
+ return _mcpserversdk.createToolResultError.call(void 0, {
19229
+ detail: "Error creating media: fileUrl is required when sourceType is 'url'"
19230
+ });
19231
+ }
19232
+ return streamingUploadToToolResult({
19233
+ sourceUrl: effectiveFileUrl,
19234
+ name: model.name,
19235
+ mediaTypeName: model.mediaTypeName,
19236
+ parentId: model.parentId
19237
+ });
19238
+ }
19019
19239
  try {
19020
19240
  const client = UmbracoManagementClient3.getClient();
19021
19241
  const temporaryFileId = _uuid.v4.call(void 0, );
19022
19242
  const { name: actualName, id } = await uploadMediaFile(client, {
19023
- sourceType: model.sourceType,
19243
+ sourceType: effectiveSourceType,
19024
19244
  name: model.name,
19025
19245
  mediaTypeName: model.mediaTypeName,
19026
19246
  filePath: model.filePath,
19027
- fileUrl: model.fileUrl,
19247
+ fileUrl: effectiveFileUrl,
19028
19248
  fileAsBase64: model.fileAsBase64,
19029
19249
  parentId: model.parentId,
19030
19250
  temporaryFileId
@@ -19068,73 +19288,125 @@ var createMediaMultipleOutputSchema = _zod.z.object({
19068
19288
  error: _zod.z.string().optional()
19069
19289
  }))
19070
19290
  });
19291
+ var fileObjectSchema2 = _zod.z.object({
19292
+ download_url: _zod.z.string().describe("Temporary URL the host provides to fetch the file bytes"),
19293
+ file_id: _zod.z.string().describe("Persistent file identifier from the host"),
19294
+ mime_type: _zod.z.string().optional().describe("MIME type if the host knows it"),
19295
+ file_name: _zod.z.string().optional().describe("Original file name if the host knows it")
19296
+ });
19297
+ var UPLOAD_CONCURRENCY = 4;
19298
+ var MAX_BATCH_SIZE = 20;
19299
+ async function mapWithConcurrency(items, concurrency, producer) {
19300
+ const results = new Array(items.length);
19301
+ let nextIndex = 0;
19302
+ async function worker() {
19303
+ while (true) {
19304
+ const current = nextIndex++;
19305
+ if (current >= items.length) return;
19306
+ results[current] = await producer(items[current], current);
19307
+ }
19308
+ }
19309
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
19310
+ await Promise.all(workers);
19311
+ return results;
19312
+ }
19071
19313
  function createCreateMediaMultipleTool(options) {
19072
- const sourceTypeValues = options.allowFilePath ? ["filePath", "url"] : ["url"];
19073
- const sourceTypeDescription = options.allowFilePath ? "Media source type: 'filePath' for local files (most efficient), 'url' for web files. Base64 not supported for batch uploads due to token usage." : "Media source type: 'url' for web files. Base64 not supported for batch uploads due to token usage.";
19314
+ const sourceTypeValues = options.allowFilePath ? ["filePath", "url", "file"] : ["url", "file"];
19315
+ const filePathPrefix = options.allowFilePath ? "'filePath' for local files (most efficient, Node stdio only), " : "";
19316
+ const sourceTypeDescription = `Media source type: ${filePathPrefix}'url' for public direct-download URLs (streamed), 'file' for host-injected attachments \u2014 the connector populates each entry's 'file' object automatically when the user attached or generated files in this chat. Base64 is not supported for batch uploads due to token usage \u2014 use single-file create-media for tiny inline payloads.`;
19074
19317
  const schema = _zod.z.object({
19075
19318
  sourceType: _zod.z.enum(sourceTypeValues).describe(sourceTypeDescription),
19076
19319
  files: _zod.z.array(_zod.z.object({
19077
19320
  name: _zod.z.string().describe("The name of the media item"),
19078
19321
  filePath: _zod.z.string().optional().describe("Absolute path to the file (required if sourceType is 'filePath')"),
19079
- fileUrl: _zod.z.string().url().optional().describe("URL to fetch the file from (required if sourceType is 'url')"),
19322
+ fileUrl: _zod.z.string().url().optional().describe("[raw] Public direct-download URL (required if sourceType is 'url'). Drive / Dropbox share links must be converted to their direct-download equivalent first \u2014 see the create-media tool description for the format. Streamed, so multi-MB files round-trip without timing out."),
19323
+ file: fileObjectSchema2.optional().describe("[raw] Host-injected file reference (required if sourceType is 'file'). ChatGPT's connector populates this per entry automatically when files are attached."),
19080
19324
  mediaTypeName: _zod.z.string().optional().describe(`Optional override: '${_mcpserversdk.MEDIA_TYPE_IMAGE}', '${_mcpserversdk.MEDIA_TYPE_ARTICLE}', '${_mcpserversdk.MEDIA_TYPE_AUDIO}', '${_mcpserversdk.MEDIA_TYPE_VIDEO}', '${_mcpserversdk.MEDIA_TYPE_VECTOR_GRAPHICS}', '${_mcpserversdk.MEDIA_TYPE_FILE}', or custom media type name. If not specified, defaults to '${_mcpserversdk.MEDIA_TYPE_FILE}'`)
19081
- })).describe("Array of files to upload (maximum 20 files per batch)"),
19325
+ })).max(MAX_BATCH_SIZE).describe(`Array of files to upload (maximum ${MAX_BATCH_SIZE} files per batch)`),
19082
19326
  parentId: _zod.z.string().uuid().optional().describe("Parent folder ID (defaults to root)")
19083
19327
  });
19084
19328
  const filePathSection = buildSourceTypeSection(options.allowFilePath, [
19085
- "url - Fetch from web URL"
19329
+ "url - Stream from any public direct-download URL (Drive / Dropbox / etc.).",
19330
+ "file - Stream a host-attached file. The connector injects { download_url, file_id, ... } per entry automatically when the user attached or generated files in this chat \u2014 prefer this over 'url' for chat-bound files."
19086
19331
  ]);
19087
19332
  const tool = {
19088
19333
  name: "create-media-multiple",
19089
- description: `Batch upload multiple media files to Umbraco (maximum 20 files per batch).
19334
+ description: `Batch upload multiple media files to Umbraco (maximum ${MAX_BATCH_SIZE} files per batch).
19090
19335
 
19091
- Supports any file type: images, documents, audio, video, SVG, or custom types.
19336
+ Use this \u2014 not several separate create-media calls \u2014 when the user attached or generated multiple files in this chat (sourceType="file"), or has a list of public URLs to upload (sourceType="url"). ChatGPT's connector populates each entry's 'file' field automatically.
19092
19337
 
19093
19338
  Source Types:
19094
19339
  ${filePathSection}
19095
19340
 
19096
19341
  Note: base64 is not supported for batch uploads due to token usage.
19097
19342
 
19098
- The tool processes files sequentially and returns detailed results for each file.
19099
- If some files fail, others will continue processing (continue-on-error strategy).`,
19343
+ The tool processes up to ${UPLOAD_CONCURRENCY} uploads in parallel and returns detailed results for each file in input order. If some files fail, others continue processing (continue-on-error strategy).`,
19100
19344
  inputSchema: schema.shape,
19101
19345
  outputSchema: createMediaMultipleOutputSchema.shape,
19102
19346
  slices: ["create"],
19347
+ _meta: { "openai/fileParams": ["files"] },
19103
19348
  handler: (async (model) => {
19104
- if (model.files.length > 20) {
19349
+ if (model.files.length > MAX_BATCH_SIZE) {
19105
19350
  return _mcpserversdk.createToolResultError.call(void 0, {
19106
- detail: `Batch upload limited to 20 files per call. You provided ${model.files.length} files. Please split into multiple batches.`
19351
+ detail: `Batch upload limited to ${MAX_BATCH_SIZE} files per call. You provided ${model.files.length} files. Please split into multiple batches.`
19107
19352
  });
19108
19353
  }
19109
- const results = [];
19110
19354
  const client = UmbracoManagementClient3.getClient();
19111
- for (const file of model.files) {
19112
- try {
19113
- const temporaryFileId = _uuid.v4.call(void 0, );
19114
- const defaultMediaType = file.mediaTypeName || _mcpserversdk.MEDIA_TYPE_FILE;
19115
- const { name: actualName, id: mediaId } = await uploadMediaFile(client, {
19116
- sourceType: model.sourceType,
19117
- name: file.name,
19118
- mediaTypeName: defaultMediaType,
19119
- filePath: file.filePath,
19120
- fileUrl: file.fileUrl,
19121
- fileAsBase64: void 0,
19122
- parentId: model.parentId,
19123
- temporaryFileId
19124
- });
19125
- results.push({
19126
- success: true,
19127
- name: actualName,
19128
- id: mediaId
19129
- });
19130
- } catch (error) {
19131
- results.push({
19132
- success: false,
19133
- name: file.name,
19134
- error: error.message
19135
- });
19355
+ const streamingAvailable = isStreamingAuthContextConfigured();
19356
+ const results = await mapWithConcurrency(
19357
+ model.files,
19358
+ UPLOAD_CONCURRENCY,
19359
+ async (file) => {
19360
+ try {
19361
+ const mediaTypeName = file.mediaTypeName || _mcpserversdk.MEDIA_TYPE_FILE;
19362
+ let effectiveSourceType;
19363
+ let effectiveFileUrl = file.fileUrl;
19364
+ if (model.sourceType === "file") {
19365
+ if (!_optionalChain([file, 'access', _65 => _65.file, 'optionalAccess', _66 => _66.download_url])) {
19366
+ return {
19367
+ success: false,
19368
+ name: file.name,
19369
+ error: "sourceType is 'file' but no file object was provided for this entry. The host connector should inject this automatically when a file is attached."
19370
+ };
19371
+ }
19372
+ effectiveSourceType = "url";
19373
+ effectiveFileUrl = file.file.download_url;
19374
+ } else {
19375
+ effectiveSourceType = model.sourceType;
19376
+ }
19377
+ if (effectiveSourceType === "url" && streamingAvailable) {
19378
+ if (!effectiveFileUrl) {
19379
+ return {
19380
+ success: false,
19381
+ name: file.name,
19382
+ error: "fileUrl is required when sourceType is 'url'."
19383
+ };
19384
+ }
19385
+ const { name: actualName2, id: mediaId2 } = await streamingUploadFromUrl({
19386
+ sourceUrl: effectiveFileUrl,
19387
+ name: file.name,
19388
+ mediaTypeName,
19389
+ parentId: model.parentId
19390
+ });
19391
+ return { success: true, name: actualName2, id: mediaId2 };
19392
+ }
19393
+ const temporaryFileId = _uuid.v4.call(void 0, );
19394
+ const { name: actualName, id: mediaId } = await uploadMediaFile(client, {
19395
+ sourceType: effectiveSourceType,
19396
+ name: file.name,
19397
+ mediaTypeName,
19398
+ filePath: file.filePath,
19399
+ fileUrl: effectiveFileUrl,
19400
+ fileAsBase64: void 0,
19401
+ parentId: model.parentId,
19402
+ temporaryFileId
19403
+ });
19404
+ return { success: true, name: actualName, id: mediaId };
19405
+ } catch (error) {
19406
+ return { success: false, name: file.name, error: error.message };
19407
+ }
19136
19408
  }
19137
- }
19409
+ );
19138
19410
  const successCount = results.filter((r) => r.success).length;
19139
19411
  const failureCount = results.filter((r) => !r.success).length;
19140
19412
  return _mcpserversdk.createToolResult.call(void 0, {
@@ -19189,7 +19461,7 @@ var CreateMediaFolderTool = {
19189
19461
  if (response.status < 200 || response.status >= 300) {
19190
19462
  throw new Error(`Request failed with status code ${response.status}`);
19191
19463
  }
19192
- const locationHeader = _optionalChain([response, 'access', _48 => _48.headers, 'optionalAccess', _49 => _49.location]) || _optionalChain([response, 'access', _50 => _50.headers, 'optionalAccess', _51 => _51.Location]);
19464
+ const locationHeader = _optionalChain([response, 'access', _67 => _67.headers, 'optionalAccess', _68 => _68.location]) || _optionalChain([response, 'access', _69 => _69.headers, 'optionalAccess', _70 => _70.Location]);
19193
19465
  if (!locationHeader) {
19194
19466
  throw new Error("No Location header in response - cannot determine created folder ID");
19195
19467
  }
@@ -20426,7 +20698,7 @@ var CreateMediaTypeFolderTool = {
20426
20698
  validateStatus: () => true
20427
20699
  });
20428
20700
  if (response.status === 201) {
20429
- const locationHeader = _optionalChain([response, 'access', _52 => _52.headers, 'optionalAccess', _53 => _53["location"]]) || _optionalChain([response, 'access', _54 => _54.headers, 'optionalAccess', _55 => _55["Location"]]);
20701
+ const locationHeader = _optionalChain([response, 'access', _71 => _71.headers, 'optionalAccess', _72 => _72["location"]]) || _optionalChain([response, 'access', _73 => _73.headers, 'optionalAccess', _74 => _74["Location"]]);
20430
20702
  let createdId = model.id || "";
20431
20703
  if (locationHeader) {
20432
20704
  const idMatch = locationHeader.match(/([0-9a-f-]{36})$/i);
@@ -20560,7 +20832,7 @@ var CreateMediaTypeTool = {
20560
20832
  validateStatus: () => true
20561
20833
  });
20562
20834
  if (response.status === 201) {
20563
- const locationHeader = _optionalChain([response, 'access', _56 => _56.headers, 'optionalAccess', _57 => _57["location"]]) || _optionalChain([response, 'access', _58 => _58.headers, 'optionalAccess', _59 => _59["Location"]]);
20835
+ const locationHeader = _optionalChain([response, 'access', _75 => _75.headers, 'optionalAccess', _76 => _76["location"]]) || _optionalChain([response, 'access', _77 => _77.headers, 'optionalAccess', _78 => _78["Location"]]);
20564
20836
  let createdId = model.id || "";
20565
20837
  if (locationHeader) {
20566
20838
  const idMatch = locationHeader.match(/([0-9a-f-]{36})$/i);
@@ -20611,7 +20883,7 @@ var CopyMediaTypeTool = {
20611
20883
  validateStatus: () => true
20612
20884
  });
20613
20885
  if (response.status === 201) {
20614
- const locationHeader = _optionalChain([response, 'access', _60 => _60.headers, 'optionalAccess', _61 => _61["location"]]) || _optionalChain([response, 'access', _62 => _62.headers, 'optionalAccess', _63 => _63["Location"]]);
20886
+ const locationHeader = _optionalChain([response, 'access', _79 => _79.headers, 'optionalAccess', _80 => _80["location"]]) || _optionalChain([response, 'access', _81 => _81.headers, 'optionalAccess', _82 => _82["Location"]]);
20615
20887
  let createdId = "";
20616
20888
  if (locationHeader) {
20617
20889
  const idMatch = locationHeader.match(/([0-9a-f-]{36})$/i);
@@ -20821,7 +21093,7 @@ var CreateMemberTool = {
20821
21093
  validateStatus: () => true
20822
21094
  });
20823
21095
  if (response.status === 201) {
20824
- const locationHeader = _optionalChain([response, 'access', _64 => _64.headers, 'optionalAccess', _65 => _65["location"]]) || _optionalChain([response, 'access', _66 => _66.headers, 'optionalAccess', _67 => _67["Location"]]);
21096
+ const locationHeader = _optionalChain([response, 'access', _83 => _83.headers, 'optionalAccess', _84 => _84["location"]]) || _optionalChain([response, 'access', _85 => _85.headers, 'optionalAccess', _86 => _86["Location"]]);
20825
21097
  let createdId = model.id || "";
20826
21098
  if (locationHeader) {
20827
21099
  const idMatch = locationHeader.match(/([0-9a-f-]{36})$/i);
@@ -21372,7 +21644,7 @@ var CreateMemberTypeTool = {
21372
21644
  validateStatus: () => true
21373
21645
  });
21374
21646
  if (response.status === 201) {
21375
- const locationHeader = _optionalChain([response, 'access', _68 => _68.headers, 'optionalAccess', _69 => _69["location"]]) || _optionalChain([response, 'access', _70 => _70.headers, 'optionalAccess', _71 => _71["Location"]]);
21647
+ const locationHeader = _optionalChain([response, 'access', _87 => _87.headers, 'optionalAccess', _88 => _88["location"]]) || _optionalChain([response, 'access', _89 => _89.headers, 'optionalAccess', _90 => _90["Location"]]);
21376
21648
  let createdId = model.id || "";
21377
21649
  if (locationHeader) {
21378
21650
  const idMatch = locationHeader.match(/([0-9a-f-]{36})$/i);
@@ -21542,7 +21814,7 @@ var CopyMemberTypeTool = {
21542
21814
  validateStatus: () => true
21543
21815
  });
21544
21816
  if (response.status === 201) {
21545
- const locationHeader = _optionalChain([response, 'access', _72 => _72.headers, 'optionalAccess', _73 => _73["location"]]) || _optionalChain([response, 'access', _74 => _74.headers, 'optionalAccess', _75 => _75["Location"]]);
21817
+ const locationHeader = _optionalChain([response, 'access', _91 => _91.headers, 'optionalAccess', _92 => _92["location"]]) || _optionalChain([response, 'access', _93 => _93.headers, 'optionalAccess', _94 => _94["Location"]]);
21546
21818
  let createdId = "";
21547
21819
  if (locationHeader) {
21548
21820
  const idMatch = locationHeader.match(/([0-9a-f-]{36})$/i);
@@ -21902,7 +22174,7 @@ var CreatePartialViewTool = {
21902
22174
  validateStatus: () => true
21903
22175
  });
21904
22176
  if (response.status === 201) {
21905
- const locationHeader = _optionalChain([response, 'access', _76 => _76.headers, 'optionalAccess', _77 => _77["location"]]) || _optionalChain([response, 'access', _78 => _78.headers, 'optionalAccess', _79 => _79["Location"]]);
22177
+ const locationHeader = _optionalChain([response, 'access', _95 => _95.headers, 'optionalAccess', _96 => _96["location"]]) || _optionalChain([response, 'access', _97 => _97.headers, 'optionalAccess', _98 => _98["Location"]]);
21906
22178
  let createdPath = "";
21907
22179
  if (locationHeader) {
21908
22180
  const pathMatch = locationHeader.match(/partial-view\/(.+)$/);
@@ -21949,7 +22221,7 @@ var CreatePartialViewFolderTool = {
21949
22221
  validateStatus: () => true
21950
22222
  });
21951
22223
  if (response.status === 201) {
21952
- const locationHeader = _optionalChain([response, 'access', _80 => _80.headers, 'optionalAccess', _81 => _81["location"]]) || _optionalChain([response, 'access', _82 => _82.headers, 'optionalAccess', _83 => _83["Location"]]);
22224
+ const locationHeader = _optionalChain([response, 'access', _99 => _99.headers, 'optionalAccess', _100 => _100["location"]]) || _optionalChain([response, 'access', _101 => _101.headers, 'optionalAccess', _102 => _102["Location"]]);
21953
22225
  let createdPath = "";
21954
22226
  if (locationHeader) {
21955
22227
  const pathMatch = locationHeader.match(/partial-view\/folder\/(.+)$/);
@@ -22771,7 +23043,7 @@ var CreateScriptTool = {
22771
23043
  validateStatus: () => true
22772
23044
  });
22773
23045
  if (response.status === 201) {
22774
- const locationHeader = _optionalChain([response, 'access', _84 => _84.headers, 'optionalAccess', _85 => _85["location"]]) || _optionalChain([response, 'access', _86 => _86.headers, 'optionalAccess', _87 => _87["Location"]]);
23046
+ const locationHeader = _optionalChain([response, 'access', _103 => _103.headers, 'optionalAccess', _104 => _104["location"]]) || _optionalChain([response, 'access', _105 => _105.headers, 'optionalAccess', _106 => _106["Location"]]);
22775
23047
  let createdPath = "";
22776
23048
  if (locationHeader) {
22777
23049
  const pathMatch = locationHeader.match(/script\/(.+)$/);
@@ -22818,7 +23090,7 @@ var CreateScriptFolderTool = {
22818
23090
  validateStatus: () => true
22819
23091
  });
22820
23092
  if (response.status === 201) {
22821
- const locationHeader = _optionalChain([response, 'access', _88 => _88.headers, 'optionalAccess', _89 => _89["location"]]) || _optionalChain([response, 'access', _90 => _90.headers, 'optionalAccess', _91 => _91["Location"]]);
23093
+ const locationHeader = _optionalChain([response, 'access', _107 => _107.headers, 'optionalAccess', _108 => _108["location"]]) || _optionalChain([response, 'access', _109 => _109.headers, 'optionalAccess', _110 => _110["Location"]]);
22822
23094
  let createdPath = "";
22823
23095
  if (locationHeader) {
22824
23096
  const pathMatch = locationHeader.match(/script\/folder\/(.+)$/);
@@ -23415,7 +23687,7 @@ var CreateStylesheetTool = {
23415
23687
  validateStatus: () => true
23416
23688
  });
23417
23689
  if (response.status === 201) {
23418
- const locationHeader = _optionalChain([response, 'access', _92 => _92.headers, 'optionalAccess', _93 => _93["location"]]) || _optionalChain([response, 'access', _94 => _94.headers, 'optionalAccess', _95 => _95["Location"]]);
23690
+ const locationHeader = _optionalChain([response, 'access', _111 => _111.headers, 'optionalAccess', _112 => _112["location"]]) || _optionalChain([response, 'access', _113 => _113.headers, 'optionalAccess', _114 => _114["Location"]]);
23419
23691
  let createdPath = "";
23420
23692
  if (locationHeader) {
23421
23693
  const pathMatch = locationHeader.match(/stylesheet\/(.+)$/);
@@ -23462,7 +23734,7 @@ var CreateStylesheetFolderTool = {
23462
23734
  validateStatus: () => true
23463
23735
  });
23464
23736
  if (response.status === 201) {
23465
- const locationHeader = _optionalChain([response, 'access', _96 => _96.headers, 'optionalAccess', _97 => _97["location"]]) || _optionalChain([response, 'access', _98 => _98.headers, 'optionalAccess', _99 => _99["Location"]]);
23737
+ const locationHeader = _optionalChain([response, 'access', _115 => _115.headers, 'optionalAccess', _116 => _116["location"]]) || _optionalChain([response, 'access', _117 => _117.headers, 'optionalAccess', _118 => _118["Location"]]);
23466
23738
  let createdPath = "";
23467
23739
  if (locationHeader) {
23468
23740
  const pathMatch = locationHeader.match(/stylesheet\/folder\/(.+)$/);
@@ -23826,7 +24098,7 @@ var CreateTemplateTool = {
23826
24098
  validateStatus: () => true
23827
24099
  });
23828
24100
  if (response.status === 201) {
23829
- const locationHeader = _optionalChain([response, 'access', _100 => _100.headers, 'optionalAccess', _101 => _101["location"]]) || _optionalChain([response, 'access', _102 => _102.headers, 'optionalAccess', _103 => _103["Location"]]);
24101
+ const locationHeader = _optionalChain([response, 'access', _119 => _119.headers, 'optionalAccess', _120 => _120["location"]]) || _optionalChain([response, 'access', _121 => _121.headers, 'optionalAccess', _122 => _122["Location"]]);
23830
24102
  let createdId = "";
23831
24103
  if (locationHeader) {
23832
24104
  const idMatch = locationHeader.match(/template\/([a-f0-9-]+)$/i);
@@ -24736,7 +25008,7 @@ var CreateUserDataTool = {
24736
25008
  validateStatus: () => true
24737
25009
  });
24738
25010
  if (response.status === 201) {
24739
- const locationHeader = _optionalChain([response, 'access', _104 => _104.headers, 'optionalAccess', _105 => _105["location"]]) || _optionalChain([response, 'access', _106 => _106.headers, 'optionalAccess', _107 => _107["Location"]]);
25011
+ const locationHeader = _optionalChain([response, 'access', _123 => _123.headers, 'optionalAccess', _124 => _124["location"]]) || _optionalChain([response, 'access', _125 => _125.headers, 'optionalAccess', _126 => _126["Location"]]);
24740
25012
  let createdId = "";
24741
25013
  if (locationHeader) {
24742
25014
  const idMatch = locationHeader.match(/user-data\/([a-f0-9-]+)$/i);
@@ -24974,7 +25246,7 @@ var CreateUserGroupTool = {
24974
25246
  validateStatus: () => true
24975
25247
  });
24976
25248
  if (response.status === 201) {
24977
- const locationHeader = _optionalChain([response, 'access', _108 => _108.headers, 'optionalAccess', _109 => _109["location"]]) || _optionalChain([response, 'access', _110 => _110.headers, 'optionalAccess', _111 => _111["Location"]]);
25249
+ const locationHeader = _optionalChain([response, 'access', _127 => _127.headers, 'optionalAccess', _128 => _128["location"]]) || _optionalChain([response, 'access', _129 => _129.headers, 'optionalAccess', _130 => _130["Location"]]);
24978
25250
  let createdId = "";
24979
25251
  if (locationHeader) {
24980
25252
  const idMatch = locationHeader.match(/user-group\/([a-f0-9-]+)$/i);
@@ -25272,7 +25544,7 @@ var CreateWebhookTool = {
25272
25544
  validateStatus: () => true
25273
25545
  });
25274
25546
  if (response.status === 201) {
25275
- const locationHeader = _optionalChain([response, 'access', _112 => _112.headers, 'optionalAccess', _113 => _113["location"]]) || _optionalChain([response, 'access', _114 => _114.headers, 'optionalAccess', _115 => _115["Location"]]);
25547
+ const locationHeader = _optionalChain([response, 'access', _131 => _131.headers, 'optionalAccess', _132 => _132["location"]]) || _optionalChain([response, 'access', _133 => _133.headers, 'optionalAccess', _134 => _134["Location"]]);
25276
25548
  let createdId = "";
25277
25549
  if (locationHeader) {
25278
25550
  const idMatch = locationHeader.match(/webhook\/([a-f0-9-]+)$/i);
@@ -25508,4 +25780,4 @@ var allSliceNames = [...toolSliceNames, "other"];
25508
25780
 
25509
25781
 
25510
25782
  exports.__commonJS = __commonJS; exports.__toESM = __toESM; exports.UmbracoManagementClient = UmbracoManagementClient3; exports.setAllowFilePathUploads = setAllowFilePathUploads; exports.setUmbracoVersion = setUmbracoVersion; exports.availableCollections = availableCollections; exports.allModes = allModes; exports.allModeNames = allModeNames; exports.allSliceNames = allSliceNames;
25511
- //# sourceMappingURL=chunk-H5ZIS43M.cjs.map
25783
+ //# sourceMappingURL=chunk-Q4LCXM42.cjs.map