@chrischall/gemini-mcp 1.4.0 → 1.5.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +64 -1
- package/dist/bundle.js +330 -27
- package/dist/client.js +6 -0
- package/dist/inputs.js +37 -1
- package/dist/jobs.js +46 -2
- package/dist/library.js +151 -0
- package/dist/media-cleanup.js +4 -1
- package/dist/media-endpoint.js +6 -0
- package/dist/media-url.js +2 -2
- package/dist/put-endpoint.js +122 -0
- package/dist/storage/media.js +23 -12
- package/dist/tools/generate.js +34 -10
- package/dist/tools/interact.js +3 -2
- package/dist/tools/library.js +186 -0
- package/dist/tools/music.js +3 -2
- package/dist/tools/set.js +37 -9
- package/dist/tools/shared.js +195 -2
- package/dist/tools/uploads.js +69 -0
- package/dist/tools/video.js +3 -2
- package/dist/upload-url.js +138 -0
- package/dist/version.js +1 -1
- package/dist/zip.js +113 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { McpToolError, textResult } from '@chrischall/mcp-utils';
|
|
3
|
+
import { UPLOAD_MAX_BYTES, RASTER_IMAGE_TYPE_PATTERN } from '../upload-url.js';
|
|
4
|
+
import { wholeMb } from '../bytes.js';
|
|
5
|
+
/**
|
|
6
|
+
* `gemini_get_upload_url` — mint a short-lived signed PUT URL so a shell can
|
|
7
|
+
* upload a reference image with ZERO auth headers:
|
|
8
|
+
*
|
|
9
|
+
* curl -sS -X PUT -H "Content-Type: image/jpeg" --data-binary @photo.jpg "<upload_url>"
|
|
10
|
+
*
|
|
11
|
+
* This closes the last gap in the no-bytes-in-the-conversation story. The two
|
|
12
|
+
* existing byte routes each exclude a common caller: `images_base64` costs
|
|
13
|
+
* ~14k tokens per photo and corrupts silently on a truncated read, and
|
|
14
|
+
* `POST /upload` needs the MCP session's OAuth bearer token, which a sandboxed
|
|
15
|
+
* shell doesn't have. A signed URL is mintable by the session and usable by
|
|
16
|
+
* the shell.
|
|
17
|
+
*
|
|
18
|
+
* Hosted connector only — stdio has no HTTP surface to PUT against (and no
|
|
19
|
+
* need: it reads local paths directly). Gated on `client.uploadUrls`, the same
|
|
20
|
+
* pattern as `gemini_sign_media` / the library tools.
|
|
21
|
+
*/
|
|
22
|
+
export function registerUploadUrlTools(server, client) {
|
|
23
|
+
const minter = client.uploadUrls;
|
|
24
|
+
if (!minter)
|
|
25
|
+
return;
|
|
26
|
+
server.registerTool('gemini_get_upload_url', {
|
|
27
|
+
description: 'Mint a short-lived (~10 min) signed PUT URL for uploading one reference image to this connector with NO auth ' +
|
|
28
|
+
'headers — usable from any shell: curl -sS -X PUT -H "Content-Type: image/jpeg" --data-binary @photo.jpg "<upload_url>". ' +
|
|
29
|
+
'The response to the PUT returns an `r2_key` to reference the upload: pass it in `images_r2_keys` on generation ' +
|
|
30
|
+
'tools, save it permanently with gemini_save_character, or convert it to a ~48h Files API reference with ' +
|
|
31
|
+
'gemini_upload_file({ r2_key }). Prefer this over images_base64 (which costs ~14k tokens per photo) whenever the ' +
|
|
32
|
+
`caller has a shell. Images only, up to ${wholeMb(UPLOAD_MAX_BYTES)}MB.`,
|
|
33
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
34
|
+
inputSchema: {
|
|
35
|
+
filename: z
|
|
36
|
+
.string()
|
|
37
|
+
.min(1)
|
|
38
|
+
.max(120)
|
|
39
|
+
.describe('Filename of the upload (e.g. "finn.jpg") — sanitized into the object key and the suggested curl command'),
|
|
40
|
+
content_type: z
|
|
41
|
+
.string()
|
|
42
|
+
// Raster only — image/svg+xml is a scriptable document and /media
|
|
43
|
+
// serves from the connector's own origin (see RASTER_IMAGE_TYPE_PATTERN).
|
|
44
|
+
.regex(RASTER_IMAGE_TYPE_PATTERN, 'must be a raster image MIME type like image/jpeg (SVG is not accepted)')
|
|
45
|
+
.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.'),
|
|
46
|
+
},
|
|
47
|
+
}, async (args) => {
|
|
48
|
+
let minted;
|
|
49
|
+
try {
|
|
50
|
+
minted = await minter.mint(args.filename, args.content_type);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
throw new McpToolError(err instanceof Error ? err.message : String(err));
|
|
54
|
+
}
|
|
55
|
+
return textResult({
|
|
56
|
+
upload_url: minted.url,
|
|
57
|
+
method: 'PUT',
|
|
58
|
+
r2_key: minted.key,
|
|
59
|
+
content_type: minted.contentType,
|
|
60
|
+
expires_at: new Date(minted.expiresAtMs).toISOString(),
|
|
61
|
+
max_bytes: UPLOAD_MAX_BYTES,
|
|
62
|
+
curl_hint: `curl -sS -X PUT -H "Content-Type: ${minted.contentType}" --data-binary @${args.filename} "${minted.url}"`,
|
|
63
|
+
hint: 'PUT the raw bytes within ~10 minutes; no Authorization header is needed (the signature in the URL is the auth). ' +
|
|
64
|
+
'Then reference the returned r2_key: images_r2_keys on generation tools (master_images_r2_keys on gemini_image_set), ' +
|
|
65
|
+
'gemini_save_character to keep it with no expiry, or gemini_upload_file({ r2_key }) for a ~48h files/… reference. ' +
|
|
66
|
+
'Uploads themselves follow the media retention schedule (default 7 days).',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
package/dist/tools/video.js
CHANGED
|
@@ -3,7 +3,7 @@ import { McpToolError } from '@chrischall/mcp-utils';
|
|
|
3
3
|
import { slugify, baseName } from '../images.js';
|
|
4
4
|
import { resolveImageInputs } from '../inputs.js';
|
|
5
5
|
import { DEFAULT_VIDEO_MODEL } from '../models.js';
|
|
6
|
-
import { emitMedia, timeoutMsSchema, idempotencyKeySchema, asyncSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema } from './shared.js';
|
|
6
|
+
import { emitMedia, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema } from './shared.js';
|
|
7
7
|
import { fingerprintRequest } from '../jobs.js';
|
|
8
8
|
import { previewLocalInputsUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
9
9
|
/** omni's own aspect-ratio enum — NOT the image tools' ASPECT_RATIOS. */
|
|
@@ -44,6 +44,7 @@ export function registerVideoTools(server, client) {
|
|
|
44
44
|
timeout_ms: timeoutMsSchema,
|
|
45
45
|
idempotency_key: idempotencyKeySchema,
|
|
46
46
|
async: asyncSchema,
|
|
47
|
+
max_wait_ms: maxWaitMsSchema,
|
|
47
48
|
confirm: schemaConfirm,
|
|
48
49
|
},
|
|
49
50
|
}, async (args, extra) => {
|
|
@@ -69,7 +70,7 @@ export function registerVideoTools(server, client) {
|
|
|
69
70
|
images_url: args.images_url, images_file_uris: args.images_file_uris,
|
|
70
71
|
previous_interaction_id: previousInteractionId,
|
|
71
72
|
});
|
|
72
|
-
return client.session.jobs.dispatch({ toolName: 'gemini_video_generate', fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
73
|
+
return client.session.jobs.dispatch({ toolName: 'gemini_video_generate', fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
73
74
|
const { inputs, report } = await resolveImageInputs(args, client);
|
|
74
75
|
const r = await withProgressHeartbeat(extra, `Generating video (${model})`, () => client.generateVideo({
|
|
75
76
|
input: args.prompt,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Short-lived signed PUT URLs — how reference bytes get INTO the connector
|
|
3
|
+
* without either base64 through the model's context or an OAuth bearer token.
|
|
4
|
+
*
|
|
5
|
+
* The problem these solve is the mirror image of `/media`: a signed GET lets a
|
|
6
|
+
* shell *download* generated media with no auth header, but uploading a
|
|
7
|
+
* reference photo still required `images_base64` (~14k tokens per JPEG, silently
|
|
8
|
+
* corrupting on a truncated read) or `POST /upload` (which 401s from any shell
|
|
9
|
+
* that doesn't hold the MCP session's bearer token). A signed PUT URL closes
|
|
10
|
+
* that gap: the authenticated MCP session mints it, and then a bare
|
|
11
|
+
* `curl -X PUT --data-binary @photo.jpg "<url>"` stores the bytes.
|
|
12
|
+
*
|
|
13
|
+
* Same HMAC secret as the media GET URLs, but a DIFFERENT payload shape, so the
|
|
14
|
+
* two kinds of signature can never be replayed across protocols:
|
|
15
|
+
*
|
|
16
|
+
* - media GET signs `<key>\n<exp>` (media-url.ts — do not change;
|
|
17
|
+
* every outstanding link was signed with that exact shape)
|
|
18
|
+
* - upload PUT signs `put\0<key>\0<ct>\0<exp>` (NUL separators)
|
|
19
|
+
*
|
|
20
|
+
* A media payload always contains a `\n` and never a NUL (the sink sanitises
|
|
21
|
+
* every key segment it mints); an upload payload always contains NULs and
|
|
22
|
+
* never a `\n` — the endpoint refuses keys and content types outside a strict
|
|
23
|
+
* charset (no whitespace, no control characters) BEFORE verifying, so no
|
|
24
|
+
* crafted value can straddle a field boundary and no byte string is valid
|
|
25
|
+
* under both interpretations in either direction.
|
|
26
|
+
*
|
|
27
|
+
* The signature also covers the content type, so the uploaded object's stored
|
|
28
|
+
* MIME is exactly what the mint call declared — a URL minted for `image/jpeg`
|
|
29
|
+
* cannot be used to store `text/html`.
|
|
30
|
+
*
|
|
31
|
+
* Like media-url.ts, this module is WebCrypto-only and free of any Cloudflare
|
|
32
|
+
* import so it tests under Node.
|
|
33
|
+
*/
|
|
34
|
+
import { base64UrlEncode, base64UrlDecode } from './media-url.js';
|
|
35
|
+
import { safeKeySegment } from './storage/media.js';
|
|
36
|
+
/** How long a minted upload URL stays usable. Deliberately short — it exists to
|
|
37
|
+
* be curled within the same conversation turn that minted it. */
|
|
38
|
+
export const UPLOAD_URL_TTL_MS = 10 * 60 * 1000;
|
|
39
|
+
/** Upload size cap enforced by the PUT endpoint (while reading, not from the
|
|
40
|
+
* declared length alone). Matches the `images_url` fetch cap — a reference
|
|
41
|
+
* photo, not an archive. */
|
|
42
|
+
export const UPLOAD_MAX_BYTES = 15 * 1024 * 1024;
|
|
43
|
+
/** Key prefix for signed uploads: `up/<tenant>/<date>/<random>-<name>`. Swept
|
|
44
|
+
* by the retention cron like generated media (`gen/`); the character library
|
|
45
|
+
* (`lib/`) is where anything meant to be permanent gets copied. */
|
|
46
|
+
export const UPLOAD_KEY_PREFIX = 'up';
|
|
47
|
+
/**
|
|
48
|
+
* The bytes an upload signature covers: method, object key, content type,
|
|
49
|
+
* expiry. NUL separators, written as the \u0000 ESCAPE and never a literal NUL
|
|
50
|
+
* byte — a literal one makes git classify this whole file as binary and stop
|
|
51
|
+
* diffing it (the same trap as fingerprintRequest in jobs.ts, and exactly how
|
|
52
|
+
* this security-critical module briefly shipped unreviewable). No crafted
|
|
53
|
+
* value can straddle a field boundary: the endpoint enforces a control-free
|
|
54
|
+
* charset on the key and content type before verifying (see
|
|
55
|
+
* `wellFormedUploadKey` / `acceptableUploadType`).
|
|
56
|
+
*/
|
|
57
|
+
function payload(key, contentType, expiresAtMs) {
|
|
58
|
+
return new TextEncoder().encode(`put\u0000${key}\u0000${contentType.toLowerCase()}\u0000${expiresAtMs}`);
|
|
59
|
+
}
|
|
60
|
+
/** Sign one upload: this key, this content type, until `expiresAtMs`. */
|
|
61
|
+
export async function signUploadKey(signingKey, key, contentType, expiresAtMs) {
|
|
62
|
+
return base64UrlEncode(await crypto.subtle.sign('HMAC', signingKey, payload(key, contentType, expiresAtMs)));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check a `?ct=&exp=&sig=` triple against an object key. Same contract as
|
|
66
|
+
* `verifyMediaSignature`: constant-time comparison via `crypto.subtle.verify`,
|
|
67
|
+
* and signature validity is checked before expiry so a forgery is never
|
|
68
|
+
* confirmed as "merely expired".
|
|
69
|
+
*/
|
|
70
|
+
export async function verifyUploadSignature(signingKey, key, contentType, exp, sig, nowMs) {
|
|
71
|
+
if (!contentType || !exp || !sig)
|
|
72
|
+
return { ok: false, reason: 'missing' };
|
|
73
|
+
const expiresAtMs = Number(exp);
|
|
74
|
+
if (!Number.isFinite(expiresAtMs))
|
|
75
|
+
return { ok: false, reason: 'invalid' };
|
|
76
|
+
let valid;
|
|
77
|
+
try {
|
|
78
|
+
valid = await crypto.subtle.verify('HMAC', signingKey, base64UrlDecode(sig), payload(key, contentType, expiresAtMs));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return { ok: false, reason: 'invalid' };
|
|
82
|
+
}
|
|
83
|
+
if (!valid)
|
|
84
|
+
return { ok: false, reason: 'invalid' };
|
|
85
|
+
if (nowMs > expiresAtMs)
|
|
86
|
+
return { ok: false, reason: 'expired' };
|
|
87
|
+
return { ok: true };
|
|
88
|
+
}
|
|
89
|
+
/** `<base>/<key>?ct=…&exp=…&sig=…` — the URL handed back by the mint tool. */
|
|
90
|
+
export function buildUploadUrl(baseUrl, key, contentType, expiresAtMs, signature) {
|
|
91
|
+
const path = key.split('/').map(encodeURIComponent).join('/');
|
|
92
|
+
return `${baseUrl.replace(/\/+$/, '')}/${path}?ct=${encodeURIComponent(contentType.toLowerCase())}&exp=${expiresAtMs}&sig=${signature}`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* RASTER image types only — an explicit allowlist, not `image/*`.
|
|
96
|
+
*
|
|
97
|
+
* The wildcard admitted `image/svg+xml`, and an SVG is a *document*: scripts
|
|
98
|
+
* inside it execute when a browser opens the `/media` link, which serves from
|
|
99
|
+
* the SAME ORIGIN as the connector's OAuth pages — stored XSS. Everything on
|
|
100
|
+
* this list is pure pixels, and it covers what Gemini accepts as reference
|
|
101
|
+
* input. `/media` also sends a no-script CSP as defense in depth, but the
|
|
102
|
+
* allowlist is the primary gate: don't widen it to a wildcard again.
|
|
103
|
+
*
|
|
104
|
+
* Shared by the mint tool's schema, the PUT endpoint, and the library's
|
|
105
|
+
* image sources (`gemini_save_character` / `_style`), so the gate cannot
|
|
106
|
+
* drift between the front door and the side doors.
|
|
107
|
+
*/
|
|
108
|
+
export const RASTER_IMAGE_TYPE_PATTERN = /^image\/(jpeg|png|webp|gif|avif|heic|heif|bmp|tiff)$/i;
|
|
109
|
+
/** True for the MIME types a signed upload will store: raster images only. */
|
|
110
|
+
export function acceptableUploadType(contentType) {
|
|
111
|
+
return RASTER_IMAGE_TYPE_PATTERN.test(contentType);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build the minter. Keys are `up/<tenant>/<YYYY-MM-DD>/<random>-<safe-name>` —
|
|
115
|
+
* tenant-scoped at MINT time, which is what "scoped to the account that minted
|
|
116
|
+
* it" means mechanically: the signature only ever covers a key inside this
|
|
117
|
+
* session's own namespace, so a signed PUT cannot write into anyone else's.
|
|
118
|
+
*/
|
|
119
|
+
export function createUploadUrlMinter(opts) {
|
|
120
|
+
const now = opts.now ?? (() => new Date());
|
|
121
|
+
const randomId = opts.randomId ?? (() => crypto.randomUUID().slice(0, 8));
|
|
122
|
+
const ttlMs = opts.ttlMs ?? UPLOAD_URL_TTL_MS;
|
|
123
|
+
return {
|
|
124
|
+
async mint(filename, contentType) {
|
|
125
|
+
const ct = contentType.trim().toLowerCase();
|
|
126
|
+
if (!acceptableUploadType(ct)) {
|
|
127
|
+
throw new Error(`content_type must be a raster image type (image/jpeg, image/png, image/webp, image/gif, image/avif, image/heic, image/heif, image/bmp, image/tiff), got "${contentType}". ` +
|
|
128
|
+
'SVG is deliberately not accepted: it can carry scripts, and /media serves from the connector origin.');
|
|
129
|
+
}
|
|
130
|
+
const tenant = typeof opts.tenant === 'function' ? await opts.tenant() : opts.tenant;
|
|
131
|
+
const day = now().toISOString().slice(0, 10);
|
|
132
|
+
const key = `${UPLOAD_KEY_PREFIX}/${tenant}/${day}/${randomId()}-${safeKeySegment(filename)}`;
|
|
133
|
+
const expiresAtMs = now().getTime() + ttlMs;
|
|
134
|
+
const signature = await opts.sign(key, ct, expiresAtMs);
|
|
135
|
+
return { url: buildUploadUrl(opts.baseUrl, key, ct, expiresAtMs, signature), key, contentType: ct, expiresAtMs };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
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.5.0'; // x-release-please-version
|
package/dist/zip.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ZIP writer (STORE only, no compression) for bundling a multi-image
|
|
3
|
+
* result into one downloadable archive.
|
|
4
|
+
*
|
|
5
|
+
* Why hand-rolled: the Worker has no filesystem and no archiver dependency,
|
|
6
|
+
* and the images being bundled are already-compressed PNG/JPEG — DEFLATE would
|
|
7
|
+
* buy nothing. STORE entries are just headers + raw bytes + CRC32, which is
|
|
8
|
+
* ~100 lines and runs identically under Node and workerd.
|
|
9
|
+
*
|
|
10
|
+
* Layout: [local header + name + data]* [central directory]* [EOCD]. No
|
|
11
|
+
* ZIP64 (a bundle is a handful of images, far under the 4 GB / 65k-entry
|
|
12
|
+
* limits), no encryption, no streaming.
|
|
13
|
+
*/
|
|
14
|
+
/** Standard CRC-32 (IEEE 802.3), table-driven. The table is immutable. */
|
|
15
|
+
const CRC_TABLE = (() => {
|
|
16
|
+
const table = new Uint32Array(256);
|
|
17
|
+
for (let n = 0; n < 256; n++) {
|
|
18
|
+
let c = n;
|
|
19
|
+
for (let k = 0; k < 8; k++)
|
|
20
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
21
|
+
table[n] = c >>> 0;
|
|
22
|
+
}
|
|
23
|
+
return table;
|
|
24
|
+
})();
|
|
25
|
+
export function crc32(bytes) {
|
|
26
|
+
let crc = 0xffffffff;
|
|
27
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
28
|
+
crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
|
|
29
|
+
}
|
|
30
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
31
|
+
}
|
|
32
|
+
/** MS-DOS date/time pair (ZIP's native timestamp format). */
|
|
33
|
+
function dosDateTime(at) {
|
|
34
|
+
const year = Math.max(1980, at.getUTCFullYear());
|
|
35
|
+
return {
|
|
36
|
+
time: (at.getUTCHours() << 11) | (at.getUTCMinutes() << 5) | (at.getUTCSeconds() >> 1),
|
|
37
|
+
date: ((year - 1980) << 9) | ((at.getUTCMonth() + 1) << 5) | at.getUTCDate(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const LOCAL_HEADER = 0x04034b50;
|
|
41
|
+
const CENTRAL_HEADER = 0x02014b50;
|
|
42
|
+
const EOCD = 0x06054b50;
|
|
43
|
+
/** Version 2.0 — the minimum that understands STORE + directories. */
|
|
44
|
+
const VERSION = 20;
|
|
45
|
+
/** General-purpose bit 11: names are UTF-8. */
|
|
46
|
+
const FLAG_UTF8 = 0x0800;
|
|
47
|
+
/**
|
|
48
|
+
* Build a STORE-only zip. `modifiedAt` stamps every entry (injectable so a
|
|
49
|
+
* given input always produces identical bytes; defaults to the epoch of the
|
|
50
|
+
* format, 1980).
|
|
51
|
+
*/
|
|
52
|
+
export function buildZip(entries, modifiedAt = new Date(Date.UTC(1980, 0, 1))) {
|
|
53
|
+
const { time, date } = dosDateTime(modifiedAt);
|
|
54
|
+
const encoder = new TextEncoder();
|
|
55
|
+
const parts = [];
|
|
56
|
+
const central = [];
|
|
57
|
+
let offset = 0;
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const name = encoder.encode(entry.name);
|
|
60
|
+
const crc = crc32(entry.bytes);
|
|
61
|
+
const size = entry.bytes.byteLength;
|
|
62
|
+
const local = new Uint8Array(30 + name.byteLength);
|
|
63
|
+
const lv = new DataView(local.buffer);
|
|
64
|
+
lv.setUint32(0, LOCAL_HEADER, true);
|
|
65
|
+
lv.setUint16(4, VERSION, true);
|
|
66
|
+
lv.setUint16(6, FLAG_UTF8, true);
|
|
67
|
+
lv.setUint16(8, 0, true); // method: STORE
|
|
68
|
+
lv.setUint16(10, time, true);
|
|
69
|
+
lv.setUint16(12, date, true);
|
|
70
|
+
lv.setUint32(14, crc, true);
|
|
71
|
+
lv.setUint32(18, size, true); // compressed == uncompressed under STORE
|
|
72
|
+
lv.setUint32(22, size, true);
|
|
73
|
+
lv.setUint16(26, name.byteLength, true);
|
|
74
|
+
lv.setUint16(28, 0, true); // extra length
|
|
75
|
+
local.set(name, 30);
|
|
76
|
+
const dir = new Uint8Array(46 + name.byteLength);
|
|
77
|
+
const dv = new DataView(dir.buffer);
|
|
78
|
+
dv.setUint32(0, CENTRAL_HEADER, true);
|
|
79
|
+
dv.setUint16(4, VERSION, true); // version made by
|
|
80
|
+
dv.setUint16(6, VERSION, true); // version needed
|
|
81
|
+
dv.setUint16(8, FLAG_UTF8, true);
|
|
82
|
+
dv.setUint16(10, 0, true);
|
|
83
|
+
dv.setUint16(12, time, true);
|
|
84
|
+
dv.setUint16(14, date, true);
|
|
85
|
+
dv.setUint32(16, crc, true);
|
|
86
|
+
dv.setUint32(20, size, true);
|
|
87
|
+
dv.setUint32(24, size, true);
|
|
88
|
+
dv.setUint16(28, name.byteLength, true);
|
|
89
|
+
// extra len, comment len, disk start, internal attrs, external attrs = 0
|
|
90
|
+
dv.setUint32(42, offset, true);
|
|
91
|
+
dir.set(name, 46);
|
|
92
|
+
parts.push(local, entry.bytes);
|
|
93
|
+
central.push(dir);
|
|
94
|
+
offset += local.byteLength + size;
|
|
95
|
+
}
|
|
96
|
+
const centralSize = central.reduce((n, c) => n + c.byteLength, 0);
|
|
97
|
+
const eocd = new Uint8Array(22);
|
|
98
|
+
const ev = new DataView(eocd.buffer);
|
|
99
|
+
ev.setUint32(0, EOCD, true);
|
|
100
|
+
ev.setUint16(8, entries.length, true); // entries on this disk
|
|
101
|
+
ev.setUint16(10, entries.length, true); // entries total
|
|
102
|
+
ev.setUint32(12, centralSize, true);
|
|
103
|
+
ev.setUint32(16, offset, true); // central directory offset
|
|
104
|
+
// comment length = 0
|
|
105
|
+
const total = offset + centralSize + eocd.byteLength;
|
|
106
|
+
const out = new Uint8Array(total);
|
|
107
|
+
let cursor = 0;
|
|
108
|
+
for (const part of [...parts, ...central, eocd]) {
|
|
109
|
+
out.set(part, cursor);
|
|
110
|
+
cursor += part.byteLength;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/gemini-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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>",
|
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.5.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.5.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|