visual-image-search-mcp
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 "Install 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 |
| — | 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 checked before each download, failing open when it is absent or unreachable.
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.
License
MIT
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseBqualityFmaintenanceMCP server for searching images with GoogleLast updated21412MIT
- 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.Last updated12Apache 2.0
- AlicenseAqualityDmaintenanceMCP server that enables web search, image search, and image download using DuckDuckGo.Last updated3MIT
- FlicenseAqualityBmaintenanceMCP server for scraping images from DuckDuckGo and Google Maps place photos without API key.Last updated2
Related MCP Connectors
MCP server for Google search results via SERP API
MCP server for Flux AI image generation
Serper MCP — wraps the Serper Google Search API (serper.dev)
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/enescanguven/visual-image-search-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server