I'm a fan of "smallest thing that could possibly work" demos for APIs. If a tool needs Node + Webpack + a build step + 800 KB of node_modules before I can see what it does, I quietly close the tab.
So when I wanted to make a visual surface for Helium MCP's top-10 volatility candidate ticker stream, I went the other way: a single HTML file, one fetch call, no dependencies, no build, ~260 lines of JS. It is live at:
connerlambden.github.io/helium-news-explorer/tickers.html
Here is the architecture and the small handful of choices that made it work.
The endpoint
GET https://heliumtrades.com/mcp_top_strategies/
Returns a JSON object:
{
"sort": "expected_vol_30d",
"short_volatility": [ ... 5 tickers ... ],
"long_volatility": [ ... 5 tickers ... ]
}
Each ticker entry packs everything I need into one object:
{
"ticker": "NVDA",
"name": "NVIDIA Corporation",
"latest_price": 138.45,
"price_forecast_days": 37,
"price_forecast_percent": 8.2,
"price_forecast_lower_bound_percent": -12.6,
"price_forecast_upper_bound_percent": 29.8,
"bullish_case": "<p>Blackwell ramp ahead of plan; ...</p>",
"bearish_case": "<p>Hyperscaler capex digestion risk; ...</p>",
"analysis_date": "2026-05-27",
"page_url": "..."
}
That's huge: the central forecast number AND the bull/bear narrative AND the uncertainty bounds come from the same model, so the chart you draw and the words you display agree on the same view of the world. No reconciliation step. No data joining. Just fetch, then render.
Crucially, Helium's MCP REST endpoints set Access-Control-Allow-Origin: *, so the page can fetch directly from the browser — no proxy, no server, no CORS tax.
The whole render path
<div id="content" class="loading">…</div>
<script>
const ENDPOINT = "https://heliumtrades.com/mcp_top_strategies/";
async function load() {
const resp = await fetch(ENDPOINT, { cache: "no-store" });
const data = await resp.json();
document.getElementById("content").innerHTML = `
<div class="columns">
<div class="column short">
${data.short_volatility.map(t => cardHTML(t, "short")).join("")}
</div>
<div class="column long">
${data.long_volatility.map(t => cardHTML(t, "long")).join("")}
</div>
</div>`;
}
load();
</script>
Two columns. Five cards each. Template literals as a render engine. That's it. No virtual DOM, no reactivity, no router. The page is static the moment the fetch resolves.
The uncertainty cone in 18 lines of SVG
The interesting visual is the per-card uncertainty cone — a small wedge that gets wider with time, shaded green or red based on the forecast direction, with a line through the center showing the central forecast and a label at the endpoint:
function renderCone(forecastPct, lowerPct, upperPct, days) {
const W = 260, H = 80, PAD = 6;
const maxAbs = Math.max(Math.abs(lowerPct), Math.abs(upperPct),
Math.abs(forecastPct), 1);
const yZero = PAD + (H - 2*PAD) / 2;
const pctToY = p => yZero - (p / maxAbs) * ((H - 2*PAD) / 2);
const xStart = PAD, xEnd = W - PAD;
const yUp = pctToY(upperPct);
const yDn = pctToY(lowerPct);
const yFc = pctToY(forecastPct);
const stroke = forecastPct > 0.1 ? "#6bff9c"
: forecastPct < -0.1 ? "#ff6b8a"
: "#8a93b8";
const fill = stroke.replace(")", ",0.18)").replace("#", "rgba(");
return `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">
<polygon points="${xStart},${yZero} ${xEnd},${yUp} ${xEnd},${yDn}"
fill="${fill}" stroke="${stroke}"/>
<line x1="${xStart}" y1="${yZero}" x2="${xEnd}" y2="${yFc}"
stroke="${stroke}" stroke-width="1.6"/>
<circle cx="${xEnd}" cy="${yFc}" r="3" fill="${stroke}"/>
</svg>`;
}
The cone is just a polygon with three points: (start, 0), (end, upper), (end, lower). The forecast line is a single segment from (start, 0) to (end, forecast). The whole shape collapses to a point at the left edge ("today") and fans out on the right ("today + 37 days").
I went back and forth on whether to plot the cone in absolute price terms vs. percent terms. Percent won: it makes a $69 KO card and a $1842 MSTR card visually comparable, since both cones are scaled to their own max-abs bound. You see the shape of uncertainty, not the units.
Why no chart library?
Because for 10 small charts with one polygon and one line each, a chart library is more code than the chart itself.
- D3: 270 KB. Sets up
d3.scaleLinear(),d3.area(), axes you don't need. - Chart.js: 200 KB. Needs a canvas, animation loop, plugin system.
- ECharts: 1 MB+.
Hand-rolled SVG: 18 lines. The browser already has a SVG renderer. The math is one division per coordinate. The styling lives in the same CSS file as the rest of the page. Less is more.
The bull/bear narratives are HTML
Helium returns the cases as <p>...</p> strings. Easiest possible strip:
function stripHTML(s) {
const tmp = document.createElement("div");
tmp.innerHTML = s;
return (tmp.textContent || tmp.innerText || "")
.replace(/\s+/g, " ").trim();
}
Then truncate to ~280 chars with a word-boundary check so you don't cut mid-word:
function truncate(s, n) {
if (s.length <= n) return s;
const cut = s.slice(0, n);
const lastSp = cut.lastIndexOf(" ");
return (lastSp > n * 0.6 ? cut.slice(0, lastSp) : cut) + "…";
}
The card links to the full Helium analysis page for the long-form version.
Failure handling
The one real failure mode is a per-IP daily quota on the free tier (Helium returns HTTP 402 with no CORS headers, which the browser surfaces as a generic TypeError: Failed to fetch). I handle it the boring honest way:
try {
const resp = await fetch(ENDPOINT, { cache: "no-store" });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
// ... render ...
} catch (e) {
root.innerHTML = `Could not load forecasts: ${e.message}.
The Helium REST API enforces a per-IP daily quota for unauthenticated calls.
Reload tomorrow, or browse the
<a href="https://github.com/connerlambden/helium-mcp-cookbook">cookbook</a>.`;
}
A casual visitor will not hit this — each page load is one GET, and the free quota is ~50/day.
Companion explorer
The same repo also hosts a News Bias Explorer at the root, built on the same pattern: one fetch to mcp_all_source_biases/, vanilla-JS render of 216 sources × 37 bias dimensions, ranked bars and scatter plots with a live Pearson correlation. Same architecture, different data surface. connerlambden.github.io/helium-news-explorer.
Why this matters for the MCP ecosystem
Most MCP server demos are dense Python notebooks or CLI tools. They demonstrate the protocol but not the data. A casual visitor — a journalist evaluating bias-scoring tools, a retail trader curious about an options model, a researcher looking for a teaching corpus — won't install Python to find out.
A 21 KB HTML file on GitHub Pages, with no signup and no key, is the lowest-friction path from "I just heard about this server" to "oh, here's what it can do."
If you're building an MCP server, consider shipping a small zero-dep web surface next to your CLI examples. The marginal cost is one HTML file. The marginal benefit is every browser becoming a demo client.
Links
- Live dashboard: connerlambden.github.io/helium-news-explorer/tickers.html
- Live news bias explorer: connerlambden.github.io/helium-news-explorer
- Source: github.com/connerlambden/helium-news-explorer
- Python recipes for the same API: github.com/connerlambden/helium-mcp-cookbook
- Helium MCP server: github.com/connerlambden/helium-mcp
The forecasts and bias scores are educational. Not investment advice. Track calibration; the model is wrong all the time.
Top comments (0)