@sentientui/mcp 0.18.0 → 0.18.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.
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
@@ -1807,8 +1808,9 @@ time.
1807
1808
  other React apps use <AdaptiveProvider> from '@sentientui/react') and add
1808
1809
  suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes pre-paint.
1809
1810
  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).
1811
+ 3. \`npm run dev\`, then open the app with \`?sentient_persona=a\` vs
1812
+ \`?sentient_persona=b\` to see it adapt \u2014 in keyless local mode any key drives the built-in
1813
+ heuristic. No API key needed. Against a real project the key must be one you declared.
1812
1814
  4. To learn from real traffic: create a project at https://sentient-ui.com and set
1813
1815
  NEXT_PUBLIC_SENTIENT_API_KEY=pk_... in .env.local.
1814
1816
 
@@ -1817,8 +1819,8 @@ time.
1817
1819
  Persona attributes on <html> (zero declaration; the attribute carries the project's own
1818
1820
  vocabulary keys \u2014 declared or inferred):
1819
1821
 
1820
- html[data-sentient-persona='deal_seeker'] .discount-banner { display: block; }
1821
- html[data-sentient-confidence='low'] .discount-banner { display: none; }
1822
+ html[data-sentient-persona='admin'] .admin-tools { display: block; }
1823
+ html[data-sentient-confidence='low'] .admin-tools { display: none; }
1822
1824
 
1823
1825
  Learned style tokens (element-scoped, SSR-safe):
1824
1826
 
@@ -1833,14 +1835,35 @@ prefers-reduced-motion: reduce override in CSS.
1833
1835
 
1834
1836
  ## Rung 2 \u2014 Swap (alternate content)
1835
1837
 
1838
+ Generated versions (start here) \u2014 wrap what the page shows today; the children are the
1839
+ original:
1840
+
1841
+ <Adaptive id="hero-cta" goal="signup_click">
1842
+ <a href="/signup">Start free trial</a>
1843
+ </Adaptive>
1844
+
1845
+ Mount + deploy registers the region. SentientUI writes versions per visitor type in the
1846
+ dashboard ("Who sees what") \u2014 no redeploy. The children render for holdout traffic, unknown
1847
+ visitor types, types with no version yet, and every error path. No server preload: mounted
1848
+ regions are requested in one batched call. First visit shows the original briefly, then swaps;
1849
+ return visits start from the last version. Keyless local mode renders originals only. Pass
1850
+ onFormSubmit to allow form versions; set reportBaselineText={false} on regions wrapping
1851
+ personalized or account content. (Formerly <AdaptiveSlot>.)
1852
+
1853
+ Code variants \u2014 when you write the alternatives yourself:
1854
+
1855
+ <Adaptive id="buy-box" goal="buy_click" // goal REQUIRED
1856
+ variants={{ control: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }} /> // first key = baseline
1857
+
1858
+ // hook form:
1836
1859
  const { value, bind } = useAdaptive('buy-box', {
1837
1860
  variants: { calm: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }, // first key = baseline
1838
1861
  goal: 'buy_click', // REQUIRED
1839
1862
  });
1840
1863
  return <div {...bind}>{value}</div>;
1841
1864
 
1842
- Always attach bind \u2014 it wires exposure tracking and goal listeners. <Adaptive> is the wrapper
1843
- form; <AdaptiveText> swaps dashboard-managed text.
1865
+ Children or variants on one <Adaptive>, never both. With the hook, always attach bind \u2014 it
1866
+ wires exposure tracking and goal listeners. <AdaptiveText> swaps dashboard-managed text.
1844
1867
 
1845
1868
  ## Rung 3 \u2014 Reorder (structure)
1846
1869
 
@@ -1858,12 +1881,20 @@ their shared parent, and applies nothing if any selector drifts.
1858
1881
 
1859
1882
  ## No-code install order (snippet)
1860
1883
 
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'.
1884
+ Two tags in <head>, in this order: one inline <script> that assigns the window.sentient config
1885
+ and then, in the same tag after the ';', runs the inline pre-paint script; then the deferred
1886
+ loader. The pre-paint script reads window.sentient when it runs, so sharing the tag is safe. It
1887
+ is optional but recommended \u2014 the loader is deferred, so on a return visit the page can paint
1888
+ its original state before the loader runs; the inline script applies the last served decision
1889
+ from the visitor's own device first, with no network call and no cloak. Get the exact bytes from
1890
+ the dashboard's Install page, or from renderSnippetInstall({ config }) in
1891
+ '@sentientui/snippet/install' (renderSnippetPrePaintScript() for the script alone).
1892
+
1893
+ Strict Content-Security-Policy (script hashes): use the three-tag form instead \u2014 config, the
1894
+ pre-paint script alone, then the loader (renderSnippetInstall({ config, split: true })). The
1895
+ combined tag contains the site's config, so its hash differs per site; the split pre-paint tag is
1896
+ byte-identical everywhere, so one hash covers it. Existing three-tag and config + loader installs
1897
+ keep working.
1867
1898
 
1868
1899
  ## Testing the integration
1869
1900
 
@@ -2043,12 +2074,17 @@ var cellOutput = z15.object({
2043
2074
  rationale: z15.string().nullable(),
2044
2075
  error: z15.string().nullable(),
2045
2076
  updatedAt: z15.string(),
2077
+ // These ride the API response too; omitting them from the schema made zod
2078
+ // STRIP them, so agents could never see refinement or retry state.
2079
+ refinedFrom: z15.string().nullable().optional().describe("Incumbent arm this cell is improving on \u2014 a pending cell with this set still SERVES the incumbent"),
2080
+ notBefore: z15.string().nullable().optional().describe("ISO time the queue will next consider this cell (retry backoff / daily-cap deferral); null when due now"),
2081
+ attempts: z15.number().optional().describe("Generation attempts so far"),
2046
2082
  performance: z15.object({
2047
2083
  shown: z15.number(),
2048
2084
  conversions: z15.number(),
2049
2085
  deltaPct: z15.number().nullable().describe("Signed % vs the original conversion rate"),
2050
2086
  tier: z15.string().describe("early | no_baseline | winning | losing | flat")
2051
- }).optional().describe("Present on live cells: the at-a-glance readout")
2087
+ }).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
2052
2088
  });
2053
2089
  function registerCellTools(server, client) {
2054
2090
  server.registerTool(
@@ -2058,7 +2094,15 @@ function registerCellTools(server, client) {
2058
2094
  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
2095
  inputSchema: { projectId: projectIdSchema },
2060
2096
  outputSchema: {
2061
- slots: z15.array(z15.object({ slotId: z15.string(), displayName: z15.string(), supportsForms: z15.boolean() })),
2097
+ slots: z15.array(
2098
+ z15.object({
2099
+ slotId: z15.string(),
2100
+ displayName: z15.string(),
2101
+ supportsForms: z15.boolean(),
2102
+ // API sends this; without it zod stripped the draft-vs-live signal.
2103
+ live: z15.boolean().optional().describe("False for an sdk-registered draft column \u2014 nothing serves until its first generation publishes it")
2104
+ })
2105
+ ),
2062
2106
  personas: z15.array(personaOutput),
2063
2107
  cells: z15.array(cellOutput),
2064
2108
  autoFill: z15.boolean(),
@@ -2072,13 +2116,19 @@ function registerCellTools(server, client) {
2072
2116
  const data = await client.get(`/projects/${id}/cells`);
2073
2117
  const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
2074
2118
  const lines = [
2075
- `Slots: ${data.slots.map((s) => untrusted(s.displayName)).join(", ") || "(none published)"}`,
2119
+ `Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
2076
2120
  `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
2077
2121
  ];
2078
2122
  for (const p of data.personas) {
2079
2123
  const states = data.slots.map((s) => {
2080
2124
  const c = byCell.get(`${s.slotId}::${p.key}`);
2081
- return `${untrusted(s.displayName, 40)}: ${c ? c.status : "original"}`;
2125
+ let state = c ? c.status : "original";
2126
+ if (c && (c.status === "pending" || c.status === "generating") && c.refinedFrom) {
2127
+ state = `${c.status} (improving on ${untrusted(c.refinedFrom, 64)} \u2014 current version still live)`;
2128
+ } else if (c && c.status === "pending" && c.notBefore && Date.parse(c.notBefore) > Date.now()) {
2129
+ state = `pending (waiting until ${c.notBefore})`;
2130
+ }
2131
+ return `${untrusted(s.displayName, 40)}: ${state}`;
2082
2132
  });
2083
2133
  lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
2084
2134
  }
@@ -2120,7 +2170,7 @@ function registerCellTools(server, client) {
2120
2170
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2121
2171
  },
2122
2172
  withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2123
- var _a, _b;
2173
+ var _a, _b, _c;
2124
2174
  const id = encodeURIComponent(projectId);
2125
2175
  const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
2126
2176
  const lines = [];
@@ -2132,7 +2182,12 @@ function registerCellTools(server, client) {
2132
2182
  lines.push("No generated version is live for this cell.");
2133
2183
  }
2134
2184
  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)}`);
2185
+ if (((_b = data.cell) == null ? void 0 : _b.refinedFrom) && (data.cell.status === "pending" || data.cell.status === "generating")) {
2186
+ lines.push(
2187
+ `An improved take on ${untrusted(data.cell.refinedFrom, 64)} is being written \u2014 the current version keeps serving meanwhile.`
2188
+ );
2189
+ }
2190
+ if ((_c = data.cell) == null ? void 0 : _c.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
2136
2191
  lines.push(data.verdict);
2137
2192
  if (data.performance) {
2138
2193
  lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
@@ -2177,7 +2232,7 @@ function registerCellTools(server, client) {
2177
2232
  }
2178
2233
 
2179
2234
  // src/server.ts
2180
- var PKG_VERSION = true ? "0.18.0" : "0.0.0-dev";
2235
+ var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
2181
2236
  function createMcpServer(client) {
2182
2237
  const server = new McpServer(
2183
2238
  {
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
@@ -1813,8 +1814,9 @@ time.
1813
1814
  other React apps use <AdaptiveProvider> from '@sentientui/react') and add
1814
1815
  suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes pre-paint.
1815
1816
  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).
1817
+ 3. \`npm run dev\`, then open the app with \`?sentient_persona=a\` vs
1818
+ \`?sentient_persona=b\` to see it adapt \u2014 in keyless local mode any key drives the built-in
1819
+ heuristic. No API key needed. Against a real project the key must be one you declared.
1818
1820
  4. To learn from real traffic: create a project at https://sentient-ui.com and set
1819
1821
  NEXT_PUBLIC_SENTIENT_API_KEY=pk_... in .env.local.
1820
1822
 
@@ -1823,8 +1825,8 @@ time.
1823
1825
  Persona attributes on <html> (zero declaration; the attribute carries the project's own
1824
1826
  vocabulary keys \u2014 declared or inferred):
1825
1827
 
1826
- html[data-sentient-persona='deal_seeker'] .discount-banner { display: block; }
1827
- html[data-sentient-confidence='low'] .discount-banner { display: none; }
1828
+ html[data-sentient-persona='admin'] .admin-tools { display: block; }
1829
+ html[data-sentient-confidence='low'] .admin-tools { display: none; }
1828
1830
 
1829
1831
  Learned style tokens (element-scoped, SSR-safe):
1830
1832
 
@@ -1839,14 +1841,35 @@ prefers-reduced-motion: reduce override in CSS.
1839
1841
 
1840
1842
  ## Rung 2 \u2014 Swap (alternate content)
1841
1843
 
1844
+ Generated versions (start here) \u2014 wrap what the page shows today; the children are the
1845
+ original:
1846
+
1847
+ <Adaptive id="hero-cta" goal="signup_click">
1848
+ <a href="/signup">Start free trial</a>
1849
+ </Adaptive>
1850
+
1851
+ Mount + deploy registers the region. SentientUI writes versions per visitor type in the
1852
+ dashboard ("Who sees what") \u2014 no redeploy. The children render for holdout traffic, unknown
1853
+ visitor types, types with no version yet, and every error path. No server preload: mounted
1854
+ regions are requested in one batched call. First visit shows the original briefly, then swaps;
1855
+ return visits start from the last version. Keyless local mode renders originals only. Pass
1856
+ onFormSubmit to allow form versions; set reportBaselineText={false} on regions wrapping
1857
+ personalized or account content. (Formerly <AdaptiveSlot>.)
1858
+
1859
+ Code variants \u2014 when you write the alternatives yourself:
1860
+
1861
+ <Adaptive id="buy-box" goal="buy_click" // goal REQUIRED
1862
+ variants={{ control: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }} /> // first key = baseline
1863
+
1864
+ // hook form:
1842
1865
  const { value, bind } = useAdaptive('buy-box', {
1843
1866
  variants: { calm: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }, // first key = baseline
1844
1867
  goal: 'buy_click', // REQUIRED
1845
1868
  });
1846
1869
  return <div {...bind}>{value}</div>;
1847
1870
 
1848
- Always attach bind \u2014 it wires exposure tracking and goal listeners. <Adaptive> is the wrapper
1849
- form; <AdaptiveText> swaps dashboard-managed text.
1871
+ Children or variants on one <Adaptive>, never both. With the hook, always attach bind \u2014 it
1872
+ wires exposure tracking and goal listeners. <AdaptiveText> swaps dashboard-managed text.
1850
1873
 
1851
1874
  ## Rung 3 \u2014 Reorder (structure)
1852
1875
 
@@ -1864,12 +1887,20 @@ their shared parent, and applies nothing if any selector drifts.
1864
1887
 
1865
1888
  ## No-code install order (snippet)
1866
1889
 
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'.
1890
+ Two tags in <head>, in this order: one inline <script> that assigns the window.sentient config
1891
+ and then, in the same tag after the ';', runs the inline pre-paint script; then the deferred
1892
+ loader. The pre-paint script reads window.sentient when it runs, so sharing the tag is safe. It
1893
+ is optional but recommended \u2014 the loader is deferred, so on a return visit the page can paint
1894
+ its original state before the loader runs; the inline script applies the last served decision
1895
+ from the visitor's own device first, with no network call and no cloak. Get the exact bytes from
1896
+ the dashboard's Install page, or from renderSnippetInstall({ config }) in
1897
+ '@sentientui/snippet/install' (renderSnippetPrePaintScript() for the script alone).
1898
+
1899
+ Strict Content-Security-Policy (script hashes): use the three-tag form instead \u2014 config, the
1900
+ pre-paint script alone, then the loader (renderSnippetInstall({ config, split: true })). The
1901
+ combined tag contains the site's config, so its hash differs per site; the split pre-paint tag is
1902
+ byte-identical everywhere, so one hash covers it. Existing three-tag and config + loader installs
1903
+ keep working.
1873
1904
 
1874
1905
  ## Testing the integration
1875
1906
 
@@ -2049,12 +2080,17 @@ var cellOutput = import_zod15.z.object({
2049
2080
  rationale: import_zod15.z.string().nullable(),
2050
2081
  error: import_zod15.z.string().nullable(),
2051
2082
  updatedAt: import_zod15.z.string(),
2083
+ // These ride the API response too; omitting them from the schema made zod
2084
+ // STRIP them, so agents could never see refinement or retry state.
2085
+ 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"),
2086
+ 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"),
2087
+ attempts: import_zod15.z.number().optional().describe("Generation attempts so far"),
2052
2088
  performance: import_zod15.z.object({
2053
2089
  shown: import_zod15.z.number(),
2054
2090
  conversions: import_zod15.z.number(),
2055
2091
  deltaPct: import_zod15.z.number().nullable().describe("Signed % vs the original conversion rate"),
2056
2092
  tier: import_zod15.z.string().describe("early | no_baseline | winning | losing | flat")
2057
- }).optional().describe("Present on live cells: the at-a-glance readout")
2093
+ }).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
2058
2094
  });
2059
2095
  function registerCellTools(server, client) {
2060
2096
  server.registerTool(
@@ -2064,7 +2100,15 @@ function registerCellTools(server, client) {
2064
2100
  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
2101
  inputSchema: { projectId: projectIdSchema },
2066
2102
  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() })),
2103
+ slots: import_zod15.z.array(
2104
+ import_zod15.z.object({
2105
+ slotId: import_zod15.z.string(),
2106
+ displayName: import_zod15.z.string(),
2107
+ supportsForms: import_zod15.z.boolean(),
2108
+ // API sends this; without it zod stripped the draft-vs-live signal.
2109
+ live: import_zod15.z.boolean().optional().describe("False for an sdk-registered draft column \u2014 nothing serves until its first generation publishes it")
2110
+ })
2111
+ ),
2068
2112
  personas: import_zod15.z.array(personaOutput),
2069
2113
  cells: import_zod15.z.array(cellOutput),
2070
2114
  autoFill: import_zod15.z.boolean(),
@@ -2078,13 +2122,19 @@ function registerCellTools(server, client) {
2078
2122
  const data = await client.get(`/projects/${id}/cells`);
2079
2123
  const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
2080
2124
  const lines = [
2081
- `Slots: ${data.slots.map((s) => untrusted(s.displayName)).join(", ") || "(none published)"}`,
2125
+ `Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
2082
2126
  `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
2083
2127
  ];
2084
2128
  for (const p of data.personas) {
2085
2129
  const states = data.slots.map((s) => {
2086
2130
  const c = byCell.get(`${s.slotId}::${p.key}`);
2087
- return `${untrusted(s.displayName, 40)}: ${c ? c.status : "original"}`;
2131
+ let state = c ? c.status : "original";
2132
+ if (c && (c.status === "pending" || c.status === "generating") && c.refinedFrom) {
2133
+ state = `${c.status} (improving on ${untrusted(c.refinedFrom, 64)} \u2014 current version still live)`;
2134
+ } else if (c && c.status === "pending" && c.notBefore && Date.parse(c.notBefore) > Date.now()) {
2135
+ state = `pending (waiting until ${c.notBefore})`;
2136
+ }
2137
+ return `${untrusted(s.displayName, 40)}: ${state}`;
2088
2138
  });
2089
2139
  lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
2090
2140
  }
@@ -2126,7 +2176,7 @@ function registerCellTools(server, client) {
2126
2176
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2127
2177
  },
2128
2178
  withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2129
- var _a2, _b;
2179
+ var _a2, _b, _c;
2130
2180
  const id = encodeURIComponent(projectId);
2131
2181
  const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
2132
2182
  const lines = [];
@@ -2138,7 +2188,12 @@ function registerCellTools(server, client) {
2138
2188
  lines.push("No generated version is live for this cell.");
2139
2189
  }
2140
2190
  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)}`);
2191
+ if (((_b = data.cell) == null ? void 0 : _b.refinedFrom) && (data.cell.status === "pending" || data.cell.status === "generating")) {
2192
+ lines.push(
2193
+ `An improved take on ${untrusted(data.cell.refinedFrom, 64)} is being written \u2014 the current version keeps serving meanwhile.`
2194
+ );
2195
+ }
2196
+ if ((_c = data.cell) == null ? void 0 : _c.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
2142
2197
  lines.push(data.verdict);
2143
2198
  if (data.performance) {
2144
2199
  lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
@@ -2183,7 +2238,7 @@ function registerCellTools(server, client) {
2183
2238
  }
2184
2239
 
2185
2240
  // src/server.ts
2186
- var PKG_VERSION = true ? "0.18.0" : "0.0.0-dev";
2241
+ var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
2187
2242
  function createMcpServer(client) {
2188
2243
  const server = new import_mcp.McpServer(
2189
2244
  {
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-HEMPCGWX.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
@@ -1837,8 +1838,9 @@ time.
1837
1838
  other React apps use <AdaptiveProvider> from '@sentientui/react') and add
1838
1839
  suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes pre-paint.
1839
1840
  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).
1841
+ 3. \`npm run dev\`, then open the app with \`?sentient_persona=a\` vs
1842
+ \`?sentient_persona=b\` to see it adapt \u2014 in keyless local mode any key drives the built-in
1843
+ heuristic. No API key needed. Against a real project the key must be one you declared.
1842
1844
  4. To learn from real traffic: create a project at https://sentient-ui.com and set
1843
1845
  NEXT_PUBLIC_SENTIENT_API_KEY=pk_... in .env.local.
1844
1846
 
@@ -1847,8 +1849,8 @@ time.
1847
1849
  Persona attributes on <html> (zero declaration; the attribute carries the project's own
1848
1850
  vocabulary keys \u2014 declared or inferred):
1849
1851
 
1850
- html[data-sentient-persona='deal_seeker'] .discount-banner { display: block; }
1851
- html[data-sentient-confidence='low'] .discount-banner { display: none; }
1852
+ html[data-sentient-persona='admin'] .admin-tools { display: block; }
1853
+ html[data-sentient-confidence='low'] .admin-tools { display: none; }
1852
1854
 
1853
1855
  Learned style tokens (element-scoped, SSR-safe):
1854
1856
 
@@ -1863,14 +1865,35 @@ prefers-reduced-motion: reduce override in CSS.
1863
1865
 
1864
1866
  ## Rung 2 \u2014 Swap (alternate content)
1865
1867
 
1868
+ Generated versions (start here) \u2014 wrap what the page shows today; the children are the
1869
+ original:
1870
+
1871
+ <Adaptive id="hero-cta" goal="signup_click">
1872
+ <a href="/signup">Start free trial</a>
1873
+ </Adaptive>
1874
+
1875
+ Mount + deploy registers the region. SentientUI writes versions per visitor type in the
1876
+ dashboard ("Who sees what") \u2014 no redeploy. The children render for holdout traffic, unknown
1877
+ visitor types, types with no version yet, and every error path. No server preload: mounted
1878
+ regions are requested in one batched call. First visit shows the original briefly, then swaps;
1879
+ return visits start from the last version. Keyless local mode renders originals only. Pass
1880
+ onFormSubmit to allow form versions; set reportBaselineText={false} on regions wrapping
1881
+ personalized or account content. (Formerly <AdaptiveSlot>.)
1882
+
1883
+ Code variants \u2014 when you write the alternatives yourself:
1884
+
1885
+ <Adaptive id="buy-box" goal="buy_click" // goal REQUIRED
1886
+ variants={{ control: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }} /> // first key = baseline
1887
+
1888
+ // hook form:
1866
1889
  const { value, bind } = useAdaptive('buy-box', {
1867
1890
  variants: { calm: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }, // first key = baseline
1868
1891
  goal: 'buy_click', // REQUIRED
1869
1892
  });
1870
1893
  return <div {...bind}>{value}</div>;
1871
1894
 
1872
- Always attach bind \u2014 it wires exposure tracking and goal listeners. <Adaptive> is the wrapper
1873
- form; <AdaptiveText> swaps dashboard-managed text.
1895
+ Children or variants on one <Adaptive>, never both. With the hook, always attach bind \u2014 it
1896
+ wires exposure tracking and goal listeners. <AdaptiveText> swaps dashboard-managed text.
1874
1897
 
1875
1898
  ## Rung 3 \u2014 Reorder (structure)
1876
1899
 
@@ -1888,12 +1911,20 @@ their shared parent, and applies nothing if any selector drifts.
1888
1911
 
1889
1912
  ## No-code install order (snippet)
1890
1913
 
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'.
1914
+ Two tags in <head>, in this order: one inline <script> that assigns the window.sentient config
1915
+ and then, in the same tag after the ';', runs the inline pre-paint script; then the deferred
1916
+ loader. The pre-paint script reads window.sentient when it runs, so sharing the tag is safe. It
1917
+ is optional but recommended \u2014 the loader is deferred, so on a return visit the page can paint
1918
+ its original state before the loader runs; the inline script applies the last served decision
1919
+ from the visitor's own device first, with no network call and no cloak. Get the exact bytes from
1920
+ the dashboard's Install page, or from renderSnippetInstall({ config }) in
1921
+ '@sentientui/snippet/install' (renderSnippetPrePaintScript() for the script alone).
1922
+
1923
+ Strict Content-Security-Policy (script hashes): use the three-tag form instead \u2014 config, the
1924
+ pre-paint script alone, then the loader (renderSnippetInstall({ config, split: true })). The
1925
+ combined tag contains the site's config, so its hash differs per site; the split pre-paint tag is
1926
+ byte-identical everywhere, so one hash covers it. Existing three-tag and config + loader installs
1927
+ keep working.
1897
1928
 
1898
1929
  ## Testing the integration
1899
1930
 
@@ -2073,12 +2104,17 @@ var cellOutput = import_zod15.z.object({
2073
2104
  rationale: import_zod15.z.string().nullable(),
2074
2105
  error: import_zod15.z.string().nullable(),
2075
2106
  updatedAt: import_zod15.z.string(),
2107
+ // These ride the API response too; omitting them from the schema made zod
2108
+ // STRIP them, so agents could never see refinement or retry state.
2109
+ 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"),
2110
+ 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"),
2111
+ attempts: import_zod15.z.number().optional().describe("Generation attempts so far"),
2076
2112
  performance: import_zod15.z.object({
2077
2113
  shown: import_zod15.z.number(),
2078
2114
  conversions: import_zod15.z.number(),
2079
2115
  deltaPct: import_zod15.z.number().nullable().describe("Signed % vs the original conversion rate"),
2080
2116
  tier: import_zod15.z.string().describe("early | no_baseline | winning | losing | flat")
2081
- }).optional().describe("Present on live cells: the at-a-glance readout")
2117
+ }).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
2082
2118
  });
2083
2119
  function registerCellTools(server, client) {
2084
2120
  server.registerTool(
@@ -2088,7 +2124,15 @@ function registerCellTools(server, client) {
2088
2124
  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
2125
  inputSchema: { projectId: projectIdSchema },
2090
2126
  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() })),
2127
+ slots: import_zod15.z.array(
2128
+ import_zod15.z.object({
2129
+ slotId: import_zod15.z.string(),
2130
+ displayName: import_zod15.z.string(),
2131
+ supportsForms: import_zod15.z.boolean(),
2132
+ // API sends this; without it zod stripped the draft-vs-live signal.
2133
+ live: import_zod15.z.boolean().optional().describe("False for an sdk-registered draft column \u2014 nothing serves until its first generation publishes it")
2134
+ })
2135
+ ),
2092
2136
  personas: import_zod15.z.array(personaOutput),
2093
2137
  cells: import_zod15.z.array(cellOutput),
2094
2138
  autoFill: import_zod15.z.boolean(),
@@ -2102,13 +2146,19 @@ function registerCellTools(server, client) {
2102
2146
  const data = await client.get(`/projects/${id}/cells`);
2103
2147
  const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
2104
2148
  const lines = [
2105
- `Slots: ${data.slots.map((s) => untrusted(s.displayName)).join(", ") || "(none published)"}`,
2149
+ `Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
2106
2150
  `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
2107
2151
  ];
2108
2152
  for (const p of data.personas) {
2109
2153
  const states = data.slots.map((s) => {
2110
2154
  const c = byCell.get(`${s.slotId}::${p.key}`);
2111
- return `${untrusted(s.displayName, 40)}: ${c ? c.status : "original"}`;
2155
+ let state = c ? c.status : "original";
2156
+ if (c && (c.status === "pending" || c.status === "generating") && c.refinedFrom) {
2157
+ state = `${c.status} (improving on ${untrusted(c.refinedFrom, 64)} \u2014 current version still live)`;
2158
+ } else if (c && c.status === "pending" && c.notBefore && Date.parse(c.notBefore) > Date.now()) {
2159
+ state = `pending (waiting until ${c.notBefore})`;
2160
+ }
2161
+ return `${untrusted(s.displayName, 40)}: ${state}`;
2112
2162
  });
2113
2163
  lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
2114
2164
  }
@@ -2150,7 +2200,7 @@ function registerCellTools(server, client) {
2150
2200
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2151
2201
  },
2152
2202
  withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2153
- var _a, _b;
2203
+ var _a, _b, _c;
2154
2204
  const id = encodeURIComponent(projectId);
2155
2205
  const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
2156
2206
  const lines = [];
@@ -2162,7 +2212,12 @@ function registerCellTools(server, client) {
2162
2212
  lines.push("No generated version is live for this cell.");
2163
2213
  }
2164
2214
  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)}`);
2215
+ if (((_b = data.cell) == null ? void 0 : _b.refinedFrom) && (data.cell.status === "pending" || data.cell.status === "generating")) {
2216
+ lines.push(
2217
+ `An improved take on ${untrusted(data.cell.refinedFrom, 64)} is being written \u2014 the current version keeps serving meanwhile.`
2218
+ );
2219
+ }
2220
+ if ((_c = data.cell) == null ? void 0 : _c.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
2166
2221
  lines.push(data.verdict);
2167
2222
  if (data.performance) {
2168
2223
  lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
@@ -2207,7 +2262,7 @@ function registerCellTools(server, client) {
2207
2262
  }
2208
2263
 
2209
2264
  // src/server.ts
2210
- var PKG_VERSION = true ? "0.18.0" : "0.0.0-dev";
2265
+ var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
2211
2266
  function createMcpServer(client) {
2212
2267
  const server = new import_mcp.McpServer(
2213
2268
  {
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-HEMPCGWX.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.1",
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",