@seldonframe/mcp 1.5.1 → 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 +3 -1
- package/src/client.js +15 -1
- package/src/tools.js +273 -0
- package/src/welcome.js +17 -1
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seldonframe/mcp",
|
|
3
|
-
"version": "1.
|
|
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).",
|
|
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.",
|
|
4
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.",
|
|
5
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.",
|
|
6
8
|
"description": "MCP server for SeldonFrame — AI-native Business OS platform. v1.4.2: HOTFIX for the booking-form regression introduced by v1.4.1. The v2 booking persist path replaced Blueprint.booking.formFields wholesale with whatever the LLM generated — wiping out the standard `fullName` + `email` fields v1's bootstrap had set. Result: every v2 workspace shipped a booking form with NO name/email inputs, and submitPublicBookingAction rejected every submit with `missing_required_field fullName_present:false email_present:false`. Confirmed on Cinder & Salt (Hamilton wood-fired pizza) where the form had Party Size + Occasion + Allergies but no name/email. v1.4.2 fixes: (1) New mergeBookingFormFields helper in lib/page-blocks/persist.ts — server ALWAYS prepends fullName + email, dedupes against any LLM-provided field with the same id; LLM-provided extras (party_size, dog_name, service_address, etc.) keep working unchanged. (2) booking SKILL.md updated — voice rules now correctly say 'EXTRAS only, server adds standard name+email' instead of the wrong 'renderer adds them automatically'. (3) Backfill scripts shipped: backfill-booking-form-fields.mjs (47 prod workspaces had their blueprint.booking.formFields patched to include the standard fields) + scripts/rerender-all-bookings-v142.ts (re-renders bookings.content_html for 63 booking templates so the live booking pages actually show the form fields). Both scripts already executed against prod. ⚠️ 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,
|
|
@@ -2660,6 +2661,278 @@ export const TOOLS = [
|
|
|
2660
2661
|
return result;
|
|
2661
2662
|
},
|
|
2662
2663
|
},
|
|
2664
|
+
|
|
2665
|
+
// ─── v1.6.0 — brain layer (Karpathy LLM-Wiki) ───────────────────────────
|
|
2666
|
+
//
|
|
2667
|
+
// Two-layer brain stored as a file-tree of markdown notes:
|
|
2668
|
+
// - Layer 1 (workspace): notes about THIS workspace's customers, voice,
|
|
2669
|
+
// pipeline patterns, learnings. Reads cost 0 LLM tokens server-side
|
|
2670
|
+
// (the IDE agent's LLM consumes them as context).
|
|
2671
|
+
// - Layer 2 (global): cross-workspace patterns, anonymized. The weekly
|
|
2672
|
+
// cron promotes high-confidence workspace notes that appear in 3+
|
|
2673
|
+
// workspaces.
|
|
2674
|
+
//
|
|
2675
|
+
// Use these tools BEFORE generating a block to give the IDE agent's
|
|
2676
|
+
// LLM workspace-specific + vertical-specific context. Use them AFTER
|
|
2677
|
+
// a successful interaction to write back what worked.
|
|
2678
|
+
|
|
2679
|
+
{
|
|
2680
|
+
name: "read_brain_path",
|
|
2681
|
+
description:
|
|
2682
|
+
"Read a single brain note from the workspace's layer-1 brain. Returns the body (markdown), confidence (0-1), uses (times read), wins (times the consuming interaction was successful), and metadata. Reading a note increments its `uses` counter — that's how the feedback loop knows the note has been consumed. Use BEFORE generating blocks: check for relevant entries (voice/copy-that-works.md, customers/recurring.md, learnings.md) so your generation reflects what's been observed about this workspace.",
|
|
2683
|
+
inputSchema: obj(
|
|
2684
|
+
{
|
|
2685
|
+
workspace_id: str("Workspace id."),
|
|
2686
|
+
path: str(
|
|
2687
|
+
"Note path. Examples: voice/copy-that-works.md, customers/recurring.md, pipeline/closed-won-patterns.md, learnings.md.",
|
|
2688
|
+
),
|
|
2689
|
+
},
|
|
2690
|
+
["workspace_id", "path"],
|
|
2691
|
+
),
|
|
2692
|
+
handler: async (args) => {
|
|
2693
|
+
const ws = args.workspace_id;
|
|
2694
|
+
const result = await api("POST", "/brain", {
|
|
2695
|
+
body: { op: "read", path: args.path },
|
|
2696
|
+
workspace_id: ws,
|
|
2697
|
+
});
|
|
2698
|
+
return result;
|
|
2699
|
+
},
|
|
2700
|
+
},
|
|
2701
|
+
|
|
2702
|
+
{
|
|
2703
|
+
name: "list_brain_dir",
|
|
2704
|
+
description:
|
|
2705
|
+
"List brain notes in the workspace's layer-1 brain. Returns metadata + a 120-char body preview per note (full body requires read_brain_path). Use to discover what the brain knows about this workspace before generating blocks. Pass `prefix` to filter by directory (e.g. 'voice/' returns voice-related notes only). Notes are returned sorted by confidence descending.",
|
|
2706
|
+
inputSchema: obj(
|
|
2707
|
+
{
|
|
2708
|
+
workspace_id: str("Workspace id."),
|
|
2709
|
+
prefix: str(
|
|
2710
|
+
"Optional path prefix to filter (e.g. 'voice/', 'customers/'). Omit for all notes.",
|
|
2711
|
+
),
|
|
2712
|
+
},
|
|
2713
|
+
["workspace_id"],
|
|
2714
|
+
),
|
|
2715
|
+
handler: async (args) => {
|
|
2716
|
+
const ws = args.workspace_id;
|
|
2717
|
+
const result = await api("POST", "/brain", {
|
|
2718
|
+
body: { op: "list", prefix: args.prefix ?? undefined },
|
|
2719
|
+
workspace_id: ws,
|
|
2720
|
+
});
|
|
2721
|
+
return result;
|
|
2722
|
+
},
|
|
2723
|
+
},
|
|
2724
|
+
|
|
2725
|
+
{
|
|
2726
|
+
name: "write_brain_note",
|
|
2727
|
+
description:
|
|
2728
|
+
"Write a brain note to the workspace's layer-1 brain. Use to capture insights the operator volunteers ('walk-ins on Saturday convert 3× better', 'don't ever say synergy in the copy', 'most leads come in via Instagram'). The note is REPLACED on subsequent writes to the same path; for append-style writes use `append: true`. Source field is recorded so the cron can attribute promotions correctly.",
|
|
2729
|
+
inputSchema: obj(
|
|
2730
|
+
{
|
|
2731
|
+
workspace_id: str("Workspace id."),
|
|
2732
|
+
path: str(
|
|
2733
|
+
"Note path. Convention: <category>/<topic>.md. Examples: customers/recurring.md, voice/copy-that-works.md, learnings.md, ops/saturday-rush.md.",
|
|
2734
|
+
),
|
|
2735
|
+
body: str(
|
|
2736
|
+
"Markdown body of the note. Concrete, specific, observation-based. Avoid generalities.",
|
|
2737
|
+
),
|
|
2738
|
+
append: {
|
|
2739
|
+
type: "boolean",
|
|
2740
|
+
description:
|
|
2741
|
+
"When true, prepend the body as a new dated paragraph to the existing note (preserves history). When false (default), replaces the existing body.",
|
|
2742
|
+
},
|
|
2743
|
+
type: str(
|
|
2744
|
+
"Optional note type for filtering: 'pattern' | 'fact' | 'preference' | 'warning' | 'playbook' | 'anti-pattern'.",
|
|
2745
|
+
),
|
|
2746
|
+
tags: {
|
|
2747
|
+
type: "array",
|
|
2748
|
+
description: "Optional tags for filtering.",
|
|
2749
|
+
items: { type: "string" },
|
|
2750
|
+
},
|
|
2751
|
+
},
|
|
2752
|
+
["workspace_id", "path", "body"],
|
|
2753
|
+
),
|
|
2754
|
+
handler: async (args) => {
|
|
2755
|
+
const ws = args.workspace_id;
|
|
2756
|
+
const op = args.append ? "append" : "write";
|
|
2757
|
+
const payload = {
|
|
2758
|
+
op,
|
|
2759
|
+
path: args.path,
|
|
2760
|
+
metadata: {
|
|
2761
|
+
source: "operator:claude-code",
|
|
2762
|
+
type: args.type ?? undefined,
|
|
2763
|
+
tags: args.tags ?? undefined,
|
|
2764
|
+
},
|
|
2765
|
+
};
|
|
2766
|
+
if (op === "append") payload.paragraph = args.body;
|
|
2767
|
+
else payload.body = args.body;
|
|
2768
|
+
const result = await api("POST", "/brain", {
|
|
2769
|
+
body: payload,
|
|
2770
|
+
workspace_id: ws,
|
|
2771
|
+
});
|
|
2772
|
+
return result;
|
|
2773
|
+
},
|
|
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
|
+
|
|
2905
|
+
{
|
|
2906
|
+
name: "list_brain_patterns",
|
|
2907
|
+
description:
|
|
2908
|
+
"List layer-2 cross-workspace patterns. These are anonymized insights the cron has promoted from workspaces that all observed the same thing (3+ workspaces, confidence >= 0.7). Use BEFORE generating blocks for a vertical-specific business — patterns/by-vertical/<vertical>.md gives you observations across every other workspace in that vertical. Compounding moat: each new workspace's interactions feed back into these patterns over time.",
|
|
2909
|
+
inputSchema: obj(
|
|
2910
|
+
{
|
|
2911
|
+
workspace_id: str(
|
|
2912
|
+
"Workspace id (used for auth; the patterns themselves are global).",
|
|
2913
|
+
),
|
|
2914
|
+
vertical: str(
|
|
2915
|
+
"Optional vertical filter: 'barbershop' | 'hvac' | 'legal' | 'restaurant' | etc. Returns patterns/by-vertical/<vertical>/* notes only.",
|
|
2916
|
+
),
|
|
2917
|
+
block_type: str(
|
|
2918
|
+
"Optional block-type filter: 'hero' | 'services' | 'faq' | etc. Returns patterns/by-block-type/<type>/* notes only.",
|
|
2919
|
+
),
|
|
2920
|
+
},
|
|
2921
|
+
["workspace_id"],
|
|
2922
|
+
),
|
|
2923
|
+
handler: async (args) => {
|
|
2924
|
+
const ws = args.workspace_id;
|
|
2925
|
+
const result = await api("POST", "/brain", {
|
|
2926
|
+
body: {
|
|
2927
|
+
op: "list_patterns",
|
|
2928
|
+
vertical: args.vertical ?? undefined,
|
|
2929
|
+
block_type: args.block_type ?? undefined,
|
|
2930
|
+
},
|
|
2931
|
+
workspace_id: ws,
|
|
2932
|
+
});
|
|
2933
|
+
return result;
|
|
2934
|
+
},
|
|
2935
|
+
},
|
|
2663
2936
|
];
|
|
2664
2937
|
|
|
2665
2938
|
export const TOOL_MAP = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
|
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.7.0";
|
|
12
12
|
|
|
13
13
|
export const WELCOME_MARKDOWN = `# SeldonFrame — create a real Business OS in one conversation
|
|
14
14
|
|
|
@@ -167,6 +167,12 @@ further natural-language requests ("change the headline to …",
|
|
|
167
167
|
- **\`create_workspace_v2\`** — PREFERRED workspace-creation tool (v1.4+).
|
|
168
168
|
MCP-native: bootstraps the workspace + returns the list of blocks YOU
|
|
169
169
|
generate using your own LLM. The first call for any new workspace.
|
|
170
|
+
v1.6+ also returns \`brain_patterns\` — anonymized cross-workspace
|
|
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.
|
|
170
176
|
- **\`list_blocks\`** — lists v2 page-block primitives available.
|
|
171
177
|
- **\`get_block_skill\`** — fetches one block's SKILL.md (the generation
|
|
172
178
|
prompt + prop schema you read before generating props).
|
|
@@ -174,6 +180,16 @@ further natural-language requests ("change the headline to …",
|
|
|
174
180
|
+ renders + replaces the matching section in the workspace's landing.
|
|
175
181
|
- **\`complete_workspace_v2\`** — marks the v2 flow finished, reports which
|
|
176
182
|
blocks landed.
|
|
183
|
+
- **\`read_brain_path\`** / **\`list_brain_dir\`** — read the workspace's
|
|
184
|
+
layer-1 brain (notes about THIS workspace's customers, voice, pipeline
|
|
185
|
+
patterns). Use BEFORE generating blocks; reads tick the note's \`uses\`
|
|
186
|
+
counter so the system knows what's actually being consumed.
|
|
187
|
+
- **\`write_brain_note\`** — capture insights the operator volunteers
|
|
188
|
+
("walk-ins on Saturday convert 3× better"). Notes live in the
|
|
189
|
+
workspace's brain forever, contribute to layer-2 cross-workspace
|
|
190
|
+
patterns when 3+ workspaces independently observe them.
|
|
191
|
+
- **\`list_brain_patterns\`** — read layer-2 cross-workspace patterns,
|
|
192
|
+
filtered by vertical or block_type.
|
|
177
193
|
- **\`create_full_workspace\`** — v1 atomic creation (legacy). Server-side,
|
|
178
194
|
deterministic. Use only when v2 is impossible.
|
|
179
195
|
- **\`finalize_workspace\`** — MANDATORY closing call. Mints the
|