@aquera/mcp-ui-render 0.0.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +437 -0
  2. package/CONFIGURATION.md +178 -0
  3. package/LICENSE +21 -0
  4. package/README.md +237 -0
  5. package/RELEASE-NOTES.md +77 -0
  6. package/TEST-PLAN.md +72 -0
  7. package/USAGE.md +100 -0
  8. package/package.json +52 -0
  9. package/src/controls.d.ts +1 -0
  10. package/src/controls.js +572 -0
  11. package/src/controls.js.map +1 -0
  12. package/src/elements/aq-mcp-config.d.ts +289 -0
  13. package/src/elements/aq-mcp-config.js +673 -0
  14. package/src/elements/aq-mcp-config.js.map +1 -0
  15. package/src/elements/aq-mcp-field.d.ts +92 -0
  16. package/src/elements/aq-mcp-field.js +275 -0
  17. package/src/elements/aq-mcp-field.js.map +1 -0
  18. package/src/elements/aq-mcp-section.d.ts +101 -0
  19. package/src/elements/aq-mcp-section.js +261 -0
  20. package/src/elements/aq-mcp-section.js.map +1 -0
  21. package/src/engine/status.d.ts +31 -0
  22. package/src/engine/status.js +44 -0
  23. package/src/engine/status.js.map +1 -0
  24. package/src/engine/submit.d.ts +26 -0
  25. package/src/engine/submit.js +37 -0
  26. package/src/engine/submit.js.map +1 -0
  27. package/src/engine/validate.d.ts +24 -0
  28. package/src/engine/validate.js +144 -0
  29. package/src/engine/validate.js.map +1 -0
  30. package/src/engine/values.d.ts +20 -0
  31. package/src/engine/values.js +42 -0
  32. package/src/engine/values.js.map +1 -0
  33. package/src/index.d.ts +18 -0
  34. package/src/index.js +20 -0
  35. package/src/index.js.map +1 -0
  36. package/src/mcp-ui-render.css +811 -0
  37. package/src/registry.d.ts +63 -0
  38. package/src/registry.js +39 -0
  39. package/src/registry.js.map +1 -0
  40. package/src/types.d.ts +239 -0
  41. package/src/types.js +8 -0
  42. package/src/types.js.map +1 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aquera Inc.
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,237 @@
1
+ # @aquera/mcp-ui-render
2
+
3
+ Framework-agnostic **Lit 3** web components that render a `bootstrap` tenant-configuration
4
+ descriptor (see `kb/specs/SPEC-mcp-ui-render.md` and the descriptor contract) using **Nile**.
5
+ Renders identically in Angular (`CUSTOM_ELEMENTS_SCHEMA`), React, or a plain HTML page: no
6
+ framework runtime dependency, no per-connector hand-built form.
7
+
8
+ - **Pure renderer.** No network I/O, holds no secrets. It renders, validates, and emits
9
+ intent (`aq-mcp-change` / `aq-mcp-submit` / `aq-mcp-action`); the host executes tools.
10
+ - **Nile is a peer dependency.** The host provides and registers `@aquera/nile-elements`.
11
+ - **Published to public npm** as `@aquera/mcp-ui-render`, MIT-licensed, alongside the other
12
+ `@aquera` packages. Development happened against the private Verdaccio registry; the public
13
+ line starts at `0.0.1` (D-6b's promotion step — see `RELEASE-NOTES.md`).
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @aquera/mcp-ui-render
19
+ ```
20
+
21
+ Requires the host to also install and register Nile:
22
+
23
+ ```bash
24
+ npm install @aquera/nile-elements @aquera/nile @aquera/nile-glyph
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ ```html
30
+ <script type="module">
31
+ import '@aquera/nile-elements'; // host registers Nile once
32
+ import '@aquera/mcp-ui-render/styles.css'; // required — the elements render into light DOM (see USAGE.md)
33
+ import '@aquera/mcp-ui-render';
34
+ const el = document.querySelector('aq-mcp-config');
35
+ el.descriptor = await callBootstrapTool(); // the bootstrap tool result
36
+ el.values = currentValues;
37
+ el.addEventListener('aq-mcp-submit', (e) => host.save(e.detail));
38
+ el.addEventListener('aq-mcp-action', (e) => host.callTool(e.detail.tool, e.detail.args));
39
+ </script>
40
+ <aq-mcp-config></aq-mcp-config>
41
+ ```
42
+
43
+ See [`USAGE.md`](./USAGE.md) for the Angular / React / plain-HTML integration guides and the
44
+ full event contract, [`CONFIGURATION.md`](./CONFIGURATION.md) for every property, method, event,
45
+ and extensibility hook on `<aq-mcp-config>` / `<aq-mcp-section>` / `<aq-mcp-field>`, and
46
+ [`TEST-PLAN.md`](./TEST-PLAN.md) for how the descriptor contract's §15 conformance checklist maps
47
+ to test coverage.
48
+
49
+ Or open [`REFERENCE.html`](./REFERENCE.html) directly in a browser — a single self-contained page
50
+ covering all of the above (getting started, the descriptor contract, every built-in control,
51
+ validation/dependency/secret rules, the four customization levers, the full API surface, and
52
+ worked examples pulled from real descriptors) in one place, with no build step.
53
+
54
+ ## Rendering a single `ui-component`, without a descriptor
55
+
56
+ Sometimes a host just wants one control's output (e.g. a `test-connection-report` result)
57
+ dropped into some div, not the whole `bootstrap` form. Two ways, no full descriptor/section
58
+ required:
59
+
60
+ **`<aq-mcp-field>` on its own** (recommended): it's an independent custom element, not tied to
61
+ `<aq-mcp-config>`. It only ever needs a minimal `field` object plus a `value`:
62
+
63
+ ```html
64
+ <aq-mcp-field></aq-mcp-field>
65
+ <script type="module">
66
+ import '@aquera/mcp-ui-render';
67
+ const el = document.querySelector('aq-mcp-field');
68
+ el.field = { attribute: 'connectionTest', datatype: 'none', 'ui-component': 'test-connection-report' };
69
+ el.value = theReportJson; // whatever value shape that ui-component expects
70
+ </script>
71
+ ```
72
+
73
+ This still renders `<aq-mcp-field>`'s own two-column row (a label column + the control), so it
74
+ reads as a form-field row rather than a bare block; override `.aq-mcp-field` in
75
+ `mcp-ui-render.css` if that layout isn't wanted. See `apps/mcp-render-demo`'s "Standalone
76
+ `<aq-mcp-field>`" section for a working example.
77
+
78
+ **Bypass the element entirely** with the registry, for the truly bare markup with no wrapper:
79
+
80
+ ```ts
81
+ import { resolveControl } from '@aquera/mcp-ui-render';
82
+ import { render } from 'lit';
83
+
84
+ const renderer = resolveControl('test-connection-report');
85
+ render(renderer({ field: theField, value: theReportJson, error: null, onInput: () => {} }), document.querySelector('#myDiv'));
86
+ ```
87
+
88
+ This needs the host to import Lit's own `render()`, so it fits better when the host is already
89
+ Lit-based; `<aq-mcp-field>` is the simpler choice for any other framework or plain HTML.
90
+
91
+ ## What it does
92
+
93
+ - **Renders a `bootstrap` descriptor**: ordered sections of ordered fields, two-column layout,
94
+ with no per-connector code (`<aq-mcp-config>` → `<aq-mcp-section>` → `<aq-mcp-field>`).
95
+ - **Eleven default controls**, each a Nile element (`properties-table` is the one exception — a
96
+ plain semantic `<table>`, since Nile has no read-only data-table primitive this use case
97
+ needs): `input`, `textarea`, `password`, `select` (+ multi-select for `array`), `radio`,
98
+ `checkbox`, `button`, `file`, `test-connection-report`, `properties-table`, and
99
+ `permissions-report`. An **extensible component registry**
100
+ (`registerControl`) lets a host add or override a `ui-component` without touching the engine;
101
+ an unknown `ui-component` always falls back to the field's datatype default (never dropped).
102
+ `select`/`radio` `options` accept a plain primitive (`value === display text`) or
103
+ `{ value, label }` (`0.5.0`) when the submitted code and its human-readable name differ.
104
+ - **Validation engine.** Compiles each field's `validation` regexes (whole-value, AND-combined),
105
+ applies `isRequired`/`min`/`max`, and pushes the result into the Nile control's own
106
+ `error`/`errorMessage` props; a non-compiling regex logs and never blocks the user. A `file`
107
+ field's `accept`/`maxSizeKB` checks run the same way — **client-side only, advisory**: this is
108
+ a pure renderer with no network I/O, so a connector must still enforce file type/size
109
+ server-side; the UI check is a fast first pass, not the real gate.
110
+ - **Secrets & hidden fields.** Hidden fields render nothing but round-trip unchanged on submit;
111
+ `secret`/`password` fields are masked and never prefilled; `writeOnce` fields render as
112
+ "Replace" and omit from submit unless the user actually enters a new value
113
+ (`WRITE_ONCE_UNCHANGED` sentinel).
114
+ - **Accessible labels.** `input`/`textarea`/`password`/`select` (`0.4.1`) and `radio` (`0.5.0`)
115
+ are given a real, natively associated accessible name — the visible label lives in
116
+ `<aq-mcp-field>`'s own left column, but each control also gets it via Nile's own `label`
117
+ property, with Nile's internal label visually hidden so nothing shows twice. **Known gap:** `checkbox`
118
+ (`nile-slide-toggle`) has no equivalent wiring in Nile itself — its internal checkbox
119
+ isn't `for`/`aria-labelledby`-associated with its `label` at all; not fixable from this
120
+ library's side.
121
+ - **Submit / save-target resolution.** Collects values keyed by `attribute` for the fields that
122
+ applied, resolves each section's save target (its own `submitTool` vs the platform default),
123
+ and emits the payload; it never calls the tool itself.
124
+ - **Dynamic `select`/`radio` options** (`0.5.0`, §9.6). A field with `tool` set and no options
125
+ yet emits `aq-mcp-options-request` once it's applicable — the host calls the tool and feeds
126
+ the result back via `dynamicOptions`, still without this library ever touching the network
127
+ itself (D-4). Pending-a-fetch and a genuine "no options, no way to get any" defect render
128
+ the same (disabled) but only the latter logs the §15 warning.
129
+ - **`properties-table` — a read-only Property/Value summary** (`0.5.0`, `datatype: "none"`,
130
+ never validated/submitted, same contract as `test-connection-report`). For surfacing a
131
+ handful of attributes' current values — typically read-only/hidden tenant attributes that
132
+ matter to see but shouldn't be directly editable fields — without duplicating them as
133
+ disabled form controls. Rows (`PropertiesTableRow[]`) come from either `field.rows` (static,
134
+ declared in the descriptor — the connector already knows the data) or `values[attribute]`
135
+ (dynamic, host-computed at render time — wins if both are present, same priority
136
+ `dynamicOptions` has over `options` for `select`/`radio`). The control formats
137
+ booleans/arrays/plain objects (JSON-stringified)/`null` and truncates long values and
138
+ property names (full text still in each cell's `title`).
139
+ - **Compound field dependency** (`0.5.0`, §12). `Field.dependsOn?: { attribute: string; value:
140
+ string | string[] }[]` — alongside the existing single-attribute
141
+ `dependencyAttribute`/`dependencyValue` — for a field relevant when EITHER of two different
142
+ attributes hits a value (e.g. an FTP-server picker relevant under a direct-connection mode OR
143
+ under an SFTP storage mode, two unrelated fields). Every condition is OR'd together; `[]` or
144
+ omitted behaves as no dependency at all, same as before this addition.
145
+ - **Collapsible field help** (`0.5.0`). `field.help` over 100 characters renders truncated with
146
+ a "View more"/"View less" toggle instead of always showing in full — purely a
147
+ `<aq-mcp-field>` presentation detail, no model change.
148
+ - **`Field.actions` — a multi-action toolbar** (`0.5.0`, §5). `properties-table` uses this to
149
+ render its own card header — title/subtitle from `field.label`/`field.help`, plus a
150
+ right-aligned toolbar of independently-wired buttons (an `icon`-bearing entry renders
151
+ icon-only via `nile-icon-button`; without `icon`, a labeled `nile-button`) — matching the live
152
+ reference's "Generated Configuration" card (a download button + "Edit Configuration"). Each
153
+ entry has its own `tool`/`args`/`confirm`; `aq-mcp-action`'s detail includes which `action`
154
+ fired. `properties-table` is consequently a full-width control (spans the whole field row)
155
+ rather than the usual two-column label/control split — its label still always renders, now in
156
+ its own header instead of `<aq-mcp-field>`'s left column. This icon-vs-label rendering is the
157
+ DEFAULT only — a host wanting different toolbar UI overrides `properties-table` entirely via
158
+ `registerControl` (below), the same escape hatch every other built-in control already has.
159
+ - **`permissions-report` — a scope/permission GRANT table** (`0.5.0`, `datatype: "none"`, never
160
+ validated/submitted). Distinct from `test-connection-report` (that one models PASS/FAIL
161
+ endpoint probes): this renders a validation banner (success/error from `report.valid`) plus a
162
+ "Validation Report" — a running "N Granted" count (always derived from the current `grants`
163
+ array, never a separately-suppliable number) and an All/Success filter, then a Resource
164
+ Type/Permission Type/Access table — matching the live reference's own "Credentials" tab Test
165
+ Connection result exactly. The filter is pure CSS (native radio inputs + a general-sibling
166
+ rule), no Lit reactive state.
167
+ - **Per-section footer customization** (`registerSectionFooter`): a host can replace a
168
+ section's default "Save `<label>`" button with custom markup, keyed by `section.key`. Absent
169
+ a registration, the library's default footer renders unchanged.
170
+ - **Host-driven validation & footer control.** `AqMcpConfig.validateAll()` lets a host bypass
171
+ per-section footers entirely and validate the whole form itself, still getting the library's
172
+ real inline field errors; `showSectionFooters`/`showFooter` hide every footer (default or
173
+ custom) at once. Every edit is also live-validated as it happens, not just on submit.
174
+ - **Configurable section layout.** `sectionsLayout: 'stacked' | 'tabs' | 'accordion'` (default
175
+ `'stacked'`) switches between one-card-per-section, a Nile nav-tab arrangement (`tabsPlacement`
176
+ for horizontal/vertical, `tabsShowErrorIndicator` for a per-tab error dot), and a single-open
177
+ `nile-accordion` stack whose cards tint their edge when a section still has a required field
178
+ empty, so a collapsed section says it needs attention (`accordionOpenSection`,
179
+ `accordionShowFooters`, `accordionColumns` for one- or two-column fields). Validation and submit behave identically in every layout. See [`CONFIGURATION.md`](./CONFIGURATION.md).
180
+ - **Extensibility by construction.** Unknown datatype is treated as `string` (+ warn if
181
+ required); unknown section/field keys are preserved on round-trip; a descriptor `version` MAJOR
182
+ ahead of what the library understands renders best-effort plus a notice, switching on MAJOR only.
183
+
184
+ ## Public API (`index.ts`)
185
+
186
+ | Export | What |
187
+ |---|---|
188
+ | `AqMcpConfig`, `AqMcpSection`, `AqMcpField` | the custom element classes (`aq-mcp-config`, `aq-mcp-section`, `aq-mcp-field`) |
189
+ | `Descriptor`, `Section`, `Field`, `FieldAction`, `Values`, `Datatype`, `AuthType` | descriptor type model |
190
+ | `registerControl` / `resolveControl` / `ControlContext` / `ControlRenderer` | the `ui-component` registry |
191
+ | `registerSectionFooter` / `resolveSectionFooter` / `SectionFooterContext` / `SectionFooterRenderer` | the per-section footer registry |
192
+ | `validateField` | the validation engine, standalone |
193
+ | `collectSubmit`, `sectionHasEditableFields`, `WRITE_ONCE_UNCHANGED`, `SubmitPayload` | submit assembly + save-target resolution |
194
+ | `fieldApplies`, `seedDefaults`, `asArray` | dependency/default-seeding helpers |
195
+ | `sectionStatus`, `SectionStatus`, `isEmpty` | section-completeness engine behind the accordion's attention state, standalone |
196
+
197
+ ## Status
198
+
199
+ Current version in this repo: **`0.0.1`** — the **first public npm release** (`npm view
200
+ @aquera/mcp-ui-render`). It is the same code as the internal `0.6.0` Verdaccio build, renumbered
201
+ for a registry that has never held this package; `RELEASE-NOTES.md` is the published record and
202
+ `CHANGELOG.md` is the pre-npm history. A `^0.6.0` range will not resolve on npm — pin `0.0.1`.
203
+
204
+ The feature history below is written against the Verdaccio version numbers it happened under, and
205
+ is kept because it explains *why* each behaviour is the way it is. `0.6.0` adds the `sectionsLayout: 'accordion'` layout — a single-open stack of `nile-accordion`
206
+ cards with a label-above-control body in one or two columns (`accordionColumns`) and each field's
207
+ `help` as hint text beneath it, where a card whose section still has a required field empty tints
208
+ its edge so it is findable while collapsed. `0.5.5` gave `datatype: "object"` a real editable
209
+ control:
210
+ `input`/`textarea` previously rendered an object value as the literal text `"[object Object]"`;
211
+ both now pretty-print it as editable JSON and parse typed text back into a real object, keeping
212
+ the raw text as an interim value (never throwing, never corrupting the display) while the JSON is
213
+ mid-typing and not yet valid. `object`'s default `ui-component` also changed from `input` to
214
+ `textarea`. See `CHANGELOG.md` for the full writeup. See
215
+ [`CHANGELOG.md`](./CHANGELOG.md) for the full version history and
216
+ [`CONFIGURATION.md`](./CONFIGURATION.md) for the full property/method/event reference.
217
+
218
+ Built and tested (all of it, via `nx test mcp-ui-render` / `nx test-browser mcp-ui-render`):
219
+ descriptor rendering, the control registry, validation, secrets/submit behaviour, the §15
220
+ conformance suite, per-section footer customization, host-driven validation (`validateAll()`,
221
+ `showSectionFooters`) and live on-change validation (`0.3.0`), the `sectionsLayout: 'tabs'`
222
+ layout (`0.4.0`), the `sectionsLayout: 'accordion'` layout and its `sectionStatus` engine
223
+ (`0.6.0`), and every `0.5.0` feature listed above — including two display fixes: a
224
+ `test-connection-report`/`permissions-report` field with no value yet now renders nothing at all
225
+ (not even its own label, previously left dangling next to blank space), and a `file` field's
226
+ upload hint is derived from its own `accept`/`maxSizeKB` instead of always showing
227
+ `nile-file-upload`'s hardcoded image-upload default. Every feature past `0.2.0` is filed as a
228
+ DRAFT amendment (A through J) in `kb/specs/SPEC-mcp-ui-render.md`, pending Hanu/Ahi ratification
229
+ — already built and shipped, per this spec's own established pattern of documenting after build.
230
+
231
+ The MCP-App (`ext-apps`) delivery mode **is built**, in the `mcp-gateway` app, not a separate
232
+ `mcp-engine` repo: `servers/mcp-gateway-service/apps/mcp-gateway/src/mcp-apps/ui/src/mcp-ui-render-app.js`
233
+ (a vanilla-JS single-file entry using `@modelcontextprotocol/ext-apps`'s core `App` class) plus
234
+ server-side `bootstrap`/`credential_test`/`saveConnectorConfig` tool stubs. Not yet done: adding
235
+ `bootstrap` to the per-appType S3 tool allowlist for any real tenant, and end-to-end verification
236
+ against a real MCP client. See "MCP delivery" in `kb/specs/SPEC-mcp-ui-render.md` and the
237
+ 2026-08-01 entry in `kb/QA-QUEUE.md` for the full detail.
@@ -0,0 +1,77 @@
1
+ # Release notes — `@aquera/mcp-ui-render`
2
+
3
+ Public releases on **npm** (`https://registry.npmjs.org/`). One entry per published version,
4
+ newest first.
5
+
6
+ **On the numbering.** This package was developed against the private Verdaccio registry and
7
+ reached `0.5.4` there. `0.0.1` is the **first public npm release** and deliberately restarts the
8
+ series: npm has never held this package, so the public line begins at the beginning rather than
9
+ inheriting a private history no npm consumer can see. The Verdaccio-era detail is preserved in
10
+ `CHANGELOG.md` and is not replayed here — this file describes what a consumer *gets*, not how it
11
+ was built.
12
+
13
+ **If you were installing from Verdaccio:** `0.0.1` is not a downgrade. It is the same code as the
14
+ internal `0.6.0` line, republished. A dependency range like `^0.6.0` will **not** resolve against
15
+ npm — pin `0.0.1` (or `^0.0.1`) when you switch your registry over.
16
+
17
+ ---
18
+
19
+ ## [0.0.1] — first public release
20
+
21
+ First publication of the framework-agnostic renderer to public npm. Same code as the internal
22
+ `0.6.0` build; no functional change was made for the move.
23
+
24
+ ### What the package is
25
+
26
+ **Lit 3 custom elements that render a `bootstrap` tenant-configuration descriptor** — ordered
27
+ sections of ordered fields — with no per-connector code. The element tree is
28
+ `<aq-mcp-config>` → `<aq-mcp-section>` → `<aq-mcp-field>`, and it renders identically in Angular
29
+ (with `CUSTOM_ELEMENTS_SCHEMA`), React, or a plain HTML page.
30
+
31
+ It is a **pure renderer**: no network I/O, and it holds no secrets. It renders, validates, and
32
+ emits intent (`aq-mcp-change`, `aq-mcp-submit`, `aq-mcp-action`); the host executes the tools.
33
+
34
+ ### What is in this release
35
+
36
+ - **Three section layouts** — `stacked` (a card per section), `tabs`, and `accordion` (a
37
+ single-open stack whose collapsed cards still report what is inside them and how much is left
38
+ to fill in).
39
+ - **Eleven controls**, each a Nile element: `input`, `textarea`, `password`, `select` (with
40
+ multi-select for `array`), `radio`, `checkbox`, `button`, `file`, `test-connection-report`,
41
+ `properties-table`, and `permissions-report`. `properties-table` is the one deliberate
42
+ exception — a plain semantic `<table>`, because Nile has no read-only data-table primitive that
43
+ fits this use.
44
+ - **An extensible control registry** (`registerControl`) so a host can add or override a
45
+ `ui-component` without touching the engine. An unknown `ui-component` falls back to the field's
46
+ datatype default and is never dropped.
47
+ - **A validation engine** — per-field regexes (whole-value, AND-combined) plus
48
+ `isRequired`/`min`/`max`, pushed into the Nile control's own `error`/`errorMessage`. A regex
49
+ that does not compile is logged and never blocks the user.
50
+ - **Secrets and hidden fields handled deliberately** — hidden fields render nothing but
51
+ round-trip unchanged; `secret`/`password` fields are masked and never prefilled; `writeOnce`
52
+ fields render as "Replace" and are omitted from submit unless the user actually enters a new
53
+ value.
54
+ - **Accessible names** wired natively for `input`, `textarea`, `password`, `select` and `radio`.
55
+ - **Typed ESM with per-element entry points** for tree-shaking, `.d.ts` types, and a stylesheet
56
+ exported as `@aquera/mcp-ui-render/styles.css`.
57
+
58
+ ### Known gaps, carried forward rather than quietly dropped
59
+
60
+ - **`checkbox` has no programmatic label association.** `nile-slide-toggle`'s internal checkbox is
61
+ not `for`/`aria-labelledby`-associated with its label in Nile itself. It is not fixable from
62
+ this library, and it is a real accessibility gap for screen-reader users on checkbox fields.
63
+ - **File `accept`/`maxSizeKB` checks are advisory only.** This is a pure renderer with no network
64
+ I/O, so a connector **must** still enforce file type and size server-side. The client-side check
65
+ is a fast first pass, not the gate.
66
+
67
+ ### Requirements
68
+
69
+ Nile is a **peer dependency** — the host installs and registers it:
70
+ `@aquera/nile >= 1.2.0`, `@aquera/nile-elements >= 1.9.0`, `@aquera/nile-glyph >= 1.0.0`.
71
+ `lit ^3.2.0` is a direct dependency.
72
+
73
+ ### Licensing
74
+
75
+ Published under **MIT**, matching the other `@aquera` packages on npm. The package previously
76
+ declared `UNLICENSED`, which was correct while it was private and wrong for a public release;
77
+ the `LICENSE` file ships inside the tarball.
package/TEST-PLAN.md ADDED
@@ -0,0 +1,72 @@
1
+ # TEST-PLAN — @aquera/mcp-ui-render
2
+
3
+ Two runnable layers:
4
+
5
+ - **Engine (pure logic)** — `nx test mcp-ui-render` (jest, node env).
6
+ Specs: `test/unit/apps/mcp-ui-render/engine.spec.ts` (validate/values/submit),
7
+ `test/unit/apps/mcp-ui-render/registry.spec.ts` (control + section-footer registries).
8
+ - **Element / rendering (browser)** — `nx test-browser mcp-ui-render`
9
+ (`@web/test-runner` + Chromium, real Nile registered).
10
+ Split by concern under `apps/mcp-ui-render/test/` (each file self-contained; shared
11
+ mount/assert helpers in `test/support/browser-helpers.js`, which is not itself a test
12
+ file):
13
+ - `conformance.browser.js` — hidden/secret fields (§7), dependency reveal (§12), a standing
14
+ regression test for a dependent field's `defaultValue` being seeded when it becomes newly
15
+ applicable mid-session (not just at initial descriptor load, §12), unknown-`ui-component`
16
+ fallback (§9.1), MAJOR-version negotiation (§9.5), section DOM ordering.
17
+ - `controls.browser.js` — control→Nile mapping (§5), the two-column layout, `select`
18
+ without `options` (§15), every built-in control end to end (`textarea`, `select`
19
+ single/multi, `checkbox`, live number validation, `file` incl. the mount-noop guard),
20
+ `datatype: "object"`'s JSON display/parse in `input`/`textarea` (pretty-printed display,
21
+ valid-JSON commits a real object, invalid/incomplete JSON kept as an interim raw string
22
+ without throwing or corrupting the display, defaults to `textarea` not `input`),
23
+ and `test-connection-report`.
24
+ - `layout.browser.js` — `sectionsLayout: 'tabs'` (0.4.0): tab-per-section, hidden-only
25
+ sections excluded from nav, `compact` styling, the error-dot's touched-vs-untouched
26
+ timing, `tabsShowErrorIndicator` opt-out, vertical-placement attributes.
27
+ - `write-once-regression.browser.js` — `writeOnce`/password masking (§7) and the
28
+ standing regression test for the 0.3.1 write-once password bug.
29
+ - `submit.browser.js` — `button` + `confirm` → `aq-mcp-action` (§5), `aq-mcp-submit`
30
+ payload (§13/§14), `validateAll()`, `showSectionFooters`/`showFooter`, a
31
+ `registerSectionFooter` override (0.3.0).
32
+
33
+ ## Conformance checklist (contract §15) → coverage
34
+
35
+ | # | §15 item | Layer | Status |
36
+ |---|---|---|---|
37
+ | 1 | Every field carries `attribute`/`datatype`/`ui-component` | descriptor authoring (server) | n/a to renderer |
38
+ | 2 | Unknown `ui-component` → datatype default; field never dropped (§9.1) | element | ✅ browser |
39
+ | 3 | Exactly one auth section `authType`; no auth-type chooser | authoring | n/a (renderer adds no chooser) |
40
+ | 4 | Every auth field is one the shape needs | authoring | n/a |
41
+ | 5 | No field ships a secret value; use `valueFrom: secretRef` | authoring + defensive | ✅ browser (hidden secret not rendered; password never prefilled) |
42
+ | 6 | Section whose every field is `hidden` renders nothing (§7/§12) | element | ✅ browser (hidden field not rendered) |
43
+ | 7 | `none` submits nothing; `writeOnce` never in a read | engine + element | ✅ engine (submit) + ✅ browser (writeOnce Replace, not prefilled) |
44
+ | 8 | Every `button`/`submitTool` names a same-server tool | authoring | n/a (renderer emits the name via `aq-mcp-action`) |
45
+ | 9 | Section with no `submitTool` saved by the platform (§13) | engine + element | ✅ engine + ✅ browser (submit target null) |
46
+ | 10 | Every `select` has `options` | element + warn | ✅ browser (empty→disabled + `console.warn`) |
47
+ | 11 | Every `attribute` under `authorization.*` / `tenantAttributes.*` | authoring | n/a |
48
+ | 12 | `validation` compiles; no pseudo-rules; range via min/max | engine | ✅ engine (§8 per-datatype, bad-regex graceful) |
49
+ | 13 | Every field with `validation` also has `validationmessage` | authoring | n/a (renderer falls back to "Invalid value.") |
50
+
51
+ Additional renderer behaviour covered:
52
+ - **engine** — dependency apply + default seeding (§12, incl. dependency-not-yet-met and array `dependencyValue`), the compound `dependsOn` OR-across-attributes dependency (§9.7: any entry matching is enough, combines with `dependencyAttribute` via OR not AND, an empty `dependsOn: []` is a no-op), unknown-datatype→string+warn (§9.2), number coercion + prefix preservation on submit (§14), file/multi-file validation (`accept`/`maxSizeKB`/`minItems`/`maxItems`), object `requiredKeys`, boundary cases for `isEmpty` (whitespace, boolean `undefined` vs explicit `false`), the `WRITE_ONCE_UNCHANGED` sentinel short-circuiting `validateField` before any datatype check runs — both for `file` (where it would otherwise crash reading `.name` off the sentinel string) and `string` (where it previously "passed" only by accident), `validateFile`'s `instanceof File` guard failing safe (not crashing) for ANY other non-`File` value a `file`-datatype field might hold, `sectionHasEditableFields`, and the `registerControl`/`registerSectionFooter`/`defaultControlFor` extensibility registries (`registry.spec.ts`).
53
+ - **browser** — every built-in control end to end with real Nile elements and real DOM events (`input`, `textarea`, `password`, `select` single/multi, `radio` incl. the §15 no-options-disabled case, `select`/`radio` `{value,label}` options (code emitted, label displayed), `checkbox`, `file` incl. the mount-noop guard, `test-connection-report` incl. the collapsible-mitigation and hard-error paths, `permissions-report` (null-renders-nothing, banner variant follows `valid`, the granted count derived from `grants` not host-supplied, badge variant follows `access`, and the All/Success filter's structural contract — two radios sharing one native group, `data-access` attributes on every row — since the harness never loads `mcp-ui-render.css` so the actual CSS-driven hide/show isn't assertable here), `properties-table` incl. empty/null, boolean/array/em-dash value formatting, plain-object values JSON-stringified (not `[object Object]`), long-value AND long-property-name truncation with the full text in `title`, static `field.rows` with no host-supplied value at all, `values[attribute]` taking priority over `field.rows` when both are present, the card header/title always rendering even with no rows, the `FULL_WIDTH_CONTROLS` layout — no standalone `.aq-mcp-field__label` column, one full-width control column instead — and `field.actions` (an icon-bearing entry renders icon-only via `nile-icon-button`, else a labeled `nile-button`; clicking one emits `aq-mcp-action` with that specific action's own `tool`/`args`, distinct from a sibling action; an action's own `confirm` opens the shared confirm dialog before emitting), dynamic `select`/`radio` options via `dynamicOptions` + `aq-mcp-options-request` (§9.6: request fires once per applicable tool-backed field, respects `dependencyAttribute`, never double-fires, `dynamicOptions` takes priority over static `options`, pending vs. §15-defect warning distinction), control→Nile mapping (§5), dependency reveal (§12), collapsible "View more"/"View less" help text (short help unaffected, long help toggles between truncated and full), `writeOnce` Replace → input (§7), a standing **regression test for the 0.3.1 write-once password bug** (keystroke-by-keystroke typing into a blank field never reverts to masked/Replace), `button` + `confirm` → `nile-dialog` → `aq-mcp-action` (§5), `aq-mcp-submit` payload (§13/§14), MAJOR-ahead version banner (§9.5), two-column layout, section DOM order following `order` (not descriptor array order), the `sectionsLayout: 'tabs'` feature (tab-per-section, hidden-only sections excluded from nav, `compact` styling, the error-indicator dot's touched-vs-untouched timing, `tabsShowErrorIndicator` opt-out, vertical-placement attributes), `validateAll()`, `showSectionFooters`/`showFooter`, and a `registerSectionFooter` override.
54
+
55
+ ## Running
56
+ ```
57
+ nx test mcp-ui-render # engine (jest, node) — 67 assertions
58
+ nx test-browser mcp-ui-render # rendering (wtr + Chromium) — 66 assertions, 6 files
59
+ ```
60
+ `test-browser` requires Playwright's Chromium: `npx playwright install chromium` once.
61
+
62
+ ## Known external gap (not a bug in this library)
63
+
64
+ `@aquera/nile-elements@1.9.9` registers `nile-file-upload`/`nile-file-preview` inside
65
+ its full bundle but does not export either as an individual subpath (unlike every other
66
+ element it ships — see its `package.json` `exports` map). A host that imports Nile
67
+ selectively (subpath imports, to avoid pulling in the WYSIWYG editor's CommonJS
68
+ dependency) must import the full `@aquera/nile-elements` bundle instead to get the
69
+ `file` control working, or ask the Nile team to add the missing subpath exports. The
70
+ browser test suite works around this with a pair of logic-free stub custom elements
71
+ (registered in `test/support/browser-helpers.js`) purely so it can test **this
72
+ library's** `file`-control wiring in isolation from that packaging gap.
package/USAGE.md ADDED
@@ -0,0 +1,100 @@
1
+ # Using @aquera/mcp-ui-render
2
+
3
+ The library ships standard custom elements, so every host uses the same tags. The host
4
+ provides the descriptor + current values, registers Nile, and handles the emitted intents.
5
+
6
+ ## Events
7
+
8
+ | Event | Detail | When |
9
+ |---|---|---|
10
+ | `aq-mcp-change` | `{ attribute, value, values }` | any field edit |
11
+ | `aq-mcp-submit` | `{ sectionKey, submitTool, values }` | per-section Save (validated); `submitTool` is the connector tool or `null` for the platform default (§13) |
12
+ | `aq-mcp-action` | `{ tool, args, confirm, field }` | a `button` (after `confirm`, if any) |
13
+ | `aq-mcp-options-request` | `{ attribute, tool, args }` | a `select`/`radio` field with `tool` set has no options yet and currently applies (§9.6) — at most once per `attribute` |
14
+
15
+ The library performs no network I/O and holds no secrets; the host executes the tools
16
+ (`aq-mcp-submit` → the platform update or the section's `submitTool`; `aq-mcp-action` → the
17
+ button's `tool`; `aq-mcp-options-request` → the field's `tool`, feeding the result back via
18
+ `dynamicOptions`), enforcing the same-server rule.
19
+
20
+ ## Styling & light DOM
21
+
22
+ `<aq-mcp-config>`, `<aq-mcp-section>`, and `<aq-mcp-field>` all render into **light DOM**
23
+ (`createRenderRoot()` returns `this` — no shadow root), deliberately: it lets Nile's CSS
24
+ custom-property tokens (`--ng-*`/`--nile-*`) and this package's own stylesheet cascade in from
25
+ the host's page the same way any other content does, with no per-shadow-root style injection.
26
+
27
+ The trade-off is **zero style encapsulation** — the host's global CSS can affect `.aq-mcp-*`
28
+ classes, and vice versa — and it means the host **must** import the stylesheet globally once:
29
+
30
+ ```ts
31
+ import '@aquera/mcp-ui-render/styles.css';
32
+ ```
33
+
34
+ Without this import the elements still render structurally (every field, control, and section
35
+ appears) but with **no styling at all** — no card borders, no two-column layout, no error
36
+ colors — and **no console error or warning** to point at why. If a new integration looks
37
+ completely unstyled, check for this import first.
38
+
39
+ ## Plain HTML
40
+
41
+ ```html
42
+ <script type="module">
43
+ import '@aquera/nile-elements'; // host registers Nile
44
+ import '@aquera/mcp-ui-render/styles.css'; // required — see "Styling & light DOM" above
45
+ import '@aquera/mcp-ui-render';
46
+ const el = document.querySelector('aq-mcp-config');
47
+ el.descriptor = await callBootstrapTool(); // the bootstrap tool result
48
+ el.values = currentValues;
49
+ el.addEventListener('aq-mcp-submit', (e) => host.save(e.detail));
50
+ el.addEventListener('aq-mcp-action', (e) => host.callTool(e.detail.tool, e.detail.args));
51
+ </script>
52
+ <aq-mcp-config></aq-mcp-config>
53
+ ```
54
+
55
+ ## Angular
56
+
57
+ ```ts
58
+ // Standalone component (or NgModule) that renders the custom element:
59
+ @Component({
60
+ selector: 'app-connector-config',
61
+ standalone: true,
62
+ schemas: [CUSTOM_ELEMENTS_SCHEMA], // required for aq-mcp-* / nile-* tags
63
+ template: `<aq-mcp-config [descriptor]="descriptor" [values]="values"
64
+ (aq-mcp-submit)="onSubmit($event)" (aq-mcp-action)="onAction($event)"></aq-mcp-config>`,
65
+ })
66
+ export class ConnectorConfigComponent {}
67
+ ```
68
+ `import '@aquera/mcp-ui-render'` **and** `import '@aquera/mcp-ui-render/styles.css'` once at
69
+ bootstrap (alongside `@aquera/nile-elements`) — see "Styling & light DOM" above.
70
+
71
+ ## React
72
+
73
+ ```tsx
74
+ import '@aquera/nile-elements';
75
+ import '@aquera/mcp-ui-render/styles.css'; // required — see "Styling & light DOM" above
76
+ import '@aquera/mcp-ui-render';
77
+
78
+ function ConnectorConfig({ descriptor, values, onSubmit }) {
79
+ const ref = useRef<HTMLElement>(null);
80
+ useEffect(() => {
81
+ const el = ref.current!;
82
+ (el as any).descriptor = descriptor;
83
+ (el as any).values = values;
84
+ const submit = (e: any) => onSubmit(e.detail);
85
+ el.addEventListener('aq-mcp-submit', submit);
86
+ return () => el.removeEventListener('aq-mcp-submit', submit);
87
+ }, [descriptor, values, onSubmit]);
88
+ return <aq-mcp-config ref={ref} />;
89
+ }
90
+ ```
91
+
92
+ ## MCP-App (ext-apps) delivery mode
93
+
94
+ For MCP clients, a thin single-file entry (built with `vite-plugin-singlefile`, Nile inlined)
95
+ imports the same Lit engine and bridges it to the MCP-Apps host: `ontoolresult` →
96
+ `descriptor`/`values`; `aq-mcp-submit`/`aq-mcp-action` → `callServerTool`. This entry is **built**,
97
+ at `servers/mcp-gateway-service/apps/mcp-gateway/src/mcp-apps/ui/src/mcp-ui-render-app.js` (a
98
+ vanilla-JS entry using `@modelcontextprotocol/ext-apps`'s core `App` class — no React), not in a
99
+ separate `mcp-engine` repo (see SPEC-mcp-ui-render §"MCP delivery" and the 2026-08-01 entry in
100
+ `kb/QA-QUEUE.md`).
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@aquera/mcp-ui-render",
3
+ "version": "0.0.1",
4
+ "description": "Framework-agnostic Lit 3 web components that render a Keycloak/MCP bootstrap tenant-configuration descriptor using Nile. Decoupled from any framework — renders in Angular, React, or plain HTML.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "module": "./src/index.js",
8
+ "types": "./src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.d.ts",
12
+ "import": "./src/index.js"
13
+ },
14
+ "./elements/*": {
15
+ "types": "./src/elements/*.d.ts",
16
+ "import": "./src/elements/*.js"
17
+ },
18
+ "./styles.css": "./src/mcp-ui-render.css"
19
+ },
20
+ "sideEffects": [
21
+ "**/controls.js",
22
+ "**/elements/*.js",
23
+ "**/index.js"
24
+ ],
25
+ "dependencies": {
26
+ "lit": "^3.2.0"
27
+ },
28
+ "peerDependencies": {
29
+ "@aquera/nile": ">=1.2.0",
30
+ "@aquera/nile-elements": ">=1.9.0",
31
+ "@aquera/nile-glyph": ">=1.0.0"
32
+ },
33
+ "keywords": [
34
+ "mcp",
35
+ "bootstrap",
36
+ "descriptor",
37
+ "lit",
38
+ "web-components",
39
+ "nile",
40
+ "aquera"
41
+ ],
42
+ "license": "MIT",
43
+ "publishConfig": {
44
+ "registry": "https://registry.npmjs.org/",
45
+ "access": "public"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+ssh://git@bitbucket.org/aquerateam/agentic-access-platform.git",
50
+ "directory": "servers/mcp-gateway-service/apps/mcp-ui-render"
51
+ }
52
+ }
@@ -0,0 +1 @@
1
+ export {};