@sutraspaces/mcp-server 1.0.0 → 1.1.1
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 +42 -12
- package/package.json +2 -2
- package/src/index.js +8 -2
- package/src/resources/admin-api.js +136 -0
- package/src/tools/automations.js +89 -0
- package/src/tools/broadcasts.js +5 -3
- package/src/tools/content.js +4 -3
- package/src/tools/coupons.js +5 -3
- package/src/tools/help-center.js +425 -0
- package/src/tools/invitations.js +6 -3
- package/src/tools/members.js +74 -3
- package/src/tools/plans.js +11 -6
- package/src/tools/properties.js +69 -0
- package/src/tools/spaces.js +19 -2
- package/src/tools/surveys.js +5 -3
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Sutra MCP Server
|
|
2
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.
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that connects AI agents to the [Sutra Admin API](https://sutra.co). Manage spaces, members, contacts, content, discussions, surveys, plans, broadcasts, and more — all through your AI tools.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install
|
|
8
|
+
npm install -g @sutraspaces/mcp-server
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Or run directly with npx:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
SUTRA_API_TOKEN="sutra_live_sk_..." npx -y @sutraspaces/mcp-server
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
You can get an API token from your Sutra account settings or by contacting support@sutra.co.
|
|
@@ -26,8 +26,8 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
26
26
|
{
|
|
27
27
|
"mcpServers": {
|
|
28
28
|
"sutra": {
|
|
29
|
-
"command": "
|
|
30
|
-
"args": ["/
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "@sutraspaces/mcp-server"],
|
|
31
31
|
"env": {
|
|
32
32
|
"SUTRA_API_TOKEN": "sutra_live_sk_..."
|
|
33
33
|
}
|
|
@@ -39,12 +39,12 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
39
39
|
### Claude Code
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
claude mcp add sutra -- env SUTRA_API_TOKEN=sutra_live_sk_...
|
|
42
|
+
claude mcp add sutra -- env SUTRA_API_TOKEN=sutra_live_sk_... npx -y @sutraspaces/mcp-server
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
### Cursor / Windsurf / Other MCP Clients
|
|
46
46
|
|
|
47
|
-
Point your MCP client at
|
|
47
|
+
Point your MCP client at `npx -y @sutraspaces/mcp-server` with the `SUTRA_API_TOKEN` environment variable set. The server communicates over stdio.
|
|
48
48
|
|
|
49
49
|
## Configuration
|
|
50
50
|
|
|
@@ -60,6 +60,7 @@ Point your MCP client at the server entry point with the `SUTRA_API_TOKEN` envir
|
|
|
60
60
|
|
|
61
61
|
### Spaces
|
|
62
62
|
- **list_spaces** — List all spaces the API token can access
|
|
63
|
+
- **list_membership_spaces** — List basic read-only information about spaces where the API token owner is a member
|
|
63
64
|
- **get_space** — Get details for a single space
|
|
64
65
|
- **list_child_spaces** — List direct child spaces
|
|
65
66
|
- **create_space** — Create a new space (top-level or child)
|
|
@@ -68,7 +69,7 @@ Point your MCP client at the server entry point with the `SUTRA_API_TOKEN` envir
|
|
|
68
69
|
- **reorder_child_spaces** — Reorder children within a parent
|
|
69
70
|
|
|
70
71
|
### Members
|
|
71
|
-
- **list_members** — List space members with optional email
|
|
72
|
+
- **list_members** — List space members with optional email, user_id, search, role, state, and custom property filtering
|
|
72
73
|
- **get_member** — Get member details
|
|
73
74
|
- **add_member** — Add a member by email or user ID
|
|
74
75
|
- **update_member** — Update role or notification settings
|
|
@@ -77,6 +78,13 @@ Point your MCP client at the server entry point with the `SUTRA_API_TOKEN` envir
|
|
|
77
78
|
- **bulk_add_members** — Add up to 100 members at once
|
|
78
79
|
- **bulk_remove_members** — Remove up to 100 members at once
|
|
79
80
|
|
|
81
|
+
### Contacts
|
|
82
|
+
- **list_contacts** — List CRM contacts from the Contacts/Interested layer with optional email, search, and custom property filtering
|
|
83
|
+
- **get_contact** — Get contact details
|
|
84
|
+
- **get_contact_property_values** — Read property values for a contact
|
|
85
|
+
- **update_contact_property_values** — Set or clear property values for a contact
|
|
86
|
+
- **bulk_update_contact_property_values** — Set or clear property values for up to 100 contacts at once
|
|
87
|
+
|
|
80
88
|
### Content & Discussions
|
|
81
89
|
- **list_content** / **get_content_block** / **create_content** / **update_content** / **delete_content** — Manage content blocks
|
|
82
90
|
- **reorder_content** — Reorder content blocks within a space
|
|
@@ -85,7 +93,8 @@ Point your MCP client at the server entry point with the `SUTRA_API_TOKEN` envir
|
|
|
85
93
|
|
|
86
94
|
### Properties
|
|
87
95
|
- **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
|
|
96
|
+
- **get_member_property_values** / **update_member_property_values** / **bulk_update_member_property_values** — Read, set, and bulk set property values for members
|
|
97
|
+
- **get_contact_property_values** / **update_contact_property_values** / **bulk_update_contact_property_values** — Read, set, and bulk set property values for contacts
|
|
89
98
|
- **list_space_properties** / **create_space_property** / **update_space_property** / **delete_space_property** — Manage custom space property definitions
|
|
90
99
|
- **get_space_property_values** / **update_space_property_values** — Read and set property values per space
|
|
91
100
|
|
|
@@ -111,19 +120,40 @@ Point your MCP client at the server entry point with the `SUTRA_API_TOKEN` envir
|
|
|
111
120
|
- **send_broadcast** — Send a broadcast to space members
|
|
112
121
|
- **get_broadcast_delivery_status** — Check delivery progress
|
|
113
122
|
|
|
123
|
+
## Available Resources
|
|
124
|
+
|
|
125
|
+
- **sutra://admin-api/overview** — Core Admin API concepts, public ID prefixes, scopes, pagination, and filtering
|
|
126
|
+
- **sutra://admin-api/membership-spaces** — Difference between admin-manageable spaces and limited membership-space inventory
|
|
127
|
+
- **sutra://admin-api/contacts-properties** — Contact listing and member/contact property value workflows
|
|
128
|
+
- **sutra://admin-api/people-filtering** — Member and contact search, email, state, role, and custom property filtering
|
|
129
|
+
|
|
114
130
|
## API Concepts
|
|
115
131
|
|
|
116
132
|
**Spaces** are the core building block — they can be courses, communities, forums, or any structured container. Spaces form hierarchies through parent-child relationships.
|
|
117
133
|
|
|
134
|
+
**Membership spaces** are spaces where the API token owner is enrolled as a member. Use `list_membership_spaces` for basic read-only inventory. Use `list_spaces` for spaces where the token has admin access; membership-only spaces may still return 404 or 403 from admin subresource tools.
|
|
135
|
+
|
|
118
136
|
**Members** belong to spaces. They have roles (member, editor, moderator) and can have custom properties attached.
|
|
119
137
|
|
|
138
|
+
**Contacts** are CRM people in a space who are not enrolled members. Contacts use the same member property definitions as members, so AI agents can tag imported contacts before they join.
|
|
139
|
+
|
|
120
140
|
**Content blocks** are structured content created by facilitators. **Messages** are discussion posts from participants. **Reflections** are threaded replies to messages.
|
|
121
141
|
|
|
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).
|
|
142
|
+
**All IDs** use readable prefixes: `sp_` (space), `mem_` (member), `contact_` (contact), `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
143
|
|
|
124
144
|
**Pagination** is cursor-based. List endpoints return `{ data, pagination: { next_cursor, has_more } }`. Pass `cursor` to get the next page.
|
|
125
145
|
|
|
126
|
-
**
|
|
146
|
+
**Filtering & Search** — most list endpoints accept optional query parameters to narrow results:
|
|
147
|
+
- `q` — case-insensitive text search (spaces by name, members/contacts by name/email when scoped, surveys/broadcasts/plans by title/name, coupons by code)
|
|
148
|
+
- `state` / `status` — exact-match enum filters (e.g. `state=active` for spaces, `status=sent` for broadcasts)
|
|
149
|
+
- `role` — filter members or invitations by role
|
|
150
|
+
- `type` / `privacy` / `frequency` — filter by type, privacy level, or billing frequency
|
|
151
|
+
- `user_id` — filter messages or payments by author/user
|
|
152
|
+
- `email` — filter members, contacts, or invitations by email (requires `members.email:read` scope)
|
|
153
|
+
- `property_key` / `property_value` — filter members or contacts by one custom member property (requires `member_properties:read` scope)
|
|
154
|
+
- `property_filters` — filter members or contacts by multiple custom member properties
|
|
155
|
+
|
|
156
|
+
**Scopes** control what the API token can access. Tokens have read/write scope pairs like `spaces:read`, `members:write`, etc. `membership_spaces:read` is required for read-only membership-space inventory. Operations that exceed the token's scopes will return 403.
|
|
127
157
|
|
|
128
158
|
## Architecture
|
|
129
159
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sutraspaces/mcp-server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for the Sutra Admin API — manage spaces, members, content, and more via AI agents",
|
|
3
|
+
"version": "1.1.1",
|
|
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",
|
|
7
7
|
"bin": {
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,9 @@ import { registerPlanTools } from "./tools/plans.js";
|
|
|
14
14
|
import { registerCouponTools } from "./tools/coupons.js";
|
|
15
15
|
import { registerBroadcastTools } from "./tools/broadcasts.js";
|
|
16
16
|
import { registerDeepTool } from "./tools/deep.js";
|
|
17
|
+
import { registerAutomationTools } from "./tools/automations.js";
|
|
18
|
+
import { registerHelpCenterTools } from "./tools/help-center.js";
|
|
19
|
+
import { registerAdminApiResources } from "./resources/admin-api.js";
|
|
17
20
|
|
|
18
21
|
const SUTRA_API_TOKEN = process.env.SUTRA_API_TOKEN;
|
|
19
22
|
const SUTRA_BASE_URL = process.env.SUTRA_BASE_URL;
|
|
@@ -29,9 +32,9 @@ if (!SUTRA_API_TOKEN) {
|
|
|
29
32
|
|
|
30
33
|
const server = new McpServer({
|
|
31
34
|
name: "sutra",
|
|
32
|
-
version: "1.
|
|
35
|
+
version: "1.1.0",
|
|
33
36
|
description:
|
|
34
|
-
"Sutra Admin API — manage spaces, members, content, discussions, surveys, plans, broadcasts, and more.",
|
|
37
|
+
"Sutra Admin API — manage spaces, members, contacts, content, discussions, surveys, plans, broadcasts, and more.",
|
|
35
38
|
});
|
|
36
39
|
|
|
37
40
|
const client = new SutraAdminClient(SUTRA_API_TOKEN, SUTRA_BASE_URL);
|
|
@@ -46,6 +49,9 @@ registerPlanTools(server, client);
|
|
|
46
49
|
registerCouponTools(server, client);
|
|
47
50
|
registerBroadcastTools(server, client);
|
|
48
51
|
registerDeepTool(server, client);
|
|
52
|
+
registerAutomationTools(server, client);
|
|
53
|
+
registerHelpCenterTools(server);
|
|
54
|
+
registerAdminApiResources(server);
|
|
49
55
|
|
|
50
56
|
async function main() {
|
|
51
57
|
const transport = new StdioServerTransport();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const RESOURCES = [
|
|
2
|
+
{
|
|
3
|
+
name: "sutra-admin-api-overview",
|
|
4
|
+
title: "Sutra Admin API Overview",
|
|
5
|
+
uri: "sutra://admin-api/overview",
|
|
6
|
+
description: "Core Sutra Admin API concepts, IDs, scopes, pagination, and filtering.",
|
|
7
|
+
text: `# Sutra Admin API Overview
|
|
8
|
+
|
|
9
|
+
The Sutra Admin API lets authorized tokens manage spaces, members, contacts, content, invitations, properties, surveys, plans, coupons, and broadcasts.
|
|
10
|
+
|
|
11
|
+
Important IDs:
|
|
12
|
+
- Spaces use sp_ IDs.
|
|
13
|
+
- Members use mem_ IDs.
|
|
14
|
+
- Contacts use contact_ IDs.
|
|
15
|
+
- Users use usr_ IDs.
|
|
16
|
+
- Member property definitions use mprop_ IDs.
|
|
17
|
+
- Space property definitions use sprop_ IDs.
|
|
18
|
+
|
|
19
|
+
Pagination:
|
|
20
|
+
- List tools return data and pagination.
|
|
21
|
+
- Pass cursor to fetch the next page.
|
|
22
|
+
- Limit defaults to 25 unless the tool says otherwise.
|
|
23
|
+
|
|
24
|
+
Scopes:
|
|
25
|
+
- members:read reads members and contacts.
|
|
26
|
+
- members:write writes member/contact-related values where supported.
|
|
27
|
+
- spaces:read lists spaces where the token owner has admin access.
|
|
28
|
+
- membership_spaces:read lists basic read-only inventory for spaces where the token owner is a member.
|
|
29
|
+
- members.email:read is required to see or filter by email.
|
|
30
|
+
- member_properties:read is required to list definitions, read values, and filter people by properties.
|
|
31
|
+
- member_properties:write is required to create/update definitions or set values.
|
|
32
|
+
`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "sutra-contacts-properties",
|
|
36
|
+
title: "Contacts and Member Properties",
|
|
37
|
+
uri: "sutra://admin-api/contacts-properties",
|
|
38
|
+
description: "How to list contacts and set custom property values for members or contacts.",
|
|
39
|
+
text: `# Contacts and Member Properties
|
|
40
|
+
|
|
41
|
+
Contacts are CRM people in a space who are not enrolled members. They appear in Sutra's Contacts/Interested layer and use the same member property definitions as members.
|
|
42
|
+
|
|
43
|
+
Useful tools:
|
|
44
|
+
- list_contacts: list contacts in a space.
|
|
45
|
+
- get_contact: get one contact by contact_ ID.
|
|
46
|
+
- get_contact_property_values: read values for one contact.
|
|
47
|
+
- update_contact_property_values: set or clear values for one contact.
|
|
48
|
+
- bulk_update_contact_property_values: set or clear values for up to 100 contacts in one idempotent request.
|
|
49
|
+
- bulk_update_member_property_values: set or clear values for up to 100 members in one idempotent request.
|
|
50
|
+
|
|
51
|
+
Property value payloads are objects keyed by property key or mprop_ ID. Use null to clear a value.
|
|
52
|
+
|
|
53
|
+
Example contact bulk update item:
|
|
54
|
+
{
|
|
55
|
+
"contact_id": "contact_abc123",
|
|
56
|
+
"properties": {
|
|
57
|
+
"mailing_list_opt_in": "Not yet subscribed",
|
|
58
|
+
"in_person_workshop": "Sweden"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Use an idempotency_key for bulk updates so retries are safe.
|
|
63
|
+
`,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "sutra-people-filtering",
|
|
67
|
+
title: "Member and Contact Filtering",
|
|
68
|
+
uri: "sutra://admin-api/people-filtering",
|
|
69
|
+
description: "Filtering members and contacts by search, email, state, role, and custom property values.",
|
|
70
|
+
text: `# Member and Contact Filtering
|
|
71
|
+
|
|
72
|
+
list_members supports:
|
|
73
|
+
- email: exact email filter, requires members.email:read.
|
|
74
|
+
- q: case-insensitive name/email search.
|
|
75
|
+
- user_id: exact usr_ filter.
|
|
76
|
+
- role: member, editor, or moderator.
|
|
77
|
+
- state: active or pending.
|
|
78
|
+
- property_key and property_value: filter by one custom property.
|
|
79
|
+
- property_filters: filter by multiple custom properties.
|
|
80
|
+
|
|
81
|
+
list_contacts supports:
|
|
82
|
+
- email: exact email filter, requires members.email:read.
|
|
83
|
+
- q: case-insensitive name/email search.
|
|
84
|
+
- property_key and property_value: filter by one custom property.
|
|
85
|
+
- property_filters: filter by multiple custom properties.
|
|
86
|
+
|
|
87
|
+
Property filtering requires member_properties:read. property_key can be a property key or an mprop_ ID. Select and multi_select filters may use option names or stored option values.
|
|
88
|
+
|
|
89
|
+
Example multiple filters:
|
|
90
|
+
[
|
|
91
|
+
{ "property_key": "mailing_list_opt_in", "property_value": "Not yet subscribed" },
|
|
92
|
+
{ "property_key": "in_person_workshop", "property_value": "Sweden" }
|
|
93
|
+
]
|
|
94
|
+
`,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "sutra-membership-spaces",
|
|
98
|
+
title: "Membership Spaces",
|
|
99
|
+
uri: "sutra://admin-api/membership-spaces",
|
|
100
|
+
description: "Difference between admin-manageable spaces and limited membership-space inventory.",
|
|
101
|
+
text: `# Membership Spaces
|
|
102
|
+
|
|
103
|
+
Use list_spaces when you need spaces where the API token can perform admin operations.
|
|
104
|
+
|
|
105
|
+
Use list_membership_spaces when you only need a basic inventory of spaces where the token owner is a member. It returns IDs, names, type, state, privacy, timestamps, membership state, membership role, joined_at, and admin_access.
|
|
106
|
+
|
|
107
|
+
Membership-space results are read-only. A space with admin_access: false is included for inventory, but admin subresource tools such as list_members, list_content, list_child_spaces, or get_space can still return 404 or 403 for that space.
|
|
108
|
+
|
|
109
|
+
Required scope:
|
|
110
|
+
- membership_spaces:read
|
|
111
|
+
`,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export function registerAdminApiResources(server) {
|
|
116
|
+
for (const resource of RESOURCES) {
|
|
117
|
+
server.registerResource(
|
|
118
|
+
resource.name,
|
|
119
|
+
resource.uri,
|
|
120
|
+
{
|
|
121
|
+
title: resource.title,
|
|
122
|
+
description: resource.description,
|
|
123
|
+
mimeType: "text/markdown",
|
|
124
|
+
},
|
|
125
|
+
async () => ({
|
|
126
|
+
contents: [
|
|
127
|
+
{
|
|
128
|
+
uri: resource.uri,
|
|
129
|
+
mimeType: "text/markdown",
|
|
130
|
+
text: resource.text,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export function registerAutomationTools(server, client) {
|
|
4
|
+
server.tool(
|
|
5
|
+
"list_automations",
|
|
6
|
+
"List automations configured on a space. Returns automation IDs (auto_...), trigger type, step summary, and member counts.",
|
|
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}/automations`, { limit, cursor });
|
|
14
|
+
return json(data);
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
server.tool(
|
|
19
|
+
"get_automation",
|
|
20
|
+
"Get details for a specific automation, including its steps.",
|
|
21
|
+
{
|
|
22
|
+
automation_id: z.string().describe("Automation ID (auto_...)"),
|
|
23
|
+
},
|
|
24
|
+
async ({ automation_id }) => {
|
|
25
|
+
const data = await client.get(`/automations/${automation_id}`);
|
|
26
|
+
return json(data);
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
server.tool(
|
|
31
|
+
"list_automation_members",
|
|
32
|
+
"List members enrolled in an automation. Filter by status (ongoing or completed).",
|
|
33
|
+
{
|
|
34
|
+
automation_id: z.string().describe("Automation ID (auto_...)"),
|
|
35
|
+
status: z.enum(["ongoing", "completed"]).optional().describe("Filter by enrollment status"),
|
|
36
|
+
limit: z.number().optional().describe("Max results (default 25)"),
|
|
37
|
+
cursor: z.string().optional().describe("Pagination cursor"),
|
|
38
|
+
},
|
|
39
|
+
async ({ automation_id, status, limit, cursor }) => {
|
|
40
|
+
const data = await client.get(`/automations/${automation_id}/members`, { status, limit, cursor });
|
|
41
|
+
return json(data);
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
server.tool(
|
|
46
|
+
"get_automation_member",
|
|
47
|
+
"Get a specific member's enrollment status in an automation.",
|
|
48
|
+
{
|
|
49
|
+
automation_id: z.string().describe("Automation ID (auto_...)"),
|
|
50
|
+
enrollment_id: z.string().describe("Automation enrollment ID (aenr_...)"),
|
|
51
|
+
},
|
|
52
|
+
async ({ automation_id, enrollment_id }) => {
|
|
53
|
+
const data = await client.get(`/automations/${automation_id}/members/${enrollment_id}`);
|
|
54
|
+
return json(data);
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
server.tool(
|
|
59
|
+
"add_automation_member",
|
|
60
|
+
"Enroll a member in an automation. This triggers the automation flow for that member. Identify the member by member_id, user_id, or email.",
|
|
61
|
+
{
|
|
62
|
+
automation_id: z.string().describe("Automation ID (auto_...)"),
|
|
63
|
+
member_id: z.string().optional().describe("Member ID (mem_...) — member must belong to the automation's space"),
|
|
64
|
+
user_id: z.string().optional().describe("User ID (usr_...) — alternative to member_id"),
|
|
65
|
+
email: z.string().optional().describe("User email — alternative to member_id or user_id"),
|
|
66
|
+
},
|
|
67
|
+
async ({ automation_id, ...body }) => {
|
|
68
|
+
const data = await client.post(`/automations/${automation_id}/members`, body);
|
|
69
|
+
return json(data);
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
server.tool(
|
|
74
|
+
"remove_automation_member",
|
|
75
|
+
"Remove a member from an automation, stopping the automation flow for them.",
|
|
76
|
+
{
|
|
77
|
+
automation_id: z.string().describe("Automation ID (auto_...)"),
|
|
78
|
+
enrollment_id: z.string().describe("Automation enrollment ID (aenr_...)"),
|
|
79
|
+
},
|
|
80
|
+
async ({ automation_id, enrollment_id }) => {
|
|
81
|
+
const data = await client.delete(`/automations/${automation_id}/members/${enrollment_id}`);
|
|
82
|
+
return json(data);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function json(data) {
|
|
88
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
89
|
+
}
|
package/src/tools/broadcasts.js
CHANGED
|
@@ -3,14 +3,16 @@ import { z } from "zod";
|
|
|
3
3
|
export function registerBroadcastTools(server, client) {
|
|
4
4
|
server.tool(
|
|
5
5
|
"list_broadcasts",
|
|
6
|
-
"List broadcasts for a space. Broadcasts are mass communications sent to space members.",
|
|
6
|
+
"List broadcasts for a space. Broadcasts are mass communications sent to space members. Use `q` to search by title.",
|
|
7
7
|
{
|
|
8
8
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
9
9
|
limit: z.number().optional().describe("Max results (default 25)"),
|
|
10
10
|
cursor: z.string().optional().describe("Pagination cursor"),
|
|
11
|
+
q: z.string().optional().describe("Search broadcasts by title (case-insensitive)"),
|
|
12
|
+
status: z.enum(["draft", "scheduled", "sent"]).optional().describe("Filter by status"),
|
|
11
13
|
},
|
|
12
|
-
async ({ space_id, limit, cursor }) => {
|
|
13
|
-
const data = await client.get(`/spaces/${space_id}/broadcasts`, { limit, cursor });
|
|
14
|
+
async ({ space_id, limit, cursor, q, status }) => {
|
|
15
|
+
const data = await client.get(`/spaces/${space_id}/broadcasts`, { limit, cursor, q, status });
|
|
14
16
|
return json(data);
|
|
15
17
|
}
|
|
16
18
|
);
|
package/src/tools/content.js
CHANGED
|
@@ -89,14 +89,15 @@ export function registerContentTools(server, client) {
|
|
|
89
89
|
|
|
90
90
|
server.tool(
|
|
91
91
|
"list_messages",
|
|
92
|
-
"List discussion messages in a space. Messages are top-level discussion posts that members create.",
|
|
92
|
+
"List discussion messages in a space. Messages are top-level discussion posts that members create. Filter by author with user_id.",
|
|
93
93
|
{
|
|
94
94
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
95
95
|
limit: z.number().optional().describe("Max results (default 25)"),
|
|
96
96
|
cursor: z.string().optional().describe("Pagination cursor"),
|
|
97
|
+
user_id: z.string().optional().describe("Filter by author user ID (usr_...)"),
|
|
97
98
|
},
|
|
98
|
-
async ({ space_id, limit, cursor }) => {
|
|
99
|
-
const data = await client.get(`/spaces/${space_id}/messages`, { limit, cursor });
|
|
99
|
+
async ({ space_id, limit, cursor, user_id }) => {
|
|
100
|
+
const data = await client.get(`/spaces/${space_id}/messages`, { limit, cursor, user_id });
|
|
100
101
|
return json(data);
|
|
101
102
|
}
|
|
102
103
|
);
|
package/src/tools/coupons.js
CHANGED
|
@@ -3,14 +3,16 @@ import { z } from "zod";
|
|
|
3
3
|
export function registerCouponTools(server, client) {
|
|
4
4
|
server.tool(
|
|
5
5
|
"list_coupons",
|
|
6
|
-
"List coupons for a space.",
|
|
6
|
+
"List coupons for a space. Use `q` to search by coupon code.",
|
|
7
7
|
{
|
|
8
8
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
9
9
|
limit: z.number().optional().describe("Max results (default 25)"),
|
|
10
10
|
cursor: z.string().optional().describe("Pagination cursor"),
|
|
11
|
+
q: z.string().optional().describe("Search coupons by code (case-insensitive)"),
|
|
12
|
+
status: z.string().optional().describe("Filter by coupon status"),
|
|
11
13
|
},
|
|
12
|
-
async ({ space_id, limit, cursor }) => {
|
|
13
|
-
const data = await client.get(`/spaces/${space_id}/coupons`, { limit, cursor });
|
|
14
|
+
async ({ space_id, limit, cursor, q, status }) => {
|
|
15
|
+
const data = await client.get(`/spaces/${space_id}/coupons`, { limit, cursor, q, status });
|
|
14
16
|
return json(data);
|
|
15
17
|
}
|
|
16
18
|
);
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const HELP_BASE_URL = process.env.SUTRA_HELP_CENTER_BASE_URL || "https://api.sutra.co/api/v4/lotus/help";
|
|
4
|
+
|
|
5
|
+
async function getImageDimensions(url) {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
8
|
+
if (!res.ok) return null;
|
|
9
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
10
|
+
if (buf[0] === 0x89 && buf[1] === 0x50) {
|
|
11
|
+
// PNG: width at bytes 16-19, height at 20-23
|
|
12
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
13
|
+
}
|
|
14
|
+
if (buf[0] === 0xFF && buf[1] === 0xD8) {
|
|
15
|
+
// JPEG: scan for SOF markers
|
|
16
|
+
let offset = 2;
|
|
17
|
+
while (offset < buf.length - 8) {
|
|
18
|
+
if (buf[offset] !== 0xFF) break;
|
|
19
|
+
const marker = buf[offset + 1];
|
|
20
|
+
const len = buf.readUInt16BE(offset + 2);
|
|
21
|
+
if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8) {
|
|
22
|
+
return { width: buf.readUInt16BE(offset + 7), height: buf.readUInt16BE(offset + 5) };
|
|
23
|
+
}
|
|
24
|
+
offset += 2 + len;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (buf.slice(0, 4).toString() === "GIF8") {
|
|
28
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
29
|
+
}
|
|
30
|
+
if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP") {
|
|
31
|
+
if (buf.slice(12, 16).toString() === "VP8 ") {
|
|
32
|
+
return { width: buf.readUInt16LE(26) & 0x3FFF, height: buf.readUInt16LE(28) & 0x3FFF };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function imageWidthForDimensions(dims) {
|
|
42
|
+
if (!dims) return "100%";
|
|
43
|
+
return dims.width >= dims.height ? "100%" : "60%";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function normalizeGleapContent(doc) {
|
|
47
|
+
if (!doc || typeof doc !== "object") return doc;
|
|
48
|
+
const nodes = doc.content;
|
|
49
|
+
if (!Array.isArray(nodes)) return doc;
|
|
50
|
+
const normalized = nodes.flatMap(normalizeNode);
|
|
51
|
+
await resolveImageWidths(normalized);
|
|
52
|
+
return { ...doc, content: normalized };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function resolveImageWidths(nodes) {
|
|
56
|
+
const imageNodes = [];
|
|
57
|
+
function collect(list) {
|
|
58
|
+
for (const node of list) {
|
|
59
|
+
if (node.type === "image" && node.attrs?.src && node.attrs?._needsResize) {
|
|
60
|
+
imageNodes.push(node);
|
|
61
|
+
}
|
|
62
|
+
if (node.content && Array.isArray(node.content)) collect(node.content);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
collect(nodes);
|
|
66
|
+
await Promise.all(imageNodes.map(async (node) => {
|
|
67
|
+
const dims = await getImageDimensions(node.attrs.src);
|
|
68
|
+
const w = imageWidthForDimensions(dims);
|
|
69
|
+
node.attrs.resizeableWidth = w;
|
|
70
|
+
delete node.attrs._needsResize;
|
|
71
|
+
delete node.attrs.style;
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeNode(node) {
|
|
76
|
+
if (!node || typeof node !== "object") return [node];
|
|
77
|
+
|
|
78
|
+
if (node.type === "callout") {
|
|
79
|
+
const children = Array.isArray(node.content) ? node.content : [];
|
|
80
|
+
const textNodes = [];
|
|
81
|
+
const blockNodes = [];
|
|
82
|
+
for (const child of children) {
|
|
83
|
+
if (child.type === "text") textNodes.push(child);
|
|
84
|
+
else if (child.type === "paragraph") blockNodes.push(child);
|
|
85
|
+
else blockNodes.push(child);
|
|
86
|
+
}
|
|
87
|
+
const paragraphs = [];
|
|
88
|
+
if (textNodes.length) paragraphs.push({ type: "paragraph", attrs: { textAlign: "left" }, content: textNodes });
|
|
89
|
+
paragraphs.push(...blockNodes);
|
|
90
|
+
return [{
|
|
91
|
+
type: "infobox",
|
|
92
|
+
attrs: {
|
|
93
|
+
customSpacing: true,
|
|
94
|
+
icon: "info",
|
|
95
|
+
iconColor: "#364fc7",
|
|
96
|
+
badgeBgColor: "rgba(255,255,255,0.6)",
|
|
97
|
+
iconPosition: "left",
|
|
98
|
+
iconVerticalAlign: "top",
|
|
99
|
+
badgeShape: "circle",
|
|
100
|
+
badgeSize: 40,
|
|
101
|
+
iconSize: 20,
|
|
102
|
+
iconOffset: 18,
|
|
103
|
+
iconVerticalOffset: 18,
|
|
104
|
+
contentOffset: 70,
|
|
105
|
+
contentVerticalOffset: 20,
|
|
106
|
+
styles: {
|
|
107
|
+
"margin-top": "0px",
|
|
108
|
+
"margin-bottom": "0px",
|
|
109
|
+
"margin-left": "0px",
|
|
110
|
+
"margin-right": "0px",
|
|
111
|
+
"padding-top": "15px",
|
|
112
|
+
"padding-bottom": "10px",
|
|
113
|
+
"padding-left": "0px",
|
|
114
|
+
"padding-right": "0px",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
content: [{
|
|
118
|
+
type: "column",
|
|
119
|
+
attrs: {
|
|
120
|
+
size: 12,
|
|
121
|
+
position: 0,
|
|
122
|
+
radius: "with-radius",
|
|
123
|
+
styles: {
|
|
124
|
+
"align-items": "flex-start",
|
|
125
|
+
"background-color": "#dbe4ff",
|
|
126
|
+
"background-image": "none",
|
|
127
|
+
"background-position": "center center",
|
|
128
|
+
"background-size": "cover",
|
|
129
|
+
"background-repeat": "no-repeat",
|
|
130
|
+
"margin-top": "0px",
|
|
131
|
+
"margin-bottom": "0px",
|
|
132
|
+
"margin-left": "0px",
|
|
133
|
+
"margin-right": "0px",
|
|
134
|
+
"padding-top": "20px",
|
|
135
|
+
"padding-bottom": "20px",
|
|
136
|
+
"padding-left": "70px",
|
|
137
|
+
"padding-right": "20px",
|
|
138
|
+
},
|
|
139
|
+
flexLayout: "stack",
|
|
140
|
+
flexGap: 0,
|
|
141
|
+
nestedRadius: "no-radius",
|
|
142
|
+
},
|
|
143
|
+
content: paragraphs.length ? paragraphs : [{ type: "paragraph", attrs: { textAlign: "left" } }],
|
|
144
|
+
}],
|
|
145
|
+
}];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (node.type === "editor-button") {
|
|
149
|
+
const label = node.content?.[0]?.text || "Button";
|
|
150
|
+
const href = node.attrs?.href || "#";
|
|
151
|
+
return [{
|
|
152
|
+
type: "paragraph",
|
|
153
|
+
attrs: { textAlign: "left" },
|
|
154
|
+
content: [{
|
|
155
|
+
type: "text",
|
|
156
|
+
marks: [{ type: "link", attrs: { href, target: "_blank", rel: "noopener noreferrer nofollow" } }, { type: "bold" }],
|
|
157
|
+
text: `👉 ${label}`,
|
|
158
|
+
}],
|
|
159
|
+
}];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (node.type === "imageBlock") {
|
|
163
|
+
return [{
|
|
164
|
+
type: "image",
|
|
165
|
+
attrs: {
|
|
166
|
+
src: node.attrs?.src,
|
|
167
|
+
alt: node.attrs?.alt || null,
|
|
168
|
+
textAlign: "center",
|
|
169
|
+
_needsResize: true,
|
|
170
|
+
},
|
|
171
|
+
}];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (node.type === "image" && node.attrs?.src) {
|
|
175
|
+
return [{
|
|
176
|
+
...node,
|
|
177
|
+
attrs: { ...node.attrs, _needsResize: true },
|
|
178
|
+
}];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (node.type === "heading") {
|
|
182
|
+
const level = node.attrs?.level || 1;
|
|
183
|
+
const paddingTop = { 1: "32px", 2: "20px", 3: "16px", 4: "12px", 5: "10px", 6: "8px" };
|
|
184
|
+
const styles = {
|
|
185
|
+
"margin-top": "0px", "margin-bottom": "0px", "margin-left": "0px", "margin-right": "0px",
|
|
186
|
+
"padding-top": paddingTop[level] || "20px", "padding-bottom": "0px",
|
|
187
|
+
"padding-left": "0px", "padding-right": "0px",
|
|
188
|
+
"font-size": "", "line-height": "", "font-family": "",
|
|
189
|
+
...(node.attrs?.styles || {}),
|
|
190
|
+
};
|
|
191
|
+
if (!node.attrs?.styles?.["padding-top"]) styles["padding-top"] = paddingTop[level] || "20px";
|
|
192
|
+
return [{ ...node, attrs: { ...node.attrs, styles } }];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (node.content && Array.isArray(node.content)) {
|
|
196
|
+
return [{ ...node, content: node.content.flatMap(normalizeNode) }];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return [node];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function registerHelpCenterTools(server) {
|
|
203
|
+
const token = process.env.SUTRA_HELP_CENTER_TOKEN;
|
|
204
|
+
if (!token) return;
|
|
205
|
+
|
|
206
|
+
async function request(method, path, { params, body } = {}) {
|
|
207
|
+
const url = new URL(`${HELP_BASE_URL}${path}`);
|
|
208
|
+
if (params) {
|
|
209
|
+
for (const [k, v] of Object.entries(params)) {
|
|
210
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const headers = {
|
|
214
|
+
Authorization: `Bearer ${token}`,
|
|
215
|
+
Accept: "application/json",
|
|
216
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
217
|
+
};
|
|
218
|
+
const res = await fetch(url.toString(), {
|
|
219
|
+
method,
|
|
220
|
+
headers,
|
|
221
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
222
|
+
});
|
|
223
|
+
const text = await res.text();
|
|
224
|
+
if (!res.ok) throw new Error(`${method} ${path} → ${res.status}: ${text}`);
|
|
225
|
+
return text ? JSON.parse(text) : {};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Articles ---
|
|
229
|
+
|
|
230
|
+
server.tool(
|
|
231
|
+
"help_list_articles",
|
|
232
|
+
"List Help Center articles with optional filters. Returns article summaries and stats.",
|
|
233
|
+
{
|
|
234
|
+
status: z.enum(["draft", "published", "archived"]).optional().describe("Filter by status"),
|
|
235
|
+
review_status: z.string().optional().describe("Filter by review status"),
|
|
236
|
+
collection_id: z.number().optional().describe("Filter by collection ID"),
|
|
237
|
+
product_area: z.string().optional().describe("Filter by product area"),
|
|
238
|
+
intent_type: z.string().optional().describe("Filter by intent type"),
|
|
239
|
+
q: z.string().optional().describe("Search title and excerpt"),
|
|
240
|
+
},
|
|
241
|
+
async (params) => {
|
|
242
|
+
const data = await request("GET", "/articles.json", { params });
|
|
243
|
+
return json(data);
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
server.tool(
|
|
248
|
+
"help_get_article",
|
|
249
|
+
"Get a Help Center article with full content, metadata, and version history.",
|
|
250
|
+
{
|
|
251
|
+
id: z.number().describe("Article ID"),
|
|
252
|
+
},
|
|
253
|
+
async ({ id }) => {
|
|
254
|
+
const data = await request("GET", `/articles/${id}.json`);
|
|
255
|
+
return json(data);
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
server.tool(
|
|
260
|
+
"help_create_article",
|
|
261
|
+
"Create a new Help Center article. Returns the article and its first version.",
|
|
262
|
+
{
|
|
263
|
+
title: z.string().describe("Article title"),
|
|
264
|
+
slug: z.string().optional().describe("URL slug (auto-generated from title if omitted)"),
|
|
265
|
+
excerpt: z.string().optional().describe("Short description/excerpt"),
|
|
266
|
+
help_collection_id: z.number().optional().describe("Collection ID to place article in"),
|
|
267
|
+
product_area: z.string().optional().describe("Product area tag"),
|
|
268
|
+
intent_type: z.string().optional().describe("Intent type tag"),
|
|
269
|
+
subsection: z.string().optional().describe("Subsection within collection"),
|
|
270
|
+
position: z.number().optional().describe("Sort position within collection"),
|
|
271
|
+
status: z.enum(["draft", "published", "archived"]).optional().describe("Initial status (default: draft)"),
|
|
272
|
+
content: z.any().optional().describe("Tiptap JSON content"),
|
|
273
|
+
commit_message: z.string().optional().describe("Version commit message"),
|
|
274
|
+
meta_title: z.string().optional().describe("SEO meta title"),
|
|
275
|
+
meta_description: z.string().optional().describe("SEO meta description"),
|
|
276
|
+
og_image_url: z.string().optional().describe("Open Graph image URL"),
|
|
277
|
+
audience: z.array(z.string()).optional().describe("Target audience tags"),
|
|
278
|
+
applies_to_plan: z.array(z.string()).optional().describe("Applicable plan types"),
|
|
279
|
+
alternate_phrasings: z.array(z.string()).optional().describe("Alternate search phrasings"),
|
|
280
|
+
publish: z.boolean().optional().describe("Publish immediately after creation"),
|
|
281
|
+
},
|
|
282
|
+
async (params) => {
|
|
283
|
+
const { publish, ...articleFields } = params;
|
|
284
|
+
if (articleFields.content) articleFields.content = await normalizeGleapContent(articleFields.content);
|
|
285
|
+
const data = await request("POST", "/articles.json", {
|
|
286
|
+
body: { article: articleFields, publish },
|
|
287
|
+
});
|
|
288
|
+
return json(data);
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
server.tool(
|
|
293
|
+
"help_update_article",
|
|
294
|
+
"Update an existing Help Center article and create a new version.",
|
|
295
|
+
{
|
|
296
|
+
id: z.number().describe("Article ID"),
|
|
297
|
+
title: z.string().optional().describe("Article title"),
|
|
298
|
+
slug: z.string().optional().describe("URL slug"),
|
|
299
|
+
excerpt: z.string().optional().describe("Short description/excerpt"),
|
|
300
|
+
help_collection_id: z.number().optional().describe("Collection ID"),
|
|
301
|
+
product_area: z.string().optional().describe("Product area tag"),
|
|
302
|
+
intent_type: z.string().optional().describe("Intent type tag"),
|
|
303
|
+
subsection: z.string().optional().describe("Subsection within collection"),
|
|
304
|
+
position: z.number().optional().describe("Sort position"),
|
|
305
|
+
status: z.string().optional().describe("Status (draft/published/archived)"),
|
|
306
|
+
content: z.any().optional().describe("Tiptap JSON content"),
|
|
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
|
+
audience: z.array(z.string()).optional().describe("Target audience tags"),
|
|
312
|
+
applies_to_plan: z.array(z.string()).optional().describe("Applicable plan types"),
|
|
313
|
+
alternate_phrasings: z.array(z.string()).optional().describe("Alternate search phrasings"),
|
|
314
|
+
publish: z.boolean().optional().describe("Publish this version immediately"),
|
|
315
|
+
},
|
|
316
|
+
async ({ id, publish, ...articleFields }) => {
|
|
317
|
+
if (articleFields.content) articleFields.content = await normalizeGleapContent(articleFields.content);
|
|
318
|
+
const data = await request("PUT", `/articles/${id}.json`, {
|
|
319
|
+
body: { article: articleFields, publish },
|
|
320
|
+
});
|
|
321
|
+
return json(data);
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
server.tool(
|
|
326
|
+
"help_publish_article",
|
|
327
|
+
"Publish a Help Center article version. Publishes the latest version if no version_id is given.",
|
|
328
|
+
{
|
|
329
|
+
id: z.number().describe("Article ID"),
|
|
330
|
+
version_id: z.number().optional().describe("Specific version ID to publish (defaults to latest)"),
|
|
331
|
+
},
|
|
332
|
+
async ({ id, version_id }) => {
|
|
333
|
+
const data = await request("POST", `/articles/${id}/publish.json`, {
|
|
334
|
+
body: version_id ? { version_id } : {},
|
|
335
|
+
});
|
|
336
|
+
return json(data);
|
|
337
|
+
}
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
server.tool(
|
|
341
|
+
"help_review_article",
|
|
342
|
+
"Approve or reject a pending Help Center article version.",
|
|
343
|
+
{
|
|
344
|
+
id: z.number().describe("Article ID"),
|
|
345
|
+
version_id: z.number().describe("Version ID to review"),
|
|
346
|
+
decision: z.enum(["approve", "reject"]).describe("Review decision"),
|
|
347
|
+
comment: z.string().optional().describe("Review comment"),
|
|
348
|
+
},
|
|
349
|
+
async ({ id, ...body }) => {
|
|
350
|
+
const data = await request("POST", `/articles/${id}/review.json`, { body });
|
|
351
|
+
return json(data);
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// --- Collections ---
|
|
356
|
+
|
|
357
|
+
server.tool(
|
|
358
|
+
"help_list_collections",
|
|
359
|
+
"List all Help Center collections (categories).",
|
|
360
|
+
{},
|
|
361
|
+
async () => {
|
|
362
|
+
const data = await request("GET", "/collections.json");
|
|
363
|
+
return json(data);
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
server.tool(
|
|
368
|
+
"help_create_collection",
|
|
369
|
+
"Create a new Help Center collection.",
|
|
370
|
+
{
|
|
371
|
+
name: z.string().describe("Collection name"),
|
|
372
|
+
slug: z.string().optional().describe("URL slug"),
|
|
373
|
+
description: z.string().optional().describe("Collection description"),
|
|
374
|
+
icon_library: z.string().optional().describe("Icon library name"),
|
|
375
|
+
icon_name: z.string().optional().describe("Icon name"),
|
|
376
|
+
icon_color: z.string().optional().describe("Icon color hex"),
|
|
377
|
+
position: z.number().optional().describe("Sort position"),
|
|
378
|
+
published: z.boolean().optional().describe("Whether collection is published"),
|
|
379
|
+
},
|
|
380
|
+
async (params) => {
|
|
381
|
+
const data = await request("POST", "/collections.json", {
|
|
382
|
+
body: { collection: params },
|
|
383
|
+
});
|
|
384
|
+
return json(data);
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
server.tool(
|
|
389
|
+
"help_update_collection",
|
|
390
|
+
"Update a Help Center collection.",
|
|
391
|
+
{
|
|
392
|
+
id: z.number().describe("Collection ID"),
|
|
393
|
+
name: z.string().optional().describe("Collection name"),
|
|
394
|
+
slug: z.string().optional().describe("URL slug"),
|
|
395
|
+
description: z.string().optional().describe("Collection description"),
|
|
396
|
+
icon_library: z.string().optional().describe("Icon library name"),
|
|
397
|
+
icon_name: z.string().optional().describe("Icon name"),
|
|
398
|
+
icon_color: z.string().optional().describe("Icon color hex"),
|
|
399
|
+
position: z.number().optional().describe("Sort position"),
|
|
400
|
+
published: z.boolean().optional().describe("Whether collection is published"),
|
|
401
|
+
},
|
|
402
|
+
async ({ id, ...fields }) => {
|
|
403
|
+
const data = await request("PUT", `/collections/${id}.json`, {
|
|
404
|
+
body: { collection: fields },
|
|
405
|
+
});
|
|
406
|
+
return json(data);
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
server.tool(
|
|
411
|
+
"help_delete_collection",
|
|
412
|
+
"Delete a Help Center collection. Archives it if it has articles, permanently deletes if empty.",
|
|
413
|
+
{
|
|
414
|
+
id: z.number().describe("Collection ID"),
|
|
415
|
+
},
|
|
416
|
+
async ({ id }) => {
|
|
417
|
+
const data = await request("DELETE", `/collections/${id}.json`);
|
|
418
|
+
return json(data);
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function json(data) {
|
|
424
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
425
|
+
}
|
package/src/tools/invitations.js
CHANGED
|
@@ -3,14 +3,17 @@ import { z } from "zod";
|
|
|
3
3
|
export function registerInvitationTools(server, client) {
|
|
4
4
|
server.tool(
|
|
5
5
|
"list_invitations",
|
|
6
|
-
"List invitations for a space.",
|
|
6
|
+
"List invitations for a space. Filter by email, state, or role.",
|
|
7
7
|
{
|
|
8
8
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
9
9
|
limit: z.number().optional().describe("Max results (default 25)"),
|
|
10
10
|
cursor: z.string().optional().describe("Pagination cursor"),
|
|
11
|
+
email: z.string().optional().describe("Search by email (case-insensitive, requires members.email:read scope)"),
|
|
12
|
+
state: z.enum(["pending", "accepted", "declined"]).optional().describe("Filter by invitation state"),
|
|
13
|
+
role: z.enum(["member", "editor", "moderator"]).optional().describe("Filter by role"),
|
|
11
14
|
},
|
|
12
|
-
async ({ space_id, limit, cursor }) => {
|
|
13
|
-
const data = await client.get(`/spaces/${space_id}/invitations`, { limit, cursor });
|
|
15
|
+
async ({ space_id, limit, cursor, email, state, role }) => {
|
|
16
|
+
const data = await client.get(`/spaces/${space_id}/invitations`, { limit, cursor, email, state, role });
|
|
14
17
|
return json(data);
|
|
15
18
|
}
|
|
16
19
|
);
|
package/src/tools/members.js
CHANGED
|
@@ -1,18 +1,85 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
export function registerMemberTools(server, client) {
|
|
4
|
+
const propertyValueSchema = z.union([z.string(), z.number(), z.boolean()]).describe("Exact property value");
|
|
5
|
+
const propertyFilterSchema = z.object({
|
|
6
|
+
property_key: z.string().optional().describe("Property key or mprop_ ID"),
|
|
7
|
+
key: z.string().optional().describe("Property key"),
|
|
8
|
+
property_id: z.string().optional().describe("Property ID (mprop_...)"),
|
|
9
|
+
member_property_id: z.string().optional().describe("Property ID (mprop_...)"),
|
|
10
|
+
property_value: propertyValueSchema.optional(),
|
|
11
|
+
value: propertyValueSchema.optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
4
14
|
server.tool(
|
|
5
15
|
"list_members",
|
|
6
|
-
"List members of a space. Returns member IDs (mem_...), user info, roles, and states.",
|
|
16
|
+
"List members of a space. Returns member IDs (mem_...), user info, roles, and states. Use `q` to search by name or email. Use property_key/property_value or property_filters for custom property filtering.",
|
|
7
17
|
{
|
|
8
18
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
9
19
|
limit: z.number().optional().describe("Max results (default 25)"),
|
|
10
20
|
cursor: z.string().optional().describe("Pagination cursor"),
|
|
11
21
|
user_id: z.string().optional().describe("Filter by user ID (usr_...)"),
|
|
12
22
|
email: z.string().optional().describe("Filter by exact email (requires members.email:read scope)"),
|
|
23
|
+
q: z.string().optional().describe("Search by member name or email (case-insensitive)"),
|
|
24
|
+
role: z.enum(["member", "editor", "moderator"]).optional().describe("Filter by role"),
|
|
25
|
+
state: z.enum(["active", "pending"]).optional().describe("Filter by state"),
|
|
26
|
+
property_key: z.string().optional().describe("Filter by member property key or mprop_ ID (requires member_properties:read)"),
|
|
27
|
+
property_value: propertyValueSchema.optional().describe("Exact property value for property_key. Select values may use option names or option values."),
|
|
28
|
+
property_filters: z.array(propertyFilterSchema).optional().describe("Multiple property filters. Each filter needs a property key/ID and value."),
|
|
29
|
+
},
|
|
30
|
+
async ({ space_id, limit, cursor, user_id, email, q, role, state, property_key, property_value, property_filters }) => {
|
|
31
|
+
const data = await client.get(`/spaces/${space_id}/members`, {
|
|
32
|
+
limit,
|
|
33
|
+
cursor,
|
|
34
|
+
user_id,
|
|
35
|
+
email,
|
|
36
|
+
q,
|
|
37
|
+
role,
|
|
38
|
+
state,
|
|
39
|
+
property_key,
|
|
40
|
+
property_value,
|
|
41
|
+
property_filters: encodePropertyFilters(property_filters),
|
|
42
|
+
});
|
|
43
|
+
return json(data);
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
server.tool(
|
|
48
|
+
"list_contacts",
|
|
49
|
+
"List CRM contacts in a space. Contacts are non-member people in the Contacts/Interested layer. Use property_key/property_value to filter by custom member properties.",
|
|
50
|
+
{
|
|
51
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
52
|
+
limit: z.number().optional().describe("Max results (default 25)"),
|
|
53
|
+
cursor: z.string().optional().describe("Pagination cursor"),
|
|
54
|
+
email: z.string().optional().describe("Filter by exact email (requires members.email:read scope)"),
|
|
55
|
+
q: z.string().optional().describe("Search by contact name, and by email when scoped"),
|
|
56
|
+
property_key: z.string().optional().describe("Filter by member property key or mprop_ ID (requires member_properties:read)"),
|
|
57
|
+
property_value: propertyValueSchema.optional().describe("Exact property value for property_key. Select values may use option names or option values."),
|
|
58
|
+
property_filters: z.array(propertyFilterSchema).optional().describe("Multiple property filters. Each filter needs a property key/ID and value."),
|
|
59
|
+
},
|
|
60
|
+
async ({ space_id, limit, cursor, email, q, property_key, property_value, property_filters }) => {
|
|
61
|
+
const data = await client.get(`/spaces/${space_id}/contacts`, {
|
|
62
|
+
limit,
|
|
63
|
+
cursor,
|
|
64
|
+
email,
|
|
65
|
+
q,
|
|
66
|
+
property_key,
|
|
67
|
+
property_value,
|
|
68
|
+
property_filters: encodePropertyFilters(property_filters),
|
|
69
|
+
});
|
|
70
|
+
return json(data);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
server.tool(
|
|
75
|
+
"get_contact",
|
|
76
|
+
"Get details for a specific CRM contact in a space.",
|
|
77
|
+
{
|
|
78
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
79
|
+
contact_id: z.string().describe("Contact ID (contact_...)"),
|
|
13
80
|
},
|
|
14
|
-
async ({ space_id,
|
|
15
|
-
const data = await client.get(`/spaces/${space_id}/
|
|
81
|
+
async ({ space_id, contact_id }) => {
|
|
82
|
+
const data = await client.get(`/spaces/${space_id}/contacts/${contact_id}`);
|
|
16
83
|
return json(data);
|
|
17
84
|
}
|
|
18
85
|
);
|
|
@@ -139,6 +206,10 @@ export function registerMemberTools(server, client) {
|
|
|
139
206
|
);
|
|
140
207
|
}
|
|
141
208
|
|
|
209
|
+
function encodePropertyFilters(filters) {
|
|
210
|
+
return filters && filters.length > 0 ? JSON.stringify(filters) : undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
142
213
|
function json(data) {
|
|
143
214
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
144
215
|
}
|
package/src/tools/plans.js
CHANGED
|
@@ -5,14 +5,17 @@ export function registerPlanTools(server, client) {
|
|
|
5
5
|
|
|
6
6
|
server.tool(
|
|
7
7
|
"list_plans",
|
|
8
|
-
"List plans (offerings) for a space. Plans describe what can be purchased or enrolled in.",
|
|
8
|
+
"List plans (offerings) for a space. Plans describe what can be purchased or enrolled in. Use `q` to search by name.",
|
|
9
9
|
{
|
|
10
10
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
11
11
|
limit: z.number().optional().describe("Max results (default 25)"),
|
|
12
12
|
cursor: z.string().optional().describe("Pagination cursor"),
|
|
13
|
+
q: z.string().optional().describe("Search plans by name (case-insensitive)"),
|
|
14
|
+
status: z.string().optional().describe("Filter by status"),
|
|
15
|
+
frequency: z.string().optional().describe("Filter by billing frequency"),
|
|
13
16
|
},
|
|
14
|
-
async ({ space_id, limit, cursor }) => {
|
|
15
|
-
const data = await client.get(`/spaces/${space_id}/plans`, { limit, cursor });
|
|
17
|
+
async ({ space_id, limit, cursor, q, status, frequency }) => {
|
|
18
|
+
const data = await client.get(`/spaces/${space_id}/plans`, { limit, cursor, q, status, frequency });
|
|
16
19
|
return json(data);
|
|
17
20
|
}
|
|
18
21
|
);
|
|
@@ -101,14 +104,16 @@ export function registerPlanTools(server, client) {
|
|
|
101
104
|
|
|
102
105
|
server.tool(
|
|
103
106
|
"list_payments",
|
|
104
|
-
"List payments for a space.",
|
|
107
|
+
"List payments for a space. Filter by user or status.",
|
|
105
108
|
{
|
|
106
109
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
107
110
|
limit: z.number().optional().describe("Max results (default 25)"),
|
|
108
111
|
cursor: z.string().optional().describe("Pagination cursor"),
|
|
112
|
+
user_id: z.string().optional().describe("Filter by user ID (usr_...)"),
|
|
113
|
+
status: z.string().optional().describe("Filter by payment status"),
|
|
109
114
|
},
|
|
110
|
-
async ({ space_id, limit, cursor }) => {
|
|
111
|
-
const data = await client.get(`/spaces/${space_id}/payments`, { limit, cursor });
|
|
115
|
+
async ({ space_id, limit, cursor, user_id, status }) => {
|
|
116
|
+
const data = await client.get(`/spaces/${space_id}/payments`, { limit, cursor, user_id, status });
|
|
112
117
|
return json(data);
|
|
113
118
|
}
|
|
114
119
|
);
|
package/src/tools/properties.js
CHANGED
|
@@ -89,6 +89,75 @@ export function registerPropertyTools(server, client) {
|
|
|
89
89
|
}
|
|
90
90
|
);
|
|
91
91
|
|
|
92
|
+
server.tool(
|
|
93
|
+
"bulk_update_member_property_values",
|
|
94
|
+
"Set property values for up to 100 members. Requires an Idempotency-Key. Use null to clear a value.",
|
|
95
|
+
{
|
|
96
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
97
|
+
idempotency_key: z.string().describe("Unique key for this batch operation"),
|
|
98
|
+
items: z.array(z.object({
|
|
99
|
+
member_id: z.string().describe("Member ID (mem_...)"),
|
|
100
|
+
properties: z.record(z.any()).describe("Object keyed by property key or mprop_ ID, values to set (null to clear)"),
|
|
101
|
+
})).describe("Array of member property updates (max 100)"),
|
|
102
|
+
},
|
|
103
|
+
async ({ space_id, idempotency_key, items }) => {
|
|
104
|
+
const data = await client.patch(
|
|
105
|
+
`/spaces/${space_id}/members/properties/bulk`,
|
|
106
|
+
{ items },
|
|
107
|
+
{ "Idempotency-Key": idempotency_key }
|
|
108
|
+
);
|
|
109
|
+
return json(data);
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
server.tool(
|
|
114
|
+
"get_contact_property_values",
|
|
115
|
+
"Get all property values for a specific contact in a space. Contacts use the same member property definitions as members.",
|
|
116
|
+
{
|
|
117
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
118
|
+
contact_id: z.string().describe("Contact ID (contact_...)"),
|
|
119
|
+
},
|
|
120
|
+
async ({ space_id, contact_id }) => {
|
|
121
|
+
const data = await client.get(`/spaces/${space_id}/contacts/${contact_id}/properties`);
|
|
122
|
+
return json(data);
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
"update_contact_property_values",
|
|
128
|
+
"Set property values for a contact. Use null to clear a value. Keys can be property keys or mprop_ IDs.",
|
|
129
|
+
{
|
|
130
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
131
|
+
contact_id: z.string().describe("Contact ID (contact_...)"),
|
|
132
|
+
properties: z.record(z.any()).describe("Object keyed by property key or mprop_ ID, values to set (null to clear)"),
|
|
133
|
+
},
|
|
134
|
+
async ({ space_id, contact_id, properties }) => {
|
|
135
|
+
const data = await client.patch(`/spaces/${space_id}/contacts/${contact_id}/properties`, { properties });
|
|
136
|
+
return json(data);
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
server.tool(
|
|
141
|
+
"bulk_update_contact_property_values",
|
|
142
|
+
"Set property values for up to 100 contacts. Requires an Idempotency-Key. Use null to clear a value.",
|
|
143
|
+
{
|
|
144
|
+
space_id: z.string().describe("Space ID (sp_...)"),
|
|
145
|
+
idempotency_key: z.string().describe("Unique key for this batch operation"),
|
|
146
|
+
items: z.array(z.object({
|
|
147
|
+
contact_id: z.string().describe("Contact ID (contact_...)"),
|
|
148
|
+
properties: z.record(z.any()).describe("Object keyed by property key or mprop_ ID, values to set (null to clear)"),
|
|
149
|
+
})).describe("Array of contact property updates (max 100)"),
|
|
150
|
+
},
|
|
151
|
+
async ({ space_id, idempotency_key, items }) => {
|
|
152
|
+
const data = await client.patch(
|
|
153
|
+
`/spaces/${space_id}/contacts/properties/bulk`,
|
|
154
|
+
{ items },
|
|
155
|
+
{ "Idempotency-Key": idempotency_key }
|
|
156
|
+
);
|
|
157
|
+
return json(data);
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
92
161
|
// --- Space Properties ---
|
|
93
162
|
|
|
94
163
|
server.tool(
|
package/src/tools/spaces.js
CHANGED
|
@@ -3,13 +3,30 @@ import { z } from "zod";
|
|
|
3
3
|
export function registerSpaceTools(server, client) {
|
|
4
4
|
server.tool(
|
|
5
5
|
"list_spaces",
|
|
6
|
-
"List all spaces the API token can access. Returns space IDs (sp_...), names, types, and states.
|
|
6
|
+
"List all spaces the API token can access. Returns space IDs (sp_...), names, types, and states. Use `q` to search by name.",
|
|
7
|
+
{
|
|
8
|
+
limit: z.number().optional().describe("Max results per page (default 25, max 100)"),
|
|
9
|
+
cursor: z.string().optional().describe("Pagination cursor from a previous response"),
|
|
10
|
+
q: z.string().optional().describe("Search spaces by name (case-insensitive)"),
|
|
11
|
+
state: z.enum(["active", "archived"]).optional().describe("Filter by state"),
|
|
12
|
+
type: z.string().optional().describe("Filter by type (e.g. content, forum)"),
|
|
13
|
+
privacy: z.enum(["open", "closed", "secret"]).optional().describe("Filter by privacy level"),
|
|
14
|
+
},
|
|
15
|
+
async ({ limit, cursor, q, state, type, privacy }) => {
|
|
16
|
+
const data = await client.get("/spaces", { limit, cursor, q, state, type, privacy });
|
|
17
|
+
return json(data);
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
server.tool(
|
|
22
|
+
"list_membership_spaces",
|
|
23
|
+
"List basic information about spaces where the API token owner is a member. This is read-only membership inventory, not admin access to space subresources.",
|
|
7
24
|
{
|
|
8
25
|
limit: z.number().optional().describe("Max results per page (default 25, max 100)"),
|
|
9
26
|
cursor: z.string().optional().describe("Pagination cursor from a previous response"),
|
|
10
27
|
},
|
|
11
28
|
async ({ limit, cursor }) => {
|
|
12
|
-
const data = await client.get("/
|
|
29
|
+
const data = await client.get("/me/membership_spaces", { limit, cursor });
|
|
13
30
|
return json(data);
|
|
14
31
|
}
|
|
15
32
|
);
|
package/src/tools/surveys.js
CHANGED
|
@@ -3,14 +3,16 @@ import { z } from "zod";
|
|
|
3
3
|
export function registerSurveyTools(server, client) {
|
|
4
4
|
server.tool(
|
|
5
5
|
"list_surveys",
|
|
6
|
-
"List surveys in a space.",
|
|
6
|
+
"List surveys in a space. Use `q` to search by title.",
|
|
7
7
|
{
|
|
8
8
|
space_id: z.string().describe("Space ID (sp_...)"),
|
|
9
9
|
limit: z.number().optional().describe("Max results (default 25)"),
|
|
10
10
|
cursor: z.string().optional().describe("Pagination cursor"),
|
|
11
|
+
q: z.string().optional().describe("Search surveys by title (case-insensitive)"),
|
|
12
|
+
type: z.string().optional().describe("Filter by survey type"),
|
|
11
13
|
},
|
|
12
|
-
async ({ space_id, limit, cursor }) => {
|
|
13
|
-
const data = await client.get(`/spaces/${space_id}/surveys`, { limit, cursor });
|
|
14
|
+
async ({ space_id, limit, cursor, q, type }) => {
|
|
15
|
+
const data = await client.get(`/spaces/${space_id}/surveys`, { limit, cursor, q, type });
|
|
14
16
|
return json(data);
|
|
15
17
|
}
|
|
16
18
|
);
|