@seldonframe/mcp 1.16.1 → 1.17.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 +3 -1
- package/src/tools.js +110 -0
- package/src/welcome.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seldonframe/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
|
+
"description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.17.0: WHITE-LABEL CRM PHASE 1 — partner-agency hierarchy + foundational primitives. SeldonFrame's Scale-tier customers (agencies / solopreneurs reselling AI-native Business OS to SMBs like HVAC contractors / dentists / lawyers / realtors) can now register as a partner agency, attach client workspaces to that agency, and have those workspaces' chrome (logo, colors, sender, support pointers, 'Powered by' badge) substitute the agency's branding for SeldonFrame's. WHAT SHIPS: (1) Migration 0040 — new partner_agencies table (id, name, slug, logo_url, colors, support pointers, sender_email_address + verified_sender_at for v1.18, agency_domain + agency_domain_verified_at for v1.20, owner_user_id, status: pending|active|suspended|archived, hide_powered_by_badge). organizations.parent_agency_id FK column. Applied to prod via Neon MCP. (2) Drizzle schema modules: partner-agencies.ts + organizations.ts extended with parentAgencyId. (3) Pure helper deriveEffectiveBranding(agency, workspaceName) — decides chrome (SF default vs agency-substituted). Defense-in-depth: doesn't expose sender_email_address until verified_sender_at is populated; doesn't expose agency_domain until agency_domain_verified_at is populated. Plan-gate semantics: only 'active' agencies substitute chrome; pending/suspended/archived fall back to SF defaults (data preserved). (4) DB-loading wrapper getEffectiveBrandingForWorkspace(workspaceId) for chrome consumers. (5) State management: registerPartnerAgency (Scale-tier check + slug uniqueness), attachWorkspaceToAgency (caller-owns-both check), detachWorkspaceFromAgency (caller is workspace owner OR agency owner). (6) Three new MCP tools: register_partner_agency, attach_workspace_to_agency, detach_workspace_from_agency. (7) shouldShowPoweredByBadgeForOrg now respects agency.hide_powered_by_badge for active agencies — first chrome surface wired up. (8) 14 new unit tests covering branding-derivation in all status states, color/sender/domain echo, defense-in-depth on unverified sender + domain. Test count: 2539 (was 2525). NO new env vars (Resend + Vercel come into play in v1.18 + v1.20). Plan gate: agency creation requires Scale-tier ownership; non-Scale callers get the agency in 'pending' status (chrome substitution only activates on active). v1.18 (next ship) — wire the dashboard chrome substitution + welcome/admin/portal-access-code emails to use the agency's verified Resend sender. v1.19 — agency self-serve management tools (list_my_agencies, update_partner_agency, list_my_workspaces). v1.20 — agency custom domain (crm.acmeai.com) with per-client wildcard subdomains via Vercel.",
|
|
5
|
+
"description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.16.2: HOTFIX — public booking + intake POST handlers correctly resolve workspace on custom domains. The C4 booking client and C5 intake client both fall back to hostname.split('.')[0] when the path-based orgSlug is missing — fine for <slug>.app.seldonframe.com (the segment IS the slug), broken for custom domains (hvac.tirionforge.com → 'hvac' is just the operator's chosen subdomain, not the workspace slug 'cypress-pine-hvac'). Reproduced 2026-05-06: visitor on hvac.tirionforge.com/book got 'Couldn't book that time'; logs showed booking_context_not_found with org_slug='hvac' (no template booking row exists for that — the workspace's slug is 'cypress-pine-hvac'). Fix: new resolveWorkspaceSlugFromRequestWithCustomDomains async helper that tries the existing subdomain extraction first, then falls back to a workspace_domains lookup by full hostname → returns the canonical workspace slug. Both /api/v1/public/bookings and /api/v1/public/intake routes now PREFER the host-derived slug (when a verified custom domain matches) over body's orgSlug, since on custom domains the body value is whatever the client guessed from the URL — unreliable. Slug-source diagnostic now reports custom_domain | body | subdomain_fallback for funnel observability. NO migrations, NO env vars, NO new deps. The MCP package itself didn't change in v1.16.2 — version bump coupled with backend so they redeploy together.",
|
|
4
6
|
"description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.16.1: HOTFIX — portal access-code email actually gets sent. Discovered during the v1.16 smoke: customer hit /portal/<orgSlug>/login, entered their email, but no code arrived. Two bugs: (1) lookup is silently no-op when the entered email doesn't match a contact (security feature, not a bug — operator just needs to enter the contact's actual email). (2) THE REAL BUG: requestPortalAccessCodeAction generated the code, hashed it, persisted to portal_access_codes — and then RETURNED. No call to Resend, no email infrastructure wired up. Codes were piling up in the DB unread; pre-v1.16.1 there was no way for customers to actually receive them. v1.16.1 wires it up: new packages/crm/src/lib/emails/portal-access-code.ts mirrors the device-auth email pattern (verified welcome@seldonframe.com sender, Resend HTTPS POST, sandbox-from fallback, structured error logs). requestPortalAccessCodeAction now calls sendPortalAccessCodeEmail after persistence; failures are logged but don't change the success response (still no info-leak via timing/error). getOrgBySlug widened to also return `name` so the email subject reads 'Your Cypress & Pine HVAC sign-in code: 123456' instead of generic. Email body shows the code in a monospace pill, includes 15-minute expiration. NO migrations, NO new env vars (uses existing RESEND_API_KEY + RESEND_FROM_ADDRESS), NO new deps. The MCP package itself didn't change in v1.16.1 — version bumps coupled with backend so they redeploy together.",
|
|
5
7
|
"description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.16.0: WIRE PORTAL TEMPLATE INTO CUSTOMER-FACING ROUTE. v1.15 shipped the templates + renderer + per-customer embeds; the operator could compose templates and verify via preview_portal MCP tool, but customers themselves had no URL to actually SEE the rendered template. v1.16 closes the loop: the existing /portal/[orgSlug] customer-facing route (with magic-link auth already in place pre-v1.16) now renders the operator-defined composite template ABOVE the existing stats grid. WHAT SHIPS: (1) renderPortalForCustomer({ orgId, contactId, workspaceTimezone, workspaceContext }) — server-side helper that loads the template, builds CustomerRenderContext via buildCustomerContext, renders each section, returns html + css. Returns null for empty templates so the page falls through to the existing stats-only view (additive change — workspaces without portal templates see the same UX as pre-v1.16). (2) app/portal/[orgSlug]/(client)/page.tsx wires the helper inline. Existing magic-link session auth provides orgId + contactId; renderPortalForCustomer is awaited in parallel with the existing message + resource queries (cheap). (3) CSS strategy: inject COMPOSITE_CSS + a small token-aliases block scoped to .sf-cmp-portal-host that maps the portal page's --color-primary etc. tokens onto the composite primitive's expected --sf-bg / --sf-text / --sf-border / --sf-primary / --sf-accent variable names. No collisions with the surrounding portal layout. (4) UX FIX from v1.15: get_portal_structure / add_portal_section / etc. used to return preview_url pointing at the MCP-only endpoint (browser-clickable look but required workspace bearer auth — confusing). v1.16 renames the field to customer_portal_url and points at the actual live URL customers visit (https://<slug>.app.seldonframe.com/portal/<slug>). For preview during template-design, operators still use the preview_portal MCP tool. Test count: 2525 (unchanged; v1.16 is a wiring change to existing tested code paths). Typecheck clean. Backward compatible — empty templates fall through cleanly. NO migrations, NO env vars, NO new deps. AUTH MODEL: customers reach the portal via magic-link sent from the operator's admin dashboard (existing pre-v1.16 flow); session is contact-scoped via signPortalSession; renderPortalForCustomer trusts the session's contactId. v1.17+ candidate: stable section IDs for cross-call composition without re-reads.",
|
|
6
8
|
"description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.15.0: PORTAL COMPOSITE TREES + CUSTOMER.* EMBEDS. The composite primitive vocabulary (12 kinds, v1.12) now extends to the customer-portal surface via 5 new embed.ref values that pull per-customer data: customer.contact_info, customer.next_appointment, customer.recent_appointments, customer.documents, customer.deals. WHY (decisive (α) over (β) on every architectural principle): a portal block IS a landing block with a different render context — same structure, different data binding, different audience. Building a separate portal pipeline (β) would have meant 2× the harness, 2× the agent vocabulary, no compounding of LLM improvements across surfaces. (α) reuses the composite engine; the only new code is per-customer embed render branches + auth-scoped DB resolvers + a workspace-level template store. WHAT SHIPS: (1) Schema extension — 5 new customer.* values added to embed.ref enum; existing 5 workspace embeds still work; unknown refs (customer.password etc) still rejected. (2) CustomerRenderContext extends CompositeRenderContext with customer + 4 per-customer data fields. assembleCustomerContext THROWS on missing customer.id (auth-scope identity, never default to empty). (3) Per-customer DB resolvers (5 functions, each takes orgId+customerId as REQUIRED args, returns auth-scoped data). All five run in parallel via buildCustomerContext. (4) Portal-template storage on organizations.settings.portal_template (CompositeNode[]). One template per workspace; every customer renders against their own data. (5) Five portal MCP tools: get_portal_structure, add_portal_section, update_portal_section, move_portal_section, delete_portal_section. Same atomic-primitive pattern as v1.11/v1.13/v1.14. Empty templates are valid (different from landing's minimum-1 rule — a portal with no template just shows built-in tabs). (6) preview_portal({ workspace_id, contact_id }) renders the template against a real contact for visual verification before customers see it. (7) Renderer extensions: 5 new render branches in renderEmbed (one per customer.* ref). HTML-escaped customer-supplied data (no XSS via document filename or appointment title). (8) Composite SKILL.md addendum: portal patterns (WELCOME / NEXT-VISIT / DOCS+DEALS-SIDE-BY-SIDE), portal voice rules (second-person, customer-addressed), anti-patterns (no cross-customer data, no marketing content). Plus the 'check for near-duplicates before add_composite_section' rule from the v1.14 followup. Test count: 2525 (was 2495; +30: schema/render/assemble + portal-structure pure helpers + XSS + auth-scope contract). NO migrations (template stored on existing settings jsonb), NO env vars, NO new deps. Backward compatible. v1.16 candidate: wire the portal template into a customer-facing rendered route at /portal/<contactId> (currently the rendered HTML is exposed only via the operator-facing preview endpoint).",
|
package/src/tools.js
CHANGED
|
@@ -3349,6 +3349,116 @@ export const TOOLS = [
|
|
|
3349
3349
|
},
|
|
3350
3350
|
},
|
|
3351
3351
|
|
|
3352
|
+
// ─── v1.17.0 — partner-agency tools (white-label CRM resellers) ─────────
|
|
3353
|
+
//
|
|
3354
|
+
// Layer-1 in the SF/Agency/Workspace/Customer hierarchy. An agency
|
|
3355
|
+
// is a SeldonFrame-paying entity that resells Business OS to SMBs;
|
|
3356
|
+
// each SMB gets its own workspace, but the chrome (logo, colors,
|
|
3357
|
+
// sender, support pointers) shows the AGENCY's brand instead of
|
|
3358
|
+
// SeldonFrame's. Plan-gated: Scale tier ($99) or higher.
|
|
3359
|
+
//
|
|
3360
|
+
// v1.17 ships the foundational primitives (register agency + attach
|
|
3361
|
+
// / detach workspaces). v1.18 adds verified-sender email branding.
|
|
3362
|
+
// v1.19 adds management tools (list_my_agencies, update_agency).
|
|
3363
|
+
// v1.20 adds the agency's own custom domain.
|
|
3364
|
+
|
|
3365
|
+
{
|
|
3366
|
+
name: "register_partner_agency",
|
|
3367
|
+
description:
|
|
3368
|
+
"Register a partner agency. Used by Scale-tier customers who resell SeldonFrame's Business OS to SMBs (HVAC contractors, dentists, lawyers, realtors) under their OWN brand. Once registered, the agency can attach client workspaces via attach_workspace_to_agency; those workspaces will show the agency's logo / colors / support links instead of SeldonFrame's. " +
|
|
3369
|
+
"Plan gate: at least one workspace owned by the caller must be on Scale tier; otherwise the agency is created in 'pending' status and chrome substitution doesn't activate until the upgrade lands. " +
|
|
3370
|
+
"Provide name (required) + slug (auto-derived from name if omitted). Optional: logo_url (uploaded image URL), primary_color / accent_color (hex like #5b21b6), support_email + support_url (where the agency's clients go for help — these REPLACE SeldonFrame's docs/Discord pointers in client chrome), hide_powered_by_badge (true to suppress the 'Powered by SeldonFrame' footer on clients' public pages — Scale-tier perk).",
|
|
3371
|
+
inputSchema: obj(
|
|
3372
|
+
{
|
|
3373
|
+
workspace_id: str(
|
|
3374
|
+
"Workspace id (any workspace owned by the caller — used to resolve the owning user for the new agency).",
|
|
3375
|
+
),
|
|
3376
|
+
name: str("Agency display name (e.g. 'Acme AI'). 2+ chars."),
|
|
3377
|
+
slug: str(
|
|
3378
|
+
"Optional URL-safe slug. Default: derived from name. Must be unique among non-archived agencies.",
|
|
3379
|
+
),
|
|
3380
|
+
logo_url: str("Optional logo URL (https://...). Use upload_workspace_image to host one if needed."),
|
|
3381
|
+
primary_color: str("Optional hex color like #5b21b6."),
|
|
3382
|
+
accent_color: str("Optional hex color like #a78bfa."),
|
|
3383
|
+
support_email: str("Optional. Where the agency's clients email for help."),
|
|
3384
|
+
support_url: str("Optional. Where the agency's clients click for docs/help."),
|
|
3385
|
+
hide_powered_by_badge: {
|
|
3386
|
+
type: "boolean",
|
|
3387
|
+
description:
|
|
3388
|
+
"Hide the 'Powered by SeldonFrame' footer on the agency's clients' public landing pages. Scale-tier feature.",
|
|
3389
|
+
},
|
|
3390
|
+
},
|
|
3391
|
+
["workspace_id", "name"],
|
|
3392
|
+
),
|
|
3393
|
+
handler: async (args) => {
|
|
3394
|
+
const ws = args.workspace_id;
|
|
3395
|
+
const result = await api("POST", "/partner-agencies", {
|
|
3396
|
+
body: {
|
|
3397
|
+
op: "register",
|
|
3398
|
+
name: args.name,
|
|
3399
|
+
slug: args.slug,
|
|
3400
|
+
logo_url: args.logo_url,
|
|
3401
|
+
primary_color: args.primary_color,
|
|
3402
|
+
accent_color: args.accent_color,
|
|
3403
|
+
support_email: args.support_email,
|
|
3404
|
+
support_url: args.support_url,
|
|
3405
|
+
hide_powered_by_badge: args.hide_powered_by_badge,
|
|
3406
|
+
},
|
|
3407
|
+
workspace_id: ws,
|
|
3408
|
+
});
|
|
3409
|
+
return result;
|
|
3410
|
+
},
|
|
3411
|
+
},
|
|
3412
|
+
|
|
3413
|
+
{
|
|
3414
|
+
name: "attach_workspace_to_agency",
|
|
3415
|
+
description:
|
|
3416
|
+
"Attach a workspace to a partner agency. The workspace's chrome (admin dashboard logo, public landing footer, customer portal branding) flips to the agency's brand. Caller must own BOTH the agency and the workspace. Agency must be in 'active' status (not pending — register first, upgrade if needed).",
|
|
3417
|
+
inputSchema: obj(
|
|
3418
|
+
{
|
|
3419
|
+
workspace_id: str("Workspace id (the bearer's workspace; also the workspace to attach)."),
|
|
3420
|
+
target_workspace_id: str(
|
|
3421
|
+
"Workspace id of the workspace you want to attach to the agency. Often this is the SAME as workspace_id (the workspace running the MCP); for an agency operator with multiple client workspaces, this is the specific client's workspace_id.",
|
|
3422
|
+
),
|
|
3423
|
+
agency_id: str("Agency id from register_partner_agency."),
|
|
3424
|
+
},
|
|
3425
|
+
["workspace_id", "target_workspace_id", "agency_id"],
|
|
3426
|
+
),
|
|
3427
|
+
handler: async (args) => {
|
|
3428
|
+
const ws = args.workspace_id;
|
|
3429
|
+
const result = await api("POST", "/partner-agencies", {
|
|
3430
|
+
body: {
|
|
3431
|
+
op: "attach",
|
|
3432
|
+
workspace_id: args.target_workspace_id,
|
|
3433
|
+
agency_id: args.agency_id,
|
|
3434
|
+
},
|
|
3435
|
+
workspace_id: ws,
|
|
3436
|
+
});
|
|
3437
|
+
return result;
|
|
3438
|
+
},
|
|
3439
|
+
},
|
|
3440
|
+
|
|
3441
|
+
{
|
|
3442
|
+
name: "detach_workspace_from_agency",
|
|
3443
|
+
description:
|
|
3444
|
+
"Detach a workspace from its current agency. Chrome falls back to SeldonFrame defaults on next render. Either the workspace owner OR the agency owner can detach.",
|
|
3445
|
+
inputSchema: obj(
|
|
3446
|
+
{
|
|
3447
|
+
workspace_id: str("Workspace id (the bearer's workspace)."),
|
|
3448
|
+
target_workspace_id: str("Workspace id to detach (often the same as workspace_id)."),
|
|
3449
|
+
},
|
|
3450
|
+
["workspace_id", "target_workspace_id"],
|
|
3451
|
+
),
|
|
3452
|
+
handler: async (args) => {
|
|
3453
|
+
const ws = args.workspace_id;
|
|
3454
|
+
const result = await api("POST", "/partner-agencies", {
|
|
3455
|
+
body: { op: "detach", workspace_id: args.target_workspace_id },
|
|
3456
|
+
workspace_id: ws,
|
|
3457
|
+
});
|
|
3458
|
+
return result;
|
|
3459
|
+
},
|
|
3460
|
+
},
|
|
3461
|
+
|
|
3352
3462
|
{
|
|
3353
3463
|
name: "reorder_landing_sections",
|
|
3354
3464
|
description:
|
package/src/welcome.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// stripped. `create_full_workspace` is the only workspace-creation
|
|
9
9
|
// path mentioned anywhere in this briefing.
|
|
10
10
|
|
|
11
|
-
export const VERSION = "1.
|
|
11
|
+
export const VERSION = "1.17.0";
|
|
12
12
|
|
|
13
13
|
export const WELCOME_MARKDOWN = `# SeldonFrame — create a real Business OS in one conversation
|
|
14
14
|
|
|
@@ -306,4 +306,4 @@ admin dashboard. Pre-fills their email automatically.
|
|
|
306
306
|
<https://seldonframe.com> · **Discord:** <https://discord.gg/sbVUu976NW>
|
|
307
307
|
`;
|
|
308
308
|
|
|
309
|
-
export const FIRST_CALL_BANNER = `🚀 SeldonFrame v1.
|
|
309
|
+
export const FIRST_CALL_BANNER = `🚀 SeldonFrame v1.17.0 is connected. PREFERRED workspace creation: create_workspace_v2 → IN PARALLEL for all 7 recommended_blocks (hero, services, about, faq, cta, booking, intake): get_block_skill + persist_block → complete_workspace_v2 → finalize_workspace({ workspace_id, email }). The v2 flow puts YOUR LLM in charge of every operator-facing surface using one SKILL.md per block. Each block's prop schema is server-validated. Run blocks in PARALLEL (Promise.all) — sequential takes 60+ seconds. v1.10+ TIER 2 CUSTOMIZE TOOLS: regenerate_block (re-do one block with operator instructions; thin-harness — server bundles context, your LLM generates), upload_workspace_image (set logo/hero_background; v1.10.1+ accepts image_url or local_file_path — DON'T base64 unless you have to, the encoded string eats your tool-call token budget). v1.11+ STRUCTURAL PRIMITIVES: get_landing_structure → move_section → delete_section. INDEX-based, handle duplicate section types, atomic. v1.12+ COMPOSITE TREES: add_composite_section / update_composite_section — manifest ANY block (comparison, pricing, "how it works," stats, custom CTAs) from 12 low-level primitives. Server validates + renders; YOUR LLM composes. Read the SKILL.md via get_block_skill('composite') before composing. Every URL is real. NEVER create local files. Skipping finalize_workspace leaves the operator with no admin login.`;
|