@sutraspaces/mcp-server 1.0.1 → 1.1.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 +69 -8
- package/package.json +5 -5
- package/src/client.js +4 -0
- package/src/index.js +18 -2
- package/src/resources/admin-api.js +136 -0
- package/src/resources/design.js +77 -0
- package/src/tools/automations.js +89 -0
- package/src/tools/blog.js +204 -0
- package/src/tools/design.js +136 -0
- package/src/tools/documents.js +108 -0
- package/src/tools/help-center.js +498 -0
- package/src/tools/media.js +86 -0
- package/src/tools/members.js +71 -3
- package/src/tools/properties.js +69 -0
- package/src/tools/spaces.js +95 -3
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const HELP_BASE_URL = process.env.SUTRA_HELP_CENTER_BASE_URL || "https://api.sutra.co/api/v4/lotus/help";
|
|
4
|
+
|
|
5
|
+
async function getImageDimensions(url) {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
8
|
+
if (!res.ok) return null;
|
|
9
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
10
|
+
if (buf[0] === 0x89 && buf[1] === 0x50) {
|
|
11
|
+
// PNG: width at bytes 16-19, height at 20-23
|
|
12
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
13
|
+
}
|
|
14
|
+
if (buf[0] === 0xFF && buf[1] === 0xD8) {
|
|
15
|
+
// JPEG: scan for SOF markers
|
|
16
|
+
let offset = 2;
|
|
17
|
+
while (offset < buf.length - 8) {
|
|
18
|
+
if (buf[offset] !== 0xFF) break;
|
|
19
|
+
const marker = buf[offset + 1];
|
|
20
|
+
const len = buf.readUInt16BE(offset + 2);
|
|
21
|
+
if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8) {
|
|
22
|
+
return { width: buf.readUInt16BE(offset + 7), height: buf.readUInt16BE(offset + 5) };
|
|
23
|
+
}
|
|
24
|
+
offset += 2 + len;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (buf.slice(0, 4).toString() === "GIF8") {
|
|
28
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
29
|
+
}
|
|
30
|
+
if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP") {
|
|
31
|
+
if (buf.slice(12, 16).toString() === "VP8 ") {
|
|
32
|
+
return { width: buf.readUInt16LE(26) & 0x3FFF, height: buf.readUInt16LE(28) & 0x3FFF };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function imageWidthForDimensions(dims) {
|
|
42
|
+
if (!dims) return "100%";
|
|
43
|
+
return dims.width >= dims.height ? "100%" : "60%";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function normalizeGleapContent(doc) {
|
|
47
|
+
if (!doc || typeof doc !== "object") return doc;
|
|
48
|
+
const nodes = doc.content;
|
|
49
|
+
if (!Array.isArray(nodes)) return doc;
|
|
50
|
+
const normalized = nodes.flatMap(normalizeNode);
|
|
51
|
+
await resolveImageWidths(normalized);
|
|
52
|
+
return { ...doc, content: normalized };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function resolveImageWidths(nodes) {
|
|
56
|
+
const imageNodes = [];
|
|
57
|
+
function collect(list) {
|
|
58
|
+
for (const node of list) {
|
|
59
|
+
if (node.type === "image" && node.attrs?.src && node.attrs?._needsResize) {
|
|
60
|
+
imageNodes.push(node);
|
|
61
|
+
}
|
|
62
|
+
if (node.content && Array.isArray(node.content)) collect(node.content);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
collect(nodes);
|
|
66
|
+
await Promise.all(imageNodes.map(async (node) => {
|
|
67
|
+
const dims = await getImageDimensions(node.attrs.src);
|
|
68
|
+
const w = imageWidthForDimensions(dims);
|
|
69
|
+
node.attrs.resizeableWidth = w;
|
|
70
|
+
delete node.attrs._needsResize;
|
|
71
|
+
delete node.attrs.style;
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeNode(node) {
|
|
76
|
+
if (!node || typeof node !== "object") return [node];
|
|
77
|
+
|
|
78
|
+
if (node.type === "callout") {
|
|
79
|
+
const children = Array.isArray(node.content) ? node.content : [];
|
|
80
|
+
const textNodes = [];
|
|
81
|
+
const blockNodes = [];
|
|
82
|
+
for (const child of children) {
|
|
83
|
+
if (child.type === "text") textNodes.push(child);
|
|
84
|
+
else if (child.type === "paragraph") blockNodes.push(child);
|
|
85
|
+
else blockNodes.push(child);
|
|
86
|
+
}
|
|
87
|
+
const paragraphs = [];
|
|
88
|
+
if (textNodes.length) paragraphs.push({ type: "paragraph", attrs: { textAlign: "left" }, content: textNodes });
|
|
89
|
+
paragraphs.push(...blockNodes);
|
|
90
|
+
return [{
|
|
91
|
+
type: "infobox",
|
|
92
|
+
attrs: {
|
|
93
|
+
customSpacing: true,
|
|
94
|
+
icon: "info",
|
|
95
|
+
iconColor: "#364fc7",
|
|
96
|
+
badgeBgColor: "rgba(255,255,255,0.6)",
|
|
97
|
+
iconPosition: "left",
|
|
98
|
+
iconVerticalAlign: "top",
|
|
99
|
+
badgeShape: "circle",
|
|
100
|
+
badgeSize: 40,
|
|
101
|
+
iconSize: 20,
|
|
102
|
+
iconOffset: 18,
|
|
103
|
+
iconVerticalOffset: 18,
|
|
104
|
+
contentOffset: 70,
|
|
105
|
+
contentVerticalOffset: 20,
|
|
106
|
+
styles: {
|
|
107
|
+
"margin-top": "0px",
|
|
108
|
+
"margin-bottom": "0px",
|
|
109
|
+
"margin-left": "0px",
|
|
110
|
+
"margin-right": "0px",
|
|
111
|
+
"padding-top": "15px",
|
|
112
|
+
"padding-bottom": "10px",
|
|
113
|
+
"padding-left": "0px",
|
|
114
|
+
"padding-right": "0px",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
content: [{
|
|
118
|
+
type: "column",
|
|
119
|
+
attrs: {
|
|
120
|
+
size: 12,
|
|
121
|
+
position: 0,
|
|
122
|
+
radius: "with-radius",
|
|
123
|
+
styles: {
|
|
124
|
+
"align-items": "flex-start",
|
|
125
|
+
"background-color": "#dbe4ff",
|
|
126
|
+
"background-image": "none",
|
|
127
|
+
"background-position": "center center",
|
|
128
|
+
"background-size": "cover",
|
|
129
|
+
"background-repeat": "no-repeat",
|
|
130
|
+
"margin-top": "0px",
|
|
131
|
+
"margin-bottom": "0px",
|
|
132
|
+
"margin-left": "0px",
|
|
133
|
+
"margin-right": "0px",
|
|
134
|
+
"padding-top": "20px",
|
|
135
|
+
"padding-bottom": "20px",
|
|
136
|
+
"padding-left": "70px",
|
|
137
|
+
"padding-right": "20px",
|
|
138
|
+
},
|
|
139
|
+
flexLayout: "stack",
|
|
140
|
+
flexGap: 0,
|
|
141
|
+
nestedRadius: "no-radius",
|
|
142
|
+
},
|
|
143
|
+
content: paragraphs.length ? paragraphs : [{ type: "paragraph", attrs: { textAlign: "left" } }],
|
|
144
|
+
}],
|
|
145
|
+
}];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (node.type === "editor-button") {
|
|
149
|
+
const label = node.content?.[0]?.text || "Button";
|
|
150
|
+
const href = node.attrs?.href || "#";
|
|
151
|
+
return [{
|
|
152
|
+
type: "paragraph",
|
|
153
|
+
attrs: { textAlign: "left" },
|
|
154
|
+
content: [{
|
|
155
|
+
type: "text",
|
|
156
|
+
marks: [{ type: "link", attrs: { href, target: "_blank", rel: "noopener noreferrer nofollow" } }, { type: "bold" }],
|
|
157
|
+
text: `👉 ${label}`,
|
|
158
|
+
}],
|
|
159
|
+
}];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (node.type === "imageBlock") {
|
|
163
|
+
return [{
|
|
164
|
+
type: "image",
|
|
165
|
+
attrs: {
|
|
166
|
+
src: node.attrs?.src,
|
|
167
|
+
alt: node.attrs?.alt || null,
|
|
168
|
+
textAlign: "center",
|
|
169
|
+
_needsResize: true,
|
|
170
|
+
},
|
|
171
|
+
}];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (node.type === "image" && node.attrs?.src) {
|
|
175
|
+
return [{
|
|
176
|
+
...node,
|
|
177
|
+
attrs: { ...node.attrs, _needsResize: true },
|
|
178
|
+
}];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (node.type === "heading") {
|
|
182
|
+
const level = node.attrs?.level || 1;
|
|
183
|
+
const paddingTop = { 1: "32px", 2: "20px", 3: "16px", 4: "12px", 5: "10px", 6: "8px" };
|
|
184
|
+
const styles = {
|
|
185
|
+
"margin-top": "0px", "margin-bottom": "0px", "margin-left": "0px", "margin-right": "0px",
|
|
186
|
+
"padding-top": paddingTop[level] || "20px", "padding-bottom": "0px",
|
|
187
|
+
"padding-left": "0px", "padding-right": "0px",
|
|
188
|
+
"font-size": "", "line-height": "", "font-family": "",
|
|
189
|
+
...(node.attrs?.styles || {}),
|
|
190
|
+
};
|
|
191
|
+
if (!node.attrs?.styles?.["padding-top"]) styles["padding-top"] = paddingTop[level] || "20px";
|
|
192
|
+
return [{ ...node, attrs: { ...node.attrs, styles } }];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (node.content && Array.isArray(node.content)) {
|
|
196
|
+
return [{ ...node, content: node.content.flatMap(normalizeNode) }];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return [node];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function registerHelpCenterTools(server) {
|
|
203
|
+
const token = process.env.SUTRA_HELP_CENTER_TOKEN;
|
|
204
|
+
if (!token) return;
|
|
205
|
+
|
|
206
|
+
async function request(method, path, { params, body } = {}) {
|
|
207
|
+
const url = new URL(`${HELP_BASE_URL}${path}`);
|
|
208
|
+
if (params) {
|
|
209
|
+
for (const [k, v] of Object.entries(params)) {
|
|
210
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const headers = {
|
|
214
|
+
Authorization: `Bearer ${token}`,
|
|
215
|
+
Accept: "application/json",
|
|
216
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
217
|
+
};
|
|
218
|
+
const res = await fetch(url.toString(), {
|
|
219
|
+
method,
|
|
220
|
+
headers,
|
|
221
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
222
|
+
});
|
|
223
|
+
const text = await res.text();
|
|
224
|
+
if (!res.ok) throw new Error(`${method} ${path} → ${res.status}: ${text}`);
|
|
225
|
+
return text ? JSON.parse(text) : {};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Articles ---
|
|
229
|
+
|
|
230
|
+
server.tool(
|
|
231
|
+
"help_list_articles",
|
|
232
|
+
"List Help Center articles with optional filters. Returns article summaries and stats.",
|
|
233
|
+
{
|
|
234
|
+
status: z.enum(["draft", "published", "archived"]).optional().describe("Filter by status"),
|
|
235
|
+
review_status: z.string().optional().describe("Filter by review status"),
|
|
236
|
+
collection_id: z.number().optional().describe("Filter by collection ID"),
|
|
237
|
+
product_area: z.string().optional().describe("Filter by product area"),
|
|
238
|
+
intent_type: z.string().optional().describe("Filter by intent type"),
|
|
239
|
+
q: z.string().optional().describe("Search title and excerpt"),
|
|
240
|
+
},
|
|
241
|
+
async (params) => {
|
|
242
|
+
const data = await request("GET", "/articles.json", { params });
|
|
243
|
+
return json(data);
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
server.tool(
|
|
248
|
+
"help_get_article",
|
|
249
|
+
"Get a Help Center article with full content, metadata, and version history.",
|
|
250
|
+
{
|
|
251
|
+
id: z.number().describe("Article ID"),
|
|
252
|
+
},
|
|
253
|
+
async ({ id }) => {
|
|
254
|
+
const data = await request("GET", `/articles/${id}.json`);
|
|
255
|
+
return json(data);
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
server.tool(
|
|
260
|
+
"help_create_article",
|
|
261
|
+
"Create a new Help Center article. Returns the article and its first version.",
|
|
262
|
+
{
|
|
263
|
+
title: z.string().describe("Article title"),
|
|
264
|
+
slug: z.string().optional().describe("URL slug (auto-generated from title if omitted)"),
|
|
265
|
+
excerpt: z.string().optional().describe("Short description/excerpt"),
|
|
266
|
+
help_collection_id: z.number().optional().describe("Collection ID to place article in"),
|
|
267
|
+
product_area: z.string().optional().describe("Product area tag"),
|
|
268
|
+
intent_type: z.string().optional().describe("Intent type tag"),
|
|
269
|
+
subsection: z.string().optional().describe("Subsection within collection"),
|
|
270
|
+
position: z.number().optional().describe("Sort position within collection"),
|
|
271
|
+
status: z.enum(["draft", "published", "archived"]).optional().describe("Initial status (default: draft)"),
|
|
272
|
+
content: z.any().optional().describe("Tiptap JSON content"),
|
|
273
|
+
commit_message: z.string().optional().describe("Version commit message"),
|
|
274
|
+
meta_title: z.string().optional().describe("SEO meta title"),
|
|
275
|
+
meta_description: z.string().optional().describe("SEO meta description"),
|
|
276
|
+
og_image_url: z.string().optional().describe("Open Graph image URL"),
|
|
277
|
+
heading_font: z.string().optional().describe("Heading font family"),
|
|
278
|
+
body_font: z.string().optional().describe("Body font family"),
|
|
279
|
+
audience: z.array(z.string()).optional().describe("Target audience tags"),
|
|
280
|
+
applies_to_plan: z.array(z.string()).optional().describe("Applicable plan tiers (basic, bronze, silver, gold)"),
|
|
281
|
+
alternate_phrasings: z.array(z.string()).optional().describe("Alternate search phrasings"),
|
|
282
|
+
publish: z.boolean().optional().describe("Publish immediately after creation"),
|
|
283
|
+
},
|
|
284
|
+
async (params) => {
|
|
285
|
+
const { publish, ...articleFields } = params;
|
|
286
|
+
if (articleFields.content) articleFields.content = await normalizeGleapContent(articleFields.content);
|
|
287
|
+
const data = await request("POST", "/articles.json", {
|
|
288
|
+
body: { article: articleFields, publish },
|
|
289
|
+
});
|
|
290
|
+
return json(data);
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
server.tool(
|
|
295
|
+
"help_propose_article",
|
|
296
|
+
"Submit an AI-authored Help Center article draft for human review. Creates an ai_cobolt version and marks the article as AI changes pending.",
|
|
297
|
+
{
|
|
298
|
+
title: z.string().describe("Article title"),
|
|
299
|
+
content: z.any().describe("Tiptap JSON content"),
|
|
300
|
+
slug: z.string().optional().describe("URL slug (auto-generated from title if omitted)"),
|
|
301
|
+
excerpt: z.string().optional().describe("Short description/excerpt"),
|
|
302
|
+
help_collection_id: z.number().optional().describe("Collection ID to place article in; omitted uses Untriaged AI Drafts"),
|
|
303
|
+
product_area: z.string().optional().describe("Product area tag"),
|
|
304
|
+
intent_type: z.string().optional().describe("Intent type tag"),
|
|
305
|
+
subsection: z.string().optional().describe("Subsection within collection"),
|
|
306
|
+
position: z.number().optional().describe("Sort position within collection"),
|
|
307
|
+
commit_message: z.string().optional().describe("Version commit message"),
|
|
308
|
+
meta_title: z.string().optional().describe("SEO meta title"),
|
|
309
|
+
meta_description: z.string().optional().describe("SEO meta description"),
|
|
310
|
+
og_image_url: z.string().optional().describe("Open Graph image URL"),
|
|
311
|
+
heading_font: z.string().optional().describe("Heading font family"),
|
|
312
|
+
body_font: z.string().optional().describe("Body font family"),
|
|
313
|
+
audience: z.array(z.string()).optional().describe("Target audience tags"),
|
|
314
|
+
applies_to_plan: z.array(z.string()).optional().describe("Applicable plan tiers (basic, bronze, silver, gold)"),
|
|
315
|
+
alternate_phrasings: z.array(z.string()).optional().describe("Alternate search phrasings"),
|
|
316
|
+
ai_agent_id: z.string().optional().describe("Identifier for the AI agent submitting the draft"),
|
|
317
|
+
source_signal: z.record(z.any()).optional().describe("Evidence or trigger that led to this draft"),
|
|
318
|
+
},
|
|
319
|
+
async ({ ai_agent_id, source_signal, ...articleFields }) => {
|
|
320
|
+
if (articleFields.content) articleFields.content = await normalizeGleapContent(articleFields.content);
|
|
321
|
+
const data = await request("POST", "/articles/propose.json", {
|
|
322
|
+
body: { article: articleFields, ai_agent_id, source_signal },
|
|
323
|
+
});
|
|
324
|
+
return json(data);
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
server.tool(
|
|
329
|
+
"help_update_article",
|
|
330
|
+
"Update an existing Help Center article and create a new version.",
|
|
331
|
+
{
|
|
332
|
+
id: z.number().describe("Article ID"),
|
|
333
|
+
title: z.string().optional().describe("Article title"),
|
|
334
|
+
slug: z.string().optional().describe("URL slug"),
|
|
335
|
+
excerpt: z.string().optional().describe("Short description/excerpt"),
|
|
336
|
+
help_collection_id: z.number().optional().describe("Collection ID"),
|
|
337
|
+
product_area: z.string().optional().describe("Product area tag"),
|
|
338
|
+
intent_type: z.string().optional().describe("Intent type tag"),
|
|
339
|
+
subsection: z.string().optional().describe("Subsection within collection"),
|
|
340
|
+
position: z.number().optional().describe("Sort position"),
|
|
341
|
+
status: z.string().optional().describe("Status (draft/published/archived)"),
|
|
342
|
+
content: z.any().optional().describe("Tiptap JSON content"),
|
|
343
|
+
commit_message: z.string().optional().describe("Version commit message"),
|
|
344
|
+
meta_title: z.string().optional().describe("SEO meta title"),
|
|
345
|
+
meta_description: z.string().optional().describe("SEO meta description"),
|
|
346
|
+
og_image_url: z.string().optional().describe("Open Graph image URL"),
|
|
347
|
+
heading_font: z.string().optional().describe("Heading font family"),
|
|
348
|
+
body_font: z.string().optional().describe("Body font family"),
|
|
349
|
+
audience: z.array(z.string()).optional().describe("Target audience tags"),
|
|
350
|
+
applies_to_plan: z.array(z.string()).optional().describe("Applicable plan tiers (basic, bronze, silver, gold)"),
|
|
351
|
+
alternate_phrasings: z.array(z.string()).optional().describe("Alternate search phrasings"),
|
|
352
|
+
publish: z.boolean().optional().describe("Publish this version immediately"),
|
|
353
|
+
},
|
|
354
|
+
async ({ id, publish, ...articleFields }) => {
|
|
355
|
+
if (articleFields.content) articleFields.content = await normalizeGleapContent(articleFields.content);
|
|
356
|
+
const data = await request("PUT", `/articles/${id}.json`, {
|
|
357
|
+
body: { article: articleFields, publish },
|
|
358
|
+
});
|
|
359
|
+
return json(data);
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
server.tool(
|
|
364
|
+
"help_propose_article_update",
|
|
365
|
+
"Submit AI-authored changes to an existing Help Center article for human review. Creates an ai_cobolt pending version without publishing it.",
|
|
366
|
+
{
|
|
367
|
+
id: z.number().describe("Article ID"),
|
|
368
|
+
title: z.string().optional().describe("Proposed article title"),
|
|
369
|
+
slug: z.string().optional().describe("Proposed URL slug"),
|
|
370
|
+
excerpt: z.string().optional().describe("Proposed short description/excerpt"),
|
|
371
|
+
help_collection_id: z.number().optional().describe("Proposed collection ID"),
|
|
372
|
+
product_area: z.string().optional().describe("Proposed product area tag"),
|
|
373
|
+
intent_type: z.string().optional().describe("Proposed intent type tag"),
|
|
374
|
+
subsection: z.string().optional().describe("Proposed subsection within collection"),
|
|
375
|
+
position: z.number().optional().describe("Proposed sort position"),
|
|
376
|
+
content: z.any().optional().describe("Proposed Tiptap JSON content; omit for metadata-only proposals"),
|
|
377
|
+
commit_message: z.string().optional().describe("Version commit message"),
|
|
378
|
+
meta_title: z.string().optional().describe("Proposed SEO meta title"),
|
|
379
|
+
meta_description: z.string().optional().describe("Proposed SEO meta description"),
|
|
380
|
+
og_image_url: z.string().optional().describe("Proposed Open Graph image URL"),
|
|
381
|
+
heading_font: z.string().optional().describe("Proposed heading font family"),
|
|
382
|
+
body_font: z.string().optional().describe("Proposed body font family"),
|
|
383
|
+
audience: z.array(z.string()).optional().describe("Proposed target audience tags"),
|
|
384
|
+
applies_to_plan: z.array(z.string()).optional().describe("Proposed applicable plan tiers (basic, bronze, silver, gold)"),
|
|
385
|
+
alternate_phrasings: z.array(z.string()).optional().describe("Proposed alternate search phrasings"),
|
|
386
|
+
ai_agent_id: z.string().optional().describe("Identifier for the AI agent submitting the proposal"),
|
|
387
|
+
source_signal: z.record(z.any()).optional().describe("Evidence or trigger that led to this update"),
|
|
388
|
+
},
|
|
389
|
+
async ({ id, ai_agent_id, source_signal, ...articleFields }) => {
|
|
390
|
+
if (articleFields.content) articleFields.content = await normalizeGleapContent(articleFields.content);
|
|
391
|
+
const data = await request("POST", `/articles/${id}/propose_update.json`, {
|
|
392
|
+
body: { article: articleFields, ai_agent_id, source_signal },
|
|
393
|
+
});
|
|
394
|
+
return json(data);
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
server.tool(
|
|
399
|
+
"help_publish_article",
|
|
400
|
+
"Publish a Help Center article version. Publishes the latest version if no version_id is given.",
|
|
401
|
+
{
|
|
402
|
+
id: z.number().describe("Article ID"),
|
|
403
|
+
version_id: z.number().optional().describe("Specific version ID to publish (defaults to latest)"),
|
|
404
|
+
},
|
|
405
|
+
async ({ id, version_id }) => {
|
|
406
|
+
const data = await request("POST", `/articles/${id}/publish.json`, {
|
|
407
|
+
body: version_id ? { version_id } : {},
|
|
408
|
+
});
|
|
409
|
+
return json(data);
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
server.tool(
|
|
414
|
+
"help_review_article",
|
|
415
|
+
"Approve or reject a pending Help Center article version.",
|
|
416
|
+
{
|
|
417
|
+
id: z.number().describe("Article ID"),
|
|
418
|
+
version_id: z.number().describe("Version ID to review"),
|
|
419
|
+
decision: z.enum(["approve", "reject"]).describe("Review decision"),
|
|
420
|
+
comment: z.string().optional().describe("Review comment"),
|
|
421
|
+
},
|
|
422
|
+
async ({ id, ...body }) => {
|
|
423
|
+
const data = await request("POST", `/articles/${id}/review.json`, { body });
|
|
424
|
+
return json(data);
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// --- Collections ---
|
|
429
|
+
|
|
430
|
+
server.tool(
|
|
431
|
+
"help_list_collections",
|
|
432
|
+
"List all Help Center collections (categories).",
|
|
433
|
+
{},
|
|
434
|
+
async () => {
|
|
435
|
+
const data = await request("GET", "/collections.json");
|
|
436
|
+
return json(data);
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
server.tool(
|
|
441
|
+
"help_create_collection",
|
|
442
|
+
"Create a new Help Center collection.",
|
|
443
|
+
{
|
|
444
|
+
name: z.string().describe("Collection name"),
|
|
445
|
+
slug: z.string().optional().describe("URL slug"),
|
|
446
|
+
description: z.string().optional().describe("Collection description"),
|
|
447
|
+
icon_library: z.string().optional().describe("Icon library name"),
|
|
448
|
+
icon_name: z.string().optional().describe("Icon name"),
|
|
449
|
+
icon_color: z.string().optional().describe("Icon color hex"),
|
|
450
|
+
position: z.number().optional().describe("Sort position"),
|
|
451
|
+
published: z.boolean().optional().describe("Whether collection is published"),
|
|
452
|
+
},
|
|
453
|
+
async (params) => {
|
|
454
|
+
const data = await request("POST", "/collections.json", {
|
|
455
|
+
body: { collection: params },
|
|
456
|
+
});
|
|
457
|
+
return json(data);
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
server.tool(
|
|
462
|
+
"help_update_collection",
|
|
463
|
+
"Update a Help Center collection.",
|
|
464
|
+
{
|
|
465
|
+
id: z.number().describe("Collection ID"),
|
|
466
|
+
name: z.string().optional().describe("Collection name"),
|
|
467
|
+
slug: z.string().optional().describe("URL slug"),
|
|
468
|
+
description: z.string().optional().describe("Collection description"),
|
|
469
|
+
icon_library: z.string().optional().describe("Icon library name"),
|
|
470
|
+
icon_name: z.string().optional().describe("Icon name"),
|
|
471
|
+
icon_color: z.string().optional().describe("Icon color hex"),
|
|
472
|
+
position: z.number().optional().describe("Sort position"),
|
|
473
|
+
published: z.boolean().optional().describe("Whether collection is published"),
|
|
474
|
+
},
|
|
475
|
+
async ({ id, ...fields }) => {
|
|
476
|
+
const data = await request("PUT", `/collections/${id}.json`, {
|
|
477
|
+
body: { collection: fields },
|
|
478
|
+
});
|
|
479
|
+
return json(data);
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
server.tool(
|
|
484
|
+
"help_delete_collection",
|
|
485
|
+
"Delete a Help Center collection. Archives it if it has articles, permanently deletes if empty.",
|
|
486
|
+
{
|
|
487
|
+
id: z.number().describe("Collection ID"),
|
|
488
|
+
},
|
|
489
|
+
async ({ id }) => {
|
|
490
|
+
const data = await request("DELETE", `/collections/${id}.json`);
|
|
491
|
+
return json(data);
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function json(data) {
|
|
497
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
498
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const mediaPlacementSchema = z.object({
|
|
4
|
+
insert_node: z.boolean().optional().describe("Insert a placeholder node in the document (default true)"),
|
|
5
|
+
insert: z.enum(["append", "before", "after", "inside"]).optional(),
|
|
6
|
+
anchor_node_uid: z.string().optional(),
|
|
7
|
+
}).optional();
|
|
8
|
+
|
|
9
|
+
export function registerMediaTools(server, client) {
|
|
10
|
+
server.tool(
|
|
11
|
+
"create_media_upload",
|
|
12
|
+
"Create a Sutra media upload session for a local file. Upload bytes to the returned PUT URL, then call complete_media_upload.",
|
|
13
|
+
{
|
|
14
|
+
space_id: z.string().describe("Space ID (sp_...). Do not use a slug."),
|
|
15
|
+
filename: z.string().describe("Original filename"),
|
|
16
|
+
content_type: z.string().describe("MIME type, e.g. video/mp4"),
|
|
17
|
+
size_bytes: z.number().int().nonnegative().describe("File size in bytes"),
|
|
18
|
+
kind: z.enum(["video", "file", "audio", "image"]).optional().describe("Media kind (default video)"),
|
|
19
|
+
placement: mediaPlacementSchema,
|
|
20
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
21
|
+
},
|
|
22
|
+
async ({ space_id, idempotency_key, ...body }) => {
|
|
23
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
24
|
+
return json(await client.post(`/spaces/${space_id}/media_uploads`, body, headers));
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
server.tool(
|
|
29
|
+
"complete_media_upload",
|
|
30
|
+
"Complete an upload after bytes have been PUT to S3. Sutra verifies the object and queues processing for videos.",
|
|
31
|
+
{
|
|
32
|
+
media_id: z.string().describe("Media ID (media_...)"),
|
|
33
|
+
size_bytes: z.number().int().nonnegative().optional(),
|
|
34
|
+
etag: z.string().optional(),
|
|
35
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
36
|
+
},
|
|
37
|
+
async ({ media_id, idempotency_key, ...body }) => {
|
|
38
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
39
|
+
return json(await client.post(`/media_uploads/${media_id}/complete`, body, headers));
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
server.tool(
|
|
44
|
+
"get_media_upload",
|
|
45
|
+
"Get upload and processing status for a media item.",
|
|
46
|
+
{
|
|
47
|
+
media_id: z.string().describe("Media ID (media_...)"),
|
|
48
|
+
},
|
|
49
|
+
async ({ media_id }) => json(await client.get(`/media_uploads/${media_id}`))
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
server.tool(
|
|
53
|
+
"cancel_media_upload",
|
|
54
|
+
"Cancel or abandon a pending media upload. This does not delete any uploaded S3 object.",
|
|
55
|
+
{
|
|
56
|
+
media_id: z.string().describe("Media ID (media_...)"),
|
|
57
|
+
remove_document_node: z.boolean().optional().describe("Remove the placeholder node when possible"),
|
|
58
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
59
|
+
},
|
|
60
|
+
async ({ media_id, remove_document_node, idempotency_key }) => {
|
|
61
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
62
|
+
const query = remove_document_node ? "?remove_document_node=true" : "";
|
|
63
|
+
return json(await client.delete(`/media_uploads/${media_id}${query}`, headers));
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
server.tool(
|
|
68
|
+
"create_media_reference",
|
|
69
|
+
"Insert an external media embed such as Loom, YouTube, or Vimeo into a Sutra document without creating an upload session.",
|
|
70
|
+
{
|
|
71
|
+
space_id: z.string().describe("Space ID (sp_...). Do not use a slug."),
|
|
72
|
+
url: z.string().min(1).describe("External media URL or iframe embed code from Loom, YouTube, or Vimeo"),
|
|
73
|
+
placement: mediaPlacementSchema,
|
|
74
|
+
base_revision: z.string().optional().describe("Optional document revision guard"),
|
|
75
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
76
|
+
},
|
|
77
|
+
async ({ space_id, idempotency_key, ...body }) => {
|
|
78
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
79
|
+
return json(await client.post(`/spaces/${space_id}/media_references`, body, headers));
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function json(data) {
|
|
85
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
86
|
+
}
|
package/src/tools/members.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
export function registerMemberTools(server, client) {
|
|
4
|
+
const propertyValueSchema = z.union([z.string(), z.number(), z.boolean()]).describe("Exact property value");
|
|
5
|
+
const propertyFilterSchema = z.object({
|
|
6
|
+
property_key: z.string().optional().describe("Property key or mprop_ ID"),
|
|
7
|
+
key: z.string().optional().describe("Property key"),
|
|
8
|
+
property_id: z.string().optional().describe("Property ID (mprop_...)"),
|
|
9
|
+
member_property_id: z.string().optional().describe("Property ID (mprop_...)"),
|
|
10
|
+
property_value: propertyValueSchema.optional(),
|
|
11
|
+
value: propertyValueSchema.optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
4
14
|
server.tool(
|
|
5
15
|
"list_members",
|
|
6
|
-
"List members of a space. Returns member IDs (mem_...), user info, roles, and states. Use `q` to search by name or email.",
|
|
16
|
+
"List members of a space. Returns member IDs (mem_...), user info, roles, and states. Use `q` to search by name or email. Use property_key/property_value or property_filters for custom property filtering.",
|
|
7
17
|
{
|
|
8
18
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
9
19
|
limit: z.number().optional().describe("Max results (default 25)"),
|
|
@@ -13,9 +23,63 @@ export function registerMemberTools(server, client) {
|
|
|
13
23
|
q: z.string().optional().describe("Search by member name or email (case-insensitive)"),
|
|
14
24
|
role: z.enum(["member", "editor", "moderator"]).optional().describe("Filter by role"),
|
|
15
25
|
state: z.enum(["active", "pending"]).optional().describe("Filter by state"),
|
|
26
|
+
property_key: z.string().optional().describe("Filter by member property key or mprop_ ID (requires member_properties:read)"),
|
|
27
|
+
property_value: propertyValueSchema.optional().describe("Exact property value for property_key. Select values may use option names or option values."),
|
|
28
|
+
property_filters: z.array(propertyFilterSchema).optional().describe("Multiple property filters. Each filter needs a property key/ID and value."),
|
|
29
|
+
},
|
|
30
|
+
async ({ space_id, limit, cursor, user_id, email, q, role, state, property_key, property_value, property_filters }) => {
|
|
31
|
+
const data = await client.get(`/spaces/${space_id}/members`, {
|
|
32
|
+
limit,
|
|
33
|
+
cursor,
|
|
34
|
+
user_id,
|
|
35
|
+
email,
|
|
36
|
+
q,
|
|
37
|
+
role,
|
|
38
|
+
state,
|
|
39
|
+
property_key,
|
|
40
|
+
property_value,
|
|
41
|
+
property_filters: encodePropertyFilters(property_filters),
|
|
42
|
+
});
|
|
43
|
+
return json(data);
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
server.tool(
|
|
48
|
+
"list_contacts",
|
|
49
|
+
"List CRM contacts in a space. Contacts are non-member people in the Contacts/Interested layer. Use property_key/property_value to filter by custom member properties.",
|
|
50
|
+
{
|
|
51
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
52
|
+
limit: z.number().optional().describe("Max results (default 25)"),
|
|
53
|
+
cursor: z.string().optional().describe("Pagination cursor"),
|
|
54
|
+
email: z.string().optional().describe("Filter by exact email (requires members.email:read scope)"),
|
|
55
|
+
q: z.string().optional().describe("Search by contact name, and by email when scoped"),
|
|
56
|
+
property_key: z.string().optional().describe("Filter by member property key or mprop_ ID (requires member_properties:read)"),
|
|
57
|
+
property_value: propertyValueSchema.optional().describe("Exact property value for property_key. Select values may use option names or option values."),
|
|
58
|
+
property_filters: z.array(propertyFilterSchema).optional().describe("Multiple property filters. Each filter needs a property key/ID and value."),
|
|
59
|
+
},
|
|
60
|
+
async ({ space_id, limit, cursor, email, q, property_key, property_value, property_filters }) => {
|
|
61
|
+
const data = await client.get(`/spaces/${space_id}/contacts`, {
|
|
62
|
+
limit,
|
|
63
|
+
cursor,
|
|
64
|
+
email,
|
|
65
|
+
q,
|
|
66
|
+
property_key,
|
|
67
|
+
property_value,
|
|
68
|
+
property_filters: encodePropertyFilters(property_filters),
|
|
69
|
+
});
|
|
70
|
+
return json(data);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
server.tool(
|
|
75
|
+
"get_contact",
|
|
76
|
+
"Get details for a specific CRM contact in a space.",
|
|
77
|
+
{
|
|
78
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
79
|
+
contact_id: z.string().describe("Contact ID (contact_...)"),
|
|
16
80
|
},
|
|
17
|
-
async ({ space_id,
|
|
18
|
-
const data = await client.get(`/spaces/${space_id}/
|
|
81
|
+
async ({ space_id, contact_id }) => {
|
|
82
|
+
const data = await client.get(`/spaces/${space_id}/contacts/${contact_id}`);
|
|
19
83
|
return json(data);
|
|
20
84
|
}
|
|
21
85
|
);
|
|
@@ -142,6 +206,10 @@ export function registerMemberTools(server, client) {
|
|
|
142
206
|
);
|
|
143
207
|
}
|
|
144
208
|
|
|
209
|
+
function encodePropertyFilters(filters) {
|
|
210
|
+
return filters && filters.length > 0 ? JSON.stringify(filters) : undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
145
213
|
function json(data) {
|
|
146
214
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
147
215
|
}
|