@seldonframe/mcp 1.0.4 → 1.0.6

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@seldonframe/mcp",
3
- "version": "1.0.4",
4
- "description": "MCP server for SeldonFrame — AI-native Business OS platform. One command creates a real hosted workspace with CRM, booking, intake, and Brain v2. v1.0.4: hardens the admin URL surface so operators always see the bearer-token /admin/<id>?token=... URL, not the legacy /switch-workspace login-required path.",
3
+ "version": "1.0.6",
4
+ "description": "MCP server for SeldonFrame — AI-native Business OS platform. One command creates a real, hosted workspace with a website, booking page, intake form, CRM, and AI agents. v1.0.6: NEVER-CREATE-LOCAL-FILES rule baked into the server's system instructions and into the create_workspace tool description so Claude Code reliably uses the MCP tool even when running inside a SeldonFrame source repo; rewritten create_workspace response payload with a copy-paste-ready operator summary (live URLs + email prompt) and zero internal jargon (no Soul / Cal.diy / Formbricks / Brain v2 references in operator-facing copy); welcome.md fully rewritten in operator language.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "seldonframe-mcp": "src/index.js"
package/src/client.js CHANGED
@@ -139,6 +139,18 @@ export async function api(method, path, opts = {}) {
139
139
  };
140
140
  if (auth) headers.Authorization = auth;
141
141
 
142
+ // May 1, 2026 — inject x-org-id from the resolved workspace for
143
+ // EVERY tool call. Several CRM endpoints (contacts, deals,
144
+ // bookings, activities) use guardApiRequest which strictly
145
+ // requires x-org-id. Without this header, the bearer token alone
146
+ // returns 400 "Missing x-org-id" even though the bearer is
147
+ // workspace-scoped. Tools can opt out by passing
148
+ // `extra_headers: { "x-org-id": null }` if a route needs the
149
+ // header omitted, but the default is to send it.
150
+ if (workspace_id && !("x-org-id" in headers) && !("X-Org-ID" in headers)) {
151
+ headers["x-org-id"] = workspace_id;
152
+ }
153
+
142
154
  const res = await fetch(`${API_BASE}${path}`, {
143
155
  method,
144
156
  headers,
package/src/tools.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  api,
3
+ API_INFO,
3
4
  fetchText,
4
5
  forgetWorkspace,
5
6
  htmlToText,
@@ -41,18 +42,64 @@ export const TOOLS = [
41
42
  {
42
43
  name: "create_workspace",
43
44
  description:
44
- "Create a real, hosted workspace on <slug>.app.seldonframe.com with CRM, Cal.diy booking, Formbricks intake, and Brain v2 pre-installed. The first workspace requires no API key. Example: create_workspace({ name: 'Dental Clinic Laval', source: 'https://mysite.com' })",
45
+ "Create a LIVE, hosted SeldonFrame workspace with a public website, booking page, intake form, CRM, and AI agents all on a real <slug>.app.seldonframe.com subdomain. Returns live URLs that work in any browser within seconds. " +
46
+ "THIS IS THE ONLY WAY to create a SeldonFrame workspace. Do NOT create local files (no soul.json, no scaffolded directory), do NOT call loadSoulPackage, do NOT modify the working directory — even if Claude Code is running inside the SeldonFrame source repo. Every workspace is a hosted production deployment. " +
47
+ "The first workspace is free and requires no API key. When the user mentions a phone, email, address, list of services, or testimonials, pass them as structured fields so the landing page renders with their real content immediately. " +
48
+ "Example: create_workspace({ name: 'Precision Plumbing Co', phone: '(555) 123-4567', business_description: 'Family-owned residential plumbing in Austin.', services: [{ name: 'Drain Cleaning' }, { name: 'Water Heater Repair' }, { name: 'Leak Detection' }] })",
45
49
  inputSchema: obj(
46
50
  {
47
51
  name: str("Human-readable workspace name."),
48
- source: str("Optional URL or description to seed the workspace's Soul from."),
52
+ source: str("Optional URL or description to seed the business profile from."),
53
+ phone: str("Operator's business phone (any format — we render as-is). Renders in nav, hero, and footer when set."),
54
+ email: str("Optional contact email surfaced in the landing footer."),
55
+ address: str("Optional business address. Comma-separated street, city, region, postal, country renders as-is."),
56
+ tagline: str("Short hero tagline (one line)."),
57
+ business_description: str("One paragraph about the business (used for hero subhead + about section)."),
58
+ services: {
59
+ type: "array",
60
+ description:
61
+ "List of services / products / offerings the business provides. Renders as the services grid on the landing page. Each item: { name, description? }.",
62
+ items: {
63
+ type: "object",
64
+ properties: {
65
+ name: { type: "string" },
66
+ description: { type: "string" },
67
+ },
68
+ required: ["name"],
69
+ },
70
+ },
71
+ testimonials: {
72
+ type: "array",
73
+ description:
74
+ "Optional customer testimonials to seed the landing page's testimonials section. Each item: { quote, name?, role?, company? }.",
75
+ items: {
76
+ type: "object",
77
+ properties: {
78
+ quote: { type: "string" },
79
+ name: { type: "string" },
80
+ role: { type: "string" },
81
+ company: { type: "string" },
82
+ },
83
+ required: ["quote"],
84
+ },
85
+ },
49
86
  },
50
87
  ["name"],
51
88
  ),
52
89
  handler: async (args) => {
53
90
  const firstEver = isFirstEverCall();
54
91
  const result = await api("POST", "/workspace/create", {
55
- body: { name: args.name, source: args.source ?? null },
92
+ body: {
93
+ name: args.name,
94
+ source: args.source ?? null,
95
+ phone: args.phone ?? null,
96
+ email: args.email ?? null,
97
+ address: args.address ?? null,
98
+ tagline: args.tagline ?? null,
99
+ business_description: args.business_description ?? null,
100
+ services: args.services ?? null,
101
+ testimonials: args.testimonials ?? null,
102
+ },
56
103
  allow_anonymous: true,
57
104
  });
58
105
  const ws = result.workspace ?? result;
@@ -77,8 +124,34 @@ export const TOOLS = [
77
124
  Object.entries(rawUrls).filter(([key]) => !key.startsWith("admin_")),
78
125
  )
79
126
  : null;
127
+ // May 2, 2026 — operator-facing response payload.
128
+ //
129
+ // Goals: lead with the live URLs (operators care about those
130
+ // first), prompt for the email next (the keystone of the
131
+ // onboarding loop), and never surface internal jargon
132
+ // (no "Soul" / "Cal.diy" / "Formbricks" / "Brain v2" /
133
+ // "loadSoulPackage" / etc).
134
+ //
135
+ // The `summary` string is what Claude Code should paraphrase to
136
+ // the operator verbatim — it's pre-formatted with bullets so
137
+ // copy-paste lands cleanly.
138
+ const websiteUrl = publicUrls?.home ?? null;
139
+ const bookingUrl = publicUrls?.book ?? null;
140
+ const intakeUrl = publicUrls?.intake ?? null;
141
+ const summaryLines = [`${ws.name} is live!`, ""];
142
+ if (websiteUrl) summaryLines.push(`- Website: ${websiteUrl}`);
143
+ if (bookingUrl) summaryLines.push(`- Book a call: ${bookingUrl}`);
144
+ if (intakeUrl) summaryLines.push(`- Service request form: ${intakeUrl}`);
145
+ if (adminUrl) summaryLines.push(`- Admin dashboard: ${adminUrl}`);
146
+ summaryLines.push("");
147
+ summaryLines.push(
148
+ "What's your email? I'll send you all these links and set up your admin login."
149
+ );
150
+ const summary = summaryLines.join("\n");
151
+
80
152
  const payload = {
81
153
  ok: true,
154
+ summary,
82
155
  workspace: {
83
156
  id,
84
157
  name: ws.name,
@@ -86,23 +159,23 @@ export const TOOLS = [
86
159
  tier: ws.tier ?? "free",
87
160
  created_at: ws.created_at,
88
161
  },
89
- // Admin Dashboard (bookmark this!): the one URL operators need.
162
+ // Live URLs present these to the operator first. Public
163
+ // ones first, admin URL second.
164
+ website_url: websiteUrl,
165
+ booking_url: bookingUrl,
166
+ intake_url: intakeUrl,
90
167
  admin_url: adminUrl,
91
168
  admin_url_expires_at: result.bearer_token_expires_at ?? null,
92
- admin_url_message: adminUrl
93
- ? `⚡ Admin Dashboard (bookmark this!): ${adminUrl}\nClick to open the dashboard — no signup needed. Token expires in 7 days; re-mint with list_workspaces({}) when it does.`
94
- : null,
95
- urls: publicUrls,
96
169
  public_urls: result.public_urls ?? publicUrls,
97
- installed: result.installed ?? ["crm", "caldiy-booking", "formbricks-intake", "brain-v2"],
98
- next: [
99
- adminUrl
100
- ? `⚡ Admin Dashboard (bookmark this!): ${adminUrl} (paste into your browser; no signup needed; token expires in 7 days)`
101
- : null,
102
- "install_vertical_pack({ pack: 'real-estate' }) // or 'dental', 'legal'",
103
- "fetch_source_for_soul({ url: 'https://yoursite.com' }) → submit_soul({ soul })",
104
- "get_workspace_snapshot({}) read workspace state to reason about next steps",
105
- ].filter(Boolean),
170
+ // Email collection is the next required step. Claude Code
171
+ // should ask the operator for their email and then call
172
+ // collect_operator_email — DO NOT skip this.
173
+ next_step: {
174
+ ask_user:
175
+ "What's your email? I'll send you all these links and set up your admin login.",
176
+ tool_to_call: "collect_operator_email",
177
+ tool_args_template: { email: "<operator_email>", name: "<optional>" },
178
+ },
106
179
  };
107
180
  return firstEver ? withFirstCallBanner(payload) : payload;
108
181
  },
@@ -207,6 +280,178 @@ export const TOOLS = [
207
280
  };
208
281
  },
209
282
  },
283
+ {
284
+ name: "send_welcome_email",
285
+ description:
286
+ "Email the active workspace's four key URLs (landing, booking, intake, admin dashboard) to a user. Use this AFTER create_workspace, only when the user has explicitly given their email — never auto-send. The admin URL is bearer-token-scoped and expires in 7 days. Example: send_welcome_email({ email: 'alice@example.com', name: 'Alice' }).",
287
+ inputSchema: obj(
288
+ {
289
+ email: str("Recipient email address."),
290
+ name: str("Optional recipient name (used in the greeting)."),
291
+ workspace_id: str("Optional workspace override. Defaults to active workspace."),
292
+ },
293
+ ["email"],
294
+ ),
295
+ handler: async (a) => {
296
+ const workspaceId = a.workspace_id ?? getDefaultWorkspace();
297
+ if (!workspaceId) {
298
+ throw new Error(
299
+ "No workspace selected. Run create_workspace({ name: '…' }) first, or pass workspace_id.",
300
+ );
301
+ }
302
+ const bearer = getWorkspaceBearer(workspaceId);
303
+ if (!bearer) {
304
+ throw new Error(
305
+ `No local bearer token for workspace ${workspaceId}. This device did not create it. Re-run create_workspace or switch to the device that did.`,
306
+ );
307
+ }
308
+
309
+ const snapshot = await api(
310
+ "GET",
311
+ `/workspace/${encodeURIComponent(workspaceId)}/snapshot`,
312
+ { workspace_id: workspaceId },
313
+ );
314
+ const publicUrls = snapshot?.public_urls ?? {};
315
+ if (!publicUrls.home || !publicUrls.book || !publicUrls.intake) {
316
+ throw new Error(
317
+ "Snapshot did not return public_urls (home/book/intake). Re-check the workspace.",
318
+ );
319
+ }
320
+
321
+ // Construct the bearer-scoped admin URL from the API base.
322
+ // API_INFO.base is `<host>/api/v1` — strip that suffix to get the app host.
323
+ const appHost = API_INFO.base.replace(/\/api\/v1\/?$/, "");
324
+ const adminUrl = `${appHost}/admin/${encodeURIComponent(workspaceId)}?token=${encodeURIComponent(bearer)}`;
325
+
326
+ await api("POST", "/email/send-welcome", {
327
+ body: {
328
+ email: a.email,
329
+ name: a.name ?? null,
330
+ workspace: {
331
+ landing_url: publicUrls.home,
332
+ booking_url: publicUrls.book,
333
+ intake_url: publicUrls.intake,
334
+ admin_url: adminUrl,
335
+ },
336
+ },
337
+ workspace_id: workspaceId,
338
+ });
339
+
340
+ return {
341
+ ok: true,
342
+ message: `Welcome email sent to ${a.email}`,
343
+ };
344
+ },
345
+ },
346
+ {
347
+ name: "collect_operator_email",
348
+ description:
349
+ "Onboarding-loop helper: ask the operator for their email post-create_workspace, then send the welcome email + record them as a lead in SeldonFrame's own CRM. Combines send_welcome_email + a lead-capture call so a single tool call closes the loop. Call this BEFORE further customization (configure_booking / customize_intake_form / install_vertical_pack) so the operator gets a permanent inbox record of their links. Example: collect_operator_email({ email: 'max@desertcool.com', name: 'Max' })",
350
+ inputSchema: obj(
351
+ {
352
+ email: str("Operator email — used as the welcome email recipient AND as the unique key for the SeldonFrame CRM lead."),
353
+ name: str("Optional operator name (used in the email greeting and on the CRM lead)."),
354
+ workspace_id: str("Optional workspace override. Defaults to the workspace just created."),
355
+ },
356
+ ["email"],
357
+ ),
358
+ handler: async (a) => {
359
+ const workspaceId = a.workspace_id ?? getDefaultWorkspace();
360
+ if (!workspaceId) {
361
+ throw new Error(
362
+ "No workspace selected. Run create_workspace({ name: '…' }) first, or pass workspace_id.",
363
+ );
364
+ }
365
+ const bearer = getWorkspaceBearer(workspaceId);
366
+ if (!bearer) {
367
+ throw new Error(
368
+ `No local bearer token for workspace ${workspaceId}. This device did not create it. Re-run create_workspace or switch to the device that did.`,
369
+ );
370
+ }
371
+
372
+ // Step 1: pull the workspace snapshot so we have the public URLs
373
+ // for the welcome email.
374
+ const snapshot = await api(
375
+ "GET",
376
+ `/workspace/${encodeURIComponent(workspaceId)}/snapshot`,
377
+ { workspace_id: workspaceId },
378
+ );
379
+ const publicUrls = snapshot?.public_urls ?? {};
380
+ const slug = snapshot?.workspace?.slug ?? null;
381
+ if (!publicUrls.home || !publicUrls.book || !publicUrls.intake) {
382
+ throw new Error(
383
+ "Snapshot did not return public_urls (home/book/intake). Re-check the workspace.",
384
+ );
385
+ }
386
+ const appHost = API_INFO.base.replace(/\/api\/v1\/?$/, "");
387
+ const adminUrl = `${appHost}/admin/${encodeURIComponent(workspaceId)}?token=${encodeURIComponent(bearer)}`;
388
+
389
+ // Step 2: send the welcome email. Failures here are surfaced
390
+ // (operator told us their email, we owe them the email) but
391
+ // don't block the lead-capture step below.
392
+ let emailSent = false;
393
+ let emailError = null;
394
+ try {
395
+ await api("POST", "/email/send-welcome", {
396
+ body: {
397
+ email: a.email,
398
+ name: a.name ?? null,
399
+ workspace: {
400
+ landing_url: publicUrls.home,
401
+ booking_url: publicUrls.book,
402
+ intake_url: publicUrls.intake,
403
+ admin_url: adminUrl,
404
+ },
405
+ },
406
+ workspace_id: workspaceId,
407
+ });
408
+ emailSent = true;
409
+ } catch (err) {
410
+ emailError = err?.message ?? String(err);
411
+ }
412
+
413
+ // Step 3: record the operator as a lead in SeldonFrame's own
414
+ // CRM workspace. Anonymous endpoint — no bearer required, ops
415
+ // workspace ID is server-side env only. Soft-failure: if the
416
+ // ops workspace isn't configured we still return ok.
417
+ let leadRecorded = false;
418
+ let leadId = null;
419
+ let leadError = null;
420
+ try {
421
+ const leadResp = await api("POST", "/leads/operator-signup", {
422
+ body: {
423
+ email: a.email,
424
+ name: a.name ?? null,
425
+ source_workspace_id: workspaceId,
426
+ source_workspace_slug: slug,
427
+ source: "mcp-onboarding",
428
+ },
429
+ allow_anonymous: true,
430
+ });
431
+ leadRecorded = Boolean(leadResp?.recorded);
432
+ leadId = leadResp?.lead_id ?? null;
433
+ } catch (err) {
434
+ leadError = err?.message ?? String(err);
435
+ }
436
+
437
+ return {
438
+ ok: emailSent || leadRecorded,
439
+ email_sent: emailSent,
440
+ email_error: emailError,
441
+ lead_recorded: leadRecorded,
442
+ lead_id: leadId,
443
+ lead_error: leadError,
444
+ message: emailSent
445
+ ? `Welcome email sent to ${a.email}. Check your inbox — the admin URL is in there too.`
446
+ : `Could not send welcome email${emailError ? `: ${emailError}` : ""}. Lead ${leadRecorded ? "recorded" : "not recorded"}.`,
447
+ next: [
448
+ "configure_booking({ title, duration_minutes, description }) — tune the booking page if you collected business hours",
449
+ "customize_intake_form({ ... }) — match your intake to the workspace's lead-qualification questions",
450
+ "install_vertical_pack({ pack: '<industry>' }) — add domain-specific objects, fields, and views",
451
+ ],
452
+ };
453
+ },
454
+ },
210
455
  {
211
456
  name: "revoke_bearer",
212
457
  description:
@@ -394,10 +639,10 @@ export const TOOLS = [
394
639
  {
395
640
  name: "install_vertical_pack",
396
641
  description:
397
- "Install a vertical pack (e.g. 'real-estate', 'dental', 'legal'). Adds domain-specific objects, fields, views.",
642
+ "Set up an industry template (real-estate, dental, legal, …). Adds industry-specific objects, fields, and views to the CRM and pre-fills the booking page + intake form for that line of work.",
398
643
  inputSchema: obj(
399
644
  {
400
- pack: str("Pack slug, e.g. 'real-estate', 'dental', 'legal'."),
645
+ pack: str("Industry template name, e.g. 'real-estate', 'dental', 'legal'."),
401
646
  workspace_id: str("Optional workspace override."),
402
647
  },
403
648
  ["pack"],
@@ -413,10 +658,10 @@ export const TOOLS = [
413
658
  {
414
659
  name: "install_caldiy_booking",
415
660
  description:
416
- "Install the Cal.diy booking block (event types, availability, bookings). Example: install_caldiy_booking({})",
661
+ "Install the booking page (event types, availability, scheduled bookings). Example: install_caldiy_booking({})",
417
662
  inputSchema: obj({
418
663
  workspace_id: str("Optional workspace override."),
419
- config: { type: "object", description: "Optional Cal.diy configuration overrides." },
664
+ config: { type: "object", description: "Optional booking-page configuration overrides." },
420
665
  }),
421
666
  handler: async (a) => {
422
667
  const ws = wsOrDefault(a.workspace_id);
@@ -429,10 +674,10 @@ export const TOOLS = [
429
674
  {
430
675
  name: "install_formbricks_intake",
431
676
  description:
432
- "Install a Formbricks intake form (surveys, conditional logic, contact sync). Example: install_formbricks_intake({})",
677
+ "Install an intake form (questions, conditional logic, automatic CRM sync). Example: install_formbricks_intake({})",
433
678
  inputSchema: obj({
434
679
  workspace_id: str("Optional workspace override."),
435
- form_id: str("Optional existing Formbricks form id to bind."),
680
+ form_id: str("Optional existing intake-form id to bind."),
436
681
  }),
437
682
  handler: async (a) => {
438
683
  const ws = wsOrDefault(a.workspace_id);
@@ -459,7 +704,7 @@ export const TOOLS = [
459
704
  {
460
705
  name: "fetch_source_for_soul",
461
706
  description:
462
- "Fetch a URL and return normalized text (headings + body, up to 256KB). Use this to gather raw content; then you (the caller) extract a structured Soul and submit it with submit_soul. Zero LLM cost to Seldon — extraction runs in this session.",
707
+ "Fetch a URL and return normalized text (headings + body, up to 256KB). Use this to gather raw content from the operator's existing website; then extract a structured business profile and save it with submit_soul. Zero LLM cost to SeldonFrame — extraction runs in this session.",
463
708
  inputSchema: obj(
464
709
  {
465
710
  url: str("Absolute URL to fetch."),
@@ -478,7 +723,7 @@ export const TOOLS = [
478
723
  truncated,
479
724
  text,
480
725
  next: [
481
- "Extract a Soul object: { mission, audience, tone, offerings[], differentiators[], faqs[] }",
726
+ "Extract a business profile: { mission, audience, tone, offerings[], differentiators[], faqs[] }",
482
727
  "submit_soul({ soul: <extracted> })",
483
728
  ],
484
729
  };
@@ -487,13 +732,13 @@ export const TOOLS = [
487
732
  {
488
733
  name: "submit_soul",
489
734
  description:
490
- "Submit a compiled Soul object to the active workspace. The caller is expected to have produced the structured Soul from fetch_source_for_soul output or user conversation.",
735
+ "Save a business profile to the active workspace. The profile drives the landing page, intake form copy, and AI-agent context. Call this after fetch_source_for_soul or after gathering details from the user. Triggers a re-render of the public landing page so changes are visible immediately.",
491
736
  inputSchema: obj(
492
737
  {
493
738
  soul: {
494
739
  type: "object",
495
740
  description:
496
- "Structured Soul. Expected keys: mission, audience, tone, offerings, differentiators, faqs. Additional keys allowed.",
741
+ "Business profile. Expected keys: business_name, tagline, soul_description, phone, email, address, offerings, faqs, testimonials. Additional keys allowed — they're preserved for future use.",
497
742
  },
498
743
  workspace_id: str("Optional workspace override."),
499
744
  },
package/src/welcome.js CHANGED
@@ -1,132 +1,130 @@
1
- export const VERSION = "1.0.2";
2
-
3
- export const WELCOME_MARKDOWN = `# SeldonFrameyour AI-native Business OS
4
-
5
- One command creates a real, hosted workspace with CRM, Cal.diy booking,
6
- Formbricks intake, and Brain v2 — live on \`<slug>.app.seldonframe.com\`.
7
- No signup, no key, no setup. Just build.
8
-
9
- ---
10
-
11
- ## Architecture: YOU are the reasoning engine
12
-
13
- SeldonFrame's backend is pure state + artifacts. Natural-language reasoning
14
- happens right here in this Claude Code session YOU interpret the user's
15
- intent and call the appropriate typed tool. The backend applies the change
16
- deterministically. Zero backend LLM cost means the free tier is genuinely
17
- free forever.
18
-
19
- ---
20
-
21
- ## Quick start describe your business
22
-
23
- To create a personalized workspace in a single turn, ask the user to paste
24
- this template into Claude Code and fill in their details:
25
-
26
- \`\`\`text
27
- Create a workspace for my business:
28
- - Business name: [your business name]
29
- - Industry: [e.g., hvac, dental, legal, coaching, real-estate, salon, auto-repair]
30
- - Location: [city, state/province]
31
- - Operating hours: [e.g., Mon-Sat 7am-7pm]
32
- - Team size: [number of people / trucks / stations]
33
- - Services offered: [list your main services]
34
- - Website: [URL, if you have one]
35
- \`\`\`
36
-
37
- When the user replies with that filled in, YOU should orchestrate the
38
- following tool sequence (each call's response is structured — chain them):
39
-
40
- 1. \`create_workspace({ name: "<business name>", source: "<website if provided, else a 1-paragraph description>" })\`
41
- mints the hosted workspace + bearer token. The \`source\` arg seeds the Soul.
42
- 2. If \`industry\` is provided, call \`install_vertical_pack({ pack: "<industry-slug>" })\`
43
- adds domain-specific objects, fields, and views.
44
- Built-in packs: \`real-estate-agency\`. For other industries, the backend
45
- synthesizes a custom pack via \`/api/v1/verticals/generate\`. If a builtin
46
- pack matches, prefer it; otherwise call generate first, then install.
47
- 3. If \`hours\` is provided, call \`configure_booking({ title, duration_minutes, description })\`
48
- sets the booking page defaults. Inline the parsed hours into \`description\`
49
- (the booking schema doesn't take a per-day hours object yet).
50
- 4. If \`website\` was provided, the \`source\` URL passed to step 1 already
51
- triggered a Soul fetch. Confirm via \`get_workspace_snapshot({})\` and
52
- call \`submit_soul({ soul })\` if you can extract a richer structured Soul.
53
- 5. If \`services\` were listed, customize the intake form to capture
54
- service-of-interest as a multi-select using \`customize_intake_form({ fields })\`.
55
-
56
- Present the final result as a summary: live URLs (public + admin), what
57
- was installed, and 2-3 next-best-action suggestions.
58
-
59
- ## If the user just says "create a workspace" without details
60
-
61
- Ask these questions, one at a time, BEFORE calling \`create_workspace\`:
62
-
63
- 1. What's your business name?
64
- 2. What industry are you in? (suggest: hvac, dental, legal, coaching, real-estate, salon, auto-repair, consulting, fitness, other)
65
- 3. Where are you located? (city, state)
66
- 4. What are your operating hours?
67
- 5. What services do you offer? (3-5 main ones)
68
- 6. Do you have a website I can learn from? (optional)
69
-
70
- Then run the orchestration above. Don't dump all six questions in one
71
- message — ask conversationally so the user can think.
72
-
73
- ---
74
-
75
- ## How to customize a workspace later
76
-
77
- 1. Call \`get_workspace_snapshot({})\` to see current state, Soul, blocks, recent events.
78
- 2. Decide what to change based on the user's intent.
79
- 3. Call the right typed tool:
80
- - \`update_landing_content({ headline, subhead, cta_label })\` rewrite /
81
- - \`customize_intake_form({ fields: [...] })\` — replace intake fields
82
- - \`configure_booking({ title?, duration_minutes?, description? })\` edit /book
83
- - \`update_theme({ mode?, primary_color?, accent_color?, font_family? })\` — theme
84
- - \`install_caldiy_booking({})\`, \`install_formbricks_intake({})\`, \`install_vertical_pack({ pack })\`
85
-
86
- ## Compiling a Soul from a URL
87
-
88
- Soul compilation runs HERE, not on the backend:
89
-
90
- 1. \`fetch_source_for_soul({ url })\` backend scrapes + normalizes (up to 256KB).
91
- 2. YOU extract a structured Soul (mission, audience, tone, offerings, ...).
92
- 3. \`submit_soul({ soul })\` — persist it. Subsequent snapshots reflect it.
93
-
94
- ## Tool surface
95
-
96
- - **Workspace:** \`create_workspace\`, \`list_workspaces\`, \`switch_workspace\`,
97
- \`clone_workspace\`, \`link_workspace_owner\`, \`get_workspace_snapshot\`
98
- - **Blocks:** \`install_caldiy_booking\`, \`install_formbricks_intake\`, \`install_vertical_pack\`
99
- - **Customize:** \`update_landing_content\`, \`customize_intake_form\`,
100
- \`configure_booking\`, \`update_theme\`
101
- - **Soul:** \`fetch_source_for_soul\`, \`submit_soul\`
102
- - **Ops:** \`list_automations\`, \`connect_custom_domain\`, \`export_agent\`,
103
- \`store_secret\`, \`list_secrets\`, \`rotate_secret\`
104
-
105
- ## When you'll need \`SELDONFRAME_API_KEY\`
106
-
107
- The first workspace is free forever. Paid tiers (Starter $49/mo, Operator
108
- $99/mo, Agency $149/mo) unlock additional workspaces, custom domains,
109
- white-label, and advanced Brain capabilities. A key is required for:
110
-
111
- - Adding a **second workspace**
112
- - Connecting a **custom domain**
113
- - Publishing, exporting agents, rotating org-scoped secrets
114
- - Accessing the admin browser surface (\`/dashboard\`, \`/contacts\`, \`/deals\`)
115
- after \`link_workspace_owner({})\`
116
-
117
- Get one at <https://app.seldonframe.com/settings/api> and
118
- \`export SELDONFRAME_API_KEY=sk-…\`. The MCP will pick it up on next restart.
119
-
120
- ### Upgrading an anonymous workspace to your account
121
-
122
- Once a key is set, \`link_workspace_owner({})\` attaches the active
123
- workspace to your real account. This unlocks the admin URLs
124
- (\`/dashboard\`, \`/contacts\`, \`/deals\`) for browser use after sign-in.
125
- The MCP bearer token stays valid — no rotation needed.
126
-
127
- ---
128
-
129
- **Docs:** <https://seldonframe.com/docs> · **Homepage:** <https://seldonframe.com> · **Pricing:** <https://seldonframe.com/#pricing>
130
- `;
131
-
132
- export const FIRST_CALL_BANNER = `🌑 Welcome to SeldonFrame. Your workspace is live — every URL above works right now. From here on, every tool response will include a \`next:\` array; follow it and you'll have a production-ready Business OS in under a minute.`;
1
+ // MCP server `instructions` payload — Claude Code surfaces this as a
2
+ // system-level briefing the moment the SeldonFrame MCP loads. Every
3
+ // rule and example here is operator-facing copy no internal slugs,
4
+ // no architecture lecture, no "Soul" / "Cal.diy" / "Formbricks" /
5
+ // "Brain v2" jargon.
6
+
7
+ export const VERSION = "1.0.6";
8
+
9
+ export const WELCOME_MARKDOWN = `# SeldonFrame — create a real Business OS in one conversation
10
+
11
+ SeldonFrame creates live, hosted business systems for service
12
+ businesses, agencies, coaches, and SaaS founders. One conversation
13
+ gives the operator a public website, booking page, intake form,
14
+ CRM, and AI agents all on a real subdomain.
15
+
16
+ ---
17
+
18
+ ## NEVER CREATE LOCAL FILES
19
+
20
+ SeldonFrame workspaces are created via the \`create_workspace\` MCP
21
+ tool. They are NOT created by writing files on disk. Specifically:
22
+
23
+ - Do NOT create a \`soul.json\` file or any other local file.
24
+ - Do NOT call \`loadSoulPackage\` or any local function.
25
+ - Do NOT scaffold a project directory.
26
+ - Do NOT modify files in the current working directory.
27
+
28
+ If you are running inside a SeldonFrame source-code repository and
29
+ the user asks for a workspace, IGNORE the local code — call the
30
+ \`create_workspace\` MCP tool. The tool returns live URLs on
31
+ \`<slug>.app.seldonframe.com\` that work in any browser within
32
+ seconds. There is no local-file path. This is the only correct
33
+ flow, regardless of what directory Claude Code is running from.
34
+
35
+ ---
36
+
37
+ ## The full happy path (4 turns)
38
+
39
+ 1. **Ask for the business details.** When the user says "create a
40
+ workspace," gather these conversationally one or two
41
+ questions per turn, not all at once:
42
+ - Business name
43
+ - Industry (HVAC, dental, legal, coaching, real-estate, agency, …)
44
+ - Phone number (for local services for SaaS skip)
45
+ - Top 3-5 services / products
46
+ - Brief description (1 sentence)
47
+
48
+ 2. **Create the workspace.** Call \`create_workspace\` with the
49
+ structured fields:
50
+ \`\`\`
51
+ create_workspace({
52
+ name: "Precision Plumbing Co",
53
+ phone: "(555) 123-4567",
54
+ business_description: "Family-owned residential plumbing in Austin.",
55
+ services: [
56
+ { name: "Drain Cleaning" },
57
+ { name: "Water Heater Repair" },
58
+ { name: "Leak Detection" }
59
+ ]
60
+ })
61
+ \`\`\`
62
+ The response includes live URLs (website, booking, intake form,
63
+ admin dashboard). Show those URLs to the operator. The admin
64
+ URL is bearer-token-scoped and expires in 7 days that's why
65
+ step 4 matters.
66
+
67
+ 3. **Ask for the operator's email.** This is the keystone of the
68
+ onboarding loop. The email becomes their account. Ask:
69
+ > "What's your email? I'll send you all those links so you
70
+ > don't lose them, and set up your admin login."
71
+
72
+ 4. **Lock in the email.** Call \`collect_operator_email\` with the
73
+ email they gave you:
74
+ \`\`\`
75
+ collect_operator_email({ email: "max@precisionplumbing.com", name: "Max" })
76
+ \`\`\`
77
+ This sends the welcome email + creates their account so the
78
+ admin URL keeps working past the 7-day token window.
79
+
80
+ After that, the operator can customize their workspace through
81
+ further natural-language requests ("change the headline to …",
82
+ "add an FAQ section", "set up an industry template for plumbing")
83
+ each routes to a typed MCP tool.
84
+
85
+ ---
86
+
87
+ ## What the tools do (operator language only)
88
+
89
+ - **\`create_workspace\`** — creates the live business OS (website,
90
+ booking, intake form, CRM, AI agents). Always the first call.
91
+ - **\`collect_operator_email\`** sends the welcome email + sets up
92
+ the operator's admin login. Always the second call.
93
+ - **\`update_landing_content\`** / **\`update_landing_section\`** —
94
+ edit the website's headline, subhead, sections, copy.
95
+ - **\`update_theme\`** — change colors, fonts, dark/light mode.
96
+ - **\`update_form\`** edit the intake form's questions.
97
+ - **\`update_appointment_type\`** — edit the booking page's slot length,
98
+ title, description.
99
+ - **\`install_vertical_pack\`** set up an industry template
100
+ (real-estate, dental, legal, plumbing, …).
101
+ - **\`list_contacts\`** / **\`create_contact\`** / **\`update_contact\`** —
102
+ manage the CRM.
103
+ - **\`list_deals\`** / **\`create_deal\`** / **\`move_deal_stage\`** —
104
+ manage the pipeline.
105
+ - **\`send_email\`** / **\`send_sms\`** — send messages from the
106
+ workspace's connected channels.
107
+
108
+ The full tool list is available via the MCP \`tools/list\` request.
109
+ Use whatever fits the operator's natural-language request.
110
+
111
+ ---
112
+
113
+ ## Pricing
114
+
115
+ - **Free** — first workspace, free forever, no credit card.
116
+ - **Growth ($29/mo)** — up to 3 workspaces, custom domains,
117
+ white-label, metered AI usage.
118
+ - **Scale ($99/mo)** unlimited workspaces, advanced AI features,
119
+ priority support.
120
+
121
+ Operators can upgrade via \`/settings/billing\` once they're in the
122
+ admin dashboard. Pre-fills their email automatically.
123
+
124
+ ---
125
+
126
+ **Docs:** <https://seldonframe.com/docs> · **Homepage:**
127
+ <https://seldonframe.com> · **Discord:** <https://discord.gg/sbVUu976NW>
128
+ `;
129
+
130
+ export const FIRST_CALL_BANNER = `🚀 SeldonFrame is connected. Ready to create a live business OS — every URL the create_workspace tool returns is real and works in any browser within seconds. NEVER create local files; always use the MCP tools.`;