@sentientui/mcp 0.17.7 → 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/AGENTS.md CHANGED
@@ -43,6 +43,9 @@ Server keys start with `sk_` — required for the management API. Public keys (`
43
43
  | `get_variant_brief` | **Start here to optimize a component.** Insight-driven brief for writing a new CODE-NATIVE variant: performance, audience, insights, data-sufficiency + best-practice fallback, and code instructions |
44
44
  | `create_variant` | Creates a no-code **managed text** DRAFT — user must activate in dashboard. Fallback only; not for code-native variants |
45
45
  | `pause_variant` | Stops traffic; no MCP resume |
46
+ | `get_cell_matrix` | "Who sees what": per-(visitor type, region) generation state + traffic share. A missing cell = they see the original |
47
+ | `get_cell_detail` | One cell's generated option, its stored rationale, and a plain-language performance verdict |
48
+ | `generate_cell` | Queues async AI generation for one cell (also retries failed ones). Paid plan; check `get_cell_matrix` for progress |
46
49
 
47
50
  ## Canonical Workflows
48
51
 
package/README.md CHANGED
@@ -101,7 +101,12 @@ A sandboxed demo token is provisioned automatically — 10 calls/month, read-onl
101
101
  | `get_variant_brief` | Insight-driven brief for writing a new **code-native** variant: performance, audience, insights, data-sufficiency (with a best-practice fallback when there's no data yet), and step-by-step code instructions |
102
102
  | `create_variant` | Create a no-code (managed) text variant (Starter+) — fallback for text-only variants without a code change |
103
103
  | `pause_variant` | Pause a variant to stop traffic assignment |
104
+ | `get_cell_matrix` | The "Who sees what" matrix: which visitor types have an AI-generated version live in each personalizable region, with traffic share and auto-fill status |
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
+ | `generate_cell` | Queue AI generation of a version for one (visitor type, region) cell — brand-locked, live in under a minute (paid plan) |
104
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
+ >
105
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.
106
111
  >
107
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
 
@@ -1856,6 +1879,23 @@ useLayoutOrder(). Non-React sites: sections: ['#hero', '#pricing', '#faq'] (CSS
1856
1879
  the theme's natural order) in window.sentient \u2014 the snippet reorders those elements within
1857
1880
  their shared parent, and applies nothing if any selector drifts.
1858
1881
 
1882
+ ## No-code install order (snippet)
1883
+
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.
1898
+
1859
1899
  ## Testing the integration
1860
1900
 
1861
1901
  Use '@sentientui/react/testing': renderWithSentient(ui, { variants, slots, persona }) forces
@@ -2017,8 +2057,182 @@ function registerAgentTrafficTools(server, client) {
2017
2057
  );
2018
2058
  }
2019
2059
 
2060
+ // src/tools/cells.ts
2061
+ import { z as z15 } from "zod";
2062
+ var personaOutput = z15.object({
2063
+ key: z15.string(),
2064
+ displayName: z15.string(),
2065
+ description: z15.string().nullable(),
2066
+ share: z15.number().describe("Fraction of 30-day decided sessions")
2067
+ });
2068
+ var cellOutput = z15.object({
2069
+ slotId: z15.string(),
2070
+ persona: z15.string(),
2071
+ status: z15.string().describe("pending | generating | live | error | rejected"),
2072
+ requestedBy: z15.string(),
2073
+ armId: z15.string().nullable(),
2074
+ rationale: z15.string().nullable(),
2075
+ error: z15.string().nullable(),
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"),
2082
+ performance: z15.object({
2083
+ shown: z15.number(),
2084
+ conversions: z15.number(),
2085
+ deltaPct: z15.number().nullable().describe("Signed % vs the original conversion rate"),
2086
+ tier: z15.string().describe("early | no_baseline | winning | losing | flat")
2087
+ }).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
2088
+ });
2089
+ function registerCellTools(server, client) {
2090
+ server.registerTool(
2091
+ "get_cell_matrix",
2092
+ {
2093
+ title: "Persona \xD7 slot cell matrix",
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,
2095
+ inputSchema: { projectId: projectIdSchema },
2096
+ outputSchema: {
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
+ ),
2106
+ personas: z15.array(personaOutput),
2107
+ cells: z15.array(cellOutput),
2108
+ autoFill: z15.boolean(),
2109
+ personaSource: z15.string().describe("Active persona-set source: default | declared | discovered"),
2110
+ undecidedShare: z15.number()
2111
+ },
2112
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2113
+ },
2114
+ withApiErrorGuidance(async ({ projectId }) => {
2115
+ const id = encodeURIComponent(projectId);
2116
+ const data = await client.get(`/projects/${id}/cells`);
2117
+ const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
2118
+ const lines = [
2119
+ `Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
2120
+ `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
2121
+ ];
2122
+ for (const p of data.personas) {
2123
+ const states = data.slots.map((s) => {
2124
+ const c = byCell.get(`${s.slotId}::${p.key}`);
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}`;
2132
+ });
2133
+ lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
2134
+ }
2135
+ lines.push(`Undecided visitors (${Math.round(data.undecidedShare * 100)}%) always see the original.`);
2136
+ return {
2137
+ content: [{ type: "text", text: lines.join("\n") }],
2138
+ structuredContent: data
2139
+ };
2140
+ })
2141
+ );
2142
+ server.registerTool(
2143
+ "get_cell_detail",
2144
+ {
2145
+ title: "Cell story",
2146
+ description: "One cell's full story: the generated option a visitor type sees in a slot, why it was written (the stored generation rationale), and how it is performing vs the original (deterministic plain-language verdict)." + UNTRUSTED_FIELDS_NOTE,
2147
+ inputSchema: {
2148
+ projectId: projectIdSchema,
2149
+ slotId: z15.string().min(1).max(128).describe("The slot id (a matrix column)"),
2150
+ persona: z15.string().min(1).max(64).describe("The persona key (a matrix row)")
2151
+ },
2152
+ outputSchema: {
2153
+ cell: cellOutput.nullable(),
2154
+ arm: z15.object({
2155
+ id: z15.string(),
2156
+ displayName: z15.string().nullable(),
2157
+ content: z15.string().nullable(),
2158
+ blocks: z15.unknown().nullable()
2159
+ }).nullable(),
2160
+ performance: z15.object({
2161
+ shown: z15.number(),
2162
+ conversions: z15.number(),
2163
+ conversionRate: z15.number(),
2164
+ baselineShown: z15.number(),
2165
+ baselineConversions: z15.number(),
2166
+ baselineConversionRate: z15.number()
2167
+ }).nullable(),
2168
+ verdict: z15.string()
2169
+ },
2170
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2171
+ },
2172
+ withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2173
+ var _a, _b, _c;
2174
+ const id = encodeURIComponent(projectId);
2175
+ const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
2176
+ const lines = [];
2177
+ if (data.arm) {
2178
+ lines.push(
2179
+ data.arm.content !== null ? `They see: "${untrusted(data.arm.content, 300)}"` : `They see a designed layout (option ${untrusted(data.arm.id, 64)}).`
2180
+ );
2181
+ } else {
2182
+ lines.push("No generated version is live for this cell.");
2183
+ }
2184
+ if ((_a = data.cell) == null ? void 0 : _a.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
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)}`);
2191
+ lines.push(data.verdict);
2192
+ if (data.performance) {
2193
+ lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
2194
+ }
2195
+ return {
2196
+ content: [{ type: "text", text: lines.join("\n") }],
2197
+ structuredContent: data
2198
+ };
2199
+ })
2200
+ );
2201
+ server.registerTool(
2202
+ "generate_cell",
2203
+ {
2204
+ title: "Generate a cell version",
2205
+ description: "Queue AI generation of a version for one (persona, slot) cell. The worker writes it asynchronously (usually under a minute), brand-locked, and it goes live as a named option the optimizer explores. Also works to regenerate or retry a failed cell. Requires a paid plan (anonymous demo tokens are read-only).",
2206
+ inputSchema: {
2207
+ projectId: projectIdSchema,
2208
+ slotId: z15.string().min(1).max(128).describe("The slot id (a matrix column)"),
2209
+ persona: z15.string().min(1).max(64).describe("The persona key (a matrix row)")
2210
+ },
2211
+ outputSchema: {
2212
+ slotId: z15.string(),
2213
+ persona: z15.string(),
2214
+ status: z15.literal("pending").describe("Queued \u2014 check get_cell_matrix for progress")
2215
+ },
2216
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2217
+ },
2218
+ withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2219
+ const id = encodeURIComponent(projectId);
2220
+ await client.post(`/projects/${id}/cells/generate`, { slotId, persona });
2221
+ return {
2222
+ content: [
2223
+ {
2224
+ type: "text",
2225
+ text: `Writing a version of ${untrusted(slotId, 64)} for ${untrusted(persona, 64)} \u2014 it usually goes live in under a minute. Check get_cell_matrix (status flips pending \u2192 live) or get_cell_detail for the story once it lands.`
2226
+ }
2227
+ ],
2228
+ structuredContent: { slotId, persona, status: "pending" }
2229
+ };
2230
+ })
2231
+ );
2232
+ }
2233
+
2020
2234
  // src/server.ts
2021
- var PKG_VERSION = true ? "0.17.7" : "0.0.0-dev";
2235
+ var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
2022
2236
  function createMcpServer(client) {
2023
2237
  const server = new McpServer(
2024
2238
  {
@@ -2053,6 +2267,7 @@ function createMcpServer(client) {
2053
2267
  registerVariantWriteTools(server, client);
2054
2268
  registerIntegrationGuideTools(server);
2055
2269
  registerAgentTrafficTools(server, client);
2270
+ registerCellTools(server, client);
2056
2271
  return server;
2057
2272
  }
2058
2273
 
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
 
@@ -1862,6 +1885,23 @@ useLayoutOrder(). Non-React sites: sections: ['#hero', '#pricing', '#faq'] (CSS
1862
1885
  the theme's natural order) in window.sentient \u2014 the snippet reorders those elements within
1863
1886
  their shared parent, and applies nothing if any selector drifts.
1864
1887
 
1888
+ ## No-code install order (snippet)
1889
+
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.
1904
+
1865
1905
  ## Testing the integration
1866
1906
 
1867
1907
  Use '@sentientui/react/testing': renderWithSentient(ui, { variants, slots, persona }) forces
@@ -2023,8 +2063,182 @@ function registerAgentTrafficTools(server, client) {
2023
2063
  );
2024
2064
  }
2025
2065
 
2066
+ // src/tools/cells.ts
2067
+ var import_zod15 = require("zod");
2068
+ var personaOutput = import_zod15.z.object({
2069
+ key: import_zod15.z.string(),
2070
+ displayName: import_zod15.z.string(),
2071
+ description: import_zod15.z.string().nullable(),
2072
+ share: import_zod15.z.number().describe("Fraction of 30-day decided sessions")
2073
+ });
2074
+ var cellOutput = import_zod15.z.object({
2075
+ slotId: import_zod15.z.string(),
2076
+ persona: import_zod15.z.string(),
2077
+ status: import_zod15.z.string().describe("pending | generating | live | error | rejected"),
2078
+ requestedBy: import_zod15.z.string(),
2079
+ armId: import_zod15.z.string().nullable(),
2080
+ rationale: import_zod15.z.string().nullable(),
2081
+ error: import_zod15.z.string().nullable(),
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"),
2088
+ performance: import_zod15.z.object({
2089
+ shown: import_zod15.z.number(),
2090
+ conversions: import_zod15.z.number(),
2091
+ deltaPct: import_zod15.z.number().nullable().describe("Signed % vs the original conversion rate"),
2092
+ tier: import_zod15.z.string().describe("early | no_baseline | winning | losing | flat")
2093
+ }).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
2094
+ });
2095
+ function registerCellTools(server, client) {
2096
+ server.registerTool(
2097
+ "get_cell_matrix",
2098
+ {
2099
+ title: "Persona \xD7 slot cell matrix",
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,
2101
+ inputSchema: { projectId: projectIdSchema },
2102
+ outputSchema: {
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
+ ),
2112
+ personas: import_zod15.z.array(personaOutput),
2113
+ cells: import_zod15.z.array(cellOutput),
2114
+ autoFill: import_zod15.z.boolean(),
2115
+ personaSource: import_zod15.z.string().describe("Active persona-set source: default | declared | discovered"),
2116
+ undecidedShare: import_zod15.z.number()
2117
+ },
2118
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2119
+ },
2120
+ withApiErrorGuidance(async ({ projectId }) => {
2121
+ const id = encodeURIComponent(projectId);
2122
+ const data = await client.get(`/projects/${id}/cells`);
2123
+ const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
2124
+ const lines = [
2125
+ `Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
2126
+ `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
2127
+ ];
2128
+ for (const p of data.personas) {
2129
+ const states = data.slots.map((s) => {
2130
+ const c = byCell.get(`${s.slotId}::${p.key}`);
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}`;
2138
+ });
2139
+ lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
2140
+ }
2141
+ lines.push(`Undecided visitors (${Math.round(data.undecidedShare * 100)}%) always see the original.`);
2142
+ return {
2143
+ content: [{ type: "text", text: lines.join("\n") }],
2144
+ structuredContent: data
2145
+ };
2146
+ })
2147
+ );
2148
+ server.registerTool(
2149
+ "get_cell_detail",
2150
+ {
2151
+ title: "Cell story",
2152
+ description: "One cell's full story: the generated option a visitor type sees in a slot, why it was written (the stored generation rationale), and how it is performing vs the original (deterministic plain-language verdict)." + UNTRUSTED_FIELDS_NOTE,
2153
+ inputSchema: {
2154
+ projectId: projectIdSchema,
2155
+ slotId: import_zod15.z.string().min(1).max(128).describe("The slot id (a matrix column)"),
2156
+ persona: import_zod15.z.string().min(1).max(64).describe("The persona key (a matrix row)")
2157
+ },
2158
+ outputSchema: {
2159
+ cell: cellOutput.nullable(),
2160
+ arm: import_zod15.z.object({
2161
+ id: import_zod15.z.string(),
2162
+ displayName: import_zod15.z.string().nullable(),
2163
+ content: import_zod15.z.string().nullable(),
2164
+ blocks: import_zod15.z.unknown().nullable()
2165
+ }).nullable(),
2166
+ performance: import_zod15.z.object({
2167
+ shown: import_zod15.z.number(),
2168
+ conversions: import_zod15.z.number(),
2169
+ conversionRate: import_zod15.z.number(),
2170
+ baselineShown: import_zod15.z.number(),
2171
+ baselineConversions: import_zod15.z.number(),
2172
+ baselineConversionRate: import_zod15.z.number()
2173
+ }).nullable(),
2174
+ verdict: import_zod15.z.string()
2175
+ },
2176
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2177
+ },
2178
+ withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2179
+ var _a2, _b, _c;
2180
+ const id = encodeURIComponent(projectId);
2181
+ const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
2182
+ const lines = [];
2183
+ if (data.arm) {
2184
+ lines.push(
2185
+ data.arm.content !== null ? `They see: "${untrusted(data.arm.content, 300)}"` : `They see a designed layout (option ${untrusted(data.arm.id, 64)}).`
2186
+ );
2187
+ } else {
2188
+ lines.push("No generated version is live for this cell.");
2189
+ }
2190
+ if ((_a2 = data.cell) == null ? void 0 : _a2.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
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)}`);
2197
+ lines.push(data.verdict);
2198
+ if (data.performance) {
2199
+ lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
2200
+ }
2201
+ return {
2202
+ content: [{ type: "text", text: lines.join("\n") }],
2203
+ structuredContent: data
2204
+ };
2205
+ })
2206
+ );
2207
+ server.registerTool(
2208
+ "generate_cell",
2209
+ {
2210
+ title: "Generate a cell version",
2211
+ description: "Queue AI generation of a version for one (persona, slot) cell. The worker writes it asynchronously (usually under a minute), brand-locked, and it goes live as a named option the optimizer explores. Also works to regenerate or retry a failed cell. Requires a paid plan (anonymous demo tokens are read-only).",
2212
+ inputSchema: {
2213
+ projectId: projectIdSchema,
2214
+ slotId: import_zod15.z.string().min(1).max(128).describe("The slot id (a matrix column)"),
2215
+ persona: import_zod15.z.string().min(1).max(64).describe("The persona key (a matrix row)")
2216
+ },
2217
+ outputSchema: {
2218
+ slotId: import_zod15.z.string(),
2219
+ persona: import_zod15.z.string(),
2220
+ status: import_zod15.z.literal("pending").describe("Queued \u2014 check get_cell_matrix for progress")
2221
+ },
2222
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2223
+ },
2224
+ withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2225
+ const id = encodeURIComponent(projectId);
2226
+ await client.post(`/projects/${id}/cells/generate`, { slotId, persona });
2227
+ return {
2228
+ content: [
2229
+ {
2230
+ type: "text",
2231
+ text: `Writing a version of ${untrusted(slotId, 64)} for ${untrusted(persona, 64)} \u2014 it usually goes live in under a minute. Check get_cell_matrix (status flips pending \u2192 live) or get_cell_detail for the story once it lands.`
2232
+ }
2233
+ ],
2234
+ structuredContent: { slotId, persona, status: "pending" }
2235
+ };
2236
+ })
2237
+ );
2238
+ }
2239
+
2026
2240
  // src/server.ts
2027
- var PKG_VERSION = true ? "0.17.7" : "0.0.0-dev";
2241
+ var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
2028
2242
  function createMcpServer(client) {
2029
2243
  const server = new import_mcp.McpServer(
2030
2244
  {
@@ -2059,6 +2273,7 @@ function createMcpServer(client) {
2059
2273
  registerVariantWriteTools(server, client);
2060
2274
  registerIntegrationGuideTools(server);
2061
2275
  registerAgentTrafficTools(server, client);
2276
+ registerCellTools(server, client);
2062
2277
  return server;
2063
2278
  }
2064
2279
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ApiClient,
4
4
  createMcpServer
5
- } from "./chunk-5PR2LCNN.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
 
@@ -1886,6 +1909,23 @@ useLayoutOrder(). Non-React sites: sections: ['#hero', '#pricing', '#faq'] (CSS
1886
1909
  the theme's natural order) in window.sentient \u2014 the snippet reorders those elements within
1887
1910
  their shared parent, and applies nothing if any selector drifts.
1888
1911
 
1912
+ ## No-code install order (snippet)
1913
+
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.
1928
+
1889
1929
  ## Testing the integration
1890
1930
 
1891
1931
  Use '@sentientui/react/testing': renderWithSentient(ui, { variants, slots, persona }) forces
@@ -2047,8 +2087,182 @@ function registerAgentTrafficTools(server, client) {
2047
2087
  );
2048
2088
  }
2049
2089
 
2090
+ // src/tools/cells.ts
2091
+ var import_zod15 = require("zod");
2092
+ var personaOutput = import_zod15.z.object({
2093
+ key: import_zod15.z.string(),
2094
+ displayName: import_zod15.z.string(),
2095
+ description: import_zod15.z.string().nullable(),
2096
+ share: import_zod15.z.number().describe("Fraction of 30-day decided sessions")
2097
+ });
2098
+ var cellOutput = import_zod15.z.object({
2099
+ slotId: import_zod15.z.string(),
2100
+ persona: import_zod15.z.string(),
2101
+ status: import_zod15.z.string().describe("pending | generating | live | error | rejected"),
2102
+ requestedBy: import_zod15.z.string(),
2103
+ armId: import_zod15.z.string().nullable(),
2104
+ rationale: import_zod15.z.string().nullable(),
2105
+ error: import_zod15.z.string().nullable(),
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"),
2112
+ performance: import_zod15.z.object({
2113
+ shown: import_zod15.z.number(),
2114
+ conversions: import_zod15.z.number(),
2115
+ deltaPct: import_zod15.z.number().nullable().describe("Signed % vs the original conversion rate"),
2116
+ tier: import_zod15.z.string().describe("early | no_baseline | winning | losing | flat")
2117
+ }).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
2118
+ });
2119
+ function registerCellTools(server, client) {
2120
+ server.registerTool(
2121
+ "get_cell_matrix",
2122
+ {
2123
+ title: "Persona \xD7 slot cell matrix",
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,
2125
+ inputSchema: { projectId: projectIdSchema },
2126
+ outputSchema: {
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
+ ),
2136
+ personas: import_zod15.z.array(personaOutput),
2137
+ cells: import_zod15.z.array(cellOutput),
2138
+ autoFill: import_zod15.z.boolean(),
2139
+ personaSource: import_zod15.z.string().describe("Active persona-set source: default | declared | discovered"),
2140
+ undecidedShare: import_zod15.z.number()
2141
+ },
2142
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2143
+ },
2144
+ withApiErrorGuidance(async ({ projectId }) => {
2145
+ const id = encodeURIComponent(projectId);
2146
+ const data = await client.get(`/projects/${id}/cells`);
2147
+ const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
2148
+ const lines = [
2149
+ `Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
2150
+ `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
2151
+ ];
2152
+ for (const p of data.personas) {
2153
+ const states = data.slots.map((s) => {
2154
+ const c = byCell.get(`${s.slotId}::${p.key}`);
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}`;
2162
+ });
2163
+ lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
2164
+ }
2165
+ lines.push(`Undecided visitors (${Math.round(data.undecidedShare * 100)}%) always see the original.`);
2166
+ return {
2167
+ content: [{ type: "text", text: lines.join("\n") }],
2168
+ structuredContent: data
2169
+ };
2170
+ })
2171
+ );
2172
+ server.registerTool(
2173
+ "get_cell_detail",
2174
+ {
2175
+ title: "Cell story",
2176
+ description: "One cell's full story: the generated option a visitor type sees in a slot, why it was written (the stored generation rationale), and how it is performing vs the original (deterministic plain-language verdict)." + UNTRUSTED_FIELDS_NOTE,
2177
+ inputSchema: {
2178
+ projectId: projectIdSchema,
2179
+ slotId: import_zod15.z.string().min(1).max(128).describe("The slot id (a matrix column)"),
2180
+ persona: import_zod15.z.string().min(1).max(64).describe("The persona key (a matrix row)")
2181
+ },
2182
+ outputSchema: {
2183
+ cell: cellOutput.nullable(),
2184
+ arm: import_zod15.z.object({
2185
+ id: import_zod15.z.string(),
2186
+ displayName: import_zod15.z.string().nullable(),
2187
+ content: import_zod15.z.string().nullable(),
2188
+ blocks: import_zod15.z.unknown().nullable()
2189
+ }).nullable(),
2190
+ performance: import_zod15.z.object({
2191
+ shown: import_zod15.z.number(),
2192
+ conversions: import_zod15.z.number(),
2193
+ conversionRate: import_zod15.z.number(),
2194
+ baselineShown: import_zod15.z.number(),
2195
+ baselineConversions: import_zod15.z.number(),
2196
+ baselineConversionRate: import_zod15.z.number()
2197
+ }).nullable(),
2198
+ verdict: import_zod15.z.string()
2199
+ },
2200
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2201
+ },
2202
+ withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2203
+ var _a, _b, _c;
2204
+ const id = encodeURIComponent(projectId);
2205
+ const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
2206
+ const lines = [];
2207
+ if (data.arm) {
2208
+ lines.push(
2209
+ data.arm.content !== null ? `They see: "${untrusted(data.arm.content, 300)}"` : `They see a designed layout (option ${untrusted(data.arm.id, 64)}).`
2210
+ );
2211
+ } else {
2212
+ lines.push("No generated version is live for this cell.");
2213
+ }
2214
+ if ((_a = data.cell) == null ? void 0 : _a.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
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)}`);
2221
+ lines.push(data.verdict);
2222
+ if (data.performance) {
2223
+ lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
2224
+ }
2225
+ return {
2226
+ content: [{ type: "text", text: lines.join("\n") }],
2227
+ structuredContent: data
2228
+ };
2229
+ })
2230
+ );
2231
+ server.registerTool(
2232
+ "generate_cell",
2233
+ {
2234
+ title: "Generate a cell version",
2235
+ description: "Queue AI generation of a version for one (persona, slot) cell. The worker writes it asynchronously (usually under a minute), brand-locked, and it goes live as a named option the optimizer explores. Also works to regenerate or retry a failed cell. Requires a paid plan (anonymous demo tokens are read-only).",
2236
+ inputSchema: {
2237
+ projectId: projectIdSchema,
2238
+ slotId: import_zod15.z.string().min(1).max(128).describe("The slot id (a matrix column)"),
2239
+ persona: import_zod15.z.string().min(1).max(64).describe("The persona key (a matrix row)")
2240
+ },
2241
+ outputSchema: {
2242
+ slotId: import_zod15.z.string(),
2243
+ persona: import_zod15.z.string(),
2244
+ status: import_zod15.z.literal("pending").describe("Queued \u2014 check get_cell_matrix for progress")
2245
+ },
2246
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2247
+ },
2248
+ withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2249
+ const id = encodeURIComponent(projectId);
2250
+ await client.post(`/projects/${id}/cells/generate`, { slotId, persona });
2251
+ return {
2252
+ content: [
2253
+ {
2254
+ type: "text",
2255
+ text: `Writing a version of ${untrusted(slotId, 64)} for ${untrusted(persona, 64)} \u2014 it usually goes live in under a minute. Check get_cell_matrix (status flips pending \u2192 live) or get_cell_detail for the story once it lands.`
2256
+ }
2257
+ ],
2258
+ structuredContent: { slotId, persona, status: "pending" }
2259
+ };
2260
+ })
2261
+ );
2262
+ }
2263
+
2050
2264
  // src/server.ts
2051
- var PKG_VERSION = true ? "0.17.7" : "0.0.0-dev";
2265
+ var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
2052
2266
  function createMcpServer(client) {
2053
2267
  const server = new import_mcp.McpServer(
2054
2268
  {
@@ -2083,6 +2297,7 @@ function createMcpServer(client) {
2083
2297
  registerVariantWriteTools(server, client);
2084
2298
  registerIntegrationGuideTools(server);
2085
2299
  registerAgentTrafficTools(server, client);
2300
+ registerCellTools(server, client);
2086
2301
  return server;
2087
2302
  }
2088
2303
  // Annotate the CommonJS export names for ESM import in node:
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-5PR2LCNN.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.17.7",
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",