September 4, 2026
·OpenEphemeris Team
One MCP App, Two Hosts: The Same Widgets in Claude and ChatGPT
OpenEphemeris renders the same interactive chart widgets in Claude and in ChatGPT, from one codebase. Here is what building on the MCP spec instead of a vendor SDK actually saved.
The OpenEphemeris chart wheel renders inside Claude. It also renders inside ChatGPT. Same wheel, same click-a-planet interaction, same house-system toggle that fires a real recalculation against the DE440 kernel.
There is one copy of that widget in our repository. Not two. No if (host === 'chatgpt') branch, no second implementation kept in nervous sync with the first, no vendor SDK pulled in per platform.
That was not cleverness on our part. It was a decision made two years upstream, by other people, that we simply did not fight.
What Adding the Second Host Actually Cost
Here is the honest accounting, because the interesting number is how small it is.
Supporting ChatGPT meant adding two lines to the _meta block a tool carries when it declares a UI:
"openai/outputTemplate": resourceUri,
"openai/widgetAccessible": visibility.includes("app"),
That is the whole of it. Both values are derived from things the tool already declared — the second is just "is this tool one the iframe calls", which visibility had encoded all along. We wrapped them in a shared builder so our stdio and remote transports stop keeping separate copies of the same object, but the ChatGPT-specific part is those two lines.
The widgets themselves were untouched. Not modified, not conditionally compiled, not wrapped. The HTML that ChatGPT loads is byte-for-byte the HTML that Claude loads.
That is what building on a standard buys you, and it is worth being specific about which standard, because "we support MCP" is not the claim here.
MCP Apps Was Co-Authored, Not Adopted
The relevant piece is SEP-1865 — MCP Apps. It defines how a tool result can carry an interface, not just data: a ui:// resource, served with the text/html;profile=mcp-app mime type, pointed at from a tool's _meta.ui.resourceUri.
The part that matters for this post is who wrote it. SEP-1865 was co-authored by Anthropic and OpenAI. It shipped on 2026-01-26 as the first official MCP extension, and was folded into the 2026-07-28 MCP specification proper — which means it stopped being an extension anyone could unilaterally reinterpret and became the thing the spec says hosts read.
Two model vendors who compete on almost everything agreed on one wire format for "here is an app to render." That agreement is the entire reason this post exists.
The Vendor Key Is an Alias, Not a Rival
You will see openai/outputTemplate in ChatGPT app examples and reasonably assume it is a competing format — the OpenAI way of doing the thing MCP does its own way. It is not.
OpenAI's own Apps SDK reference documents openai/outputTemplate as an "optional/compatibility alias for _meta.ui.resourceUri." An alias. Same value, second key, pointing at the same ui:// resource the standard already named.
So our builder emits three forms of the same pointer: the canonical _meta.ui object the spec defines, the deprecated flat ui/resourceUri key for hosts that have not yet moved off the pre-release shape, and the openai/* aliases. All three carry one string. A host reads whichever key it knows and gets the same answer.
The alternative — the path we did not take — was to reach for a host-specific SDK the first time we shipped a widget. That would have worked. It would also have meant that the second host was a second project: its own build, its own render path, its own set of bugs that reproduce in one client and not the other. Instead the second host was a config change.
What Renders
Seven apps, from that one codebase: a natal chart wheel, a synastry bi-wheel, a Human Design bodygraph, a moon phase dial, a transit timeline, a Vedic Rashi grid, and a BaZi four-pillars layout.
They are interactive in the real sense. Click a planet, a house, or an aspect line and the widget composes a question about that exact placement and hands it to the model — which means the interpretation you read is the model's own prose, written fresh from the data on screen, not a blurb pulled out of a lookup table. Switch house systems and the chart recalculates. Change the date, time, or location and hit Recalculate, and the numbers come back from the engine, not from the browser's memory of the last call.
Underneath every one of them is the same calculation path as the plain-JSON endpoints: NASA JPL's DE440 kernel, sub-arcsecond precision, 1550 through 2650 CE. The widget is a face on the engine, not a second engine.
Each Widget Is a Sealed Room
One design choice made portability cheaper than it had any right to be: the widgets cannot phone home.
Each one builds to a single self-contained HTML file. Scripts and styles inlined. Images and fonts as data: URIs. Every conversation with the host happens over postMessage. The build injects a Content Security Policy into the bundle itself:
default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline';
img-src data:; font-src data:; connect-src 'none'
Read the last clause twice. connect-src 'none' — which means the iframe cannot make a network request at all, not to our API, not to a CDN, not to anywhere. Chart data arrives exclusively through MCP tool results.
A bundle with no external dependencies has nothing to negotiate with a new host about. There is no domain to allowlist, no font server to whitelist, no fetch to fail behind a stricter sandbox. It is a sealed room, and a sealed room travels well.
Connecting It Yourself
Now the part that needs saying plainly, because it is the easiest thing in this post to overstate.
OpenEphemeris is not in the ChatGPT app directory. It has not been submitted and it has not been approved. What works today is connecting it yourself as a custom MCP app, which ChatGPT supports through developer mode:
1. Open chatgpt.com/plugins
2. Settings → Plugins → Advanced → turn on Developer mode
3. Back on chatgpt.com/plugins, click "+ Create app"
4. Server URL: https://mcp.openephemeris.com/mcp
Custom MCP apps require a ChatGPT account. Developer mode was available on a Free account when we tested this on 2026-09-04 — but plan availability is OpenAI's to change, so treat that as an observation from a specific day rather than a promise.
Claude is the shorter path, because the connector flow is a first-class feature there: in Settings → Connectors, add a custom connector, paste https://mcp.openephemeris.com/mcp, and leave the OAuth Client ID and Secret blank — the server handles registration itself and Claude opens a sign-in popup on first connect.
One URL. Two hosts. Nothing different on the server between them.
The Part That Generalizes
We are not claiming to be the first MCP server rendering in both places, or the only one — we have not checked, and it would be a strange thing to claim without checking.
The claim is narrower and more useful. When a capability arrives simultaneously as an open specification and as a vendor SDK, those two doors lead to different buildings. The SDK is faster on day one; it is written for exactly the host you are targeting and it assumes exactly the things that host does. The specification is slightly more work on day one and pays you back the first time a second host appears — because the second host already agreed to read what you wrote.
For MCP Apps, that second door was unusually easy to see, since the vendors co-signed the spec and then documented their own keys as aliases of it. The signal could hardly have been louder. Build to _meta.ui.resourceUri, keep your bundle sealed and self-contained, and let the compatibility aliases be the thin layer they were designed to be.
Then the next host costs you a config change instead of a project.
