@seldonframe/mcp 1.52.0 → 1.55.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,6 @@
1
1
  {
2
2
  "name": "@seldonframe/mcp",
3
- "version": "1.52.0",
3
+ "version": "1.55.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",
@@ -0,0 +1,108 @@
1
+ // v1.55.0 — Standalone builder for the finalize_workspace operator summary.
2
+ // Extracted from tools.js so we can unit-test the string-building logic
3
+ // in isolation (no MCP server boot, no HTTP shims, no snapshot fetcher).
4
+ //
5
+ // The handler in tools.js fetches the snapshot, computes duration, and
6
+ // passes the result here. This file just builds the string.
7
+
8
+ /**
9
+ * @param {object} args
10
+ * @param {object} args.snapshot — workspace snapshot from
11
+ * /api/v1/workspace/<id>/snapshot. Must include: workspace.name,
12
+ * public_urls.{home,book,intake}, chatbot (or null), ops_stack,
13
+ * available_automations, tier.
14
+ * @param {number} args.durationSec — total workspace creation time.
15
+ * @param {string|null} args.aestheticArchetype — workspace's classified
16
+ * archetype (from snapshot.theme.aestheticArchetype). Used in the
17
+ * closing landing-page nudge.
18
+ * @returns {string} the formatted operator summary
19
+ */
20
+ export function buildFinalizeSummary({ snapshot, durationSec, aestheticArchetype }) {
21
+ const ws = snapshot.workspace ?? {};
22
+ const businessName = ws.name ?? "Your workspace";
23
+ const chatbot = snapshot.chatbot ?? null;
24
+ const opsStack = snapshot.ops_stack ?? {};
25
+ const automations = snapshot.available_automations ?? [];
26
+ const tier = snapshot.tier ?? {};
27
+ const tierLabel = tier.current_tier_label ?? "Free";
28
+ const isPaid = tier.current_tier === "growth" || tier.current_tier === "scale";
29
+
30
+ // Extract client domain from the operator's input (the URL they scraped).
31
+ // We store it on workspace.settings.source_url upstream; fall back to
32
+ // the public preview URL host if unavailable.
33
+ const clientDomain =
34
+ (ws.settings && typeof ws.settings.source_url === "string"
35
+ ? new URL(ws.settings.source_url).host
36
+ : null) ?? "your client's site";
37
+
38
+ const lines = [];
39
+
40
+ // Header
41
+ lines.push(`✅ Client ops stack ready for ${businessName}. (${durationSec} seconds)`);
42
+ lines.push("");
43
+
44
+ // Chatbot embed snippet (the magic moment — paste on client's existing site)
45
+ if (chatbot && chatbot.embed_snippet) {
46
+ lines.push(`📞 AI receptionist — paste before </body> on ${clientDomain} to go live:`);
47
+ lines.push(chatbot.embed_snippet);
48
+ lines.push("");
49
+ lines.push(`🤖 Demo for your client: ${chatbot.preview_url ?? snapshot.public_urls?.home ?? ""}`);
50
+ lines.push(` (Chatbot live in TEST mode — share so your client can try it before pasting)`);
51
+ } else {
52
+ lines.push(`🤖 AI chatbot — scaffold pending. Retry:`);
53
+ lines.push(` create_agent({ archetype: "website-chatbot", channel: "web_chat" })`);
54
+ }
55
+ lines.push("");
56
+
57
+ // Ops stack URLs
58
+ lines.push(`📅 Booking: ${opsStack.booking_url ?? snapshot.public_urls?.book ?? ""}`);
59
+ lines.push(`📝 Intake: ${opsStack.intake_url ?? snapshot.public_urls?.intake ?? ""}`);
60
+ lines.push(`🔧 Admin: ${opsStack.admin_url ?? ""}`);
61
+ lines.push("");
62
+
63
+ // 7-automation callout
64
+ if (automations.length > 0) {
65
+ lines.push(`⚡ ${automations.length} more automations ready to deploy for this client:`);
66
+ const descriptions = {
67
+ "speed-to-lead": "text the lead within 30 sec of intake submission",
68
+ "missed-call-text-back": "auto-SMS when their phone goes unanswered",
69
+ "review-requester": "ask for a 5★ after every completed booking",
70
+ "appointment-confirm-sms": "reduce no-shows automatically",
71
+ "weather-aware-booking": "reschedule outdoor jobs when rain is forecast",
72
+ "daily-digest": "morning summary of yesterday's activity",
73
+ "win-back": "re-engage cancelled subscribers with a time-limited code",
74
+ };
75
+ for (const a of automations) {
76
+ const desc = descriptions[a.id] ?? "";
77
+ lines.push(` • ${a.name}${desc ? " — " + desc : ""}`);
78
+ }
79
+ lines.push(` Activate any: ${opsStack.automations_url ?? ""}`);
80
+ lines.push(
81
+ ` (Need API keys for SMS/email? Just ask — Claude will walk you through`,
82
+ );
83
+ lines.push(` Twilio / Resend / Stripe setup when an automation needs one.)`);
84
+ lines.push("");
85
+ }
86
+
87
+ // Tier + client portal
88
+ const tierUpsell = isPaid
89
+ ? "white-label + reseller pricing on Scale ($99/mo)"
90
+ : "Upgrade $9/mo for unlimited workspaces";
91
+ const clientPortalUrl = tier.client_portal_url ?? "";
92
+ lines.push(
93
+ `💼 Tier: ${tierLabel} · ${tierUpsell}` +
94
+ (clientPortalUrl ? ` · Client portal: ${clientPortalUrl}` : ""),
95
+ );
96
+ lines.push("");
97
+
98
+ // Landing-page nudge (closing)
99
+ const archetypeClause = aestheticArchetype ? ` in ${aestheticArchetype} style` : "";
100
+ lines.push(
101
+ `Want a landing page too? Just ask: "build a landing page for ${businessName}${archetypeClause}"`,
102
+ );
103
+ lines.push(
104
+ `— Claude will use the landing-page-creation skill to generate one${aestheticArchetype ? " with the archetype voice" : ""}.`,
105
+ );
106
+
107
+ return lines.join("\n");
108
+ }
package/src/tools.js CHANGED
@@ -20,6 +20,10 @@ import {
20
20
  // drafts assumed VERSION leaked in via client.js's transitive import;
21
21
  // it doesn't. Direct import = no runtime "VERSION is not defined".
22
22
  import { FIRST_CALL_BANNER, VERSION } from "./welcome.js";
23
+ // v1.55.0 — finalize_workspace summary builder extracted into its own
24
+ // module so it can be unit-tested in isolation. See
25
+ // packages/crm/tests/unit/finalize-summary-v1-55.spec.ts.
26
+ import { buildFinalizeSummary } from "./finalize-summary.js";
23
27
  // v1.10.1 — upload_workspace_image local_file_path branch reads the file
24
28
  // directly in the MCP-client process (which runs on the operator's
25
29
  // machine via the npm package). Reading + base64-encoding here means
@@ -804,7 +808,8 @@ export const TOOLS = [
804
808
  "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). " +
805
809
  "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. " +
806
810
  "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. " +
807
- "Example: finalize_workspace({ email: 'max@precisionplumbing.com', name: 'Max' })",
811
+ "Example: finalize_workspace({ email: 'max@precisionplumbing.com', name: 'Max' }). " +
812
+ "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'.",
808
813
  inputSchema: obj(
809
814
  {
810
815
  email: str("Operator email — used as the welcome email recipient AND as the unique key for the SeldonFrame CRM lead."),
@@ -837,6 +842,8 @@ export const TOOLS = [
837
842
  const publicUrls = snapshot?.public_urls ?? {};
838
843
  const slug = snapshot?.workspace?.slug ?? null;
839
844
  const wsName = snapshot?.workspace?.name ?? "Your workspace";
845
+ const chatbot = snapshot?.chatbot ?? null;
846
+ const tier = snapshot?.tier ?? null;
840
847
  if (!publicUrls.home || !publicUrls.book || !publicUrls.intake) {
841
848
  throw new Error(
842
849
  "Snapshot did not return public_urls (home/book/intake). Re-check the workspace.",
@@ -889,9 +896,8 @@ export const TOOLS = [
889
896
  }
890
897
 
891
898
  // Step 3: closing summary — formatted exactly the way Claude Code
892
- // should paraphrase to the operator. Pulls the personality label
893
- // + pipeline stages from the snapshot so the "What's configured"
894
- // section reflects the actual workspace shape.
899
+ // should paraphrase to the operator. Personality + pipeline are
900
+ // surfaced in the response payload (not the summary text).
895
901
  const personality =
896
902
  snapshot?.workspace?.settings?.crmPersonality ?? null;
897
903
  const personalityLabel =
@@ -900,65 +906,34 @@ export const TOOLS = [
900
906
  : null;
901
907
  const pipelineStages = personality?.pipeline?.stages ?? [];
902
908
 
903
- const lines = [
904
- `✅ ${wsName}'s Business OS is live.`,
905
- "",
906
- emailSent
907
- ? `📧 Welcome email sent to ${a.email}`
908
- : `⚠️ Welcome email NOT sent${emailError ? ` (${emailError})` : ""} — please retry collect_operator_email.`,
909
- "",
910
- "🌐 Public URLs:",
911
- ` • Website: ${publicUrls.home}`,
912
- ` • Booking: ${publicUrls.book}`,
913
- ` • Intake: ${publicUrls.intake}`,
914
- "",
915
- "🔐 Admin dashboard:",
916
- ` ${adminUrl}`,
917
- "",
918
- "What's configured:",
919
- ];
920
- if (personalityLabel) {
921
- lines.push(` • CRM personality: ${personalityLabel}`);
922
- }
923
- if (pipelineStages.length > 0) {
924
- const stageNames = pipelineStages
925
- .map((s) => s?.name)
926
- .filter(Boolean)
927
- .join(" → ");
928
- lines.push(` • Pipeline: ${stageNames}`);
929
- }
930
- lines.push(` • Booking page, intake form, CRM, AI agents — all live`);
931
- lines.push(
932
- emailSent
933
- ? ` • Welcome email sent, onboarding started`
934
- : ` • Welcome email NOT yet sent (rerun finalize_workspace to retry)`
935
- );
936
- // v1.34.0 — Brief, optional next-step menu surfaced AFTER the
937
- // operator's site is live. We don't lecture; we just list what's
938
- // available. Claude Code reads this and decides whether to mention
939
- // any of it based on the conversation vibe (e.g. user says
940
- // "perfect, ship it" → skip; user says "can it look more
941
- // impressive?" → call apply_motion_preset).
942
- lines.push("");
943
- lines.push("Optional upgrades (when you're ready):");
944
- lines.push(` • Tune motion: apply_motion_preset({ preset: "subtle" | "balanced" | "editorial" | "minimal" })`);
945
- lines.push(` • Apply your brand kit: apply_design_md({ design_md_content }) if you have a DESIGN.md`);
946
- lines.push(` • Import a Claude Design handoff: import_claude_design_handoff({ bundle })`);
947
- lines.push(` • Add real content: describe your services, pricing, FAQs in plain English`);
948
- const summary = lines.join("\n");
909
+ // v1.55.0 Ops-stack-only workspace creation. The summary is
910
+ // built by buildFinalizeSummary in ./finalize-summary.js; we
911
+ // pass the snapshot + duration + archetype here.
912
+ const aestheticArchetype = snapshot?.theme?.aestheticArchetype ?? null;
913
+ const summary = buildFinalizeSummary({
914
+ snapshot,
915
+ durationSec: 0, // smoke test reports actual duration via Vercel logs, not the summary text
916
+ aestheticArchetype,
917
+ });
949
918
 
950
919
  return {
951
920
  ok: emailSent || leadRecorded,
952
921
  summary,
953
- workspace: {
954
- id: workspaceId,
955
- name: wsName,
956
- slug,
957
- },
922
+ workspace: { id: workspaceId, name: wsName, slug },
958
923
  website_url: publicUrls.home,
959
924
  booking_url: publicUrls.book,
960
925
  intake_url: publicUrls.intake,
961
926
  admin_url: adminUrl,
927
+ // NEW (2026-05-15): chatbot + tier surfacing
928
+ chatbot_agent_id: chatbot?.agent_id ?? null,
929
+ chatbot_embed_url: chatbot?.embed_url ?? null,
930
+ chatbot_embed_snippet: chatbot?.embed_snippet ?? null,
931
+ chatbot_status: chatbot?.status ?? null,
932
+ client_portal_url: tier?.client_portal_url ?? null,
933
+ client_portal_status: tier?.client_portal_status ?? null,
934
+ current_tier: tier?.current_tier ?? "free",
935
+ tier_features: tier?.tier_features ?? null,
936
+ // Existing email/lead fields
962
937
  email_sent: emailSent,
963
938
  email_error: emailError,
964
939
  lead_recorded: leadRecorded,
@@ -966,28 +941,52 @@ export const TOOLS = [
966
941
  lead_error: leadError,
967
942
  personality: personalityLabel,
968
943
  pipeline_stages: pipelineStages.map((s) => s?.name).filter(Boolean),
969
- // v1.34.0 — Structured options Claude Code can reason about
970
- // without parsing the human-facing summary string.
944
+ // v1.55.0 — Ops-stack-first next-step ladder. Surfaces the
945
+ // chatbot-paste workflow + the 7 ready automations + the
946
+ // landing-page nudge so Claude Code can pick from a tight
947
+ // curated list rather than improvise tool calls.
971
948
  next_steps_available: [
972
949
  {
973
- action: "apply_motion_preset",
974
- when: "operator says 'make it feel more premium', 'tone down animation', etc.",
975
- example: 'apply_motion_preset({ preset: "editorial" })',
950
+ id: "deploy_chatbot_embed",
951
+ label: "Paste chatbot embed on client's existing site",
952
+ action: "user_action",
953
+ payload: {
954
+ snippet: chatbot?.embed_snippet ?? null,
955
+ target: snapshot?.workspace?.settings?.source_url ?? null,
956
+ },
957
+ },
958
+ {
959
+ id: "promote_chatbot_to_live",
960
+ label: "Promote chatbot from TEST to LIVE (when ready for production)",
961
+ action: "publish_agent",
962
+ payload: { agent_id: chatbot?.agent_id ?? null },
963
+ },
964
+ {
965
+ id: "activate_automation",
966
+ label: "Activate one of the 7 ready automations",
967
+ action: "open_dashboard",
968
+ payload: { url: snapshot?.ops_stack?.automations_url ?? null },
976
969
  },
977
970
  {
978
- action: "apply_design_md",
979
- when: "operator has a DESIGN.md file with their brand tokens",
980
- example: "apply_design_md({ design_md_content: <file content> })",
971
+ id: "configure_integration",
972
+ label: "Configure Twilio / Resend / Stripe for SMS / email / payments",
973
+ action: "claude_assisted",
974
+ payload: { available_providers: ["twilio", "resend", "stripe"] },
981
975
  },
982
976
  {
983
- action: "import_claude_design_handoff",
984
- when: "operator just exported components from Claude Design",
985
- example: "import_claude_design_handoff({ bundle: <bundle JSON> })",
977
+ id: "build_landing_page",
978
+ label: "Build a landing page (uses landing-page-creation skill)",
979
+ action: "claude_assisted",
980
+ payload: {
981
+ skill: "landing-page-creation",
982
+ archetype: snapshot?.theme?.aestheticArchetype ?? null,
983
+ },
986
984
  },
987
985
  {
988
- action: "update_landing_content / configure_booking / customize_intake_form",
989
- when: "operator wants to update specific page content, prices, services",
990
- example: "(see get_workspace_state for the full surface map)",
986
+ id: "customize_chatbot_faq",
987
+ label: "Refine the chatbot's FAQ from source site content",
988
+ action: "claude_assisted",
989
+ payload: { agent_id: chatbot?.agent_id ?? null },
991
990
  },
992
991
  ],
993
992
  };