@sudomock/mcp 2.7.1 → 3.0.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/dist/index.js CHANGED
@@ -3,7 +3,8 @@
3
3
  * SudoMock MCP Server
4
4
  *
5
5
  * Local stdio MCP server for Claude Desktop, Claude Code, Cursor, and VS Code.
6
- * Renders photorealistic product mockups from Photoshop PSD templates via the SudoMock API.
6
+ * Renders photorealistic product mockups from Photoshop PSD templates and
7
+ * product photos via the SudoMock API.
7
8
  *
8
9
  * Auth: SUDOMOCK_API_KEY environment variable
9
10
  * Transport: stdio (local process)
@@ -172,7 +173,7 @@ export function formatJobAccepted(result) {
172
173
  };
173
174
  return JSON.stringify(summary, null, 2);
174
175
  }
175
- export const TERMINAL_JOB_STATUSES = new Set(["succeeded", "failed"]);
176
+ export const TERMINAL_JOB_STATUSES = new Set(["succeeded", "failed", "cancelled"]);
176
177
  /** GET /api/v1/jobs/{job_id} -- owner-scoped job status snapshot. */
177
178
  async function getJob(jobId, timeout = DEFAULT_TIMEOUT) {
178
179
  const result = (await apiRequest({
@@ -583,6 +584,9 @@ function publicWebhookEndpoint(result) {
583
584
  secret: endpoint.secret ?? null,
584
585
  description: endpoint.description ?? null,
585
586
  event_types: Array.isArray(endpoint.event_types) ? endpoint.event_types : [],
587
+ // Which spelling of the photo-mockup events this endpoint receives
588
+ // ('legacy' or 'current'); the payload's kind follows the same pin.
589
+ event_naming: endpoint.event_naming ?? null,
586
590
  enabled: endpoint.enabled === true,
587
591
  created_at: endpoint.created_at ?? null,
588
592
  updated_at: endpoint.updated_at ?? null,
@@ -670,9 +674,9 @@ const server = new McpServer({
670
674
  });
671
675
  instrumentToolCalls(server);
672
676
  // ---------------------------------------------------------------------------
673
- // Tool 1: list_mockups
677
+ // Tool 1: list_psd_mockups
674
678
  // ---------------------------------------------------------------------------
675
- server.tool("list_mockups", "List your uploaded mockup templates with UUIDs, names, and thumbnails. Use returned UUIDs with render_mockup or get_mockup_details.", {
679
+ server.tool("list_psd_mockups", "List your uploaded PSD mockup templates with UUIDs, names, and thumbnails. Use returned UUIDs with render_psd_mockup or get_psd_mockup.", {
676
680
  limit: z.number().min(1).max(100).default(20).describe("Results per page (1-100, default 20)"),
677
681
  offset: z.number().min(0).default(0).describe("Pagination offset (default 0)"),
678
682
  name: z.string().optional().describe("Filter by name (case-insensitive substring match)"),
@@ -683,7 +687,7 @@ server.tool("list_mockups", "List your uploaded mockup templates with UUIDs, nam
683
687
  }, async ({ limit, offset, name, created_after, created_before, sort_by, sort_order }) => {
684
688
  const result = await apiRequest({
685
689
  method: "GET",
686
- path: "/api/v1/mockups",
690
+ path: "/api/v1/psd-mockups",
687
691
  params: { limit, offset, name, created_after, created_before, sort: sort_by, order: sort_order },
688
692
  });
689
693
  return {
@@ -694,14 +698,14 @@ server.tool("list_mockups", "List your uploaded mockup templates with UUIDs, nam
694
698
  };
695
699
  });
696
700
  // ---------------------------------------------------------------------------
697
- // Tool 2: get_mockup_details
701
+ // Tool 2: get_psd_mockup
698
702
  // ---------------------------------------------------------------------------
699
- server.tool("get_mockup_details", "Get full details of a mockup: smart object UUIDs, layer names, dimensions, positions, blend modes, and thumbnail URLs.", {
700
- mockup_uuid: z.string().describe("The UUID of the mockup to inspect"),
703
+ server.tool("get_psd_mockup", "Get full details of a PSD mockup: smart object UUIDs, layer names, dimensions, positions, blend modes, editable text layers, and thumbnail URLs.", {
704
+ mockup_uuid: z.string().describe("The UUID of the PSD mockup to inspect"),
701
705
  }, async ({ mockup_uuid }) => {
702
706
  const result = await apiRequest({
703
707
  method: "GET",
704
- path: `/api/v1/mockups/${mockup_uuid}`,
708
+ path: `/api/v1/psd-mockups/${mockup_uuid}`,
705
709
  });
706
710
  return {
707
711
  content: [{
@@ -711,15 +715,15 @@ server.tool("get_mockup_details", "Get full details of a mockup: smart object UU
711
715
  };
712
716
  });
713
717
  // ---------------------------------------------------------------------------
714
- // Tool 3: update_mockup
718
+ // Tool 3: update_psd_mockup
715
719
  // ---------------------------------------------------------------------------
716
- server.tool("update_mockup", "Rename a mockup template.", {
717
- mockup_uuid: z.string().describe("The UUID of the mockup to rename"),
720
+ server.tool("update_psd_mockup", "Rename a PSD mockup template.", {
721
+ mockup_uuid: z.string().describe("The UUID of the PSD mockup to rename"),
718
722
  name: z.string().describe("New display name for the mockup"),
719
723
  }, async ({ mockup_uuid, name }) => {
720
724
  const result = await apiRequest({
721
725
  method: "PATCH",
722
- path: `/api/v1/mockups/${mockup_uuid}`,
726
+ path: `/api/v1/psd-mockups/${mockup_uuid}`,
723
727
  body: { name },
724
728
  });
725
729
  return {
@@ -730,19 +734,19 @@ server.tool("update_mockup", "Rename a mockup template.", {
730
734
  };
731
735
  });
732
736
  // ---------------------------------------------------------------------------
733
- // Tool 4: delete_mockup
737
+ // Tool 4: delete_psd_mockup
734
738
  // ---------------------------------------------------------------------------
735
- server.tool("delete_mockup", "Permanently delete a mockup template. Cannot be undone.", {
736
- mockup_uuid: z.string().describe("The UUID of the mockup to delete"),
739
+ server.tool("delete_psd_mockup", "Permanently delete a PSD mockup template. Cannot be undone.", {
740
+ mockup_uuid: z.string().describe("The UUID of the PSD mockup to delete"),
737
741
  }, async ({ mockup_uuid }) => {
738
742
  await apiRequest({
739
743
  method: "DELETE",
740
- path: `/api/v1/mockups/${mockup_uuid}`,
744
+ path: `/api/v1/psd-mockups/${mockup_uuid}`,
741
745
  });
742
746
  return { content: [{ type: "text", text: `Mockup ${mockup_uuid} deleted successfully.` }] };
743
747
  });
744
748
  // ---------------------------------------------------------------------------
745
- // Tool 5: render_mockup
749
+ // Tool 5: render_psd_mockup
746
750
  // ---------------------------------------------------------------------------
747
751
  const smartObjectInputSchema = z
748
752
  .object({
@@ -807,7 +811,7 @@ const textLayerInputSchema = z
807
811
  uuid: z
808
812
  .string()
809
813
  .regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
810
- .describe("UUID from get_mockup_details text_layers"),
814
+ .describe("UUID from get_psd_mockup text_layers"),
811
815
  text: z.string().min(1).max(500).optional().describe("Replacement text for a single-style layer"),
812
816
  segments: z
813
817
  .array(z.object({
@@ -854,8 +858,8 @@ const textLayerInputSchema = z
854
858
  ctx.addIssue({ code: "custom", message: "Segment indexes must be unique" });
855
859
  }
856
860
  });
857
- server.tool("render_mockup", "Render a PSD mockup with artwork, editable text, or both. Supports one or multiple smart objects and preserves the template's authored appearance. Returns the rendered image URL. Costs 1 credit. Use list_mockups and get_mockup_details to find target UUIDs.", {
858
- mockup_uuid: z.string().describe("UUID of the mockup template (from list_mockups)"),
861
+ server.tool("render_psd_mockup", "Render a PSD mockup with artwork, editable text, or both. Supports one or multiple smart objects and preserves the template's authored appearance. Returns the rendered image URL. Costs 1 credit. Use list_psd_mockups and get_psd_mockup to find target UUIDs.", {
862
+ mockup_uuid: z.string().describe("UUID of the PSD mockup template (from list_psd_mockups)"),
859
863
  smart_object_uuid: z.string().optional().describe("UUID of one smart object layer. Provide with artwork_url, or use smart_objects for one or more entries."),
860
864
  artwork_url: z.string().optional().describe("Public artwork URL for smart_object_uuid. Provide both singular fields, or use smart_objects."),
861
865
  smart_objects: z
@@ -868,7 +872,7 @@ server.tool("render_mockup", "Render a PSD mockup with artwork, editable text, o
868
872
  .min(1)
869
873
  .max(50)
870
874
  .optional()
871
- .describe("Editable text overrides from get_mockup_details. Each entry needs exactly one of text or segments. May be used alone or with smart objects."),
875
+ .describe("Editable text overrides from get_psd_mockup. Each entry needs exactly one of text or segments. May be used alone or with smart objects."),
872
876
  fit: z.enum(["fill", "contain", "cover"]).default("fill").describe("How singular artwork_url fills its smart object area"),
873
877
  image_format: z.enum(["webp", "png", "jpg"]).default("webp").describe("Output format"),
874
878
  image_size: z.number().min(100).max(10000).default(2048).describe("Output width in pixels (default 2048)"),
@@ -998,11 +1002,11 @@ server.tool("remove_background", "Remove the background from any image and retur
998
1002
  };
999
1003
  });
1000
1004
  // ---------------------------------------------------------------------------
1001
- // Tool: create_2d_mockup
1005
+ // Tool: create_photo_mockup
1002
1006
  // ---------------------------------------------------------------------------
1003
- server.tool("create_2d_mockup", "Create a reusable 2D mockup from a public image URL. Returns the mockup ID and public render targets synchronously. Costs 25 credits. If the image is unsuitable, the 25 credits are refunded automatically. Set is_async=true to queue instead and receive a job_id to poll with get_job or wait_for_job. Use the dashboard for visual fine-tuning.", {
1007
+ server.tool("create_photo_mockup", "Create a reusable photo mockup from a public image URL of a product. Returns the mockup_id and its render targets synchronously. Costs 25 credits. If the image is unsuitable, the 25 credits are refunded automatically. Set is_async=true to queue instead and receive a job_id to poll with get_job or wait_for_job. Use the dashboard for visual fine-tuning.", {
1004
1008
  source_url: z.string().describe("Public HTTPS URL of the product image"),
1005
- name: z.string().optional().describe("Optional display name for the 2D mockup"),
1009
+ name: z.string().optional().describe("Optional display name for the photo mockup"),
1006
1010
  idempotency_key: z.string().min(1).max(255).optional().describe("Optional retry-stable key for this create request"),
1007
1011
  is_async: z
1008
1012
  .boolean()
@@ -1016,7 +1020,7 @@ server.tool("create_2d_mockup", "Create a reusable 2D mockup from a public image
1016
1020
  body.is_async = true;
1017
1021
  const result = (await apiRequest({
1018
1022
  method: "POST",
1019
- path: "/api/v1/sudoai/2d-mockups",
1023
+ path: "/api/v1/photo-mockups",
1020
1024
  body,
1021
1025
  headers: { "Idempotency-Key": idempotency_key ?? randomUUID() },
1022
1026
  }));
@@ -1050,15 +1054,16 @@ server.tool("create_2d_mockup", "Create a reusable 2D mockup from a public image
1050
1054
  return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
1051
1055
  });
1052
1056
  // ---------------------------------------------------------------------------
1053
- // 2D render: one tool per kind of target
1057
+ // Tool: render_photo_mockup
1054
1058
  // ---------------------------------------------------------------------------
1055
- // A render names one target and nothing else. A product surface takes a
1056
- // coverage percentage; a print area takes a fit or an explicit box. Two tools
1057
- // rather than one, because those are two different dials and a single tool
1058
- // could only describe in prose what the shape can state outright: whichever
1059
- // tool the caller reaches for, every field on it applies.
1060
- const TWO_D_SHARED = {
1061
- mockup_uuid: z.string().describe("UUID of the 2D mockup template (from list_2d_mockups, returned as mockup_id)."),
1059
+ // A render names one target and nothing else: exactly one of print_area_uuid
1060
+ // or surface_uuid. A product surface is sized by a coverage percentage, a
1061
+ // print area by a fit, and either by an exact width + height. One tool holds
1062
+ // both dials, the same shape the hosted server exposes, so the dial that
1063
+ // belongs to the other kind of target is refused here by name instead of
1064
+ // being a field the caller could never reach.
1065
+ const PHOTO_RENDER_SHARED = {
1066
+ mockup_id: z.string().describe("UUID of the photo mockup (mockup_id from list_photo_mockups, get_photo_mockup or create_photo_mockup)."),
1062
1067
  artwork_url: z.string().describe("Public URL of the artwork image (PNG/JPG/WebP) to place on the mockup"),
1063
1068
  remove_background: z.boolean().default(false).describe("Remove the artwork's background before placing it. Adds 25 credits per artwork."),
1064
1069
  opacity: z.number().min(0).max(100).default(100).describe("Artwork opacity percentage (0-100)"),
@@ -1104,72 +1109,80 @@ const TWO_D_SHARED = {
1104
1109
  .default(false)
1105
1110
  .describe("Queue the render instead of waiting. When true the API returns 202 with a job_id immediately -- poll with get_job, or call wait_for_job to block until it finishes and hands back result_url. Default false returns print_files + render_uuid synchronously (200). Use for long renders or when running many in parallel."),
1106
1111
  };
1107
- // The option a caller can write on one of these two tools but not the other,
1108
- // and the sentence that sends them to the one that works.
1112
+ // The sentences a refused option is answered with.
1109
1113
  //
1110
- // An argument a tool never declared is dropped before the handler ever sees it,
1111
- // so `coverage` on a print area, `fit` on a surface, and the retired `scale` on
1112
- // either used to come back as a successful render with the caller's option
1113
- // gone and nothing said about it. The API answers all three with a 422. The
1114
- // consumer on this side is a language model: an option it wrote disappearing in
1115
- // silence teaches it nothing and it writes the same option again, while one
1116
- // named sentence teaches it in a single turn.
1117
- //
1118
- // The wording is the API's own, so the caller reads the same reason whichever
1114
+ // The API answers each of these with a 422, and refusing them here as well is
1115
+ // deliberate: a client that can put a shape on the wire the API is guaranteed
1116
+ // to reject has spent the caller's round trip telling them something it
1117
+ // already knew. The consumer on this side is a language model: an option it
1118
+ // wrote disappearing in silence teaches it nothing and it writes the same
1119
+ // option again, while one named sentence teaches it in a single turn. The
1120
+ // wording is the API's own, so the caller reads the same reason whichever
1119
1121
  // side answers first.
1122
+ const ONE_TARGET = "Provide exactly one of print_area_uuid or surface_uuid";
1123
+ const FIT_ON_SURFACE = "A surface covers the whole product, so fit has nothing to fit against. Send coverage to choose how much of the surface the artwork spans, or width and height to give it an exact size, or render a print area instead.";
1124
+ const COVERAGE_ON_PRINT_AREA = "A print area is sized by fit, or by an explicit width and height, so coverage has no meaning on one. Send fit or a width and height, or render the product surface instead.";
1120
1125
  const RETIRED_SCALE = "scale has been retired and nothing replaces the name. Send width and height together to draw the artwork at an exact size; a single multiplier could not express two independent axes.";
1121
- const TWO_D_REFUSED_OPTIONS = {
1122
- surface: {
1123
- fit: "A surface covers the whole product, so fit has nothing to fit against. Send coverage to choose how much of the surface the artwork spans, or width and height to give it an exact size, or render a print area instead.",
1124
- scale: RETIRED_SCALE,
1125
- },
1126
- print_area: {
1127
- coverage: "A print area is sized by fit, or by an explicit width and height, so coverage has no meaning on one. Send fit or a width and height, or render the product surface instead.",
1128
- scale: RETIRED_SCALE,
1129
- },
1130
- };
1131
1126
  /**
1132
- * Build the arguments one 2D render tool accepts, and refuse the rest by name.
1133
- *
1134
- * Strict on purpose, and only on these two tools: the pair exists precisely
1135
- * because the sizing options are split between them, so an option landing on
1136
- * the wrong one of the two is the mistake most worth catching. Refusing is
1137
- * keyed on the argument being written rather than on the value it holds, so an
1138
- * explicitly written null is caught as well: writing it is the caller naming
1139
- * the option.
1140
- *
1141
- * An option with no sentence of its own still gets refused rather than dropped.
1142
- * A name we never published is either a typo or an option the caller believes
1143
- * exists, and both are worth one line of reply.
1127
+ * The tool's input. Strict on purpose: an argument a tool never declared is
1128
+ * dropped before the handler ever sees it, so the retired `scale` used to come
1129
+ * back as a successful render with the caller's option gone and nothing said
1130
+ * about it. A name we never published is either a typo or an option the
1131
+ * caller believes exists, and both are worth one line of reply.
1144
1132
  */
1145
- function twoDRenderInput(kind, shape) {
1146
- const refused = TWO_D_REFUSED_OPTIONS[kind];
1147
- return z.strictObject(shape, {
1148
- error: (issue) => {
1149
- if (issue.code !== "unrecognized_keys")
1150
- return undefined;
1151
- return issue.keys
1152
- .map((key) => refused[key] ??
1153
- `${key} is not an option on this tool. Send only the options it lists, and read them from the tool's own arguments rather than from another tool's.`)
1154
- .join(" ");
1155
- },
1156
- });
1157
- }
1133
+ const renderPhotoMockupInput = z.strictObject({
1134
+ ...PHOTO_RENDER_SHARED,
1135
+ print_area_uuid: z
1136
+ .string()
1137
+ .optional()
1138
+ .describe("UUID of a saved print area from get_photo_mockup's print_areas[] (its print_area_id): a bounded zone somebody drew on the product, such as a chest logo. Omit when surface_uuid is used."),
1139
+ surface_uuid: z
1140
+ .string()
1141
+ .optional()
1142
+ .describe("UUID of a product surface from get_photo_mockup's surfaces[]: a whole printable product, for an all-over print. Omit when print_area_uuid is used."),
1143
+ coverage: z
1144
+ .number()
1145
+ .min(10)
1146
+ .max(100)
1147
+ .optional()
1148
+ .describe("How much of the surface the artwork spans, as a percentage (10-100). Belongs to surface_uuid; sending it with print_area_uuid is refused. Omit to span the whole surface, which is what an all-over print usually wants. Send width and height instead to give the artwork an exact size."),
1149
+ fit: z
1150
+ .enum(["contain", "fill", "cover"])
1151
+ .optional()
1152
+ .describe("How the artwork meets the print area, which it always fills edge to edge: 'contain' keeps the proportions and fits inside (the default), 'fill' stretches to the edges, 'cover' fills and crops the overflow. Belongs to print_area_uuid; sending it with surface_uuid is refused. Leave it out to get 'contain'. To sit inside the area with room around it, send width and height instead."),
1153
+ // A percentage cannot express a box whose proportions differ from the
1154
+ // surface's, which is exactly what an artwork resized on a canvas is, so
1155
+ // the exact box belongs to both kinds of target.
1156
+ width: z
1157
+ .number()
1158
+ .min(1)
1159
+ .max(30000)
1160
+ .optional()
1161
+ .describe("Artwork width in pixels, drawn at that exact size instead of by coverage or fit. Send together with height, and without coverage or fit. Width and height are independent, so any aspect ratio is allowed - stretching on one axis only is a supported placement."),
1162
+ height: z
1163
+ .number()
1164
+ .min(1)
1165
+ .max(30000)
1166
+ .optional()
1167
+ .describe("Artwork height in pixels. Send together with width. Sending only one of the two is rejected rather than silently completed, so the aspect ratio is never guessed for you."),
1168
+ }, {
1169
+ error: (issue) => {
1170
+ if (issue.code !== "unrecognized_keys")
1171
+ return undefined;
1172
+ return issue.keys
1173
+ .map((key) => key === "scale"
1174
+ ? RETIRED_SCALE
1175
+ : `${key} is not an option on this tool. Send only the options it lists.`)
1176
+ .join(" ");
1177
+ },
1178
+ });
1158
1179
  /**
1159
1180
  * Refuse a placement that answers the sizing question more than once, or half.
1160
1181
  *
1161
- * The API refuses these too, and refusing them here as well is deliberate. A
1162
- * client that can put a shape on the wire the API is guaranteed to reject has
1163
- * spent the caller's round trip telling them something it already knew.
1164
- *
1165
- * This is not the thing that was taken out of here. A default invents a value
1166
- * the caller never typed and quietly changes the render when it drifts from
1167
- * the renderer's; a refusal invents nothing, and if it ever drifts it drifts
1168
- * into refusing out loud, with the API still refusing behind it. It also
1169
- * cannot drift unnoticed: the contract test reads the shared placement wire
1170
- * fixture and asserts every shape listed there as rejected is still
1171
- * unreachable from every tool, and the wording below is the wording the API
1172
- * answers with, so a caller reads the same reason from either side.
1182
+ * A default invents a value the caller never typed and quietly changes the
1183
+ * render when it drifts from the renderer's; a refusal invents nothing, and if
1184
+ * it ever drifts it drifts into refusing out loud, with the API still refusing
1185
+ * behind it.
1173
1186
  *
1174
1187
  * `relativeSizing` is whichever proportional option belongs to this kind of
1175
1188
  * target -- `coverage` on a surface, `fit` on a print area.
@@ -1187,16 +1200,15 @@ function assertOneSizingAnswer(placement, relativeSizing) {
1187
1200
  /**
1188
1201
  * Send one render and hand back what the caller can act on.
1189
1202
  *
1190
- * `target` carries the address field, `placement` the dials that belong to that
1191
- * kind of target. Neither is inspected here: the dials a caller may turn are
1192
- * settled by which tool they called, so there is nothing left to check.
1203
+ * `target` carries the address field, `placement` the sizing dials that
1204
+ * belong to that kind of target. Neither is inspected here: the handler has
1205
+ * already settled which target this is and which dials it may carry.
1193
1206
  */
1194
- async function renderTwoD(args, target, placement) {
1195
- // Anchoring belongs to both kinds of target; only sizing is split, and the
1196
- // caller's tool already settled which sizing options exist. What the caller
1197
- // did not name is absent, and a placement nobody touched is absent entirely
1198
- // rather than sent as an empty object -- an empty object still claims the
1199
- // caller reached for placement.
1207
+ async function renderPhotoMockup(args, target, placement) {
1208
+ // Anchoring belongs to both kinds of target; only sizing is split. What the
1209
+ // caller did not name is absent, and a placement nobody touched is absent
1210
+ // entirely rather than sent as an empty object -- an empty object still
1211
+ // claims the caller reached for placement.
1200
1212
  const sent = {
1201
1213
  ...(args.position === undefined ? {} : { position: args.position }),
1202
1214
  ...(args.offset_x === undefined ? {} : { offset_x: args.offset_x }),
@@ -1229,12 +1241,13 @@ async function renderTwoD(args, target, placement) {
1229
1241
  body.is_async = true;
1230
1242
  const result = await apiRequest({
1231
1243
  method: "POST",
1232
- path: `/api/v1/sudoai/2d-mockups/${args.mockup_uuid}/render`,
1244
+ path: `/api/v1/photo-mockups/${args.mockup_id}/render`,
1233
1245
  body,
1234
1246
  timeout: RENDER_TIMEOUT,
1235
1247
  });
1236
- // is_async=true -> 202 + job_id (kind "2d_render"); hand back the poll contract.
1237
- // Reuse get_job / wait_for_job to reach the terminal job (result_url).
1248
+ // is_async=true -> 202 + job_id (kind "photo_mockup_render" on this path);
1249
+ // hand back the poll contract. Reuse get_job / wait_for_job to reach the
1250
+ // terminal job (result_url).
1238
1251
  if (args.is_async) {
1239
1252
  return { content: [{ type: "text", text: formatJobAccepted(result) }] };
1240
1253
  }
@@ -1254,71 +1267,23 @@ async function renderTwoD(args, target, placement) {
1254
1267
  };
1255
1268
  return { content: [{ type: "text", text: JSON.stringify(rendered, null, 2) }] };
1256
1269
  }
1257
- // ---------------------------------------------------------------------------
1258
- // Tool: render_2d_surface
1259
- // ---------------------------------------------------------------------------
1260
- server.registerTool("render_2d_surface", {
1261
- description: "Render artwork across a whole product surface -- an all-over print. Every printable product in the photo is a surface with its own surface_uuid, listed by get_2d_mockup. Returns print_files (each with an export_path) and a render_uuid. Costs 5 credits. Use the dashboard for visual fine-tuning.",
1262
- inputSchema: twoDRenderInput("surface", {
1263
- ...TWO_D_SHARED,
1264
- surface_uuid: z.string().describe("UUID of a product surface from get_2d_mockup's surfaces[]."),
1265
- coverage: z
1266
- .number()
1267
- .min(10)
1268
- .max(100)
1269
- .optional()
1270
- .describe("How much of the surface the artwork spans, as a percentage (10-100). Omit to span the whole surface, which is what an all-over print usually wants. Send width and height instead to give the artwork an exact size."),
1271
- // A percentage cannot express a box whose proportions differ from the
1272
- // surface's, which is exactly what an artwork resized on a canvas is.
1273
- width: z
1274
- .number()
1275
- .min(1)
1276
- .max(30000)
1277
- .optional()
1278
- .describe("Artwork width in pixels, drawn at that exact size rather than as a percentage of the surface. Send together with height, and without coverage."),
1279
- height: z
1280
- .number()
1281
- .min(1)
1282
- .max(30000)
1283
- .optional()
1284
- .describe("Artwork height in pixels. Send together with width, and without coverage. The two axes are independent, so any aspect ratio is allowed."),
1285
- }),
1270
+ server.registerTool("render_photo_mockup", {
1271
+ description: "Render artwork onto a saved photo mockup. Name exactly one target: a print_area_uuid (a bounded zone somebody drew on the product, such as a chest logo; sized by fit or by width + height) or a surface_uuid (a whole printable product, for an all-over print; sized by coverage or by width + height). Read both from get_photo_mockup. Returns print_files (each with an export_path) and a render_uuid. Costs 5 credits. Use the dashboard for visual fine-tuning.",
1272
+ inputSchema: renderPhotoMockupInput,
1286
1273
  }, async (args) => {
1274
+ const hasPrintArea = args.print_area_uuid !== undefined;
1275
+ const hasSurface = args.surface_uuid !== undefined;
1276
+ if (hasPrintArea === hasSurface)
1277
+ throw new Error(ONE_TARGET);
1278
+ // The dial of the other kind of target is refused by name. Refusing is
1279
+ // keyed on the argument being written rather than on the value it holds:
1280
+ // writing it is the caller naming the option.
1281
+ if (hasSurface && args.fit !== undefined)
1282
+ throw new Error(FIT_ON_SURFACE);
1283
+ if (hasPrintArea && args.coverage !== undefined)
1284
+ throw new Error(COVERAGE_ON_PRINT_AREA);
1287
1285
  const sizing = {
1288
1286
  ...(args.coverage === undefined ? {} : { coverage: args.coverage }),
1289
- ...(args.width === undefined ? {} : { width: args.width }),
1290
- ...(args.height === undefined ? {} : { height: args.height }),
1291
- };
1292
- assertOneSizingAnswer(sizing, "coverage");
1293
- return renderTwoD(args, { surface_uuid: args.surface_uuid }, sizing);
1294
- });
1295
- // ---------------------------------------------------------------------------
1296
- // Tool: render_2d_print_area
1297
- // ---------------------------------------------------------------------------
1298
- server.registerTool("render_2d_print_area", {
1299
- description: "Render artwork onto one saved print area -- a bounded zone somebody drew on the product, such as a chest logo. Read the print_area_id values from get_2d_mockup. Returns print_files (each with an export_path) and a render_uuid. Costs 5 credits. Use the dashboard for visual fine-tuning.",
1300
- inputSchema: twoDRenderInput("print_area", {
1301
- ...TWO_D_SHARED,
1302
- print_area_uuid: z.string().describe("UUID of a saved print area from get_2d_mockup's print_areas[] (its print_area_id)."),
1303
- fit: z
1304
- .enum(["contain", "fill", "cover"])
1305
- .optional()
1306
- .describe("How the artwork meets the print area, which it always fills edge to edge: 'contain' keeps the proportions and fits inside (the default), 'fill' stretches to the edges, 'cover' fills and crops the overflow. Leave it out to get 'contain'. To sit inside the area with room around it, send width and height instead."),
1307
- width: z
1308
- .number()
1309
- .min(1)
1310
- .max(30000)
1311
- .optional()
1312
- .describe("Artwork width in print-area pixels. Send together with height, and without fit. Width and height are independent, so any aspect ratio is allowed - stretching on one axis only is a supported placement."),
1313
- height: z
1314
- .number()
1315
- .min(1)
1316
- .max(30000)
1317
- .optional()
1318
- .describe("Artwork height in print-area pixels. Send together with width. Sending only one of the two is rejected rather than silently completed, so the aspect ratio is never guessed for you."),
1319
- }),
1320
- }, async (args) => {
1321
- const sizing = {
1322
1287
  ...(args.fit === undefined ? {} : { fit: args.fit }),
1323
1288
  ...(args.width === undefined ? {} : { width: args.width }),
1324
1289
  ...(args.height === undefined ? {} : { height: args.height }),
@@ -1327,20 +1292,20 @@ server.registerTool("render_2d_print_area", {
1327
1292
  // the half instead would render a size the caller never asked for with no
1328
1293
  // error to notice, and forwarding it spends a round trip to be told what
1329
1294
  // the shape already said.
1330
- assertOneSizingAnswer(sizing, "fit");
1331
- return renderTwoD(args, { uuid: args.print_area_uuid }, sizing);
1295
+ assertOneSizingAnswer(sizing, hasSurface ? "coverage" : "fit");
1296
+ return renderPhotoMockup(args, hasSurface ? { surface_uuid: args.surface_uuid } : { uuid: args.print_area_uuid }, sizing);
1332
1297
  });
1333
1298
  // ---------------------------------------------------------------------------
1334
- // Tool: list_2d_mockups
1299
+ // Tool: list_photo_mockups
1335
1300
  // ---------------------------------------------------------------------------
1336
- server.tool("list_2d_mockups", "List your saved SudoAI 2D mockup templates (no PSD). Returns each mockup's mockup_id, name, status, thumbnail, dimensions, and print_areas. Use the mockup_id with get_2d_mockup to read the print-area and surface UUIDs a render needs. Costs 0 credits.", {
1301
+ server.tool("list_photo_mockups", "List your saved photo mockup templates, made from product photos with no PSD. Returns each mockup's mockup_id, name, status, thumbnail, dimensions, and print_areas. Use the mockup_id with get_photo_mockup to read the print-area and surface UUIDs a render needs. Costs 0 credits.", {
1337
1302
  limit: z.number().min(1).max(100).default(20).describe("Results per page (1-100, default 20)"),
1338
1303
  offset: z.number().min(0).default(0).describe("Pagination offset (default 0)"),
1339
1304
  customizable_only: z.boolean().default(false).describe("Return only mockups ready for shopper customization"),
1340
1305
  }, async ({ limit, offset, customizable_only }) => {
1341
1306
  const result = await apiRequest({
1342
1307
  method: "GET",
1343
- path: "/api/v1/sudoai/2d-mockups",
1308
+ path: "/api/v1/photo-mockups",
1344
1309
  params: { limit, offset, customizable_only },
1345
1310
  });
1346
1311
  return {
@@ -1351,14 +1316,14 @@ server.tool("list_2d_mockups", "List your saved SudoAI 2D mockup templates (no P
1351
1316
  };
1352
1317
  });
1353
1318
  // ---------------------------------------------------------------------------
1354
- // Tool: get_2d_mockup
1319
+ // Tool: get_photo_mockup
1355
1320
  // ---------------------------------------------------------------------------
1356
- server.tool("get_2d_mockup", "Get one SudoAI 2D mockup's full details: the saved print_areas[] somebody drew on it, and the surfaces[] -- one entry per printable product in the photo. Pass a print_area_id to render_2d_print_area, or a surfaces[].surface_uuid to render_2d_surface. Costs 0 credits.", {
1357
- mockup_id: z.string().describe("UUID of the 2D mockup (mockup_id from list_2d_mockups)"),
1321
+ server.tool("get_photo_mockup", "Get one photo mockup's full details: the saved print_areas[] somebody drew on it, and the surfaces[] -- one entry per printable product in the photo. Pass a print_area_id as print_area_uuid, or a surfaces[].surface_uuid, to render_photo_mockup. Costs 0 credits.", {
1322
+ mockup_id: z.string().describe("UUID of the photo mockup (mockup_id from list_photo_mockups)"),
1358
1323
  }, async ({ mockup_id }) => {
1359
1324
  const result = await apiRequest({
1360
1325
  method: "GET",
1361
- path: `/api/v1/sudoai/2d-mockups/${mockup_id}`,
1326
+ path: `/api/v1/photo-mockups/${mockup_id}`,
1362
1327
  });
1363
1328
  return {
1364
1329
  content: [{
@@ -1368,10 +1333,10 @@ server.tool("get_2d_mockup", "Get one SudoAI 2D mockup's full details: the saved
1368
1333
  };
1369
1334
  });
1370
1335
  // ---------------------------------------------------------------------------
1371
- // Tool: update_2d_print_areas
1336
+ // Tool: update_photo_mockup_print_areas
1372
1337
  // ---------------------------------------------------------------------------
1373
- server.tool("update_2d_print_areas", "Replace a 2D mockup's print areas with up to 8 four-point quads and return the updated geometry. An empty list is accepted only for verified full product surfaces. Costs 0 credits.", {
1374
- mockup_id: z.string().describe("UUID of the 2D mockup to update"),
1338
+ server.tool("update_photo_mockup_print_areas", "Replace a photo mockup's print areas with up to 8 four-point quads and return the updated geometry. An empty list is accepted only for verified full product surfaces. Costs 0 credits.", {
1339
+ mockup_id: z.string().describe("UUID of the photo mockup to update"),
1375
1340
  print_areas: z
1376
1341
  .array(z.object({
1377
1342
  points: z
@@ -1386,7 +1351,7 @@ server.tool("update_2d_print_areas", "Replace a 2D mockup's print areas with up
1386
1351
  }, async ({ mockup_id, print_areas }) => {
1387
1352
  const result = await apiRequest({
1388
1353
  method: "PUT",
1389
- path: `/api/v1/sudoai/2d-mockups/${mockup_id}/print-areas`,
1354
+ path: `/api/v1/photo-mockups/${mockup_id}/print-areas`,
1390
1355
  body: { print_areas },
1391
1356
  });
1392
1357
  const envelope = asRecord(result);
@@ -1403,14 +1368,14 @@ server.tool("update_2d_print_areas", "Replace a 2D mockup's print areas with up
1403
1368
  return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
1404
1369
  });
1405
1370
  // ---------------------------------------------------------------------------
1406
- // Tool: delete_2d_mockup
1371
+ // Tool: delete_photo_mockup
1407
1372
  // ---------------------------------------------------------------------------
1408
- server.tool("delete_2d_mockup", "Permanently delete a SudoAI 2D mockup template and all of its data. Cannot be undone. Costs 0 credits.", {
1409
- mockup_id: z.string().describe("UUID of the 2D mockup to delete (mockup_id from list_2d_mockups)"),
1373
+ server.tool("delete_photo_mockup", "Permanently delete a photo mockup template and all of its data. Cannot be undone. Costs 0 credits.", {
1374
+ mockup_id: z.string().describe("UUID of the photo mockup to delete (mockup_id from list_photo_mockups)"),
1410
1375
  }, async ({ mockup_id }) => {
1411
1376
  const result = await apiRequest({
1412
1377
  method: "DELETE",
1413
- path: `/api/v1/sudoai/2d-mockups/${mockup_id}`,
1378
+ path: `/api/v1/photo-mockups/${mockup_id}`,
1414
1379
  });
1415
1380
  const envelope = asRecord(result);
1416
1381
  const data = asRecord(envelope.data ?? result);
@@ -1458,7 +1423,7 @@ server.tool("upload_psd", "Upload a Photoshop PSD/PSB file as a new mockup templ
1458
1423
  // ---------------------------------------------------------------------------
1459
1424
  // Tool: get_job
1460
1425
  // ---------------------------------------------------------------------------
1461
- server.tool("get_job", "Get the current status of any async render, video, upload, or 2D job by its job_id. Returns status (queued|running|succeeded|failed), completed-result details and credits charged, or an error if failed. To block until done, use wait_for_job instead.", {
1426
+ server.tool("get_job", "Get the current status of any async render, video, upload, or photo mockup (2D) job by its job_id. Returns status (queued|dispatched|running|succeeded|failed|cancelled), completed-result details and credits charged, or an error if failed. To block until done, use wait_for_job instead.", {
1462
1427
  job_id: z.string().describe("The job_id returned by any async submission"),
1463
1428
  }, async ({ job_id }) => {
1464
1429
  const result = await getJob(job_id);
@@ -1467,8 +1432,11 @@ server.tool("get_job", "Get the current status of any async render, video, uploa
1467
1432
  // ---------------------------------------------------------------------------
1468
1433
  // Tool: list_jobs
1469
1434
  // ---------------------------------------------------------------------------
1470
- server.tool("list_jobs", "List your async jobs, including PSD renders, videos, uploads, and 2D creation/renders, newest first. Use this when you do not already hold a job_id. Pass the returned next_cursor to fetch the next page.", {
1471
- kind: z.enum(["video", "render", "upload", "2d_create", "2d_render"]).optional().describe("Filter by job kind. Omit for all kinds."),
1435
+ server.tool("list_jobs", "List your async jobs, including PSD renders, videos, uploads, and photo mockup (2D) creation/renders, newest first. Use this when you do not already hold a job_id. Pass the returned next_cursor to fetch the next page.", {
1436
+ kind: z
1437
+ .enum(["video", "render", "upload", "2d_create", "2d_render", "photo_mockup_create", "photo_mockup_render"])
1438
+ .optional()
1439
+ .describe("Filter by job kind. A photo mockup job has two spellings of its kind (photo_mockup_create / photo_mockup_render, or the legacy 2d_create / 2d_render); either spelling selects both. Omit for all kinds."),
1472
1440
  mockup_uuid: z.string().optional().describe("Filter by source mockup UUID (e.g. one mockup's videos). Raw-image videos are never returned by this filter."),
1473
1441
  limit: z.number().int().min(1).max(50).default(20).describe("Max jobs per page (1-50, default 20)"),
1474
1442
  cursor: z.string().optional().describe("Opaque keyset cursor from a prior page's next_cursor"),
@@ -1487,7 +1455,7 @@ server.tool("list_jobs", "List your async jobs, including PSD renders, videos, u
1487
1455
  // ---------------------------------------------------------------------------
1488
1456
  // Tool: wait_for_job
1489
1457
  // ---------------------------------------------------------------------------
1490
- server.tool("wait_for_job", "Poll any async render, video, upload, or 2D job until it succeeds or fails, then return the final result and credits charged. Blocks while polling.", {
1458
+ server.tool("wait_for_job", "Poll any async render, video, upload, or photo mockup (2D) job until it succeeds, fails, or is cancelled, then return the final result and credits charged. Blocks while polling.", {
1491
1459
  job_id: z.string().describe("The job_id to wait on (from an async submission or render_video)"),
1492
1460
  poll_interval_seconds: z
1493
1461
  .number()
@@ -1525,8 +1493,8 @@ server.tool("wait_for_job", "Poll any async render, video, upload, or 2D job unt
1525
1493
  // Tool: render_video
1526
1494
  // ---------------------------------------------------------------------------
1527
1495
  server.tool("render_video", "Create a short AI video from either a mockup with artwork or a public image URL. Supply exactly one input mode. Always async: returns a job_id immediately for get_job or wait_for_job. Credit cost depends on clip length, audio, and the automatically selected quality. Unsupported durations are rejected.", {
1528
- mockup_uuid: z.string().optional().describe("RENDER MODE: UUID of the mockup to animate (from list_mockups or upload_psd). Required in render mode. In raw-image mode it is an optional association (groups the clip under that mockup's 'Past clips')."),
1529
- smart_object_uuid: z.string().optional().describe("RENDER MODE: UUID of the smart object layer to place artwork on (from get_mockup_details). Required in render mode; omit in raw-image mode."),
1496
+ mockup_uuid: z.string().optional().describe("RENDER MODE: UUID of the PSD mockup to animate (from list_psd_mockups or upload_psd). Required in render mode. In raw-image mode it is an optional association (groups the clip under that mockup's 'Past clips')."),
1497
+ smart_object_uuid: z.string().optional().describe("RENDER MODE: UUID of the smart object layer to place artwork on (from get_psd_mockup). Required in render mode; omit in raw-image mode."),
1530
1498
  artwork_url: z.string().optional().describe("RENDER MODE: public URL of the artwork image (PNG/JPG/WebP) to place on the mockup before animating. Provide this OR artwork_base64. Omit in raw-image mode."),
1531
1499
  artwork_base64: z.string().optional().describe("RENDER MODE: raw base64-encoded artwork bytes (no data: prefix). Provide this OR artwork_url."),
1532
1500
  artwork_content_type: z.enum(["image/png", "image/jpeg", "image/webp", "image/gif"]).optional().describe("MIME type for artwork_base64 (defaults to image/png if omitted)."),
@@ -1633,11 +1601,23 @@ server.tool("get_account", "Get your account info: subscription plan, remaining
1633
1601
  // HMAC-SHA256 over `${timestamp}.${rawBody}` with the endpoint's secret) and
1634
1602
  // "X-SudoMock-Timestamp: <unix-seconds>". Verify in constant time and reject if
1635
1603
  // |now - timestamp| > 300s (User-Agent SudoMock-Webhook/1.0). Standard job
1636
- // events and the typed 2D create/render events have separate documented bodies.
1604
+ // events and the typed photo mockup create/render events have separate
1605
+ // documented bodies.
1637
1606
  //
1638
1607
  // Event types: render.succeeded, render.failed, upload.succeeded,
1639
- // video.succeeded, video.failed, 2d_mockup.ready, 2d_mockup.rejected,
1640
- // 2d_mockup.failed, 2d_render.succeeded, 2d_render.failed, webhook.test.
1608
+ // video.succeeded, video.failed, photo_mockup.ready, photo_mockup.rejected,
1609
+ // photo_mockup.failed, photo_mockup_render.succeeded,
1610
+ // photo_mockup_render.failed, webhook.test.
1611
+ //
1612
+ // The five photo mockup events also have a legacy spelling (2d_mockup.ready,
1613
+ // 2d_mockup.rejected, 2d_mockup.failed, 2d_render.succeeded,
1614
+ // 2d_render.failed). Both spellings are subscribable; which one a delivery
1615
+ // carries is the endpoint's `event_naming` pin ('current' = family names,
1616
+ // 'legacy' = 2d_* names), and the payload's `kind` follows the same pin.
1617
+ // Endpoints created before the pin existed are 'legacy'; new endpoints default
1618
+ // to 'current' on the API side. The pin is set at create and changed at update;
1619
+ // a re-pin sent alone re-spells the stored subscription list to match. The list
1620
+ // below is in the API's own order.
1641
1621
  // ---------------------------------------------------------------------------
1642
1622
  const WEBHOOK_EVENT_TYPES = [
1643
1623
  "render.succeeded",
@@ -1650,17 +1630,29 @@ const WEBHOOK_EVENT_TYPES = [
1650
1630
  "2d_mockup.failed",
1651
1631
  "2d_render.succeeded",
1652
1632
  "2d_render.failed",
1633
+ "photo_mockup.ready",
1634
+ "photo_mockup.rejected",
1635
+ "photo_mockup.failed",
1636
+ "photo_mockup_render.succeeded",
1637
+ "photo_mockup_render.failed",
1653
1638
  "webhook.test",
1654
1639
  ];
1640
+ const WEBHOOK_EVENT_NAMINGS = ["legacy", "current"];
1655
1641
  server.tool("create_webhook_endpoint", "Register a webhook endpoint that SudoMock calls when async jobs finish. The signing secret is returned IN FULL exactly once here -- store it to verify the HMAC carried in the X-SudoMock-Signature header (with X-SudoMock-Timestamp) on incoming deliveries. URL must be https and publicly routable.", {
1656
1642
  url: z.string().describe("https endpoint URL to receive POST deliveries (publicly routable; private/loopback hosts are rejected)"),
1657
1643
  event_types: z
1658
1644
  .array(z.enum(WEBHOOK_EVENT_TYPES))
1659
1645
  .default([])
1660
- .describe("Event types to subscribe to. Supports render, upload, video, 2D mockup, 2D render, and webhook.test events. Pass an empty array (the default) to subscribe to ALL events."),
1646
+ .describe("Event types to subscribe to. Supports render, upload, video, photo mockup create (photo_mockup.*), photo mockup render (photo_mockup_render.*), and webhook.test events; the legacy 2d_mockup.* / 2d_render.* spellings of the photo mockup events are accepted too. Pass an empty array (the default) to subscribe to ALL events."),
1647
+ event_naming: z
1648
+ .enum(WEBHOOK_EVENT_NAMINGS)
1649
+ .optional()
1650
+ .describe("Which spelling of the photo mockup events this endpoint receives: 'current' (photo_mockup.*, photo_mockup_render.*, kind photo_mockup_create/photo_mockup_render) or 'legacy' (2d_mockup.*, 2d_render.*, kind 2d_create/2d_render). Omit to take the API default ('current'). Pick 'legacy' only for a receiver written against the old names."),
1661
1651
  description: z.string().max(255).optional().describe("Optional human-readable label for this endpoint"),
1662
- }, async ({ url, event_types, description }) => {
1652
+ }, async ({ url, event_types, event_naming, description }) => {
1663
1653
  const body = { url, event_types };
1654
+ if (event_naming !== undefined)
1655
+ body.event_naming = event_naming;
1664
1656
  if (description)
1665
1657
  body.description = description;
1666
1658
  const result = await apiRequest({
@@ -1683,7 +1675,7 @@ server.tool("list_webhook_endpoints", "List your registered webhook endpoints (i
1683
1675
  const output = Array.isArray(result) ? result.map(publicWebhookEndpoint) : [];
1684
1676
  return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
1685
1677
  });
1686
- server.tool("update_webhook_endpoint", "Update a webhook endpoint in place: change its url, description, subscribed event_types, or enable/disable it (enabled:false pauses deliveries without losing the signing secret). All fields optional -- only the ones you pass are changed. The secret is NOT rotated or returned here.", {
1678
+ server.tool("update_webhook_endpoint", "Update a webhook endpoint in place: change its url, description, subscribed event_types, its event_naming pin, or enable/disable it (enabled:false pauses deliveries without losing the signing secret). All fields optional -- only the ones you pass are changed. The secret is NOT rotated or returned here.", {
1687
1679
  endpoint_id: z.string().describe("The id of the webhook endpoint to update (from list_webhook_endpoints)"),
1688
1680
  url: z.string().optional().describe("New https endpoint URL (publicly routable; private/loopback hosts are rejected)"),
1689
1681
  description: z.string().max(255).optional().describe("New human-readable label"),
@@ -1691,8 +1683,12 @@ server.tool("update_webhook_endpoint", "Update a webhook endpoint in place: chan
1691
1683
  .array(z.enum(WEBHOOK_EVENT_TYPES))
1692
1684
  .optional()
1693
1685
  .describe("Replacement list of subscribed event types. Pass an empty array to subscribe to ALL events."),
1686
+ event_naming: z
1687
+ .enum(WEBHOOK_EVENT_NAMINGS)
1688
+ .optional()
1689
+ .describe("Re-pin which spelling of the photo mockup events this endpoint receives: 'current' (photo_mockup.*, photo_mockup_render.*, kind photo_mockup_create/photo_mockup_render) or 'legacy' (2d_mockup.*, 2d_render.*, kind 2d_create/2d_render). Omit to keep the endpoint's current pin. Sent on its own, the re-pin re-spells the stored subscription list to match. Move an endpoint to 'current' only once its receiver handles the new names."),
1694
1690
  enabled: z.boolean().optional().describe("Set false to pause deliveries (secret preserved), true to resume"),
1695
- }, async ({ endpoint_id, url, description, event_types, enabled }) => {
1691
+ }, async ({ endpoint_id, url, description, event_types, event_naming, enabled }) => {
1696
1692
  const body = {};
1697
1693
  if (url !== undefined)
1698
1694
  body.url = url;
@@ -1700,6 +1696,8 @@ server.tool("update_webhook_endpoint", "Update a webhook endpoint in place: chan
1700
1696
  body.description = description;
1701
1697
  if (event_types !== undefined)
1702
1698
  body.event_types = event_types;
1699
+ if (event_naming !== undefined)
1700
+ body.event_naming = event_naming;
1703
1701
  if (enabled !== undefined)
1704
1702
  body.enabled = enabled;
1705
1703
  const result = await apiRequest({