Putting an AI Agent in Front of HOOPS AI via MCP

Putting an AI Agent in Front of HOOPS AI via MCP

I put an MCP layer in front of a small FastAPI service wrapping HOOPS AI, so an AI agent (Claude Desktop) can drive HOOPS AI tutorial workflows through the same API a conventional web app would use. Short verdict: it works, and skipping the UI turned out to be the easy part. The harder, more interesting part showed up once I started checking the agent’s answers against the numbers it was supposedly reading: ask the same B-rep endpoint the same kind of generic question twice, and it can report a different, confidently wrong face and edge count each time — not a formatting quirk, an actual wrong answer, read off data it had in hand the whole time. That’s the real subject of this piece. Once the agent is the front end, wiring in another system — a PLM database, an email inbox — turns out to be nearly free; but so, it turns out, is quietly getting the numbers wrong, and I don’t have a settled answer for what QA even means when the agent, not my code, decides both how to present an answer and, apparently, what the answer is.

The same Web API, two front doors: a conventional web app, and now an AI agent talking to it through MCP.

Repo: hoops_ai_mcp_sample. Four feature groups on both sides, kept deliberately small so the pattern stays visible:

WebAPI endpoint MCP tool Purpose
POST /files/upload upload_cad_model SHA-256-deduplicated CAD upload → file_id
POST /CAD/viewer open_cad_viewer Server-side SCS export, served through the HOOPS Web Viewer
POST /MFR/inference run_mfr_inference Per-face manufacturing feature recognition, colorized in the viewer
POST /BRep/attributes get_brep_attributes Raw per-face / per-edge B-rep attributes (types, areas, lengths, dihedral angles, …) as JSON, no rendering

What I found

The MCP server is the application — there’s no dashboard to build

This is the headline finding. Normally, “using” a WebAPI means writing a client: a script, a page, something that calls the endpoint and renders the response. With the MCP layer in front, Claude Desktop is the client — I never wrote a line of frontend code, with one asterisk covered further down (the viewer). Point a chat at a local file path, say “run manufacturing feature recognition on this part,” and the agent chains upload_cad_modelrun_mfr_inference on its own and reports back — table, summary, viewer link, whatever fits — for an SDK capability that previously only existed as “run this notebook cell yourself”:

The entire client for this capability, in practice: a one-line prompt.

There’s still a spec — it just lives in docstrings, not a UI

“No UI” doesn’t mean no spec. It means the spec moves into server.py, as the docstring on each @mcp.tool()-decorated function. The agent reads that prose at call time, and it does the job a UI, an onboarding doc, and a style guide would normally split between them. The whole server is four of these; here’s one:

@mcp.tool()
def open_cad_viewer(cad_file_path: str = "", file_id: str = "") -> dict:
    """Open a CAD file in the HOOPS Web Viewer and return the viewer URL.

    Call this tool ONLY when the user explicitly asks to open or display a
    viewer (e.g. "open the viewer", "show it in CADViewer").

    Provide either:
    - cad_file_path: local path to a CAD file (uploaded automatically)
    - file_id: ID from a previous upload_cad_model call (avoids re-upload)
    ...
    """

That “ONLY when the user explicitly asks” line is a guardrail written directly into the tool description — without it, nothing stops the agent from popping open a viewer window every time a CAD file comes up in conversation. There’s no settings screen, no checkbox, no product manager to negotiate that behavior with; you just write the sentence.

One more thing that falls out of this for free: no localization work. Every docstring stays in plain English, but asking in Japanese or Korean — “このパーツのMFR推論を実行して,” “이 부품에 MFR 추론을 실행해줘” — still resolves to the same tool chain, reply and all, in whatever language I asked in. No translated labels, no per-locale UI strings.

The clearest example: raw B-rep JSON, cooked into whatever the user asked for

get_brep_attributes is the tool that shows this best, because it does nothing except return numbers. It wraps BrepEncoder.push_face_attributes() and push_edge_attributes() and hands back exactly what those return — per-face type codes, areas, centroids, loop counts; per-edge type codes, lengths, dihedral angles, convexities — plus a types_description lookup table for the numeric codes:

{
  "faces": {"types": [...], "areas": [...], "centroids": [...], "loops": [...], "types_description": {...}},
  "edges": {"types": [...], "lengths": [...], "dihedrals": [...], "convexities": [...], "types_description": {...}}
}

No formatting, no rendering, no chart — and that’s not an emergent quirk, it’s spelled out directly in the tool’s docstring:

"""
...
Returns raw per-face and per-edge attribute data. Present it however best
fits the user's request — as a table, a chart, a filtered/sorted list, or a
summary — the response format is not fixed.
"""

That closing line is the entire presentation spec for this endpoint. I asked Claude Desktop “which face is the largest, and what type is it?” and it joined areas against types and types_description itself and answered in a sentence. Asked for “a table of edges sorted by dihedral angle” and got a table. Asked to “chart the edge length distribution” and got a description of the distribution (no chart artifact, but the right numbers, bucketed sensibly). None of that formatting logic exists anywhere in my code — the WebAPI response is identical in all three cases.

The upside: every extra MCP is another free feature

This cuts both ways, and it’s worth stating the upside first. Reformatting requests — a pie chart instead of a bar, an Excel export, a side-by-side comparison across models — the agent just handles, against the same get_brep_attributes JSON, no new endpoint or UI code required. (Requests that hinge on getting a count right are a different story — see the fix a few sections down.) In a conventionally-built app, each of those would have been its own feature request, triaged and often declined.

It doesn’t stop at this sample’s own data, either: MCP is a standard, so the same conversation calling upload_cad_model and get_brep_attributes can just as easily reach into a completely unrelated MCP server — internal or off-the-shelf — with no integration work beyond having both running:

The AI agent branches out to systems through MCP. None of these connections required a line of UI code — each is just another MCP server the same agent can reach.

Concretely: search HOOPS AI for similar parts, pull cost and lead time from a PLM/ERP MCP, have a Gmail MCP draft the resulting quote — and that cross-system workflow assembles itself in one conversation, from tools never written to know about each other. In a conventionally-built app, that’s three integration projects and a UI to tie them together; here, it’s three MCP servers already running. That’s the strongest case for putting an agent in front of an engineering API at all — the same non-determinism that makes reproducibility hard, the subject of the next section, is exactly what makes “pull in this other system too” free.

Open question: what does QA even mean when the agent can get the numbers wrong, not just the write-up?

This is the part I don’t have a tidy answer for, and it turned out to be more serious than I first thought. I asked Claude Desktop about the same CAD file two different ways. A generic “Get B-rep attributes” — reading the raw get_brep_attributes response and summarizing it — came back with 145 faces and 325 edges. “Chart the model’s faces and edges by type,” asked against the same file, came back with 144 faces and 312 edges, cleanly broken down by surface and curve type. The second number is right; the first isn’t — and it wasn’t the same wrong number twice, either. Repeating that same generic “Get B-rep attributes” ask against the same file, a second run came back with 139 faces and 340 edges, and a third with 149 faces and 316 edges — a different wrong answer every time.

Left: asked generically, wrong count — and the standard “Claude is AI and can make mistakes” disclaimer under the reply, this time, wasn’t just boilerplate. Right: asked for a typed breakdown, correct count — same tool, same kind of request in spirit.

What seems to be happening: once the raw array runs into the hundreds of entries, Claude appears to fall back on something closer to a visual estimate than a programmatic count — skimming the returned data and dropping a plausible-looking summary straight into the reply, rather than actually tallying every entry. That would explain why a request that forces a structured breakdown by type comes back right, run after run, while a plain “get the attributes” ask lands on a different wrong number each time: the model is eyeballing a few hundred numbers instead of counting them.

That’s a different order of problem from a different write-up. A skipped detail or a different chart type is a presentation choice; a face count stated with full confidence, from a tool response the agent had in hand the whole time, that turns out to just be wrong, is a wrong answer. “Ask Claude and read the number back” isn’t enough on its own if the question is generic enough to leave the agent eyeballing a long raw array.

The fix: don’t make the agent tally a several-hundred-element array by hand

The “chart…by type” run is the tell. It came out right because grouping-and-counting by type is exactly the kind of structured aggregation an agent tends to do carefully — whereas a generic “get me the attributes” leaves it staring at a flat array of a few hundred numbers and summarizing by eye. The fix isn’t “phrase the prompt better” and hope every user asks the lucky way; it’s to stop handing the agent one large JSON blob and trusting whatever’s on the other end to read it correctly. The real fix belongs in the WebAPI, not the prompt: don’t return a few hundred raw face/edge entries as a single flat array and hope the client tallies them right. Split the endpoint up instead — a small, cheap call for face/edge counts grouped by type, a separate call for the per-face area list, another for per-edge lengths or dihedral angles — each scoped narrowly enough that answering “how many faces of type X” or “what’s the largest face” doesn’t require the agent to eyeball a few hundred numbers at once. Design the API as if the client reading it can’t reliably count past a handful of items, because that’s exactly the constraint it’s operating under.

That’s a more general lesson than this one endpoint: if an MCP tool hands back a large flat array and leaves summarizing it to the agent’s reply, budget for the summary being silently wrong some fraction of the time. Anything a user might reasonably ask to count, sum, or rank is worth its own narrow, pre-aggregated endpoint, rather than trusting a model reading a few hundred JSON entries to get it right. (This sample deliberately leaves get_brep_attributes returning the full raw arrays rather than fixing it this way — it’s kept exactly as-is so the failure above stays reproducible.)

There’s a milder version of the same issue that pre-aggregating counts wouldn’t fix. Ask “chart the model’s faces and edges by type” twice against the same file, and the counts now come back right and identical every time — 144 faces, 312 edges, the same breakdown by surface and curve type — while the write-up underneath still differs. One run reads the part as “mostly prismatic/turned,” with fillets and blends; the other reads the same numbers as a “milled prismatic block” with countersinks and ball-end features:

Same prompt, same file, same counts and breakdown — only the follow-on read of what the part is differs.

Nothing in that case was wrong, just differently presented — and note that this correctness didn’t come from the WebAPI fix proposed above, which this sample never actually implements; it’s the same “ask for a typed breakdown” phrasing from earlier that happens to land on the right numbers here too. Even if the endpoint were split the way “The fix” describes, that would only guarantee the counts, not the commentary around them. For traditional software QA, “same input, same output” is close to a base assumption. Here the numbers are reproducible, but the commentary wrapped around them isn’t — and that’s a real departure from how software QA usually treats reproducibility, one no API redesign is going to touch. How much variance in the write-up is acceptable, and what a regression test would even look like for “the agent told a different story about the same numbers,” I still don’t have a tidy answer for. I’d like to hear how others building agent-fronted engineering tools are thinking about this.

The one piece of UI I couldn’t skip: the viewer

Claude Desktop can’t render 3D graphics inside its own chat window, so open_cad_viewer and run_mfr_inference can’t just hand back data the way get_brep_attributes does — they return a viewer_url, which the agent surfaces as a link you open in an ordinary browser tab. HOOPS AI’s own visualization helper is built for local Jupyter use only, so I exported the model to a stream cache and served it through the HOOPS Web Viewer JS component instead, adding per-face colors and a legend for the MFR result:

The one browser tab this whole “no UI” sample still needs.

Worth flagging before you copy this pattern: it assumes a HOOPS Visualize Web license in addition to HOOPS AI. HOOPS AI’s own viewer module is scoped to notebook-based development — not licensed for an end-user-facing production viewer — and it isn’t meant to substitute for HOOPS Visualize Web in any market where the latter is supposed to be sold. Budget for that licensing conversation before assuming this sample’s approach is redistributable as-is.

To answer the core concern head-on

Worth stating this plainly, since it’s the first thing people ask: doesn’t using Claude as the front end mean my CAD data ends up on Anthropic’s servers? Short answer — the CAD data doesn’t. Here is the split in one place:

Stays on your machine (never sent to Anthropic): the CAD file itself and its geometry — it’s uploaded only to the WebAPI on 127.0.0.1 — plus the SCS stream cache, the MFR colorization, and everything drawn in the viewer, which renders in a local browser tab straight from a localhost URL.

Sent to Anthropic’s servers (the same as any Claude session): your typed prompts, file paths and folder names as text, and the structured tool results — B-rep attribute numbers, MFR labels, counts, and the localhost URLs. The model reasons over those numbers and strings; it never receives the geometry or a rendered image.

So the accurate framing is: the agent exchanges text and metadata with its cloud exactly as it would for any task, but your CAD models never leave your environment.

Verdict

Recommended, with a caveat. As a way to expose an SDK capability without building or maintaining a UI, this works better than I expected — the agent chains tool calls correctly and the viewer makes the output legible. The caveat isn’t just process — if you’re used to shipping software where the presentation layer is code you wrote and tested, be ready for the fact that here it isn’t — it’s also correctness: don’t trust a summary number the agent read off a large raw array — not because the prompt was phrased wrong, but because the endpoint handed over a blob too big to eyeball reliably in the first place, so budget for narrower, pre-aggregated endpoints wherever a user might reasonably ask for a count, sum, or rank. That’s a trade I’d make again for an internal tool or a demo where a person is checking the output. I’m less sure yet how it should be evaluated before something client-facing, or decision-relevant, ships on top of it.

Source code & setup

Repo: github.com/toshi-bata/hoops_ai_mcp_sample. This is a minimal starting point for a forum article, not production-quality code — error handling and security are intentionally light.

Prerequisites: HOOPS AI SDK + license, Python 3.12. Beyond that, it’s two READMEs: webapi/README.md to start the FastAPI server, then mcp_server/README.md to register the MCP server in claude_desktop_config.json. The only setting that matters for the client-server case is HOOPS_WEBAPI_URL in the MCP server’s env block, pointed at the WebAPI host.

Related

See also Wrapping HOOPS AI as a REST API with FastAPI, which built the WebAPI this MCP layer sits in front of. Questions, comments, and especially takes on the QA/reproducibility question above are welcome in this thread.

1 Like