all notes
engineering · 2026 / 09

How We Picked Claude Skills Over MCP Apps for Generative Widgets

Our product needed widgets flexible enough to reshape around a user's own data on the fly. That sent us after Claude Skills instead of MCP Apps — here's how we weighed the two, and what building it actually taught us.

Generative UI is an AI architecture that supports the dynamic building of data visualizations, dashboards, and reports. Getting it right unlocks a new realm of what's possible for enterprise AI applications like ours.

We were already building custom dashboards for our clients before, and they were supported by a core UI kit of components (standard ways to display tables, charts, buttons, layout conventions). But as models and harnesses improved, we started to ask ourselves: "The same way we build MCP connectors to allow our clients to access their data, could we empower them to turn that data into consistent, refreshing, sharable dashboards themselves?"

Ask any AI developer this question and they'd say MCP Apps are the answer. MCP Apps, formally called MCP UI, is the MCP protocol standard for rendering dynamic user interfaces within Claude, ChatGPT, etc sessions. They're helpful when the MCP app creator has a clear idea of what they'd want to show up in specific scenarios. For example, imagine you're building an MCP connector for a ride-sharing app. After a user confirms a ride, you might want to show the user a map tracking the car's GPS location so you know when it's approaching. That map would render directly in your Claude chat via an MCP app.

But asset management use cases are very different. Different investors might want to render the same data source in different ways, and they might want to overlay it with different data as well. So how do you build visualizations when you're not sure what the user will want to visualize? The answer that we found most effective was skills.

This is the story of how flexibility won, and the challenges we experienced along the way.

What we built first: an MCP Apps reference

We started with a reference MCP Apps server, built around four tool groups — plus three slash prompts to steer Claude toward the right one:

  • generativeClaude writes the widget's JSX on the fly, for requests that don't match anything pre-built.
  • universalLine charts, bar charts, and tables assembled from whatever data the user hands over.
  • pre-builtFixed widgets already designed for the platform — NAV, GMV, strike zones.
  • get_dataMocked inputs, so the other three groups could be tested without a live backend.

What we built with skills

Alongside the MCP Apps reference, we prototyped a visualization skill so the product team could compare both.

A skill here is a short description, a known widget template, and instructions for how to rewrite it. Claude writes the widget in-session around the user's data — it doesn't invent how to build a chart, it fills that template.

The same skill can produce different views from different asks. That was the option we wanted product to weigh against the pre-built MCP Apps path.

The Challenge with MCP Apps

We put both prototypes in front of the product team side by side. MCP Apps was faster, but every widget it could produce was one we'd already built for it — a fixed shape from a fixed backend. Skills were slower, but a user could ask for something we hadn't pre-built and Claude would write it on the fly, around their own data. For a platform whose whole premise is that users bring their own data, that mattered more than the extra seconds.

The trade-off is simple to state. A skill is, functionally, a generalized piece of widget code — Claude doesn't invent how to build a chart, it rewrites a known template with real data. All the time in that rewrite goes to writing, not thinking. Skip the rewrite by shipping ready-made code from a backend, and you're back to MCP Apps: fast, but back to a fixed set of shapes.

That flexibility is also what makes growing this product easier over time. With the rigidity of MCP apps, every new feedback or request requires engineering a new app directly within the MCP connector. But while only engineers can edit a production MCP connector, anyone can edit a skill. All that's required is describing what the data represents and how best to visualize it, and the AI can automatically pick this up in the next session run. Many times, our clients don't even need to send the request to us. They can make the modifications on their end with ease.

Fig. 1 — the trade-off in one picture: who authors the component, and what that buys on each side.

Here's what that looks like in practice. Two runs below, leveraging the same skill to render two different kinds of visuals.

Run 01 · Longs vs. shorts, live. "Show me the core book — how big each name is, and whether it's making or losing money versus NAV." No pre-built book map existed; Claude read the file as-is and built one — a position map sized by exposure, colored by P&L.
Run 02 · Same skill, a different ask. "Lay this out so I can scan the book in one go." One file, three pieces — snapshot KPIs, a six-month sleeve-mix bar, and the cash walk — laid out as a single scannable board.

How fast can skills actually be?

Before we shared this with our clients, we wanted to make sure the experience wasn't too slow. So we tested the strategy thoroughly and thoughtfully.

How we measured it

  • One widget, held constant across every run — no variance from widget complexity.
  • The prompt already carried the aggregated data — the clock measures build time only, never data-fetch or aggregation.
  • A brand-new chat for every run — no warm context, no cached scaffolding carried over.
  • Models alternated run to run, so no model got a fresher session than the others.
  • Timer starts the moment the prompt is sent, stops the moment the widget renders successfully in chat.
medians across runs · one widget held constant · prompt sent → widget rendered TTFT reasoning code-gen render + rest
Haiku29.1sfastest
ttft0.7s
reasoning2.5s
code-gen14.1s
throughput89.4 t/s
output1,260

Fastest wall-clock, but the widget it lands needs the most follow-up: shallow reasoning shows up later as rewrites and small regressions we have to catch.

Sonnet69.5sour pick
ttft1.3s
reasoning11.4s
code-gen34.4s
throughput53.9 t/s
output1,850

The pick. Roughly half Opus's wall-clock, and the fewest regressions of any model in the set — one or two writes, and the widget holds up without hand-fixing.

Fable99.7s
ttft1.8s
reasoning17.2s
code-gen41.3s
throughput41.7 t/s
output1,720

Slowest of the four with no quality edge over Sonnet to pay for it.

Opus90.0s
ttft1.9s
reasoning14.5s
code-gen44.0s
throughput38.5 t/s
output1,690

Two to three writes, revised quickly and cleanly — but the extra care costs 20s over Sonnet for output we couldn't tell apart.

The split is the useful part: time-to-first-token is under two seconds everywhere, and reasoning is single-digit-to-teens — the bulk of the wall-clock is writing the widget. That's the rewrite cost from Fig. 1, measured: the faster models spend proportionally less time in code-gen.

Fastest isn't the same as best. Unsurprisingly, Haiku delivered the fastest result, but there were more follow-up passes, and small regressions to fix before the widget was usable. Sonnet came out of the set with the fewest regressions of any model, lands the widget in one or two writes, and still runs roughly half of Opus's wall-clock. When weighing time against quality, it was the best trade in the set. Opus and Fable spent 20-30 extra seconds for output, and the output wasn't significantly better. We eventually shipped the feature with Sonnet set as the default model.

The Routing Problem

Claude ships with its own built-in visualization tool, on by default, and it competes with our skill for the exact same intent: "chart," "graph," "visualize." We've been fighting it since the skill first shipped.

The first fix we tried was an org-level system prompt telling Claude to prefer our skill. It worked in testing, but we backed away from it — a rule like that has to be maintained centrally for every team and every future tool conflict, and it doesn't scale past one skill or one workspace.

So we went after the one lever that scales with the skill itself: instructions inside a skill's body only get read after that skill has already been picked. The only thing Claude sees before choosing is the short description field. The fix was rewriting description to claim the intent aggressively and name the built-in tool directly.

Fig. 2 — When opening a new session, AI will read every skill description, but not the full skill content. This creates a problem where Claude and ChatGPT only route to our visualization skills if and when they deem it necessary.
This is an example of the context conflict problem we're describing. In this session, Claude routed the problem straight to its internal tooling to return a visual before it even read our visualization skill. The result is a generic visual that might not work well for more complex finance use cases.

Closing the loop

After launching these visualization tools to select clients, we've seen an upwards trend in adoption. MCP connectors returning financial data are now significantly more powerful as a result of the visualizations that the AI can render directly within the chat session. We're actively working on a follow-up implementation, one that leverages a hybrid approach of skills for flexible requests, and MCP apps for more predictable ones. More to come on that soon!

If you're working on similar problems around generative UI for enterprise use cases, reach out. We'd love to hear more.

Next noteHow we rolled out Claude Code Auto Mode across our engineering team
all notes