@orbitlogistics/mcp-plain 0.1.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 +76 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2096 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2096 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
|
|
11
|
+
// src/tools/threads.ts
|
|
12
|
+
import { ThreadStatus, ThreadFieldSchemaType } from "@team-plain/typescript-sdk";
|
|
13
|
+
|
|
14
|
+
// src/client.ts
|
|
15
|
+
import { PlainClient } from "@team-plain/typescript-sdk";
|
|
16
|
+
var client = null;
|
|
17
|
+
var getPlainClient = () => {
|
|
18
|
+
if (client) {
|
|
19
|
+
return client;
|
|
20
|
+
}
|
|
21
|
+
const apiKey = process.env.PLAIN_API_KEY;
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
throw new Error("PLAIN_API_KEY environment variable is required");
|
|
24
|
+
}
|
|
25
|
+
client = new PlainClient({ apiKey });
|
|
26
|
+
return client;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/types.ts
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
var IMPACT_LEVELS = [
|
|
32
|
+
"P0",
|
|
33
|
+
"P1",
|
|
34
|
+
"P2",
|
|
35
|
+
"RBS",
|
|
36
|
+
"RBP",
|
|
37
|
+
"not-a-bug"
|
|
38
|
+
];
|
|
39
|
+
var FIELD_KEY_MAP = {
|
|
40
|
+
impact_level: "impactLevel",
|
|
41
|
+
notion_ticket: "notionTicket",
|
|
42
|
+
posthog_session: "posthogSession",
|
|
43
|
+
github_pr: "githubPr",
|
|
44
|
+
tenant_id: "tenant",
|
|
45
|
+
app: "affectedApp",
|
|
46
|
+
reported_from: "reportedFromUrl",
|
|
47
|
+
stage: "affectedStage",
|
|
48
|
+
request_feature: "requestFeature",
|
|
49
|
+
sentry_session: "sentrySession"
|
|
50
|
+
};
|
|
51
|
+
var listThreadsInputSchema = z.object({
|
|
52
|
+
statuses: z.array(z.string()).optional().describe("Filter by thread statuses (e.g., 'TODO', 'DONE', 'SNOOZED')"),
|
|
53
|
+
priority: z.number().min(0).max(3).optional().describe("Filter by priority (0 = urgent, 3 = low)"),
|
|
54
|
+
labelTypeIds: z.array(z.string()).optional().describe("Filter by label type IDs"),
|
|
55
|
+
customerId: z.string().optional().describe("Filter by customer ID"),
|
|
56
|
+
tenantId: z.string().optional().describe("Filter by tenant ID (via thread field)"),
|
|
57
|
+
first: z.number().min(1).max(100).default(50).optional().describe("Number of threads to return (default 50, max 100)"),
|
|
58
|
+
after: z.string().optional().describe("Cursor for pagination")
|
|
59
|
+
});
|
|
60
|
+
var getThreadInputSchema = z.object({
|
|
61
|
+
threadId: z.string().describe("The ID of the thread to retrieve")
|
|
62
|
+
});
|
|
63
|
+
var getThreadFieldsInputSchema = z.object({
|
|
64
|
+
threadId: z.string().describe("The ID of the thread to get fields for")
|
|
65
|
+
});
|
|
66
|
+
var getThreadByRefInputSchema = z.object({
|
|
67
|
+
ref: z.string().describe("Thread reference number (e.g., 'T-510')")
|
|
68
|
+
});
|
|
69
|
+
var getAttachmentDownloadUrlInputSchema = z.object({
|
|
70
|
+
attachmentId: z.string().describe("The ID of the attachment to get a download URL for")
|
|
71
|
+
});
|
|
72
|
+
var getAttachmentContentInputSchema = z.object({
|
|
73
|
+
attachmentId: z.string().describe("The ID of the attachment to fetch content for")
|
|
74
|
+
});
|
|
75
|
+
var upsertThreadFieldInputSchema = z.object({
|
|
76
|
+
threadId: z.string().describe("The ID of the thread to update"),
|
|
77
|
+
key: z.string().describe(
|
|
78
|
+
"Custom field key in snake_case, e.g. impact_level, app, stage, tenant_id, notion_ticket, github_pr, posthog_session, sentry_session, reported_from, request_feature"
|
|
79
|
+
),
|
|
80
|
+
value: z.string().describe("Field value to set. For boolean fields like request_feature, use 'true' or 'false'")
|
|
81
|
+
});
|
|
82
|
+
var addInternalNoteInputSchema = z.object({
|
|
83
|
+
threadId: z.string().describe("The ID of the thread to add a note to"),
|
|
84
|
+
markdown: z.string().describe("Markdown content for the internal note")
|
|
85
|
+
});
|
|
86
|
+
var replyToThreadInputSchema = z.object({
|
|
87
|
+
threadId: z.string().describe("The ID of the thread to reply to"),
|
|
88
|
+
textContent: z.string().describe("Plain text content of the reply"),
|
|
89
|
+
markdownContent: z.string().optional().describe("Optional markdown-formatted content of the reply")
|
|
90
|
+
});
|
|
91
|
+
var markThreadAsDoneInputSchema = z.object({
|
|
92
|
+
threadId: z.string().describe("The ID of the thread to mark as done")
|
|
93
|
+
});
|
|
94
|
+
var addLabelsInputSchema = z.object({
|
|
95
|
+
threadId: z.string().describe("The ID of the thread to add labels to"),
|
|
96
|
+
labelTypeIds: z.array(z.string()).describe(
|
|
97
|
+
"Array of label type IDs to add. Use get_label_types first to discover available label type IDs."
|
|
98
|
+
)
|
|
99
|
+
});
|
|
100
|
+
var getLabelTypesInputSchema = z.object({
|
|
101
|
+
first: z.number().min(1).max(100).optional().describe("Number of label types to return (default 50)")
|
|
102
|
+
});
|
|
103
|
+
var createThreadInputSchema = z.object({
|
|
104
|
+
customerEmail: z.string().optional().describe("Customer email address. Provide either customerEmail, customerId, or customerExternalId."),
|
|
105
|
+
customerId: z.string().optional().describe("Existing Plain customer ID. Provide either customerEmail, customerId, or customerExternalId."),
|
|
106
|
+
customerExternalId: z.string().optional().describe("Customer external ID. Provide either customerEmail, customerId, or customerExternalId."),
|
|
107
|
+
customerFullName: z.string().optional().describe("Customer full name. Used when upserting a customer by email (ignored when customerId is provided)."),
|
|
108
|
+
title: z.string().optional().describe("Thread title"),
|
|
109
|
+
description: z.string().optional().describe("Thread description / preview text"),
|
|
110
|
+
markdown: z.string().describe("Markdown content for the first timeline entry in the thread"),
|
|
111
|
+
priority: z.number().min(0).max(3).optional().describe("Priority: 0 = urgent, 1 = high, 2 = normal (default), 3 = low"),
|
|
112
|
+
labelTypeIds: z.array(z.string()).optional().describe("Label type IDs to attach. Use get_label_types to discover available IDs."),
|
|
113
|
+
externalId: z.string().optional().describe("Your own unique identifier for this thread"),
|
|
114
|
+
// Custom thread fields (Plain snake_case keys)
|
|
115
|
+
impactLevel: z.enum(["P0", "P1", "P2", "RBS", "RBP", "not-a-bug"]).optional().describe("Impact level: P0 (critical), P1, P2, RBS (release blocker staging), RBP (release blocker production), not-a-bug"),
|
|
116
|
+
app: z.string().optional().describe("Affected app identifier (e.g. 'web', 'mobile', 'api')"),
|
|
117
|
+
tenantId: z.string().optional().describe("Tenant ID to associate with the thread"),
|
|
118
|
+
stage: z.string().optional().describe("Environment stage (e.g. 'production', 'staging', 'develop')"),
|
|
119
|
+
reportedFrom: z.string().optional().describe("URL where the issue was reported from"),
|
|
120
|
+
posthogSession: z.string().optional().describe("PostHog session replay URL or session recording ID"),
|
|
121
|
+
sentrySession: z.string().optional().describe("Sentry replay URL or replay ID"),
|
|
122
|
+
notionTicket: z.string().optional().describe("Notion ticket URL or ID"),
|
|
123
|
+
githubPr: z.string().optional().describe("GitHub PR URL"),
|
|
124
|
+
requestFeature: z.boolean().optional().describe("Whether this is a feature request")
|
|
125
|
+
});
|
|
126
|
+
var listHelpCentersInputSchema = z.object({});
|
|
127
|
+
var getHelpCenterInputSchema = z.object({
|
|
128
|
+
helpCenterId: z.string().describe("The ID of the help center to retrieve")
|
|
129
|
+
});
|
|
130
|
+
var listHelpCenterArticlesInputSchema = z.object({
|
|
131
|
+
helpCenterId: z.string().describe("The ID of the help center to list articles from"),
|
|
132
|
+
first: z.number().min(1).max(100).optional().describe("Number of articles to return (default 20, max 100)")
|
|
133
|
+
});
|
|
134
|
+
var getHelpCenterArticleInputSchema = z.object({
|
|
135
|
+
helpCenterArticleId: z.string().describe("The ID of the help center article to retrieve")
|
|
136
|
+
});
|
|
137
|
+
var getHelpCenterArticleBySlugInputSchema = z.object({
|
|
138
|
+
helpCenterId: z.string().describe("The ID of the help center the article belongs to"),
|
|
139
|
+
slug: z.string().describe("The URL slug of the article")
|
|
140
|
+
});
|
|
141
|
+
var upsertHelpCenterArticleInputSchema = z.object({
|
|
142
|
+
helpCenterId: z.string().describe("The ID of the help center to create/update the article in"),
|
|
143
|
+
title: z.string().describe("Article title"),
|
|
144
|
+
contentHtml: z.string().describe("Article content as HTML (not markdown)"),
|
|
145
|
+
helpCenterArticleId: z.string().optional().describe("Existing article ID for updates. Omit to create a new article."),
|
|
146
|
+
description: z.string().describe("Short description / summary of the article"),
|
|
147
|
+
slug: z.string().optional().describe("URL slug for the article"),
|
|
148
|
+
helpCenterArticleGroupId: z.string().optional().describe("Article group ID to place the article in")
|
|
149
|
+
});
|
|
150
|
+
var createHelpCenterArticleGroupInputSchema = z.object({
|
|
151
|
+
helpCenterId: z.string().describe("The ID of the help center to create the group in"),
|
|
152
|
+
name: z.string().describe("Name of the article group"),
|
|
153
|
+
parentHelpCenterArticleGroupId: z.string().optional().describe("Parent group ID for nested groups")
|
|
154
|
+
});
|
|
155
|
+
var deleteHelpCenterArticleGroupInputSchema = z.object({
|
|
156
|
+
helpCenterArticleGroupId: z.string().describe("The ID of the article group to delete")
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// src/tools/threads.ts
|
|
160
|
+
var THREAD_QUERY = `
|
|
161
|
+
query GetThread($threadId: ID!) {
|
|
162
|
+
thread(threadId: $threadId) {
|
|
163
|
+
id
|
|
164
|
+
externalId
|
|
165
|
+
title
|
|
166
|
+
previewText
|
|
167
|
+
status
|
|
168
|
+
priority
|
|
169
|
+
customer {
|
|
170
|
+
id
|
|
171
|
+
fullName
|
|
172
|
+
email { email }
|
|
173
|
+
externalId
|
|
174
|
+
}
|
|
175
|
+
labels {
|
|
176
|
+
labelType {
|
|
177
|
+
id
|
|
178
|
+
name
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
threadFields {
|
|
182
|
+
key
|
|
183
|
+
stringValue
|
|
184
|
+
booleanValue
|
|
185
|
+
}
|
|
186
|
+
createdAt { iso8601 }
|
|
187
|
+
updatedAt { iso8601 }
|
|
188
|
+
timelineEntries(first: 50) {
|
|
189
|
+
edges {
|
|
190
|
+
node {
|
|
191
|
+
id
|
|
192
|
+
timestamp { iso8601 }
|
|
193
|
+
actor {
|
|
194
|
+
__typename
|
|
195
|
+
... on UserActor { user { fullName } }
|
|
196
|
+
... on CustomerActor { customer { fullName } }
|
|
197
|
+
... on MachineUserActor { machineUser { fullName } }
|
|
198
|
+
}
|
|
199
|
+
entry {
|
|
200
|
+
__typename
|
|
201
|
+
... on NoteEntry {
|
|
202
|
+
markdown
|
|
203
|
+
}
|
|
204
|
+
... on EmailEntry {
|
|
205
|
+
subject
|
|
206
|
+
markdownContent
|
|
207
|
+
from { email name }
|
|
208
|
+
attachments {
|
|
209
|
+
id
|
|
210
|
+
fileName
|
|
211
|
+
fileSize { kiloBytes }
|
|
212
|
+
fileMimeType
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
... on CustomEntry {
|
|
216
|
+
title
|
|
217
|
+
externalId
|
|
218
|
+
components {
|
|
219
|
+
__typename
|
|
220
|
+
... on ComponentText {
|
|
221
|
+
text
|
|
222
|
+
}
|
|
223
|
+
... on ComponentPlainText {
|
|
224
|
+
plainText
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
... on SlackMessageEntry {
|
|
229
|
+
text
|
|
230
|
+
slackMessageLink
|
|
231
|
+
attachments {
|
|
232
|
+
id
|
|
233
|
+
fileName
|
|
234
|
+
fileSize { kiloBytes }
|
|
235
|
+
fileMimeType
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
... on SlackReplyEntry {
|
|
239
|
+
text
|
|
240
|
+
slackMessageLink
|
|
241
|
+
attachments {
|
|
242
|
+
id
|
|
243
|
+
fileName
|
|
244
|
+
fileSize { kiloBytes }
|
|
245
|
+
fileMimeType
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
... on ThreadDiscussionEntry {
|
|
249
|
+
threadDiscussionId
|
|
250
|
+
discussionType
|
|
251
|
+
slackChannelName
|
|
252
|
+
slackMessageLink
|
|
253
|
+
emailRecipients
|
|
254
|
+
}
|
|
255
|
+
... on ThreadDiscussionMessageEntry {
|
|
256
|
+
threadDiscussionId
|
|
257
|
+
threadDiscussionMessageId
|
|
258
|
+
discussionType
|
|
259
|
+
text
|
|
260
|
+
resolvedText
|
|
261
|
+
slackMessageLink
|
|
262
|
+
attachments {
|
|
263
|
+
id
|
|
264
|
+
fileName
|
|
265
|
+
fileSize { kiloBytes }
|
|
266
|
+
fileMimeType
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
... on ThreadDiscussionResolvedEntry {
|
|
270
|
+
threadDiscussionId
|
|
271
|
+
discussionType
|
|
272
|
+
resolvedAt { iso8601 }
|
|
273
|
+
slackChannelName
|
|
274
|
+
slackMessageLink
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
283
|
+
var THREAD_BY_REF_QUERY = `
|
|
284
|
+
query GetThreadByRef($ref: String!) {
|
|
285
|
+
threadByRef(ref: $ref) {
|
|
286
|
+
id
|
|
287
|
+
externalId
|
|
288
|
+
title
|
|
289
|
+
previewText
|
|
290
|
+
status
|
|
291
|
+
priority
|
|
292
|
+
customer {
|
|
293
|
+
id
|
|
294
|
+
fullName
|
|
295
|
+
email { email }
|
|
296
|
+
externalId
|
|
297
|
+
}
|
|
298
|
+
labels {
|
|
299
|
+
labelType {
|
|
300
|
+
id
|
|
301
|
+
name
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
threadFields {
|
|
305
|
+
key
|
|
306
|
+
stringValue
|
|
307
|
+
booleanValue
|
|
308
|
+
}
|
|
309
|
+
createdAt { iso8601 }
|
|
310
|
+
updatedAt { iso8601 }
|
|
311
|
+
timelineEntries(first: 50) {
|
|
312
|
+
edges {
|
|
313
|
+
node {
|
|
314
|
+
id
|
|
315
|
+
timestamp { iso8601 }
|
|
316
|
+
actor {
|
|
317
|
+
__typename
|
|
318
|
+
... on UserActor { user { fullName } }
|
|
319
|
+
... on CustomerActor { customer { fullName } }
|
|
320
|
+
... on MachineUserActor { machineUser { fullName } }
|
|
321
|
+
}
|
|
322
|
+
entry {
|
|
323
|
+
__typename
|
|
324
|
+
... on NoteEntry {
|
|
325
|
+
markdown
|
|
326
|
+
}
|
|
327
|
+
... on EmailEntry {
|
|
328
|
+
subject
|
|
329
|
+
markdownContent
|
|
330
|
+
from { email name }
|
|
331
|
+
attachments {
|
|
332
|
+
id
|
|
333
|
+
fileName
|
|
334
|
+
fileSize { kiloBytes }
|
|
335
|
+
fileMimeType
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
... on CustomEntry {
|
|
339
|
+
title
|
|
340
|
+
externalId
|
|
341
|
+
components {
|
|
342
|
+
__typename
|
|
343
|
+
... on ComponentText {
|
|
344
|
+
text
|
|
345
|
+
}
|
|
346
|
+
... on ComponentPlainText {
|
|
347
|
+
plainText
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
... on SlackMessageEntry {
|
|
352
|
+
text
|
|
353
|
+
slackMessageLink
|
|
354
|
+
attachments {
|
|
355
|
+
id
|
|
356
|
+
fileName
|
|
357
|
+
fileSize { kiloBytes }
|
|
358
|
+
fileMimeType
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
... on SlackReplyEntry {
|
|
362
|
+
text
|
|
363
|
+
slackMessageLink
|
|
364
|
+
attachments {
|
|
365
|
+
id
|
|
366
|
+
fileName
|
|
367
|
+
fileSize { kiloBytes }
|
|
368
|
+
fileMimeType
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
... on ThreadDiscussionEntry {
|
|
372
|
+
threadDiscussionId
|
|
373
|
+
discussionType
|
|
374
|
+
slackChannelName
|
|
375
|
+
slackMessageLink
|
|
376
|
+
emailRecipients
|
|
377
|
+
}
|
|
378
|
+
... on ThreadDiscussionMessageEntry {
|
|
379
|
+
threadDiscussionId
|
|
380
|
+
threadDiscussionMessageId
|
|
381
|
+
discussionType
|
|
382
|
+
text
|
|
383
|
+
resolvedText
|
|
384
|
+
slackMessageLink
|
|
385
|
+
attachments {
|
|
386
|
+
id
|
|
387
|
+
fileName
|
|
388
|
+
fileSize { kiloBytes }
|
|
389
|
+
fileMimeType
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
... on ThreadDiscussionResolvedEntry {
|
|
393
|
+
threadDiscussionId
|
|
394
|
+
discussionType
|
|
395
|
+
resolvedAt { iso8601 }
|
|
396
|
+
slackChannelName
|
|
397
|
+
slackMessageLink
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
`;
|
|
406
|
+
var mapStatus = (status) => {
|
|
407
|
+
const statusMap = {
|
|
408
|
+
TODO: ThreadStatus.Todo,
|
|
409
|
+
DONE: ThreadStatus.Done,
|
|
410
|
+
SNOOZED: ThreadStatus.Snoozed
|
|
411
|
+
};
|
|
412
|
+
return statusMap[status];
|
|
413
|
+
};
|
|
414
|
+
var parseThreadFields = (fields) => {
|
|
415
|
+
const result = {};
|
|
416
|
+
for (const field of fields) {
|
|
417
|
+
const mappedKey = FIELD_KEY_MAP[field.key];
|
|
418
|
+
if (!mappedKey) continue;
|
|
419
|
+
if (mappedKey === "requestFeature") {
|
|
420
|
+
result[mappedKey] = field.booleanValue ?? void 0;
|
|
421
|
+
} else if (mappedKey === "impactLevel") {
|
|
422
|
+
const value = field.stringValue;
|
|
423
|
+
if (value && IMPACT_LEVELS.includes(value)) {
|
|
424
|
+
result[mappedKey] = value;
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
result[mappedKey] = field.stringValue ?? void 0;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return result;
|
|
431
|
+
};
|
|
432
|
+
var extractCustomerInfo = (customer) => ({
|
|
433
|
+
id: customer.id,
|
|
434
|
+
email: customer.email?.email,
|
|
435
|
+
fullName: customer.fullName ?? void 0,
|
|
436
|
+
externalId: customer.externalId ?? void 0
|
|
437
|
+
});
|
|
438
|
+
var extractLabels = (labels) => labels.map((l) => ({
|
|
439
|
+
id: l.labelType.id,
|
|
440
|
+
name: l.labelType.name
|
|
441
|
+
}));
|
|
442
|
+
var extractActorName = (actor) => {
|
|
443
|
+
if (!actor) return void 0;
|
|
444
|
+
switch (actor.__typename) {
|
|
445
|
+
case "UserActor":
|
|
446
|
+
return actor.user?.fullName ?? void 0;
|
|
447
|
+
case "CustomerActor":
|
|
448
|
+
return actor.customer?.fullName ?? void 0;
|
|
449
|
+
case "MachineUserActor":
|
|
450
|
+
return actor.machineUser?.fullName ?? void 0;
|
|
451
|
+
default:
|
|
452
|
+
return void 0;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
var parseAttachments = (attachments) => {
|
|
456
|
+
if (!attachments || attachments.length === 0) return void 0;
|
|
457
|
+
return attachments.map((att) => ({
|
|
458
|
+
id: att.id,
|
|
459
|
+
fileName: att.fileName,
|
|
460
|
+
fileSizeKb: att.fileSize?.kiloBytes ?? void 0,
|
|
461
|
+
mimeType: att.fileMimeType ?? void 0
|
|
462
|
+
}));
|
|
463
|
+
};
|
|
464
|
+
var parseTimelineEntry = (node) => {
|
|
465
|
+
const { id, timestamp, actor, entry } = node;
|
|
466
|
+
const baseEntry = {
|
|
467
|
+
id,
|
|
468
|
+
timestamp: timestamp?.iso8601 ?? ""
|
|
469
|
+
};
|
|
470
|
+
switch (entry.__typename) {
|
|
471
|
+
case "NoteEntry":
|
|
472
|
+
return {
|
|
473
|
+
...baseEntry,
|
|
474
|
+
entryType: "NOTE",
|
|
475
|
+
text: entry.text || entry.markdown || "",
|
|
476
|
+
// fallback to markdown if text is empty
|
|
477
|
+
markdown: entry.markdown ?? void 0,
|
|
478
|
+
createdBy: actor ? { name: extractActorName(actor) } : void 0
|
|
479
|
+
};
|
|
480
|
+
case "EmailEntry":
|
|
481
|
+
return {
|
|
482
|
+
...baseEntry,
|
|
483
|
+
entryType: "EMAIL",
|
|
484
|
+
subject: entry.subject ?? void 0,
|
|
485
|
+
textContent: entry.markdownContent || entry.textContent || void 0,
|
|
486
|
+
from: entry.from ? { email: entry.from.email, name: entry.from.name ?? void 0 } : void 0,
|
|
487
|
+
attachments: parseAttachments(entry.attachments)
|
|
488
|
+
};
|
|
489
|
+
case "ChatEntry":
|
|
490
|
+
return {
|
|
491
|
+
...baseEntry,
|
|
492
|
+
entryType: "CHAT",
|
|
493
|
+
text: entry.text ?? "",
|
|
494
|
+
createdBy: actor ? {
|
|
495
|
+
name: extractActorName(actor),
|
|
496
|
+
type: actor.__typename === "CustomerActor" ? "customer" : "user"
|
|
497
|
+
} : void 0
|
|
498
|
+
};
|
|
499
|
+
case "CustomEntry": {
|
|
500
|
+
const componentTexts = entry.components?.map((c) => c.text || c.plainText).filter((t) => !!t) ?? [];
|
|
501
|
+
const content = componentTexts.length > 0 ? componentTexts.join("\n") : void 0;
|
|
502
|
+
return {
|
|
503
|
+
...baseEntry,
|
|
504
|
+
entryType: "CUSTOM",
|
|
505
|
+
title: entry.title ?? void 0,
|
|
506
|
+
externalId: entry.externalId ?? void 0,
|
|
507
|
+
content
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
case "SlackMessageEntry":
|
|
511
|
+
return {
|
|
512
|
+
...baseEntry,
|
|
513
|
+
entryType: "SLACK",
|
|
514
|
+
text: entry.text ?? "",
|
|
515
|
+
isReply: false,
|
|
516
|
+
slackMessageLink: entry.slackMessageLink ?? void 0,
|
|
517
|
+
attachments: parseAttachments(entry.attachments),
|
|
518
|
+
createdBy: actor ? { name: extractActorName(actor) } : void 0
|
|
519
|
+
};
|
|
520
|
+
case "SlackReplyEntry":
|
|
521
|
+
return {
|
|
522
|
+
...baseEntry,
|
|
523
|
+
entryType: "SLACK",
|
|
524
|
+
text: entry.text ?? "",
|
|
525
|
+
isReply: true,
|
|
526
|
+
slackMessageLink: entry.slackMessageLink ?? void 0,
|
|
527
|
+
attachments: parseAttachments(entry.attachments),
|
|
528
|
+
createdBy: actor ? { name: extractActorName(actor) } : void 0
|
|
529
|
+
};
|
|
530
|
+
case "ThreadDiscussionEntry":
|
|
531
|
+
return {
|
|
532
|
+
...baseEntry,
|
|
533
|
+
entryType: "DISCUSSION",
|
|
534
|
+
threadDiscussionId: entry.threadDiscussionId ?? "",
|
|
535
|
+
discussionType: entry.discussionType ?? "",
|
|
536
|
+
slackChannelName: entry.slackChannelName ?? void 0,
|
|
537
|
+
slackMessageLink: entry.slackMessageLink ?? void 0,
|
|
538
|
+
emailRecipients: entry.emailRecipients ?? void 0
|
|
539
|
+
};
|
|
540
|
+
case "ThreadDiscussionMessageEntry":
|
|
541
|
+
return {
|
|
542
|
+
...baseEntry,
|
|
543
|
+
entryType: "DISCUSSION_MESSAGE",
|
|
544
|
+
threadDiscussionId: entry.threadDiscussionId ?? "",
|
|
545
|
+
threadDiscussionMessageId: entry.threadDiscussionMessageId ?? "",
|
|
546
|
+
discussionType: entry.discussionType ?? "",
|
|
547
|
+
text: entry.resolvedText || entry.text || "",
|
|
548
|
+
slackMessageLink: entry.slackMessageLink ?? void 0,
|
|
549
|
+
attachments: parseAttachments(entry.attachments),
|
|
550
|
+
createdBy: actor ? { name: extractActorName(actor) } : void 0
|
|
551
|
+
};
|
|
552
|
+
case "ThreadDiscussionResolvedEntry":
|
|
553
|
+
return {
|
|
554
|
+
...baseEntry,
|
|
555
|
+
entryType: "DISCUSSION_RESOLVED",
|
|
556
|
+
threadDiscussionId: entry.threadDiscussionId ?? "",
|
|
557
|
+
discussionType: entry.discussionType ?? "",
|
|
558
|
+
resolvedAt: entry.resolvedAt?.iso8601 ?? "",
|
|
559
|
+
slackChannelName: entry.slackChannelName ?? void 0,
|
|
560
|
+
slackMessageLink: entry.slackMessageLink ?? void 0
|
|
561
|
+
};
|
|
562
|
+
default:
|
|
563
|
+
return {
|
|
564
|
+
...baseEntry,
|
|
565
|
+
entryType: "UNKNOWN"
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
var listThreads = async (input) => {
|
|
570
|
+
const client2 = getPlainClient();
|
|
571
|
+
const statuses = input.statuses?.map(mapStatus).filter((s) => s !== void 0);
|
|
572
|
+
const result = await client2.getThreads({
|
|
573
|
+
filters: {
|
|
574
|
+
...statuses && statuses.length > 0 ? { statuses } : {},
|
|
575
|
+
...input.customerId ? { customerIds: [input.customerId] } : {},
|
|
576
|
+
...input.labelTypeIds ? { labelTypeIds: input.labelTypeIds } : {}
|
|
577
|
+
},
|
|
578
|
+
first: input.first ?? 50,
|
|
579
|
+
...input.after ? { after: input.after } : {}
|
|
580
|
+
});
|
|
581
|
+
if (result.error) {
|
|
582
|
+
throw new Error(`Failed to list threads: ${result.error.message}`);
|
|
583
|
+
}
|
|
584
|
+
const threads = result.data.threads.map((thread) => ({
|
|
585
|
+
id: thread.id,
|
|
586
|
+
title: thread.title ?? void 0,
|
|
587
|
+
status: thread.status,
|
|
588
|
+
priority: thread.priority,
|
|
589
|
+
customer: thread.customer ? extractCustomerInfo(thread.customer) : void 0,
|
|
590
|
+
labels: extractLabels(thread.labels),
|
|
591
|
+
createdAt: thread.createdAt.iso8601,
|
|
592
|
+
updatedAt: thread.updatedAt.iso8601
|
|
593
|
+
}));
|
|
594
|
+
const filteredThreads = input.priority !== void 0 ? threads.filter((t) => t.priority === input.priority) : threads;
|
|
595
|
+
let finalThreads = filteredThreads;
|
|
596
|
+
if (input.tenantId) {
|
|
597
|
+
const threadsWithTenant = await Promise.all(
|
|
598
|
+
filteredThreads.map(async (thread) => {
|
|
599
|
+
const fields = await getThreadFields({ threadId: thread.id });
|
|
600
|
+
return { thread, tenant: fields.tenant };
|
|
601
|
+
})
|
|
602
|
+
);
|
|
603
|
+
finalThreads = threadsWithTenant.filter((t) => t.tenant === input.tenantId).map((t) => t.thread);
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
threads: finalThreads,
|
|
607
|
+
pageInfo: {
|
|
608
|
+
hasNextPage: result.data.pageInfo.hasNextPage,
|
|
609
|
+
hasPreviousPage: result.data.pageInfo.hasPreviousPage,
|
|
610
|
+
startCursor: result.data.pageInfo.startCursor ?? void 0,
|
|
611
|
+
endCursor: result.data.pageInfo.endCursor ?? void 0
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
};
|
|
615
|
+
var getThread = async (input) => {
|
|
616
|
+
const client2 = getPlainClient();
|
|
617
|
+
const result = await client2.rawRequest({
|
|
618
|
+
query: THREAD_QUERY,
|
|
619
|
+
variables: { threadId: input.threadId }
|
|
620
|
+
});
|
|
621
|
+
if (result.error) {
|
|
622
|
+
throw new Error(`Failed to get thread: ${result.error.message}`);
|
|
623
|
+
}
|
|
624
|
+
const data = result.data;
|
|
625
|
+
const thread = data.thread;
|
|
626
|
+
if (!thread) {
|
|
627
|
+
throw new Error(`Thread not found: ${input.threadId}`);
|
|
628
|
+
}
|
|
629
|
+
const customFields = parseThreadFields(thread.threadFields ?? []);
|
|
630
|
+
const timeline = thread.timelineEntries?.edges.map((edge) => parseTimelineEntry(edge.node)).filter((entry) => {
|
|
631
|
+
if (entry.entryType === "CUSTOM" && entry.title?.toLowerCase().includes("autoresponse")) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
return true;
|
|
635
|
+
}) ?? [];
|
|
636
|
+
return {
|
|
637
|
+
id: thread.id,
|
|
638
|
+
title: thread.title ?? void 0,
|
|
639
|
+
description: thread.previewText ?? void 0,
|
|
640
|
+
status: thread.status,
|
|
641
|
+
priority: thread.priority,
|
|
642
|
+
externalId: thread.externalId ?? void 0,
|
|
643
|
+
customer: thread.customer ? extractCustomerInfo(thread.customer) : void 0,
|
|
644
|
+
labels: extractLabels(thread.labels),
|
|
645
|
+
createdAt: thread.createdAt.iso8601,
|
|
646
|
+
updatedAt: thread.updatedAt.iso8601,
|
|
647
|
+
customFields,
|
|
648
|
+
timeline
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
var getThreadByRef = async (input) => {
|
|
652
|
+
const client2 = getPlainClient();
|
|
653
|
+
const result = await client2.rawRequest({
|
|
654
|
+
query: THREAD_BY_REF_QUERY,
|
|
655
|
+
variables: { ref: input.ref }
|
|
656
|
+
});
|
|
657
|
+
if (result.error) {
|
|
658
|
+
throw new Error(`Failed to get thread: ${result.error.message}`);
|
|
659
|
+
}
|
|
660
|
+
const data = result.data;
|
|
661
|
+
const thread = data.threadByRef;
|
|
662
|
+
if (!thread) {
|
|
663
|
+
throw new Error(`Thread not found: ${input.ref}`);
|
|
664
|
+
}
|
|
665
|
+
const customFields = parseThreadFields(thread.threadFields ?? []);
|
|
666
|
+
const timeline = thread.timelineEntries?.edges.map((edge) => parseTimelineEntry(edge.node)).filter((entry) => {
|
|
667
|
+
if (entry.entryType === "CUSTOM" && entry.title?.toLowerCase().includes("autoresponse")) {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
return true;
|
|
671
|
+
}) ?? [];
|
|
672
|
+
return {
|
|
673
|
+
id: thread.id,
|
|
674
|
+
title: thread.title ?? void 0,
|
|
675
|
+
description: thread.previewText ?? void 0,
|
|
676
|
+
status: thread.status,
|
|
677
|
+
priority: thread.priority,
|
|
678
|
+
externalId: thread.externalId ?? void 0,
|
|
679
|
+
customer: thread.customer ? extractCustomerInfo(thread.customer) : void 0,
|
|
680
|
+
labels: extractLabels(thread.labels),
|
|
681
|
+
createdAt: thread.createdAt.iso8601,
|
|
682
|
+
updatedAt: thread.updatedAt.iso8601,
|
|
683
|
+
customFields,
|
|
684
|
+
timeline
|
|
685
|
+
};
|
|
686
|
+
};
|
|
687
|
+
var getThreadFields = async (input) => {
|
|
688
|
+
const client2 = getPlainClient();
|
|
689
|
+
const result = await client2.getThread({ threadId: input.threadId });
|
|
690
|
+
if (result.error) {
|
|
691
|
+
throw new Error(`Failed to get thread fields: ${result.error.message}`);
|
|
692
|
+
}
|
|
693
|
+
const thread = result.data;
|
|
694
|
+
if (!thread) {
|
|
695
|
+
throw new Error(`Thread not found: ${input.threadId}`);
|
|
696
|
+
}
|
|
697
|
+
return parseThreadFields(thread.threadFields ?? []);
|
|
698
|
+
};
|
|
699
|
+
var CREATE_ATTACHMENT_DOWNLOAD_URL_MUTATION = `
|
|
700
|
+
mutation CreateAttachmentDownloadUrl($input: CreateAttachmentDownloadUrlInput!) {
|
|
701
|
+
createAttachmentDownloadUrl(input: $input) {
|
|
702
|
+
attachmentDownloadUrl {
|
|
703
|
+
downloadUrl
|
|
704
|
+
expiresAt { iso8601 }
|
|
705
|
+
}
|
|
706
|
+
error {
|
|
707
|
+
message
|
|
708
|
+
type
|
|
709
|
+
code
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
`;
|
|
714
|
+
var getAttachmentDownloadUrl = async (input) => {
|
|
715
|
+
const client2 = getPlainClient();
|
|
716
|
+
const result = await client2.rawRequest({
|
|
717
|
+
query: CREATE_ATTACHMENT_DOWNLOAD_URL_MUTATION,
|
|
718
|
+
variables: { input: { attachmentId: input.attachmentId } }
|
|
719
|
+
});
|
|
720
|
+
if (result.error) {
|
|
721
|
+
throw new Error(`Failed to get attachment download URL: ${result.error.message}`);
|
|
722
|
+
}
|
|
723
|
+
const data = result.data;
|
|
724
|
+
const response = data.createAttachmentDownloadUrl;
|
|
725
|
+
if (response.error) {
|
|
726
|
+
throw new Error(`Failed to get attachment download URL: ${response.error.message}`);
|
|
727
|
+
}
|
|
728
|
+
if (!response.attachmentDownloadUrl) {
|
|
729
|
+
throw new Error("No download URL returned");
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
downloadUrl: response.attachmentDownloadUrl.downloadUrl,
|
|
733
|
+
expiresAt: response.attachmentDownloadUrl.expiresAt.iso8601
|
|
734
|
+
};
|
|
735
|
+
};
|
|
736
|
+
var isTextMimeType = (mimeType) => {
|
|
737
|
+
if (!mimeType) return false;
|
|
738
|
+
if (mimeType.startsWith("text/")) return true;
|
|
739
|
+
const textApplicationTypes = [
|
|
740
|
+
"application/json",
|
|
741
|
+
"application/xml",
|
|
742
|
+
"application/javascript",
|
|
743
|
+
"application/typescript",
|
|
744
|
+
"application/x-yaml",
|
|
745
|
+
"application/yaml",
|
|
746
|
+
"application/x-sh",
|
|
747
|
+
"application/x-python",
|
|
748
|
+
"application/sql",
|
|
749
|
+
"application/graphql",
|
|
750
|
+
"application/ld+json",
|
|
751
|
+
"application/manifest+json"
|
|
752
|
+
];
|
|
753
|
+
return textApplicationTypes.includes(mimeType);
|
|
754
|
+
};
|
|
755
|
+
var getAttachmentContent = async (input) => {
|
|
756
|
+
const { downloadUrl } = await getAttachmentDownloadUrl({ attachmentId: input.attachmentId });
|
|
757
|
+
const response = await fetch(downloadUrl);
|
|
758
|
+
if (!response.ok) {
|
|
759
|
+
throw new Error(`Failed to fetch attachment content: ${response.status} ${response.statusText}`);
|
|
760
|
+
}
|
|
761
|
+
const contentType = response.headers.get("content-type") || void 0;
|
|
762
|
+
const isText = isTextMimeType(contentType);
|
|
763
|
+
if (isText) {
|
|
764
|
+
const text = await response.text();
|
|
765
|
+
return {
|
|
766
|
+
content: text,
|
|
767
|
+
mimeType: contentType,
|
|
768
|
+
encoding: "text"
|
|
769
|
+
};
|
|
770
|
+
} else {
|
|
771
|
+
const buffer = await response.arrayBuffer();
|
|
772
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
773
|
+
return {
|
|
774
|
+
content: base64,
|
|
775
|
+
mimeType: contentType,
|
|
776
|
+
encoding: "base64"
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
var replyToThread = async (input) => {
|
|
781
|
+
const client2 = getPlainClient();
|
|
782
|
+
const result = await client2.replyToThread({
|
|
783
|
+
threadId: input.threadId,
|
|
784
|
+
textContent: input.textContent,
|
|
785
|
+
...input.markdownContent ? { markdownContent: input.markdownContent } : {}
|
|
786
|
+
});
|
|
787
|
+
if (result.error) {
|
|
788
|
+
throw new Error(`Failed to reply to thread: ${result.error.message}`);
|
|
789
|
+
}
|
|
790
|
+
return { success: true };
|
|
791
|
+
};
|
|
792
|
+
var markThreadAsDone = async (input) => {
|
|
793
|
+
const client2 = getPlainClient();
|
|
794
|
+
const result = await client2.markThreadAsDone({
|
|
795
|
+
threadId: input.threadId
|
|
796
|
+
});
|
|
797
|
+
if (result.error) {
|
|
798
|
+
throw new Error(`Failed to mark thread as done: ${result.error.message}`);
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
threadId: result.data.id,
|
|
802
|
+
status: result.data.status
|
|
803
|
+
};
|
|
804
|
+
};
|
|
805
|
+
var UPSERT_THREAD_FIELD_MUTATION = `
|
|
806
|
+
mutation UpsertThreadField($input: UpsertThreadFieldInput!) {
|
|
807
|
+
upsertThreadField(input: $input) {
|
|
808
|
+
threadField {
|
|
809
|
+
key
|
|
810
|
+
stringValue
|
|
811
|
+
booleanValue
|
|
812
|
+
}
|
|
813
|
+
error {
|
|
814
|
+
message
|
|
815
|
+
type
|
|
816
|
+
code
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
`;
|
|
821
|
+
var BOOLEAN_FIELD_KEYS = /* @__PURE__ */ new Set(["request_feature"]);
|
|
822
|
+
var upsertThreadField = async (input) => {
|
|
823
|
+
const client2 = getPlainClient();
|
|
824
|
+
const isBoolean = BOOLEAN_FIELD_KEYS.has(input.key);
|
|
825
|
+
const threadField = isBoolean ? { key: input.key, booleanValue: input.value === "true" } : { key: input.key, stringValue: input.value };
|
|
826
|
+
const result = await client2.rawRequest({
|
|
827
|
+
query: UPSERT_THREAD_FIELD_MUTATION,
|
|
828
|
+
variables: { input: { threadId: input.threadId, threadField } }
|
|
829
|
+
});
|
|
830
|
+
if (result.error) {
|
|
831
|
+
throw new Error(`Failed to upsert thread field: ${result.error.message}`);
|
|
832
|
+
}
|
|
833
|
+
const data = result.data;
|
|
834
|
+
const response = data.upsertThreadField;
|
|
835
|
+
if (response.error) {
|
|
836
|
+
throw new Error(`Failed to upsert thread field: ${response.error.message}`);
|
|
837
|
+
}
|
|
838
|
+
return { success: true, key: input.key, value: input.value };
|
|
839
|
+
};
|
|
840
|
+
var CREATE_NOTE_MUTATION = `
|
|
841
|
+
mutation CreateNote($input: CreateNoteInput!) {
|
|
842
|
+
createNote(input: $input) {
|
|
843
|
+
note {
|
|
844
|
+
id
|
|
845
|
+
}
|
|
846
|
+
error {
|
|
847
|
+
message
|
|
848
|
+
type
|
|
849
|
+
code
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
`;
|
|
854
|
+
var addInternalNote = async (input) => {
|
|
855
|
+
const client2 = getPlainClient();
|
|
856
|
+
const result = await client2.rawRequest({
|
|
857
|
+
query: CREATE_NOTE_MUTATION,
|
|
858
|
+
variables: { input: { threadId: input.threadId, markdown: input.markdown } }
|
|
859
|
+
});
|
|
860
|
+
if (result.error) {
|
|
861
|
+
throw new Error(`Failed to add internal note: ${result.error.message}`);
|
|
862
|
+
}
|
|
863
|
+
const data = result.data;
|
|
864
|
+
const response = data.createNote;
|
|
865
|
+
if (response.error) {
|
|
866
|
+
throw new Error(`Failed to add internal note: ${response.error.message}`);
|
|
867
|
+
}
|
|
868
|
+
if (!response.note) {
|
|
869
|
+
throw new Error("No note returned from createNote mutation");
|
|
870
|
+
}
|
|
871
|
+
return { success: true, noteId: response.note.id };
|
|
872
|
+
};
|
|
873
|
+
var addLabelsToThread = async (input) => {
|
|
874
|
+
const client2 = getPlainClient();
|
|
875
|
+
const result = await client2.addLabels({
|
|
876
|
+
threadId: input.threadId,
|
|
877
|
+
labelTypeIds: input.labelTypeIds
|
|
878
|
+
});
|
|
879
|
+
if (result.error) {
|
|
880
|
+
throw new Error(`Failed to add labels: ${result.error.message}`);
|
|
881
|
+
}
|
|
882
|
+
return { success: true, labelCount: input.labelTypeIds.length };
|
|
883
|
+
};
|
|
884
|
+
var getLabelTypes = async (input) => {
|
|
885
|
+
const client2 = getPlainClient();
|
|
886
|
+
const result = await client2.getLabelTypes({
|
|
887
|
+
first: input.first ?? 50
|
|
888
|
+
});
|
|
889
|
+
if (result.error) {
|
|
890
|
+
throw new Error(`Failed to get label types: ${result.error.message}`);
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
labelTypes: result.data.labelTypes.map((lt) => ({
|
|
894
|
+
id: lt.id,
|
|
895
|
+
name: lt.name,
|
|
896
|
+
isArchived: lt.isArchived
|
|
897
|
+
}))
|
|
898
|
+
};
|
|
899
|
+
};
|
|
900
|
+
var formatPlainError = (error) => {
|
|
901
|
+
let msg = error.message;
|
|
902
|
+
if (error.type) msg += ` [${error.type}]`;
|
|
903
|
+
if (error.code) msg += ` (code: ${error.code})`;
|
|
904
|
+
if (error.fields && error.fields.length > 0) {
|
|
905
|
+
const fieldDetails = error.fields.map((f) => ` - ${f.field}: ${f.message} (${f.type})`).join("\n");
|
|
906
|
+
msg += `
|
|
907
|
+
Field errors:
|
|
908
|
+
${fieldDetails}`;
|
|
909
|
+
}
|
|
910
|
+
return msg;
|
|
911
|
+
};
|
|
912
|
+
var upsertCustomerByEmail = async (email, fullName, externalId) => {
|
|
913
|
+
const client2 = getPlainClient();
|
|
914
|
+
const result = await client2.upsertCustomer({
|
|
915
|
+
identifier: { emailAddress: email },
|
|
916
|
+
onCreate: {
|
|
917
|
+
email: { email, isVerified: true },
|
|
918
|
+
fullName: fullName ?? email,
|
|
919
|
+
...externalId ? { externalId } : {}
|
|
920
|
+
},
|
|
921
|
+
onUpdate: {
|
|
922
|
+
...fullName ? { fullName: { value: fullName } } : {},
|
|
923
|
+
...externalId ? { externalId: { value: externalId } } : {}
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
if (result.error) {
|
|
927
|
+
throw new Error(`Failed to upsert customer: ${formatPlainError(result.error)}`);
|
|
928
|
+
}
|
|
929
|
+
return result.data.customer.id;
|
|
930
|
+
};
|
|
931
|
+
var createThread = async (input) => {
|
|
932
|
+
const client2 = getPlainClient();
|
|
933
|
+
const identifierCount = [input.customerEmail, input.customerId, input.customerExternalId].filter(Boolean).length;
|
|
934
|
+
if (identifierCount === 0) {
|
|
935
|
+
throw new Error("Provide at least one of customerEmail, customerId, or customerExternalId");
|
|
936
|
+
}
|
|
937
|
+
if (identifierCount > 1) {
|
|
938
|
+
throw new Error("Provide only one of customerEmail, customerId, or customerExternalId");
|
|
939
|
+
}
|
|
940
|
+
let customerIdentifier;
|
|
941
|
+
if (input.customerEmail) {
|
|
942
|
+
const customerId = await upsertCustomerByEmail(
|
|
943
|
+
input.customerEmail,
|
|
944
|
+
input.customerFullName,
|
|
945
|
+
input.customerExternalId
|
|
946
|
+
);
|
|
947
|
+
customerIdentifier = { customerId };
|
|
948
|
+
} else if (input.customerId) {
|
|
949
|
+
customerIdentifier = { customerId: input.customerId };
|
|
950
|
+
} else {
|
|
951
|
+
customerIdentifier = { externalId: input.customerExternalId };
|
|
952
|
+
}
|
|
953
|
+
const threadFields = [];
|
|
954
|
+
const stringFieldMap = [
|
|
955
|
+
["app", input.app, ThreadFieldSchemaType.String],
|
|
956
|
+
["tenant_id", input.tenantId, ThreadFieldSchemaType.String],
|
|
957
|
+
["stage", input.stage, ThreadFieldSchemaType.String],
|
|
958
|
+
["reported_from", input.reportedFrom, ThreadFieldSchemaType.String],
|
|
959
|
+
["posthog_session", input.posthogSession, ThreadFieldSchemaType.String],
|
|
960
|
+
["sentry_session", input.sentrySession, ThreadFieldSchemaType.String],
|
|
961
|
+
["notion_ticket", input.notionTicket, ThreadFieldSchemaType.String],
|
|
962
|
+
["github_pr", input.githubPr, ThreadFieldSchemaType.String],
|
|
963
|
+
["impact_level", input.impactLevel, ThreadFieldSchemaType.Enum]
|
|
964
|
+
];
|
|
965
|
+
for (const [key, value, type] of stringFieldMap) {
|
|
966
|
+
if (value !== void 0) {
|
|
967
|
+
threadFields.push({ key, stringValue: value, type });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (input.requestFeature !== void 0) {
|
|
971
|
+
threadFields.push({
|
|
972
|
+
key: "request_feature",
|
|
973
|
+
booleanValue: input.requestFeature,
|
|
974
|
+
type: ThreadFieldSchemaType.Bool
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
const result = await client2.createThread({
|
|
978
|
+
customerIdentifier,
|
|
979
|
+
title: input.title ?? void 0,
|
|
980
|
+
description: input.description ?? void 0,
|
|
981
|
+
components: [{ componentText: { text: input.markdown } }],
|
|
982
|
+
...input.priority !== void 0 ? { priority: input.priority } : {},
|
|
983
|
+
...input.labelTypeIds ? { labelTypeIds: input.labelTypeIds } : {},
|
|
984
|
+
...input.externalId ? { externalId: input.externalId } : {},
|
|
985
|
+
...threadFields.length > 0 ? { threadFields } : {}
|
|
986
|
+
});
|
|
987
|
+
if (result.error) {
|
|
988
|
+
throw new Error(`Failed to create thread: ${formatPlainError(result.error)}`);
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
threadId: result.data.id,
|
|
992
|
+
title: result.data.title ?? void 0,
|
|
993
|
+
status: result.data.status,
|
|
994
|
+
priority: result.data.priority
|
|
995
|
+
};
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// src/tools/helpCenter.ts
|
|
999
|
+
var LIST_HELP_CENTERS_QUERY = `
|
|
1000
|
+
query ListHelpCenters($first: Int, $after: String) {
|
|
1001
|
+
helpCenters(first: $first, after: $after) {
|
|
1002
|
+
edges {
|
|
1003
|
+
node {
|
|
1004
|
+
id
|
|
1005
|
+
publicName
|
|
1006
|
+
internalName
|
|
1007
|
+
description
|
|
1008
|
+
type
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
pageInfo {
|
|
1012
|
+
hasNextPage
|
|
1013
|
+
endCursor
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
`;
|
|
1018
|
+
var GET_HELP_CENTER_QUERY = `
|
|
1019
|
+
query GetHelpCenter($helpCenterId: ID!) {
|
|
1020
|
+
helpCenter(id: $helpCenterId) {
|
|
1021
|
+
id
|
|
1022
|
+
publicName
|
|
1023
|
+
internalName
|
|
1024
|
+
description
|
|
1025
|
+
type
|
|
1026
|
+
articles(first: 100) {
|
|
1027
|
+
edges {
|
|
1028
|
+
node {
|
|
1029
|
+
id
|
|
1030
|
+
title
|
|
1031
|
+
slug
|
|
1032
|
+
status
|
|
1033
|
+
description
|
|
1034
|
+
articleGroup {
|
|
1035
|
+
id
|
|
1036
|
+
name
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
articleGroups(first: 50) {
|
|
1042
|
+
edges {
|
|
1043
|
+
node {
|
|
1044
|
+
id
|
|
1045
|
+
name
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
`;
|
|
1052
|
+
var LIST_HELP_CENTER_ARTICLES_QUERY = `
|
|
1053
|
+
query ListHelpCenterArticles($helpCenterId: ID!, $first: Int) {
|
|
1054
|
+
helpCenter(id: $helpCenterId) {
|
|
1055
|
+
articles(first: $first) {
|
|
1056
|
+
edges {
|
|
1057
|
+
node {
|
|
1058
|
+
id
|
|
1059
|
+
title
|
|
1060
|
+
slug
|
|
1061
|
+
status
|
|
1062
|
+
description
|
|
1063
|
+
contentHtml
|
|
1064
|
+
articleGroup {
|
|
1065
|
+
id
|
|
1066
|
+
name
|
|
1067
|
+
}
|
|
1068
|
+
createdAt { iso8601 }
|
|
1069
|
+
updatedAt { iso8601 }
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
`;
|
|
1076
|
+
var GET_HELP_CENTER_ARTICLE_QUERY = `
|
|
1077
|
+
query GetHelpCenterArticle($helpCenterArticleId: ID!) {
|
|
1078
|
+
helpCenterArticle(id: $helpCenterArticleId) {
|
|
1079
|
+
id
|
|
1080
|
+
title
|
|
1081
|
+
slug
|
|
1082
|
+
status
|
|
1083
|
+
description
|
|
1084
|
+
contentHtml
|
|
1085
|
+
articleGroup {
|
|
1086
|
+
id
|
|
1087
|
+
name
|
|
1088
|
+
}
|
|
1089
|
+
createdAt { iso8601 }
|
|
1090
|
+
updatedAt { iso8601 }
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
`;
|
|
1094
|
+
var GET_HELP_CENTER_ARTICLE_BY_SLUG_QUERY = `
|
|
1095
|
+
query GetHelpCenterArticleBySlug($helpCenterId: ID!, $slug: String!) {
|
|
1096
|
+
helpCenterArticleBySlug(helpCenterId: $helpCenterId, slug: $slug) {
|
|
1097
|
+
id
|
|
1098
|
+
title
|
|
1099
|
+
slug
|
|
1100
|
+
status
|
|
1101
|
+
description
|
|
1102
|
+
contentHtml
|
|
1103
|
+
articleGroup {
|
|
1104
|
+
id
|
|
1105
|
+
name
|
|
1106
|
+
}
|
|
1107
|
+
createdAt { iso8601 }
|
|
1108
|
+
updatedAt { iso8601 }
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
`;
|
|
1112
|
+
var UPSERT_HELP_CENTER_ARTICLE_MUTATION = `
|
|
1113
|
+
mutation UpsertHelpCenterArticle($input: UpsertHelpCenterArticleInput!) {
|
|
1114
|
+
upsertHelpCenterArticle(input: $input) {
|
|
1115
|
+
helpCenterArticle {
|
|
1116
|
+
id
|
|
1117
|
+
title
|
|
1118
|
+
slug
|
|
1119
|
+
status
|
|
1120
|
+
description
|
|
1121
|
+
contentHtml
|
|
1122
|
+
articleGroup {
|
|
1123
|
+
id
|
|
1124
|
+
name
|
|
1125
|
+
}
|
|
1126
|
+
createdAt { iso8601 }
|
|
1127
|
+
updatedAt { iso8601 }
|
|
1128
|
+
}
|
|
1129
|
+
error {
|
|
1130
|
+
message
|
|
1131
|
+
type
|
|
1132
|
+
code
|
|
1133
|
+
fields {
|
|
1134
|
+
field
|
|
1135
|
+
message
|
|
1136
|
+
type
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
`;
|
|
1142
|
+
var CREATE_HELP_CENTER_ARTICLE_GROUP_MUTATION = `
|
|
1143
|
+
mutation CreateHelpCenterArticleGroup($input: CreateHelpCenterArticleGroupInput!) {
|
|
1144
|
+
createHelpCenterArticleGroup(input: $input) {
|
|
1145
|
+
helpCenterArticleGroup {
|
|
1146
|
+
id
|
|
1147
|
+
name
|
|
1148
|
+
}
|
|
1149
|
+
error {
|
|
1150
|
+
message
|
|
1151
|
+
type
|
|
1152
|
+
code
|
|
1153
|
+
fields {
|
|
1154
|
+
field
|
|
1155
|
+
message
|
|
1156
|
+
type
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
`;
|
|
1162
|
+
var DELETE_HELP_CENTER_ARTICLE_GROUP_MUTATION = `
|
|
1163
|
+
mutation DeleteHelpCenterArticleGroup($input: DeleteHelpCenterArticleGroupInput!) {
|
|
1164
|
+
deleteHelpCenterArticleGroup(input: $input) {
|
|
1165
|
+
error {
|
|
1166
|
+
message
|
|
1167
|
+
type
|
|
1168
|
+
code
|
|
1169
|
+
fields {
|
|
1170
|
+
field
|
|
1171
|
+
message
|
|
1172
|
+
type
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
`;
|
|
1178
|
+
var MY_WORKSPACE_QUERY = `
|
|
1179
|
+
query MyWorkspace {
|
|
1180
|
+
myWorkspace {
|
|
1181
|
+
id
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
`;
|
|
1185
|
+
var listHelpCenters = async (_input) => {
|
|
1186
|
+
const client2 = getPlainClient();
|
|
1187
|
+
const result = await client2.rawRequest({
|
|
1188
|
+
query: LIST_HELP_CENTERS_QUERY,
|
|
1189
|
+
variables: { first: 25 }
|
|
1190
|
+
});
|
|
1191
|
+
if (result.error) {
|
|
1192
|
+
throw new Error(`Failed to list help centers: ${result.error.message}`);
|
|
1193
|
+
}
|
|
1194
|
+
const data = result.data;
|
|
1195
|
+
return {
|
|
1196
|
+
helpCenters: data.helpCenters.edges.map((edge) => ({
|
|
1197
|
+
id: edge.node.id,
|
|
1198
|
+
publicName: edge.node.publicName,
|
|
1199
|
+
internalName: edge.node.internalName,
|
|
1200
|
+
description: edge.node.description ?? void 0,
|
|
1201
|
+
type: edge.node.type
|
|
1202
|
+
})),
|
|
1203
|
+
pageInfo: data.helpCenters.pageInfo
|
|
1204
|
+
};
|
|
1205
|
+
};
|
|
1206
|
+
var getHelpCenter = async (input) => {
|
|
1207
|
+
const client2 = getPlainClient();
|
|
1208
|
+
const result = await client2.rawRequest({
|
|
1209
|
+
query: GET_HELP_CENTER_QUERY,
|
|
1210
|
+
variables: { helpCenterId: input.helpCenterId }
|
|
1211
|
+
});
|
|
1212
|
+
if (result.error) {
|
|
1213
|
+
throw new Error(`Failed to get help center: ${result.error.message}`);
|
|
1214
|
+
}
|
|
1215
|
+
const data = result.data;
|
|
1216
|
+
const hc = data.helpCenter;
|
|
1217
|
+
if (!hc) {
|
|
1218
|
+
throw new Error(`Help center not found: ${input.helpCenterId}`);
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
id: hc.id,
|
|
1222
|
+
publicName: hc.publicName,
|
|
1223
|
+
internalName: hc.internalName,
|
|
1224
|
+
description: hc.description ?? void 0,
|
|
1225
|
+
type: hc.type,
|
|
1226
|
+
articles: (hc.articles?.edges ?? []).map((edge) => ({
|
|
1227
|
+
id: edge.node.id,
|
|
1228
|
+
title: edge.node.title,
|
|
1229
|
+
slug: edge.node.slug,
|
|
1230
|
+
status: edge.node.status,
|
|
1231
|
+
description: edge.node.description ?? void 0,
|
|
1232
|
+
articleGroup: edge.node.articleGroup ?? void 0
|
|
1233
|
+
})),
|
|
1234
|
+
articleGroups: (hc.articleGroups?.edges ?? []).map((edge) => ({
|
|
1235
|
+
id: edge.node.id,
|
|
1236
|
+
name: edge.node.name
|
|
1237
|
+
}))
|
|
1238
|
+
};
|
|
1239
|
+
};
|
|
1240
|
+
var listHelpCenterArticles = async (input) => {
|
|
1241
|
+
const client2 = getPlainClient();
|
|
1242
|
+
const result = await client2.rawRequest({
|
|
1243
|
+
query: LIST_HELP_CENTER_ARTICLES_QUERY,
|
|
1244
|
+
variables: {
|
|
1245
|
+
helpCenterId: input.helpCenterId,
|
|
1246
|
+
first: input.first ?? 20
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
if (result.error) {
|
|
1250
|
+
throw new Error(`Failed to list help center articles: ${result.error.message}`);
|
|
1251
|
+
}
|
|
1252
|
+
const data = result.data;
|
|
1253
|
+
const hc = data.helpCenter;
|
|
1254
|
+
if (!hc) {
|
|
1255
|
+
throw new Error(`Help center not found: ${input.helpCenterId}`);
|
|
1256
|
+
}
|
|
1257
|
+
return {
|
|
1258
|
+
articles: (hc.articles?.edges ?? []).map((edge) => {
|
|
1259
|
+
const node = edge.node;
|
|
1260
|
+
return {
|
|
1261
|
+
id: node.id,
|
|
1262
|
+
title: node.title,
|
|
1263
|
+
slug: node.slug,
|
|
1264
|
+
status: node.status,
|
|
1265
|
+
description: node.description ?? void 0,
|
|
1266
|
+
contentHtml: node.contentHtml,
|
|
1267
|
+
articleGroup: node.articleGroup ?? void 0,
|
|
1268
|
+
createdAt: node.createdAt.iso8601,
|
|
1269
|
+
updatedAt: node.updatedAt.iso8601
|
|
1270
|
+
};
|
|
1271
|
+
})
|
|
1272
|
+
};
|
|
1273
|
+
};
|
|
1274
|
+
var getHelpCenterArticle = async (input) => {
|
|
1275
|
+
const client2 = getPlainClient();
|
|
1276
|
+
const result = await client2.rawRequest({
|
|
1277
|
+
query: GET_HELP_CENTER_ARTICLE_QUERY,
|
|
1278
|
+
variables: { helpCenterArticleId: input.helpCenterArticleId }
|
|
1279
|
+
});
|
|
1280
|
+
if (result.error) {
|
|
1281
|
+
throw new Error(`Failed to get help center article: ${result.error.message}`);
|
|
1282
|
+
}
|
|
1283
|
+
const data = result.data;
|
|
1284
|
+
const article = data.helpCenterArticle;
|
|
1285
|
+
if (!article) {
|
|
1286
|
+
throw new Error(`Help center article not found: ${input.helpCenterArticleId}`);
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
id: article.id,
|
|
1290
|
+
title: article.title,
|
|
1291
|
+
slug: article.slug,
|
|
1292
|
+
status: article.status,
|
|
1293
|
+
description: article.description ?? void 0,
|
|
1294
|
+
contentHtml: article.contentHtml,
|
|
1295
|
+
articleGroup: article.articleGroup ?? void 0,
|
|
1296
|
+
createdAt: article.createdAt.iso8601,
|
|
1297
|
+
updatedAt: article.updatedAt.iso8601
|
|
1298
|
+
};
|
|
1299
|
+
};
|
|
1300
|
+
var getHelpCenterArticleBySlug = async (input) => {
|
|
1301
|
+
const client2 = getPlainClient();
|
|
1302
|
+
const result = await client2.rawRequest({
|
|
1303
|
+
query: GET_HELP_CENTER_ARTICLE_BY_SLUG_QUERY,
|
|
1304
|
+
variables: {
|
|
1305
|
+
helpCenterId: input.helpCenterId,
|
|
1306
|
+
slug: input.slug
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
if (result.error) {
|
|
1310
|
+
throw new Error(`Failed to get help center article by slug: ${result.error.message}`);
|
|
1311
|
+
}
|
|
1312
|
+
const data = result.data;
|
|
1313
|
+
const article = data.helpCenterArticleBySlug;
|
|
1314
|
+
if (!article) {
|
|
1315
|
+
throw new Error(`Help center article not found with slug: ${input.slug}`);
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
id: article.id,
|
|
1319
|
+
title: article.title,
|
|
1320
|
+
slug: article.slug,
|
|
1321
|
+
status: article.status,
|
|
1322
|
+
description: article.description ?? void 0,
|
|
1323
|
+
contentHtml: article.contentHtml,
|
|
1324
|
+
articleGroup: article.articleGroup ?? void 0,
|
|
1325
|
+
createdAt: article.createdAt.iso8601,
|
|
1326
|
+
updatedAt: article.updatedAt.iso8601
|
|
1327
|
+
};
|
|
1328
|
+
};
|
|
1329
|
+
var upsertHelpCenterArticle = async (input) => {
|
|
1330
|
+
const client2 = getPlainClient();
|
|
1331
|
+
const variables = {
|
|
1332
|
+
helpCenterId: input.helpCenterId,
|
|
1333
|
+
title: input.title,
|
|
1334
|
+
contentHtml: input.contentHtml,
|
|
1335
|
+
description: input.description,
|
|
1336
|
+
status: "DRAFT"
|
|
1337
|
+
// Always force DRAFT
|
|
1338
|
+
};
|
|
1339
|
+
if (input.helpCenterArticleId) {
|
|
1340
|
+
variables.helpCenterArticleId = input.helpCenterArticleId;
|
|
1341
|
+
}
|
|
1342
|
+
if (input.slug !== void 0) {
|
|
1343
|
+
variables.slug = input.slug;
|
|
1344
|
+
}
|
|
1345
|
+
if (input.helpCenterArticleGroupId !== void 0 && !input.helpCenterArticleId) {
|
|
1346
|
+
variables.helpCenterArticleGroupId = input.helpCenterArticleGroupId;
|
|
1347
|
+
}
|
|
1348
|
+
const result = await client2.rawRequest({
|
|
1349
|
+
query: UPSERT_HELP_CENTER_ARTICLE_MUTATION,
|
|
1350
|
+
variables: { input: variables }
|
|
1351
|
+
});
|
|
1352
|
+
if (result.error) {
|
|
1353
|
+
throw new Error(`Failed to upsert help center article: ${result.error.message}`);
|
|
1354
|
+
}
|
|
1355
|
+
const data = result.data;
|
|
1356
|
+
const mutationResult = data.upsertHelpCenterArticle;
|
|
1357
|
+
if (mutationResult.error) {
|
|
1358
|
+
const fieldErrors = mutationResult.error.fields?.map((f) => `${f.field}: ${f.message}`).join(", ");
|
|
1359
|
+
throw new Error(
|
|
1360
|
+
`Failed to upsert article: ${mutationResult.error.message}${fieldErrors ? ` (${fieldErrors})` : ""}`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
const article = mutationResult.helpCenterArticle;
|
|
1364
|
+
const workspaceResult = await client2.rawRequest({
|
|
1365
|
+
query: MY_WORKSPACE_QUERY,
|
|
1366
|
+
variables: {}
|
|
1367
|
+
});
|
|
1368
|
+
let link;
|
|
1369
|
+
if (!workspaceResult.error) {
|
|
1370
|
+
const wsData = workspaceResult.data;
|
|
1371
|
+
const workspaceId = wsData.myWorkspace.id;
|
|
1372
|
+
link = `https://app.plain.com/workspace/${workspaceId}/help-center/${input.helpCenterId}/articles/${article.id}/`;
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
id: article.id,
|
|
1376
|
+
title: article.title,
|
|
1377
|
+
slug: article.slug,
|
|
1378
|
+
status: article.status,
|
|
1379
|
+
description: article.description ?? void 0,
|
|
1380
|
+
contentHtml: article.contentHtml,
|
|
1381
|
+
articleGroup: article.articleGroup ?? void 0,
|
|
1382
|
+
createdAt: article.createdAt.iso8601,
|
|
1383
|
+
updatedAt: article.updatedAt.iso8601,
|
|
1384
|
+
...link ? { link } : {}
|
|
1385
|
+
};
|
|
1386
|
+
};
|
|
1387
|
+
var createHelpCenterArticleGroup = async (input) => {
|
|
1388
|
+
const client2 = getPlainClient();
|
|
1389
|
+
const variables = {
|
|
1390
|
+
helpCenterId: input.helpCenterId,
|
|
1391
|
+
name: input.name
|
|
1392
|
+
};
|
|
1393
|
+
if (input.parentHelpCenterArticleGroupId !== void 0) {
|
|
1394
|
+
variables.parentHelpCenterArticleGroupId = input.parentHelpCenterArticleGroupId;
|
|
1395
|
+
}
|
|
1396
|
+
const result = await client2.rawRequest({
|
|
1397
|
+
query: CREATE_HELP_CENTER_ARTICLE_GROUP_MUTATION,
|
|
1398
|
+
variables: { input: variables }
|
|
1399
|
+
});
|
|
1400
|
+
if (result.error) {
|
|
1401
|
+
throw new Error(`Failed to create article group: ${result.error.message}`);
|
|
1402
|
+
}
|
|
1403
|
+
const data = result.data;
|
|
1404
|
+
const mutationResult = data.createHelpCenterArticleGroup;
|
|
1405
|
+
if (mutationResult.error) {
|
|
1406
|
+
const fieldErrors = mutationResult.error.fields?.map((f) => `${f.field}: ${f.message}`).join(", ");
|
|
1407
|
+
throw new Error(
|
|
1408
|
+
`Failed to create article group: ${mutationResult.error.message}${fieldErrors ? ` (${fieldErrors})` : ""}`
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
id: mutationResult.helpCenterArticleGroup.id,
|
|
1413
|
+
name: mutationResult.helpCenterArticleGroup.name
|
|
1414
|
+
};
|
|
1415
|
+
};
|
|
1416
|
+
var deleteHelpCenterArticleGroup = async (input) => {
|
|
1417
|
+
const client2 = getPlainClient();
|
|
1418
|
+
const result = await client2.rawRequest({
|
|
1419
|
+
query: DELETE_HELP_CENTER_ARTICLE_GROUP_MUTATION,
|
|
1420
|
+
variables: {
|
|
1421
|
+
input: { helpCenterArticleGroupId: input.helpCenterArticleGroupId }
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
if (result.error) {
|
|
1425
|
+
throw new Error(`Failed to delete article group: ${result.error.message}`);
|
|
1426
|
+
}
|
|
1427
|
+
const data = result.data;
|
|
1428
|
+
const mutationResult = data.deleteHelpCenterArticleGroup;
|
|
1429
|
+
if (mutationResult.error) {
|
|
1430
|
+
throw new Error(`Failed to delete article group: ${mutationResult.error.message}`);
|
|
1431
|
+
}
|
|
1432
|
+
return { success: true };
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
// src/index.ts
|
|
1436
|
+
if (!process.env.PLAIN_API_KEY) {
|
|
1437
|
+
console.error("Error: PLAIN_API_KEY environment variable is required.");
|
|
1438
|
+
console.error("Set it in your MCP server configuration or shell environment.");
|
|
1439
|
+
process.exit(1);
|
|
1440
|
+
}
|
|
1441
|
+
var server = new Server(
|
|
1442
|
+
{
|
|
1443
|
+
name: "mcp-plain",
|
|
1444
|
+
version: "0.1.0"
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
capabilities: {
|
|
1448
|
+
tools: {}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
);
|
|
1452
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1453
|
+
tools: [
|
|
1454
|
+
{
|
|
1455
|
+
name: "list_threads",
|
|
1456
|
+
description: "List support threads from Plain with optional filters. Returns thread summaries including id, title, status, priority, customer info, labels, and timestamps.",
|
|
1457
|
+
inputSchema: {
|
|
1458
|
+
type: "object",
|
|
1459
|
+
properties: {
|
|
1460
|
+
statuses: {
|
|
1461
|
+
type: "array",
|
|
1462
|
+
items: { type: "string" },
|
|
1463
|
+
description: "Filter by thread statuses (e.g., 'TODO', 'DONE', 'SNOOZED')"
|
|
1464
|
+
},
|
|
1465
|
+
priority: {
|
|
1466
|
+
type: "number",
|
|
1467
|
+
minimum: 0,
|
|
1468
|
+
maximum: 3,
|
|
1469
|
+
description: "Filter by priority (0 = urgent, 3 = low)"
|
|
1470
|
+
},
|
|
1471
|
+
labelTypeIds: {
|
|
1472
|
+
type: "array",
|
|
1473
|
+
items: { type: "string" },
|
|
1474
|
+
description: "Filter by label type IDs"
|
|
1475
|
+
},
|
|
1476
|
+
customerId: {
|
|
1477
|
+
type: "string",
|
|
1478
|
+
description: "Filter by customer ID"
|
|
1479
|
+
},
|
|
1480
|
+
tenantId: {
|
|
1481
|
+
type: "string",
|
|
1482
|
+
description: "Filter by tenant ID (via thread field)"
|
|
1483
|
+
},
|
|
1484
|
+
first: {
|
|
1485
|
+
type: "number",
|
|
1486
|
+
minimum: 1,
|
|
1487
|
+
maximum: 100,
|
|
1488
|
+
default: 50,
|
|
1489
|
+
description: "Number of threads to return (default 50, max 100)"
|
|
1490
|
+
},
|
|
1491
|
+
after: {
|
|
1492
|
+
type: "string",
|
|
1493
|
+
description: "Cursor for pagination"
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
},
|
|
1498
|
+
{
|
|
1499
|
+
name: "get_thread",
|
|
1500
|
+
description: "Get full details of a specific thread including basic info, customer, labels, custom fields, and timeline entries.",
|
|
1501
|
+
inputSchema: {
|
|
1502
|
+
type: "object",
|
|
1503
|
+
properties: {
|
|
1504
|
+
threadId: {
|
|
1505
|
+
type: "string",
|
|
1506
|
+
description: "The ID of the thread to retrieve"
|
|
1507
|
+
}
|
|
1508
|
+
},
|
|
1509
|
+
required: ["threadId"]
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
1512
|
+
{
|
|
1513
|
+
name: "get_thread_by_ref",
|
|
1514
|
+
description: "Get full details of a thread by its reference number (e.g., T-510). Returns the same details as get_thread.",
|
|
1515
|
+
inputSchema: {
|
|
1516
|
+
type: "object",
|
|
1517
|
+
properties: {
|
|
1518
|
+
ref: {
|
|
1519
|
+
type: "string",
|
|
1520
|
+
description: "Thread reference number (e.g., 'T-510')"
|
|
1521
|
+
}
|
|
1522
|
+
},
|
|
1523
|
+
required: ["ref"]
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
name: "get_thread_fields",
|
|
1528
|
+
description: "Get just the custom field values for a thread. Returns fields like impactLevel, posthogSession, sentrySession, tenant, etc.",
|
|
1529
|
+
inputSchema: {
|
|
1530
|
+
type: "object",
|
|
1531
|
+
properties: {
|
|
1532
|
+
threadId: {
|
|
1533
|
+
type: "string",
|
|
1534
|
+
description: "The ID of the thread to get fields for"
|
|
1535
|
+
}
|
|
1536
|
+
},
|
|
1537
|
+
required: ["threadId"]
|
|
1538
|
+
}
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
name: "get_attachment_download_url",
|
|
1542
|
+
description: "Get a temporary download URL for an attachment. Use this to download attachment content when needed. The URL expires after a short time.",
|
|
1543
|
+
inputSchema: {
|
|
1544
|
+
type: "object",
|
|
1545
|
+
properties: {
|
|
1546
|
+
attachmentId: {
|
|
1547
|
+
type: "string",
|
|
1548
|
+
description: "The ID of the attachment to get a download URL for"
|
|
1549
|
+
}
|
|
1550
|
+
},
|
|
1551
|
+
required: ["attachmentId"]
|
|
1552
|
+
}
|
|
1553
|
+
},
|
|
1554
|
+
{
|
|
1555
|
+
name: "get_attachment_content",
|
|
1556
|
+
description: "Fetch the content of an attachment. Returns the content as text for text-based files (text/*, JSON, XML, etc.) or as base64-encoded string for binary files (images, PDFs, etc.). The response includes the encoding type ('text' or 'base64') and MIME type.",
|
|
1557
|
+
inputSchema: {
|
|
1558
|
+
type: "object",
|
|
1559
|
+
properties: {
|
|
1560
|
+
attachmentId: {
|
|
1561
|
+
type: "string",
|
|
1562
|
+
description: "The ID of the attachment to fetch content for"
|
|
1563
|
+
}
|
|
1564
|
+
},
|
|
1565
|
+
required: ["attachmentId"]
|
|
1566
|
+
}
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
name: "reply_to_thread",
|
|
1570
|
+
description: "Send a reply to a customer thread. The reply is sent through the original channel (email, Slack, chat). Provide textContent (required) and optionally markdownContent for rich formatting.",
|
|
1571
|
+
inputSchema: {
|
|
1572
|
+
type: "object",
|
|
1573
|
+
properties: {
|
|
1574
|
+
threadId: {
|
|
1575
|
+
type: "string",
|
|
1576
|
+
description: "The ID of the thread to reply to"
|
|
1577
|
+
},
|
|
1578
|
+
textContent: {
|
|
1579
|
+
type: "string",
|
|
1580
|
+
description: "Plain text content of the reply"
|
|
1581
|
+
},
|
|
1582
|
+
markdownContent: {
|
|
1583
|
+
type: "string",
|
|
1584
|
+
description: "Optional markdown-formatted content of the reply"
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1587
|
+
required: ["threadId", "textContent"]
|
|
1588
|
+
}
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
name: "mark_thread_as_done",
|
|
1592
|
+
description: "Mark a thread as done/resolved. Use this after replying to a thread when the issue has been resolved.",
|
|
1593
|
+
inputSchema: {
|
|
1594
|
+
type: "object",
|
|
1595
|
+
properties: {
|
|
1596
|
+
threadId: {
|
|
1597
|
+
type: "string",
|
|
1598
|
+
description: "The ID of the thread to mark as done"
|
|
1599
|
+
}
|
|
1600
|
+
},
|
|
1601
|
+
required: ["threadId"]
|
|
1602
|
+
}
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
name: "upsert_thread_field",
|
|
1606
|
+
description: "Set or update a custom field value on a thread. Use this to write investigation results back to the ticket (e.g., impact_level, app, stage). Field keys use Plain's snake_case format.",
|
|
1607
|
+
inputSchema: {
|
|
1608
|
+
type: "object",
|
|
1609
|
+
properties: {
|
|
1610
|
+
threadId: {
|
|
1611
|
+
type: "string",
|
|
1612
|
+
description: "The ID of the thread to update"
|
|
1613
|
+
},
|
|
1614
|
+
key: {
|
|
1615
|
+
type: "string",
|
|
1616
|
+
description: "Custom field key in snake_case, e.g. impact_level, app, stage, tenant_id, notion_ticket, github_pr, posthog_session, sentry_session, reported_from, request_feature"
|
|
1617
|
+
},
|
|
1618
|
+
value: {
|
|
1619
|
+
type: "string",
|
|
1620
|
+
description: "Field value to set. For boolean fields like request_feature, use 'true' or 'false'"
|
|
1621
|
+
}
|
|
1622
|
+
},
|
|
1623
|
+
required: ["threadId", "key", "value"]
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
name: "add_internal_note",
|
|
1628
|
+
description: "Post an internal note on a thread. Internal notes are visible to the support team only and are NOT sent to the customer. Use this to post investigation summaries, findings, and recommendations.",
|
|
1629
|
+
inputSchema: {
|
|
1630
|
+
type: "object",
|
|
1631
|
+
properties: {
|
|
1632
|
+
threadId: {
|
|
1633
|
+
type: "string",
|
|
1634
|
+
description: "The ID of the thread to add a note to"
|
|
1635
|
+
},
|
|
1636
|
+
markdown: {
|
|
1637
|
+
type: "string",
|
|
1638
|
+
description: "Markdown content for the internal note"
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
required: ["threadId", "markdown"]
|
|
1642
|
+
}
|
|
1643
|
+
},
|
|
1644
|
+
{
|
|
1645
|
+
name: "add_labels",
|
|
1646
|
+
description: "Add category labels to a thread. Use get_label_types first to discover available label type IDs for bug, support, feature-request, sales categories.",
|
|
1647
|
+
inputSchema: {
|
|
1648
|
+
type: "object",
|
|
1649
|
+
properties: {
|
|
1650
|
+
threadId: {
|
|
1651
|
+
type: "string",
|
|
1652
|
+
description: "The ID of the thread to add labels to"
|
|
1653
|
+
},
|
|
1654
|
+
labelTypeIds: {
|
|
1655
|
+
type: "array",
|
|
1656
|
+
items: { type: "string" },
|
|
1657
|
+
description: "Array of label type IDs to add (get IDs from get_label_types)"
|
|
1658
|
+
}
|
|
1659
|
+
},
|
|
1660
|
+
required: ["threadId", "labelTypeIds"]
|
|
1661
|
+
}
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
name: "get_label_types",
|
|
1665
|
+
description: "List available label types in the Plain workspace. Returns label type IDs and names. Call this once to discover the IDs for category labels (bug, support, feature-request, sales) before using add_labels.",
|
|
1666
|
+
inputSchema: {
|
|
1667
|
+
type: "object",
|
|
1668
|
+
properties: {
|
|
1669
|
+
first: {
|
|
1670
|
+
type: "number",
|
|
1671
|
+
minimum: 1,
|
|
1672
|
+
maximum: 100,
|
|
1673
|
+
default: 50,
|
|
1674
|
+
description: "Number of label types to return (default 50)"
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
name: "create_thread",
|
|
1681
|
+
description: "Create a new support thread for a customer. Provide exactly one customer identifier (email, ID, or external ID) and markdown content for the initial message. When using customerEmail, the customer is automatically created in Plain if they don't exist. Optionally set title, description, priority, labels, custom fields, and external ID.",
|
|
1682
|
+
inputSchema: {
|
|
1683
|
+
type: "object",
|
|
1684
|
+
properties: {
|
|
1685
|
+
customerEmail: {
|
|
1686
|
+
type: "string",
|
|
1687
|
+
description: "Customer email address. Provide either customerEmail, customerId, or customerExternalId."
|
|
1688
|
+
},
|
|
1689
|
+
customerId: {
|
|
1690
|
+
type: "string",
|
|
1691
|
+
description: "Existing Plain customer ID. Provide either customerEmail, customerId, or customerExternalId."
|
|
1692
|
+
},
|
|
1693
|
+
customerExternalId: {
|
|
1694
|
+
type: "string",
|
|
1695
|
+
description: "Customer external ID. Provide either customerEmail, customerId, or customerExternalId."
|
|
1696
|
+
},
|
|
1697
|
+
customerFullName: {
|
|
1698
|
+
type: "string",
|
|
1699
|
+
description: "Customer full name. Used when upserting a customer by email (ignored when customerId is provided)."
|
|
1700
|
+
},
|
|
1701
|
+
title: {
|
|
1702
|
+
type: "string",
|
|
1703
|
+
description: "Thread title"
|
|
1704
|
+
},
|
|
1705
|
+
description: {
|
|
1706
|
+
type: "string",
|
|
1707
|
+
description: "Thread description / preview text"
|
|
1708
|
+
},
|
|
1709
|
+
markdown: {
|
|
1710
|
+
type: "string",
|
|
1711
|
+
description: "Markdown content for the first timeline entry in the thread"
|
|
1712
|
+
},
|
|
1713
|
+
priority: {
|
|
1714
|
+
type: "number",
|
|
1715
|
+
minimum: 0,
|
|
1716
|
+
maximum: 3,
|
|
1717
|
+
description: "Priority: 0 = urgent, 1 = high, 2 = normal (default), 3 = low"
|
|
1718
|
+
},
|
|
1719
|
+
labelTypeIds: {
|
|
1720
|
+
type: "array",
|
|
1721
|
+
items: { type: "string" },
|
|
1722
|
+
description: "Label type IDs to attach. Use get_label_types to discover available IDs."
|
|
1723
|
+
},
|
|
1724
|
+
externalId: {
|
|
1725
|
+
type: "string",
|
|
1726
|
+
description: "Your own unique identifier for this thread"
|
|
1727
|
+
},
|
|
1728
|
+
impactLevel: {
|
|
1729
|
+
type: "string",
|
|
1730
|
+
enum: ["P0", "P1", "P2", "RBS", "RBP", "not-a-bug"],
|
|
1731
|
+
description: "Impact level: P0 (critical), P1, P2, RBS (release blocker staging), RBP (release blocker production), not-a-bug"
|
|
1732
|
+
},
|
|
1733
|
+
app: {
|
|
1734
|
+
type: "string",
|
|
1735
|
+
description: "Affected app identifier (e.g. 'web', 'mobile', 'api')"
|
|
1736
|
+
},
|
|
1737
|
+
tenantId: {
|
|
1738
|
+
type: "string",
|
|
1739
|
+
description: "Tenant ID to associate with the thread"
|
|
1740
|
+
},
|
|
1741
|
+
stage: {
|
|
1742
|
+
type: "string",
|
|
1743
|
+
description: "Environment stage (e.g. 'production', 'staging', 'develop')"
|
|
1744
|
+
},
|
|
1745
|
+
reportedFrom: {
|
|
1746
|
+
type: "string",
|
|
1747
|
+
description: "URL where the issue was reported from"
|
|
1748
|
+
},
|
|
1749
|
+
posthogSession: {
|
|
1750
|
+
type: "string",
|
|
1751
|
+
description: "PostHog session replay URL or session recording ID"
|
|
1752
|
+
},
|
|
1753
|
+
sentrySession: {
|
|
1754
|
+
type: "string",
|
|
1755
|
+
description: "Sentry replay URL or replay ID"
|
|
1756
|
+
},
|
|
1757
|
+
notionTicket: {
|
|
1758
|
+
type: "string",
|
|
1759
|
+
description: "Notion ticket URL or ID"
|
|
1760
|
+
},
|
|
1761
|
+
githubPr: {
|
|
1762
|
+
type: "string",
|
|
1763
|
+
description: "GitHub PR URL"
|
|
1764
|
+
},
|
|
1765
|
+
requestFeature: {
|
|
1766
|
+
type: "boolean",
|
|
1767
|
+
description: "Whether this is a feature request"
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
required: ["markdown"]
|
|
1771
|
+
}
|
|
1772
|
+
},
|
|
1773
|
+
// --- Help Center tools ---
|
|
1774
|
+
{
|
|
1775
|
+
name: "list_help_centers",
|
|
1776
|
+
description: "List all help centers in the Plain workspace. Returns help center IDs, names, descriptions, and types. Use this to discover help center IDs before using other help center tools.",
|
|
1777
|
+
inputSchema: {
|
|
1778
|
+
type: "object",
|
|
1779
|
+
properties: {}
|
|
1780
|
+
}
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
name: "get_help_center",
|
|
1784
|
+
description: "Get a help center by ID with an overview of its article groups and articles (titles, slugs, statuses). Use this to browse the structure of a help center.",
|
|
1785
|
+
inputSchema: {
|
|
1786
|
+
type: "object",
|
|
1787
|
+
properties: {
|
|
1788
|
+
helpCenterId: {
|
|
1789
|
+
type: "string",
|
|
1790
|
+
description: "The ID of the help center to retrieve"
|
|
1791
|
+
}
|
|
1792
|
+
},
|
|
1793
|
+
required: ["helpCenterId"]
|
|
1794
|
+
}
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
name: "list_help_center_articles",
|
|
1798
|
+
description: "List articles in a help center with full content (contentHtml). Use this to read all articles at once for review or analysis.",
|
|
1799
|
+
inputSchema: {
|
|
1800
|
+
type: "object",
|
|
1801
|
+
properties: {
|
|
1802
|
+
helpCenterId: {
|
|
1803
|
+
type: "string",
|
|
1804
|
+
description: "The ID of the help center to list articles from"
|
|
1805
|
+
},
|
|
1806
|
+
first: {
|
|
1807
|
+
type: "number",
|
|
1808
|
+
minimum: 1,
|
|
1809
|
+
maximum: 100,
|
|
1810
|
+
default: 20,
|
|
1811
|
+
description: "Number of articles to return (default 20, max 100)"
|
|
1812
|
+
}
|
|
1813
|
+
},
|
|
1814
|
+
required: ["helpCenterId"]
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
name: "get_help_center_article",
|
|
1819
|
+
description: "Get a single help center article by ID including its full HTML content, metadata, and group assignment.",
|
|
1820
|
+
inputSchema: {
|
|
1821
|
+
type: "object",
|
|
1822
|
+
properties: {
|
|
1823
|
+
helpCenterArticleId: {
|
|
1824
|
+
type: "string",
|
|
1825
|
+
description: "The ID of the help center article to retrieve"
|
|
1826
|
+
}
|
|
1827
|
+
},
|
|
1828
|
+
required: ["helpCenterArticleId"]
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
name: "get_help_center_article_by_slug",
|
|
1833
|
+
description: "Get a single help center article by its URL slug. Useful when you know the slug from a URL but not the article ID.",
|
|
1834
|
+
inputSchema: {
|
|
1835
|
+
type: "object",
|
|
1836
|
+
properties: {
|
|
1837
|
+
helpCenterId: {
|
|
1838
|
+
type: "string",
|
|
1839
|
+
description: "The ID of the help center the article belongs to"
|
|
1840
|
+
},
|
|
1841
|
+
slug: {
|
|
1842
|
+
type: "string",
|
|
1843
|
+
description: "The URL slug of the article"
|
|
1844
|
+
}
|
|
1845
|
+
},
|
|
1846
|
+
required: ["helpCenterId", "slug"]
|
|
1847
|
+
}
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
name: "upsert_help_center_article",
|
|
1851
|
+
description: "Create or update a help center article. Articles are always saved as DRAFT status. To update an existing article, provide helpCenterArticleId. Content must be HTML (not markdown). Returns the article data and a link to edit it in the Plain UI.\n\nIMPORTANT: When updating an existing article, you MUST preserve the original HTML formatting exactly. Copy the existing contentHtml verbatim and only modify the specific parts that need changing. Do NOT reformat, re-indent, restructure tags, collapse whitespace, change tag styles, or rewrite any HTML that isn't part of your intended edit. Treat the HTML as a surgical edit, not a rewrite.",
|
|
1852
|
+
inputSchema: {
|
|
1853
|
+
type: "object",
|
|
1854
|
+
properties: {
|
|
1855
|
+
helpCenterId: {
|
|
1856
|
+
type: "string",
|
|
1857
|
+
description: "The ID of the help center to create/update the article in"
|
|
1858
|
+
},
|
|
1859
|
+
title: {
|
|
1860
|
+
type: "string",
|
|
1861
|
+
description: "Article title"
|
|
1862
|
+
},
|
|
1863
|
+
contentHtml: {
|
|
1864
|
+
type: "string",
|
|
1865
|
+
description: "Article content as HTML (not markdown)"
|
|
1866
|
+
},
|
|
1867
|
+
helpCenterArticleId: {
|
|
1868
|
+
type: "string",
|
|
1869
|
+
description: "Existing article ID for updates. Omit to create a new article."
|
|
1870
|
+
},
|
|
1871
|
+
description: {
|
|
1872
|
+
type: "string",
|
|
1873
|
+
description: "Short description / summary of the article"
|
|
1874
|
+
},
|
|
1875
|
+
slug: {
|
|
1876
|
+
type: "string",
|
|
1877
|
+
description: "URL slug for the article"
|
|
1878
|
+
},
|
|
1879
|
+
helpCenterArticleGroupId: {
|
|
1880
|
+
type: "string",
|
|
1881
|
+
description: "Article group ID to place the article in"
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
required: ["helpCenterId", "title", "contentHtml", "description"]
|
|
1885
|
+
}
|
|
1886
|
+
},
|
|
1887
|
+
{
|
|
1888
|
+
name: "create_help_center_article_group",
|
|
1889
|
+
description: "Create a new article group (category/folder) in a help center. Groups organize articles and can be nested.",
|
|
1890
|
+
inputSchema: {
|
|
1891
|
+
type: "object",
|
|
1892
|
+
properties: {
|
|
1893
|
+
helpCenterId: {
|
|
1894
|
+
type: "string",
|
|
1895
|
+
description: "The ID of the help center to create the group in"
|
|
1896
|
+
},
|
|
1897
|
+
name: {
|
|
1898
|
+
type: "string",
|
|
1899
|
+
description: "Name of the article group"
|
|
1900
|
+
},
|
|
1901
|
+
parentHelpCenterArticleGroupId: {
|
|
1902
|
+
type: "string",
|
|
1903
|
+
description: "Parent group ID for nested groups"
|
|
1904
|
+
}
|
|
1905
|
+
},
|
|
1906
|
+
required: ["helpCenterId", "name"]
|
|
1907
|
+
}
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
name: "delete_help_center_article_group",
|
|
1911
|
+
description: "Delete an article group from a help center. The group must be empty (no articles or child groups).",
|
|
1912
|
+
inputSchema: {
|
|
1913
|
+
type: "object",
|
|
1914
|
+
properties: {
|
|
1915
|
+
helpCenterArticleGroupId: {
|
|
1916
|
+
type: "string",
|
|
1917
|
+
description: "The ID of the article group to delete"
|
|
1918
|
+
}
|
|
1919
|
+
},
|
|
1920
|
+
required: ["helpCenterArticleGroupId"]
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
]
|
|
1924
|
+
}));
|
|
1925
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1926
|
+
const { name, arguments: args } = request.params;
|
|
1927
|
+
try {
|
|
1928
|
+
switch (name) {
|
|
1929
|
+
case "list_threads": {
|
|
1930
|
+
const input = listThreadsInputSchema.parse(args);
|
|
1931
|
+
const result = await listThreads(input);
|
|
1932
|
+
return {
|
|
1933
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
case "get_thread": {
|
|
1937
|
+
const input = getThreadInputSchema.parse(args);
|
|
1938
|
+
const result = await getThread(input);
|
|
1939
|
+
return {
|
|
1940
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
case "get_thread_by_ref": {
|
|
1944
|
+
const input = getThreadByRefInputSchema.parse(args);
|
|
1945
|
+
const result = await getThreadByRef(input);
|
|
1946
|
+
return {
|
|
1947
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
case "get_thread_fields": {
|
|
1951
|
+
const input = getThreadFieldsInputSchema.parse(args);
|
|
1952
|
+
const result = await getThreadFields(input);
|
|
1953
|
+
return {
|
|
1954
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
case "get_attachment_download_url": {
|
|
1958
|
+
const input = getAttachmentDownloadUrlInputSchema.parse(args);
|
|
1959
|
+
const result = await getAttachmentDownloadUrl(input);
|
|
1960
|
+
return {
|
|
1961
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
case "get_attachment_content": {
|
|
1965
|
+
const input = getAttachmentContentInputSchema.parse(args);
|
|
1966
|
+
const result = await getAttachmentContent(input);
|
|
1967
|
+
return {
|
|
1968
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
case "reply_to_thread": {
|
|
1972
|
+
const input = replyToThreadInputSchema.parse(args);
|
|
1973
|
+
const result = await replyToThread(input);
|
|
1974
|
+
return {
|
|
1975
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
case "mark_thread_as_done": {
|
|
1979
|
+
const input = markThreadAsDoneInputSchema.parse(args);
|
|
1980
|
+
const result = await markThreadAsDone(input);
|
|
1981
|
+
return {
|
|
1982
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
case "upsert_thread_field": {
|
|
1986
|
+
const input = upsertThreadFieldInputSchema.parse(args);
|
|
1987
|
+
const result = await upsertThreadField(input);
|
|
1988
|
+
return {
|
|
1989
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
case "add_internal_note": {
|
|
1993
|
+
const input = addInternalNoteInputSchema.parse(args);
|
|
1994
|
+
const result = await addInternalNote(input);
|
|
1995
|
+
return {
|
|
1996
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
case "add_labels": {
|
|
2000
|
+
const input = addLabelsInputSchema.parse(args);
|
|
2001
|
+
const result = await addLabelsToThread(input);
|
|
2002
|
+
return {
|
|
2003
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
case "get_label_types": {
|
|
2007
|
+
const input = getLabelTypesInputSchema.parse(args);
|
|
2008
|
+
const result = await getLabelTypes(input);
|
|
2009
|
+
return {
|
|
2010
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
case "create_thread": {
|
|
2014
|
+
const input = createThreadInputSchema.parse(args);
|
|
2015
|
+
const result = await createThread(input);
|
|
2016
|
+
return {
|
|
2017
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
// Help Center tools
|
|
2021
|
+
case "list_help_centers": {
|
|
2022
|
+
const input = listHelpCentersInputSchema.parse(args);
|
|
2023
|
+
const result = await listHelpCenters(input);
|
|
2024
|
+
return {
|
|
2025
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
case "get_help_center": {
|
|
2029
|
+
const input = getHelpCenterInputSchema.parse(args);
|
|
2030
|
+
const result = await getHelpCenter(input);
|
|
2031
|
+
return {
|
|
2032
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
case "list_help_center_articles": {
|
|
2036
|
+
const input = listHelpCenterArticlesInputSchema.parse(args);
|
|
2037
|
+
const result = await listHelpCenterArticles(input);
|
|
2038
|
+
return {
|
|
2039
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
case "get_help_center_article": {
|
|
2043
|
+
const input = getHelpCenterArticleInputSchema.parse(args);
|
|
2044
|
+
const result = await getHelpCenterArticle(input);
|
|
2045
|
+
return {
|
|
2046
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
case "get_help_center_article_by_slug": {
|
|
2050
|
+
const input = getHelpCenterArticleBySlugInputSchema.parse(args);
|
|
2051
|
+
const result = await getHelpCenterArticleBySlug(input);
|
|
2052
|
+
return {
|
|
2053
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
case "upsert_help_center_article": {
|
|
2057
|
+
const input = upsertHelpCenterArticleInputSchema.parse(args);
|
|
2058
|
+
const result = await upsertHelpCenterArticle(input);
|
|
2059
|
+
return {
|
|
2060
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
case "create_help_center_article_group": {
|
|
2064
|
+
const input = createHelpCenterArticleGroupInputSchema.parse(args);
|
|
2065
|
+
const result = await createHelpCenterArticleGroup(input);
|
|
2066
|
+
return {
|
|
2067
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
case "delete_help_center_article_group": {
|
|
2071
|
+
const input = deleteHelpCenterArticleGroupInputSchema.parse(args);
|
|
2072
|
+
const result = await deleteHelpCenterArticleGroup(input);
|
|
2073
|
+
return {
|
|
2074
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
default:
|
|
2078
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2079
|
+
}
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2082
|
+
return {
|
|
2083
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
2084
|
+
isError: true
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
var main = async () => {
|
|
2089
|
+
const transport = new StdioServerTransport();
|
|
2090
|
+
await server.connect(transport);
|
|
2091
|
+
console.error("Plain MCP server running on stdio");
|
|
2092
|
+
};
|
|
2093
|
+
main().catch((error) => {
|
|
2094
|
+
console.error("Fatal error:", error);
|
|
2095
|
+
process.exit(1);
|
|
2096
|
+
});
|