feat: MCP icon and stock-image tools (zero-config + key fallback)#48
Merged
trmquang93 merged 6 commits intomainfrom Apr 27, 2026
Merged
feat: MCP icon and stock-image tools (zero-config + key fallback)#48trmquang93 merged 6 commits intomainfrom
trmquang93 merged 6 commits intomainfrom
Conversation
Introduces `mcp-server/src/asset-fetchers/` with two reusable primitives that upcoming icon and stock-photo tools share: - `http.js` — `fetchWithTimeout` / `fetchText` / `fetchJson` / `fetchBinary` built on native fetch + AbortSignal.timeout. No new runtime deps. - `cache.js` — generic three-tier cache (in-flight Map → in-memory → disk) cloned from the emoji-loader pattern, parameterised so icons, search results, and image bytes can all reuse it. TTL support for search caches. Caches live under `~/.cache/drawd-mcp/` to match the existing convention.
Adds `asset-fetchers/iconify.js` wrapping the public Iconify HTTP API:
- `fetchIcon(collection, name, {size, color})` returns the SVG body.
- `searchIcons(query, {prefix, limit})` returns ranked candidate icon IDs.
Slug components are validated against `/^[a-z0-9][a-z0-9-]*$/` to prevent
path-traversal via crafted names. Iconify's stub-empty `<svg></svg>`
"not found" responses are normalised to a clear error.
SVGs are cached forever (immutable per id); search results have a 7-day TTL.
Both share the new three-tier `Cache` class.
…chain
Adds three photo providers behind a uniform `searchPhotos(query, {limit})`
interface plus an orchestrator that picks among them based on configured
API keys.
- `picsum.js` — keyless deterministic seeded URLs. Always available.
- `unsplash.js` — reads `UNSPLASH_ACCESS_KEY` per call. Throws a typed
`MissingApiKeyError` when unset so the orchestrator can fall through.
- `pexels.js` — same pattern, reads `PEXELS_API_KEY`.
- `index.js` — `findStockImage(query, {source, limit})` implements the
`unsplash → pexels → picsum` chain. Includes a `warning` field in the
result envelope when a keyed source was skipped silently.
API keys are read from env on every call — never logged, never written
to disk, never stashed on module state.
Adds three new MCP tools (net +3 → 32 tools total) backed by the new
asset-fetchers infrastructure:
- `generate_icon(collection, name, {size, color})` — Iconify SVG fetch.
- `search_icons(query, {collection, limit})` — Iconify search.
- `find_stock_image(query, {source, limit})` — orchestrated photo search
(Unsplash → Pexels → Picsum) with `warning` on key-fallback.
Per the implementation plan, `search_stock_images` was merged into
`find_stock_image` — one tool, returns N results.
Asset tools are stateless (no flow context required) so they bypass
the `withFilePath` injection used by all other tool groups.
Satori cannot fetch image URLs itself, so screens that reference stock photos via `<img src="https://...">` would otherwise render with broken images. This pre-pass downloads each unique image URL and rewrites `src` to a base64 data URI before Satori parses the HTML. Concurrency is capped at 4 in-flight downloads. The image-bytes cache is the same three-tier cache used by the other asset fetchers, so re-renders are fast. SECURITY: an explicit hostname allowlist is enforced — only the provider hosts the asset tools emit (api.iconify.design, images.unsplash.com, api.unsplash.com, api.pexels.com, images.pexels.com, picsum.photos, fastly.picsum.photos) are fetched. Any other host (or a failed/timed-out fetch) is replaced with a transparent 1×1 PNG so prompt-injected `<img src="https://attacker.example/...">` cannot turn the MCP into an SSRF gadget and a single bad URL never breaks the whole render.
- userGuide.md gains an "Icons and stock photos" section under MCP usage, describing the 3 new tools, the zero-config / key-upgrade paths, and the renderer's image-inlining + hostname allowlist behaviour. - Tool count bumped from 29 to 32. New "Assets" category added. - mcp-server/index.js gains a header comment listing the new UNSPLASH_ACCESS_KEY / PEXELS_API_KEY env vars alongside existing args with an explicit "never logged, never persisted" reminder.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three new MCP tools and a renderer pre-pass that lets agents enrich screens with real icons and photos instead of hand-drawn shapes or emoji substitutes:
generate_icon(collection, name, {size, color})— Iconify SVG fetch (275k+ icons acrossmdi,ph,lucide,tabler,heroicons,solar,carbon, …). Returns inline SVG the agent embeds verbatim in screen HTML.search_icons(query, {collection, limit})— Iconify search returning ranked candidate IDs.find_stock_image(query, {source, limit})— orchestrated photo search with a graceful fallback chain: Unsplash → Pexels → Picsum.Tool count: 29 → 32. Per the implementation plan,
find_stock_imageandsearch_stock_imageswere merged into a single tool.Renderer pre-pass
<img src="https://...">URLs increate_screenHTML are now downloaded and inlined as base64 data URIs before Satori parses the HTML. Concurrency capped at 4. Failed/timeout fetches fall back to a transparent 1×1 PNG so a single bad URL never breaks the whole render.Security: an explicit hostname allowlist is enforced — only provider hosts (
api.iconify.design,images.unsplash.com,api.unsplash.com,api.pexels.com,images.pexels.com,picsum.photos,fastly.picsum.photos) are fetched. Any other host is replaced with the transparent placeholder so prompt-injected<img src="https://attacker.example/...">cannot turn the MCP into an SSRF gadget.Env-var setup
Both keys are optional — the tools work zero-config on a fresh install (Iconify + Picsum paths only). Set either to upgrade quality without code changes:
UNSPLASH_ACCESS_KEY— enables query-relevant Unsplash photos.PEXELS_API_KEY— enables Pexels as the secondary photo source.API keys are read from env on every call — never logged, never persisted to disk, never stashed on module state.
Caching
All caches live under
~/.cache/drawd-mcp/:icons/— Iconify SVGs (no TTL — icons are immutable per id)icon-search/— Iconify search results (7-day TTL)images/— inlined photo bytes (30-day TTL)image-search/— stock photo search results (1-day TTL)Zero new runtime npm dependencies — native
fetchonly.Test plan
npm test) — 834/834 passingnpm run lint— cleannpm run build— production bundle buildsmcp-server/bundled with esbuild — boots cleanly vianpm startUNSPLASH_ACCESS_KEYset,find_stock_imagereturns query-relevant resultsPEXELS_API_KEYonly,find_stock_image source="unsplash"warns and falls back<img src="https://attacker.example/track.gif">— pre-pass rejects, render produces transparent placeholder