@seldonframe/mcp 1.0.4 → 1.0.5

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.5",
4
+ "description": "MCP server for SeldonFrame — AI-native Business OS platform. One command creates a real hosted workspace with CRM, booking page, intake form, and AI agents. v1.0.5: collect_operator_email tool closes the onboarding loop (welcome email + lead capture); auto-injects x-org-id from workspace bearer for ALL tool calls (fixes list_contacts / list_deals / list_bookings / send_welcome_email); structured Soul-seed fields on create_workspace (phone, services, testimonials) so the landing page renders real data on the first GET; operator-facing labels replace internal slugs in responses.",
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,61 @@ 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 real, hosted workspace on <slug>.app.seldonframe.com with a CRM, booking page, intake form, and AI agents pre-installed. The first workspace 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. Example: create_workspace({ name: 'Desert Cool HVAC', phone: '(602) 555-0188', services: [{ name: 'AC Repair' }, { name: 'Heating' }] })",
45
46
  inputSchema: obj(
46
47
  {
47
48
  name: str("Human-readable workspace name."),
48
- source: str("Optional URL or description to seed the workspace's Soul from."),
49
+ source: str("Optional URL or description to seed the business profile from."),
50
+ phone: str("Operator's business phone (any format — we render as-is). Renders in nav, hero, and footer when set."),
51
+ email: str("Optional contact email surfaced in the landing footer."),
52
+ address: str("Optional business address. Comma-separated street, city, region, postal, country renders as-is."),
53
+ tagline: str("Short hero tagline (one line)."),
54
+ business_description: str("One paragraph about the business (used for hero subhead + about section)."),
55
+ services: {
56
+ type: "array",
57
+ description:
58
+ "List of services / products / offerings the business provides. Renders as the services grid on the landing page. Each item: { name, description? }.",
59
+ items: {
60
+ type: "object",
61
+ properties: {
62
+ name: { type: "string" },
63
+ description: { type: "string" },
64
+ },
65
+ required: ["name"],
66
+ },
67
+ },
68
+ testimonials: {
69
+ type: "array",
70
+ description:
71
+ "Optional customer testimonials to seed the landing page's testimonials section. Each item: { quote, name?, role?, company? }.",
72
+ items: {
73
+ type: "object",
74
+ properties: {
75
+ quote: { type: "string" },
76
+ name: { type: "string" },
77
+ role: { type: "string" },
78
+ company: { type: "string" },
79
+ },
80
+ required: ["quote"],
81
+ },
82
+ },
49
83
  },
50
84
  ["name"],
51
85
  ),
52
86
  handler: async (args) => {
53
87
  const firstEver = isFirstEverCall();
54
88
  const result = await api("POST", "/workspace/create", {
55
- body: { name: args.name, source: args.source ?? null },
89
+ body: {
90
+ name: args.name,
91
+ source: args.source ?? null,
92
+ phone: args.phone ?? null,
93
+ email: args.email ?? null,
94
+ address: args.address ?? null,
95
+ tagline: args.tagline ?? null,
96
+ business_description: args.business_description ?? null,
97
+ services: args.services ?? null,
98
+ testimonials: args.testimonials ?? null,
99
+ },
56
100
  allow_anonymous: true,
57
101
  });
58
102
  const ws = result.workspace ?? result;
@@ -94,13 +138,41 @@ export const TOOLS = [
94
138
  : null,
95
139
  urls: publicUrls,
96
140
  public_urls: result.public_urls ?? publicUrls,
97
- installed: result.installed ?? ["crm", "caldiy-booking", "formbricks-intake", "brain-v2"],
141
+ // May 1, 2026 — installed-block labels translated to operator
142
+ // copy. The MCP-internal slugs ("caldiy-booking",
143
+ // "formbricks-intake", "brain-v2") are deployment details no
144
+ // operator should need to learn — surfacing them on every
145
+ // create_workspace turn was friction without payoff.
146
+ installed: (result.installed ?? [
147
+ "crm",
148
+ "caldiy-booking",
149
+ "formbricks-intake",
150
+ "brain-v2",
151
+ ]).map((slug) => {
152
+ if (slug === "caldiy-booking") return "Booking page";
153
+ if (slug === "formbricks-intake") return "Intake form";
154
+ if (slug === "brain-v2") return "AI learning";
155
+ if (slug === "crm") return "CRM";
156
+ return slug;
157
+ }),
158
+ // May 1, 2026 — email collection promoted to the first
159
+ // suggested next step. Per the onboarding-loop spec, the
160
+ // operator gets a permanent inbox record (admin URLs,
161
+ // landing/booking/intake links) AND we earn a CRM lead in
162
+ // SeldonFrame's own workspace, so every workspace creation
163
+ // becomes a tracked relationship.
164
+ prompt_for_email: {
165
+ message:
166
+ "What's your email? I'll send you a summary with all the links above (so you don't lose them) and use it for your admin login when you upgrade. — calls collect_operator_email({ email, name? })",
167
+ tool: "collect_operator_email",
168
+ },
98
169
  next: [
99
170
  adminUrl
100
171
  ? `⚡ Admin Dashboard (bookmark this!): ${adminUrl} (paste into your browser; no signup needed; token expires in 7 days)`
101
172
  : null,
102
- "install_vertical_pack({ pack: 'real-estate' }) // or 'dental', 'legal'",
103
- "fetch_source_for_soul({ url: 'https://yoursite.com' }) submit_soul({ soul })",
173
+ "Ask the user for their email, then call collect_operator_email({ email, name? }) sends the welcome email + creates a permanent record. Do this BEFORE any further customization.",
174
+ "install_vertical_pack({ pack: '<industry>' }) set up an industry template (real-estate, dental, legal, …)",
175
+ "fetch_source_for_soul({ url: 'https://yoursite.com' }) → submit_soul({ soul }) — pull the business profile from a website",
104
176
  "get_workspace_snapshot({}) — read workspace state to reason about next steps",
105
177
  ].filter(Boolean),
106
178
  };
@@ -207,6 +279,178 @@ export const TOOLS = [
207
279
  };
208
280
  },
209
281
  },
282
+ {
283
+ name: "send_welcome_email",
284
+ description:
285
+ "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' }).",
286
+ inputSchema: obj(
287
+ {
288
+ email: str("Recipient email address."),
289
+ name: str("Optional recipient name (used in the greeting)."),
290
+ workspace_id: str("Optional workspace override. Defaults to active workspace."),
291
+ },
292
+ ["email"],
293
+ ),
294
+ handler: async (a) => {
295
+ const workspaceId = a.workspace_id ?? getDefaultWorkspace();
296
+ if (!workspaceId) {
297
+ throw new Error(
298
+ "No workspace selected. Run create_workspace({ name: '…' }) first, or pass workspace_id.",
299
+ );
300
+ }
301
+ const bearer = getWorkspaceBearer(workspaceId);
302
+ if (!bearer) {
303
+ throw new Error(
304
+ `No local bearer token for workspace ${workspaceId}. This device did not create it. Re-run create_workspace or switch to the device that did.`,
305
+ );
306
+ }
307
+
308
+ const snapshot = await api(
309
+ "GET",
310
+ `/workspace/${encodeURIComponent(workspaceId)}/snapshot`,
311
+ { workspace_id: workspaceId },
312
+ );
313
+ const publicUrls = snapshot?.public_urls ?? {};
314
+ if (!publicUrls.home || !publicUrls.book || !publicUrls.intake) {
315
+ throw new Error(
316
+ "Snapshot did not return public_urls (home/book/intake). Re-check the workspace.",
317
+ );
318
+ }
319
+
320
+ // Construct the bearer-scoped admin URL from the API base.
321
+ // API_INFO.base is `<host>/api/v1` — strip that suffix to get the app host.
322
+ const appHost = API_INFO.base.replace(/\/api\/v1\/?$/, "");
323
+ const adminUrl = `${appHost}/admin/${encodeURIComponent(workspaceId)}?token=${encodeURIComponent(bearer)}`;
324
+
325
+ await api("POST", "/email/send-welcome", {
326
+ body: {
327
+ email: a.email,
328
+ name: a.name ?? null,
329
+ workspace: {
330
+ landing_url: publicUrls.home,
331
+ booking_url: publicUrls.book,
332
+ intake_url: publicUrls.intake,
333
+ admin_url: adminUrl,
334
+ },
335
+ },
336
+ workspace_id: workspaceId,
337
+ });
338
+
339
+ return {
340
+ ok: true,
341
+ message: `Welcome email sent to ${a.email}`,
342
+ };
343
+ },
344
+ },
345
+ {
346
+ name: "collect_operator_email",
347
+ description:
348
+ "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' })",
349
+ inputSchema: obj(
350
+ {
351
+ email: str("Operator email — used as the welcome email recipient AND as the unique key for the SeldonFrame CRM lead."),
352
+ name: str("Optional operator name (used in the email greeting and on the CRM lead)."),
353
+ workspace_id: str("Optional workspace override. Defaults to the workspace just created."),
354
+ },
355
+ ["email"],
356
+ ),
357
+ handler: async (a) => {
358
+ const workspaceId = a.workspace_id ?? getDefaultWorkspace();
359
+ if (!workspaceId) {
360
+ throw new Error(
361
+ "No workspace selected. Run create_workspace({ name: '…' }) first, or pass workspace_id.",
362
+ );
363
+ }
364
+ const bearer = getWorkspaceBearer(workspaceId);
365
+ if (!bearer) {
366
+ throw new Error(
367
+ `No local bearer token for workspace ${workspaceId}. This device did not create it. Re-run create_workspace or switch to the device that did.`,
368
+ );
369
+ }
370
+
371
+ // Step 1: pull the workspace snapshot so we have the public URLs
372
+ // for the welcome email.
373
+ const snapshot = await api(
374
+ "GET",
375
+ `/workspace/${encodeURIComponent(workspaceId)}/snapshot`,
376
+ { workspace_id: workspaceId },
377
+ );
378
+ const publicUrls = snapshot?.public_urls ?? {};
379
+ const slug = snapshot?.workspace?.slug ?? null;
380
+ if (!publicUrls.home || !publicUrls.book || !publicUrls.intake) {
381
+ throw new Error(
382
+ "Snapshot did not return public_urls (home/book/intake). Re-check the workspace.",
383
+ );
384
+ }
385
+ const appHost = API_INFO.base.replace(/\/api\/v1\/?$/, "");
386
+ const adminUrl = `${appHost}/admin/${encodeURIComponent(workspaceId)}?token=${encodeURIComponent(bearer)}`;
387
+
388
+ // Step 2: send the welcome email. Failures here are surfaced
389
+ // (operator told us their email, we owe them the email) but
390
+ // don't block the lead-capture step below.
391
+ let emailSent = false;
392
+ let emailError = null;
393
+ try {
394
+ await api("POST", "/email/send-welcome", {
395
+ body: {
396
+ email: a.email,
397
+ name: a.name ?? null,
398
+ workspace: {
399
+ landing_url: publicUrls.home,
400
+ booking_url: publicUrls.book,
401
+ intake_url: publicUrls.intake,
402
+ admin_url: adminUrl,
403
+ },
404
+ },
405
+ workspace_id: workspaceId,
406
+ });
407
+ emailSent = true;
408
+ } catch (err) {
409
+ emailError = err?.message ?? String(err);
410
+ }
411
+
412
+ // Step 3: record the operator as a lead in SeldonFrame's own
413
+ // CRM workspace. Anonymous endpoint — no bearer required, ops
414
+ // workspace ID is server-side env only. Soft-failure: if the
415
+ // ops workspace isn't configured we still return ok.
416
+ let leadRecorded = false;
417
+ let leadId = null;
418
+ let leadError = null;
419
+ try {
420
+ const leadResp = await api("POST", "/leads/operator-signup", {
421
+ body: {
422
+ email: a.email,
423
+ name: a.name ?? null,
424
+ source_workspace_id: workspaceId,
425
+ source_workspace_slug: slug,
426
+ source: "mcp-onboarding",
427
+ },
428
+ allow_anonymous: true,
429
+ });
430
+ leadRecorded = Boolean(leadResp?.recorded);
431
+ leadId = leadResp?.lead_id ?? null;
432
+ } catch (err) {
433
+ leadError = err?.message ?? String(err);
434
+ }
435
+
436
+ return {
437
+ ok: emailSent || leadRecorded,
438
+ email_sent: emailSent,
439
+ email_error: emailError,
440
+ lead_recorded: leadRecorded,
441
+ lead_id: leadId,
442
+ lead_error: leadError,
443
+ message: emailSent
444
+ ? `Welcome email sent to ${a.email}. Check your inbox — the admin URL is in there too.`
445
+ : `Could not send welcome email${emailError ? `: ${emailError}` : ""}. Lead ${leadRecorded ? "recorded" : "not recorded"}.`,
446
+ next: [
447
+ "configure_booking({ title, duration_minutes, description }) — tune the booking page if you collected business hours",
448
+ "customize_intake_form({ ... }) — match your intake to the workspace's lead-qualification questions",
449
+ "install_vertical_pack({ pack: '<industry>' }) — add domain-specific objects, fields, and views",
450
+ ],
451
+ };
452
+ },
453
+ },
210
454
  {
211
455
  name: "revoke_bearer",
212
456
  description:
@@ -394,10 +638,10 @@ export const TOOLS = [
394
638
  {
395
639
  name: "install_vertical_pack",
396
640
  description:
397
- "Install a vertical pack (e.g. 'real-estate', 'dental', 'legal'). Adds domain-specific objects, fields, views.",
641
+ "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
642
  inputSchema: obj(
399
643
  {
400
- pack: str("Pack slug, e.g. 'real-estate', 'dental', 'legal'."),
644
+ pack: str("Industry template name, e.g. 'real-estate', 'dental', 'legal'."),
401
645
  workspace_id: str("Optional workspace override."),
402
646
  },
403
647
  ["pack"],
@@ -413,10 +657,10 @@ export const TOOLS = [
413
657
  {
414
658
  name: "install_caldiy_booking",
415
659
  description:
416
- "Install the Cal.diy booking block (event types, availability, bookings). Example: install_caldiy_booking({})",
660
+ "Install the booking page (event types, availability, scheduled bookings). Example: install_caldiy_booking({})",
417
661
  inputSchema: obj({
418
662
  workspace_id: str("Optional workspace override."),
419
- config: { type: "object", description: "Optional Cal.diy configuration overrides." },
663
+ config: { type: "object", description: "Optional booking-page configuration overrides." },
420
664
  }),
421
665
  handler: async (a) => {
422
666
  const ws = wsOrDefault(a.workspace_id);
@@ -429,10 +673,10 @@ export const TOOLS = [
429
673
  {
430
674
  name: "install_formbricks_intake",
431
675
  description:
432
- "Install a Formbricks intake form (surveys, conditional logic, contact sync). Example: install_formbricks_intake({})",
676
+ "Install an intake form (questions, conditional logic, automatic CRM sync). Example: install_formbricks_intake({})",
433
677
  inputSchema: obj({
434
678
  workspace_id: str("Optional workspace override."),
435
- form_id: str("Optional existing Formbricks form id to bind."),
679
+ form_id: str("Optional existing intake-form id to bind."),
436
680
  }),
437
681
  handler: async (a) => {
438
682
  const ws = wsOrDefault(a.workspace_id);
@@ -459,7 +703,7 @@ export const TOOLS = [
459
703
  {
460
704
  name: "fetch_source_for_soul",
461
705
  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.",
706
+ "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
707
  inputSchema: obj(
464
708
  {
465
709
  url: str("Absolute URL to fetch."),
@@ -478,7 +722,7 @@ export const TOOLS = [
478
722
  truncated,
479
723
  text,
480
724
  next: [
481
- "Extract a Soul object: { mission, audience, tone, offerings[], differentiators[], faqs[] }",
725
+ "Extract a business profile: { mission, audience, tone, offerings[], differentiators[], faqs[] }",
482
726
  "submit_soul({ soul: <extracted> })",
483
727
  ],
484
728
  };
@@ -487,13 +731,13 @@ export const TOOLS = [
487
731
  {
488
732
  name: "submit_soul",
489
733
  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.",
734
+ "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
735
  inputSchema: obj(
492
736
  {
493
737
  soul: {
494
738
  type: "object",
495
739
  description:
496
- "Structured Soul. Expected keys: mission, audience, tone, offerings, differentiators, faqs. Additional keys allowed.",
740
+ "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
741
  },
498
742
  workspace_id: str("Optional workspace override."),
499
743
  },
package/src/welcome.js CHANGED
@@ -104,9 +104,9 @@ Soul compilation runs HERE, not on the backend:
104
104
 
105
105
  ## When you'll need \`SELDONFRAME_API_KEY\`
106
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:
107
+ The first workspace is free forever. Paid tiers (Growth $29/mo, Scale
108
+ $99/mo, both with metered usage) unlock additional workspaces, custom
109
+ domains, white-label, and advanced Brain capabilities. A key is required for:
110
110
 
111
111
  - Adding a **second workspace**
112
112
  - Connecting a **custom domain**