@sutraspaces/mcp-server 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -7
- package/package.json +6 -5
- package/src/auth/oauth.js +470 -0
- package/src/auth/pkce.js +38 -0
- package/src/auth/store.js +84 -0
- package/src/client.js +66 -8
- package/src/index.js +102 -12
- package/src/resources/design.js +77 -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 +75 -2
- package/src/tools/media.js +86 -0
- package/src/tools/spaces.js +82 -3
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const BUNDLED_DESIGN_CAPABILITIES = {
|
|
4
|
+
version: "1.0.0-bundled",
|
|
5
|
+
rules: {
|
|
6
|
+
grid_distribution: "Use 12-unit dist arrays such as [6,6], [8,4], or [4,4,4].",
|
|
7
|
+
buttons: "Use actionCallbackValue and actionCallbackTarget for URLs and navigation targets.",
|
|
8
|
+
images: "Use a real URL for image src, include alt text, and avoid data: URLs.",
|
|
9
|
+
conflicts: "When publish returns 409, re-read the design and rebase before retrying.",
|
|
10
|
+
},
|
|
11
|
+
warning: "Bundled fallback only. The live Sutra API capabilities endpoint was unreachable, so this manifest may be stale.",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function registerDesignTools(server, client) {
|
|
15
|
+
server.tool(
|
|
16
|
+
"get_design_capabilities",
|
|
17
|
+
"Get the live Sutra design capability manifest. Read this before creating or editing page designs.",
|
|
18
|
+
{},
|
|
19
|
+
async () => {
|
|
20
|
+
try {
|
|
21
|
+
return json(await client.get("/design/capabilities"));
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return json({
|
|
24
|
+
data: BUNDLED_DESIGN_CAPABILITIES,
|
|
25
|
+
warning: `Live capabilities unavailable: ${err.message}`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
server.tool(
|
|
32
|
+
"get_space_design",
|
|
33
|
+
"Fetch the current Sutra-rendered design for a space, including nodes, content_digest, circles, interactive blocks, and published URL.",
|
|
34
|
+
{
|
|
35
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
36
|
+
},
|
|
37
|
+
async ({ space_id }) => json(await client.get(`/spaces/${space_id}/design`))
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
server.tool(
|
|
41
|
+
"validate_space_design",
|
|
42
|
+
"Validate proposed Sutra design nodes and return structured diagnostics. Validate before creating or publishing a draft.",
|
|
43
|
+
{
|
|
44
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
45
|
+
nodes: z.any().describe("Tiptap design nodes array or wrapped { default: { type: 'doc', content: [...] } } document"),
|
|
46
|
+
base_digest: z.string().optional().describe("Digest from get_space_design, if available"),
|
|
47
|
+
},
|
|
48
|
+
async ({ space_id, nodes, base_digest }) => {
|
|
49
|
+
const body = { nodes, base_digest };
|
|
50
|
+
return json(await client.post(`/spaces/${space_id}/design/validate`, body));
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
server.tool(
|
|
55
|
+
"create_or_update_design_draft",
|
|
56
|
+
"Create or update a design draft for a space. Use content_digest from get_space_design as base_digest. Mutating retries should pass idempotency_key.",
|
|
57
|
+
{
|
|
58
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
59
|
+
nodes: z.any().describe("Tiptap design nodes array or wrapped document"),
|
|
60
|
+
base_digest: z.string().describe("Digest from get_space_design"),
|
|
61
|
+
client_draft_key: z.string().describe("Stable key for this draft variant/session"),
|
|
62
|
+
title: z.string().optional().describe("Human-readable draft title"),
|
|
63
|
+
note: z.string().optional().describe("Short note about the draft"),
|
|
64
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
65
|
+
},
|
|
66
|
+
async ({ space_id, idempotency_key, ...body }) => {
|
|
67
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
68
|
+
return json(await client.post(`/spaces/${space_id}/design_drafts`, body, headers));
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
server.tool(
|
|
73
|
+
"publish_design_draft",
|
|
74
|
+
"Publish a design draft. If the API returns a conflict, re-read/rebase or provide the explicit destructive confirmation arrays from the conflict response.",
|
|
75
|
+
{
|
|
76
|
+
draft_id: z.string().describe("Design draft ID (dsdft_...)"),
|
|
77
|
+
confirm_destroy_circles: z.array(z.string()).optional().describe("Circle slugs or sp_ IDs explicitly confirmed for removal"),
|
|
78
|
+
confirm_destroy_interactive_with_responses: z.array(z.string()).optional().describe("Interactive confirm_token values explicitly confirmed for removal"),
|
|
79
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
80
|
+
},
|
|
81
|
+
async ({ draft_id, idempotency_key, confirm_destroy_circles, confirm_destroy_interactive_with_responses }) => {
|
|
82
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
83
|
+
const body = {
|
|
84
|
+
confirm_destroy_circles: confirm_destroy_circles || [],
|
|
85
|
+
confirm_destroy_interactive_with_responses: confirm_destroy_interactive_with_responses || [],
|
|
86
|
+
};
|
|
87
|
+
return json(await client.post(`/design_drafts/${draft_id}/publish`, body, headers));
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
server.tool(
|
|
92
|
+
"restore_design_draft",
|
|
93
|
+
"Restore the pre-publish backup for a published design draft. If live content changed after publish, pass force_restore with current_digest from the conflict response.",
|
|
94
|
+
{
|
|
95
|
+
draft_id: z.string().describe("Design draft ID (dsdft_...)"),
|
|
96
|
+
force_restore: z.boolean().optional().describe("Confirm restore even when live content changed after publish"),
|
|
97
|
+
current_digest: z.string().optional().describe("Current digest required when force_restore is true"),
|
|
98
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
99
|
+
},
|
|
100
|
+
async ({ draft_id, idempotency_key, force_restore, current_digest }) => {
|
|
101
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
102
|
+
const body = { force_restore: force_restore === true, current_digest };
|
|
103
|
+
return json(await client.post(`/design_drafts/${draft_id}/restore`, body, headers));
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
server.tool(
|
|
108
|
+
"import_design_asset",
|
|
109
|
+
"Import an external image URL into Sutra-controlled storage and return the canonical URL to use as image.attrs.src.",
|
|
110
|
+
{
|
|
111
|
+
space_id: z.string().describe("Space ID (sp_...) used for authorization and asset organization"),
|
|
112
|
+
source_url: z.string().url().describe("Public http(s) image URL to fetch and re-host"),
|
|
113
|
+
alt: z.string().optional().describe("Alt text for the image"),
|
|
114
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
115
|
+
},
|
|
116
|
+
async ({ idempotency_key, ...body }) => {
|
|
117
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
118
|
+
return json(await client.post("/design/assets/import", body, headers));
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
server.tool(
|
|
123
|
+
"get_design_preview_link",
|
|
124
|
+
"Get a short-lived, signed, read-only link that opens a design draft's rendered preview in a browser with no Sutra login required. Share it with the user (or open it yourself if you can view web pages) to review the design before publishing.",
|
|
125
|
+
{
|
|
126
|
+
draft_id: z.string().describe("Design draft ID (dsdft_...)"),
|
|
127
|
+
},
|
|
128
|
+
async ({ draft_id }) => {
|
|
129
|
+
return json(await client.post(`/design_drafts/${draft_id}/preview_link`, {}));
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function json(data) {
|
|
135
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
136
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const documentPlacementSchema = z.object({
|
|
4
|
+
insert: z.enum(["append", "before", "after", "inside"]).optional().describe("Where to insert or move nodes."),
|
|
5
|
+
anchor_node_uid: z.string().optional().describe("Tiptap node UID required for before/after/inside."),
|
|
6
|
+
}).optional();
|
|
7
|
+
|
|
8
|
+
export function registerDocumentTools(server, client) {
|
|
9
|
+
server.tool(
|
|
10
|
+
"get_document_capabilities",
|
|
11
|
+
"Get supported Tiptap document node capabilities and reserved-node policy. Read this before writing document nodes.",
|
|
12
|
+
{},
|
|
13
|
+
async () => json(await client.get("/document/capabilities"))
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
server.tool(
|
|
17
|
+
"get_document",
|
|
18
|
+
"Get the visible Tiptap document for a space by sp_ ID. Use the returned revision for replace_document.",
|
|
19
|
+
{
|
|
20
|
+
space_id: z.string().describe("Space ID (sp_...). Do not use a slug."),
|
|
21
|
+
},
|
|
22
|
+
async ({ space_id }) => json(await client.get(`/spaces/${space_id}/document`))
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
server.tool(
|
|
26
|
+
"replace_document",
|
|
27
|
+
"Replace a space document. Preserve managed space/media nodes; use structure/media tools for those. Requires base_revision or force.",
|
|
28
|
+
{
|
|
29
|
+
space_id: z.string().describe("Space ID (sp_...). Do not use a slug."),
|
|
30
|
+
document: z.any().describe("Tiptap doc: { type: 'doc', content: [...] }"),
|
|
31
|
+
base_revision: z.string().optional().describe("Revision from get_document"),
|
|
32
|
+
force: z.boolean().optional().describe("Explicitly replace without a base revision"),
|
|
33
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
34
|
+
},
|
|
35
|
+
async ({ space_id, idempotency_key, ...body }) => {
|
|
36
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
37
|
+
return json(await client.put(`/spaces/${space_id}/document`, body, headers));
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
server.tool(
|
|
42
|
+
"insert_document_nodes",
|
|
43
|
+
"Insert visible Tiptap nodes into a space document. Use structure tools for space cards and media tools for uploads.",
|
|
44
|
+
{
|
|
45
|
+
space_id: z.string().describe("Space ID (sp_...). Do not use a slug."),
|
|
46
|
+
nodes: z.array(z.any()).describe("Array of Tiptap node objects"),
|
|
47
|
+
placement: documentPlacementSchema,
|
|
48
|
+
base_revision: z.string().optional(),
|
|
49
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
50
|
+
},
|
|
51
|
+
async ({ space_id, idempotency_key, ...body }) => {
|
|
52
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
53
|
+
return json(await client.post(`/spaces/${space_id}/document/nodes`, body, headers));
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
server.tool(
|
|
58
|
+
"update_document_node",
|
|
59
|
+
"Replace one document node by its Tiptap attrs.uid.",
|
|
60
|
+
{
|
|
61
|
+
space_id: z.string().describe("Space ID (sp_...). Do not use a slug."),
|
|
62
|
+
node_uid: z.string().describe("Tiptap node attrs.uid"),
|
|
63
|
+
node: z.any().describe("Replacement Tiptap node object"),
|
|
64
|
+
base_revision: z.string().optional(),
|
|
65
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
66
|
+
},
|
|
67
|
+
async ({ space_id, node_uid, idempotency_key, ...body }) => {
|
|
68
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
69
|
+
return json(await client.patch(`/spaces/${space_id}/document/nodes/${node_uid}`, body, headers));
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
server.tool(
|
|
74
|
+
"delete_document_node",
|
|
75
|
+
"Delete one document node by its Tiptap attrs.uid. Managed space cards must be detached with detach_child_space.",
|
|
76
|
+
{
|
|
77
|
+
space_id: z.string().describe("Space ID (sp_...). Do not use a slug."),
|
|
78
|
+
node_uid: z.string().describe("Tiptap node attrs.uid"),
|
|
79
|
+
base_revision: z.string().optional(),
|
|
80
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
81
|
+
},
|
|
82
|
+
async ({ space_id, node_uid, base_revision, idempotency_key }) => {
|
|
83
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
84
|
+
const query = base_revision ? `?base_revision=${encodeURIComponent(base_revision)}` : "";
|
|
85
|
+
return json(await client.delete(`/spaces/${space_id}/document/nodes/${node_uid}${query}`, headers));
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
server.tool(
|
|
90
|
+
"move_document_node",
|
|
91
|
+
"Move one document node by its Tiptap attrs.uid.",
|
|
92
|
+
{
|
|
93
|
+
space_id: z.string().describe("Space ID (sp_...). Do not use a slug."),
|
|
94
|
+
node_uid: z.string().describe("Tiptap node attrs.uid"),
|
|
95
|
+
placement: documentPlacementSchema,
|
|
96
|
+
base_revision: z.string().optional(),
|
|
97
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
98
|
+
},
|
|
99
|
+
async ({ space_id, node_uid, idempotency_key, ...body }) => {
|
|
100
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
101
|
+
return json(await client.patch(`/spaces/${space_id}/document/nodes/${node_uid}/placement`, body, headers));
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function json(data) {
|
|
107
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
108
|
+
}
|
package/src/tools/help-center.js
CHANGED
|
@@ -274,8 +274,10 @@ export function registerHelpCenterTools(server) {
|
|
|
274
274
|
meta_title: z.string().optional().describe("SEO meta title"),
|
|
275
275
|
meta_description: z.string().optional().describe("SEO meta description"),
|
|
276
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"),
|
|
277
279
|
audience: z.array(z.string()).optional().describe("Target audience tags"),
|
|
278
|
-
applies_to_plan: z.array(z.string()).optional().describe("Applicable plan
|
|
280
|
+
applies_to_plan: z.array(z.string()).optional().describe("Applicable plan tiers (basic, bronze, silver, gold)"),
|
|
279
281
|
alternate_phrasings: z.array(z.string()).optional().describe("Alternate search phrasings"),
|
|
280
282
|
publish: z.boolean().optional().describe("Publish immediately after creation"),
|
|
281
283
|
},
|
|
@@ -289,6 +291,40 @@ export function registerHelpCenterTools(server) {
|
|
|
289
291
|
}
|
|
290
292
|
);
|
|
291
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
|
+
|
|
292
328
|
server.tool(
|
|
293
329
|
"help_update_article",
|
|
294
330
|
"Update an existing Help Center article and create a new version.",
|
|
@@ -308,8 +344,10 @@ export function registerHelpCenterTools(server) {
|
|
|
308
344
|
meta_title: z.string().optional().describe("SEO meta title"),
|
|
309
345
|
meta_description: z.string().optional().describe("SEO meta description"),
|
|
310
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"),
|
|
311
349
|
audience: z.array(z.string()).optional().describe("Target audience tags"),
|
|
312
|
-
applies_to_plan: z.array(z.string()).optional().describe("Applicable plan
|
|
350
|
+
applies_to_plan: z.array(z.string()).optional().describe("Applicable plan tiers (basic, bronze, silver, gold)"),
|
|
313
351
|
alternate_phrasings: z.array(z.string()).optional().describe("Alternate search phrasings"),
|
|
314
352
|
publish: z.boolean().optional().describe("Publish this version immediately"),
|
|
315
353
|
},
|
|
@@ -322,6 +360,41 @@ export function registerHelpCenterTools(server) {
|
|
|
322
360
|
}
|
|
323
361
|
);
|
|
324
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
|
+
|
|
325
398
|
server.tool(
|
|
326
399
|
"help_publish_article",
|
|
327
400
|
"Publish a Help Center article version. Publishes the latest version if no version_id is given.",
|
|
@@ -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/spaces.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
const placementSchema = z.object({
|
|
4
|
+
surface: z.enum(["auto", "collection", "document"]).optional().describe("Where the child appears. Use auto for normal course/section placement."),
|
|
5
|
+
position: z.number().int().nonnegative().optional().describe("Zero-based order in the resolved surface."),
|
|
6
|
+
view_as: z.enum(["list", "hcard", "vcard", "button", "link"]).optional().describe("Document card render mode. Ignored for collection placement."),
|
|
7
|
+
insert: z.enum(["append", "before", "after", "inside"]).optional().describe("Document insertion mode."),
|
|
8
|
+
anchor_node_uid: z.string().optional().describe("Tiptap node UID used with before/after/inside."),
|
|
9
|
+
set_parent_ordering: z.enum(["custom_order"]).optional().describe("Set parent list ordering so collection position is visually effective."),
|
|
10
|
+
}).optional();
|
|
11
|
+
|
|
3
12
|
export function registerSpaceTools(server, client) {
|
|
4
13
|
server.tool(
|
|
5
14
|
"list_spaces",
|
|
@@ -59,7 +68,7 @@ export function registerSpaceTools(server, client) {
|
|
|
59
68
|
|
|
60
69
|
server.tool(
|
|
61
70
|
"create_space",
|
|
62
|
-
"Create a new space. Omit parent_id for a top-level space, or include
|
|
71
|
+
"Create a new space. Omit parent_id for a top-level space, or include a parent sp_ ID to create a child. Use sp_ IDs, not slugs.",
|
|
63
72
|
{
|
|
64
73
|
name: z.string().describe("Space name"),
|
|
65
74
|
description: z.string().optional().describe("Space description"),
|
|
@@ -67,14 +76,84 @@ export function registerSpaceTools(server, client) {
|
|
|
67
76
|
privacy: z.string().optional().describe("Privacy level (default: open)"),
|
|
68
77
|
pod_type: z.enum(["standard", "readonly"]).optional().describe("Pod type (default: standard)"),
|
|
69
78
|
parent_id: z.string().optional().describe("Parent space ID (sp_...) to create as a child"),
|
|
79
|
+
content_scaffold: z.enum(["none", "ui_default"]).optional().describe("Use none when document content will be written through document tools."),
|
|
80
|
+
placement: placementSchema,
|
|
70
81
|
},
|
|
71
|
-
async ({ name, description, type, privacy, pod_type, parent_id }) => {
|
|
72
|
-
const body = { name, description, type, privacy, pod_type, parent_id };
|
|
82
|
+
async ({ name, description, type, privacy, pod_type, parent_id, content_scaffold, placement }) => {
|
|
83
|
+
const body = { name, description, type, privacy, pod_type, parent_id, content_scaffold, placement };
|
|
73
84
|
const data = await client.post("/spaces", body);
|
|
74
85
|
return json(data);
|
|
75
86
|
}
|
|
76
87
|
);
|
|
77
88
|
|
|
89
|
+
server.tool(
|
|
90
|
+
"create_child_space",
|
|
91
|
+
"Create a child space under a parent sp_ ID and make it visible through placement. For courses and sections use placement.surface = auto.",
|
|
92
|
+
{
|
|
93
|
+
parent_id: z.string().describe("Parent space ID (sp_...). Do not use a slug."),
|
|
94
|
+
name: z.string().describe("Child space name"),
|
|
95
|
+
description: z.string().optional(),
|
|
96
|
+
type: z.string().optional().describe("Space type (default: content)"),
|
|
97
|
+
privacy: z.string().optional(),
|
|
98
|
+
pod_type: z.enum(["standard", "readonly"]).optional(),
|
|
99
|
+
content_scaffold: z.enum(["none", "ui_default"]).optional().describe("Use none when document content will be written through document tools."),
|
|
100
|
+
placement: placementSchema,
|
|
101
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
102
|
+
},
|
|
103
|
+
async ({ parent_id, idempotency_key, ...body }) => {
|
|
104
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
105
|
+
const data = await client.post(`/spaces/${parent_id}/children`, body, headers);
|
|
106
|
+
return json(data);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
server.tool(
|
|
111
|
+
"attach_child_space",
|
|
112
|
+
"Attach an existing child sp_ ID to a parent sp_ ID and make it visible. Use this when a space already exists but is not showing in the course/section/page.",
|
|
113
|
+
{
|
|
114
|
+
parent_id: z.string().describe("Parent space ID (sp_...). Do not use a slug."),
|
|
115
|
+
child_id: z.string().describe("Existing child space ID (sp_...). Do not use a slug."),
|
|
116
|
+
placement: placementSchema,
|
|
117
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
118
|
+
},
|
|
119
|
+
async ({ parent_id, child_id, placement, idempotency_key }) => {
|
|
120
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
121
|
+
const data = await client.put(`/spaces/${parent_id}/children/${child_id}`, { placement }, headers);
|
|
122
|
+
return json(data);
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
"update_child_space_placement",
|
|
128
|
+
"Move or repair a child space display under a parent. Use auto for normal course/section placement; use document for a card in a content page.",
|
|
129
|
+
{
|
|
130
|
+
parent_id: z.string().describe("Parent space ID (sp_...). Do not use a slug."),
|
|
131
|
+
child_id: z.string().describe("Child space ID (sp_...). Do not use a slug."),
|
|
132
|
+
placement: placementSchema,
|
|
133
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
134
|
+
},
|
|
135
|
+
async ({ parent_id, child_id, placement, idempotency_key }) => {
|
|
136
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
137
|
+
const data = await client.patch(`/spaces/${parent_id}/children/${child_id}/placement`, { placement }, headers);
|
|
138
|
+
return json(data);
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
server.tool(
|
|
143
|
+
"detach_child_space",
|
|
144
|
+
"Detach a child space from one parent without deleting the child space.",
|
|
145
|
+
{
|
|
146
|
+
parent_id: z.string().describe("Parent space ID (sp_...). Do not use a slug."),
|
|
147
|
+
child_id: z.string().describe("Child space ID (sp_...). Do not use a slug."),
|
|
148
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
149
|
+
},
|
|
150
|
+
async ({ parent_id, child_id, idempotency_key }) => {
|
|
151
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
152
|
+
const data = await client.delete(`/spaces/${parent_id}/children/${child_id}`, headers);
|
|
153
|
+
return json(data);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
78
157
|
server.tool(
|
|
79
158
|
"update_space",
|
|
80
159
|
"Update an existing space's name, description, type, privacy, or state.",
|