@chrischall/gemini-mcp 1.11.1 → 1.12.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Google Gemini image generation and editing — generate and edit images via the Gemini API",
10
- "version": "1.11.1"
10
+ "version": "1.12.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Gemini Images",
16
16
  "source": "./",
17
17
  "description": "Generate and edit images with Google Gemini image models (Nano Banana / Nano Banana Pro) via natural language prompts",
18
- "version": "1.11.1",
18
+ "version": "1.12.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gemini-mcp",
3
3
  "displayName": "Gemini Images",
4
- "version": "1.11.1",
4
+ "version": "1.12.0",
5
5
  "description": "MCP server for Google Gemini image generation and editing — generate and edit images via the Gemini API",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/bundle.js CHANGED
@@ -32110,6 +32110,11 @@ function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
32110
32110
  return redacted;
32111
32111
  return `${redacted.slice(0, max)}\u2026 [truncated]`;
32112
32112
  }
32113
+ function messageOf(err) {
32114
+ if (err instanceof Error)
32115
+ return err.message;
32116
+ return String(err);
32117
+ }
32113
32118
  var DEFAULT_ERROR_MESSAGE_MAX, McpToolError, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, QUERY_SECRET_RE, AWS_SIGV4_RE, JSON_SECRET_KEYS, JSON_SECRET_DQ_RE, JSON_SECRET_SQ_RE;
32114
32119
  var init_errors4 = __esm({
32115
32120
  "node_modules/@chrischall/mcp-utils/dist/errors/index.js"() {
@@ -32876,7 +32881,7 @@ async function loadStdioDotenv() {
32876
32881
  }
32877
32882
 
32878
32883
  // src/version.ts
32879
- var VERSION = "1.11.1";
32884
+ var VERSION = "1.12.0";
32880
32885
 
32881
32886
  // src/errors.ts
32882
32887
  init_dist();
@@ -32952,6 +32957,135 @@ function registerModelTools(server, client2) {
32952
32957
  );
32953
32958
  }
32954
32959
 
32960
+ // node_modules/@chrischall/mcp-utils/dist/healthcheck/index.js
32961
+ init_errors4();
32962
+ function statusOf(err) {
32963
+ if (typeof err !== "object" || err === null)
32964
+ return void 0;
32965
+ const s = err.status ?? err.statusCode;
32966
+ return typeof s === "number" ? s : void 0;
32967
+ }
32968
+ function credentialHint(arm, prefix, hostLabel, source) {
32969
+ switch (arm) {
32970
+ case "ok":
32971
+ return `Credential from '${source}' works: ${hostLabel} accepted an authenticated request. If a real tool still fails, the problem is that tool, not auth.`;
32972
+ case "no_credential":
32973
+ return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
32974
+ case "credential_rejected":
32975
+ return `${hostLabel} rejected the credential from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
32976
+ case "timeout":
32977
+ return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
32978
+ case "http":
32979
+ return `${hostLabel} answered with an error status that is not an auth rejection. That is USUALLY a ${hostLabel}-side problem rather than an auth one \u2014 but a 404 here more often means the probe path is wrong than that ${hostLabel} is broken, so check error.message and probe.url before concluding anything about the credential.`;
32980
+ case "transport":
32981
+ return `Could not reach ${hostLabel} at all. Check network egress; the credential itself was never judged.`;
32982
+ default:
32983
+ return `Unexpected failure \u2014 see error.message.`;
32984
+ }
32985
+ }
32986
+ function registerCredentialHealthcheckTool(args) {
32987
+ const { server, prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
32988
+ const probeUrl = probePath ? `https://${hostLabel}${probePath}` : void 0;
32989
+ server.registerTool(`${prefix}_healthcheck`, {
32990
+ title: "Verify credentials and upstream reachability",
32991
+ description: `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Call this when a real tool fails and you want to know which hop broke. Read-only; never returns the credential itself.`,
32992
+ annotations: {
32993
+ title: "Verify credentials and upstream reachability",
32994
+ readOnlyHint: true,
32995
+ idempotentHint: true,
32996
+ openWorldHint: true
32997
+ },
32998
+ inputSchema: {}
32999
+ }, async () => {
33000
+ let probeStarted = 0;
33001
+ let state;
33002
+ try {
33003
+ state = await resolveCredential();
33004
+ } catch (e) {
33005
+ const result2 = {
33006
+ ok: false,
33007
+ credential: { source: null, resolved: false },
33008
+ // No `url`: nothing was probed, and naming one implies it was tried.
33009
+ probe: { elapsed_ms: 0 },
33010
+ error: { kind: "no_credential", message: truncateErrorMessage(messageOf(e)) },
33011
+ hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
33012
+ };
33013
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
33014
+ }
33015
+ const credential = {
33016
+ source: state.source,
33017
+ resolved: state.source !== null,
33018
+ ...state.detail !== void 0 ? { detail: state.detail } : {}
33019
+ };
33020
+ if (!credential.resolved) {
33021
+ const result2 = {
33022
+ ok: false,
33023
+ credential,
33024
+ probe: { elapsed_ms: 0 },
33025
+ error: { kind: "no_credential", message: "no credential source resolved" },
33026
+ hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
33027
+ };
33028
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
33029
+ }
33030
+ let arm = "ok";
33031
+ let error51;
33032
+ let status;
33033
+ let customHint;
33034
+ probeStarted = Date.now();
33035
+ try {
33036
+ await probeFn();
33037
+ } catch (e) {
33038
+ status = statusOf(e);
33039
+ const aborted2 = e instanceof Error && e.name === "AbortError";
33040
+ arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
33041
+ let kind = arm;
33042
+ let detail;
33043
+ const custom2 = classifyThrown?.(e);
33044
+ if (custom2) {
33045
+ kind = custom2.kind;
33046
+ customHint = custom2.hint;
33047
+ detail = custom2.detail;
33048
+ }
33049
+ error51 = {
33050
+ kind,
33051
+ // Redacted AND bounded before it reaches the result: an upstream
33052
+ // failure routinely quotes what it was sent, and a healthcheck is
33053
+ // the tool people paste into a chat when something is broken.
33054
+ message: truncateErrorMessage(messageOf(e)),
33055
+ ...detail !== void 0 ? { detail } : {}
33056
+ };
33057
+ }
33058
+ const result = {
33059
+ ok: error51 === void 0,
33060
+ credential,
33061
+ probe: {
33062
+ ...probeUrl ? { url: probeUrl } : {},
33063
+ elapsed_ms: Date.now() - probeStarted,
33064
+ ...status !== void 0 ? { status } : {}
33065
+ },
33066
+ ...error51 ? { error: error51 } : {},
33067
+ hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
33068
+ };
33069
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
33070
+ });
33071
+ }
33072
+
33073
+ // src/tools/healthcheck.ts
33074
+ function registerHealthcheckTools(server, client2) {
33075
+ registerCredentialHealthcheckTool({
33076
+ server,
33077
+ prefix: "gemini",
33078
+ hostLabel: "generativelanguage.googleapis.com",
33079
+ probePath: "/v1beta/models",
33080
+ resolveCredential: async () => client2.describeCredential(),
33081
+ probeFn: () => client2.listModels(),
33082
+ hints: {
33083
+ no_credential: "No Gemini API key resolved. Set GEMINI_API_KEY (https://aistudio.google.com/apikey), or reconnect the connector so it supplies one.",
33084
+ credential_rejected: "Google rejected the API key. It is present but no longer valid \u2014 most often revoked, or restricted to different APIs/referrers. Issue a new key at https://aistudio.google.com/apikey."
33085
+ }
33086
+ });
33087
+ }
33088
+
32955
33089
  // src/tools/generate.ts
32956
33090
  init_zod();
32957
33091
  init_dist();
@@ -35551,6 +35685,16 @@ var GeminiClient = class {
35551
35685
  * That is an invisible, silent failure (the key just stops working); not
35552
35686
  * depending on the order removes the hazard instead of documenting it.
35553
35687
  */
35688
+ /**
35689
+ * Which source supplies the API key, for `gemini_healthcheck` — a LABEL or
35690
+ * `null`, never the key. Mirrors `requireKey`'s resolution order exactly, so
35691
+ * the healthcheck reports the same source the real tools will use, but does
35692
+ * NOT throw: "no key" is an answer here rather than an error.
35693
+ */
35694
+ describeCredential() {
35695
+ if (this.explicitApiKey) return { source: "connector-session" };
35696
+ return { source: readEnvVar("GEMINI_API_KEY") ? "env" : null };
35697
+ }
35554
35698
  requireKey() {
35555
35699
  const key = this.explicitApiKey || readEnvVar("GEMINI_API_KEY");
35556
35700
  if (!key) {
@@ -37278,6 +37422,7 @@ function registerLibraryTools(server, client2) {
37278
37422
  var reporting = (register) => (server, deps) => register(surfaceToolErrors(server), deps);
37279
37423
  var TOOL_REGISTRARS = [
37280
37424
  registerModelTools,
37425
+ registerHealthcheckTools,
37281
37426
  registerGenerateTools,
37282
37427
  registerSetTools,
37283
37428
  registerInteractTools,
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) {
@@ -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,
@@ -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
+ }
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.12.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/gemini-mcp",
3
- "version": "1.11.1",
3
+ "version": "1.12.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.19.2",
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.12.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/gemini-mcp",
14
- "version": "1.11.1",
14
+ "version": "1.12.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },