@facelessad/mcp 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -6
- package/index.js +51 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
FacelessAd as MCP tools — let your AI assistant create faceless video ads.
|
|
4
4
|
|
|
5
5
|
Works with any MCP-speaking harness: Claude Desktop, Claude Code, Cursor,
|
|
6
|
-
Windsurf, OpenClaw.
|
|
6
|
+
Windsurf, OpenClaw. Eight tools, each a single call to the FacelessAd API;
|
|
7
7
|
the tool/style registry lives on the server, so new tools and styles are
|
|
8
8
|
available the day they ship without updating this package.
|
|
9
9
|
|
|
@@ -27,26 +27,60 @@ Create a key at https://facelessad.com/developers. OpenClaw users: add the
|
|
|
27
27
|
same block under `openclaw mcp` config and verify with
|
|
28
28
|
`openclaw mcp doctor --probe`.
|
|
29
29
|
|
|
30
|
+
Needs Node 18 or newer. If your harness pins the MCP SDK, it has to be
|
|
31
|
+
1.23.0 or newer — that is the first version whose schema conversion
|
|
32
|
+
understands zod 4, and an older one silently registers the tools with no
|
|
33
|
+
parameters at all.
|
|
34
|
+
|
|
35
|
+
There is also a hosted version that needs no install: add
|
|
36
|
+
`https://facelessad.com/mcp` as a custom connector in Claude and authorise it
|
|
37
|
+
with your account. Same eight tools, same fields.
|
|
38
|
+
|
|
30
39
|
## Tools
|
|
31
40
|
|
|
32
41
|
| Tool | What it does |
|
|
33
42
|
|------|--------------|
|
|
34
43
|
| `facelessad_list_tools` | Registry: tools, styles, structures, hooks, durations |
|
|
35
|
-
|
|
36
|
-
Ask for a look of your own and the assistant sends `style: "custom"` plus
|
|
37
|
-
`custom_style` — `facelessad_list_tools` marks which tools accept it
|
|
38
|
-
(`supports.customStyle`). Product Showcase also takes `custom_graphics_style`
|
|
39
|
-
for the text layer drawn over the product video.
|
|
40
44
|
| `facelessad_create_video` | Create an ad; returns an id immediately |
|
|
41
45
|
| `facelessad_get_video` | Status + download URL when done |
|
|
42
46
|
| `facelessad_list_videos` | Your videos, newest first |
|
|
43
47
|
| `facelessad_estimate` | Upper-bound credit cost without creating |
|
|
44
48
|
| `facelessad_balance` | Plan + credits |
|
|
49
|
+
| `facelessad_list_brand_kits` | Your brands and their ids (for `brand_kit_id`) |
|
|
45
50
|
| `facelessad_voices` | Curated voice pool |
|
|
46
51
|
|
|
52
|
+
Ask for a look of your own and the assistant sends `style: "custom"` plus
|
|
53
|
+
`custom_style` — `facelessad_list_tools` marks which tools accept it
|
|
54
|
+
(`supports.customStyle`). Product Showcase also takes `custom_graphics_style`
|
|
55
|
+
for the text layer drawn over the product video.
|
|
56
|
+
|
|
47
57
|
Videos build in the background (3–10 min); the assistant polls
|
|
48
58
|
`facelessad_get_video`. You are only charged for steps that succeed.
|
|
49
59
|
|
|
60
|
+
## 1.1.0 – 1.1.1
|
|
61
|
+
|
|
62
|
+
**A look of your own.** `custom_style` (with `style: "custom"`) is the field
|
|
63
|
+
that actually changes how the video looks; `custom_graphics_style` styles the
|
|
64
|
+
text layer Product Showcase draws over the product video, and
|
|
65
|
+
`custom_style_refine: false` uses your text verbatim so a campaign keeps one
|
|
66
|
+
look. `facelessad_get_video` returns the expanded text as `customStyle` for
|
|
67
|
+
exactly that.
|
|
68
|
+
|
|
69
|
+
**SaaS UI Ad takes `screenshot_urls`** — public https URLs of your app's
|
|
70
|
+
screens. They are decomposed with vision and rebuilt as an animated demo,
|
|
71
|
+
which is what the tool is for; without them the ad is built from the written
|
|
72
|
+
description alone. How many fit depends on the length (`screenshots.max` in
|
|
73
|
+
`facelessad_list_tools`): 1 short, 3 medium, 4 long.
|
|
74
|
+
|
|
75
|
+
**Character does dialogue.** `speakers: 2` makes it a conversation between two
|
|
76
|
+
characters, and the ad structure has to match — `facelessad_list_tools`
|
|
77
|
+
returns `speakerStructures.one` and `.two`, and a mismatch is rejected rather
|
|
78
|
+
than quietly built as a one-voice video. `voice_right` is the second
|
|
79
|
+
speaker's voice; omit it and a distinct one is picked automatically.
|
|
80
|
+
|
|
81
|
+
**`brand_kit_id`** picks which brand to use on a multi-brand account instead
|
|
82
|
+
of always taking the account default (`facelessad_list_brand_kits`).
|
|
83
|
+
|
|
50
84
|
## 1.0.1
|
|
51
85
|
|
|
52
86
|
Seven tool-specific fields were reachable through the HTTP API but missing
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @facelessad/mcp — FacelessAd as MCP tools.
|
|
4
4
|
*
|
|
5
|
-
* A thin mirror, not a second brain: every one of the
|
|
5
|
+
* A thin mirror, not a second brain: every one of the eight tools is a
|
|
6
6
|
* single /api/v1 call, and nothing is decided locally. The style and
|
|
7
7
|
* structure registry lives on the server (facelessad_list_tools), so new
|
|
8
8
|
* tools and styles are available to the assistant the day they ship,
|
|
@@ -53,7 +53,15 @@ function result(data) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/** §706: kyselymerkkijono ilman `URLSearchParams.size`ä — se on Nodessa vasta
|
|
57
|
+
* 18.16 / 19.8, ja tämä paketti lupaa node>=18. Vanhemmalla 18:lla `q.size`
|
|
58
|
+
* on undefined, jolloin `?`-osa jäi kokonaan pois eikä kutsuja saanut mitään
|
|
59
|
+
* ilmoitusta: suodatin katosi ja koko lista palautui. */
|
|
60
|
+
function qs(q) {
|
|
61
|
+
return [...q.keys()].length ? '?' + q.toString() : '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const server = new McpServer({ name: 'facelessad', version: '1.1.1' });
|
|
57
65
|
|
|
58
66
|
server.tool(
|
|
59
67
|
'facelessad_list_tools',
|
|
@@ -71,7 +79,7 @@ server.tool(
|
|
|
71
79
|
|
|
72
80
|
server.tool(
|
|
73
81
|
'facelessad_list_brand_kits',
|
|
74
|
-
'The brands on this account: id, name, whether it is the default, how many of the
|
|
82
|
+
'The brands on this account: id, name, whether it is the default, how many of the brand questions are answered (filled / questions), and whether it carries a colour and a logo. Call this before naming brand_kit_id — ids are per account and cannot be guessed.',
|
|
75
83
|
{},
|
|
76
84
|
async () => result(await api('GET', '/api/v1/brand-kits'))
|
|
77
85
|
);
|
|
@@ -87,7 +95,10 @@ server.tool(
|
|
|
87
95
|
const q = new URLSearchParams();
|
|
88
96
|
if (language) q.set('language', language);
|
|
89
97
|
if (gender) q.set('gender', gender);
|
|
90
|
-
|
|
98
|
+
// §706: `q.size` on Nodessa vasta 18.16/19.8. Paketti lupaa node>=18, ja
|
|
99
|
+
// sitä vanhemmalla 18:lla se on undefined -> suodattimet katosivat
|
|
100
|
+
// HILJAA ja koko lista palautui. [...q.keys()] toimii kaikilla.
|
|
101
|
+
return result(await api('GET', '/api/v1/voices' + (qs(q))));
|
|
91
102
|
}
|
|
92
103
|
);
|
|
93
104
|
|
|
@@ -99,7 +110,18 @@ const createShape = {
|
|
|
99
110
|
landing_page_url: z.string().optional().describe('http(s) URL of the product/landing page'),
|
|
100
111
|
text: z.string().optional().describe('Free-text brief (min 20 chars if no URL)'),
|
|
101
112
|
}).describe('What the ad is about: a URL, free text, or both'),
|
|
102
|
-
|
|
113
|
+
// §706: kesto on kolme vaihtoehtoa, ei väli. Palvelin hylkää muut arvot
|
|
114
|
+
// (invalid_duration); ennen se puristi ne hiljaa rajoihin ja worker pudotti
|
|
115
|
+
// tuloksen samoihin kolmeen ämpäriin, joten duration:45 teki saman videon
|
|
116
|
+
// kuin duration:50 eikä kutsuja nähnyt sitä mistään.
|
|
117
|
+
// §724: skeema hyväksyy MINKÄ TAHANSA merkkijonon, ei enumia. Enum esti
|
|
118
|
+
// tasan sen mitä kuvaus käskee tehdä: video-bannerin `durations.options`
|
|
119
|
+
// palauttaa id:t "5" / "10" / "15", ja avustin joka luki ne rekisteristä ja
|
|
120
|
+
// välitti sellaisenaan sai zodilta pelkän "Invalid input" -hylkäyksen ennen
|
|
121
|
+
// kuin pyyntö ehti palvelimelle. Palvelin osaa validoida tämän kentän
|
|
122
|
+
// työkalukohtaisesti ja nimeää kelvolliset arvot; zod ei voi, koska se ei
|
|
123
|
+
// tiedä mikä työkalu on valittu.
|
|
124
|
+
duration: z.union([z.number().int(), z.string()]).optional().describe('One of the three lengths this tool renders: the id from facelessad_list_tools (durations.options) or the matching seconds. Most tools are "short" | "medium" | "long" = 15/30/50 s; video-banner\'s ids are "5" | "10" | "15". Any other value is rejected with invalid_duration, which names the valid set. Omit for the middle option (30 s, banner 10 s).'),
|
|
103
125
|
aspect_ratio: z.string().optional().describe('"9:16" | "1:1" | "4:5" | "16:9" (default per tool)'),
|
|
104
126
|
language: z.string().optional().describe('Default "English (US)"'),
|
|
105
127
|
style: z.string().optional().describe('Style id for the tool (registry). Omit to let the server pick. Use "custom" together with custom_style to describe a look of your own — facelessad_list_tools marks which tools accept it (supports.customStyle)'),
|
|
@@ -112,29 +134,40 @@ const createShape = {
|
|
|
112
134
|
custom_graphics_style: z.string().max(2000).optional().describe('product-showcase only: how the text graphics rendered in code OVER the product video should look (typography, colors, contrast against the video). Optional — left out, it is derived from custom_style.'),
|
|
113
135
|
custom_style_refine: z.boolean().optional().describe('Default true: your description is expanded into a full style specification before the video is built. Set false to use your text verbatim — do that when reusing the customStyle returned by a previous video so a campaign keeps one look.'),
|
|
114
136
|
style_hint: z.string().optional().describe('Legacy alias: when no style is given, this is treated exactly like custom_style. Prefer custom_style.'),
|
|
115
|
-
ad_structure: z.string().optional().describe('Ad structure id
|
|
137
|
+
ad_structure: z.string().optional().describe('Ad structure id. facelessad_list_tools gives three sources: adStructures is the default pool, adStructureGroups the full grouped set (animated-ad 46 in 4 families, motion-graphics 119 in 10 genres), and structuresByStyle overrides both for styles that carry their own. Omit to have one chosen from the materials.'),
|
|
116
138
|
hook_formula: z.string().optional().describe('Hook formula id (registry)'),
|
|
117
|
-
video_mode: z.enum(['continuous', 'cuts']).optional(),
|
|
139
|
+
video_mode: z.enum(['continuous', 'cuts']).optional().describe('animated-ad and music-video ONLY — the two tools whose registry entry carries supports.videoMode. Every other tool rejects it (video_mode_not_supported): they render one way only.'),
|
|
118
140
|
brand_color: z.string().optional().describe('Hex like #4A9BFF'),
|
|
119
|
-
brand_name: z.string().optional(),
|
|
141
|
+
brand_name: z.string().optional().describe('The advertiser\'s name, used in the script and on the CTA. Omit to have it read from the materials.'),
|
|
120
142
|
cta: z.string().optional().describe('Call to action text'),
|
|
143
|
+
// §708: character-dialogi. Rakenne ja puhujamäärä on pakko täsmätä —
|
|
144
|
+
// dialogirakenne yhdellä äänellä tuottaa [L]/[R]-skriptin jonka yksi ääni
|
|
145
|
+
// lukee, ja se oli ennen API:n hiljainen lopputulos.
|
|
146
|
+
speakers: z.union([z.literal(1), z.literal(2)]).optional().describe('character ONLY (supports.speakers in the registry). 1 = one narrator, 2 = a dialogue between two characters. The ad structure must match — facelessad_list_tools returns speakerStructures.one and .two for character, and a mismatch is rejected. With 2, the second voice is voice_right.'),
|
|
147
|
+
voice_right: z.object({
|
|
148
|
+
id: z.string().optional().describe('A voice id from facelessad_voices. Must differ from voice.id.'),
|
|
149
|
+
gender: z.enum(['female', 'male', 'any']).optional().describe('Narrows the automatic pick for the second speaker'),
|
|
150
|
+
}).optional().describe('The second speaker\'s voice, for speakers: 2. Same shape as voice. Omit to have a distinct one picked automatically (different gender first, then a different id).'),
|
|
121
151
|
voice: z.object({
|
|
122
152
|
id: z.string().optional().describe('A voice id from facelessad_voices'),
|
|
123
153
|
gender: z.enum(['female', 'male', 'any']).optional().describe('Narrows the automatic pick'),
|
|
124
|
-
}).optional().describe('Omit
|
|
154
|
+
}).optional().describe('Omit and the voice is CAST from the finished script — a casting-director model reads it and picks the best match from the curated pool for this language. Give gender to cast from that half only, or id to skip casting.'),
|
|
125
155
|
voice_over: z.boolean().optional().describe('false = silent video (only on tools that support the toggle)'),
|
|
126
|
-
music: z.boolean().optional(),
|
|
127
|
-
sfx: z.boolean().optional(),
|
|
128
|
-
captions: z.boolean().optional(),
|
|
129
|
-
use_brand_kit: z.boolean().optional().describe('Default true'),
|
|
130
|
-
brand_kit_id: z.number().int().optional().describe('
|
|
131
|
-
use_winners: z.boolean().optional().describe('Default true'),
|
|
156
|
+
music: z.boolean().optional().describe('Background music. Default on for tools that have a soundtrack; music-video is always off (its song IS the audio) and video-banner rejects the field (silent loop).'),
|
|
157
|
+
sfx: z.boolean().optional().describe('Sound effects. Default off. Rejected on video-banner (silent loop).'),
|
|
158
|
+
captions: z.boolean().optional().describe('Burned-in captions timed to the voice-over. Omit to use the tool default (text-animation defaults off — its words are the visual). Follows the voice-over: no narration, no captions.'),
|
|
159
|
+
use_brand_kit: z.boolean().optional().describe('Default true. NOT video-banner: that tool is a few lines of text in a loop and does not read the Brand Kit at all — sending this on it is rejected (brand_kit_not_supported). Set its colour with brand_color.'),
|
|
160
|
+
brand_kit_id: z.number().int().optional().describe('Which brand to use. Omit for the account default. Ids come from facelessad_list_brand_kits. Not accepted on video-banner.'),
|
|
132
161
|
name: z.string().optional().describe('Display name in My Files'),
|
|
133
|
-
product_image_url: z.string().optional().describe('
|
|
162
|
+
product_image_url: z.string().optional().describe('A photo of your physical product. On product-showcase it is REQUIRED and every clip is animated from it. On animated-ad and crude it is optional: the photo is attached to the image generation as a reference and composed into the scenes where the product appears, drawn in that tool\'s own style rather than pasted in as a photo. facelessad_list_tools marks which tools compose it (supports.productInScenes). Public https URL.'),
|
|
134
163
|
// 1.0.1 (§639): nämä API on hyväksynyt alusta asti, mutta ne puuttuivat
|
|
135
164
|
// tästä skeemasta — ja koska SDK riisuu tuntemattomat avaimet, avustin ei
|
|
136
165
|
// voinut käyttää niitä lainkaan. Peilaa McpController::toolList():ia 1:1.
|
|
137
|
-
product_image_urls: z.array(z.string()).optional().describe('product-showcase only: up to 8 extra angle photos of the same product (https URLs). Helps the product keep its shape and label across clips.'),
|
|
166
|
+
product_image_urls: z.array(z.string()).optional().describe('product-showcase only: up to 8 extra angle photos of the same product (https URLs). Helps the product keep its shape and label across clips. Not accepted on the tools that compose a single reference into their scenes.'),
|
|
167
|
+
// §711: SaaS UI Ad — kuvakaappaukset. Ilman niitä työkalu tekee UI-mainoksen
|
|
168
|
+
// pelkän sanallisen kuvauksen varassa, ja koko idea on oikeiden ruutujen
|
|
169
|
+
// purku ja uudelleenrakennus animoituna.
|
|
170
|
+
screenshot_urls: z.array(z.string()).optional().describe('saas-ui-ad only: public https URLs of screenshots of the app. They are decomposed with vision and rebuilt as an animated demo, which is what this tool does; without them the ad is built from the written description alone. How many fit depends on the duration (facelessad_list_tools -> screenshots.max): 1 for short, 3 for medium, 4 for long.'),
|
|
138
171
|
texts: z.object({
|
|
139
172
|
headline: z.string().optional().describe('Max 80 characters'),
|
|
140
173
|
subline: z.string().optional().describe('Max 120 characters'),
|
|
@@ -179,7 +212,7 @@ server.tool(
|
|
|
179
212
|
const q = new URLSearchParams();
|
|
180
213
|
if (limit) q.set('limit', String(limit));
|
|
181
214
|
if (offset) q.set('offset', String(offset));
|
|
182
|
-
return result(await api('GET', '/api/v1/videos' + (q
|
|
215
|
+
return result(await api('GET', '/api/v1/videos' + (qs(q))));
|
|
183
216
|
}
|
|
184
217
|
);
|
|
185
218
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@facelessad/mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "FacelessAd as MCP tools — let your AI assistant create faceless video ads (Claude Desktop, Claude Code, Cursor, Windsurf, OpenClaw).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"node": ">=18"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
19
19
|
"zod": "^4.4.3"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|