You can compress and convert images from Claude Code, Cursor, VS Code or Codex by adding a remote MCP server to the editor. Then you ask in plain words, and the agent calls a tool. I asked for "this photo as an AVIF under 200 KB, 1600 px wide". One convert_image call took a 9,804,300-byte JPEG to a 196,540-byte AVIF. This article shows the setup, what goes over the wire, and the mistakes that stop a connection.
What does an image MCP server do for you?
An MCP server gives your AI assistant tools it can call. A remote image server runs the encode on its own machines. Your editor sends JSON-RPC; the server fetches or receives the file, encodes it and hands back a download link.
The server here is the one I build: Blinkimg's, at https://api.blinkimg.com/mcp. It speaks Streamable HTTP, the transport in the MCP spec where every client message is a POST to one endpoint. It registers six tools:
| Tool | What it does | Credits |
|---|---|---|
upload_image |
Returns a one-time upload link (15 minutes) and a ready curl command for a local file |
Free |
compress_image |
Makes an image smaller from an image_url or an upload_id
|
1; 2 when target_format changes the format |
convert_image |
Converts to JPEG, PNG, WebP or AVIF; HEIC, JPEG, PNG, WebP and AVIF go in | 2 |
get_job |
Reads a job: status, sizes, download link | Free |
analyze_page |
Scans a public page for image weight | Free; 25 scans a day on Free |
get_quota |
Reads your plan, credits left and limits | Free |
Every account gets 500 free credits a month. The MCP server page lists the setup for each client.
How do you add it to Claude Code?
One command. Leave the header out and Claude Code signs you in with OAuth in the browser:
claude mcp add --transport http blinkimg https://api.blinkimg.com/mcp
To use an API key instead, keep it in an environment variable and put a .mcp.json in the project root:
{
"mcpServers": {
"blinkimg": {
"type": "http",
"url": "https://api.blinkimg.com/mcp",
"headers": {
"Authorization": "Bearer ${BLINKIMG_API_KEY}"
}
}
}
}
Cursor reads the same shape from ~/.cursor/mcp.json, with ${env:BLINKIMG_API_KEY} for the variable. The Claude Code MCP docs and the Cursor MCP docs cover the scopes.
Note: A configured
Authorizationheader turns off the OAuth sign-in in Claude Code. If the editor never offers to sign you in, look for a header you forgot. And if Claude Code reports a missing environment variable,BLINKIMG_API_KEYwas not set when the editor started. Set it, then restart the editor.
What actually goes over the wire?
Before you trust a tool in your editor, it helps to see the protocol by hand. Here is a tools/list request with no key:
curl -s -i -X POST https://api.blinkimg.com/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
The accept header lists both types because the spec requires it. The server answered (headers trimmed):
HTTP/2 401
content-type: application/json; charset=utf-8
www-authenticate: Bearer realm="blinkimg", resource_metadata="https://api.blinkimg.com/.well-known/oauth-protected-resource/mcp", scope="mcp"
{"error":{"code":"UNAUTHORIZED","message":"The MCP endpoint takes an API key. Send Authorization: Bearer bk_... (or X-API-Key: bk_...). How to get one: https://blinkimg.com/auth.md","requestId":"5411e245-b480-4d8f-993a-bc7bb28b8f31"}}
The resource_metadata address is how an OAuth-capable client finds where to sign in:
curl -s https://api.blinkimg.com/.well-known/oauth-protected-resource/mcp
{"resource":"https://api.blinkimg.com/mcp","authorization_servers":["https://api.blinkimg.com"],"scopes_supported":["mcp"],"bearer_methods_supported":["header"],"resource_name":"Blinkimg","resource_documentation":"https://blinkimg.com/mcp"}
That pair, a 401 with WWW-Authenticate and a metadata document, is all a client needs to start the sign-in. When Claude Code shows "Needs authentication" in claude mcp list, this is the exchange it saw.
What does one tool call return?
I asked the agent to convert the site's 4964 × 2782 sample photo to AVIF, under 200 KB and no wider than 1600 px. The agent made one call:
{
"name": "convert_image",
"arguments": {
"image_url": "https://blinkimg.com/images/index/image-compressor-sample-camera-photo.jpeg",
"target_format": "avif",
"target_bytes": 200000,
"max_width": 1600
}
}
The result, with the signed URLs cut:
{
"status": "succeeded",
"format": "avif",
"input_format": "jpeg",
"input_bytes": 9804300,
"output_bytes": 196540,
"saved_percent": 98,
"width": 1600,
"height": 897,
"returned_original": false,
"download_command": "curl -sS --fail -o 'image-compressor-sample-camera-photo.avif' '...'",
"next": "Download the result from download_url before ..."
}
| Bytes | KB | Size | |
|---|---|---|---|
| Source JPEG | 9,804,300 | 9,804.3 | 4964 × 2782 |
| AVIF result | 196,540 | 196.5 | 1600 × 897 |
Three things in that result matter for an agent. output_bytes stays under target_bytes: the cap is a ceiling, and the engine lowers quality, then dimensions, until the file fits. download_command is a ready curl -o line, so an agent in a terminal saves the file without parsing anything. And next tells the agent the deadline in words: the link lasts an hour, the file a day.
The same cap on the site's reference photo, a 1920 × 1280 JPEG of 576.8 KB, lands at 99.7 KB when target_bytes is 100,000.
How do local files get in?
A hosted server cannot read your disk. So upload_image returns a one-time upload link and a curl command. An agent that can run commands (Claude Code, Cursor, Codex) runs the command, the file goes straight to storage, and compress_image takes the upload_id. A chat app without a shell cannot do this step; give it a public image address instead.
A prompt like "Convert every PNG in ./public to AVIF under 100 KB" turns into one upload_image and one convert_image per file.
Which mistakes stop the connection?
Most failures are config, not the server:
-
Gemini CLI: use
httpUrl, noturl. In Gemini CLI,urlmeans the older SSE transport. -
VS Code: in
.vscode/mcp.jsonthe top-level key isservers, notmcpServers, with"type": "http". -
Cline: add
"type": "streamableHttp", or it falls back to legacy SSE. -
Codex:
bearer_token_env_vartakes the variable's name,BLINKIMG_API_KEY, not the key itself. -
An app opened from the Mac Dock: it does not see variables you export in
~/.zshrc, so the key is empty and you get a 401.
One limit you may meet in a batch: one key runs 2 image_url downloads at once. A third call waits up to 20 seconds, then asks you to retry. A tool waits up to 25 seconds for the engine; a slower job comes back as a job_id for get_job.
If you would rather call the same engine from your own code, the developer API takes the same options over plain HTTP.
How I measured
- Machine: Apple Silicon Mac (M1 Pro), 2026-10-17.
- The 401 and the metadata document:
curlagainst the live server, output pasted with Cloudflare and date headers removed. - The tool call: Claude Code's MCP client calling
convert_imageon the live server with my account. 2 credits spent. - Source:
image-compressor-sample-camera-photo.jpeg, 4964 × 2782, 9,804,300 bytes (thecontent-lengththe site served). - The 99.7 KB figure comes from the site's measured engine output (
measured.ts,sizeTargets.to100kb). - Tool list, credits and limits: read from the server's tool definitions and
get_quotaon the same day.
What I did not test
I ran one photo through one client. I did not test the OAuth sign-in flow in Cursor, VS Code, Gemini CLI or Codex; their config shapes come from each client's docs.
I build Blinkimg, an image compressor and converter with a developer API. I wrote this article with help from an AI assistant (Claude), which ran the code on my machine; every number in it comes from those runs.
Top comments (0)