@sutraspaces/mcp-server 1.1.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 +41 -2
- package/package.json +4 -4
- package/src/client.js +4 -0
- package/src/index.js +10 -0
- 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
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sutra MCP Server
|
|
2
2
|
|
|
3
|
-
An
|
|
3
|
+
An MCP server that connects AI agents to the Sutra Admin API. Manage spaces, members, contacts, content, discussions, surveys, plans, broadcasts, and more — all through your AI tools. See the [Sutra developer docs](https://sutra.co/developers/docs) for API documentation.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
@@ -52,6 +52,8 @@ Point your MCP client at `npx -y @sutraspaces/mcp-server` with the `SUTRA_API_TO
|
|
|
52
52
|
|---|---|---|
|
|
53
53
|
| `SUTRA_API_TOKEN` | Yes | Your Sutra Admin API token (`sutra_live_sk_...`) |
|
|
54
54
|
| `SUTRA_BASE_URL` | No | Override the API URL (default: `https://api.sutra.co/api/admin/v1`) |
|
|
55
|
+
| `SUTRA_HELP_CENTER_TOKEN` | No | Internal scoped admin key (`sutra_admin_...`) for Lotus Help Center tools |
|
|
56
|
+
| `SUTRA_HELP_CENTER_BASE_URL` | No | Override the Help Center API URL (default: `https://api.sutra.co/api/v4/lotus/help`) |
|
|
55
57
|
|
|
56
58
|
## Available Tools
|
|
57
59
|
|
|
@@ -64,10 +66,16 @@ Point your MCP client at `npx -y @sutraspaces/mcp-server` with the `SUTRA_API_TO
|
|
|
64
66
|
- **get_space** — Get details for a single space
|
|
65
67
|
- **list_child_spaces** — List direct child spaces
|
|
66
68
|
- **create_space** — Create a new space (top-level or child)
|
|
69
|
+
- **create_child_space** — Create a child space and make it visible through placement
|
|
70
|
+
- **attach_child_space** — Attach an existing space under a parent and make it visible
|
|
71
|
+
- **update_child_space_placement** — Move or repair a child space display
|
|
72
|
+
- **detach_child_space** — Detach a child from one parent without deleting it
|
|
67
73
|
- **update_space** — Update space name, description, type, privacy, or state
|
|
68
74
|
- **delete_space** — Delete/archive a space
|
|
69
75
|
- **reorder_child_spaces** — Reorder children within a parent
|
|
70
76
|
|
|
77
|
+
Use `sp_...` IDs, not slugs. For normal course and section pages, use `placement.surface = "auto"`.
|
|
78
|
+
|
|
71
79
|
### Members
|
|
72
80
|
- **list_members** — List space members with optional email, user_id, search, role, state, and custom property filtering
|
|
73
81
|
- **get_member** — Get member details
|
|
@@ -86,8 +94,27 @@ Point your MCP client at `npx -y @sutraspaces/mcp-server` with the `SUTRA_API_TO
|
|
|
86
94
|
- **bulk_update_contact_property_values** — Set or clear property values for up to 100 contacts at once
|
|
87
95
|
|
|
88
96
|
### Content & Discussions
|
|
89
|
-
- **
|
|
97
|
+
- **get_document_capabilities** / **get_document** — Read document editing rules and the visible Tiptap document
|
|
98
|
+
- **replace_document** / **insert_document_nodes** / **update_document_node** / **delete_document_node** / **move_document_node** — Edit visible document content by Tiptap node UID
|
|
99
|
+
- **list_content** / **get_content_block** / **create_content** / **update_content** / **delete_content** — Legacy content-block mirror tools
|
|
90
100
|
- **reorder_content** — Reorder content blocks within a space
|
|
101
|
+
|
|
102
|
+
### Media Uploads
|
|
103
|
+
- **create_media_upload** — Create a server-mediated direct-to-S3 upload session
|
|
104
|
+
- **complete_media_upload** — Verify the uploaded object and queue video processing
|
|
105
|
+
- **get_media_upload** — Read upload and processing status
|
|
106
|
+
- **cancel_media_upload** — Abandon a pending upload
|
|
107
|
+
- **create_media_reference** — Insert a Loom, YouTube, or Vimeo embed without uploading a file
|
|
108
|
+
|
|
109
|
+
### Design Tools
|
|
110
|
+
|
|
111
|
+
- **get_design_capabilities** — Read Sutra's renderer-aware design manifest
|
|
112
|
+
- **get_space_design** — Read current page-design nodes and digest for a space
|
|
113
|
+
- **validate_space_design** — Validate proposed page-design nodes
|
|
114
|
+
- **create_or_update_design_draft** — Create or update a digest-guarded design draft
|
|
115
|
+
- **publish_design_draft** — Publish a draft with conflict and destructive-omission protection
|
|
116
|
+
- **restore_design_draft** — Restore the pre-publish backup for a published design draft
|
|
117
|
+
- **import_design_asset** — Re-host an external image URL and return the canonical Sutra URL
|
|
91
118
|
- **list_messages** / **get_message** / **create_message** / **update_message** / **delete_message** — Manage discussion messages
|
|
92
119
|
- **list_reflections** / **get_reflection** / **create_reflection** / **update_reflection** / **delete_reflection** — Manage threaded replies
|
|
93
120
|
|
|
@@ -120,12 +147,24 @@ Point your MCP client at `npx -y @sutraspaces/mcp-server` with the `SUTRA_API_TO
|
|
|
120
147
|
- **send_broadcast** — Send a broadcast to space members
|
|
121
148
|
- **get_broadcast_delivery_status** — Check delivery progress
|
|
122
149
|
|
|
150
|
+
### Help Center
|
|
151
|
+
These tools are available only when `SUTRA_HELP_CENTER_TOKEN` is set.
|
|
152
|
+
|
|
153
|
+
- **help_list_articles** / **help_get_article** — Read Lotus Help Center articles, metadata, and version history
|
|
154
|
+
- **help_create_article** / **help_update_article** — Human/admin-style create and update through Lotus
|
|
155
|
+
- **help_propose_article** — Submit an AI-authored draft as `ai_cobolt` and mark it pending human review
|
|
156
|
+
- **help_propose_article_update** — Submit AI-authored changes to an existing article without publishing them
|
|
157
|
+
- **help_review_article** — Approve or reject a pending article version
|
|
158
|
+
- **help_publish_article** — Explicitly publish an article version
|
|
159
|
+
- **help_list_collections** / **help_create_collection** / **help_update_collection** / **help_delete_collection** — Manage Help Center collections
|
|
160
|
+
|
|
123
161
|
## Available Resources
|
|
124
162
|
|
|
125
163
|
- **sutra://admin-api/overview** — Core Admin API concepts, public ID prefixes, scopes, pagination, and filtering
|
|
126
164
|
- **sutra://admin-api/membership-spaces** — Difference between admin-manageable spaces and limited membership-space inventory
|
|
127
165
|
- **sutra://admin-api/contacts-properties** — Contact listing and member/contact property value workflows
|
|
128
166
|
- **sutra://admin-api/people-filtering** — Member and contact search, email, state, role, and custom property filtering
|
|
167
|
+
- **sutra://design/capabilities** — Renderer-aware design rules and supported fonts for Sutra page-design tools
|
|
129
168
|
|
|
130
169
|
## API Concepts
|
|
131
170
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sutraspaces/mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "MCP server for the Sutra Admin API — manage spaces, members, contacts, content, and more via AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
],
|
|
34
34
|
"author": "Sutra <support@sutra.co> (https://sutra.co)",
|
|
35
35
|
"license": "MIT",
|
|
36
|
-
"homepage": "https://github.com/
|
|
36
|
+
"homepage": "https://github.com/lorenzsell/sutra-mcp",
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/
|
|
39
|
+
"url": "git+https://github.com/lorenzsell/sutra-mcp.git"
|
|
40
40
|
},
|
|
41
41
|
"bugs": {
|
|
42
|
-
"url": "https://github.com/
|
|
42
|
+
"url": "https://github.com/lorenzsell/sutra-mcp/issues"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/src/client.js
CHANGED
|
@@ -47,6 +47,10 @@ export class SutraAdminClient {
|
|
|
47
47
|
return this.request("PATCH", path, { body, headers });
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
put(path, body, headers) {
|
|
51
|
+
return this.request("PUT", path, { body, headers });
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
delete(path, headers) {
|
|
51
55
|
return this.request("DELETE", path, { headers });
|
|
52
56
|
}
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { SutraAdminClient } from "./client.js";
|
|
|
7
7
|
import { registerSpaceTools } from "./tools/spaces.js";
|
|
8
8
|
import { registerMemberTools } from "./tools/members.js";
|
|
9
9
|
import { registerContentTools } from "./tools/content.js";
|
|
10
|
+
import { registerDocumentTools } from "./tools/documents.js";
|
|
11
|
+
import { registerMediaTools } from "./tools/media.js";
|
|
10
12
|
import { registerPropertyTools } from "./tools/properties.js";
|
|
11
13
|
import { registerInvitationTools } from "./tools/invitations.js";
|
|
12
14
|
import { registerSurveyTools } from "./tools/surveys.js";
|
|
@@ -16,7 +18,10 @@ import { registerBroadcastTools } from "./tools/broadcasts.js";
|
|
|
16
18
|
import { registerDeepTool } from "./tools/deep.js";
|
|
17
19
|
import { registerAutomationTools } from "./tools/automations.js";
|
|
18
20
|
import { registerHelpCenterTools } from "./tools/help-center.js";
|
|
21
|
+
import { registerBlogTools } from "./tools/blog.js";
|
|
22
|
+
import { registerDesignTools } from "./tools/design.js";
|
|
19
23
|
import { registerAdminApiResources } from "./resources/admin-api.js";
|
|
24
|
+
import { registerDesignResources } from "./resources/design.js";
|
|
20
25
|
|
|
21
26
|
const SUTRA_API_TOKEN = process.env.SUTRA_API_TOKEN;
|
|
22
27
|
const SUTRA_BASE_URL = process.env.SUTRA_BASE_URL;
|
|
@@ -42,6 +47,8 @@ const client = new SutraAdminClient(SUTRA_API_TOKEN, SUTRA_BASE_URL);
|
|
|
42
47
|
registerSpaceTools(server, client);
|
|
43
48
|
registerMemberTools(server, client);
|
|
44
49
|
registerContentTools(server, client);
|
|
50
|
+
registerDocumentTools(server, client);
|
|
51
|
+
registerMediaTools(server, client);
|
|
45
52
|
registerPropertyTools(server, client);
|
|
46
53
|
registerInvitationTools(server, client);
|
|
47
54
|
registerSurveyTools(server, client);
|
|
@@ -50,8 +57,11 @@ registerCouponTools(server, client);
|
|
|
50
57
|
registerBroadcastTools(server, client);
|
|
51
58
|
registerDeepTool(server, client);
|
|
52
59
|
registerAutomationTools(server, client);
|
|
60
|
+
registerDesignTools(server, client);
|
|
53
61
|
registerHelpCenterTools(server);
|
|
62
|
+
registerBlogTools(server);
|
|
54
63
|
registerAdminApiResources(server);
|
|
64
|
+
registerDesignResources(server, client);
|
|
55
65
|
|
|
56
66
|
async function main() {
|
|
57
67
|
const transport = new StdioServerTransport();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const BUNDLED_CAPABILITIES_MARKDOWN = `# Sutra Design Capabilities
|
|
2
|
+
|
|
3
|
+
Read this before designing Sutra pages.
|
|
4
|
+
|
|
5
|
+
- Use get_design_capabilities for the live manifest whenever possible.
|
|
6
|
+
- Read get_space_design before changing a page and keep its content_digest as base_digest.
|
|
7
|
+
- Validate nodes before creating or publishing a draft.
|
|
8
|
+
- Use create_or_update_design_draft and publish_design_draft instead of raw content writes.
|
|
9
|
+
- If publish returns 409 conflict, re-read the design and rebase instead of retrying blindly.
|
|
10
|
+
- Do not emit image_keywords; direct design writes require real image URLs.
|
|
11
|
+
- Use actionCallbackValue and actionCallbackTarget for action buttons.
|
|
12
|
+
- Use 12-unit grid dist arrays such as [6,6], [8,4], or [4,4,4].
|
|
13
|
+
- Use only font families returned by get_design_capabilities fonts.available; other fonts fall back at render time.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export function registerDesignResources(server, client) {
|
|
17
|
+
server.registerResource(
|
|
18
|
+
"sutra-design-capabilities",
|
|
19
|
+
"sutra://design/capabilities",
|
|
20
|
+
{
|
|
21
|
+
title: "Sutra Design Capabilities",
|
|
22
|
+
description: "Renderer-aware Sutra design rules and supported fonts for page-generation tools.",
|
|
23
|
+
mimeType: "text/markdown",
|
|
24
|
+
},
|
|
25
|
+
async () => {
|
|
26
|
+
let text = BUNDLED_CAPABILITIES_MARKDOWN;
|
|
27
|
+
try {
|
|
28
|
+
const live = await client.get("/design/capabilities");
|
|
29
|
+
text = manifestToMarkdown(live.data || live);
|
|
30
|
+
} catch (_err) {
|
|
31
|
+
text += "\n\nLive capabilities could not be fetched; this bundled guidance may be stale.\n";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
contents: [
|
|
36
|
+
{
|
|
37
|
+
uri: "sutra://design/capabilities",
|
|
38
|
+
mimeType: "text/markdown",
|
|
39
|
+
text,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function manifestToMarkdown(manifest) {
|
|
48
|
+
const lines = [
|
|
49
|
+
"# Sutra Design Capabilities",
|
|
50
|
+
"",
|
|
51
|
+
`Version: ${manifest.version || "unknown"}`,
|
|
52
|
+
"",
|
|
53
|
+
"## Rules",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
for (const rule of Object.values(manifest.rules || {})) lines.push(`- ${rule}`);
|
|
57
|
+
lines.push("", "## Node Types");
|
|
58
|
+
for (const [type, spec] of Object.entries(manifest.nodes || {})) {
|
|
59
|
+
lines.push(`- \`${type}\` attrs: ${(spec.attrs || []).map((attr) => `\`${attr}\``).join(", ")}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push("", "## Marks");
|
|
62
|
+
for (const [type, spec] of Object.entries(manifest.marks || {})) {
|
|
63
|
+
lines.push(`- \`${type}\` attrs: ${(spec.attrs || []).map((attr) => `\`${attr}\``).join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
const fonts = manifest.fonts || {};
|
|
66
|
+
const availableFonts = fonts.available || [];
|
|
67
|
+
lines.push("", "## Fonts");
|
|
68
|
+
if (availableFonts.length > 0) {
|
|
69
|
+
for (const font of availableFonts) lines.push(`- ${font}`);
|
|
70
|
+
} else {
|
|
71
|
+
lines.push("- No font list returned by the live manifest.");
|
|
72
|
+
}
|
|
73
|
+
if (fonts.note) lines.push(`- ${fonts.note}`);
|
|
74
|
+
lines.push("", "## Anti-patterns");
|
|
75
|
+
for (const rule of manifest.anti_patterns || []) lines.push(`- ${rule}`);
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// MCP tools for the Sutra Blog (public surface at /resources). Mirrors
|
|
4
|
+
// help-center.js, blog-shaped: category is optional, tags are supported, and
|
|
5
|
+
// the support-only fields (product_area, intent_type, applies_to_plan, …) are
|
|
6
|
+
// dropped. Calls the admin API under /api/v4/lotus/blog.
|
|
7
|
+
const BLOG_BASE_URL = process.env.SUTRA_BLOG_BASE_URL || "https://api.sutra.co/api/v4/lotus/blog";
|
|
8
|
+
|
|
9
|
+
export function registerBlogTools(server) {
|
|
10
|
+
const token = process.env.SUTRA_BLOG_TOKEN || process.env.SUTRA_HELP_CENTER_TOKEN;
|
|
11
|
+
if (!token) return;
|
|
12
|
+
|
|
13
|
+
async function request(method, path, { params, body } = {}) {
|
|
14
|
+
const url = new URL(`${BLOG_BASE_URL}${path}`);
|
|
15
|
+
if (params) {
|
|
16
|
+
for (const [k, v] of Object.entries(params)) {
|
|
17
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const headers = {
|
|
21
|
+
Authorization: `Bearer ${token}`,
|
|
22
|
+
Accept: "application/json",
|
|
23
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
24
|
+
};
|
|
25
|
+
const res = await fetch(url.toString(), {
|
|
26
|
+
method,
|
|
27
|
+
headers,
|
|
28
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
29
|
+
});
|
|
30
|
+
const text = await res.text();
|
|
31
|
+
if (!res.ok) throw new Error(`${method} ${path} → ${res.status}: ${text}`);
|
|
32
|
+
return text ? JSON.parse(text) : {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Shared optional post fields (used by create / propose / update).
|
|
36
|
+
const postFields = {
|
|
37
|
+
title: z.string().optional().describe("Post title"),
|
|
38
|
+
slug: z.string().optional().describe("URL slug (auto-generated from title if omitted; cannot equal a category slug)"),
|
|
39
|
+
subtitle: z.string().optional().describe("Subtitle / deck"),
|
|
40
|
+
excerpt: z.string().optional().describe("Short summary shown on cards"),
|
|
41
|
+
blog_category_id: z.number().optional().describe("Category ID — OPTIONAL (posts can be uncategorized)"),
|
|
42
|
+
hero_image_url: z.string().optional().describe("Hero / cover image URL"),
|
|
43
|
+
og_image_url: z.string().optional().describe("Open Graph image URL"),
|
|
44
|
+
meta_title: z.string().optional().describe("SEO meta title"),
|
|
45
|
+
meta_description: z.string().optional().describe("SEO meta description"),
|
|
46
|
+
featured: z.boolean().optional().describe("Feature on the landing page"),
|
|
47
|
+
position: z.number().optional().describe("Sort position"),
|
|
48
|
+
search_keywords: z.array(z.string()).optional().describe("Extra search keywords for recall"),
|
|
49
|
+
tags: z.array(z.string()).optional().describe("Tag slugs (created if missing)"),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// --- Posts ---
|
|
53
|
+
|
|
54
|
+
server.tool(
|
|
55
|
+
"blog_list_posts",
|
|
56
|
+
"List blog posts (the public Resources surface, served at /resources) with optional filters. Returns post summaries and stats.",
|
|
57
|
+
{
|
|
58
|
+
status: z.enum(["draft", "published", "archived"]).optional().describe("Filter by status"),
|
|
59
|
+
review_status: z.string().optional().describe("Filter by review status"),
|
|
60
|
+
category_id: z.number().optional().describe("Filter by category ID"),
|
|
61
|
+
featured: z.boolean().optional().describe("Filter by featured flag"),
|
|
62
|
+
q: z.string().optional().describe("Search title and excerpt"),
|
|
63
|
+
},
|
|
64
|
+
async (params) => json(await request("GET", "/posts.json", { params }))
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
server.tool(
|
|
68
|
+
"blog_get_post",
|
|
69
|
+
"Get a blog post with full content, metadata, and version history.",
|
|
70
|
+
{ id: z.number().describe("Post ID") },
|
|
71
|
+
async ({ id }) => json(await request("GET", `/posts/${id}.json`))
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
server.tool(
|
|
75
|
+
"blog_create_post",
|
|
76
|
+
"Create a new blog post. Returns the post and its first version.",
|
|
77
|
+
{
|
|
78
|
+
...postFields,
|
|
79
|
+
title: z.string().describe("Post title"),
|
|
80
|
+
status: z.enum(["draft", "published", "archived"]).optional().describe("Initial status (default: draft)"),
|
|
81
|
+
content: z.any().optional().describe("Tiptap JSON content"),
|
|
82
|
+
commit_message: z.string().optional().describe("Version commit message"),
|
|
83
|
+
publish: z.boolean().optional().describe("Publish immediately after creation"),
|
|
84
|
+
},
|
|
85
|
+
async ({ publish, ...post }) => json(await request("POST", "/posts.json", { body: { post, publish } }))
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
server.tool(
|
|
89
|
+
"blog_propose_post",
|
|
90
|
+
"Submit an AI-authored blog post draft for human review. Creates an ai_cobolt version and marks the post as AI changes pending (not published).",
|
|
91
|
+
{
|
|
92
|
+
...postFields,
|
|
93
|
+
title: z.string().describe("Post title"),
|
|
94
|
+
content: z.any().describe("Tiptap JSON content"),
|
|
95
|
+
commit_message: z.string().optional().describe("Version commit message"),
|
|
96
|
+
ai_agent_id: z.string().optional().describe("Identifier for the AI agent submitting the draft"),
|
|
97
|
+
source_signal: z.record(z.any()).optional().describe("Evidence or trigger that led to this draft"),
|
|
98
|
+
},
|
|
99
|
+
async ({ ai_agent_id, source_signal, ...post }) =>
|
|
100
|
+
json(await request("POST", "/posts/propose.json", { body: { post, ai_agent_id, source_signal } }))
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
server.tool(
|
|
104
|
+
"blog_update_post",
|
|
105
|
+
"Update an existing blog post and create a new version.",
|
|
106
|
+
{
|
|
107
|
+
id: z.number().describe("Post ID"),
|
|
108
|
+
...postFields,
|
|
109
|
+
status: z.string().optional().describe("Status (draft/published/archived)"),
|
|
110
|
+
content: z.any().optional().describe("Tiptap JSON content"),
|
|
111
|
+
commit_message: z.string().optional().describe("Version commit message"),
|
|
112
|
+
publish: z.boolean().optional().describe("Publish this version immediately"),
|
|
113
|
+
},
|
|
114
|
+
async ({ id, publish, ...post }) => json(await request("PUT", `/posts/${id}.json`, { body: { post, publish } }))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
server.tool(
|
|
118
|
+
"blog_propose_post_update",
|
|
119
|
+
"Submit AI-authored changes to an existing blog post for human review. Creates an ai_cobolt pending version without publishing.",
|
|
120
|
+
{
|
|
121
|
+
id: z.number().describe("Post ID"),
|
|
122
|
+
...postFields,
|
|
123
|
+
content: z.any().optional().describe("Proposed Tiptap content; omit for metadata-only proposals"),
|
|
124
|
+
commit_message: z.string().optional().describe("Version commit message"),
|
|
125
|
+
ai_agent_id: z.string().optional().describe("Identifier for the AI agent submitting the proposal"),
|
|
126
|
+
source_signal: z.record(z.any()).optional().describe("Evidence or trigger that led to this update"),
|
|
127
|
+
},
|
|
128
|
+
async ({ id, ai_agent_id, source_signal, ...post }) =>
|
|
129
|
+
json(await request("POST", `/posts/${id}/propose_update.json`, { body: { post, ai_agent_id, source_signal } }))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
server.tool(
|
|
133
|
+
"blog_publish_post",
|
|
134
|
+
"Publish a blog post version. Publishes the latest version if no version_id is given.",
|
|
135
|
+
{
|
|
136
|
+
id: z.number().describe("Post ID"),
|
|
137
|
+
version_id: z.number().optional().describe("Specific version ID to publish (defaults to latest)"),
|
|
138
|
+
},
|
|
139
|
+
async ({ id, version_id }) =>
|
|
140
|
+
json(await request("POST", `/posts/${id}/publish.json`, { body: version_id ? { version_id } : {} }))
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
server.tool(
|
|
144
|
+
"blog_review_post",
|
|
145
|
+
"Approve or reject a pending blog post version.",
|
|
146
|
+
{
|
|
147
|
+
id: z.number().describe("Post ID"),
|
|
148
|
+
version_id: z.number().describe("Version ID to review"),
|
|
149
|
+
decision: z.enum(["approve", "reject"]).describe("Review decision"),
|
|
150
|
+
comment: z.string().optional().describe("Review comment"),
|
|
151
|
+
},
|
|
152
|
+
async ({ id, ...body }) => json(await request("POST", `/posts/${id}/review.json`, { body }))
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// --- Categories ---
|
|
156
|
+
|
|
157
|
+
server.tool("blog_list_categories", "List all blog categories.", {}, async () =>
|
|
158
|
+
json(await request("GET", "/categories.json")));
|
|
159
|
+
|
|
160
|
+
server.tool(
|
|
161
|
+
"blog_create_category",
|
|
162
|
+
"Create a new blog category.",
|
|
163
|
+
{
|
|
164
|
+
name: z.string().describe("Category name"),
|
|
165
|
+
slug: z.string().optional().describe("URL slug"),
|
|
166
|
+
description: z.string().optional().describe("Category description"),
|
|
167
|
+
icon_library: z.string().optional().describe("Icon library name"),
|
|
168
|
+
icon_name: z.string().optional().describe("Icon name"),
|
|
169
|
+
icon_color: z.string().optional().describe("Icon color hex"),
|
|
170
|
+
position: z.number().optional().describe("Sort position"),
|
|
171
|
+
published: z.boolean().optional().describe("Whether the category is published"),
|
|
172
|
+
},
|
|
173
|
+
async (category) => json(await request("POST", "/categories.json", { body: { category } }))
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
server.tool(
|
|
177
|
+
"blog_update_category",
|
|
178
|
+
"Update a blog category.",
|
|
179
|
+
{
|
|
180
|
+
id: z.number().describe("Category ID"),
|
|
181
|
+
name: z.string().optional(),
|
|
182
|
+
slug: z.string().optional(),
|
|
183
|
+
description: z.string().optional(),
|
|
184
|
+
icon_library: z.string().optional(),
|
|
185
|
+
icon_name: z.string().optional(),
|
|
186
|
+
icon_color: z.string().optional(),
|
|
187
|
+
position: z.number().optional(),
|
|
188
|
+
published: z.boolean().optional(),
|
|
189
|
+
},
|
|
190
|
+
async ({ id, ...category }) => json(await request("PUT", `/categories/${id}.json`, { body: { category } }))
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
server.tool("blog_delete_category", "Delete a blog category.", { id: z.number().describe("Category ID") },
|
|
194
|
+
async ({ id }) => json(await request("DELETE", `/categories/${id}.json`)));
|
|
195
|
+
|
|
196
|
+
// --- Tags ---
|
|
197
|
+
|
|
198
|
+
server.tool("blog_list_tags", "List all blog tags.", {}, async () =>
|
|
199
|
+
json(await request("GET", "/tags.json")));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function json(data) {
|
|
203
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
204
|
+
}
|
|
@@ -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.",
|