@sudomock/mcp 3.0.0 → 3.2.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 +71 -65
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +717 -181
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
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
|
|
7
|
-
* product photos via the SudoMock API.
|
|
6
|
+
* Renders photorealistic product mockups from Photoshop PSD templates via the SudoMock API.
|
|
8
7
|
*
|
|
9
8
|
* Auth: SUDOMOCK_API_KEY environment variable
|
|
10
9
|
* Transport: stdio (local process)
|
|
@@ -173,7 +172,7 @@ export function formatJobAccepted(result) {
|
|
|
173
172
|
};
|
|
174
173
|
return JSON.stringify(summary, null, 2);
|
|
175
174
|
}
|
|
176
|
-
export const TERMINAL_JOB_STATUSES = new Set(["succeeded", "failed"
|
|
175
|
+
export const TERMINAL_JOB_STATUSES = new Set(["succeeded", "failed"]);
|
|
177
176
|
/** GET /api/v1/jobs/{job_id} -- owner-scoped job status snapshot. */
|
|
178
177
|
async function getJob(jobId, timeout = DEFAULT_TIMEOUT) {
|
|
179
178
|
const result = (await apiRequest({
|
|
@@ -528,6 +527,7 @@ function publicAccount(result) {
|
|
|
528
527
|
const envelope = asRecord(result);
|
|
529
528
|
const data = asRecord(envelope.data ?? result);
|
|
530
529
|
const account = asRecord(data.account);
|
|
530
|
+
const organization = asRecord(data.organization);
|
|
531
531
|
const subscription = asRecord(data.subscription);
|
|
532
532
|
const usage = asRecord(data.usage);
|
|
533
533
|
const apiKey = asRecord(data.api_key);
|
|
@@ -540,6 +540,12 @@ function publicAccount(result) {
|
|
|
540
540
|
name: account.name ?? null,
|
|
541
541
|
created_at: account.created_at ?? null,
|
|
542
542
|
},
|
|
543
|
+
// The plan, the credits and the balance below belong to this
|
|
544
|
+
// organization, not to the person: the API key is the organization's.
|
|
545
|
+
organization: {
|
|
546
|
+
id: organization.id ?? null,
|
|
547
|
+
name: organization.name ?? null,
|
|
548
|
+
},
|
|
543
549
|
subscription: {
|
|
544
550
|
plan: subscription.plan ?? null,
|
|
545
551
|
tier: subscription.tier ?? null,
|
|
@@ -602,6 +608,57 @@ function publicWebhookAction(result) {
|
|
|
602
608
|
status: data.status ?? null,
|
|
603
609
|
};
|
|
604
610
|
}
|
|
611
|
+
function publicWebhookBulkReplay(result) {
|
|
612
|
+
const envelope = asRecord(result);
|
|
613
|
+
const data = asRecord(envelope.data ?? result);
|
|
614
|
+
return {
|
|
615
|
+
success: envelope.success !== false,
|
|
616
|
+
status: data.status ?? null,
|
|
617
|
+
count: data.count ?? null,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function publicFontList(result) {
|
|
621
|
+
const envelope = asRecord(result);
|
|
622
|
+
const rows = Array.isArray(envelope.data) ? envelope.data : [];
|
|
623
|
+
const pagination = asRecord(envelope.pagination);
|
|
624
|
+
return {
|
|
625
|
+
success: envelope.success !== false,
|
|
626
|
+
data: rows.map((row) => {
|
|
627
|
+
const font = asRecord(row);
|
|
628
|
+
return {
|
|
629
|
+
uuid: font.uuid ?? null,
|
|
630
|
+
family: font.family ?? null,
|
|
631
|
+
subfamily: font.subfamily ?? null,
|
|
632
|
+
postscript_name: font.postscript_name ?? null,
|
|
633
|
+
category: font.category ?? null,
|
|
634
|
+
license: font.license ?? null,
|
|
635
|
+
is_premium: font.is_premium === true,
|
|
636
|
+
is_system: font.is_system === true,
|
|
637
|
+
preview_url: font.preview_url ?? null,
|
|
638
|
+
file_url: font.file_url ?? null,
|
|
639
|
+
created_at: font.created_at ?? null,
|
|
640
|
+
};
|
|
641
|
+
}),
|
|
642
|
+
pagination: {
|
|
643
|
+
page: pagination.page ?? null,
|
|
644
|
+
per_page: pagination.per_page ?? null,
|
|
645
|
+
total: pagination.total ?? rows.length,
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function publicUploadTarget(result) {
|
|
650
|
+
const envelope = asRecord(result);
|
|
651
|
+
const data = asRecord(envelope.data ?? result);
|
|
652
|
+
return {
|
|
653
|
+
success: envelope.success !== false,
|
|
654
|
+
data: {
|
|
655
|
+
upload_url: data.upload_url ?? null,
|
|
656
|
+
file_url: data.file_url ?? null,
|
|
657
|
+
method: data.method ?? null,
|
|
658
|
+
expires_in: data.expires_in ?? null,
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
605
662
|
/**
|
|
606
663
|
* A job is done when its status is terminal. The poll response (GET
|
|
607
664
|
* /api/v1/jobs/{job_id}) always reports the job state in the `status` field.
|
|
@@ -647,10 +704,20 @@ function withToolLog(tool, cb) {
|
|
|
647
704
|
}
|
|
648
705
|
};
|
|
649
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* What each tool was registered with, keyed by the name it was registered
|
|
709
|
+
* under. Replaying an entry under a second name publishes the same tool twice
|
|
710
|
+
* without a second copy of its handler or its input schema, which is how the
|
|
711
|
+
* family spellings at the bottom of this file are added.
|
|
712
|
+
*/
|
|
713
|
+
const registrations = new Map();
|
|
650
714
|
/**
|
|
651
715
|
* Wraps every tool callback registered through `tool()` / `registerTool()` so
|
|
652
|
-
* each call writes one log line
|
|
653
|
-
*
|
|
716
|
+
* each call writes one log line, and records the registration on the way past.
|
|
717
|
+
* Both SDK methods take the callback as their last argument; the registration
|
|
718
|
+
* call sites below stay untouched and typed. The copy is taken before the
|
|
719
|
+
* callback is wrapped, so a replay logs under the name it is replayed as
|
|
720
|
+
* rather than under the name it was first registered with.
|
|
654
721
|
*/
|
|
655
722
|
function instrumentToolCalls(target) {
|
|
656
723
|
for (const method of ["tool", "registerTool"]) {
|
|
@@ -658,6 +725,7 @@ function instrumentToolCalls(target) {
|
|
|
658
725
|
const wrapped = (...args) => {
|
|
659
726
|
const last = args.length - 1;
|
|
660
727
|
if (typeof args[0] === "string" && typeof args[last] === "function") {
|
|
728
|
+
registrations.set(args[0], { method, args: [...args] });
|
|
661
729
|
args[last] = withToolLog(args[0], args[last]);
|
|
662
730
|
}
|
|
663
731
|
return original.apply(target, args);
|
|
@@ -674,9 +742,9 @@ const server = new McpServer({
|
|
|
674
742
|
});
|
|
675
743
|
instrumentToolCalls(server);
|
|
676
744
|
// ---------------------------------------------------------------------------
|
|
677
|
-
// Tool 1:
|
|
745
|
+
// Tool 1: list_mockups
|
|
678
746
|
// ---------------------------------------------------------------------------
|
|
679
|
-
server.tool("
|
|
747
|
+
server.tool("list_mockups", "List your uploaded mockup templates with UUIDs, names, and thumbnails. Use returned UUIDs with render_mockup or get_mockup_details.", {
|
|
680
748
|
limit: z.number().min(1).max(100).default(20).describe("Results per page (1-100, default 20)"),
|
|
681
749
|
offset: z.number().min(0).default(0).describe("Pagination offset (default 0)"),
|
|
682
750
|
name: z.string().optional().describe("Filter by name (case-insensitive substring match)"),
|
|
@@ -687,7 +755,7 @@ server.tool("list_psd_mockups", "List your uploaded PSD mockup templates with UU
|
|
|
687
755
|
}, async ({ limit, offset, name, created_after, created_before, sort_by, sort_order }) => {
|
|
688
756
|
const result = await apiRequest({
|
|
689
757
|
method: "GET",
|
|
690
|
-
path: "/api/v1/
|
|
758
|
+
path: "/api/v1/mockups",
|
|
691
759
|
params: { limit, offset, name, created_after, created_before, sort: sort_by, order: sort_order },
|
|
692
760
|
});
|
|
693
761
|
return {
|
|
@@ -698,14 +766,14 @@ server.tool("list_psd_mockups", "List your uploaded PSD mockup templates with UU
|
|
|
698
766
|
};
|
|
699
767
|
});
|
|
700
768
|
// ---------------------------------------------------------------------------
|
|
701
|
-
// Tool 2:
|
|
769
|
+
// Tool 2: get_mockup_details
|
|
702
770
|
// ---------------------------------------------------------------------------
|
|
703
|
-
server.tool("
|
|
704
|
-
mockup_uuid: z.string().describe("The UUID of the
|
|
771
|
+
server.tool("get_mockup_details", "Get full details of a mockup: smart object UUIDs, layer names, dimensions, positions, blend modes, and thumbnail URLs.", {
|
|
772
|
+
mockup_uuid: z.string().describe("The UUID of the mockup to inspect"),
|
|
705
773
|
}, async ({ mockup_uuid }) => {
|
|
706
774
|
const result = await apiRequest({
|
|
707
775
|
method: "GET",
|
|
708
|
-
path: `/api/v1/
|
|
776
|
+
path: `/api/v1/mockups/${mockup_uuid}`,
|
|
709
777
|
});
|
|
710
778
|
return {
|
|
711
779
|
content: [{
|
|
@@ -715,15 +783,15 @@ server.tool("get_psd_mockup", "Get full details of a PSD mockup: smart object UU
|
|
|
715
783
|
};
|
|
716
784
|
});
|
|
717
785
|
// ---------------------------------------------------------------------------
|
|
718
|
-
// Tool 3:
|
|
786
|
+
// Tool 3: update_mockup
|
|
719
787
|
// ---------------------------------------------------------------------------
|
|
720
|
-
server.tool("
|
|
721
|
-
mockup_uuid: z.string().describe("The UUID of the
|
|
788
|
+
server.tool("update_mockup", "Rename a mockup template.", {
|
|
789
|
+
mockup_uuid: z.string().describe("The UUID of the mockup to rename"),
|
|
722
790
|
name: z.string().describe("New display name for the mockup"),
|
|
723
791
|
}, async ({ mockup_uuid, name }) => {
|
|
724
792
|
const result = await apiRequest({
|
|
725
793
|
method: "PATCH",
|
|
726
|
-
path: `/api/v1/
|
|
794
|
+
path: `/api/v1/mockups/${mockup_uuid}`,
|
|
727
795
|
body: { name },
|
|
728
796
|
});
|
|
729
797
|
return {
|
|
@@ -734,20 +802,28 @@ server.tool("update_psd_mockup", "Rename a PSD mockup template.", {
|
|
|
734
802
|
};
|
|
735
803
|
});
|
|
736
804
|
// ---------------------------------------------------------------------------
|
|
737
|
-
// Tool 4:
|
|
805
|
+
// Tool 4: delete_mockup
|
|
738
806
|
// ---------------------------------------------------------------------------
|
|
739
|
-
server.tool("
|
|
740
|
-
mockup_uuid: z.string().describe("The UUID of the
|
|
807
|
+
server.tool("delete_mockup", "Permanently delete a mockup template. Cannot be undone.", {
|
|
808
|
+
mockup_uuid: z.string().describe("The UUID of the mockup to delete"),
|
|
741
809
|
}, async ({ mockup_uuid }) => {
|
|
742
810
|
await apiRequest({
|
|
743
811
|
method: "DELETE",
|
|
744
|
-
path: `/api/v1/
|
|
812
|
+
path: `/api/v1/mockups/${mockup_uuid}`,
|
|
745
813
|
});
|
|
746
814
|
return { content: [{ type: "text", text: `Mockup ${mockup_uuid} deleted successfully.` }] };
|
|
747
815
|
});
|
|
748
816
|
// ---------------------------------------------------------------------------
|
|
749
|
-
// Tool 5:
|
|
817
|
+
// Tool 5: render_mockup
|
|
750
818
|
// ---------------------------------------------------------------------------
|
|
819
|
+
// Every name the API takes for how artwork meets its area, on a smart object,
|
|
820
|
+
// on a photo mockup print area and on a video's still frame alike: 'fill',
|
|
821
|
+
// 'fit' and 'crop', and 'contain' and 'cover', the older names for 'fit' and
|
|
822
|
+
// 'crop', which it still accepts. A name is sent as given. On a smart object
|
|
823
|
+
// this package keeps 'fill' as its default when fit is left out; on a print
|
|
824
|
+
// area nothing is sent and the API's own default applies.
|
|
825
|
+
const FIT_MODES = ["fill", "fit", "crop", "contain", "cover"];
|
|
826
|
+
const SMART_OBJECT_FIT_HELP = "'fill' stretches the artwork to the bounds, 'fit' fits it inside keeping its proportions, 'crop' covers the area keeping its proportions and cuts the overflow. 'contain' and 'cover' are the older names for 'fit' and 'crop' and are still accepted. Default 'fill'.";
|
|
751
827
|
const smartObjectInputSchema = z
|
|
752
828
|
.object({
|
|
753
829
|
uuid: z
|
|
@@ -762,18 +838,21 @@ const smartObjectInputSchema = z
|
|
|
762
838
|
.enum(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"])
|
|
763
839
|
.optional()
|
|
764
840
|
.describe("MIME type for base64 artwork; defaults to image/png"),
|
|
765
|
-
fit: z
|
|
841
|
+
fit: z
|
|
842
|
+
.enum(FIT_MODES)
|
|
843
|
+
.default("fill")
|
|
844
|
+
.describe(`How the artwork meets the smart object area. ${SMART_OBJECT_FIT_HELP}`),
|
|
766
845
|
size: z
|
|
767
846
|
.object({
|
|
768
|
-
width: z.number().
|
|
769
|
-
height: z.number().
|
|
847
|
+
width: z.number().min(1).optional().describe("Artwork width in pixels, at least 1. Fractions are accepted."),
|
|
848
|
+
height: z.number().min(1).optional().describe("Artwork height in pixels, at least 1. Fractions are accepted."),
|
|
770
849
|
})
|
|
771
850
|
.optional()
|
|
772
851
|
.describe("Optional custom artwork size"),
|
|
773
852
|
position: z
|
|
774
853
|
.object({
|
|
775
|
-
top: z.number().
|
|
776
|
-
left: z.number().
|
|
854
|
+
top: z.number().optional().describe("Top offset in pixels. Fractions are accepted."),
|
|
855
|
+
left: z.number().optional().describe("Left offset in pixels. Fractions are accepted."),
|
|
777
856
|
})
|
|
778
857
|
.optional()
|
|
779
858
|
.describe("Optional custom artwork position"),
|
|
@@ -789,9 +868,20 @@ const smartObjectInputSchema = z
|
|
|
789
868
|
.optional(),
|
|
790
869
|
color: z
|
|
791
870
|
.object({
|
|
792
|
-
hex: z
|
|
871
|
+
hex: z
|
|
872
|
+
.string()
|
|
873
|
+
.regex(/^#[0-9a-fA-F]{6}$/)
|
|
874
|
+
.optional()
|
|
875
|
+
.describe("Color overlay as a six-digit hex code, e.g. '#FF5733'. Send this or label."),
|
|
876
|
+
label: z
|
|
877
|
+
.string()
|
|
878
|
+
.min(1)
|
|
879
|
+
.max(32)
|
|
880
|
+
.optional()
|
|
881
|
+
.describe("The name of a color saved on this mockup, e.g. 'blue jean', matched exactly. Send this instead of hex."),
|
|
793
882
|
blending_mode: z.string().default("normal").describe("Blend mode for the color overlay"),
|
|
794
883
|
})
|
|
884
|
+
.refine((color) => (color.hex === undefined) !== (color.label === undefined), "Provide exactly one of color.hex or color.label")
|
|
795
885
|
.optional(),
|
|
796
886
|
adjustment_layers: z
|
|
797
887
|
.object({
|
|
@@ -811,7 +901,7 @@ const textLayerInputSchema = z
|
|
|
811
901
|
uuid: z
|
|
812
902
|
.string()
|
|
813
903
|
.regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
|
|
814
|
-
.describe("UUID from
|
|
904
|
+
.describe("UUID from get_mockup_details text_layers"),
|
|
815
905
|
text: z.string().min(1).max(500).optional().describe("Replacement text for a single-style layer"),
|
|
816
906
|
segments: z
|
|
817
907
|
.array(z.object({
|
|
@@ -836,6 +926,10 @@ const textLayerInputSchema = z
|
|
|
836
926
|
.enum(["shrink", "clip", "overflow"])
|
|
837
927
|
.default("overflow")
|
|
838
928
|
.describe("Long-text handling for single-style point text; default overflow"),
|
|
929
|
+
vertical_align: z
|
|
930
|
+
.enum(["top", "bottom", "center"])
|
|
931
|
+
.optional()
|
|
932
|
+
.describe("Where text that fit 'shrink' scaled down sits in the original area: 'top' (the default), 'center' or 'bottom'. Single-style point text only."),
|
|
839
933
|
})
|
|
840
934
|
.superRefine((layer, ctx) => {
|
|
841
935
|
if ((layer.text === undefined) === (layer.segments === undefined)) {
|
|
@@ -858,22 +952,121 @@ const textLayerInputSchema = z
|
|
|
858
952
|
ctx.addIssue({ code: "custom", message: "Segment indexes must be unique" });
|
|
859
953
|
}
|
|
860
954
|
});
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
955
|
+
const LAYER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
956
|
+
const HEX_COLOR = /^#?[0-9a-fA-F]{6}$/;
|
|
957
|
+
/** One outline color, or up to eight front to back; null keeps an authored color. */
|
|
958
|
+
function outlineColors() {
|
|
959
|
+
return z.union([
|
|
960
|
+
z.string().regex(HEX_COLOR),
|
|
961
|
+
z.array(z.string().regex(HEX_COLOR).nullable()).min(1).max(8),
|
|
962
|
+
]);
|
|
963
|
+
}
|
|
964
|
+
// One text layer and one group written as flat arguments: the names the hosted
|
|
965
|
+
// server publishes on render_mockup. They become a single text_layers entry and
|
|
966
|
+
// a single group_layers entry, built by the same rules the hosted server builds
|
|
967
|
+
// them with (singleTextLayer and singleGroupLayer below), so the same flat
|
|
968
|
+
// arguments put the same text_layers and group_layers entries on the wire from
|
|
969
|
+
// either server.
|
|
970
|
+
const SINGLE_LAYER_SHORTCUT = {
|
|
971
|
+
text_layer_uuid: z
|
|
972
|
+
.string()
|
|
973
|
+
.regex(LAYER_UUID)
|
|
974
|
+
.optional()
|
|
975
|
+
.describe("UUID of one editable text layer from get_mockup_details text_layers. Send with text or text_segments; it becomes one text_layers entry, placed before any text_layers entries sent with it."),
|
|
976
|
+
text: z
|
|
977
|
+
.string()
|
|
978
|
+
.min(1)
|
|
979
|
+
.max(500)
|
|
980
|
+
.optional()
|
|
981
|
+
.describe("Replacement text for the text_layer_uuid layer when it has one style (segment_count 1). Use text_segments for a styled layer."),
|
|
982
|
+
text_segments: z
|
|
983
|
+
.array(z.object({
|
|
984
|
+
index: z.number().int().min(0).max(31).describe("Zero-based segment index"),
|
|
985
|
+
text: z.string().min(1).max(200).describe("Replacement text for this segment"),
|
|
986
|
+
}))
|
|
987
|
+
.min(1)
|
|
988
|
+
.max(32)
|
|
989
|
+
.optional()
|
|
990
|
+
.describe("For a styled text_layer_uuid layer (segment_count above 1): {index, text} entries. Listed segments are replaced, the rest keep their text, and each keeps its own styling. Used instead of text."),
|
|
991
|
+
text_font: z.string().max(255).optional().describe("Font UUID or PostScript name from list_fonts. Single-style layers only. Omit to keep the layer's font."),
|
|
992
|
+
text_font_size: z.number().positive().max(2000).optional().describe("Font size in pixels at the mockup's native resolution. Single-style layers only. Omit to keep the original size."),
|
|
993
|
+
text_color: z.string().regex(HEX_COLOR).optional().describe("Text color as a six-digit hex code, e.g. '#1A1A1A'. Single-style layers only. Omit to keep the original color."),
|
|
994
|
+
text_stroke_color: outlineColors()
|
|
995
|
+
.optional()
|
|
996
|
+
.describe("Colors for the text layer's own outlines: one hex code for the front outline, or up to eight front to back; null keeps an authored color. Single-style layers only."),
|
|
997
|
+
text_fit: z
|
|
998
|
+
.enum(["shrink", "clip", "overflow"])
|
|
999
|
+
.optional()
|
|
1000
|
+
.describe("Long-text handling for single-style point text: 'overflow' keeps the designed size, 'shrink' scales it down to fit, 'clip' cuts it at the last character that fits. Omit for 'overflow'."),
|
|
1001
|
+
text_vertical_align: z
|
|
1002
|
+
.enum(["top", "bottom", "center"])
|
|
1003
|
+
.optional()
|
|
1004
|
+
.describe("Where text that text_fit 'shrink' scaled down sits in the original area: 'top' (the default), 'center' or 'bottom'. Single-style layers only."),
|
|
1005
|
+
group_layer_uuid: z
|
|
1006
|
+
.string()
|
|
1007
|
+
.regex(LAYER_UUID)
|
|
1008
|
+
.optional()
|
|
1009
|
+
.describe("UUID of an editable group layer from the mockup's group_layers. Send with group_stroke_color."),
|
|
1010
|
+
group_stroke_color: outlineColors()
|
|
1011
|
+
.optional()
|
|
1012
|
+
.describe("Colors for that group's outlines: one hex code for the front outline, or up to eight front to back; null keeps an authored color. The change affects everything inside the group."),
|
|
1013
|
+
};
|
|
1014
|
+
/**
|
|
1015
|
+
* The text_layers entry the flat text arguments describe, or undefined when
|
|
1016
|
+
* they describe none. The hosted server's rules, one for one: the entry needs a
|
|
1017
|
+
* layer UUID and either segments or text; segments win when both arrive and
|
|
1018
|
+
* then travel without styling, because each segment keeps its own; a styling
|
|
1019
|
+
* argument left out is left off the wire.
|
|
1020
|
+
*/
|
|
1021
|
+
function singleTextLayer(args) {
|
|
1022
|
+
if (!args.text_layer_uuid || (!args.text_segments && !args.text))
|
|
1023
|
+
return undefined;
|
|
1024
|
+
const layer = { uuid: args.text_layer_uuid };
|
|
1025
|
+
if (args.text_segments) {
|
|
1026
|
+
layer.segments = args.text_segments;
|
|
1027
|
+
return layer;
|
|
1028
|
+
}
|
|
1029
|
+
layer.text = args.text;
|
|
1030
|
+
if (args.text_font)
|
|
1031
|
+
layer.font = args.text_font;
|
|
1032
|
+
if (args.text_font_size)
|
|
1033
|
+
layer.font_size = args.text_font_size;
|
|
1034
|
+
if (args.text_color)
|
|
1035
|
+
layer.color = args.text_color;
|
|
1036
|
+
if (args.text_stroke_color)
|
|
1037
|
+
layer.stroke_color = args.text_stroke_color;
|
|
1038
|
+
if (args.text_fit)
|
|
1039
|
+
layer.fit = args.text_fit;
|
|
1040
|
+
if (args.text_vertical_align)
|
|
1041
|
+
layer.vertical_align = args.text_vertical_align;
|
|
1042
|
+
return layer;
|
|
1043
|
+
}
|
|
1044
|
+
/** The group_layers entry the flat group arguments describe; both halves are needed. */
|
|
1045
|
+
function singleGroupLayer(args) {
|
|
1046
|
+
if (!args.group_layer_uuid || args.group_stroke_color === undefined)
|
|
1047
|
+
return undefined;
|
|
1048
|
+
return { uuid: args.group_layer_uuid, stroke_color: args.group_stroke_color };
|
|
1049
|
+
}
|
|
1050
|
+
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. Name what to fill with any of these, alone or together: smart_object_uuid + artwork_url for one smart object, smart_objects entries ({uuid, asset or color}) for one or more, text_layers entries ({uuid, text or segments}), or text_layer_uuid + text for one text layer. Sent together they are combined into one render: the smart_object_uuid entry comes before the smart_objects entries, and the text_layer_uuid entry before the text_layers entries. A call that names nothing to render returns an error. Returns the rendered image URL. Costs 1 credit. Use list_mockups and get_mockup_details to find target UUIDs.", {
|
|
1051
|
+
mockup_uuid: z.string().describe("UUID of the mockup template (from list_mockups)"),
|
|
1052
|
+
smart_object_uuid: z.string().optional().describe("UUID of one smart object layer (from get_mockup_details). Send with artwork_url, or leave both out and use smart_objects or text_layers. With smart_objects, this entry comes first."),
|
|
1053
|
+
artwork_url: z.string().optional().describe("Public artwork URL for smart_object_uuid. Send the two together, or leave both out and use smart_objects or text_layers."),
|
|
1054
|
+
// An empty list is accepted and counts as not sent, as on the hosted
|
|
1055
|
+
// server: smart_objects: [] beside the single pair renders the pair.
|
|
865
1056
|
smart_objects: z
|
|
866
1057
|
.array(smartObjectInputSchema)
|
|
867
|
-
.min(1)
|
|
868
1058
|
.optional()
|
|
869
|
-
.describe("
|
|
1059
|
+
.describe("Smart objects to fill, each with a uuid and asset or color. Works on its own; with smart_object_uuid these entries follow that one."),
|
|
870
1060
|
text_layers: z
|
|
871
1061
|
.array(textLayerInputSchema)
|
|
872
|
-
.min(1)
|
|
873
1062
|
.max(50)
|
|
874
1063
|
.optional()
|
|
875
|
-
.describe("
|
|
876
|
-
|
|
1064
|
+
.describe("Text layer overrides from get_mockup_details, each with a uuid and exactly one of text or segments. Works on its own; with text_layer_uuid these entries follow that one."),
|
|
1065
|
+
...SINGLE_LAYER_SHORTCUT,
|
|
1066
|
+
fit: z
|
|
1067
|
+
.enum(FIT_MODES)
|
|
1068
|
+
.default("fill")
|
|
1069
|
+
.describe(`How the singular artwork_url meets its smart object area. ${SMART_OBJECT_FIT_HELP}`),
|
|
877
1070
|
image_format: z.enum(["webp", "png", "jpg"]).default("webp").describe("Output format"),
|
|
878
1071
|
image_size: z.number().min(100).max(10000).default(2048).describe("Output width in pixels (default 2048)"),
|
|
879
1072
|
quality: z.number().min(1).max(100).default(90).describe("Compression quality for webp/jpg (default 90)"),
|
|
@@ -899,17 +1092,24 @@ server.tool("render_psd_mockup", "Render a PSD mockup with artwork, editable tex
|
|
|
899
1092
|
const hasSmartObjectUuid = args.smart_object_uuid !== undefined;
|
|
900
1093
|
const hasArtworkUrl = args.artwork_url !== undefined;
|
|
901
1094
|
if (hasSmartObjectUuid !== hasArtworkUrl) {
|
|
902
|
-
throw new Error("Provide
|
|
903
|
-
}
|
|
904
|
-
if (args.smart_objects && hasSmartObjectUuid) {
|
|
905
|
-
throw new Error("Provide smart_objects or smart_object_uuid/artwork_url, not both.");
|
|
1095
|
+
throw new Error("Provide smart_object_uuid and artwork_url together, or leave both out and use smart_objects or text_layers.");
|
|
906
1096
|
}
|
|
907
|
-
|
|
908
|
-
|
|
1097
|
+
const shortcutTextLayer = singleTextLayer(args);
|
|
1098
|
+
const shortcutGroupLayer = singleGroupLayer(args);
|
|
1099
|
+
// An empty list names nothing, so it counts as not sent.
|
|
1100
|
+
const listedSmartObjects = args.smart_objects ?? [];
|
|
1101
|
+
const listedTextLayers = args.text_layers ?? [];
|
|
1102
|
+
if (!listedSmartObjects.length &&
|
|
1103
|
+
!hasSmartObjectUuid &&
|
|
1104
|
+
!listedTextLayers.length &&
|
|
1105
|
+
!shortcutTextLayer &&
|
|
1106
|
+
!shortcutGroupLayer) {
|
|
1107
|
+
throw new Error("Nothing to render: provide smart_object_uuid with artwork_url, smart_objects, text_layers, text_layer_uuid with text or text_segments, or group_layer_uuid with group_stroke_color.");
|
|
909
1108
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1109
|
+
// The single pair and the lists are combined the way the hosted server
|
|
1110
|
+
// combines them: the single entry first, then every list entry as given.
|
|
1111
|
+
// An entry that names the same uuid twice reaches the API twice.
|
|
1112
|
+
const smartObjects = [];
|
|
913
1113
|
if (hasSmartObjectUuid && hasArtworkUrl) {
|
|
914
1114
|
const smartObject = {
|
|
915
1115
|
uuid: args.smart_object_uuid,
|
|
@@ -945,6 +1145,11 @@ server.tool("render_psd_mockup", "Render a PSD mockup with artwork, editable tex
|
|
|
945
1145
|
}
|
|
946
1146
|
smartObjects.push(smartObject);
|
|
947
1147
|
}
|
|
1148
|
+
smartObjects.push(...listedSmartObjects);
|
|
1149
|
+
const textLayers = [
|
|
1150
|
+
...(shortcutTextLayer ? [shortcutTextLayer] : []),
|
|
1151
|
+
...listedTextLayers,
|
|
1152
|
+
];
|
|
948
1153
|
const body = {
|
|
949
1154
|
mockup_uuid: args.mockup_uuid,
|
|
950
1155
|
export_options: {
|
|
@@ -956,8 +1161,10 @@ server.tool("render_psd_mockup", "Render a PSD mockup with artwork, editable tex
|
|
|
956
1161
|
};
|
|
957
1162
|
if (smartObjects.length)
|
|
958
1163
|
body.smart_objects = smartObjects;
|
|
959
|
-
if (
|
|
960
|
-
body.text_layers =
|
|
1164
|
+
if (textLayers.length)
|
|
1165
|
+
body.text_layers = textLayers;
|
|
1166
|
+
if (shortcutGroupLayer)
|
|
1167
|
+
body.group_layers = [shortcutGroupLayer];
|
|
961
1168
|
if (args.export_label) {
|
|
962
1169
|
body.export_label = args.export_label;
|
|
963
1170
|
}
|
|
@@ -1002,9 +1209,40 @@ server.tool("remove_background", "Remove the background from any image and retur
|
|
|
1002
1209
|
};
|
|
1003
1210
|
});
|
|
1004
1211
|
// ---------------------------------------------------------------------------
|
|
1005
|
-
// Tool:
|
|
1212
|
+
// Tool: list_fonts
|
|
1006
1213
|
// ---------------------------------------------------------------------------
|
|
1007
|
-
server.tool("
|
|
1214
|
+
server.tool("list_fonts", "List fonts available for text layers: the shared system catalog plus any fonts you have uploaded. Filter by family name (search), by category (serif, sans-serif, display), or by scope ('all', 'system', or 'custom' for your uploads only); results are paginated (per_page up to 100). Use a font's uuid or postscript_name as the font of a text_layers entry when rendering with render_mockup. Costs 0 credits.", {
|
|
1215
|
+
search: z.string().optional().describe("Filter by family name (case-insensitive contains), e.g. 'sans'"),
|
|
1216
|
+
category: z.string().optional().describe("Filter by category, e.g. 'serif', 'sans-serif', 'display'"),
|
|
1217
|
+
scope: z
|
|
1218
|
+
.enum(["all", "system", "custom"])
|
|
1219
|
+
.default("all")
|
|
1220
|
+
.describe("Which fonts to return: 'all' (system fonts plus your uploads, the default), 'system', or 'custom' (your uploads only)"),
|
|
1221
|
+
page: z.number().int().min(1).default(1).describe("Page number (1-based, default 1)"),
|
|
1222
|
+
per_page: z.number().int().min(1).max(100).default(50).describe("Results per page (1-100, default 50)"),
|
|
1223
|
+
}, async ({ search, category, scope, page, per_page }) => {
|
|
1224
|
+
const result = await apiRequest({
|
|
1225
|
+
method: "GET",
|
|
1226
|
+
path: "/api/v1/fonts",
|
|
1227
|
+
params: {
|
|
1228
|
+
page,
|
|
1229
|
+
per_page,
|
|
1230
|
+
scope,
|
|
1231
|
+
search: search || undefined,
|
|
1232
|
+
category: category || undefined,
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
return {
|
|
1236
|
+
content: [{
|
|
1237
|
+
type: "text",
|
|
1238
|
+
text: JSON.stringify(publicFontList(result), null, 2),
|
|
1239
|
+
}],
|
|
1240
|
+
};
|
|
1241
|
+
});
|
|
1242
|
+
// ---------------------------------------------------------------------------
|
|
1243
|
+
// Tool: create_2d_mockup
|
|
1244
|
+
// ---------------------------------------------------------------------------
|
|
1245
|
+
server.tool("create_2d_mockup", "Create a reusable photo 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.", {
|
|
1008
1246
|
source_url: z.string().describe("Public HTTPS URL of the product image"),
|
|
1009
1247
|
name: z.string().optional().describe("Optional display name for the photo mockup"),
|
|
1010
1248
|
idempotency_key: z.string().min(1).max(255).optional().describe("Optional retry-stable key for this create request"),
|
|
@@ -1020,7 +1258,7 @@ server.tool("create_photo_mockup", "Create a reusable photo mockup from a public
|
|
|
1020
1258
|
body.is_async = true;
|
|
1021
1259
|
const result = (await apiRequest({
|
|
1022
1260
|
method: "POST",
|
|
1023
|
-
path: "/api/v1/
|
|
1261
|
+
path: "/api/v1/sudoai/2d-mockups",
|
|
1024
1262
|
body,
|
|
1025
1263
|
headers: { "Idempotency-Key": idempotency_key ?? randomUUID() },
|
|
1026
1264
|
}));
|
|
@@ -1054,16 +1292,15 @@ server.tool("create_photo_mockup", "Create a reusable photo mockup from a public
|
|
|
1054
1292
|
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
1055
1293
|
});
|
|
1056
1294
|
// ---------------------------------------------------------------------------
|
|
1057
|
-
//
|
|
1295
|
+
// 2D render: one tool per kind of target
|
|
1058
1296
|
// ---------------------------------------------------------------------------
|
|
1059
|
-
// A render names one target and nothing else
|
|
1060
|
-
//
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1063
|
-
//
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
mockup_id: z.string().describe("UUID of the photo mockup (mockup_id from list_photo_mockups, get_photo_mockup or create_photo_mockup)."),
|
|
1297
|
+
// A render names one target and nothing else. A product surface takes a
|
|
1298
|
+
// coverage percentage; a print area takes a fit or an explicit box. Two tools
|
|
1299
|
+
// rather than one, because those are two different dials and a single tool
|
|
1300
|
+
// could only describe in prose what the shape can state outright: whichever
|
|
1301
|
+
// tool the caller reaches for, every field on it applies.
|
|
1302
|
+
const TWO_D_SHARED = {
|
|
1303
|
+
mockup_uuid: z.string().describe("UUID of the photo mockup template (from list_2d_mockups, returned as mockup_id)."),
|
|
1067
1304
|
artwork_url: z.string().describe("Public URL of the artwork image (PNG/JPG/WebP) to place on the mockup"),
|
|
1068
1305
|
remove_background: z.boolean().default(false).describe("Remove the artwork's background before placing it. Adds 25 credits per artwork."),
|
|
1069
1306
|
opacity: z.number().min(0).max(100).default(100).describe("Artwork opacity percentage (0-100)"),
|
|
@@ -1107,82 +1344,74 @@ const PHOTO_RENDER_SHARED = {
|
|
|
1107
1344
|
is_async: z
|
|
1108
1345
|
.boolean()
|
|
1109
1346
|
.default(false)
|
|
1110
|
-
.describe("Queue the render instead of waiting. When true the API returns 202 with a job_id immediately
|
|
1347
|
+
.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."),
|
|
1111
1348
|
};
|
|
1112
|
-
// The
|
|
1349
|
+
// The option a caller can write on one of these two tools but not the other,
|
|
1350
|
+
// and the sentence that sends them to the one that works.
|
|
1351
|
+
//
|
|
1352
|
+
// An argument a tool never declared is dropped before the handler ever sees it,
|
|
1353
|
+
// so `coverage` on a print area, `fit` on a surface, and the retired `scale` on
|
|
1354
|
+
// either used to come back as a successful render with the caller's option
|
|
1355
|
+
// gone and nothing said about it. The API answers all three with a 422. The
|
|
1356
|
+
// consumer on this side is a language model: an option it wrote disappearing in
|
|
1357
|
+
// silence teaches it nothing and it writes the same option again, while one
|
|
1358
|
+
// named sentence teaches it in a single turn.
|
|
1113
1359
|
//
|
|
1114
|
-
// The
|
|
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
|
|
1360
|
+
// The wording is the API's own, so the caller reads the same reason whichever
|
|
1121
1361
|
// 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.";
|
|
1125
1362
|
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.";
|
|
1363
|
+
const TWO_D_REFUSED_OPTIONS = {
|
|
1364
|
+
surface: {
|
|
1365
|
+
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.",
|
|
1366
|
+
scale: RETIRED_SCALE,
|
|
1367
|
+
},
|
|
1368
|
+
print_area: {
|
|
1369
|
+
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.",
|
|
1370
|
+
scale: RETIRED_SCALE,
|
|
1371
|
+
},
|
|
1372
|
+
};
|
|
1126
1373
|
/**
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1374
|
+
* Build the arguments one 2D render tool accepts, and refuse the rest by name.
|
|
1375
|
+
*
|
|
1376
|
+
* Strict on purpose, and only on these two tools: the pair exists precisely
|
|
1377
|
+
* because the sizing options are split between them, so an option landing on
|
|
1378
|
+
* the wrong one of the two is the mistake most worth catching. Refusing is
|
|
1379
|
+
* keyed on the argument being written rather than on the value it holds, so an
|
|
1380
|
+
* explicitly written null is caught as well: writing it is the caller naming
|
|
1381
|
+
* the option.
|
|
1382
|
+
*
|
|
1383
|
+
* An option with no sentence of its own still gets refused rather than dropped.
|
|
1384
|
+
* A name we never published is either a typo or an option the caller believes
|
|
1385
|
+
* exists, and both are worth one line of reply.
|
|
1132
1386
|
*/
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
});
|
|
1387
|
+
function twoDRenderInput(kind, shape) {
|
|
1388
|
+
const refused = TWO_D_REFUSED_OPTIONS[kind];
|
|
1389
|
+
return z.strictObject(shape, {
|
|
1390
|
+
error: (issue) => {
|
|
1391
|
+
if (issue.code !== "unrecognized_keys")
|
|
1392
|
+
return undefined;
|
|
1393
|
+
return issue.keys
|
|
1394
|
+
.map((key) => refused[key] ??
|
|
1395
|
+
`${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.`)
|
|
1396
|
+
.join(" ");
|
|
1397
|
+
},
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1179
1400
|
/**
|
|
1180
1401
|
* Refuse a placement that answers the sizing question more than once, or half.
|
|
1181
1402
|
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1403
|
+
* The API refuses these too, and refusing them here as well is deliberate. A
|
|
1404
|
+
* client that can put a shape on the wire the API is guaranteed to reject has
|
|
1405
|
+
* spent the caller's round trip telling them something it already knew.
|
|
1406
|
+
*
|
|
1407
|
+
* This is not the thing that was taken out of here. A default invents a value
|
|
1408
|
+
* the caller never typed and quietly changes the render when it drifts from
|
|
1409
|
+
* the renderer's; a refusal invents nothing, and if it ever drifts it drifts
|
|
1410
|
+
* into refusing out loud, with the API still refusing behind it. It also
|
|
1411
|
+
* cannot drift unnoticed: the contract test reads the shared placement wire
|
|
1412
|
+
* fixture and asserts every shape listed there as rejected is still
|
|
1413
|
+
* unreachable from every tool, and the wording below is the wording the API
|
|
1414
|
+
* answers with, so a caller reads the same reason from either side.
|
|
1186
1415
|
*
|
|
1187
1416
|
* `relativeSizing` is whichever proportional option belongs to this kind of
|
|
1188
1417
|
* target -- `coverage` on a surface, `fit` on a print area.
|
|
@@ -1200,15 +1429,16 @@ function assertOneSizingAnswer(placement, relativeSizing) {
|
|
|
1200
1429
|
/**
|
|
1201
1430
|
* Send one render and hand back what the caller can act on.
|
|
1202
1431
|
*
|
|
1203
|
-
* `target` carries the address field, `placement` the
|
|
1204
|
-
*
|
|
1205
|
-
*
|
|
1432
|
+
* `target` carries the address field, `placement` the dials that belong to that
|
|
1433
|
+
* kind of target. Neither is inspected here: the dials a caller may turn are
|
|
1434
|
+
* settled by which tool they called, so there is nothing left to check.
|
|
1206
1435
|
*/
|
|
1207
|
-
async function
|
|
1208
|
-
// Anchoring belongs to both kinds of target; only sizing is split
|
|
1209
|
-
// caller
|
|
1210
|
-
//
|
|
1211
|
-
//
|
|
1436
|
+
async function renderTwoD(args, target, placement) {
|
|
1437
|
+
// Anchoring belongs to both kinds of target; only sizing is split, and the
|
|
1438
|
+
// caller's tool already settled which sizing options exist. What the caller
|
|
1439
|
+
// did not name is absent, and a placement nobody touched is absent entirely
|
|
1440
|
+
// rather than sent as an empty object -- an empty object still claims the
|
|
1441
|
+
// caller reached for placement.
|
|
1212
1442
|
const sent = {
|
|
1213
1443
|
...(args.position === undefined ? {} : { position: args.position }),
|
|
1214
1444
|
...(args.offset_x === undefined ? {} : { offset_x: args.offset_x }),
|
|
@@ -1223,6 +1453,7 @@ async function renderPhotoMockup(args, target, placement) {
|
|
|
1223
1453
|
artwork_url: args.artwork_url,
|
|
1224
1454
|
...(args.remove_background ? { remove_background: true } : {}),
|
|
1225
1455
|
adjustments: {
|
|
1456
|
+
...(args.blend_mode === undefined ? {} : { blend_mode: args.blend_mode }),
|
|
1226
1457
|
opacity: args.opacity,
|
|
1227
1458
|
brightness: args.brightness,
|
|
1228
1459
|
contrast: args.contrast,
|
|
@@ -1241,13 +1472,13 @@ async function renderPhotoMockup(args, target, placement) {
|
|
|
1241
1472
|
body.is_async = true;
|
|
1242
1473
|
const result = await apiRequest({
|
|
1243
1474
|
method: "POST",
|
|
1244
|
-
path: `/api/v1/
|
|
1475
|
+
path: `/api/v1/sudoai/2d-mockups/${args.mockup_uuid}/render`,
|
|
1245
1476
|
body,
|
|
1246
1477
|
timeout: RENDER_TIMEOUT,
|
|
1247
1478
|
});
|
|
1248
|
-
// is_async=true -> 202 + job_id (kind "
|
|
1249
|
-
// hand back the poll contract.
|
|
1250
|
-
// terminal job (result_url).
|
|
1479
|
+
// is_async=true -> 202 + job_id (kind "2d_render" on this path; the family
|
|
1480
|
+
// spelling is "photo_mockup_render"); hand back the poll contract.
|
|
1481
|
+
// Reuse get_job / wait_for_job to reach the terminal job (result_url).
|
|
1251
1482
|
if (args.is_async) {
|
|
1252
1483
|
return { content: [{ type: "text", text: formatJobAccepted(result) }] };
|
|
1253
1484
|
}
|
|
@@ -1267,23 +1498,71 @@ async function renderPhotoMockup(args, target, placement) {
|
|
|
1267
1498
|
};
|
|
1268
1499
|
return { content: [{ type: "text", text: JSON.stringify(rendered, null, 2) }] };
|
|
1269
1500
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1501
|
+
// ---------------------------------------------------------------------------
|
|
1502
|
+
// Tool: render_2d_surface
|
|
1503
|
+
// ---------------------------------------------------------------------------
|
|
1504
|
+
server.registerTool("render_2d_surface", {
|
|
1505
|
+
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.",
|
|
1506
|
+
inputSchema: twoDRenderInput("surface", {
|
|
1507
|
+
...TWO_D_SHARED,
|
|
1508
|
+
surface_uuid: z.string().describe("UUID of a product surface from get_2d_mockup's surfaces[]."),
|
|
1509
|
+
coverage: z
|
|
1510
|
+
.number()
|
|
1511
|
+
.min(10)
|
|
1512
|
+
.max(100)
|
|
1513
|
+
.optional()
|
|
1514
|
+
.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."),
|
|
1515
|
+
// A percentage cannot express a box whose proportions differ from the
|
|
1516
|
+
// surface's, which is exactly what an artwork resized on a canvas is.
|
|
1517
|
+
width: z
|
|
1518
|
+
.number()
|
|
1519
|
+
.min(1)
|
|
1520
|
+
.max(30000)
|
|
1521
|
+
.optional()
|
|
1522
|
+
.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."),
|
|
1523
|
+
height: z
|
|
1524
|
+
.number()
|
|
1525
|
+
.min(1)
|
|
1526
|
+
.max(30000)
|
|
1527
|
+
.optional()
|
|
1528
|
+
.describe("Artwork height in pixels. Send together with width, and without coverage. The two axes are independent, so any aspect ratio is allowed."),
|
|
1529
|
+
}),
|
|
1273
1530
|
}, 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);
|
|
1285
1531
|
const sizing = {
|
|
1286
1532
|
...(args.coverage === undefined ? {} : { coverage: args.coverage }),
|
|
1533
|
+
...(args.width === undefined ? {} : { width: args.width }),
|
|
1534
|
+
...(args.height === undefined ? {} : { height: args.height }),
|
|
1535
|
+
};
|
|
1536
|
+
assertOneSizingAnswer(sizing, "coverage");
|
|
1537
|
+
return renderTwoD(args, { surface_uuid: args.surface_uuid }, sizing);
|
|
1538
|
+
});
|
|
1539
|
+
// ---------------------------------------------------------------------------
|
|
1540
|
+
// Tool: render_2d_print_area
|
|
1541
|
+
// ---------------------------------------------------------------------------
|
|
1542
|
+
server.registerTool("render_2d_print_area", {
|
|
1543
|
+
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.",
|
|
1544
|
+
inputSchema: twoDRenderInput("print_area", {
|
|
1545
|
+
...TWO_D_SHARED,
|
|
1546
|
+
print_area_uuid: z.string().describe("UUID of a saved print area from get_2d_mockup's print_areas[] (its print_area_id)."),
|
|
1547
|
+
fit: z
|
|
1548
|
+
.enum(FIT_MODES)
|
|
1549
|
+
.optional()
|
|
1550
|
+
.describe("How the artwork meets the print area, which it always fills edge to edge: 'fit' keeps the proportions and fits inside (the default), 'fill' stretches to the edges, 'crop' fills and crops the overflow. 'contain' and 'cover' are the older names for 'fit' and 'crop' and are still accepted. Leave it out to get 'fit'. To sit inside the area with room around it, send width and height instead."),
|
|
1551
|
+
width: z
|
|
1552
|
+
.number()
|
|
1553
|
+
.min(1)
|
|
1554
|
+
.max(30000)
|
|
1555
|
+
.optional()
|
|
1556
|
+
.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."),
|
|
1557
|
+
height: z
|
|
1558
|
+
.number()
|
|
1559
|
+
.min(1)
|
|
1560
|
+
.max(30000)
|
|
1561
|
+
.optional()
|
|
1562
|
+
.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."),
|
|
1563
|
+
}),
|
|
1564
|
+
}, async (args) => {
|
|
1565
|
+
const sizing = {
|
|
1287
1566
|
...(args.fit === undefined ? {} : { fit: args.fit }),
|
|
1288
1567
|
...(args.width === undefined ? {} : { width: args.width }),
|
|
1289
1568
|
...(args.height === undefined ? {} : { height: args.height }),
|
|
@@ -1292,20 +1571,20 @@ server.registerTool("render_photo_mockup", {
|
|
|
1292
1571
|
// the half instead would render a size the caller never asked for with no
|
|
1293
1572
|
// error to notice, and forwarding it spends a round trip to be told what
|
|
1294
1573
|
// the shape already said.
|
|
1295
|
-
assertOneSizingAnswer(sizing,
|
|
1296
|
-
return
|
|
1574
|
+
assertOneSizingAnswer(sizing, "fit");
|
|
1575
|
+
return renderTwoD(args, { uuid: args.print_area_uuid }, sizing);
|
|
1297
1576
|
});
|
|
1298
1577
|
// ---------------------------------------------------------------------------
|
|
1299
|
-
// Tool:
|
|
1578
|
+
// Tool: list_2d_mockups
|
|
1300
1579
|
// ---------------------------------------------------------------------------
|
|
1301
|
-
server.tool("
|
|
1580
|
+
server.tool("list_2d_mockups", "List your saved photo 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.", {
|
|
1302
1581
|
limit: z.number().min(1).max(100).default(20).describe("Results per page (1-100, default 20)"),
|
|
1303
1582
|
offset: z.number().min(0).default(0).describe("Pagination offset (default 0)"),
|
|
1304
1583
|
customizable_only: z.boolean().default(false).describe("Return only mockups ready for shopper customization"),
|
|
1305
1584
|
}, async ({ limit, offset, customizable_only }) => {
|
|
1306
1585
|
const result = await apiRequest({
|
|
1307
1586
|
method: "GET",
|
|
1308
|
-
path: "/api/v1/
|
|
1587
|
+
path: "/api/v1/sudoai/2d-mockups",
|
|
1309
1588
|
params: { limit, offset, customizable_only },
|
|
1310
1589
|
});
|
|
1311
1590
|
return {
|
|
@@ -1316,14 +1595,14 @@ server.tool("list_photo_mockups", "List your saved photo mockup templates, made
|
|
|
1316
1595
|
};
|
|
1317
1596
|
});
|
|
1318
1597
|
// ---------------------------------------------------------------------------
|
|
1319
|
-
// Tool:
|
|
1598
|
+
// Tool: get_2d_mockup
|
|
1320
1599
|
// ---------------------------------------------------------------------------
|
|
1321
|
-
server.tool("
|
|
1322
|
-
mockup_id: z.string().describe("UUID of the photo mockup (mockup_id from
|
|
1600
|
+
server.tool("get_2d_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 to render_2d_print_area, or a surfaces[].surface_uuid to render_2d_surface. Costs 0 credits.", {
|
|
1601
|
+
mockup_id: z.string().describe("UUID of the photo mockup (mockup_id from list_2d_mockups)"),
|
|
1323
1602
|
}, async ({ mockup_id }) => {
|
|
1324
1603
|
const result = await apiRequest({
|
|
1325
1604
|
method: "GET",
|
|
1326
|
-
path: `/api/v1/
|
|
1605
|
+
path: `/api/v1/sudoai/2d-mockups/${mockup_id}`,
|
|
1327
1606
|
});
|
|
1328
1607
|
return {
|
|
1329
1608
|
content: [{
|
|
@@ -1333,9 +1612,9 @@ server.tool("get_photo_mockup", "Get one photo mockup's full details: the saved
|
|
|
1333
1612
|
};
|
|
1334
1613
|
});
|
|
1335
1614
|
// ---------------------------------------------------------------------------
|
|
1336
|
-
// Tool:
|
|
1615
|
+
// Tool: update_2d_print_areas
|
|
1337
1616
|
// ---------------------------------------------------------------------------
|
|
1338
|
-
server.tool("
|
|
1617
|
+
server.tool("update_2d_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
1618
|
mockup_id: z.string().describe("UUID of the photo mockup to update"),
|
|
1340
1619
|
print_areas: z
|
|
1341
1620
|
.array(z.object({
|
|
@@ -1351,7 +1630,7 @@ server.tool("update_photo_mockup_print_areas", "Replace a photo mockup's print a
|
|
|
1351
1630
|
}, async ({ mockup_id, print_areas }) => {
|
|
1352
1631
|
const result = await apiRequest({
|
|
1353
1632
|
method: "PUT",
|
|
1354
|
-
path: `/api/v1/
|
|
1633
|
+
path: `/api/v1/sudoai/2d-mockups/${mockup_id}/print-areas`,
|
|
1355
1634
|
body: { print_areas },
|
|
1356
1635
|
});
|
|
1357
1636
|
const envelope = asRecord(result);
|
|
@@ -1368,14 +1647,14 @@ server.tool("update_photo_mockup_print_areas", "Replace a photo mockup's print a
|
|
|
1368
1647
|
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
1369
1648
|
});
|
|
1370
1649
|
// ---------------------------------------------------------------------------
|
|
1371
|
-
// Tool:
|
|
1650
|
+
// Tool: delete_2d_mockup
|
|
1372
1651
|
// ---------------------------------------------------------------------------
|
|
1373
|
-
server.tool("
|
|
1374
|
-
mockup_id: z.string().describe("UUID of the photo mockup to delete (mockup_id from
|
|
1652
|
+
server.tool("delete_2d_mockup", "Permanently delete a photo mockup template and all of its data. Cannot be undone. Costs 0 credits.", {
|
|
1653
|
+
mockup_id: z.string().describe("UUID of the photo mockup to delete (mockup_id from list_2d_mockups)"),
|
|
1375
1654
|
}, async ({ mockup_id }) => {
|
|
1376
1655
|
const result = await apiRequest({
|
|
1377
1656
|
method: "DELETE",
|
|
1378
|
-
path: `/api/v1/
|
|
1657
|
+
path: `/api/v1/sudoai/2d-mockups/${mockup_id}`,
|
|
1379
1658
|
});
|
|
1380
1659
|
const envelope = asRecord(result);
|
|
1381
1660
|
const data = asRecord(envelope.data ?? result);
|
|
@@ -1423,7 +1702,7 @@ server.tool("upload_psd", "Upload a Photoshop PSD/PSB file as a new mockup templ
|
|
|
1423
1702
|
// ---------------------------------------------------------------------------
|
|
1424
1703
|
// Tool: get_job
|
|
1425
1704
|
// ---------------------------------------------------------------------------
|
|
1426
|
-
server.tool("get_job", "Get the current status of any async render, video, upload, or photo mockup
|
|
1705
|
+
server.tool("get_job", "Get the current status of any async render, video, upload, or photo mockup 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.", {
|
|
1427
1706
|
job_id: z.string().describe("The job_id returned by any async submission"),
|
|
1428
1707
|
}, async ({ job_id }) => {
|
|
1429
1708
|
const result = await getJob(job_id);
|
|
@@ -1432,7 +1711,7 @@ server.tool("get_job", "Get the current status of any async render, video, uploa
|
|
|
1432
1711
|
// ---------------------------------------------------------------------------
|
|
1433
1712
|
// Tool: list_jobs
|
|
1434
1713
|
// ---------------------------------------------------------------------------
|
|
1435
|
-
server.tool("list_jobs", "List your async jobs, including PSD renders, videos, uploads, and photo mockup
|
|
1714
|
+
server.tool("list_jobs", "List your async jobs, including PSD renders, videos, uploads, and photo mockup creation and 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
1715
|
kind: z
|
|
1437
1716
|
.enum(["video", "render", "upload", "2d_create", "2d_render", "photo_mockup_create", "photo_mockup_render"])
|
|
1438
1717
|
.optional()
|
|
@@ -1455,7 +1734,7 @@ server.tool("list_jobs", "List your async jobs, including PSD renders, videos, u
|
|
|
1455
1734
|
// ---------------------------------------------------------------------------
|
|
1456
1735
|
// Tool: wait_for_job
|
|
1457
1736
|
// ---------------------------------------------------------------------------
|
|
1458
|
-
server.tool("wait_for_job", "Poll any async render, video, upload, or photo mockup
|
|
1737
|
+
server.tool("wait_for_job", "Poll any async render, video, upload, or photo mockup job until it succeeds or fails, then return the final result and credits charged. Blocks while polling.", {
|
|
1459
1738
|
job_id: z.string().describe("The job_id to wait on (from an async submission or render_video)"),
|
|
1460
1739
|
poll_interval_seconds: z
|
|
1461
1740
|
.number()
|
|
@@ -1493,13 +1772,13 @@ server.tool("wait_for_job", "Poll any async render, video, upload, or photo mock
|
|
|
1493
1772
|
// Tool: render_video
|
|
1494
1773
|
// ---------------------------------------------------------------------------
|
|
1495
1774
|
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.", {
|
|
1496
|
-
mockup_uuid: z.string().optional().describe("RENDER MODE: UUID of the
|
|
1497
|
-
smart_object_uuid: z.string().optional().describe("RENDER MODE: UUID of the smart object layer to place artwork on (from
|
|
1775
|
+
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')."),
|
|
1776
|
+
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."),
|
|
1498
1777
|
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."),
|
|
1499
1778
|
artwork_base64: z.string().optional().describe("RENDER MODE: raw base64-encoded artwork bytes (no data: prefix). Provide this OR artwork_url."),
|
|
1500
1779
|
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)."),
|
|
1501
1780
|
image_url: z.string().optional().describe("RAW-IMAGE MODE: a public https png/jpg URL to animate without a mockup. Supply this OR (mockup_uuid + smart_object_uuid + artwork), never both."),
|
|
1502
|
-
fit: z.enum(
|
|
1781
|
+
fit: z.enum(FIT_MODES).default("fill").describe(`RENDER MODE: how the artwork meets the smart object area in the still frame. ${SMART_OBJECT_FIT_HELP}`),
|
|
1503
1782
|
asset_width: z.number().int().min(1).optional().describe("RENDER MODE: custom artwork width in pixels (overrides fit sizing)."),
|
|
1504
1783
|
asset_height: z.number().int().min(1).optional().describe("RENDER MODE: custom artwork height in pixels (overrides fit sizing)."),
|
|
1505
1784
|
asset_top: z.number().int().optional().describe("RENDER MODE: artwork top offset in pixels within the smart object area."),
|
|
@@ -1519,6 +1798,10 @@ server.tool("render_video", "Create a short AI video from either a mockup with a
|
|
|
1519
1798
|
.enum(["ambient", "showcase"])
|
|
1520
1799
|
.default("ambient")
|
|
1521
1800
|
.describe("'ambient' = subtle looping hero motion that keeps the print readable; 'showcase' = one deliberate camera/product move."),
|
|
1801
|
+
advanced_model: z
|
|
1802
|
+
.string()
|
|
1803
|
+
.optional()
|
|
1804
|
+
.describe("Advanced override for the automatic quality selection. Unsupported values are rejected. Omit for automatic selection (recommended)."),
|
|
1522
1805
|
image_format: z.enum(["webp", "png", "jpg"]).default("webp").describe("Output format of the still input frame"),
|
|
1523
1806
|
image_size: z.number().min(100).max(10000).default(2048).describe("Width in pixels of the still input frame (default 2048)"),
|
|
1524
1807
|
quality: z.number().min(1).max(100).default(90).describe("Compression quality for the still input frame (webp/jpg, default 90)"),
|
|
@@ -1532,6 +1815,8 @@ server.tool("render_video", "Create a short AI video from either a mockup with a
|
|
|
1532
1815
|
audio: args.audio,
|
|
1533
1816
|
motion: args.motion,
|
|
1534
1817
|
};
|
|
1818
|
+
if (args.advanced_model)
|
|
1819
|
+
video.advanced_model = args.advanced_model;
|
|
1535
1820
|
const body = {
|
|
1536
1821
|
export_options: {
|
|
1537
1822
|
image_format: args.image_format,
|
|
@@ -1581,7 +1866,7 @@ server.tool("render_video", "Create a short AI video from either a mockup with a
|
|
|
1581
1866
|
// ---------------------------------------------------------------------------
|
|
1582
1867
|
// Tool 8: get_account
|
|
1583
1868
|
// ---------------------------------------------------------------------------
|
|
1584
|
-
server.tool("get_account", "Get your account info: subscription plan, remaining credits, prepaid balance, usage stats, billing period, and API key details. An account is funded by a subscription allowance or by a prepaid balance, so read both: an account paying as it goes reports 0 credits and pays from its balance. Quote funding_summary rather than reading the credits fields alone.", {}, async () => {
|
|
1869
|
+
server.tool("get_account", "Get your account info: the organization the API key belongs to, subscription plan, remaining credits, prepaid balance, usage stats, billing period, and API key details. The plan, credits and balance belong to that organization, so name it when you report them. An account is funded by a subscription allowance or by a prepaid balance, so read both: an account paying as it goes reports 0 credits and pays from its balance. Quote funding_summary rather than reading the credits fields alone.", {}, async () => {
|
|
1585
1870
|
const result = await apiRequest({
|
|
1586
1871
|
method: "GET",
|
|
1587
1872
|
path: "/api/v1/me",
|
|
@@ -1614,10 +1899,11 @@ server.tool("get_account", "Get your account info: subscription plan, remaining
|
|
|
1614
1899
|
// 2d_render.failed). Both spellings are subscribable; which one a delivery
|
|
1615
1900
|
// carries is the endpoint's `event_naming` pin ('current' = family names,
|
|
1616
1901
|
// 'legacy' = 2d_* names), and the payload's `kind` follows the same pin.
|
|
1617
|
-
// Endpoints created before the pin existed are 'legacy'
|
|
1618
|
-
//
|
|
1619
|
-
//
|
|
1620
|
-
//
|
|
1902
|
+
// Endpoints created before the pin existed are 'legacy'. A create without a pin
|
|
1903
|
+
// takes 'current' only when event_types names photo mockup events by their
|
|
1904
|
+
// family names alone, and 'legacy' otherwise. The pin is set at create and
|
|
1905
|
+
// changed at update; a re-pin sent alone re-spells the stored subscription list
|
|
1906
|
+
// to match. The list below is in the API's own order.
|
|
1621
1907
|
// ---------------------------------------------------------------------------
|
|
1622
1908
|
const WEBHOOK_EVENT_TYPES = [
|
|
1623
1909
|
"render.succeeded",
|
|
@@ -1647,7 +1933,7 @@ server.tool("create_webhook_endpoint", "Register a webhook endpoint that SudoMoc
|
|
|
1647
1933
|
event_naming: z
|
|
1648
1934
|
.enum(WEBHOOK_EVENT_NAMINGS)
|
|
1649
1935
|
.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
|
|
1936
|
+
.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 let event_types decide: 'current' when it names photo mockup events by their family names only, otherwise 'legacy' (an empty list, a list of events spelled the same either way, or any legacy name). Pick 'legacy' only for a receiver written against the old names."),
|
|
1651
1937
|
description: z.string().max(255).optional().describe("Optional human-readable label for this endpoint"),
|
|
1652
1938
|
}, async ({ url, event_types, event_naming, description }) => {
|
|
1653
1939
|
const body = { url, event_types };
|
|
@@ -1780,6 +2066,20 @@ server.tool("replay_webhook_delivery", "Replay a single webhook delivery while p
|
|
|
1780
2066
|
}],
|
|
1781
2067
|
};
|
|
1782
2068
|
});
|
|
2069
|
+
server.tool("replay_failed_webhook_deliveries", "Re-send every failed or dead delivery for a webhook endpoint in one call, e.g. after fixing your endpoint. Returns status and count: how many deliveries were queued to be sent again. To re-send a single delivery, use replay_webhook_delivery.", {
|
|
2070
|
+
endpoint_id: z.string().describe("The id of the webhook endpoint whose failed deliveries are all sent again (from list_webhook_endpoints)"),
|
|
2071
|
+
}, async ({ endpoint_id }) => {
|
|
2072
|
+
const result = await apiRequest({
|
|
2073
|
+
method: "POST",
|
|
2074
|
+
path: `/api/v1/webhook-endpoints/${endpoint_id}/deliveries/replay-failed`,
|
|
2075
|
+
});
|
|
2076
|
+
return {
|
|
2077
|
+
content: [{
|
|
2078
|
+
type: "text",
|
|
2079
|
+
text: JSON.stringify(publicWebhookBulkReplay(result), null, 2),
|
|
2080
|
+
}],
|
|
2081
|
+
};
|
|
2082
|
+
});
|
|
1783
2083
|
// ---------------------------------------------------------------------------
|
|
1784
2084
|
// Local file upload
|
|
1785
2085
|
//
|
|
@@ -1887,6 +2187,242 @@ server.tool("upload_local_file", "Upload a file from this machine and get back a
|
|
|
1887
2187
|
}],
|
|
1888
2188
|
};
|
|
1889
2189
|
});
|
|
2190
|
+
// The same signing call as upload_local_file, with the upload itself left to the
|
|
2191
|
+
// caller: the upload URL comes back in the result instead of being used here.
|
|
2192
|
+
// It is the tool for a file this process cannot read, and the name the hosted
|
|
2193
|
+
// server publishes for the same step.
|
|
2194
|
+
server.tool("create_upload_url", "Get a place to send a file that only exists on your machine. Every other tool takes a URL, so a local file has to be uploaded first. Returns an upload_url to PUT the bytes to and the file_url they will have afterwards; pass that file_url as psd_file_url to upload_psd, or as artwork_url to any render tool. Set kind to \"psd\" for a .psd or .psb template, or \"artwork\" for a .png, .jpg, .webp or .gif image. The upload target is scoped to a single file and cannot be overwritten or read back through the same URL. When the file is on the machine running this server, upload_local_file uploads it in one step. Costs 0 credits.", {
|
|
2195
|
+
filename: z.string().describe("The file's name, e.g. shirt.psd. Only its extension is read, and a type that cannot be processed is refused."),
|
|
2196
|
+
kind: z
|
|
2197
|
+
.enum(["psd", "artwork"])
|
|
2198
|
+
.default("psd")
|
|
2199
|
+
.describe("\"psd\" for a .psd or .psb template, \"artwork\" for a .png, .jpg, .webp or .gif image."),
|
|
2200
|
+
}, async ({ filename, kind }) => {
|
|
2201
|
+
const result = await apiRequest({
|
|
2202
|
+
method: "POST",
|
|
2203
|
+
path: "/api/v1/uploads/sign",
|
|
2204
|
+
body: { filename, kind },
|
|
2205
|
+
});
|
|
2206
|
+
return {
|
|
2207
|
+
content: [{
|
|
2208
|
+
type: "text",
|
|
2209
|
+
text: JSON.stringify(publicUploadTarget(result), null, 2),
|
|
2210
|
+
}],
|
|
2211
|
+
};
|
|
2212
|
+
});
|
|
2213
|
+
// ---------------------------------------------------------------------------
|
|
2214
|
+
// Family names, added beside the names above
|
|
2215
|
+
// ---------------------------------------------------------------------------
|
|
2216
|
+
// The product names its two kinds of template PSD mockups and photo mockups,
|
|
2217
|
+
// and the tools are spelled that way too. Added, not substituted: a name that
|
|
2218
|
+
// shipped once is answered to forever, because a setup written against it is
|
|
2219
|
+
// not something we can see from here, and a caller who reached for the family
|
|
2220
|
+
// spelling in the window it existed is owed the same. Both spellings are
|
|
2221
|
+
// registered, both are supported, and neither is deprecated.
|
|
2222
|
+
/**
|
|
2223
|
+
* Publish an already-registered tool under a second name.
|
|
2224
|
+
*
|
|
2225
|
+
* The two names share one handler and one input schema: the registration
|
|
2226
|
+
* arguments are replayed with a different name, so there is no second copy to
|
|
2227
|
+
* keep in step and no way for the two to drift apart. The original
|
|
2228
|
+
* registration is not touched, including its description. `relation` is how
|
|
2229
|
+
* the added description names the pairing.
|
|
2230
|
+
*/
|
|
2231
|
+
function alsoRegisterAs(existing, secondName, relation = "Family-name spelling") {
|
|
2232
|
+
const source = registrations.get(existing);
|
|
2233
|
+
if (source === undefined || source.method !== "tool") {
|
|
2234
|
+
throw new Error(`Cannot publish ${secondName}: ${existing} has no replayable registration.`);
|
|
2235
|
+
}
|
|
2236
|
+
const [, description, ...rest] = source.args;
|
|
2237
|
+
if (typeof description !== "string") {
|
|
2238
|
+
throw new Error(`Cannot publish ${secondName}: ${existing} was registered without a description, ` +
|
|
2239
|
+
"so replaying its arguments would drop the input schema and take any argument.");
|
|
2240
|
+
}
|
|
2241
|
+
server.tool(secondName, `${description} ${relation} of ${existing}: one tool under two names, same arguments, same behavior. Either name works.`, ...rest);
|
|
2242
|
+
}
|
|
2243
|
+
for (const [existing, familyName] of [
|
|
2244
|
+
// PSD mockups.
|
|
2245
|
+
["list_mockups", "list_psd_mockups"],
|
|
2246
|
+
["get_mockup_details", "get_psd_mockup"],
|
|
2247
|
+
["update_mockup", "update_psd_mockup"],
|
|
2248
|
+
["delete_mockup", "delete_psd_mockup"],
|
|
2249
|
+
["render_mockup", "render_psd_mockup"],
|
|
2250
|
+
// Photo mockups.
|
|
2251
|
+
["create_2d_mockup", "create_photo_mockup"],
|
|
2252
|
+
["list_2d_mockups", "list_photo_mockups"],
|
|
2253
|
+
["get_2d_mockup", "get_photo_mockup"],
|
|
2254
|
+
["update_2d_print_areas", "update_photo_mockup_print_areas"],
|
|
2255
|
+
["delete_2d_mockup", "delete_photo_mockup"],
|
|
2256
|
+
]) {
|
|
2257
|
+
alsoRegisterAs(existing, familyName);
|
|
2258
|
+
}
|
|
2259
|
+
// ---------------------------------------------------------------------------
|
|
2260
|
+
// Names the hosted server publishes for tools this package already has
|
|
2261
|
+
// ---------------------------------------------------------------------------
|
|
2262
|
+
// The hosted server and this package offer one set of tools, so a setup moved
|
|
2263
|
+
// from one to the other keeps working. Where the hosted server calls a tool
|
|
2264
|
+
// that exists here by another name, that name is added here the same way the
|
|
2265
|
+
// family names are: the same handler and the same input schema.
|
|
2266
|
+
for (const [existing, secondName] of [
|
|
2267
|
+
["get_2d_mockup", "get_2d_mockup_details"],
|
|
2268
|
+
["test_webhook_endpoint", "send_webhook_test_event"],
|
|
2269
|
+
]) {
|
|
2270
|
+
alsoRegisterAs(existing, secondName, "Second name");
|
|
2271
|
+
}
|
|
2272
|
+
// ---------------------------------------------------------------------------
|
|
2273
|
+
// Tool: render_photo_mockup
|
|
2274
|
+
// ---------------------------------------------------------------------------
|
|
2275
|
+
// The one family name that is not a second name for a single tool. Under the
|
|
2276
|
+
// 2D spelling a render picks its target by picking a tool; under the family
|
|
2277
|
+
// spelling one tool takes both kinds and the target is an argument, which is
|
|
2278
|
+
// the shape this name was published with. It is a dispatcher and nothing more:
|
|
2279
|
+
// the sizing rules, the refusals and the request itself are the same functions
|
|
2280
|
+
// `render_2d_surface` and `render_2d_print_area` call, so there is one
|
|
2281
|
+
// implementation of a 2D render on this side no matter which name reaches it.
|
|
2282
|
+
// The one field the two single-target tools do not offer is blend_mode, which
|
|
2283
|
+
// the hosted server publishes on this tool and on render_2d_mockup.
|
|
2284
|
+
const ONE_TARGET = "Provide exactly one of print_area_uuid or surface_uuid";
|
|
2285
|
+
// Every shared field belongs to this tool too; only the mockup's own argument
|
|
2286
|
+
// is spelled differently, as `mockup_id` -- the name this tool was published
|
|
2287
|
+
// with, and the name the photo mockup tools return the id under. `mockup_uuid`
|
|
2288
|
+
// is accepted beside it (see renderPhotoMockupInput).
|
|
2289
|
+
const { mockup_uuid: _twoDMockupUuid, ...PHOTO_RENDER_SHARED } = TWO_D_SHARED;
|
|
2290
|
+
// The target and the dials that size it. They are the same fields whichever
|
|
2291
|
+
// name the render is reached by, so they are written down once.
|
|
2292
|
+
const PHOTO_RENDER_TARGET = {
|
|
2293
|
+
print_area_uuid: z
|
|
2294
|
+
.string()
|
|
2295
|
+
.optional()
|
|
2296
|
+
.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."),
|
|
2297
|
+
surface_uuid: z
|
|
2298
|
+
.string()
|
|
2299
|
+
.optional()
|
|
2300
|
+
.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."),
|
|
2301
|
+
coverage: z
|
|
2302
|
+
.number()
|
|
2303
|
+
.min(10)
|
|
2304
|
+
.max(100)
|
|
2305
|
+
.optional()
|
|
2306
|
+
.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."),
|
|
2307
|
+
fit: z
|
|
2308
|
+
.enum(FIT_MODES)
|
|
2309
|
+
.optional()
|
|
2310
|
+
.describe("How the artwork meets the print area, which it always fills edge to edge: 'fit' keeps the proportions and fits inside (the default), 'fill' stretches to the edges, 'crop' fills and crops the overflow. 'contain' and 'cover' are the older names for 'fit' and 'crop' and are still accepted. Belongs to print_area_uuid; sending it with surface_uuid is refused. Leave it out to get 'fit'. To sit inside the area with room around it, send width and height instead."),
|
|
2311
|
+
// A percentage cannot express a box whose proportions differ from the
|
|
2312
|
+
// target's, which is exactly what an artwork resized on a canvas is, so
|
|
2313
|
+
// the exact box belongs to both kinds of target.
|
|
2314
|
+
width: z
|
|
2315
|
+
.number()
|
|
2316
|
+
.min(1)
|
|
2317
|
+
.max(30000)
|
|
2318
|
+
.optional()
|
|
2319
|
+
.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."),
|
|
2320
|
+
height: z
|
|
2321
|
+
.number()
|
|
2322
|
+
.min(1)
|
|
2323
|
+
.max(30000)
|
|
2324
|
+
.optional()
|
|
2325
|
+
.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."),
|
|
2326
|
+
};
|
|
2327
|
+
/** The strict input of a one-tool photo mockup render, refusing unknown options by name. */
|
|
2328
|
+
function photoRenderInput(shape) {
|
|
2329
|
+
return z.strictObject(shape, {
|
|
2330
|
+
error: (issue) => {
|
|
2331
|
+
if (issue.code !== "unrecognized_keys")
|
|
2332
|
+
return undefined;
|
|
2333
|
+
return issue.keys
|
|
2334
|
+
.map((key) => key === "scale"
|
|
2335
|
+
? RETIRED_SCALE
|
|
2336
|
+
: `${key} is not an option on this tool. Send only the options it lists.`)
|
|
2337
|
+
.join(" ");
|
|
2338
|
+
},
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
// The blend the hosted server publishes on its one-tool photo mockup render,
|
|
2342
|
+
// under both of its names. Left out, nothing is sent and the API's default,
|
|
2343
|
+
// 'multiply', applies; the values are the ones the API accepts.
|
|
2344
|
+
const PHOTO_BLEND_MODE = {
|
|
2345
|
+
blend_mode: z
|
|
2346
|
+
.enum(["multiply", "normal", "screen", "lighten", "soft_light", "overlay", "darken"])
|
|
2347
|
+
.optional()
|
|
2348
|
+
.describe("How the artwork sits on the product: 'multiply' (the default) keeps the material texture visible, 'normal' reproduces the artwork colors exactly. 'screen', 'lighten', 'soft_light', 'overlay' and 'darken' are also accepted."),
|
|
2349
|
+
};
|
|
2350
|
+
// The mockup travels as `mockup_id`, the name this tool was published with, or
|
|
2351
|
+
// as `mockup_uuid`, the name the hosted server publishes it under. One of the
|
|
2352
|
+
// two is needed; both are accepted when they agree.
|
|
2353
|
+
const renderPhotoMockupInput = photoRenderInput({
|
|
2354
|
+
mockup_id: z
|
|
2355
|
+
.string()
|
|
2356
|
+
.optional()
|
|
2357
|
+
.describe("UUID of the photo mockup (mockup_id from list_photo_mockups, get_photo_mockup or create_photo_mockup). Send this or mockup_uuid."),
|
|
2358
|
+
mockup_uuid: z
|
|
2359
|
+
.string()
|
|
2360
|
+
.optional()
|
|
2361
|
+
.describe("Same as mockup_id, under the name the hosted server uses. Send this or mockup_id; if both are sent they must match."),
|
|
2362
|
+
...PHOTO_RENDER_SHARED,
|
|
2363
|
+
...PHOTO_RENDER_TARGET,
|
|
2364
|
+
...PHOTO_BLEND_MODE,
|
|
2365
|
+
});
|
|
2366
|
+
/** The photo mockup a render_photo_mockup call names, by either spelling. */
|
|
2367
|
+
function photoMockupId(mockupId, mockupUuid) {
|
|
2368
|
+
if (mockupId !== undefined && mockupUuid !== undefined && mockupId !== mockupUuid) {
|
|
2369
|
+
throw new Error("mockup_id and mockup_uuid name the same photo mockup but hold different values. Send one of them.");
|
|
2370
|
+
}
|
|
2371
|
+
const id = mockupId ?? mockupUuid;
|
|
2372
|
+
if (id === undefined) {
|
|
2373
|
+
throw new Error("Provide mockup_id (or mockup_uuid): the UUID of the photo mockup to render.");
|
|
2374
|
+
}
|
|
2375
|
+
return id;
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* Render one photo mockup target named by argument rather than by tool.
|
|
2379
|
+
*
|
|
2380
|
+
* Shared by every name that takes the target as an argument, so the refusals
|
|
2381
|
+
* and the request are one implementation whichever of them is called.
|
|
2382
|
+
*/
|
|
2383
|
+
async function renderNamedTarget(args, mockupUuid) {
|
|
2384
|
+
const hasPrintArea = args.print_area_uuid !== undefined;
|
|
2385
|
+
const hasSurface = args.surface_uuid !== undefined;
|
|
2386
|
+
if (hasPrintArea === hasSurface)
|
|
2387
|
+
throw new Error(ONE_TARGET);
|
|
2388
|
+
// The dial of the other kind of target is refused by name, in the words
|
|
2389
|
+
// the tool that owns that dial already refuses it with. Refusing is keyed
|
|
2390
|
+
// on the argument being written rather than on the value it holds: writing
|
|
2391
|
+
// it is the caller naming the option.
|
|
2392
|
+
if (hasSurface && args.fit !== undefined)
|
|
2393
|
+
throw new Error(TWO_D_REFUSED_OPTIONS.surface.fit);
|
|
2394
|
+
if (hasPrintArea && args.coverage !== undefined) {
|
|
2395
|
+
throw new Error(TWO_D_REFUSED_OPTIONS.print_area.coverage);
|
|
2396
|
+
}
|
|
2397
|
+
const sizing = {
|
|
2398
|
+
...(args.coverage === undefined ? {} : { coverage: args.coverage }),
|
|
2399
|
+
...(args.fit === undefined ? {} : { fit: args.fit }),
|
|
2400
|
+
...(args.width === undefined ? {} : { width: args.width }),
|
|
2401
|
+
...(args.height === undefined ? {} : { height: args.height }),
|
|
2402
|
+
};
|
|
2403
|
+
assertOneSizingAnswer(sizing, hasSurface ? "coverage" : "fit");
|
|
2404
|
+
return renderTwoD({ ...args, mockup_uuid: mockupUuid }, hasSurface ? { surface_uuid: args.surface_uuid } : { uuid: args.print_area_uuid }, sizing);
|
|
2405
|
+
}
|
|
2406
|
+
server.registerTool("render_photo_mockup", {
|
|
2407
|
+
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. Family-name spelling of render_2d_surface and render_2d_print_area, which stay available and behave exactly as they did. Use the dashboard for visual fine-tuning.",
|
|
2408
|
+
inputSchema: renderPhotoMockupInput,
|
|
2409
|
+
}, async (args) => renderNamedTarget(args, photoMockupId(args.mockup_id, args.mockup_uuid)));
|
|
2410
|
+
// ---------------------------------------------------------------------------
|
|
2411
|
+
// Tool: render_2d_mockup
|
|
2412
|
+
// ---------------------------------------------------------------------------
|
|
2413
|
+
// The name the hosted server publishes render_photo_mockup under, and the name
|
|
2414
|
+
// this package's single render tool had before the render was split in two.
|
|
2415
|
+
// It is render_photo_mockup under a second name, with one difference in
|
|
2416
|
+
// spelling: the mockup travels as `mockup_uuid`, as it does on
|
|
2417
|
+
// render_2d_surface and render_2d_print_area and as that name always took it.
|
|
2418
|
+
server.registerTool("render_2d_mockup", {
|
|
2419
|
+
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_2d_mockup. Returns print_files (each with an export_path) and a render_uuid. Costs 5 credits. Second name of render_photo_mockup: the same tool and behavior, with the mockup passed as mockup_uuid. Use the dashboard for visual fine-tuning.",
|
|
2420
|
+
inputSchema: photoRenderInput({
|
|
2421
|
+
...TWO_D_SHARED,
|
|
2422
|
+
...PHOTO_RENDER_TARGET,
|
|
2423
|
+
...PHOTO_BLEND_MODE,
|
|
2424
|
+
}),
|
|
2425
|
+
}, async (args) => renderNamedTarget(args, args.mockup_uuid));
|
|
1890
2426
|
// ---------------------------------------------------------------------------
|
|
1891
2427
|
// Start server
|
|
1892
2428
|
// ---------------------------------------------------------------------------
|