visual-image-search-mcp
A zero-config MCP server that searches the web for images, composites results into a numbered contact sheet for visual selection, and downloads chosen images with provenance metadata.
image_search: Find images by query. Returns a contact sheet (2x2, 3x3, or 4x4 grid for 1–16 results) and structured metadata per cell (id, title, dimensions, source page, license). Supports pagination, safe search levels, license filtering, minimum width, orientation (square, landscape, portrait), and forcing specific providers (duckduckgo, openverse, wikimedia, brave, google_cse, serper) or automatic provider chain.
image_inspect: View a single result at higher resolution (up to 1092px) for detail checking before download.
image_download: Download one or more images by their ids, with optional subdirectory, filename prefix, and per-file size limit (default 20MB). Each download includes a sidecar JSON with source page, license, attribution, query, and provider. Built-in safety: SSRF prevention, path traversal blocking, magic-byte format validation, and stream-based size enforcement. Falls back to thumbnail if original is hotlink-protected, marking the result as degraded.
Searches for images using the Brave Search API, an independent index with a generous free tier.
Searches for images using DuckDuckGo's image search, the default keyless provider.
Searches for images using Google Custom Search Engine, requiring GOOGLE_CSE_KEY and GOOGLE_CSE_CX.
Searches for images using Openverse, which provides per-image licence data and is used when a license filter is applied.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@visual-image-search-mcpfind images of vintage travel posters and choose the best"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
visual-image-search-mcp
An MCP server that searches the web for images, composites the results into one numbered contact sheet the model can actually look at, and downloads the ones it picks.
Zero-config: it works with no API key.

Why a contact sheet
A normal image-search tool returns thirty URLs and some alt-text, and the model chooses blind — it is picking by filename. This one fetches the thumbnails, tiles them into a single image with numbered cells, and returns that. The model scans it the way you scan a Google Images page and says "download 2 and 7".
One image instead of thirty. Cheaper in context, and the choice is made on what the pictures look like.
Full image URLs never enter the conversation. They stay server-side, keyed by ids like s3:4.
What "choosing blind" actually looks like
Here is a real search — manifest best girl band ever, count: 16 — as a text-based tool would hand it over. Nine of the sixteen results, verbatim:
id | size | source | title |
| 2560x2560 | manifestgirlband.com | Manifest — Altı Kız. Bir Enerji. |
| 466x466 | manifestgirlband.com | Manifest — Six Girls. One Motion. |
| 1245x698 | manifestgirlband.com | Manifest — Six Girls. One Motion. |
| 1245x859 | manifestgirlband.com | Manifest — Altı Kız. Bir Enerji. |
| 370x370 | manifestgirlband.com | Manifest — Six Girls. One Motion. |
| 1245x698 | manifestgirlband.com | Manifest — Altı Kız. Bir Enerji. |
| 1245x698 | manifestgirlband.com | Manifest — Six Girls. One Motion. |
| 1245x698 | manifestgirlband.com | Manifest — Altı Kız. Bir Enerji. |
| 405x506 | manifestgirlband.com | Manifest — Six Girls. One Motion. |
Same site, two titles alternating, and the same 1245x698 four separate times. There is no signal here. A model asked to "pick a good group photo" is choosing at random, and it cannot tell you otherwise — it will pick one and sound confident.
Now the same sixteen results as a sheet:

The differences the text could not carry are the only ones that matter:
7, 10 and 15 are video thumbnails with titles burned into the pixels —
BİR GÜN VLOG,OFFICIAL DANCE VIDEO,SNAP VLOG. All three are1245x698, the YouTube thumbnail aspect. Indistinguishable in the table; obvious in the sheet.8 is a backlit silhouette. You cannot see a single face. Its metadata is the best-looking row of the nine.
3 and 9 are small artwork tiles with heavy typography over them, not photographs.
1 and 16 are actual press photos — which is what was asked for.
That is the whole argument. Not that the sheet is prettier, but that nine of these rows are the same row until you look at them.
Grid sizes
count picks the layout; the canvas stays within 1092x1092 either way.
count | grid | cell |
1–4 | 2x2 | 546px |
5–9 | 3x3 | 364px |
10–16 | 4x4 | 273px |
Rows that would be empty are dropped, so a 5-result search is 3x2 and not 3x3 with a blank band. Smaller counts mean bigger cells and more detail per image; 16 is for scanning breadth. When a cell is too small to judge, image_inspect returns that one image on its own at full size — which is how you catch things like site chrome baked into an "original", where the sheet only hints at it.
Related MCP server: Wikimedia-Search-Images
Install
{
"mcpServers": {
"visual-image-search": {
"command": "npx",
"args": ["-y", "visual-image-search-mcp"]
}
}
}Or from the CLI:
claude mcp add visual-image-search -- npx -y visual-image-search-mcpTo run the current main instead of the published version — or before the
first npm release — install straight from the repository:
claude mcp add visual-image-search -- npx -y github:enescanguven/visual-image-search-mcpThat path compiles on install, so it needs a working toolchain; the npm package ships prebuilt.
Requires Node 20 or newer.
Tools
image_search
Searches, builds the sheet, returns it.
param | type | default | notes |
| string | — | required |
| 1–16 | 9 | ≤4 gives a 2x2 sheet, ≤9 a 3x3, ≤16 a 4x4 |
| int | 1 | more results for the same query |
|
|
| |
|
|
| anything but |
| int | — | drops narrower results |
|
|
| pushed into the provider query where supported |
| string | auto | force one provider |
Returns the sheet as an image, plus structuredContent with one entry per cell: id, title, dimensions, source page, licence. Cells whose thumbnail failed to load are drawn as a placeholder marked failed — the numbering never shifts to close the gap, so an id always points at the picture you saw in that cell.
image_inspect
{ id: "s3:4" } — returns that one image on its own at up to 1092px, for checking detail before committing to a download.
image_download
{ ids: ["s3:2", "s3:7"], dir?, prefix?, max_bytes? }
Writes each image under the output root, alongside a <filename>.json sidecar holding the source page, licence, attribution, original query and provider — so downstream use has provenance without the model having to remember it.
If a host refuses the original (hotlink protection is common), the thumbnail is saved instead and the result is flagged degraded: true.
Providers
The chain is tried in order and moves on when one errors or comes back empty. The response always names the provider that actually served the results.
provider | key | notes |
| none | default. Best coverage. |
| none | proper per-image licence data. Used for every |
| none | small index, unambiguous licensing, never blocks you. |
|
| independent index, generous free tier |
|
| 100 free queries/day |
|
| real Google Images results, paid |
Default chain: duckduckgo → wikimedia. With a license filter: openverse → wikimedia. Any configured keyed provider goes to the front.
The honest part
The keyless providers scrape public endpoints that were never meant for us. DuckDuckGo image search in particular is a two-step browser handshake — fetch a page, pull a vqd token out of the HTML, pass it to an internal JSON endpoint. There is no contract here:
It will break. When DuckDuckGo changes that HTML, the token extraction fails. Errors name which of the two steps broke so you are not guessing.
It is rate-limited by IP. Search a lot and you get throttled. The server sends at most one search per second by default.
Openverse's anonymous tier is small, and caps
page_sizeat 20.
If you need this to be reliable, set BRAVE_API_KEY or GOOGLE_CSE_KEY + GOOGLE_CSE_CX. Those are real APIs with terms of service and they do not break on a markup change.
The keyed provider parsers were written against published response shapes rather than captured traffic — there was no key on hand to record fixtures with. They are conservative about missing fields, but if you are the first to run one with a real key, check the field mapping.
Configuration
All optional.
variable | default | purpose |
|
| root that every download must land inside |
|
| minimum gap between outbound searches |
| unset | set to |
| — | enables |
| — | enables |
| — | enables |
Download safety
Downloads write remote bytes to your disk, so nothing the remote side says is trusted:
Paths.
dirresolves inside the output root. Absolute paths and../are refused, not sanitised.SSRF. Hostnames are resolved and checked against private, loopback and link-local ranges — including cloud metadata at
169.254.169.254and IPv4-mapped IPv6 — before a byte is sent, and again on every redirect hop. Redirects cap at 5.Size.
max_bytes(default 20MB) is enforced against the stream, not justContent-Length, so a lying header aborts mid-download.Type. The format is decided by magic bytes. An HTML error page served as
image/jpegis rejected rather than saved as a.jpg. SVG is deliberately unsupported — it is a script-bearing document and rendering an untrusted one to read its dimensions is not worth the risk.Filenames are slugs derived from the title, falling back to the id. A remote value never decides the path.
robots.txtis not consulted by default. SetIMAGE_MCP_RESPECT_ROBOTS=1to turn the check on; it then fails open when the file is absent or unreachable. The reasoning is in the next section.
One caveat worth stating plainly: the SSRF check validates the hostname as it resolves at check time. A host that re-binds to a private address between the check and the connection could still get through. Closing that needs connection-level IP pinning, which is not exposed here.
Development
npm install
npm test # offline: parser fixtures, compositor, safety
npm run test:live # hits the network; exercises all three keyless providers end to end
npm run preview "red panda" 9 sheet.jpg # render a sheet to look at
npm run inspector # manual pass with @modelcontextprotocol/inspectorProvider parsers are tested offline against captured responses in test/fixtures/. When a provider breaks, re-capture its fixture and the diff shows you what changed.
Notes on the implementation
A few decisions that differ from what you might expect:
Index badges are drawn as vector rectangles, not text. sharp renders SVG text through fontconfig, which resolves to nothing on a host with no fonts installed. Badges that fail to draw make the whole sheet useless, so they do not depend on a font being present. Captions do use real text, and are simply omitted when no font is available.
The sheet is JPEG, not PNG. Measured on a real 9-cell sheet: 1994KB as PNG against 229KB as JPEG q85, with badge edges indistinguishable at 2x zoom.
Thumbnails are never cropped.
fit: containon a dark ground — cropping changes what the picture is of, and the model would be judging a composition we invented.robots.txtis off by default. Earlier versions enforced it on downloads. Two things were wrong with that. The Robots Exclusion Protocol governs automated crawlers, and this is not one: someone looked at a contact sheet, picked cell 7, and asked for that file — the same act as "save image as", which consults no robots.txt either. And the enforcement was incoherent, running on one of three outbound paths: not on thumbnail fetches, and not on the search itself, where DuckDuckGo'sDisallow: /*?covers the exact/i.jsendpoint this server is built on. It is still available behindIMAGE_MCP_RESPECT_ROBOTS=1, and the parser it uses was fixed at the same time — it had been treating blank and comment lines as group separators, which discarded every rule after the first comment. On DuckDuckGo's own file that was 5 of 7 rules, so hosts were being checked against a fragment of what they published.
License
MIT
Available Tools
3 toolsimage_downloadDownload chosen imagesA
Download images picked from a contact sheet. Each file is written with a sidecar .json holding the source page, licence, attribution, query and provider.
| Name | Required | Description | Default |
|---|---|---|---|
| dir | No | Subdirectory under the output root (/app/downloaded-images). | |
| ids | Yes | Ids from image_search, e.g. ["s3:2", "s3:7"]. | |
| prefix | No | Prepended to each filename. | |
| max_bytes | No | Per-file size ceiling. Default 20MB. |
Output Schema
| Name | Required | Description |
|---|---|---|
| root | Yes | |
| downloads | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description adds behavioral detail beyond the annotations by mentioning each file is written with a sidecar .json containing metadata (source, license, attribution, query, provider). This complements the readOnlyHint=false annotation and provides useful context about file output. No contradiction found.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two concise sentences, front-loaded with the primary action. Every word earns its place, and the sidecar detail is efficiently appended.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's moderate complexity, the presence of an output schema, and adequate annotations, the description covers the essential purpose and side effects. It could mention overwrite or error behavior, but it is sufficient for an agent to select and invoke the tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema has 100% parameter description coverage, so the schema already explains each parameter. The tool description does not add extra parameter-specific meaning, but the baseline of 3 is appropriate given the schema is sufficiently descriptive.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool downloads images selected from a contact sheet, using a specific verb ('Download') and resource ('images'). It distinguishes itself from siblings image_search and image_inspect by indicating this step comes after selection from a contact sheet.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies the tool is used after picking images from a contact sheet, which suggests a workflow following image_search. It provides clear context but does not explicitly exclude alternatives or state when not to use this tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
image_inspectInspect one image up closeARead-only
Return a single image from a previous search at higher resolution, for checking detail before committing to a download.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | An id from image_search, e.g. "s3:4". |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, so the safe, read-only nature is known. The description adds valuable behavioral context by specifying 'higher resolution' and the purpose of 'checking detail before committing to a download.' No contradictions with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, tightly worded sentence that delivers the core purpose and usage context without any redundancy or unnecessary detail.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple tool with one parameter, clear annotations, and no output schema, the description is sufficiently complete. It explains the tool's role in the workflow and what it returns, covering the essential context for an AI agent to select and invoke it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema fully describes the single parameter 'id' with a clear explanation and example. Since schema description coverage is 100%, the description need not add parameter details; baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's function: returning a single image from a previous search at higher resolution. This distinguishes it from siblings image_search (multiple images) and image_download (downloading), making the purpose unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides clear context on when to use the tool: after a search ('from a previous search') and before downloading ('before committing to a download'). It implies the workflow but does not explicitly name alternatives or exclude other uses.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
image_searchSearch images (contact sheet)ARead-only
Search the web for images and get back a single numbered contact sheet showing the results. Look at the sheet, decide which cells you want, then pass their ids to image_inspect or image_download. Image URLs are held server-side and referenced by id.
| Name | Required | Description | Default |
|---|---|---|---|
| page | No | Result page, for seeing more of the same query. | |
| safe | No | moderate | |
| count | No | How many cells on the sheet (1-16). | |
| query | Yes | What to search for. | |
| license | No | Anything but "any" routes to a provider that reports per-image licences. | any |
| provider | No | Force one provider instead of the automatic chain. One of: duckduckgo, openverse, wikimedia, brave, google_cse, serper. | |
| min_width | No | Drop results narrower than this. | |
| orientation | No | any |
Output Schema
| Name | Required | Description |
|---|---|---|
| page | Yes | |
| results | Yes | |
| has_more | Yes | |
| provider | Yes | |
| warnings | Yes | |
| search_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint and openWorldHint. The description adds valuable behavioral context: results arrive as a single numbered contact sheet and URLs are held server-side referenced by id, which is not obvious from annotations or schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, front-loaded with the core purpose, followed by workflow and an important id-based detail. Every sentence earns its place with no redundant fluff.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return-value details are covered elsewhere. The description explains the contact sheet model, workflow with siblings, and server-side id handling, making it complete enough for a search tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 75%, so most parameters already have descriptions. The tool description itself adds no param-level detail, and the two schema-documented parameters (safe, orientation) are not compensated for in the description.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: 'Search the web for images and get back a single numbered contact sheet'. It clearly distinguishes from sibling tools by instructing users to pass selected cell ids to image_inspect or image_download.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It provides a clear workflow: search, inspect the contact sheet, then pass ids to sibling tools. It does not explicitly state when not to use this tool, but the context and sibling relationship give sufficient usage guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.1.0- First observed
image_download - First observed
image_inspect - First observed
image_search
TDQS
Scored across 3 tools
Each tool has a distinct role in the workflow: image_search retrieves a contact sheet, image_inspect provides a higher-resolution view of a single result, and image_download saves the chosen image. There is no overlap or ambiguity between them.
All tool names follow the exact same verb_noun pattern: image_search, image_inspect, image_download. This makes the set predictable and easy to navigate.
Three tools is well within the ideal range for a focused server. Each tool serves a necessary step in the image search and acquisition pipeline, and no tool feels redundant or missing.
The tool set covers the complete workflow from search to inspection to download. It includes the essential operations for the domain, with no obvious gaps that would prevent an agent from accomplishing the intended task.
Maintenance
Related MCP Connectors
MCP server for visual regression testing: triage a PR's UI diffs from your coding agent.
Docs: https://docs.keenable.ai/mcp-server Keenable is a free, remote MCP server that gives agents access to the web index. Search the web with ranked results and date/site filters, then fetch any indexed page as clean markdown. Works out of the box with no account or API key.
Read-only MCP server over the JENRIKS daily photoblog: one photo per day since 2006. No auth.
Related MCP Servers
- AlicenseBqualityFmaintenanceMCP server for searching images with Google221 npm13MIT
- AlicenseBqualityDmaintenanceThis MCP server enables AI assistants to search for images on Wikimedia Commons, providing detailed metadata and optional thumbnail combinations to assist AI models in visual comparisons.12Apache 2.0
- AlicenseAqualityDmaintenanceMCP server that enables web search, image search, and image download using DuckDuckGo.3MIT
- FlicenseAqualityBmaintenanceMCP server for scraping images from DuckDuckGo and Google Maps place photos without API key.2-