@chrischall/gemini-mcp 1.10.0 → 1.11.1
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 +1 -0
- package/dist/bundle.js +339 -7
- package/dist/client.js +23 -4
- package/dist/jobs.js +4 -0
- package/dist/pricing.js +201 -0
- package/dist/registrars.js +2 -0
- package/dist/session.js +52 -0
- package/dist/tools/generate.js +13 -0
- package/dist/tools/interact.js +4 -0
- package/dist/tools/music.js +4 -0
- package/dist/tools/set.js +17 -2
- package/dist/tools/usage.js +71 -0
- package/dist/tools/video.js +4 -0
- package/dist/usage.js +156 -0
- package/dist/version.js +1 -1
- package/mint.yaml +12 -6
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/usage.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a generation cost, in tokens.
|
|
3
|
+
*
|
|
4
|
+
* Both Gemini APIs already report this and this server used to discard it. The
|
|
5
|
+
* two disagree on more than casing: `generateContent` returns camelCase
|
|
6
|
+
* `usageMetadata` with `promptTokenCount` / `candidatesTokenCount`, while the
|
|
7
|
+
* Interactions API returns snake_case `usage` with `total_input_tokens` /
|
|
8
|
+
* `total_output_tokens` plus cached- and thought-token counts that
|
|
9
|
+
* `generateContent` has no equivalent for. This module is the one place that
|
|
10
|
+
* knows both, so nothing downstream has to.
|
|
11
|
+
*
|
|
12
|
+
* Absence is preserved deliberately. A response with no usage block yields
|
|
13
|
+
* `undefined`, never a zeroed record: "we do not know what this cost" and "this
|
|
14
|
+
* cost nothing" are different claims, and a caller adding up a workflow has to
|
|
15
|
+
* be able to tell them apart.
|
|
16
|
+
*/
|
|
17
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
|
|
18
|
+
/**
|
|
19
|
+
* Pull one modality's entry out of either API's by-modality list.
|
|
20
|
+
*
|
|
21
|
+
* Modality matters for money, not just curiosity: each is billed at its own
|
|
22
|
+
* rate, and they differ by more than an order of magnitude (text output vs
|
|
23
|
+
* image output is 20-40x on the same model). A modality nobody breaks out is
|
|
24
|
+
* priced as text, which is the cheap end — so missing one understates rather
|
|
25
|
+
* than inventing spend.
|
|
26
|
+
*/
|
|
27
|
+
function modalityTokens(list, modality) {
|
|
28
|
+
if (!Array.isArray(list))
|
|
29
|
+
return undefined;
|
|
30
|
+
for (const entry of list) {
|
|
31
|
+
if (!entry || typeof entry !== 'object')
|
|
32
|
+
continue;
|
|
33
|
+
const e = entry;
|
|
34
|
+
// generateContent shouts ("IMAGE"); the Interactions API does not ("image").
|
|
35
|
+
if (typeof e.modality === 'string' && e.modality.toLowerCase() === modality) {
|
|
36
|
+
return num(e.tokenCount) ?? num(e.tokens);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read whichever usage block a response carries, or `undefined` when it
|
|
43
|
+
* carries neither — including the case where the block exists but has no
|
|
44
|
+
* totals in it, which is indistinguishable from absent for our purposes.
|
|
45
|
+
*/
|
|
46
|
+
export function readUsage(body) {
|
|
47
|
+
if (!body || typeof body !== 'object')
|
|
48
|
+
return undefined;
|
|
49
|
+
const b = body;
|
|
50
|
+
const meta = b.usageMetadata;
|
|
51
|
+
if (meta) {
|
|
52
|
+
const input = num(meta.promptTokenCount);
|
|
53
|
+
const output = num(meta.candidatesTokenCount);
|
|
54
|
+
const total = num(meta.totalTokenCount);
|
|
55
|
+
if (input !== undefined || output !== undefined || total !== undefined) {
|
|
56
|
+
return compact({
|
|
57
|
+
input_tokens: input ?? 0,
|
|
58
|
+
output_tokens: output ?? 0,
|
|
59
|
+
total_tokens: total ?? (input ?? 0) + (output ?? 0),
|
|
60
|
+
image_tokens: modalityTokens(meta.candidatesTokensDetails, 'image'),
|
|
61
|
+
video_tokens: modalityTokens(meta.candidatesTokensDetails, 'video'),
|
|
62
|
+
audio_tokens: modalityTokens(meta.candidatesTokensDetails, 'audio'),
|
|
63
|
+
cached_tokens: num(meta.cachedContentTokenCount),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const usage = b.usage;
|
|
68
|
+
if (usage) {
|
|
69
|
+
const input = num(usage.total_input_tokens);
|
|
70
|
+
const output = num(usage.total_output_tokens);
|
|
71
|
+
const total = num(usage.total_tokens);
|
|
72
|
+
if (input !== undefined || output !== undefined || total !== undefined) {
|
|
73
|
+
return compact({
|
|
74
|
+
input_tokens: input ?? 0,
|
|
75
|
+
output_tokens: output ?? 0,
|
|
76
|
+
total_tokens: total ?? (input ?? 0) + (output ?? 0),
|
|
77
|
+
image_tokens: modalityTokens(usage.output_tokens_by_modality, 'image'),
|
|
78
|
+
video_tokens: modalityTokens(usage.output_tokens_by_modality, 'video'),
|
|
79
|
+
audio_tokens: modalityTokens(usage.output_tokens_by_modality, 'audio'),
|
|
80
|
+
cached_tokens: num(usage.total_cached_tokens),
|
|
81
|
+
thought_tokens: num(usage.total_thought_tokens),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/** Drop the optional counts that are absent or zero — a constant 0 is noise. */
|
|
88
|
+
function compact(u) {
|
|
89
|
+
const out = {
|
|
90
|
+
input_tokens: u.input_tokens,
|
|
91
|
+
output_tokens: u.output_tokens,
|
|
92
|
+
total_tokens: u.total_tokens,
|
|
93
|
+
};
|
|
94
|
+
if (u.image_tokens)
|
|
95
|
+
out.image_tokens = u.image_tokens;
|
|
96
|
+
if (u.video_tokens)
|
|
97
|
+
out.video_tokens = u.video_tokens;
|
|
98
|
+
if (u.audio_tokens)
|
|
99
|
+
out.audio_tokens = u.audio_tokens;
|
|
100
|
+
if (u.cached_tokens)
|
|
101
|
+
out.cached_tokens = u.cached_tokens;
|
|
102
|
+
if (u.thought_tokens)
|
|
103
|
+
out.thought_tokens = u.thought_tokens;
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Add up several calls — `count: N` bills N times, so a tool that reports only
|
|
108
|
+
* the first call understates itself. Gaps are skipped rather than counted as
|
|
109
|
+
* zero; if nothing at all is known the sum is `undefined`, so silence does not
|
|
110
|
+
* turn into a confident total.
|
|
111
|
+
*/
|
|
112
|
+
export function sumUsage(parts) {
|
|
113
|
+
const known = parts.filter((p) => p !== undefined);
|
|
114
|
+
if (known.length === 0)
|
|
115
|
+
return undefined;
|
|
116
|
+
return compact(known.reduce((acc, u) => ({
|
|
117
|
+
input_tokens: acc.input_tokens + u.input_tokens,
|
|
118
|
+
output_tokens: acc.output_tokens + u.output_tokens,
|
|
119
|
+
total_tokens: acc.total_tokens + u.total_tokens,
|
|
120
|
+
image_tokens: (acc.image_tokens ?? 0) + (u.image_tokens ?? 0),
|
|
121
|
+
video_tokens: (acc.video_tokens ?? 0) + (u.video_tokens ?? 0),
|
|
122
|
+
audio_tokens: (acc.audio_tokens ?? 0) + (u.audio_tokens ?? 0),
|
|
123
|
+
cached_tokens: (acc.cached_tokens ?? 0) + (u.cached_tokens ?? 0),
|
|
124
|
+
thought_tokens: (acc.thought_tokens ?? 0) + (u.thought_tokens ?? 0),
|
|
125
|
+
}), { input_tokens: 0, output_tokens: 0, total_tokens: 0 }));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Mark a replayed result's usage as already billed.
|
|
129
|
+
*
|
|
130
|
+
* `JobRegistry.dispatch()` can serve a matching `idempotency_key` from the
|
|
131
|
+
* registry without calling upstream — that is what makes a host-timeout retry
|
|
132
|
+
* free. The recorded result still carries the ORIGINAL call's usage, which is
|
|
133
|
+
* correct as a description of the image you are being handed and wrong as a
|
|
134
|
+
* measure of what this call cost: nothing.
|
|
135
|
+
*
|
|
136
|
+
* So the counts stay and gain `usage_billed: false`, which is what lets a
|
|
137
|
+
* workflow total exclude them. Dropping them instead would be worse — the
|
|
138
|
+
* caller could no longer see how the image was produced — and leaving them
|
|
139
|
+
* unmarked would double-count a generation that was paid for once.
|
|
140
|
+
*
|
|
141
|
+
* A result with no usage at all is left untouched: "unknown" must not turn
|
|
142
|
+
* into the positive claim "not billed".
|
|
143
|
+
*/
|
|
144
|
+
export function annotateReusedUsage(meta) {
|
|
145
|
+
if (!meta.usage)
|
|
146
|
+
return meta;
|
|
147
|
+
meta.usage_billed = false;
|
|
148
|
+
// The cost estimate needs the same treatment for the same reason, and is
|
|
149
|
+
// worse if it does not get it: a token count someone forgets to exclude is a
|
|
150
|
+
// rounding error in a report, a dollar figure is the report. Marked rather
|
|
151
|
+
// than deleted, so the result still shows what the generation cost when it
|
|
152
|
+
// was actually made.
|
|
153
|
+
if (meta.cost_estimate)
|
|
154
|
+
meta.cost_billed = false;
|
|
155
|
+
return meta;
|
|
156
|
+
}
|
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.
|
|
2
|
+
export const VERSION = '1.11.1'; // x-release-please-version
|
package/mint.yaml
CHANGED
|
@@ -44,12 +44,18 @@ egress:
|
|
|
44
44
|
# solely in a URL this server BUILDS and returns are excluded.
|
|
45
45
|
- generativelanguage.googleapis.com
|
|
46
46
|
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
47
|
+
# That one host covers every Gemini call: generateContent, the Files API
|
|
48
|
+
# upload start, the resumable upload session URL it hands back (same
|
|
49
|
+
# host — live-verified, docs/GEMINI-API.md) and the media download, which
|
|
50
|
+
# client.ts refuses to follow to any other host.
|
|
51
|
+
#
|
|
52
|
+
# NOTE — this server also reaches ONE host determined at RUNTIME, which
|
|
53
|
+
# cannot appear as a literal here: the blob store at MCP_BLOB_BASE_URL
|
|
54
|
+
# (src/blob-store.ts PUTs/GETs generated media there). On mcp-host that
|
|
55
|
+
# is the gateway's own public hostname, injected at spawn; add that host
|
|
56
|
+
# for the deployment you register on, or every hosted image save is
|
|
57
|
+
# blocked. Nothing else is dialled at runtime: no GCS/S3 host is ever
|
|
58
|
+
# contacted.
|
|
53
59
|
#
|
|
54
60
|
# Also: the images_url argument makes this server fetch an ARBITRARY
|
|
55
61
|
# caller-supplied https host. That cannot be enumerated in advance, so on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/gemini-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
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>",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"dev": "node dist/index.js",
|
|
43
43
|
"test": "npm run typecheck && vitest run",
|
|
44
44
|
"test:watch": "vitest",
|
|
45
|
-
"test:coverage": "vitest run --coverage",
|
|
45
|
+
"test:coverage": "npm run typecheck && vitest run --coverage",
|
|
46
46
|
"typecheck": "tsc -p tsconfig.test.json"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
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.
|
|
9
|
+
"version": "1.11.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.11.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|