gemini-studio-mcp 0.1.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/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/gemini/client.js +61 -0
- package/dist/gemini/client.js.map +1 -0
- package/dist/gemini/types.js +2 -0
- package/dist/gemini/types.js.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/server.js +29 -0
- package/dist/server.js.map +1 -0
- package/dist/session/session.js +6 -0
- package/dist/session/session.js.map +1 -0
- package/dist/store/assetStore.js +54 -0
- package/dist/store/assetStore.js.map +1 -0
- package/dist/store/types.js +2 -0
- package/dist/store/types.js.map +1 -0
- package/dist/tools/deps.js +2 -0
- package/dist/tools/deps.js.map +1 -0
- package/dist/tools/editImage.js +39 -0
- package/dist/tools/editImage.js.map +1 -0
- package/dist/tools/generateImage.js +32 -0
- package/dist/tools/generateImage.js.map +1 -0
- package/dist/tools/generateVideo.js +31 -0
- package/dist/tools/generateVideo.js.map +1 -0
- package/dist/tools/iterate.js +21 -0
- package/dist/tools/iterate.js.map +1 -0
- package/dist/tools/result.js +16 -0
- package/dist/tools/result.js.map +1 -0
- package/dist/util/mime.js +8 -0
- package/dist/util/mime.js.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Armen (@arterorx)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🎨 Gemini Studio MCP
|
|
4
|
+
|
|
5
|
+
**Generate _and iteratively refine_ images & video — right inside Claude, Cursor, or any MCP client.**
|
|
6
|
+
|
|
7
|
+
Powered by Google's Gemini API (Imagen 4 · Gemini 3 Pro Image · Veo 3.1).
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/gemini-studio-mcp)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
[](https://modelcontextprotocol.io)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why this one?
|
|
19
|
+
|
|
20
|
+
Most Gemini image MCPs are thin wrappers: _call → file → done_. The moment you want to **change** something, you start over from a blank prompt.
|
|
21
|
+
|
|
22
|
+
**Gemini Studio MCP keeps the thread.** It remembers what you just made and lets you refine it in plain words — _"make the suit gold, swap the sky for a nebula"_ — while keeping the same subject. Every result is saved with its full lineage (prompt, model, parent), so your work is reproducible.
|
|
23
|
+
|
|
24
|
+
> Tell it _what to change_, not how. The character stays; only what you asked changes.
|
|
25
|
+
|
|
26
|
+
### Generate → Iterate (same cat, one sentence later)
|
|
27
|
+
|
|
28
|
+
| `generate_image` | `iterate` → "gold suit, purple nebula sky" |
|
|
29
|
+
|:---:|:---:|
|
|
30
|
+
|  |  |
|
|
31
|
+
|
|
32
|
+
*Both produced through this server, end-to-end. Notice the cat, pose and Martian ground are preserved — only the requested changes applied.*
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- 🖼️ **Text → image** via Imagen 4
|
|
39
|
+
- 🔁 **Conversational iteration** — refine the last (or any) image by describing the change; the subject stays consistent
|
|
40
|
+
- ✏️ **Targeted edits** of any stored image by id
|
|
41
|
+
- 🎬 **Text → video** via Veo (auto-downloads the rendered clip)
|
|
42
|
+
- 🧬 **Lineage & reproducibility** — every asset saved with `{ prompt, model, parentId, ... }` next to the file
|
|
43
|
+
- 🪄 **Zero-friction setup** — one line in your MCP config; a friendly hint instead of a stack trace if the key is missing
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
Add to your MCP client config (Claude Code, Claude Desktop, Cursor, …):
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"gemini-studio": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["-y", "gemini-studio-mcp"],
|
|
55
|
+
"env": { "GEMINI_API_KEY": "YOUR_KEY" }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Get a key at **https://aistudio.google.com/apikey** — then restart your client.
|
|
62
|
+
|
|
63
|
+
## Tools
|
|
64
|
+
|
|
65
|
+
| Tool | What it does |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `generate_image` | Text → image (Imagen 4). |
|
|
68
|
+
| `edit_image` | Edit a stored image by `id` with a text instruction. |
|
|
69
|
+
| `iterate` | Refine the **last** (or a given) image in words — _"warmer", "different angle", "same character"_. |
|
|
70
|
+
| `generate_video` | Text → video (Veo). v1 waits for the render to finish. |
|
|
71
|
+
|
|
72
|
+
### Example conversation
|
|
73
|
+
|
|
74
|
+
> **You:** generate an image: a ginger cat in an astronaut suit on Mars, Earth in the sky
|
|
75
|
+
> **→** `generate_image` returns the image and saves it.
|
|
76
|
+
>
|
|
77
|
+
> **You:** make the suit gold and the sky a purple nebula
|
|
78
|
+
> **→** `iterate` returns the same cat, restyled — saved with `parentId` pointing at the original.
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
| Variable | Purpose | Default |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `GEMINI_API_KEY` | Gemini API key (**required**) | — |
|
|
85
|
+
| `GEMINI_STUDIO_DIR` | Output folder for assets | `./.gemini-studio` |
|
|
86
|
+
| `GEMINI_IMAGE_MODEL` | Image generation model | `imagen-4.0-generate-001` |
|
|
87
|
+
| `GEMINI_EDIT_MODEL` | Edit / iterate model | `gemini-3-pro-image-preview` |
|
|
88
|
+
| `GEMINI_VIDEO_MODEL` | Video model | `veo-3.1-generate-preview` |
|
|
89
|
+
|
|
90
|
+
> **Tip:** when launched via `npx` from a desktop client, the process working directory may be non-obvious — set `GEMINI_STUDIO_DIR` to an absolute path so you can always find your output.
|
|
91
|
+
|
|
92
|
+
> **Prompt tip:** Imagen follows **English** prompts more reliably than other languages.
|
|
93
|
+
|
|
94
|
+
## How it works
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
your client ──stdio──> gemini-studio-mcp ──> @google/genai ──> Gemini API
|
|
98
|
+
│
|
|
99
|
+
├─ AssetStore (media file + <id>.json manifest + index.json)
|
|
100
|
+
└─ Session (remembers the last image, so `iterate` just works)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Each generation writes the media file plus a manifest:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{ "id": "3968fcb4", "parentId": null, "kind": "image",
|
|
107
|
+
"prompt": "a ginger cat ...", "model": "imagen-4.0-generate-001",
|
|
108
|
+
"file": "3968fcb4.png", "mimeType": "image/png", "createdAt": "..." }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`parentId` links an edit/iteration back to its source — that chain is what powers consistent, reproducible refinement.
|
|
112
|
+
|
|
113
|
+
## Verified
|
|
114
|
+
|
|
115
|
+
Tested against the live Gemini API: ✅ `generate_image` (Imagen 4) · ✅ `edit_image` / `iterate` (Gemini 3 Pro Image) · ✅ `generate_video` (Veo, auto-downloaded MP4).
|
|
116
|
+
|
|
117
|
+
## Roadmap
|
|
118
|
+
|
|
119
|
+
- **Now (v1):** generate · edit · iterate · video, with asset lineage.
|
|
120
|
+
- **Next:** style presets, a gallery tool, and cost shown before each generation.
|
|
121
|
+
- **Later:** non-blocking / resumable video jobs with progress.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm install
|
|
127
|
+
npm run build # tsc → dist/
|
|
128
|
+
npm test # vitest (unit tests, no API calls)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Contributing
|
|
132
|
+
|
|
133
|
+
Issues and PRs welcome. Please keep changes focused and covered by tests.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
[MIT](./LICENSE) © Armen ([@arterorx](https://github.com/arterorx))
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export class MissingApiKeyError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export function loadConfig(env = process.env) {
|
|
5
|
+
const apiKey = env.GEMINI_API_KEY;
|
|
6
|
+
if (!apiKey) {
|
|
7
|
+
throw new MissingApiKeyError('GEMINI_API_KEY не задан. Получи ключ на https://aistudio.google.com/apikey ' +
|
|
8
|
+
'и добавь его в раздел env конфигурации этого MCP-сервера.');
|
|
9
|
+
}
|
|
10
|
+
const baseDir = env.GEMINI_STUDIO_DIR ?? path.resolve(env.PWD ?? process.cwd(), '.gemini-studio');
|
|
11
|
+
return {
|
|
12
|
+
apiKey,
|
|
13
|
+
workingDir: baseDir,
|
|
14
|
+
defaultImageModel: env.GEMINI_IMAGE_MODEL ?? 'imagen-4.0-generate-001',
|
|
15
|
+
editModel: env.GEMINI_EDIT_MODEL ?? 'gemini-3-pro-image-preview',
|
|
16
|
+
videoModel: env.GEMINI_VIDEO_MODEL ?? 'veo-3.1-generate-preview',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAU7B,MAAM,OAAO,kBAAmB,SAAQ,KAAK;CAAG;AAEhD,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,MAAM,GAAG,GAAG,CAAC,cAAc,CAAC;IAClC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,kBAAkB,CAC1B,6EAA6E;YAC7E,2DAA2D,CAC5D,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,CAAC,iBAAiB,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,gBAAgB,CAAC,CAAC;IAClG,OAAO;QACL,MAAM;QACN,UAAU,EAAE,OAAO;QACnB,iBAAiB,EAAE,GAAG,CAAC,kBAAkB,IAAI,yBAAyB;QACtE,SAAS,EAAE,GAAG,CAAC,iBAAiB,IAAI,4BAA4B;QAChE,UAAU,EAAE,GAAG,CAAC,kBAAkB,IAAI,0BAA0B;KACjE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
|
+
export class RealGeminiClient {
|
|
7
|
+
ai;
|
|
8
|
+
opts;
|
|
9
|
+
constructor(ai, opts = {}) {
|
|
10
|
+
this.ai = ai;
|
|
11
|
+
this.opts = opts;
|
|
12
|
+
}
|
|
13
|
+
async generateImage({ prompt, model }) {
|
|
14
|
+
const res = await this.ai.models.generateImages({ model, prompt, config: { numberOfImages: 1 } });
|
|
15
|
+
const img = res?.generatedImages?.[0]?.image;
|
|
16
|
+
if (!img?.imageBytes) {
|
|
17
|
+
throw new Error('Gemini не вернул изображение (возможно, блокировка по политике контента).');
|
|
18
|
+
}
|
|
19
|
+
return { data: Buffer.from(img.imageBytes, 'base64'), mimeType: img.mimeType ?? 'image/png' };
|
|
20
|
+
}
|
|
21
|
+
async editImage({ prompt, model, image }) {
|
|
22
|
+
const res = await this.ai.interactions.create({
|
|
23
|
+
model,
|
|
24
|
+
input: [
|
|
25
|
+
{ type: 'text', text: prompt },
|
|
26
|
+
{ type: 'image', data: image.data.toString('base64'), mime_type: image.mimeType },
|
|
27
|
+
],
|
|
28
|
+
response_modalities: ['image'],
|
|
29
|
+
});
|
|
30
|
+
const out = (res?.outputs ?? []).find((o) => o.type === 'image');
|
|
31
|
+
if (!out?.data) {
|
|
32
|
+
throw new Error('Gemini не вернул отредактированное изображение (возможно, блокировка по политике контента).');
|
|
33
|
+
}
|
|
34
|
+
return { data: Buffer.from(out.data, 'base64'), mimeType: out.mime_type ?? 'image/png' };
|
|
35
|
+
}
|
|
36
|
+
async generateVideo({ prompt, model }) {
|
|
37
|
+
let op = await this.ai.models.generateVideos({ model, source: { prompt }, config: { numberOfVideos: 1 } });
|
|
38
|
+
while (!op?.done) {
|
|
39
|
+
await sleep(this.opts.pollMs ?? 10000);
|
|
40
|
+
op = await this.ai.operations.getVideosOperation({ operation: op });
|
|
41
|
+
}
|
|
42
|
+
const video = op?.response?.generatedVideos?.[0]?.video;
|
|
43
|
+
if (!video) {
|
|
44
|
+
throw new Error('Gemini не вернул видео (возможно, блокировка по политике контента).');
|
|
45
|
+
}
|
|
46
|
+
const mimeType = video.mimeType ?? 'video/mp4';
|
|
47
|
+
// Veo обычно возвращает не inline-байты, а ссылку (uri); скачиваем во временный файл.
|
|
48
|
+
if (video.videoBytes) {
|
|
49
|
+
return { data: Buffer.from(video.videoBytes, 'base64'), mimeType };
|
|
50
|
+
}
|
|
51
|
+
if (video.uri) {
|
|
52
|
+
const tmp = path.join(os.tmpdir(), `veo-${randomUUID()}.mp4`);
|
|
53
|
+
await this.ai.files.download({ file: video, downloadPath: tmp });
|
|
54
|
+
const data = await fs.readFile(tmp);
|
|
55
|
+
await fs.unlink(tmp).catch(() => { });
|
|
56
|
+
return { data, mimeType };
|
|
57
|
+
}
|
|
58
|
+
throw new Error('Gemini вернул видео без байтов и без ссылки на скачивание.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/gemini/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/gemini/types.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startServer } from './server.js';
|
|
3
|
+
import { MissingApiKeyError } from './config.js';
|
|
4
|
+
startServer().catch((err) => {
|
|
5
|
+
if (err instanceof MissingApiKeyError) {
|
|
6
|
+
process.stderr.write(`\n${err.message}\n\n`);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
process.stderr.write(`gemini-studio-mcp failed to start: ${err?.message ?? err}\n`);
|
|
10
|
+
}
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IAC1B,IAAI,GAAG,YAAY,kBAAkB,EAAE,CAAC;QACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC;IAC/C,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sCAAsC,GAAG,EAAE,OAAO,IAAI,GAAG,IAAI,CAAC,CAAC;IACtF,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { GoogleGenAI } from '@google/genai';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { RealGeminiClient } from './gemini/client.js';
|
|
6
|
+
import { AssetStore } from './store/assetStore.js';
|
|
7
|
+
import { Session } from './session/session.js';
|
|
8
|
+
import { registerGenerateImage } from './tools/generateImage.js';
|
|
9
|
+
import { registerEditImage } from './tools/editImage.js';
|
|
10
|
+
import { registerIterate } from './tools/iterate.js';
|
|
11
|
+
import { registerGenerateVideo } from './tools/generateVideo.js';
|
|
12
|
+
export async function startServer() {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const ai = new GoogleGenAI({ apiKey: config.apiKey });
|
|
15
|
+
const deps = {
|
|
16
|
+
client: new RealGeminiClient(ai),
|
|
17
|
+
store: new AssetStore(config.workingDir),
|
|
18
|
+
session: new Session(),
|
|
19
|
+
config,
|
|
20
|
+
};
|
|
21
|
+
const server = new McpServer({ name: 'gemini-studio-mcp', version: '0.1.0' });
|
|
22
|
+
registerGenerateImage(server, deps);
|
|
23
|
+
registerEditImage(server, deps);
|
|
24
|
+
registerIterate(server, deps);
|
|
25
|
+
registerGenerateVideo(server, deps);
|
|
26
|
+
const transport = new StdioServerTransport();
|
|
27
|
+
await server.connect(transport);
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,EAAE,GAAG,IAAI,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACtD,MAAM,IAAI,GAAa;QACrB,MAAM,EAAE,IAAI,gBAAgB,CAAC,EAAS,CAAC;QACvC,KAAK,EAAE,IAAI,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC;QACxC,OAAO,EAAE,IAAI,OAAO,EAAE;QACtB,MAAM;KACP,CAAC;IAEF,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAC9E,qBAAqB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACpC,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAChC,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC9B,qBAAqB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAEpC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/session/session.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,OAAO;IACV,WAAW,GAAkB,IAAI,CAAC;IAC1C,cAAc,CAAC,EAAU,IAAU,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC,CAAC,CAAC;IAC3D,cAAc,KAAoB,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;CAC7D"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export class AssetStore {
|
|
4
|
+
rootDir;
|
|
5
|
+
constructor(rootDir) {
|
|
6
|
+
this.rootDir = rootDir;
|
|
7
|
+
}
|
|
8
|
+
manifestPath(id) { return path.join(this.rootDir, `${id}.json`); }
|
|
9
|
+
indexPath() { return path.join(this.rootDir, 'index.json'); }
|
|
10
|
+
mediaPath(file) { return path.join(this.rootDir, path.basename(file)); }
|
|
11
|
+
queue = Promise.resolve();
|
|
12
|
+
save(manifest, data) {
|
|
13
|
+
const run = this.queue.then(() => this.doSave(manifest, data));
|
|
14
|
+
// keep the chain alive even if a save rejects, so later saves still run
|
|
15
|
+
this.queue = run.catch(() => { });
|
|
16
|
+
return run;
|
|
17
|
+
}
|
|
18
|
+
async doSave(manifest, data) {
|
|
19
|
+
await fs.mkdir(this.rootDir, { recursive: true });
|
|
20
|
+
await fs.writeFile(this.mediaPath(manifest.file), data);
|
|
21
|
+
await fs.writeFile(this.manifestPath(manifest.id), JSON.stringify(manifest, null, 2));
|
|
22
|
+
const index = await this.list();
|
|
23
|
+
const next = [...index.filter((m) => m.id !== manifest.id), manifest];
|
|
24
|
+
await fs.writeFile(this.indexPath(), JSON.stringify(next, null, 2));
|
|
25
|
+
}
|
|
26
|
+
async get(id) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(await fs.readFile(this.manifestPath(id), 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async readBytes(id) {
|
|
35
|
+
const m = await this.get(id);
|
|
36
|
+
if (!m)
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
return await fs.readFile(this.mediaPath(m.file));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async list() {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(await fs.readFile(this.indexPath(), 'utf8'));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=assetStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assetStore.js","sourceRoot":"","sources":["../../src/store/assetStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deps.js","sourceRoot":"","sources":["../../src/tools/deps.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { imageResult, errorResult } from './result.js';
|
|
4
|
+
import { extFromMime } from '../util/mime.js';
|
|
5
|
+
export const editImageInput = {
|
|
6
|
+
id: z.string().describe('id ранее сгенерированного изображения'),
|
|
7
|
+
prompt: z.string().describe('Что изменить'),
|
|
8
|
+
model: z.string().optional(),
|
|
9
|
+
};
|
|
10
|
+
export async function editImageHandler(input, deps) {
|
|
11
|
+
try {
|
|
12
|
+
const source = await deps.store.get(input.id);
|
|
13
|
+
const bytes = await deps.store.readBytes(input.id);
|
|
14
|
+
if (!source || !bytes)
|
|
15
|
+
throw new Error(`Изображение с id="${input.id}" не найдено.`);
|
|
16
|
+
const model = input.model ?? deps.config.editModel;
|
|
17
|
+
const media = await deps.client.editImage({
|
|
18
|
+
prompt: input.prompt, model, image: { data: bytes, mimeType: source.mimeType },
|
|
19
|
+
});
|
|
20
|
+
const id = randomUUID().slice(0, 8);
|
|
21
|
+
const manifest = {
|
|
22
|
+
id, parentId: source.id, kind: 'image', prompt: input.prompt, model,
|
|
23
|
+
file: `${id}.${extFromMime(media.mimeType)}`, mimeType: media.mimeType,
|
|
24
|
+
createdAt: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
await deps.store.save(manifest, media.data);
|
|
27
|
+
deps.session.setLastImageId(id);
|
|
28
|
+
return imageResult(media, `Готово. id=${id} (правка ${source.id}), файл: ${manifest.file}.`);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
return errorResult(err);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function registerEditImage(server, deps) {
|
|
35
|
+
server.registerTool('edit_image', { title: 'Edit image', description: 'Отредактировать сохранённое изображение по его id и текстовой инструкции.', inputSchema: editImageInput },
|
|
36
|
+
// cast: handler returns our CallToolResult; SDK expects its own index-signature variant (runtime-compatible).
|
|
37
|
+
(input) => editImageHandler(input, deps));
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=editImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editImage.js","sourceRoot":"","sources":["../../src/tools/editImage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAuB,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,uCAAuC,CAAC;IAChE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;IAC3C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAqD,EACrD,IAAc;IAEd,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACnD,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,KAAK,CAAC,EAAE,eAAe,CAAC,CAAC;QACrF,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC;QACnD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC;YACxC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE;SAC/E,CAAC,CAAC;QACH,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAkB;YAC9B,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK;YACnE,IAAI,EAAE,GAAG,EAAE,IAAI,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACtE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC;QACF,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;QAChC,OAAO,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC,EAAE,YAAY,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC;IAC/F,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAiB,EAAE,IAAc;IACjE,MAAM,CAAC,YAAY,CACjB,YAAY,EACZ,EAAE,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,2EAA2E,EAAE,WAAW,EAAE,cAAc,EAAE;IAC9I,8GAA8G;IAC9G,CAAC,KAAK,EAAE,EAAE,CAAC,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAQ,CAChD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { imageResult, errorResult } from './result.js';
|
|
4
|
+
import { extFromMime } from '../util/mime.js';
|
|
5
|
+
export const generateImageInput = {
|
|
6
|
+
prompt: z.string().describe('Что нарисовать'),
|
|
7
|
+
model: z.string().optional().describe('Переопределить модель (по умолчанию Imagen 4)'),
|
|
8
|
+
};
|
|
9
|
+
export async function generateImageHandler(input, deps) {
|
|
10
|
+
try {
|
|
11
|
+
const model = input.model ?? deps.config.defaultImageModel;
|
|
12
|
+
const media = await deps.client.generateImage({ prompt: input.prompt, model });
|
|
13
|
+
const id = randomUUID().slice(0, 8);
|
|
14
|
+
const manifest = {
|
|
15
|
+
id, parentId: null, kind: 'image', prompt: input.prompt, model,
|
|
16
|
+
file: `${id}.${extFromMime(media.mimeType)}`, mimeType: media.mimeType,
|
|
17
|
+
createdAt: new Date().toISOString(),
|
|
18
|
+
};
|
|
19
|
+
await deps.store.save(manifest, media.data);
|
|
20
|
+
deps.session.setLastImageId(id);
|
|
21
|
+
return imageResult(media, `Готово. id=${id}, файл: ${manifest.file} (в ${deps.config.workingDir}).`);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return errorResult(err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function registerGenerateImage(server, deps) {
|
|
28
|
+
server.registerTool('generate_image', { title: 'Generate image', description: 'Сгенерировать изображение по тексту через Gemini (Imagen 4).', inputSchema: generateImageInput },
|
|
29
|
+
// cast: handler returns our CallToolResult; SDK expects its own index-signature variant (runtime-compatible).
|
|
30
|
+
(input) => generateImageHandler(input, deps));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=generateImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generateImage.js","sourceRoot":"","sources":["../../src/tools/generateImage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAuB,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAC7C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+CAA+C,CAAC;CACvF,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAyC,EACzC,IAAc;IAEd,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC;QAC3D,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/E,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAkB;YAC9B,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK;YAC9D,IAAI,EAAE,GAAG,EAAE,IAAI,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACtE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC;QACF,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;QAChC,OAAO,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,WAAW,QAAQ,CAAC,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;IACvG,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAAiB,EAAE,IAAc;IACrE,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB,EAAE,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,8DAA8D,EAAE,WAAW,EAAE,kBAAkB,EAAE;IACzI,8GAA8G;IAC9G,CAAC,KAAK,EAAE,EAAE,CAAC,oBAAoB,CAAC,KAAK,EAAE,IAAI,CAAQ,CACpD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { imageResult, errorResult } from './result.js';
|
|
4
|
+
import { extFromMime } from '../util/mime.js';
|
|
5
|
+
export const generateVideoInput = {
|
|
6
|
+
prompt: z.string().describe('Что снять'),
|
|
7
|
+
model: z.string().optional(),
|
|
8
|
+
};
|
|
9
|
+
export async function generateVideoHandler(input, deps) {
|
|
10
|
+
try {
|
|
11
|
+
const model = input.model ?? deps.config.videoModel;
|
|
12
|
+
const media = await deps.client.generateVideo({ prompt: input.prompt, model });
|
|
13
|
+
const id = randomUUID().slice(0, 8);
|
|
14
|
+
const manifest = {
|
|
15
|
+
id, parentId: null, kind: 'video', prompt: input.prompt, model,
|
|
16
|
+
file: `${id}.${extFromMime(media.mimeType)}`, mimeType: media.mimeType,
|
|
17
|
+
createdAt: new Date().toISOString(),
|
|
18
|
+
};
|
|
19
|
+
await deps.store.save(manifest, media.data);
|
|
20
|
+
return imageResult(media, `Видео готово. id=${id}, файл: ${manifest.file} (в ${deps.config.workingDir}).`);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
return errorResult(err);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function registerGenerateVideo(server, deps) {
|
|
27
|
+
server.registerTool('generate_video', { title: 'Generate video', description: 'Сгенерировать видео по тексту через Gemini (Veo). v1 ждёт завершения рендера.', inputSchema: generateVideoInput },
|
|
28
|
+
// cast: handler returns our CallToolResult; SDK expects its own index-signature variant (runtime-compatible).
|
|
29
|
+
(input) => generateVideoHandler(input, deps));
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=generateVideo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generateVideo.js","sourceRoot":"","sources":["../../src/tools/generateVideo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAuB,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC;IACxC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAyC,EACzC,IAAc;IAEd,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;QACpD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/E,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAkB;YAC9B,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK;YAC9D,IAAI,EAAE,GAAG,EAAE,IAAI,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACtE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC;QACF,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5C,OAAO,WAAW,CAAC,KAAK,EAAE,oBAAoB,EAAE,WAAW,QAAQ,CAAC,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;IAC7G,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAAiB,EAAE,IAAc;IACrE,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB,EAAE,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,+EAA+E,EAAE,WAAW,EAAE,kBAAkB,EAAE;IAC1J,8GAA8G;IAC9G,CAAC,KAAK,EAAE,EAAE,CAAC,oBAAoB,CAAC,KAAK,EAAE,IAAI,CAAQ,CACpD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { errorResult } from './result.js';
|
|
3
|
+
import { editImageHandler } from './editImage.js';
|
|
4
|
+
export const iterateInput = {
|
|
5
|
+
change: z.string().describe('Что изменить относительно текущего изображения'),
|
|
6
|
+
id: z.string().optional().describe('id базового изображения (по умолчанию — последнее в сессии)'),
|
|
7
|
+
model: z.string().optional(),
|
|
8
|
+
};
|
|
9
|
+
export async function iterateHandler(input, deps) {
|
|
10
|
+
const baseId = input.id ?? deps.session.getLastImageId();
|
|
11
|
+
if (!baseId) {
|
|
12
|
+
return errorResult(new Error('Нет изображения для итерации: сначала сгенерируй картинку или укажи id.'));
|
|
13
|
+
}
|
|
14
|
+
return editImageHandler({ id: baseId, prompt: input.change, model: input.model }, deps);
|
|
15
|
+
}
|
|
16
|
+
export function registerIterate(server, deps) {
|
|
17
|
+
server.registerTool('iterate', { title: 'Iterate on image', description: 'Доработать последнее (или указанное) изображение словами: «теплее», «тот же персонаж, другой ракурс» и т.п.', inputSchema: iterateInput },
|
|
18
|
+
// cast: handler returns our CallToolResult; SDK expects its own index-signature variant (runtime-compatible).
|
|
19
|
+
(input) => iterateHandler(input, deps));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=iterate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"iterate.js","sourceRoot":"","sources":["../../src/tools/iterate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,WAAW,EAAuB,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,gDAAgD,CAAC;IAC7E,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,6DAA6D,CAAC;IACjG,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,KAAsD,EACtD,IAAc;IAEd,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;IACzD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,WAAW,CAAC,IAAI,KAAK,CAAC,yEAAyE,CAAC,CAAC,CAAC;IAC3G,CAAC;IACD,OAAO,gBAAgB,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;AAC1F,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAiB,EAAE,IAAc;IAC/D,MAAM,CAAC,YAAY,CACjB,SAAS,EACT,EAAE,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,6GAA6G,EAAE,WAAW,EAAE,YAAY,EAAE;IACpL,8GAA8G;IAC9G,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAQ,CAC9C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function imageResult(media, summary) {
|
|
2
|
+
if (media.mimeType.startsWith('image/')) {
|
|
3
|
+
return {
|
|
4
|
+
content: [
|
|
5
|
+
{ type: 'text', text: summary },
|
|
6
|
+
{ type: 'image', data: media.data.toString('base64'), mimeType: media.mimeType },
|
|
7
|
+
],
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return { content: [{ type: 'text', text: summary }] };
|
|
11
|
+
}
|
|
12
|
+
export function errorResult(err) {
|
|
13
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14
|
+
return { content: [{ type: 'text', text: `Ошибка генерации: ${message}` }], isError: true };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=result.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.js","sourceRoot":"","sources":["../../src/tools/result.ts"],"names":[],"mappings":"AAUA,MAAM,UAAU,WAAW,CAAC,KAAkB,EAAE,OAAe;IAC7D,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxC,OAAO;YACL,OAAO,EAAE;gBACP,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE;gBAC/B,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE;aACjF;SACF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB,OAAO,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC9F,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mime.js","sourceRoot":"","sources":["../../src/util/mime.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,MAAM,GAAG,GAA2B;QAClC,WAAW,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM;QAC7D,WAAW,EAAE,KAAK;KACnB,CAAC;IACF,OAAO,GAAG,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC;AAChC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gemini-studio-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for iterative image & video generation via the Gemini API (Imagen 4, Gemini 3 Pro Image, Veo) — generate and refine in plain words, with lineage.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "gemini-studio-mcp": "dist/index.js" },
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"engines": { "node": ">=20" },
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Armen (@arterorx)",
|
|
11
|
+
"homepage": "https://github.com/arterorx/gemini-studio-mcp#readme",
|
|
12
|
+
"repository": { "type": "git", "url": "git+https://github.com/arterorx/gemini-studio-mcp.git" },
|
|
13
|
+
"bugs": { "url": "https://github.com/arterorx/gemini-studio-mcp/issues" },
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"gemini",
|
|
18
|
+
"imagen",
|
|
19
|
+
"veo",
|
|
20
|
+
"image-generation",
|
|
21
|
+
"video-generation",
|
|
22
|
+
"ai",
|
|
23
|
+
"claude",
|
|
24
|
+
"anthropic"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@google/genai": "^1.52.0",
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
|
+
"zod": "^3.23.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.5.0",
|
|
39
|
+
"vitest": "^2.0.0",
|
|
40
|
+
"@types/node": "^20.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|