Skip to main content
Glama
nguyen1oc

OpenRouter Custom Image Generation MCP Server

by nguyen1oc
README.md
# šŸ–¼ļø MCP Image Generation Server for LibreChat

A custom [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that provides **AI image generation** capabilities to [LibreChat](https://github.com/danny-avila/LibreChat) via [OpenRouter](https://openrouter.ai/).

> **Model**: Locked to `google/gemini-2.5-flash-image` for all requests. To change the model, modify the source code in `tools.js`.

---

## šŸ“‚ Project Structure

```
mcp-image-gen-custom/
ā”œā”€ā”€ index.js          # Express app, SSE transport, MCP server + tool registration
ā”œā”€ā”€ tools.js          # Tool handlers: get_user_images, generate_image, rate limiting
ā”œā”€ā”€ openrouter.js     # OpenRouter API communication (chat completions with image modality)
ā”œā”€ā”€ files.js          # MongoDB file record lookup + local file stream reader
ā”œā”€ā”€ db.js             # MongoDB connection pool manager
ā”œā”€ā”€ Dockerfile        # Container build definition
ā”œā”€ā”€ package.json
└── example/
    ā”œā”€ā”€ docker-compose.override.example.yml   # Docker service config example
    ā”œā”€ā”€ librechat.example.yaml                # librechat.yaml MCP config example
    └── system_instructions.example.md        # Agent system prompt template
```

---

## šŸ› ļø Tools Provided

### 1. `get_user_images`
Retrieves images uploaded by the current user in LibreChat, returning index aliases and `file_id`s for use as reference images.

| Argument | Type | Required | Description |
|---|---|---|---|
| `limit` | number | No | Max images to return. Default: `10` |

**Example response:**
```
Found 2 uploaded image(s):
1. [INDEX_1] file_id: "cab88c4e-aecb-4812-9fff-6c85aede6362" — mycar.png
2. [INDEX_2] file_id: "f6752381-3a6d-4b69-a92b-5555a9521069" — cat.png

Use EITHER the file_id OR the index number (e.g., '1', '2' or 'INDEX_1', 'INDEX_2') as the reference_image_url when calling generate_image.
```

---

### 2. `generate_image`
Generates an image from a text prompt. Supports **Text-to-Image** and **Image-to-Image** (editing) via reference images.

| Argument | Type | Required | Description |
|---|---|---|---|
| `prompt` | string | āœ… | Text description of the image to generate |
| `reference_image_url` | string | No | Index alias (`"1"`, `"INDEX_1"`) or local `file_id` of a single reference image |
| `reference_image_urls` | string[] | No | List of index aliases or `file_id`s for multiple reference images |
| `aspect_ratio` | string | No | Output image dimensions. See options below |

**Aspect ratio options:**

| Value | Shape | Common use |
|---|---|---|
| `1:1` | Square | Profile pictures, feed posts |
| `16:9` | Wide landscape | Banners, cover photos |
| `9:16` | Tall portrait | Stories, Reels, TikTok |
| `4:5` | Portrait | Feed posts (portrait) |

> **Note:** `aspect_ratio` only controls image dimensions — it does **not** affect image style or content.

> **Note:** Do **NOT** pass Google/OpenAI temporary `file-xxxx` IDs into `reference_image_url`. These are not stored in the local database. Always use `get_user_images` first to get valid index aliases or local `file_id`s.

---

## šŸ”’ Per-User Rate Limiting

The server enforces configurable usage limits per user to prevent spam. Limits are tracked in MongoDB (`mcp_image_gen_usage` collection).

| Env Variable | Default | Description |
|---|---|---|
| `IMAGE_GEN_DAILY_LIMIT` | `3` | Max images per user per day (reset at midnight local time) |
| `IMAGE_GEN_COOLDOWN_SEC` | `30` | Min seconds between generations per user |

**To reset limits manually (all users):**
```bash
docker exec chat-mongodb mongosh LibreChat --eval "db.mcp_image_gen_usage.deleteMany({})"
```

**To reset limits for a specific user:**
```bash
docker exec chat-mongodb mongosh LibreChat --eval "db.mcp_image_gen_usage.deleteMany({ userId: ObjectId('<userId>') })"
```

**To view current usage:**
```bash
docker exec chat-mongodb mongosh LibreChat --eval "db.mcp_image_gen_usage.aggregate([{ \$group: { _id: '\$userId', count: { \$sum: 1 } } }]).forEach(r => print(r._id, '->', r.count))"
```

---

## āš™ļø How Index Resolution Works

The LLM agent only sees image bytes in chat history — it does not know the database `file_id`. This server resolves short index aliases (e.g., `"1"` or `"INDEX_1"`) to the correct local `file_id` automatically.

```mermaid
sequenceDiagram
    actor User
    participant Agent as LLM Agent
    participant MCP as MCP Server
    participant DB as MongoDB

    User->>Agent: "Edit this image" (uploads car.png)
    Agent->>MCP: get_user_images()
    MCP->>DB: Query user's latest images
    DB-->>MCP: [car.png → file_id: "abc-123"]
    MCP-->>Agent: "1. [INDEX_1] file_id: 'abc-123' — car.png"
    Agent->>MCP: generate_image(prompt="make it red", reference_image_url="INDEX_1")
    Note over MCP: Resolves "INDEX_1" → "abc-123"
    MCP->>DB: Fetch file "abc-123" from disk
    MCP-->>User: Returns edited image āœ…
```

---

## šŸš€ Installation & Deployment

### Prerequisites
- LibreChat running via Docker Compose
- An [OpenRouter](https://openrouter.ai/) API key

---

### Step 1: Place this folder

Clone or copy this `mcp-image-gen-custom/` directory into the root of your LibreChat repository:

```
LibreChat/
ā”œā”€ā”€ mcp-image-gen-custom/   ← here
ā”œā”€ā”€ api/
ā”œā”€ā”€ docker-compose.yml
└── ...
```

---

### Step 2: Configure Environment Variables

Add the following to your `.env` file at the LibreChat root:

```env
# Required
OPENROUTER_KEY=your_openrouter_api_key_here

# Optional — image generation rate limits
IMAGE_GEN_DAILY_LIMIT=3
IMAGE_GEN_COOLDOWN_SEC=30
```

---

### Step 3: Add the MCP service to Docker Compose

Add the following to your `docker-compose.override.yml` (create it if it doesn't exist):

```yaml
services:
  api:
    volumes:
      - type: bind
        source: ./librechat.yaml
        target: /app/librechat.yaml

  mcp-image-gen:
    build: ./mcp-image-gen-custom
    restart: always
    environment:
      NODE_ENV: ${NODE_ENV:-production}
      PORT: 3013
      OPENROUTER_KEY: ${OPENROUTER_KEY}
      OPENROUTER_API_KEY: ${OPENROUTER_KEY}
      MONGO_URI: mongodb://mongodb:27017/LibreChat
      IMAGE_GEN_DAILY_LIMIT: ${IMAGE_GEN_DAILY_LIMIT:-3}
      IMAGE_GEN_COOLDOWN_SEC: ${IMAGE_GEN_COOLDOWN_SEC:-30}
    volumes:
      - ./uploads:/app/uploads   # shared with LibreChat api container
      - ./images:/app/images     # shared with LibreChat api container
    expose:
      - "3013"
    networks:
      - default
```

> See `example/docker-compose.override.example.yml` for a complete example.

---

### Step 4: Register the MCP server in `librechat.yaml`

Append the following to your `librechat.yaml`:

```yaml
mcpSettings:
  allowedDomains:
    - 'mcp-image-gen'
  allowedAddresses:
    - 'mcp-image-gen:3013'

mcpServers:
  openrouter-imager:
    type: sse
    url: http://mcp-image-gen:3013/sse
    headers:
      x-user-id: '{{LIBRECHAT_USER_ID}}'
```

> See `example/librechat.example.yaml` for a complete example.

---

### Step 5: Build and start

```bash
# Build and start all services
docker compose up -d --build

# Restart the LibreChat API to reload librechat.yaml
docker compose restart api
```

---

### Step 6: Configure the Agent in LibreChat UI

1. Go to **LibreChat → Agents** and create a new agent (or edit an existing one).
2. Enable the **`openrouter-imager`** MCP tool.
3. Paste the system prompt from `example/system_instructions.example.md` into the **Instructions** field.

---

## šŸ”§ Changing the Model

The model is hardcoded to `google/gemini-2.5-flash-image` in [`tools.js`](./tools.js):

```js
const selectedModel = "google/gemini-2.5-flash-image";
```

To use a different model, edit this line directly and rebuild the container:
```bash
docker compose up -d --build mcp-image-gen
```

---

## šŸ“‹ Useful Commands

```bash
# View live MCP server logs
docker logs librechat-mcp-image-gen-1 -f

# Rebuild and restart MCP server only
docker compose up -d --build mcp-image-gen

# Reset all image generation usage limits
docker exec chat-mongodb mongosh LibreChat --eval "db.mcp_image_gen_usage.deleteMany({})"
```