@seldonframe/mcp 1.6.0 → 1.7.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 CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@seldonframe/mcp",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
+ "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.7.0: MAGIC-LINK DEVICE-FLOW AUTH. Operators can now connect a fresh IDE/device (Claude Code on a new laptop, Cursor on a different machine, etc.) to an existing SeldonFrame workspace with one email click — no token copy-paste, no re-running create_workspace, no friction. WHAT SHIPS: (1) device_auth_requests table (migration 0038, applied to prod) — atok (single-use 256-bit URL-safe random), workspace_id, email, device_label, status walk (pending → approved/rejected/expired), expires_at (5-min TTL), issuedTokenRaw (encrypted at rest, one-shot cleared on claim). (2) lib/auth/device-auth.ts: initiateDeviceAuth (mints atok + writes pending row), approveDeviceAuth (marks approved + mints workspace bearer via existing mintWorkspaceToken with 7-day TTL), rejectDeviceAuth, checkDeviceAuth (atomic claim — concurrent polls race; only one wins; raw token cleared after claim), lookupDeviceAuthForApprovalPage (read-only, for the browser page render). (3) lib/emails/device-auth.ts: rendered HTML + plain-text email template, sender via existing Resend infrastructure with verified welcome@seldonframe.com. (4) Four anonymous API endpoints — POST /api/v1/auth/initiate (rate-limited 10/hr/IP, sends email + returns atok), POST /api/v1/auth/approve (called by browser approval page on Yes), POST /api/v1/auth/reject (called on No), GET /api/v1/auth/check?atok=... (polled by the MCP server). (5) Browser approval page at /auth?atok=... — server-component shell + client-component Yes/No buttons. Renders workspace name + device label + email so the operator can verify they're authorizing the right device. Pre-renders different states for already-approved / rejected / expired / claimed. (6) New MCP tool connect_workspace(workspace_slug, email, device_label?). Calls initiate, then polls /api/v1/auth/check every 2s for up to 5 minutes. On approval, stores the bearer locally + sets as default workspace. Auto-detects device label from os.hostname() unless overridden. (7) Welcome message updated to teach the new tool — `create_workspace_v2` for new workspaces, `connect_workspace` for existing ones from new devices. MCP loads 94 tools (was 93; +1). ⚠️ BACKEND REDEPLOY REQUIRED. Migration 0038 already applied to prod by the script in this commit. RESEND_API_KEY env var must be set (already is — same one welcome emails use).",
4
5
  "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.6.0: BRAIN LAYER (the compounding moat). Two-layer file-tree of markdown notes implementing the Karpathy LLM-Wiki pattern: Layer 1 is per-workspace (notes about THIS workspace's customers, voice, pipeline patterns, learnings); Layer 2 is anonymized cross-workspace patterns the weekly cron promotes when 3+ workspaces independently observe the same thing with confidence ≥ 0.7. Each note carries a Bayesian-smoothed confidence score `(wins + 1) / (uses + 2)` so the system self-prunes bad entries (confidence drops, weekly cron archives) and self-promotes good ones (workspace pattern hits threshold, cron creates global note). The IDE agent reads relevant notes before generating blocks; the brain compounds across every workspace interaction. WHAT SHIPS: (1) brain_notes table (migration 0037, applied to prod) with org_id-NULL-for-global discriminator, scope text, path file-tree key, body markdown, metadata jsonb, confidence/uses/wins counters, and indexes for prefix listing + promotion + pruning. (2) lib/brain/store.ts CRUD: readBrainNote (ticks uses + last_used_at), listBrainDir (prefix-filtered, returns body previews), writeBrainNote (upsert, preserves confidence), appendToBrainNote (prepends dated paragraphs, 8KB cap), deleteBrainNote, computeConfidence, markBrainOutcome (win/loss feedback), findPromotionCandidates (uses ≥ 10, confidence ≥ 0.7, ≥ 3 workspaces), findPruneCandidates (confidence < 0.3, uses ≥ 10). (3) Single REST endpoint POST /api/v1/brain dispatching on `op` field (read|list|write|append|delete|list_patterns) with workspace bearer auth + path-traversal guards. (4) Four new MCP tools: read_brain_path, list_brain_dir, write_brain_note (with append flag), list_brain_patterns. Welcome message updated to teach the brain pattern. (5) Per-interaction triggers: submitPublicBookingAction appends to pipeline/booked-appointments.md when a booking confirms; the public intake POST appends a non-PII summary (answered/skipped field counts + low-risk values) to intake/recent-leads.md. Best-effort — failures don't block the user-facing response. (6) Weekly cron at /api/cron/brain-promote scheduled Sundays 05:00 UTC in vercel.json. Promotes high-confidence workspace notes to layer 2 (synthesized as bullet-list aggregations for v1.6; LLM-based synthesis lands in v1.7); prunes low-confidence workspace notes. Both ops idempotent. (7) create_workspace_v2 now pre-fetches up to 10 layer-2 patterns matching the resolved personality vertical and returns them inline as v2.brain_patterns so the IDE agent has cross-workspace context without a second round-trip. The brain compounds: each new workspace's interactions feed the brain, the cron promotes patterns weekly, the next workspace's IDE agent reads richer context, output quality improves over time WITHOUT code changes — the Karpathy compounding moat. ⚠️ BACKEND REDEPLOY REQUIRED. Migration 0037 already applied to prod. Set CRON_SECRET env var on Vercel if not already set.",
5
6
  "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.5.1: HOTFIX — five fixes from the Coastline Music workspace test. (1) BLOCKING — apply-0031-prod.mjs applied the missing portal_documents migration that was crashing every contact-detail page load with `relation \"portal_documents\" does not exist`. Operators can now view lead records again. (2) Lucide icon allowlist now ENFORCED — pre-1.5.1 the LLM could pick icon names like \"piano\", \"microphone\", \"wood_oven\" that the renderer didn't have, so all those services-grid cards rendered with the same fallback icon (visible on Coastline Music + Cinder & Salt). New ICON_NAMES export from lucide-icons.ts (derived from ICON_PATHS so it stays in sync); new services validator rejects unknown icons and returns the full allowlist in the error so the LLM can self-correct on retry. (3) Smart pipeline-stage selection on booking — pre-1.5.1 every booking landed at the FIRST pipeline stage (\"Inquiry\" / \"Lead\"), forcing operators to manually move every deal up a stage. Now the deal-insert path matches stages whose names contain booked / scheduled / trial / appointment / consultation / reservation and uses that stage instead, falling back to first stage when no match exists. Personality-generated pipelines that declare \"Trial Lesson Booked\" / \"Estimate Scheduled\" automatically get the right behavior. New stage_match telemetry tag (\"smart\" vs \"first_stage_fallback\") + available_stages array in the public_booking_deal_created log. (4) FAQ headline centering bulletproofed with !important + max-width: none override on .sf-faq > .sf-faq__headline — the .sf-faq > * { max-width: 48rem } rule was constraining the H2 narrower than the page, making centered text appear left-anchored. (5) Hero subhead text-align: center pinned in base CSS (was inheriting parent text-align which varied by overlay), plus eyebrow pill background hardened to rgba(0,0,0,0.45) with white border + backdrop-filter blur on hero-with-image so the eyebrow text stays readable against any photo. ⚠️ BACKEND REDEPLOY REQUIRED. Migration 0031 already applied to prod by the same script that ships in this commit.",
6
7
  "description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.5.0: BLOCK CODEGEN — kills the prop-schema-drift bug class structurally. Pre-1.5 every block's prop schema lived in TWO places: the SKILL.md frontmatter (LLM-readable YAML, the source of truth for the agent's generation prompt) AND lib/page-blocks/registry.ts (runtime Zod, the source of truth for server validation). Editing one without the other was undetectable until a runtime failure surfaced it; the Cinder & Salt booking-form bug (v1.4.2 hotfix) was one such failure. v1.5 makes drift structurally impossible: SKILL.md frontmatter is the single source of truth, and `pnpm blocks:emit` parses each block's frontmatter and generates packages/crm/src/blocks/<name>/__generated__/block.ts containing the Zod schema, the inferred TypeScript Props type, and the block metadata constants (name, version, surface, sectionType, description). The registry imports from these generated files; the toSection mapping + deterministic copy validators stay handwritten because they're logic, not schema. CI gate: `pnpm blocks:emit:check` re-runs the emitter and exits non-zero if any __generated__/block.ts is stale relative to its SKILL.md — a new unit test (tests/unit/blocks-codegen-staleness.spec.ts) makes every PR fail if anyone edits a SKILL.md without regenerating. Codegen handles all the types we use across 7 blocks: string/number/boolean with min/max bounds, enum, object with nested properties, array of objects with min_items/max_items, tuple (booking weekly_availability uses [openHour, closeHour] pairs), nullable, optional/required. Adds `yaml` devDependency for frontmatter parsing. Documented in packages/crm/src/blocks/README.md. ⚠️ BACKEND REDEPLOY REQUIRED.",
package/src/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
2
- import { homedir } from "node:os";
2
+ import { homedir, hostname } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { VERSION } from "./welcome.js";
5
5
 
@@ -204,3 +204,17 @@ export function htmlToText(html) {
204
204
  }
205
205
 
206
206
  export const API_INFO = { base: API_BASE };
207
+
208
+ /**
209
+ * v1.7.0 — best-effort device label for magic-link device-flow auth.
210
+ * Falls back to "Claude Code (unknown device)" if hostname lookup
211
+ * fails (some sandboxed runtimes block it).
212
+ */
213
+ export function defaultDeviceLabel() {
214
+ try {
215
+ const host = hostname();
216
+ return `Claude Code on ${host}`;
217
+ } catch {
218
+ return "Claude Code (unknown device)";
219
+ }
220
+ }
package/src/tools.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  api,
3
3
  API_INFO,
4
+ defaultDeviceLabel,
4
5
  fetchText,
5
6
  forgetWorkspace,
6
7
  htmlToText,
@@ -2772,6 +2773,135 @@ export const TOOLS = [
2772
2773
  },
2773
2774
  },
2774
2775
 
2776
+ // ─── v1.7.0 — magic-link device-flow auth ──────────────────────────────
2777
+ //
2778
+ // Use connect_workspace when the operator wants to ADMIN AN EXISTING
2779
+ // workspace from a NEW device/IDE. The flow:
2780
+ // 1. operator: "connect me to my iron-oak-barbershop workspace,
2781
+ // my email is marc@ironoak.ca"
2782
+ // 2. tool calls /api/v1/auth/initiate, gets atok + emails operator
2783
+ // 3. operator opens email, clicks the magic link
2784
+ // 4. browser approval page renders (workspace + device label),
2785
+ // operator clicks "Yes, authorize"
2786
+ // 5. tool's internal poll resolves, gets a fresh workspace bearer,
2787
+ // stores it locally + sets as default workspace
2788
+ //
2789
+ // For NEW workspaces (no existing slug), use create_workspace_v2
2790
+ // which mints a bearer at creation time and doesn't need a magic link.
2791
+
2792
+ {
2793
+ name: "connect_workspace",
2794
+ description:
2795
+ "Connect this device/IDE to an EXISTING SeldonFrame workspace via magic-link email. Use when the operator already has a workspace (e.g. created from another device) and wants to admin it from this Claude Code / Cursor / Windsurf session. Sends a confirmation email with a one-click approval link; the tool polls until approval (5-min timeout) then stores the workspace bearer locally. For brand-new workspaces, use create_workspace_v2 instead.",
2796
+ inputSchema: obj(
2797
+ {
2798
+ workspace_slug: str(
2799
+ "Workspace slug (the subdomain prefix). Example: 'iron-oak-barbershop' for iron-oak-barbershop.app.seldonframe.com.",
2800
+ ),
2801
+ email: str(
2802
+ "Operator's email — must match an email associated with the workspace owner. Magic link is sent here.",
2803
+ ),
2804
+ device_label: str(
2805
+ "Optional human-readable label shown to the operator on the approval page so they can verify they're authorizing the right device. Defaults to a hostname-based label.",
2806
+ ),
2807
+ },
2808
+ ["workspace_slug", "email"],
2809
+ ),
2810
+ handler: async (args) => {
2811
+ const deviceLabel = args.device_label?.trim() || defaultDeviceLabel();
2812
+
2813
+ // Step 1: initiate. Anonymous endpoint; no bearer required.
2814
+ const initiateRes = await fetch(`${API_INFO.base}/auth/initiate`, {
2815
+ method: "POST",
2816
+ headers: {
2817
+ "Content-Type": "application/json",
2818
+ "User-Agent": `seldonframe-mcp/${VERSION}`,
2819
+ },
2820
+ body: JSON.stringify({
2821
+ workspace_slug: args.workspace_slug,
2822
+ email: args.email,
2823
+ device_label: deviceLabel,
2824
+ }),
2825
+ });
2826
+ const initiateBody = await initiateRes.json().catch(() => ({}));
2827
+ if (!initiateRes.ok || !initiateBody.ok) {
2828
+ throw new Error(
2829
+ `connect_workspace: initiate failed (${initiateRes.status}): ${initiateBody.error ?? initiateRes.statusText}`,
2830
+ );
2831
+ }
2832
+
2833
+ const { atok, approval_url, expires_at, workspace } = initiateBody;
2834
+
2835
+ // Step 2: poll until approved or expired. 2-second cadence,
2836
+ // 5-minute total budget (the atok TTL on the server).
2837
+ const POLL_INTERVAL_MS = 2000;
2838
+ const POLL_BUDGET_MS = 5 * 60 * 1000;
2839
+ const start = Date.now();
2840
+ let token = null;
2841
+ let workspaceId = null;
2842
+ let lastStatus = "pending";
2843
+ while (Date.now() - start < POLL_BUDGET_MS) {
2844
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
2845
+ const checkRes = await fetch(
2846
+ `${API_INFO.base}/auth/check?atok=${encodeURIComponent(atok)}`,
2847
+ {
2848
+ headers: { "User-Agent": `seldonframe-mcp/${VERSION}` },
2849
+ },
2850
+ );
2851
+ const checkBody = await checkRes.json().catch(() => ({}));
2852
+ lastStatus = checkBody.status ?? "pending";
2853
+ if (lastStatus === "pending") continue;
2854
+ if (lastStatus === "approved" && checkBody.token) {
2855
+ token = checkBody.token;
2856
+ workspaceId = checkBody.workspace_id;
2857
+ break;
2858
+ }
2859
+ // Terminal failure.
2860
+ return {
2861
+ ok: false,
2862
+ status: lastStatus,
2863
+ error:
2864
+ lastStatus === "rejected"
2865
+ ? `You (or someone with access to ${args.email}) clicked "No, this wasn't me" on the approval page. The connection was not authorized.`
2866
+ : lastStatus === "expired"
2867
+ ? `The approval link expired before being clicked. Run connect_workspace again to get a fresh link.`
2868
+ : lastStatus === "already_claimed"
2869
+ ? `This authorization was already claimed by another session — the bearer can only be issued once.`
2870
+ : `Authorization failed with status: ${lastStatus}`,
2871
+ approval_url,
2872
+ };
2873
+ }
2874
+
2875
+ if (!token) {
2876
+ return {
2877
+ ok: false,
2878
+ status: lastStatus,
2879
+ error: `Authorization timed out after 5 minutes. The link is still valid — open the email and click "Authorize", or run connect_workspace again to get a fresh one.`,
2880
+ approval_url,
2881
+ expires_at,
2882
+ };
2883
+ }
2884
+
2885
+ // Step 3: store the bearer locally + set as default workspace.
2886
+ rememberWorkspace({
2887
+ workspace_id: workspaceId,
2888
+ bearer_token: token,
2889
+ });
2890
+
2891
+ return {
2892
+ ok: true,
2893
+ connected: {
2894
+ workspace_id: workspaceId,
2895
+ slug: workspace.slug,
2896
+ name: workspace.name,
2897
+ device_label: deviceLabel,
2898
+ public_url: `https://${workspace.slug}.app.seldonframe.com/`,
2899
+ },
2900
+ message: `Connected ${deviceLabel} to ${workspace.name}. You can now run any workspace tool (list_contacts, persist_block, customize_block, etc.) and it will act on this workspace by default.`,
2901
+ };
2902
+ },
2903
+ },
2904
+
2775
2905
  {
2776
2906
  name: "list_brain_patterns",
2777
2907
  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.6.0";
11
+ export const VERSION = "1.7.0";
12
12
 
13
13
  export const WELCOME_MARKDOWN = `# SeldonFrame — create a real Business OS in one conversation
14
14
 
@@ -169,6 +169,10 @@ further natural-language requests ("change the headline to …",
169
169
  generate using your own LLM. The first call for any new workspace.
170
170
  v1.6+ also returns \`brain_patterns\` — anonymized cross-workspace
171
171
  insights for this vertical that you should fold into your generation.
172
+ - **\`connect_workspace\`** (v1.7+) — connect this device to an EXISTING
173
+ workspace via magic-link email. Use when the operator already has a
174
+ workspace (created from another device) and wants to admin it from
175
+ this IDE. Sends a confirmation email; tool polls until approved.
172
176
  - **\`list_blocks\`** — lists v2 page-block primitives available.
173
177
  - **\`get_block_skill\`** — fetches one block's SKILL.md (the generation
174
178
  prompt + prop schema you read before generating props).