@seldonframe/mcp 1.51.0 → 1.53.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tools.js +169 -112
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seldonframe/mcp",
3
- "version": "1.51.0",
3
+ "version": "1.53.0",
4
4
  "mcpName": "io.github.seldonframe/seldonframe-mcp",
5
5
  "description": "Open-source GoHighLevel alternative for agencies. 146+ MCP tools that let Claude Code spin up white-labeled client workspaces — CRM, booking, intake forms, landing pages, AI chatbot, and pre-wired agent archetypes (speed-to-lead, missed-call-text-back, review-requester) — in minutes. AGPL-3.0.",
6
6
  "license": "AGPL-3.0-or-later",
package/src/tools.js CHANGED
@@ -60,11 +60,9 @@ export const TOOLS = [
60
60
  {
61
61
  name: "create_full_workspace",
62
62
  description:
63
- " DO NOT USE WHEN A URL IS PROVIDED use `create_workspace_from_url` instead. THIS tool is for STRUCTURED, PRE-EXTRACTED business info ONLY (manual paste, operator-typed fields, etc.). " +
64
- "PREFERRED for new workspaces FROM STRUCTURED INPUT. Atomic, server-side workspace creation: takes structured business info and creates everything in ONE call workspace, business profile, CRM with industry-specific pipeline stages, booking page with availability, intake form, themed landing page, all deployed with live URLs. " +
65
- "Use this instead of create_workspace + a long sequence of customization tools. The pipeline runs server-side with a fixed order same input always produces same output, no retries, no 404s. " +
66
- "MANDATORY FOLLOW-UP: After this returns `status: 'ready'`, ask the operator verbatim 'What email should I use for your account? This is where you'll get your login link and notifications.' Then call `finalize_workspace({ workspace_id, email })`. The admin dashboard URL is ONLY created by finalize_workspace — it does not exist in this response (so there's nothing for you to display prematurely). " +
67
- "Example: create_full_workspace({ business_name: 'Summit Air Comfort', city: 'Phoenix', state: 'AZ', phone: '(480) 555-2100', services: ['AC repair', 'heating installation', 'duct cleaning'], business_description: 'Residential and commercial HVAC in Phoenix', review_count: 950, review_rating: 4.7, trust_signals: ['licensed', 'bonded', 'insured'], emergency_service: true, same_day: true, service_area: ['Scottsdale', 'Tempe', 'Mesa'] })",
63
+ "Atomic workspace creation from pre-extracted business facts. Equivalent to create_workspace_v2 but single-call (no block-by-block iteration). Used as the follow-up after `create_workspace_from_url` for cases where the operator wants the workspace produced in one shot rather than block-iterated. " +
64
+ "Example: create_full_workspace({ business_name: 'Summit Air Comfort', city: 'Phoenix', state: 'AZ', phone: '(480) 555-2100', services: ['AC repair', 'heating installation', 'duct cleaning'], business_description: 'Residential and commercial HVAC in Phoenix', review_count: 950, review_rating: 4.7, trust_signals: ['licensed', 'bonded', 'insured'], emergency_service: true, same_day: true, service_area: ['Scottsdale', 'Tempe', 'Mesa'] }). " +
65
+ "MANDATORY FOLLOW-UP: same as create_workspace_v2 after this returns `status: 'ready'`, ask 'What email should I use for your account?' and call finalize_workspace({ workspace_id, email }).",
68
66
  inputSchema: obj(
69
67
  {
70
68
  business_name: str("Business display name (e.g. 'Summit Air Comfort')."),
@@ -101,19 +99,8 @@ export const TOOLS = [
101
99
  },
102
100
  email: str("Optional contact email surfaced in the landing footer (NOT the operator's account email — that comes via finalize_workspace)."),
103
101
  address: str("Optional business address."),
104
- // v1.48 — schema-level routing guard. The model MUST pass `true`
105
- // to confirm no URL was available. When the operator's input
106
- // contains a URL, this assertion is false (don't lie); use
107
- // `create_workspace_from_url` instead. This makes the routing
108
- // a hard schema constraint, not just description prose.
109
- confirmed_no_url_available: {
110
- type: "boolean",
111
- enum: [true],
112
- description:
113
- "MUST be `true`. Set to true ONLY if the operator did NOT provide a website URL (e.g. they pasted Google Maps text or typed structured fields). If a URL IS available, use `create_workspace_from_url` instead — do NOT lie here. The schema rejects calls without this confirmation.",
114
- },
115
102
  },
116
- ["business_name", "city", "state", "phone", "services", "business_description", "confirmed_no_url_available"]
103
+ ["business_name", "city", "state", "phone", "services", "business_description"]
117
104
  ),
118
105
  handler: async (args) => {
119
106
  const firstEver = isFirstEverCall();
@@ -489,33 +476,22 @@ export const TOOLS = [
489
476
  {
490
477
  name: "create_workspace_from_url",
491
478
  description:
492
- " MUST USE WHEN A URL IS PROVIDED this is the LEAN atomic URL-input path for agency client workspace creation (v1.47, ~10-30s). " +
493
- "PATTERNS THAT ROUTE HERE (do not even consider other workspace-creation tools when input matches): " +
494
- "'create a workspace for <URL>', 'set up <client> at <URL>', 'spin up <URL>', '<URL>' alone, or any operator message containing http://, https://, or a recognizable domain (.com/.io/.net/.co/.app/.dev/.us etc). " +
495
- "WHAT THIS TOOL DOES: backend crawls the URL extracts business info creates CRM + booking + intake + eval-gated AI chatbot wired to FAQs auto-pulled from the site. " +
496
- "DOES NOT GENERATE A LANDING PAGE BY DEFAULT — agency clients typically already have their own website (the URL the operator passed). The chatbot ships as an embed snippet (`chatbot_embed_snippet` in the response) which the agency pastes onto the client's existing site. " +
497
- "If you ALSO need a SeldonFrame-hosted landing page (only when the client has no site of their own), call `generate_landing_page({ workspace_id })` AFTER this returns — it's a separate ~30-60s opt-in step. " +
498
- "DO NOT use create_workspace_v2 or create_full_workspace when a URL is available. Those tools require PRE-EXTRACTED structured fields (business_name, services[], phone) and will produce INFERIOR results for URL input because they skip the URL crawler, the FAQ extractor, and the auto-chatbot build. " +
499
- "DO NOT manually WebFetch the URL first — the SeldonFrame backend's soul-compiler is already wired to scrape + extract. Forwarding pre-fetched HTML wastes a round-trip and bypasses the FAQ-from-URL pipeline. " +
500
- "Eval gate: chatbot must pass ≥10 of 11 safety + behavior scenarios to ship 'live'. White-label-ready under partner-agency attachment. " +
501
- "Returns: workspace + agent + chatbot_embed_snippet + faq_summary. MANDATORY FOLLOW-UP: ask the operator verbatim 'What email should I use for your account?' then call finalize_workspace({ workspace_id, email }) — the admin dashboard URL is ONLY created by finalize_workspace. " +
502
- "⭐ WHEN RENDERING THE RESPONSE BACK TO THE OPERATOR: lead with `primary_deliverable.embed_snippet` + `primary_deliverable.paste_instruction`. The chatbot embed snippet is the agency's HEADLINE deliverable — the agency pastes it onto their CLIENT'S existing website (e.g. dallasplumbing.com), not onto a SeldonFrame-hosted page. DO NOT lead the response summary with 'Public URLs' or a `*.app.seldonframe.com` subdomain. When `include_landing_page: false` (the default for URL flow), there is NO SeldonFrame-hosted landing page — the subdomain is for previewing the chatbot only; don't promote it as a deliverable.",
479
+ "Entry point for URL-based workspace creation. Returns instructions Claude follows: WebFetch the URL, optionally WebFetch up to 2 priority sub-pages (e.g. /about, /services, /contact, /pricing), extract structured business facts, ask the operator for any required field that can't be determined, then call `create_workspace_v2` with the result. " +
480
+ "THIS TOOL DOES NOT CREATE A WORKSPACE it returns the playbook. The workspace is created by the follow-up `create_workspace_v2` call (then per-block: get_block_skill + persist_block + complete_workspace_v2 + finalize_workspace). " +
481
+ "When operator provides ANY of: 'http://', 'https://', a domain (.com/.io/.net/.co/.app/.dev/.us etc), or 'create workspace for <URL>' call this tool. Do NOT manually WebFetch first; the instructions returned by this tool tell you exactly what to fetch and in what order. " +
482
+ "MANDATORY FOLLOW-UP: After create_workspace_v2 returns 'ready' and all blocks land via persist_block + complete_workspace_v2, ask the operator 'What email should I use for your account?' and call finalize_workspace({ workspace_id, email }).",
503
483
  inputSchema: obj(
504
484
  {
505
- url: str("Business website URL, e.g. https://dallasplumbing.com"),
485
+ url: str("Business website URL the operator provided, e.g. https://quigleyac.com"),
506
486
  },
507
487
  ["url"]
508
488
  ),
509
489
  handler: async (args) =>
510
- api("POST", "/workspace/create", {
511
- body: {
512
- url: args.url,
513
- include_chatbot: true,
514
- auto_extract_faq: true,
515
- include_landing_page: false,
516
- },
517
- allow_anonymous: true,
518
- }),
490
+ api(
491
+ "GET",
492
+ `/workspace/extract-instructions?url=${encodeURIComponent(args.url)}`,
493
+ { allow_anonymous: true }
494
+ ),
519
495
  },
520
496
  // v1.47 — explicit opt-in landing-page generator. create_workspace_from_url
521
497
  // defaults to NO landing page (the client already has their own site);
@@ -828,7 +804,8 @@ export const TOOLS = [
828
804
  "ONE-CALL CLOSING WRAPPER for the workspace creation flow. Bundles email collection (welcome email + lead capture via collect_operator_email) AND produces the final operator-facing summary (live URLs, what's configured, admin link). " +
829
805
  "Call this as the LAST step of every workspace creation. After create_workspace returns, ask the user 'What email should I use for your account? This is where you'll get your login link and any notifications.' Then call this tool with the email they give you. Returns a `summary` string Claude Code should paraphrase verbatim to the operator. " +
830
806
  "Use this instead of calling collect_operator_email directly when you want a single tool call to close the loop. Skipping this is the same as skipping email collection — leaves the operator with a one-shot URL and no recovery path. " +
831
- "Example: finalize_workspace({ email: 'max@precisionplumbing.com', name: 'Max' })",
807
+ "Example: finalize_workspace({ email: 'max@precisionplumbing.com', name: 'Max' }). " +
808
+ "The summary is agency-voice: addresses the operator AS an agency delivering for their SMB client, not as the workspace owner. When relaying to the operator, preserve the 'your client' framing throughout — don't rewrite to 'your workspace'.",
832
809
  inputSchema: obj(
833
810
  {
834
811
  email: str("Operator email — used as the welcome email recipient AND as the unique key for the SeldonFrame CRM lead."),
@@ -861,6 +838,10 @@ export const TOOLS = [
861
838
  const publicUrls = snapshot?.public_urls ?? {};
862
839
  const slug = snapshot?.workspace?.slug ?? null;
863
840
  const wsName = snapshot?.workspace?.name ?? "Your workspace";
841
+ const chatbot = snapshot?.chatbot ?? null;
842
+ const tier = snapshot?.tier ?? null;
843
+ const bookingInfo = snapshot?.booking ?? null;
844
+ const intakeInfo = snapshot?.intake ?? null;
864
845
  if (!publicUrls.home || !publicUrls.book || !publicUrls.intake) {
865
846
  throw new Error(
866
847
  "Snapshot did not return public_urls (home/book/intake). Re-check the workspace.",
@@ -924,65 +905,144 @@ export const TOOLS = [
924
905
  : null;
925
906
  const pipelineStages = personality?.pipeline?.stages ?? [];
926
907
 
927
- const lines = [
928
- `✅ ${wsName}'s Business OS is live.`,
929
- "",
930
- emailSent
931
- ? `📧 Welcome email sent to ${a.email}`
932
- : `⚠️ Welcome email NOT sent${emailError ? ` (${emailError})` : ""} — please retry collect_operator_email.`,
933
- "",
934
- "🌐 Public URLs:",
935
- ` • Website: ${publicUrls.home}`,
936
- ` • Booking: ${publicUrls.book}`,
937
- ` • Intake: ${publicUrls.intake}`,
938
- "",
939
- "🔐 Admin dashboard:",
940
- ` ${adminUrl}`,
941
- "",
942
- "What's configured:",
943
- ];
908
+ // 2026-05-15 Agency-voice product moment summary. See spec
909
+ // docs/superpowers/specs/2026-05-15-agency-output-product-moment-design.md
910
+ const intakeSpecialNote =
911
+ personality?.vertical === "hvac" || personality?.vertical === "plumbing"
912
+ ? "emergency-line fallback"
913
+ : "structured lead-qualification";
914
+
915
+ const tierLabel = tier?.current_tier_label ?? "Free";
916
+ const isPaid =
917
+ tier?.current_tier === "growth" || tier?.current_tier === "scale";
918
+ const isScale = tier?.current_tier === "scale";
919
+
920
+ const intakeFieldCount = intakeInfo?.field_count ?? 0;
921
+ const intakeTitle = intakeInfo?.title ?? "lead qualification";
922
+
923
+ const lines = [];
924
+
925
+ // Header
926
+ lines.push(`✅ ${wsName} — client OS shipped.`);
927
+ lines.push("");
928
+ lines.push("Your client's stack is wired and live:");
929
+ lines.push("");
930
+
931
+ // Public site
932
+ lines.push("🌐 Public site (paste a screenshot in your Slack)");
933
+ lines.push(` ${publicUrls.home}`);
934
+ lines.push("");
935
+
936
+ // Chatbot
937
+ if (chatbot && chatbot.embed_snippet) {
938
+ lines.push("🤖 AI chatbot — paste on the client's existing site (before </body>):");
939
+ lines.push(` ${chatbot.embed_snippet}`);
940
+ lines.push(
941
+ ` In ${String(chatbot.status).toUpperCase()} mode. Powered by your Claude Code key (swap in settings).`,
942
+ );
943
+ lines.push(
944
+ ` Publish live: publish_agent({ agent_id: "${chatbot.agent_id}", status: "live" })`,
945
+ );
946
+ } else {
947
+ lines.push("🤖 AI chatbot — scaffold pending. Retry:");
948
+ lines.push(` create_agent({ archetype: "website-chatbot", channel: "web_chat" })`);
949
+ }
950
+ lines.push("");
951
+
952
+ // Booking
953
+ lines.push("📋 Booking page (client's customers self-serve appointments)");
954
+ lines.push(` ${publicUrls.book}`);
955
+ lines.push("");
956
+
957
+ // Intake
958
+ lines.push(`📝 Intake form (${intakeFieldCount}-question ${intakeTitle})`);
959
+ lines.push(` ${publicUrls.intake}`);
960
+ lines.push("");
961
+
962
+ // Admin
963
+ lines.push("🔐 Your admin (CRM, pipeline, leads, deals)");
964
+ lines.push(` ${adminUrl}`);
965
+ lines.push("");
966
+
967
+ // Client portal
968
+ if (tier?.client_portal_url) {
969
+ lines.push("👥 Client portal (your client logs in here to see their leads + bookings)");
970
+ lines.push(` ${tier.client_portal_url}`);
971
+ if (isPaid) {
972
+ lines.push(` ✅ Active. Forward this URL to your client; they log in via magic email.`);
973
+ } else {
974
+ lines.push(` 🔒 Growth tier ($29/mo) unlocks this for your client. Preview it`);
975
+ lines.push(` yourself at the URL above right now.`);
976
+ }
977
+ lines.push("");
978
+ }
979
+
980
+ // What's wired
981
+ lines.push("What's wired:");
944
982
  if (personalityLabel) {
945
- lines.push(` • CRM personality: ${personalityLabel}`);
983
+ const stageCount = pipelineStages.length;
984
+ lines.push(` • ${personalityLabel} personality • ${stageCount}-stage CRM pipeline`);
946
985
  }
947
- if (pipelineStages.length > 0) {
948
- const stageNames = pipelineStages
949
- .map((s) => s?.name)
950
- .filter(Boolean)
951
- .join(" → ");
952
- lines.push(` • Pipeline: ${stageNames}`);
986
+ if (bookingInfo) {
987
+ lines.push(
988
+ ` • ${bookingInfo.hours_summary} bookings, ${bookingInfo.duration_minutes}-min slots`,
989
+ );
990
+ }
991
+ if (intakeInfo) {
992
+ lines.push(` • ${intakeFieldCount}-question intake with ${intakeSpecialNote}`);
993
+ }
994
+ if (chatbot) {
995
+ lines.push(` • AI chatbot trained on the homepage (FAQ scaffold ready to refine)`);
953
996
  }
954
- lines.push(` • Booking page, intake form, CRM, AI agents — all live`);
955
997
  lines.push(
956
998
  emailSent
957
- ? ` • Welcome email sent, onboarding started`
958
- : ` • Welcome email NOT yet sent (rerun finalize_workspace to retry)`
999
+ ? ` • Welcome email + admin link sent to ${a.email}`
1000
+ : ` • Welcome email NOT yet sent (rerun finalize_workspace to retry)`,
959
1001
  );
960
- // v1.34.0 — Brief, optional next-step menu surfaced AFTER the
961
- // operator's site is live. We don't lecture; we just list what's
962
- // available. Claude Code reads this and decides whether to mention
963
- // any of it based on the conversation vibe (e.g. user says
964
- // "perfect, ship it" → skip; user says "can it look more
965
- // impressive?" → call apply_motion_preset).
966
1002
  lines.push("");
967
- lines.push("Optional upgrades (when you're ready):");
968
- lines.push(` • Tune motion: apply_motion_preset({ preset: "subtle" | "balanced" | "editorial" | "minimal" })`);
969
- lines.push(` • Apply your brand kit: apply_design_md({ design_md_content }) if you have a DESIGN.md`);
970
- lines.push(` Import a Claude Design handoff: import_claude_design_handoff({ bundle })`);
971
- lines.push(` • Add real content: describe your services, pricing, FAQs in plain English`);
1003
+
1004
+ // What you can prompt next (5 agency-meaningful examples)
1005
+ lines.push("What you can prompt next:");
1006
+ lines.push(` "Refine the chatbot FAQ from the site" → update_website_chatbot`);
1007
+ lines.push(` "Add SMS missed-call-text-back automation" install_archetype`);
1008
+ lines.push(` • "Customize the hero with the client's brand voice" → customize_block`);
1009
+ lines.push(` • "Wire Google Calendar so bookings sync" → connect_integration`);
1010
+ lines.push(` • "Add a Spanish version of the landing page" → clone_workspace + translate`);
1011
+ lines.push("");
1012
+
1013
+ // Tier ladder (omit on Scale tier)
1014
+ if (!isScale) {
1015
+ lines.push(`Tier ladder (you're on ${tierLabel}):`);
1016
+ lines.push(` Free → 1 client workspace, everything above wired`);
1017
+ lines.push(` Growth $29/mo → 3 workspaces, client portal goes live, custom domain`);
1018
+ lines.push(` (e.g. crm.youragency.com), SMS/email automations`);
1019
+ lines.push(` Scale $99/mo → unlimited workspaces, full white-label, reseller pricing`);
1020
+ lines.push("");
1021
+ }
1022
+
1023
+ // Closer
1024
+ lines.push("Forward your client this admin link when ready. Or stay here and iterate.");
1025
+
972
1026
  const summary = lines.join("\n");
973
1027
 
974
1028
  return {
975
1029
  ok: emailSent || leadRecorded,
976
1030
  summary,
977
- workspace: {
978
- id: workspaceId,
979
- name: wsName,
980
- slug,
981
- },
1031
+ workspace: { id: workspaceId, name: wsName, slug },
982
1032
  website_url: publicUrls.home,
983
1033
  booking_url: publicUrls.book,
984
1034
  intake_url: publicUrls.intake,
985
1035
  admin_url: adminUrl,
1036
+ // NEW (2026-05-15): chatbot + tier surfacing
1037
+ chatbot_agent_id: chatbot?.agent_id ?? null,
1038
+ chatbot_embed_url: chatbot?.embed_url ?? null,
1039
+ chatbot_embed_snippet: chatbot?.embed_snippet ?? null,
1040
+ chatbot_status: chatbot?.status ?? null,
1041
+ client_portal_url: tier?.client_portal_url ?? null,
1042
+ client_portal_status: tier?.client_portal_status ?? null,
1043
+ current_tier: tier?.current_tier ?? "free",
1044
+ tier_features: tier?.tier_features ?? null,
1045
+ // Existing email/lead fields
986
1046
  email_sent: emailSent,
987
1047
  email_error: emailError,
988
1048
  lead_recorded: leadRecorded,
@@ -990,28 +1050,37 @@ export const TOOLS = [
990
1050
  lead_error: leadError,
991
1051
  personality: personalityLabel,
992
1052
  pipeline_stages: pipelineStages.map((s) => s?.name).filter(Boolean),
993
- // v1.34.0Structured options Claude Code can reason about
994
- // without parsing the human-facing summary string.
1053
+ // 2026-05-15agency-meaningful next-step examples
995
1054
  next_steps_available: [
996
1055
  {
997
- action: "apply_motion_preset",
998
- when: "operator says 'make it feel more premium', 'tone down animation', etc.",
999
- example: 'apply_motion_preset({ preset: "editorial" })',
1056
+ action: "publish_agent",
1057
+ when: "operator has reviewed the chatbot and is ready to take it live for the client's website",
1058
+ example: `publish_agent({ agent_id: "${chatbot?.agent_id ?? "ag_..."}", status: "live" })`,
1000
1059
  },
1001
1060
  {
1002
- action: "apply_design_md",
1003
- when: "operator has a DESIGN.md file with their brand tokens",
1004
- example: "apply_design_md({ design_md_content: <file content> })",
1061
+ action: "update_website_chatbot",
1062
+ when: "operator wants to refine the chatbot's FAQ before publishing",
1063
+ example: `update_website_chatbot({ workspace_id, faq: [{ q: '...', a: '...' }] })`,
1005
1064
  },
1006
1065
  {
1007
- action: "import_claude_design_handoff",
1008
- when: "operator just exported components from Claude Design",
1009
- example: "import_claude_design_handoff({ bundle: <bundle JSON> })",
1066
+ action: "install_archetype",
1067
+ when: "operator wants to wire pre-built automations (missed-call-text-back, speed-to-lead, review-requester)",
1068
+ example: `install_archetype({ archetype: "missed-call-text-back" })`,
1010
1069
  },
1011
1070
  {
1012
- action: "update_landing_content / configure_booking / customize_intake_form",
1013
- when: "operator wants to update specific page content, prices, services",
1014
- example: "(see get_workspace_state for the full surface map)",
1071
+ action: "customize_block",
1072
+ when: "operator wants to refine landing-page hero / services / FAQ with brand voice",
1073
+ example: `customize_block({ workspace_id, block_name: "hero", prompt: "make this feel more premium" })`,
1074
+ },
1075
+ {
1076
+ action: "connect_integration",
1077
+ when: "operator wants Google Calendar sync, Stripe payments, Twilio SMS",
1078
+ example: `connect_integration({ workspace_id, provider: "google_calendar" })`,
1079
+ },
1080
+ {
1081
+ action: "configure_llm_provider",
1082
+ when: "operator wants to swap from the default Claude Code key to a different Anthropic key",
1083
+ example: `configure_llm_provider({ workspace_id, provider: "anthropic", api_key: "sk-ant-..." })`,
1015
1084
  },
1016
1085
  ],
1017
1086
  };
@@ -3018,11 +3087,9 @@ export const TOOLS = [
3018
3087
  {
3019
3088
  name: "create_workspace_v2",
3020
3089
  description:
3021
- " DO NOT USE WHEN A URL IS PROVIDED use `create_workspace_from_url` instead. THIS tool is for STRUCTURED, PRE-EXTRACTED business info ONLY (typically a Google Maps paste the operator already parsed into fields). " +
3022
- "PREFERRED for STRUCTURED-INPUT workspaces (v1.4+) when no URL is available. 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. " +
3023
- "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 }). " +
3024
- "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. " +
3025
- "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.",
3090
+ "Create a workspace from pre-extracted business facts. Used as the follow-up call after `create_workspace_from_url` returns extraction instructions, OR directly when the operator provides structured info (no URL). " +
3091
+ "Flow: 1) call this tool with the business facts; 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 }). " +
3092
+ "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.",
3026
3093
  inputSchema: obj(
3027
3094
  {
3028
3095
  business_name: str("Business display name."),
@@ -3046,18 +3113,8 @@ export const TOOLS = [
3046
3113
  service_area: { type: "array", items: { type: "string" } },
3047
3114
  email: str("Optional contact email surfaced in the landing footer (NOT the operator's account email)."),
3048
3115
  address: str("Optional business address."),
3049
- // v1.48 — schema-level routing guard (same as create_full_workspace).
3050
- // Forces the model to acknowledge no URL was available, preventing
3051
- // accidental routing to v2 when create_workspace_from_url is the
3052
- // correct path.
3053
- confirmed_no_url_available: {
3054
- type: "boolean",
3055
- enum: [true],
3056
- description:
3057
- "MUST be `true`. Set to true ONLY if the operator did NOT provide a website URL. If a URL IS available, use `create_workspace_from_url` instead — do NOT lie here.",
3058
- },
3059
3116
  },
3060
- ["business_name", "city", "state", "phone", "services", "business_description", "confirmed_no_url_available"],
3117
+ ["business_name", "city", "state", "phone", "services", "business_description"],
3061
3118
  ),
3062
3119
  handler: async (args) => {
3063
3120
  const firstEver = isFirstEverCall();