@seldonframe/mcp 1.9.0 → 1.10.1

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,8 @@
1
1
  {
2
2
  "name": "@seldonframe/mcp",
3
- "version": "1.9.0",
3
+ "version": "1.10.1",
4
+ "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.10.1: HOTFIX — upload_workspace_image now accepts image_url + local_file_path, eliminating the agent-side base64 round-trip that v1.10.0 forced. WHY: v1.10.0's image_data_b64-only design made the agent base64-encode bytes into the JSON tool argument; the encoded string had to fit in the agent's tool-input token budget (~16 KB base64 ≈ ~12 KB raw). A 100 KB logo forced the agent into manual resize loops (verified 2026-05-05 smoke test: 7m 31s for one Cloudinary URL upload — 4 resize iterations before bytes fit). WHAT SHIPS: (1) image_url body field — SF backend fetches the URL directly via fetch + streamed read with running byte counter (5 MB cap, 5s timeout). SSRF guards: HTTPS-only, loopback / RFC1918 private / link-local IPv4 (incl. 169.254.169.254 cloud metadata) / IPv6 ::1 rejected. file_name + content_type auto-derived from URL path extension; falls back to response Content-Type header for extensionless URLs. (2) local_file_path MCP-client branch — reads the file in the npm-package process (which runs on the operator's machine, no agent token budget involved), base64-encodes for transport to the SF backend. file_name + content_type auto-derived from path extension. (3) image_data_b64 unchanged — kept as a backward-compatible fallback for callers that already have bytes in hand. (4) Tool descriptor explicitly ranks the three sources (image_url > local_file_path > image_data_b64) so the agent picks correctly. (5) 18 new unit tests covering URL validation (HTTPS-only enforcement, loopback / RFC1918 / link-local rejection, public IPv4 acceptance, malformed URL rejection), content-type derivation from URL extension (handles query strings), and file-name basename extraction. Test count: 2385 (was 2367). Time-to-upload-a-logo: ~7 min → ~3 sec (>99% reduction). NO migrations, NO new env vars, NO new dependencies. Backward compatible.",
5
+ "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.10.0: TIER 2 CUSTOMIZE TOOLS — three new operator-driven block-customization tools that respect the thin-harness/fat-skill principle (server only assembles + persists; the IDE agent's LLM does all creative work, so quality compounds as models improve with zero MCP changes). (1) regenerate_block({ workspace_id, block_name, new_instructions? }) — bundles current_props + workspace_summary (business name, industry, services, voice from organizations.soul) + brain_patterns (cross-workspace patterns for this vertical) + customization_history + the operator's new_instructions, returns next_step prose telling the agent: fetch SKILL.md, generate new props, call persist_block with customization: { prompt: <new_instructions> }. Handles first_generation (block never persisted) AND regenerate (iterate from current_props) cases with the same flow. New endpoint GET /api/v1/workspace/v2/blocks/[name]/regenerate. (2) reorder_landing_sections({ workspace_id, new_order: string[] }) — purely mechanical reorder of Blueprint.landing.sections; new_order is the full ordered array of section types ([\"hero\", \"services-grid\", \"about\", \"mid-cta\", \"faq\"]); validates the multiset matches current sections (no add/remove); rejects empty new_order, duplicates, missing types, unknown types. New endpoint POST /api/v1/workspace/v2/landing/reorder. Operator says \"move FAQ to bottom\" → agent computes new_order → server reorders + re-renders. (3) upload_workspace_image({ workspace_id, slot, file_name, content_type, image_data_b64 }) — uploads a base64-encoded image to Vercel Blob and applies the URL to one of two slots: 'logo' (organizations.theme.logoUrl, surfaces in header/footer/og-image) or 'hero_background' (Blueprint.landing.sections[hero].imageUrl + landing re-render). 5 MB max, allowed types image/png|jpeg|webp|svg+xml|gif. Sanitized blob path with random uuid suffix so re-uploads don't overwrite previous URLs. New endpoint POST /api/v1/workspace/v2/images. WHAT SHIPS BESIDES THE TOOLS: 22 new unit tests (6 regenerate context assembly, 6 reorder validation, 11 upload validation + path sanitization). Pure-function-first design — DB integration left for the v1.10+ harness; what's tested here is the security-relevant + correctness-relevant logic that lives outside any DB. Test count: 2366 (was 2344; +22). NO migrations, NO env vars, NO new dependencies (Vercel Blob already used for portal documents). Backend redeploy auto-triggers via main-branch push. v1.11+ candidates: customize_theme advanced presets (palette generation from logo / industry-aware mood), additional image slots (og_image, gallery), section delete/insert (currently reorder is the only structural change supported).",
4
6
  "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.9.0: BEHAVIORAL CONTRACT TESTS — kills the handler-execution-state-drift bug class structurally. Three bugs in 7 days hit the same shape: a server-side function `throw new Error()`s for an edge user/workspace state (no users row, no Stripe customer, free tier, empty form_fields), and the throw cascades through the SSR boundary as 'This page couldn't load.' WHAT SHIPS: (1) static analysis test (tests/unit/contract-no-uncaught-throws.spec.ts) that scans designated directories — lib/billing/**, lib/auth/**, lib/page-blocks/persist.ts, app/(dashboard)/** — for `throw new Error()` patterns. Every throw must EITHER be wrapped in try/catch OR carry a `// contract:throw-ok: <reason>` annotation. CI fails if any throw is unannotated. (2) 34 existing throws audited and annotated with semantic reasons (Stripe API errors, programmer-error sentinels, deployment config, form-submit error UX, etc.). Going forward, every NEW throw added to scoped directories must be annotated explicitly. (3) Behavioral runtime test (tests/unit/contract-booking-form-fields.spec.ts) for the v1.4.2 bug: 5 tests asserting mergeBookingFormFields always prepends standard fullName + email regardless of LLM input — preserves order, dedupes redundant LLM-supplied standards, sets required=true. mergeBookingFormFields exported from lib/page-blocks/persist.ts for testability. (4) docs/CONTRACTS.md documenting the rule, annotation pattern, scoped directories, when NOT to throw, how to add a new contract test. The MCP package itself didn't change in v1.9.0 — version bumps coupled with backend so they redeploy together. Test count: 2344 (was 2338; +1 static check + 5 booking contract tests). v1.10 candidate: integration test harness so we can write contract tests for the v1.7.3 + v1.8.1 fixes (currently documented in CONTRACTS.md but not yet runtime-asserted).",
5
7
  "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.8.1: HOTFIX for /settings/billing crashing when free-tier users click 'Manage subscription'. createBillingPortalSessionAction threw 'No Stripe customer is associated with this account' when the user had never completed a Stripe checkout — the throw cascaded through the SSR boundary and produced 'This page couldn't load — A server error occurred'. Two-layer fix: (1) UI conditionally hides 'Manage subscription' button for free tier, shows 'Upgrade' link instead. Free-tier operators see the right CTA. (2) The action gracefully redirects to /settings/billing?upgrade=needed instead of throwing — defense in depth in case a stale page state, direct form POST, or admin-token edge case still hits the action with no customer. New banner on /settings/billing surfaces the redirect reason ('No active subscription to manage yet — upgrade to manage'). No code changes to the MCP package itself; version bump is coupled with the backend redeploy.",
6
8
  "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.8.0: CUSTOM DOMAINS for paying tiers (Growth $29 / Scale $99). Operators on a paid plan can route their own hostname (joescuts.com, ironandoak.ca, bookings.coastlinemusic.ca, etc.) to their workspace; Vercel auto-provisions SSL via Let's Encrypt; subdomain path stays the default for free tier. WHAT SHIPS: (1) workspace_domains table (migration 0039, applied to prod) — workspace_id, hostname (unique among non-removed rows), status walk pending→verified→failed→removed, verification_record jsonb, vercel_domain_id, is_primary, with three indexes including a hot-path lookup for proxy.ts host→workspace resolution. (2) lib/integrations/vercel-domains.ts: thin Vercel Domains API wrapper (vercelAddDomain, vercelGetDomainConfig, vercelRemoveDomain, isValidHostname, isVercelConfigured). Requires VERCEL_TOKEN + VERCEL_PROJECT_ID env vars; returns 'vercel_not_configured' cleanly when missing. (3) lib/domains/store.ts: addCustomDomain (validate + Vercel register + persist + DNS instructions), verifyCustomDomain (Vercel /config check + state transition), listCustomDomainsForWorkspace, removeCustomDomain (mark removed in DB first, then best-effort Vercel cleanup), resolveWorkspaceForCustomDomain (the proxy.ts hot path). (4) Single REST endpoint POST /api/v1/domains dispatching on `op` field (add/verify/list/remove) with workspace bearer auth + tier gate (free returns 402 upgrade_required) + Vercel-configured gate (503 vercel_not_configured) + try/catch wrapping for JSON 500 shape. (5) Four new MCP tools: add_custom_domain, verify_domain, list_workspace_domains, remove_workspace_domain. Free-tier MCP calls surface the 402 with upgrade CTA. (6) /api/v1/public/domain (the proxy hostname-to-workspace resolver) now checks workspace_domains FIRST, then falls back to legacy organizations.settings.customDomain, then to subdomain extraction. (7) Welcome email v2: tier-conditional block between Next-Steps and Discord CTA — free tier sees 'Want your own domain?' upgrade pitch; paid tier sees 'Custom domain ready — ask Claude to add_custom_domain'. WelcomeEmailRequest now accepts optional `tier` param, validateWelcomeRequest passes it through. MCP loads 98 tools (was 94; +4 domain tools). ⚠️ BACKEND REDEPLOY REQUIRED. Migration 0039 already applied. SET VERCEL_TOKEN + VERCEL_PROJECT_ID (+ optional VERCEL_TEAM_ID) on Vercel project env vars to enable the feature; without these, MCP tools return vercel_not_configured.",
package/src/tools.js CHANGED
@@ -20,6 +20,13 @@ 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.10.1 — upload_workspace_image local_file_path branch reads the file
24
+ // directly in the MCP-client process (which runs on the operator's
25
+ // machine via the npm package). Reading + base64-encoding here means
26
+ // the agent's tool-call body stays small (just a path string), bypassing
27
+ // the agent token budget that v1.10.0's image_data_b64 path was bound by.
28
+ import { readFileSync } from "node:fs";
29
+ import path from "node:path";
23
30
 
24
31
  const str = (description, extra = {}) => ({ type: "string", description, ...extra });
25
32
  const obj = (properties, required = []) => ({
@@ -2667,6 +2674,209 @@ export const TOOLS = [
2667
2674
  },
2668
2675
  },
2669
2676
 
2677
+ // ─── v1.10.0 — block customization (regenerate / reorder / image) ──────
2678
+ //
2679
+ // These tools let the operator iterate on a workspace AFTER the v2
2680
+ // creation flow has shipped: regenerate one block with new
2681
+ // instructions ("make the hero punchier"), reorder landing sections
2682
+ // ("move FAQ to the bottom"), or upload an image (logo, hero
2683
+ // background).
2684
+ //
2685
+ // Thin harness, fat skill: server-side these tools do NO creative
2686
+ // work. regenerate_block just bundles the IDE agent everything it
2687
+ // needs (current props, workspace summary, brain patterns); the
2688
+ // agent's own LLM does the actual regeneration and calls
2689
+ // persist_block to save the result.
2690
+
2691
+ {
2692
+ name: "regenerate_block",
2693
+ description:
2694
+ "Get the bundle needed to regenerate ONE v2 page block with new operator instructions. Use this when the operator asks for a targeted change to an existing block ('make the hero punchier', 'add a card about kids cuts', 'rewrite the FAQ to be less salesy'). " +
2695
+ "Returns: current_props (so your LLM can iterate rather than start fresh), workspace_summary (business name, industry, services, voice from the workspace's soul), brain_patterns (anonymized cross-workspace patterns for this vertical), customization_history (previous edits — useful for understanding what NOT to revert), and the operator's new_instructions (echoed back so they're visible in your context). " +
2696
+ "The next move is YOURS: fetch the block's SKILL.md via get_block_skill, generate new props that satisfy the prop schema while applying new_instructions, then call persist_block with `customization: { prompt: <new_instructions> }` to record the change. " +
2697
+ "If the block has never been persisted (status=first_generation), this is a normal first-time generation path — same downstream flow, just no current_props to iterate from. " +
2698
+ "Antifragile design note: this tool only ASSEMBLES context. Your LLM does the creative work. As models improve, regeneration quality improves with zero MCP changes.",
2699
+ inputSchema: obj(
2700
+ {
2701
+ workspace_id: str("Workspace id from create_workspace_v2."),
2702
+ block_name: str(
2703
+ "Block name to regenerate. Must match a v2 block: hero, services, about, faq, cta, booking, intake.",
2704
+ ),
2705
+ new_instructions: str(
2706
+ "Optional: the operator's natural-language regeneration request ('make it more urgent', 'shorter copy', 'less salesy'). When provided, surfaced in the response and used in the customization field of the subsequent persist_block call. Omit when the operator just wants a fresh roll without specific guidance.",
2707
+ ),
2708
+ },
2709
+ ["workspace_id", "block_name"],
2710
+ ),
2711
+ handler: async (args) => {
2712
+ const ws = args.workspace_id;
2713
+ const blockName = encodeURIComponent(args.block_name);
2714
+ const qs = args.new_instructions
2715
+ ? `?new_instructions=${encodeURIComponent(args.new_instructions)}`
2716
+ : "";
2717
+ const result = await api(
2718
+ "GET",
2719
+ `/workspace/v2/blocks/${blockName}/regenerate${qs}`,
2720
+ { workspace_id: ws },
2721
+ );
2722
+ return result;
2723
+ },
2724
+ },
2725
+
2726
+ {
2727
+ name: "reorder_landing_sections",
2728
+ description:
2729
+ "Reorder the sections of a workspace's landing page WITHOUT changing their content. Use when the operator says 'move FAQ to the bottom', 'put services after the about section', 'rearrange so the CTA is below testimonials'. " +
2730
+ "Pass `new_order` as the full ordered array of section types as they should appear top-to-bottom. The multiset of types in new_order MUST equal the current landing's section types — no add/remove. Section types include: hero, services-grid, about, mid-cta, faq, testimonials, trust-strip, emergency-strip, service-area, partners, footer (the actual set depends on what's currently on the page). " +
2731
+ "Returns the new sections_order on success or validation_errors on failure (missing/extra types, duplicates). For content edits use update_landing_section. To regenerate a block's content use regenerate_block. To get the current order, fetch the workspace's landing or call regenerate_block (which exposes block names) — most landing pages start as: hero → services → about → faq → mid-cta.",
2732
+ inputSchema: obj(
2733
+ {
2734
+ workspace_id: str("Workspace id."),
2735
+ new_order: {
2736
+ type: "array",
2737
+ description:
2738
+ "Ordered array of section type strings. Must contain EVERY section type currently on the landing page, exactly once each. Example: [\"hero\", \"services-grid\", \"about\", \"mid-cta\", \"faq\"].",
2739
+ items: { type: "string" },
2740
+ },
2741
+ },
2742
+ ["workspace_id", "new_order"],
2743
+ ),
2744
+ handler: async (args) => {
2745
+ const ws = args.workspace_id;
2746
+ const result = await api("POST", "/workspace/v2/landing/reorder", {
2747
+ body: { workspace_id: ws, new_order: args.new_order },
2748
+ workspace_id: ws,
2749
+ });
2750
+ return result;
2751
+ },
2752
+ },
2753
+
2754
+ {
2755
+ name: "upload_workspace_image",
2756
+ description:
2757
+ "Upload an image to a workspace and apply it to one of two slots: 'logo' (replaces organizations.theme.logoUrl, surfaces in header / footer / og-image / favicon) or 'hero_background' (replaces the hero section's background image and re-renders the landing page). " +
2758
+ "Use when the operator says 'use this as my logo', 'replace the hero image with this photo', 'change the header logo'. " +
2759
+ "PICK ONE source — the others are mutually exclusive: " +
2760
+ "(a) `image_url` (PREFERRED, v1.10.1+) — public HTTPS URL to the image. The SF backend fetches it directly. Best path for Cloudinary, Unsplash, S3, or any image already on the web. file_name + content_type are auto-derived from the URL — you don't need to pass them. " +
2761
+ "(b) `local_file_path` (v1.10.1+) — absolute path on the operator's machine. The MCP server (running locally) reads the file and forwards bytes to the backend. Best path for files on the operator's desktop. file_name + content_type derived from the path. " +
2762
+ "(c) `image_data_b64` (legacy v1.10.0) — image bytes base64-encoded. Use only when you've generated bytes yourself (e.g. dynamic image gen) and there's no URL or path. Be aware: the encoded string consumes your tool-call token budget; for files >~12 KB raw, prefer (a) or (b). " +
2763
+ "Max 5 MB across all paths. Allowed types: image/png, image/jpeg, image/webp, image/svg+xml, image/gif. " +
2764
+ "Returns the public Blob URL on success; that URL is now live on the workspace's public surface within seconds. " +
2765
+ "Antifragile design: server only validates file shape + applies URL to the right column. Your LLM picks which slot ('they said logo, that maps to slot=logo'). As you get better at intent-mapping, the harness doesn't change.",
2766
+ inputSchema: obj(
2767
+ {
2768
+ workspace_id: str("Workspace id."),
2769
+ slot: str(
2770
+ "Image slot: 'logo' (workspace logo, used in header/footer/og-image) or 'hero_background' (hero section background image). Other slots may be added in future versions.",
2771
+ ),
2772
+ image_url: str(
2773
+ "Public HTTPS URL to the image. PREFERRED source. SF backend fetches directly (no base64 round-trip). file_name + content_type auto-derived. https:// only; loopback / private / link-local IPs rejected.",
2774
+ ),
2775
+ local_file_path: str(
2776
+ "Absolute path to a file on the operator's machine (the MCP server runs there). MCP reads the file and forwards bytes to the backend. file_name + content_type auto-derived from the path. Use when the operator gives you a local file rather than a URL.",
2777
+ ),
2778
+ image_data_b64: str(
2779
+ "Image bytes, base64-encoded. LEGACY path — prefer image_url or local_file_path because base64 consumes your tool-call token budget. Max 5 MB after decoding.",
2780
+ ),
2781
+ file_name: str(
2782
+ "Optional filename (auto-derived from image_url or local_file_path). REQUIRED with image_data_b64.",
2783
+ ),
2784
+ content_type: str(
2785
+ "Optional MIME type (auto-derived from image_url or local_file_path extension). REQUIRED with image_data_b64. Must be one of: image/png, image/jpeg, image/webp, image/svg+xml, image/gif.",
2786
+ ),
2787
+ },
2788
+ ["workspace_id", "slot"],
2789
+ ),
2790
+ handler: async (args) => {
2791
+ const ws = args.workspace_id;
2792
+
2793
+ // Resolve to ONE source. Reject ambiguous + missing input early so
2794
+ // the agent gets a clean error rather than a server-side 400.
2795
+ const sourceCount =
2796
+ (args.image_url ? 1 : 0) +
2797
+ (args.local_file_path ? 1 : 0) +
2798
+ (args.image_data_b64 ? 1 : 0);
2799
+ if (sourceCount === 0) {
2800
+ throw new Error(
2801
+ "upload_workspace_image: provide ONE of image_url, local_file_path, or image_data_b64.",
2802
+ );
2803
+ }
2804
+ if (sourceCount > 1) {
2805
+ throw new Error(
2806
+ "upload_workspace_image: provide ONE of image_url, local_file_path, or image_data_b64 — not multiple.",
2807
+ );
2808
+ }
2809
+
2810
+ const body = {
2811
+ workspace_id: ws,
2812
+ slot: args.slot,
2813
+ };
2814
+
2815
+ if (args.image_url) {
2816
+ body.image_url = args.image_url;
2817
+ if (args.file_name) body.file_name = args.file_name;
2818
+ if (args.content_type) body.content_type = args.content_type;
2819
+ } else if (args.local_file_path) {
2820
+ // Read the file in the MCP-client process. Path is absolute (per
2821
+ // the schema). Reject obvious dir-traversal — readFileSync would
2822
+ // succeed on any readable file but operators expect file paths,
2823
+ // not directories.
2824
+ const filePath = args.local_file_path;
2825
+ if (!path.isAbsolute(filePath)) {
2826
+ throw new Error(
2827
+ `upload_workspace_image: local_file_path must be absolute. Got: ${filePath}`,
2828
+ );
2829
+ }
2830
+ let buf;
2831
+ try {
2832
+ buf = readFileSync(filePath);
2833
+ } catch (err) {
2834
+ throw new Error(
2835
+ `upload_workspace_image: failed to read local_file_path "${filePath}" — ${err?.message ?? err}`,
2836
+ );
2837
+ }
2838
+ // Derive file_name + content_type from the path extension, mirroring
2839
+ // the server-side image_url logic so the two paths feel identical
2840
+ // to the operator.
2841
+ const baseName = path.basename(filePath);
2842
+ const ext = path.extname(filePath).toLowerCase().replace(/^\./, "");
2843
+ const extToContentType = {
2844
+ png: "image/png",
2845
+ jpg: "image/jpeg",
2846
+ jpeg: "image/jpeg",
2847
+ gif: "image/gif",
2848
+ webp: "image/webp",
2849
+ svg: "image/svg+xml",
2850
+ };
2851
+ const derivedContentType = extToContentType[ext] ?? null;
2852
+ body.image_data_b64 = buf.toString("base64");
2853
+ body.file_name = args.file_name || baseName;
2854
+ body.content_type = args.content_type || derivedContentType || "";
2855
+ if (!body.content_type) {
2856
+ throw new Error(
2857
+ `upload_workspace_image: could not infer content_type from extension "${ext}" — pass content_type explicitly. Allowed: image/png, image/jpeg, image/webp, image/svg+xml, image/gif.`,
2858
+ );
2859
+ }
2860
+ } else {
2861
+ // image_data_b64 — caller supplied bytes directly.
2862
+ body.image_data_b64 = args.image_data_b64;
2863
+ if (!args.file_name || !args.content_type) {
2864
+ throw new Error(
2865
+ "upload_workspace_image: file_name and content_type are required with image_data_b64. (image_url and local_file_path auto-derive them.)",
2866
+ );
2867
+ }
2868
+ body.file_name = args.file_name;
2869
+ body.content_type = args.content_type;
2870
+ }
2871
+
2872
+ const result = await api("POST", "/workspace/v2/images", {
2873
+ body,
2874
+ workspace_id: ws,
2875
+ });
2876
+ return result;
2877
+ },
2878
+ },
2879
+
2670
2880
  // ─── v1.6.0 — brain layer (Karpathy LLM-Wiki) ───────────────────────────
2671
2881
  //
2672
2882
  // Two-layer brain stored as a file-tree of markdown notes:
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.9.0";
11
+ export const VERSION = "1.10.1";
12
12
 
13
13
  export const WELCOME_MARKDOWN = `# SeldonFrame — create a real Business OS in one conversation
14
14
 
@@ -185,6 +185,24 @@ further natural-language requests ("change the headline to …",
185
185
  + renders + replaces the matching section in the workspace's landing.
186
186
  - **\`complete_workspace_v2\`** — marks the v2 flow finished, reports which
187
187
  blocks landed.
188
+ - **\`regenerate_block\`** (v1.10+) — bundles current props + workspace
189
+ summary + brain patterns + the operator's new instructions for
190
+ block re-generation ("make the hero punchier", "rewrite the FAQ to
191
+ be less salesy"). Server only assembles context; YOUR LLM does the
192
+ generation, then call persist_block with \`customization\`.
193
+ - **\`reorder_landing_sections\`** (v1.10+) — reorder landing-page
194
+ sections without changing content. Pass the full ordered array of
195
+ section types; multiset must equal current. For content edits use
196
+ update_landing_section; for regeneration use regenerate_block.
197
+ - **\`upload_workspace_image\`** (v1.10+, fast path in v1.10.1+) — set
198
+ the workspace logo (slot=logo → organizations.theme.logoUrl) or hero
199
+ background (slot=hero_background → Blueprint.landing hero imageUrl
200
+ + landing re-render). PREFERRED: pass \`image_url\` (HTTPS — server
201
+ fetches directly, no base64) or \`local_file_path\` (absolute path —
202
+ MCP reads the file). Auto-derives file_name + content_type. Legacy:
203
+ \`image_data_b64\` for caller-generated bytes, but base64 consumes
204
+ your tool-call token budget — avoid for files >~12 KB raw. 5 MB max,
205
+ image/png|jpeg|webp|svg+xml|gif. Vercel Blob auto-CDN.
188
206
  - **\`read_brain_path\`** / **\`list_brain_dir\`** — read the workspace's
189
207
  layer-1 brain (notes about THIS workspace's customers, voice, pipeline
190
208
  patterns). Use BEFORE generating blocks; reads tick the note's \`uses\`
@@ -244,4 +262,4 @@ admin dashboard. Pre-fills their email automatically.
244
262
  <https://seldonframe.com> · **Discord:** <https://discord.gg/sbVUu976NW>
245
263
  `;
246
264
 
247
- export const FIRST_CALL_BANNER = `🚀 SeldonFrame v1.4.1 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. Every URL is real. NEVER create local files. Skipping finalize_workspace leaves the operator with no admin login.`;
265
+ export const FIRST_CALL_BANNER = `🚀 SeldonFrame v1.10.1 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), reorder_landing_sections (purely mechanical reorder by section type), 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). Every URL is real. NEVER create local files. Skipping finalize_workspace leaves the operator with no admin login.`;