@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.
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+
3
+ export function registerCouponTools(server, client) {
4
+ server.tool(
5
+ "list_coupons",
6
+ "List coupons for a space.",
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}/coupons`, { limit, cursor });
14
+ return json(data);
15
+ }
16
+ );
17
+
18
+ server.tool(
19
+ "get_coupon",
20
+ "Get a single coupon by ID.",
21
+ {
22
+ coupon_id: z.string().describe("Coupon ID (cpn_...)"),
23
+ },
24
+ async ({ coupon_id }) => {
25
+ const data = await client.get(`/coupons/${coupon_id}`);
26
+ return json(data);
27
+ }
28
+ );
29
+
30
+ server.tool(
31
+ "create_coupon",
32
+ "Create a coupon for a space.",
33
+ {
34
+ space_id: z.string().describe("Space ID (sp_...)"),
35
+ code: z.string().optional().describe("Coupon code"),
36
+ discount_type: z.string().optional().describe("Discount type"),
37
+ discount_amount: z.number().optional().describe("Discount amount"),
38
+ },
39
+ async ({ space_id, ...body }) => {
40
+ const data = await client.post(`/spaces/${space_id}/coupons`, body);
41
+ return json(data);
42
+ }
43
+ );
44
+
45
+ server.tool(
46
+ "update_coupon",
47
+ "Update a coupon.",
48
+ {
49
+ coupon_id: z.string().describe("Coupon ID (cpn_...)"),
50
+ code: z.string().optional(),
51
+ discount_type: z.string().optional(),
52
+ discount_amount: z.number().optional(),
53
+ },
54
+ async ({ coupon_id, ...body }) => {
55
+ const data = await client.patch(`/coupons/${coupon_id}`, body);
56
+ return json(data);
57
+ }
58
+ );
59
+
60
+ server.tool(
61
+ "delete_coupon",
62
+ "Delete a coupon.",
63
+ {
64
+ coupon_id: z.string().describe("Coupon ID (cpn_...)"),
65
+ },
66
+ async ({ coupon_id }) => {
67
+ const data = await client.delete(`/coupons/${coupon_id}`);
68
+ return json(data);
69
+ }
70
+ );
71
+ }
72
+
73
+ function json(data) {
74
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
75
+ }
@@ -0,0 +1,181 @@
1
+ import { z } from "zod";
2
+ import { extractText } from "../utils/extract-text.js";
3
+
4
+ const CONCURRENCY = 5;
5
+
6
+ export function registerDeepTool(server, client) {
7
+ server.tool(
8
+ "get_space_deep",
9
+ "Recursively fetch a space and all its nested content in one call. Returns content blocks, discussion messages, reflections (threaded replies), and child spaces as a single nested object. Rich text is converted to plain text for readability. Best for exploring an entire course, community, or discussion hierarchy.",
10
+ {
11
+ space_id: z.string().describe("Space ID (sp_...) to traverse"),
12
+ max_depth: z.number().optional().describe("How deep to recurse into child spaces (default 3)"),
13
+ max_items_per_list: z.number().optional().describe("Max content blocks, messages, or reflections per space (default 100)"),
14
+ max_children: z.number().optional().describe("Max child spaces to traverse per level (default 25)"),
15
+ include_reflections: z.boolean().optional().describe("Include threaded replies on messages (default true)"),
16
+ },
17
+ async ({ space_id, max_depth, max_items_per_list, max_children, include_reflections }) => {
18
+ const opts = {
19
+ maxDepth: max_depth ?? 3,
20
+ maxItems: max_items_per_list ?? 100,
21
+ maxChildren: max_children ?? 25,
22
+ includeReflections: include_reflections !== false,
23
+ };
24
+
25
+ const result = await traverse(client, space_id, 0, opts);
26
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
27
+ }
28
+ );
29
+ }
30
+
31
+ async function traverse(client, spaceId, depth, opts) {
32
+ const res = await client.get(`/spaces/${spaceId}`);
33
+ const space = res.data || res;
34
+
35
+ const node = {
36
+ id: space.id,
37
+ name: space.name,
38
+ type: space.type,
39
+ state: space.state,
40
+ privacy: space.privacy,
41
+ content_blocks: [],
42
+ messages: [],
43
+ children: [],
44
+ };
45
+
46
+ // Fetch content blocks
47
+ try {
48
+ const blocks = await client.getAll(`/spaces/${spaceId}/content`, {}, opts.maxItems);
49
+ node.content_blocks = blocks.map(formatBlock);
50
+ if (blocks.length >= opts.maxItems) {
51
+ node.content_blocks_truncated = true;
52
+ }
53
+ } catch (err) {
54
+ node.content_blocks_error = err.message;
55
+ }
56
+
57
+ // Fetch messages
58
+ try {
59
+ const messages = await client.getAll(`/spaces/${spaceId}/messages`, {}, opts.maxItems);
60
+ for (const msg of messages) {
61
+ const formatted = formatMessage(msg);
62
+
63
+ if (opts.includeReflections) {
64
+ try {
65
+ const reflections = await client.getAll(
66
+ `/messages/${msg.id}/reflections`, {}, opts.maxItems
67
+ );
68
+ if (reflections.length > 0) {
69
+ formatted.reflections = reflections.map(formatReflection);
70
+ if (reflections.length >= opts.maxItems) {
71
+ formatted.reflections_truncated = true;
72
+ }
73
+ }
74
+ } catch {
75
+ // Skip reflection errors silently
76
+ }
77
+ }
78
+
79
+ node.messages.push(formatted);
80
+ }
81
+ if (messages.length >= opts.maxItems) {
82
+ node.messages_truncated = true;
83
+ }
84
+ } catch (err) {
85
+ node.messages_error = err.message;
86
+ }
87
+
88
+ // Recurse into children
89
+ if (depth < opts.maxDepth) {
90
+ try {
91
+ const allChildren = await client.getAll(`/spaces/${spaceId}/children`, {}, opts.maxChildren + 1);
92
+ const toTraverse = allChildren.slice(0, opts.maxChildren);
93
+ const remaining = allChildren.length - toTraverse.length;
94
+
95
+ const results = await parallelMap(toTraverse, async (child) => {
96
+ try {
97
+ return await traverse(client, child.id, depth + 1, opts);
98
+ } catch (err) {
99
+ return { id: child.id, name: child.name, error: err.message };
100
+ }
101
+ }, CONCURRENCY);
102
+
103
+ node.children = results;
104
+
105
+ if (remaining > 0) {
106
+ node.children_truncated = true;
107
+ node.children_remaining = remaining;
108
+ }
109
+ } catch (err) {
110
+ node.children_error = err.message;
111
+ }
112
+ } else {
113
+ try {
114
+ const children = await client.getAll(`/spaces/${spaceId}/children`, {}, 1);
115
+ if (children.length > 0) {
116
+ node.children_note = `Has children — not fetched (max_depth ${opts.maxDepth} reached). Increase max_depth or call list_child_spaces.`;
117
+ }
118
+ } catch {
119
+ // Ignore
120
+ }
121
+ }
122
+
123
+ // Clean up empty arrays
124
+ if (node.content_blocks.length === 0) delete node.content_blocks;
125
+ if (node.messages.length === 0) delete node.messages;
126
+ if (node.children.length === 0 && !node.children_note) delete node.children;
127
+
128
+ return node;
129
+ }
130
+
131
+ function formatBlock(block) {
132
+ return {
133
+ id: block.id,
134
+ author: formatAuthor(block.author),
135
+ text: extractText(block.content) || block.raw_content || "",
136
+ priority: block.priority,
137
+ created_at: block.created_at,
138
+ };
139
+ }
140
+
141
+ function formatMessage(msg) {
142
+ return {
143
+ id: msg.id,
144
+ type: msg.type,
145
+ author: formatAuthor(msg.author),
146
+ text: extractText(msg.content) || msg.raw_content || "",
147
+ created_at: msg.created_at,
148
+ };
149
+ }
150
+
151
+ function formatReflection(ref) {
152
+ return {
153
+ id: ref.id,
154
+ author: formatAuthor(ref.author),
155
+ text: extractText(ref.content) || ref.raw_content || "",
156
+ created_at: ref.created_at,
157
+ };
158
+ }
159
+
160
+ function formatAuthor(user) {
161
+ if (!user) return null;
162
+ return {
163
+ id: user.id,
164
+ name: user.name || [user.first_name, user.last_name].filter(Boolean).join(" ") || user.username,
165
+ };
166
+ }
167
+
168
+ async function parallelMap(items, fn, concurrency) {
169
+ const results = [];
170
+ let index = 0;
171
+ async function worker() {
172
+ while (index < items.length) {
173
+ const i = index++;
174
+ results[i] = await fn(items[i]);
175
+ }
176
+ }
177
+ await Promise.all(
178
+ Array.from({ length: Math.min(concurrency, items.length) }, () => worker())
179
+ );
180
+ return results;
181
+ }
@@ -0,0 +1,117 @@
1
+ import { z } from "zod";
2
+
3
+ export function registerInvitationTools(server, client) {
4
+ server.tool(
5
+ "list_invitations",
6
+ "List invitations for a space.",
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}/invitations`, { limit, cursor });
14
+ return json(data);
15
+ }
16
+ );
17
+
18
+ server.tool(
19
+ "get_invitation",
20
+ "Get a single invitation by ID.",
21
+ {
22
+ space_id: z.string().describe("Space ID (sp_...)"),
23
+ invitation_id: z.string().describe("Invitation ID (inv_...)"),
24
+ },
25
+ async ({ space_id, invitation_id }) => {
26
+ const data = await client.get(`/spaces/${space_id}/invitations/${invitation_id}`);
27
+ return json(data);
28
+ }
29
+ );
30
+
31
+ server.tool(
32
+ "create_invitation",
33
+ "Create an invitation to a space.",
34
+ {
35
+ space_id: z.string().describe("Space ID (sp_...)"),
36
+ email: z.string().describe("Email address to invite"),
37
+ message: z.string().optional().describe("Custom invitation message"),
38
+ role: z.enum(["member", "editor", "moderator"]).optional().describe("Role for the invitee"),
39
+ notify: z.boolean().optional().describe("Send email notification (default: false)"),
40
+ free_access: z.boolean().optional().describe("Grant free access (default: false)"),
41
+ },
42
+ async ({ space_id, ...body }) => {
43
+ const data = await client.post(`/spaces/${space_id}/invitations`, body);
44
+ return json(data);
45
+ }
46
+ );
47
+
48
+ server.tool(
49
+ "update_invitation",
50
+ "Update an invitation.",
51
+ {
52
+ space_id: z.string().describe("Space ID (sp_...)"),
53
+ invitation_id: z.string().describe("Invitation ID (inv_...)"),
54
+ message: z.string().optional(),
55
+ role: z.enum(["member", "editor", "moderator"]).optional(),
56
+ free_access: z.boolean().optional(),
57
+ },
58
+ async ({ space_id, invitation_id, ...body }) => {
59
+ const data = await client.patch(`/spaces/${space_id}/invitations/${invitation_id}`, body);
60
+ return json(data);
61
+ }
62
+ );
63
+
64
+ server.tool(
65
+ "delete_invitation",
66
+ "Delete an invitation.",
67
+ {
68
+ space_id: z.string().describe("Space ID (sp_...)"),
69
+ invitation_id: z.string().describe("Invitation ID (inv_...)"),
70
+ },
71
+ async ({ space_id, invitation_id }) => {
72
+ const data = await client.delete(`/spaces/${space_id}/invitations/${invitation_id}`);
73
+ return json(data);
74
+ }
75
+ );
76
+
77
+ server.tool(
78
+ "resend_invitation",
79
+ "Resend an invitation email.",
80
+ {
81
+ space_id: z.string().describe("Space ID (sp_...)"),
82
+ invitation_id: z.string().describe("Invitation ID (inv_...)"),
83
+ },
84
+ async ({ space_id, invitation_id }) => {
85
+ const data = await client.post(`/spaces/${space_id}/invitations/${invitation_id}/resend`);
86
+ return json(data);
87
+ }
88
+ );
89
+
90
+ server.tool(
91
+ "bulk_create_invitations",
92
+ "Create up to 100 invitations at once. Requires an Idempotency-Key.",
93
+ {
94
+ space_id: z.string().describe("Space ID (sp_...)"),
95
+ idempotency_key: z.string().describe("Unique key for this batch operation"),
96
+ items: z.array(z.object({
97
+ email: z.string(),
98
+ message: z.string().optional(),
99
+ role: z.enum(["member", "editor", "moderator"]).optional(),
100
+ notify: z.boolean().optional(),
101
+ free_access: z.boolean().optional(),
102
+ })).describe("Array of invitation objects (max 100)"),
103
+ },
104
+ async ({ space_id, idempotency_key, items }) => {
105
+ const data = await client.post(
106
+ `/spaces/${space_id}/invitations/bulk`,
107
+ { items },
108
+ { "Idempotency-Key": idempotency_key }
109
+ );
110
+ return json(data);
111
+ }
112
+ );
113
+ }
114
+
115
+ function json(data) {
116
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
117
+ }
@@ -0,0 +1,144 @@
1
+ import { z } from "zod";
2
+
3
+ export function registerMemberTools(server, client) {
4
+ server.tool(
5
+ "list_members",
6
+ "List members of a space. Returns member IDs (mem_...), user info, roles, and states.",
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
+ user_id: z.string().optional().describe("Filter by user ID (usr_...)"),
12
+ email: z.string().optional().describe("Filter by exact email (requires members.email:read scope)"),
13
+ },
14
+ async ({ space_id, limit, cursor, user_id, email }) => {
15
+ const data = await client.get(`/spaces/${space_id}/members`, { limit, cursor, user_id, email });
16
+ return json(data);
17
+ }
18
+ );
19
+
20
+ server.tool(
21
+ "get_member",
22
+ "Get details for a specific member in a space.",
23
+ {
24
+ space_id: z.string().describe("Space ID (sp_...)"),
25
+ member_id: z.string().describe("Member ID (mem_...)"),
26
+ },
27
+ async ({ space_id, member_id }) => {
28
+ const data = await client.get(`/spaces/${space_id}/members/${member_id}`);
29
+ return json(data);
30
+ }
31
+ );
32
+
33
+ server.tool(
34
+ "add_member",
35
+ "Add a member to a space by email or user ID. Use email to auto-resolve or create a user account.",
36
+ {
37
+ space_id: z.string().describe("Space ID (sp_...)"),
38
+ email: z.string().optional().describe("Member's email address"),
39
+ user_id: z.string().optional().describe("User ID (usr_...) — alternative to email"),
40
+ first_name: z.string().optional().describe("First name (used when creating a new user via email)"),
41
+ last_name: z.string().optional().describe("Last name"),
42
+ username: z.string().optional().describe("Username"),
43
+ role: z.enum(["member", "editor", "moderator"]).optional().describe("Role (default: member)"),
44
+ notify: z.boolean().optional().describe("Send email notification (default: false)"),
45
+ },
46
+ async ({ space_id, ...body }) => {
47
+ const data = await client.post(`/spaces/${space_id}/members`, body);
48
+ return json(data);
49
+ }
50
+ );
51
+
52
+ server.tool(
53
+ "update_member",
54
+ "Update a member's role or notification settings.",
55
+ {
56
+ space_id: z.string().describe("Space ID (sp_...)"),
57
+ member_id: z.string().describe("Member ID (mem_...)"),
58
+ role: z.enum(["member", "editor", "moderator"]).optional().describe("New role"),
59
+ email_frequency: z.string().optional().describe("Email notification frequency"),
60
+ },
61
+ async ({ space_id, member_id, ...body }) => {
62
+ const data = await client.patch(`/spaces/${space_id}/members/${member_id}`, body);
63
+ return json(data);
64
+ }
65
+ );
66
+
67
+ server.tool(
68
+ "remove_member",
69
+ "Remove a member from a space. This also removes memberships in descendant spaces. Requires an Idempotency-Key.",
70
+ {
71
+ space_id: z.string().describe("Space ID (sp_...)"),
72
+ member_id: z.string().describe("Member ID (mem_...)"),
73
+ idempotency_key: z.string().describe("Unique key for this operation (e.g. remove-mem_abc-2026-04-30)"),
74
+ },
75
+ async ({ space_id, member_id, idempotency_key }) => {
76
+ const data = await client.delete(`/spaces/${space_id}/members/${member_id}`, {
77
+ "Idempotency-Key": idempotency_key,
78
+ });
79
+ return json(data);
80
+ }
81
+ );
82
+
83
+ server.tool(
84
+ "approve_member",
85
+ "Approve a pending member in a space.",
86
+ {
87
+ space_id: z.string().describe("Space ID (sp_...)"),
88
+ member_id: z.string().describe("Member ID (mem_...)"),
89
+ },
90
+ async ({ space_id, member_id }) => {
91
+ const data = await client.post(`/spaces/${space_id}/members/${member_id}/approve`);
92
+ return json(data);
93
+ }
94
+ );
95
+
96
+ server.tool(
97
+ "bulk_add_members",
98
+ "Add up to 100 members to a space in one call. Requires an Idempotency-Key. Returns per-item results.",
99
+ {
100
+ space_id: z.string().describe("Space ID (sp_...)"),
101
+ idempotency_key: z.string().describe("Unique key for this batch operation"),
102
+ items: z.array(z.object({
103
+ email: z.string().optional(),
104
+ user_id: z.string().optional(),
105
+ first_name: z.string().optional(),
106
+ last_name: z.string().optional(),
107
+ role: z.enum(["member", "editor", "moderator"]).optional(),
108
+ notify: z.boolean().optional(),
109
+ })).describe("Array of member objects (max 100). Each needs email or user_id."),
110
+ },
111
+ async ({ space_id, idempotency_key, items }) => {
112
+ const data = await client.post(
113
+ `/spaces/${space_id}/members/bulk`,
114
+ { items },
115
+ { "Idempotency-Key": idempotency_key }
116
+ );
117
+ return json(data);
118
+ }
119
+ );
120
+
121
+ server.tool(
122
+ "bulk_remove_members",
123
+ "Remove up to 100 members from a space. Requires an Idempotency-Key. Returns per-item results.",
124
+ {
125
+ space_id: z.string().describe("Space ID (sp_...)"),
126
+ idempotency_key: z.string().describe("Unique key for this batch operation"),
127
+ items: z.array(z.object({
128
+ member_id: z.string().describe("Member ID (mem_...)"),
129
+ })).describe("Array of member references (max 100)"),
130
+ },
131
+ async ({ space_id, idempotency_key, items }) => {
132
+ const data = await client.post(
133
+ `/spaces/${space_id}/members/bulk_delete`,
134
+ { items },
135
+ { "Idempotency-Key": idempotency_key }
136
+ );
137
+ return json(data);
138
+ }
139
+ );
140
+ }
141
+
142
+ function json(data) {
143
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
144
+ }
@@ -0,0 +1,131 @@
1
+ import { z } from "zod";
2
+
3
+ export function registerPlanTools(server, client) {
4
+ // --- Plans ---
5
+
6
+ server.tool(
7
+ "list_plans",
8
+ "List plans (offerings) for a space. Plans describe what can be purchased or enrolled in.",
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}/plans`, { limit, cursor });
16
+ return json(data);
17
+ }
18
+ );
19
+
20
+ server.tool(
21
+ "get_plan",
22
+ "Get a single plan by ID.",
23
+ {
24
+ plan_id: z.string().describe("Plan ID (plan_...)"),
25
+ },
26
+ async ({ plan_id }) => {
27
+ const data = await client.get(`/plans/${plan_id}`);
28
+ return json(data);
29
+ }
30
+ );
31
+
32
+ server.tool(
33
+ "create_plan",
34
+ "Create a plan for a space.",
35
+ {
36
+ space_id: z.string().describe("Space ID (sp_...)"),
37
+ name: z.string().optional().describe("Plan name"),
38
+ price: z.number().optional().describe("Price amount"),
39
+ },
40
+ async ({ space_id, ...body }) => {
41
+ const data = await client.post(`/spaces/${space_id}/plans`, body);
42
+ return json(data);
43
+ }
44
+ );
45
+
46
+ server.tool(
47
+ "update_plan",
48
+ "Update a plan.",
49
+ {
50
+ plan_id: z.string().describe("Plan ID (plan_...)"),
51
+ name: z.string().optional(),
52
+ price: z.number().optional(),
53
+ },
54
+ async ({ plan_id, ...body }) => {
55
+ const data = await client.patch(`/plans/${plan_id}`, body);
56
+ return json(data);
57
+ }
58
+ );
59
+
60
+ server.tool(
61
+ "delete_plan",
62
+ "Delete a plan.",
63
+ {
64
+ plan_id: z.string().describe("Plan ID (plan_...)"),
65
+ },
66
+ async ({ plan_id }) => {
67
+ const data = await client.delete(`/plans/${plan_id}`);
68
+ return json(data);
69
+ }
70
+ );
71
+
72
+ // --- Enrollments ---
73
+
74
+ server.tool(
75
+ "list_enrollments",
76
+ "List enrollments for a plan. Enrollments represent people enrolled through a specific plan.",
77
+ {
78
+ plan_id: z.string().describe("Plan ID (plan_...)"),
79
+ limit: z.number().optional().describe("Max results (default 25)"),
80
+ cursor: z.string().optional().describe("Pagination cursor"),
81
+ },
82
+ async ({ plan_id, limit, cursor }) => {
83
+ const data = await client.get(`/plans/${plan_id}/enrollments`, { limit, cursor });
84
+ return json(data);
85
+ }
86
+ );
87
+
88
+ server.tool(
89
+ "get_enrollment",
90
+ "Get a single enrollment by ID.",
91
+ {
92
+ enrollment_id: z.string().describe("Enrollment ID (enr_...)"),
93
+ },
94
+ async ({ enrollment_id }) => {
95
+ const data = await client.get(`/enrollments/${enrollment_id}`);
96
+ return json(data);
97
+ }
98
+ );
99
+
100
+ // --- Payments ---
101
+
102
+ server.tool(
103
+ "list_payments",
104
+ "List payments for a space.",
105
+ {
106
+ space_id: z.string().describe("Space ID (sp_...)"),
107
+ limit: z.number().optional().describe("Max results (default 25)"),
108
+ cursor: z.string().optional().describe("Pagination cursor"),
109
+ },
110
+ async ({ space_id, limit, cursor }) => {
111
+ const data = await client.get(`/spaces/${space_id}/payments`, { limit, cursor });
112
+ return json(data);
113
+ }
114
+ );
115
+
116
+ server.tool(
117
+ "get_payment",
118
+ "Get a single payment by ID.",
119
+ {
120
+ payment_id: z.string().describe("Payment ID (pay_...)"),
121
+ },
122
+ async ({ payment_id }) => {
123
+ const data = await client.get(`/payments/${payment_id}`);
124
+ return json(data);
125
+ }
126
+ );
127
+ }
128
+
129
+ function json(data) {
130
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
131
+ }