@sutraspaces/mcp-server 1.3.0 → 1.4.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 +9 -0
- package/package.json +1 -1
- package/src/index.js +3 -1
- package/src/resources/admin-api.js +4 -1
- package/src/tools/design.js +8 -2
- package/src/tools/website.js +195 -0
package/README.md
CHANGED
|
@@ -162,6 +162,15 @@ Use `sp_...` IDs, not slugs. For normal course and section pages, use `placement
|
|
|
162
162
|
- **publish_design_draft** — Publish a draft with conflict and destructive-omission protection
|
|
163
163
|
- **restore_design_draft** — Restore the pre-publish backup for a published design draft
|
|
164
164
|
- **import_design_asset** — Re-host an external image URL and return the canonical Sutra URL
|
|
165
|
+
|
|
166
|
+
### Website Tools
|
|
167
|
+
|
|
168
|
+
Website mode is root-scoped (any managed space resolves to its website root) and tier-gated (enabling requires the Silver plan or above). Enabling auto-creates a default header (a navbar) and footer; you then author those configs. A header's nav lives in a NavbarConfig referenced by the site_header share block's navbar node.
|
|
169
|
+
|
|
170
|
+
- **get_space_website** / **update_space_website** — Read and set website-mode state: enable/disable, `show_spaces_in_page`, and the header/footer share-block pointers
|
|
171
|
+
- **list_share_block_configs** / **get_share_block_config** / **create_share_block_config** / **update_share_block_config** / **delete_share_block_config** — Manage site_header / site_footer / generic share blocks (deleting an in-use header or footer returns 409)
|
|
172
|
+
- **list_navbar_configs** / **get_navbar_config** / **create_navbar_config** / **update_navbar_config** / **delete_navbar_config** / **duplicate_navbar_config** — Manage the website header's navbar (links, logo, title, alignment)
|
|
173
|
+
|
|
165
174
|
- **list_messages** / **get_message** / **create_message** / **update_message** / **delete_message** — Manage discussion messages
|
|
166
175
|
- **list_reflections** / **get_reflection** / **create_reflection** / **update_reflection** / **delete_reflection** — Manage threaded replies
|
|
167
176
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { registerAutomationTools } from "./tools/automations.js";
|
|
|
21
21
|
import { registerHelpCenterTools } from "./tools/help-center.js";
|
|
22
22
|
import { registerBlogTools } from "./tools/blog.js";
|
|
23
23
|
import { registerDesignTools } from "./tools/design.js";
|
|
24
|
+
import { registerWebsiteTools } from "./tools/website.js";
|
|
24
25
|
import { registerAdminApiResources } from "./resources/admin-api.js";
|
|
25
26
|
import { registerDesignResources } from "./resources/design.js";
|
|
26
27
|
import { getValidAccessToken, login, refresh } from "./auth/oauth.js";
|
|
@@ -121,7 +122,7 @@ if (SUTRA_API_TOKEN) {
|
|
|
121
122
|
const server = new McpServer({
|
|
122
123
|
name: "sutra",
|
|
123
124
|
// Keep in lockstep with package.json.
|
|
124
|
-
version: "1.
|
|
125
|
+
version: "1.4.0",
|
|
125
126
|
description:
|
|
126
127
|
"Sutra Admin API — manage spaces, members, contacts, content, discussions, surveys, plans, broadcasts, and more.",
|
|
127
128
|
});
|
|
@@ -141,6 +142,7 @@ registerBroadcastTools(server, client);
|
|
|
141
142
|
registerDeepTool(server, client);
|
|
142
143
|
registerAutomationTools(server, client);
|
|
143
144
|
registerDesignTools(server, client);
|
|
145
|
+
registerWebsiteTools(server, client);
|
|
144
146
|
registerHelpCenterTools(server);
|
|
145
147
|
registerBlogTools(server);
|
|
146
148
|
registerAdminApiResources(server);
|
|
@@ -14,7 +14,8 @@ Space configuration tools:
|
|
|
14
14
|
- update_space_appearance covers theme colors, fonts, accents, gradients, corner style, density, and hidden layout elements.
|
|
15
15
|
- set_space_image sets or removes the cover, logo, thumbnail, or meta image from a URL.
|
|
16
16
|
- update_space_payment enables/disables paid access (plans system) and configures checkout toggles. Plans themselves are managed with create_plan/update_plan/delete_plan (amounts in integer cents).
|
|
17
|
-
-
|
|
17
|
+
- update_space_website enables/disables website mode (root-scoped; Silver+ to enable) and points the site at header/footer share blocks. Enabling auto-creates a default header + footer. Author the header with create/update_navbar_config (links, logo, title) referenced by a site_header share block, and the footer with update_share_block_config. get_space_website reads the current state.
|
|
18
|
+
- get_space returns the detail read with nested settings, appearance, payment, and website objects.
|
|
18
19
|
|
|
19
20
|
Important IDs:
|
|
20
21
|
- Spaces use sp_ IDs.
|
|
@@ -23,6 +24,8 @@ Important IDs:
|
|
|
23
24
|
- Users use usr_ IDs.
|
|
24
25
|
- Member property definitions use mprop_ IDs.
|
|
25
26
|
- Space property definitions use sprop_ IDs.
|
|
27
|
+
- Share-block configs (website header/footer) use shbk_ IDs.
|
|
28
|
+
- Navbar configs use nvbr_ IDs.
|
|
26
29
|
|
|
27
30
|
Pagination:
|
|
28
31
|
- List tools return data and pagination.
|
package/src/tools/design.js
CHANGED
|
@@ -42,7 +42,11 @@ export function registerDesignTools(server, client) {
|
|
|
42
42
|
"Validate proposed Sutra design nodes and return structured diagnostics. Validate before creating or publishing a draft.",
|
|
43
43
|
{
|
|
44
44
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
45
|
-
|
|
45
|
+
// Typed as an array (not z.any()) so MCP clients serialize it as
|
|
46
|
+
// structured JSON. With an untyped schema the harness can stringify the
|
|
47
|
+
// value, and the API's Document.wrap rejects a JSON string. Pass the bare
|
|
48
|
+
// node array; the API still also accepts a wrapped doc when not stringified.
|
|
49
|
+
nodes: z.array(z.any()).describe("Array of Tiptap design node objects (bare array — a stringified JSON doc is rejected by the API)."),
|
|
46
50
|
base_digest: z.string().optional().describe("Digest from get_space_design, if available"),
|
|
47
51
|
},
|
|
48
52
|
async ({ space_id, nodes, base_digest }) => {
|
|
@@ -56,7 +60,9 @@ export function registerDesignTools(server, client) {
|
|
|
56
60
|
"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
61
|
{
|
|
58
62
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
59
|
-
|
|
63
|
+
// Typed as an array (see validate_space_design) so the value is sent as
|
|
64
|
+
// structured JSON rather than being stringified by the MCP client.
|
|
65
|
+
nodes: z.array(z.any()).describe("Array of Tiptap design node objects (bare array — a stringified JSON doc is rejected by the API)."),
|
|
60
66
|
base_digest: z.string().describe("Digest from get_space_design"),
|
|
61
67
|
client_draft_key: z.string().describe("Stable key for this draft variant/session"),
|
|
62
68
|
title: z.string().optional().describe("Human-readable draft title"),
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Website mode + header/footer authoring.
|
|
4
|
+
//
|
|
5
|
+
// Website mode is ROOT-scoped: pass any space_id you manage and the API
|
|
6
|
+
// resolves it to its website root. Enabling is tier-gated (Silver plan and
|
|
7
|
+
// above) and auto-creates a default header (a navbar) + footer; you then edit
|
|
8
|
+
// those configs. The header's nav (links/logo/title/alignment) lives in a
|
|
9
|
+
// NavbarConfig that the site_header share block references via a navbar node.
|
|
10
|
+
//
|
|
11
|
+
// Tiptap arrays (content/links) and style objects are TYPED (z.array/z.record),
|
|
12
|
+
// never bare z.any(), so the MCP client serializes them as structured JSON
|
|
13
|
+
// rather than stringifying them (which the API rejects).
|
|
14
|
+
export function registerWebsiteTools(server, client) {
|
|
15
|
+
server.tool(
|
|
16
|
+
"get_space_website",
|
|
17
|
+
"Get a space's website-mode state: enabled, show_spaces_in_page, and the header/footer share-block config IDs (shbk_...). Website mode is root-scoped — any managed space resolves to its website root.",
|
|
18
|
+
{
|
|
19
|
+
space_id: z.string().describe("Space ID (sp_...). Resolves to its website root."),
|
|
20
|
+
},
|
|
21
|
+
async ({ space_id }) => json(await client.get(`/spaces/${space_id}/website`))
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
server.tool(
|
|
25
|
+
"update_space_website",
|
|
26
|
+
"Enable/disable website mode and point the website at header/footer share blocks. enabled:true is tier-gated (Silver+) and, on first enable, auto-creates a default header (navbar) + footer; enabled:false preserves the configs so re-enabling is lossless. header_config_id/footer_config_id must reference a site_header/site_footer share block in the same space (null clears the pointer). Operates on the resolved website root. Typical flow: enable -> get_space_website to read the auto-created config IDs -> update_navbar_config / update_share_block_config to author them.",
|
|
27
|
+
{
|
|
28
|
+
space_id: z.string().describe("Space ID (sp_...). Resolves to its website root."),
|
|
29
|
+
enabled: z.boolean().optional().describe("true enables website mode (Silver+); false disables it (configs are preserved)."),
|
|
30
|
+
show_spaces_in_page: z.boolean().optional().describe("Show child spaces inline in the page while in website mode."),
|
|
31
|
+
header_config_id: z.string().nullable().optional().describe("shbk_ ID of a site_header share block to use as the header, or null to clear."),
|
|
32
|
+
footer_config_id: z.string().nullable().optional().describe("shbk_ ID of a site_footer share block to use as the footer, or null to clear."),
|
|
33
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
34
|
+
},
|
|
35
|
+
async ({ space_id, idempotency_key, ...updates }) => {
|
|
36
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
37
|
+
return json(await client.patch(`/spaces/${space_id}/website`, updates, headers));
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// ----- Share block configs (site_header / site_footer / generic) ----------
|
|
42
|
+
|
|
43
|
+
server.tool(
|
|
44
|
+
"list_share_block_configs",
|
|
45
|
+
"List the share-block configs (kinds: site_header, site_footer, generic) for a space's website root.",
|
|
46
|
+
{
|
|
47
|
+
space_id: z.string().describe("Space ID (sp_...). Resolves to its website root."),
|
|
48
|
+
},
|
|
49
|
+
async ({ space_id }) => json(await client.get(`/spaces/${space_id}/share_block_configs`))
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
server.tool(
|
|
53
|
+
"get_share_block_config",
|
|
54
|
+
"Get one share-block config, including its full Tiptap content and style.",
|
|
55
|
+
{
|
|
56
|
+
space_id: z.string().describe("Space ID (sp_...)."),
|
|
57
|
+
config_id: z.string().describe("Share-block config ID (shbk_...)."),
|
|
58
|
+
},
|
|
59
|
+
async ({ space_id, config_id }) => json(await client.get(`/spaces/${space_id}/share_block_configs/${config_id}`))
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
server.tool(
|
|
63
|
+
"create_share_block_config",
|
|
64
|
+
"Create a share-block config. For a website header use kind 'site_header' with content = [{ type:'navbar', attrs:{ uid:'<uuid>', navbarConfigId:<int> } }] (create the navbar first with create_navbar_config); for a footer use kind 'site_footer' with freeform blocks. name must be unique within the space.",
|
|
65
|
+
{
|
|
66
|
+
space_id: z.string().describe("Space ID (sp_...). Resolves to its website root."),
|
|
67
|
+
name: z.string().describe("Config name (unique per space)."),
|
|
68
|
+
kind: z.enum(["generic", "site_header", "site_footer"]).optional().describe("Defaults to generic."),
|
|
69
|
+
content: z.array(z.any()).optional().describe("Tiptap node array (bare array, not a stringified doc)."),
|
|
70
|
+
style: z.record(z.any()).optional().describe("Style object (passthrough)."),
|
|
71
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
72
|
+
},
|
|
73
|
+
async ({ space_id, idempotency_key, ...body }) => {
|
|
74
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
75
|
+
return json(await client.post(`/spaces/${space_id}/share_block_configs`, body, headers));
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
server.tool(
|
|
80
|
+
"update_share_block_config",
|
|
81
|
+
"Update a share-block config's name, content, or style. Kind is immutable through this tool (changing a header's kind would break the website pointer).",
|
|
82
|
+
{
|
|
83
|
+
space_id: z.string().describe("Space ID (sp_...)."),
|
|
84
|
+
config_id: z.string().describe("Share-block config ID (shbk_...)."),
|
|
85
|
+
name: z.string().optional(),
|
|
86
|
+
content: z.array(z.any()).optional().describe("Tiptap node array (bare array)."),
|
|
87
|
+
style: z.record(z.any()).optional(),
|
|
88
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
89
|
+
},
|
|
90
|
+
async ({ space_id, config_id, idempotency_key, ...body }) => {
|
|
91
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
92
|
+
return json(await client.patch(`/spaces/${space_id}/share_block_configs/${config_id}`, body, headers));
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
server.tool(
|
|
97
|
+
"delete_share_block_config",
|
|
98
|
+
"Delete a share-block config. Returns 409 config_in_use if it is the active website header or footer — repoint or disable website mode first.",
|
|
99
|
+
{
|
|
100
|
+
space_id: z.string().describe("Space ID (sp_...)."),
|
|
101
|
+
config_id: z.string().describe("Share-block config ID (shbk_...)."),
|
|
102
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
103
|
+
},
|
|
104
|
+
async ({ space_id, config_id, idempotency_key }) => {
|
|
105
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
106
|
+
return json(await client.delete(`/spaces/${space_id}/share_block_configs/${config_id}`, headers));
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// ----- Navbar configs (the website header's nav) --------------------------
|
|
111
|
+
|
|
112
|
+
server.tool(
|
|
113
|
+
"list_navbar_configs",
|
|
114
|
+
"List the navbar configs for a space's website root.",
|
|
115
|
+
{
|
|
116
|
+
space_id: z.string().describe("Space ID (sp_...). Resolves to its website root."),
|
|
117
|
+
},
|
|
118
|
+
async ({ space_id }) => json(await client.get(`/spaces/${space_id}/navbar_configs`))
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
server.tool(
|
|
122
|
+
"get_navbar_config",
|
|
123
|
+
"Get one navbar config, including its links and style.",
|
|
124
|
+
{
|
|
125
|
+
space_id: z.string().describe("Space ID (sp_...)."),
|
|
126
|
+
config_id: z.string().describe("Navbar config ID (nvbr_...)."),
|
|
127
|
+
},
|
|
128
|
+
async ({ space_id, config_id }) => json(await client.get(`/spaces/${space_id}/navbar_configs/${config_id}`))
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
server.tool(
|
|
132
|
+
"create_navbar_config",
|
|
133
|
+
"Create a navbar config (links + style) for a website header. Reference it from a site_header share block's navbar node via navbarConfigId. name must be unique within the space.",
|
|
134
|
+
{
|
|
135
|
+
space_id: z.string().describe("Space ID (sp_...). Resolves to its website root."),
|
|
136
|
+
name: z.string().describe("Config name (unique per space)."),
|
|
137
|
+
links: z.array(z.any()).optional().describe("Navbar link objects (bare array)."),
|
|
138
|
+
style: z.record(z.any()).optional().describe("Navbar style object (linkSource, alignment, showLogo, titleText, etc.)."),
|
|
139
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
140
|
+
},
|
|
141
|
+
async ({ space_id, idempotency_key, ...body }) => {
|
|
142
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
143
|
+
return json(await client.post(`/spaces/${space_id}/navbar_configs`, body, headers));
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
server.tool(
|
|
148
|
+
"update_navbar_config",
|
|
149
|
+
"Update a navbar config's name, links, or style.",
|
|
150
|
+
{
|
|
151
|
+
space_id: z.string().describe("Space ID (sp_...)."),
|
|
152
|
+
config_id: z.string().describe("Navbar config ID (nvbr_...)."),
|
|
153
|
+
name: z.string().optional(),
|
|
154
|
+
links: z.array(z.any()).optional().describe("Navbar link objects (bare array)."),
|
|
155
|
+
style: z.record(z.any()).optional(),
|
|
156
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
157
|
+
},
|
|
158
|
+
async ({ space_id, config_id, idempotency_key, ...body }) => {
|
|
159
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
160
|
+
return json(await client.patch(`/spaces/${space_id}/navbar_configs/${config_id}`, body, headers));
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
server.tool(
|
|
165
|
+
"delete_navbar_config",
|
|
166
|
+
"Delete a navbar config. A site_header still referencing it by navbarConfigId will render an empty nav until repointed.",
|
|
167
|
+
{
|
|
168
|
+
space_id: z.string().describe("Space ID (sp_...)."),
|
|
169
|
+
config_id: z.string().describe("Navbar config ID (nvbr_...)."),
|
|
170
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
171
|
+
},
|
|
172
|
+
async ({ space_id, config_id, idempotency_key }) => {
|
|
173
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
174
|
+
return json(await client.delete(`/spaces/${space_id}/navbar_configs/${config_id}`, headers));
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
server.tool(
|
|
179
|
+
"duplicate_navbar_config",
|
|
180
|
+
"Duplicate a navbar config (copies its links and style under a new unique name).",
|
|
181
|
+
{
|
|
182
|
+
space_id: z.string().describe("Space ID (sp_...)."),
|
|
183
|
+
config_id: z.string().describe("Navbar config ID (nvbr_...) to duplicate."),
|
|
184
|
+
idempotency_key: z.string().optional().describe("Idempotency-Key header for safe retries"),
|
|
185
|
+
},
|
|
186
|
+
async ({ space_id, config_id, idempotency_key }) => {
|
|
187
|
+
const headers = idempotency_key ? { "Idempotency-Key": idempotency_key } : undefined;
|
|
188
|
+
return json(await client.post(`/spaces/${space_id}/navbar_configs/${config_id}/duplicate`, {}, headers));
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function json(data) {
|
|
194
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
195
|
+
}
|