@chrischall/gemini-mcp 1.12.0 → 1.14.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -3
- package/dist/bundle.js +7402 -2377
- package/dist/client.js +38 -35
- package/dist/inputs.js +80 -3
- package/dist/jobs.js +49 -15
- package/dist/models.js +14 -4
- package/dist/pricing.js +9 -1
- package/dist/session.js +7 -6
- package/dist/storage/media.js +265 -42
- package/dist/tools/_confirm.js +2 -2
- package/dist/tools/files.js +73 -17
- package/dist/tools/generate.js +5 -5
- package/dist/tools/interact.js +65 -32
- package/dist/tools/jobs.js +2 -2
- package/dist/tools/library.js +7 -7
- package/dist/tools/models.js +2 -2
- package/dist/tools/music.js +31 -44
- package/dist/tools/set.js +3 -3
- package/dist/tools/shared.js +74 -43
- package/dist/tools/uploads.js +8 -8
- package/dist/tools/usage.js +2 -2
- package/dist/tools/video.js +27 -10
- package/dist/upload-url.js +3 -3
- package/dist/version.js +1 -1
- package/mint.yaml +6 -0
- package/package.json +4 -4
- package/server.json +2 -2
- package/skills/gemini-mcp/SKILL.md +16 -8
package/dist/tools/interact.js
CHANGED
|
@@ -5,7 +5,8 @@ import { ChainedRequest404Error } from '../client.js';
|
|
|
5
5
|
import { slugify, baseName, resolveImagePath, writeSidecar, resolveOutputDir, readImageAsInline } from '../images.js';
|
|
6
6
|
import { resolveImageInputs } from '../inputs.js';
|
|
7
7
|
import { findInteractionImages, latestInteractionId } from '../sidecar.js';
|
|
8
|
-
import { emit, reportShape, resolveAspectRatio, orientationSchema, ASPECT_RATIOS, IMAGE_SIZES, MODEL_CHOICE_GUIDE, resolveVideoInput, videoPathSchema, timeoutMsSchema, timeoutRiskHint, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema } from './shared.js';
|
|
8
|
+
import { emit, reportShape, resolveAspectRatio, orientationSchema, ASPECT_RATIOS, IMAGE_SIZES, MODEL_CHOICE_GUIDE, resolveVideoInput, videoPathSchema, timeoutMsSchema, timeoutRiskHint, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesBase64Schema } from './shared.js';
|
|
9
|
+
import { bytesToBase64 } from '../bytes.js';
|
|
9
10
|
import { fingerprintRequest } from '../jobs.js';
|
|
10
11
|
import { attachCost } from '../pricing.js';
|
|
11
12
|
import { previewLocalInputsUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
@@ -36,6 +37,35 @@ function splitReattachedOutputs(images, writtenOutputs) {
|
|
|
36
37
|
}
|
|
37
38
|
return { kept, dropped };
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The image a given interaction produced, off disk, as inline input.
|
|
42
|
+
*
|
|
43
|
+
* `refs` is what the result reports as `reanchored_on`, so it is the caller's
|
|
44
|
+
* own vocabulary in each deployment: absolute paths here, object keys below.
|
|
45
|
+
*/
|
|
46
|
+
async function reanchorFromDisk(outputDir, interactionId) {
|
|
47
|
+
const refs = await findInteractionImages(outputDir, interactionId);
|
|
48
|
+
return { refs, carried: await Promise.all(refs.map((p) => readImageAsInline(p))) };
|
|
49
|
+
}
|
|
50
|
+
/** The same, from the object store, for a deployment with no filesystem. */
|
|
51
|
+
async function reanchorFromStore(client, interactionId) {
|
|
52
|
+
try {
|
|
53
|
+
const found = await client.mediaSink?.findByInteraction?.(interactionId);
|
|
54
|
+
if (!found)
|
|
55
|
+
return { refs: [], carried: [] };
|
|
56
|
+
const stored = await client.readStoredMedia(found.key);
|
|
57
|
+
return {
|
|
58
|
+
refs: [found.key],
|
|
59
|
+
carried: [{ base64: bytesToBase64(stored.bytes), mimeType: stored.mimeType }],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Recovery is best-effort: a store that cannot answer leaves the caller
|
|
64
|
+
// with the original 404, which is the honest outcome — never a re-anchor
|
|
65
|
+
// onto something else.
|
|
66
|
+
return { refs: [], carried: [] };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
39
69
|
export function registerInteractTools(server, client) {
|
|
40
70
|
// The sidecar/re-anchor half of this description is only true where there is
|
|
41
71
|
// a filesystem. Describing it anyway on the hosted connector would tell the
|
|
@@ -43,23 +73,21 @@ export function registerInteractTools(server, client) {
|
|
|
43
73
|
// description is built from the sink's capability, not hardcoded.
|
|
44
74
|
const onDiskRuntime = client.mediaSink?.persistsFiles ?? true;
|
|
45
75
|
const recoveryDescription = onDiskRuntime
|
|
46
|
-
? 'If a call times out
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'gemini_get_result for long generations. ';
|
|
76
|
+
? 'If a call times out client-side the generation usually still completes: the image and an `<image>.json` sidecar ' +
|
|
77
|
+
'holding its interaction id land in the output dir, and `continue_last: true` still resumes it — look there before ' +
|
|
78
|
+
're-issuing, which bills a second generation. A chained 404 is re-anchored on the prior output and re-issued ' +
|
|
79
|
+
'un-chained (`chain_recovered`); a second 404 means the interaction id was not the cause — check the model id / ' +
|
|
80
|
+
'files uri. '
|
|
81
|
+
: 'Each result returns an image URL to open or share (`media[].url`) — show it to the user rather than assuming they ' +
|
|
82
|
+
'can see the image. There is no output dir here, but a lost response is still recoverable: the interaction id is ' +
|
|
83
|
+
'stored beside each image, so `continue_last: true` survives a restart and a chained 404 is re-anchored on the ' +
|
|
84
|
+
'image that interaction produced (reported as `chain_recovered`). gemini_list_recent_media shows what each stored ' +
|
|
85
|
+
'image was. Give long generations a `max_wait_ms` budget. ';
|
|
57
86
|
server.registerTool('gemini_interact', {
|
|
58
|
-
description: 'Preferred tool for iterative
|
|
59
|
-
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'`continue_last: true` chains from this session\'s most recent interaction without threading the id. ' +
|
|
87
|
+
description: 'Preferred tool for iterative refinement of a single image — multi-turn generation and editing via the ' +
|
|
88
|
+
'Interactions API. To refine, pass the returned interaction `id` as `previous_interaction_id` on the next call; ' +
|
|
89
|
+
'do NOT start a new interaction or re-send the image for each tweak. `continue_last: true` chains from this ' +
|
|
90
|
+
"session's most recent interaction without threading the id. " +
|
|
63
91
|
recoveryDescription +
|
|
64
92
|
'Output is JPEG.',
|
|
65
93
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
@@ -79,11 +107,7 @@ export function registerInteractTools(server, client) {
|
|
|
79
107
|
.describe(`Paths to reference input images. ${NEW_REFERENCES_ONLY}`),
|
|
80
108
|
images_url: imagesUrlSchema(`Reference images. ${NEW_REFERENCES_ONLY} Given`),
|
|
81
109
|
images_file_uris: imagesFileUrisSchema(`Reference images. ${NEW_REFERENCES_ONLY} Given`),
|
|
82
|
-
images_base64:
|
|
83
|
-
.array(z.string().min(1))
|
|
84
|
-
.optional()
|
|
85
|
-
.describe(`Reference images as base64 strings or data URIs. Last resort: prefer images_url or images_file_uris, ` +
|
|
86
|
-
`which keep image bytes out of the conversation. ${NEW_REFERENCES_ONLY}`),
|
|
110
|
+
images_base64: imagesBase64Schema(`Reference images. ${NEW_REFERENCES_ONLY} Given`),
|
|
87
111
|
model: z
|
|
88
112
|
.string()
|
|
89
113
|
.optional()
|
|
@@ -153,15 +177,23 @@ export function registerInteractTools(server, client) {
|
|
|
153
177
|
let continuedFromSidecar = false;
|
|
154
178
|
if (!previousInteractionId && args.continue_last) {
|
|
155
179
|
previousInteractionId = client.session.lastInteractionId;
|
|
156
|
-
if (!previousInteractionId
|
|
157
|
-
|
|
180
|
+
if (!previousInteractionId) {
|
|
181
|
+
// Both branches answer the same question from the same kind of
|
|
182
|
+
// record — an `<image>.json` on disk, an object beside the media in
|
|
183
|
+
// the store — so a restart no longer ends a chain on either.
|
|
184
|
+
previousInteractionId = onDisk
|
|
185
|
+
? await latestInteractionId(resolveOutputDir(args.output_dir))
|
|
186
|
+
// 'image' explicitly: video and music write records too, and a
|
|
187
|
+
// chained Lyria call is a documented 400. Resuming the wrong
|
|
188
|
+
// medium's chain is worse than not resuming.
|
|
189
|
+
: await client.mediaSink?.latestInteractionId?.('image');
|
|
158
190
|
continuedFromSidecar = previousInteractionId !== undefined;
|
|
159
191
|
}
|
|
160
192
|
if (!previousInteractionId) {
|
|
161
193
|
throw new McpToolError('No previous interaction to continue in this session.', {
|
|
162
194
|
hint: onDisk
|
|
163
195
|
? 'Call gemini_interact once without continue_last first, or pass an explicit previous_interaction_id.'
|
|
164
|
-
: 'Call gemini_interact once without continue_last first, or pass an explicit previous_interaction_id. (
|
|
196
|
+
: 'Call gemini_interact once without continue_last first, or pass an explicit previous_interaction_id. (Nothing this connector has stored carries an interaction id to resume from.)',
|
|
165
197
|
});
|
|
166
198
|
}
|
|
167
199
|
}
|
|
@@ -231,14 +263,15 @@ export function registerInteractTools(server, client) {
|
|
|
231
263
|
// probe itself could not be completed.
|
|
232
264
|
if (err.chainExists === true)
|
|
233
265
|
throw err;
|
|
234
|
-
// Re-anchoring needs the
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const reanchorOn
|
|
266
|
+
// Re-anchoring needs the image that interaction produced, located
|
|
267
|
+
// by ID — never "the newest image". On disk that index is the
|
|
268
|
+
// `<image>.json` sidecars; hosted it is the record beside each
|
|
269
|
+
// stored object. Same rule either way: no match, no guess.
|
|
270
|
+
const { refs: reanchorOn, carried } = onDisk
|
|
271
|
+
? await reanchorFromDisk(resolveOutputDir(args.output_dir), err.previousInteractionId)
|
|
272
|
+
: await reanchorFromStore(client, err.previousInteractionId);
|
|
239
273
|
if (!reanchorOn.length)
|
|
240
274
|
throw err;
|
|
241
|
-
const carried = await Promise.all(reanchorOn.map((p) => readImageAsInline(p)));
|
|
242
275
|
try {
|
|
243
276
|
const probe = await client.interact({ ...callOpts, images: [...carried, ...inputs], previousInteractionId: undefined });
|
|
244
277
|
chainRecovered = { expired_interaction_id: err.previousInteractionId, reanchored_on: reanchorOn };
|
|
@@ -298,7 +331,7 @@ export function registerInteractTools(server, client) {
|
|
|
298
331
|
// writeImage returns already-resolved absolute paths. `emit` only
|
|
299
332
|
// invokes this callback for a filesystem-backed sink, so there is no
|
|
300
333
|
// branch here claiming a sidecar the hosted connector cannot write.
|
|
301
|
-
return emit(named, { ...args, sink: client.mediaSink }, meta, async (paths) => {
|
|
334
|
+
return emit(named, { ...args, sink: client.mediaSink, sidecar: { prompt: args.input } }, meta, async (paths) => {
|
|
302
335
|
for (const p of paths)
|
|
303
336
|
client.session.writtenOutputs.add(p);
|
|
304
337
|
// Sidecar per image so the interaction id survives a lost MCP response
|
package/dist/tools/jobs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { McpToolError,
|
|
2
|
+
import { McpToolError, minifiedResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { TERMINAL_INTERACTION_STATUSES } from '../client.js';
|
|
4
4
|
import { emitMedia } from './shared.js';
|
|
5
5
|
import { slugify } from '../images.js';
|
|
@@ -128,7 +128,7 @@ function recoveryNote(recordedError) {
|
|
|
128
128
|
}
|
|
129
129
|
/** The generation outlived its executor and is not finished yet. */
|
|
130
130
|
function stillRunning(jobId, interactionId) {
|
|
131
|
-
return
|
|
131
|
+
return minifiedResult({
|
|
132
132
|
job_id: jobId,
|
|
133
133
|
status: 'running',
|
|
134
134
|
interaction_id: interactionId,
|
package/dist/tools/library.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { McpToolError,
|
|
2
|
+
import { McpToolError, minifiedResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { LIBRARY_NAME_PATTERN } from '../library.js';
|
|
4
4
|
import { acceptableUploadType } from '../upload-url.js';
|
|
5
5
|
import { withProgressHeartbeat } from './shared.js';
|
|
@@ -79,7 +79,7 @@ export function registerLibraryTools(server, client) {
|
|
|
79
79
|
}, async (args, extra) => {
|
|
80
80
|
const image = (await withProgressHeartbeat(extra, 'Loading the reference image', () => resolveLibraryImage(client, args, true)));
|
|
81
81
|
const record = await library.saveCharacter({ name: args.name, description: args.description, image });
|
|
82
|
-
return
|
|
82
|
+
return minifiedResult({
|
|
83
83
|
saved: 'character',
|
|
84
84
|
name: record.name,
|
|
85
85
|
description: record.description,
|
|
@@ -96,7 +96,7 @@ export function registerLibraryTools(server, client) {
|
|
|
96
96
|
inputSchema: {},
|
|
97
97
|
}, async () => {
|
|
98
98
|
const characters = await library.listCharacters();
|
|
99
|
-
return
|
|
99
|
+
return minifiedResult({
|
|
100
100
|
count: characters.length,
|
|
101
101
|
characters: characters.map((c) => ({
|
|
102
102
|
name: c.name,
|
|
@@ -122,7 +122,7 @@ export function registerLibraryTools(server, client) {
|
|
|
122
122
|
if (!existed) {
|
|
123
123
|
throw new McpToolError(`No saved character "${args.name}". List what exists with gemini_list_characters.`);
|
|
124
124
|
}
|
|
125
|
-
return
|
|
125
|
+
return minifiedResult({ deleted: 'character', name: args.name });
|
|
126
126
|
});
|
|
127
127
|
server.registerTool('gemini_save_style', {
|
|
128
128
|
description: 'Save a named style preset to this account\'s persistent reference library: a reusable prompt fragment (and ' +
|
|
@@ -141,7 +141,7 @@ export function registerLibraryTools(server, client) {
|
|
|
141
141
|
}, async (args, extra) => {
|
|
142
142
|
const image = await withProgressHeartbeat(extra, 'Loading the style reference image', () => resolveLibraryImage(client, args, false));
|
|
143
143
|
const record = await library.saveStyle({ name: args.name, promptFragment: args.prompt_fragment, image });
|
|
144
|
-
return
|
|
144
|
+
return minifiedResult({
|
|
145
145
|
saved: 'style',
|
|
146
146
|
name: record.name,
|
|
147
147
|
prompt_fragment: record.prompt_fragment,
|
|
@@ -157,7 +157,7 @@ export function registerLibraryTools(server, client) {
|
|
|
157
157
|
inputSchema: {},
|
|
158
158
|
}, async () => {
|
|
159
159
|
const styles = await library.listStyles();
|
|
160
|
-
return
|
|
160
|
+
return minifiedResult({
|
|
161
161
|
count: styles.length,
|
|
162
162
|
styles: styles.map((s) => ({
|
|
163
163
|
name: s.name,
|
|
@@ -181,6 +181,6 @@ export function registerLibraryTools(server, client) {
|
|
|
181
181
|
if (!existed) {
|
|
182
182
|
throw new McpToolError(`No saved style "${args.name}". List what exists with gemini_list_styles.`);
|
|
183
183
|
}
|
|
184
|
-
return
|
|
184
|
+
return minifiedResult({ deleted: 'style', name: args.name });
|
|
185
185
|
});
|
|
186
186
|
}
|
package/dist/tools/models.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { minifiedResult } from '@chrischall/mcp-utils';
|
|
2
2
|
export function registerModelTools(server, client) {
|
|
3
3
|
server.registerTool('gemini_list_models', {
|
|
4
4
|
description: 'List the Gemini image-generation models available to your API key (Nano Banana / Nano Banana Pro family), and the current default model.',
|
|
5
5
|
annotations: { readOnlyHint: true },
|
|
6
6
|
}, async () => {
|
|
7
7
|
const models = await client.listModels();
|
|
8
|
-
return
|
|
8
|
+
return minifiedResult({ default: client.defaultModel(), models });
|
|
9
9
|
});
|
|
10
10
|
}
|
package/dist/tools/music.js
CHANGED
|
@@ -1,42 +1,46 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { McpToolError } from '@chrischall/mcp-utils';
|
|
3
2
|
import { slugify, baseName } from '../images.js';
|
|
4
3
|
import { resolveImageInputs } from '../inputs.js';
|
|
5
4
|
import { DEFAULT_MUSIC_MODEL } from '../models.js';
|
|
6
|
-
import { emitMedia, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema } from './shared.js';
|
|
5
|
+
import { emitMedia, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesBase64Schema } from './shared.js';
|
|
7
6
|
import { fingerprintRequest } from '../jobs.js';
|
|
8
7
|
import { attachCost } from '../pricing.js';
|
|
9
8
|
import { previewLocalInputsUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
10
|
-
const MUSIC_MODELS = ['lyria-3-clip-preview', 'lyria-3-pro-preview'];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
const MUSIC_MODELS = ['lyria-3-clip-preview', 'lyria-3.5', 'lyria-3-pro-preview'];
|
|
10
|
+
/**
|
|
11
|
+
* There is no output-format parameter, and there is no chaining.
|
|
12
|
+
*
|
|
13
|
+
* Both were docs-derived and both were wrong (probed live 2026-09-09):
|
|
14
|
+
* `response_format.audio_format` is not a field — every Lyria model answers
|
|
15
|
+
* `400 Unknown parameter 'audio_format' at 'response_format'` — and its real
|
|
16
|
+
* name, `mime_type`, currently refuses every value but MP3 per-model
|
|
17
|
+
* (`Audio MIME type AUDIO_WAV is not supported for models/lyria-3.5`). A
|
|
18
|
+
* chained call dies inside the model, on the track it is handed back:
|
|
19
|
+
* `400 Unsupported input mime type for this model: audio/s16le`.
|
|
20
|
+
*
|
|
21
|
+
* So neither is exposed. A parameter whose every non-default value is a 400 is
|
|
22
|
+
* worse than an absent one: it costs schema tokens on every request and spends
|
|
23
|
+
* a caller's turn discovering it does not work.
|
|
24
|
+
*/
|
|
19
25
|
export function registerMusicTools(server, client) {
|
|
20
26
|
server.registerTool('gemini_music_generate', {
|
|
21
|
-
description: 'Generate music from a text prompt (mood, genre, instruments, structure, or lyrics inline) via a Lyria model ' +
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
27
|
+
description: 'Generate music from a text prompt (mood, genre, instruments, structure, or lyrics inline) via a Lyria model: ' +
|
|
28
|
+
'lyria-3-clip-preview (30s instrumental clip, default, cheapest), lyria-3.5 (full-length song with vocals) or ' +
|
|
29
|
+
'lyria-3-pro-preview (longer-form). Output is MP3, written to disk (or returned inline). Single-turn: a track ' +
|
|
30
|
+
'cannot be refined by a follow-up call, so put the whole brief in the prompt. Runs long — give it a `max_wait_ms` ' +
|
|
31
|
+
'budget (or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account.',
|
|
25
32
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
26
33
|
inputSchema: {
|
|
27
34
|
prompt: z.string().min(1).describe('Description of the music: mood, genre, instruments, tempo, structure, or lyrics'),
|
|
28
|
-
model: z.enum(MUSIC_MODELS).optional().describe(`Lyria model (default: ${DEFAULT_MUSIC_MODEL}).
|
|
29
|
-
audio_format: z.enum(AUDIO_FORMATS).optional().describe('Output format (default mp3). wav is lyria-3-pro-preview-only.'),
|
|
35
|
+
model: z.enum(MUSIC_MODELS).optional().describe(`Lyria model (default: ${DEFAULT_MUSIC_MODEL} — 30s, $0.04). lyria-3.5 and lyria-3-pro-preview run minutes-long at $0.08.`),
|
|
30
36
|
images: z.array(z.string().min(1)).optional().describe('Optional reference image path(s) to condition the music'),
|
|
31
37
|
images_url: imagesUrlSchema('Reference images'),
|
|
32
38
|
images_file_uris: imagesFileUrisSchema('Reference images'),
|
|
33
|
-
images_base64:
|
|
39
|
+
images_base64: imagesBase64Schema(),
|
|
34
40
|
from_clipboard: z.boolean().optional().describe('Use the image currently on the macOS clipboard as a reference'),
|
|
35
41
|
filename: z.string().optional().describe('Base filename for the output audio (extension stripped; default: slugified prompt)'),
|
|
36
42
|
output_dir: z.string().optional().describe('Directory to write audio to (default: $GEMINI_OUTPUT_DIR or cwd)'),
|
|
37
|
-
inline: z.boolean().optional().describe('Return base64 audio inline instead of writing to disk'),
|
|
38
|
-
previous_interaction_id: z.string().optional().describe('Interaction id to continue from'),
|
|
39
|
-
continue_last: z.boolean().optional().describe('Continue from the most recent music interaction this server created (explicit previous_interaction_id wins)'),
|
|
43
|
+
inline: z.boolean().optional().describe('Return base64 audio inline instead of writing to disk. A 30s track is ~1.9MB of base64 — the default hands back a path instead'),
|
|
40
44
|
background: z.boolean().optional().describe('Run the generation on Google\'s side and poll it, so a killed job can be recovered by gemini_get_result. Off by default — see gemini_video_generate'),
|
|
41
45
|
timeout_ms: timeoutMsSchema,
|
|
42
46
|
idempotency_key: idempotencyKeySchema,
|
|
@@ -47,28 +51,13 @@ export function registerMusicTools(server, client) {
|
|
|
47
51
|
}, async (args, extra) => {
|
|
48
52
|
assertLocalInputsAvailable(client.mediaSink, args);
|
|
49
53
|
const model = args.model ?? DEFAULT_MUSIC_MODEL;
|
|
50
|
-
if (args.audio_format === 'wav' && !isProModel(model)) {
|
|
51
|
-
throw new McpToolError(`WAV output requires a Pro model; ${model} only produces MP3.`, {
|
|
52
|
-
hint: 'Use model: "lyria-3-pro-preview" for WAV, or drop audio_format to get MP3.',
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
let previousInteractionId = args.previous_interaction_id;
|
|
56
|
-
if (!previousInteractionId && args.continue_last) {
|
|
57
|
-
if (!client.session.lastMusicInteractionId) {
|
|
58
|
-
throw new McpToolError('No previous music interaction to continue in this session.', {
|
|
59
|
-
hint: 'Generate music first, or pass an explicit previous_interaction_id.',
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
previousInteractionId = client.session.lastMusicInteractionId;
|
|
63
|
-
}
|
|
64
54
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, 'Send local image input(s) to the Gemini Lyria API', '/v1beta/interactions', args.images);
|
|
65
55
|
if (gate)
|
|
66
56
|
return gate;
|
|
67
57
|
const fingerprint = fingerprintRequest('gemini_music_generate', {
|
|
68
|
-
model, prompt: args.prompt,
|
|
58
|
+
model, prompt: args.prompt,
|
|
69
59
|
images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard,
|
|
70
60
|
images_url: args.images_url, images_file_uris: args.images_file_uris,
|
|
71
|
-
previous_interaction_id: previousInteractionId,
|
|
72
61
|
});
|
|
73
62
|
return client.session.jobs.dispatch({ toolName: 'gemini_music_generate', fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async (ctx) => {
|
|
74
63
|
const { inputs, report } = await resolveImageInputs(args, client);
|
|
@@ -76,17 +65,15 @@ export function registerMusicTools(server, client) {
|
|
|
76
65
|
input: args.prompt,
|
|
77
66
|
images: inputs.length ? inputs : undefined,
|
|
78
67
|
model: args.model,
|
|
79
|
-
audioFormat: args.audio_format,
|
|
80
|
-
previousInteractionId,
|
|
81
68
|
timeoutMs: args.timeout_ms,
|
|
82
69
|
background: args.background,
|
|
83
70
|
// See gemini_video_generate: the id is what survives the executor.
|
|
84
71
|
onInteractionStarted: ctx.reportInteraction,
|
|
85
72
|
}));
|
|
86
|
-
|
|
73
|
+
// The interaction id is still reported — it is the handle
|
|
74
|
+
// gemini_get_result recovers a killed job's output with — but it is not
|
|
75
|
+
// a conversation handle: Lyria has no second turn.
|
|
87
76
|
const meta = { model, interaction_id: r.id };
|
|
88
|
-
if (previousInteractionId)
|
|
89
|
-
meta.previous_interaction_id = previousInteractionId;
|
|
90
77
|
if (r.usage)
|
|
91
78
|
meta.usage = r.usage;
|
|
92
79
|
attachCost(meta, model, r.usage);
|
|
@@ -94,13 +81,13 @@ export function registerMusicTools(server, client) {
|
|
|
94
81
|
meta.text = r.text;
|
|
95
82
|
if (report)
|
|
96
83
|
meta.image_inputs = report;
|
|
97
|
-
meta.hint =
|
|
84
|
+
meta.hint = 'Lyria is single-turn: to change this track, call gemini_music_generate again with a fuller prompt rather than a follow-up instruction.';
|
|
98
85
|
const slug = args.filename ? baseName(args.filename) : slugify(args.prompt);
|
|
99
86
|
const named = r.audios.map((media, i) => ({
|
|
100
87
|
media,
|
|
101
88
|
base: r.audios.length > 1 ? `${slug}-${String(i + 1).padStart(2, '0')}` : slug,
|
|
102
89
|
}));
|
|
103
|
-
return emitMedia(named, 'audio', { inline: args.inline, output_dir: args.output_dir, sink: client.mediaSink }, meta);
|
|
90
|
+
return emitMedia(named, 'audio', { inline: args.inline, output_dir: args.output_dir, sink: client.mediaSink, sidecar: { prompt: args.prompt } }, meta);
|
|
104
91
|
});
|
|
105
92
|
});
|
|
106
93
|
}
|
package/dist/tools/set.js
CHANGED
|
@@ -3,7 +3,7 @@ import { McpToolError, readEnvVar } from '@chrischall/mcp-utils';
|
|
|
3
3
|
import { resolveModel } from '../models.js';
|
|
4
4
|
import { slugify, baseName } from '../images.js';
|
|
5
5
|
import { resolveImageInputs } from '../inputs.js';
|
|
6
|
-
import { emit, resolveAspectRatio, ASPECT_RATIOS, sharedImageSchema, pickSeed, buildMeta, timeoutRiskHint, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesR2KeysSchema, charactersSchema, styleSchema, resolveCharacterRefs, composePrompt, persistBundle, SET_URL_TTL_MS } from './shared.js';
|
|
6
|
+
import { emit, resolveAspectRatio, ASPECT_RATIOS, sharedImageSchema, pickSeed, buildMeta, timeoutRiskHint, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesBase64Schema, imagesR2KeysSchema, charactersSchema, styleSchema, resolveCharacterRefs, composePrompt, persistBundle, SET_URL_TTL_MS } from './shared.js';
|
|
7
7
|
import { fingerprintRequest } from '../jobs.js';
|
|
8
8
|
import { sumUsage } from '../usage.js';
|
|
9
9
|
import { attachCost } from '../pricing.js';
|
|
@@ -25,7 +25,7 @@ export function registerSetTools(server, client) {
|
|
|
25
25
|
master_images_r2_keys: imagesR2KeysSchema('Reference images passed to the master AND to every scene call'),
|
|
26
26
|
characters: charactersSchema,
|
|
27
27
|
style: styleSchema,
|
|
28
|
-
master_images_base64:
|
|
28
|
+
master_images_base64: imagesBase64Schema('Reference images for the master generation', 'master_images'),
|
|
29
29
|
confirm: schemaConfirm,
|
|
30
30
|
...sharedImageSchema,
|
|
31
31
|
},
|
|
@@ -190,7 +190,7 @@ export function registerSetTools(server, client) {
|
|
|
190
190
|
// A set is a batch someone comes back to: sign its links (and the
|
|
191
191
|
// bundle's) for ~7 days rather than the default ~48h. The sink clamps
|
|
192
192
|
// to the retention window; the disk sink ignores it entirely.
|
|
193
|
-
return emit(named, { ...args, sink: client.mediaSink, urlTtlMs: SET_URL_TTL_MS }, meta);
|
|
193
|
+
return emit(named, { ...args, sink: client.mediaSink, urlTtlMs: SET_URL_TTL_MS, sidecar: { prompt: args.master_prompt } }, meta);
|
|
194
194
|
});
|
|
195
195
|
});
|
|
196
196
|
}
|