@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 +134 -0
- package/package.json +44 -0
- package/src/client.js +66 -0
- package/src/index.js +59 -0
- package/src/tools/broadcasts.js +102 -0
- package/src/tools/content.js +228 -0
- package/src/tools/coupons.js +75 -0
- package/src/tools/deep.js +181 -0
- package/src/tools/invitations.js +117 -0
- package/src/tools/members.js +144 -0
- package/src/tools/plans.js +131 -0
- package/src/tools/properties.js +181 -0
- package/src/tools/spaces.js +106 -0
- package/src/tools/surveys.js +87 -0
- package/src/utils/extract-text.js +85 -0
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
|
+
}
|