MCP server
Reference for the SFW API MCP server. The hosted endpoint and the local package, the analyze_url, analyze_file and get_account tools, their inputs, results, errors and cost.
Updated on · View as Markdown
The MCP server gives an AI agent the API as tools: it passes a URL or a file and gets the content level back. This page is the reference. For copy-and-paste setup per client (Claude Code, Cursor, VS Code, Codex, Claude Desktop) see the MCP server page.
Use it when the agent itself should check media, for example while triaging reports or reviewing a folder of assets. To have an agent write the API into your own application, use the skill and the Markdown docs instead.
Two forms
| Hosted endpoint | Local package | |
|---|---|---|
| Address | https://api.sfwapi.dev/mcp | npx -y @sfwapi/mcp |
| Transport | Streamable HTTP, stateless | stdio |
| Key | Authorization: Bearer <key> header | SFWAPI_API_KEY environment variable |
| Tools | analyze_url, get_account | analyze_url, analyze_file, get_account |
| Needs | A client that can send a header | Node.js 20 or newer |
Both use the same key and the same credits as the HTTP API. analyze_file exists only locally, because a hosted server cannot read your disk.
Hosted
claude mcp add --transport http sfwapi https://api.sfwapi.dev/mcp --header "Authorization: Bearer $SFWAPI_API_KEY"Any other client: point it at the URL and send the header. Each request is independent; there is no session to keep alive. The endpoint has no OAuth sign-in, so clients that can only connect through OAuth cannot use it.
Local
claude mcp add sfwapi -e SFWAPI_API_KEY=$SFWAPI_API_KEY -- npx -y @sfwapi/mcpClients with a JSON config use the same command:
{
"mcpServers": {
"sfwapi": {
"command": "npx",
"args": ["-y", "@sfwapi/mcp"],
"env": { "SFWAPI_API_KEY": "sk_live_..." }
}
}
}Tools
analyze_url
Analyse an image, GIF or video at a public http(s) URL. Our servers download it; nothing is stored.
| Input | Type | |
|---|---|---|
url | string, required | Public URL of the media, up to 2048 characters |
block_level | 1, 2 or 3 | Apply a policy: block at this level or above. The result then has policy.blocked |
block_sex_objects | boolean | With block_level: also block adult toys and objects |
art | string | With block_level: how drawn content is treated. same_as_photos (default), allow, block_from_1, block_from_2, block_from_3 |
max_frames | 1 to 32 | Frames sampled from a GIF or video. Default 5 for a GIF, 10 for a video |
include_frames | boolean | Return the scores of each frame |
analyze_file
Local package only. Analyse a file on the machine the server runs on. The file is uploaded to the API without its name, analysed in memory and discarded.
| Input | Type | |
|---|---|---|
path | string, required | Path of the file, absolute or relative to the working directory |
block_level, block_sex_objects, art, max_frames, include_frames | As for analyze_url |
get_account
No input. Returns the plan, its limits, the credit balance and this month's usage. Free.
Results
Every tool answers with one readable line and the full result as structured content.
Level 2 (Implied Nudity). Scores 0-100: revealing 96.2, implied nudity 71.5, explicit 8.1. Blocked by the policy (level). 1 credit used, 499 left.The structured content of analyze_url and analyze_file is the response of POST /v1/analyze, field for field:
{
"id": "an_01JD4Q0K7X3Y9Z8W2V1U0T9S8R",
"media": { "kind": "image", "width": 1200, "height": 800, "frames_analyzed": 1, "duration_ms": null },
"nudity": { "level": 2, "scores": { "revealing": 96.2, "implied_nudity": 71.5, "explicit": 8.1 } },
"flags": { "art": { "score": 3.4, "detected": false }, "sex_object": { "score": 0.9, "detected": false } },
"policy": { "blocked": true, "reason": "level", "matched_level": 2 },
"usage": { "credits": 1, "balance": 499 },
"timing_ms": { "upstream": 41, "total": 58 }
}Levels are 0 Everyday, 1 Revealing, 2 Implied nudity and 3 Explicit. Scores run from 0 to 100 and count above 50. policy is present only when block_level was passed; see Policies.
get_account returns tier, limits, balance (free, paid, total, free_resets_at) and month (from, requests, credits, bytes_in).
Errors
A failed call comes back as a tool error (isError: true) whose text is the API's error code, its message, a link to the docs and the request id:
download_failed: The URL returned 404
Docs: https://sfwapi.dev/docs/errors#download_failed
Request id: 4f1c2a9e7b5d4c3fa1e0b9d8c7f6e5a4A missing or wrong key is different on the hosted endpoint: the HTTP request itself is answered with 401 and the usual error body, so the client reports that it could not connect. The local package refuses to start without SFWAPI_API_KEY.
Cost and limits
- One credit per image, or one per analysed frame of a GIF or video, plus the size surcharge above 10 MB. The same as the HTTP API.
- Connecting, listing the tools,
get_accountand every failed call are free. - The rate limits of your plan apply to the analyses. An agent that fans out too widely gets
rate_limitedwith the time to wait. - Video needs a paid account.
The key
Keep the key in SFWAPI_API_KEY or in your client's secret prompt, not in a file you commit. Anyone with the key can spend your credits; rotate or revoke it in the console. The tool result never contains the key or the media.
Environment variables of the local package
| Variable | |
|---|---|
SFWAPI_API_KEY | Required. Your API key |
SFWAPI_BASE_URL | Optional. API origin, default https://api.sfwapi.dev |
Use it from Claude Code, Codex or Cursor
Install the SFW API skill in your coding agent or paste one prompt, and let the agent write the integration against our Markdown docs.
Authentication
Send your API key as a Bearer token. How keys are created in the console, stored as hashes, rotated with a 24-hour grace period and revoked at once.