Skip to main content
Glama
README.md
# gemini-image-mcp

MCP server cho **AI Image Studio**: agent vừa **chạy service** (bridge), vừa **gọi API tạo ảnh** —
ảnh do Gemini sinh trong chính trình duyệt của bạn, kết quả trả về dưới dạng file.

```
MCP client (Claude Desktop / Cursor / VS Code)
      │  stdio
      ▼
gemini-image-mcp  ──►  bridge (127.0.0.1)  ──►  trang studio trong trình duyệt  ──►  Gemini
```

4 tool: `bridge_status` · `bridge_start` · `bridge_stop` · `image_generate`

## Bắt đầu trong 3 bước

**1. Thêm config vào MCP client**

*Claude Desktop* (`~/Library/Application Support/Claude/claude_desktop_config.json`;
Windows: `%APPDATA%\Claude\claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "gemini-image-mcp": {
      "command": "npx",
      "args": ["-y", "github:nguyennamkkb/gemini-image-mcp"]
    }
  }
}
```

*Cursor* (`~/.cursor/mcp.json`) và *VS Code* (`.vscode/mcp.json`): cùng dạng `mcpServers` ở trên
(VS Code còn nhận `{"servers": {…: {"type": "stdio", …}}}`).

*opencode* — dùng 2 dòng ngắn (đừng dán 1 dòng dài, dễ bị xuống dòng làm `github:…` thành lệnh riêng):

```bash
SPEC=github:nguyennamkkb/gemini-image-mcp
opencode mcp add --global gemini-image-mcp -- npx -y $SPEC
opencode mcp list        # phải thấy "✓ gemini-image-mcp connected" (lần đầu chờ ~10s cho npx tải)
```

Hoặc viết tay vào `~/.config/opencode/opencode.json` (toàn cục) hay `opencode.json` ở gốc project:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "gemini-image-mcp": {
      "type": "local",
      "command": ["npx", "-y", "github:nguyennamkkb/gemini-image-mcp"],
      "enabled": true,
      "timeout": 30000
    }
  }
}
```

Hai điểm bắt buộc phải biết cho opencode:

- **Đặt `timeout` ≥ 30000.** Lần chạy đầu `npx` phải tải package (~9s đo thực tế), mà mặc định
  opencode chỉ chờ 5000ms để lấy danh sách tool → server hiện `pending`/failed dù cấu hình đúng.
- `opencode.json` ở gốc project **chỉ được đọc khi thư mục đó là git workspace** (ngoài git repo
  opencode báo "No MCP servers configured"). Muốn dùng mọi nơi thì thêm `--global` như trên.
- Shape phẳng `"mcp": {"gemini-image-mcp": {…}}` (như trên) và shape lồng `"mcp": {"servers": {…}}`
  (CLI v2 sinh ra) đều chạy — đã thử cả hai trên opencode 2.0.32.

> Config trên luôn lấy bản mới nhất từ nhánh `main`. Muốn cố định, thêm `#v1.0.2` (hoặc commit sha)
> vào sau tên repo, ví dụ `github:nguyennamkkb/gemini-image-mcp#v1.0.2`.

**2. Khởi động lại client** (thoát hẳn rồi mở lại). Trong danh sách tool phải thấy đủ 4 tool ở trên.

**3. Mở trang studio thành worker.** Nhờ agent gọi `bridge_start` — nó trả về `pageUrl`
(dạng `http://127.0.0.1:8765/?token=…`). **Mở link đó trong Chrome** rồi để yên tab đấy.
Sau đó `image_generate` mới có worker để chạy.

> Đang dùng **Gemini Canvas** (để Canvas cấp key Gemini) thì gọi
> `bridge_start {"allowCanvas": true}` và mở trang studio trong tab Canvas — xem
> [ràng buộc trình duyệt](#vì-sao-phải-có-trang-studio-mở).

## Workflow đang dùng: Gemini Canvas (1–1)

Cách dùng **cố định của máy này** — worker là **tab Gemini Canvas**, không phải tab local.
Mọi thứ khác chạy tự động; đọc mục này trước khi sửa/nâng cấp để không đi lạc hướng.

```
opencode ──stdio──► gemini-image-mcp ──HTTP/WS──► bridge (127.0.0.1:8765)
                                                      ▲
                                     HTTP long-poll  │  (Canvas cấm WebSocket)
                                                      │
                                  tab Gemini Canvas (ai_image_studio.html) ──► Gemini
                                  (Canvas tự cấp key — KHÔNG cần key trong trang)
```

### 1. Config MCP (`opencode.json` ở gốc project)

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "gemini-image-mcp": {
      "type": "local",
      "command": ["npx", "-y", "github:nguyennamkkb/gemini-image-mcp"],
      "enabled": true,
      "timeout": 30000,
      "environment": {
        "AI_STUDIO_ALLOW_CANVAS": "true",
        "AI_STUDIO_TOKEN": "aistudio-1-1-7f2c9d41b8e35a60"
      }
    }
  }
}
```

| Dòng | Vì sao cần |
|---|---|
| `timeout: 30000` | Lần đầu `npx` tải package ~9s; mặc định opencode chờ 5000ms → server hiện `pending` dù đúng config |
| `AI_STUDIO_ALLOW_CANVAS: "true"` | Bridge **tự start** (do `image_generate`) luôn có `--allow-canvas` → nhận worker Canvas |
| `AI_STUDIO_TOKEN` | Token **cố định** để bridge ↔ trang bắt tay, không lệch khi file `~/.ai-image-studio/.token` bị xoá |

Giá trị cần dán vào tab Canvas (một lần, localStorage giữ mãi):

- URL: `ws://127.0.0.1:8765/ws`
- Token: `aistudio-1-1-7f2c9d41b8e35a60`

> Để trống ô token cũng được — trang tự dò qua `GET /api/discover` (endpoint này trả token),
> nên hai bên luôn khớp kể cả khi bạn quên.

**Nếu dùng Claude Code (`.mcp.json`)** — cùng env nhưng tên key khác:

```json
{
  "mcpServers": {
    "gemini-image-mcp": {
      "command": "npx",
      "args": ["-y", "github:nguyennamkkb/gemini-image-mcp"],
      "env": { "AI_STUDIO_ALLOW_CANVAS": "true", "AI_STUDIO_TOKEN": "aistudio-1-1-7f2c9d41b8e35a60" }
    }
  }
}
```

| opencode | Claude Code | Ghi chú |
|---|---|---|
| `mcp` | `mcpServers` | key gốc — **`mcpServers` KHÔNG có tác dụng với opencode** (và ngược lại) |
| `command` (mảng) | `command` (chuỗi) + `args` (mảng) | |
| `environment` | `env` | biến cho tiến trình server (đặt `AI_STUDIO_*` ở đây) |
| `timeout: 30000` | — | Claude Code chỉnh bằng env `MCP_TIMEOUT` / `MCP_TOOL_TIMEOUT` của **client**, đặt trong `env` của server là vô tác dụng: `export MCP_TOOL_TIMEOUT=600000` (ảnh có thể mất vài phút) |

### 2. Chrome phải tắt Local Network Access (Chrome 142+)

Trang Canvas là origin `https://…usercontent.goog`; Chrome 155 **chặn** nó gọi `http://127.0.0.1`
→ mọi cổng báo `Failed to fetch`. Bắt buộc mở Chrome kèm cờ:

```bash
# alias đã có trong ~/.zshrc:
alias chromedev='open -na "Google Chrome" --args --disable-features=LocalNetworkAccessChecks'

pkill -f "Google Chrome"   # PHẢI thoát hẳn Chrome trước khi chạy chromedev
chromedev                  # dùng profile mặc định (giữ đăng nhập Google/Gemini)

# kiểm chứng cờ đã vào (kỳ vọng ≥1):
ps aux | grep "Contents/MacOS/Google Chrome" | grep -v grep | grep -c LocalNetworkAccessChecks
```

### 3. Vận hành hằng ngày

1. `chromedev` → mở lại tab **Gemini Canvas** chứa `ai_image_studio.html` → để yên tab đó
2. Gọi `image_generate {"prompt": "…"}` từ opencode — bridge tự start, tự chờ worker, trả `filePath`
3. Tab Canvas hiện `Đang tạo ảnh` / worker `h_… HTTP long-poll` là dấu hiệu đúng

**Quy tắc bất di bất dịch**

- **KHÔNG sửa `ai_image_studio.html`.** Bản trong tab Canvas là bản bạn đã dán; sửa file trong repo
  sẽ làm lệch hai bên và gây nhầm lẫn. Mọi bản vá nằm ở `bridge/` + `mcp/`.
  - *Ngoại lệ đã chấp nhận (1.0.9):* ô token trong panel "Cấu hình API local" đổi
    `type="password"` → `type="text"` để **nhìn thấy token cố định** cho tiện đối chiếu hai bên.
    Đây là token local (loopback + origin policy), không phải khoá Gemini. Sửa trên do người dùng
    chủ động; tab Canvas muốn thấy y hệt thì **dán lại** `ai_image_studio.html` mới vào Canvas một lần.
- **Không cần key Gemini** trong trang (Canvas cấp) và **không cần dán token** (tự dò).
- Chỉ cần **1 worker** (tab Canvas). Bridge ở chế độ canvas **không mở tab local** nữa, nên không
  có chuyện job rơi vào tab không key (xem bảng sự cố bên dưới).

### 4. Nâng cấp lên bản mới (repo không dùng tag, luôn theo `main`)

```bash
# phía repo (người phát hành):
#   1) sửa code → 2) tăng "version" trong package.json → 3) commit & push main
git commit -am "..." && git push origin main

# phía máy dùng (bắt buộc, nếu không npx vẫn dùng bản cũ đã cache):
rm -rf ~/.npm/_npx/*/node_modules/gemini-image-mcp ~/.npm/_npx/<hash>
# rồi thoát hẳn opencode và mở lại → npx tải bản mới
```

Kiểm tra đang chạy bản nào:

```bash
opencode mcp list                     # phải thấy ✓ gemini-image-mcp connected
# trong phiên: gọi bridge_status {}   → "version" của bridge
#              initialize (client tự gọi) → serverInfo.version = version trong package.json
python3 -c "import json;print(json.load(open('$(ls -d ~/.npm/_npx/*/node_modules/gemini-image-mcp/package.json | head -1)'))['version'])"
```

Muốn cố định một bản thay vì theo `main`: thêm `#<commit-sha>` sau tên repo trong `command`.

### 5. Sự cố thường gặp (đã gặp thật, kèm cách sửa)

| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Trang Canvas: mọi cổng `Failed to fetch` | Chrome chặn LNA (142+) | Thoát hẳn Chrome → `chromedev` → mở lại tab Canvas |
| `origin-not-allowed` / HTTP 403 từ origin Canvas | Bridge chạy thiếu `--allow-canvas` | Từ 1.0.5: `bridge_start {allowCanvas:true}` tự dừng + start lại bridge thiếu quyền; hoặc đặt `AI_STUDIO_ALLOW_CANVAS=true` |
| `502 Method doesn't allow unregistered callers` | Tab local (`?token=…`) không có key Gemini lại nhận job | Từ 1.0.6: bridge chế độ canvas **không mở tab local** → đóng tab local nếu còn mở |
| `503 chưa có worker` | Tab Canvas chưa mở/chưa bấm "Thử lại" | `bridge_status` → mở tab Canvas theo `pageUrl`/panel |
| Server `pending`/failed trong opencode | `timeout` < 30000, hoặc config sai key (`mcpServers` là của Claude, opencode dùng `mcp`) | Sửa `opencode.json` như mục 1 |
| Bridge start nhưng token lệch với trang | Token ngẫu nhiên bị sinh lại (xoá `~/.ai-image-studio/.token`) | Dùng `AI_STUDIO_TOKEN` cố định như mục 1 |

Dừng dịch vụ khi cần: `bridge_stop {"port": 8765}` (chỉ kill khi `/api/status` xác nhận đúng bridge).

## Dùng

```
bridge_status {}                               → bridge nào đang chạy, worker nào nối, link mở trang
bridge_start  {"allowCanvas": true}            → chạy service nền, trả pageUrl + pid + logFile
image_generate {"prompt":"mèo phi hành gia, 3D cinematic","aspectRatio":"16:9"}
image_generate {"prompt":"biến thể lúc hoàng hôn","refImagePath":"/tmp/in.png","inline":true}
bridge_stop   {"port":8765}                    → dừng bridge (chỉ kill khi /api/status xác nhận đúng bridge)
```

`image_generate` trả về `{ok, filePath, bytes, mime, aspectRatio, tookMs, workerId}`; đặt
`inline:true` để kèm luôn ảnh vào kết quả cho client hiển thị.

## Dữ liệu lưu ở đâu

| Đường dẫn | Nội dung |
|---|---|
| `<project đang gọi tool>/generated-images/` | Ảnh tạo ra **khi client chạy trong một project** (opencode/Cursor/VS Code spawn MCP server với `cwd` = gốc workspace, đã đo thực tế) |
| `~/Pictures/ai-image-studio/` | Ảnh tạo ra khi không xác định được project (ví dụ Claude Desktop) |
| `~/.ai-image-studio/.token` | Token bí mật để local API/bridge xác thực (sinh tự động, quyền `600`) |
| `~/.ai-image-studio/logs/` | Log của bridge do MCP chạy nền |
| `~/.ai-image-studio/.bridge-state.json` | pid/port của bridge mà MCP đã chạy (để `bridge_stop` biết) |

Hai biến môi trường để đổi chỗ lưu (thêm vào `"env"` trong config nếu cần):

| Biến | Mặc định | Ý nghĩa |
|---|---|---|
| `AI_STUDIO_HOME` | `~/.ai-image-studio` | Token, log, state của bridge |
| `AI_STUDIO_OUTPUT` | *(tự chọn theo project)* | Ép nơi lưu ảnh, ghi đè cả quy tắc theo project |
| `AI_STUDIO_ALLOW_CANVAS` | *(tắt)* | `true` → bridge tự start (bởi `image_generate`) luôn mang `--allow-canvas`, sẵn sàng nhận worker Gemini Canvas |
| `AI_STUDIO_START_ARGS` | *(trống)* | Chèn thêm tham số cho bridge tự start, ví dụ `--allow-origin https://your-host` |
| `AI_STUDIO_TOKEN` | *(tự sinh)* | **Token cố định** để bridge ↔ trang studio bắt tay (thay vì token ngẫu nhiên sinh một lần rồi lưu ở `~/.ai-image-studio/.token`). Đặt trong `env` của config MCP ⇒ bridge và MCP dùng chung đúng một token, dán vào trang Canvas một lần là dùng mãi. Tên cũ `BRIDGE_TOKEN` vẫn chấp nhận |

### Ảnh lưu vào đâu

Thứ tự ưu tiên (cao → thấp), mỗi kết quả `image_generate` đều trả `filePath` + `outputDir` + `outputSource` để biết vì sao:

1. `savePath` — đường dẫn file chính xác truyền trong lệnh.
2. `outputDir` — thư mục truyền trong lệnh (tự đặt tên file).
3. `AI_STUDIO_OUTPUT` — biến môi trường.
4. `<project đang gọi tool>/generated-images` — khi `cwd` của server là một project (có `.git`/`package.json`).
5. `~/Pictures/ai-image-studio` — khi không xác định được project.

Muốn ảnh luôn vào project hiện tại khi dùng opencode thì không cần làm gì thêm (nó spawn server ở gốc
workspace). Muốn đổi chỗ khác thì đặt `AI_STUDIO_OUTPUT` trong `"environment"`/`"env"` của config.

## Yêu cầu

- **Node ≥ 20** (MCP server + bridge đều zero-dependency ngoài SDK chính thức).
- **Một trang studio đang mở làm worker**: hoặc mở link bridge (`pageUrl`) trong Chrome, hoặc
  dán `ai_image_studio.html` vào **Gemini Canvas** (Canvas cấp key Gemini nên không cần API key).

## Vì sao phải có trang studio mở

Ảnh do Gemini sinh **trong trình duyệt** (Canvas tự inject key), nên luôn cần ít nhất một worker là
trang `ai_image_studio.html` đang mở. Trình duyệt cũng chặn khá nhiều đường về máy local, và bridge
đã xử lý từng trường hợp:

| Trang chạy ở | Rào cản | Cách dùng |
|---|---|---|
| `http://127.0.0.1:8765/` (link `pageUrl`) | không có | Mở link, xong. WebSocket chạy luôn |
| Tab **Gemini Canvas** | Canvas cấm `new WebSocket`; Chrome chặn Local Network Access với origin công khai; origin Canvas không phải loopback | `bridge_start {"allowCanvas": true}` + Chrome chạy kèm `--disable-features=LocalNetworkAccessChecks` |
| Origin khác (host LAN, https khác) | như trên, và phải khai báo origin | `bridge_start {"allowOrigin": ["https://your-host"]}` |

Chi tiết + bằng chứng đo được: [bridge/README.md](bridge/README.md). MCP: [mcp/README.md](mcp/README.md).

## Phát triển

```bash
npm install
npm run smoke                                  # handshake + tools/list + bridge_status + kiểm tra đường dẫn
npm run smoke -- --with-service                # thêm vòng đời start → status → stop
npm run smoke -- --with-image                  # thêm tạo ảnh thật (cần worker đang mở)
npm pack                                       # gói phát hành, kiểm tra nội dung .tgz
```

Cấu trúc:

| Đường dẫn | Vai trò |
|---|---|
| `mcp/server.mjs` | MCP server (stdio) + 4 tool |
| `mcp/smoke-test.mjs` | Test qua stdio JSON-RPC thật |
| `bridge/server.mjs` | Bridge: WS hub + HTTP API + HTTP long-poll cho Canvas |
| `ai_image_studio.html` | Trang studio (UI + client bridge) |

### Phát hành bản mới

Không dùng tag — người dùng luôn lấy bản mới nhất từ `main`:

1. Sửa code, tăng `version` trong `package.json` (version này được MCP trả trong `initialize`
   nên kiểm tra được đang chạy bản nào).
2. `git commit && git push origin main`
3. Người dùng chạy lại client là xong (`npx` sẽ lấy bản mới). Ai muốn cố định thì pin
   `github:nguyennamkkb/gemini-image-mcp#<commit-sha>`.

Các tag cũ (`v1.0.0` → `v1.0.2`) vẫn còn trên remote để ai đang pin thì không gãy; xoá bằng
`git push origin :refs/tags/v1.0.0` nếu không muốn giữ.

## License

MIT — xem [LICENSE](LICENSE).