Commit 6119ea8
authored
feat: MCP icon and stock-image tools (zero-config + key fallback) (#48)
* feat: shared HTTP + three-tier cache for MCP asset fetchers
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.
* feat: Iconify provider for the MCP server
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.
* feat: stock-photo providers (Unsplash, Pexels, Picsum) with fallback 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.
* feat: MCP asset tools — generate_icon, search_icons, find_stock_image
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.
* feat: renderer pre-pass to inline remote <img src> URLs
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.
* feat: document MCP icon and stock-image tools
- 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.
---------
Co-authored-by: Quang Tran <16215255+trmquang93@users.noreply.github.com>1 parent ee5fcc8 commit 6119ea8
19 files changed
Lines changed: 1544 additions & 2 deletions
File tree
- mcp-server
- src
- asset-fetchers
- __tests__
- renderer
- __tests__
- tools
- __tests__
- src/pages/docs
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
1 | 20 | | |
2 | 21 | | |
3 | 22 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
Lines changed: 94 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
0 commit comments