@womp/mcp-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/image/index.html +259 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +26 -0
- package/dist/mesh/index.html +4378 -0
- package/package.json +42 -0
- package/src/image/index.html +185 -0
- package/src/image/main.ts +76 -0
- package/src/mesh/index.html +197 -0
- package/src/mesh/main.ts +186 -0
- package/src/shared/bridge.ts +165 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Womp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# @womp/mcp-ui
|
|
2
|
+
|
|
3
|
+
Interactive cards for Womp's MCP server, rendered inline in Claude (and any
|
|
4
|
+
other MCP Apps host) instead of the default tool-result thumbnail.
|
|
5
|
+
|
|
6
|
+
Built with [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — tools
|
|
7
|
+
declare a `ui://` resource containing an HTML interface, and the host renders it
|
|
8
|
+
in a sandboxed iframe, pushing tool data in over postMessage.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
generate_image ──► ui://womp/image-<version>.html (280 KB) image card
|
|
12
|
+
image_to_mesh ──► ui://womp/mesh-<version>.html (900 KB) 3D GLB viewer
|
|
13
|
+
get_generation ──► ui://womp/mesh-<version>.html (copes with either)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it exports
|
|
17
|
+
|
|
18
|
+
The package ships the built HTML **as a string**, not as files:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const {
|
|
22
|
+
IMAGE_APP_URI, IMAGE_APP_CSP, IMAGE_APP_HTML,
|
|
23
|
+
MESH_APP_URI, MESH_APP_CSP, MESH_APP_HTML,
|
|
24
|
+
APP_MIME_TYPE,
|
|
25
|
+
} = require("@womp/mcp-ui");
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
No `fs.readFile`, no `__dirname`. That is deliberate: swan-backend runs from
|
|
29
|
+
`build/` on one deploy path and `releases/<sha>/` on another, and a path that
|
|
30
|
+
resolves in one does not resolve in the other.
|
|
31
|
+
|
|
32
|
+
## Consuming it from swan-backend
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import {
|
|
36
|
+
IMAGE_APP_URI, IMAGE_APP_CSP, IMAGE_APP_HTML,
|
|
37
|
+
MESH_APP_URI, MESH_APP_CSP, MESH_APP_HTML,
|
|
38
|
+
APP_MIME_TYPE,
|
|
39
|
+
} from "@womp/mcp-ui";
|
|
40
|
+
|
|
41
|
+
for (const card of [
|
|
42
|
+
{ name: "Womp image card", uri: IMAGE_APP_URI, html: IMAGE_APP_HTML, csp: IMAGE_APP_CSP },
|
|
43
|
+
{ name: "Womp 3D mesh card", uri: MESH_APP_URI, html: MESH_APP_HTML, csp: MESH_APP_CSP },
|
|
44
|
+
]) {
|
|
45
|
+
server.registerResource(card.name, card.uri, { description: card.name }, async () => ({
|
|
46
|
+
contents: [{ uri: card.uri, mimeType: APP_MIME_TYPE, text: card.html,
|
|
47
|
+
_meta: { ui: { csp: card.csp } } }],
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then on each tool:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
_meta: { ui: { resourceUri: IMAGE_APP_URI }, "ui/resourceUri": IMAGE_APP_URI },
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`get_generation` additionally needs `visibility: ["model", "app"]` — see below.
|
|
59
|
+
|
|
60
|
+
The cards consume swan-backend's existing `GENERATION_OUTPUT_SCHEMA`
|
|
61
|
+
(`status` / `generationId` / `operationId` / `imageUrl` / `glbUrl` /
|
|
62
|
+
`previewUrl`) unchanged, so no server contract changes are required.
|
|
63
|
+
|
|
64
|
+
## Things that will waste your day if you don't know them
|
|
65
|
+
|
|
66
|
+
Everything here was established empirically against Claude on 2026-09-01.
|
|
67
|
+
|
|
68
|
+
### The bridge is mandatory, not polish
|
|
69
|
+
|
|
70
|
+
The host mounts the iframe **hidden** (`visibility: hidden; height: 100px`) and
|
|
71
|
+
reveals it only after the app completes:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
host → ui/notifications/sandbox-resource-ready
|
|
75
|
+
app → ui/initialize
|
|
76
|
+
host → McpUiInitializeResult (carries theme, styles, displayMode)
|
|
77
|
+
app → ui/notifications/initialized
|
|
78
|
+
app → ui/notifications/size-changed (ResizeObserver)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`app.connect()` does all of it including auto-resize. Skip it and the card
|
|
82
|
+
renders perfectly and is never displayed. A static HTML card **cannot** work.
|
|
83
|
+
|
|
84
|
+
Handlers must be attached **before** `connect()` or they never fire.
|
|
85
|
+
|
|
86
|
+
### CSP: two different fields
|
|
87
|
+
|
|
88
|
+
- `_meta.ui.csp.resourceDomains` — scripts, styles, **images**
|
|
89
|
+
- `_meta.ui.csp.connectDomains` — **fetch/XHR**, needed for the GLB
|
|
90
|
+
|
|
91
|
+
The mesh card declares both. Missing `connectDomains` looks like a broken
|
|
92
|
+
viewer, not a blocked request.
|
|
93
|
+
|
|
94
|
+
**Do not** also send the flatter `_meta.ui.resourceDomains` variant that the
|
|
95
|
+
package's SKILL.md prose uses. An unrecognised key under `ui` invalidates the
|
|
96
|
+
resource and the widget stops rendering entirely. Only the shape above works.
|
|
97
|
+
|
|
98
|
+
### Host theme variables do not exist on the document
|
|
99
|
+
|
|
100
|
+
Reading `--color-text-primary` off `documentElement` returns nothing. Host
|
|
101
|
+
styles arrive via `onhostcontextchanged` and the app applies them itself
|
|
102
|
+
(`applyHostStyleVariables`). **Every `var()` must ship a fallback.**
|
|
103
|
+
|
|
104
|
+
The complete set a host may send is `McpUiStyleVariableKeySchema` in
|
|
105
|
+
`@modelcontextprotocol/ext-apps` — anything outside it will never resolve.
|
|
106
|
+
Notably it is `--color-border-primary`, not `--color-border`.
|
|
107
|
+
|
|
108
|
+
### Hosts cache UI resources per connector
|
|
109
|
+
|
|
110
|
+
A changed card does **not** reach an existing connector. Bumping the `ui://`
|
|
111
|
+
URI is not enough — the old URI 404'd server-side while Claude still rendered
|
|
112
|
+
its cached copy. The only fix found was **remove and re-add the connector**.
|
|
113
|
+
|
|
114
|
+
This is why the version is baked into the URI, and why the first published
|
|
115
|
+
version should be one you are happy to live with: users will not reconnect on
|
|
116
|
+
their own.
|
|
117
|
+
|
|
118
|
+
### Widget-composed messages trigger a security warning
|
|
119
|
+
|
|
120
|
+
`app.sendMessage()` pre-fills the composer with a red *"Malicious conversation
|
|
121
|
+
content could trick Claude…"* banner. That is correct behaviour — a widget is
|
|
122
|
+
untrusted content — and there is no opt-out.
|
|
123
|
+
|
|
124
|
+
So there is **no "Make 3D" button**. The affordance lives in `generate_image`'s
|
|
125
|
+
response text instead, nudging the model to offer the conversion in prose. Zero
|
|
126
|
+
friction, intent still lands in the transcript, and you get a separate mesh
|
|
127
|
+
card.
|
|
128
|
+
|
|
129
|
+
Rule of thumb: apps may call tools to **observe** (the polling below), but
|
|
130
|
+
should send messages to **act** — and acting costs a warning.
|
|
131
|
+
|
|
132
|
+
### Self-updating cards
|
|
133
|
+
|
|
134
|
+
`CallToolRequest` is part of `AppRequest`, so a card can call tools through the
|
|
135
|
+
host. The running state uses this: the tool returns immediately with
|
|
136
|
+
`{ status: "running", operationId }`, and the card polls `get_generation`
|
|
137
|
+
itself — 2s backing off to 10s, giving up after 6 minutes, stopping on
|
|
138
|
+
teardown.
|
|
139
|
+
|
|
140
|
+
This is why the conversation is never blocked by a slow generation, and it
|
|
141
|
+
replaced a day of MCP Tasks work with a polling loop.
|
|
142
|
+
|
|
143
|
+
**`get_generation` must declare `visibility: ["model", "app"]`.** Without
|
|
144
|
+
`"app"` the host refuses the call and the running state never resolves.
|
|
145
|
+
|
|
146
|
+
### Display modes
|
|
147
|
+
|
|
148
|
+
`ctx.displayMode` is `inline` | `fullscreen` | `pip`. Cards are capped narrow
|
|
149
|
+
inline (360px) so they do not wall off the transcript — which made fullscreen
|
|
150
|
+
useless until CSS started keying off `:root[data-display-mode="fullscreen"]`.
|
|
151
|
+
**PiP is untested.**
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
swan-backend cannot be run locally, so the repo ships its own harness.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
yarn install
|
|
159
|
+
yarn build # build:ui (two vite passes) + build:bake
|
|
160
|
+
yarn dev:mcp # stub MCP server on :3001 with fake slow generations
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`FAKE_DURATION_MS=8000` controls how long a fake generation takes, so the
|
|
164
|
+
running → complete transition is actually exercised.
|
|
165
|
+
|
|
166
|
+
**Local hosts** (nothing exposed): point VS Code or Postman at
|
|
167
|
+
`http://localhost:3001/mcp`. Proves the card and the bridge — but **not** that
|
|
168
|
+
Claude renders it.
|
|
169
|
+
|
|
170
|
+
**Claude** needs a public URL, because it connects from Anthropic's cloud and
|
|
171
|
+
cannot reach localhost:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
ngrok http 3001 --url https://<reserved>.ngrok-free.dev
|
|
175
|
+
# add https://<reserved>.ngrok-free.dev/mcp as a custom connector
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Remember to remove and re-add the connector after every rebuild.
|
|
179
|
+
|
|
180
|
+
There is no console inside the sandboxed iframe, so each card carries a status
|
|
181
|
+
line that names the failing layer instead of going blank. Read it first.
|
|
182
|
+
|
|
183
|
+
## Build pipeline
|
|
184
|
+
|
|
185
|
+
`vite-plugin-singlefile` inlines everything into one HTML file — mandatory,
|
|
186
|
+
since the sandboxed iframe cannot fetch sibling assets, and it fails
|
|
187
|
+
**silently** (a blank card, no error). `scripts/bake-html.mjs` refuses to
|
|
188
|
+
publish if any `<script src>` or `<link href>` survived.
|
|
189
|
+
|
|
190
|
+
The plugin also disables code splitting, which rollup refuses to combine with
|
|
191
|
+
multiple inputs — so the cards build in **two separate passes** driven by a
|
|
192
|
+
`CARD` env var, not one multi-entry build.
|
|
193
|
+
|
|
194
|
+
## Releasing
|
|
195
|
+
|
|
196
|
+
1. `yarn lint && yarn build`
|
|
197
|
+
2. bump `version` in `package.json` — this changes both `ui://` URIs, which is
|
|
198
|
+
what defeats the host cache
|
|
199
|
+
3. `yarn publish` (`prepublishOnly` rebuilds from clean)
|
|
200
|
+
4. bump the dependency in swan-backend and deploy
|
|
201
|
+
5. users must reconnect their connector to see the change
|
|
202
|
+
|
|
203
|
+
## Known gaps
|
|
204
|
+
|
|
205
|
+
- **PiP display mode** untested.
|
|
206
|
+
- **Mesh lighting** is three directional/ambient lights, not an HDR
|
|
207
|
+
environment — an env map means another cross-origin fetch and another CSP
|
|
208
|
+
entry. PBR materials may read flatter than in the Womp editor.
|
|
209
|
+
- **Mesh card is 900 KB.** Almost all of it is three.js plus ~260 KB of
|
|
210
|
+
zod/MCP-SDK from the bridge. A hand-rolled bridge (the handshake is ~5
|
|
211
|
+
messages) would cut that 260 KB from both cards; deferred because three.js
|
|
212
|
+
dominates anyway.
|
|
213
|
+
- **No corner `…` menu** from swan-frontend's tile.
|
|
214
|
+
- **`edit_image`** does not exist in swan-backend, so no edit affordance.
|
|
215
|
+
- The prose nudge for 3D conversion is **not deterministic** — the model may
|
|
216
|
+
not always offer it.
|