ifPNG exposes its conversion pipeline to Claude and other AI agents through a
Model Context Protocol server at
POST /api/mcp. It is available to active Pro subscribers only.
| Endpoint | https://ifpng.com/api/mcp |
| Transport | Streamable HTTP, stateless (POST only, no SSE, no session ids) |
| Protocol revision | 2025-06-18 (also answers 2025-03-26, 2024-11-05) |
| Auth | OAuth 2.1 (PKCE + dynamic client registration), or Authorization: Bearer ifpng_mcp_… API key |
| Tools | convert_image, inspect_image, list_formats, get_usage |
| Quota | 5,000 conversions per calendar month, 30 requests/min per credential |
| Public page | /mcp |
Paste https://ifpng.com/api/mcp into your client as a custom connector and
sign in. Claude's apps, Claude Code, Cursor and VS Code all discover the
authorization server from the endpoint itself and run the OAuth flow for you:
you land on an ifPNG consent screen, approve, and you are connected.
# Claude Code
claude mcp add --transport http ifpng https://ifpng.com/api/mcp
// Cursor, VS Code, Claude Agent SDK
{
"mcpServers": {
"ifpng": { "type": "http", "url": "https://ifpng.com/api/mcp" }
}
}
Manage connected clients — and disconnect any of them — in Dashboard → MCP.
Where no browser can complete a sign-in, mint a key in Dashboard → MCP (shown once, five active per account) and send it as a bearer token:
claude mcp add --transport http ifpng https://ifpng.com/api/mcp \
--header "Authorization: Bearer ifpng_mcp_YOURKEY"
Use one key per machine, so revoking one does not disconnect everything.
Hosts that only launch local stdio servers — use the bridge in
mcp-bridge/, a dependency-free script that
forwards stdio JSON-RPC to the same endpoint.
cli/ifpng.mjs is a zero-dependency CLI over the same endpoint — no second
conversion path, so it cannot drift from what agents get:
node cli/ifpng.mjs login ifpng_mcp_YOURKEY
node cli/ifpng.mjs convert shot.png --to webp --width 1200
node cli/ifpng.mjs info photo.jpg
node cli/ifpng.mjs usage
curl -s https://ifpng.com/api/mcp \
-H "Authorization: Bearer ifpng_mcp_YOURKEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq
convert_imageConverts to PNG, JPEG, WebP or AVIF, with optional resizing. Counts one conversion against the monthly quota.
| Argument | Type | Notes |
|---|---|---|
image_base64 | string | The bytes, base64. A data: prefix is stripped. Max 25 MB decoded. |
image_url | string | Public https URL. Exactly one of this or image_base64. |
to | enum | Required. png | jpeg | webp | avif |
quality | 1–100 | Defaults: jpeg/webp 82, avif 50. For png, enables palette quantisation. |
width, height | int | Target box. |
fit | enum | cover | contain | inside (default) | outside | fill |
allow_enlargement | bool | Default false — images are never upscaled unless asked. |
background | hex | Fill when flattening alpha into JPEG, or padding with contain. Default #ffffff. |
keep_metadata | bool | Default false. See Privacy. |
filename | string | Preferred output name; the extension comes from to. |
output | enum | auto (default) | inline | url |
Delivery. Results at or under ~1.5 MB come back as an MCP resource blob the
host can write to disk. Larger ones are uploaded to R2 and returned as a signed
URL valid for one hour. auto picks between them; inline on an oversized
result returns an error rather than silently switching, so a client that needs
bytes never receives a link by surprise.
A resource blob is used rather than an image content block on purpose: an
image block pushes the whole picture into the model's context window, which is
expensive and almost never what you want from a file conversion.
inspect_imageFormat, dimensions, byte size, alpha, frame count, colour space, density, orientation, and whether metadata is present — without converting. Free.
list_formatsSupported input/output formats and every enforced limit. Free, no arguments.
get_usageConversions used this month, remaining, and the reset time. Free.
ifPNG is both the resource server and its own authorization server. Running our own rather than delegating to an identity vendor keeps the consent screen honest — the user is already signed in with NextAuth — and puts no third party between a subscriber and the tools they pay for.
/api/mcp with no token → 401 carrying
WWW-Authenticate: Bearer resource_metadata="https://ifpng.com/.well-known/oauth-protected-resource"./.well-known/oauth-authorization-server (RFC 8414) for the endpoints./api/mcp/oauth/register (RFC 7591) — no developer
portal, no manual client id./api/mcp/oauth/authorize with PKCE S256 and a resource
parameter. We validate, park the request server-side, and redirect to the
consent screen at /mcp/authorize./api/mcp/oauth/consent mints a
single-use code and returns them to the client./api/mcp/oauth/token for a 1-hour access
token and a 30-day refresh token.Both .well-known documents are also served with the resource path appended
(/.well-known/oauth-protected-resource/api/mcp), because RFC 9728 builds the
URL that way and clients differ on which they try first.
| Scope | Grants |
|---|---|
image.convert | convert_image, inspect_image |
account.usage | get_usage |
list_formats needs none. Tools outside a connection's scopes are omitted
from tools/list rather than listed and refused — a model that cannot see a
tool will not plan around it. API keys carry every scope, since they are minted
by the account owner for their own use.
S256 only. plain is not advertised and is rejected at /authorize.The threat model starts from the assumption that arguments are chosen by a language model that may be acting on text from an untrusted source.
Credentials. Both credential types are 256 bits of CSPRNG output. Only a SHA-256 hash is
stored, so a database leak cannot be replayed; the hash doubles as the lookup
index. Plain SHA-256 rather than bcrypt is correct here — there is no
low-entropy guess space for a slow KDF to defend, and an unindexable column
would force a full scan plus N bcrypt compares on every call. Keys are accepted
only in the Authorization header, never a query parameter, which keeps them
out of access logs and browser history.
Entitlement. Re-derived from users.subscription_status on every request
rather than baked into the key. A cancelled subscription, a failed renewal, an
admin suspension or a self-service deletion all cut off agent access on the next
call, with no cache to expire and no revocation sweep to run.
SSRF. image_url is the sharpest edge in the system, so lib/mcp/safe-fetch.ts
applies: an https-only scheme check (http is allowed off-production), a port
allowlist of 80/443, a private/reserved IP blocklist covering RFC1918, loopback,
link-local (including 169.254.169.254), CGNAT, ULA and IPv4-mapped/NAT64
IPv6 forms, connection pinning to an already-validated address via a custom DNS
lookup so rebinding cannot win the race, manual redirect following with every
hop re-validated, a streamed byte cap, Accept-Encoding: identity, and a
10-second deadline. Fetched bodies are only ever decoded as images, never
echoed back, so failures cannot be used to read internal content.
Decoding. The container format comes from sharp's probe of the bytes, never
from a filename or Content-Type, and must be on the allowlist. SVG and PDF are
excluded because their renderers resolve external references — that would turn
a conversion into a file-read primitive. limitInputPixels caps the decoded
surface at 50 MP so a compressed decompression bomb fails at decode rather than
exhausting memory.
Abuse. 30 requests/minute per key (bucketed on a hash of the key, so no credential material reaches Redis) and 5,000 conversions/month per account. Rate limiting keys on the credential rather than the IP because agent traffic arrives from shared egress, where per-IP limits would have one subscriber throttling another.
Browser isolation. The endpoint emits no CORS headers at all, so a malicious page cannot ride along on a user's stored credentials.
EXIF, GPS, IPTC and XMP are stripped by default. Orientation is baked into the
pixels first, so stripping never silently rotates a portrait photo. Setting
keep_metadata: true preserves everything, including location — the tool
description says so explicitly so a model does not enable it casually.
Large outputs land in R2 under mcp/<userId>/ and are removed after 24 hours by
scripts/prune-mcp-outputs.js. Inline results are never persisted at all.
| Path | Role |
|---|---|
src/pages/api/mcp/index.ts | HTTP shell: method handling, rate limit, auth, dispatch |
src/lib/mcp/protocol.ts | JSON-RPC 2.0 / MCP dispatch |
src/lib/mcp/tools.ts | Tool definitions, schemas and handlers |
src/lib/mcp/image.ts | sharp pipeline, R2 storage of large outputs |
src/lib/mcp/safe-fetch.ts | SSRF-hardened fetcher |
src/lib/mcp/auth.ts | Bearer auth + subscriber gate |
src/lib/mcp/keys.ts | Key mint/hash/list/revoke |
src/lib/mcp/usage.ts | Monthly quota accounting |
src/lib/mcp/config.ts | Every tunable limit |
src/lib/mcp/oauth/ | OAuth 2.1 server: metadata, DCR, codes, tokens |
src/pages/api/mcp/oauth/ | authorize, consent, token, register, revoke, metadata |
src/pages/mcp/index.tsx | Public landing page (/mcp) |
src/pages/mcp/authorize.tsx | Consent screen |
src/pages/api/user/mcp-keys/ | Dashboard key management |
src/pages/api/user/mcp-connections/ | Disconnect a connected AI client |
cli/ifpng.mjs | Terminal client over the same endpoint |
src/components/dashboard/McpKeys.tsx | Dashboard MCP tab |
mcp-bridge/ | stdio → HTTP bridge for stdio-only hosts |
migrations/0019_mcp-api-keys.cjs | mcp_api_keys, mcp_usage_daily |
migrations/0020_mcp-oauth.cjs | mcp_oauth_clients, _requests, _codes, _tokens |
The protocol layer is hand-written rather than built on
@modelcontextprotocol/sdk. The server offers four tools over stateless
request/response with no sampling, roots or server-initiated messages; that
subset is a few hundred readable lines, which beats adding a dependency tree to
a payment-handling app. Nothing else in the repo depends on that choice — the
tools and the transport are separate modules.
All in src/lib/mcp/config.ts. The MCP ceilings are deliberately stricter
than the browser upload limits in lib/plans.ts: the web converter streams
100 MB through chunked uploads to R2, while an MCP call carries the bytes inline
in a JSON body the route must hold in memory. Do not "align" the two.
pnpm db:migrate — creates the mcp_api_keys, mcp_usage_daily and
mcp_oauth_* tables, and grants the prune role DELETE on the OAuth ones.NEXT_PUBLIC_SITE_URL matters more than
before: it is the OAuth issuer and the audience clients bind tokens to.
It must be the real public origin, with no trailing slash — if it is wrong,
discovery advertises endpoints nobody can reach. R2 credentials and
REDIS_URL are used as before.node scripts/prune-mcp-outputs.js daily, alongside the other
retention jobs in scripts/systemd/. Optional
MCP_OUTPUT_RETENTION_HOURS (default 24).pnpm test src/lib/mcp — protocol conformance, the SSRF blocklist, key
handling, the auth/subscriber gate, the sharp pipeline (including a
decompression-bomb case), and an end-to-end pass through the real route with a
genuine PNG → WebP conversion.
npx.