@chrischall/gemini-mcp 1.11.1 → 1.13.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/dist/client.js CHANGED
@@ -287,6 +287,17 @@ export class GeminiClient {
287
287
  * That is an invisible, silent failure (the key just stops working); not
288
288
  * depending on the order removes the hazard instead of documenting it.
289
289
  */
290
+ /**
291
+ * Which source supplies the API key, for `gemini_healthcheck` — a LABEL or
292
+ * `null`, never the key. Mirrors `requireKey`'s resolution order exactly, so
293
+ * the healthcheck reports the same source the real tools will use, but does
294
+ * NOT throw: "no key" is an answer here rather than an error.
295
+ */
296
+ describeCredential() {
297
+ if (this.explicitApiKey)
298
+ return { source: 'connector-session' };
299
+ return { source: readEnvVar('GEMINI_API_KEY') ? 'env' : null };
300
+ }
290
301
  requireKey() {
291
302
  const key = this.explicitApiKey || readEnvVar('GEMINI_API_KEY');
292
303
  if (!key) {
package/dist/jobs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomUUID, createHash } from 'node:crypto';
2
- import { textResult, McpToolError } from '@chrischall/mcp-utils';
2
+ import { McpToolError, minifiedResult } from '@chrischall/mcp-utils';
3
3
  import { downloadFilename } from './media-name.js';
4
4
  import { HEARTBEAT_MS } from './job-store.js';
5
5
  import { annotateReusedUsage } from './usage.js';
@@ -151,7 +151,7 @@ function annotateReused(result, jobId) {
151
151
  }
152
152
  /** Immediate handle for an async call — the caller polls `gemini_get_result`. */
153
153
  function jobHandle(jobId, status, durable = false) {
154
- return textResult({
154
+ return minifiedResult({
155
155
  job_id: jobId,
156
156
  status,
157
157
  hint: durable
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { surfaceToolErrors } from './errors.js';
13
13
  import { registerModelTools } from './tools/models.js';
14
+ import { registerHealthcheckTools } from './tools/healthcheck.js';
14
15
  import { registerGenerateTools } from './tools/generate.js';
15
16
  import { registerSetTools } from './tools/set.js';
16
17
  import { registerInteractTools } from './tools/interact.js';
@@ -39,6 +40,7 @@ export const reporting = (register) => (server, deps) => register(surfaceToolErr
39
40
  */
40
41
  export const TOOL_REGISTRARS = [
41
42
  registerModelTools,
43
+ registerHealthcheckTools,
42
44
  registerGenerateTools,
43
45
  registerSetTools,
44
46
  registerInteractTools,
@@ -1,4 +1,4 @@
1
- import { schemaConfirm, textResult } from '@chrischall/mcp-utils';
1
+ import { minifiedResult, schemaConfirm } from '@chrischall/mcp-utils';
2
2
  import { previewImageInput, previewVideoInput } from '../images.js';
3
3
  export { schemaConfirm };
4
4
  /**
@@ -9,7 +9,7 @@ export { schemaConfirm };
9
9
  export function previewUnlessConfirmed(confirm, action, method, path, body) {
10
10
  if (confirm === true)
11
11
  return null;
12
- return textResult({
12
+ return minifiedResult({
13
13
  dryRun: true,
14
14
  action,
15
15
  method,
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { McpToolError, textResult } from '@chrischall/mcp-utils';
2
+ import { McpToolError, minifiedResult } from '@chrischall/mcp-utils';
3
3
  import { withProgressHeartbeat } from './shared.js';
4
4
  import { base64ToBytes, wholeMb } from '../bytes.js';
5
5
  import { fileNameFromUrl } from '../fetch-image.js';
@@ -47,7 +47,7 @@ const RETENTION_NOTE = 'Gemini retains uploaded files for ~48h. Until then this
47
47
  '(images_file_uris on gemini_image_generate / _edit / _set / gemini_interact, or video_url) without re-sending the bytes.';
48
48
  /** Shape returned by every upload path. */
49
49
  function uploadResult(file) {
50
- return textResult({
50
+ return minifiedResult({
51
51
  // The `files/<id>` form is what every tool parameter documents, so it leads.
52
52
  file_uri: file.name,
53
53
  // The absolute uri, for callers that would rather pass it through verbatim.
@@ -178,7 +178,7 @@ export function registerFileTools(server, client) {
178
178
  },
179
179
  }, async (args) => {
180
180
  const files = await client.listFiles(args.page_size ?? 100);
181
- return textResult({
181
+ return minifiedResult({
182
182
  count: files.length,
183
183
  files: files.map((f) => ({
184
184
  file_uri: f.name,
@@ -220,7 +220,7 @@ export function registerFileTools(server, client) {
220
220
  // Remediation goes in the MESSAGE: MCP serialization drops hints.
221
221
  throw new McpToolError(`No stored media for r2_key "${args.r2_key}" — it has probably passed its retention window. Re-run the generation to get a new one.`, { hint: 'Objects are swept on a retention schedule (MEDIA_TTL_DAYS, default 7 days).' });
222
222
  }
223
- return textResult({
223
+ return minifiedResult({
224
224
  url: fresh.ref,
225
225
  r2_key: fresh.key,
226
226
  ...(fresh.expiresAt ? { expires_at: fresh.expiresAt } : {}),
@@ -258,7 +258,7 @@ export function registerFileTools(server, client) {
258
258
  ? await sink.listRecentPage({ limit, sinceDay: args.since_day })
259
259
  : { media: await sink.listRecent({ limit, sinceDay: args.since_day }), truncated: false };
260
260
  const found = page.media;
261
- return textResult({
261
+ return minifiedResult({
262
262
  count: found.length,
263
263
  // Never let a bounded walk pass for the whole picture: concluding an
264
264
  // image is gone is what makes someone pay to generate it twice.
@@ -293,6 +293,6 @@ export function registerFileTools(server, client) {
293
293
  if (gate)
294
294
  return gate;
295
295
  await client.deleteFile(args.file_uri);
296
- return textResult({ deleted: args.file_uri });
296
+ return minifiedResult({ deleted: args.file_uri });
297
297
  });
298
298
  }
@@ -0,0 +1,29 @@
1
+ import { registerCredentialHealthcheckTool } from '@chrischall/mcp-utils/healthcheck';
2
+ /**
3
+ * Register `gemini_healthcheck` — resolves the API key the way real tools do,
4
+ * then makes one authenticated call to `/models`.
5
+ *
6
+ * Gemini has no browser bridge, so health here is entirely about the
7
+ * credential: whether one resolved, and whether Google still accepts it. Those
8
+ * are different problems with different fixes, and without this both surface
9
+ * as the same opaque tool error.
10
+ *
11
+ * `/models` is the probe because it is the cheapest endpoint that REQUIRES a
12
+ * key — an unauthenticated one would pass while the key was revoked, which is
13
+ * the failure most worth catching. It is also read-only and free, so a
14
+ * healthcheck never costs a generation.
15
+ */
16
+ export function registerHealthcheckTools(server, client) {
17
+ registerCredentialHealthcheckTool({
18
+ server,
19
+ prefix: 'gemini',
20
+ hostLabel: 'generativelanguage.googleapis.com',
21
+ probePath: '/v1beta/models',
22
+ resolveCredential: async () => client.describeCredential(),
23
+ probeFn: () => client.listModels(),
24
+ hints: {
25
+ no_credential: 'No Gemini API key resolved. Set GEMINI_API_KEY (https://aistudio.google.com/apikey), or reconnect the connector so it supplies one.',
26
+ credential_rejected: 'Google rejected the API key. It is present but no longer valid — most often revoked, or restricted to different APIs/referrers. Issue a new key at https://aistudio.google.com/apikey.',
27
+ },
28
+ });
29
+ }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { McpToolError, textResult } from '@chrischall/mcp-utils';
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 textResult({
131
+ return minifiedResult({
132
132
  job_id: jobId,
133
133
  status: 'running',
134
134
  interaction_id: interactionId,
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { McpToolError, textResult } from '@chrischall/mcp-utils';
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 textResult({
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 textResult({
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 textResult({ deleted: 'character', name: args.name });
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 textResult({
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 textResult({
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 textResult({ deleted: 'style', name: args.name });
184
+ return minifiedResult({ deleted: 'style', name: args.name });
185
185
  });
186
186
  }
@@ -1,10 +1,10 @@
1
- import { textResult } from '@chrischall/mcp-utils';
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 textResult({ default: client.defaultModel(), models });
8
+ return minifiedResult({ default: client.defaultModel(), models });
9
9
  });
10
10
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textResult, McpToolError, readEnvVar } from '@chrischall/mcp-utils';
2
+ import { McpToolError, minifiedResult, readEnvVar } from '@chrischall/mcp-utils';
3
3
  import { resolveVideoPath, videoMimeType } from '../images.js';
4
4
  import { IMAGE_URL_MAX_BYTES, IMAGE_INLINE_MAX_BYTES } from '../fetch-image.js';
5
5
  import { bytesToBase64, base64ToBytes, wholeMb } from '../bytes.js';
@@ -535,7 +535,7 @@ export async function emitMedia(named, kind, opts, meta, onWritten) {
535
535
  }
536
536
  : {};
537
537
  const storage = sink.persistsFiles ? {} : { storage: sink.kind, storage_note: storageNote };
538
- return textResult({ [`${kind}s`]: refs, ...media, ...unavailable, ...note, ...storage, ...meta });
538
+ return minifiedResult({ [`${kind}s`]: refs, ...media, ...unavailable, ...note, ...storage, ...meta });
539
539
  }
540
540
  /**
541
541
  * Image convenience wrapper over {@link emitMedia} — preserves the original
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textResult } from '@chrischall/mcp-utils';
2
+ import { minifiedResult } from '@chrischall/mcp-utils';
3
3
  import { UPLOAD_MAX_BYTES, RASTER_IMAGE_TYPE_PATTERN } from '../upload-url.js';
4
4
  import { wholeMb } from '../bytes.js';
5
5
  import { step } from '../errors.js';
@@ -71,7 +71,7 @@ export function registerUploadUrlTools(server, client) {
71
71
  // between them a mint failure can no longer reach the client as a bare
72
72
  // "Error occurred during tool execution".
73
73
  const minted = await step(`minting a signed upload URL for ${args.filename} (${args.content_type})`, () => minter.mint(args.filename, args.content_type));
74
- return textResult({
74
+ return minifiedResult({
75
75
  upload_url: minted.url,
76
76
  method: 'PUT',
77
77
  r2_key: minted.key,
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textResult, toolAnnotations } from '@chrischall/mcp-utils';
2
+ import { minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
3
3
  import { PRICED_AT } from '../pricing.js';
4
4
  /**
5
5
  * What this session has spent, in tokens.
@@ -51,7 +51,7 @@ export function registerUsageTools(server, client) {
51
51
  client.session.costUsd = undefined;
52
52
  client.session.pricedCalls = 0;
53
53
  }
54
- return textResult({
54
+ return minifiedResult({
55
55
  usage: usage ?? null,
56
56
  billed_calls: calls,
57
57
  estimated_cost_usd: costUsd ?? null,
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.11.1'; // x-release-please-version
2
+ export const VERSION = '1.13.0'; // x-release-please-version
package/mint.yaml CHANGED
@@ -38,6 +38,12 @@ env:
38
38
  How often, in milliseconds, to emit a progress notification while a
39
39
  generation is in flight. Hosts reset their tools/call timeout on progress,
40
40
  so this lets a long 4K/Pro generation outlive the default budget.
41
+ - name: GEMINI_RATE_CARD
42
+ required: false
43
+ help: >-
44
+ JSON overriding or extending the built-in pricing card, so a stale rate is
45
+ yours to fix in your own environment rather than waiting on a release.
46
+ Anything absent from the card prices as unknown rather than zero.
41
47
  egress:
42
48
  allow:
43
49
  # Only hosts the SERVER process actually fetches. Hosts that appear
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/gemini-mcp",
3
- "version": "1.11.1",
3
+ "version": "1.13.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>",
@@ -46,7 +46,7 @@
46
46
  "typecheck": "tsc -p tsconfig.test.json"
47
47
  },
48
48
  "dependencies": {
49
- "@chrischall/mcp-utils": "^0.15.0",
49
+ "@chrischall/mcp-utils": "^0.23.0",
50
50
  "@modelcontextprotocol/sdk": "^1.29.0",
51
51
  "dotenv": "^17.4.0",
52
52
  "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": "1.11.1",
9
+ "version": "1.13.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/gemini-mcp",
14
- "version": "1.11.1",
14
+ "version": "1.13.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },