mikser-io 11.3.0 → 11.3.1
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.
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# ADR-0008 — MCP-UI: spec-compatible shell + `tools/call` delivery + optional webhook
|
|
2
|
+
|
|
3
|
+
**Status:** Superseded
|
|
4
|
+
**Date:** 2026
|
|
5
|
+
**Supersedes:** —
|
|
6
|
+
**Superseded by:** MCP Apps (SEP-1865), implemented in `mikser-io-mcp-app`
|
|
7
|
+
|
|
8
|
+
## Why this is kept
|
|
9
|
+
|
|
10
|
+
Archived here because the index row above it linked to this file in the
|
|
11
|
+
`mikser-io-mcp` repository, and the surface moved on to `mikser-io-mcp-app`,
|
|
12
|
+
taking the file with it — leaving core's own decision log with a dead link and
|
|
13
|
+
a hole at 0008.
|
|
14
|
+
|
|
15
|
+
Most of it was right, which is the interesting part. What survived into
|
|
16
|
+
`mikser-io-mcp-app` is the shape decided here: a static shell resource
|
|
17
|
+
declared as the tool's `_meta.ui.resourceUri`, per-call fragments delivered as
|
|
18
|
+
data rather than markup, clicks arriving as an ordinary `tools/call` against
|
|
19
|
+
an app-only tool, and the declared action list as the authorization boundary.
|
|
20
|
+
|
|
21
|
+
Two things did not.
|
|
22
|
+
|
|
23
|
+
**The webhook handler.** `mcpUi.handler.url` forwarded the action to an
|
|
24
|
+
external endpoint, and the ADR argued this respects ADR-0001 because the
|
|
25
|
+
application logic stays outside mikser. The logic did; the URL did not. It
|
|
26
|
+
was an MCP concern sitting in the engine, read by nothing but the MCP
|
|
27
|
+
surface — a field core carried on behalf of one plugin. It is gone, and what
|
|
28
|
+
it reached for is a layout sidecar instead: `<layout>.js` beside the template,
|
|
29
|
+
exporting `call` / `read` / `list`, running in the project rather than at the
|
|
30
|
+
end of an HTTP hop.
|
|
31
|
+
|
|
32
|
+
**The package.** A host for applications wants a route whose tool list *is*
|
|
33
|
+
the surface, and `mikser_app_action` is app-callable by specification — it has
|
|
34
|
+
no business appearing on the agent's endpoint. So the surface left
|
|
35
|
+
`mikser-io-mcp` for `mikser-io-mcp-app`, and `mcpUi` became `mcpApp`.
|
|
36
|
+
|
|
37
|
+
One earlier draft of this ADR, on the since-deleted `feat/mcp-ui-handler`
|
|
38
|
+
branch of this repository, is worth one line because the error is easy to
|
|
39
|
+
repeat: it had the iframe POST its action to mikser, reasoning the fetch is
|
|
40
|
+
same-origin because the iframe is served from mikser's origin. Under MCP Apps
|
|
41
|
+
the iframe's CSP is `default-src 'none'; connect-src 'none'` — there is no
|
|
42
|
+
network inside it at all, same-origin included. `postMessage` to the host is
|
|
43
|
+
the only way out. The version below had already corrected that to
|
|
44
|
+
`tools/call`.
|
|
45
|
+
|
|
46
|
+
## Context
|
|
47
|
+
|
|
48
|
+
ADR-0007 (`MCP-UI: layouts as the agent's UI surface`) introduced the idea that mikser layouts can serve as the agent's UI surface inside an MCP host. The `mikser_preview_ui` tool renders an `mcpUi`-decorated layout against an entity and returns HTML for the host to surface as a sandboxed iframe. The user interacts with the iframe — clicks Approve, fills a form, picks a status — and the click needs to get back to mikser as a structured tool result.
|
|
49
|
+
|
|
50
|
+
Three facts about the surrounding ecosystem shape this decision:
|
|
51
|
+
|
|
52
|
+
1. **The MCP Apps spec ([2026-01-26](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx)) defines how iframe-to-server delivery works.** The iframe runs as an MCP client speaking JSON-RPC over `window.parent.postMessage` to the host. The host's "AppBridge" translates `tools/call` frames into real MCP tool calls on the existing transport. Every conformant host (Goose, ChatGPT/Apps SDK, mcp-ui's reference host, basic-host, VS Code Insiders) implements this same pattern.
|
|
53
|
+
|
|
54
|
+
2. **The spec mandates the iframe is cross-origin from the host with a restrictive default CSP.** "The Host and the Sandbox MUST have different origins." Default CSP when `ui.csp` is omitted: `default-src 'none'; connect-src 'none'`. So a `fetch` to mikser from inside the iframe — even on the same machine — is blocked by the browser. The only outbound channel is `postMessage`.
|
|
55
|
+
|
|
56
|
+
3. **The spec mandates that UI is delivered as a static *resource*, not inline tool-result HTML.** Tools that want UI declare `_meta.ui.resourceUri` on their tool definition, pointing at a `ui://` resource with `mimeType: 'text/html;profile=mcp-app'`. Spec-conformant hosts fetch that resource once via `resources/read`, load it in a sandboxed iframe, then push the tool's per-call result to the iframe via `ui/notifications/tool-result`. Empirically, hosts like basic-host display tools that lack `_meta.ui.resourceUri` as plain text rather than rendering an iframe — even if `content[0]` declares `mimeType: 'text/html'`.
|
|
57
|
+
|
|
58
|
+
These three facts together rule out two tempting alternatives:
|
|
59
|
+
|
|
60
|
+
- **An in-process HTTP endpoint that the iframe POSTs to**, with a server-minted "callId" as the capability URL. Prototyped on `feat/mcp-ui-handler`, ruled out — incompatible with `connect-src 'none'`, invisible to the host's consent/audit surface, inverts every other MCP App implementation in the ecosystem.
|
|
61
|
+
- **Returning the rendered HTML inline as `content[0].text` and trusting hosts to render it.** Initially shipped in 8.0.x; surfaced as the symptom "basic-host received the tool result but shows it as text, not an iframe." Spec-conformant hosts need the resource URI on the tool definition; they ignore content with `mimeType: 'text/html'` when no resource URI is set.
|
|
62
|
+
|
|
63
|
+
Separately, productized workflows want to intercept the action server-side without forcing the agent to learn application-specific schemas — a CRM, support system, or admin tool wants to receive the click, do its work, and return a domain-specific result. Pure relay (mikser returns the click data, agent decides what `approve` means) is right for AI-native workflows; webhook delegation (mikser forwards the click to an external URL) is right for productized ones. We want both, without turning mikser into a workflow engine.
|
|
64
|
+
|
|
65
|
+
## Decision
|
|
66
|
+
|
|
67
|
+
### Part A — Rendering: static shell resource + structured tool result
|
|
68
|
+
|
|
69
|
+
**A1. Mikser ships a single static UI resource: `ui://mikser/preview-ui-shell`.**
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
mcp.registerResource(
|
|
73
|
+
'mikser-preview-ui-shell',
|
|
74
|
+
'ui://mikser/preview-ui-shell',
|
|
75
|
+
{ mimeType: 'text/html;profile=mcp-app', ... },
|
|
76
|
+
async (uri) => ({
|
|
77
|
+
contents: [{ uri: uri.href, mimeType: 'text/html;profile=mcp-app', text: SHELL_HTML }],
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The shell is ~120 lines of self-contained HTML+JS implementing the MCP Apps protocol:
|
|
83
|
+
|
|
84
|
+
- Sends `ui/initialize` to the host on load (2-second timeout, fails open if no host replies).
|
|
85
|
+
- Listens for `ui/notifications/tool-result`. Reads `structuredContent.html` from the result and injects it into a `#mikser-ui-root` div. Re-executes any `<script>` tags the layout brought (innerHTML doesn't execute embedded scripts by default).
|
|
86
|
+
- Exposes `window.sendAction(action, payload?)` — the API layouts use to deliver clicks back as `tools/call` against `mikser_ui_action`. The shell tracks `entityId` and `layoutId` from the tool result, so layouts don't have to.
|
|
87
|
+
- Renders an in-iframe debug panel showing every protocol event with timestamps — so authors can see exactly where the round-trip fails on hosts that don't bridge.
|
|
88
|
+
|
|
89
|
+
The shell is static. It does not change between tool calls, between entities, or between mikser versions. It is a fixed bundle of protocol plumbing.
|
|
90
|
+
|
|
91
|
+
**A2. `mikser_preview_ui` declares `_meta.ui.resourceUri` pointing at the shell.**
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
mcp.registerTool('mikser_preview_ui', {
|
|
95
|
+
description: '...',
|
|
96
|
+
inputSchema: { entityId, mode },
|
|
97
|
+
_meta: {
|
|
98
|
+
ui: { resourceUri: 'ui://mikser/preview-ui-shell' },
|
|
99
|
+
},
|
|
100
|
+
}, handler)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This is the spec-mandated signal that this tool renders UI. Hosts that implement MCP Apps fetch the resource once via `resources/read` and use it as the iframe template for every call to this tool. Hosts that don't implement MCP Apps display `content[0].text` as plain text (the fallback path; better than nothing).
|
|
104
|
+
|
|
105
|
+
**A3. The tool result returns content + `structuredContent`.**
|
|
106
|
+
|
|
107
|
+
```jsonc
|
|
108
|
+
{
|
|
109
|
+
"content": [
|
|
110
|
+
{ "type": "text", "text": "<rendered fragment>", "mimeType": "text/html" }
|
|
111
|
+
],
|
|
112
|
+
"structuredContent": {
|
|
113
|
+
"entityId": "/blog/launch.md",
|
|
114
|
+
"layoutId": "/layouts/mcp-ui/post-approval.hbs",
|
|
115
|
+
"mode": "approval",
|
|
116
|
+
"html": "<rendered fragment>",
|
|
117
|
+
"mcpUi": { "actions": [...], "sandbox": [...], "actionTool": "mikser_ui_action" }
|
|
118
|
+
},
|
|
119
|
+
"_meta": { "mcpUi": { ... } }
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`content[0].text` is the fallback for non-UI hosts. `structuredContent` is what the host passes to the iframe via `ui/notifications/tool-result`. The shell reads `structuredContent.html` and injects it; it reads `entityId` and `layoutId` to scope subsequent `sendAction` calls.
|
|
124
|
+
|
|
125
|
+
**A4. Layouts are body fragments, not full HTML documents.**
|
|
126
|
+
|
|
127
|
+
The shell wraps `<!DOCTYPE>` / `<html>` / `<head>` / `<body>` around the injected content. Layouts produce a fragment containing inline `<style>`, body content, and optional inline `<script>`. The script can call `sendAction(action, payload?)` directly — it's exposed on `window` by the shell. No protocol code, no `ui/initialize`, no RPC helper, no postMessage shape.
|
|
128
|
+
|
|
129
|
+
Compare a layout authored before this ADR (~85 lines) with one authored after (~40 lines): everything below `<style>` stays; everything above `<style>` and the entire 50-line protocol `<script>` block disappears.
|
|
130
|
+
|
|
131
|
+
### Part B — Action delivery: `tools/call` against `mikser_ui_action` (visibility=['app'])
|
|
132
|
+
|
|
133
|
+
**B1. Mikser registers a separate, app-callable tool: `mikser_ui_action`.**
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
mcp.registerTool('mikser_ui_action', {
|
|
137
|
+
description: '...',
|
|
138
|
+
inputSchema: { entityId, layoutId, action, payload },
|
|
139
|
+
_meta: { ui: { visibility: ['app'] } },
|
|
140
|
+
}, handler)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`visibility: ['app']` makes the tool invisible to the agent — it never appears in the agent's tool surface — but callable from inside iframes via the host's AppBridge. The agent sees the result as a normal tool turn in its conversation.
|
|
144
|
+
|
|
145
|
+
**B2. The action allow-list is the auth boundary.**
|
|
146
|
+
|
|
147
|
+
`mikser_ui_action`'s handler looks up the layout by `layoutId`, reads its `mcpUi.actions` list, and rejects any action not in that list with an error result. This is the single place where layout-declared "you can do these things" meets iframe-supplied "I want to do this thing." Unknown actions never reach pure relay, never reach `handler.url`.
|
|
148
|
+
|
|
149
|
+
**B3. There is no callId, no signature, no token on this channel.**
|
|
150
|
+
|
|
151
|
+
The host's MCP transport is already authenticated. The visibility flag already gates which tools the iframe can invoke. The action allow-list already scopes what the iframe can ask for. Layered defenses; no per-call crypto.
|
|
152
|
+
|
|
153
|
+
**B4. There is no in-process HTTP endpoint for action delivery.**
|
|
154
|
+
|
|
155
|
+
`/api/mcp-ui/action/...` does not exist. Adding one would create a second delivery path with a different auth model, double the test surface, and offer no benefit on conformant hosts (CSP blocks the fetch) or non-conformant ones (the iframe wouldn't render anyway). One channel, one auth model.
|
|
156
|
+
|
|
157
|
+
### Part C — Optional webhook handler
|
|
158
|
+
|
|
159
|
+
**C1. Layouts may declare a `handler` block in their `mcpUi` frontmatter.**
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
---
|
|
163
|
+
match: "@/articles/*"
|
|
164
|
+
mcpUi:
|
|
165
|
+
mode: approval
|
|
166
|
+
actions: [approve, reject, request-changes]
|
|
167
|
+
sandbox: [allow-scripts]
|
|
168
|
+
handler:
|
|
169
|
+
url: https://app.example.com/mikser-actions
|
|
170
|
+
secret: ${MIKSER_HANDLER_SECRET} # optional, enables HMAC signing
|
|
171
|
+
timeout: 5000 # optional, ms; default 5000
|
|
172
|
+
---
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
When `handler.url` is set, `mikser_ui_action`'s handler forwards the action data to that URL instead of returning the pure-relay payload. The handler's JSON response body becomes the tool result.
|
|
176
|
+
|
|
177
|
+
**C2. The forwarded request is a standard webhook.**
|
|
178
|
+
|
|
179
|
+
```http
|
|
180
|
+
POST https://app.example.com/mikser-actions
|
|
181
|
+
Content-Type: application/json
|
|
182
|
+
X-Mikser-Signature: sha256=...
|
|
183
|
+
X-Mikser-Request-Id: <opaque uuid for idempotency>
|
|
184
|
+
X-Mikser-Layout-Id: /layouts/mcp-ui/post-approval.hbs
|
|
185
|
+
X-Mikser-Mode: approval
|
|
186
|
+
|
|
187
|
+
{
|
|
188
|
+
"entityId": "/documents/blog/launch.md",
|
|
189
|
+
"layoutId": "/layouts/mcp-ui/post-approval.hbs",
|
|
190
|
+
"action": "approve",
|
|
191
|
+
"payload": {},
|
|
192
|
+
"mode": "approval",
|
|
193
|
+
"timestamp": "2026-06-07T15:00:00Z"
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`X-Mikser-Signature` is HMAC-SHA256 of the request body using `handler.secret`. Receivers verify before processing. If `secret` is unset, no signature is sent — fine for development; not recommended in production.
|
|
198
|
+
|
|
199
|
+
**C3. The handler's JSON response is the tool result.**
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"ok": true,
|
|
204
|
+
"summary": "Committed to main; deployment queued (build #4821).",
|
|
205
|
+
"url": "https://app.example.com/deploys/4821",
|
|
206
|
+
"_meta": { "buildId": 4821 }
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Mikser passes this through to the agent unchanged. The agent composes its next message from it. No domain knowledge in mikser.
|
|
211
|
+
|
|
212
|
+
**C4. Handler failures fall back to pure relay.**
|
|
213
|
+
|
|
214
|
+
Network error, timeout, non-2xx response, non-JSON response: mikser logs a warning and resolves the tool call with the default `{ entityId, action, payload }` plus a `handlerError` field carrying the failure reason. The user's click is never lost.
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"entityId": "/documents/blog/launch.md",
|
|
219
|
+
"action": "approve",
|
|
220
|
+
"payload": {},
|
|
221
|
+
"handlerError": "Handler timeout (5000ms) — https://app.example.com/mikser-actions"
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**C5. The handler block is the entire extension surface.**
|
|
226
|
+
|
|
227
|
+
Mikser does not learn about action semantics — what `approve` means, what `request-changes` should do, where the result goes. Adding that knowledge to mikser would violate ADR-0001 (`Mikser is the content layer of the application, not the app`). The webhook contract IS the extension point; if you want behaviour, write a service.
|
|
228
|
+
|
|
229
|
+
## Consequences
|
|
230
|
+
|
|
231
|
+
**Easier:**
|
|
232
|
+
|
|
233
|
+
- Spec compliance is unconditional. Spec-conformant hosts (Goose, ChatGPT, mcp-ui's reference host, basic-host, VS Code Insiders) render mikser layouts as iframes correctly. No host-specific shims.
|
|
234
|
+
- Layouts shrink dramatically — no more per-layout 50-line protocol boilerplate. Author content + inline styles + click handlers; the shell handles everything else. Comparing the blog example's `post-approval.hbs` v8.0.x vs v8.1.0: ~85 lines → ~40 lines, roughly half. The protocol bug surface collapses to one place.
|
|
235
|
+
- The action allow-list + visibility flag are the entire auth model. No bespoke crypto primitive to debug.
|
|
236
|
+
- One static resource served forever; one tool result shape that's the same on every call. Cacheable, predictable, testable.
|
|
237
|
+
|
|
238
|
+
**Harder:**
|
|
239
|
+
|
|
240
|
+
- Layouts authored before 8.1.0 — full HTML documents with embedded `ui/initialize` and RPC plumbing — need rewriting. The new shape is mechanically simpler but it is *not* drop-in compatible with 8.0.x layouts. Mikser's blog example layouts ship pre-rewritten as canonical references.
|
|
241
|
+
- Hosts that haven't implemented MCP Apps (`Claude Desktop` per the open `upstream-host` bug [anthropics/claude-ai-mcp#165](https://github.com/anthropics/claude-ai-mcp/issues/165)) show the iframe as raw text. There is no fallback — and that's deliberate. Both alternatives explored above introduce more problems than they solve.
|
|
242
|
+
- The shell's debug panel is on by default. Production use will want to either gate it behind a query param or remove it entirely. Tracked as a follow-up.
|
|
243
|
+
|
|
244
|
+
## Examples
|
|
245
|
+
|
|
246
|
+
See `documentation/mcp.md` "Layout frontmatter and MCP-UI" — every worked example was rewritten when this ADR landed. Each one collapses to roughly 30-50 lines including inline styles.
|
|
247
|
+
|
|
248
|
+
## Alternatives considered
|
|
249
|
+
|
|
250
|
+
**Direct HTTP from iframe to mikser (capability URL pattern).** Prototyped on `feat/mcp-ui-handler`. Random callId, single-use, action allow-list, loopback bind — textbook capability URL, genuinely secure against CSRF/replay/unknown-actions. Ruled out because:
|
|
251
|
+
|
|
252
|
+
1. Default MCP Apps CSP is `connect-src 'none'` — the browser blocks the fetch on conformant hosts.
|
|
253
|
+
2. The iframe is cross-origin from the host by spec — there is no "same-origin" with mikser to leverage.
|
|
254
|
+
3. The action bypasses the host's audit/consent surface.
|
|
255
|
+
4. It inverts the direction of every other MCP App implementation, making mikser layouts non-portable.
|
|
256
|
+
|
|
257
|
+
**Returning HTML inline in `content[0].text` with `mimeType: 'text/html'`.** Shipped initially in 8.0.x. Surfaced as the symptom "basic-host received the tool result, displayed it as plain text, never rendered an iframe." Conformant hosts read `_meta.ui.resourceUri` off the tool definition (static) to decide what iframe template to load — they do not infer it from response content. The fix is the resource pattern in Part A.
|
|
258
|
+
|
|
259
|
+
**Per-layout tools (`mikser_preview_approval`, `mikser_preview_edit`, etc.) each with their own static `_meta.ui.resourceUri`.** Cleaner mapping but tool count grows with layout count, and the agent has to learn which tool handles which mode. The shell-as-template approach keeps the agent's tool surface stable (one `mikser_preview_ui` for all UIs) while still satisfying the spec's static-URI requirement.
|
|
260
|
+
|
|
261
|
+
**Dual-channel (postMessage primary, HTTP fallback).** Rejected as "no legacy" — two delivery paths means two auth models, two test surfaces, two failure modes to debug, and no host where both are needed. Pick one channel; pick the one the spec specifies.
|
|
262
|
+
|
|
263
|
+
**Built-in action vocabulary (mikser knows what `approve` / `reject` mean).** Rejected. This is the application layer; mikser is the content layer (ADR-0001). The agent owns semantics by default; `handler.url` is the escape hatch for productized cases.
|
|
264
|
+
|
|
265
|
+
**Server-side handler scripts (layouts ship a `handler:` callback in JS).** Rejected. Same reasoning — mikser would become a workflow engine. External webhooks compose; in-mikser handlers would couple action behaviour to mikser deployment.
|
|
266
|
+
|
|
267
|
+
**Per-action handlers (`handler` is a map: `{ approve: url1, reject: url2 }`).** Rejected as YAGNI. Single URL with the action in the payload is enough — the receiver multiplexes.
|
|
268
|
+
|
|
269
|
+
## Watch for drift
|
|
270
|
+
|
|
271
|
+
These are the failure modes this decision is protecting against. If you see them, push back.
|
|
272
|
+
|
|
273
|
+
- **The shell grows application-specific knowledge.** Someone proposes "the shell could pre-format the date before injecting" or "the shell could add a global retry banner." Refuse. The shell is protocol + injection + sendAction relay. Application concerns live in layouts.
|
|
274
|
+
- **A second action-delivery channel sneaks in.** Someone notices `mikser_ui_action` doesn't work on a non-conformant host and proposes adding an HTTP endpoint as a "fallback." Don't. The 8.1.0 design is one channel by deliberate choice.
|
|
275
|
+
- **`_meta.ui.resourceUri` drift on the tool definition.** Someone removes it or changes it to point at something dynamic. Spec-conformant hosts will stop rendering iframes — they only read this field at tool-list time, not per-call.
|
|
276
|
+
- **Layouts start re-implementing the protocol.** Someone writes a layout with its own `ui/initialize` handshake "for control." That layout will fight the shell. The shell exposes `sendAction`; that's the entire contract a layout uses.
|
|
277
|
+
- **The visibility flag drifts on `mikser_ui_action`.** If `_meta.ui.visibility` is removed or changed to `['model', 'app']`, the tool leaks into the agent's surface — it'll appear as an action the agent can take "out of context" without any iframe ever rendering. Strict `['app']`.
|
|
278
|
+
- **Action vocabulary creeps into core.** Someone proposes a built-in `approve` semantic so simple layouts don't need a handler. Refuse; that's application-layer logic.
|
|
279
|
+
- **Per-action handler URLs.** Someone proposes `handler: { approve: '...', reject: '...' }`. Refuse. One URL, the action goes in the payload, the receiver routes.
|
|
280
|
+
- **Retry / backoff on the handler.** Mikser doesn't retry. If the handler is down, the user re-clicks.
|
|
281
|
+
- **Persistent pending state.** There is no pending state — `mikser_preview_ui` returns synchronously and the shell handles per-render state in the iframe. Don't add a "pending action" table.
|
package/docs/decisions/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Decisions don't expire. They get **superseded** when we learn enough to change t
|
|
|
33
33
|
| [0005](./0005-engine-infrastructure-runs-before-plugin-hooks.md) | Engine infrastructure (journal, catalog) is ready before any plugin hook runs; `runtime.update` is upsert; `useSource` codifies the folder-of-files pattern | Accepted |
|
|
34
34
|
| [0006](./0006-when-to-add-to-core.md) | The five-test check for adding capability to the engine vs. shipping it as a plugin | Accepted |
|
|
35
35
|
| [0007](./0007-references-declaration-and-expansion.md) | Entity references: `$`-prefixed declaration (canonical on disk, normalized for render/SDK) and `expand` resolution (inline, GET-cacheable, engine-level `runtime.refs` drives invalidation + live-expand) | Accepted |
|
|
36
|
-
| [0008](
|
|
36
|
+
| [0008](./0008-mcp-ui-action-delivery.md) | MCP-UI rendering + action delivery: static shell resource declared as `_meta.ui.resourceUri`, per-call fragments via `structuredContent`, clicks riding `tools/call` against an app-only tool, declared action list as the authorization boundary. The shape survived; the `mcpUi.handler.url` webhook did not — an MCP concern in the engine, replaced by a layout sidecar. The surface moved to [`mikser-io-mcp-app`](https://github.com/almero-digital-marketing/mikser-io-mcp-app) and `mcpUi` became `mcpApp`. Archived here because the file left with it. | Superseded |
|
|
37
37
|
| [0009](./0009-database-engine-substrate.md) | Sqlite is the engine's persistence substrate. Single `runtime/mikser.sqlite` file holds catalog/refs/manifest/journal as `mikser_entities` / `mikser_refs` / `mikser_snapshots` / `mikser_journal` tables. Plugins register schemas via `registerSchema(name, sql)` + `useDatabase()`. Sift→SQL translator with indexed pushdown, LRU cache for `findById`, worker-side read-only sqlite for sync template helpers, chunked journal walks + `iterateEntities` streaming, `useJournal` auto-persist (mutate the yielded entity; no explicit `updateEntry` needed), `--resume` after interrupted cycles. Replaces `Map<id, entity>` + NDJSON across every engine subsystem. | Accepted |
|
|
38
38
|
| [0010](./0010-plugin-bundles-and-inline-options.md) | Plugin bundles + factory-call form + inline options. Plugins are imported by name and called as factories; `plugins: []` carries factory returns, never strings. Lifecycle plugins are `(options) => (core) => void`; renderers return `{name, options, load?, render?}`; postprocessors return `{name, options, output?, setup?, postprocess, teardown?}`. Per-plugin config moved off `runtime.config.<plugin>` — it arrives as the factory arg and is passed as `config` to `load`/`render`/`setup`/`postprocess`. | Accepted |
|
|
39
39
|
| [0011](./0011-served-entities-expose-deployed-urls.md) | File and resource entities expose deployed URLs. References to served files (image/video/PDF) are `$`-keyed **served paths** (`/img/X.jpg`, `/media/clip.mp4` — the path content authors, = the entity's `meta.url`), resolving through a new `refFilter` `{ 'meta.url': … }` clause backed by an indexed `meta_url` column (schema 9.0.1 → 9.0.2). No collection-prefixed ids leak into content. (Id-refs were tried and rejected — gpoint references content by served path and its `/media/**` `resources()` library means the entity only exists because content references `/media/…`.) The `files`/`resources` plugins stamp `meta.url`, the `assets` plugin stamps `meta.presets` — so expanding a ref yields the served entity's URL set instead of a string to reconstruct. Base-relative in the live catalog (host-agnostic; consumer holds `base`), absolute in static renders (baked from `runtime.options.url`, so logic-less consumers — email, RSS, foreign apps — read a whole URL); `lookupUrl` render helper resolves a ref to `meta.url` or a named preset. SDK collapses `assetUrl(source, preset, {ext})` into one `url(ref)` join + a dev-mode SPA-fallback detector. *("Served entity" — file/resource/preset — avoids colliding with the `assets` plugin's own "asset reference" term.)* | Accepted (proven against gpoint) |
|