@sutraspaces/mcp-server 1.0.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 ADDED
@@ -0,0 +1,134 @@
1
+ # Sutra MCP Server
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server that connects AI agents to the [Sutra Admin API](https://sutra.co). Manage spaces, members, content, discussions, surveys, plans, broadcasts, and more — all through your AI tools.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm install
9
+ ```
10
+
11
+ Set your Sutra API token:
12
+
13
+ ```bash
14
+ export SUTRA_API_TOKEN="sutra_live_sk_..."
15
+ ```
16
+
17
+ You can get an API token from your Sutra account settings or by contacting support@sutra.co.
18
+
19
+ ## Usage
20
+
21
+ ### Claude Desktop
22
+
23
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "sutra": {
29
+ "command": "node",
30
+ "args": ["/path/to/sutra-mcp/src/index.js"],
31
+ "env": {
32
+ "SUTRA_API_TOKEN": "sutra_live_sk_..."
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Claude Code
40
+
41
+ ```bash
42
+ claude mcp add sutra -- env SUTRA_API_TOKEN=sutra_live_sk_... node /path/to/sutra-mcp/src/index.js
43
+ ```
44
+
45
+ ### Cursor / Windsurf / Other MCP Clients
46
+
47
+ Point your MCP client at the server entry point with the `SUTRA_API_TOKEN` environment variable set. The server communicates over stdio.
48
+
49
+ ## Configuration
50
+
51
+ | Variable | Required | Description |
52
+ |---|---|---|
53
+ | `SUTRA_API_TOKEN` | Yes | Your Sutra Admin API token (`sutra_live_sk_...`) |
54
+ | `SUTRA_BASE_URL` | No | Override the API URL (default: `https://api.sutra.co/api/admin/v1`) |
55
+
56
+ ## Available Tools
57
+
58
+ ### Deep Traversal
59
+ - **get_space_deep** — Recursively fetch a space and all its nested content in one call: content blocks, discussion messages, reflections, and child spaces. Rich text is converted to plain text. Configurable depth, item limits, and whether to include reflections. Best for exploring an entire course, community, or discussion hierarchy.
60
+
61
+ ### Spaces
62
+ - **list_spaces** — List all spaces the API token can access
63
+ - **get_space** — Get details for a single space
64
+ - **list_child_spaces** — List direct child spaces
65
+ - **create_space** — Create a new space (top-level or child)
66
+ - **update_space** — Update space name, description, type, privacy, or state
67
+ - **delete_space** — Delete/archive a space
68
+ - **reorder_child_spaces** — Reorder children within a parent
69
+
70
+ ### Members
71
+ - **list_members** — List space members with optional email/user_id filtering
72
+ - **get_member** — Get member details
73
+ - **add_member** — Add a member by email or user ID
74
+ - **update_member** — Update role or notification settings
75
+ - **remove_member** — Remove a member (cascades to descendant spaces)
76
+ - **approve_member** — Approve a pending member
77
+ - **bulk_add_members** — Add up to 100 members at once
78
+ - **bulk_remove_members** — Remove up to 100 members at once
79
+
80
+ ### Content & Discussions
81
+ - **list_content** / **get_content_block** / **create_content** / **update_content** / **delete_content** — Manage content blocks
82
+ - **reorder_content** — Reorder content blocks within a space
83
+ - **list_messages** / **get_message** / **create_message** / **update_message** / **delete_message** — Manage discussion messages
84
+ - **list_reflections** / **get_reflection** / **create_reflection** / **update_reflection** / **delete_reflection** — Manage threaded replies
85
+
86
+ ### Properties
87
+ - **list_member_properties** / **create_member_property** / **update_member_property** / **delete_member_property** — Manage custom member property definitions
88
+ - **get_member_property_values** / **update_member_property_values** — Read and set property values per member
89
+ - **list_space_properties** / **create_space_property** / **update_space_property** / **delete_space_property** — Manage custom space property definitions
90
+ - **get_space_property_values** / **update_space_property_values** — Read and set property values per space
91
+
92
+ ### Invitations
93
+ - **list_invitations** / **get_invitation** / **create_invitation** / **update_invitation** / **delete_invitation** — Manage invitations
94
+ - **resend_invitation** — Resend an invitation email
95
+ - **bulk_create_invitations** — Create up to 100 invitations at once
96
+
97
+ ### Surveys
98
+ - **list_surveys** / **get_survey** / **create_survey** / **update_survey** / **delete_survey** — Manage surveys
99
+ - **get_survey_submissions** — Read survey responses
100
+
101
+ ### Plans & Enrollments
102
+ - **list_plans** / **get_plan** / **create_plan** / **update_plan** / **delete_plan** — Manage plans
103
+ - **list_enrollments** / **get_enrollment** — Read enrollments
104
+ - **list_payments** / **get_payment** — Read payment history
105
+
106
+ ### Coupons
107
+ - **list_coupons** / **get_coupon** / **create_coupon** / **update_coupon** / **delete_coupon** — Manage discount codes
108
+
109
+ ### Broadcasts
110
+ - **list_broadcasts** / **get_broadcast** / **create_broadcast** / **update_broadcast** / **delete_broadcast** — Manage broadcasts
111
+ - **send_broadcast** — Send a broadcast to space members
112
+ - **get_broadcast_delivery_status** — Check delivery progress
113
+
114
+ ## API Concepts
115
+
116
+ **Spaces** are the core building block — they can be courses, communities, forums, or any structured container. Spaces form hierarchies through parent-child relationships.
117
+
118
+ **Members** belong to spaces. They have roles (member, editor, moderator) and can have custom properties attached.
119
+
120
+ **Content blocks** are structured content created by facilitators. **Messages** are discussion posts from participants. **Reflections** are threaded replies to messages.
121
+
122
+ **All IDs** use readable prefixes: `sp_` (space), `mem_` (member), `usr_` (user), `blk_` (block/message), `reply_` (reflection), `surv_` (survey), `plan_` (plan), `enr_` (enrollment), `pay_` (payment), `cpn_` (coupon), `bcst_` (broadcast), `inv_` (invitation), `mprop_` / `sprop_` (property definitions).
123
+
124
+ **Pagination** is cursor-based. List endpoints return `{ data, pagination: { next_cursor, has_more } }`. Pass `cursor` to get the next page.
125
+
126
+ **Scopes** control what the API token can access. Tokens have read/write scope pairs like `spaces:read`, `members:write`, etc. Operations that exceed the token's scopes will return 403.
127
+
128
+ ## Architecture
129
+
130
+ ```
131
+ AI Agent → MCP Protocol (stdio) → sutra-mcp → Sutra Admin API → Sutra Platform
132
+ ```
133
+
134
+ The server is a thin, stateless wrapper. All data flows through the Sutra Admin API with Bearer token authentication. No data is cached or stored locally.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@sutraspaces/mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for the Sutra Admin API — manage spaces, members, content, and more via AI agents",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "sutra-mcp": "src/index.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "start": "node src/index.js"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.12.1",
19
+ "zod": "^3.23.0"
20
+ },
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "sutra",
28
+ "ai",
29
+ "agent",
30
+ "lms",
31
+ "learning",
32
+ "community"
33
+ ],
34
+ "author": "Sutra <support@sutra.co> (https://sutra.co)",
35
+ "license": "MIT",
36
+ "homepage": "https://github.com/joinsutra/mcp-server",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/joinsutra/mcp-server.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/joinsutra/mcp-server/issues"
43
+ }
44
+ }
package/src/client.js ADDED
@@ -0,0 +1,66 @@
1
+ const BASE_URL = "https://api.sutra.co/api/admin/v1";
2
+
3
+ export class SutraAdminClient {
4
+ constructor(apiToken, baseUrl) {
5
+ this.apiToken = apiToken;
6
+ this.baseUrl = (baseUrl || BASE_URL).replace(/\/+$/, "");
7
+ }
8
+
9
+ async request(method, path, { params, body, headers: extra } = {}) {
10
+ const url = new URL(`${this.baseUrl}${path}`);
11
+ if (params) {
12
+ for (const [k, v] of Object.entries(params)) {
13
+ if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
14
+ }
15
+ }
16
+
17
+ const headers = {
18
+ Authorization: `Bearer ${this.apiToken}`,
19
+ Accept: "application/json",
20
+ ...(body ? { "Content-Type": "application/json" } : {}),
21
+ ...extra,
22
+ };
23
+
24
+ const res = await fetch(url.toString(), {
25
+ method,
26
+ headers,
27
+ body: body ? JSON.stringify(body) : undefined,
28
+ });
29
+
30
+ const text = await res.text();
31
+ if (!res.ok) {
32
+ throw new Error(`${method} ${path} → ${res.status}: ${text}`);
33
+ }
34
+
35
+ return text ? JSON.parse(text) : {};
36
+ }
37
+
38
+ get(path, params) {
39
+ return this.request("GET", path, { params });
40
+ }
41
+
42
+ post(path, body, headers) {
43
+ return this.request("POST", path, { body, headers });
44
+ }
45
+
46
+ patch(path, body, headers) {
47
+ return this.request("PATCH", path, { body, headers });
48
+ }
49
+
50
+ delete(path, headers) {
51
+ return this.request("DELETE", path, { headers });
52
+ }
53
+
54
+ async getAll(path, params = {}, maxItems = 500) {
55
+ const items = [];
56
+ let cursor = undefined;
57
+ while (items.length < maxItems) {
58
+ const res = await this.get(path, { ...params, limit: 100, cursor });
59
+ const data = res.data || [];
60
+ items.push(...data);
61
+ if (!res.pagination?.has_more || !res.pagination?.next_cursor) break;
62
+ cursor = res.pagination.next_cursor;
63
+ }
64
+ return items.slice(0, maxItems);
65
+ }
66
+ }
package/src/index.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ import { SutraAdminClient } from "./client.js";
7
+ import { registerSpaceTools } from "./tools/spaces.js";
8
+ import { registerMemberTools } from "./tools/members.js";
9
+ import { registerContentTools } from "./tools/content.js";
10
+ import { registerPropertyTools } from "./tools/properties.js";
11
+ import { registerInvitationTools } from "./tools/invitations.js";
12
+ import { registerSurveyTools } from "./tools/surveys.js";
13
+ import { registerPlanTools } from "./tools/plans.js";
14
+ import { registerCouponTools } from "./tools/coupons.js";
15
+ import { registerBroadcastTools } from "./tools/broadcasts.js";
16
+ import { registerDeepTool } from "./tools/deep.js";
17
+
18
+ const SUTRA_API_TOKEN = process.env.SUTRA_API_TOKEN;
19
+ const SUTRA_BASE_URL = process.env.SUTRA_BASE_URL;
20
+
21
+ if (!SUTRA_API_TOKEN) {
22
+ console.error(
23
+ "SUTRA_API_TOKEN is required.\n\n" +
24
+ "Set it to your Sutra Admin API token (sutra_live_sk_...).\n" +
25
+ "Get one from your Sutra account settings or contact support@sutra.co.\n"
26
+ );
27
+ process.exit(1);
28
+ }
29
+
30
+ const server = new McpServer({
31
+ name: "sutra",
32
+ version: "1.0.0",
33
+ description:
34
+ "Sutra Admin API — manage spaces, members, content, discussions, surveys, plans, broadcasts, and more.",
35
+ });
36
+
37
+ const client = new SutraAdminClient(SUTRA_API_TOKEN, SUTRA_BASE_URL);
38
+
39
+ registerSpaceTools(server, client);
40
+ registerMemberTools(server, client);
41
+ registerContentTools(server, client);
42
+ registerPropertyTools(server, client);
43
+ registerInvitationTools(server, client);
44
+ registerSurveyTools(server, client);
45
+ registerPlanTools(server, client);
46
+ registerCouponTools(server, client);
47
+ registerBroadcastTools(server, client);
48
+ registerDeepTool(server, client);
49
+
50
+ async function main() {
51
+ const transport = new StdioServerTransport();
52
+ await server.connect(transport);
53
+ console.error("Sutra MCP server running on stdio");
54
+ }
55
+
56
+ main().catch((err) => {
57
+ console.error("Fatal:", err);
58
+ process.exit(1);
59
+ });
@@ -0,0 +1,102 @@
1
+ import { z } from "zod";
2
+
3
+ export function registerBroadcastTools(server, client) {
4
+ server.tool(
5
+ "list_broadcasts",
6
+ "List broadcasts for a space. Broadcasts are mass communications sent to space members.",
7
+ {
8
+ space_id: z.string().describe("Space ID (sp_...)"),
9
+ limit: z.number().optional().describe("Max results (default 25)"),
10
+ cursor: z.string().optional().describe("Pagination cursor"),
11
+ },
12
+ async ({ space_id, limit, cursor }) => {
13
+ const data = await client.get(`/spaces/${space_id}/broadcasts`, { limit, cursor });
14
+ return json(data);
15
+ }
16
+ );
17
+
18
+ server.tool(
19
+ "get_broadcast",
20
+ "Get a single broadcast by ID.",
21
+ {
22
+ broadcast_id: z.string().describe("Broadcast ID (bcst_...)"),
23
+ },
24
+ async ({ broadcast_id }) => {
25
+ const data = await client.get(`/broadcasts/${broadcast_id}`);
26
+ return json(data);
27
+ }
28
+ );
29
+
30
+ server.tool(
31
+ "create_broadcast",
32
+ "Create a draft broadcast for a space. Use send_broadcast to actually send it.",
33
+ {
34
+ space_id: z.string().describe("Space ID (sp_...)"),
35
+ title: z.string().optional().describe("Broadcast title/subject"),
36
+ content: z.any().optional().describe("Broadcast content (rich text or plain)"),
37
+ },
38
+ async ({ space_id, ...body }) => {
39
+ const data = await client.post(`/spaces/${space_id}/broadcasts`, body);
40
+ return json(data);
41
+ }
42
+ );
43
+
44
+ server.tool(
45
+ "update_broadcast",
46
+ "Update a draft broadcast before sending.",
47
+ {
48
+ broadcast_id: z.string().describe("Broadcast ID (bcst_...)"),
49
+ title: z.string().optional(),
50
+ content: z.any().optional(),
51
+ },
52
+ async ({ broadcast_id, ...body }) => {
53
+ const data = await client.patch(`/broadcasts/${broadcast_id}`, body);
54
+ return json(data);
55
+ }
56
+ );
57
+
58
+ server.tool(
59
+ "delete_broadcast",
60
+ "Delete a broadcast.",
61
+ {
62
+ broadcast_id: z.string().describe("Broadcast ID (bcst_...)"),
63
+ },
64
+ async ({ broadcast_id }) => {
65
+ const data = await client.delete(`/broadcasts/${broadcast_id}`);
66
+ return json(data);
67
+ }
68
+ );
69
+
70
+ server.tool(
71
+ "send_broadcast",
72
+ "Send a broadcast to space members. This is irreversible. Requires broadcasts:send scope and an Idempotency-Key.",
73
+ {
74
+ broadcast_id: z.string().describe("Broadcast ID (bcst_...)"),
75
+ idempotency_key: z.string().describe("Unique key for this send operation"),
76
+ },
77
+ async ({ broadcast_id, idempotency_key }) => {
78
+ const data = await client.post(
79
+ `/broadcasts/${broadcast_id}/send`,
80
+ {},
81
+ { "Idempotency-Key": idempotency_key }
82
+ );
83
+ return json(data);
84
+ }
85
+ );
86
+
87
+ server.tool(
88
+ "get_broadcast_delivery_status",
89
+ "Check delivery status of a sent broadcast.",
90
+ {
91
+ broadcast_id: z.string().describe("Broadcast ID (bcst_...)"),
92
+ },
93
+ async ({ broadcast_id }) => {
94
+ const data = await client.get(`/broadcasts/${broadcast_id}/delivery_status`);
95
+ return json(data);
96
+ }
97
+ );
98
+ }
99
+
100
+ function json(data) {
101
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
102
+ }
@@ -0,0 +1,228 @@
1
+ import { z } from "zod";
2
+
3
+ export function registerContentTools(server, client) {
4
+ // --- Content Blocks ---
5
+
6
+ server.tool(
7
+ "list_content",
8
+ "List content blocks in a space. Content blocks are the structured content that facilitators create on space pages.",
9
+ {
10
+ space_id: z.string().describe("Space ID (sp_...)"),
11
+ limit: z.number().optional().describe("Max results (default 25)"),
12
+ cursor: z.string().optional().describe("Pagination cursor"),
13
+ },
14
+ async ({ space_id, limit, cursor }) => {
15
+ const data = await client.get(`/spaces/${space_id}/content`, { limit, cursor });
16
+ return json(data);
17
+ }
18
+ );
19
+
20
+ server.tool(
21
+ "get_content_block",
22
+ "Get a single content block by its ID.",
23
+ {
24
+ block_id: z.string().describe("Content block ID (blk_...)"),
25
+ },
26
+ async ({ block_id }) => {
27
+ const data = await client.get(`/content/${block_id}`);
28
+ return json(data);
29
+ }
30
+ );
31
+
32
+ server.tool(
33
+ "create_content",
34
+ "Create a content block in a space.",
35
+ {
36
+ space_id: z.string().describe("Space ID (sp_...)"),
37
+ content: z.any().optional().describe("Rich text content (Tiptap JSON object)"),
38
+ raw_content: z.string().optional().describe("Plain text content"),
39
+ message_body: z.string().optional().describe("Message body text"),
40
+ priority: z.number().optional().describe("Sort order priority"),
41
+ },
42
+ async ({ space_id, ...body }) => {
43
+ const data = await client.post(`/spaces/${space_id}/content`, body);
44
+ return json(data);
45
+ }
46
+ );
47
+
48
+ server.tool(
49
+ "update_content",
50
+ "Update a content block.",
51
+ {
52
+ block_id: z.string().describe("Content block ID (blk_...)"),
53
+ content: z.any().optional().describe("Rich text content (Tiptap JSON)"),
54
+ raw_content: z.string().optional().describe("Plain text content"),
55
+ priority: z.number().optional().describe("Sort order priority"),
56
+ },
57
+ async ({ block_id, ...body }) => {
58
+ const data = await client.patch(`/content/${block_id}`, body);
59
+ return json(data);
60
+ }
61
+ );
62
+
63
+ server.tool(
64
+ "delete_content",
65
+ "Delete a content block.",
66
+ {
67
+ block_id: z.string().describe("Content block ID (blk_...)"),
68
+ },
69
+ async ({ block_id }) => {
70
+ const data = await client.delete(`/content/${block_id}`);
71
+ return json(data);
72
+ }
73
+ );
74
+
75
+ server.tool(
76
+ "reorder_content",
77
+ "Reorder content blocks within a space.",
78
+ {
79
+ space_id: z.string().describe("Space ID (sp_...)"),
80
+ block_ids: z.array(z.string()).describe("Ordered array of content block IDs (blk_...)"),
81
+ },
82
+ async ({ space_id, block_ids }) => {
83
+ const data = await client.patch(`/spaces/${space_id}/content/reorder`, { block_ids });
84
+ return json(data);
85
+ }
86
+ );
87
+
88
+ // --- Messages (discussion posts) ---
89
+
90
+ server.tool(
91
+ "list_messages",
92
+ "List discussion messages in a space. Messages are top-level discussion posts that members create.",
93
+ {
94
+ space_id: z.string().describe("Space ID (sp_...)"),
95
+ limit: z.number().optional().describe("Max results (default 25)"),
96
+ cursor: z.string().optional().describe("Pagination cursor"),
97
+ },
98
+ async ({ space_id, limit, cursor }) => {
99
+ const data = await client.get(`/spaces/${space_id}/messages`, { limit, cursor });
100
+ return json(data);
101
+ }
102
+ );
103
+
104
+ server.tool(
105
+ "get_message",
106
+ "Get a single message by its ID.",
107
+ {
108
+ message_id: z.string().describe("Message ID (blk_...)"),
109
+ },
110
+ async ({ message_id }) => {
111
+ const data = await client.get(`/messages/${message_id}`);
112
+ return json(data);
113
+ }
114
+ );
115
+
116
+ server.tool(
117
+ "create_message",
118
+ "Create a discussion message in a space.",
119
+ {
120
+ space_id: z.string().describe("Space ID (sp_...)"),
121
+ content: z.any().optional().describe("Rich text content (Tiptap JSON)"),
122
+ raw_content: z.string().optional().describe("Plain text content"),
123
+ message_body: z.string().optional().describe("Message body text"),
124
+ },
125
+ async ({ space_id, ...body }) => {
126
+ const data = await client.post(`/spaces/${space_id}/messages`, body);
127
+ return json(data);
128
+ }
129
+ );
130
+
131
+ server.tool(
132
+ "update_message",
133
+ "Update a discussion message.",
134
+ {
135
+ message_id: z.string().describe("Message ID (blk_...)"),
136
+ content: z.any().optional().describe("Rich text content (Tiptap JSON)"),
137
+ raw_content: z.string().optional().describe("Plain text content"),
138
+ },
139
+ async ({ message_id, ...body }) => {
140
+ const data = await client.patch(`/messages/${message_id}`, body);
141
+ return json(data);
142
+ }
143
+ );
144
+
145
+ server.tool(
146
+ "delete_message",
147
+ "Delete a discussion message.",
148
+ {
149
+ message_id: z.string().describe("Message ID (blk_...)"),
150
+ },
151
+ async ({ message_id }) => {
152
+ const data = await client.delete(`/messages/${message_id}`);
153
+ return json(data);
154
+ }
155
+ );
156
+
157
+ // --- Reflections (replies to messages) ---
158
+
159
+ server.tool(
160
+ "list_reflections",
161
+ "List reflections (threaded replies) on a message.",
162
+ {
163
+ message_id: z.string().describe("Message ID (blk_...)"),
164
+ limit: z.number().optional().describe("Max results (default 25)"),
165
+ cursor: z.string().optional().describe("Pagination cursor"),
166
+ },
167
+ async ({ message_id, limit, cursor }) => {
168
+ const data = await client.get(`/messages/${message_id}/reflections`, { limit, cursor });
169
+ return json(data);
170
+ }
171
+ );
172
+
173
+ server.tool(
174
+ "get_reflection",
175
+ "Get a single reflection by its ID.",
176
+ {
177
+ reflection_id: z.string().describe("Reflection ID (reply_...)"),
178
+ },
179
+ async ({ reflection_id }) => {
180
+ const data = await client.get(`/reflections/${reflection_id}`);
181
+ return json(data);
182
+ }
183
+ );
184
+
185
+ server.tool(
186
+ "create_reflection",
187
+ "Create a reflection (reply) on a message.",
188
+ {
189
+ message_id: z.string().describe("Message ID (blk_...)"),
190
+ content: z.any().optional().describe("Rich text content (Tiptap JSON)"),
191
+ raw_content: z.string().optional().describe("Plain text content"),
192
+ },
193
+ async ({ message_id, ...body }) => {
194
+ const data = await client.post(`/messages/${message_id}/reflections`, body);
195
+ return json(data);
196
+ }
197
+ );
198
+
199
+ server.tool(
200
+ "update_reflection",
201
+ "Update a reflection.",
202
+ {
203
+ reflection_id: z.string().describe("Reflection ID (reply_...)"),
204
+ content: z.any().optional().describe("Rich text content (Tiptap JSON)"),
205
+ raw_content: z.string().optional().describe("Plain text content"),
206
+ },
207
+ async ({ reflection_id, ...body }) => {
208
+ const data = await client.patch(`/reflections/${reflection_id}`, body);
209
+ return json(data);
210
+ }
211
+ );
212
+
213
+ server.tool(
214
+ "delete_reflection",
215
+ "Delete a reflection.",
216
+ {
217
+ reflection_id: z.string().describe("Reflection ID (reply_...)"),
218
+ },
219
+ async ({ reflection_id }) => {
220
+ const data = await client.delete(`/reflections/${reflection_id}`);
221
+ return json(data);
222
+ }
223
+ );
224
+ }
225
+
226
+ function json(data) {
227
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
228
+ }