Inspired visual product search
Give Inspired one photograph. It names every separately purchasable object in the frame, cuts each one out on its own white background, and returns places to buy it.
That is three operations most image APIs make you assemble yourself: a reader that can tell a shirt from the vest worn over it, an editor that can rebuild the half of the shirt the vest was hiding, and a visual search that shops the rebuilt cutout rather than the original crowded photo.
What one call gives you
For a photograph of a person in an outfit, a living room, or a desk:
- Up to six objects, each with a plain name (
skirt,floor lamp,over-ear headphones) and a description of what is actually visible — colour, material, shape, pattern, the details you can see and the ones you cannot. - One product image per object, extracted individually by Pruna
p-image-editand centred on plain white, with whatever the occluding object hid rebuilt in the same material and finish. - Up to ten shopping matches per object, from Google Lens in product mode, ordered as Lens ordered them, with title, retailer link, price and image.
- A mood board: the cutouts composed into one square image.
The subject is a parameter
The same three moves work on anything photographed. domain selects what to
look for, and with it the reading prompt, the JSON schema the reader fills, and
the extraction instruction:
| domain | Looks for | Aliases |
| --- | --- | --- |
| fashion (default) | Garments, shoes, bags, jewellery worn by a person | clothing, clothes, outfit, apparel, style |
| furniture | Sofas, chairs, tables, lighting, rugs, decor | interior, home |
| devices | Phones, laptops, audio, cameras, appliances | tech, electronics |
| beauty | Skincare, makeup, fragrance, haircare, tools | — |
| everything | Any distinctly purchasable object | any, all |
focus narrows a domain with up to 160 characters of your own words —
domain=everything, focus=kitchen appliances only.
Two ways to call it
MCP, at https://inspired.jetskibay.com/mcp, is the executable endpoint an
agent runtime connects to. Three tools: list_focus_domains,
start_visual_search, get_visual_search. Authenticated with your API key as a
bearer token.
HTTP, at https://inspired.jetskibay.com/api/v1, is the same three
capabilities for anything that is not an agent, plus a synchronous variant and
key management. Both surfaces mint the same jobs from the same code, so neither
can do something the other cannot.
What it does not do
- It does not verify. No stage re-checks an extracted object against the original photo. Every object is returned as spotted and every match as visually similar, not as confirmed.
- It does not read brands. A brand is only named when a brand mark is clearly legible; otherwise the description stays descriptive.
- It does not guess. A photo with nothing clearly enough visible to shop for returns an empty list, and that is a successful read — it still costs one request.
- It is not fast. A full read is minutes, not seconds: one vision call, one extraction per object run strictly one at a time, and up to two searches in flight. Start a job and poll it.
- It does not accept image URLs. Send bytes, or a public Pinterest pin URL. There is no parameter that would make this an open image proxy.
Where the work happens
Cloudflare Workers, with a Cloudflare Workflow as the durable job. The reading
call goes to OpenAI, extraction to Pruna p-image-edit, search to Google Lens
through SerpAPI. Uploads, cutouts and results live in Cloudflare R2 for the life
of the job. Keys are stored only as a SHA-256 digest, and the email address a
key was minted against is stored only as a keyed digest.
Make it your own.
EXAMPLES & PROMPTS#!/usr/bin/env bash
# One whole read over MCP, with curl and nothing else.
#
# The Streamable HTTP transport is plain POSTed JSON-RPC, so a shell is enough
# to prove a connection works before you wire up a runtime.
#
# Needs: INSPIRED_KEY (your API key) and a photograph.
# export INSPIRED_KEY=isk_your_key_here
# ./mcp-session.sh living-room.jpg furniture
set -euo pipefail
MCP_URL="${MCP_URL:-https://inspired.jetskibay.com/mcp}"
PHOTO="${1:?usage: mcp-session.sh <photo> [domain]}"
DOMAIN="${2:-fashion}"
: "${INSPIRED_KEY:?set INSPIRED_KEY to your Inspired API key}"
call() {
curl -sS "$MCP_URL" \
-H "Authorization: Bearer $INSPIRED_KEY" \
-H 'content-type: application/json' \
-H 'accept: application/json' \
--data-binary @-
}
# 1. Handshake. The server answers with the one protocol revision it implements.
printf '\n== initialize ==\n'
call <<'JSON'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2026-07-28",
"capabilities":{},
"clientInfo":{"name":"mcp-session.sh","version":"1.0.0"}}}
JSON
# A notification has no id, so the server answers 202 with no body.
call >/dev/null <<'JSON'
{"jsonrpc":"2.0","method":"notifications/initialized"}
JSON
# 2. Discovery. Free, but still authenticated: there is no anonymous listing.
printf '\n== tools/list ==\n'
call <<'JSON'
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
JSON
printf '\n== list_focus_domains ==\n'
call <<'JSON'
{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"list_focus_domains","arguments":{}}}
JSON
# 3. Start the read. This is the call that spends one request.
# The image travels as base64 inside the JSON-RPC arguments.
printf '\n== start_visual_search ==\n'
case "$PHOTO" in
*.png) MEDIA_TYPE=image/png ;;
*.webp) MEDIA_TYPE=image/webp ;;
*.gif) MEDIA_TYPE=image/gif ;;
*) MEDIA_TYPE=image/jpeg ;;
esac
IMAGE_B64="$(base64 < "$PHOTO" | tr -d '\n')"
STARTED="$(
jq -nc --arg image "$IMAGE_B64" --arg type "$MEDIA_TYPE" --arg domain "$DOMAIN" \
'{jsonrpc:"2.0",id:4,method:"tools/call",params:{
name:"start_visual_search",
arguments:{image:$image,imageMediaType:$type,domain:$domain}}}' | call
)"
echo "$STARTED" | jq '.result.structuredContent'
REQUEST_ID="$(echo "$STARTED" | jq -r '.result.structuredContent.requestId')"
if [ "$REQUEST_ID" = "null" ]; then
echo "the read did not start; the tool error above says why" >&2
exit 1
fi
# 4. Collect it. Polling is free, so the only cost of a slow read is patience.
printf '\n== get_visual_search ==\n'
for _ in $(seq 1 60); do
STATE="$(
jq -nc --arg id "$REQUEST_ID" \
'{jsonrpc:"2.0",id:5,method:"tools/call",params:{
name:"get_visual_search",arguments:{requestId:$id}}}' | call
)"
STATUS="$(echo "$STATE" | jq -r '.result.structuredContent.status')"
echo " $STATUS"
if [ "$STATUS" = "complete" ]; then
echo "$STATE" | jq -r '.result.content[0].text'
exit 0
fi
if [ "$(echo "$STATE" | jq -r '.result.isError')" = "true" ]; then
echo "$STATE" | jq -r '.result.content[0].text' >&2
exit 1
fi
sleep "$(echo "$STATE" | jq -r '(.result.structuredContent.pollAfterMs // 3000) / 1000')"
done
echo "still running after 60 polls; the requestId stays valid: $REQUEST_ID" >&2
exit 1
Connect your workflow.
https://inspired.jetskibay.com/mcpStreamable HTTP MCP. Three tools: list_focus_domains, start_visual_search, get_visual_search.
https://inspired.jetskibay.com/api/v1The same pipeline over JSON: POST /jobs to start a read, GET /jobs/{id} to collect it.
Published by Inspired. Fictional brand, working prototype.