@seldonframe/mcp 1.3.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@seldonframe/mcp",
3
- "version": "1.3.5",
3
+ "version": "1.4.0",
4
+ "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.4.0: ARCHITECTURAL SHIFT — block-as-skill primitives + MCP-native workspace creation (the Karpathy-inflected 'thin harness, fat skills' move). Pre-1.4 the SF backend generated all landing-page copy server-side from a hardcoded personality system + JSON templates + 6+ deterministic transforms. Every new niche surfaced a fresh layer-mismatch bug (10 v1.3.x releases shipped fixes for the same bug class). v1.4.0 inverts the architecture for the highest-stakes copy surfaces — hero, services, faq — by moving block GENERATION out of the SF backend and into the operator's IDE agent (Claude Code, Cursor, Windsurf, etc.). Each block now lives as a single-source-of-truth folder under packages/crm/src/blocks/<name>/SKILL.md containing the prop schema (YAML frontmatter) + generation prompt + voice rules + worked examples + validator definitions (markdown body). Five new MCP tools enable the v2 flow: (1) create_workspace_v2 — bootstraps the workspace via the v1 orchestrator, returns recommended_blocks + workspace context for the IDE agent's LLM; (2) list_blocks — discovers v2 page primitives; (3) get_block_skill — returns raw SKILL.md the IDE agent reads; (4) persist_block — validates props against the block's Zod schema + deterministic copy-quality validators (headline_quantified, distinct_icons, no_coaching_leak, no_throat_clearing, etc.), maps props onto the existing LandingSection shape, re-renders the full landing via the v1 renderer, persists block_instances row + landing_pages update; (5) complete_workspace_v2 — reports which blocks landed vs. fell back to v1 defaults. Forever-frozen edits live in block_instances.customizations (jsonb append-only) so operator edits via 'customize' calls survive workspace re-rolls. v1 (create_full_workspace) still owns booking, intake, about, theme, pipeline, CRM — and remains the safe fallback for any caller that doesn't know about v2 yet. ⚠️ BACKEND REDEPLOY REQUIRED + apply migration 0036_block_instances.sql in production. v1.5 will land the SKILL.md → __generated__/ codegen so the prop schema lives in exactly one place.",
4
5
  "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.3.5: Three Karpathy-flavored root-cause fixes shipped in one cycle, all motivated by what the Iron & Oak Barbershop test surfaced. (FIX D — booking + intake 400 on subdomains) The C4 booking client and C5 intake client both extracted orgSlug from window.location.pathname, expecting /book/<slug>/<bookingSlug> or /forms/<slug>/<formSlug>. On a workspace subdomain (<slug>.app.seldonframe.com) the proxy rewrites the path server-side but the BROWSER URL stays at /book or /intake — clients saw no slug, posted orgSlug='', and every visitor's submission rejected with missing_required_field 400. Server-side defense in depth: new resolveWorkspaceSlugFromHostHeader helper in lib/workspace/host-to-slug.ts mirrors proxy.ts logic; both public POST routes now derive orgSlug from the Host header when the body value is missing. Client-side defense in depth: both renderers fall back to `window.location.hostname.split('.')[0]` when pathParts[1] is empty. Plus public_intake_succeeded/_rejected logs (parity with v1.3.3 booking observability) so the intake funnel is visible end-to-end. New slug_source tag on success logs lets us measure how often the host fallback rescues a request. (FIX E — pipeline validator's HTML-entity blind spot) The 'BUSINESS NAME NOT IN HTML' check in pipeline-validator.ts did a literal substring search on the rendered HTML. Business names with HTML-special chars ('Iron & Oak Barbershop', \"Joe's Plumbing\", '<3 Salon') get entity-encoded by the renderer ('Iron &amp; Oak Barbershop'), so the literal check missed them and emitted false-positive blocking errors even though the name WAS rendered correctly. New htmlContainsText helper tries the literal needle, then its HTML-escaped form, then the &apos; variant — same logic for the first-offering fallback. Validator was wrong, not the rendering pipeline. (FIX F — model upgrade) Default ANTHROPIC_MODEL is now claude-opus-4-7 (was claude-sonnet-4-20250514). Personality generation is a once-per-niche cost — the cache amortizes Opus across every future workspace in that vertical, so the per-workspace blended cost stays near-zero while cold-call quality improves meaningfully on long-tail niches (pet grooming, accounting, voiceover, etc.). Three-tier fallback chain: Opus 4.7 (primary) → Sonnet 4.5 / claude-sonnet-4-5-20250929 (secondary) → Haiku 3.5 (tertiary). Each tier overridable via ANTHROPIC_MODEL / ANTHROPIC_MODEL_FALLBACK / ANTHROPIC_MODEL_TERTIARY env vars; chain walks down on any 404 so a single model retirement never takes the system offline. ⚠️ BACKEND REDEPLOY REQUIRED. No env vars to set on Vercel — Opus is the new default; if cost is a concern set ANTHROPIC_MODEL=claude-sonnet-4-5-20250929 to revert.",
5
6
  "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.3.4: ROOT-CAUSE the last 3 'wrong default everywhere' bugs from the Iron & Oak Barbershop test by extending CRMPersonality with two new LLM-populated fields + closing the booking → CRM kanban loop. (FIX A — booking metadata personalization) Pre-1.3.4 the booking calendar showed 'Free consultation / 30-minute conversation' for ANY niche not in personalityBookingDefaults (only 7 hardcoded verticals — barbershop, photography, accounting, tutoring, etc. all leaked the general.json template default). New optional CRMPersonality.booking field { title, description, duration_minutes, location_kind } populated by the LLM via the personality generator's prompt. createAnonymousWorkspace applies it as the highest-priority source (LLM > personalityBookingDefaults > JSON template). location_kind enum mirrors the blueprint's eventType.location.kind exactly (on-site-business / on-site-customer / phone / video / hybrid) so there's zero translation. (FIX B — hero image relevance) Pre-1.3.4 every workspace whose vertical wasn't in the curated IMAGES map fell back to GENERAL_IMAGES (hand-picked CONTRACTOR photos — bizarre for nail salons, tutoring, barbershops). New optional CRMPersonality.images.hero_query field — free-text Unsplash search query the LLM picks per-niche ('barbershop interior', 'math tutoring student', 'nail salon manicure'). New resolveHeroImageUrlForQuery function tries the official Unsplash API first when UNSPLASH_ACCESS_KEY is set, then falls back to source.unsplash.com keyless redirect. applyPersonalityImagesToSchema is now async and prefers LLM > curated > skip. (FIX C — bookings appear in CRM kanban) Pre-1.3.4 submitPublicBookingAction inserted into bookings + activities + contacts but NEVER into deals — so the visitor's confirmed appointment never appeared in /deals where operators actually triage their pipeline. v1.3.4 calls ensureDefaultPipelineForOrg (lazy-seeds when missing) and INSERTs a deal at the first stage on every successful free booking, with customFields.bookingId for reconciliation against future booking events. Wrapped in try/catch + structured public_booking_deal_created/_failed events — a deal-insert failure does NOT roll back the booking. (VALIDATOR) +3 new checks: hero_image_loadable (asserts the rendered URL belongs to a known Unsplash host pattern + has sizing params), booking_title_personalized (asserts the booking title doesn't contain 'Free consultation' / '30-minute conversation' AND uses the personality's title if declared), booking_to_deal_pipeline_ready (asserts the org's first pipeline stage has a non-empty name so deal-insertion lands in a renderable kanban column). ⚠️ BACKEND REDEPLOY REQUIRED. Optional: set UNSPLASH_ACCESS_KEY on Vercel to use the official API for hero images instead of the keyless source.unsplash.com redirect.",
6
7
  "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.3.3: ROOT CAUSE for the recurring 'wrong personality everywhere except landing' bug pattern + booking observability + per-service enrichment. (FIX 1) The booking POST /api/v1/public/bookings was returning 400 with literally NO LOGS ('No logs found for this request' in Vercel UI). Every reject path now emits a single-line structured JSON event (event: public_booking_rejected, reason: <stable_id>, ...context) so we can finally see WHICH validation is rejecting bookings. submitPublicBookingAction got the same treatment: every throw now logs reason + booking context (org_slug, weekday_in_workspace_tz, slot_local_hhmm, day_start, day_end, ...). The next time someone hits 'Couldn't book that time', the Vercel log will name the exact cause. (FIX 2 — REAL ROOT CAUSE) The LLM-generated CRMPersonality from create-full.ts was being THROWN AWAY by createAnonymousWorkspace — which called selectCRMPersonality (keyword-based) internally, ignoring the resolved personality. So booking template + intake form + landing seed all used the keyword-fallback personality (e.g. 'general' for nail salons → 'Get in Touch' intake title, 'New Lead → Quoted → ...' pipeline) while only the post-seed defensive override updated settings.crmPersonality with the LLM personality. Result: rendered intake HTML never had the personality title; that's why intake_title_rendered_html: FAIL kept appearing. Fix: createAnonymousWorkspace now accepts an opts.personality parameter; create-full.ts passes the LLM-resolved personality through. Booking template + intake form + landing seed all use the LLM personality from the start. (FIX 3a) Light mode is now the default fallback when personality.theme.mode is unset — premium verticals must opt in to dark via theme.mode='dark'. Previously the fallback used the BusinessType heuristic which routed saas/agency to cinematic (dark) by default. (FIX 3d/e) New optional services_enrichment field on CRMPersonality: array with one entry per operator service { service_name, description, icon }. The LLM is prompted to populate it with customer-facing descriptions + distinct icons per service from a known allowlist. Pipeline merges descriptions onto soul.offerings (so service cards show body copy) and overrides applyServiceIconsFromPersonality (so each card has a distinct LLM-picked icon instead of the keyword classifier's homogenous defaults). ⚠️ BACKEND REDEPLOY REQUIRED. Run a niche test post-deploy: every personality_resolved log line now also tells you whether bookings persist (public_booking_succeeded) or why they reject (public_booking_rejected with reason).",
package/src/tools.js CHANGED
@@ -2433,6 +2433,233 @@ export const TOOLS = [
2433
2433
  return result;
2434
2434
  },
2435
2435
  },
2436
+
2437
+ // ─── v1.4.0 — v2 (MCP-native) workspace creation ────────────────────────
2438
+ //
2439
+ // The v2 flow shifts block GENERATION out of the SF backend and into the
2440
+ // IDE agent's LLM context. Operator's IDE agent (Claude Code, Cursor,
2441
+ // Windsurf, etc.) reads each block's SKILL.md, generates props with its
2442
+ // own LLM, and posts the props back to SF. SF persists + renders.
2443
+ //
2444
+ // PREFERRED for new workspace creation as of v1.4. The v1
2445
+ // create_full_workspace tool above still works (and v1 still owns
2446
+ // booking, intake, about, theme, pipeline) — v2 only owns hero, services,
2447
+ // and faq for now (the highest-stakes copy surfaces, where the v1 layer-
2448
+ // mismatch bug class hurt most).
2449
+
2450
+ {
2451
+ name: "create_workspace_v2",
2452
+ description:
2453
+ "PREFERRED for new workspaces (v1.4+). MCP-native workspace creation: bootstraps the workspace via the v1 orchestrator (CRM, booking, intake, theme, pipeline) AND returns a list of v2 page blocks the IDE agent will now generate using its own LLM. " +
2454
+ "Flow: 1) call this tool with the operator's business info; 2) for each block in `v2.recommended_blocks`, call get_block_skill(name) and use your LLM to generate props matching the SKILL.md prompt + schema; 3) call persist_block({ workspace_id, block_name, generation_prompt, props }) for each; 4) call complete_workspace_v2({ workspace_id }). " +
2455
+ "MANDATORY FOLLOW-UP: After this returns `status: 'ready'` AND after all blocks land via persist_block + complete_workspace_v2, ask the operator verbatim 'What email should I use for your account?' Then call finalize_workspace({ workspace_id, email }). The admin dashboard URL is created by finalize_workspace, not here. " +
2456
+ "Why v2: v1 generated all copy server-side from a hardcoded personality system, which produced layer-mismatch bugs every time a new niche was tested. v2 puts the LLM in your context (the IDE agent), reads from one SKILL.md per block, and the generated copy is naturally niche-aware. The operator can later say 'change the hero' and you customize it via persist_block with a customization payload.",
2457
+ inputSchema: obj(
2458
+ {
2459
+ business_name: str("Business display name."),
2460
+ city: str("Operator's city. Drives timezone inference."),
2461
+ state: str("US state code or full name (or Canadian province)."),
2462
+ phone: str("Business phone, any format."),
2463
+ services: {
2464
+ type: "array",
2465
+ description: "Services / offerings the business provides — each as a plain string.",
2466
+ items: { type: "string" },
2467
+ },
2468
+ business_description: str(
2469
+ "One paragraph describing the business — drives the personality classifier and feeds into block prompts."
2470
+ ),
2471
+ review_count: { type: "number", description: "Optional — number of reviews." },
2472
+ review_rating: { type: "number", description: "Optional — average star rating." },
2473
+ certifications: { type: "array", items: { type: "string" } },
2474
+ trust_signals: { type: "array", items: { type: "string" } },
2475
+ emergency_service: { type: "boolean" },
2476
+ same_day: { type: "boolean" },
2477
+ service_area: { type: "array", items: { type: "string" } },
2478
+ email: str("Optional contact email surfaced in the landing footer (NOT the operator's account email)."),
2479
+ address: str("Optional business address."),
2480
+ },
2481
+ ["business_name", "city", "state", "phone", "services", "business_description"],
2482
+ ),
2483
+ handler: async (args) => {
2484
+ const firstEver = isFirstEverCall();
2485
+ const result = await api("POST", "/workspace/v2/create", {
2486
+ body: {
2487
+ business_name: args.business_name,
2488
+ city: args.city,
2489
+ state: args.state,
2490
+ phone: args.phone,
2491
+ services: args.services,
2492
+ business_description: args.business_description,
2493
+ review_count: args.review_count ?? null,
2494
+ review_rating: args.review_rating ?? null,
2495
+ certifications: args.certifications ?? null,
2496
+ trust_signals: args.trust_signals ?? null,
2497
+ emergency_service: args.emergency_service ?? null,
2498
+ same_day: args.same_day ?? null,
2499
+ service_area: args.service_area ?? null,
2500
+ email: args.email ?? null,
2501
+ address: args.address ?? null,
2502
+ },
2503
+ allow_anonymous: true,
2504
+ });
2505
+
2506
+ if (result?.status !== "ready" || !result?.workspace_id) {
2507
+ return result;
2508
+ }
2509
+
2510
+ // Stash the bearer locally so subsequent persist_block + finalize calls
2511
+ // can authenticate. Stripped from the visible response.
2512
+ if (result._bearer_token) {
2513
+ rememberWorkspace({
2514
+ workspace_id: result.workspace_id,
2515
+ bearer_token: result._bearer_token,
2516
+ });
2517
+ }
2518
+
2519
+ // Operator-facing payload + structural enforcement that the IDE agent
2520
+ // does the block generation step before showing URLs to the user.
2521
+ const guardrail = [
2522
+ "⚠️ V2 WORKSPACE BOOTSTRAPPED — DO NOT END YET ⚠️",
2523
+ "",
2524
+ `The workspace "${args.business_name}" exists on the backend with default copy from the v1 personality system. To complete the v2 flow you MUST:`,
2525
+ "",
2526
+ " 1. For each block in v2.recommended_blocks:",
2527
+ " a. call get_block_skill({ block_name }) to load the SKILL.md prompt + prop schema",
2528
+ " b. use your LLM to generate props matching that schema, using v2.context as the input",
2529
+ " c. call persist_block({ workspace_id, block_name, generation_prompt, props })",
2530
+ " 2. call complete_workspace_v2({ workspace_id }) to validate the final state",
2531
+ " 3. ask the operator: " + JSON.stringify(result.operator_prompt ?? "What email should I use for your account?"),
2532
+ " 4. call finalize_workspace({ workspace_id, email }) to mint the admin URL",
2533
+ "",
2534
+ "DO NOT show the operator any URLs from this response yet — the page is rendering with v1 default copy until step 1 lands. Step 1 produces the niche-aware copy the operator is going to actually see.",
2535
+ ].join("\n");
2536
+
2537
+ const payload = {
2538
+ ok: true,
2539
+ DO_NOT_DISPLAY_TO_USER: guardrail,
2540
+ workspace: { id: result.workspace_id, slug: result.slug },
2541
+ configured: result.configured,
2542
+ v2: result.v2,
2543
+ next_step: {
2544
+ required: true,
2545
+ do_not_show_urls_until_blocks_persisted_and_email_collected: true,
2546
+ tool_to_call_first: "get_block_skill",
2547
+ then_for_each_block: "persist_block",
2548
+ then: "complete_workspace_v2",
2549
+ finally: "finalize_workspace",
2550
+ ask_user_verbatim: result.operator_prompt,
2551
+ },
2552
+ _pending_after_completion: {
2553
+ website_url: result.public_urls?.home ?? null,
2554
+ booking_url: result.public_urls?.book ?? null,
2555
+ intake_url: result.public_urls?.intake ?? null,
2556
+ },
2557
+ };
2558
+ return firstEver ? withFirstCallBanner(payload) : payload;
2559
+ },
2560
+ },
2561
+
2562
+ {
2563
+ name: "list_blocks",
2564
+ description:
2565
+ "List all v2 page-block primitives available in this SF backend. Returns each block's name, version, section type, one-line description, and the URL where its full SKILL.md (the generation prompt + prop schema) lives. Use this when you need to discover what blocks exist; for actual block content use get_block_skill.",
2566
+ inputSchema: obj({}),
2567
+ handler: async () => {
2568
+ const result = await api("GET", "/public/blocks/list", { allow_anonymous: true });
2569
+ return result;
2570
+ },
2571
+ },
2572
+
2573
+ {
2574
+ name: "get_block_skill",
2575
+ description:
2576
+ "Fetch the SKILL.md (the full generation prompt + prop schema + voice rules + worked examples + validator definitions) for one v2 page block. Returns raw markdown text. Read it carefully BEFORE generating props — the prop schema in the YAML frontmatter is enforced by the persist_block endpoint, and the validators run on every save. Generation that ignores the SKILL.md will fail validation and the operator will see worse output.",
2577
+ inputSchema: obj(
2578
+ { block_name: str("Block name. Use list_blocks to discover. As of v1.4: hero, services, faq.") },
2579
+ ["block_name"],
2580
+ ),
2581
+ handler: async (args) => {
2582
+ const path = `/public/blocks/${encodeURIComponent(args.block_name)}/skill`;
2583
+ // Custom fetch — the SKILL.md endpoint returns text/markdown, not JSON.
2584
+ const res = await fetch(`${API_INFO.base}${path}`);
2585
+ if (!res.ok) {
2586
+ const errBody = await res.text();
2587
+ throw new Error(`get_block_skill ${res.status}: ${errBody}`);
2588
+ }
2589
+ const skill_md = await res.text();
2590
+ return {
2591
+ block_name: args.block_name,
2592
+ skill_md,
2593
+ usage:
2594
+ "The frontmatter (between the --- markers) defines the prop schema and validators. The body is the generation prompt — read it as if it were addressed to you. Generate JSON matching the prop schema, then call persist_block.",
2595
+ };
2596
+ },
2597
+ },
2598
+
2599
+ {
2600
+ name: "persist_block",
2601
+ description:
2602
+ "Persist a v2 block instance. Call this after you've read the block's SKILL.md and generated props matching its schema. The server validates props (Zod schema + deterministic copy-quality validators), renders the block via the existing renderer, replaces the matching section in the workspace's landing page, and returns the public URL where the change is now visible. " +
2603
+ "For initial generation, omit `customization`. For operator-driven edits ('make the hero warmer', 'add a card about kids cuts'), pass `customization: { prompt }` — the operator's prompt is appended to the row's customization history (forever-frozen rule), and the new props replace the previous render. " +
2604
+ "Returns `validation_errors` on failure — if you see them, regenerate the props with the rules from SKILL.md applied more carefully and retry. Don't show validation errors to the operator; they're for you.",
2605
+ inputSchema: obj(
2606
+ {
2607
+ workspace_id: str("Workspace id from create_workspace_v2."),
2608
+ block_name: str("Block name (must match a get_block_skill name): hero, services, or faq."),
2609
+ generation_prompt: str(
2610
+ "The full prompt your LLM consumed to produce these props. Stored as the source of truth for re-renders. Include the workspace context (business name, services, etc.) — not just the SKILL.md body."
2611
+ ),
2612
+ props: {
2613
+ type: "object",
2614
+ description:
2615
+ "Block props matching the prop schema in the block's SKILL.md frontmatter. Validated server-side; mismatches return 422 with structured validation_errors.",
2616
+ additionalProperties: true,
2617
+ },
2618
+ customization: {
2619
+ type: "object",
2620
+ description:
2621
+ "Optional operator-customization layer. When set, append-only override of the initial generation. Use this when the operator says 'change X about my hero' rather than 'rewrite my hero'.",
2622
+ properties: {
2623
+ prompt: str("The operator's natural-language customization request."),
2624
+ source: str("Optional source identifier (e.g. 'claude-code/desktop-7af3') for audit logs."),
2625
+ },
2626
+ },
2627
+ },
2628
+ ["workspace_id", "block_name", "generation_prompt", "props"],
2629
+ ),
2630
+ handler: async (args) => {
2631
+ const ws = args.workspace_id;
2632
+ const result = await api("POST", "/workspace/v2/blocks", {
2633
+ body: {
2634
+ workspace_id: ws,
2635
+ block_name: args.block_name,
2636
+ generation_prompt: args.generation_prompt,
2637
+ props: args.props,
2638
+ customization: args.customization ?? null,
2639
+ },
2640
+ workspace_id: ws,
2641
+ });
2642
+ return result;
2643
+ },
2644
+ },
2645
+
2646
+ {
2647
+ name: "complete_workspace_v2",
2648
+ description:
2649
+ "Mark the v2 flow finished for a workspace. Returns which blocks landed vs. were skipped (skipped ones still render via the v1 default pipeline), plus the next steps. Call after every recommended_block has been persisted via persist_block. The operator-facing summary (admin URL, etc.) still requires finalize_workspace afterward.",
2650
+ inputSchema: obj(
2651
+ { workspace_id: str("Workspace id from create_workspace_v2.") },
2652
+ ["workspace_id"],
2653
+ ),
2654
+ handler: async (args) => {
2655
+ const ws = args.workspace_id;
2656
+ const result = await api("POST", "/workspace/v2/complete", {
2657
+ body: { workspace_id: ws },
2658
+ workspace_id: ws,
2659
+ });
2660
+ return result;
2661
+ },
2662
+ },
2436
2663
  ];
2437
2664
 
2438
2665
  export const TOOL_MAP = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
package/src/welcome.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // stripped. `create_full_workspace` is the only workspace-creation
9
9
  // path mentioned anywhere in this briefing.
10
10
 
11
- export const VERSION = "1.3.3";
11
+ export const VERSION = "1.4.0";
12
12
 
13
13
  export const WELCOME_MARKDOWN = `# SeldonFrame — create a real Business OS in one conversation
14
14
 
@@ -38,16 +38,20 @@ flow, regardless of what directory Claude Code is running from.
38
38
 
39
39
  ---
40
40
 
41
- ## The full happy path — 4 steps, NO EXCEPTIONS
41
+ ## The full happy path — 6 steps (v2 — PREFERRED)
42
42
 
43
- EVERY workspace creation flow has exactly these 4 steps, in order.
44
- Step 4 is MANDATORY. A flow that ends after step 2 or 3 is broken
45
- the operator gets no welcome email, no admin login, and no
46
- follow-up channel. Always finish with step 4.
43
+ As of v1.4.0 the workspace-creation flow is MCP-native: YOU (the IDE
44
+ agent) generate the high-stakes copy blocks (hero, services, faq) using
45
+ your own LLM, reading from a SKILL.md the SF backend serves. v1's
46
+ server-side personality system still runs underneath for everything
47
+ else (CRM, booking, intake, theme, pipeline) — v2 only owns the copy
48
+ surfaces where v1's layer-mismatch bugs hurt most.
47
49
 
48
- 1. **Ask for the business details.** When the user says "create a
49
- workspace," gather these conversationally one or two
50
- questions per turn, not all at once:
50
+ If you find yourself reaching for \`create_full_workspace\` instead of
51
+ \`create_workspace_v2\`, stop that's the legacy path. It still works
52
+ but the output quality is worse on niches outside SF's curated set.
53
+
54
+ 1. **Ask for the business details** (same as before — gather conversationally):
51
55
  - Business name
52
56
  - Industry (HVAC, plumbing, dental, legal, coaching, real-estate, agency, …)
53
57
  - City + state (US state code or full name; Canadian province also OK)
@@ -55,21 +59,15 @@ follow-up channel. Always finish with step 4.
55
59
  - Top 3-5 services / products
56
60
  - Brief description (1 sentence)
57
61
 
58
- 2. **Create the workspace atomically.** Call \`create_full_workspace\`.
59
- One call → the entire workspace deploys server-side: org +
60
- business profile + CRM with industry-specific pipeline + booking
61
- page with availability + intake form + themed landing page.
62
- Same input always produces the same output — no retries, no
63
- 404s, no Claude-Code orchestration.
62
+ 2. **Bootstrap the workspace via v2.** Call \`create_workspace_v2\`:
64
63
  \`\`\`
65
- create_full_workspace({
64
+ create_workspace_v2({
66
65
  business_name: "Pacific Coast Heating & Air",
67
66
  city: "San Diego",
68
67
  state: "CA",
69
68
  phone: "(555) 123-4567",
70
69
  services: ["AC Repair", "Heating Installation", "Indoor Air Quality"],
71
70
  business_description: "Family-owned residential HVAC contractor — heating, cooling, AC repair in the San Diego area.",
72
- // Optional but recommended — enrich the hero proof metrics:
73
71
  review_count: 950,
74
72
  review_rating: 4.7,
75
73
  trust_signals: ["licensed", "bonded", "insured"],
@@ -78,12 +76,45 @@ follow-up channel. Always finish with step 4.
78
76
  service_area: ["San Diego", "Chula Vista", "Oceanside"]
79
77
  })
80
78
  \`\`\`
81
- The response carries a guardrail (\`DO_NOT_DISPLAY_TO_USER\`) and
82
- intentionally OMITS the admin dashboard URL it does not exist
83
- yet. Do NOT present anything from this response to the operator.
84
- Move directly to step 3.
79
+ The response carries \`v2.recommended_blocks\` (which blocks YOU now
80
+ generate) and \`v2.context\` (the input to feed into each block's
81
+ prompt). Do NOT show URLs to the operator yet the page is still
82
+ rendering with v1 default copy.
83
+
84
+ 3. **For each block in \`v2.recommended_blocks\` — generate + persist.**
85
+ Iterate over them sequentially or in parallel; both work.
86
+ For each one:
87
+ \`\`\`
88
+ // a. Read the SKILL.md
89
+ const skill = await get_block_skill({ block_name: "hero" });
90
+ // skill.skill_md is markdown text. Read it carefully — the YAML
91
+ // frontmatter is the prop schema (enforced server-side); the
92
+ // body is YOUR generation prompt.
93
+
94
+ // b. Generate props with your own LLM. Use v2.context as input.
95
+ // Example prompt to yourself: "Following the SKILL.md below,
96
+ // produce a JSON object that satisfies its schema for the
97
+ // business described in <v2.context>. ..."
98
+ // Output: a JSON object matching the schema.
99
+
100
+ // c. Persist
101
+ await persist_block({
102
+ workspace_id,
103
+ block_name: "hero",
104
+ generation_prompt: "<the prompt you used, full text>",
105
+ props: { headline: "...", subhead: "...", cta_primary: {...}, ... }
106
+ });
107
+ \`\`\`
108
+ If \`persist_block\` returns \`validation_errors\`, regenerate with
109
+ the SKILL.md rules applied more carefully. Do NOT show validation
110
+ errors to the operator — they're for you to self-correct.
85
111
 
86
- 3. **Ask the operator for their email — VERBATIM.** Use exactly this
112
+ 4. **Mark v2 complete.** Call \`complete_workspace_v2({ workspace_id })\`.
113
+ Returns which blocks landed and any that were skipped. Skipped
114
+ blocks still render via v1 default copy — the workspace is fully
115
+ usable either way, but v2 blocks are higher quality.
116
+
117
+ 5. **Ask the operator for their email — VERBATIM.** Use exactly this
87
118
  wording so the operator understands why it's needed:
88
119
  > "What email should I use for your account? This is where
89
120
  > you'll get your login link and any notifications."
@@ -91,28 +122,24 @@ follow-up channel. Always finish with step 4.
91
122
  is required to keep their admin login alive past 7 days — then
92
123
  ask again. Don't move on without an answer.
93
124
 
94
- 4. **MANDATORY: call \`finalize_workspace({ workspace_id, email })\`.**
95
- This is the one tool call that closes the loop AND creates the
96
- admin dashboard URL (which doesn't exist until this runs):
97
- \`\`\`
98
- finalize_workspace({
99
- workspace_id: "<from-create_full_workspace-response>",
100
- email: "max@pacificcoasthvac.com",
101
- name: "Max"
102
- })
103
- \`\`\`
104
- It mints the admin auth token, sends the welcome email (with
105
- all the URLs and the admin link), captures the operator as a
106
- lead in our CRM, and returns a \`summary\` field with the
107
- formatted final output. PARAPHRASE that summary verbatim to the
108
- operator — that's how they see what was configured (CRM
109
- personality, pipeline stages, live URLs, admin link, email
110
- confirmation).
111
-
112
- Alternative: call \`collect_operator_email({ email })\` if you
113
- want finer control without the formatted summary. Either tool
114
- satisfies step 4; skipping both does not — and the operator
115
- is left with NO admin access at all.
125
+ 6. **MANDATORY: call \`finalize_workspace({ workspace_id, email })\`.**
126
+ This mints the admin auth token, sends the welcome email (with
127
+ all the URLs and the admin link), captures the operator as a lead
128
+ in CRM, and returns a \`summary\` field with the formatted final
129
+ output. PARAPHRASE that summary verbatim to the operator — that's
130
+ how they see what was configured.
131
+
132
+ ## v1 fallback (legacy — NOT preferred)
133
+
134
+ \`create_full_workspace\` still works as the v1 atomic creation path.
135
+ Use it ONLY when:
136
+ - The operator's IDE agent has no LLM (script context, no Anthropic key)
137
+ - A network failure prevents repeated tool calls and you need atomicity
138
+ - You're writing automated tests that don't want to think about block
139
+ generation
140
+
141
+ In normal interactive operator-facing flows, prefer v2 every time. The
142
+ v2 quality gradient over v1 is meaningful on long-tail niches.
116
143
 
117
144
  After step 4 the operator can customize their workspace through
118
145
  further natural-language requests ("change the headline to …",
@@ -123,9 +150,18 @@ further natural-language requests ("change the headline to …",
123
150
 
124
151
  ## What the tools do (operator language only)
125
152
 
126
- - **\`create_full_workspace\`** — the ONE workspace-creation tool.
127
- Atomic, server-side, deterministic. Always the first call for
128
- new workspaces.
153
+ - **\`create_workspace_v2\`** — PREFERRED workspace-creation tool (v1.4+).
154
+ MCP-native: bootstraps the workspace + returns the list of blocks YOU
155
+ generate using your own LLM. The first call for any new workspace.
156
+ - **\`list_blocks\`** — lists v2 page-block primitives available.
157
+ - **\`get_block_skill\`** — fetches one block's SKILL.md (the generation
158
+ prompt + prop schema you read before generating props).
159
+ - **\`persist_block\`** — saves a block instance you generated. Validates
160
+ + renders + replaces the matching section in the workspace's landing.
161
+ - **\`complete_workspace_v2\`** — marks the v2 flow finished, reports which
162
+ blocks landed.
163
+ - **\`create_full_workspace\`** — v1 atomic creation (legacy). Server-side,
164
+ deterministic. Use only when v2 is impossible.
129
165
  - **\`finalize_workspace\`** — MANDATORY closing call. Mints the
130
166
  admin auth token (the admin URL doesn't exist until this runs),
131
167
  bundles email collection (welcome email + lead capture), and
@@ -173,4 +209,4 @@ admin dashboard. Pre-fills their email automatically.
173
209
  <https://seldonframe.com> · **Discord:** <https://discord.gg/sbVUu976NW>
174
210
  `;
175
211
 
176
- export const FIRST_CALL_BANNER = `🚀 SeldonFrame is connected. Ready to create a live business OS every URL the create_full_workspace tool returns is real and works in any browser within seconds. NEVER create local files; always use the MCP tools. EVERY workspace creation flow must end with finalize_workspace({ workspace_id, email }) so the operator gets their welcome email + admin login — skipping it is a broken flow.`;
212
+ export const FIRST_CALL_BANNER = `🚀 SeldonFrame v1.4 is connected. PREFERRED workspace creation: create_workspace_v2 for each recommended_block: get_block_skill + persist_block complete_workspace_v2 finalize_workspace({ workspace_id, email }). The v2 flow puts YOUR LLM in charge of high-stakes copy (hero, services, faq) using one SKILL.md per block. Every URL is real and works in any browser within seconds. NEVER create local files. Skipping finalize_workspace leaves the operator with no admin login — always close the loop.`;