@seldonframe/mcp 1.10.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,7 @@
1
1
  {
2
2
  "name": "@seldonframe/mcp",
3
- "version": "1.10.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.",
4
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).",
5
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).",
6
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.",
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 = []) => ({
@@ -2749,9 +2756,12 @@ export const TOOLS = [
2749
2756
  description:
2750
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). " +
2751
2758
  "Use when the operator says 'use this as my logo', 'replace the hero image with this photo', 'change the header logo'. " +
2752
- "Pass the image bytes as base64 in `image_data_b64`. The MCP server forwards them to Vercel Blob; SSL + CDN are auto-provisioned. Max 5 MB; allowed types: image/png, image/jpeg, image/webp, image/svg+xml, image/gif. " +
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. " +
2753
2764
  "Returns the public Blob URL on success; that URL is now live on the workspace's public surface within seconds. " +
2754
- "If the operator gave you a local file path, read it as bytes and base64-encode it before calling this tool. If they gave you a URL, fetch it first then re-upload (we re-host so the asset is on our CDN, not theirs). " +
2755
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.",
2756
2766
  inputSchema: obj(
2757
2767
  {
@@ -2759,28 +2769,108 @@ export const TOOLS = [
2759
2769
  slot: str(
2760
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.",
2761
2771
  ),
2762
- file_name: str(
2763
- "Original filename (for the blob path). Sanitized server-side special chars stripped, path traversal blocked.",
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.",
2764
2774
  ),
2765
- content_type: str(
2766
- "MIME type. Must be one of: image/png, image/jpeg, image/webp, image/svg+xml, image/gif.",
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.",
2767
2777
  ),
2768
2778
  image_data_b64: str(
2769
- "Image bytes, base64-encoded. Max 5 MB after decoding.",
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.",
2770
2786
  ),
2771
2787
  },
2772
- ["workspace_id", "slot", "file_name", "content_type", "image_data_b64"],
2788
+ ["workspace_id", "slot"],
2773
2789
  ),
2774
2790
  handler: async (args) => {
2775
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
+
2776
2872
  const result = await api("POST", "/workspace/v2/images", {
2777
- body: {
2778
- workspace_id: ws,
2779
- slot: args.slot,
2780
- file_name: args.file_name,
2781
- content_type: args.content_type,
2782
- image_data_b64: args.image_data_b64,
2783
- },
2873
+ body,
2784
2874
  workspace_id: ws,
2785
2875
  });
2786
2876
  return result;
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.10.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
 
@@ -194,10 +194,15 @@ further natural-language requests ("change the headline to …",
194
194
  sections without changing content. Pass the full ordered array of
195
195
  section types; multiset must equal current. For content edits use
196
196
  update_landing_section; for regeneration use regenerate_block.
197
- - **\`upload_workspace_image\`** (v1.10+) — upload a base64-encoded
198
- image to slot=logo (organizations.theme.logoUrl) or
199
- slot=hero_background (Blueprint.landing hero imageUrl + landing
200
- re-render). 5 MB max, image/* MIME types. Vercel Blob auto-CDN.
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.
201
206
  - **\`read_brain_path\`** / **\`list_brain_dir\`** — read the workspace's
202
207
  layer-1 brain (notes about THIS workspace's customers, voice, pipeline
203
208
  patterns). Use BEFORE generating blocks; reads tick the note's \`uses\`
@@ -257,4 +262,4 @@ admin dashboard. Pre-fills their email automatically.
257
262
  <https://seldonframe.com> · **Discord:** <https://discord.gg/sbVUu976NW>
258
263
  `;
259
264
 
260
- export const FIRST_CALL_BANNER = `🚀 SeldonFrame v1.10.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), reorder_landing_sections (purely mechanical reorder by section type), upload_workspace_image (base64 image Vercel Blob applied to slot=logo or slot=hero_background). 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.`;