@seldonframe/mcp 1.0.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/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # @seldonframe/mcp
2
+
3
+ Official SeldonFrame MCP server for Claude Code and Claude Desktop.
4
+
5
+ **One command, one real workspace.** The first time you run it, no API key is needed. Say `create_workspace({ name: "Dental Clinic Laval" })` and the server mints a real hosted workspace on `dental-clinic-laval.app.seldonframe.com` — with CRM, Cal.diy booking, Formbricks intake, and Brain v2 pre-installed — and returns live URLs. No signup wall, no "guest mode", no claim step.
6
+
7
+ ## 60-second quickstart
8
+
9
+ ```bash
10
+ # 1. From the repo root
11
+ cd skills/mcp-server && npm install
12
+
13
+ # 2. Register with Claude Code — no env vars needed
14
+ claude mcp add seldonframe -s user -- node "$(pwd)/src/index.js"
15
+
16
+ # 3. In Claude Code, just ask:
17
+ # create_workspace({ name: "My Business OS" })
18
+ ```
19
+
20
+ That call returns something like:
21
+
22
+ ```json
23
+ {
24
+ "workspace": { "id": "wsp_…", "slug": "my-business-os", "tier": "free" },
25
+ "urls": {
26
+ "dashboard": "https://my-business-os.app.seldonframe.com",
27
+ "book": "https://my-business-os.app.seldonframe.com/book",
28
+ "intake": "https://my-business-os.app.seldonframe.com/intake"
29
+ },
30
+ "installed": ["crm", "caldiy-booking", "formbricks-intake", "brain-v2"],
31
+ "next": [ "…" ]
32
+ }
33
+ ```
34
+
35
+ Every subsequent tool response includes a `next:` array — follow the rails.
36
+
37
+ ## How auth works
38
+
39
+ | Situation | What happens |
40
+ |---|---|
41
+ | First ever call | `create_workspace` POSTs with no auth. Server mints a workspace + bearer token. MCP stores the token in `~/.seldonframe/device.json`. |
42
+ | Subsequent calls | MCP sends `Authorization: Bearer <workspace token>` automatically. |
43
+ | `SELDONFRAME_API_KEY` set | Takes precedence over device tokens. Unlocks Pro capabilities. |
44
+
45
+ You never have to juggle modes. The first workspace is free forever.
46
+
47
+ ## When you need `SELDONFRAME_API_KEY`
48
+
49
+ - Adding a **second workspace**
50
+ - Connecting a **custom domain**
51
+ - **Full Brain v2** intelligence (heuristic Brain is always free)
52
+ - Publishing, exporting, org-scoped secret rotation
53
+
54
+ Get one at <https://app.seldonframe.com/settings/api>, then:
55
+
56
+ ```bash
57
+ export SELDONFRAME_API_KEY=sk-…
58
+ ```
59
+
60
+ Restart the MCP server. Your existing workspaces continue to work untouched.
61
+
62
+ ## Install via plugin manifest (one-shot)
63
+
64
+ The repo ships a `.claude-plugin/plugin.json` at the root. From inside Claude Code:
65
+
66
+ ```
67
+ /plugin install <path-to-repo>
68
+ ```
69
+
70
+ ## Install via npm
71
+
72
+ ```bash
73
+ claude mcp add seldonframe -- npx -y @seldonframe/mcp
74
+ ```
75
+
76
+ This is the canonical install command surfaced on the marketing site,
77
+ the `/docs/quickstart` page, and the repo README. It uses `npx -y`
78
+ so users never have to globally install anything; the latest version
79
+ is fetched and run on demand.
80
+
81
+ ## Environment
82
+
83
+ - `SELDONFRAME_API_KEY` *(optional)* — Enables Pro capabilities (second workspace, custom domains, full Brain v2).
84
+ - `SELDONFRAME_API_BASE` *(optional)* — Override the API base URL. Defaults to `https://app.seldonframe.com/api/v1`.
85
+
86
+ ## Tools
87
+
88
+ **Workspace:** `create_workspace`, `list_workspaces`, `switch_workspace`, `clone_workspace`, `link_workspace_owner`, `get_workspace_snapshot`.
89
+ **Blocks:** `install_caldiy_booking`, `install_formbricks_intake`, `install_vertical_pack`.
90
+ **Customize (typed, no backend LLM):** `update_landing_content`, `customize_intake_form`, `configure_booking`, `update_theme`.
91
+ **Soul:** `fetch_source_for_soul`, `submit_soul`.
92
+ **Ops:** `list_automations`, `connect_custom_domain`, `export_agent`, `store_secret`, `list_secrets`, `rotate_secret`.
93
+
94
+ ### Architecture: zero backend LLM cost
95
+
96
+ Natural-language reasoning happens in the MCP session (Claude Code on the user's side). The backend only accepts **structured** commands and applies them deterministically. There is no `seldon_it` endpoint that parses prompts server-side; the old `query_brain` is replaced by `get_workspace_snapshot`, which returns raw state for Claude to reason over. Seldon spends $0 on LLM for the free tier — the user's Claude Code subscription is the reasoning engine.
97
+
98
+ ### Soul compilation (zero cost to Seldon)
99
+
100
+ Soul compilation runs in **your** Claude Code session, not on Seldon's servers:
101
+
102
+ 1. `fetch_source_for_soul({ url })` — returns up to 256KB of normalized text.
103
+ 2. You (the agent) extract a structured Soul object.
104
+ 3. `submit_soul({ soul })` — persists it to the workspace.
105
+
106
+ ## Troubleshooting
107
+
108
+ **`Seldon API 401`** — Your `SELDONFRAME_API_KEY` is invalid or expired. Regenerate at <https://app.seldonframe.com/settings/api>, or unset the env var to fall back to your device token.
109
+
110
+ **`Seldon API 402`** — You tried a Pro capability without a key. Set `SELDONFRAME_API_KEY` and restart.
111
+
112
+ **Want to reset device state?** Delete `~/.seldonframe/device.json`. Your hosted workspaces stay live at `app.seldonframe.com`; only the local tokens are cleared.
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@seldonframe/mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for SeldonFrame — AI-native Business OS platform. One command creates a real hosted workspace with CRM, booking, intake, and Brain v2.",
5
+ "type": "module",
6
+ "bin": {
7
+ "seldonframe-mcp": "src/index.js"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "scripts": {
17
+ "start": "node src/index.js"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.0.4"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "model-context-protocol",
25
+ "seldonframe",
26
+ "ai",
27
+ "agents",
28
+ "business-os",
29
+ "claude-code",
30
+ "crm"
31
+ ],
32
+ "license": "MIT",
33
+ "homepage": "https://seldonframe.com",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/seldonframe/seldonframe",
37
+ "directory": "skills/mcp-server"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }
package/src/client.js ADDED
@@ -0,0 +1,194 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { VERSION } from "./welcome.js";
5
+
6
+ const API_BASE =
7
+ process.env.SELDONFRAME_API_BASE ?? "https://app.seldonframe.com/api/v1";
8
+ const API_KEY = process.env.SELDONFRAME_API_KEY;
9
+
10
+ const SELDON_DIR = join(homedir(), ".seldonframe");
11
+ const DEVICE_FILE = join(SELDON_DIR, "device.json");
12
+
13
+ function loadDevice() {
14
+ if (!existsSync(DEVICE_FILE)) {
15
+ return { tokens: {}, default_workspace: null, created_at: null };
16
+ }
17
+ try {
18
+ const parsed = JSON.parse(readFileSync(DEVICE_FILE, "utf8"));
19
+ return {
20
+ tokens: parsed.tokens ?? {},
21
+ default_workspace: parsed.default_workspace ?? null,
22
+ created_at: parsed.created_at ?? null,
23
+ };
24
+ } catch {
25
+ return { tokens: {}, default_workspace: null, created_at: null };
26
+ }
27
+ }
28
+
29
+ function saveDevice(device) {
30
+ mkdirSync(SELDON_DIR, { recursive: true });
31
+ writeFileSync(DEVICE_FILE, JSON.stringify(device, null, 2));
32
+ }
33
+
34
+ export function rememberWorkspace({ workspace_id, bearer_token, make_default = true }) {
35
+ const d = loadDevice();
36
+ if (bearer_token) d.tokens[workspace_id] = bearer_token;
37
+ if (make_default || !d.default_workspace) d.default_workspace = workspace_id;
38
+ if (!d.created_at) d.created_at = new Date().toISOString();
39
+ saveDevice(d);
40
+ }
41
+
42
+ export function setDefaultWorkspace(workspace_id) {
43
+ const d = loadDevice();
44
+ d.default_workspace = workspace_id;
45
+ saveDevice(d);
46
+ }
47
+
48
+ // Remove a workspace's bearer token from the local device store. If it was the
49
+ // default, clear the default pointer too. Used by revoke_bearer when the call
50
+ // revokes the caller's own token — the device can't keep pretending it has
51
+ // access.
52
+ export function forgetWorkspace(workspace_id) {
53
+ const d = loadDevice();
54
+ delete d.tokens[workspace_id];
55
+ if (d.default_workspace === workspace_id) {
56
+ const remaining = Object.keys(d.tokens);
57
+ d.default_workspace = remaining[0] ?? null;
58
+ }
59
+ saveDevice(d);
60
+ }
61
+
62
+ export function getDefaultWorkspace() {
63
+ return loadDevice().default_workspace;
64
+ }
65
+
66
+ export function knownWorkspaceIds() {
67
+ return Object.keys(loadDevice().tokens);
68
+ }
69
+
70
+ export function hasApiKey() {
71
+ return !!API_KEY;
72
+ }
73
+
74
+ export function isFirstEverCall() {
75
+ const d = loadDevice();
76
+ return Object.keys(d.tokens).length === 0 && !API_KEY;
77
+ }
78
+
79
+ function resolveAuth(workspace_id) {
80
+ if (API_KEY) return `Bearer ${API_KEY}`;
81
+ const d = loadDevice();
82
+ const target = workspace_id ?? d.default_workspace;
83
+ if (target && d.tokens[target]) return `Bearer ${d.tokens[target]}`;
84
+ return null;
85
+ }
86
+
87
+ export function getWorkspaceBearer(workspace_id) {
88
+ const d = loadDevice();
89
+ const target = workspace_id ?? d.default_workspace;
90
+ if (!target) return null;
91
+ return d.tokens[target] ?? null;
92
+ }
93
+
94
+ export function getApiKey() {
95
+ return API_KEY ?? null;
96
+ }
97
+
98
+ function hintFor(status, data) {
99
+ if (status === 401) {
100
+ return " — credentials rejected. If you meant to use a paid capability, check SELDONFRAME_API_KEY at https://app.seldonframe.com/settings/api.";
101
+ }
102
+ if (status === 402) {
103
+ const need = data?.error?.requires ?? "a SeldonFrame API key";
104
+ return ` — this capability requires ${need}. Get one at https://app.seldonframe.com/settings/api and \`export SELDONFRAME_API_KEY=sk-…\`.`;
105
+ }
106
+ if (status === 409) {
107
+ return " — the requested resource already exists or conflicts with existing state.";
108
+ }
109
+ if (status === 429) return " — rate limited. Try again in a moment.";
110
+ return "";
111
+ }
112
+
113
+ export async function api(method, path, opts = {}) {
114
+ const {
115
+ body,
116
+ workspace_id,
117
+ allow_anonymous = false,
118
+ force_workspace_bearer = false,
119
+ extra_headers = {},
120
+ } = opts;
121
+
122
+ let auth = null;
123
+ if (force_workspace_bearer) {
124
+ const token = getWorkspaceBearer(workspace_id);
125
+ if (token) auth = `Bearer ${token}`;
126
+ } else {
127
+ auth = resolveAuth(workspace_id);
128
+ }
129
+
130
+ if (!auth && !allow_anonymous) {
131
+ throw new Error(
132
+ "No workspace available. Run create_workspace({ name: '…' }) first, or set SELDONFRAME_API_KEY.",
133
+ );
134
+ }
135
+ const headers = {
136
+ "Content-Type": "application/json",
137
+ "User-Agent": `seldonframe-mcp/${VERSION}`,
138
+ ...extra_headers,
139
+ };
140
+ if (auth) headers.Authorization = auth;
141
+
142
+ const res = await fetch(`${API_BASE}${path}`, {
143
+ method,
144
+ headers,
145
+ body: body === undefined ? undefined : JSON.stringify(body),
146
+ });
147
+ const text = await res.text();
148
+ let data;
149
+ try {
150
+ data = text ? JSON.parse(text) : {};
151
+ } catch {
152
+ data = { raw: text };
153
+ }
154
+ if (!res.ok) {
155
+ const msg = data?.error?.message ?? data?.message ?? text ?? res.statusText;
156
+ throw new Error(`Seldon API ${res.status}: ${msg}${hintFor(res.status, data)}`);
157
+ }
158
+ return data;
159
+ }
160
+
161
+ export async function fetchText(url, { maxBytes = 256 * 1024 } = {}) {
162
+ const res = await fetch(url, {
163
+ headers: { "User-Agent": `seldonframe-mcp/${VERSION} (+soul-compiler)` },
164
+ redirect: "follow",
165
+ });
166
+ if (!res.ok) {
167
+ throw new Error(`Fetch ${url} failed: ${res.status} ${res.statusText}`);
168
+ }
169
+ const raw = await res.text();
170
+ const truncated = raw.length > maxBytes;
171
+ const html = truncated ? raw.slice(0, maxBytes) : raw;
172
+ return { html, truncated, status: res.status, final_url: res.url };
173
+ }
174
+
175
+ export function htmlToText(html) {
176
+ return html
177
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
178
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
179
+ .replace(/<noscript[\s\S]*?<\/noscript>/gi, " ")
180
+ .replace(/<!--[\s\S]*?-->/g, " ")
181
+ .replace(/<(h[1-6]|p|li|br|div|section|article|header|footer)[^>]*>/gi, "\n")
182
+ .replace(/<[^>]+>/g, " ")
183
+ .replace(/&nbsp;/g, " ")
184
+ .replace(/&amp;/g, "&")
185
+ .replace(/&lt;/g, "<")
186
+ .replace(/&gt;/g, ">")
187
+ .replace(/&quot;/g, '"')
188
+ .replace(/&#39;/g, "'")
189
+ .replace(/[ \t]+/g, " ")
190
+ .replace(/\n{3,}/g, "\n\n")
191
+ .trim();
192
+ }
193
+
194
+ export const API_INFO = { base: API_BASE };
package/src/index.js ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+ import { WELCOME_MARKDOWN, VERSION } from "./welcome.js";
9
+ import { TOOLS, TOOL_MAP } from "./tools.js";
10
+
11
+ const server = new Server(
12
+ { name: "seldonframe", version: VERSION },
13
+ {
14
+ capabilities: { tools: {} },
15
+ instructions: WELCOME_MARKDOWN,
16
+ },
17
+ );
18
+
19
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
20
+ tools: TOOLS.map(({ name, description, inputSchema }) => ({
21
+ name,
22
+ description,
23
+ inputSchema,
24
+ })),
25
+ }));
26
+
27
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
28
+ const tool = TOOL_MAP[req.params.name];
29
+ if (!tool) {
30
+ return {
31
+ isError: true,
32
+ content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }],
33
+ };
34
+ }
35
+ try {
36
+ const result = await tool.handler(req.params.arguments ?? {});
37
+ return {
38
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
39
+ };
40
+ } catch (err) {
41
+ return {
42
+ isError: true,
43
+ content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
44
+ };
45
+ }
46
+ });
47
+
48
+ const transport = new StdioServerTransport();
49
+ await server.connect(transport);