@seldonframe/mcp 1.0.0 → 1.0.2
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 +112 -112
- package/package.json +42 -42
- package/src/client.js +194 -194
- package/src/index.js +81 -49
- package/src/tools.js +2001 -2001
- package/src/welcome.js +132 -80
package/src/tools.js
CHANGED
|
@@ -1,2001 +1,2001 @@
|
|
|
1
|
-
import {
|
|
2
|
-
api,
|
|
3
|
-
fetchText,
|
|
4
|
-
forgetWorkspace,
|
|
5
|
-
htmlToText,
|
|
6
|
-
rememberWorkspace,
|
|
7
|
-
setDefaultWorkspace,
|
|
8
|
-
getDefaultWorkspace,
|
|
9
|
-
getWorkspaceBearer,
|
|
10
|
-
getApiKey,
|
|
11
|
-
knownWorkspaceIds,
|
|
12
|
-
hasApiKey,
|
|
13
|
-
isFirstEverCall,
|
|
14
|
-
} from "./client.js";
|
|
15
|
-
import { FIRST_CALL_BANNER } from "./welcome.js";
|
|
16
|
-
|
|
17
|
-
const str = (description, extra = {}) => ({ type: "string", description, ...extra });
|
|
18
|
-
const obj = (properties, required = []) => ({
|
|
19
|
-
type: "object",
|
|
20
|
-
properties,
|
|
21
|
-
required,
|
|
22
|
-
additionalProperties: false,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
function withFirstCallBanner(payload) {
|
|
26
|
-
if (!isFirstEverCall()) return payload;
|
|
27
|
-
return { ...payload, _welcome: FIRST_CALL_BANNER };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function wsOrDefault(workspace_id) {
|
|
31
|
-
const id = workspace_id ?? getDefaultWorkspace();
|
|
32
|
-
if (!id) {
|
|
33
|
-
throw new Error(
|
|
34
|
-
"No workspace selected. Run create_workspace({ name: '…' }) first, or pass workspace_id.",
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return id;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const TOOLS = [
|
|
41
|
-
{
|
|
42
|
-
name: "create_workspace",
|
|
43
|
-
description:
|
|
44
|
-
"Create a real, hosted workspace on <slug>.app.seldonframe.com with CRM, Cal.diy booking, Formbricks intake, and Brain v2 pre-installed. The first workspace requires no API key. Example: create_workspace({ name: 'Dental Clinic Laval', source: 'https://mysite.com' })",
|
|
45
|
-
inputSchema: obj(
|
|
46
|
-
{
|
|
47
|
-
name: str("Human-readable workspace name."),
|
|
48
|
-
source: str("Optional URL or description to seed the workspace's Soul from."),
|
|
49
|
-
},
|
|
50
|
-
["name"],
|
|
51
|
-
),
|
|
52
|
-
handler: async (args) => {
|
|
53
|
-
const firstEver = isFirstEverCall();
|
|
54
|
-
const result = await api("POST", "/workspace/create", {
|
|
55
|
-
body: { name: args.name, source: args.source ?? null },
|
|
56
|
-
allow_anonymous: true,
|
|
57
|
-
});
|
|
58
|
-
const ws = result.workspace ?? result;
|
|
59
|
-
const id = ws.id;
|
|
60
|
-
if (!id) throw new Error("Server did not return a workspace id.");
|
|
61
|
-
if (result.bearer_token) {
|
|
62
|
-
rememberWorkspace({ workspace_id: id, bearer_token: result.bearer_token });
|
|
63
|
-
} else {
|
|
64
|
-
setDefaultWorkspace(id);
|
|
65
|
-
}
|
|
66
|
-
const payload = {
|
|
67
|
-
ok: true,
|
|
68
|
-
workspace: {
|
|
69
|
-
id,
|
|
70
|
-
name: ws.name,
|
|
71
|
-
slug: ws.slug,
|
|
72
|
-
tier: ws.tier ?? "free",
|
|
73
|
-
created_at: ws.created_at,
|
|
74
|
-
},
|
|
75
|
-
urls: result.urls ?? ws.urls ?? null,
|
|
76
|
-
installed: result.installed ?? ["crm", "caldiy-booking", "formbricks-intake", "brain-v2"],
|
|
77
|
-
next: [
|
|
78
|
-
"install_vertical_pack({ pack: 'real-estate' }) // or 'dental', 'legal'",
|
|
79
|
-
"fetch_source_for_soul({ url: 'https://yoursite.com' }) → submit_soul({ soul })",
|
|
80
|
-
"get_workspace_snapshot({}) — read workspace state to reason about next steps",
|
|
81
|
-
],
|
|
82
|
-
};
|
|
83
|
-
return firstEver ? withFirstCallBanner(payload) : payload;
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "list_workspaces",
|
|
88
|
-
description: "List all workspaces known to this device (plus any Pro workspaces if SELDONFRAME_API_KEY is set).",
|
|
89
|
-
inputSchema: obj({}),
|
|
90
|
-
handler: async () => {
|
|
91
|
-
const local = knownWorkspaceIds();
|
|
92
|
-
const data = await api("GET", "/workspaces", { allow_anonymous: true });
|
|
93
|
-
return {
|
|
94
|
-
ok: true,
|
|
95
|
-
default_workspace: getDefaultWorkspace(),
|
|
96
|
-
device_known: local,
|
|
97
|
-
workspaces: data.workspaces ?? data,
|
|
98
|
-
};
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: "switch_workspace",
|
|
103
|
-
description: "Set the active workspace. Subsequent tool calls act on it by default.",
|
|
104
|
-
inputSchema: obj({ workspace_id: str("Target workspace id.") }, ["workspace_id"]),
|
|
105
|
-
handler: async ({ workspace_id }) => {
|
|
106
|
-
setDefaultWorkspace(workspace_id);
|
|
107
|
-
return { ok: true, default_workspace: workspace_id };
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
name: "clone_workspace",
|
|
112
|
-
description:
|
|
113
|
-
"Clone an existing workspace as a template. Example: clone_workspace({ source_workspace_id: 'wsp_x', name: 'Copy' })",
|
|
114
|
-
inputSchema: obj(
|
|
115
|
-
{
|
|
116
|
-
source_workspace_id: str("Workspace to clone from."),
|
|
117
|
-
name: str("Name for the new workspace."),
|
|
118
|
-
},
|
|
119
|
-
["source_workspace_id", "name"],
|
|
120
|
-
),
|
|
121
|
-
handler: async (a) => {
|
|
122
|
-
const result = await api(
|
|
123
|
-
"POST",
|
|
124
|
-
`/workspaces/${encodeURIComponent(a.source_workspace_id)}/clone`,
|
|
125
|
-
{ body: { name: a.name }, workspace_id: a.source_workspace_id },
|
|
126
|
-
);
|
|
127
|
-
const id = result.workspace?.id ?? result.id;
|
|
128
|
-
if (id && result.bearer_token) {
|
|
129
|
-
rememberWorkspace({ workspace_id: id, bearer_token: result.bearer_token });
|
|
130
|
-
}
|
|
131
|
-
return { ok: true, ...result };
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
name: "link_workspace_owner",
|
|
136
|
-
description:
|
|
137
|
-
"Claim an anonymously-created workspace under your real account. After linking, the admin URLs (dashboard, contacts, deals) become usable once you sign in at app.seldonframe.com. Requires SELDONFRAME_API_KEY to be set in the MCP environment. The workspace bearer token continues to work — no rotation needed. Example: link_workspace_owner({}) to claim the active workspace.",
|
|
138
|
-
inputSchema: obj({
|
|
139
|
-
workspace_id: str(
|
|
140
|
-
"Optional workspace id to claim. Defaults to the active workspace from this device."
|
|
141
|
-
),
|
|
142
|
-
}),
|
|
143
|
-
handler: async (a) => {
|
|
144
|
-
const workspaceId = a.workspace_id ?? getDefaultWorkspace();
|
|
145
|
-
if (!workspaceId) {
|
|
146
|
-
throw new Error(
|
|
147
|
-
"No workspace to link. Run create_workspace first, or pass workspace_id."
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
const bearer = getWorkspaceBearer(workspaceId);
|
|
151
|
-
if (!bearer) {
|
|
152
|
-
throw new Error(
|
|
153
|
-
`No local bearer token for workspace ${workspaceId}. This device did not create it. Re-run create_workspace or switch to the device that did.`
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
const apiKey = getApiKey();
|
|
157
|
-
if (!apiKey) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
"Linking an owner requires SELDONFRAME_API_KEY. Get one at https://app.seldonframe.com/settings/api, then `export SELDONFRAME_API_KEY=sk-…` and restart the MCP server."
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
const result = await api(
|
|
163
|
-
"POST",
|
|
164
|
-
`/workspace/${encodeURIComponent(workspaceId)}/link-owner`,
|
|
165
|
-
{
|
|
166
|
-
body: {},
|
|
167
|
-
workspace_id: workspaceId,
|
|
168
|
-
force_workspace_bearer: true,
|
|
169
|
-
extra_headers: { "x-seldon-api-key": apiKey },
|
|
170
|
-
},
|
|
171
|
-
);
|
|
172
|
-
const magicLink = result?.urls?.claim_magic_link ?? null;
|
|
173
|
-
const baseNote = result.already_linked
|
|
174
|
-
? "This workspace was already linked to your account."
|
|
175
|
-
: "Workspace linked to your account.";
|
|
176
|
-
const magicNote = magicLink
|
|
177
|
-
? ` A one-click sign-in link is in urls.claim_magic_link — opens a browser session as the workspace owner, expires in 15 min, single-use.`
|
|
178
|
-
: " No magic link minted (user has no email on file); sign in the normal way at urls.admin_dashboard.";
|
|
179
|
-
return {
|
|
180
|
-
ok: true,
|
|
181
|
-
...result,
|
|
182
|
-
note: `${baseNote}${magicNote} Your MCP bearer token continues to work — no rotation needed.`,
|
|
183
|
-
};
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
name: "revoke_bearer",
|
|
188
|
-
description:
|
|
189
|
-
"Revoke workspace bearer tokens. Useful if a device token has leaked or if a builder wants to rotate. Modes (pick exactly one): `{}` revokes ALL tokens except the current device's (safe default — other devices kicked off, this device keeps working); `{ token_id }` revokes a specific token by its UUID; `{ all: true }` revokes every token including the current one — requires SELDONFRAME_API_KEY because it locks this device out. After revoking the current token the MCP clears the local entry from ~/.seldonframe/device.json.",
|
|
190
|
-
inputSchema: obj({
|
|
191
|
-
workspace_id: str("Optional workspace override. Defaults to active workspace."),
|
|
192
|
-
token_id: str("UUID of a specific token to revoke (from api_keys.id)."),
|
|
193
|
-
all: { type: "boolean", description: "Revoke ALL tokens including caller. Requires SELDONFRAME_API_KEY." },
|
|
194
|
-
}),
|
|
195
|
-
handler: async (a) => {
|
|
196
|
-
const workspaceId = a.workspace_id ?? getDefaultWorkspace();
|
|
197
|
-
if (!workspaceId) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
"No workspace to revoke tokens for. Run create_workspace first, or pass workspace_id."
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
const bearer = getWorkspaceBearer(workspaceId);
|
|
203
|
-
const apiKey = getApiKey();
|
|
204
|
-
if (!bearer && !apiKey) {
|
|
205
|
-
throw new Error(
|
|
206
|
-
`No local bearer for workspace ${workspaceId} and no SELDONFRAME_API_KEY. Cannot authenticate.`
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
if (a.all === true && !apiKey) {
|
|
210
|
-
throw new Error(
|
|
211
|
-
"Revoking ALL tokens (including this device's) requires SELDONFRAME_API_KEY — bearer identity can't lock itself out. Either omit `all` to use all_except_current, or set SELDONFRAME_API_KEY."
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
let body;
|
|
216
|
-
if (a.token_id) {
|
|
217
|
-
body = { token_id: a.token_id };
|
|
218
|
-
} else if (a.all === true) {
|
|
219
|
-
body = { all: true };
|
|
220
|
-
} else {
|
|
221
|
-
body = { all_except_current: true };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Prefer workspace bearer when present; fall back to api_key auth otherwise.
|
|
225
|
-
const useBearer = Boolean(bearer);
|
|
226
|
-
const result = await api(
|
|
227
|
-
"POST",
|
|
228
|
-
`/workspace/${encodeURIComponent(workspaceId)}/revoke-bearer`,
|
|
229
|
-
{
|
|
230
|
-
body,
|
|
231
|
-
workspace_id: workspaceId,
|
|
232
|
-
force_workspace_bearer: useBearer,
|
|
233
|
-
extra_headers: apiKey && !useBearer ? { "x-seldon-api-key": apiKey } : {},
|
|
234
|
-
},
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
// If the caller's own token got revoked, clear it from device.json so
|
|
238
|
-
// future tool calls don't authenticate with a dead token.
|
|
239
|
-
if (useBearer && result?.caller_still_valid === false) {
|
|
240
|
-
forgetWorkspace(workspaceId);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
ok: true,
|
|
245
|
-
...result,
|
|
246
|
-
device_cleared: useBearer && result?.caller_still_valid === false,
|
|
247
|
-
};
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
name: "update_landing_content",
|
|
252
|
-
description:
|
|
253
|
-
"Rewrite the workspace's public landing page at / — headline, subhead, and primary CTA label. YOU decide the copy based on the user's request + the workspace Soul; this tool persists it.",
|
|
254
|
-
inputSchema: obj(
|
|
255
|
-
{
|
|
256
|
-
headline: str("Main hero heading. Keep short; 1 line."),
|
|
257
|
-
subhead: str("One-sentence supporting line under the headline."),
|
|
258
|
-
cta_label: str("Primary call-to-action button text, e.g. 'Book a call'."),
|
|
259
|
-
workspace_id: str("Optional workspace override."),
|
|
260
|
-
},
|
|
261
|
-
["headline", "subhead", "cta_label"],
|
|
262
|
-
),
|
|
263
|
-
handler: async (a) => {
|
|
264
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
265
|
-
return api("POST", "/landing/update", {
|
|
266
|
-
body: {
|
|
267
|
-
headline: a.headline,
|
|
268
|
-
subhead: a.subhead,
|
|
269
|
-
cta_label: a.cta_label,
|
|
270
|
-
workspace_id: ws,
|
|
271
|
-
},
|
|
272
|
-
workspace_id: ws,
|
|
273
|
-
});
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
// Note: `customize_intake_form` used to live here pointing at POST
|
|
277
|
-
// /api/v1/intake/customize. Phase 2.d unified it under update_form; the
|
|
278
|
-
// alias that preserves the old name now lives in the Phase 2.d block at
|
|
279
|
-
// the bottom of this file. The POST /intake/customize endpoint still
|
|
280
|
-
// exists server-side for backwards compatibility until Phase 11 cleanup.
|
|
281
|
-
// Note: `configure_booking` used to live here pointing at POST
|
|
282
|
-
// /api/v1/booking/configure. Phase 2.c unified it under
|
|
283
|
-
// update_appointment_type; the alias that preserves the old name now
|
|
284
|
-
// lives at the bottom of this file (end of Phase 2.c block). The POST
|
|
285
|
-
// /booking/configure endpoint still exists server-side for backwards
|
|
286
|
-
// compatibility until Phase 11 cleanup.
|
|
287
|
-
{
|
|
288
|
-
name: "update_theme",
|
|
289
|
-
description:
|
|
290
|
-
"Change workspace theme: mode (dark|light), primary_color (#hex), accent_color (#hex), font_family. Any subset. Available fonts: Inter, DM Sans, Playfair Display, Space Grotesk, Lora, Outfit.",
|
|
291
|
-
inputSchema: obj(
|
|
292
|
-
{
|
|
293
|
-
mode: { type: "string", enum: ["dark", "light"] },
|
|
294
|
-
primary_color: str("Hex color like '#14b8a6'."),
|
|
295
|
-
accent_color: str("Hex color."),
|
|
296
|
-
font_family: {
|
|
297
|
-
type: "string",
|
|
298
|
-
enum: ["Inter", "DM Sans", "Playfair Display", "Space Grotesk", "Lora", "Outfit"],
|
|
299
|
-
},
|
|
300
|
-
workspace_id: str("Optional workspace override."),
|
|
301
|
-
},
|
|
302
|
-
),
|
|
303
|
-
handler: async (a) => {
|
|
304
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
305
|
-
return api("POST", "/theme/update", {
|
|
306
|
-
body: {
|
|
307
|
-
mode: a.mode,
|
|
308
|
-
primary_color: a.primary_color,
|
|
309
|
-
accent_color: a.accent_color,
|
|
310
|
-
font_family: a.font_family,
|
|
311
|
-
workspace_id: ws,
|
|
312
|
-
},
|
|
313
|
-
workspace_id: ws,
|
|
314
|
-
});
|
|
315
|
-
},
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
name: "list_automations",
|
|
319
|
-
description: "List automations configured in the active (or specified) workspace.",
|
|
320
|
-
inputSchema: obj({ workspace_id: str("Optional workspace override.") }),
|
|
321
|
-
handler: async (a) => {
|
|
322
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
323
|
-
return api("GET", `/automations?workspace_id=${encodeURIComponent(ws)}`, { workspace_id: ws });
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
name: "install_vertical_pack",
|
|
328
|
-
description:
|
|
329
|
-
"Install a vertical pack (e.g. 'real-estate', 'dental', 'legal'). Adds domain-specific objects, fields, views.",
|
|
330
|
-
inputSchema: obj(
|
|
331
|
-
{
|
|
332
|
-
pack: str("Pack slug, e.g. 'real-estate', 'dental', 'legal'."),
|
|
333
|
-
workspace_id: str("Optional workspace override."),
|
|
334
|
-
},
|
|
335
|
-
["pack"],
|
|
336
|
-
),
|
|
337
|
-
handler: async (a) => {
|
|
338
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
339
|
-
return api("POST", "/packs/install", {
|
|
340
|
-
body: { pack: a.pack, workspace_id: ws },
|
|
341
|
-
workspace_id: ws,
|
|
342
|
-
});
|
|
343
|
-
},
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
name: "install_caldiy_booking",
|
|
347
|
-
description:
|
|
348
|
-
"Install the Cal.diy booking block (event types, availability, bookings). Example: install_caldiy_booking({})",
|
|
349
|
-
inputSchema: obj({
|
|
350
|
-
workspace_id: str("Optional workspace override."),
|
|
351
|
-
config: { type: "object", description: "Optional Cal.diy configuration overrides." },
|
|
352
|
-
}),
|
|
353
|
-
handler: async (a) => {
|
|
354
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
355
|
-
return api("POST", "/packs/caldiy-booking/install", {
|
|
356
|
-
body: { workspace_id: ws, config: a.config },
|
|
357
|
-
workspace_id: ws,
|
|
358
|
-
});
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
name: "install_formbricks_intake",
|
|
363
|
-
description:
|
|
364
|
-
"Install a Formbricks intake form (surveys, conditional logic, contact sync). Example: install_formbricks_intake({})",
|
|
365
|
-
inputSchema: obj({
|
|
366
|
-
workspace_id: str("Optional workspace override."),
|
|
367
|
-
form_id: str("Optional existing Formbricks form id to bind."),
|
|
368
|
-
}),
|
|
369
|
-
handler: async (a) => {
|
|
370
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
371
|
-
return api("POST", "/packs/formbricks-intake/install", {
|
|
372
|
-
body: { workspace_id: ws, form_id: a.form_id },
|
|
373
|
-
workspace_id: ws,
|
|
374
|
-
});
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
name: "get_workspace_snapshot",
|
|
379
|
-
description:
|
|
380
|
-
"Return a structured read-only snapshot of workspace state: workspace metadata, Soul (if submitted), theme, enabled blocks with configs, entity counts (contacts/bookings/intake forms/submissions), recent Seldon It events, and public URLs. YOU reason over this snapshot to decide what to do next, then call the appropriate typed tools (update_landing_content, configure_booking, customize_intake_form, update_theme, install_*). Zero server-side LLM cost.",
|
|
381
|
-
inputSchema: obj({
|
|
382
|
-
workspace_id: str("Optional workspace override."),
|
|
383
|
-
}),
|
|
384
|
-
handler: async (a) => {
|
|
385
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
386
|
-
return api("GET", `/workspace/${encodeURIComponent(ws)}/snapshot`, {
|
|
387
|
-
workspace_id: ws,
|
|
388
|
-
});
|
|
389
|
-
},
|
|
390
|
-
},
|
|
391
|
-
{
|
|
392
|
-
name: "fetch_source_for_soul",
|
|
393
|
-
description:
|
|
394
|
-
"Fetch a URL and return normalized text (headings + body, up to 256KB). Use this to gather raw content; then you (the caller) extract a structured Soul and submit it with submit_soul. Zero LLM cost to Seldon — extraction runs in this session.",
|
|
395
|
-
inputSchema: obj(
|
|
396
|
-
{
|
|
397
|
-
url: str("Absolute URL to fetch."),
|
|
398
|
-
},
|
|
399
|
-
["url"],
|
|
400
|
-
),
|
|
401
|
-
handler: async ({ url }) => {
|
|
402
|
-
const { html, truncated, status, final_url } = await fetchText(url);
|
|
403
|
-
const text = htmlToText(html);
|
|
404
|
-
return {
|
|
405
|
-
ok: true,
|
|
406
|
-
url,
|
|
407
|
-
final_url,
|
|
408
|
-
status,
|
|
409
|
-
bytes: text.length,
|
|
410
|
-
truncated,
|
|
411
|
-
text,
|
|
412
|
-
next: [
|
|
413
|
-
"Extract a Soul object: { mission, audience, tone, offerings[], differentiators[], faqs[] }",
|
|
414
|
-
"submit_soul({ soul: <extracted> })",
|
|
415
|
-
],
|
|
416
|
-
};
|
|
417
|
-
},
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
name: "submit_soul",
|
|
421
|
-
description:
|
|
422
|
-
"Submit a compiled Soul object to the active workspace. The caller is expected to have produced the structured Soul from fetch_source_for_soul output or user conversation.",
|
|
423
|
-
inputSchema: obj(
|
|
424
|
-
{
|
|
425
|
-
soul: {
|
|
426
|
-
type: "object",
|
|
427
|
-
description:
|
|
428
|
-
"Structured Soul. Expected keys: mission, audience, tone, offerings, differentiators, faqs. Additional keys allowed.",
|
|
429
|
-
},
|
|
430
|
-
workspace_id: str("Optional workspace override."),
|
|
431
|
-
},
|
|
432
|
-
["soul"],
|
|
433
|
-
),
|
|
434
|
-
handler: async (a) => {
|
|
435
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
436
|
-
return api("POST", "/soul/submit", {
|
|
437
|
-
body: { workspace_id: ws, soul: a.soul },
|
|
438
|
-
workspace_id: ws,
|
|
439
|
-
});
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
name: "connect_custom_domain",
|
|
444
|
-
description:
|
|
445
|
-
"Connect + verify a custom domain. Pro capability — requires SELDONFRAME_API_KEY. Example: connect_custom_domain({ domain: 'app.mysite.com' })",
|
|
446
|
-
inputSchema: obj(
|
|
447
|
-
{
|
|
448
|
-
domain: str("Fully qualified domain, e.g. client.example.com."),
|
|
449
|
-
workspace_id: str("Optional workspace override."),
|
|
450
|
-
},
|
|
451
|
-
["domain"],
|
|
452
|
-
),
|
|
453
|
-
handler: async (a) => {
|
|
454
|
-
if (!hasApiKey()) {
|
|
455
|
-
throw new Error(
|
|
456
|
-
"Custom domains are a Pro capability. Get a key at https://app.seldonframe.com/settings/api and `export SELDONFRAME_API_KEY=sk-…`.",
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
460
|
-
return api("POST", "/domains/connect", {
|
|
461
|
-
body: { domain: a.domain, workspace_id: ws },
|
|
462
|
-
workspace_id: ws,
|
|
463
|
-
});
|
|
464
|
-
},
|
|
465
|
-
},
|
|
466
|
-
{
|
|
467
|
-
name: "export_agent",
|
|
468
|
-
description: "Export the current workspace as a portable .agent/ bundle.",
|
|
469
|
-
inputSchema: obj({ workspace_id: str("Optional workspace override.") }),
|
|
470
|
-
handler: async (a) => {
|
|
471
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
472
|
-
return api("POST", "/export/agent", { body: { workspace_id: ws }, workspace_id: ws });
|
|
473
|
-
},
|
|
474
|
-
},
|
|
475
|
-
{
|
|
476
|
-
name: "store_secret",
|
|
477
|
-
description:
|
|
478
|
-
"Store a workspace-scoped secret (encrypted at rest). Example: store_secret({ key: 'STRIPE_API_KEY', value: 'sk_…' })",
|
|
479
|
-
inputSchema: obj(
|
|
480
|
-
{
|
|
481
|
-
key: str("Secret name, e.g. 'STRIPE_API_KEY'."),
|
|
482
|
-
value: str("Secret plaintext value."),
|
|
483
|
-
workspace_id: str("Optional workspace override."),
|
|
484
|
-
},
|
|
485
|
-
["key", "value"],
|
|
486
|
-
),
|
|
487
|
-
handler: async (a) => {
|
|
488
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
489
|
-
return api("POST", "/secrets", {
|
|
490
|
-
body: { key: a.key, value: a.value, workspace_id: ws },
|
|
491
|
-
workspace_id: ws,
|
|
492
|
-
});
|
|
493
|
-
},
|
|
494
|
-
},
|
|
495
|
-
{
|
|
496
|
-
name: "list_secrets",
|
|
497
|
-
description: "List secret metadata (names, timestamps) without exposing plaintext.",
|
|
498
|
-
inputSchema: obj({ workspace_id: str("Optional workspace override.") }),
|
|
499
|
-
handler: async (a) => {
|
|
500
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
501
|
-
return api("GET", `/secrets?workspace_id=${encodeURIComponent(ws)}`, { workspace_id: ws });
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
name: "rotate_secret",
|
|
506
|
-
description: "Rotate or delete a workspace secret. Omit new_value to delete.",
|
|
507
|
-
inputSchema: obj(
|
|
508
|
-
{
|
|
509
|
-
key: str("Secret name to rotate."),
|
|
510
|
-
new_value: str("New plaintext value. Omit to delete the secret."),
|
|
511
|
-
workspace_id: str("Optional workspace override."),
|
|
512
|
-
},
|
|
513
|
-
["key"],
|
|
514
|
-
),
|
|
515
|
-
handler: async (a) => {
|
|
516
|
-
const ws = wsOrDefault(a.workspace_id);
|
|
517
|
-
if (a.new_value === undefined) {
|
|
518
|
-
return api("DELETE", `/secrets/${encodeURIComponent(a.key)}`, {
|
|
519
|
-
body: { workspace_id: ws },
|
|
520
|
-
workspace_id: ws,
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
return api("PUT", `/secrets/${encodeURIComponent(a.key)}`, {
|
|
524
|
-
body: { value: a.new_value, workspace_id: ws },
|
|
525
|
-
workspace_id: ws,
|
|
526
|
-
});
|
|
527
|
-
},
|
|
528
|
-
},
|
|
529
|
-
// ════════════════════════════════════════════════════════════════════
|
|
530
|
-
// CRM tools — Phase 2.b per tasks/mcp-gap-audit.md
|
|
531
|
-
// Thin wrappers over v1 endpoints at /api/v1/{contacts,deals,activities}.
|
|
532
|
-
// Naming convention locked in the audit: list_/get_/create_/update_/
|
|
533
|
-
// delete_ for CRUD; verb_noun for state changes (move_deal_stage).
|
|
534
|
-
// ════════════════════════════════════════════════════════════════════
|
|
535
|
-
|
|
536
|
-
{
|
|
537
|
-
name: "list_contacts",
|
|
538
|
-
description:
|
|
539
|
-
"List contacts in the active workspace. Returns every contact the caller can read. Example: list_contacts({}).",
|
|
540
|
-
inputSchema: obj({
|
|
541
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
542
|
-
}),
|
|
543
|
-
handler: async (args) => {
|
|
544
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
545
|
-
const result = await api("GET", "/contacts", { workspace_id: ws });
|
|
546
|
-
return { ok: true, contacts: result.data ?? [], meta: result.meta ?? null };
|
|
547
|
-
},
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
name: "get_contact",
|
|
551
|
-
description:
|
|
552
|
-
"Fetch one contact by id. Example: get_contact({ contact_id: 'abc-...' }).",
|
|
553
|
-
inputSchema: obj(
|
|
554
|
-
{
|
|
555
|
-
contact_id: str("UUID of the contact."),
|
|
556
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
557
|
-
},
|
|
558
|
-
["contact_id"],
|
|
559
|
-
),
|
|
560
|
-
handler: async (args) => {
|
|
561
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
562
|
-
const result = await api("GET", `/contacts/${encodeURIComponent(args.contact_id)}`, {
|
|
563
|
-
workspace_id: ws,
|
|
564
|
-
});
|
|
565
|
-
return { ok: true, contact: result.data ?? null };
|
|
566
|
-
},
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
name: "create_contact",
|
|
570
|
-
description:
|
|
571
|
-
"Create a new contact. Typical use: 'Add Jane Doe jane@acme.co as a lead'. Example: create_contact({ first_name: 'Jane', last_name: 'Doe', email: 'jane@acme.co', status: 'lead' }).",
|
|
572
|
-
inputSchema: obj(
|
|
573
|
-
{
|
|
574
|
-
first_name: str("Required. Contact's first name."),
|
|
575
|
-
last_name: str("Optional. Last name."),
|
|
576
|
-
email: str("Optional but strongly recommended — unlocks form auto-linking and email sends."),
|
|
577
|
-
status: str("Optional lifecycle stage (e.g., 'lead', 'prospect', 'customer'). Defaults to 'lead'."),
|
|
578
|
-
source: str("Optional source tag (e.g., 'manual', 'intake-form', 'import')."),
|
|
579
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
580
|
-
},
|
|
581
|
-
["first_name"],
|
|
582
|
-
),
|
|
583
|
-
handler: async (args) => {
|
|
584
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
585
|
-
const result = await api("POST", "/contacts", {
|
|
586
|
-
body: {
|
|
587
|
-
firstName: args.first_name,
|
|
588
|
-
lastName: args.last_name ?? "",
|
|
589
|
-
email: args.email ?? null,
|
|
590
|
-
status: args.status ?? "lead",
|
|
591
|
-
source: args.source ?? "mcp",
|
|
592
|
-
},
|
|
593
|
-
workspace_id: ws,
|
|
594
|
-
});
|
|
595
|
-
return { ok: true, contact: result.data };
|
|
596
|
-
},
|
|
597
|
-
},
|
|
598
|
-
{
|
|
599
|
-
name: "update_contact",
|
|
600
|
-
description:
|
|
601
|
-
"Update fields on an existing contact. Partial — omit fields you don't want to change. Example: update_contact({ contact_id: '...', status: 'customer' }).",
|
|
602
|
-
inputSchema: obj(
|
|
603
|
-
{
|
|
604
|
-
contact_id: str("UUID of the contact to update."),
|
|
605
|
-
first_name: str("Optional new first name."),
|
|
606
|
-
last_name: str("Optional new last name."),
|
|
607
|
-
email: str("Optional new email."),
|
|
608
|
-
status: str("Optional new lifecycle stage."),
|
|
609
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
610
|
-
},
|
|
611
|
-
["contact_id"],
|
|
612
|
-
),
|
|
613
|
-
handler: async (args) => {
|
|
614
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
615
|
-
const patch = {};
|
|
616
|
-
if (args.first_name !== undefined) patch.firstName = args.first_name;
|
|
617
|
-
if (args.last_name !== undefined) patch.lastName = args.last_name;
|
|
618
|
-
if (args.email !== undefined) patch.email = args.email;
|
|
619
|
-
if (args.status !== undefined) patch.status = args.status;
|
|
620
|
-
const result = await api("PATCH", `/contacts/${encodeURIComponent(args.contact_id)}`, {
|
|
621
|
-
body: patch,
|
|
622
|
-
workspace_id: ws,
|
|
623
|
-
});
|
|
624
|
-
return { ok: true, contact: result.data };
|
|
625
|
-
},
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
name: "delete_contact",
|
|
629
|
-
description:
|
|
630
|
-
"Delete a contact and all linked deals/activities (cascades via FK). Irreversible. Example: delete_contact({ contact_id: '...' }).",
|
|
631
|
-
inputSchema: obj(
|
|
632
|
-
{
|
|
633
|
-
contact_id: str("UUID of the contact to delete."),
|
|
634
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
635
|
-
},
|
|
636
|
-
["contact_id"],
|
|
637
|
-
),
|
|
638
|
-
handler: async (args) => {
|
|
639
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
640
|
-
await api("DELETE", `/contacts/${encodeURIComponent(args.contact_id)}`, {
|
|
641
|
-
workspace_id: ws,
|
|
642
|
-
});
|
|
643
|
-
return { ok: true, deleted: args.contact_id };
|
|
644
|
-
},
|
|
645
|
-
},
|
|
646
|
-
{
|
|
647
|
-
name: "list_deals",
|
|
648
|
-
description: "List deals in the active workspace. Example: list_deals({}).",
|
|
649
|
-
inputSchema: obj({
|
|
650
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
651
|
-
}),
|
|
652
|
-
handler: async (args) => {
|
|
653
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
654
|
-
const result = await api("GET", "/deals", { workspace_id: ws });
|
|
655
|
-
return { ok: true, deals: result.data ?? [] };
|
|
656
|
-
},
|
|
657
|
-
},
|
|
658
|
-
{
|
|
659
|
-
name: "get_deal",
|
|
660
|
-
description: "Fetch one deal by id. Example: get_deal({ deal_id: '...' }).",
|
|
661
|
-
inputSchema: obj(
|
|
662
|
-
{
|
|
663
|
-
deal_id: str("UUID of the deal."),
|
|
664
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
665
|
-
},
|
|
666
|
-
["deal_id"],
|
|
667
|
-
),
|
|
668
|
-
handler: async (args) => {
|
|
669
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
670
|
-
const result = await api("GET", `/deals/${encodeURIComponent(args.deal_id)}`, {
|
|
671
|
-
workspace_id: ws,
|
|
672
|
-
});
|
|
673
|
-
return { ok: true, deal: result.data ?? null };
|
|
674
|
-
},
|
|
675
|
-
},
|
|
676
|
-
{
|
|
677
|
-
name: "create_deal",
|
|
678
|
-
description:
|
|
679
|
-
"Create a new deal attached to a contact on the default pipeline. Typical use: 'Create a $5k deal for Jane Doe at the Discovery stage'. Example: create_deal({ contact_id: '...', title: 'Q2 retainer', value: 5000, stage: 'Discovery' }).",
|
|
680
|
-
inputSchema: obj(
|
|
681
|
-
{
|
|
682
|
-
contact_id: str("UUID of the contact this deal belongs to."),
|
|
683
|
-
title: str("Human-readable deal name."),
|
|
684
|
-
value: { type: "number", description: "Optional deal value in workspace's default currency. Defaults to 0." },
|
|
685
|
-
stage: str("Optional stage name (e.g. 'Discovery', 'Proposal'). Defaults to the first stage of the default pipeline."),
|
|
686
|
-
probability: { type: "number", description: "Optional win probability 0-100. Defaults to 0." },
|
|
687
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
688
|
-
},
|
|
689
|
-
["contact_id", "title"],
|
|
690
|
-
),
|
|
691
|
-
handler: async (args) => {
|
|
692
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
693
|
-
const result = await api("POST", "/deals", {
|
|
694
|
-
body: {
|
|
695
|
-
contactId: args.contact_id,
|
|
696
|
-
title: args.title,
|
|
697
|
-
value: args.value ?? 0,
|
|
698
|
-
stage: args.stage ?? "New",
|
|
699
|
-
probability: args.probability ?? 0,
|
|
700
|
-
},
|
|
701
|
-
workspace_id: ws,
|
|
702
|
-
});
|
|
703
|
-
return { ok: true, deal: result.data };
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
{
|
|
707
|
-
name: "update_deal",
|
|
708
|
-
description:
|
|
709
|
-
"Update a deal. Partial — omit fields to keep them. For stage-only moves prefer move_deal_stage (clearer intent). Example: update_deal({ deal_id: '...', value: 7500 }).",
|
|
710
|
-
inputSchema: obj(
|
|
711
|
-
{
|
|
712
|
-
deal_id: str("UUID of the deal."),
|
|
713
|
-
title: str("Optional new title."),
|
|
714
|
-
stage: str("Optional new stage."),
|
|
715
|
-
value: { type: "number", description: "Optional new value." },
|
|
716
|
-
probability: { type: "number", description: "Optional new probability (0-100)." },
|
|
717
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
718
|
-
},
|
|
719
|
-
["deal_id"],
|
|
720
|
-
),
|
|
721
|
-
handler: async (args) => {
|
|
722
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
723
|
-
const patch = {};
|
|
724
|
-
if (args.title !== undefined) patch.title = args.title;
|
|
725
|
-
if (args.stage !== undefined) patch.stage = args.stage;
|
|
726
|
-
if (args.value !== undefined) patch.value = args.value;
|
|
727
|
-
if (args.probability !== undefined) patch.probability = args.probability;
|
|
728
|
-
const result = await api("PATCH", `/deals/${encodeURIComponent(args.deal_id)}`, {
|
|
729
|
-
body: patch,
|
|
730
|
-
workspace_id: ws,
|
|
731
|
-
});
|
|
732
|
-
return { ok: true, deal: result.data };
|
|
733
|
-
},
|
|
734
|
-
},
|
|
735
|
-
{
|
|
736
|
-
name: "move_deal_stage",
|
|
737
|
-
description:
|
|
738
|
-
"Move a deal to a new stage. Same effect as dragging the card on the kanban. Example: move_deal_stage({ deal_id: '...', to_stage: 'Proposal' }).",
|
|
739
|
-
inputSchema: obj(
|
|
740
|
-
{
|
|
741
|
-
deal_id: str("UUID of the deal."),
|
|
742
|
-
to_stage: str("Destination stage name."),
|
|
743
|
-
probability: { type: "number", description: "Optional. Stage probability (0-100) if the workspace's pipeline has one defined for this stage." },
|
|
744
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
745
|
-
},
|
|
746
|
-
["deal_id", "to_stage"],
|
|
747
|
-
),
|
|
748
|
-
handler: async (args) => {
|
|
749
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
750
|
-
const body = { stage: args.to_stage };
|
|
751
|
-
if (args.probability !== undefined) body.probability = args.probability;
|
|
752
|
-
const result = await api("PATCH", `/deals/${encodeURIComponent(args.deal_id)}`, {
|
|
753
|
-
body,
|
|
754
|
-
workspace_id: ws,
|
|
755
|
-
});
|
|
756
|
-
return { ok: true, deal: result.data };
|
|
757
|
-
},
|
|
758
|
-
},
|
|
759
|
-
{
|
|
760
|
-
name: "delete_deal",
|
|
761
|
-
description: "Delete a deal. Irreversible. Example: delete_deal({ deal_id: '...' }).",
|
|
762
|
-
inputSchema: obj(
|
|
763
|
-
{
|
|
764
|
-
deal_id: str("UUID of the deal."),
|
|
765
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
766
|
-
},
|
|
767
|
-
["deal_id"],
|
|
768
|
-
),
|
|
769
|
-
handler: async (args) => {
|
|
770
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
771
|
-
await api("DELETE", `/deals/${encodeURIComponent(args.deal_id)}`, {
|
|
772
|
-
workspace_id: ws,
|
|
773
|
-
});
|
|
774
|
-
return { ok: true, deleted: args.deal_id };
|
|
775
|
-
},
|
|
776
|
-
},
|
|
777
|
-
{
|
|
778
|
-
name: "list_activities",
|
|
779
|
-
description:
|
|
780
|
-
"List activity log entries (tasks, notes, email sent, booking created, etc.) across the workspace. Example: list_activities({}).",
|
|
781
|
-
inputSchema: obj({
|
|
782
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
783
|
-
}),
|
|
784
|
-
handler: async (args) => {
|
|
785
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
786
|
-
const result = await api("GET", "/activities", { workspace_id: ws });
|
|
787
|
-
return { ok: true, activities: result.data ?? [] };
|
|
788
|
-
},
|
|
789
|
-
},
|
|
790
|
-
|
|
791
|
-
// ════════════════════════════════════════════════════════════════════
|
|
792
|
-
// Booking tools — Phase 2.c per tasks/mcp-gap-audit.md
|
|
793
|
-
// CRUD for appointment types (template rows in the bookings table).
|
|
794
|
-
// The v1 endpoints at /api/v1/booking/appointment-types[/<slug>] enforce
|
|
795
|
-
// `status='template'` so tools here cannot accidentally touch real
|
|
796
|
-
// scheduled bookings. Cancel / reschedule / list_bookings are deferred
|
|
797
|
-
// until bookings block has real scheduled data to test against.
|
|
798
|
-
// ════════════════════════════════════════════════════════════════════
|
|
799
|
-
|
|
800
|
-
{
|
|
801
|
-
name: "create_activity",
|
|
802
|
-
description:
|
|
803
|
-
"Append an activity-log entry to a contact (and/or deal). Use this instead of stuffing agent reminders into contacts.notes — notes gets overwritten on updates; activities are append-only. Valid types: task, note, email, sms, call, meeting, stage_change, payment, review_request, agent_action. Example: create_activity({ contact_id: 'ctc_...', type: 'agent_action', subject: 'Speed-to-Lead agent booked consult', body: 'Scheduled for 2026-05-01' })",
|
|
804
|
-
inputSchema: obj(
|
|
805
|
-
{
|
|
806
|
-
contact_id: str("Contact to log against. Either contact_id or deal_id is required."),
|
|
807
|
-
deal_id: str("Deal to log against. Either contact_id or deal_id is required."),
|
|
808
|
-
type: str("task | note | email | sms | call | meeting | stage_change | payment | review_request | agent_action"),
|
|
809
|
-
subject: str("One-line title (≤200 chars). Either subject or body is required."),
|
|
810
|
-
body: str("Optional multi-line detail (≤4000 chars)."),
|
|
811
|
-
scheduled_at: str("Optional ISO timestamp if the activity is planned for a future time (e.g., a task)."),
|
|
812
|
-
completed_at: str("Optional ISO timestamp if logging a completed past action."),
|
|
813
|
-
metadata: {
|
|
814
|
-
type: "object",
|
|
815
|
-
description: "Optional JSON metadata — e.g., { agentId: 'agt_...', confidence: 0.87 }",
|
|
816
|
-
},
|
|
817
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
818
|
-
},
|
|
819
|
-
["type"],
|
|
820
|
-
),
|
|
821
|
-
handler: async (args) => {
|
|
822
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
823
|
-
return api("POST", "/activities", {
|
|
824
|
-
body: {
|
|
825
|
-
contact_id: args.contact_id ?? null,
|
|
826
|
-
deal_id: args.deal_id ?? null,
|
|
827
|
-
type: args.type,
|
|
828
|
-
subject: args.subject ?? null,
|
|
829
|
-
body: args.body ?? null,
|
|
830
|
-
scheduled_at: args.scheduled_at ?? null,
|
|
831
|
-
completed_at: args.completed_at ?? null,
|
|
832
|
-
metadata: args.metadata ?? {},
|
|
833
|
-
},
|
|
834
|
-
workspace_id: ws,
|
|
835
|
-
});
|
|
836
|
-
},
|
|
837
|
-
},
|
|
838
|
-
{
|
|
839
|
-
name: "list_bookings",
|
|
840
|
-
description:
|
|
841
|
-
"List scheduled bookings (not appointment-type templates — see list_appointment_types for those). Supports filtering by contact, status, and date range. Default sort: most-recent-first; if `from` is set, switches to earliest-upcoming-first for reminder flows. Example: list_bookings({ from: '2026-04-22T00:00:00Z', limit: 20 })",
|
|
842
|
-
inputSchema: obj(
|
|
843
|
-
{
|
|
844
|
-
contact_id: str("Optional. Filter to a specific contact's bookings."),
|
|
845
|
-
status: str("Optional. Filter by status (scheduled | completed | cancelled | no_show)."),
|
|
846
|
-
from: str("Optional ISO timestamp. Only bookings starting at or after this moment."),
|
|
847
|
-
to: str("Optional ISO timestamp. Only bookings starting at or before this moment."),
|
|
848
|
-
limit: { type: "number", description: "Max rows (default 50, max 200)." },
|
|
849
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
850
|
-
},
|
|
851
|
-
[],
|
|
852
|
-
),
|
|
853
|
-
handler: async (args) => {
|
|
854
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
855
|
-
const params = new URLSearchParams();
|
|
856
|
-
if (args.contact_id) params.set("contact_id", args.contact_id);
|
|
857
|
-
if (args.status) params.set("status", args.status);
|
|
858
|
-
if (args.from) params.set("from", args.from);
|
|
859
|
-
if (args.to) params.set("to", args.to);
|
|
860
|
-
if (typeof args.limit === "number") params.set("limit", String(Math.min(args.limit, 200)));
|
|
861
|
-
const qs = params.toString();
|
|
862
|
-
return api("GET", `/bookings${qs ? `?${qs}` : ""}`, { workspace_id: ws });
|
|
863
|
-
},
|
|
864
|
-
},
|
|
865
|
-
{
|
|
866
|
-
name: "create_coupon",
|
|
867
|
-
description:
|
|
868
|
-
"Create a Stripe coupon + matching per-contact redeemable promotion code on the workspace's connected Stripe account. Use for Win-Back / retention agents that need UNIQUE codes per recipient (shared codes are vulnerable to abuse + lose attribution signal). Default max_redemptions=1 + auto-generated code string. Requires the workspace to have completed Stripe Connect onboarding. Example: create_coupon({ percent_off: 20, duration: 'once', name: 'Win-Back 20% off' })",
|
|
869
|
-
inputSchema: obj(
|
|
870
|
-
{
|
|
871
|
-
percent_off: { type: "number", description: "Discount percentage (0 < n ≤ 100). Either percent_off or amount_off is required." },
|
|
872
|
-
amount_off: { type: "number", description: "Flat discount in the currency's major unit (e.g., 25.00 for $25 off). Either percent_off or amount_off is required." },
|
|
873
|
-
currency: str("Only used with amount_off. 3-letter ISO code. Defaults to usd."),
|
|
874
|
-
duration: str("'once' (default) | 'forever' | 'repeating'. 'repeating' requires duration_in_months."),
|
|
875
|
-
duration_in_months: { type: "number", description: "Required when duration='repeating'." },
|
|
876
|
-
name: str("Optional display name for the coupon (≤60 chars)."),
|
|
877
|
-
code: str("Optional fixed redeemable code string. If omitted, Stripe auto-generates one."),
|
|
878
|
-
max_redemptions: { type: "number", description: "Max total redemptions. Default 1 — per-contact unique code." },
|
|
879
|
-
expires_at: str("Optional ISO timestamp. Code becomes invalid after this moment. Prefer expires_in_days for agent archetypes."),
|
|
880
|
-
expires_in_days: { type: "number", description: "Relative expiry: code becomes invalid N days after this call fires (1–365). Preferred over expires_at for agent archetypes so the window stays meaningful no matter when the agent was last deployed." },
|
|
881
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
882
|
-
},
|
|
883
|
-
[],
|
|
884
|
-
),
|
|
885
|
-
handler: async (args) => {
|
|
886
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
887
|
-
const body = {};
|
|
888
|
-
if (typeof args.percent_off === "number") body.percent_off = args.percent_off;
|
|
889
|
-
if (typeof args.amount_off === "number") body.amount_off = args.amount_off;
|
|
890
|
-
if (args.currency) body.currency = args.currency;
|
|
891
|
-
if (args.duration) body.duration = args.duration;
|
|
892
|
-
if (typeof args.duration_in_months === "number") body.duration_in_months = args.duration_in_months;
|
|
893
|
-
if (args.name) body.name = args.name;
|
|
894
|
-
if (args.code) body.code = args.code;
|
|
895
|
-
if (typeof args.max_redemptions === "number") body.max_redemptions = args.max_redemptions;
|
|
896
|
-
if (args.expires_at) body.expires_at = args.expires_at;
|
|
897
|
-
if (typeof args.expires_in_days === "number") body.expires_in_days = args.expires_in_days;
|
|
898
|
-
return api("POST", "/coupons", { body, workspace_id: ws });
|
|
899
|
-
},
|
|
900
|
-
},
|
|
901
|
-
{
|
|
902
|
-
name: "create_booking",
|
|
903
|
-
description:
|
|
904
|
-
"Schedule a real booking against an existing appointment type. Looks up the template by id, creates a scheduled row on the workspace calendar, stamps the contact's name + email, emits booking.created, and — if the appointment type has a price > 0 — returns a Stripe Checkout URL routed to the SMB's connected Stripe account so the builder / agent can text or email the payment link to the contact. Example: create_booking({ contact_id: 'ctc_...', appointment_type_id: 'appt_...', starts_at: '2026-05-01T15:00:00Z' })",
|
|
905
|
-
inputSchema: obj(
|
|
906
|
-
{
|
|
907
|
-
contact_id: str("Required. CRM contact being booked."),
|
|
908
|
-
appointment_type_id: str("Required. Appointment-type template id from list_appointment_types."),
|
|
909
|
-
starts_at: str("Required. ISO 8601 timestamp for the appointment start (e.g. 2026-05-01T15:00:00Z). Duration is read from the appointment type."),
|
|
910
|
-
notes: str("Optional free-form booking notes."),
|
|
911
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
912
|
-
},
|
|
913
|
-
["contact_id", "appointment_type_id", "starts_at"],
|
|
914
|
-
),
|
|
915
|
-
handler: async (args) => {
|
|
916
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
917
|
-
return api("POST", "/bookings", {
|
|
918
|
-
body: {
|
|
919
|
-
contact_id: args.contact_id,
|
|
920
|
-
appointment_type_id: args.appointment_type_id,
|
|
921
|
-
starts_at: args.starts_at,
|
|
922
|
-
notes: args.notes ?? null,
|
|
923
|
-
},
|
|
924
|
-
workspace_id: ws,
|
|
925
|
-
});
|
|
926
|
-
},
|
|
927
|
-
},
|
|
928
|
-
{
|
|
929
|
-
name: "get_booking",
|
|
930
|
-
description:
|
|
931
|
-
"Fetch one scheduled booking by id. Returns the full detail (contact, times, status, notes, meeting URL, cancellation timestamp, metadata). Appointment-type templates are NOT returned here — use list_appointment_types for those. 404s if the id is unknown OR belongs to a different workspace. Example: get_booking({ booking_id: 'bkg_...' }).",
|
|
932
|
-
inputSchema: obj(
|
|
933
|
-
{
|
|
934
|
-
booking_id: str("Required. UUID of the booking."),
|
|
935
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
936
|
-
},
|
|
937
|
-
["booking_id"],
|
|
938
|
-
),
|
|
939
|
-
handler: async (args) => {
|
|
940
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
941
|
-
const result = await api("GET", `/bookings/${encodeURIComponent(args.booking_id)}`, {
|
|
942
|
-
workspace_id: ws,
|
|
943
|
-
});
|
|
944
|
-
return { ok: true, booking: result.data ?? null };
|
|
945
|
-
},
|
|
946
|
-
},
|
|
947
|
-
{
|
|
948
|
-
name: "cancel_booking",
|
|
949
|
-
description:
|
|
950
|
-
"Cancel a scheduled booking. Sets status to 'cancelled', stamps cancelledAt, deletes the Google Calendar event, and emits booking.cancelled. Idempotent — re-cancelling an already-cancelled booking is a 200 no-op with alreadyCancelled=true (no duplicate events, no calendar errors). Past-dated bookings CAN be cancelled (legitimate retroactive cleanup). Does NOT touch linked payments — linkedPaymentIds is returned so the agent can compose refund_payment explicitly if the business rule is 'cancel AND refund'. Example: cancel_booking({ booking_id: 'bkg_...' }).",
|
|
951
|
-
inputSchema: obj(
|
|
952
|
-
{
|
|
953
|
-
booking_id: str("Required. UUID of the booking to cancel."),
|
|
954
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
955
|
-
},
|
|
956
|
-
["booking_id"],
|
|
957
|
-
),
|
|
958
|
-
handler: async (args) => {
|
|
959
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
960
|
-
const result = await api("POST", `/bookings/${encodeURIComponent(args.booking_id)}/cancel`, {
|
|
961
|
-
workspace_id: ws,
|
|
962
|
-
});
|
|
963
|
-
return { ok: true, ...result.data };
|
|
964
|
-
},
|
|
965
|
-
},
|
|
966
|
-
{
|
|
967
|
-
name: "reschedule_booking",
|
|
968
|
-
description:
|
|
969
|
-
"Move a scheduled booking to a new starts_at. Preserves the original duration — endsAt tracks the move so a 30-min consult stays 30 mins at the new time. Updates the Google Calendar event in place (event id preserved; attendees see the time change on their existing invite) and emits booking.rescheduled with both previousStartsAt and newStartsAt so follow-up agents can describe the change. Rejects past-dated new starts_at (400) and refuses to reschedule a cancelled booking (422 — reviving a cancellation should be a new create_booking). Does NOT change appointment type; does NOT touch linked payments. Example: reschedule_booking({ booking_id: 'bkg_...', starts_at: '2026-05-02T15:00:00Z' }).",
|
|
970
|
-
inputSchema: obj(
|
|
971
|
-
{
|
|
972
|
-
booking_id: str("Required. UUID of the booking to move."),
|
|
973
|
-
starts_at: str("Required. New ISO 8601 timestamp. Must be in the future. Duration is preserved from the current booking."),
|
|
974
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
975
|
-
},
|
|
976
|
-
["booking_id", "starts_at"],
|
|
977
|
-
),
|
|
978
|
-
handler: async (args) => {
|
|
979
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
980
|
-
const result = await api("POST", `/bookings/${encodeURIComponent(args.booking_id)}/reschedule`, {
|
|
981
|
-
body: { starts_at: args.starts_at },
|
|
982
|
-
workspace_id: ws,
|
|
983
|
-
});
|
|
984
|
-
return { ok: true, ...result.data };
|
|
985
|
-
},
|
|
986
|
-
},
|
|
987
|
-
{
|
|
988
|
-
name: "list_appointment_types",
|
|
989
|
-
description:
|
|
990
|
-
"List all appointment types (bookable templates) in the workspace. Example: list_appointment_types({}).",
|
|
991
|
-
inputSchema: obj({
|
|
992
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
993
|
-
}),
|
|
994
|
-
handler: async (args) => {
|
|
995
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
996
|
-
const result = await api("GET", "/booking/appointment-types", { workspace_id: ws });
|
|
997
|
-
return { ok: true, appointment_types: result.appointment_types ?? [] };
|
|
998
|
-
},
|
|
999
|
-
},
|
|
1000
|
-
{
|
|
1001
|
-
name: "create_appointment_type",
|
|
1002
|
-
description:
|
|
1003
|
-
"Create a new appointment type with its own public /book/<slug> URL. Defaults availability to Mon–Fri 9am–5pm (edit on /bookings to change). Example: create_appointment_type({ title: 'Strategy call', duration_minutes: 45, price: 150 }).",
|
|
1004
|
-
inputSchema: obj(
|
|
1005
|
-
{
|
|
1006
|
-
title: str("Required. Human-readable name, e.g., 'Strategy call'."),
|
|
1007
|
-
booking_slug: str("Optional. URL-safe slug. Auto-derived from title if omitted."),
|
|
1008
|
-
duration_minutes: { type: "number", description: "Optional. 5–240. Defaults to 30." },
|
|
1009
|
-
description: str("Optional. Up to 800 chars. Shown on the public booking page."),
|
|
1010
|
-
price: { type: "number", description: "Optional. Defaults to 0 (free). Non-zero prices route through Stripe checkout on submit (requires Stripe connected)." },
|
|
1011
|
-
buffer_before_minutes: { type: "number", description: "Optional. 0–120. Defaults to 0." },
|
|
1012
|
-
buffer_after_minutes: { type: "number", description: "Optional. 0–120. Defaults to 0." },
|
|
1013
|
-
max_bookings_per_day: { type: "number", description: "Optional. Hard daily cap (1–100). Omit for unlimited." },
|
|
1014
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1015
|
-
},
|
|
1016
|
-
["title"],
|
|
1017
|
-
),
|
|
1018
|
-
handler: async (args) => {
|
|
1019
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1020
|
-
const result = await api("POST", "/booking/appointment-types", {
|
|
1021
|
-
body: {
|
|
1022
|
-
title: args.title,
|
|
1023
|
-
booking_slug: args.booking_slug,
|
|
1024
|
-
duration_minutes: args.duration_minutes,
|
|
1025
|
-
description: args.description,
|
|
1026
|
-
price: args.price,
|
|
1027
|
-
buffer_before_minutes: args.buffer_before_minutes,
|
|
1028
|
-
buffer_after_minutes: args.buffer_after_minutes,
|
|
1029
|
-
max_bookings_per_day: args.max_bookings_per_day,
|
|
1030
|
-
},
|
|
1031
|
-
workspace_id: ws,
|
|
1032
|
-
});
|
|
1033
|
-
return {
|
|
1034
|
-
ok: true,
|
|
1035
|
-
appointment_type: result.appointment_type,
|
|
1036
|
-
public_url: result.public_url,
|
|
1037
|
-
};
|
|
1038
|
-
},
|
|
1039
|
-
},
|
|
1040
|
-
{
|
|
1041
|
-
name: "update_appointment_type",
|
|
1042
|
-
description:
|
|
1043
|
-
"Update an existing appointment type. Partial — omit fields to keep them. Example: update_appointment_type({ booking_slug: 'default', duration_minutes: 60, price: 200 }). Pass booking_slug='default' to edit the auto-seeded 'Book a call' template.",
|
|
1044
|
-
inputSchema: obj(
|
|
1045
|
-
{
|
|
1046
|
-
booking_slug: str("Slug of the appointment type. Use 'default' for the auto-seeded template."),
|
|
1047
|
-
title: str("Optional new title."),
|
|
1048
|
-
duration_minutes: { type: "number", description: "Optional new duration (5–240)." },
|
|
1049
|
-
description: str("Optional new description (≤800 chars). Empty string clears it."),
|
|
1050
|
-
price: { type: "number", description: "Optional new price. 0 = free." },
|
|
1051
|
-
buffer_before_minutes: { type: "number", description: "Optional. 0–120." },
|
|
1052
|
-
buffer_after_minutes: { type: "number", description: "Optional. 0–120." },
|
|
1053
|
-
max_bookings_per_day: { type: "number", description: "Optional. 1–100. Pass null to remove cap." },
|
|
1054
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1055
|
-
},
|
|
1056
|
-
["booking_slug"],
|
|
1057
|
-
),
|
|
1058
|
-
handler: async (args) => {
|
|
1059
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1060
|
-
const result = await api(
|
|
1061
|
-
"PATCH",
|
|
1062
|
-
`/booking/appointment-types/${encodeURIComponent(args.booking_slug)}`,
|
|
1063
|
-
{
|
|
1064
|
-
body: {
|
|
1065
|
-
title: args.title,
|
|
1066
|
-
duration_minutes: args.duration_minutes,
|
|
1067
|
-
description: args.description,
|
|
1068
|
-
price: args.price,
|
|
1069
|
-
buffer_before_minutes: args.buffer_before_minutes,
|
|
1070
|
-
buffer_after_minutes: args.buffer_after_minutes,
|
|
1071
|
-
max_bookings_per_day: args.max_bookings_per_day,
|
|
1072
|
-
},
|
|
1073
|
-
workspace_id: ws,
|
|
1074
|
-
},
|
|
1075
|
-
);
|
|
1076
|
-
return result;
|
|
1077
|
-
},
|
|
1078
|
-
},
|
|
1079
|
-
{
|
|
1080
|
-
name: "configure_booking",
|
|
1081
|
-
description:
|
|
1082
|
-
"DEPRECATED alias for update_appointment_type({ booking_slug: 'default', ... }). Kept so existing Claude Code sessions don't break. Prefer update_appointment_type for new scripts.",
|
|
1083
|
-
inputSchema: obj(
|
|
1084
|
-
{
|
|
1085
|
-
title: str("Optional new title."),
|
|
1086
|
-
duration_minutes: { type: "number", description: "Optional new duration in minutes." },
|
|
1087
|
-
description: str("Optional description."),
|
|
1088
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1089
|
-
},
|
|
1090
|
-
[],
|
|
1091
|
-
),
|
|
1092
|
-
handler: async (args) => {
|
|
1093
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1094
|
-
const result = await api("PATCH", "/booking/appointment-types/default", {
|
|
1095
|
-
body: {
|
|
1096
|
-
title: args.title,
|
|
1097
|
-
duration_minutes: args.duration_minutes,
|
|
1098
|
-
description: args.description,
|
|
1099
|
-
},
|
|
1100
|
-
workspace_id: ws,
|
|
1101
|
-
});
|
|
1102
|
-
return result;
|
|
1103
|
-
},
|
|
1104
|
-
},
|
|
1105
|
-
|
|
1106
|
-
// ════════════════════════════════════════════════════════════════════
|
|
1107
|
-
// Intake forms tools — Phase 2.d per tasks/mcp-gap-audit.md
|
|
1108
|
-
// CRUD on intake_forms + list_submissions read path. Template-backed
|
|
1109
|
-
// create_form uses the 6 templates from lib/forms/templates.ts. The old
|
|
1110
|
-
// `customize_intake_form` is kept as a deprecated alias for the default
|
|
1111
|
-
// 'intake' form; new code should use update_form.
|
|
1112
|
-
// ════════════════════════════════════════════════════════════════════
|
|
1113
|
-
|
|
1114
|
-
{
|
|
1115
|
-
name: "list_forms",
|
|
1116
|
-
description:
|
|
1117
|
-
"List intake forms in the workspace. Example: list_forms({}).",
|
|
1118
|
-
inputSchema: obj({
|
|
1119
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1120
|
-
}),
|
|
1121
|
-
handler: async (args) => {
|
|
1122
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1123
|
-
const result = await api("GET", "/forms", { workspace_id: ws });
|
|
1124
|
-
return { ok: true, forms: result.forms ?? [] };
|
|
1125
|
-
},
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
name: "get_form",
|
|
1129
|
-
description:
|
|
1130
|
-
"Fetch one form by id or slug. Example: get_form({ form: 'contact' }) or get_form({ form: 'uuid…' }).",
|
|
1131
|
-
inputSchema: obj(
|
|
1132
|
-
{
|
|
1133
|
-
form: str("Form id (uuid) or slug (e.g., 'contact', 'intake')."),
|
|
1134
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1135
|
-
},
|
|
1136
|
-
["form"],
|
|
1137
|
-
),
|
|
1138
|
-
handler: async (args) => {
|
|
1139
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1140
|
-
const result = await api("GET", `/forms/${encodeURIComponent(args.form)}`, {
|
|
1141
|
-
workspace_id: ws,
|
|
1142
|
-
});
|
|
1143
|
-
return { ok: true, form: result.form ?? null };
|
|
1144
|
-
},
|
|
1145
|
-
},
|
|
1146
|
-
{
|
|
1147
|
-
name: "create_form",
|
|
1148
|
-
description:
|
|
1149
|
-
"Create a new intake form. Pass template_id to pre-fill fields from a built-in template (contact, lead-qualification, booking-request, nps-feedback, event-registration, blank). Example: create_form({ template_id: 'contact' }) → uses 'Contact us' template. Or pass explicit fields: create_form({ name: 'Intake', fields: [{ key: 'email', label: 'Email', type: 'email', required: true }] }).",
|
|
1150
|
-
inputSchema: obj(
|
|
1151
|
-
{
|
|
1152
|
-
template_id: str("Optional. One of: blank, contact, lead-qualification, booking-request, nps-feedback, event-registration."),
|
|
1153
|
-
name: str("Optional. Falls back to template name or 'New intake form'."),
|
|
1154
|
-
slug: str("Optional URL-safe slug. Falls back to template defaultSlug or slugified name."),
|
|
1155
|
-
fields: {
|
|
1156
|
-
type: "array",
|
|
1157
|
-
description: "Optional field list. Overrides template fields. Each: { key, label, type ('text'|'email'|'tel'|'textarea'|'select'), required, options? }.",
|
|
1158
|
-
items: { type: "object" },
|
|
1159
|
-
},
|
|
1160
|
-
is_active: { type: "boolean", description: "Optional. Defaults to true." },
|
|
1161
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1162
|
-
},
|
|
1163
|
-
[],
|
|
1164
|
-
),
|
|
1165
|
-
handler: async (args) => {
|
|
1166
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1167
|
-
const result = await api("POST", "/forms", {
|
|
1168
|
-
body: {
|
|
1169
|
-
template_id: args.template_id,
|
|
1170
|
-
name: args.name,
|
|
1171
|
-
slug: args.slug,
|
|
1172
|
-
fields: args.fields,
|
|
1173
|
-
is_active: args.is_active,
|
|
1174
|
-
},
|
|
1175
|
-
workspace_id: ws,
|
|
1176
|
-
});
|
|
1177
|
-
return result;
|
|
1178
|
-
},
|
|
1179
|
-
},
|
|
1180
|
-
{
|
|
1181
|
-
name: "update_form",
|
|
1182
|
-
description:
|
|
1183
|
-
"Update a form. Partial — omit fields to keep them. Replacing `fields` replaces the whole array (each field: { key, label, type, required, options? }). Example: update_form({ form: 'intake', fields: [...] }).",
|
|
1184
|
-
inputSchema: obj(
|
|
1185
|
-
{
|
|
1186
|
-
form: str("Form id (uuid) or slug."),
|
|
1187
|
-
name: str("Optional new name."),
|
|
1188
|
-
slug: str("Optional new slug (URL-safe)."),
|
|
1189
|
-
fields: {
|
|
1190
|
-
type: "array",
|
|
1191
|
-
description: "Optional new field array. Whole replacement.",
|
|
1192
|
-
items: { type: "object" },
|
|
1193
|
-
},
|
|
1194
|
-
is_active: { type: "boolean", description: "Optional. Toggle publish state." },
|
|
1195
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1196
|
-
},
|
|
1197
|
-
["form"],
|
|
1198
|
-
),
|
|
1199
|
-
handler: async (args) => {
|
|
1200
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1201
|
-
const result = await api("PATCH", `/forms/${encodeURIComponent(args.form)}`, {
|
|
1202
|
-
body: {
|
|
1203
|
-
name: args.name,
|
|
1204
|
-
slug: args.slug,
|
|
1205
|
-
fields: args.fields,
|
|
1206
|
-
is_active: args.is_active,
|
|
1207
|
-
},
|
|
1208
|
-
workspace_id: ws,
|
|
1209
|
-
});
|
|
1210
|
-
return result;
|
|
1211
|
-
},
|
|
1212
|
-
},
|
|
1213
|
-
{
|
|
1214
|
-
name: "delete_form",
|
|
1215
|
-
description:
|
|
1216
|
-
"Delete a form. Irreversible. Submissions are NOT deleted (form_submissions has ON DELETE SET NULL on form_id). Example: delete_form({ form: 'old-survey' }).",
|
|
1217
|
-
inputSchema: obj(
|
|
1218
|
-
{
|
|
1219
|
-
form: str("Form id (uuid) or slug."),
|
|
1220
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1221
|
-
},
|
|
1222
|
-
["form"],
|
|
1223
|
-
),
|
|
1224
|
-
handler: async (args) => {
|
|
1225
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1226
|
-
await api("DELETE", `/forms/${encodeURIComponent(args.form)}`, { workspace_id: ws });
|
|
1227
|
-
return { ok: true, deleted: args.form };
|
|
1228
|
-
},
|
|
1229
|
-
},
|
|
1230
|
-
{
|
|
1231
|
-
name: "list_submissions",
|
|
1232
|
-
description:
|
|
1233
|
-
"List submissions for a form. Example: list_submissions({ form_id: 'uuid…' }).",
|
|
1234
|
-
inputSchema: obj(
|
|
1235
|
-
{
|
|
1236
|
-
form_id: str("UUID of the form. Slug lookup not supported on this endpoint — use get_form first if you only have the slug."),
|
|
1237
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1238
|
-
},
|
|
1239
|
-
["form_id"],
|
|
1240
|
-
),
|
|
1241
|
-
handler: async (args) => {
|
|
1242
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1243
|
-
const result = await api(
|
|
1244
|
-
"GET",
|
|
1245
|
-
`/forms/${encodeURIComponent(args.form_id)}/submissions`,
|
|
1246
|
-
{ workspace_id: ws },
|
|
1247
|
-
);
|
|
1248
|
-
return { ok: true, submissions: result.data ?? result.submissions ?? [] };
|
|
1249
|
-
},
|
|
1250
|
-
},
|
|
1251
|
-
{
|
|
1252
|
-
name: "customize_intake_form",
|
|
1253
|
-
description:
|
|
1254
|
-
"DEPRECATED alias for update_form({ form: 'intake', fields }). Only edits the auto-seeded default form; prefer update_form for new scripts so you can target any form in the workspace.",
|
|
1255
|
-
inputSchema: obj(
|
|
1256
|
-
{
|
|
1257
|
-
fields: {
|
|
1258
|
-
type: "array",
|
|
1259
|
-
description: "Replacement field list.",
|
|
1260
|
-
items: { type: "object" },
|
|
1261
|
-
},
|
|
1262
|
-
form_name: str("Optional new display name for the default form."),
|
|
1263
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1264
|
-
},
|
|
1265
|
-
[],
|
|
1266
|
-
),
|
|
1267
|
-
handler: async (args) => {
|
|
1268
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1269
|
-
const result = await api("PATCH", "/forms/intake", {
|
|
1270
|
-
body: { name: args.form_name, fields: args.fields },
|
|
1271
|
-
workspace_id: ws,
|
|
1272
|
-
});
|
|
1273
|
-
return result;
|
|
1274
|
-
},
|
|
1275
|
-
},
|
|
1276
|
-
|
|
1277
|
-
// ===== Phase 3 — Email + conversation tools =====
|
|
1278
|
-
|
|
1279
|
-
{
|
|
1280
|
-
name: "send_email",
|
|
1281
|
-
description:
|
|
1282
|
-
"Send a one-off email through the workspace's configured provider (Resend by default). Checks the suppression list before sending and skips with {suppressed: true} if the recipient has opted out. Example: send_email({ to: 'alex@acme.com', subject: 'Welcome', body: 'Thanks for signing up', contact_id: 'ctc_123' })",
|
|
1283
|
-
inputSchema: obj(
|
|
1284
|
-
{
|
|
1285
|
-
to: str("Recipient email address."),
|
|
1286
|
-
subject: str("Email subject line."),
|
|
1287
|
-
body: str("Plain-text body — rendered into the default HTML shell."),
|
|
1288
|
-
contact_id: str("Optional. Links the email to a CRM contact for threading."),
|
|
1289
|
-
provider: str("Optional. Force a specific provider (default: resend)."),
|
|
1290
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1291
|
-
},
|
|
1292
|
-
["to", "subject", "body"],
|
|
1293
|
-
),
|
|
1294
|
-
handler: async (args) => {
|
|
1295
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1296
|
-
const result = await api("POST", "/emails", {
|
|
1297
|
-
body: {
|
|
1298
|
-
to: args.to,
|
|
1299
|
-
subject: args.subject,
|
|
1300
|
-
body: args.body,
|
|
1301
|
-
contactId: args.contact_id ?? null,
|
|
1302
|
-
provider: args.provider ?? null,
|
|
1303
|
-
},
|
|
1304
|
-
workspace_id: ws,
|
|
1305
|
-
});
|
|
1306
|
-
return result;
|
|
1307
|
-
},
|
|
1308
|
-
},
|
|
1309
|
-
{
|
|
1310
|
-
name: "list_emails",
|
|
1311
|
-
description:
|
|
1312
|
-
"List recent emails sent from the workspace, newest first. Useful for checking delivery status before following up.",
|
|
1313
|
-
inputSchema: obj(
|
|
1314
|
-
{
|
|
1315
|
-
limit: {
|
|
1316
|
-
type: "number",
|
|
1317
|
-
description: "Max rows to return (default 50, max 200).",
|
|
1318
|
-
},
|
|
1319
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1320
|
-
},
|
|
1321
|
-
[],
|
|
1322
|
-
),
|
|
1323
|
-
handler: async (args) => {
|
|
1324
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1325
|
-
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1326
|
-
const result = await api("GET", `/emails${qs}`, { workspace_id: ws });
|
|
1327
|
-
return result;
|
|
1328
|
-
},
|
|
1329
|
-
},
|
|
1330
|
-
{
|
|
1331
|
-
name: "get_email",
|
|
1332
|
-
description:
|
|
1333
|
-
"Fetch a single email with its full provider-event history (sent / delivered / opened / clicked / bounced).",
|
|
1334
|
-
inputSchema: obj(
|
|
1335
|
-
{
|
|
1336
|
-
email_id: str("Email ID returned from send_email or list_emails."),
|
|
1337
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1338
|
-
},
|
|
1339
|
-
["email_id"],
|
|
1340
|
-
),
|
|
1341
|
-
handler: async (args) => {
|
|
1342
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1343
|
-
const result = await api("GET", `/emails/${encodeURIComponent(args.email_id)}`, {
|
|
1344
|
-
workspace_id: ws,
|
|
1345
|
-
});
|
|
1346
|
-
return result;
|
|
1347
|
-
},
|
|
1348
|
-
},
|
|
1349
|
-
{
|
|
1350
|
-
name: "list_suppressions",
|
|
1351
|
-
description:
|
|
1352
|
-
"List all suppressed email addresses for the workspace — who is opted out and why (manual / unsubscribe / bounce / complaint).",
|
|
1353
|
-
inputSchema: obj(
|
|
1354
|
-
{
|
|
1355
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1356
|
-
},
|
|
1357
|
-
[],
|
|
1358
|
-
),
|
|
1359
|
-
handler: async (args) => {
|
|
1360
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1361
|
-
const result = await api("GET", "/emails/suppressions", { workspace_id: ws });
|
|
1362
|
-
return result;
|
|
1363
|
-
},
|
|
1364
|
-
},
|
|
1365
|
-
{
|
|
1366
|
-
name: "suppress_email",
|
|
1367
|
-
description:
|
|
1368
|
-
"Add an email address to the workspace suppression list so future sends skip it. Use for manual unsubscribes or policy blocks.",
|
|
1369
|
-
inputSchema: obj(
|
|
1370
|
-
{
|
|
1371
|
-
email: str("Email address to suppress."),
|
|
1372
|
-
reason: str(
|
|
1373
|
-
"Reason code: 'manual' | 'unsubscribe' | 'bounce' | 'complaint'. Default: 'manual'.",
|
|
1374
|
-
),
|
|
1375
|
-
source: str("Optional free-form provenance tag."),
|
|
1376
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1377
|
-
},
|
|
1378
|
-
["email"],
|
|
1379
|
-
),
|
|
1380
|
-
handler: async (args) => {
|
|
1381
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1382
|
-
const result = await api("POST", "/emails/suppressions", {
|
|
1383
|
-
body: {
|
|
1384
|
-
email: args.email,
|
|
1385
|
-
reason: args.reason ?? "manual",
|
|
1386
|
-
source: args.source ?? null,
|
|
1387
|
-
},
|
|
1388
|
-
workspace_id: ws,
|
|
1389
|
-
});
|
|
1390
|
-
return result;
|
|
1391
|
-
},
|
|
1392
|
-
},
|
|
1393
|
-
{
|
|
1394
|
-
name: "unsuppress_email",
|
|
1395
|
-
description:
|
|
1396
|
-
"Remove an email address from the workspace suppression list so future sends go through again.",
|
|
1397
|
-
inputSchema: obj(
|
|
1398
|
-
{
|
|
1399
|
-
email: str("Email address to un-suppress."),
|
|
1400
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1401
|
-
},
|
|
1402
|
-
["email"],
|
|
1403
|
-
),
|
|
1404
|
-
handler: async (args) => {
|
|
1405
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1406
|
-
const result = await api(
|
|
1407
|
-
"DELETE",
|
|
1408
|
-
`/emails/suppressions/${encodeURIComponent(args.email)}`,
|
|
1409
|
-
{ workspace_id: ws },
|
|
1410
|
-
);
|
|
1411
|
-
return result;
|
|
1412
|
-
},
|
|
1413
|
-
},
|
|
1414
|
-
// ===== Phase 4 — SMS tools =====
|
|
1415
|
-
|
|
1416
|
-
{
|
|
1417
|
-
name: "send_sms",
|
|
1418
|
-
description:
|
|
1419
|
-
"Send an SMS via the workspace's Twilio integration. Checks the SMS suppression list first (STOP keyword + carrier blocks + manual opt-outs) and skips with {suppressed: true} if the recipient has opted out. Example: send_sms({ to: '+15551234567', body: 'Your appointment is confirmed for Tuesday 2pm', contact_id: 'ctc_123' })",
|
|
1420
|
-
inputSchema: obj(
|
|
1421
|
-
{
|
|
1422
|
-
to: str("Recipient phone number. E.164 or 10-digit US will be normalized."),
|
|
1423
|
-
body: str("SMS body. Twilio will segment if over 160 chars; charges per segment."),
|
|
1424
|
-
contact_id: str("Optional. Links the message to a CRM contact for threading."),
|
|
1425
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1426
|
-
},
|
|
1427
|
-
["to", "body"],
|
|
1428
|
-
),
|
|
1429
|
-
handler: async (args) => {
|
|
1430
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1431
|
-
const result = await api("POST", "/sms", {
|
|
1432
|
-
body: {
|
|
1433
|
-
to: args.to,
|
|
1434
|
-
body: args.body,
|
|
1435
|
-
contactId: args.contact_id ?? null,
|
|
1436
|
-
},
|
|
1437
|
-
workspace_id: ws,
|
|
1438
|
-
});
|
|
1439
|
-
return result;
|
|
1440
|
-
},
|
|
1441
|
-
},
|
|
1442
|
-
{
|
|
1443
|
-
name: "list_sms",
|
|
1444
|
-
description:
|
|
1445
|
-
"List recent SMS messages (inbound + outbound) for the workspace, newest first.",
|
|
1446
|
-
inputSchema: obj(
|
|
1447
|
-
{
|
|
1448
|
-
limit: {
|
|
1449
|
-
type: "number",
|
|
1450
|
-
description: "Max rows to return (default 50, max 200).",
|
|
1451
|
-
},
|
|
1452
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1453
|
-
},
|
|
1454
|
-
[],
|
|
1455
|
-
),
|
|
1456
|
-
handler: async (args) => {
|
|
1457
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1458
|
-
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1459
|
-
const result = await api("GET", `/sms${qs}`, { workspace_id: ws });
|
|
1460
|
-
return result;
|
|
1461
|
-
},
|
|
1462
|
-
},
|
|
1463
|
-
{
|
|
1464
|
-
name: "get_sms",
|
|
1465
|
-
description:
|
|
1466
|
-
"Fetch a single SMS with its full provider-event history (queued / sent / delivered / failed / undelivered).",
|
|
1467
|
-
inputSchema: obj(
|
|
1468
|
-
{
|
|
1469
|
-
sms_id: str("SMS ID returned from send_sms or list_sms."),
|
|
1470
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1471
|
-
},
|
|
1472
|
-
["sms_id"],
|
|
1473
|
-
),
|
|
1474
|
-
handler: async (args) => {
|
|
1475
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1476
|
-
const result = await api("GET", `/sms/${encodeURIComponent(args.sms_id)}`, {
|
|
1477
|
-
workspace_id: ws,
|
|
1478
|
-
});
|
|
1479
|
-
return result;
|
|
1480
|
-
},
|
|
1481
|
-
},
|
|
1482
|
-
{
|
|
1483
|
-
name: "list_sms_suppressions",
|
|
1484
|
-
description:
|
|
1485
|
-
"List all suppressed phone numbers for the workspace — who is opted out and why (manual / stop_keyword / carrier_block / complaint).",
|
|
1486
|
-
inputSchema: obj(
|
|
1487
|
-
{
|
|
1488
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1489
|
-
},
|
|
1490
|
-
[],
|
|
1491
|
-
),
|
|
1492
|
-
handler: async (args) => {
|
|
1493
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1494
|
-
const result = await api("GET", "/sms/suppressions", { workspace_id: ws });
|
|
1495
|
-
return result;
|
|
1496
|
-
},
|
|
1497
|
-
},
|
|
1498
|
-
{
|
|
1499
|
-
name: "suppress_phone",
|
|
1500
|
-
description:
|
|
1501
|
-
"Add a phone number to the SMS suppression list so future SMS sends skip it. STOP replies + carrier permanent-failure codes auto-suppress via the Twilio webhook; use this for manual opt-outs.",
|
|
1502
|
-
inputSchema: obj(
|
|
1503
|
-
{
|
|
1504
|
-
phone: str("Phone number to suppress. E.164 or 10-digit US will be normalized."),
|
|
1505
|
-
reason: str(
|
|
1506
|
-
"Reason code: 'manual' | 'stop_keyword' | 'carrier_block' | 'complaint'. Default: 'manual'.",
|
|
1507
|
-
),
|
|
1508
|
-
source: str("Optional free-form provenance tag."),
|
|
1509
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1510
|
-
},
|
|
1511
|
-
["phone"],
|
|
1512
|
-
),
|
|
1513
|
-
handler: async (args) => {
|
|
1514
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1515
|
-
const result = await api("POST", "/sms/suppressions", {
|
|
1516
|
-
body: {
|
|
1517
|
-
phone: args.phone,
|
|
1518
|
-
reason: args.reason ?? "manual",
|
|
1519
|
-
source: args.source ?? null,
|
|
1520
|
-
},
|
|
1521
|
-
workspace_id: ws,
|
|
1522
|
-
});
|
|
1523
|
-
return result;
|
|
1524
|
-
},
|
|
1525
|
-
},
|
|
1526
|
-
{
|
|
1527
|
-
name: "unsuppress_phone",
|
|
1528
|
-
description:
|
|
1529
|
-
"Remove a phone number from the SMS suppression list so future sends go through again.",
|
|
1530
|
-
inputSchema: obj(
|
|
1531
|
-
{
|
|
1532
|
-
phone: str("Phone number to un-suppress."),
|
|
1533
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1534
|
-
},
|
|
1535
|
-
["phone"],
|
|
1536
|
-
),
|
|
1537
|
-
handler: async (args) => {
|
|
1538
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1539
|
-
const result = await api(
|
|
1540
|
-
"DELETE",
|
|
1541
|
-
`/sms/suppressions/${encodeURIComponent(args.phone)}`,
|
|
1542
|
-
{ workspace_id: ws },
|
|
1543
|
-
);
|
|
1544
|
-
return result;
|
|
1545
|
-
},
|
|
1546
|
-
},
|
|
1547
|
-
|
|
1548
|
-
// ===== Phase 5 — Payments tools (Stripe Connect Standard) =====
|
|
1549
|
-
|
|
1550
|
-
{
|
|
1551
|
-
name: "create_invoice",
|
|
1552
|
-
description:
|
|
1553
|
-
"Draft a Stripe invoice on the workspace's connected Stripe account. Invoice is created but not sent — call send_invoice separately so agents can review before dispatch. Contact must have an email. Example: create_invoice({ contact_id: 'ctc_123', items: [{ description: '1 hr consulting', quantity: 1, unit_amount: 200 }], due_at: '2026-05-21T00:00:00Z' })",
|
|
1554
|
-
inputSchema: obj(
|
|
1555
|
-
{
|
|
1556
|
-
contact_id: str("CRM contact to bill."),
|
|
1557
|
-
items: {
|
|
1558
|
-
type: "array",
|
|
1559
|
-
description: "Line items. Each: {description, quantity, unit_amount} (unit_amount in the workspace's currency).",
|
|
1560
|
-
items: { type: "object" },
|
|
1561
|
-
},
|
|
1562
|
-
currency: str("3-letter ISO currency code. Defaults to USD."),
|
|
1563
|
-
due_at: str("ISO timestamp for invoice due date. Defaults to 30 days out."),
|
|
1564
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1565
|
-
},
|
|
1566
|
-
["contact_id", "items"],
|
|
1567
|
-
),
|
|
1568
|
-
handler: async (args) => {
|
|
1569
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1570
|
-
const normalizedItems = (args.items ?? []).map((item) => ({
|
|
1571
|
-
description: item.description,
|
|
1572
|
-
quantity: item.quantity ?? 1,
|
|
1573
|
-
unitAmount: item.unit_amount ?? item.unitAmount,
|
|
1574
|
-
currency: item.currency,
|
|
1575
|
-
}));
|
|
1576
|
-
const result = await api("POST", "/invoices", {
|
|
1577
|
-
body: {
|
|
1578
|
-
contactId: args.contact_id,
|
|
1579
|
-
items: normalizedItems,
|
|
1580
|
-
currency: args.currency ?? null,
|
|
1581
|
-
dueAt: args.due_at ?? null,
|
|
1582
|
-
},
|
|
1583
|
-
workspace_id: ws,
|
|
1584
|
-
});
|
|
1585
|
-
return result;
|
|
1586
|
-
},
|
|
1587
|
-
},
|
|
1588
|
-
{
|
|
1589
|
-
name: "list_invoices",
|
|
1590
|
-
description:
|
|
1591
|
-
"List workspace invoices (draft + sent + paid + past_due + voided), newest first.",
|
|
1592
|
-
inputSchema: obj(
|
|
1593
|
-
{
|
|
1594
|
-
limit: {
|
|
1595
|
-
type: "number",
|
|
1596
|
-
description: "Max rows (default 50, max 200).",
|
|
1597
|
-
},
|
|
1598
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1599
|
-
},
|
|
1600
|
-
[],
|
|
1601
|
-
),
|
|
1602
|
-
handler: async (args) => {
|
|
1603
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1604
|
-
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1605
|
-
return api("GET", `/invoices${qs}`, { workspace_id: ws });
|
|
1606
|
-
},
|
|
1607
|
-
},
|
|
1608
|
-
{
|
|
1609
|
-
name: "get_invoice",
|
|
1610
|
-
description:
|
|
1611
|
-
"Fetch an invoice + its line items + hosted invoice URL (for payment).",
|
|
1612
|
-
inputSchema: obj(
|
|
1613
|
-
{
|
|
1614
|
-
invoice_id: str("Invoice ID returned from create_invoice or list_invoices."),
|
|
1615
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1616
|
-
},
|
|
1617
|
-
["invoice_id"],
|
|
1618
|
-
),
|
|
1619
|
-
handler: async (args) => {
|
|
1620
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1621
|
-
return api("GET", `/invoices/${encodeURIComponent(args.invoice_id)}`, { workspace_id: ws });
|
|
1622
|
-
},
|
|
1623
|
-
},
|
|
1624
|
-
{
|
|
1625
|
-
name: "send_invoice",
|
|
1626
|
-
description:
|
|
1627
|
-
"Dispatch a draft invoice to the contact via Stripe (Stripe emails the invoice + provides a hosted pay page).",
|
|
1628
|
-
inputSchema: obj(
|
|
1629
|
-
{
|
|
1630
|
-
invoice_id: str("Invoice to send."),
|
|
1631
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1632
|
-
},
|
|
1633
|
-
["invoice_id"],
|
|
1634
|
-
),
|
|
1635
|
-
handler: async (args) => {
|
|
1636
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1637
|
-
return api("POST", `/invoices/${encodeURIComponent(args.invoice_id)}/send`, { workspace_id: ws });
|
|
1638
|
-
},
|
|
1639
|
-
},
|
|
1640
|
-
{
|
|
1641
|
-
name: "void_invoice",
|
|
1642
|
-
description:
|
|
1643
|
-
"Void an invoice (undo a billing error). Only valid for draft / open invoices; paid invoices must be refunded instead.",
|
|
1644
|
-
inputSchema: obj(
|
|
1645
|
-
{
|
|
1646
|
-
invoice_id: str("Invoice to void."),
|
|
1647
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1648
|
-
},
|
|
1649
|
-
["invoice_id"],
|
|
1650
|
-
),
|
|
1651
|
-
handler: async (args) => {
|
|
1652
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1653
|
-
return api("POST", `/invoices/${encodeURIComponent(args.invoice_id)}/void`, { workspace_id: ws });
|
|
1654
|
-
},
|
|
1655
|
-
},
|
|
1656
|
-
{
|
|
1657
|
-
name: "create_subscription",
|
|
1658
|
-
description:
|
|
1659
|
-
"Start a recurring subscription for a contact against a Stripe Price id. The Price must already exist in the workspace's Stripe dashboard — v1 does not create Prices. Example: create_subscription({ contact_id: 'ctc_123', price_id: 'price_1ABCxyz', trial_days: 14 })",
|
|
1660
|
-
inputSchema: obj(
|
|
1661
|
-
{
|
|
1662
|
-
contact_id: str("CRM contact to subscribe."),
|
|
1663
|
-
price_id: str("Stripe Price id (e.g., 'price_1ABC...') from the workspace's Stripe dashboard."),
|
|
1664
|
-
trial_days: {
|
|
1665
|
-
type: "number",
|
|
1666
|
-
description: "Optional free trial days before first charge.",
|
|
1667
|
-
},
|
|
1668
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1669
|
-
},
|
|
1670
|
-
["contact_id", "price_id"],
|
|
1671
|
-
),
|
|
1672
|
-
handler: async (args) => {
|
|
1673
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1674
|
-
return api("POST", "/subscriptions", {
|
|
1675
|
-
body: {
|
|
1676
|
-
contactId: args.contact_id,
|
|
1677
|
-
priceId: args.price_id,
|
|
1678
|
-
trialDays: args.trial_days,
|
|
1679
|
-
},
|
|
1680
|
-
workspace_id: ws,
|
|
1681
|
-
});
|
|
1682
|
-
},
|
|
1683
|
-
},
|
|
1684
|
-
{
|
|
1685
|
-
name: "list_subscriptions",
|
|
1686
|
-
description:
|
|
1687
|
-
"List workspace subscriptions (active + trialing + past_due + canceled), newest first.",
|
|
1688
|
-
inputSchema: obj(
|
|
1689
|
-
{
|
|
1690
|
-
limit: {
|
|
1691
|
-
type: "number",
|
|
1692
|
-
description: "Max rows (default 50, max 200).",
|
|
1693
|
-
},
|
|
1694
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1695
|
-
},
|
|
1696
|
-
[],
|
|
1697
|
-
),
|
|
1698
|
-
handler: async (args) => {
|
|
1699
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1700
|
-
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1701
|
-
return api("GET", `/subscriptions${qs}`, { workspace_id: ws });
|
|
1702
|
-
},
|
|
1703
|
-
},
|
|
1704
|
-
{
|
|
1705
|
-
name: "cancel_subscription",
|
|
1706
|
-
description:
|
|
1707
|
-
"Cancel a subscription. Default: cancel at period end (contact keeps access until renewal date). Pass immediate=true for an instant termination + prorated refund.",
|
|
1708
|
-
inputSchema: obj(
|
|
1709
|
-
{
|
|
1710
|
-
subscription_id: str("Subscription to cancel."),
|
|
1711
|
-
immediate: {
|
|
1712
|
-
type: "boolean",
|
|
1713
|
-
description: "If true, terminate now. Default: cancel at period end.",
|
|
1714
|
-
},
|
|
1715
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1716
|
-
},
|
|
1717
|
-
["subscription_id"],
|
|
1718
|
-
),
|
|
1719
|
-
handler: async (args) => {
|
|
1720
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1721
|
-
return api("POST", `/subscriptions/${encodeURIComponent(args.subscription_id)}/cancel`, {
|
|
1722
|
-
body: { immediate: Boolean(args.immediate) },
|
|
1723
|
-
workspace_id: ws,
|
|
1724
|
-
});
|
|
1725
|
-
},
|
|
1726
|
-
},
|
|
1727
|
-
{
|
|
1728
|
-
name: "list_payments",
|
|
1729
|
-
description:
|
|
1730
|
-
"List recent payments (completed + failed + refunded + disputed) across the workspace, newest first.",
|
|
1731
|
-
inputSchema: obj(
|
|
1732
|
-
{
|
|
1733
|
-
limit: {
|
|
1734
|
-
type: "number",
|
|
1735
|
-
description: "Max rows (default 50, max 200).",
|
|
1736
|
-
},
|
|
1737
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1738
|
-
},
|
|
1739
|
-
[],
|
|
1740
|
-
),
|
|
1741
|
-
handler: async (args) => {
|
|
1742
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1743
|
-
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1744
|
-
return api("GET", `/payments${qs}`, { workspace_id: ws });
|
|
1745
|
-
},
|
|
1746
|
-
},
|
|
1747
|
-
{
|
|
1748
|
-
name: "get_payment",
|
|
1749
|
-
description:
|
|
1750
|
-
"Fetch a single payment record with status + refund/dispute state.",
|
|
1751
|
-
inputSchema: obj(
|
|
1752
|
-
{
|
|
1753
|
-
payment_id: str("Payment ID from list_payments."),
|
|
1754
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1755
|
-
},
|
|
1756
|
-
["payment_id"],
|
|
1757
|
-
),
|
|
1758
|
-
handler: async (args) => {
|
|
1759
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1760
|
-
return api("GET", `/payments/${encodeURIComponent(args.payment_id)}`, { workspace_id: ws });
|
|
1761
|
-
},
|
|
1762
|
-
},
|
|
1763
|
-
{
|
|
1764
|
-
name: "refund_payment",
|
|
1765
|
-
description:
|
|
1766
|
-
"Refund a payment. Omit amount to refund the full payment; pass amount for a partial refund. reason should be 'duplicate' | 'fraudulent' | 'requested_by_customer'.",
|
|
1767
|
-
inputSchema: obj(
|
|
1768
|
-
{
|
|
1769
|
-
payment_id: str("Payment to refund."),
|
|
1770
|
-
amount: {
|
|
1771
|
-
type: "number",
|
|
1772
|
-
description: "Optional partial-refund amount in the payment's currency. Omit to refund in full.",
|
|
1773
|
-
},
|
|
1774
|
-
reason: str("'duplicate' | 'fraudulent' | 'requested_by_customer'. Default: requested_by_customer."),
|
|
1775
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1776
|
-
},
|
|
1777
|
-
["payment_id"],
|
|
1778
|
-
),
|
|
1779
|
-
handler: async (args) => {
|
|
1780
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1781
|
-
return api("POST", `/payments/${encodeURIComponent(args.payment_id)}/refund`, {
|
|
1782
|
-
body: {
|
|
1783
|
-
amount: args.amount,
|
|
1784
|
-
reason: args.reason ?? "requested_by_customer",
|
|
1785
|
-
},
|
|
1786
|
-
workspace_id: ws,
|
|
1787
|
-
});
|
|
1788
|
-
},
|
|
1789
|
-
},
|
|
1790
|
-
|
|
1791
|
-
// ===== Phase 6 — Landing Pages tools =====
|
|
1792
|
-
|
|
1793
|
-
{
|
|
1794
|
-
name: "list_landing_pages",
|
|
1795
|
-
description:
|
|
1796
|
-
"List the workspace's landing pages (draft + published), newest-updated first.",
|
|
1797
|
-
inputSchema: obj(
|
|
1798
|
-
{
|
|
1799
|
-
limit: { type: "number", description: "Max rows (default 50, max 200)." },
|
|
1800
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1801
|
-
},
|
|
1802
|
-
[],
|
|
1803
|
-
),
|
|
1804
|
-
handler: async (args) => {
|
|
1805
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1806
|
-
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1807
|
-
return api("GET", `/landing${qs}`, { workspace_id: ws });
|
|
1808
|
-
},
|
|
1809
|
-
},
|
|
1810
|
-
{
|
|
1811
|
-
name: "get_landing_page",
|
|
1812
|
-
description:
|
|
1813
|
-
"Fetch a single landing page with its full Puck payload + metadata.",
|
|
1814
|
-
inputSchema: obj(
|
|
1815
|
-
{
|
|
1816
|
-
page_id: str("Landing page ID from list_landing_pages."),
|
|
1817
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1818
|
-
},
|
|
1819
|
-
["page_id"],
|
|
1820
|
-
),
|
|
1821
|
-
handler: async (args) => {
|
|
1822
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1823
|
-
return api("GET", `/landing/${encodeURIComponent(args.page_id)}`, { workspace_id: ws });
|
|
1824
|
-
},
|
|
1825
|
-
},
|
|
1826
|
-
{
|
|
1827
|
-
name: "create_landing_page",
|
|
1828
|
-
description:
|
|
1829
|
-
"Create a landing page from an optional Puck payload. Without puck_data, creates a blank draft. With puck_data, validates the payload against the Puck schema and rejects on mismatch. Set published=true to publish immediately.",
|
|
1830
|
-
inputSchema: obj(
|
|
1831
|
-
{
|
|
1832
|
-
title: str("Page title (used for the dashboard; not the public URL)."),
|
|
1833
|
-
slug: str("Optional URL slug. Derived from title if omitted."),
|
|
1834
|
-
puck_data: {
|
|
1835
|
-
type: "object",
|
|
1836
|
-
description: "Optional Puck payload { content: [], root: {props}, zones: {} }.",
|
|
1837
|
-
},
|
|
1838
|
-
published: {
|
|
1839
|
-
type: "boolean",
|
|
1840
|
-
description: "If true, publish the page immediately. Default: draft.",
|
|
1841
|
-
},
|
|
1842
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1843
|
-
},
|
|
1844
|
-
["title"],
|
|
1845
|
-
),
|
|
1846
|
-
handler: async (args) => {
|
|
1847
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1848
|
-
return api("POST", "/landing", {
|
|
1849
|
-
body: {
|
|
1850
|
-
title: args.title,
|
|
1851
|
-
slug: args.slug,
|
|
1852
|
-
puckData: args.puck_data,
|
|
1853
|
-
published: Boolean(args.published),
|
|
1854
|
-
},
|
|
1855
|
-
workspace_id: ws,
|
|
1856
|
-
});
|
|
1857
|
-
},
|
|
1858
|
-
},
|
|
1859
|
-
{
|
|
1860
|
-
name: "update_landing_page",
|
|
1861
|
-
description:
|
|
1862
|
-
"Update a landing page's title and/or Puck payload. Validates puck_data on the way through. Does not change publish status — use publish_landing_page for that.",
|
|
1863
|
-
inputSchema: obj(
|
|
1864
|
-
{
|
|
1865
|
-
page_id: str("Landing page to update."),
|
|
1866
|
-
title: str("Optional new title."),
|
|
1867
|
-
puck_data: {
|
|
1868
|
-
type: "object",
|
|
1869
|
-
description: "Optional new Puck payload. Pass null to clear.",
|
|
1870
|
-
},
|
|
1871
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1872
|
-
},
|
|
1873
|
-
["page_id"],
|
|
1874
|
-
),
|
|
1875
|
-
handler: async (args) => {
|
|
1876
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1877
|
-
const bodyObj = {};
|
|
1878
|
-
if (typeof args.title === "string") bodyObj.title = args.title;
|
|
1879
|
-
if (args.puck_data !== undefined) bodyObj.puckData = args.puck_data;
|
|
1880
|
-
return api("PATCH", `/landing/${encodeURIComponent(args.page_id)}`, {
|
|
1881
|
-
body: bodyObj,
|
|
1882
|
-
workspace_id: ws,
|
|
1883
|
-
});
|
|
1884
|
-
},
|
|
1885
|
-
},
|
|
1886
|
-
{
|
|
1887
|
-
name: "publish_landing_page",
|
|
1888
|
-
description:
|
|
1889
|
-
"Flip a landing page between draft and published. Publishing busts the public-URL cache immediately and emits landing.published. Pass published=false to unpublish.",
|
|
1890
|
-
inputSchema: obj(
|
|
1891
|
-
{
|
|
1892
|
-
page_id: str("Landing page to publish."),
|
|
1893
|
-
published: {
|
|
1894
|
-
type: "boolean",
|
|
1895
|
-
description: "true = publish (default), false = unpublish.",
|
|
1896
|
-
},
|
|
1897
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1898
|
-
},
|
|
1899
|
-
["page_id"],
|
|
1900
|
-
),
|
|
1901
|
-
handler: async (args) => {
|
|
1902
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1903
|
-
return api("POST", `/landing/${encodeURIComponent(args.page_id)}/publish`, {
|
|
1904
|
-
body: { published: args.published !== false },
|
|
1905
|
-
workspace_id: ws,
|
|
1906
|
-
});
|
|
1907
|
-
},
|
|
1908
|
-
},
|
|
1909
|
-
{
|
|
1910
|
-
name: "list_landing_templates",
|
|
1911
|
-
description:
|
|
1912
|
-
"List the pre-built vertical landing-page templates. Each has a validated Puck payload ready to seed a new page via create_landing_page({puck_data: template.payload}).",
|
|
1913
|
-
inputSchema: obj(
|
|
1914
|
-
{
|
|
1915
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1916
|
-
},
|
|
1917
|
-
[],
|
|
1918
|
-
),
|
|
1919
|
-
handler: async (args) => {
|
|
1920
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1921
|
-
return api("GET", "/landing/templates", { workspace_id: ws });
|
|
1922
|
-
},
|
|
1923
|
-
},
|
|
1924
|
-
{
|
|
1925
|
-
name: "get_landing_template",
|
|
1926
|
-
description:
|
|
1927
|
-
"Fetch a single landing-page template including its Puck payload. Pair with create_landing_page to seed a new page from the template.",
|
|
1928
|
-
inputSchema: obj(
|
|
1929
|
-
{
|
|
1930
|
-
template_id: str("Template ID from list_landing_templates."),
|
|
1931
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1932
|
-
},
|
|
1933
|
-
["template_id"],
|
|
1934
|
-
),
|
|
1935
|
-
handler: async (args) => {
|
|
1936
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1937
|
-
return api("GET", `/landing/templates/${encodeURIComponent(args.template_id)}`, { workspace_id: ws });
|
|
1938
|
-
},
|
|
1939
|
-
},
|
|
1940
|
-
{
|
|
1941
|
-
name: "generate_landing_page",
|
|
1942
|
-
description:
|
|
1943
|
-
"Generate a Puck landing-page payload from a natural-language prompt using Claude + the workspace's Soul + theme. Returns the payload (validated against the Puck schema) but does NOT persist — pair with create_landing_page to save the result. Example: generate_landing_page({ prompt: 'A landing for a Laval dental clinic, focus on new-patient consultations' })",
|
|
1944
|
-
inputSchema: obj(
|
|
1945
|
-
{
|
|
1946
|
-
prompt: str("One-sentence page description. The more specific, the better."),
|
|
1947
|
-
existing: {
|
|
1948
|
-
type: "object",
|
|
1949
|
-
description: "Optional existing Puck payload to revise rather than start fresh.",
|
|
1950
|
-
},
|
|
1951
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1952
|
-
},
|
|
1953
|
-
["prompt"],
|
|
1954
|
-
),
|
|
1955
|
-
handler: async (args) => {
|
|
1956
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1957
|
-
return api("POST", "/landing/generate", {
|
|
1958
|
-
body: {
|
|
1959
|
-
prompt: args.prompt,
|
|
1960
|
-
existing: args.existing,
|
|
1961
|
-
},
|
|
1962
|
-
workspace_id: ws,
|
|
1963
|
-
});
|
|
1964
|
-
},
|
|
1965
|
-
},
|
|
1966
|
-
|
|
1967
|
-
{
|
|
1968
|
-
name: "send_conversation_turn",
|
|
1969
|
-
description:
|
|
1970
|
-
"Route an incoming message through the Conversation Primitive runtime. Loads prior turns for (contact, channel), generates a Soul-aware reply with Claude, writes both inbound + outbound turns, and emits conversation.turn.received / sent events. Use when building an always-on conversational agent (speed-to-lead, qualification chatbot). Example: send_conversation_turn({ contact_id: 'ctc_123', channel: 'sms', message: 'Do you have Saturday appointments?' })",
|
|
1971
|
-
inputSchema: obj(
|
|
1972
|
-
{
|
|
1973
|
-
contact_id: str("CRM contact to converse with."),
|
|
1974
|
-
channel: str("Transport channel: 'email' | 'sms'."),
|
|
1975
|
-
message: str("Incoming message content to reason about."),
|
|
1976
|
-
conversation_id: str(
|
|
1977
|
-
"Optional existing conversation id. Omit to let the runtime reuse the most recent active thread or open a new one.",
|
|
1978
|
-
),
|
|
1979
|
-
subject: str("Optional subject for email threads."),
|
|
1980
|
-
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1981
|
-
},
|
|
1982
|
-
["contact_id", "channel", "message"],
|
|
1983
|
-
),
|
|
1984
|
-
handler: async (args) => {
|
|
1985
|
-
const ws = wsOrDefault(args.workspace_id);
|
|
1986
|
-
const result = await api("POST", "/conversations/turn", {
|
|
1987
|
-
body: {
|
|
1988
|
-
contactId: args.contact_id,
|
|
1989
|
-
channel: args.channel,
|
|
1990
|
-
message: args.message,
|
|
1991
|
-
conversationId: args.conversation_id ?? null,
|
|
1992
|
-
subject: args.subject ?? null,
|
|
1993
|
-
},
|
|
1994
|
-
workspace_id: ws,
|
|
1995
|
-
});
|
|
1996
|
-
return result;
|
|
1997
|
-
},
|
|
1998
|
-
},
|
|
1999
|
-
];
|
|
2000
|
-
|
|
2001
|
-
export const TOOL_MAP = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
|
|
1
|
+
import {
|
|
2
|
+
api,
|
|
3
|
+
fetchText,
|
|
4
|
+
forgetWorkspace,
|
|
5
|
+
htmlToText,
|
|
6
|
+
rememberWorkspace,
|
|
7
|
+
setDefaultWorkspace,
|
|
8
|
+
getDefaultWorkspace,
|
|
9
|
+
getWorkspaceBearer,
|
|
10
|
+
getApiKey,
|
|
11
|
+
knownWorkspaceIds,
|
|
12
|
+
hasApiKey,
|
|
13
|
+
isFirstEverCall,
|
|
14
|
+
} from "./client.js";
|
|
15
|
+
import { FIRST_CALL_BANNER } from "./welcome.js";
|
|
16
|
+
|
|
17
|
+
const str = (description, extra = {}) => ({ type: "string", description, ...extra });
|
|
18
|
+
const obj = (properties, required = []) => ({
|
|
19
|
+
type: "object",
|
|
20
|
+
properties,
|
|
21
|
+
required,
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function withFirstCallBanner(payload) {
|
|
26
|
+
if (!isFirstEverCall()) return payload;
|
|
27
|
+
return { ...payload, _welcome: FIRST_CALL_BANNER };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function wsOrDefault(workspace_id) {
|
|
31
|
+
const id = workspace_id ?? getDefaultWorkspace();
|
|
32
|
+
if (!id) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"No workspace selected. Run create_workspace({ name: '…' }) first, or pass workspace_id.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const TOOLS = [
|
|
41
|
+
{
|
|
42
|
+
name: "create_workspace",
|
|
43
|
+
description:
|
|
44
|
+
"Create a real, hosted workspace on <slug>.app.seldonframe.com with CRM, Cal.diy booking, Formbricks intake, and Brain v2 pre-installed. The first workspace requires no API key. Example: create_workspace({ name: 'Dental Clinic Laval', source: 'https://mysite.com' })",
|
|
45
|
+
inputSchema: obj(
|
|
46
|
+
{
|
|
47
|
+
name: str("Human-readable workspace name."),
|
|
48
|
+
source: str("Optional URL or description to seed the workspace's Soul from."),
|
|
49
|
+
},
|
|
50
|
+
["name"],
|
|
51
|
+
),
|
|
52
|
+
handler: async (args) => {
|
|
53
|
+
const firstEver = isFirstEverCall();
|
|
54
|
+
const result = await api("POST", "/workspace/create", {
|
|
55
|
+
body: { name: args.name, source: args.source ?? null },
|
|
56
|
+
allow_anonymous: true,
|
|
57
|
+
});
|
|
58
|
+
const ws = result.workspace ?? result;
|
|
59
|
+
const id = ws.id;
|
|
60
|
+
if (!id) throw new Error("Server did not return a workspace id.");
|
|
61
|
+
if (result.bearer_token) {
|
|
62
|
+
rememberWorkspace({ workspace_id: id, bearer_token: result.bearer_token });
|
|
63
|
+
} else {
|
|
64
|
+
setDefaultWorkspace(id);
|
|
65
|
+
}
|
|
66
|
+
const payload = {
|
|
67
|
+
ok: true,
|
|
68
|
+
workspace: {
|
|
69
|
+
id,
|
|
70
|
+
name: ws.name,
|
|
71
|
+
slug: ws.slug,
|
|
72
|
+
tier: ws.tier ?? "free",
|
|
73
|
+
created_at: ws.created_at,
|
|
74
|
+
},
|
|
75
|
+
urls: result.urls ?? ws.urls ?? null,
|
|
76
|
+
installed: result.installed ?? ["crm", "caldiy-booking", "formbricks-intake", "brain-v2"],
|
|
77
|
+
next: [
|
|
78
|
+
"install_vertical_pack({ pack: 'real-estate' }) // or 'dental', 'legal'",
|
|
79
|
+
"fetch_source_for_soul({ url: 'https://yoursite.com' }) → submit_soul({ soul })",
|
|
80
|
+
"get_workspace_snapshot({}) — read workspace state to reason about next steps",
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
return firstEver ? withFirstCallBanner(payload) : payload;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "list_workspaces",
|
|
88
|
+
description: "List all workspaces known to this device (plus any Pro workspaces if SELDONFRAME_API_KEY is set).",
|
|
89
|
+
inputSchema: obj({}),
|
|
90
|
+
handler: async () => {
|
|
91
|
+
const local = knownWorkspaceIds();
|
|
92
|
+
const data = await api("GET", "/workspaces", { allow_anonymous: true });
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
default_workspace: getDefaultWorkspace(),
|
|
96
|
+
device_known: local,
|
|
97
|
+
workspaces: data.workspaces ?? data,
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "switch_workspace",
|
|
103
|
+
description: "Set the active workspace. Subsequent tool calls act on it by default.",
|
|
104
|
+
inputSchema: obj({ workspace_id: str("Target workspace id.") }, ["workspace_id"]),
|
|
105
|
+
handler: async ({ workspace_id }) => {
|
|
106
|
+
setDefaultWorkspace(workspace_id);
|
|
107
|
+
return { ok: true, default_workspace: workspace_id };
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "clone_workspace",
|
|
112
|
+
description:
|
|
113
|
+
"Clone an existing workspace as a template. Example: clone_workspace({ source_workspace_id: 'wsp_x', name: 'Copy' })",
|
|
114
|
+
inputSchema: obj(
|
|
115
|
+
{
|
|
116
|
+
source_workspace_id: str("Workspace to clone from."),
|
|
117
|
+
name: str("Name for the new workspace."),
|
|
118
|
+
},
|
|
119
|
+
["source_workspace_id", "name"],
|
|
120
|
+
),
|
|
121
|
+
handler: async (a) => {
|
|
122
|
+
const result = await api(
|
|
123
|
+
"POST",
|
|
124
|
+
`/workspaces/${encodeURIComponent(a.source_workspace_id)}/clone`,
|
|
125
|
+
{ body: { name: a.name }, workspace_id: a.source_workspace_id },
|
|
126
|
+
);
|
|
127
|
+
const id = result.workspace?.id ?? result.id;
|
|
128
|
+
if (id && result.bearer_token) {
|
|
129
|
+
rememberWorkspace({ workspace_id: id, bearer_token: result.bearer_token });
|
|
130
|
+
}
|
|
131
|
+
return { ok: true, ...result };
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "link_workspace_owner",
|
|
136
|
+
description:
|
|
137
|
+
"Claim an anonymously-created workspace under your real account. After linking, the admin URLs (dashboard, contacts, deals) become usable once you sign in at app.seldonframe.com. Requires SELDONFRAME_API_KEY to be set in the MCP environment. The workspace bearer token continues to work — no rotation needed. Example: link_workspace_owner({}) to claim the active workspace.",
|
|
138
|
+
inputSchema: obj({
|
|
139
|
+
workspace_id: str(
|
|
140
|
+
"Optional workspace id to claim. Defaults to the active workspace from this device."
|
|
141
|
+
),
|
|
142
|
+
}),
|
|
143
|
+
handler: async (a) => {
|
|
144
|
+
const workspaceId = a.workspace_id ?? getDefaultWorkspace();
|
|
145
|
+
if (!workspaceId) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"No workspace to link. Run create_workspace first, or pass workspace_id."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const bearer = getWorkspaceBearer(workspaceId);
|
|
151
|
+
if (!bearer) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`No local bearer token for workspace ${workspaceId}. This device did not create it. Re-run create_workspace or switch to the device that did.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const apiKey = getApiKey();
|
|
157
|
+
if (!apiKey) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"Linking an owner requires SELDONFRAME_API_KEY. Get one at https://app.seldonframe.com/settings/api, then `export SELDONFRAME_API_KEY=sk-…` and restart the MCP server."
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const result = await api(
|
|
163
|
+
"POST",
|
|
164
|
+
`/workspace/${encodeURIComponent(workspaceId)}/link-owner`,
|
|
165
|
+
{
|
|
166
|
+
body: {},
|
|
167
|
+
workspace_id: workspaceId,
|
|
168
|
+
force_workspace_bearer: true,
|
|
169
|
+
extra_headers: { "x-seldon-api-key": apiKey },
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
const magicLink = result?.urls?.claim_magic_link ?? null;
|
|
173
|
+
const baseNote = result.already_linked
|
|
174
|
+
? "This workspace was already linked to your account."
|
|
175
|
+
: "Workspace linked to your account.";
|
|
176
|
+
const magicNote = magicLink
|
|
177
|
+
? ` A one-click sign-in link is in urls.claim_magic_link — opens a browser session as the workspace owner, expires in 15 min, single-use.`
|
|
178
|
+
: " No magic link minted (user has no email on file); sign in the normal way at urls.admin_dashboard.";
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
...result,
|
|
182
|
+
note: `${baseNote}${magicNote} Your MCP bearer token continues to work — no rotation needed.`,
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "revoke_bearer",
|
|
188
|
+
description:
|
|
189
|
+
"Revoke workspace bearer tokens. Useful if a device token has leaked or if a builder wants to rotate. Modes (pick exactly one): `{}` revokes ALL tokens except the current device's (safe default — other devices kicked off, this device keeps working); `{ token_id }` revokes a specific token by its UUID; `{ all: true }` revokes every token including the current one — requires SELDONFRAME_API_KEY because it locks this device out. After revoking the current token the MCP clears the local entry from ~/.seldonframe/device.json.",
|
|
190
|
+
inputSchema: obj({
|
|
191
|
+
workspace_id: str("Optional workspace override. Defaults to active workspace."),
|
|
192
|
+
token_id: str("UUID of a specific token to revoke (from api_keys.id)."),
|
|
193
|
+
all: { type: "boolean", description: "Revoke ALL tokens including caller. Requires SELDONFRAME_API_KEY." },
|
|
194
|
+
}),
|
|
195
|
+
handler: async (a) => {
|
|
196
|
+
const workspaceId = a.workspace_id ?? getDefaultWorkspace();
|
|
197
|
+
if (!workspaceId) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"No workspace to revoke tokens for. Run create_workspace first, or pass workspace_id."
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const bearer = getWorkspaceBearer(workspaceId);
|
|
203
|
+
const apiKey = getApiKey();
|
|
204
|
+
if (!bearer && !apiKey) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`No local bearer for workspace ${workspaceId} and no SELDONFRAME_API_KEY. Cannot authenticate.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (a.all === true && !apiKey) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
"Revoking ALL tokens (including this device's) requires SELDONFRAME_API_KEY — bearer identity can't lock itself out. Either omit `all` to use all_except_current, or set SELDONFRAME_API_KEY."
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let body;
|
|
216
|
+
if (a.token_id) {
|
|
217
|
+
body = { token_id: a.token_id };
|
|
218
|
+
} else if (a.all === true) {
|
|
219
|
+
body = { all: true };
|
|
220
|
+
} else {
|
|
221
|
+
body = { all_except_current: true };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Prefer workspace bearer when present; fall back to api_key auth otherwise.
|
|
225
|
+
const useBearer = Boolean(bearer);
|
|
226
|
+
const result = await api(
|
|
227
|
+
"POST",
|
|
228
|
+
`/workspace/${encodeURIComponent(workspaceId)}/revoke-bearer`,
|
|
229
|
+
{
|
|
230
|
+
body,
|
|
231
|
+
workspace_id: workspaceId,
|
|
232
|
+
force_workspace_bearer: useBearer,
|
|
233
|
+
extra_headers: apiKey && !useBearer ? { "x-seldon-api-key": apiKey } : {},
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// If the caller's own token got revoked, clear it from device.json so
|
|
238
|
+
// future tool calls don't authenticate with a dead token.
|
|
239
|
+
if (useBearer && result?.caller_still_valid === false) {
|
|
240
|
+
forgetWorkspace(workspaceId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
...result,
|
|
246
|
+
device_cleared: useBearer && result?.caller_still_valid === false,
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "update_landing_content",
|
|
252
|
+
description:
|
|
253
|
+
"Rewrite the workspace's public landing page at / — headline, subhead, and primary CTA label. YOU decide the copy based on the user's request + the workspace Soul; this tool persists it.",
|
|
254
|
+
inputSchema: obj(
|
|
255
|
+
{
|
|
256
|
+
headline: str("Main hero heading. Keep short; 1 line."),
|
|
257
|
+
subhead: str("One-sentence supporting line under the headline."),
|
|
258
|
+
cta_label: str("Primary call-to-action button text, e.g. 'Book a call'."),
|
|
259
|
+
workspace_id: str("Optional workspace override."),
|
|
260
|
+
},
|
|
261
|
+
["headline", "subhead", "cta_label"],
|
|
262
|
+
),
|
|
263
|
+
handler: async (a) => {
|
|
264
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
265
|
+
return api("POST", "/landing/update", {
|
|
266
|
+
body: {
|
|
267
|
+
headline: a.headline,
|
|
268
|
+
subhead: a.subhead,
|
|
269
|
+
cta_label: a.cta_label,
|
|
270
|
+
workspace_id: ws,
|
|
271
|
+
},
|
|
272
|
+
workspace_id: ws,
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
// Note: `customize_intake_form` used to live here pointing at POST
|
|
277
|
+
// /api/v1/intake/customize. Phase 2.d unified it under update_form; the
|
|
278
|
+
// alias that preserves the old name now lives in the Phase 2.d block at
|
|
279
|
+
// the bottom of this file. The POST /intake/customize endpoint still
|
|
280
|
+
// exists server-side for backwards compatibility until Phase 11 cleanup.
|
|
281
|
+
// Note: `configure_booking` used to live here pointing at POST
|
|
282
|
+
// /api/v1/booking/configure. Phase 2.c unified it under
|
|
283
|
+
// update_appointment_type; the alias that preserves the old name now
|
|
284
|
+
// lives at the bottom of this file (end of Phase 2.c block). The POST
|
|
285
|
+
// /booking/configure endpoint still exists server-side for backwards
|
|
286
|
+
// compatibility until Phase 11 cleanup.
|
|
287
|
+
{
|
|
288
|
+
name: "update_theme",
|
|
289
|
+
description:
|
|
290
|
+
"Change workspace theme: mode (dark|light), primary_color (#hex), accent_color (#hex), font_family. Any subset. Available fonts: Inter, DM Sans, Playfair Display, Space Grotesk, Lora, Outfit.",
|
|
291
|
+
inputSchema: obj(
|
|
292
|
+
{
|
|
293
|
+
mode: { type: "string", enum: ["dark", "light"] },
|
|
294
|
+
primary_color: str("Hex color like '#14b8a6'."),
|
|
295
|
+
accent_color: str("Hex color."),
|
|
296
|
+
font_family: {
|
|
297
|
+
type: "string",
|
|
298
|
+
enum: ["Inter", "DM Sans", "Playfair Display", "Space Grotesk", "Lora", "Outfit"],
|
|
299
|
+
},
|
|
300
|
+
workspace_id: str("Optional workspace override."),
|
|
301
|
+
},
|
|
302
|
+
),
|
|
303
|
+
handler: async (a) => {
|
|
304
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
305
|
+
return api("POST", "/theme/update", {
|
|
306
|
+
body: {
|
|
307
|
+
mode: a.mode,
|
|
308
|
+
primary_color: a.primary_color,
|
|
309
|
+
accent_color: a.accent_color,
|
|
310
|
+
font_family: a.font_family,
|
|
311
|
+
workspace_id: ws,
|
|
312
|
+
},
|
|
313
|
+
workspace_id: ws,
|
|
314
|
+
});
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "list_automations",
|
|
319
|
+
description: "List automations configured in the active (or specified) workspace.",
|
|
320
|
+
inputSchema: obj({ workspace_id: str("Optional workspace override.") }),
|
|
321
|
+
handler: async (a) => {
|
|
322
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
323
|
+
return api("GET", `/automations?workspace_id=${encodeURIComponent(ws)}`, { workspace_id: ws });
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "install_vertical_pack",
|
|
328
|
+
description:
|
|
329
|
+
"Install a vertical pack (e.g. 'real-estate', 'dental', 'legal'). Adds domain-specific objects, fields, views.",
|
|
330
|
+
inputSchema: obj(
|
|
331
|
+
{
|
|
332
|
+
pack: str("Pack slug, e.g. 'real-estate', 'dental', 'legal'."),
|
|
333
|
+
workspace_id: str("Optional workspace override."),
|
|
334
|
+
},
|
|
335
|
+
["pack"],
|
|
336
|
+
),
|
|
337
|
+
handler: async (a) => {
|
|
338
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
339
|
+
return api("POST", "/packs/install", {
|
|
340
|
+
body: { pack: a.pack, workspace_id: ws },
|
|
341
|
+
workspace_id: ws,
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "install_caldiy_booking",
|
|
347
|
+
description:
|
|
348
|
+
"Install the Cal.diy booking block (event types, availability, bookings). Example: install_caldiy_booking({})",
|
|
349
|
+
inputSchema: obj({
|
|
350
|
+
workspace_id: str("Optional workspace override."),
|
|
351
|
+
config: { type: "object", description: "Optional Cal.diy configuration overrides." },
|
|
352
|
+
}),
|
|
353
|
+
handler: async (a) => {
|
|
354
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
355
|
+
return api("POST", "/packs/caldiy-booking/install", {
|
|
356
|
+
body: { workspace_id: ws, config: a.config },
|
|
357
|
+
workspace_id: ws,
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: "install_formbricks_intake",
|
|
363
|
+
description:
|
|
364
|
+
"Install a Formbricks intake form (surveys, conditional logic, contact sync). Example: install_formbricks_intake({})",
|
|
365
|
+
inputSchema: obj({
|
|
366
|
+
workspace_id: str("Optional workspace override."),
|
|
367
|
+
form_id: str("Optional existing Formbricks form id to bind."),
|
|
368
|
+
}),
|
|
369
|
+
handler: async (a) => {
|
|
370
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
371
|
+
return api("POST", "/packs/formbricks-intake/install", {
|
|
372
|
+
body: { workspace_id: ws, form_id: a.form_id },
|
|
373
|
+
workspace_id: ws,
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: "get_workspace_snapshot",
|
|
379
|
+
description:
|
|
380
|
+
"Return a structured read-only snapshot of workspace state: workspace metadata, Soul (if submitted), theme, enabled blocks with configs, entity counts (contacts/bookings/intake forms/submissions), recent Seldon It events, and public URLs. YOU reason over this snapshot to decide what to do next, then call the appropriate typed tools (update_landing_content, configure_booking, customize_intake_form, update_theme, install_*). Zero server-side LLM cost.",
|
|
381
|
+
inputSchema: obj({
|
|
382
|
+
workspace_id: str("Optional workspace override."),
|
|
383
|
+
}),
|
|
384
|
+
handler: async (a) => {
|
|
385
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
386
|
+
return api("GET", `/workspace/${encodeURIComponent(ws)}/snapshot`, {
|
|
387
|
+
workspace_id: ws,
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: "fetch_source_for_soul",
|
|
393
|
+
description:
|
|
394
|
+
"Fetch a URL and return normalized text (headings + body, up to 256KB). Use this to gather raw content; then you (the caller) extract a structured Soul and submit it with submit_soul. Zero LLM cost to Seldon — extraction runs in this session.",
|
|
395
|
+
inputSchema: obj(
|
|
396
|
+
{
|
|
397
|
+
url: str("Absolute URL to fetch."),
|
|
398
|
+
},
|
|
399
|
+
["url"],
|
|
400
|
+
),
|
|
401
|
+
handler: async ({ url }) => {
|
|
402
|
+
const { html, truncated, status, final_url } = await fetchText(url);
|
|
403
|
+
const text = htmlToText(html);
|
|
404
|
+
return {
|
|
405
|
+
ok: true,
|
|
406
|
+
url,
|
|
407
|
+
final_url,
|
|
408
|
+
status,
|
|
409
|
+
bytes: text.length,
|
|
410
|
+
truncated,
|
|
411
|
+
text,
|
|
412
|
+
next: [
|
|
413
|
+
"Extract a Soul object: { mission, audience, tone, offerings[], differentiators[], faqs[] }",
|
|
414
|
+
"submit_soul({ soul: <extracted> })",
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: "submit_soul",
|
|
421
|
+
description:
|
|
422
|
+
"Submit a compiled Soul object to the active workspace. The caller is expected to have produced the structured Soul from fetch_source_for_soul output or user conversation.",
|
|
423
|
+
inputSchema: obj(
|
|
424
|
+
{
|
|
425
|
+
soul: {
|
|
426
|
+
type: "object",
|
|
427
|
+
description:
|
|
428
|
+
"Structured Soul. Expected keys: mission, audience, tone, offerings, differentiators, faqs. Additional keys allowed.",
|
|
429
|
+
},
|
|
430
|
+
workspace_id: str("Optional workspace override."),
|
|
431
|
+
},
|
|
432
|
+
["soul"],
|
|
433
|
+
),
|
|
434
|
+
handler: async (a) => {
|
|
435
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
436
|
+
return api("POST", "/soul/submit", {
|
|
437
|
+
body: { workspace_id: ws, soul: a.soul },
|
|
438
|
+
workspace_id: ws,
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: "connect_custom_domain",
|
|
444
|
+
description:
|
|
445
|
+
"Connect + verify a custom domain. Pro capability — requires SELDONFRAME_API_KEY. Example: connect_custom_domain({ domain: 'app.mysite.com' })",
|
|
446
|
+
inputSchema: obj(
|
|
447
|
+
{
|
|
448
|
+
domain: str("Fully qualified domain, e.g. client.example.com."),
|
|
449
|
+
workspace_id: str("Optional workspace override."),
|
|
450
|
+
},
|
|
451
|
+
["domain"],
|
|
452
|
+
),
|
|
453
|
+
handler: async (a) => {
|
|
454
|
+
if (!hasApiKey()) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
"Custom domains are a Pro capability. Get a key at https://app.seldonframe.com/settings/api and `export SELDONFRAME_API_KEY=sk-…`.",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
460
|
+
return api("POST", "/domains/connect", {
|
|
461
|
+
body: { domain: a.domain, workspace_id: ws },
|
|
462
|
+
workspace_id: ws,
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
name: "export_agent",
|
|
468
|
+
description: "Export the current workspace as a portable .agent/ bundle.",
|
|
469
|
+
inputSchema: obj({ workspace_id: str("Optional workspace override.") }),
|
|
470
|
+
handler: async (a) => {
|
|
471
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
472
|
+
return api("POST", "/export/agent", { body: { workspace_id: ws }, workspace_id: ws });
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "store_secret",
|
|
477
|
+
description:
|
|
478
|
+
"Store a workspace-scoped secret (encrypted at rest). Example: store_secret({ key: 'STRIPE_API_KEY', value: 'sk_…' })",
|
|
479
|
+
inputSchema: obj(
|
|
480
|
+
{
|
|
481
|
+
key: str("Secret name, e.g. 'STRIPE_API_KEY'."),
|
|
482
|
+
value: str("Secret plaintext value."),
|
|
483
|
+
workspace_id: str("Optional workspace override."),
|
|
484
|
+
},
|
|
485
|
+
["key", "value"],
|
|
486
|
+
),
|
|
487
|
+
handler: async (a) => {
|
|
488
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
489
|
+
return api("POST", "/secrets", {
|
|
490
|
+
body: { key: a.key, value: a.value, workspace_id: ws },
|
|
491
|
+
workspace_id: ws,
|
|
492
|
+
});
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: "list_secrets",
|
|
497
|
+
description: "List secret metadata (names, timestamps) without exposing plaintext.",
|
|
498
|
+
inputSchema: obj({ workspace_id: str("Optional workspace override.") }),
|
|
499
|
+
handler: async (a) => {
|
|
500
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
501
|
+
return api("GET", `/secrets?workspace_id=${encodeURIComponent(ws)}`, { workspace_id: ws });
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: "rotate_secret",
|
|
506
|
+
description: "Rotate or delete a workspace secret. Omit new_value to delete.",
|
|
507
|
+
inputSchema: obj(
|
|
508
|
+
{
|
|
509
|
+
key: str("Secret name to rotate."),
|
|
510
|
+
new_value: str("New plaintext value. Omit to delete the secret."),
|
|
511
|
+
workspace_id: str("Optional workspace override."),
|
|
512
|
+
},
|
|
513
|
+
["key"],
|
|
514
|
+
),
|
|
515
|
+
handler: async (a) => {
|
|
516
|
+
const ws = wsOrDefault(a.workspace_id);
|
|
517
|
+
if (a.new_value === undefined) {
|
|
518
|
+
return api("DELETE", `/secrets/${encodeURIComponent(a.key)}`, {
|
|
519
|
+
body: { workspace_id: ws },
|
|
520
|
+
workspace_id: ws,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
return api("PUT", `/secrets/${encodeURIComponent(a.key)}`, {
|
|
524
|
+
body: { value: a.new_value, workspace_id: ws },
|
|
525
|
+
workspace_id: ws,
|
|
526
|
+
});
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
// ════════════════════════════════════════════════════════════════════
|
|
530
|
+
// CRM tools — Phase 2.b per tasks/mcp-gap-audit.md
|
|
531
|
+
// Thin wrappers over v1 endpoints at /api/v1/{contacts,deals,activities}.
|
|
532
|
+
// Naming convention locked in the audit: list_/get_/create_/update_/
|
|
533
|
+
// delete_ for CRUD; verb_noun for state changes (move_deal_stage).
|
|
534
|
+
// ════════════════════════════════════════════════════════════════════
|
|
535
|
+
|
|
536
|
+
{
|
|
537
|
+
name: "list_contacts",
|
|
538
|
+
description:
|
|
539
|
+
"List contacts in the active workspace. Returns every contact the caller can read. Example: list_contacts({}).",
|
|
540
|
+
inputSchema: obj({
|
|
541
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
542
|
+
}),
|
|
543
|
+
handler: async (args) => {
|
|
544
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
545
|
+
const result = await api("GET", "/contacts", { workspace_id: ws });
|
|
546
|
+
return { ok: true, contacts: result.data ?? [], meta: result.meta ?? null };
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "get_contact",
|
|
551
|
+
description:
|
|
552
|
+
"Fetch one contact by id. Example: get_contact({ contact_id: 'abc-...' }).",
|
|
553
|
+
inputSchema: obj(
|
|
554
|
+
{
|
|
555
|
+
contact_id: str("UUID of the contact."),
|
|
556
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
557
|
+
},
|
|
558
|
+
["contact_id"],
|
|
559
|
+
),
|
|
560
|
+
handler: async (args) => {
|
|
561
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
562
|
+
const result = await api("GET", `/contacts/${encodeURIComponent(args.contact_id)}`, {
|
|
563
|
+
workspace_id: ws,
|
|
564
|
+
});
|
|
565
|
+
return { ok: true, contact: result.data ?? null };
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
name: "create_contact",
|
|
570
|
+
description:
|
|
571
|
+
"Create a new contact. Typical use: 'Add Jane Doe jane@acme.co as a lead'. Example: create_contact({ first_name: 'Jane', last_name: 'Doe', email: 'jane@acme.co', status: 'lead' }).",
|
|
572
|
+
inputSchema: obj(
|
|
573
|
+
{
|
|
574
|
+
first_name: str("Required. Contact's first name."),
|
|
575
|
+
last_name: str("Optional. Last name."),
|
|
576
|
+
email: str("Optional but strongly recommended — unlocks form auto-linking and email sends."),
|
|
577
|
+
status: str("Optional lifecycle stage (e.g., 'lead', 'prospect', 'customer'). Defaults to 'lead'."),
|
|
578
|
+
source: str("Optional source tag (e.g., 'manual', 'intake-form', 'import')."),
|
|
579
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
580
|
+
},
|
|
581
|
+
["first_name"],
|
|
582
|
+
),
|
|
583
|
+
handler: async (args) => {
|
|
584
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
585
|
+
const result = await api("POST", "/contacts", {
|
|
586
|
+
body: {
|
|
587
|
+
firstName: args.first_name,
|
|
588
|
+
lastName: args.last_name ?? "",
|
|
589
|
+
email: args.email ?? null,
|
|
590
|
+
status: args.status ?? "lead",
|
|
591
|
+
source: args.source ?? "mcp",
|
|
592
|
+
},
|
|
593
|
+
workspace_id: ws,
|
|
594
|
+
});
|
|
595
|
+
return { ok: true, contact: result.data };
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: "update_contact",
|
|
600
|
+
description:
|
|
601
|
+
"Update fields on an existing contact. Partial — omit fields you don't want to change. Example: update_contact({ contact_id: '...', status: 'customer' }).",
|
|
602
|
+
inputSchema: obj(
|
|
603
|
+
{
|
|
604
|
+
contact_id: str("UUID of the contact to update."),
|
|
605
|
+
first_name: str("Optional new first name."),
|
|
606
|
+
last_name: str("Optional new last name."),
|
|
607
|
+
email: str("Optional new email."),
|
|
608
|
+
status: str("Optional new lifecycle stage."),
|
|
609
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
610
|
+
},
|
|
611
|
+
["contact_id"],
|
|
612
|
+
),
|
|
613
|
+
handler: async (args) => {
|
|
614
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
615
|
+
const patch = {};
|
|
616
|
+
if (args.first_name !== undefined) patch.firstName = args.first_name;
|
|
617
|
+
if (args.last_name !== undefined) patch.lastName = args.last_name;
|
|
618
|
+
if (args.email !== undefined) patch.email = args.email;
|
|
619
|
+
if (args.status !== undefined) patch.status = args.status;
|
|
620
|
+
const result = await api("PATCH", `/contacts/${encodeURIComponent(args.contact_id)}`, {
|
|
621
|
+
body: patch,
|
|
622
|
+
workspace_id: ws,
|
|
623
|
+
});
|
|
624
|
+
return { ok: true, contact: result.data };
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: "delete_contact",
|
|
629
|
+
description:
|
|
630
|
+
"Delete a contact and all linked deals/activities (cascades via FK). Irreversible. Example: delete_contact({ contact_id: '...' }).",
|
|
631
|
+
inputSchema: obj(
|
|
632
|
+
{
|
|
633
|
+
contact_id: str("UUID of the contact to delete."),
|
|
634
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
635
|
+
},
|
|
636
|
+
["contact_id"],
|
|
637
|
+
),
|
|
638
|
+
handler: async (args) => {
|
|
639
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
640
|
+
await api("DELETE", `/contacts/${encodeURIComponent(args.contact_id)}`, {
|
|
641
|
+
workspace_id: ws,
|
|
642
|
+
});
|
|
643
|
+
return { ok: true, deleted: args.contact_id };
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "list_deals",
|
|
648
|
+
description: "List deals in the active workspace. Example: list_deals({}).",
|
|
649
|
+
inputSchema: obj({
|
|
650
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
651
|
+
}),
|
|
652
|
+
handler: async (args) => {
|
|
653
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
654
|
+
const result = await api("GET", "/deals", { workspace_id: ws });
|
|
655
|
+
return { ok: true, deals: result.data ?? [] };
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: "get_deal",
|
|
660
|
+
description: "Fetch one deal by id. Example: get_deal({ deal_id: '...' }).",
|
|
661
|
+
inputSchema: obj(
|
|
662
|
+
{
|
|
663
|
+
deal_id: str("UUID of the deal."),
|
|
664
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
665
|
+
},
|
|
666
|
+
["deal_id"],
|
|
667
|
+
),
|
|
668
|
+
handler: async (args) => {
|
|
669
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
670
|
+
const result = await api("GET", `/deals/${encodeURIComponent(args.deal_id)}`, {
|
|
671
|
+
workspace_id: ws,
|
|
672
|
+
});
|
|
673
|
+
return { ok: true, deal: result.data ?? null };
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: "create_deal",
|
|
678
|
+
description:
|
|
679
|
+
"Create a new deal attached to a contact on the default pipeline. Typical use: 'Create a $5k deal for Jane Doe at the Discovery stage'. Example: create_deal({ contact_id: '...', title: 'Q2 retainer', value: 5000, stage: 'Discovery' }).",
|
|
680
|
+
inputSchema: obj(
|
|
681
|
+
{
|
|
682
|
+
contact_id: str("UUID of the contact this deal belongs to."),
|
|
683
|
+
title: str("Human-readable deal name."),
|
|
684
|
+
value: { type: "number", description: "Optional deal value in workspace's default currency. Defaults to 0." },
|
|
685
|
+
stage: str("Optional stage name (e.g. 'Discovery', 'Proposal'). Defaults to the first stage of the default pipeline."),
|
|
686
|
+
probability: { type: "number", description: "Optional win probability 0-100. Defaults to 0." },
|
|
687
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
688
|
+
},
|
|
689
|
+
["contact_id", "title"],
|
|
690
|
+
),
|
|
691
|
+
handler: async (args) => {
|
|
692
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
693
|
+
const result = await api("POST", "/deals", {
|
|
694
|
+
body: {
|
|
695
|
+
contactId: args.contact_id,
|
|
696
|
+
title: args.title,
|
|
697
|
+
value: args.value ?? 0,
|
|
698
|
+
stage: args.stage ?? "New",
|
|
699
|
+
probability: args.probability ?? 0,
|
|
700
|
+
},
|
|
701
|
+
workspace_id: ws,
|
|
702
|
+
});
|
|
703
|
+
return { ok: true, deal: result.data };
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
name: "update_deal",
|
|
708
|
+
description:
|
|
709
|
+
"Update a deal. Partial — omit fields to keep them. For stage-only moves prefer move_deal_stage (clearer intent). Example: update_deal({ deal_id: '...', value: 7500 }).",
|
|
710
|
+
inputSchema: obj(
|
|
711
|
+
{
|
|
712
|
+
deal_id: str("UUID of the deal."),
|
|
713
|
+
title: str("Optional new title."),
|
|
714
|
+
stage: str("Optional new stage."),
|
|
715
|
+
value: { type: "number", description: "Optional new value." },
|
|
716
|
+
probability: { type: "number", description: "Optional new probability (0-100)." },
|
|
717
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
718
|
+
},
|
|
719
|
+
["deal_id"],
|
|
720
|
+
),
|
|
721
|
+
handler: async (args) => {
|
|
722
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
723
|
+
const patch = {};
|
|
724
|
+
if (args.title !== undefined) patch.title = args.title;
|
|
725
|
+
if (args.stage !== undefined) patch.stage = args.stage;
|
|
726
|
+
if (args.value !== undefined) patch.value = args.value;
|
|
727
|
+
if (args.probability !== undefined) patch.probability = args.probability;
|
|
728
|
+
const result = await api("PATCH", `/deals/${encodeURIComponent(args.deal_id)}`, {
|
|
729
|
+
body: patch,
|
|
730
|
+
workspace_id: ws,
|
|
731
|
+
});
|
|
732
|
+
return { ok: true, deal: result.data };
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: "move_deal_stage",
|
|
737
|
+
description:
|
|
738
|
+
"Move a deal to a new stage. Same effect as dragging the card on the kanban. Example: move_deal_stage({ deal_id: '...', to_stage: 'Proposal' }).",
|
|
739
|
+
inputSchema: obj(
|
|
740
|
+
{
|
|
741
|
+
deal_id: str("UUID of the deal."),
|
|
742
|
+
to_stage: str("Destination stage name."),
|
|
743
|
+
probability: { type: "number", description: "Optional. Stage probability (0-100) if the workspace's pipeline has one defined for this stage." },
|
|
744
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
745
|
+
},
|
|
746
|
+
["deal_id", "to_stage"],
|
|
747
|
+
),
|
|
748
|
+
handler: async (args) => {
|
|
749
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
750
|
+
const body = { stage: args.to_stage };
|
|
751
|
+
if (args.probability !== undefined) body.probability = args.probability;
|
|
752
|
+
const result = await api("PATCH", `/deals/${encodeURIComponent(args.deal_id)}`, {
|
|
753
|
+
body,
|
|
754
|
+
workspace_id: ws,
|
|
755
|
+
});
|
|
756
|
+
return { ok: true, deal: result.data };
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
name: "delete_deal",
|
|
761
|
+
description: "Delete a deal. Irreversible. Example: delete_deal({ deal_id: '...' }).",
|
|
762
|
+
inputSchema: obj(
|
|
763
|
+
{
|
|
764
|
+
deal_id: str("UUID of the deal."),
|
|
765
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
766
|
+
},
|
|
767
|
+
["deal_id"],
|
|
768
|
+
),
|
|
769
|
+
handler: async (args) => {
|
|
770
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
771
|
+
await api("DELETE", `/deals/${encodeURIComponent(args.deal_id)}`, {
|
|
772
|
+
workspace_id: ws,
|
|
773
|
+
});
|
|
774
|
+
return { ok: true, deleted: args.deal_id };
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
name: "list_activities",
|
|
779
|
+
description:
|
|
780
|
+
"List activity log entries (tasks, notes, email sent, booking created, etc.) across the workspace. Example: list_activities({}).",
|
|
781
|
+
inputSchema: obj({
|
|
782
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
783
|
+
}),
|
|
784
|
+
handler: async (args) => {
|
|
785
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
786
|
+
const result = await api("GET", "/activities", { workspace_id: ws });
|
|
787
|
+
return { ok: true, activities: result.data ?? [] };
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
// ════════════════════════════════════════════════════════════════════
|
|
792
|
+
// Booking tools — Phase 2.c per tasks/mcp-gap-audit.md
|
|
793
|
+
// CRUD for appointment types (template rows in the bookings table).
|
|
794
|
+
// The v1 endpoints at /api/v1/booking/appointment-types[/<slug>] enforce
|
|
795
|
+
// `status='template'` so tools here cannot accidentally touch real
|
|
796
|
+
// scheduled bookings. Cancel / reschedule / list_bookings are deferred
|
|
797
|
+
// until bookings block has real scheduled data to test against.
|
|
798
|
+
// ════════════════════════════════════════════════════════════════════
|
|
799
|
+
|
|
800
|
+
{
|
|
801
|
+
name: "create_activity",
|
|
802
|
+
description:
|
|
803
|
+
"Append an activity-log entry to a contact (and/or deal). Use this instead of stuffing agent reminders into contacts.notes — notes gets overwritten on updates; activities are append-only. Valid types: task, note, email, sms, call, meeting, stage_change, payment, review_request, agent_action. Example: create_activity({ contact_id: 'ctc_...', type: 'agent_action', subject: 'Speed-to-Lead agent booked consult', body: 'Scheduled for 2026-05-01' })",
|
|
804
|
+
inputSchema: obj(
|
|
805
|
+
{
|
|
806
|
+
contact_id: str("Contact to log against. Either contact_id or deal_id is required."),
|
|
807
|
+
deal_id: str("Deal to log against. Either contact_id or deal_id is required."),
|
|
808
|
+
type: str("task | note | email | sms | call | meeting | stage_change | payment | review_request | agent_action"),
|
|
809
|
+
subject: str("One-line title (≤200 chars). Either subject or body is required."),
|
|
810
|
+
body: str("Optional multi-line detail (≤4000 chars)."),
|
|
811
|
+
scheduled_at: str("Optional ISO timestamp if the activity is planned for a future time (e.g., a task)."),
|
|
812
|
+
completed_at: str("Optional ISO timestamp if logging a completed past action."),
|
|
813
|
+
metadata: {
|
|
814
|
+
type: "object",
|
|
815
|
+
description: "Optional JSON metadata — e.g., { agentId: 'agt_...', confidence: 0.87 }",
|
|
816
|
+
},
|
|
817
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
818
|
+
},
|
|
819
|
+
["type"],
|
|
820
|
+
),
|
|
821
|
+
handler: async (args) => {
|
|
822
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
823
|
+
return api("POST", "/activities", {
|
|
824
|
+
body: {
|
|
825
|
+
contact_id: args.contact_id ?? null,
|
|
826
|
+
deal_id: args.deal_id ?? null,
|
|
827
|
+
type: args.type,
|
|
828
|
+
subject: args.subject ?? null,
|
|
829
|
+
body: args.body ?? null,
|
|
830
|
+
scheduled_at: args.scheduled_at ?? null,
|
|
831
|
+
completed_at: args.completed_at ?? null,
|
|
832
|
+
metadata: args.metadata ?? {},
|
|
833
|
+
},
|
|
834
|
+
workspace_id: ws,
|
|
835
|
+
});
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
name: "list_bookings",
|
|
840
|
+
description:
|
|
841
|
+
"List scheduled bookings (not appointment-type templates — see list_appointment_types for those). Supports filtering by contact, status, and date range. Default sort: most-recent-first; if `from` is set, switches to earliest-upcoming-first for reminder flows. Example: list_bookings({ from: '2026-04-22T00:00:00Z', limit: 20 })",
|
|
842
|
+
inputSchema: obj(
|
|
843
|
+
{
|
|
844
|
+
contact_id: str("Optional. Filter to a specific contact's bookings."),
|
|
845
|
+
status: str("Optional. Filter by status (scheduled | completed | cancelled | no_show)."),
|
|
846
|
+
from: str("Optional ISO timestamp. Only bookings starting at or after this moment."),
|
|
847
|
+
to: str("Optional ISO timestamp. Only bookings starting at or before this moment."),
|
|
848
|
+
limit: { type: "number", description: "Max rows (default 50, max 200)." },
|
|
849
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
850
|
+
},
|
|
851
|
+
[],
|
|
852
|
+
),
|
|
853
|
+
handler: async (args) => {
|
|
854
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
855
|
+
const params = new URLSearchParams();
|
|
856
|
+
if (args.contact_id) params.set("contact_id", args.contact_id);
|
|
857
|
+
if (args.status) params.set("status", args.status);
|
|
858
|
+
if (args.from) params.set("from", args.from);
|
|
859
|
+
if (args.to) params.set("to", args.to);
|
|
860
|
+
if (typeof args.limit === "number") params.set("limit", String(Math.min(args.limit, 200)));
|
|
861
|
+
const qs = params.toString();
|
|
862
|
+
return api("GET", `/bookings${qs ? `?${qs}` : ""}`, { workspace_id: ws });
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
name: "create_coupon",
|
|
867
|
+
description:
|
|
868
|
+
"Create a Stripe coupon + matching per-contact redeemable promotion code on the workspace's connected Stripe account. Use for Win-Back / retention agents that need UNIQUE codes per recipient (shared codes are vulnerable to abuse + lose attribution signal). Default max_redemptions=1 + auto-generated code string. Requires the workspace to have completed Stripe Connect onboarding. Example: create_coupon({ percent_off: 20, duration: 'once', name: 'Win-Back 20% off' })",
|
|
869
|
+
inputSchema: obj(
|
|
870
|
+
{
|
|
871
|
+
percent_off: { type: "number", description: "Discount percentage (0 < n ≤ 100). Either percent_off or amount_off is required." },
|
|
872
|
+
amount_off: { type: "number", description: "Flat discount in the currency's major unit (e.g., 25.00 for $25 off). Either percent_off or amount_off is required." },
|
|
873
|
+
currency: str("Only used with amount_off. 3-letter ISO code. Defaults to usd."),
|
|
874
|
+
duration: str("'once' (default) | 'forever' | 'repeating'. 'repeating' requires duration_in_months."),
|
|
875
|
+
duration_in_months: { type: "number", description: "Required when duration='repeating'." },
|
|
876
|
+
name: str("Optional display name for the coupon (≤60 chars)."),
|
|
877
|
+
code: str("Optional fixed redeemable code string. If omitted, Stripe auto-generates one."),
|
|
878
|
+
max_redemptions: { type: "number", description: "Max total redemptions. Default 1 — per-contact unique code." },
|
|
879
|
+
expires_at: str("Optional ISO timestamp. Code becomes invalid after this moment. Prefer expires_in_days for agent archetypes."),
|
|
880
|
+
expires_in_days: { type: "number", description: "Relative expiry: code becomes invalid N days after this call fires (1–365). Preferred over expires_at for agent archetypes so the window stays meaningful no matter when the agent was last deployed." },
|
|
881
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
882
|
+
},
|
|
883
|
+
[],
|
|
884
|
+
),
|
|
885
|
+
handler: async (args) => {
|
|
886
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
887
|
+
const body = {};
|
|
888
|
+
if (typeof args.percent_off === "number") body.percent_off = args.percent_off;
|
|
889
|
+
if (typeof args.amount_off === "number") body.amount_off = args.amount_off;
|
|
890
|
+
if (args.currency) body.currency = args.currency;
|
|
891
|
+
if (args.duration) body.duration = args.duration;
|
|
892
|
+
if (typeof args.duration_in_months === "number") body.duration_in_months = args.duration_in_months;
|
|
893
|
+
if (args.name) body.name = args.name;
|
|
894
|
+
if (args.code) body.code = args.code;
|
|
895
|
+
if (typeof args.max_redemptions === "number") body.max_redemptions = args.max_redemptions;
|
|
896
|
+
if (args.expires_at) body.expires_at = args.expires_at;
|
|
897
|
+
if (typeof args.expires_in_days === "number") body.expires_in_days = args.expires_in_days;
|
|
898
|
+
return api("POST", "/coupons", { body, workspace_id: ws });
|
|
899
|
+
},
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
name: "create_booking",
|
|
903
|
+
description:
|
|
904
|
+
"Schedule a real booking against an existing appointment type. Looks up the template by id, creates a scheduled row on the workspace calendar, stamps the contact's name + email, emits booking.created, and — if the appointment type has a price > 0 — returns a Stripe Checkout URL routed to the SMB's connected Stripe account so the builder / agent can text or email the payment link to the contact. Example: create_booking({ contact_id: 'ctc_...', appointment_type_id: 'appt_...', starts_at: '2026-05-01T15:00:00Z' })",
|
|
905
|
+
inputSchema: obj(
|
|
906
|
+
{
|
|
907
|
+
contact_id: str("Required. CRM contact being booked."),
|
|
908
|
+
appointment_type_id: str("Required. Appointment-type template id from list_appointment_types."),
|
|
909
|
+
starts_at: str("Required. ISO 8601 timestamp for the appointment start (e.g. 2026-05-01T15:00:00Z). Duration is read from the appointment type."),
|
|
910
|
+
notes: str("Optional free-form booking notes."),
|
|
911
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
912
|
+
},
|
|
913
|
+
["contact_id", "appointment_type_id", "starts_at"],
|
|
914
|
+
),
|
|
915
|
+
handler: async (args) => {
|
|
916
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
917
|
+
return api("POST", "/bookings", {
|
|
918
|
+
body: {
|
|
919
|
+
contact_id: args.contact_id,
|
|
920
|
+
appointment_type_id: args.appointment_type_id,
|
|
921
|
+
starts_at: args.starts_at,
|
|
922
|
+
notes: args.notes ?? null,
|
|
923
|
+
},
|
|
924
|
+
workspace_id: ws,
|
|
925
|
+
});
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
name: "get_booking",
|
|
930
|
+
description:
|
|
931
|
+
"Fetch one scheduled booking by id. Returns the full detail (contact, times, status, notes, meeting URL, cancellation timestamp, metadata). Appointment-type templates are NOT returned here — use list_appointment_types for those. 404s if the id is unknown OR belongs to a different workspace. Example: get_booking({ booking_id: 'bkg_...' }).",
|
|
932
|
+
inputSchema: obj(
|
|
933
|
+
{
|
|
934
|
+
booking_id: str("Required. UUID of the booking."),
|
|
935
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
936
|
+
},
|
|
937
|
+
["booking_id"],
|
|
938
|
+
),
|
|
939
|
+
handler: async (args) => {
|
|
940
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
941
|
+
const result = await api("GET", `/bookings/${encodeURIComponent(args.booking_id)}`, {
|
|
942
|
+
workspace_id: ws,
|
|
943
|
+
});
|
|
944
|
+
return { ok: true, booking: result.data ?? null };
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
name: "cancel_booking",
|
|
949
|
+
description:
|
|
950
|
+
"Cancel a scheduled booking. Sets status to 'cancelled', stamps cancelledAt, deletes the Google Calendar event, and emits booking.cancelled. Idempotent — re-cancelling an already-cancelled booking is a 200 no-op with alreadyCancelled=true (no duplicate events, no calendar errors). Past-dated bookings CAN be cancelled (legitimate retroactive cleanup). Does NOT touch linked payments — linkedPaymentIds is returned so the agent can compose refund_payment explicitly if the business rule is 'cancel AND refund'. Example: cancel_booking({ booking_id: 'bkg_...' }).",
|
|
951
|
+
inputSchema: obj(
|
|
952
|
+
{
|
|
953
|
+
booking_id: str("Required. UUID of the booking to cancel."),
|
|
954
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
955
|
+
},
|
|
956
|
+
["booking_id"],
|
|
957
|
+
),
|
|
958
|
+
handler: async (args) => {
|
|
959
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
960
|
+
const result = await api("POST", `/bookings/${encodeURIComponent(args.booking_id)}/cancel`, {
|
|
961
|
+
workspace_id: ws,
|
|
962
|
+
});
|
|
963
|
+
return { ok: true, ...result.data };
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
name: "reschedule_booking",
|
|
968
|
+
description:
|
|
969
|
+
"Move a scheduled booking to a new starts_at. Preserves the original duration — endsAt tracks the move so a 30-min consult stays 30 mins at the new time. Updates the Google Calendar event in place (event id preserved; attendees see the time change on their existing invite) and emits booking.rescheduled with both previousStartsAt and newStartsAt so follow-up agents can describe the change. Rejects past-dated new starts_at (400) and refuses to reschedule a cancelled booking (422 — reviving a cancellation should be a new create_booking). Does NOT change appointment type; does NOT touch linked payments. Example: reschedule_booking({ booking_id: 'bkg_...', starts_at: '2026-05-02T15:00:00Z' }).",
|
|
970
|
+
inputSchema: obj(
|
|
971
|
+
{
|
|
972
|
+
booking_id: str("Required. UUID of the booking to move."),
|
|
973
|
+
starts_at: str("Required. New ISO 8601 timestamp. Must be in the future. Duration is preserved from the current booking."),
|
|
974
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
975
|
+
},
|
|
976
|
+
["booking_id", "starts_at"],
|
|
977
|
+
),
|
|
978
|
+
handler: async (args) => {
|
|
979
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
980
|
+
const result = await api("POST", `/bookings/${encodeURIComponent(args.booking_id)}/reschedule`, {
|
|
981
|
+
body: { starts_at: args.starts_at },
|
|
982
|
+
workspace_id: ws,
|
|
983
|
+
});
|
|
984
|
+
return { ok: true, ...result.data };
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
name: "list_appointment_types",
|
|
989
|
+
description:
|
|
990
|
+
"List all appointment types (bookable templates) in the workspace. Example: list_appointment_types({}).",
|
|
991
|
+
inputSchema: obj({
|
|
992
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
993
|
+
}),
|
|
994
|
+
handler: async (args) => {
|
|
995
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
996
|
+
const result = await api("GET", "/booking/appointment-types", { workspace_id: ws });
|
|
997
|
+
return { ok: true, appointment_types: result.appointment_types ?? [] };
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
name: "create_appointment_type",
|
|
1002
|
+
description:
|
|
1003
|
+
"Create a new appointment type with its own public /book/<slug> URL. Defaults availability to Mon–Fri 9am–5pm (edit on /bookings to change). Example: create_appointment_type({ title: 'Strategy call', duration_minutes: 45, price: 150 }).",
|
|
1004
|
+
inputSchema: obj(
|
|
1005
|
+
{
|
|
1006
|
+
title: str("Required. Human-readable name, e.g., 'Strategy call'."),
|
|
1007
|
+
booking_slug: str("Optional. URL-safe slug. Auto-derived from title if omitted."),
|
|
1008
|
+
duration_minutes: { type: "number", description: "Optional. 5–240. Defaults to 30." },
|
|
1009
|
+
description: str("Optional. Up to 800 chars. Shown on the public booking page."),
|
|
1010
|
+
price: { type: "number", description: "Optional. Defaults to 0 (free). Non-zero prices route through Stripe checkout on submit (requires Stripe connected)." },
|
|
1011
|
+
buffer_before_minutes: { type: "number", description: "Optional. 0–120. Defaults to 0." },
|
|
1012
|
+
buffer_after_minutes: { type: "number", description: "Optional. 0–120. Defaults to 0." },
|
|
1013
|
+
max_bookings_per_day: { type: "number", description: "Optional. Hard daily cap (1–100). Omit for unlimited." },
|
|
1014
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1015
|
+
},
|
|
1016
|
+
["title"],
|
|
1017
|
+
),
|
|
1018
|
+
handler: async (args) => {
|
|
1019
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1020
|
+
const result = await api("POST", "/booking/appointment-types", {
|
|
1021
|
+
body: {
|
|
1022
|
+
title: args.title,
|
|
1023
|
+
booking_slug: args.booking_slug,
|
|
1024
|
+
duration_minutes: args.duration_minutes,
|
|
1025
|
+
description: args.description,
|
|
1026
|
+
price: args.price,
|
|
1027
|
+
buffer_before_minutes: args.buffer_before_minutes,
|
|
1028
|
+
buffer_after_minutes: args.buffer_after_minutes,
|
|
1029
|
+
max_bookings_per_day: args.max_bookings_per_day,
|
|
1030
|
+
},
|
|
1031
|
+
workspace_id: ws,
|
|
1032
|
+
});
|
|
1033
|
+
return {
|
|
1034
|
+
ok: true,
|
|
1035
|
+
appointment_type: result.appointment_type,
|
|
1036
|
+
public_url: result.public_url,
|
|
1037
|
+
};
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
name: "update_appointment_type",
|
|
1042
|
+
description:
|
|
1043
|
+
"Update an existing appointment type. Partial — omit fields to keep them. Example: update_appointment_type({ booking_slug: 'default', duration_minutes: 60, price: 200 }). Pass booking_slug='default' to edit the auto-seeded 'Book a call' template.",
|
|
1044
|
+
inputSchema: obj(
|
|
1045
|
+
{
|
|
1046
|
+
booking_slug: str("Slug of the appointment type. Use 'default' for the auto-seeded template."),
|
|
1047
|
+
title: str("Optional new title."),
|
|
1048
|
+
duration_minutes: { type: "number", description: "Optional new duration (5–240)." },
|
|
1049
|
+
description: str("Optional new description (≤800 chars). Empty string clears it."),
|
|
1050
|
+
price: { type: "number", description: "Optional new price. 0 = free." },
|
|
1051
|
+
buffer_before_minutes: { type: "number", description: "Optional. 0–120." },
|
|
1052
|
+
buffer_after_minutes: { type: "number", description: "Optional. 0–120." },
|
|
1053
|
+
max_bookings_per_day: { type: "number", description: "Optional. 1–100. Pass null to remove cap." },
|
|
1054
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1055
|
+
},
|
|
1056
|
+
["booking_slug"],
|
|
1057
|
+
),
|
|
1058
|
+
handler: async (args) => {
|
|
1059
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1060
|
+
const result = await api(
|
|
1061
|
+
"PATCH",
|
|
1062
|
+
`/booking/appointment-types/${encodeURIComponent(args.booking_slug)}`,
|
|
1063
|
+
{
|
|
1064
|
+
body: {
|
|
1065
|
+
title: args.title,
|
|
1066
|
+
duration_minutes: args.duration_minutes,
|
|
1067
|
+
description: args.description,
|
|
1068
|
+
price: args.price,
|
|
1069
|
+
buffer_before_minutes: args.buffer_before_minutes,
|
|
1070
|
+
buffer_after_minutes: args.buffer_after_minutes,
|
|
1071
|
+
max_bookings_per_day: args.max_bookings_per_day,
|
|
1072
|
+
},
|
|
1073
|
+
workspace_id: ws,
|
|
1074
|
+
},
|
|
1075
|
+
);
|
|
1076
|
+
return result;
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
name: "configure_booking",
|
|
1081
|
+
description:
|
|
1082
|
+
"DEPRECATED alias for update_appointment_type({ booking_slug: 'default', ... }). Kept so existing Claude Code sessions don't break. Prefer update_appointment_type for new scripts.",
|
|
1083
|
+
inputSchema: obj(
|
|
1084
|
+
{
|
|
1085
|
+
title: str("Optional new title."),
|
|
1086
|
+
duration_minutes: { type: "number", description: "Optional new duration in minutes." },
|
|
1087
|
+
description: str("Optional description."),
|
|
1088
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1089
|
+
},
|
|
1090
|
+
[],
|
|
1091
|
+
),
|
|
1092
|
+
handler: async (args) => {
|
|
1093
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1094
|
+
const result = await api("PATCH", "/booking/appointment-types/default", {
|
|
1095
|
+
body: {
|
|
1096
|
+
title: args.title,
|
|
1097
|
+
duration_minutes: args.duration_minutes,
|
|
1098
|
+
description: args.description,
|
|
1099
|
+
},
|
|
1100
|
+
workspace_id: ws,
|
|
1101
|
+
});
|
|
1102
|
+
return result;
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
|
|
1106
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1107
|
+
// Intake forms tools — Phase 2.d per tasks/mcp-gap-audit.md
|
|
1108
|
+
// CRUD on intake_forms + list_submissions read path. Template-backed
|
|
1109
|
+
// create_form uses the 6 templates from lib/forms/templates.ts. The old
|
|
1110
|
+
// `customize_intake_form` is kept as a deprecated alias for the default
|
|
1111
|
+
// 'intake' form; new code should use update_form.
|
|
1112
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1113
|
+
|
|
1114
|
+
{
|
|
1115
|
+
name: "list_forms",
|
|
1116
|
+
description:
|
|
1117
|
+
"List intake forms in the workspace. Example: list_forms({}).",
|
|
1118
|
+
inputSchema: obj({
|
|
1119
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1120
|
+
}),
|
|
1121
|
+
handler: async (args) => {
|
|
1122
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1123
|
+
const result = await api("GET", "/forms", { workspace_id: ws });
|
|
1124
|
+
return { ok: true, forms: result.forms ?? [] };
|
|
1125
|
+
},
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
name: "get_form",
|
|
1129
|
+
description:
|
|
1130
|
+
"Fetch one form by id or slug. Example: get_form({ form: 'contact' }) or get_form({ form: 'uuid…' }).",
|
|
1131
|
+
inputSchema: obj(
|
|
1132
|
+
{
|
|
1133
|
+
form: str("Form id (uuid) or slug (e.g., 'contact', 'intake')."),
|
|
1134
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1135
|
+
},
|
|
1136
|
+
["form"],
|
|
1137
|
+
),
|
|
1138
|
+
handler: async (args) => {
|
|
1139
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1140
|
+
const result = await api("GET", `/forms/${encodeURIComponent(args.form)}`, {
|
|
1141
|
+
workspace_id: ws,
|
|
1142
|
+
});
|
|
1143
|
+
return { ok: true, form: result.form ?? null };
|
|
1144
|
+
},
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
name: "create_form",
|
|
1148
|
+
description:
|
|
1149
|
+
"Create a new intake form. Pass template_id to pre-fill fields from a built-in template (contact, lead-qualification, booking-request, nps-feedback, event-registration, blank). Example: create_form({ template_id: 'contact' }) → uses 'Contact us' template. Or pass explicit fields: create_form({ name: 'Intake', fields: [{ key: 'email', label: 'Email', type: 'email', required: true }] }).",
|
|
1150
|
+
inputSchema: obj(
|
|
1151
|
+
{
|
|
1152
|
+
template_id: str("Optional. One of: blank, contact, lead-qualification, booking-request, nps-feedback, event-registration."),
|
|
1153
|
+
name: str("Optional. Falls back to template name or 'New intake form'."),
|
|
1154
|
+
slug: str("Optional URL-safe slug. Falls back to template defaultSlug or slugified name."),
|
|
1155
|
+
fields: {
|
|
1156
|
+
type: "array",
|
|
1157
|
+
description: "Optional field list. Overrides template fields. Each: { key, label, type ('text'|'email'|'tel'|'textarea'|'select'), required, options? }.",
|
|
1158
|
+
items: { type: "object" },
|
|
1159
|
+
},
|
|
1160
|
+
is_active: { type: "boolean", description: "Optional. Defaults to true." },
|
|
1161
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1162
|
+
},
|
|
1163
|
+
[],
|
|
1164
|
+
),
|
|
1165
|
+
handler: async (args) => {
|
|
1166
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1167
|
+
const result = await api("POST", "/forms", {
|
|
1168
|
+
body: {
|
|
1169
|
+
template_id: args.template_id,
|
|
1170
|
+
name: args.name,
|
|
1171
|
+
slug: args.slug,
|
|
1172
|
+
fields: args.fields,
|
|
1173
|
+
is_active: args.is_active,
|
|
1174
|
+
},
|
|
1175
|
+
workspace_id: ws,
|
|
1176
|
+
});
|
|
1177
|
+
return result;
|
|
1178
|
+
},
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
name: "update_form",
|
|
1182
|
+
description:
|
|
1183
|
+
"Update a form. Partial — omit fields to keep them. Replacing `fields` replaces the whole array (each field: { key, label, type, required, options? }). Example: update_form({ form: 'intake', fields: [...] }).",
|
|
1184
|
+
inputSchema: obj(
|
|
1185
|
+
{
|
|
1186
|
+
form: str("Form id (uuid) or slug."),
|
|
1187
|
+
name: str("Optional new name."),
|
|
1188
|
+
slug: str("Optional new slug (URL-safe)."),
|
|
1189
|
+
fields: {
|
|
1190
|
+
type: "array",
|
|
1191
|
+
description: "Optional new field array. Whole replacement.",
|
|
1192
|
+
items: { type: "object" },
|
|
1193
|
+
},
|
|
1194
|
+
is_active: { type: "boolean", description: "Optional. Toggle publish state." },
|
|
1195
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1196
|
+
},
|
|
1197
|
+
["form"],
|
|
1198
|
+
),
|
|
1199
|
+
handler: async (args) => {
|
|
1200
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1201
|
+
const result = await api("PATCH", `/forms/${encodeURIComponent(args.form)}`, {
|
|
1202
|
+
body: {
|
|
1203
|
+
name: args.name,
|
|
1204
|
+
slug: args.slug,
|
|
1205
|
+
fields: args.fields,
|
|
1206
|
+
is_active: args.is_active,
|
|
1207
|
+
},
|
|
1208
|
+
workspace_id: ws,
|
|
1209
|
+
});
|
|
1210
|
+
return result;
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
name: "delete_form",
|
|
1215
|
+
description:
|
|
1216
|
+
"Delete a form. Irreversible. Submissions are NOT deleted (form_submissions has ON DELETE SET NULL on form_id). Example: delete_form({ form: 'old-survey' }).",
|
|
1217
|
+
inputSchema: obj(
|
|
1218
|
+
{
|
|
1219
|
+
form: str("Form id (uuid) or slug."),
|
|
1220
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1221
|
+
},
|
|
1222
|
+
["form"],
|
|
1223
|
+
),
|
|
1224
|
+
handler: async (args) => {
|
|
1225
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1226
|
+
await api("DELETE", `/forms/${encodeURIComponent(args.form)}`, { workspace_id: ws });
|
|
1227
|
+
return { ok: true, deleted: args.form };
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
name: "list_submissions",
|
|
1232
|
+
description:
|
|
1233
|
+
"List submissions for a form. Example: list_submissions({ form_id: 'uuid…' }).",
|
|
1234
|
+
inputSchema: obj(
|
|
1235
|
+
{
|
|
1236
|
+
form_id: str("UUID of the form. Slug lookup not supported on this endpoint — use get_form first if you only have the slug."),
|
|
1237
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1238
|
+
},
|
|
1239
|
+
["form_id"],
|
|
1240
|
+
),
|
|
1241
|
+
handler: async (args) => {
|
|
1242
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1243
|
+
const result = await api(
|
|
1244
|
+
"GET",
|
|
1245
|
+
`/forms/${encodeURIComponent(args.form_id)}/submissions`,
|
|
1246
|
+
{ workspace_id: ws },
|
|
1247
|
+
);
|
|
1248
|
+
return { ok: true, submissions: result.data ?? result.submissions ?? [] };
|
|
1249
|
+
},
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
name: "customize_intake_form",
|
|
1253
|
+
description:
|
|
1254
|
+
"DEPRECATED alias for update_form({ form: 'intake', fields }). Only edits the auto-seeded default form; prefer update_form for new scripts so you can target any form in the workspace.",
|
|
1255
|
+
inputSchema: obj(
|
|
1256
|
+
{
|
|
1257
|
+
fields: {
|
|
1258
|
+
type: "array",
|
|
1259
|
+
description: "Replacement field list.",
|
|
1260
|
+
items: { type: "object" },
|
|
1261
|
+
},
|
|
1262
|
+
form_name: str("Optional new display name for the default form."),
|
|
1263
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1264
|
+
},
|
|
1265
|
+
[],
|
|
1266
|
+
),
|
|
1267
|
+
handler: async (args) => {
|
|
1268
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1269
|
+
const result = await api("PATCH", "/forms/intake", {
|
|
1270
|
+
body: { name: args.form_name, fields: args.fields },
|
|
1271
|
+
workspace_id: ws,
|
|
1272
|
+
});
|
|
1273
|
+
return result;
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
|
|
1277
|
+
// ===== Phase 3 — Email + conversation tools =====
|
|
1278
|
+
|
|
1279
|
+
{
|
|
1280
|
+
name: "send_email",
|
|
1281
|
+
description:
|
|
1282
|
+
"Send a one-off email through the workspace's configured provider (Resend by default). Checks the suppression list before sending and skips with {suppressed: true} if the recipient has opted out. Example: send_email({ to: 'alex@acme.com', subject: 'Welcome', body: 'Thanks for signing up', contact_id: 'ctc_123' })",
|
|
1283
|
+
inputSchema: obj(
|
|
1284
|
+
{
|
|
1285
|
+
to: str("Recipient email address."),
|
|
1286
|
+
subject: str("Email subject line."),
|
|
1287
|
+
body: str("Plain-text body — rendered into the default HTML shell."),
|
|
1288
|
+
contact_id: str("Optional. Links the email to a CRM contact for threading."),
|
|
1289
|
+
provider: str("Optional. Force a specific provider (default: resend)."),
|
|
1290
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1291
|
+
},
|
|
1292
|
+
["to", "subject", "body"],
|
|
1293
|
+
),
|
|
1294
|
+
handler: async (args) => {
|
|
1295
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1296
|
+
const result = await api("POST", "/emails", {
|
|
1297
|
+
body: {
|
|
1298
|
+
to: args.to,
|
|
1299
|
+
subject: args.subject,
|
|
1300
|
+
body: args.body,
|
|
1301
|
+
contactId: args.contact_id ?? null,
|
|
1302
|
+
provider: args.provider ?? null,
|
|
1303
|
+
},
|
|
1304
|
+
workspace_id: ws,
|
|
1305
|
+
});
|
|
1306
|
+
return result;
|
|
1307
|
+
},
|
|
1308
|
+
},
|
|
1309
|
+
{
|
|
1310
|
+
name: "list_emails",
|
|
1311
|
+
description:
|
|
1312
|
+
"List recent emails sent from the workspace, newest first. Useful for checking delivery status before following up.",
|
|
1313
|
+
inputSchema: obj(
|
|
1314
|
+
{
|
|
1315
|
+
limit: {
|
|
1316
|
+
type: "number",
|
|
1317
|
+
description: "Max rows to return (default 50, max 200).",
|
|
1318
|
+
},
|
|
1319
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1320
|
+
},
|
|
1321
|
+
[],
|
|
1322
|
+
),
|
|
1323
|
+
handler: async (args) => {
|
|
1324
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1325
|
+
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1326
|
+
const result = await api("GET", `/emails${qs}`, { workspace_id: ws });
|
|
1327
|
+
return result;
|
|
1328
|
+
},
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
name: "get_email",
|
|
1332
|
+
description:
|
|
1333
|
+
"Fetch a single email with its full provider-event history (sent / delivered / opened / clicked / bounced).",
|
|
1334
|
+
inputSchema: obj(
|
|
1335
|
+
{
|
|
1336
|
+
email_id: str("Email ID returned from send_email or list_emails."),
|
|
1337
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1338
|
+
},
|
|
1339
|
+
["email_id"],
|
|
1340
|
+
),
|
|
1341
|
+
handler: async (args) => {
|
|
1342
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1343
|
+
const result = await api("GET", `/emails/${encodeURIComponent(args.email_id)}`, {
|
|
1344
|
+
workspace_id: ws,
|
|
1345
|
+
});
|
|
1346
|
+
return result;
|
|
1347
|
+
},
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
name: "list_suppressions",
|
|
1351
|
+
description:
|
|
1352
|
+
"List all suppressed email addresses for the workspace — who is opted out and why (manual / unsubscribe / bounce / complaint).",
|
|
1353
|
+
inputSchema: obj(
|
|
1354
|
+
{
|
|
1355
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1356
|
+
},
|
|
1357
|
+
[],
|
|
1358
|
+
),
|
|
1359
|
+
handler: async (args) => {
|
|
1360
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1361
|
+
const result = await api("GET", "/emails/suppressions", { workspace_id: ws });
|
|
1362
|
+
return result;
|
|
1363
|
+
},
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
name: "suppress_email",
|
|
1367
|
+
description:
|
|
1368
|
+
"Add an email address to the workspace suppression list so future sends skip it. Use for manual unsubscribes or policy blocks.",
|
|
1369
|
+
inputSchema: obj(
|
|
1370
|
+
{
|
|
1371
|
+
email: str("Email address to suppress."),
|
|
1372
|
+
reason: str(
|
|
1373
|
+
"Reason code: 'manual' | 'unsubscribe' | 'bounce' | 'complaint'. Default: 'manual'.",
|
|
1374
|
+
),
|
|
1375
|
+
source: str("Optional free-form provenance tag."),
|
|
1376
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1377
|
+
},
|
|
1378
|
+
["email"],
|
|
1379
|
+
),
|
|
1380
|
+
handler: async (args) => {
|
|
1381
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1382
|
+
const result = await api("POST", "/emails/suppressions", {
|
|
1383
|
+
body: {
|
|
1384
|
+
email: args.email,
|
|
1385
|
+
reason: args.reason ?? "manual",
|
|
1386
|
+
source: args.source ?? null,
|
|
1387
|
+
},
|
|
1388
|
+
workspace_id: ws,
|
|
1389
|
+
});
|
|
1390
|
+
return result;
|
|
1391
|
+
},
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
name: "unsuppress_email",
|
|
1395
|
+
description:
|
|
1396
|
+
"Remove an email address from the workspace suppression list so future sends go through again.",
|
|
1397
|
+
inputSchema: obj(
|
|
1398
|
+
{
|
|
1399
|
+
email: str("Email address to un-suppress."),
|
|
1400
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1401
|
+
},
|
|
1402
|
+
["email"],
|
|
1403
|
+
),
|
|
1404
|
+
handler: async (args) => {
|
|
1405
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1406
|
+
const result = await api(
|
|
1407
|
+
"DELETE",
|
|
1408
|
+
`/emails/suppressions/${encodeURIComponent(args.email)}`,
|
|
1409
|
+
{ workspace_id: ws },
|
|
1410
|
+
);
|
|
1411
|
+
return result;
|
|
1412
|
+
},
|
|
1413
|
+
},
|
|
1414
|
+
// ===== Phase 4 — SMS tools =====
|
|
1415
|
+
|
|
1416
|
+
{
|
|
1417
|
+
name: "send_sms",
|
|
1418
|
+
description:
|
|
1419
|
+
"Send an SMS via the workspace's Twilio integration. Checks the SMS suppression list first (STOP keyword + carrier blocks + manual opt-outs) and skips with {suppressed: true} if the recipient has opted out. Example: send_sms({ to: '+15551234567', body: 'Your appointment is confirmed for Tuesday 2pm', contact_id: 'ctc_123' })",
|
|
1420
|
+
inputSchema: obj(
|
|
1421
|
+
{
|
|
1422
|
+
to: str("Recipient phone number. E.164 or 10-digit US will be normalized."),
|
|
1423
|
+
body: str("SMS body. Twilio will segment if over 160 chars; charges per segment."),
|
|
1424
|
+
contact_id: str("Optional. Links the message to a CRM contact for threading."),
|
|
1425
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1426
|
+
},
|
|
1427
|
+
["to", "body"],
|
|
1428
|
+
),
|
|
1429
|
+
handler: async (args) => {
|
|
1430
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1431
|
+
const result = await api("POST", "/sms", {
|
|
1432
|
+
body: {
|
|
1433
|
+
to: args.to,
|
|
1434
|
+
body: args.body,
|
|
1435
|
+
contactId: args.contact_id ?? null,
|
|
1436
|
+
},
|
|
1437
|
+
workspace_id: ws,
|
|
1438
|
+
});
|
|
1439
|
+
return result;
|
|
1440
|
+
},
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
name: "list_sms",
|
|
1444
|
+
description:
|
|
1445
|
+
"List recent SMS messages (inbound + outbound) for the workspace, newest first.",
|
|
1446
|
+
inputSchema: obj(
|
|
1447
|
+
{
|
|
1448
|
+
limit: {
|
|
1449
|
+
type: "number",
|
|
1450
|
+
description: "Max rows to return (default 50, max 200).",
|
|
1451
|
+
},
|
|
1452
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1453
|
+
},
|
|
1454
|
+
[],
|
|
1455
|
+
),
|
|
1456
|
+
handler: async (args) => {
|
|
1457
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1458
|
+
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1459
|
+
const result = await api("GET", `/sms${qs}`, { workspace_id: ws });
|
|
1460
|
+
return result;
|
|
1461
|
+
},
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
name: "get_sms",
|
|
1465
|
+
description:
|
|
1466
|
+
"Fetch a single SMS with its full provider-event history (queued / sent / delivered / failed / undelivered).",
|
|
1467
|
+
inputSchema: obj(
|
|
1468
|
+
{
|
|
1469
|
+
sms_id: str("SMS ID returned from send_sms or list_sms."),
|
|
1470
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1471
|
+
},
|
|
1472
|
+
["sms_id"],
|
|
1473
|
+
),
|
|
1474
|
+
handler: async (args) => {
|
|
1475
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1476
|
+
const result = await api("GET", `/sms/${encodeURIComponent(args.sms_id)}`, {
|
|
1477
|
+
workspace_id: ws,
|
|
1478
|
+
});
|
|
1479
|
+
return result;
|
|
1480
|
+
},
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
name: "list_sms_suppressions",
|
|
1484
|
+
description:
|
|
1485
|
+
"List all suppressed phone numbers for the workspace — who is opted out and why (manual / stop_keyword / carrier_block / complaint).",
|
|
1486
|
+
inputSchema: obj(
|
|
1487
|
+
{
|
|
1488
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1489
|
+
},
|
|
1490
|
+
[],
|
|
1491
|
+
),
|
|
1492
|
+
handler: async (args) => {
|
|
1493
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1494
|
+
const result = await api("GET", "/sms/suppressions", { workspace_id: ws });
|
|
1495
|
+
return result;
|
|
1496
|
+
},
|
|
1497
|
+
},
|
|
1498
|
+
{
|
|
1499
|
+
name: "suppress_phone",
|
|
1500
|
+
description:
|
|
1501
|
+
"Add a phone number to the SMS suppression list so future SMS sends skip it. STOP replies + carrier permanent-failure codes auto-suppress via the Twilio webhook; use this for manual opt-outs.",
|
|
1502
|
+
inputSchema: obj(
|
|
1503
|
+
{
|
|
1504
|
+
phone: str("Phone number to suppress. E.164 or 10-digit US will be normalized."),
|
|
1505
|
+
reason: str(
|
|
1506
|
+
"Reason code: 'manual' | 'stop_keyword' | 'carrier_block' | 'complaint'. Default: 'manual'.",
|
|
1507
|
+
),
|
|
1508
|
+
source: str("Optional free-form provenance tag."),
|
|
1509
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1510
|
+
},
|
|
1511
|
+
["phone"],
|
|
1512
|
+
),
|
|
1513
|
+
handler: async (args) => {
|
|
1514
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1515
|
+
const result = await api("POST", "/sms/suppressions", {
|
|
1516
|
+
body: {
|
|
1517
|
+
phone: args.phone,
|
|
1518
|
+
reason: args.reason ?? "manual",
|
|
1519
|
+
source: args.source ?? null,
|
|
1520
|
+
},
|
|
1521
|
+
workspace_id: ws,
|
|
1522
|
+
});
|
|
1523
|
+
return result;
|
|
1524
|
+
},
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
name: "unsuppress_phone",
|
|
1528
|
+
description:
|
|
1529
|
+
"Remove a phone number from the SMS suppression list so future sends go through again.",
|
|
1530
|
+
inputSchema: obj(
|
|
1531
|
+
{
|
|
1532
|
+
phone: str("Phone number to un-suppress."),
|
|
1533
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1534
|
+
},
|
|
1535
|
+
["phone"],
|
|
1536
|
+
),
|
|
1537
|
+
handler: async (args) => {
|
|
1538
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1539
|
+
const result = await api(
|
|
1540
|
+
"DELETE",
|
|
1541
|
+
`/sms/suppressions/${encodeURIComponent(args.phone)}`,
|
|
1542
|
+
{ workspace_id: ws },
|
|
1543
|
+
);
|
|
1544
|
+
return result;
|
|
1545
|
+
},
|
|
1546
|
+
},
|
|
1547
|
+
|
|
1548
|
+
// ===== Phase 5 — Payments tools (Stripe Connect Standard) =====
|
|
1549
|
+
|
|
1550
|
+
{
|
|
1551
|
+
name: "create_invoice",
|
|
1552
|
+
description:
|
|
1553
|
+
"Draft a Stripe invoice on the workspace's connected Stripe account. Invoice is created but not sent — call send_invoice separately so agents can review before dispatch. Contact must have an email. Example: create_invoice({ contact_id: 'ctc_123', items: [{ description: '1 hr consulting', quantity: 1, unit_amount: 200 }], due_at: '2026-05-21T00:00:00Z' })",
|
|
1554
|
+
inputSchema: obj(
|
|
1555
|
+
{
|
|
1556
|
+
contact_id: str("CRM contact to bill."),
|
|
1557
|
+
items: {
|
|
1558
|
+
type: "array",
|
|
1559
|
+
description: "Line items. Each: {description, quantity, unit_amount} (unit_amount in the workspace's currency).",
|
|
1560
|
+
items: { type: "object" },
|
|
1561
|
+
},
|
|
1562
|
+
currency: str("3-letter ISO currency code. Defaults to USD."),
|
|
1563
|
+
due_at: str("ISO timestamp for invoice due date. Defaults to 30 days out."),
|
|
1564
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1565
|
+
},
|
|
1566
|
+
["contact_id", "items"],
|
|
1567
|
+
),
|
|
1568
|
+
handler: async (args) => {
|
|
1569
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1570
|
+
const normalizedItems = (args.items ?? []).map((item) => ({
|
|
1571
|
+
description: item.description,
|
|
1572
|
+
quantity: item.quantity ?? 1,
|
|
1573
|
+
unitAmount: item.unit_amount ?? item.unitAmount,
|
|
1574
|
+
currency: item.currency,
|
|
1575
|
+
}));
|
|
1576
|
+
const result = await api("POST", "/invoices", {
|
|
1577
|
+
body: {
|
|
1578
|
+
contactId: args.contact_id,
|
|
1579
|
+
items: normalizedItems,
|
|
1580
|
+
currency: args.currency ?? null,
|
|
1581
|
+
dueAt: args.due_at ?? null,
|
|
1582
|
+
},
|
|
1583
|
+
workspace_id: ws,
|
|
1584
|
+
});
|
|
1585
|
+
return result;
|
|
1586
|
+
},
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
name: "list_invoices",
|
|
1590
|
+
description:
|
|
1591
|
+
"List workspace invoices (draft + sent + paid + past_due + voided), newest first.",
|
|
1592
|
+
inputSchema: obj(
|
|
1593
|
+
{
|
|
1594
|
+
limit: {
|
|
1595
|
+
type: "number",
|
|
1596
|
+
description: "Max rows (default 50, max 200).",
|
|
1597
|
+
},
|
|
1598
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1599
|
+
},
|
|
1600
|
+
[],
|
|
1601
|
+
),
|
|
1602
|
+
handler: async (args) => {
|
|
1603
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1604
|
+
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1605
|
+
return api("GET", `/invoices${qs}`, { workspace_id: ws });
|
|
1606
|
+
},
|
|
1607
|
+
},
|
|
1608
|
+
{
|
|
1609
|
+
name: "get_invoice",
|
|
1610
|
+
description:
|
|
1611
|
+
"Fetch an invoice + its line items + hosted invoice URL (for payment).",
|
|
1612
|
+
inputSchema: obj(
|
|
1613
|
+
{
|
|
1614
|
+
invoice_id: str("Invoice ID returned from create_invoice or list_invoices."),
|
|
1615
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1616
|
+
},
|
|
1617
|
+
["invoice_id"],
|
|
1618
|
+
),
|
|
1619
|
+
handler: async (args) => {
|
|
1620
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1621
|
+
return api("GET", `/invoices/${encodeURIComponent(args.invoice_id)}`, { workspace_id: ws });
|
|
1622
|
+
},
|
|
1623
|
+
},
|
|
1624
|
+
{
|
|
1625
|
+
name: "send_invoice",
|
|
1626
|
+
description:
|
|
1627
|
+
"Dispatch a draft invoice to the contact via Stripe (Stripe emails the invoice + provides a hosted pay page).",
|
|
1628
|
+
inputSchema: obj(
|
|
1629
|
+
{
|
|
1630
|
+
invoice_id: str("Invoice to send."),
|
|
1631
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1632
|
+
},
|
|
1633
|
+
["invoice_id"],
|
|
1634
|
+
),
|
|
1635
|
+
handler: async (args) => {
|
|
1636
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1637
|
+
return api("POST", `/invoices/${encodeURIComponent(args.invoice_id)}/send`, { workspace_id: ws });
|
|
1638
|
+
},
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
name: "void_invoice",
|
|
1642
|
+
description:
|
|
1643
|
+
"Void an invoice (undo a billing error). Only valid for draft / open invoices; paid invoices must be refunded instead.",
|
|
1644
|
+
inputSchema: obj(
|
|
1645
|
+
{
|
|
1646
|
+
invoice_id: str("Invoice to void."),
|
|
1647
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1648
|
+
},
|
|
1649
|
+
["invoice_id"],
|
|
1650
|
+
),
|
|
1651
|
+
handler: async (args) => {
|
|
1652
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1653
|
+
return api("POST", `/invoices/${encodeURIComponent(args.invoice_id)}/void`, { workspace_id: ws });
|
|
1654
|
+
},
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
name: "create_subscription",
|
|
1658
|
+
description:
|
|
1659
|
+
"Start a recurring subscription for a contact against a Stripe Price id. The Price must already exist in the workspace's Stripe dashboard — v1 does not create Prices. Example: create_subscription({ contact_id: 'ctc_123', price_id: 'price_1ABCxyz', trial_days: 14 })",
|
|
1660
|
+
inputSchema: obj(
|
|
1661
|
+
{
|
|
1662
|
+
contact_id: str("CRM contact to subscribe."),
|
|
1663
|
+
price_id: str("Stripe Price id (e.g., 'price_1ABC...') from the workspace's Stripe dashboard."),
|
|
1664
|
+
trial_days: {
|
|
1665
|
+
type: "number",
|
|
1666
|
+
description: "Optional free trial days before first charge.",
|
|
1667
|
+
},
|
|
1668
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1669
|
+
},
|
|
1670
|
+
["contact_id", "price_id"],
|
|
1671
|
+
),
|
|
1672
|
+
handler: async (args) => {
|
|
1673
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1674
|
+
return api("POST", "/subscriptions", {
|
|
1675
|
+
body: {
|
|
1676
|
+
contactId: args.contact_id,
|
|
1677
|
+
priceId: args.price_id,
|
|
1678
|
+
trialDays: args.trial_days,
|
|
1679
|
+
},
|
|
1680
|
+
workspace_id: ws,
|
|
1681
|
+
});
|
|
1682
|
+
},
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
name: "list_subscriptions",
|
|
1686
|
+
description:
|
|
1687
|
+
"List workspace subscriptions (active + trialing + past_due + canceled), newest first.",
|
|
1688
|
+
inputSchema: obj(
|
|
1689
|
+
{
|
|
1690
|
+
limit: {
|
|
1691
|
+
type: "number",
|
|
1692
|
+
description: "Max rows (default 50, max 200).",
|
|
1693
|
+
},
|
|
1694
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1695
|
+
},
|
|
1696
|
+
[],
|
|
1697
|
+
),
|
|
1698
|
+
handler: async (args) => {
|
|
1699
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1700
|
+
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1701
|
+
return api("GET", `/subscriptions${qs}`, { workspace_id: ws });
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
name: "cancel_subscription",
|
|
1706
|
+
description:
|
|
1707
|
+
"Cancel a subscription. Default: cancel at period end (contact keeps access until renewal date). Pass immediate=true for an instant termination + prorated refund.",
|
|
1708
|
+
inputSchema: obj(
|
|
1709
|
+
{
|
|
1710
|
+
subscription_id: str("Subscription to cancel."),
|
|
1711
|
+
immediate: {
|
|
1712
|
+
type: "boolean",
|
|
1713
|
+
description: "If true, terminate now. Default: cancel at period end.",
|
|
1714
|
+
},
|
|
1715
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1716
|
+
},
|
|
1717
|
+
["subscription_id"],
|
|
1718
|
+
),
|
|
1719
|
+
handler: async (args) => {
|
|
1720
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1721
|
+
return api("POST", `/subscriptions/${encodeURIComponent(args.subscription_id)}/cancel`, {
|
|
1722
|
+
body: { immediate: Boolean(args.immediate) },
|
|
1723
|
+
workspace_id: ws,
|
|
1724
|
+
});
|
|
1725
|
+
},
|
|
1726
|
+
},
|
|
1727
|
+
{
|
|
1728
|
+
name: "list_payments",
|
|
1729
|
+
description:
|
|
1730
|
+
"List recent payments (completed + failed + refunded + disputed) across the workspace, newest first.",
|
|
1731
|
+
inputSchema: obj(
|
|
1732
|
+
{
|
|
1733
|
+
limit: {
|
|
1734
|
+
type: "number",
|
|
1735
|
+
description: "Max rows (default 50, max 200).",
|
|
1736
|
+
},
|
|
1737
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1738
|
+
},
|
|
1739
|
+
[],
|
|
1740
|
+
),
|
|
1741
|
+
handler: async (args) => {
|
|
1742
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1743
|
+
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1744
|
+
return api("GET", `/payments${qs}`, { workspace_id: ws });
|
|
1745
|
+
},
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
name: "get_payment",
|
|
1749
|
+
description:
|
|
1750
|
+
"Fetch a single payment record with status + refund/dispute state.",
|
|
1751
|
+
inputSchema: obj(
|
|
1752
|
+
{
|
|
1753
|
+
payment_id: str("Payment ID from list_payments."),
|
|
1754
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1755
|
+
},
|
|
1756
|
+
["payment_id"],
|
|
1757
|
+
),
|
|
1758
|
+
handler: async (args) => {
|
|
1759
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1760
|
+
return api("GET", `/payments/${encodeURIComponent(args.payment_id)}`, { workspace_id: ws });
|
|
1761
|
+
},
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
name: "refund_payment",
|
|
1765
|
+
description:
|
|
1766
|
+
"Refund a payment. Omit amount to refund the full payment; pass amount for a partial refund. reason should be 'duplicate' | 'fraudulent' | 'requested_by_customer'.",
|
|
1767
|
+
inputSchema: obj(
|
|
1768
|
+
{
|
|
1769
|
+
payment_id: str("Payment to refund."),
|
|
1770
|
+
amount: {
|
|
1771
|
+
type: "number",
|
|
1772
|
+
description: "Optional partial-refund amount in the payment's currency. Omit to refund in full.",
|
|
1773
|
+
},
|
|
1774
|
+
reason: str("'duplicate' | 'fraudulent' | 'requested_by_customer'. Default: requested_by_customer."),
|
|
1775
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1776
|
+
},
|
|
1777
|
+
["payment_id"],
|
|
1778
|
+
),
|
|
1779
|
+
handler: async (args) => {
|
|
1780
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1781
|
+
return api("POST", `/payments/${encodeURIComponent(args.payment_id)}/refund`, {
|
|
1782
|
+
body: {
|
|
1783
|
+
amount: args.amount,
|
|
1784
|
+
reason: args.reason ?? "requested_by_customer",
|
|
1785
|
+
},
|
|
1786
|
+
workspace_id: ws,
|
|
1787
|
+
});
|
|
1788
|
+
},
|
|
1789
|
+
},
|
|
1790
|
+
|
|
1791
|
+
// ===== Phase 6 — Landing Pages tools =====
|
|
1792
|
+
|
|
1793
|
+
{
|
|
1794
|
+
name: "list_landing_pages",
|
|
1795
|
+
description:
|
|
1796
|
+
"List the workspace's landing pages (draft + published), newest-updated first.",
|
|
1797
|
+
inputSchema: obj(
|
|
1798
|
+
{
|
|
1799
|
+
limit: { type: "number", description: "Max rows (default 50, max 200)." },
|
|
1800
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1801
|
+
},
|
|
1802
|
+
[],
|
|
1803
|
+
),
|
|
1804
|
+
handler: async (args) => {
|
|
1805
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1806
|
+
const qs = typeof args.limit === "number" ? `?limit=${Math.min(args.limit, 200)}` : "";
|
|
1807
|
+
return api("GET", `/landing${qs}`, { workspace_id: ws });
|
|
1808
|
+
},
|
|
1809
|
+
},
|
|
1810
|
+
{
|
|
1811
|
+
name: "get_landing_page",
|
|
1812
|
+
description:
|
|
1813
|
+
"Fetch a single landing page with its full Puck payload + metadata.",
|
|
1814
|
+
inputSchema: obj(
|
|
1815
|
+
{
|
|
1816
|
+
page_id: str("Landing page ID from list_landing_pages."),
|
|
1817
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1818
|
+
},
|
|
1819
|
+
["page_id"],
|
|
1820
|
+
),
|
|
1821
|
+
handler: async (args) => {
|
|
1822
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1823
|
+
return api("GET", `/landing/${encodeURIComponent(args.page_id)}`, { workspace_id: ws });
|
|
1824
|
+
},
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
name: "create_landing_page",
|
|
1828
|
+
description:
|
|
1829
|
+
"Create a landing page from an optional Puck payload. Without puck_data, creates a blank draft. With puck_data, validates the payload against the Puck schema and rejects on mismatch. Set published=true to publish immediately.",
|
|
1830
|
+
inputSchema: obj(
|
|
1831
|
+
{
|
|
1832
|
+
title: str("Page title (used for the dashboard; not the public URL)."),
|
|
1833
|
+
slug: str("Optional URL slug. Derived from title if omitted."),
|
|
1834
|
+
puck_data: {
|
|
1835
|
+
type: "object",
|
|
1836
|
+
description: "Optional Puck payload { content: [], root: {props}, zones: {} }.",
|
|
1837
|
+
},
|
|
1838
|
+
published: {
|
|
1839
|
+
type: "boolean",
|
|
1840
|
+
description: "If true, publish the page immediately. Default: draft.",
|
|
1841
|
+
},
|
|
1842
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1843
|
+
},
|
|
1844
|
+
["title"],
|
|
1845
|
+
),
|
|
1846
|
+
handler: async (args) => {
|
|
1847
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1848
|
+
return api("POST", "/landing", {
|
|
1849
|
+
body: {
|
|
1850
|
+
title: args.title,
|
|
1851
|
+
slug: args.slug,
|
|
1852
|
+
puckData: args.puck_data,
|
|
1853
|
+
published: Boolean(args.published),
|
|
1854
|
+
},
|
|
1855
|
+
workspace_id: ws,
|
|
1856
|
+
});
|
|
1857
|
+
},
|
|
1858
|
+
},
|
|
1859
|
+
{
|
|
1860
|
+
name: "update_landing_page",
|
|
1861
|
+
description:
|
|
1862
|
+
"Update a landing page's title and/or Puck payload. Validates puck_data on the way through. Does not change publish status — use publish_landing_page for that.",
|
|
1863
|
+
inputSchema: obj(
|
|
1864
|
+
{
|
|
1865
|
+
page_id: str("Landing page to update."),
|
|
1866
|
+
title: str("Optional new title."),
|
|
1867
|
+
puck_data: {
|
|
1868
|
+
type: "object",
|
|
1869
|
+
description: "Optional new Puck payload. Pass null to clear.",
|
|
1870
|
+
},
|
|
1871
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1872
|
+
},
|
|
1873
|
+
["page_id"],
|
|
1874
|
+
),
|
|
1875
|
+
handler: async (args) => {
|
|
1876
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1877
|
+
const bodyObj = {};
|
|
1878
|
+
if (typeof args.title === "string") bodyObj.title = args.title;
|
|
1879
|
+
if (args.puck_data !== undefined) bodyObj.puckData = args.puck_data;
|
|
1880
|
+
return api("PATCH", `/landing/${encodeURIComponent(args.page_id)}`, {
|
|
1881
|
+
body: bodyObj,
|
|
1882
|
+
workspace_id: ws,
|
|
1883
|
+
});
|
|
1884
|
+
},
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
name: "publish_landing_page",
|
|
1888
|
+
description:
|
|
1889
|
+
"Flip a landing page between draft and published. Publishing busts the public-URL cache immediately and emits landing.published. Pass published=false to unpublish.",
|
|
1890
|
+
inputSchema: obj(
|
|
1891
|
+
{
|
|
1892
|
+
page_id: str("Landing page to publish."),
|
|
1893
|
+
published: {
|
|
1894
|
+
type: "boolean",
|
|
1895
|
+
description: "true = publish (default), false = unpublish.",
|
|
1896
|
+
},
|
|
1897
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1898
|
+
},
|
|
1899
|
+
["page_id"],
|
|
1900
|
+
),
|
|
1901
|
+
handler: async (args) => {
|
|
1902
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1903
|
+
return api("POST", `/landing/${encodeURIComponent(args.page_id)}/publish`, {
|
|
1904
|
+
body: { published: args.published !== false },
|
|
1905
|
+
workspace_id: ws,
|
|
1906
|
+
});
|
|
1907
|
+
},
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
name: "list_landing_templates",
|
|
1911
|
+
description:
|
|
1912
|
+
"List the pre-built vertical landing-page templates. Each has a validated Puck payload ready to seed a new page via create_landing_page({puck_data: template.payload}).",
|
|
1913
|
+
inputSchema: obj(
|
|
1914
|
+
{
|
|
1915
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1916
|
+
},
|
|
1917
|
+
[],
|
|
1918
|
+
),
|
|
1919
|
+
handler: async (args) => {
|
|
1920
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1921
|
+
return api("GET", "/landing/templates", { workspace_id: ws });
|
|
1922
|
+
},
|
|
1923
|
+
},
|
|
1924
|
+
{
|
|
1925
|
+
name: "get_landing_template",
|
|
1926
|
+
description:
|
|
1927
|
+
"Fetch a single landing-page template including its Puck payload. Pair with create_landing_page to seed a new page from the template.",
|
|
1928
|
+
inputSchema: obj(
|
|
1929
|
+
{
|
|
1930
|
+
template_id: str("Template ID from list_landing_templates."),
|
|
1931
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1932
|
+
},
|
|
1933
|
+
["template_id"],
|
|
1934
|
+
),
|
|
1935
|
+
handler: async (args) => {
|
|
1936
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1937
|
+
return api("GET", `/landing/templates/${encodeURIComponent(args.template_id)}`, { workspace_id: ws });
|
|
1938
|
+
},
|
|
1939
|
+
},
|
|
1940
|
+
{
|
|
1941
|
+
name: "generate_landing_page",
|
|
1942
|
+
description:
|
|
1943
|
+
"Generate a Puck landing-page payload from a natural-language prompt using Claude + the workspace's Soul + theme. Returns the payload (validated against the Puck schema) but does NOT persist — pair with create_landing_page to save the result. Example: generate_landing_page({ prompt: 'A landing for a Laval dental clinic, focus on new-patient consultations' })",
|
|
1944
|
+
inputSchema: obj(
|
|
1945
|
+
{
|
|
1946
|
+
prompt: str("One-sentence page description. The more specific, the better."),
|
|
1947
|
+
existing: {
|
|
1948
|
+
type: "object",
|
|
1949
|
+
description: "Optional existing Puck payload to revise rather than start fresh.",
|
|
1950
|
+
},
|
|
1951
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1952
|
+
},
|
|
1953
|
+
["prompt"],
|
|
1954
|
+
),
|
|
1955
|
+
handler: async (args) => {
|
|
1956
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1957
|
+
return api("POST", "/landing/generate", {
|
|
1958
|
+
body: {
|
|
1959
|
+
prompt: args.prompt,
|
|
1960
|
+
existing: args.existing,
|
|
1961
|
+
},
|
|
1962
|
+
workspace_id: ws,
|
|
1963
|
+
});
|
|
1964
|
+
},
|
|
1965
|
+
},
|
|
1966
|
+
|
|
1967
|
+
{
|
|
1968
|
+
name: "send_conversation_turn",
|
|
1969
|
+
description:
|
|
1970
|
+
"Route an incoming message through the Conversation Primitive runtime. Loads prior turns for (contact, channel), generates a Soul-aware reply with Claude, writes both inbound + outbound turns, and emits conversation.turn.received / sent events. Use when building an always-on conversational agent (speed-to-lead, qualification chatbot). Example: send_conversation_turn({ contact_id: 'ctc_123', channel: 'sms', message: 'Do you have Saturday appointments?' })",
|
|
1971
|
+
inputSchema: obj(
|
|
1972
|
+
{
|
|
1973
|
+
contact_id: str("CRM contact to converse with."),
|
|
1974
|
+
channel: str("Transport channel: 'email' | 'sms'."),
|
|
1975
|
+
message: str("Incoming message content to reason about."),
|
|
1976
|
+
conversation_id: str(
|
|
1977
|
+
"Optional existing conversation id. Omit to let the runtime reuse the most recent active thread or open a new one.",
|
|
1978
|
+
),
|
|
1979
|
+
subject: str("Optional subject for email threads."),
|
|
1980
|
+
workspace_id: str("Optional. Falls back to the active workspace."),
|
|
1981
|
+
},
|
|
1982
|
+
["contact_id", "channel", "message"],
|
|
1983
|
+
),
|
|
1984
|
+
handler: async (args) => {
|
|
1985
|
+
const ws = wsOrDefault(args.workspace_id);
|
|
1986
|
+
const result = await api("POST", "/conversations/turn", {
|
|
1987
|
+
body: {
|
|
1988
|
+
contactId: args.contact_id,
|
|
1989
|
+
channel: args.channel,
|
|
1990
|
+
message: args.message,
|
|
1991
|
+
conversationId: args.conversation_id ?? null,
|
|
1992
|
+
subject: args.subject ?? null,
|
|
1993
|
+
},
|
|
1994
|
+
workspace_id: ws,
|
|
1995
|
+
});
|
|
1996
|
+
return result;
|
|
1997
|
+
},
|
|
1998
|
+
},
|
|
1999
|
+
];
|
|
2000
|
+
|
|
2001
|
+
export const TOOL_MAP = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
|