Chart Generator MCP
Click on "Deploy 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., "@Chart Generator MCPCreate a column chart of monthly active users for the last six months."
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.
Chart Generator MCP
A small, working reference example: an MCP server hosted on Adobe I/O Runtime (App Builder), exposing two "turn-by-turn" tools that render a finished chart image in one tool call — no interactive UI, no follow-up steps. You ask for a chart, you get a PNG back in the conversation and a link to the underlying SVG.
This exists to answer one question concretely: what does it actually take to host an MCP server on App Builder, correctly? Not the happy-path tutorial version — the actual constraints (an older Node runtime, how chat clients render images, what breaks silently) that only show up once you deploy and try it against a real client. Everything in here was hit and fixed against a real Adobe I/O Runtime deployment and a real MCP client (Claude Desktop), not just written and assumed to work.
What it does
Two tools:
generate_column_chart— a vertical bar chart from a list of values and labels.generate_statistic_infographic— a single-stat card: a big number, a label, supporting text.
Call either one, and the response contains:
A real inline image (the PNG), which displays directly in the conversation.
A link to the SVG (a short-lived presigned URL), for whoever wants the vector source.
That split is deliberate — see Why PNG inline and SVG as a link below, it's the single most important design decision in this repo and the one most likely to matter for whatever you build next.
Related MCP server: Chart-Output MCP Server
Project structure
actions/
mcp-chart-generator/
index.js # The MCP action entrypoint - wires everything together
tools.js # Tool registration: schemas, handlers, response shaping
charts/
column.js # Renders the column chart to SVG, then PNG, then uploads both
infographics/
statistic.js # Renders the stat-card infographic, same pipeline
shared/
svg.js, rect.js, rounded-rect.js, text.js, wrap-text.js
# Small SVG-string-building primitives - see below
mcp/
create-handler.js # Generic "wire an MCP server into an OW web action" helper
node18-web-globals.js # Polyfills the MCP SDK needs on Node 18 (see below)
validator.js # Optional inbound auth (static key or Adobe IMS)
utils/
fonts.js # Loads the bundled Roboto font once per warm container
svg-to-png.js # Rasterizes SVG -> PNG via @resvg/resvg-wasm
fonts/
Roboto-Regular.ttf, Roboto-Medium.ttf
app.config.yaml # The one action this project deploys
package.jsonThe only files you'd actually need to read to understand this specific pattern are actions/mcp-chart-generator/index.js, tools.js, and actions/shared/mcp/create-handler.js. Everything else is chart-rendering implementation detail that happens to also be a reasonably clean small example of hand-built SVG generation, but isn't the point.
How it's structured, and why
create-handler.js is the reusable core. It takes a registerTools(server) function and gives back a complete OpenWhisk web-action main(params) — CORS, health checks, the Streamable-HTTP transport, optional auth, all handled once. If you're building a different MCP server on App Builder, this file (plus node18-web-globals.js) is what you'd actually copy. Everything else in this repo is specific to generating charts.
tools.js is a real, working example of registering MCP tools with Zod schemas against the actual @modelcontextprotocol/sdk McpServer API (server.registerTool(name, { title, description, inputSchema }, handler)), not a simplified version of it.
column.js/statistic.js render SVG by hand, as template strings and small composable primitives (rect(), roundedRect(), text(), wrappedText()), not a charting library. That's a deliberate choice for a small, fixed set of chart types you want full visual control over — not a recommendation against using a real charting library for anything more ambitious. wrappedText()/wrap-text.js in particular is worth a look if you need word-wrapped text in an SVG-only pipeline: it measures glyph widths manually with opentype.js rather than shaping text (see the comment in wrap-text.js for why).
Why PNG inline, and SVG as a link (not the other way around)
Two separate lessons, learned the hard way, both baked into imageContent() in tools.js:
A plain link to an image does not display in most MCP chat clients. Returning { type: 'text', text: 'Here: https://...png' } just shows as a link the user has to click. To actually get an image to render in the conversation, the response needs a real { type: 'image', data: <base64>, mimeType: 'image/png' } content block. That means fetching the bytes and embedding them, not just handing back a URL — see fetchAsBase64().
Raw SVG markup should not go in the response text at all. It's tempting to just inline the SVG string the same way, since it's already text. Don't — MCP tool output becomes part of the model's own context, and an LLM that sees raw SVG markup in its context can end up "helpfully" rewriting, summarizing, or mistranscribing it the next time it's discussed or repeated. Keep it as an opaque link instead: the model can still hand the link back to the user on request (it's sitting right there in the text), but the actual markup is never something the model is asked to reproduce from memory.
The practical effect: column.js/statistic.js upload both the SVG and the PNG to blob storage (@adobe/aio-lib-files) and return presigned URLs for both; tools.js fetches only the PNG bytes to embed, and passes the SVG URL through as a link, untouched.
The presigned links expire in 10 minutes (PRESIGN_EXPIRY_SECONDS in both chart files) — intentional, not a bug to fix. If your use case needs the artifact to outlive that (e.g. referenced from a saved conversation later), you'll want to either extend that expiry or move to permanent storage instead of presigned URLs — that trade-off is yours to make per use case, this repo just shows the mechanism.
Node 18 gotchas (the part that isn't documented anywhere obvious)
Adobe I/O Runtime's actual deployed Node 18 is missing a few globals that modern SDKs assume exist. Both bit us building this, and both are fixed in ways worth understanding rather than copying blind:
@modelcontextprotocol/sdkreferences the globalFileclass at module load time. Node only madeFilea global in v20.node18-web-globals.jspolyfills it fromnode:buffer'sBlobbefore anything requires an MCP SDK module — order matters here, which is why it's required first, at the top ofcreate-handler.js.The SDK, and separately
@adobe/aio-lib-files(via@azure/storage-blob), callcrypto.randomUUID()as a bare global. Node only exposed Web Crypto as an unflagged global starting in v19. Bothcolumn.jsandstatistic.jspolyfill it locally fromnode:crypto'swebcrypto.Avoid requiring the
@adobe/aio-sdkumbrella package. One of its bundled sub-SDKs (Target/Analytics/Campaign Standard, pulled in viaswagger-client) references the globalFileclass unconditionally at load time and crashes on Node 18 before the polyfill above even gets a chance to run, if something else requires it first. Require the narrower packages directly instead —@adobe/aio-lib-core-logging,@adobe/aio-lib-files— as this repo does everywhere.
If you're targeting nodejs:20 or later for your own action, none of this applies — those globals exist natively. This project intentionally stays on nodejs:18 (see app.config.yaml) specifically to prove the pattern works even on the older, more constrained runtime, since that's what you're more likely to inherit in an existing App Builder project.
Getting started
Prerequisites: Node 18+, the Adobe I/O CLI (npm install -g @adobe/aio-cli), and an Adobe Developer Console project/workspace with App Builder enabled.
npm install
aio app use # link this project to a real Console workspace - creates .env/.aio (gitignored)
aio app dev # run locally; prints local action URLscolumn.js/statistic.js also accept local: 'true' as a parameter, which skips the Files/blob-storage upload and PNG render entirely and hands back raw SVG directly — useful for fast visual iteration on the template without needing Files credentials or a full deploy. Hitting an action's URL directly with ?local=true opens the SVG straight in a browser.
Deploy for real:
aio app deployThis deploys one action, mcp-chart-generator, under whatever package name your workspace uses (see app.config.yaml) — the resulting URL is your MCP endpoint.
Testing the deployed endpoint
With the real MCP SDK client:
const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
const { StreamableHTTPClientTransport } = require('@modelcontextprotocol/sdk/client/streamableHttp.js');
const transport = new StreamableHTTPClientTransport(new URL('<your deployed URL>'));
const client = new Client({ name: 'test-client', version: '1.0.0' });
await client.connect(transport);
const result = await client.callTool({
name: 'generate_column_chart',
arguments: { values: [63, 80, 78, 37], labels: ['A', 'B', 'C', 'D'] }
});Or connect it as a custom connector in Claude Desktop and just ask for a chart.
Auth (optional)
actions/shared/mcp/validator.js supports two opt-in modes, controlled by inputs in app.config.yaml (set via .env, not committed):
SERVICE_API_KEY— a static shared secret; callers sendAuthorization: Bearer <key>orx-api-key: <key>.AUTH_VALIDATE_IMS=true— requires a valid Adobe IMS bearer token, validated against IMS userinfo.
Leave both unset for no auth (fine for local dev and demos; not for anything real).
Adapting this for a new use case
This is meant to be a starting point, not a finished product. A few notes if you're using it as a base:
Add a chart type by writing a new file alongside
column.js/statistic.jsthat takesparamsand returns the same{ statusCode, headers, body }shape (raw SVG forlocal: 'true',{ svg, png }presigned URLs otherwise), then register a matching tool intools.js. The SVG-building primitives inshared/(rect,roundedRect,text,wrappedText) are there to reuse.All the default/placeholder values you'll see (category labels, sample metrics, etc.) are generic placeholders, not real example data from anywhere — replace them freely.
column.js'slgsize is intentionally incomplete — its dimensions table is missingchartWidth/chartHeight(it's a different aspect ratio fromsm/md, not just a bigger version of them, so guessing plausible numbers risked shipping something subtly wrong rather than honestly unfinished). Requesting it returns a clean400rather than silently producing broken output. Finishing it — or deciding it should look different entirely — is a reasonable first exercise if you fork this.If your use case needs interactivity (the user adjusts the chart, sees it update live, rather than one tool call producing one finished image), that's a materially different, more involved pattern — MCP Apps (SEP-1865), an interactive resource with its own UI rendered inline by the host — not covered by this repo. This one is specifically the simpler "one tool call, one finished artifact" shape.
License
Apache-2.0 (matches app.config.yaml's package license).
This server cannot be deployed
Maintenance
Related MCP Connectors
Create, inspect, manage, and render charts and data visualizations as SVG/PNG or interactive embeds.
Renders interactive Chart.js charts and dashboards inline in AI conversations. Supports bar, line, area, pie, doughnut, scatter, and radar charts with multi-chart dashboard grids.
Renders interactive Chart.js charts and dashboards inline in AI conversations.
Diagrams, badges, charts and QR codes as plain image URLs you can paste into Markdown.
Related MCP Servers
- AlicenseAqualityAmaintenanceRenders 45+ interactive chart types, dashboards, and KPI widgets directly inside AI conversations. Supports drill-down, live API polling, 20 themes, and one-click export to PNG, PowerPoint, and A4 documents.40359 npm47Functional Source , Version 1.1, MIT Future
- AlicenseAqualityDmaintenanceEnables AI agents to generate and render charts as PNG, SVG, or WebP images directly in chat interfaces. Supports Chart.js specifications and natural language descriptions for creating visualizations from data.39 npm1MIT
- AlicenseAqualityCmaintenanceEnables AI agents to create data visualizations like bar charts, line charts, pie charts, scatter plots, and histograms, returning inline SVG or PNG files.5MIT
- AlicenseBqualityDmaintenanceEnables AI agents to render branded charts as inline images and persistent hosted URLs, supporting explicit chart types and automatic chart suggestion from data.260 npmMIT