@seed-ship/mcp-ui-solid 6.14.0 → 6.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-ship/mcp-ui-solid",
3
- "version": "6.14.0",
3
+ "version": "6.15.0",
4
4
  "description": "SolidJS components for rendering MCP-generated UI resources",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -1478,6 +1478,14 @@ function ComponentRenderer(props: {
1478
1478
  const mode: ValidationErrorMode = props.errorMode ?? 'block'
1479
1479
  const firstError = validation.errors?.[0]?.message || 'Unknown validation error'
1480
1480
 
1481
+ // P1.6 — an UNKNOWN component type must never produce a silent blank,
1482
+ // whatever the errorMode. The renderer has no branch for it, so even
1483
+ // `silent` would otherwise render nothing. Always surface a visible
1484
+ // "Unsupported component type" notice + a render:error telemetry signal.
1485
+ if (validation.errors?.some((e) => e.code === 'UNKNOWN_COMPONENT_TYPE')) {
1486
+ return <UnsupportedComponentFallback component={props.component} />
1487
+ }
1488
+
1481
1489
  if (mode === 'silent') {
1482
1490
  return null
1483
1491
  }
@@ -1586,10 +1594,37 @@ function ComponentRenderer(props: {
1586
1594
  <Show when={props.component.type === 'graph'}>
1587
1595
  <GraphRenderer component={props.component} toolbarVariant={props.toolbarVariant} />
1588
1596
  </Show>
1597
+ {/* P1.6 — `footer` is a valid type with a real renderer but had no
1598
+ dispatch branch, so a standalone footer component rendered a silent
1599
+ blank (it was only auto-injected at the layout level). */}
1600
+ <Show when={props.component.type === 'footer'}>
1601
+ <FooterRenderer params={props.component.params as any} />
1602
+ </Show>
1589
1603
  </GenerativeUIErrorBoundary>
1590
1604
  )
1591
1605
  }
1592
1606
 
1607
+ /**
1608
+ * Visible fallback for a component whose `type` is not recognized (audit P1.6).
1609
+ *
1610
+ * An unknown type has no renderer branch, so without this it would render a
1611
+ * **silent blank** — even under `errorMode: 'silent'`. The validation gate
1612
+ * routes unknown types here regardless of mode, so the user always sees an
1613
+ * "Unsupported component type: X" notice. The telemetry signal is emitted by
1614
+ * the gate itself (`validation:failed` with `firstErrorCode:
1615
+ * 'UNKNOWN_COMPONENT_TYPE'`), so this component stays purely presentational.
1616
+ */
1617
+ function UnsupportedComponentFallback(props: { component: UIComponent }) {
1618
+ return (
1619
+ <div
1620
+ role="alert"
1621
+ class="w-full rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-200"
1622
+ >
1623
+ Unsupported component type: <code class="font-mono">{props.component.type}</code>
1624
+ </div>
1625
+ )
1626
+ }
1627
+
1593
1628
  /**
1594
1629
  * Render an action component (button or link)
1595
1630
  * Refactored in Phase 5.0 to use useAction hook for Context-based execution
@@ -0,0 +1,77 @@
1
+ /**
2
+ * v6.15.0 — audit P1.6: a component that reaches the renderer with a type that
3
+ * has no render branch must never produce a silent blank.
4
+ *
5
+ * `errorMode: 'silent'` lets a payload past the validation gate without an
6
+ * error card, so a bogus / known-but-unrendered type would otherwise render
7
+ * nothing. We assert the visible "Unsupported component type" notice instead,
8
+ * and that a real type (`footer`, previously missing a branch) now renders.
9
+ */
10
+ import { describe, it, expect, afterEach } from 'vitest'
11
+ import { render, cleanup, waitFor } from '@solidjs/testing-library'
12
+ import { UIResourceRenderer } from './UIResourceRenderer'
13
+ import { MCPUITelemetryProvider } from '../context/MCPUITelemetryContext'
14
+ import type { UIComponent } from '../types'
15
+
16
+ afterEach(cleanup)
17
+
18
+ const bogus = {
19
+ id: 'x1',
20
+ type: 'totally-made-up' as any,
21
+ position: { colStart: 1, colSpan: 12 },
22
+ params: {},
23
+ } satisfies UIComponent
24
+
25
+ describe('Unsupported component never renders blank (P1.6)', () => {
26
+ it('shows a visible "Unsupported component type" notice', async () => {
27
+ // errorMode: 'silent' skips the validation error card, so the bogus type
28
+ // reaches the render dispatch — the catch-all must still surface it.
29
+ const { container } = render(() => (
30
+ <UIResourceRenderer content={bogus} errorMode="silent" />
31
+ ))
32
+
33
+ await waitFor(() => {
34
+ expect(container.textContent ?? '').toContain('Unsupported component type')
35
+ })
36
+ expect(container.textContent).toContain('totally-made-up')
37
+ })
38
+
39
+ it('emits a validation:failed telemetry signal for the unsupported type', async () => {
40
+ const events: Array<{ type: string; errorMessage?: string }> = []
41
+ render(() => (
42
+ <MCPUITelemetryProvider
43
+ sink={(batch) => events.push(...batch)}
44
+ options={{ bufferMs: 0 }}
45
+ >
46
+ <UIResourceRenderer content={bogus} errorMode="silent" />
47
+ </MCPUITelemetryProvider>
48
+ ))
49
+
50
+ // The validation gate emits `validation:failed` with the precise
51
+ // `UNKNOWN_COMPONENT_TYPE` code — the privacy-safe drift signal hosts watch.
52
+ await waitFor(() => {
53
+ expect(
54
+ events.some(
55
+ (e: any) =>
56
+ e.type === 'validation:failed' && e.firstErrorCode === 'UNKNOWN_COMPONENT_TYPE'
57
+ )
58
+ ).toBe(true)
59
+ })
60
+ })
61
+
62
+ it('renders a standalone footer component (previously a silent blank)', async () => {
63
+ const footer: UIComponent = {
64
+ id: 'f1',
65
+ type: 'footer',
66
+ position: { colStart: 1, colSpan: 12 },
67
+ params: { poweredBy: 'Deposium', sourceCount: 3 } as any,
68
+ }
69
+ const { container } = render(() => <UIResourceRenderer content={footer} />)
70
+
71
+ await waitFor(() => {
72
+ expect(container.textContent ?? '').toContain('Deposium')
73
+ })
74
+ // Not the unsupported notice — footer is a real renderer now.
75
+ expect(container.textContent).not.toContain('Unsupported component type')
76
+ })
77
+ })