Skip to main content
Glama
README.md
# img2pdf

> Built by [Tamim](https://tamimlikhon.me) · [@tamimbuilds](https://x.com/tamimbuilds)

A local MCP (Model Context Protocol) server that converts images to PDF and PDFs to images. Supports
**any AI agent** via two transport modes:

- **Streamable HTTP** — for Cursor, Claude Code, and any HTTP-capable MCP client
- **stdio** — for Claude Desktop, VS Code MCP, Cline, Roo Code, Windsurf, Continue, and any stdio-spawning agent

## What's inside

| Tool                      | Description                                                 |
| ------------------------- | ----------------------------------------------------------- |
| `convert_images_to_pdf`   | Convert local files, URLs, or base64 data into a single PDF |
| `convert_pdf_to_images`   | Extract individual images from a PDF file                   |
| `get_server_time`         | Sanity-check tool to confirm the connection is working      |

## File structure

```
tamim-mcp-server/
├── src/
│   ├── server.ts            # Fastify HTTP transport (Streamable HTTP)
│   ├── stdio-server.ts      # stdio transport
│   ├── mcp.ts               # createMcpServer() factory
│   ├── config.ts            # env vars (port, token)
│   └── tools/
│       ├── index.ts         # registerTools() - wires every tool in
│       ├── time.ts          # get_server_time
│       └── imagesToPdf.ts   # convert_images_to_pdf
├── .env.example
├── .gitignore
├── package.json
├── tsconfig.json
└── README.md
```

## 1. Install

```bash
npm install
cp .env.example .env
# open .env and set MCP_TOKEN (openssl rand -hex 24), or leave it blank for no auth
```

## 2. Run it

```bash
npm run dev      # tsx watch, auto-reloads on save
# or, for a built version:
npm run build && npm start
```

You should see:

```
MCP server ready on http://127.0.0.1:8787/mcp
```

## 3. Test it before touching any client

```bash
curl -i -X POST http://127.0.0.1:8787/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
```

A `200 OK` with a `mcp-session-id` header back means the server is healthy.
Or use the official MCP Inspector for a UI instead of raw curl:

```bash
npx @modelcontextprotocol/inspector
# point it at http://127.0.0.1:8787/mcp
```

---

## 4. Connect any AI agent

### HTTP agents (server must already be running)

#### Cursor

`.cursor/mcp.json` (project root) or `~/.cursor/mcp.json` (global):

```json
{
  "mcpServers": {
    "tamim-local-mcp": {
      "url": "http://127.0.0.1:8787/mcp",
      "headers": { "Authorization": "Bearer YOUR_TOKEN_HERE" }
    }
  }
}
```

Omit the `headers` block entirely if you left `MCP_TOKEN` blank.

#### Claude Code

```bash
claude mcp add --transport http tamim-local-mcp http://127.0.0.1:8787/mcp \
  --header "Authorization: Bearer YOUR_TOKEN_HERE"
```

---

### stdio agents (auto-spawned, no manual server start needed)

#### Claude Desktop

`claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "tamim-local-mcp": {
      "command": "npx",
      "args": ["-y", "tamim-mcp-stdio"]
    }
  }
}
```

If you installed locally instead of publishing to npm:

```json
{
  "mcpServers": {
    "tamim-local-mcp": {
      "command": "node",
      "args": ["dist/stdio-server.js"],
      "cwd": "/absolute/path/to/tamim-mcp-server"
    }
  }
}
```

#### VS Code / GitHub Copilot

`.vscode/mcp.json` (project root):

```json
{
  "servers": {
    "tamim-local-mcp": {
      "command": "node",
      "args": ["dist/stdio-server.js"],
      "cwd": "${workspaceFolder}"
    }
  }
}
```

#### Cline / Roo Code

`~/.cline/mcp.json` or `~/.roo/mcp.json`:

```json
{
  "mcpServers": {
    "tamim-local-mcp": {
      "command": "node",
      "args": ["dist/stdio-server.js"],
      "cwd": "/absolute/path/to/tamim-mcp-server"
    }
  }
}
```

#### Windsurf

`.windsurf/mcp.json`:

```json
{
  "mcpServers": {
    "tamim-local-mcp": {
      "command": "node",
      "args": ["dist/stdio-server.js"],
      "cwd": "/absolute/path/to/tamim-mcp-server"
    }
  }
}
```

#### Continue

`.continue/config.yaml`:

```yaml
mcpServers:
  - name: tamim-local-mcp
    command: node
    args:
      - dist/stdio-server.js
    cwd: /absolute/path/to/tamim-mcp-server
```

#### Generic stdio config (works with any MCP client)

```json
{
  "command": "node",
  "args": ["dist/stdio-server.js"],
  "cwd": "/absolute/path/to/tamim-mcp-server"
}
```

---

## 5. Tool usage

### `convert_images_to_pdf`

Accepts images from three sources (at least one required):

```json
// Local file paths
{
  "imagePaths": ["/path/to/photo1.jpg", "/path/to/photo2.png"],
  "outputPath": "/path/to/output.pdf"
}

// URLs
{
  "urls": ["https://example.com/image1.jpg", "https://example.com/image2.png"],
  "outputPath": "/path/to/output.pdf"
}

// Base64 data
{
  "base64Images": ["/9j/4AAQSkZJRg...", "iVBORw0KGgo..."],
  "outputPath": "/path/to/output.pdf"
}

// Mix of all three
{
  "imagePaths": ["/path/local.jpg"],
  "urls": ["https://example.com/photo.png"],
  "base64Images": ["iVBORw0KGgo..."],
  "outputPath": "/path/to/output.pdf"
}
```

Options:

- `pageSize`: `"fit"` (default) — each page matches its image, or `"a4"` — centers on A4
- `marginPt`: margin in points when using A4 (default: 24)

### `convert_pdf_to_images`

Extracts individual pages from a PDF as images (JPEG or PNG). It creates a dedicated folder `[PDF_NAME]_to_images` within your specified output directory.

```json
{
  "pdfPath": "/path/to/document.pdf",
  "outputDir": "/path/to/downloads"
}
```

Options:

- `format`: `"jpeg"` (default) or `"png"`
- `scale`: Rendering scale factor (default: 2, where 1 = 72 DPI)
- `pageNumbers`: Array of specific 1-indexed pages to render (default: all pages)
- `quality`: JPEG quality 1-100 (default: 90)

### `get_server_time`

No parameters. Returns the current server time in ISO format.

---

## Keeping it running (HTTP mode)

HTTP-based MCP servers don't get auto-started by the client the way stdio
ones do - the server has to already be running when HTTP clients try
to connect. Options, easiest first:

- Leave `npm run dev` in a spare terminal tab
- `pm2 start dist/server.js --name mcp` to survive terminal closes and
  auto-restart on crash

**stdio mode does not need this** — the agent spawns the process itself.

## Adding a new tool

1. Create `src/tools/yourTool.ts` exporting `registerYourTool(server)`
2. Call it from `src/tools/index.ts`
3. Keep the tool narrow - one job, a tight zod schema, a description that
   tells the model exactly when to use it and what format inputs should be in

## Security notes

- The HTTP server binds to `127.0.0.1` only - not reachable from outside this
  machine
- DNS rebinding protection is applied automatically by `@modelcontextprotocol/fastify`
- Set `MCP_TOKEN` to require a bearer token even for local HTTP calls
- stdio transport is inherently authenticated by process spawn — no token needed
- Keep total tools per server in the 6-10 range - Cursor caps out around 40
  active tools across all connected servers combined
- [127.0.0.1:8787/mcp](http://127.0.0.1:8787/mcp)
- npx @modelcontextprotocol/inspector node dist/stdio-server.js

node dist/cli.js image.png