@chrischall/gemini-mcp 0.7.2 → 0.8.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.
@@ -1,16 +1,11 @@
1
1
  import { z } from 'zod';
2
- import { textResult, McpToolError } from '@chrischall/mcp-utils';
2
+ import { textResult, McpToolError, readEnvVar } from '@chrischall/mcp-utils';
3
3
  import { client } from '../client.js';
4
4
  import { writeImage, resolveOutputDir, resolveVideoPath, videoMimeType } from '../images.js';
5
5
  /** Supported output aspect ratios (Gemini image API). */
6
6
  export const ASPECT_RATIOS = ['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9', '1:4', '4:1', '1:8', '8:1'];
7
7
  /** Supported output resolutions. `512` is Flash-only (0.5K); `1K`/`2K`/`4K` work on all image models. */
8
8
  export const IMAGE_SIZES = ['512', '1K', '2K', '4K'];
9
- /**
10
- * The model/aspect/size/output fields every generation tool shares. Defined once
11
- * here so descriptions can't drift between `generate.ts` and `set.ts`. Spread
12
- * into each tool's `inputSchema`.
13
- */
14
9
  /**
15
10
  * When to pick which Nano Banana model — shared by every tool's `model` param
16
11
  * so the guidance can't drift between them.
@@ -20,6 +15,22 @@ export const MODEL_CHOICE_GUIDE = 'gemini-3.1-flash-image (Nano Banana 2) is the
20
15
  'processing and consistency. gemini-3-pro-image (Nano Banana Pro) is the premium choice for the most complex ' +
21
16
  'visual tasks — highest world knowledge, advanced localization, accurate brand consistency, precision creative control. ' +
22
17
  'gemini-3.1-flash-lite-image (Nano Banana 2 Lite) is the fastest/cheapest for simple tasks (1K only, no search grounding).';
18
+ /**
19
+ * Per-call upstream timeout override, shared by every generation tool
20
+ * (spread via {@link sharedImageSchema}; `gemini_interact` imports it directly
21
+ * since its schema doesn't take the seed/clipboard fields).
22
+ */
23
+ export const timeoutMsSchema = z
24
+ .number()
25
+ .int()
26
+ .positive()
27
+ .optional()
28
+ .describe('Upstream request timeout in ms for this call (default: $GEMINI_TIMEOUT_MS, else 60000 — or 120000 when image_size is 4K, which routinely runs past 60s)');
29
+ /**
30
+ * The model/aspect/size/output fields every generation tool shares. Defined once
31
+ * here so descriptions can't drift between `generate.ts` and `set.ts`. Spread
32
+ * into each tool's `inputSchema`.
33
+ */
23
34
  export const sharedImageSchema = {
24
35
  // Bare id only: the model is interpolated into the URL path
25
36
  // (`/models/${model}:generateContent`), so slashes/colons/queries must be
@@ -37,6 +48,7 @@ export const sharedImageSchema = {
37
48
  thinking_level: z.enum(['minimal', 'high']).optional().describe('Reasoning depth (Gemini 3 models); higher can help complex/structural edits'),
38
49
  google_search: z.boolean().optional().describe('Ground the image in live Google Search results (current events, weather, data)'),
39
50
  from_clipboard: z.boolean().optional().describe('Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)'),
51
+ timeout_ms: timeoutMsSchema,
40
52
  };
41
53
  /**
42
54
  * Turn the `video_url` / `video_path` tool args into the client's video opts.
@@ -71,6 +83,45 @@ export const videoPathSchema = z
71
83
  export function pickSeed(seed) {
72
84
  return seed ?? Math.floor(Math.random() * 2_147_483_000);
73
85
  }
86
+ // Progress-heartbeat cadence while a generation is in flight. MCP hosts
87
+ // enforce their own tools/call timeout (surfaced as -32001) but reset it on
88
+ // notifications/progress, so a heartbeat lets a 4K/Pro generation outlive the
89
+ // host's default budget. Tunable via $GEMINI_HEARTBEAT_MS (0 disables).
90
+ const HEARTBEAT_DEFAULT_MS = 10_000;
91
+ /**
92
+ * Run `fn`, emitting a `notifications/progress` heartbeat while it is pending —
93
+ * but only when the caller asked for progress (sent a `progressToken`).
94
+ * Send failures are swallowed (the client may have abandoned the request);
95
+ * `fn`'s result/rejection passes through untouched.
96
+ */
97
+ export async function withProgressHeartbeat(extra, message, fn) {
98
+ const progressToken = extra?._meta?.progressToken;
99
+ const send = extra?.sendNotification;
100
+ const env = Number(readEnvVar('GEMINI_HEARTBEAT_MS'));
101
+ const intervalMs = Number.isFinite(env) ? env : HEARTBEAT_DEFAULT_MS;
102
+ if (progressToken === undefined || !send || intervalMs <= 0)
103
+ return fn();
104
+ let ticks = 0;
105
+ const timer = setInterval(() => {
106
+ ticks++;
107
+ void send({
108
+ method: 'notifications/progress',
109
+ params: {
110
+ progressToken,
111
+ progress: ticks,
112
+ message: `${message} — still working (${Math.round((ticks * intervalMs) / 1000)}s)`,
113
+ },
114
+ }).catch(() => { });
115
+ }, intervalMs);
116
+ // Never hold the process open just for a heartbeat.
117
+ timer.unref?.();
118
+ try {
119
+ return await fn();
120
+ }
121
+ finally {
122
+ clearInterval(timer);
123
+ }
124
+ }
74
125
  /** Build the result metadata echoed to the caller (omitting unset optionals). */
75
126
  export function buildMeta(model, seed, opts) {
76
127
  const meta = { model, seed };
@@ -86,7 +137,8 @@ export function buildMeta(model, seed, opts) {
86
137
  * Optional `meta` is merged into the disk-path JSON result, or prepended as a
87
138
  * text block in inline mode (only if meta has keys).
88
139
  * Optional `onWritten` receives the absolute paths written in disk mode (never
89
- * called inline) — lets a tool remember its own outputs.
140
+ * called inline) — lets a tool remember its own outputs or write sidecar
141
+ * files; an async callback is awaited before the result is returned.
90
142
  */
91
143
  export async function emit(named, opts, meta, onWritten) {
92
144
  if (opts.inline) {
@@ -102,6 +154,6 @@ export async function emit(named, opts, meta, onWritten) {
102
154
  const images = [];
103
155
  for (const n of named)
104
156
  images.push(await writeImage(dir, n.base, n.image.base64, n.image.mimeType));
105
- onWritten?.(images);
157
+ await onWritten?.(images);
106
158
  return textResult({ images, ...meta });
107
159
  }
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 = '0.7.2'; // x-release-please-version
2
+ export const VERSION = '0.8.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/gemini-mcp",
3
- "version": "0.7.2",
3
+ "version": "0.8.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>",
@@ -44,7 +44,7 @@
44
44
  "test:coverage": "vitest run --coverage"
45
45
  },
46
46
  "dependencies": {
47
- "@chrischall/mcp-utils": "^0.10.3",
47
+ "@chrischall/mcp-utils": "^0.12.0",
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
49
  "dotenv": "^17.4.0",
50
50
  "zod": "^4.4.2"
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": "0.7.2",
9
+ "version": "0.8.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/gemini-mcp",
14
- "version": "0.7.2",
14
+ "version": "0.8.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },