@simpligen/mcp 0.1.0 → 0.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/LICENSE +21 -21
- package/package.json +37 -23
- package/src/client.js +14 -0
- package/src/tools.js +210 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 SimpliGen
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SimpliGen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@simpligen/mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "MCP server for SimpliGen -- drive local/cloud AI image & video generation from an agent.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"homepage": "https://simpligen.io",
|
|
8
|
-
"repository": {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@simpligen/mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP server for SimpliGen -- drive local/cloud AI image & video generation from an agent.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://simpligen.io",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/RaynoldVanHeyningen/SimpliGen.git",
|
|
11
|
+
"directory": "packages/mcp-server"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"simpligen-mcp": "src/bin.js"
|
|
15
|
+
},
|
|
16
|
+
"main": "src/index.js",
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node --test src/*.test.js",
|
|
30
|
+
"prepublishOnly": "node --test src/*.test.js"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1",
|
|
34
|
+
"sharp": "^0.35.1",
|
|
35
|
+
"zod": "^3"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/client.js
CHANGED
|
@@ -53,6 +53,20 @@ export class ControlApiClient {
|
|
|
53
53
|
preparePreset(args) { return this.#req('POST', '/presets/prepare', args); }
|
|
54
54
|
getActiveDownloads() { return this.#req('GET', '/presets/prepare'); }
|
|
55
55
|
|
|
56
|
+
listCharacters() { return this.#req('GET', '/characters'); }
|
|
57
|
+
getCharacter(id) { return this.#req('GET', `/characters/${encodeURIComponent(id)}`); }
|
|
58
|
+
listCharacterFeatures() { return this.#req('GET', '/character-features'); }
|
|
59
|
+
listCharacterPresets() { return this.#req('GET', '/character-presets'); }
|
|
60
|
+
createCharacter(args) { return this.#req('POST', '/characters', args); }
|
|
61
|
+
generateCharacterImage(id, args) { return this.#req('POST', `/characters/${encodeURIComponent(id)}/generate-image`, args); }
|
|
62
|
+
generateCharacterFrame(id, args) { return this.#req('POST', `/characters/${encodeURIComponent(id)}/generate-frame`, args); }
|
|
63
|
+
animateCharacter(id, args) { return this.#req('POST', `/characters/${encodeURIComponent(id)}/animate`, args); }
|
|
64
|
+
acceptBase(id, args) { return this.#req('POST', `/characters/${encodeURIComponent(id)}/accept-base`, args); }
|
|
65
|
+
setCharacterPreset(id, args) { return this.#req('POST', `/characters/${encodeURIComponent(id)}/preset`, args); }
|
|
66
|
+
enhanceCharacterMedia(id, args) { return this.#req('POST', `/characters/${encodeURIComponent(id)}/enhance`, args); }
|
|
67
|
+
regenerateCharacterBase(id, args) { return this.#req('POST', `/characters/${encodeURIComponent(id)}/regenerate-base`, args); }
|
|
68
|
+
removeCharacter(id) { return this.#req('DELETE', `/characters/${encodeURIComponent(id)}`); }
|
|
69
|
+
|
|
56
70
|
async uploadFile(filePath) {
|
|
57
71
|
let buf;
|
|
58
72
|
try {
|
package/src/tools.js
CHANGED
|
@@ -9,6 +9,10 @@ export const TOOL_NAMES = [
|
|
|
9
9
|
'list_capabilities', 'get_status', 'list_projects', 'create_project',
|
|
10
10
|
'upload_file', 'generate', 'get_job', 'wait_for_result', 'list_jobs',
|
|
11
11
|
'cancel_job', 'prepare_preset', 'get_result_image',
|
|
12
|
+
'list_characters', 'get_character', 'list_character_features', 'list_character_presets',
|
|
13
|
+
'remove_character', 'create_character', 'generate_character_image', 'generate_character_video',
|
|
14
|
+
'generate_character_frame', 'animate_character', 'set_character_preset',
|
|
15
|
+
'enhance_character_media', 'regenerate_character_base',
|
|
12
16
|
];
|
|
13
17
|
|
|
14
18
|
const ok = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
@@ -46,7 +50,7 @@ export function registerTools(server, client) {
|
|
|
46
50
|
image: z.string().optional().describe('upload_file handle'),
|
|
47
51
|
referenceImages: z.array(z.string()).optional().describe('upload_file handles'),
|
|
48
52
|
video: z.string().optional().describe('upload_file handle (driving video)'),
|
|
49
|
-
options: z.object({ resolution: z.string().optional(), durationSeconds: z.number().optional(), steps: z.number().optional(), cfg: z.number().optional(), seed: z.number().optional() }).optional(),
|
|
53
|
+
options: z.object({ resolution: z.string().optional(), aspectRatio: z.string().optional().describe('Aspect ratio, e.g. "1:1", "16:9", "9:16" (portrait/vertical), "4:3", "3:4". See list_capabilities -> options.aspectRatio for the values each preset accepts. Defaults to 1:1 for images (16:9 for video) if omitted.'), durationSeconds: z.number().optional(), steps: z.number().optional(), cfg: z.number().optional(), seed: z.number().optional() }).optional(),
|
|
50
54
|
},
|
|
51
55
|
},
|
|
52
56
|
wrap((args) => client.generate(args)));
|
|
@@ -93,4 +97,209 @@ export function registerTools(server, client) {
|
|
|
93
97
|
return fail(e);
|
|
94
98
|
}
|
|
95
99
|
});
|
|
100
|
+
|
|
101
|
+
// ---- Character Studio tools ----
|
|
102
|
+
|
|
103
|
+
server.registerTool('list_characters',
|
|
104
|
+
{ title: 'List characters', description: 'List all saved characters in Character Studio.', inputSchema: {} },
|
|
105
|
+
wrap(() => client.listCharacters()));
|
|
106
|
+
|
|
107
|
+
server.registerTool('get_character',
|
|
108
|
+
{ title: 'Get character', description: 'Get a single character by ID, including its current base portrait and identity features.', inputSchema: { characterId: z.string() } },
|
|
109
|
+
wrap(({ characterId }) => client.getCharacter(characterId)));
|
|
110
|
+
|
|
111
|
+
server.registerTool('list_character_features',
|
|
112
|
+
{ title: 'List character features', description: 'List available feature categories and their options (e.g. hairColor, eyeColor) for use with create_character identity.', inputSchema: {} },
|
|
113
|
+
wrap(() => client.listCharacterFeatures()));
|
|
114
|
+
|
|
115
|
+
server.registerTool('list_character_presets',
|
|
116
|
+
{ title: 'List character presets', description: 'List generation presets available for Character Studio. Returns base, identityEdit, and i2v presets as well as enhanceImage and enhanceVideo presets (each with supportedScales). Use the returned ids as presetId/imagePresetId/videoPresetId/basePresetId arguments in other character tools.', inputSchema: {} },
|
|
117
|
+
wrap(() => client.listCharacterPresets()));
|
|
118
|
+
|
|
119
|
+
server.registerTool('remove_character',
|
|
120
|
+
{ title: 'Remove character', description: 'Permanently delete a character and its associated data.', inputSchema: { characterId: z.string() } },
|
|
121
|
+
wrap(({ characterId }) => client.removeCharacter(characterId)));
|
|
122
|
+
|
|
123
|
+
server.registerTool('create_character',
|
|
124
|
+
{
|
|
125
|
+
title: 'Create character',
|
|
126
|
+
description: 'Create a new character. Three paths: (1) describe -- pass gender and identity features from list_character_features with mode "generate"; the server builds a base portrait automatically and this tool waits until ready. (2) upload as base -- pass baseFilePath pointing to a local image with mode "upload"; the image becomes the base portrait immediately. (3) vision recreate -- describe path but fill identity by having the agent analyse a reference photo first; the agent extracts features and passes them as identity.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
name: z.string().describe('Display name for the character'),
|
|
129
|
+
gender: z.enum(['woman', 'man', 'nonbinary']),
|
|
130
|
+
mode: z.enum(['generate', 'upload']).describe('generate: build base portrait from description; upload: use a provided image as base'),
|
|
131
|
+
identity: z.record(z.string()).optional().describe('An object of featureId to optionId pairs chosen from list_character_features (e.g. { hairColor: "black", eyeColor: "green" }). Do not include gender here; pass gender separately.'),
|
|
132
|
+
baseFilePath: z.string().optional().describe('Absolute local image path; required for mode "upload"'),
|
|
133
|
+
timeoutSeconds: z.number().optional().describe('Max seconds to wait for base portrait generation (default 300, mode generate only)'),
|
|
134
|
+
pollMs: z.number().optional().describe('Poll interval in milliseconds (default 2000, mode generate only)'),
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
async ({ name, gender, mode, identity, baseFilePath, timeoutSeconds, pollMs }) => {
|
|
138
|
+
try {
|
|
139
|
+
if (mode === 'upload') {
|
|
140
|
+
const up = await client.uploadFile(baseFilePath);
|
|
141
|
+
const char = await client.createCharacter({ name, gender, mode: 'upload', identity, baseHandle: up.handle });
|
|
142
|
+
return ok({ ...char, ready: true });
|
|
143
|
+
}
|
|
144
|
+
// mode === 'generate': create then poll until base_image_url is set
|
|
145
|
+
const c = await client.createCharacter({ name, gender, mode: 'generate', identity });
|
|
146
|
+
const { characterId } = c;
|
|
147
|
+
const deadline = Date.now() + (timeoutSeconds ?? 300) * 1000;
|
|
148
|
+
while (true) {
|
|
149
|
+
const { character } = await client.getCharacter(characterId);
|
|
150
|
+
if (character?.base_image_url) return ok(character);
|
|
151
|
+
if (Date.now() >= deadline) return ok({ ...character, base_pending: true });
|
|
152
|
+
await new Promise((r) => setTimeout(r, pollMs ?? 2000));
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
return fail(e);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
server.registerTool('generate_character_image',
|
|
160
|
+
{
|
|
161
|
+
title: 'Generate character image',
|
|
162
|
+
description: 'Generate one or more images of a character in a scene. Returns jobIds; use wait_for_result then get_result_image for each. Pass presetId to override the default image preset (ids from list_character_presets).',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
characterId: z.string(),
|
|
165
|
+
prompt: z.string(),
|
|
166
|
+
aspect: z.enum(['16:9', '9:16', '1:1']).optional(),
|
|
167
|
+
count: z.number().optional(),
|
|
168
|
+
presetId: z.string().optional().describe('Override image preset (id from list_character_presets)'),
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
wrap(({ characterId, prompt, aspect, count, presetId }) => {
|
|
172
|
+
const args = { prompt, aspect, count };
|
|
173
|
+
if (presetId !== undefined) args.presetId = presetId;
|
|
174
|
+
return client.generateCharacterImage(characterId, args);
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
server.registerTool('generate_character_video',
|
|
178
|
+
{
|
|
179
|
+
title: 'Generate character video',
|
|
180
|
+
description: 'Generate a short video of a character in a scene. Internally generates a frame then animates it (I2V). Returns frameJobId and videoJobId; use wait_for_result then get_result_image on videoJobId. Use framePresetId to override the image-frame preset and videoPresetId to override the video/animate preset (ids from list_character_presets).',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
characterId: z.string(),
|
|
183
|
+
prompt: z.string(),
|
|
184
|
+
motion: z.string().optional().describe('Optional motion description or style hint'),
|
|
185
|
+
aspect: z.enum(['16:9', '9:16', '1:1']).optional(),
|
|
186
|
+
timeoutSeconds: z.number().optional().describe('Max seconds to wait for the frame job (default 300)'),
|
|
187
|
+
pollMs: z.number().optional().describe('Poll interval in milliseconds (default 2000)'),
|
|
188
|
+
framePresetId: z.string().optional().describe('Override image-frame preset (id from list_character_presets)'),
|
|
189
|
+
videoPresetId: z.string().optional().describe('Override video/animate preset (id from list_character_presets)'),
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
async ({ characterId, prompt, motion, aspect, timeoutSeconds = 300, pollMs = 2000, framePresetId, videoPresetId }) => {
|
|
193
|
+
try {
|
|
194
|
+
// 1. Generate a frame (still image for I2V)
|
|
195
|
+
const { jobIds: [frameJobId] } = await client.generateCharacterFrame(characterId, { prompt, aspect, presetId: framePresetId });
|
|
196
|
+
// 2. Wait for frame to complete
|
|
197
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
198
|
+
while (true) {
|
|
199
|
+
const { job: frameJob } = await client.getJob(frameJobId);
|
|
200
|
+
if (frameJob?.status === 'completed') break;
|
|
201
|
+
if (frameJob?.status === 'failed') return fail({ message: `Frame job failed: ${frameJobId}`, errorCode: 'frame_failed' });
|
|
202
|
+
if (Date.now() >= deadline) return fail({ message: `Frame job timed out: ${frameJobId}`, errorCode: 'frame_timeout' });
|
|
203
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
204
|
+
}
|
|
205
|
+
// 3. Animate from the completed frame
|
|
206
|
+
const { jobIds: [videoJobId] } = await client.animateCharacter(characterId, { frameJobId, motion, aspect, presetId: videoPresetId });
|
|
207
|
+
return ok({ frameJobId, videoJobId });
|
|
208
|
+
} catch (e) { return fail(e); }
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ---- New Character Studio tools (Task 4) ----
|
|
212
|
+
|
|
213
|
+
server.registerTool('generate_character_frame',
|
|
214
|
+
{
|
|
215
|
+
title: 'Generate character frame',
|
|
216
|
+
description: 'Generate a start-frame image for a character that you can review or enhance before animating. Returns jobIds; use wait_for_result then get_result_image. Pass presetId to override the frame-image preset (ids from list_character_presets).',
|
|
217
|
+
inputSchema: {
|
|
218
|
+
characterId: z.string(),
|
|
219
|
+
prompt: z.string(),
|
|
220
|
+
aspect: z.enum(['16:9', '9:16', '1:1']).optional(),
|
|
221
|
+
presetId: z.string().optional().describe('Override image preset (id from list_character_presets)'),
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
wrap(({ characterId, prompt, aspect, presetId }) => {
|
|
225
|
+
const args = { prompt, aspect };
|
|
226
|
+
if (presetId !== undefined) args.presetId = presetId;
|
|
227
|
+
return client.generateCharacterFrame(characterId, args);
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
server.registerTool('animate_character',
|
|
231
|
+
{
|
|
232
|
+
title: 'Animate character',
|
|
233
|
+
description: 'Animate a character from a completed image/frame job or a local file path (I2V). jobId may be any completed character image, frame, or upscale job. Returns jobIds; use wait_for_result then get_result_image. Pass videoPresetId to override the video preset (ids from list_character_presets).',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
characterId: z.string(),
|
|
236
|
+
jobId: z.string().optional().describe('Any completed character image/frame/upscale jobId to use as the source frame'),
|
|
237
|
+
filePath: z.string().optional().describe('Absolute local image path to use as the source frame'),
|
|
238
|
+
motion: z.string().optional().describe('Optional motion description or style hint'),
|
|
239
|
+
aspect: z.enum(['16:9', '9:16', '1:1']).optional(),
|
|
240
|
+
videoPresetId: z.string().optional().describe('Override video/animate preset (id from list_character_presets)'),
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
wrap(({ characterId, jobId, filePath, motion, aspect, videoPresetId }) =>
|
|
244
|
+
client.animateCharacter(characterId, { frameJobId: jobId, framePath: filePath, motion, aspect, presetId: videoPresetId })));
|
|
245
|
+
|
|
246
|
+
server.registerTool('set_character_preset',
|
|
247
|
+
{
|
|
248
|
+
title: 'Set character preset',
|
|
249
|
+
description: 'Set sticky per-character default presets for image and/or video generation. Ids come from list_character_presets. Pass null to clear a preset back to the system default.',
|
|
250
|
+
inputSchema: {
|
|
251
|
+
characterId: z.string(),
|
|
252
|
+
imagePresetId: z.string().nullable().optional().describe('Default image preset id (null to clear)'),
|
|
253
|
+
videoPresetId: z.string().nullable().optional().describe('Default video preset id (null to clear)'),
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
wrap(({ characterId, imagePresetId, videoPresetId }) => client.setCharacterPreset(characterId, { imagePresetId, videoPresetId })));
|
|
257
|
+
|
|
258
|
+
server.registerTool('enhance_character_media',
|
|
259
|
+
{
|
|
260
|
+
title: 'Enhance character media',
|
|
261
|
+
description: 'Upscale a generated image/video, a local file, or the character base portrait (target: "base"). Returns a jobId; use wait_for_result then get_result_image. scale sets the upscale factor; keepResolution upscales then resizes back to source size for a sharper same-size result; applyAsBase replaces the character base portrait with the upscaled result. Enhance presets come from list_character_presets (enhanceImage/enhanceVideo, each with supportedScales).',
|
|
262
|
+
inputSchema: {
|
|
263
|
+
characterId: z.string(),
|
|
264
|
+
jobId: z.string().optional().describe('Completed job whose result to enhance'),
|
|
265
|
+
filePath: z.string().optional().describe('Absolute local image/video path to enhance'),
|
|
266
|
+
target: z.enum(['base']).optional().describe('Pass "base" to enhance the character base portrait'),
|
|
267
|
+
mediaType: z.enum(['image', 'video']).optional(),
|
|
268
|
+
presetId: z.string().optional().describe('Enhance preset id from list_character_presets'),
|
|
269
|
+
scale: z.number().optional().describe('Upscale factor (e.g. 2 for 2x)'),
|
|
270
|
+
keepResolution: z.boolean().optional().describe('Upscale then resize back to source dimensions for a sharper same-size result'),
|
|
271
|
+
applyAsBase: z.boolean().optional().describe('Replace the character base portrait with the upscaled result'),
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
wrap(({ characterId, jobId, filePath, target, mediaType, presetId, scale, keepResolution, applyAsBase }) =>
|
|
275
|
+
client.enhanceCharacterMedia(characterId, { jobId, filePath, target, mediaType, presetId, scale, keepResolution, applyAsBase })));
|
|
276
|
+
|
|
277
|
+
server.registerTool('regenerate_character_base',
|
|
278
|
+
{
|
|
279
|
+
title: 'Regenerate character base',
|
|
280
|
+
description: 'Switch the base model and regenerate the base portrait IN PLACE (no new character created). Replaces the current base when completed. Uses basePresetId from list_character_presets. Optionally update identity features at the same time. Polls until completed, then auto-accepts the new base.',
|
|
281
|
+
inputSchema: {
|
|
282
|
+
characterId: z.string(),
|
|
283
|
+
basePresetId: z.string().optional().describe('Base-generation preset id from list_character_presets'),
|
|
284
|
+
identity: z.record(z.string()).optional().describe('Updated identity feature map (featureId -> optionId) to apply alongside the new base'),
|
|
285
|
+
timeoutSeconds: z.number().optional().describe('Max seconds to wait for the base job (default 300)'),
|
|
286
|
+
pollMs: z.number().optional().describe('Poll interval in milliseconds (default 2000)'),
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
async ({ characterId, basePresetId, identity, timeoutSeconds = 300, pollMs = 2000 }) => {
|
|
290
|
+
try {
|
|
291
|
+
const { baseJobId } = await client.regenerateCharacterBase(characterId, { basePresetId, identity });
|
|
292
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
293
|
+
while (true) {
|
|
294
|
+
const { job } = await client.getJob(baseJobId);
|
|
295
|
+
if (job?.status === 'completed') {
|
|
296
|
+
await client.acceptBase(characterId, { altImageId: baseJobId });
|
|
297
|
+
return ok(await client.getCharacter(characterId));
|
|
298
|
+
}
|
|
299
|
+
if (job?.status === 'failed') return fail({ message: `Base regeneration job failed: ${baseJobId}`, errorCode: 'base_failed' });
|
|
300
|
+
if (Date.now() >= deadline) return ok({ baseJobId, note: 'timeout_still_running' });
|
|
301
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
302
|
+
}
|
|
303
|
+
} catch (e) { return fail(e); }
|
|
304
|
+
});
|
|
96
305
|
}
|