@chrischall/gemini-mcp 1.14.2 → 2.1.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.
@@ -89,7 +89,7 @@ export function registerFileTools(server, client) {
89
89
  : ' To upload a local file without base64: mint a signed PUT URL with gemini_get_upload_url, PUT the bytes to it, ' +
90
90
  'then pass the returned r2_key here.'),
91
91
  annotations: { readOnlyHint: false, openWorldHint: true },
92
- inputSchema: {
92
+ inputSchema: z.object({
93
93
  url: z
94
94
  .string()
95
95
  .url()
@@ -126,7 +126,7 @@ export function registerFileTools(server, client) {
126
126
  .describe('Override the detected MIME type (sniffed from the bytes / taken from the server response otherwise)'),
127
127
  display_name: z.string().min(1).optional().describe('Human-readable name recorded against the upload'),
128
128
  confirm: schemaConfirm,
129
- },
129
+ }),
130
130
  }, async (args, extra) => {
131
131
  const sources = [args.url && 'url', args.data_base64 && 'data_base64', args.r2_key && 'r2_key', args.path && 'path'].filter(Boolean);
132
132
  if (sources.length !== 1) {
@@ -184,9 +184,9 @@ export function registerFileTools(server, client) {
184
184
  description: 'List files, images and photos currently uploaded to the Gemini Files API under this API key, with their reusable `file_uri` (`files/<id>`) references, ' +
185
185
  'MIME types and expiry times. Retention is ~48h, so an entry that has vanished has expired rather than failed.',
186
186
  annotations: { readOnlyHint: true, openWorldHint: true },
187
- inputSchema: {
187
+ inputSchema: z.object({
188
188
  page_size: z.number().int().positive().max(100).optional().describe('Maximum files to return (1-100, default 100)'),
189
- },
189
+ }),
190
190
  }, async (args) => {
191
191
  const files = await client.listFiles(args.page_size ?? 100);
192
192
  return minifiedResult({
@@ -216,9 +216,9 @@ export function registerFileTools(server, client) {
216
216
  'end — re-sign it instead of re-generating (and re-paying for) the image. Keywords: expired link, refresh url, ' +
217
217
  're-sign, media url, download again.',
218
218
  annotations: { readOnlyHint: true, openWorldHint: false },
219
- inputSchema: {
219
+ inputSchema: z.object({
220
220
  r2_key: z.string().min(1).describe('The `media[].r2_key` from an earlier generation result'),
221
- },
221
+ }),
222
222
  }, async (args) => {
223
223
  const sink = client.mediaSink;
224
224
  if (!sink?.resign) {
@@ -260,9 +260,9 @@ export function registerFileTools(server, client) {
260
260
  'Costs image tokens on the turns you use it, which is why it is a separate call and not part of every result. ' +
261
261
  'Keywords: see the image, view, look at, verify output, check the result, preview.',
262
262
  annotations: { readOnlyHint: true, openWorldHint: false },
263
- inputSchema: {
263
+ inputSchema: z.object({
264
264
  r2_key: z.string().min(1).describe('The `media[].r2_key` from a generation result or gemini_list_recent_media'),
265
- },
265
+ }),
266
266
  }, async (args) => {
267
267
  const key = args.r2_key.trim();
268
268
  const { bytes, mimeType } = await client.readStoredMedia(key);
@@ -287,14 +287,14 @@ export function registerFileTools(server, client) {
287
287
  'to find what already exists BEFORE re-running (and re-paying for) it. Keywords: lost result, orphaned job, ' +
288
288
  'missing image, what did I generate, recover, recent media.',
289
289
  annotations: { readOnlyHint: true, openWorldHint: false },
290
- inputSchema: {
290
+ inputSchema: z.object({
291
291
  limit: z.number().int().positive().max(100).optional().describe('How many to return (default 20, newest first)'),
292
292
  since_day: z
293
293
  .string()
294
294
  .regex(/^\d{4}-\d{2}-\d{2}$/)
295
295
  .optional()
296
296
  .describe('Only media stored on or after this UTC day, e.g. "2026-08-18"'),
297
- },
297
+ }),
298
298
  }, async (args) => {
299
299
  const sink = client.mediaSink;
300
300
  if (!sink?.listRecent) {
@@ -338,10 +338,10 @@ export function registerFileTools(server, client) {
338
338
  description: 'Delete an uploaded file, image or photo (by file_uri) from the Gemini Files API before its ~48h expiry. Any tool call still referencing it ' +
339
339
  'will then fail with a generic 404, so delete only references you are finished with.',
340
340
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
341
- inputSchema: {
341
+ inputSchema: z.object({
342
342
  file_uri: z.string().min(1).describe('The `files/<id>` reference (or full uri) to delete'),
343
343
  confirm: schemaConfirm,
344
- },
344
+ }),
345
345
  }, async (args) => {
346
346
  // Deleting is a remote mutation with no undo — confirm-gated per the
347
347
  // fleet convention (unlike writing a generated image, which is local).
@@ -19,7 +19,7 @@ export function registerGenerateTools(server, client) {
19
19
  description: 'Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro). ' +
20
20
  'If the result will likely be refined iteratively, prefer gemini_interact (multi-turn) as the entry point.',
21
21
  annotations: { readOnlyHint: false, openWorldHint: true },
22
- inputSchema: {
22
+ inputSchema: z.object({
23
23
  prompt: z.string().min(1).describe('Text prompt describing the image'),
24
24
  count: z.number().int().positive().max(8).optional().describe('Number of independent images (default 1)'),
25
25
  filename: z.string().optional().describe('Base filename for the output image (extension stripped; default: slugified prompt)'),
@@ -34,7 +34,7 @@ export function registerGenerateTools(server, client) {
34
34
  video_path: videoPathSchema,
35
35
  confirm: schemaConfirm,
36
36
  ...sharedImageSchema,
37
- },
37
+ }),
38
38
  }, async (args, extra) => {
39
39
  // Local-path / clipboard / disk-upload inputs simply do not exist on the
40
40
  // hosted connector. Fail here — before the confirm gate previews files and
@@ -161,7 +161,7 @@ export function registerGenerateTools(server, client) {
161
161
  'and avoids re-processing the full image each round; use gemini_image_edit for one-off edits or composing multiple distinct inputs. ' +
162
162
  'Gemini over-preserves the input; there is no edit-strength control — for large structural changes, reroll with a different `seed` or more forceful wording.',
163
163
  annotations: { readOnlyHint: false, openWorldHint: true },
164
- inputSchema: {
164
+ inputSchema: z.object({
165
165
  prompt: z.string().min(1).describe('Instruction describing the edit or composition'),
166
166
  images: z.array(z.string().min(1)).optional().describe('Paths to input image file(s) (1 = edit, 2+ = compose)'),
167
167
  images_url: imagesUrlSchema('Input images'),
@@ -173,7 +173,7 @@ export function registerGenerateTools(server, client) {
173
173
  filename: z.string().optional().describe('Base filename for the output image (extension stripped; default: slugified prompt)'),
174
174
  confirm: schemaConfirm,
175
175
  ...sharedImageSchema,
176
- },
176
+ }),
177
177
  }, async (args, extra) => {
178
178
  assertLocalInputsAvailable(client.mediaSink, args);
179
179
  // Saved characters count as input images for the required-input check —
@@ -91,7 +91,7 @@ export function registerInteractTools(server, client) {
91
91
  recoveryDescription +
92
92
  'Output is JPEG.',
93
93
  annotations: { readOnlyHint: false, openWorldHint: true },
94
- inputSchema: {
94
+ inputSchema: z.object({
95
95
  input: z.string().min(1).describe('Text prompt or editing instruction'),
96
96
  previous_interaction_id: z
97
97
  .string()
@@ -161,7 +161,7 @@ export function registerInteractTools(server, client) {
161
161
  .optional()
162
162
  .describe('Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)'),
163
163
  confirm: schemaConfirm,
164
- },
164
+ }),
165
165
  }, async (args, extra) => {
166
166
  assertLocalInputsAvailable(client.mediaSink, args);
167
167
  // Every sidecar-backed recovery below reads the output dir off disk. A
@@ -24,10 +24,10 @@ export function registerJobTools(server, client) {
24
24
  // NOT read-only any more: recovering a killed job writes the media it
25
25
  // pulls back, exactly as the generation tool would have.
26
26
  annotations: { readOnlyHint: false, openWorldHint: true },
27
- inputSchema: {
27
+ inputSchema: z.object({
28
28
  job_id: z.string().min(1).describe('The job_id returned by a generation tool called with async: true'),
29
29
  output_dir: z.string().optional().describe('Where to write media recovered from a killed job (default: $GEMINI_OUTPUT_DIR or cwd)'),
30
- },
30
+ }),
31
31
  }, async (args) => {
32
32
  try {
33
33
  return await client.session.jobs.getResult(args.job_id);
@@ -63,7 +63,7 @@ export function registerLibraryTools(server, client) {
63
63
  '— so a name saved once keeps working in every later session. Re-saving a name replaces it. ' +
64
64
  USE_CHARACTERS_HINT,
65
65
  annotations: { readOnlyHint: false, openWorldHint: false },
66
- inputSchema: {
66
+ inputSchema: z.object({
67
67
  name: NAME_FIELD,
68
68
  description: z
69
69
  .string()
@@ -75,7 +75,7 @@ export function registerLibraryTools(server, client) {
75
75
  .optional()
76
76
  .describe('Reference image already in this connector\'s store: an `r2_key` from gemini_get_upload_url + PUT, or from a generation result\'s media[].r2_key'),
77
77
  image_url: z.string().url().optional().describe('Reference image as a public https URL the SERVER fetches'),
78
- },
78
+ }),
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 });
@@ -93,7 +93,7 @@ export function registerLibraryTools(server, client) {
93
93
  description: 'List this account\'s saved characters (persistent reference library): names, descriptions and timestamps. ' +
94
94
  USE_CHARACTERS_HINT,
95
95
  annotations: { readOnlyHint: true, openWorldHint: false },
96
- inputSchema: {},
96
+ inputSchema: z.object({}),
97
97
  }, async () => {
98
98
  const characters = await library.listCharacters();
99
99
  return minifiedResult({
@@ -113,7 +113,7 @@ export function registerLibraryTools(server, client) {
113
113
  server.registerTool('gemini_delete_character', {
114
114
  description: 'Delete a saved character from this account\'s reference library. Generation calls naming it will then fail until it is re-saved.',
115
115
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
116
- inputSchema: { name: NAME_FIELD, confirm: schemaConfirm },
116
+ inputSchema: z.object({ name: NAME_FIELD, confirm: schemaConfirm }),
117
117
  }, async (args) => {
118
118
  const gate = previewUnlessConfirmed(args.confirm, 'Delete a saved character (its reference image and description)', 'DELETE', `characters/${args.name}`);
119
119
  if (gate)
@@ -129,7 +129,7 @@ export function registerLibraryTools(server, client) {
129
129
  'optionally a reference image) that a single word then applies to any generation. No expiry; re-saving a name replaces it. ' +
130
130
  USE_STYLE_HINT,
131
131
  annotations: { readOnlyHint: false, openWorldHint: false },
132
- inputSchema: {
132
+ inputSchema: z.object({
133
133
  name: NAME_FIELD,
134
134
  prompt_fragment: z
135
135
  .string()
@@ -137,7 +137,7 @@ export function registerLibraryTools(server, client) {
137
137
  .describe('The style text appended to generation prompts, e.g. "bold cartoon style, thick outlines, saturated colors"'),
138
138
  image_r2_key: z.string().min(1).optional().describe('Optional style reference image already in this connector\'s store (an r2_key)'),
139
139
  image_url: z.string().url().optional().describe('Optional style reference image as a public https URL the SERVER fetches'),
140
- },
140
+ }),
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 });
@@ -154,7 +154,7 @@ export function registerLibraryTools(server, client) {
154
154
  server.registerTool('gemini_list_styles', {
155
155
  description: 'List this account\'s saved style presets (persistent reference library). ' + USE_STYLE_HINT,
156
156
  annotations: { readOnlyHint: true, openWorldHint: false },
157
- inputSchema: {},
157
+ inputSchema: z.object({}),
158
158
  }, async () => {
159
159
  const styles = await library.listStyles();
160
160
  return minifiedResult({
@@ -172,7 +172,7 @@ export function registerLibraryTools(server, client) {
172
172
  server.registerTool('gemini_delete_style', {
173
173
  description: 'Delete a saved style preset from this account\'s reference library.',
174
174
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
175
- inputSchema: { name: NAME_FIELD, confirm: schemaConfirm },
175
+ inputSchema: z.object({ name: NAME_FIELD, confirm: schemaConfirm }),
176
176
  }, async (args) => {
177
177
  const gate = previewUnlessConfirmed(args.confirm, 'Delete a saved style preset', 'DELETE', `styles/${args.name}`);
178
178
  if (gate)
@@ -30,7 +30,7 @@ export function registerMusicTools(server, client) {
30
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
31
  'budget (or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account.',
32
32
  annotations: { readOnlyHint: false, openWorldHint: true },
33
- inputSchema: {
33
+ inputSchema: z.object({
34
34
  prompt: z.string().min(1).describe('Description of the music: mood, genre, instruments, tempo, structure, or lyrics'),
35
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.`),
36
36
  images: z.array(z.string().min(1)).optional().describe('Optional reference image path(s) to condition the music'),
@@ -47,7 +47,7 @@ export function registerMusicTools(server, client) {
47
47
  async: asyncSchema,
48
48
  max_wait_ms: maxWaitMsSchema,
49
49
  confirm: schemaConfirm,
50
- },
50
+ }),
51
51
  }, async (args, extra) => {
52
52
  assertLocalInputsAvailable(client.mediaSink, args);
53
53
  const model = args.model ?? DEFAULT_MUSIC_MODEL;
package/dist/tools/set.js CHANGED
@@ -13,7 +13,7 @@ export function registerSetTools(server, client) {
13
13
  description: 'Generate a consistent SET of images: a master image from master_prompt, then one image per scene that references the master so the subject/style stays consistent. Provide `scenes` (explicit per-image prompts) OR `count` (variations of the master). ' +
14
14
  'Scene generations run in parallel (reference_mode "master", the default). On the hosted connector: saved `characters` and a saved `style` can seed the whole set by name, multi-image results include a `bundle_url` zip of every image (one curl instead of N), and `max_wait_ms` returns a pollable job handle if the batch runs long.',
15
15
  annotations: { readOnlyHint: false, openWorldHint: true },
16
- inputSchema: {
16
+ inputSchema: z.object({
17
17
  master_prompt: z.string().min(1).describe('Prompt for the master/reference image'),
18
18
  scenes: z.array(z.string().min(1)).min(1).max(8).optional().describe('Per-image prompts (1-8); each references the master'),
19
19
  count: z.number().int().positive().max(8).optional().describe('Number of variations of master_prompt (when scenes omitted)'),
@@ -28,7 +28,7 @@ export function registerSetTools(server, client) {
28
28
  master_images_base64: imagesBase64Schema('Reference images for the master generation', 'master_images'),
29
29
  confirm: schemaConfirm,
30
30
  ...sharedImageSchema,
31
- },
31
+ }),
32
32
  }, async (args, extra) => {
33
33
  assertLocalInputsAvailable(client.mediaSink, args);
34
34
  // `scenes` is min(1) at the schema, so a non-undefined value is non-empty.
@@ -356,8 +356,8 @@ const HEARTBEAT_DEFAULT_MS = 10_000;
356
356
  * `fn`'s result/rejection passes through untouched.
357
357
  */
358
358
  export async function withProgressHeartbeat(extra, message, fn) {
359
- const progressToken = extra?._meta?.progressToken;
360
- const send = extra?.sendNotification;
359
+ const progressToken = extra?.mcpReq?._meta?.progressToken;
360
+ const send = extra?.mcpReq?.notify;
361
361
  const env = Number(readEnvVar('GEMINI_HEARTBEAT_MS'));
362
362
  const intervalMs = Number.isFinite(env) ? env : HEARTBEAT_DEFAULT_MS;
363
363
  const debug = !!readEnvVar('GEMINI_DEBUG');
@@ -376,7 +376,7 @@ export async function withProgressHeartbeat(extra, message, fn) {
376
376
  const reason = progressToken === undefined
377
377
  ? 'no progressToken from host (host cannot extend its tools/call timeout; long calls will hit it)'
378
378
  : !send
379
- ? 'host provided no sendNotification channel'
379
+ ? 'no mcpReq.notify on the request extra (SDK v2 renamed v1\'s sendNotification; without it there is no channel to send progress on)'
380
380
  : 'heartbeat disabled (GEMINI_HEARTBEAT_MS=0)';
381
381
  console.error(`[gemini-mcp] heartbeat inactive: ${reason} — ${message}`);
382
382
  }
@@ -52,7 +52,7 @@ export function registerUploadUrlTools(server, client) {
52
52
  `${wholeMb(UPLOAD_MAX_BYTES)}MB. The PUT must send exactly the Content-Type this URL was minted for — the signature ` +
53
53
  'covers it, so a mismatched header is rejected.',
54
54
  annotations: { readOnlyHint: true, openWorldHint: false },
55
- inputSchema: {
55
+ inputSchema: z.object({
56
56
  filename: z
57
57
  .string()
58
58
  .min(1)
@@ -64,7 +64,7 @@ export function registerUploadUrlTools(server, client) {
64
64
  // serves from the connector's own origin (see RASTER_IMAGE_TYPE_PATTERN).
65
65
  .regex(RASTER_IMAGE_TYPE_PATTERN, 'must be a raster image MIME type like image/jpeg (SVG is not accepted)')
66
66
  .describe('MIME type of the bytes that will be PUT — raster images only (jpeg/png/webp/gif/avif/heic/heif/bmp/tiff; not SVG). The PUT must send exactly this Content-Type.'),
67
- },
67
+ }),
68
68
  }, async (args) => {
69
69
  // `step` names the operation in the message, and `surfaceToolErrors`
70
70
  // (wired in index.ts) prefixes the tool and flattens the cause chain —
@@ -34,12 +34,12 @@ export function registerUsageTools(server, client) {
34
34
  'Call it before and after a workflow and subtract to get that workflow\'s cost; call it after a ' +
35
35
  'single generation for that call\'s. Reports tokens AND an estimated USD cost, priced per call against each call\'s own model and stamped with the date its rates were read (override with GEMINI_RATE_CARD). Note there is no account-balance endpoint to query — Google Cloud is post-paid and its billing data lags by hours — so this is the accurate way to attribute spend to a call.',
36
36
  annotations: toolAnnotations({ readOnly: true }),
37
- inputSchema: {
37
+ inputSchema: z.object({
38
38
  reset: z
39
39
  .boolean()
40
40
  .optional()
41
41
  .describe('Zero the running total after reporting it, so the next call measures from here. Use it to bracket a workflow without arithmetic.'),
42
- },
42
+ }),
43
43
  }, async (args) => {
44
44
  const usage = client.session.usageTotal;
45
45
  const calls = client.session.billedCalls;
@@ -38,7 +38,7 @@ export function registerVideoTools(server, client) {
38
38
  'Output is written to disk as MP4 (video has no inline MCP block). Video runs long — give it a `max_wait_ms` budget ' +
39
39
  '(or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account.',
40
40
  annotations: { readOnlyHint: false, openWorldHint: true },
41
- inputSchema: {
41
+ inputSchema: z.object({
42
42
  prompt: z.string().min(1).describe('Description of the video to generate (or the edit instruction when task=edit)'),
43
43
  aspect_ratio: z.enum(VIDEO_ASPECT_RATIOS).optional().describe('Exact output aspect ratio (omni: 16:9 or 9:16). `orientation` is the plain-language shorthand; this wins if both are given.'),
44
44
  orientation: orientationSchema,
@@ -65,7 +65,7 @@ export function registerVideoTools(server, client) {
65
65
  async: asyncSchema,
66
66
  max_wait_ms: maxWaitMsSchema,
67
67
  confirm: schemaConfirm,
68
- },
68
+ }),
69
69
  }, async (args, extra) => {
70
70
  assertLocalInputsAvailable(client.mediaSink, args);
71
71
  let previousInteractionId = args.previous_interaction_id;
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Single source of the server version. release-please bumps the literal below. */
2
- export const VERSION = '1.14.2'; // x-release-please-version
2
+ export const VERSION = '2.1.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/gemini-mcp",
3
- "version": "1.14.2",
3
+ "version": "2.1.0",
4
4
  "mcpName": "io.github.chrischall/gemini-mcp",
5
5
  "description": "Gemini image-generation MCP server for Claude — developed and maintained by AI (Claude Code)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38,7 +38,7 @@
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "tsc && npm run bundle",
41
- "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
41
+ "bundle": "esbuild src/index.ts --bundle --alias:zod/v4=./node_modules/zod/v4/index.cjs --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
42
42
  "dev": "node dist/index.js",
43
43
  "test": "npm run typecheck && vitest run",
44
44
  "test:watch": "vitest",
@@ -46,12 +46,13 @@
46
46
  "typecheck": "tsc -p tsconfig.test.json"
47
47
  },
48
48
  "dependencies": {
49
- "@chrischall/mcp-utils": "^0.26.1",
50
- "@modelcontextprotocol/sdk": "^1.30.0",
49
+ "@chrischall/mcp-utils": "^1.0.0",
50
+ "@modelcontextprotocol/server": "^2.0.0",
51
51
  "dotenv": "^17.4.0",
52
- "zod": "^4.5.4"
52
+ "zod": "^4.6.2"
53
53
  },
54
54
  "devDependencies": {
55
+ "@modelcontextprotocol/client": "^2.0.0",
55
56
  "@types/node": "^26.0.0",
56
57
  "@vitest/coverage-v8": "^5.0.0",
57
58
  "esbuild": "^0.28.0",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/gemini-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.14.2",
9
+ "version": "2.1.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/gemini-mcp",
14
- "version": "1.14.2",
14
+ "version": "2.1.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },