@sentientui/mcp 0.18.0 → 0.18.2

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/README.md CHANGED
@@ -105,6 +105,8 @@ A sandboxed demo token is provisioned automatically — 10 calls/month, read-onl
105
105
  | `get_cell_detail` | One cell's story: the generated option, why it was written (stored rationale), and how it performs vs the original |
106
106
  | `generate_cell` | Queue AI generation of a version for one (visitor type, region) cell — brand-locked, live in under a minute (paid plan) |
107
107
 
108
+ > **Generated versions.** An `<Adaptive id="…">` that wraps your original as children (no `variants`) registers as a personalizable region once it is mounted and deployed. Its versions per visitor type are written in SentientUI — inspect and fill them with `get_cell_matrix`, `get_cell_detail`, and `generate_cell` — with no code change or redeploy.
109
+ >
108
110
  > **Code-native vs no-code variants.** Variants you declare in your app (`<Adaptive variants={{…}}>`) register automatically the first time the SDK requests an assignment after you deploy — they go live immediately and need **no** `create_variant` call. Use `create_variant` only for **no-code** variants whose content is stored in SentientUI and rendered without a code change; these start as drafts you activate from the dashboard.
109
111
  >
110
112
  > **Optimizing a component?** Call `get_variant_brief` first. It returns the component's performance, audience, insights, and a data-sufficiency assessment — then your AI assistant writes a new on-brand variant directly into your code (which auto-registers on deploy). When there's no data yet, the brief falls back to best-practice priors for your project's context type so the assistant still makes a sensible change.
@@ -1789,8 +1789,9 @@ import { z as z13 } from "zod";
1789
1789
  var GUIDE = `# SentientUI integration guide \u2014 the adaptive ladder
1790
1790
 
1791
1791
  SentientUI adapts a site per visitor type (personas), learning from real conversions. Each
1792
- project has its own persona vocabulary (dashboard -> Settings -> Personas; the default is
1793
- buyer, researcher, deal_seeker, browser + unknown). Personas are inferred from behavior \u2014 or
1792
+ project has its own persona vocabulary (dashboard -> Settings -> Personas), and it starts
1793
+ EMPTY: every visitor is 'unknown' until a persona exists, and the optimizer learns across all
1794
+ traffic pooled in the meantime. Personas are DISCOVERED from behavior \u2014 or, far faster,
1794
1795
  DECLARED by the app when it already knows the visitor's role: init({ persona: 'admin' }) /
1795
1796
  <AdaptiveProvider persona=...> / window.sentient.persona. Declared beats inferred and serves
1796
1797
  at full confidence; unrecognized values are ignored server-side and surfaced in the dashboard
@@ -1803,12 +1804,14 @@ time.
1803
1804
  1. Run \`npx @sentientui/cli init\` (detects Next App/Pages, Vite, Remix, CRA; installs
1804
1805
  @sentientui/react; writes .env.local; scaffolds an example). It does NOT edit your layout \u2014
1805
1806
  it prints the wrap snippet for step 2.
1806
- 2. Wrap the root layout with <AdaptiveRoot apiKey context> (from '@sentientui/react/next';
1807
- other React apps use <AdaptiveProvider> from '@sentientui/react') and add
1808
- suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes pre-paint.
1809
- Nothing adapts and nothing is tracked until this wrap is in place.
1810
- 3. \`npm run dev\`, then open the app with \`?sentient_persona=buyer\` vs
1811
- \`?sentient_persona=deal_seeker\` to see it adapt. No API key needed (keyless local mode).
1807
+ 2. Wrap the root layout with <AdaptiveRoot apiKey>{children}</AdaptiveRoot> (from
1808
+ '@sentientui/react/next'; other React apps use <AdaptiveProvider> from '@sentientui/react')
1809
+ and add suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes
1810
+ pre-paint. Nothing adapts and nothing is tracked until this wrap is in place. Declare no
1811
+ components here: every <Adaptive> registers itself when it renders (see Rung 2).
1812
+ 3. \`npm run dev\`, then open the app with \`?sentient_persona=a\` vs
1813
+ \`?sentient_persona=b\` to see it adapt \u2014 in keyless local mode any key drives the built-in
1814
+ heuristic. No API key needed. Against a real project the key must be one you declared.
1812
1815
  4. To learn from real traffic: create a project at https://sentient-ui.com and set
1813
1816
  NEXT_PUBLIC_SENTIENT_API_KEY=pk_... in .env.local.
1814
1817
 
@@ -1817,8 +1820,8 @@ time.
1817
1820
  Persona attributes on <html> (zero declaration; the attribute carries the project's own
1818
1821
  vocabulary keys \u2014 declared or inferred):
1819
1822
 
1820
- html[data-sentient-persona='deal_seeker'] .discount-banner { display: block; }
1821
- html[data-sentient-confidence='low'] .discount-banner { display: none; }
1823
+ html[data-sentient-persona='admin'] .admin-tools { display: block; }
1824
+ html[data-sentient-confidence='low'] .admin-tools { display: none; }
1822
1825
 
1823
1826
  Learned style tokens (element-scoped, SSR-safe):
1824
1827
 
@@ -1833,14 +1836,43 @@ prefers-reduced-motion: reduce override in CSS.
1833
1836
 
1834
1837
  ## Rung 2 \u2014 Swap (alternate content)
1835
1838
 
1839
+ Generated versions (start here) \u2014 wrap what the page shows today; the children are the
1840
+ original:
1841
+
1842
+ <Adaptive id="hero-cta" goal="signup_click">
1843
+ <a href="/signup">Start free trial</a>
1844
+ </Adaptive>
1845
+
1846
+ Mount + deploy registers the region. SentientUI writes versions per visitor type in the
1847
+ dashboard ("Who sees what") \u2014 no redeploy. The children render for holdout traffic, unknown
1848
+ visitor types, types with no version yet, and every error path. Mounted regions are requested
1849
+ in one batched call after mount: the first visit shows the original briefly, then swaps; return
1850
+ visits start from the last version. Keyless local mode renders originals only.
1851
+
1852
+ Optional server preload (Next.js App Router): registrySlotIds={['hero-cta']} for generated
1853
+ regions, components={[{ id, variantIds }]} for code variants, both on AdaptiveRoot \u2014 the first
1854
+ paint then shows the served version. List ONLY ids rendered on every page that AdaptiveRoot
1855
+ wraps (in app/layout.tsx, every route): a declared id is decided on every request, rendered or
1856
+ not, and those visits count as views that can never convert.
1857
+
1858
+ Pass
1859
+ onFormSubmit to allow form versions; set reportBaselineText={false} on regions wrapping
1860
+ personalized or account content. (Formerly <AdaptiveSlot>.)
1861
+
1862
+ Code variants \u2014 when you write the alternatives yourself:
1863
+
1864
+ <Adaptive id="buy-box" goal="buy_click" // goal REQUIRED
1865
+ variants={{ control: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }} /> // first key = baseline
1866
+
1867
+ // hook form:
1836
1868
  const { value, bind } = useAdaptive('buy-box', {
1837
1869
  variants: { calm: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }, // first key = baseline
1838
1870
  goal: 'buy_click', // REQUIRED
1839
1871
  });
1840
1872
  return <div {...bind}>{value}</div>;
1841
1873
 
1842
- Always attach bind \u2014 it wires exposure tracking and goal listeners. <Adaptive> is the wrapper
1843
- form; <AdaptiveText> swaps dashboard-managed text.
1874
+ Children or variants on one <Adaptive>, never both. With the hook, always attach bind \u2014 it
1875
+ wires exposure tracking and goal listeners. <AdaptiveText> swaps dashboard-managed text.
1844
1876
 
1845
1877
  ## Rung 3 \u2014 Reorder (structure)
1846
1878
 
@@ -1858,12 +1890,20 @@ their shared parent, and applies nothing if any selector drifts.
1858
1890
 
1859
1891
  ## No-code install order (snippet)
1860
1892
 
1861
- Three tags in <head>, in this order: the window.sentient config, the inline pre-paint script,
1862
- then the deferred loader. The middle tag is optional but recommended \u2014 the loader is deferred,
1863
- so on a return visit the page can paint its original state before the loader runs; the inline
1864
- script applies the last served decision from the visitor's own device first, with no network
1865
- call and no cloak. Get its exact bytes from the dashboard's Install page, or from
1866
- renderSnippetPrePaintScript() in '@sentientui/snippet/install'.
1893
+ Two tags in <head>, in this order: one inline <script> that assigns the window.sentient config
1894
+ and then, in the same tag after the ';', runs the inline pre-paint script; then the deferred
1895
+ loader. The pre-paint script reads window.sentient when it runs, so sharing the tag is safe. It
1896
+ is optional but recommended \u2014 the loader is deferred, so on a return visit the page can paint
1897
+ its original state before the loader runs; the inline script applies the last served decision
1898
+ from the visitor's own device first, with no network call and no cloak. Get the exact bytes from
1899
+ the dashboard's Install page, or from renderSnippetInstall({ config }) in
1900
+ '@sentientui/snippet/install' (renderSnippetPrePaintScript() for the script alone).
1901
+
1902
+ Strict Content-Security-Policy (script hashes): use the three-tag form instead \u2014 config, the
1903
+ pre-paint script alone, then the loader (renderSnippetInstall({ config, split: true })). The
1904
+ combined tag contains the site's config, so its hash differs per site; the split pre-paint tag is
1905
+ byte-identical everywhere, so one hash covers it. Existing three-tag and config + loader installs
1906
+ keep working.
1867
1907
 
1868
1908
  ## Testing the integration
1869
1909
 
@@ -2043,12 +2083,17 @@ var cellOutput = z15.object({
2043
2083
  rationale: z15.string().nullable(),
2044
2084
  error: z15.string().nullable(),
2045
2085
  updatedAt: z15.string(),
2086
+ // These ride the API response too; omitting them from the schema made zod
2087
+ // STRIP them, so agents could never see refinement or retry state.
2088
+ refinedFrom: z15.string().nullable().optional().describe("Incumbent arm this cell is improving on \u2014 a pending cell with this set still SERVES the incumbent"),
2089
+ notBefore: z15.string().nullable().optional().describe("ISO time the queue will next consider this cell (retry backoff / daily-cap deferral); null when due now"),
2090
+ attempts: z15.number().optional().describe("Generation attempts so far"),
2046
2091
  performance: z15.object({
2047
2092
  shown: z15.number(),
2048
2093
  conversions: z15.number(),
2049
2094
  deltaPct: z15.number().nullable().describe("Signed % vs the original conversion rate"),
2050
2095
  tier: z15.string().describe("early | no_baseline | winning | losing | flat")
2051
- }).optional().describe("Present on live cells: the at-a-glance readout")
2096
+ }).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
2052
2097
  });
2053
2098
  function registerCellTools(server, client) {
2054
2099
  server.registerTool(
@@ -2058,7 +2103,15 @@ function registerCellTools(server, client) {
2058
2103
  description: 'The "Who sees what" matrix: which visitor types (personas) have an AI-generated version live in each personalizable region (slot), with per-persona traffic share and the first-miss auto-fill setting. A cell with no row means that visitor type sees the original.' + UNTRUSTED_FIELDS_NOTE,
2059
2104
  inputSchema: { projectId: projectIdSchema },
2060
2105
  outputSchema: {
2061
- slots: z15.array(z15.object({ slotId: z15.string(), displayName: z15.string(), supportsForms: z15.boolean() })),
2106
+ slots: z15.array(
2107
+ z15.object({
2108
+ slotId: z15.string(),
2109
+ displayName: z15.string(),
2110
+ supportsForms: z15.boolean(),
2111
+ // API sends this; without it zod stripped the draft-vs-live signal.
2112
+ live: z15.boolean().optional().describe("False for an sdk-registered draft column \u2014 nothing serves until its first generation publishes it")
2113
+ })
2114
+ ),
2062
2115
  personas: z15.array(personaOutput),
2063
2116
  cells: z15.array(cellOutput),
2064
2117
  autoFill: z15.boolean(),
@@ -2072,13 +2125,19 @@ function registerCellTools(server, client) {
2072
2125
  const data = await client.get(`/projects/${id}/cells`);
2073
2126
  const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
2074
2127
  const lines = [
2075
- `Slots: ${data.slots.map((s) => untrusted(s.displayName)).join(", ") || "(none published)"}`,
2128
+ `Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
2076
2129
  `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
2077
2130
  ];
2078
2131
  for (const p of data.personas) {
2079
2132
  const states = data.slots.map((s) => {
2080
2133
  const c = byCell.get(`${s.slotId}::${p.key}`);
2081
- return `${untrusted(s.displayName, 40)}: ${c ? c.status : "original"}`;
2134
+ let state = c ? c.status : "original";
2135
+ if (c && (c.status === "pending" || c.status === "generating") && c.refinedFrom) {
2136
+ state = `${c.status} (improving on ${untrusted(c.refinedFrom, 64)} \u2014 current version still live)`;
2137
+ } else if (c && c.status === "pending" && c.notBefore && Date.parse(c.notBefore) > Date.now()) {
2138
+ state = `pending (waiting until ${c.notBefore})`;
2139
+ }
2140
+ return `${untrusted(s.displayName, 40)}: ${state}`;
2082
2141
  });
2083
2142
  lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
2084
2143
  }
@@ -2120,7 +2179,7 @@ function registerCellTools(server, client) {
2120
2179
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2121
2180
  },
2122
2181
  withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2123
- var _a, _b;
2182
+ var _a, _b, _c;
2124
2183
  const id = encodeURIComponent(projectId);
2125
2184
  const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
2126
2185
  const lines = [];
@@ -2132,7 +2191,12 @@ function registerCellTools(server, client) {
2132
2191
  lines.push("No generated version is live for this cell.");
2133
2192
  }
2134
2193
  if ((_a = data.cell) == null ? void 0 : _a.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
2135
- if ((_b = data.cell) == null ? void 0 : _b.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
2194
+ if (((_b = data.cell) == null ? void 0 : _b.refinedFrom) && (data.cell.status === "pending" || data.cell.status === "generating")) {
2195
+ lines.push(
2196
+ `An improved take on ${untrusted(data.cell.refinedFrom, 64)} is being written \u2014 the current version keeps serving meanwhile.`
2197
+ );
2198
+ }
2199
+ if ((_c = data.cell) == null ? void 0 : _c.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
2136
2200
  lines.push(data.verdict);
2137
2201
  if (data.performance) {
2138
2202
  lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
@@ -2177,7 +2241,7 @@ function registerCellTools(server, client) {
2177
2241
  }
2178
2242
 
2179
2243
  // src/server.ts
2180
- var PKG_VERSION = true ? "0.18.0" : "0.0.0-dev";
2244
+ var PKG_VERSION = true ? "0.18.2" : "0.0.0-dev";
2181
2245
  function createMcpServer(client) {
2182
2246
  const server = new McpServer(
2183
2247
  {
package/dist/index.cjs CHANGED
@@ -1795,8 +1795,9 @@ var import_zod13 = require("zod");
1795
1795
  var GUIDE = `# SentientUI integration guide \u2014 the adaptive ladder
1796
1796
 
1797
1797
  SentientUI adapts a site per visitor type (personas), learning from real conversions. Each
1798
- project has its own persona vocabulary (dashboard -> Settings -> Personas; the default is
1799
- buyer, researcher, deal_seeker, browser + unknown). Personas are inferred from behavior \u2014 or
1798
+ project has its own persona vocabulary (dashboard -> Settings -> Personas), and it starts
1799
+ EMPTY: every visitor is 'unknown' until a persona exists, and the optimizer learns across all
1800
+ traffic pooled in the meantime. Personas are DISCOVERED from behavior \u2014 or, far faster,
1800
1801
  DECLARED by the app when it already knows the visitor's role: init({ persona: 'admin' }) /
1801
1802
  <AdaptiveProvider persona=...> / window.sentient.persona. Declared beats inferred and serves
1802
1803
  at full confidence; unrecognized values are ignored server-side and surfaced in the dashboard
@@ -1809,12 +1810,14 @@ time.
1809
1810
  1. Run \`npx @sentientui/cli init\` (detects Next App/Pages, Vite, Remix, CRA; installs
1810
1811
  @sentientui/react; writes .env.local; scaffolds an example). It does NOT edit your layout \u2014
1811
1812
  it prints the wrap snippet for step 2.
1812
- 2. Wrap the root layout with <AdaptiveRoot apiKey context> (from '@sentientui/react/next';
1813
- other React apps use <AdaptiveProvider> from '@sentientui/react') and add
1814
- suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes pre-paint.
1815
- Nothing adapts and nothing is tracked until this wrap is in place.
1816
- 3. \`npm run dev\`, then open the app with \`?sentient_persona=buyer\` vs
1817
- \`?sentient_persona=deal_seeker\` to see it adapt. No API key needed (keyless local mode).
1813
+ 2. Wrap the root layout with <AdaptiveRoot apiKey>{children}</AdaptiveRoot> (from
1814
+ '@sentientui/react/next'; other React apps use <AdaptiveProvider> from '@sentientui/react')
1815
+ and add suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes
1816
+ pre-paint. Nothing adapts and nothing is tracked until this wrap is in place. Declare no
1817
+ components here: every <Adaptive> registers itself when it renders (see Rung 2).
1818
+ 3. \`npm run dev\`, then open the app with \`?sentient_persona=a\` vs
1819
+ \`?sentient_persona=b\` to see it adapt \u2014 in keyless local mode any key drives the built-in
1820
+ heuristic. No API key needed. Against a real project the key must be one you declared.
1818
1821
  4. To learn from real traffic: create a project at https://sentient-ui.com and set
1819
1822
  NEXT_PUBLIC_SENTIENT_API_KEY=pk_... in .env.local.
1820
1823
 
@@ -1823,8 +1826,8 @@ time.
1823
1826
  Persona attributes on <html> (zero declaration; the attribute carries the project's own
1824
1827
  vocabulary keys \u2014 declared or inferred):
1825
1828
 
1826
- html[data-sentient-persona='deal_seeker'] .discount-banner { display: block; }
1827
- html[data-sentient-confidence='low'] .discount-banner { display: none; }
1829
+ html[data-sentient-persona='admin'] .admin-tools { display: block; }
1830
+ html[data-sentient-confidence='low'] .admin-tools { display: none; }
1828
1831
 
1829
1832
  Learned style tokens (element-scoped, SSR-safe):
1830
1833
 
@@ -1839,14 +1842,43 @@ prefers-reduced-motion: reduce override in CSS.
1839
1842
 
1840
1843
  ## Rung 2 \u2014 Swap (alternate content)
1841
1844
 
1845
+ Generated versions (start here) \u2014 wrap what the page shows today; the children are the
1846
+ original:
1847
+
1848
+ <Adaptive id="hero-cta" goal="signup_click">
1849
+ <a href="/signup">Start free trial</a>
1850
+ </Adaptive>
1851
+
1852
+ Mount + deploy registers the region. SentientUI writes versions per visitor type in the
1853
+ dashboard ("Who sees what") \u2014 no redeploy. The children render for holdout traffic, unknown
1854
+ visitor types, types with no version yet, and every error path. Mounted regions are requested
1855
+ in one batched call after mount: the first visit shows the original briefly, then swaps; return
1856
+ visits start from the last version. Keyless local mode renders originals only.
1857
+
1858
+ Optional server preload (Next.js App Router): registrySlotIds={['hero-cta']} for generated
1859
+ regions, components={[{ id, variantIds }]} for code variants, both on AdaptiveRoot \u2014 the first
1860
+ paint then shows the served version. List ONLY ids rendered on every page that AdaptiveRoot
1861
+ wraps (in app/layout.tsx, every route): a declared id is decided on every request, rendered or
1862
+ not, and those visits count as views that can never convert.
1863
+
1864
+ Pass
1865
+ onFormSubmit to allow form versions; set reportBaselineText={false} on regions wrapping
1866
+ personalized or account content. (Formerly <AdaptiveSlot>.)
1867
+
1868
+ Code variants \u2014 when you write the alternatives yourself:
1869
+
1870
+ <Adaptive id="buy-box" goal="buy_click" // goal REQUIRED
1871
+ variants={{ control: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }} /> // first key = baseline
1872
+
1873
+ // hook form:
1842
1874
  const { value, bind } = useAdaptive('buy-box', {
1843
1875
  variants: { calm: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }, // first key = baseline
1844
1876
  goal: 'buy_click', // REQUIRED
1845
1877
  });
1846
1878
  return <div {...bind}>{value}</div>;
1847
1879
 
1848
- Always attach bind \u2014 it wires exposure tracking and goal listeners. <Adaptive> is the wrapper
1849
- form; <AdaptiveText> swaps dashboard-managed text.
1880
+ Children or variants on one <Adaptive>, never both. With the hook, always attach bind \u2014 it
1881
+ wires exposure tracking and goal listeners. <AdaptiveText> swaps dashboard-managed text.
1850
1882
 
1851
1883
  ## Rung 3 \u2014 Reorder (structure)
1852
1884
 
@@ -1864,12 +1896,20 @@ their shared parent, and applies nothing if any selector drifts.
1864
1896
 
1865
1897
  ## No-code install order (snippet)
1866
1898
 
1867
- Three tags in <head>, in this order: the window.sentient config, the inline pre-paint script,
1868
- then the deferred loader. The middle tag is optional but recommended \u2014 the loader is deferred,
1869
- so on a return visit the page can paint its original state before the loader runs; the inline
1870
- script applies the last served decision from the visitor's own device first, with no network
1871
- call and no cloak. Get its exact bytes from the dashboard's Install page, or from
1872
- renderSnippetPrePaintScript() in '@sentientui/snippet/install'.
1899
+ Two tags in <head>, in this order: one inline <script> that assigns the window.sentient config
1900
+ and then, in the same tag after the ';', runs the inline pre-paint script; then the deferred
1901
+ loader. The pre-paint script reads window.sentient when it runs, so sharing the tag is safe. It
1902
+ is optional but recommended \u2014 the loader is deferred, so on a return visit the page can paint
1903
+ its original state before the loader runs; the inline script applies the last served decision
1904
+ from the visitor's own device first, with no network call and no cloak. Get the exact bytes from
1905
+ the dashboard's Install page, or from renderSnippetInstall({ config }) in
1906
+ '@sentientui/snippet/install' (renderSnippetPrePaintScript() for the script alone).
1907
+
1908
+ Strict Content-Security-Policy (script hashes): use the three-tag form instead \u2014 config, the
1909
+ pre-paint script alone, then the loader (renderSnippetInstall({ config, split: true })). The
1910
+ combined tag contains the site's config, so its hash differs per site; the split pre-paint tag is
1911
+ byte-identical everywhere, so one hash covers it. Existing three-tag and config + loader installs
1912
+ keep working.
1873
1913
 
1874
1914
  ## Testing the integration
1875
1915
 
@@ -2049,12 +2089,17 @@ var cellOutput = import_zod15.z.object({
2049
2089
  rationale: import_zod15.z.string().nullable(),
2050
2090
  error: import_zod15.z.string().nullable(),
2051
2091
  updatedAt: import_zod15.z.string(),
2092
+ // These ride the API response too; omitting them from the schema made zod
2093
+ // STRIP them, so agents could never see refinement or retry state.
2094
+ refinedFrom: import_zod15.z.string().nullable().optional().describe("Incumbent arm this cell is improving on \u2014 a pending cell with this set still SERVES the incumbent"),
2095
+ notBefore: import_zod15.z.string().nullable().optional().describe("ISO time the queue will next consider this cell (retry backoff / daily-cap deferral); null when due now"),
2096
+ attempts: import_zod15.z.number().optional().describe("Generation attempts so far"),
2052
2097
  performance: import_zod15.z.object({
2053
2098
  shown: import_zod15.z.number(),
2054
2099
  conversions: import_zod15.z.number(),
2055
2100
  deltaPct: import_zod15.z.number().nullable().describe("Signed % vs the original conversion rate"),
2056
2101
  tier: import_zod15.z.string().describe("early | no_baseline | winning | losing | flat")
2057
- }).optional().describe("Present on live cells: the at-a-glance readout")
2102
+ }).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
2058
2103
  });
2059
2104
  function registerCellTools(server, client) {
2060
2105
  server.registerTool(
@@ -2064,7 +2109,15 @@ function registerCellTools(server, client) {
2064
2109
  description: 'The "Who sees what" matrix: which visitor types (personas) have an AI-generated version live in each personalizable region (slot), with per-persona traffic share and the first-miss auto-fill setting. A cell with no row means that visitor type sees the original.' + UNTRUSTED_FIELDS_NOTE,
2065
2110
  inputSchema: { projectId: projectIdSchema },
2066
2111
  outputSchema: {
2067
- slots: import_zod15.z.array(import_zod15.z.object({ slotId: import_zod15.z.string(), displayName: import_zod15.z.string(), supportsForms: import_zod15.z.boolean() })),
2112
+ slots: import_zod15.z.array(
2113
+ import_zod15.z.object({
2114
+ slotId: import_zod15.z.string(),
2115
+ displayName: import_zod15.z.string(),
2116
+ supportsForms: import_zod15.z.boolean(),
2117
+ // API sends this; without it zod stripped the draft-vs-live signal.
2118
+ live: import_zod15.z.boolean().optional().describe("False for an sdk-registered draft column \u2014 nothing serves until its first generation publishes it")
2119
+ })
2120
+ ),
2068
2121
  personas: import_zod15.z.array(personaOutput),
2069
2122
  cells: import_zod15.z.array(cellOutput),
2070
2123
  autoFill: import_zod15.z.boolean(),
@@ -2078,13 +2131,19 @@ function registerCellTools(server, client) {
2078
2131
  const data = await client.get(`/projects/${id}/cells`);
2079
2132
  const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
2080
2133
  const lines = [
2081
- `Slots: ${data.slots.map((s) => untrusted(s.displayName)).join(", ") || "(none published)"}`,
2134
+ `Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
2082
2135
  `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
2083
2136
  ];
2084
2137
  for (const p of data.personas) {
2085
2138
  const states = data.slots.map((s) => {
2086
2139
  const c = byCell.get(`${s.slotId}::${p.key}`);
2087
- return `${untrusted(s.displayName, 40)}: ${c ? c.status : "original"}`;
2140
+ let state = c ? c.status : "original";
2141
+ if (c && (c.status === "pending" || c.status === "generating") && c.refinedFrom) {
2142
+ state = `${c.status} (improving on ${untrusted(c.refinedFrom, 64)} \u2014 current version still live)`;
2143
+ } else if (c && c.status === "pending" && c.notBefore && Date.parse(c.notBefore) > Date.now()) {
2144
+ state = `pending (waiting until ${c.notBefore})`;
2145
+ }
2146
+ return `${untrusted(s.displayName, 40)}: ${state}`;
2088
2147
  });
2089
2148
  lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
2090
2149
  }
@@ -2126,7 +2185,7 @@ function registerCellTools(server, client) {
2126
2185
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2127
2186
  },
2128
2187
  withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2129
- var _a2, _b;
2188
+ var _a2, _b, _c;
2130
2189
  const id = encodeURIComponent(projectId);
2131
2190
  const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
2132
2191
  const lines = [];
@@ -2138,7 +2197,12 @@ function registerCellTools(server, client) {
2138
2197
  lines.push("No generated version is live for this cell.");
2139
2198
  }
2140
2199
  if ((_a2 = data.cell) == null ? void 0 : _a2.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
2141
- if ((_b = data.cell) == null ? void 0 : _b.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
2200
+ if (((_b = data.cell) == null ? void 0 : _b.refinedFrom) && (data.cell.status === "pending" || data.cell.status === "generating")) {
2201
+ lines.push(
2202
+ `An improved take on ${untrusted(data.cell.refinedFrom, 64)} is being written \u2014 the current version keeps serving meanwhile.`
2203
+ );
2204
+ }
2205
+ if ((_c = data.cell) == null ? void 0 : _c.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
2142
2206
  lines.push(data.verdict);
2143
2207
  if (data.performance) {
2144
2208
  lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
@@ -2183,7 +2247,7 @@ function registerCellTools(server, client) {
2183
2247
  }
2184
2248
 
2185
2249
  // src/server.ts
2186
- var PKG_VERSION = true ? "0.18.0" : "0.0.0-dev";
2250
+ var PKG_VERSION = true ? "0.18.2" : "0.0.0-dev";
2187
2251
  function createMcpServer(client) {
2188
2252
  const server = new import_mcp.McpServer(
2189
2253
  {
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ApiClient,
4
4
  createMcpServer
5
- } from "./chunk-PX6RCSB2.js";
5
+ } from "./chunk-5TEQLWMW.js";
6
6
 
7
7
  // src/index.ts
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/dist/lib.cjs CHANGED
@@ -1819,8 +1819,9 @@ var import_zod13 = require("zod");
1819
1819
  var GUIDE = `# SentientUI integration guide \u2014 the adaptive ladder
1820
1820
 
1821
1821
  SentientUI adapts a site per visitor type (personas), learning from real conversions. Each
1822
- project has its own persona vocabulary (dashboard -> Settings -> Personas; the default is
1823
- buyer, researcher, deal_seeker, browser + unknown). Personas are inferred from behavior \u2014 or
1822
+ project has its own persona vocabulary (dashboard -> Settings -> Personas), and it starts
1823
+ EMPTY: every visitor is 'unknown' until a persona exists, and the optimizer learns across all
1824
+ traffic pooled in the meantime. Personas are DISCOVERED from behavior \u2014 or, far faster,
1824
1825
  DECLARED by the app when it already knows the visitor's role: init({ persona: 'admin' }) /
1825
1826
  <AdaptiveProvider persona=...> / window.sentient.persona. Declared beats inferred and serves
1826
1827
  at full confidence; unrecognized values are ignored server-side and surfaced in the dashboard
@@ -1833,12 +1834,14 @@ time.
1833
1834
  1. Run \`npx @sentientui/cli init\` (detects Next App/Pages, Vite, Remix, CRA; installs
1834
1835
  @sentientui/react; writes .env.local; scaffolds an example). It does NOT edit your layout \u2014
1835
1836
  it prints the wrap snippet for step 2.
1836
- 2. Wrap the root layout with <AdaptiveRoot apiKey context> (from '@sentientui/react/next';
1837
- other React apps use <AdaptiveProvider> from '@sentientui/react') and add
1838
- suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes pre-paint.
1839
- Nothing adapts and nothing is tracked until this wrap is in place.
1840
- 3. \`npm run dev\`, then open the app with \`?sentient_persona=buyer\` vs
1841
- \`?sentient_persona=deal_seeker\` to see it adapt. No API key needed (keyless local mode).
1837
+ 2. Wrap the root layout with <AdaptiveRoot apiKey>{children}</AdaptiveRoot> (from
1838
+ '@sentientui/react/next'; other React apps use <AdaptiveProvider> from '@sentientui/react')
1839
+ and add suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes
1840
+ pre-paint. Nothing adapts and nothing is tracked until this wrap is in place. Declare no
1841
+ components here: every <Adaptive> registers itself when it renders (see Rung 2).
1842
+ 3. \`npm run dev\`, then open the app with \`?sentient_persona=a\` vs
1843
+ \`?sentient_persona=b\` to see it adapt \u2014 in keyless local mode any key drives the built-in
1844
+ heuristic. No API key needed. Against a real project the key must be one you declared.
1842
1845
  4. To learn from real traffic: create a project at https://sentient-ui.com and set
1843
1846
  NEXT_PUBLIC_SENTIENT_API_KEY=pk_... in .env.local.
1844
1847
 
@@ -1847,8 +1850,8 @@ time.
1847
1850
  Persona attributes on <html> (zero declaration; the attribute carries the project's own
1848
1851
  vocabulary keys \u2014 declared or inferred):
1849
1852
 
1850
- html[data-sentient-persona='deal_seeker'] .discount-banner { display: block; }
1851
- html[data-sentient-confidence='low'] .discount-banner { display: none; }
1853
+ html[data-sentient-persona='admin'] .admin-tools { display: block; }
1854
+ html[data-sentient-confidence='low'] .admin-tools { display: none; }
1852
1855
 
1853
1856
  Learned style tokens (element-scoped, SSR-safe):
1854
1857
 
@@ -1863,14 +1866,43 @@ prefers-reduced-motion: reduce override in CSS.
1863
1866
 
1864
1867
  ## Rung 2 \u2014 Swap (alternate content)
1865
1868
 
1869
+ Generated versions (start here) \u2014 wrap what the page shows today; the children are the
1870
+ original:
1871
+
1872
+ <Adaptive id="hero-cta" goal="signup_click">
1873
+ <a href="/signup">Start free trial</a>
1874
+ </Adaptive>
1875
+
1876
+ Mount + deploy registers the region. SentientUI writes versions per visitor type in the
1877
+ dashboard ("Who sees what") \u2014 no redeploy. The children render for holdout traffic, unknown
1878
+ visitor types, types with no version yet, and every error path. Mounted regions are requested
1879
+ in one batched call after mount: the first visit shows the original briefly, then swaps; return
1880
+ visits start from the last version. Keyless local mode renders originals only.
1881
+
1882
+ Optional server preload (Next.js App Router): registrySlotIds={['hero-cta']} for generated
1883
+ regions, components={[{ id, variantIds }]} for code variants, both on AdaptiveRoot \u2014 the first
1884
+ paint then shows the served version. List ONLY ids rendered on every page that AdaptiveRoot
1885
+ wraps (in app/layout.tsx, every route): a declared id is decided on every request, rendered or
1886
+ not, and those visits count as views that can never convert.
1887
+
1888
+ Pass
1889
+ onFormSubmit to allow form versions; set reportBaselineText={false} on regions wrapping
1890
+ personalized or account content. (Formerly <AdaptiveSlot>.)
1891
+
1892
+ Code variants \u2014 when you write the alternatives yourself:
1893
+
1894
+ <Adaptive id="buy-box" goal="buy_click" // goal REQUIRED
1895
+ variants={{ control: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }} /> // first key = baseline
1896
+
1897
+ // hook form:
1866
1898
  const { value, bind } = useAdaptive('buy-box', {
1867
1899
  variants: { calm: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }, // first key = baseline
1868
1900
  goal: 'buy_click', // REQUIRED
1869
1901
  });
1870
1902
  return <div {...bind}>{value}</div>;
1871
1903
 
1872
- Always attach bind \u2014 it wires exposure tracking and goal listeners. <Adaptive> is the wrapper
1873
- form; <AdaptiveText> swaps dashboard-managed text.
1904
+ Children or variants on one <Adaptive>, never both. With the hook, always attach bind \u2014 it
1905
+ wires exposure tracking and goal listeners. <AdaptiveText> swaps dashboard-managed text.
1874
1906
 
1875
1907
  ## Rung 3 \u2014 Reorder (structure)
1876
1908
 
@@ -1888,12 +1920,20 @@ their shared parent, and applies nothing if any selector drifts.
1888
1920
 
1889
1921
  ## No-code install order (snippet)
1890
1922
 
1891
- Three tags in <head>, in this order: the window.sentient config, the inline pre-paint script,
1892
- then the deferred loader. The middle tag is optional but recommended \u2014 the loader is deferred,
1893
- so on a return visit the page can paint its original state before the loader runs; the inline
1894
- script applies the last served decision from the visitor's own device first, with no network
1895
- call and no cloak. Get its exact bytes from the dashboard's Install page, or from
1896
- renderSnippetPrePaintScript() in '@sentientui/snippet/install'.
1923
+ Two tags in <head>, in this order: one inline <script> that assigns the window.sentient config
1924
+ and then, in the same tag after the ';', runs the inline pre-paint script; then the deferred
1925
+ loader. The pre-paint script reads window.sentient when it runs, so sharing the tag is safe. It
1926
+ is optional but recommended \u2014 the loader is deferred, so on a return visit the page can paint
1927
+ its original state before the loader runs; the inline script applies the last served decision
1928
+ from the visitor's own device first, with no network call and no cloak. Get the exact bytes from
1929
+ the dashboard's Install page, or from renderSnippetInstall({ config }) in
1930
+ '@sentientui/snippet/install' (renderSnippetPrePaintScript() for the script alone).
1931
+
1932
+ Strict Content-Security-Policy (script hashes): use the three-tag form instead \u2014 config, the
1933
+ pre-paint script alone, then the loader (renderSnippetInstall({ config, split: true })). The
1934
+ combined tag contains the site's config, so its hash differs per site; the split pre-paint tag is
1935
+ byte-identical everywhere, so one hash covers it. Existing three-tag and config + loader installs
1936
+ keep working.
1897
1937
 
1898
1938
  ## Testing the integration
1899
1939
 
@@ -2073,12 +2113,17 @@ var cellOutput = import_zod15.z.object({
2073
2113
  rationale: import_zod15.z.string().nullable(),
2074
2114
  error: import_zod15.z.string().nullable(),
2075
2115
  updatedAt: import_zod15.z.string(),
2116
+ // These ride the API response too; omitting them from the schema made zod
2117
+ // STRIP them, so agents could never see refinement or retry state.
2118
+ refinedFrom: import_zod15.z.string().nullable().optional().describe("Incumbent arm this cell is improving on \u2014 a pending cell with this set still SERVES the incumbent"),
2119
+ notBefore: import_zod15.z.string().nullable().optional().describe("ISO time the queue will next consider this cell (retry backoff / daily-cap deferral); null when due now"),
2120
+ attempts: import_zod15.z.number().optional().describe("Generation attempts so far"),
2076
2121
  performance: import_zod15.z.object({
2077
2122
  shown: import_zod15.z.number(),
2078
2123
  conversions: import_zod15.z.number(),
2079
2124
  deltaPct: import_zod15.z.number().nullable().describe("Signed % vs the original conversion rate"),
2080
2125
  tier: import_zod15.z.string().describe("early | no_baseline | winning | losing | flat")
2081
- }).optional().describe("Present on live cells: the at-a-glance readout")
2126
+ }).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
2082
2127
  });
2083
2128
  function registerCellTools(server, client) {
2084
2129
  server.registerTool(
@@ -2088,7 +2133,15 @@ function registerCellTools(server, client) {
2088
2133
  description: 'The "Who sees what" matrix: which visitor types (personas) have an AI-generated version live in each personalizable region (slot), with per-persona traffic share and the first-miss auto-fill setting. A cell with no row means that visitor type sees the original.' + UNTRUSTED_FIELDS_NOTE,
2089
2134
  inputSchema: { projectId: projectIdSchema },
2090
2135
  outputSchema: {
2091
- slots: import_zod15.z.array(import_zod15.z.object({ slotId: import_zod15.z.string(), displayName: import_zod15.z.string(), supportsForms: import_zod15.z.boolean() })),
2136
+ slots: import_zod15.z.array(
2137
+ import_zod15.z.object({
2138
+ slotId: import_zod15.z.string(),
2139
+ displayName: import_zod15.z.string(),
2140
+ supportsForms: import_zod15.z.boolean(),
2141
+ // API sends this; without it zod stripped the draft-vs-live signal.
2142
+ live: import_zod15.z.boolean().optional().describe("False for an sdk-registered draft column \u2014 nothing serves until its first generation publishes it")
2143
+ })
2144
+ ),
2092
2145
  personas: import_zod15.z.array(personaOutput),
2093
2146
  cells: import_zod15.z.array(cellOutput),
2094
2147
  autoFill: import_zod15.z.boolean(),
@@ -2102,13 +2155,19 @@ function registerCellTools(server, client) {
2102
2155
  const data = await client.get(`/projects/${id}/cells`);
2103
2156
  const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
2104
2157
  const lines = [
2105
- `Slots: ${data.slots.map((s) => untrusted(s.displayName)).join(", ") || "(none published)"}`,
2158
+ `Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
2106
2159
  `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
2107
2160
  ];
2108
2161
  for (const p of data.personas) {
2109
2162
  const states = data.slots.map((s) => {
2110
2163
  const c = byCell.get(`${s.slotId}::${p.key}`);
2111
- return `${untrusted(s.displayName, 40)}: ${c ? c.status : "original"}`;
2164
+ let state = c ? c.status : "original";
2165
+ if (c && (c.status === "pending" || c.status === "generating") && c.refinedFrom) {
2166
+ state = `${c.status} (improving on ${untrusted(c.refinedFrom, 64)} \u2014 current version still live)`;
2167
+ } else if (c && c.status === "pending" && c.notBefore && Date.parse(c.notBefore) > Date.now()) {
2168
+ state = `pending (waiting until ${c.notBefore})`;
2169
+ }
2170
+ return `${untrusted(s.displayName, 40)}: ${state}`;
2112
2171
  });
2113
2172
  lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
2114
2173
  }
@@ -2150,7 +2209,7 @@ function registerCellTools(server, client) {
2150
2209
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2151
2210
  },
2152
2211
  withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2153
- var _a, _b;
2212
+ var _a, _b, _c;
2154
2213
  const id = encodeURIComponent(projectId);
2155
2214
  const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
2156
2215
  const lines = [];
@@ -2162,7 +2221,12 @@ function registerCellTools(server, client) {
2162
2221
  lines.push("No generated version is live for this cell.");
2163
2222
  }
2164
2223
  if ((_a = data.cell) == null ? void 0 : _a.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
2165
- if ((_b = data.cell) == null ? void 0 : _b.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
2224
+ if (((_b = data.cell) == null ? void 0 : _b.refinedFrom) && (data.cell.status === "pending" || data.cell.status === "generating")) {
2225
+ lines.push(
2226
+ `An improved take on ${untrusted(data.cell.refinedFrom, 64)} is being written \u2014 the current version keeps serving meanwhile.`
2227
+ );
2228
+ }
2229
+ if ((_c = data.cell) == null ? void 0 : _c.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
2166
2230
  lines.push(data.verdict);
2167
2231
  if (data.performance) {
2168
2232
  lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
@@ -2207,7 +2271,7 @@ function registerCellTools(server, client) {
2207
2271
  }
2208
2272
 
2209
2273
  // src/server.ts
2210
- var PKG_VERSION = true ? "0.18.0" : "0.0.0-dev";
2274
+ var PKG_VERSION = true ? "0.18.2" : "0.0.0-dev";
2211
2275
  function createMcpServer(client) {
2212
2276
  const server = new import_mcp.McpServer(
2213
2277
  {
package/dist/lib.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  ApiError,
5
5
  PUBLIC_UI_RESOURCE_URIS,
6
6
  createMcpServer
7
- } from "./chunk-PX6RCSB2.js";
7
+ } from "./chunk-5TEQLWMW.js";
8
8
  export {
9
9
  ApiClient,
10
10
  ApiError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentientui/mcp",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "MCP server for SentientUI — exposes project data and actions to AI agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentient-ui.com",