@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/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
+ });