Template mode inherits the user's icons. Where the template has none — and especially where an
icon has to sit on a coloured card, which a stock PNG with its baked-in white background cannot —
the agent previously had nothing to reach for. This release lets it draw one.
Three tools, registered unconditionally (tools/icon_tools.py):
get_icon_guidanceservesdocs/ICON_GUIDANCE.md— the style contract: one stroke weight on a
200×200 viewBox,fill="none", rounded caps and joins, 6–8 segments, the two variants (line art on
light, white-on-filled-disc on colour), and eleven worked path examples. Read from disk and
mtime-cached, like the design guidance, so it costs nothing until a caller is building.render_svg_icontakes the agent's SVG, rasterizes it and judges it.add_icon_to_slideplaces the result.
The SVG is untrusted input
It is parsed in-process, so validate_svg refuses DOCTYPE/ENTITY (XXE), <script>,
<foreignObject>, <image>, event handlers, and any external href/url() (SSRF) before anything
touches a parser. SVG_ICON_MAX_KB (default 64) bounds the source — an icon is a handful of paths;
the limit is what stops traced artwork arriving as one.
<text> is refused too, for a different reason: a substituted glyph is exactly the artifact class
the review below exists to catch, and it would arrive pre-approved.
Rasterizing without a new dependency
PyMuPDF is already in requirements.txt for PDF export, and it reads SVG. So icons work on an
installation with no LibreOffice at all — which is not true of any other rendering path in this
server.
The review, and why the icon is judged alone
A hand-written path is valid XML long before it is a pictogram. The agent cannot see what it drew,
and the visual-repair whitelist can only move, resize or delete a picture — there is no operation
that fixes a malformed icon once it is on a slide. So the icon is reviewed before placement, on
its own: full size, and again downscaled to ~1in and composited onto the slide_background it will
actually sit on, because legibility at slide size is the property that matters and it is not visible
at 800px.
A failed review keeps nothing and returns no handle. An icon_id in the response means the icon
is fit to place, which is what makes the retry loop unambiguous — redraw and call again.
Two cheap guards run before the model is called at all: an empty render is refused with its likely
cause (usually a stroke with no fill="none", or geometry outside the viewBox), and
near-blank/near-solid ink coverage comes back as a render_note.
The review is gated on whether a vision model is configured, not on VISUAL_QA_ENFORCE. That
switch hides the slide inspect/repair tools; whether a model exists to look at one icon is a
different decision, and visual_qa.vision_configured() now expresses it separately from
enforcement_enabled().
The rendered PNG stays on the server
render_svg_icon returns an icon_id — a UUID handle into an in-memory IconStore with the same
TTL and LRU bounds as PresentationStore (PPT_MCP_MAX_ICONS, default 100 — roughly 4 MB) — and
add_icon_to_slide places from it.
Uploading the PNG to DIAL storage and passing its URL to add_image_from_dial_url is the obvious
design, and it does not work. A file this server writes lands in {user}/appdata/dial-pptx-mcp/.
Placing it means the orchestrator asking Core to grant that file to the toolset key — for a folder
it neither owns nor is. Core answers 403 before the tool is entered, so no DIAL_AUTH_MODE value
and no retry helps.
Exports and summary cards are unaffected: their URLs go to the end user, who owns the bucket. An
image-model PNG is unaffected: it reaches the conversation as an attachment the orchestrator can
share. Icons are the one case where the server is both producer and consumer, and the handle is what
keeps them out of that boundary entirely.
Upgrading
Nothing changes for existing decks or tools. Two new environment variables, both optional:
SVG_ICON_MAX_KB (64) and PPT_MCP_MAX_ICONS (100).
If you run this behind a DIAL Quick App, update its system prompt. The icon flow is
get_icon_guidance → render_svg_icon → icon_id → add_icon_to_slide; an icon handle is not a
DIAL file URL and must not be passed to add_image_from_dial_url. The README's Icons section has a
prompt block to paste.