mailtea-mcp 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.
@@ -0,0 +1,4034 @@
1
+ // src/index.ts
2
+ var DEFAULT_API_BASE_URL = "https://api.mailtea.app";
3
+ var MCP_TOOLS = [
4
+ {
5
+ name: "auth.me",
6
+ description: "Return auth identity for current token",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {}
10
+ }
11
+ },
12
+ {
13
+ name: "issue.create_draft",
14
+ description: "Create a marketing email draft. Set kind to 'newsletter' (default) for recurring content that can publish to the public site, or 'broadcast' for a one-time email-only send (promotion, launch, announcement). Provide content one of three ways: templateId (seed from a published server template \u2014 use template.list/template.get to find one \u2014 with {{variables}} substituted), contentHtml (raw HTML), or contentSpec (json-render spec). Spec is recommended for AI agents \u2014 use components: Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown, MailteaHeader, MailteaFooter, MailteaSpacer, MailteaContentBlock.",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {
18
+ publicationId: { type: "string" },
19
+ title: { type: "string" },
20
+ kind: { type: "string", enum: ["newsletter", "broadcast"], description: "Email kind. 'newsletter' (default) can publish to the public site; 'broadcast' is one-time email-only." },
21
+ templateId: { type: "string", description: "Seed the draft from a published server template (see template.list). Takes precedence over contentHtml/contentSpec." },
22
+ variables: { type: "object", description: "Key/value map substituted into the template's {{variable}} placeholders when templateId is set." },
23
+ contentHtml: { type: "string", description: "Raw HTML content (use this OR contentSpec OR templateId)" },
24
+ contentSpec: {
25
+ type: "object",
26
+ description: "json-render spec (flat element map). Use this OR contentHtml OR templateId. Properties: root (string, element key), elements (record of {type, props?, children?})",
27
+ properties: {
28
+ root: { type: "string" },
29
+ elements: { type: "object" }
30
+ },
31
+ required: ["root", "elements"]
32
+ }
33
+ },
34
+ required: ["publicationId", "title"]
35
+ }
36
+ },
37
+ {
38
+ name: "issue.get_editor",
39
+ description: "Load issue editor payload for a draft or scheduled issue",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: {
43
+ issueId: { type: "string" }
44
+ },
45
+ required: ["issueId"]
46
+ }
47
+ },
48
+ {
49
+ name: "issue.update_draft",
50
+ description: "Update an existing draft issue. Provide contentHtml (raw HTML) or contentSpec (json-render spec).",
51
+ inputSchema: {
52
+ type: "object",
53
+ properties: {
54
+ issueId: { type: "string" },
55
+ title: { type: "string" },
56
+ contentHtml: { type: "string", description: "Raw HTML content (use this OR contentSpec)" },
57
+ contentSpec: {
58
+ type: "object",
59
+ description: "json-render spec (flat element map). Use this OR contentHtml.",
60
+ properties: {
61
+ root: { type: "string" },
62
+ elements: { type: "object" }
63
+ },
64
+ required: ["root", "elements"]
65
+ }
66
+ },
67
+ required: ["issueId", "title"]
68
+ }
69
+ },
70
+ {
71
+ name: "issue.remove_draft",
72
+ description: "Delete an existing draft issue",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ issueId: { type: "string" }
77
+ },
78
+ required: ["issueId"]
79
+ }
80
+ },
81
+ {
82
+ name: "issue.list_recent",
83
+ description: "List recent issues for a publication",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: {
87
+ publicationId: { type: "string" },
88
+ limit: { type: "number" }
89
+ },
90
+ required: ["publicationId"]
91
+ }
92
+ },
93
+ {
94
+ name: "publication.list",
95
+ description: "List publication workspaces available to current user/token",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {}
99
+ }
100
+ },
101
+ {
102
+ name: "publication.create",
103
+ description: "Create a publication workspace and attach owner membership",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ publicationId: { type: "string" },
108
+ name: { type: "string" },
109
+ timezone: { type: "string" }
110
+ },
111
+ required: ["name"]
112
+ }
113
+ },
114
+ {
115
+ name: "publication.domain_list",
116
+ description: "List custom domains connected to a publication",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ publicationId: { type: "string" },
121
+ limit: { type: "number" }
122
+ },
123
+ required: ["publicationId"]
124
+ }
125
+ },
126
+ {
127
+ name: "publication.domain_upsert",
128
+ description: "Create or update a publication custom domain",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ publicationId: { type: "string" },
133
+ host: { type: "string" },
134
+ isPrimary: { type: "boolean" },
135
+ proxyTarget: { type: "string" }
136
+ },
137
+ required: ["publicationId", "host"]
138
+ }
139
+ },
140
+ {
141
+ name: "publication.domain_verify",
142
+ description: "Mark a publication domain as verified",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ publicationId: { type: "string" },
147
+ domainId: { type: "string" },
148
+ verificationValue: { type: "string" }
149
+ },
150
+ required: ["publicationId", "domainId"]
151
+ }
152
+ },
153
+ {
154
+ name: "publication.domain_set_primary",
155
+ description: "Set an existing publication domain as primary",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ publicationId: { type: "string" },
160
+ domainId: { type: "string" }
161
+ },
162
+ required: ["publicationId", "domainId"]
163
+ }
164
+ },
165
+ {
166
+ name: "publication.domain_remove",
167
+ description: "Remove a publication custom domain",
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ publicationId: { type: "string" },
172
+ domainId: { type: "string" }
173
+ },
174
+ required: ["publicationId", "domainId"]
175
+ }
176
+ },
177
+ {
178
+ name: "publication.domain_traefik_preview",
179
+ description: "Generate Traefik dynamic-file preview for publication domains",
180
+ inputSchema: {
181
+ type: "object",
182
+ properties: {
183
+ publicationId: { type: "string" }
184
+ },
185
+ required: ["publicationId"]
186
+ }
187
+ },
188
+ {
189
+ name: "contact.list",
190
+ description: "List contacts for a publication",
191
+ inputSchema: {
192
+ type: "object",
193
+ properties: {
194
+ publicationId: { type: "string" },
195
+ status: {
196
+ type: "string",
197
+ enum: ["active", "unsubscribed", "suppressed"]
198
+ },
199
+ query: { type: "string" },
200
+ limit: { type: "number" }
201
+ },
202
+ required: ["publicationId"]
203
+ }
204
+ },
205
+ {
206
+ name: "contact.upsert",
207
+ description: "Create or reactivate a contact by email",
208
+ inputSchema: {
209
+ type: "object",
210
+ properties: {
211
+ publicationId: { type: "string" },
212
+ email: { type: "string" },
213
+ referrerContactId: { type: "string" }
214
+ },
215
+ required: ["publicationId", "email"]
216
+ }
217
+ },
218
+ {
219
+ name: "contact.set_status",
220
+ description: "Set contact status (active, unsubscribed, suppressed)",
221
+ inputSchema: {
222
+ type: "object",
223
+ properties: {
224
+ publicationId: { type: "string" },
225
+ contactId: { type: "string" },
226
+ status: {
227
+ type: "string",
228
+ enum: ["active", "unsubscribed", "suppressed"]
229
+ }
230
+ },
231
+ required: ["publicationId", "contactId", "status"]
232
+ }
233
+ },
234
+ {
235
+ name: "contact.import_csv",
236
+ description: "Import contacts from CSV text payload",
237
+ inputSchema: {
238
+ type: "object",
239
+ properties: {
240
+ publicationId: { type: "string" },
241
+ csvText: { type: "string" }
242
+ },
243
+ required: ["publicationId", "csvText"]
244
+ }
245
+ },
246
+ {
247
+ name: "contact.referral_summary",
248
+ description: "Load referral conversion leaderboard for a publication",
249
+ inputSchema: {
250
+ type: "object",
251
+ properties: {
252
+ publicationId: { type: "string" },
253
+ limit: { type: "number" }
254
+ },
255
+ required: ["publicationId"]
256
+ }
257
+ },
258
+ {
259
+ name: "contact.referral_milestones",
260
+ description: "List referral milestone rules for a publication",
261
+ inputSchema: {
262
+ type: "object",
263
+ properties: {
264
+ publicationId: { type: "string" },
265
+ limit: { type: "number" }
266
+ },
267
+ required: ["publicationId"]
268
+ }
269
+ },
270
+ {
271
+ name: "contact.referral_milestone_upsert",
272
+ description: "Create or update a referral milestone rule",
273
+ inputSchema: {
274
+ type: "object",
275
+ properties: {
276
+ publicationId: { type: "string" },
277
+ milestoneId: { type: "string" },
278
+ title: { type: "string" },
279
+ description: { type: "string" },
280
+ referralCount: { type: "number" }
281
+ },
282
+ required: ["publicationId", "title", "referralCount"]
283
+ }
284
+ },
285
+ {
286
+ name: "contact.referral_milestone_remove",
287
+ description: "Delete a referral milestone rule",
288
+ inputSchema: {
289
+ type: "object",
290
+ properties: {
291
+ publicationId: { type: "string" },
292
+ milestoneId: { type: "string" }
293
+ },
294
+ required: ["publicationId", "milestoneId"]
295
+ }
296
+ },
297
+ {
298
+ name: "contact.referral_rewards",
299
+ description: "List awarded referral rewards for a publication",
300
+ inputSchema: {
301
+ type: "object",
302
+ properties: {
303
+ publicationId: { type: "string" },
304
+ contactId: { type: "string" },
305
+ limit: { type: "number" }
306
+ },
307
+ required: ["publicationId"]
308
+ }
309
+ },
310
+ {
311
+ name: "monetize.offer_list",
312
+ description: "List sponsor offers for a publication",
313
+ inputSchema: {
314
+ type: "object",
315
+ properties: {
316
+ publicationId: { type: "string" },
317
+ status: {
318
+ type: "string",
319
+ enum: ["draft", "active", "paused", "archived"]
320
+ },
321
+ limit: { type: "number" }
322
+ },
323
+ required: ["publicationId"]
324
+ }
325
+ },
326
+ {
327
+ name: "monetize.offer_upsert",
328
+ description: "Create or update a sponsor offer",
329
+ inputSchema: {
330
+ type: "object",
331
+ properties: {
332
+ publicationId: { type: "string" },
333
+ offerId: { type: "string" },
334
+ title: { type: "string" },
335
+ sponsorName: { type: "string" },
336
+ description: { type: "string" },
337
+ pricingModel: {
338
+ type: "string",
339
+ enum: ["flat", "cpm"]
340
+ },
341
+ rateCents: { type: "number" },
342
+ estimatedPlacements: { type: "number" },
343
+ status: {
344
+ type: "string",
345
+ enum: ["draft", "active", "paused", "archived"]
346
+ },
347
+ startsAt: {
348
+ type: "string",
349
+ description: "Optional ISO start timestamp."
350
+ },
351
+ endsAt: {
352
+ type: "string",
353
+ description: "Optional ISO end timestamp."
354
+ }
355
+ },
356
+ required: ["publicationId", "title", "sponsorName", "pricingModel", "rateCents"]
357
+ }
358
+ },
359
+ {
360
+ name: "monetize.offer_remove",
361
+ description: "Delete a sponsor offer",
362
+ inputSchema: {
363
+ type: "object",
364
+ properties: {
365
+ publicationId: { type: "string" },
366
+ offerId: { type: "string" }
367
+ },
368
+ required: ["publicationId", "offerId"]
369
+ }
370
+ },
371
+ {
372
+ name: "section.list",
373
+ description: "List reusable editor sections for a publication",
374
+ inputSchema: {
375
+ type: "object",
376
+ properties: {
377
+ publicationId: { type: "string" },
378
+ limit: { type: "number" }
379
+ },
380
+ required: ["publicationId"]
381
+ }
382
+ },
383
+ {
384
+ name: "section.catalog",
385
+ description: "List shared marketplace section packs",
386
+ inputSchema: {
387
+ type: "object",
388
+ properties: {
389
+ publicationId: {
390
+ type: "string",
391
+ description: "Optional publication id to include custom packs"
392
+ }
393
+ }
394
+ }
395
+ },
396
+ {
397
+ name: "section.pack_create",
398
+ description: "Create a custom marketplace pack for a publication",
399
+ inputSchema: {
400
+ type: "object",
401
+ properties: {
402
+ publicationId: { type: "string" },
403
+ title: { type: "string" },
404
+ description: { type: "string" },
405
+ styleProfile: {
406
+ type: "object",
407
+ description: "Optional React Email style profile for this template pack."
408
+ },
409
+ sections: {
410
+ type: "array",
411
+ items: {
412
+ type: "object",
413
+ properties: {
414
+ name: { type: "string" },
415
+ contentJson: {
416
+ type: "array",
417
+ items: { type: "object" },
418
+ minItems: 1
419
+ }
420
+ },
421
+ required: ["name", "contentJson"]
422
+ },
423
+ minItems: 1
424
+ }
425
+ },
426
+ required: ["publicationId", "title", "sections"]
427
+ }
428
+ },
429
+ {
430
+ name: "section.pack_update",
431
+ description: "Update a custom marketplace pack",
432
+ inputSchema: {
433
+ type: "object",
434
+ properties: {
435
+ publicationId: { type: "string" },
436
+ packId: { type: "string" },
437
+ title: { type: "string" },
438
+ description: { type: "string" },
439
+ styleProfile: {
440
+ type: "object",
441
+ description: "Optional React Email style profile for this template pack."
442
+ },
443
+ sections: {
444
+ type: "array",
445
+ items: {
446
+ type: "object",
447
+ properties: {
448
+ name: { type: "string" },
449
+ contentJson: {
450
+ type: "array",
451
+ items: { type: "object" },
452
+ minItems: 1
453
+ }
454
+ },
455
+ required: ["name", "contentJson"]
456
+ },
457
+ minItems: 1
458
+ }
459
+ },
460
+ required: ["publicationId", "packId", "title", "sections"]
461
+ }
462
+ },
463
+ {
464
+ name: "section.pack_remove",
465
+ description: "Delete a custom marketplace pack",
466
+ inputSchema: {
467
+ type: "object",
468
+ properties: {
469
+ publicationId: { type: "string" },
470
+ packId: { type: "string" }
471
+ },
472
+ required: ["publicationId", "packId"]
473
+ }
474
+ },
475
+ {
476
+ name: "section.pack_revisions",
477
+ description: "List version history for a custom marketplace pack",
478
+ inputSchema: {
479
+ type: "object",
480
+ properties: {
481
+ publicationId: { type: "string" },
482
+ packId: { type: "string" },
483
+ limit: { type: "number" }
484
+ },
485
+ required: ["publicationId", "packId"]
486
+ }
487
+ },
488
+ {
489
+ name: "section.pack_restore_revision",
490
+ description: "Restore a custom marketplace pack to a specific revision",
491
+ inputSchema: {
492
+ type: "object",
493
+ properties: {
494
+ publicationId: { type: "string" },
495
+ packId: { type: "string" },
496
+ revisionId: { type: "string" }
497
+ },
498
+ required: ["publicationId", "packId", "revisionId"]
499
+ }
500
+ },
501
+ {
502
+ name: "section.import_pack",
503
+ description: "Import a marketplace pack into reusable sections",
504
+ inputSchema: {
505
+ type: "object",
506
+ properties: {
507
+ publicationId: { type: "string" },
508
+ presetId: { type: "string" }
509
+ },
510
+ required: ["publicationId", "presetId"]
511
+ }
512
+ },
513
+ {
514
+ name: "section.create",
515
+ description: "Create a reusable section snippet",
516
+ inputSchema: {
517
+ type: "object",
518
+ properties: {
519
+ publicationId: { type: "string" },
520
+ name: { type: "string" },
521
+ contentJson: {
522
+ type: "array",
523
+ items: { type: "object" },
524
+ minItems: 1
525
+ }
526
+ },
527
+ required: ["publicationId", "name", "contentJson"]
528
+ }
529
+ },
530
+ {
531
+ name: "section.update",
532
+ description: "Update a reusable section snippet",
533
+ inputSchema: {
534
+ type: "object",
535
+ properties: {
536
+ sectionId: { type: "string" },
537
+ name: { type: "string" },
538
+ contentJson: {
539
+ type: "array",
540
+ items: { type: "object" },
541
+ minItems: 1
542
+ }
543
+ },
544
+ required: ["sectionId", "name", "contentJson"]
545
+ }
546
+ },
547
+ {
548
+ name: "section.remove",
549
+ description: "Delete a reusable section snippet",
550
+ inputSchema: {
551
+ type: "object",
552
+ properties: {
553
+ sectionId: { type: "string" }
554
+ },
555
+ required: ["sectionId"]
556
+ }
557
+ },
558
+ {
559
+ name: "issue.preview",
560
+ description: "Preview newsletter HTML for an issue",
561
+ inputSchema: {
562
+ type: "object",
563
+ properties: {
564
+ issueId: { type: "string" }
565
+ },
566
+ required: ["issueId"]
567
+ }
568
+ },
569
+ {
570
+ name: "issue.preview_draft",
571
+ description: "Preview newsletter HTML/text for unsaved draft content",
572
+ inputSchema: {
573
+ type: "object",
574
+ properties: {
575
+ publicationId: { type: "string" },
576
+ title: { type: "string" },
577
+ html: { type: "string" },
578
+ plainText: { type: "string" },
579
+ styleProfile: {
580
+ type: "object",
581
+ description: "Optional React Email style profile overrides."
582
+ }
583
+ },
584
+ required: ["publicationId", "title", "html"]
585
+ }
586
+ },
587
+ {
588
+ name: "issue.delivery_progress",
589
+ description: "Fetch send progress snapshot for an issue",
590
+ inputSchema: {
591
+ type: "object",
592
+ properties: {
593
+ publicationId: { type: "string" },
594
+ issueId: { type: "string" }
595
+ },
596
+ required: ["publicationId", "issueId"]
597
+ }
598
+ },
599
+ {
600
+ name: "issue.wait_delivery",
601
+ description: "Poll issue send progress until sent/failed or timeout",
602
+ inputSchema: {
603
+ type: "object",
604
+ properties: {
605
+ publicationId: { type: "string" },
606
+ issueId: { type: "string" },
607
+ timeoutMs: {
608
+ type: "number",
609
+ description: "Optional timeout in milliseconds. Defaults to 60000."
610
+ },
611
+ pollIntervalMs: {
612
+ type: "number",
613
+ description: "Optional polling interval in milliseconds. Defaults to 2000."
614
+ }
615
+ },
616
+ required: ["publicationId", "issueId"]
617
+ }
618
+ },
619
+ {
620
+ name: "analytics.poll_results",
621
+ description: "Fetch poll vote totals for a specific issue",
622
+ inputSchema: {
623
+ type: "object",
624
+ properties: {
625
+ publicationId: { type: "string" },
626
+ issueId: { type: "string" }
627
+ },
628
+ required: ["publicationId", "issueId"]
629
+ }
630
+ },
631
+ {
632
+ name: "analytics.issue_performance",
633
+ description: "Fetch open/click delivery analytics for an issue",
634
+ inputSchema: {
635
+ type: "object",
636
+ properties: {
637
+ publicationId: { type: "string" },
638
+ issueId: { type: "string" },
639
+ range: {
640
+ type: "string",
641
+ enum: ["24h", "7d", "30d", "all"],
642
+ description: "Analytics window. Defaults to all."
643
+ }
644
+ },
645
+ required: ["publicationId", "issueId"]
646
+ }
647
+ },
648
+ {
649
+ name: "analytics.issue_trend",
650
+ description: "Fetch daily open/click trend buckets for an issue",
651
+ inputSchema: {
652
+ type: "object",
653
+ properties: {
654
+ publicationId: { type: "string" },
655
+ issueId: { type: "string" },
656
+ range: {
657
+ type: "string",
658
+ enum: ["24h", "7d", "30d", "all"],
659
+ description: "Analytics window. Defaults to all."
660
+ }
661
+ },
662
+ required: ["publicationId", "issueId"]
663
+ }
664
+ },
665
+ {
666
+ name: "analytics.latest_summary",
667
+ description: "Fetch latest issue analytics snapshot for a publication",
668
+ inputSchema: {
669
+ type: "object",
670
+ properties: {
671
+ publicationId: {
672
+ type: "string",
673
+ description: "Optional publication id. Falls back to MAILTEA_PUBLICATION_ID."
674
+ },
675
+ range: {
676
+ type: "string",
677
+ enum: ["24h", "7d", "30d", "all"],
678
+ description: "Analytics window. Defaults to 7d."
679
+ }
680
+ }
681
+ }
682
+ },
683
+ {
684
+ name: "analytics.issue_export_csv",
685
+ description: "Export issue performance and poll analytics as CSV",
686
+ inputSchema: {
687
+ type: "object",
688
+ properties: {
689
+ publicationId: { type: "string" },
690
+ issueId: { type: "string" },
691
+ range: {
692
+ type: "string",
693
+ enum: ["24h", "7d", "30d", "all"],
694
+ description: "Analytics window. Defaults to all."
695
+ },
696
+ exportType: {
697
+ type: "string",
698
+ enum: ["combined", "performance", "polls"],
699
+ description: "CSV type. Defaults to combined."
700
+ }
701
+ },
702
+ required: ["publicationId", "issueId"]
703
+ }
704
+ },
705
+ {
706
+ name: "analytics.issue_export_performance_csv",
707
+ description: "Export issue performance-only analytics as CSV",
708
+ inputSchema: {
709
+ type: "object",
710
+ properties: {
711
+ publicationId: { type: "string" },
712
+ issueId: { type: "string" },
713
+ range: {
714
+ type: "string",
715
+ enum: ["24h", "7d", "30d", "all"],
716
+ description: "Analytics window. Defaults to all."
717
+ }
718
+ },
719
+ required: ["publicationId", "issueId"]
720
+ }
721
+ },
722
+ {
723
+ name: "analytics.issue_export_polls_csv",
724
+ description: "Export issue poll-only analytics as CSV",
725
+ inputSchema: {
726
+ type: "object",
727
+ properties: {
728
+ publicationId: { type: "string" },
729
+ issueId: { type: "string" },
730
+ range: {
731
+ type: "string",
732
+ enum: ["24h", "7d", "30d", "all"],
733
+ description: "Analytics window. Defaults to all."
734
+ }
735
+ },
736
+ required: ["publicationId", "issueId"]
737
+ }
738
+ },
739
+ {
740
+ name: "issue.schedule",
741
+ description: "Schedule an issue for delivery",
742
+ inputSchema: {
743
+ type: "object",
744
+ properties: {
745
+ issueId: { type: "string" },
746
+ scheduledFor: {
747
+ type: "string",
748
+ description: "ISO timestamp (optional). Defaults to now."
749
+ }
750
+ },
751
+ required: ["issueId"]
752
+ }
753
+ },
754
+ {
755
+ name: "issue.unschedule",
756
+ description: "Cancel an issue schedule and move it back to draft",
757
+ inputSchema: {
758
+ type: "object",
759
+ properties: {
760
+ issueId: { type: "string" }
761
+ },
762
+ required: ["issueId"]
763
+ }
764
+ },
765
+ {
766
+ name: "issue.publish_to_web",
767
+ description: "Publish an issue to the publication's public website, making it readable on the web (typically a sent issue).",
768
+ inputSchema: {
769
+ type: "object",
770
+ properties: { issueId: { type: "string" } },
771
+ required: ["issueId"]
772
+ }
773
+ },
774
+ {
775
+ name: "issue.unpublish_from_web",
776
+ description: "Remove an issue from the publication's public website.",
777
+ inputSchema: {
778
+ type: "object",
779
+ properties: { issueId: { type: "string" } },
780
+ required: ["issueId"]
781
+ }
782
+ },
783
+ {
784
+ name: "issue.send_now",
785
+ description: "Send an issue immediately",
786
+ inputSchema: {
787
+ type: "object",
788
+ properties: {
789
+ issueId: { type: "string" }
790
+ },
791
+ required: ["issueId"]
792
+ }
793
+ },
794
+ {
795
+ name: "issue.send_and_wait",
796
+ description: "Send an issue now and poll until sent/failed or timeout",
797
+ inputSchema: {
798
+ type: "object",
799
+ properties: {
800
+ issueId: { type: "string" },
801
+ timeoutMs: {
802
+ type: "number",
803
+ description: "Optional timeout in milliseconds. Defaults to 60000."
804
+ },
805
+ pollIntervalMs: {
806
+ type: "number",
807
+ description: "Optional polling interval in milliseconds. Defaults to 2000."
808
+ }
809
+ },
810
+ required: ["issueId"]
811
+ }
812
+ },
813
+ {
814
+ name: "ai.generate_draft",
815
+ description: "Generate a draft from a prompt",
816
+ inputSchema: {
817
+ type: "object",
818
+ properties: {
819
+ publicationId: { type: "string" },
820
+ prompt: { type: "string" },
821
+ tone: {
822
+ type: "string",
823
+ enum: ["neutral", "friendly", "formal"]
824
+ }
825
+ },
826
+ required: ["publicationId", "prompt"]
827
+ }
828
+ },
829
+ {
830
+ name: "template.create",
831
+ description: "Create an email template. Provide html (raw HTML) or spec (json-render spec). Spec is recommended \u2014 the platform renders email-safe HTML server-side. Available spec components: Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown, MailteaHeader, MailteaFooter, MailteaSpacer, MailteaContentBlock.",
832
+ inputSchema: {
833
+ type: "object",
834
+ properties: {
835
+ publicationId: { type: "string", description: "Publication to create template in" },
836
+ name: { type: "string", description: "Template name (max 120 chars)" },
837
+ html: { type: "string", description: "Raw HTML content (use this OR spec)" },
838
+ spec: {
839
+ type: "object",
840
+ description: "json-render spec (flat element map). Use this OR html.",
841
+ properties: {
842
+ root: { type: "string" },
843
+ elements: { type: "object" }
844
+ },
845
+ required: ["root", "elements"]
846
+ },
847
+ description: { type: "string", description: "Template description (max 500 chars)" },
848
+ subject: { type: "string", description: "Default email subject line" },
849
+ variables: {
850
+ type: "array",
851
+ description: "Template variables with fallback values",
852
+ items: {
853
+ type: "object",
854
+ properties: {
855
+ key: { type: "string" },
856
+ type: { type: "string", enum: ["string", "number"] },
857
+ fallback_value: {}
858
+ },
859
+ required: ["key", "type"]
860
+ }
861
+ }
862
+ },
863
+ required: ["publicationId", "name"]
864
+ }
865
+ },
866
+ {
867
+ name: "template.list",
868
+ description: "List email templates for a publication",
869
+ inputSchema: {
870
+ type: "object",
871
+ properties: {
872
+ publicationId: { type: "string" },
873
+ limit: { type: "number", description: "Max results (1-100, default 20)" }
874
+ },
875
+ required: ["publicationId"]
876
+ }
877
+ },
878
+ {
879
+ name: "template.get",
880
+ description: "Get a single email template by ID, including its spec and rendered HTML",
881
+ inputSchema: {
882
+ type: "object",
883
+ properties: {
884
+ publicationId: { type: "string" },
885
+ templateId: { type: "string" }
886
+ },
887
+ required: ["publicationId", "templateId"]
888
+ }
889
+ },
890
+ {
891
+ name: "template.update",
892
+ description: "Update an email template. Pass only the fields to change. Providing spec re-renders email-safe HTML server-side; providing html switches the template to raw HTML.",
893
+ inputSchema: {
894
+ type: "object",
895
+ properties: {
896
+ publicationId: { type: "string" },
897
+ templateId: { type: "string" },
898
+ name: { type: "string" },
899
+ html: { type: "string", description: "Raw HTML content (switches the template to raw HTML)." },
900
+ spec: {
901
+ type: "object",
902
+ description: "json-render spec (flat element map). Re-renders email-safe HTML server-side.",
903
+ properties: { root: { type: "string" }, elements: { type: "object" } },
904
+ required: ["root", "elements"]
905
+ },
906
+ description: { type: "string" },
907
+ text: { type: "string", description: "Plain-text body." },
908
+ subject: { type: "string" },
909
+ from: { type: "string", description: "Default sender address." },
910
+ reply_to: { type: "string" },
911
+ variables: {
912
+ type: "array",
913
+ items: {
914
+ type: "object",
915
+ properties: {
916
+ key: { type: "string" },
917
+ type: { type: "string", enum: ["string", "number"] },
918
+ fallback_value: {}
919
+ },
920
+ required: ["key", "type"]
921
+ }
922
+ }
923
+ },
924
+ required: ["publicationId", "templateId"]
925
+ }
926
+ },
927
+ {
928
+ name: "template.publish",
929
+ description: "Publish a draft email template, making it the active version for sends.",
930
+ inputSchema: {
931
+ type: "object",
932
+ properties: {
933
+ publicationId: { type: "string" },
934
+ templateId: { type: "string" }
935
+ },
936
+ required: ["publicationId", "templateId"]
937
+ }
938
+ },
939
+ {
940
+ name: "template.duplicate",
941
+ description: "Duplicate an email template into a new draft copy.",
942
+ inputSchema: {
943
+ type: "object",
944
+ properties: {
945
+ publicationId: { type: "string" },
946
+ templateId: { type: "string" }
947
+ },
948
+ required: ["publicationId", "templateId"]
949
+ }
950
+ },
951
+ {
952
+ name: "template.delete",
953
+ description: "Delete an email template.",
954
+ inputSchema: {
955
+ type: "object",
956
+ properties: {
957
+ publicationId: { type: "string" },
958
+ templateId: { type: "string" }
959
+ },
960
+ required: ["publicationId", "templateId"]
961
+ }
962
+ },
963
+ {
964
+ name: "email.send",
965
+ description: "Send a transactional email to one or more specific recipients. Provide inline content (html and/or text) OR a template reference \u2014 not both. Unlike issue.send_now (which sends a newsletter to the WHOLE publication list), this delivers a one-shot email to the exact addresses in 'to'. Set scheduled_at to send later. Returns the new email's id.",
966
+ inputSchema: {
967
+ type: "object",
968
+ properties: {
969
+ from: {
970
+ type: "string",
971
+ description: "Sender, e.g. 'Acme <hello@acme.com>'. Must use a verified domain."
972
+ },
973
+ to: {
974
+ type: ["string", "array"],
975
+ items: { type: "string" },
976
+ description: "Recipient address, or array of up to 50 addresses."
977
+ },
978
+ subject: { type: "string", description: "Subject line (max 998 chars)." },
979
+ html: { type: "string", description: "HTML body. Use this OR template, not both." },
980
+ text: { type: "string", description: "Plain-text body." },
981
+ template: {
982
+ type: "object",
983
+ description: "Stored template reference to render instead of inline html.",
984
+ properties: {
985
+ id: { type: "string" },
986
+ variables: { type: "object", description: "Template variable values." }
987
+ },
988
+ required: ["id"]
989
+ },
990
+ cc: { type: ["string", "array"], items: { type: "string" }, description: "CC address(es)." },
991
+ bcc: { type: ["string", "array"], items: { type: "string" }, description: "BCC address(es)." },
992
+ reply_to: {
993
+ type: ["string", "array"],
994
+ items: { type: "string" },
995
+ description: "Reply-To address(es)."
996
+ },
997
+ scheduled_at: {
998
+ type: "string",
999
+ description: "ISO 8601 datetime to schedule the send. Omit to send immediately."
1000
+ },
1001
+ tags: {
1002
+ type: "array",
1003
+ description: "Custom tags for filtering/analytics.",
1004
+ items: {
1005
+ type: "object",
1006
+ properties: { name: { type: "string" }, value: { type: "string" } },
1007
+ required: ["name", "value"]
1008
+ }
1009
+ },
1010
+ headers: { type: "object", description: "Custom email headers (string values)." },
1011
+ attachments: {
1012
+ type: "array",
1013
+ description: "File attachments.",
1014
+ items: {
1015
+ type: "object",
1016
+ properties: {
1017
+ filename: { type: "string" },
1018
+ content: { type: "string", description: "Base64-encoded file content." },
1019
+ content_type: { type: "string" },
1020
+ content_id: { type: "string", description: "For inline images (cid:)." }
1021
+ },
1022
+ required: ["filename", "content"]
1023
+ }
1024
+ }
1025
+ },
1026
+ required: ["from", "to", "subject"]
1027
+ }
1028
+ },
1029
+ {
1030
+ name: "email.batch",
1031
+ description: "Send up to 100 transactional emails in one request. Each item is shaped like email.send but WITHOUT attachments or scheduled_at. Returns the ids in request order.",
1032
+ inputSchema: {
1033
+ type: "object",
1034
+ properties: {
1035
+ emails: {
1036
+ type: "array",
1037
+ description: "1-100 email objects: { from, to, subject, html?|text?|template?, cc?, bcc?, reply_to?, tags?, headers? }.",
1038
+ items: { type: "object" }
1039
+ }
1040
+ },
1041
+ required: ["emails"]
1042
+ }
1043
+ },
1044
+ {
1045
+ name: "email.get",
1046
+ description: "Retrieve a transactional email by id with its delivery status (last_event) and tracking counters (open_count, click_count).",
1047
+ inputSchema: {
1048
+ type: "object",
1049
+ properties: { id: { type: "string" } },
1050
+ required: ["id"]
1051
+ }
1052
+ },
1053
+ {
1054
+ name: "email.reschedule",
1055
+ description: "Reschedule a still-scheduled transactional email to a new time.",
1056
+ inputSchema: {
1057
+ type: "object",
1058
+ properties: {
1059
+ id: { type: "string" },
1060
+ scheduled_at: { type: "string", description: "New ISO 8601 datetime." }
1061
+ },
1062
+ required: ["id", "scheduled_at"]
1063
+ }
1064
+ },
1065
+ {
1066
+ name: "email.cancel",
1067
+ description: "Cancel a scheduled transactional email before it sends.",
1068
+ inputSchema: {
1069
+ type: "object",
1070
+ properties: { id: { type: "string" } },
1071
+ required: ["id"]
1072
+ }
1073
+ },
1074
+ {
1075
+ name: "email.resend",
1076
+ description: "Resend a failed or bounced transactional email \u2014 creates a fresh copy with the same content. Only emails whose status is 'failed' or 'bounced' can be resent; others return an error.",
1077
+ inputSchema: {
1078
+ type: "object",
1079
+ properties: { id: { type: "string" } },
1080
+ required: ["id"]
1081
+ }
1082
+ },
1083
+ {
1084
+ name: "email.list",
1085
+ description: "List transactional emails (most recent first). Filter by status and tags; paginate with limit/offset. Returns id, status, subject, recipient per email.",
1086
+ inputSchema: {
1087
+ type: "object",
1088
+ properties: {
1089
+ status: {
1090
+ type: "string",
1091
+ enum: [
1092
+ "queued",
1093
+ "sent",
1094
+ "delivered",
1095
+ "bounced",
1096
+ "complained",
1097
+ "failed",
1098
+ "scheduled",
1099
+ "canceled"
1100
+ ],
1101
+ description: "Filter by delivery status."
1102
+ },
1103
+ limit: { type: "number", description: "Max results 1-100 (default 50)." },
1104
+ offset: { type: "number", description: "Pagination offset (default 0)." },
1105
+ tag_name: { type: "string", description: "Filter by a custom tag name." },
1106
+ tag_value: { type: "string", description: "Filter by a custom tag value (use with tag_name)." },
1107
+ from_date: { type: "string", description: "ISO 8601 lower bound on created_at." },
1108
+ to_date: { type: "string", description: "ISO 8601 upper bound on created_at." }
1109
+ }
1110
+ }
1111
+ },
1112
+ {
1113
+ name: "email.analytics",
1114
+ description: "Aggregate transactional email metrics over an optional date window: totals, delivered/bounced/opened/clicked counts, per-status counts, and delivery/open/click/bounce rates.",
1115
+ inputSchema: {
1116
+ type: "object",
1117
+ properties: {
1118
+ from_date: { type: "string", description: "ISO 8601 lower bound on created_at." },
1119
+ to_date: { type: "string", description: "ISO 8601 upper bound on created_at." }
1120
+ }
1121
+ }
1122
+ },
1123
+ // --- Inbound email (REST /v1/emails/inbound) -----------------------------
1124
+ // Only inbound_list takes a publicationId; get/attachments/reply resolve
1125
+ // tenancy from the inbound email id.
1126
+ {
1127
+ name: "email.inbound_list",
1128
+ description: "List inbound (received) emails for a publication, most recent first; paginate with limit/cursor. Only this inbound tool needs publicationId \u2014 email.inbound_get/list_attachments/get_attachment/reply resolve tenancy from the email id.",
1129
+ inputSchema: {
1130
+ type: "object",
1131
+ properties: {
1132
+ publicationId: { type: "string" },
1133
+ limit: { type: "number", description: "Max results (1-100, default 20)." },
1134
+ cursor: { type: "string", description: "Opaque pagination cursor from a prior response's next_cursor." }
1135
+ },
1136
+ required: ["publicationId"]
1137
+ }
1138
+ },
1139
+ {
1140
+ name: "email.inbound_get",
1141
+ description: "Retrieve a single inbound email by id (rxemail_) \u2014 headers, html/text body, a signed download URL for the raw message, and its attachments. Tenancy is resolved from the id, so no publicationId is needed.",
1142
+ inputSchema: {
1143
+ type: "object",
1144
+ properties: { id: { type: "string", description: "Inbound email id (rxemail_)." } },
1145
+ required: ["id"]
1146
+ }
1147
+ },
1148
+ {
1149
+ name: "email.inbound_list_attachments",
1150
+ description: "List the attachments of an inbound email, each with a signed download URL. Tenancy is resolved from the email id, so no publicationId is needed.",
1151
+ inputSchema: {
1152
+ type: "object",
1153
+ properties: { id: { type: "string", description: "Inbound email id (rxemail_)." } },
1154
+ required: ["id"]
1155
+ }
1156
+ },
1157
+ {
1158
+ name: "email.inbound_get_attachment",
1159
+ description: "Get a single inbound-email attachment (rxatt_) with a signed download URL. Tenancy is resolved from the parent email id, so no publicationId is needed.",
1160
+ inputSchema: {
1161
+ type: "object",
1162
+ properties: {
1163
+ id: { type: "string", description: "Inbound email id (rxemail_)." },
1164
+ attachmentId: { type: "string", description: "Attachment id (rxatt_)." }
1165
+ },
1166
+ required: ["id", "attachmentId"]
1167
+ }
1168
+ },
1169
+ {
1170
+ name: "email.inbound_reply",
1171
+ description: "Reply to an inbound email. The reply threads automatically \u2014 In-Reply-To, References, and the To (reply target) are all derived by the server from the original, so they are NOT inputs. Provide html and/or text. Omit `from` to send from the verified domain the mail was delivered to. Tenancy is resolved from the email id (no publicationId). Returns the resulting transactional email id (txemail_) with its status.",
1172
+ inputSchema: {
1173
+ type: "object",
1174
+ properties: {
1175
+ id: { type: "string", description: "Inbound email id (rxemail_) to reply to." },
1176
+ from: {
1177
+ type: "object",
1178
+ description: "Sender override. Omit to send from the verified domain the original was delivered to.",
1179
+ properties: {
1180
+ email: { type: "string" },
1181
+ name: { type: "string" }
1182
+ },
1183
+ required: ["email"]
1184
+ },
1185
+ subject: { type: "string", description: "Defaults to the original subject with a single 'Re: ' prefix." },
1186
+ html: { type: "string", description: "HTML body (provide html and/or text)." },
1187
+ text: { type: "string", description: "Plain-text body (provide html and/or text)." },
1188
+ cc: { type: "array", items: { type: "string" }, description: "CC addresses." },
1189
+ bcc: { type: "array", items: { type: "string" }, description: "BCC addresses." },
1190
+ idempotency_key: { type: "string", description: "Optional key to make the reply idempotent (the Idempotency-Key header wins when both are set)." }
1191
+ },
1192
+ required: ["id"]
1193
+ }
1194
+ },
1195
+ {
1196
+ name: "issue.send_test",
1197
+ description: "Send a TEST copy of a newsletter draft to specific recipients (yourself/teammates) to check it before subscribers see it. Renders the issue exactly as a subscriber would receive it and delivers it as a one-shot email with a '[TEST]' subject prefix. This does NOT send to the publication's audience \u2014 use issue.send_now for the real send.",
1198
+ inputSchema: {
1199
+ type: "object",
1200
+ properties: {
1201
+ issueId: { type: "string" },
1202
+ recipients: {
1203
+ type: ["string", "array"],
1204
+ items: { type: "string" },
1205
+ description: "Test recipient address, or array of addresses."
1206
+ },
1207
+ from: {
1208
+ type: "string",
1209
+ description: "Sender, e.g. 'Acme <hello@acme.com>'. Must use a verified domain."
1210
+ }
1211
+ },
1212
+ required: ["issueId", "recipients", "from"]
1213
+ }
1214
+ },
1215
+ // --- Email sending domains (REST /v1/domains) ----------------------------
1216
+ // Provision and verify the 'from' domain that email.send / issue.send_test
1217
+ // require. (Distinct from publication.domain_* which manage the Traefik
1218
+ // website surface of the same publication.)
1219
+ {
1220
+ name: "domain.create",
1221
+ description: "Register an email sending domain for a publication. Returns the DNS TXT record (in 'records') the operator must add before the domain can be verified. Set purpose to 'email' (or 'both') to use it as a sending 'from' domain.",
1222
+ inputSchema: {
1223
+ type: "object",
1224
+ properties: {
1225
+ publicationId: { type: "string" },
1226
+ name: { type: "string", description: "Domain host, e.g. 'mail.acme.com'." },
1227
+ purpose: {
1228
+ type: "string",
1229
+ enum: ["email", "site", "both"],
1230
+ description: "Use 'email' or 'both' for a sending domain. Defaults to 'site'."
1231
+ },
1232
+ is_primary: { type: "boolean" }
1233
+ },
1234
+ required: ["publicationId", "name"]
1235
+ }
1236
+ },
1237
+ {
1238
+ name: "domain.list",
1239
+ description: "List email/site domains for a publication.",
1240
+ inputSchema: {
1241
+ type: "object",
1242
+ properties: {
1243
+ publicationId: { type: "string" },
1244
+ limit: { type: "number", description: "Max results (1-100, default 20)." }
1245
+ },
1246
+ required: ["publicationId"]
1247
+ }
1248
+ },
1249
+ {
1250
+ name: "domain.get",
1251
+ description: "Get a single domain including the DNS 'records' to add for verification.",
1252
+ inputSchema: {
1253
+ type: "object",
1254
+ properties: {
1255
+ publicationId: { type: "string" },
1256
+ domainId: { type: "string" }
1257
+ },
1258
+ required: ["publicationId", "domainId"]
1259
+ }
1260
+ },
1261
+ {
1262
+ name: "domain.verify",
1263
+ description: "Verify a domain by checking its DNS TXT record. On success the domain's status becomes 'verified' and it can be used as a sending 'from' domain.",
1264
+ inputSchema: {
1265
+ type: "object",
1266
+ properties: {
1267
+ publicationId: { type: "string" },
1268
+ domainId: { type: "string" }
1269
+ },
1270
+ required: ["publicationId", "domainId"]
1271
+ }
1272
+ },
1273
+ {
1274
+ name: "domain.update",
1275
+ description: "Update a domain's purpose (email/site/both) or primary flag.",
1276
+ inputSchema: {
1277
+ type: "object",
1278
+ properties: {
1279
+ publicationId: { type: "string" },
1280
+ domainId: { type: "string" },
1281
+ purpose: { type: "string", enum: ["email", "site", "both"] },
1282
+ is_primary: { type: "boolean" }
1283
+ },
1284
+ required: ["publicationId", "domainId"]
1285
+ }
1286
+ },
1287
+ {
1288
+ name: "domain.delete",
1289
+ description: "Remove a domain from a publication.",
1290
+ inputSchema: {
1291
+ type: "object",
1292
+ properties: {
1293
+ publicationId: { type: "string" },
1294
+ domainId: { type: "string" }
1295
+ },
1296
+ required: ["publicationId", "domainId"]
1297
+ }
1298
+ },
1299
+ // --- Tracking sub-domains (CNAME, under a domain) -------------------------
1300
+ {
1301
+ name: "domain.tracking_create",
1302
+ description: "Add a tracking sub-domain (CNAME) under a domain \u2014 used to serve open-pixel/click-tracking links from your own domain. Returns the CNAME record to add, then call domain.tracking_verify.",
1303
+ inputSchema: {
1304
+ type: "object",
1305
+ properties: {
1306
+ publicationId: { type: "string" },
1307
+ domainId: { type: "string", description: "Parent domain ID." },
1308
+ subdomain: {
1309
+ type: "string",
1310
+ description: "Sub-domain label (lowercase alphanumeric and hyphens), e.g. 'links'."
1311
+ }
1312
+ },
1313
+ required: ["publicationId", "domainId", "subdomain"]
1314
+ }
1315
+ },
1316
+ {
1317
+ name: "domain.tracking_list",
1318
+ description: "List tracking sub-domains for a domain.",
1319
+ inputSchema: {
1320
+ type: "object",
1321
+ properties: {
1322
+ publicationId: { type: "string" },
1323
+ domainId: { type: "string" }
1324
+ },
1325
+ required: ["publicationId", "domainId"]
1326
+ }
1327
+ },
1328
+ {
1329
+ name: "domain.tracking_verify",
1330
+ description: "Verify a tracking sub-domain by checking its CNAME record; status becomes 'verified'.",
1331
+ inputSchema: {
1332
+ type: "object",
1333
+ properties: {
1334
+ publicationId: { type: "string" },
1335
+ domainId: { type: "string" },
1336
+ trackingDomainId: { type: "string" }
1337
+ },
1338
+ required: ["publicationId", "domainId", "trackingDomainId"]
1339
+ }
1340
+ },
1341
+ {
1342
+ name: "domain.tracking_delete",
1343
+ description: "Remove a tracking sub-domain.",
1344
+ inputSchema: {
1345
+ type: "object",
1346
+ properties: {
1347
+ publicationId: { type: "string" },
1348
+ domainId: { type: "string" },
1349
+ trackingDomainId: { type: "string" }
1350
+ },
1351
+ required: ["publicationId", "domainId", "trackingDomainId"]
1352
+ }
1353
+ },
1354
+ // --- Outbound webhooks (REST /v1/webhooks/endpoints) ----------------------
1355
+ {
1356
+ name: "webhook.create",
1357
+ description: "Register an outbound webhook endpoint that receives delivery/engagement events. Returns a signing_secret (shown only once) used to verify payloads.",
1358
+ inputSchema: {
1359
+ type: "object",
1360
+ properties: {
1361
+ publicationId: { type: "string" },
1362
+ endpoint: { type: "string", description: "HTTPS URL to deliver events to." },
1363
+ events: {
1364
+ type: "array",
1365
+ items: { type: "string" },
1366
+ description: "Event types, e.g. email.received, email.sent, email.delivered, email.bounced, email.opened, email.clicked, contact.created, contact.unsubscribed."
1367
+ }
1368
+ },
1369
+ required: ["publicationId", "endpoint", "events"]
1370
+ }
1371
+ },
1372
+ {
1373
+ name: "webhook.list",
1374
+ description: "List outbound webhooks for a publication (signing secrets omitted).",
1375
+ inputSchema: {
1376
+ type: "object",
1377
+ properties: {
1378
+ publicationId: { type: "string" },
1379
+ limit: { type: "number", description: "Max results (1-100, default 20)." }
1380
+ },
1381
+ required: ["publicationId"]
1382
+ }
1383
+ },
1384
+ {
1385
+ name: "webhook.get",
1386
+ description: "Get a single outbound webhook by ID.",
1387
+ inputSchema: {
1388
+ type: "object",
1389
+ properties: {
1390
+ publicationId: { type: "string" },
1391
+ webhookId: { type: "string" }
1392
+ },
1393
+ required: ["publicationId", "webhookId"]
1394
+ }
1395
+ },
1396
+ {
1397
+ name: "webhook.update",
1398
+ description: "Update a webhook's endpoint, subscribed events, or enabled/disabled status.",
1399
+ inputSchema: {
1400
+ type: "object",
1401
+ properties: {
1402
+ publicationId: { type: "string" },
1403
+ webhookId: { type: "string" },
1404
+ endpoint: { type: "string" },
1405
+ events: { type: "array", items: { type: "string" } },
1406
+ status: { type: "string", enum: ["enabled", "disabled"] }
1407
+ },
1408
+ required: ["publicationId", "webhookId"]
1409
+ }
1410
+ },
1411
+ {
1412
+ name: "webhook.delete",
1413
+ description: "Delete an outbound webhook.",
1414
+ inputSchema: {
1415
+ type: "object",
1416
+ properties: {
1417
+ publicationId: { type: "string" },
1418
+ webhookId: { type: "string" }
1419
+ },
1420
+ required: ["publicationId", "webhookId"]
1421
+ }
1422
+ },
1423
+ // --- Audience segments (REST /v1/segments) --------------------------------
1424
+ {
1425
+ name: "segment.create",
1426
+ description: "Create a saved audience segment defined by a status filter and/or a query filter (filter-based, not manual membership).",
1427
+ inputSchema: {
1428
+ type: "object",
1429
+ properties: {
1430
+ publicationId: { type: "string" },
1431
+ name: { type: "string" },
1432
+ description: { type: "string" },
1433
+ status_filter: { type: "string", enum: ["active", "unsubscribed", "suppressed"] },
1434
+ query_filter: { type: "string", description: "Search/filter expression over contacts." }
1435
+ },
1436
+ required: ["publicationId", "name"]
1437
+ }
1438
+ },
1439
+ {
1440
+ name: "segment.list",
1441
+ description: "List saved audience segments for a publication.",
1442
+ inputSchema: {
1443
+ type: "object",
1444
+ properties: {
1445
+ publicationId: { type: "string" },
1446
+ limit: { type: "number", description: "Max results (1-100, default 20)." }
1447
+ },
1448
+ required: ["publicationId"]
1449
+ }
1450
+ },
1451
+ {
1452
+ name: "segment.get",
1453
+ description: "Get a single audience segment by ID.",
1454
+ inputSchema: {
1455
+ type: "object",
1456
+ properties: {
1457
+ publicationId: { type: "string" },
1458
+ segmentId: { type: "string" }
1459
+ },
1460
+ required: ["publicationId", "segmentId"]
1461
+ }
1462
+ },
1463
+ {
1464
+ name: "segment.update",
1465
+ description: "Update an audience segment's name, description, or filters.",
1466
+ inputSchema: {
1467
+ type: "object",
1468
+ properties: {
1469
+ publicationId: { type: "string" },
1470
+ segmentId: { type: "string" },
1471
+ name: { type: "string" },
1472
+ description: { type: "string" },
1473
+ status_filter: {
1474
+ type: ["string", "null"],
1475
+ enum: ["active", "unsubscribed", "suppressed", null],
1476
+ description: "Filter by contact status, or null to clear it."
1477
+ },
1478
+ query_filter: {
1479
+ type: ["string", "null"],
1480
+ description: "Search/filter expression, or null to clear it."
1481
+ }
1482
+ },
1483
+ required: ["publicationId", "segmentId"]
1484
+ }
1485
+ },
1486
+ {
1487
+ name: "segment.delete",
1488
+ description: "Delete an audience segment.",
1489
+ inputSchema: {
1490
+ type: "object",
1491
+ properties: {
1492
+ publicationId: { type: "string" },
1493
+ segmentId: { type: "string" }
1494
+ },
1495
+ required: ["publicationId", "segmentId"]
1496
+ }
1497
+ },
1498
+ // --- Contact custom properties (REST /v1/contact-properties; team-scoped) -
1499
+ {
1500
+ name: "contact_property.create",
1501
+ description: "Define a custom contact property (custom field) for the team. Property values are set per-contact via contact.set_properties.",
1502
+ inputSchema: {
1503
+ type: "object",
1504
+ properties: {
1505
+ key: {
1506
+ type: "string",
1507
+ description: "Property key (starts with a letter; letters, numbers, underscores)."
1508
+ },
1509
+ type: { type: "string", enum: ["string", "number"] },
1510
+ fallback_value: { description: "Default value when a contact has none (matches type)." },
1511
+ description: { type: "string" }
1512
+ },
1513
+ required: ["key", "type"]
1514
+ }
1515
+ },
1516
+ {
1517
+ name: "contact_property.list",
1518
+ description: "List the team's custom contact property definitions.",
1519
+ inputSchema: {
1520
+ type: "object",
1521
+ properties: {
1522
+ limit: { type: "number", description: "Max results (1-100, default 20)." }
1523
+ }
1524
+ }
1525
+ },
1526
+ {
1527
+ name: "contact_property.update",
1528
+ description: "Update a custom contact property's fallback value or description.",
1529
+ inputSchema: {
1530
+ type: "object",
1531
+ properties: {
1532
+ propertyId: { type: "string" },
1533
+ fallback_value: {},
1534
+ description: { type: "string" }
1535
+ },
1536
+ required: ["propertyId"]
1537
+ }
1538
+ },
1539
+ {
1540
+ name: "contact_property.delete",
1541
+ description: "Delete a custom contact property definition.",
1542
+ inputSchema: {
1543
+ type: "object",
1544
+ properties: {
1545
+ propertyId: { type: "string" }
1546
+ },
1547
+ required: ["propertyId"]
1548
+ }
1549
+ },
1550
+ // --- Contact completeness (REST GET/DELETE + tRPC property values) --------
1551
+ {
1552
+ name: "contact.get",
1553
+ description: "Get a single contact by its ID or email address.",
1554
+ inputSchema: {
1555
+ type: "object",
1556
+ properties: {
1557
+ publicationId: { type: "string" },
1558
+ idOrEmail: { type: "string", description: "Contact ID or email address." }
1559
+ },
1560
+ required: ["publicationId", "idOrEmail"]
1561
+ }
1562
+ },
1563
+ {
1564
+ name: "contact.delete",
1565
+ description: "Permanently delete a contact by its ID or email address.",
1566
+ inputSchema: {
1567
+ type: "object",
1568
+ properties: {
1569
+ publicationId: { type: "string" },
1570
+ idOrEmail: { type: "string", description: "Contact ID or email address." }
1571
+ },
1572
+ required: ["publicationId", "idOrEmail"]
1573
+ }
1574
+ },
1575
+ {
1576
+ name: "contact.get_properties",
1577
+ description: "Read a contact's custom property values.",
1578
+ inputSchema: {
1579
+ type: "object",
1580
+ properties: {
1581
+ publicationId: { type: "string" },
1582
+ contactId: { type: "string" }
1583
+ },
1584
+ required: ["publicationId", "contactId"]
1585
+ }
1586
+ },
1587
+ {
1588
+ name: "contact.set_properties",
1589
+ description: "Set a contact's custom property values. Each value references a property by propertyId (from contact_property.list/create).",
1590
+ inputSchema: {
1591
+ type: "object",
1592
+ properties: {
1593
+ publicationId: { type: "string" },
1594
+ contactId: { type: "string" },
1595
+ values: {
1596
+ type: "array",
1597
+ items: {
1598
+ type: "object",
1599
+ properties: {
1600
+ propertyId: { type: "string" },
1601
+ value: { type: "string" }
1602
+ },
1603
+ required: ["propertyId", "value"]
1604
+ }
1605
+ }
1606
+ },
1607
+ required: ["publicationId", "contactId", "values"]
1608
+ }
1609
+ },
1610
+ // --- Tag definitions (REST /v1/tags) -------------------------------------
1611
+ {
1612
+ name: "tag.create",
1613
+ description: "Create a tag definition. NOTE: tags cannot yet be assigned to individual contacts via the API \u2014 this manages tag definitions only.",
1614
+ inputSchema: {
1615
+ type: "object",
1616
+ properties: {
1617
+ publicationId: { type: "string" },
1618
+ name: { type: "string" },
1619
+ default_subscription: { type: "string", enum: ["opt_in", "opt_out"] },
1620
+ description: { type: "string" },
1621
+ visibility: { type: "string", enum: ["public", "private"] }
1622
+ },
1623
+ required: ["publicationId", "name", "default_subscription"]
1624
+ }
1625
+ },
1626
+ {
1627
+ name: "tag.list",
1628
+ description: "List tag definitions for a publication.",
1629
+ inputSchema: {
1630
+ type: "object",
1631
+ properties: {
1632
+ publicationId: { type: "string" },
1633
+ limit: { type: "number", description: "Max results (1-100, default 20)." }
1634
+ },
1635
+ required: ["publicationId"]
1636
+ }
1637
+ },
1638
+ {
1639
+ name: "tag.update",
1640
+ description: "Update a tag definition's name, description, default subscription, or visibility.",
1641
+ inputSchema: {
1642
+ type: "object",
1643
+ properties: {
1644
+ publicationId: { type: "string" },
1645
+ tagId: { type: "string" },
1646
+ name: { type: "string" },
1647
+ description: { type: "string" },
1648
+ default_subscription: { type: "string", enum: ["opt_in", "opt_out"] },
1649
+ visibility: { type: "string", enum: ["public", "private"] }
1650
+ },
1651
+ required: ["publicationId", "tagId"]
1652
+ }
1653
+ },
1654
+ {
1655
+ name: "tag.delete",
1656
+ description: "Delete a tag definition.",
1657
+ inputSchema: {
1658
+ type: "object",
1659
+ properties: {
1660
+ publicationId: { type: "string" },
1661
+ tagId: { type: "string" }
1662
+ },
1663
+ required: ["publicationId", "tagId"]
1664
+ }
1665
+ },
1666
+ // --- API keys (REST /v1/api-keys; requires settings:write) ----------------
1667
+ {
1668
+ name: "api_key.create",
1669
+ description: "Create an API key (PAT). Requires the calling token to hold settings:write AND every scope the new key would grant. The token value is returned ONCE \u2014 store it securely. 'full_access' grants all scopes; 'sending_access' grants issue read/write/send only.",
1670
+ inputSchema: {
1671
+ type: "object",
1672
+ properties: {
1673
+ name: { type: "string", description: "Human label for the key (max 50 chars)." },
1674
+ permission: {
1675
+ type: "string",
1676
+ enum: ["full_access", "sending_access"],
1677
+ description: "Defaults to full_access."
1678
+ },
1679
+ domain_id: { type: "string", description: "Optional publication scope for sending_access." }
1680
+ },
1681
+ required: ["name"]
1682
+ }
1683
+ },
1684
+ {
1685
+ name: "api_key.list",
1686
+ description: "List the team's API keys (token values are never returned).",
1687
+ inputSchema: {
1688
+ type: "object",
1689
+ properties: {}
1690
+ }
1691
+ },
1692
+ {
1693
+ name: "api_key.revoke",
1694
+ description: "Revoke (delete) an API key by its ID.",
1695
+ inputSchema: {
1696
+ type: "object",
1697
+ properties: {
1698
+ keyId: { type: "string" }
1699
+ },
1700
+ required: ["keyId"]
1701
+ }
1702
+ }
1703
+ ];
1704
+ var MCP_RESOURCES = [
1705
+ {
1706
+ uri: "publication://current/brand-guidelines",
1707
+ name: "Brand Guidelines",
1708
+ description: "Publication writing style and visual preferences",
1709
+ mimeType: "application/json"
1710
+ },
1711
+ {
1712
+ uri: "mailtea://capabilities",
1713
+ name: "Mailtea MCP Capabilities",
1714
+ description: "Tool and endpoint capabilities for this MCP runtime",
1715
+ mimeType: "application/json"
1716
+ },
1717
+ {
1718
+ uri: "analytics://current/latest-summary",
1719
+ name: "Latest Analytics Summary",
1720
+ description: "Most recent newsletter engagement snapshot",
1721
+ mimeType: "application/json"
1722
+ }
1723
+ ];
1724
+ var MCP_PROMPTS = [
1725
+ {
1726
+ name: "newsletter.draft_from_brief",
1727
+ description: "Create a newsletter draft from a short brief"
1728
+ },
1729
+ {
1730
+ name: "newsletter.subject_line_pack",
1731
+ description: "Generate subject line variants"
1732
+ }
1733
+ ];
1734
+ function response(id, result) {
1735
+ return {
1736
+ jsonrpc: "2.0",
1737
+ id,
1738
+ result
1739
+ };
1740
+ }
1741
+ function error(id, code, message, data) {
1742
+ return {
1743
+ jsonrpc: "2.0",
1744
+ id,
1745
+ error: {
1746
+ code,
1747
+ message,
1748
+ data
1749
+ }
1750
+ };
1751
+ }
1752
+ function asObject(value) {
1753
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1754
+ return {};
1755
+ }
1756
+ return value;
1757
+ }
1758
+ function asOptionalString(value) {
1759
+ if (typeof value !== "string") {
1760
+ return void 0;
1761
+ }
1762
+ const trimmed = value.trim();
1763
+ return trimmed.length > 0 ? trimmed : void 0;
1764
+ }
1765
+ function readRequiredString(args, key) {
1766
+ const value = asOptionalString(args[key]);
1767
+ if (!value) {
1768
+ throw new Error(`Missing required string argument: ${key}`);
1769
+ }
1770
+ return value;
1771
+ }
1772
+ function readIssueAnalyticsRange(args, key) {
1773
+ const value = asOptionalString(args[key]);
1774
+ return parseIssueAnalyticsRange(value, key);
1775
+ }
1776
+ function readIssueAnalyticsExportType(args, key) {
1777
+ const value = asOptionalString(args[key]);
1778
+ if (!value) {
1779
+ return void 0;
1780
+ }
1781
+ if (value === "combined" || value === "performance" || value === "polls") {
1782
+ return value;
1783
+ }
1784
+ throw new Error(`Argument ${key} must be one of: combined, performance, polls`);
1785
+ }
1786
+ function parseIssueAnalyticsRange(value, key) {
1787
+ if (!value) {
1788
+ return void 0;
1789
+ }
1790
+ if (value === "24h" || value === "7d" || value === "30d" || value === "all") {
1791
+ return value;
1792
+ }
1793
+ throw new Error(`Argument ${key} must be one of: 24h, 7d, 30d, all`);
1794
+ }
1795
+ function readContactStatus(args, key) {
1796
+ const value = asOptionalString(args[key]);
1797
+ if (!value) {
1798
+ return void 0;
1799
+ }
1800
+ if (value === "active" || value === "unsubscribed" || value === "suppressed") {
1801
+ return value;
1802
+ }
1803
+ throw new Error(`Argument ${key} must be one of: active, unsubscribed, suppressed`);
1804
+ }
1805
+ function readSponsorOfferStatus(args, key) {
1806
+ const value = asOptionalString(args[key]);
1807
+ if (!value) {
1808
+ return void 0;
1809
+ }
1810
+ if (value === "draft" || value === "active" || value === "paused" || value === "archived") {
1811
+ return value;
1812
+ }
1813
+ throw new Error(`Argument ${key} must be one of: draft, active, paused, archived`);
1814
+ }
1815
+ function readSponsorOfferPricingModel(args, key) {
1816
+ const value = asOptionalString(args[key]);
1817
+ if (!value) {
1818
+ return void 0;
1819
+ }
1820
+ if (value === "flat" || value === "cpm") {
1821
+ return value;
1822
+ }
1823
+ throw new Error(`Argument ${key} must be one of: flat, cpm`);
1824
+ }
1825
+ function readOptionalNumber(args, key) {
1826
+ const value = args[key];
1827
+ if (value === void 0) {
1828
+ return void 0;
1829
+ }
1830
+ if (typeof value === "string" && value.trim() !== "") {
1831
+ const parsed = Number(value);
1832
+ if (Number.isFinite(parsed)) {
1833
+ return parsed;
1834
+ }
1835
+ }
1836
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1837
+ throw new Error(`Argument ${key} must be a finite number`);
1838
+ }
1839
+ return value;
1840
+ }
1841
+ function sleep(ms) {
1842
+ return new Promise((resolve) => {
1843
+ setTimeout(resolve, ms);
1844
+ });
1845
+ }
1846
+ function readOptionalBoolean(args, key) {
1847
+ const value = args[key];
1848
+ if (value === void 0) {
1849
+ return void 0;
1850
+ }
1851
+ if (typeof value !== "boolean") {
1852
+ throw new Error(`Argument ${key} must be a boolean`);
1853
+ }
1854
+ return value;
1855
+ }
1856
+ function readOptionalJsonObject(args, key) {
1857
+ const value = args[key];
1858
+ if (value === void 0) {
1859
+ return void 0;
1860
+ }
1861
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1862
+ throw new Error(`Argument ${key} must be a JSON object`);
1863
+ }
1864
+ return value;
1865
+ }
1866
+ function readRequiredJsonObjectArray(args, key) {
1867
+ const value = args[key];
1868
+ if (!Array.isArray(value) || value.length === 0) {
1869
+ throw new Error(`Argument ${key} must be a non-empty array of objects`);
1870
+ }
1871
+ return value.map((item) => {
1872
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1873
+ throw new Error(`Argument ${key} must be a non-empty array of objects`);
1874
+ }
1875
+ return item;
1876
+ });
1877
+ }
1878
+ function readRequiredPackSections(args, key) {
1879
+ const rows = readRequiredJsonObjectArray(args, key);
1880
+ return rows.map((row) => {
1881
+ const name = asOptionalString(row.name);
1882
+ if (!name) {
1883
+ throw new Error(`Argument ${key} item requires a non-empty name`);
1884
+ }
1885
+ const contentJson = row.contentJson;
1886
+ if (!Array.isArray(contentJson) || contentJson.length === 0) {
1887
+ throw new Error(`Argument ${key} item requires non-empty contentJson`);
1888
+ }
1889
+ const nodes = contentJson.flatMap((node) => {
1890
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
1891
+ return [];
1892
+ }
1893
+ return [node];
1894
+ });
1895
+ if (nodes.length === 0) {
1896
+ throw new Error(`Argument ${key} item requires non-empty contentJson`);
1897
+ }
1898
+ return {
1899
+ name,
1900
+ contentJson: nodes
1901
+ };
1902
+ });
1903
+ }
1904
+ function resolveApiBaseUrl(options) {
1905
+ return (options.apiBaseUrl ?? process.env.MAILTEA_API_BASE_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
1906
+ }
1907
+ function resolveToken(options) {
1908
+ return options.token ?? process.env.MAILTEA_API_TOKEN ?? null;
1909
+ }
1910
+ function resolvePublicationId(options) {
1911
+ return asOptionalString(options.publicationId ?? void 0) ?? asOptionalString(process.env.MAILTEA_PUBLICATION_ID) ?? null;
1912
+ }
1913
+ function parseAnalyticsSummaryUri(uri) {
1914
+ let parsed;
1915
+ try {
1916
+ parsed = new URL(uri);
1917
+ } catch {
1918
+ return null;
1919
+ }
1920
+ if (parsed.protocol !== "analytics:" || parsed.hostname !== "current" || parsed.pathname !== "/latest-summary") {
1921
+ return null;
1922
+ }
1923
+ const publicationId = asOptionalString(parsed.searchParams.get("publicationId"));
1924
+ const range = parseIssueAnalyticsRange(asOptionalString(parsed.searchParams.get("range")), "range") ?? "7d";
1925
+ return { publicationId, range };
1926
+ }
1927
+ async function loadLatestAnalyticsSummary(options, input) {
1928
+ const publicationId = input.publicationId ?? resolvePublicationId(options);
1929
+ if (!publicationId) {
1930
+ return {
1931
+ status: "missing_publication_context",
1932
+ message: "Set MAILTEA_PUBLICATION_ID or pass publicationId in the request.",
1933
+ exampleUri: "analytics://current/latest-summary?publicationId=pub_demo"
1934
+ };
1935
+ }
1936
+ const summary = await callTrpc(
1937
+ "issue.latestSummary",
1938
+ {
1939
+ publicationId,
1940
+ range: input.range
1941
+ },
1942
+ options,
1943
+ "query"
1944
+ );
1945
+ return summary;
1946
+ }
1947
+ function makeToolResult(text, structuredContent) {
1948
+ return {
1949
+ content: [
1950
+ {
1951
+ type: "text",
1952
+ text
1953
+ }
1954
+ ],
1955
+ ...structuredContent ? { structuredContent } : {}
1956
+ };
1957
+ }
1958
+ function toErrorMessage(err) {
1959
+ return err instanceof Error ? err.message : "Internal error";
1960
+ }
1961
+ async function callTrpc(path, input, options, procedureType = "mutation") {
1962
+ const fetchImpl = options.fetchImpl ?? fetch;
1963
+ const apiBaseUrl = resolveApiBaseUrl(options);
1964
+ const token = resolveToken(options);
1965
+ if (!token) {
1966
+ throw new Error(
1967
+ "Missing API token. Set MAILTEA_API_TOKEN or pass Authorization header when using /mcp."
1968
+ );
1969
+ }
1970
+ const headers = {
1971
+ authorization: `Bearer ${token}`
1972
+ };
1973
+ const request = procedureType === "query" ? {
1974
+ url: `${apiBaseUrl}/trpc/${path}?input=${encodeURIComponent(
1975
+ JSON.stringify(input ?? {})
1976
+ )}`,
1977
+ init: {
1978
+ method: "GET",
1979
+ headers
1980
+ }
1981
+ } : {
1982
+ url: `${apiBaseUrl}/trpc/${path}`,
1983
+ init: {
1984
+ method: "POST",
1985
+ headers: {
1986
+ ...headers,
1987
+ "content-type": "application/json"
1988
+ },
1989
+ body: JSON.stringify(input)
1990
+ }
1991
+ };
1992
+ const httpResponse = await fetchImpl(request.url, request.init);
1993
+ const payload = await httpResponse.json().catch(() => null);
1994
+ if (!httpResponse.ok || payload?.error) {
1995
+ throw new Error(
1996
+ payload?.error?.message ?? `${httpResponse.status} ${httpResponse.statusText}`
1997
+ );
1998
+ }
1999
+ if (!payload?.result || typeof payload.result !== "object" || !("data" in payload.result)) {
2000
+ throw new Error(`Malformed tRPC response for ${path}`);
2001
+ }
2002
+ return payload.result.data;
2003
+ }
2004
+ async function callRestApi(method, path, body, options) {
2005
+ const fetchImpl = options.fetchImpl ?? fetch;
2006
+ const apiBaseUrl = resolveApiBaseUrl(options);
2007
+ const token = resolveToken(options);
2008
+ if (!token) {
2009
+ throw new Error("Missing API token.");
2010
+ }
2011
+ const headers = {
2012
+ authorization: `Bearer ${token}`
2013
+ };
2014
+ const init = { method, headers };
2015
+ if (body !== void 0) {
2016
+ headers["content-type"] = "application/json";
2017
+ init.body = JSON.stringify(body);
2018
+ }
2019
+ const httpResponse = await fetchImpl(`${apiBaseUrl}${path}`, init);
2020
+ const payload = await httpResponse.json().catch(() => null);
2021
+ if (!httpResponse.ok) {
2022
+ throw new Error(
2023
+ payload?.error ?? `${httpResponse.status} ${httpResponse.statusText}`
2024
+ );
2025
+ }
2026
+ return payload;
2027
+ }
2028
+ var EMAIL_SEND_FIELDS = [
2029
+ "from",
2030
+ "to",
2031
+ "subject",
2032
+ "html",
2033
+ "text",
2034
+ "template",
2035
+ "cc",
2036
+ "bcc",
2037
+ "reply_to",
2038
+ "scheduled_at",
2039
+ "tags",
2040
+ "headers",
2041
+ "attachments"
2042
+ ];
2043
+ async function runTool(toolName, args, options) {
2044
+ if (toolName === "auth.me") {
2045
+ const me = await callTrpc("auth.me", {}, options, "query");
2046
+ return makeToolResult(
2047
+ me.userId ? `Authenticated as ${me.role} (${me.userId})` : "Not authenticated",
2048
+ me
2049
+ );
2050
+ }
2051
+ if (toolName === "issue.create_draft") {
2052
+ const publicationId = readRequiredString(args, "publicationId");
2053
+ const title = readRequiredString(args, "title");
2054
+ const kind = args.kind === "broadcast" ? "broadcast" : args.kind === "newsletter" ? "newsletter" : void 0;
2055
+ const templateId = asOptionalString(args.templateId);
2056
+ const variables = args.variables;
2057
+ const contentHtml = asOptionalString(args.contentHtml);
2058
+ const contentSpec = args.contentSpec;
2059
+ let resolvedHtml = contentHtml;
2060
+ if (!templateId && contentSpec && typeof contentSpec === "object" && contentSpec.root) {
2061
+ const rendered = await callRestApi(
2062
+ "POST",
2063
+ "/v1/templates/render",
2064
+ { spec: contentSpec },
2065
+ options
2066
+ );
2067
+ resolvedHtml = rendered.html;
2068
+ }
2069
+ const draft = await callTrpc(
2070
+ "issue.createDraft",
2071
+ {
2072
+ publicationId,
2073
+ title,
2074
+ ...kind ? { kind } : {},
2075
+ ...templateId ? { templateId, ...variables ? { variables } : {} } : resolvedHtml ? {
2076
+ contentJson: {
2077
+ type: "mailtea.rich.v1",
2078
+ html: resolvedHtml
2079
+ }
2080
+ } : {}
2081
+ },
2082
+ options
2083
+ );
2084
+ return makeToolResult(`Draft created: ${draft.id}`, { issue: draft });
2085
+ }
2086
+ if (toolName === "issue.get_editor") {
2087
+ const issueId = readRequiredString(args, "issueId");
2088
+ const issue = await callTrpc(
2089
+ "issue.getEditor",
2090
+ { issueId },
2091
+ options,
2092
+ "query"
2093
+ );
2094
+ return makeToolResult(`Loaded editor state for ${issue.id} (${issue.status})`, { issue });
2095
+ }
2096
+ if (toolName === "issue.update_draft") {
2097
+ const issueId = readRequiredString(args, "issueId");
2098
+ const title = readRequiredString(args, "title");
2099
+ const contentHtml = asOptionalString(args.contentHtml);
2100
+ const contentSpec = args.contentSpec;
2101
+ let resolvedHtml = contentHtml;
2102
+ if (contentSpec && typeof contentSpec === "object" && contentSpec.root) {
2103
+ const rendered = await callRestApi(
2104
+ "POST",
2105
+ "/v1/templates/render",
2106
+ { spec: contentSpec },
2107
+ options
2108
+ );
2109
+ resolvedHtml = rendered.html;
2110
+ }
2111
+ const draft = await callTrpc(
2112
+ "issue.updateDraft",
2113
+ {
2114
+ issueId,
2115
+ title,
2116
+ ...resolvedHtml ? {
2117
+ contentJson: {
2118
+ type: "mailtea.rich.v1",
2119
+ html: resolvedHtml
2120
+ }
2121
+ } : {}
2122
+ },
2123
+ options
2124
+ );
2125
+ return makeToolResult(`Draft updated: ${draft.id}`, { issue: draft });
2126
+ }
2127
+ if (toolName === "issue.remove_draft") {
2128
+ const issueId = readRequiredString(args, "issueId");
2129
+ const removed = await callTrpc(
2130
+ "issue.removeDraft",
2131
+ { issueId },
2132
+ options
2133
+ );
2134
+ return makeToolResult(`Draft removed: ${removed.issueId}`, removed);
2135
+ }
2136
+ if (toolName === "issue.list_recent") {
2137
+ const publicationId = readRequiredString(args, "publicationId");
2138
+ const requestedLimit = readOptionalNumber(args, "limit");
2139
+ const limit = Math.max(1, Math.min(50, Math.trunc(requestedLimit ?? 10)));
2140
+ const rows = await callTrpc(
2141
+ "issue.listRecent",
2142
+ { publicationId, limit },
2143
+ options,
2144
+ "query"
2145
+ );
2146
+ return makeToolResult(
2147
+ rows.length === 0 ? `No issues found for ${publicationId}` : `Loaded ${rows.length} issues for ${publicationId}`,
2148
+ {
2149
+ publicationId,
2150
+ issues: rows
2151
+ }
2152
+ );
2153
+ }
2154
+ if (toolName === "publication.list") {
2155
+ const workspaces = await callTrpc(
2156
+ "publication.listMine",
2157
+ {},
2158
+ options,
2159
+ "query"
2160
+ );
2161
+ return makeToolResult(
2162
+ workspaces.length === 0 ? "No publication workspaces available for this identity yet" : `Loaded ${workspaces.length} publication workspaces`,
2163
+ {
2164
+ publications: workspaces
2165
+ }
2166
+ );
2167
+ }
2168
+ if (toolName === "publication.create") {
2169
+ const publicationId = asOptionalString(args.publicationId);
2170
+ const name = readRequiredString(args, "name");
2171
+ const timezone = asOptionalString(args.timezone);
2172
+ const result = await callTrpc(
2173
+ "publication.create",
2174
+ {
2175
+ ...publicationId ? { publicationId } : {},
2176
+ name,
2177
+ ...timezone ? { timezone } : {}
2178
+ },
2179
+ options
2180
+ );
2181
+ return makeToolResult(`Publication created: ${result.publication.id}`, result);
2182
+ }
2183
+ if (toolName === "publication.domain_list") {
2184
+ const publicationId = readRequiredString(args, "publicationId");
2185
+ const requestedLimit = readOptionalNumber(args, "limit");
2186
+ const limit = Math.max(1, Math.min(100, Math.trunc(requestedLimit ?? 50)));
2187
+ const domains = await callTrpc(
2188
+ "publication.domains",
2189
+ {
2190
+ publicationId,
2191
+ limit
2192
+ },
2193
+ options,
2194
+ "query"
2195
+ );
2196
+ return makeToolResult(
2197
+ domains.length === 0 ? `No custom domains configured for ${publicationId}` : `Loaded ${domains.length} custom domains for ${publicationId}`,
2198
+ { publicationId, domains }
2199
+ );
2200
+ }
2201
+ if (toolName === "publication.domain_upsert") {
2202
+ const publicationId = readRequiredString(args, "publicationId");
2203
+ const host = readRequiredString(args, "host");
2204
+ const isPrimary = readOptionalBoolean(args, "isPrimary");
2205
+ const proxyTarget = asOptionalString(args.proxyTarget);
2206
+ const result = await callTrpc(
2207
+ "publication.domainUpsert",
2208
+ {
2209
+ publicationId,
2210
+ host,
2211
+ ...typeof isPrimary === "boolean" ? { isPrimary } : {},
2212
+ ...proxyTarget ? { proxyTarget } : {}
2213
+ },
2214
+ options
2215
+ );
2216
+ return makeToolResult(
2217
+ `Publication domain saved: ${result.domain.host} (${result.domain.status})`,
2218
+ result
2219
+ );
2220
+ }
2221
+ if (toolName === "publication.domain_verify") {
2222
+ const publicationId = readRequiredString(args, "publicationId");
2223
+ const domainId = readRequiredString(args, "domainId");
2224
+ const verificationValue = asOptionalString(args.verificationValue);
2225
+ const result = await callTrpc(
2226
+ "publication.domainVerify",
2227
+ {
2228
+ publicationId,
2229
+ domainId,
2230
+ ...verificationValue ? { verificationValue } : {}
2231
+ },
2232
+ options
2233
+ );
2234
+ return makeToolResult(
2235
+ `Publication domain verified: ${result.domain.host}`,
2236
+ result
2237
+ );
2238
+ }
2239
+ if (toolName === "publication.domain_set_primary") {
2240
+ const publicationId = readRequiredString(args, "publicationId");
2241
+ const domainId = readRequiredString(args, "domainId");
2242
+ const result = await callTrpc(
2243
+ "publication.domainSetPrimary",
2244
+ {
2245
+ publicationId,
2246
+ domainId
2247
+ },
2248
+ options
2249
+ );
2250
+ return makeToolResult(`Primary domain set: ${result.domain.host}`, result);
2251
+ }
2252
+ if (toolName === "publication.domain_remove") {
2253
+ const publicationId = readRequiredString(args, "publicationId");
2254
+ const domainId = readRequiredString(args, "domainId");
2255
+ const result = await callTrpc(
2256
+ "publication.domainRemove",
2257
+ {
2258
+ publicationId,
2259
+ domainId
2260
+ },
2261
+ options
2262
+ );
2263
+ return makeToolResult(`Publication domain removed: ${result.domainId}`, result);
2264
+ }
2265
+ if (toolName === "publication.domain_traefik_preview") {
2266
+ const publicationId = readRequiredString(args, "publicationId");
2267
+ const preview = await callTrpc(
2268
+ "publication.domainTraefikPreview",
2269
+ {
2270
+ publicationId
2271
+ },
2272
+ options,
2273
+ "query"
2274
+ );
2275
+ return makeToolResult(
2276
+ `Generated Traefik preview for ${publicationId} (${preview.entries.length} domains)`,
2277
+ preview
2278
+ );
2279
+ }
2280
+ if (toolName === "contact.list") {
2281
+ const publicationId = readRequiredString(args, "publicationId");
2282
+ const status = readContactStatus(args, "status");
2283
+ const query = asOptionalString(args.query);
2284
+ const requestedLimit = readOptionalNumber(args, "limit");
2285
+ const limit = Math.max(1, Math.min(500, Math.trunc(requestedLimit ?? 200)));
2286
+ const contacts = await callTrpc(
2287
+ "contact.list",
2288
+ {
2289
+ publicationId,
2290
+ ...status ? { status } : {},
2291
+ ...query ? { query } : {},
2292
+ limit
2293
+ },
2294
+ options,
2295
+ "query"
2296
+ );
2297
+ return makeToolResult(
2298
+ contacts.length === 0 ? `No contacts found for ${publicationId}` : `Loaded ${contacts.length} contacts for ${publicationId}`,
2299
+ {
2300
+ publicationId,
2301
+ contacts
2302
+ }
2303
+ );
2304
+ }
2305
+ if (toolName === "contact.upsert") {
2306
+ const publicationId = readRequiredString(args, "publicationId");
2307
+ const email = readRequiredString(args, "email");
2308
+ const referrerContactId = asOptionalString(args.referrerContactId);
2309
+ const contact = await callTrpc(
2310
+ "contact.upsert",
2311
+ {
2312
+ publicationId,
2313
+ email,
2314
+ ...referrerContactId ? { referrerContactId } : {}
2315
+ },
2316
+ options
2317
+ );
2318
+ return makeToolResult(`Contact saved: ${contact.id} (${contact.status})`, {
2319
+ contact
2320
+ });
2321
+ }
2322
+ if (toolName === "contact.set_status") {
2323
+ const publicationId = readRequiredString(args, "publicationId");
2324
+ const contactId = readRequiredString(args, "contactId");
2325
+ const status = readContactStatus(args, "status");
2326
+ if (!status) {
2327
+ throw new Error("status is required");
2328
+ }
2329
+ const contact = await callTrpc(
2330
+ "contact.setStatus",
2331
+ {
2332
+ publicationId,
2333
+ contactId,
2334
+ status
2335
+ },
2336
+ options
2337
+ );
2338
+ return makeToolResult(`Contact status updated: ${contact.id} -> ${contact.status}`, {
2339
+ contact
2340
+ });
2341
+ }
2342
+ if (toolName === "contact.import_csv") {
2343
+ const publicationId = readRequiredString(args, "publicationId");
2344
+ const csvText = readRequiredString(args, "csvText");
2345
+ const result = await callTrpc(
2346
+ "contact.importCsv",
2347
+ {
2348
+ publicationId,
2349
+ csvText
2350
+ },
2351
+ options
2352
+ );
2353
+ return makeToolResult(
2354
+ `Contact import complete for ${publicationId}: +${result.createdCount} new, ${result.reactivatedCount} reactivated, ${result.invalidCount} invalid`,
2355
+ result
2356
+ );
2357
+ }
2358
+ if (toolName === "contact.referral_summary") {
2359
+ const publicationId = readRequiredString(args, "publicationId");
2360
+ const requestedLimit = readOptionalNumber(args, "limit");
2361
+ const limit = Math.max(1, Math.min(100, Math.trunc(requestedLimit ?? 20)));
2362
+ const summary = await callTrpc(
2363
+ "contact.referralSummary",
2364
+ {
2365
+ publicationId,
2366
+ limit
2367
+ },
2368
+ options,
2369
+ "query"
2370
+ );
2371
+ return makeToolResult(
2372
+ summary.totalReferrals === 0 ? `No referrals tracked yet for ${publicationId}` : `Loaded referral summary for ${publicationId}: ${summary.totalReferrals} total referrals`,
2373
+ summary
2374
+ );
2375
+ }
2376
+ if (toolName === "contact.referral_milestones") {
2377
+ const publicationId = readRequiredString(args, "publicationId");
2378
+ const requestedLimit = readOptionalNumber(args, "limit");
2379
+ const limit = Math.max(1, Math.min(200, Math.trunc(requestedLimit ?? 100)));
2380
+ const milestones = await callTrpc(
2381
+ "contact.referralMilestones",
2382
+ {
2383
+ publicationId,
2384
+ limit
2385
+ },
2386
+ options,
2387
+ "query"
2388
+ );
2389
+ return makeToolResult(
2390
+ milestones.length === 0 ? `No referral milestones configured for ${publicationId}` : `Loaded ${milestones.length} referral milestones for ${publicationId}`,
2391
+ { publicationId, milestones }
2392
+ );
2393
+ }
2394
+ if (toolName === "contact.referral_milestone_upsert") {
2395
+ const publicationId = readRequiredString(args, "publicationId");
2396
+ const title = readRequiredString(args, "title");
2397
+ const description = asOptionalString(args.description);
2398
+ const milestoneId = asOptionalString(args.milestoneId);
2399
+ const referralCountInput = readOptionalNumber(args, "referralCount");
2400
+ if (typeof referralCountInput !== "number" || !Number.isFinite(referralCountInput)) {
2401
+ throw new Error("referralCount is required and must be a number");
2402
+ }
2403
+ const referralCount = Math.max(1, Math.trunc(referralCountInput));
2404
+ const result = await callTrpc(
2405
+ "contact.upsertReferralMilestone",
2406
+ {
2407
+ publicationId,
2408
+ ...milestoneId ? { milestoneId } : {},
2409
+ title,
2410
+ ...description ? { description } : {},
2411
+ referralCount
2412
+ },
2413
+ options
2414
+ );
2415
+ return makeToolResult(
2416
+ `Referral milestone saved: ${result.milestone.id} (${result.rewardedCount} rewards granted)`,
2417
+ result
2418
+ );
2419
+ }
2420
+ if (toolName === "contact.referral_milestone_remove") {
2421
+ const publicationId = readRequiredString(args, "publicationId");
2422
+ const milestoneId = readRequiredString(args, "milestoneId");
2423
+ const result = await callTrpc(
2424
+ "contact.removeReferralMilestone",
2425
+ {
2426
+ publicationId,
2427
+ milestoneId
2428
+ },
2429
+ options
2430
+ );
2431
+ return makeToolResult(`Referral milestone removed: ${result.milestoneId}`, result);
2432
+ }
2433
+ if (toolName === "contact.referral_rewards") {
2434
+ const publicationId = readRequiredString(args, "publicationId");
2435
+ const contactId = asOptionalString(args.contactId);
2436
+ const requestedLimit = readOptionalNumber(args, "limit");
2437
+ const limit = Math.max(1, Math.min(500, Math.trunc(requestedLimit ?? 100)));
2438
+ const rewards = await callTrpc(
2439
+ "contact.referralRewards",
2440
+ {
2441
+ publicationId,
2442
+ ...contactId ? { contactId } : {},
2443
+ limit
2444
+ },
2445
+ options,
2446
+ "query"
2447
+ );
2448
+ return makeToolResult(
2449
+ rewards.length === 0 ? `No referral rewards found for ${publicationId}` : `Loaded ${rewards.length} referral rewards for ${publicationId}`,
2450
+ { publicationId, rewards }
2451
+ );
2452
+ }
2453
+ if (toolName === "monetize.offer_list") {
2454
+ const publicationId = readRequiredString(args, "publicationId");
2455
+ const status = readSponsorOfferStatus(args, "status");
2456
+ const requestedLimit = readOptionalNumber(args, "limit");
2457
+ const limit = Math.max(1, Math.min(200, Math.trunc(requestedLimit ?? 100)));
2458
+ const offers = await callTrpc(
2459
+ "monetize.listOffers",
2460
+ {
2461
+ publicationId,
2462
+ ...status ? { status } : {},
2463
+ limit
2464
+ },
2465
+ options,
2466
+ "query"
2467
+ );
2468
+ return makeToolResult(
2469
+ offers.length === 0 ? `No sponsor offers found for ${publicationId}` : `Loaded ${offers.length} sponsor offers for ${publicationId}`,
2470
+ { publicationId, offers }
2471
+ );
2472
+ }
2473
+ if (toolName === "monetize.offer_upsert") {
2474
+ const publicationId = readRequiredString(args, "publicationId");
2475
+ const offerId = asOptionalString(args.offerId);
2476
+ const title = readRequiredString(args, "title");
2477
+ const sponsorName = readRequiredString(args, "sponsorName");
2478
+ const description = asOptionalString(args.description);
2479
+ const pricingModel = readSponsorOfferPricingModel(args, "pricingModel");
2480
+ if (!pricingModel) {
2481
+ throw new Error("pricingModel is required");
2482
+ }
2483
+ const rateCentsInput = readOptionalNumber(args, "rateCents");
2484
+ if (typeof rateCentsInput !== "number" || !Number.isFinite(rateCentsInput)) {
2485
+ throw new Error("rateCents is required and must be a number");
2486
+ }
2487
+ const rateCents = Math.max(1, Math.trunc(rateCentsInput));
2488
+ const estimatedPlacementsInput = readOptionalNumber(args, "estimatedPlacements");
2489
+ const estimatedPlacements = typeof estimatedPlacementsInput === "number" && Number.isFinite(estimatedPlacementsInput) ? Math.max(1, Math.trunc(estimatedPlacementsInput)) : void 0;
2490
+ const status = readSponsorOfferStatus(args, "status");
2491
+ const startsAt = asOptionalString(args.startsAt);
2492
+ const endsAt = asOptionalString(args.endsAt);
2493
+ const result = await callTrpc(
2494
+ "monetize.upsertOffer",
2495
+ {
2496
+ publicationId,
2497
+ ...offerId ? { offerId } : {},
2498
+ title,
2499
+ sponsorName,
2500
+ ...description ? { description } : {},
2501
+ pricingModel,
2502
+ rateCents,
2503
+ ...typeof estimatedPlacements === "number" ? { estimatedPlacements } : {},
2504
+ ...status ? { status } : {},
2505
+ ...startsAt ? { startsAt } : {},
2506
+ ...endsAt ? { endsAt } : {}
2507
+ },
2508
+ options
2509
+ );
2510
+ return makeToolResult(`Sponsor offer saved: ${result.offer.id}`, result);
2511
+ }
2512
+ if (toolName === "monetize.offer_remove") {
2513
+ const publicationId = readRequiredString(args, "publicationId");
2514
+ const offerId = readRequiredString(args, "offerId");
2515
+ const result = await callTrpc(
2516
+ "monetize.removeOffer",
2517
+ {
2518
+ publicationId,
2519
+ offerId
2520
+ },
2521
+ options
2522
+ );
2523
+ return makeToolResult(`Sponsor offer removed: ${result.offerId}`, result);
2524
+ }
2525
+ if (toolName === "section.list") {
2526
+ const publicationId = readRequiredString(args, "publicationId");
2527
+ const requestedLimit = readOptionalNumber(args, "limit");
2528
+ const limit = Math.max(1, Math.min(200, Math.trunc(requestedLimit ?? 100)));
2529
+ const rows = await callTrpc(
2530
+ "section.list",
2531
+ { publicationId, limit },
2532
+ options,
2533
+ "query"
2534
+ );
2535
+ return makeToolResult(
2536
+ rows.length === 0 ? `No reusable sections found for ${publicationId}` : `Loaded ${rows.length} reusable sections for ${publicationId}`,
2537
+ {
2538
+ publicationId,
2539
+ sections: rows
2540
+ }
2541
+ );
2542
+ }
2543
+ if (toolName === "section.catalog") {
2544
+ const publicationId = asOptionalString(args.publicationId);
2545
+ const packs = await callTrpc(
2546
+ "section.catalog",
2547
+ publicationId ? { publicationId } : {},
2548
+ options,
2549
+ "query"
2550
+ );
2551
+ return makeToolResult(
2552
+ packs.length === 0 ? "No marketplace packs available" : `Loaded ${packs.length} marketplace packs`,
2553
+ { packs }
2554
+ );
2555
+ }
2556
+ if (toolName === "section.pack_create") {
2557
+ const publicationId = readRequiredString(args, "publicationId");
2558
+ const title = readRequiredString(args, "title");
2559
+ const description = asOptionalString(args.description);
2560
+ const styleProfile = readOptionalJsonObject(args, "styleProfile");
2561
+ const sections = readRequiredPackSections(args, "sections");
2562
+ const result = await callTrpc(
2563
+ "section.createPack",
2564
+ {
2565
+ publicationId,
2566
+ title,
2567
+ ...description ? { description } : {},
2568
+ ...styleProfile ? { styleProfile } : {},
2569
+ sections
2570
+ },
2571
+ options
2572
+ );
2573
+ return makeToolResult(`Marketplace pack created: ${result.pack.id}`, result);
2574
+ }
2575
+ if (toolName === "section.pack_update") {
2576
+ const publicationId = readRequiredString(args, "publicationId");
2577
+ const packId = readRequiredString(args, "packId");
2578
+ const title = readRequiredString(args, "title");
2579
+ const description = asOptionalString(args.description);
2580
+ const styleProfile = readOptionalJsonObject(args, "styleProfile");
2581
+ const sections = readRequiredPackSections(args, "sections");
2582
+ const result = await callTrpc(
2583
+ "section.updatePack",
2584
+ {
2585
+ publicationId,
2586
+ packId,
2587
+ title,
2588
+ ...description ? { description } : {},
2589
+ ...styleProfile ? { styleProfile } : {},
2590
+ sections
2591
+ },
2592
+ options
2593
+ );
2594
+ return makeToolResult(`Marketplace pack updated: ${result.pack.id}`, result);
2595
+ }
2596
+ if (toolName === "section.pack_remove") {
2597
+ const publicationId = readRequiredString(args, "publicationId");
2598
+ const packId = readRequiredString(args, "packId");
2599
+ const result = await callTrpc(
2600
+ "section.removePack",
2601
+ {
2602
+ publicationId,
2603
+ packId
2604
+ },
2605
+ options
2606
+ );
2607
+ return makeToolResult(`Marketplace pack removed: ${result.packId}`, result);
2608
+ }
2609
+ if (toolName === "section.pack_revisions") {
2610
+ const publicationId = readRequiredString(args, "publicationId");
2611
+ const packId = readRequiredString(args, "packId");
2612
+ const requestedLimit = readOptionalNumber(args, "limit");
2613
+ const limit = Math.max(1, Math.min(100, Math.trunc(requestedLimit ?? 30)));
2614
+ const result = await callTrpc(
2615
+ "section.revisions",
2616
+ {
2617
+ publicationId,
2618
+ packId,
2619
+ limit
2620
+ },
2621
+ options,
2622
+ "query"
2623
+ );
2624
+ return makeToolResult(
2625
+ result.length === 0 ? `No revisions found for ${packId}` : `Loaded ${result.length} revisions for ${packId}`,
2626
+ {
2627
+ publicationId,
2628
+ packId,
2629
+ revisions: result
2630
+ }
2631
+ );
2632
+ }
2633
+ if (toolName === "section.pack_restore_revision") {
2634
+ const publicationId = readRequiredString(args, "publicationId");
2635
+ const packId = readRequiredString(args, "packId");
2636
+ const revisionId = readRequiredString(args, "revisionId");
2637
+ const result = await callTrpc(
2638
+ "section.restorePackRevision",
2639
+ {
2640
+ publicationId,
2641
+ packId,
2642
+ revisionId
2643
+ },
2644
+ options
2645
+ );
2646
+ return makeToolResult(
2647
+ `Marketplace pack restored: ${result.pack.id} (from v${result.restoredFrom.version} -> v${result.createdRevision.version})`,
2648
+ result
2649
+ );
2650
+ }
2651
+ if (toolName === "section.import_pack") {
2652
+ const publicationId = readRequiredString(args, "publicationId");
2653
+ const presetId = readRequiredString(args, "presetId");
2654
+ const result = await callTrpc(
2655
+ "section.importPack",
2656
+ {
2657
+ publicationId,
2658
+ presetId
2659
+ },
2660
+ options
2661
+ );
2662
+ return makeToolResult(
2663
+ `Imported pack ${result.presetId}: ${result.createdCount} created, ${result.updatedCount} updated`,
2664
+ result
2665
+ );
2666
+ }
2667
+ if (toolName === "section.create") {
2668
+ const publicationId = readRequiredString(args, "publicationId");
2669
+ const name = readRequiredString(args, "name");
2670
+ const contentJson = readRequiredJsonObjectArray(args, "contentJson");
2671
+ const section = await callTrpc(
2672
+ "section.create",
2673
+ {
2674
+ publicationId,
2675
+ name,
2676
+ contentJson
2677
+ },
2678
+ options
2679
+ );
2680
+ return makeToolResult(`Reusable section saved: ${section.id}`, { section });
2681
+ }
2682
+ if (toolName === "section.update") {
2683
+ const sectionId = readRequiredString(args, "sectionId");
2684
+ const name = readRequiredString(args, "name");
2685
+ const contentJson = readRequiredJsonObjectArray(args, "contentJson");
2686
+ const section = await callTrpc(
2687
+ "section.update",
2688
+ {
2689
+ sectionId,
2690
+ name,
2691
+ contentJson
2692
+ },
2693
+ options
2694
+ );
2695
+ return makeToolResult(`Reusable section updated: ${section.id}`, { section });
2696
+ }
2697
+ if (toolName === "section.remove") {
2698
+ const sectionId = readRequiredString(args, "sectionId");
2699
+ const result = await callTrpc(
2700
+ "section.remove",
2701
+ { sectionId },
2702
+ options
2703
+ );
2704
+ return makeToolResult(`Reusable section removed: ${result.sectionId}`, result);
2705
+ }
2706
+ if (toolName === "issue.preview") {
2707
+ const issueId = readRequiredString(args, "issueId");
2708
+ const preview = await callTrpc(
2709
+ "issue.preview",
2710
+ { issueId },
2711
+ options,
2712
+ "query"
2713
+ );
2714
+ return makeToolResult(`Preview generated for ${preview.issueId}`, preview);
2715
+ }
2716
+ if (toolName === "issue.preview_draft") {
2717
+ const publicationId = readRequiredString(args, "publicationId");
2718
+ const title = readRequiredString(args, "title");
2719
+ const html = readRequiredString(args, "html");
2720
+ const plainText = asOptionalString(args.plainText);
2721
+ const styleProfile = readOptionalJsonObject(args, "styleProfile");
2722
+ const preview = await callTrpc(
2723
+ "issue.previewDraft",
2724
+ {
2725
+ publicationId,
2726
+ title,
2727
+ html,
2728
+ ...plainText ? { plainText } : {},
2729
+ ...styleProfile ? { styleProfile } : {}
2730
+ },
2731
+ options
2732
+ );
2733
+ return makeToolResult(
2734
+ `Draft preview generated for ${preview.publicationId}: ${preview.title}`,
2735
+ preview
2736
+ );
2737
+ }
2738
+ if (toolName === "issue.delivery_progress") {
2739
+ const publicationId = readRequiredString(args, "publicationId");
2740
+ const issueId = readRequiredString(args, "issueId");
2741
+ const progress = await callTrpc(
2742
+ "issue.deliveryProgress",
2743
+ {
2744
+ publicationId,
2745
+ issueId
2746
+ },
2747
+ options,
2748
+ "query"
2749
+ );
2750
+ if (progress.delivery.isPreparing) {
2751
+ return makeToolResult(
2752
+ `Issue ${progress.issueId} is preparing delivery (status: ${progress.status})`,
2753
+ progress
2754
+ );
2755
+ }
2756
+ return makeToolResult(
2757
+ `Issue ${progress.issueId} delivery: ${progress.delivery.processed}/${progress.delivery.total} processed (${progress.delivery.completionPercent}%)`,
2758
+ progress
2759
+ );
2760
+ }
2761
+ if (toolName === "issue.wait_delivery") {
2762
+ const publicationId = readRequiredString(args, "publicationId");
2763
+ const issueId = readRequiredString(args, "issueId");
2764
+ const timeoutMs = Math.max(1e3, Math.min(3e5, Math.trunc(readOptionalNumber(args, "timeoutMs") ?? 6e4)));
2765
+ const pollIntervalMs = Math.max(250, Math.min(1e4, Math.trunc(readOptionalNumber(args, "pollIntervalMs") ?? 2e3)));
2766
+ const startedAt = Date.now();
2767
+ while (true) {
2768
+ const progress = await callTrpc(
2769
+ "issue.deliveryProgress",
2770
+ {
2771
+ publicationId,
2772
+ issueId
2773
+ },
2774
+ options,
2775
+ "query"
2776
+ );
2777
+ if (progress.status === "sent" || progress.status === "failed") {
2778
+ return makeToolResult(
2779
+ `Issue ${progress.issueId} delivery finished with status ${progress.status}: ${progress.delivery.processed}/${progress.delivery.total} processed (${progress.delivery.completionPercent}%)`,
2780
+ {
2781
+ timedOut: false,
2782
+ elapsedMs: Date.now() - startedAt,
2783
+ progress
2784
+ }
2785
+ );
2786
+ }
2787
+ const elapsedMs = Date.now() - startedAt;
2788
+ if (elapsedMs >= timeoutMs) {
2789
+ return makeToolResult(
2790
+ `Issue ${progress.issueId} delivery still in status ${progress.status} after ${elapsedMs}ms`,
2791
+ {
2792
+ timedOut: true,
2793
+ elapsedMs,
2794
+ progress
2795
+ }
2796
+ );
2797
+ }
2798
+ await sleep(pollIntervalMs);
2799
+ }
2800
+ }
2801
+ if (toolName === "analytics.poll_results") {
2802
+ const publicationId = readRequiredString(args, "publicationId");
2803
+ const issueId = readRequiredString(args, "issueId");
2804
+ const analytics = await callTrpc(
2805
+ "issue.pollResults",
2806
+ {
2807
+ publicationId,
2808
+ issueId
2809
+ },
2810
+ options,
2811
+ "query"
2812
+ );
2813
+ return makeToolResult(
2814
+ analytics.polls.length === 0 ? `No poll results yet for ${issueId}` : `Loaded ${analytics.polls.length} polls for ${issueId}`,
2815
+ analytics
2816
+ );
2817
+ }
2818
+ if (toolName === "analytics.issue_performance") {
2819
+ const publicationId = readRequiredString(args, "publicationId");
2820
+ const issueId = readRequiredString(args, "issueId");
2821
+ const range = readIssueAnalyticsRange(args, "range") ?? "all";
2822
+ const analytics = await callTrpc(
2823
+ "issue.analytics",
2824
+ {
2825
+ publicationId,
2826
+ issueId,
2827
+ range
2828
+ },
2829
+ options,
2830
+ "query"
2831
+ );
2832
+ return makeToolResult(
2833
+ `Issue analytics loaded for ${issueId}: ${analytics.opens.unique} unique opens, ${analytics.clicks.unique} unique clicks`,
2834
+ analytics
2835
+ );
2836
+ }
2837
+ if (toolName === "analytics.issue_export_csv") {
2838
+ const publicationId = readRequiredString(args, "publicationId");
2839
+ const issueId = readRequiredString(args, "issueId");
2840
+ const range = readIssueAnalyticsRange(args, "range") ?? "all";
2841
+ const exportType = readIssueAnalyticsExportType(args, "exportType") ?? "combined";
2842
+ const exported = await callTrpc(
2843
+ "issue.analyticsExportCsv",
2844
+ {
2845
+ publicationId,
2846
+ issueId,
2847
+ range,
2848
+ exportType
2849
+ },
2850
+ options,
2851
+ "query"
2852
+ );
2853
+ return makeToolResult(
2854
+ `Issue analytics ${exported.exportType} CSV ready for ${issueId}: ${exported.filename} (${exported.rowCount} rows)`,
2855
+ exported
2856
+ );
2857
+ }
2858
+ if (toolName === "analytics.issue_export_performance_csv") {
2859
+ const publicationId = readRequiredString(args, "publicationId");
2860
+ const issueId = readRequiredString(args, "issueId");
2861
+ const range = readIssueAnalyticsRange(args, "range") ?? "all";
2862
+ const exported = await callTrpc(
2863
+ "issue.analyticsExportCsv",
2864
+ {
2865
+ publicationId,
2866
+ issueId,
2867
+ range,
2868
+ exportType: "performance"
2869
+ },
2870
+ options,
2871
+ "query"
2872
+ );
2873
+ return makeToolResult(
2874
+ `Issue performance CSV ready for ${issueId}: ${exported.filename} (${exported.rowCount} rows)`,
2875
+ exported
2876
+ );
2877
+ }
2878
+ if (toolName === "analytics.issue_export_polls_csv") {
2879
+ const publicationId = readRequiredString(args, "publicationId");
2880
+ const issueId = readRequiredString(args, "issueId");
2881
+ const range = readIssueAnalyticsRange(args, "range") ?? "all";
2882
+ const exported = await callTrpc(
2883
+ "issue.analyticsExportCsv",
2884
+ {
2885
+ publicationId,
2886
+ issueId,
2887
+ range,
2888
+ exportType: "polls"
2889
+ },
2890
+ options,
2891
+ "query"
2892
+ );
2893
+ return makeToolResult(
2894
+ `Issue poll CSV ready for ${issueId}: ${exported.filename} (${exported.rowCount} rows)`,
2895
+ exported
2896
+ );
2897
+ }
2898
+ if (toolName === "analytics.issue_trend") {
2899
+ const publicationId = readRequiredString(args, "publicationId");
2900
+ const issueId = readRequiredString(args, "issueId");
2901
+ const range = readIssueAnalyticsRange(args, "range") ?? "all";
2902
+ const trend = await callTrpc(
2903
+ "issue.analyticsTrend",
2904
+ {
2905
+ publicationId,
2906
+ issueId,
2907
+ range
2908
+ },
2909
+ options,
2910
+ "query"
2911
+ );
2912
+ return makeToolResult(
2913
+ `Issue trend loaded for ${issueId}: ${trend.points.length} daily points`,
2914
+ trend
2915
+ );
2916
+ }
2917
+ if (toolName === "analytics.latest_summary") {
2918
+ const publicationId = asOptionalString(args.publicationId);
2919
+ const range = readIssueAnalyticsRange(args, "range") ?? "7d";
2920
+ const summary = await loadLatestAnalyticsSummary(options, {
2921
+ publicationId,
2922
+ range
2923
+ });
2924
+ if (summary.status === "missing_publication_context") {
2925
+ return makeToolResult("Missing publication context for analytics.latest_summary", summary);
2926
+ }
2927
+ if (summary.status === "no_issues") {
2928
+ return makeToolResult(`No issues found for ${summary.publicationId}`, summary);
2929
+ }
2930
+ return makeToolResult(
2931
+ `Latest analytics loaded for ${summary.issue.id}: ${summary.analytics.opens.unique} unique opens, ${summary.analytics.clicks.unique} unique clicks`,
2932
+ summary
2933
+ );
2934
+ }
2935
+ if (toolName === "issue.schedule") {
2936
+ const issueId = readRequiredString(args, "issueId");
2937
+ const scheduledFor = asOptionalString(args.scheduledFor);
2938
+ const issue = await callTrpc(
2939
+ "issue.schedule",
2940
+ {
2941
+ issueId,
2942
+ ...scheduledFor ? { scheduledFor } : {}
2943
+ },
2944
+ options
2945
+ );
2946
+ return makeToolResult(`Issue scheduled: ${issue.id}`, { issue });
2947
+ }
2948
+ if (toolName === "issue.unschedule") {
2949
+ const issueId = readRequiredString(args, "issueId");
2950
+ const issue = await callTrpc(
2951
+ "issue.unschedule",
2952
+ { issueId },
2953
+ options
2954
+ );
2955
+ return makeToolResult(`Issue unscheduled: ${issue.id}`, { issue });
2956
+ }
2957
+ if (toolName === "issue.publish_to_web") {
2958
+ const issueId = readRequiredString(args, "issueId");
2959
+ const issue = await callTrpc(
2960
+ "issue.publishToWeb",
2961
+ { issueId },
2962
+ options
2963
+ );
2964
+ return makeToolResult(`Issue published to web: ${issue.id}`, { issue });
2965
+ }
2966
+ if (toolName === "issue.unpublish_from_web") {
2967
+ const issueId = readRequiredString(args, "issueId");
2968
+ const issue = await callTrpc(
2969
+ "issue.unpublishFromWeb",
2970
+ { issueId },
2971
+ options
2972
+ );
2973
+ return makeToolResult(`Issue unpublished from web: ${issue.id}`, { issue });
2974
+ }
2975
+ if (toolName === "issue.send_now") {
2976
+ const issueId = readRequiredString(args, "issueId");
2977
+ const issue = await callTrpc(
2978
+ "issue.sendNow",
2979
+ { issueId },
2980
+ options
2981
+ );
2982
+ return makeToolResult(`Issue sent: ${issue.id}`, { issue });
2983
+ }
2984
+ if (toolName === "issue.send_and_wait") {
2985
+ const issueId = readRequiredString(args, "issueId");
2986
+ const timeoutMs = Math.max(1e3, Math.min(3e5, Math.trunc(readOptionalNumber(args, "timeoutMs") ?? 6e4)));
2987
+ const pollIntervalMs = Math.max(250, Math.min(1e4, Math.trunc(readOptionalNumber(args, "pollIntervalMs") ?? 2e3)));
2988
+ const issue = await callTrpc(
2989
+ "issue.sendNow",
2990
+ { issueId },
2991
+ options
2992
+ );
2993
+ const startedAt = Date.now();
2994
+ while (true) {
2995
+ const progress = await callTrpc(
2996
+ "issue.deliveryProgress",
2997
+ {
2998
+ publicationId: issue.publicationId,
2999
+ issueId: issue.id
3000
+ },
3001
+ options,
3002
+ "query"
3003
+ );
3004
+ if (progress.status === "sent" || progress.status === "failed") {
3005
+ return makeToolResult(
3006
+ `Issue ${issue.id} send-and-wait finished with status ${progress.status}: ${progress.delivery.processed}/${progress.delivery.total} processed (${progress.delivery.completionPercent}%)`,
3007
+ {
3008
+ issue,
3009
+ timedOut: false,
3010
+ elapsedMs: Date.now() - startedAt,
3011
+ progress
3012
+ }
3013
+ );
3014
+ }
3015
+ const elapsedMs = Date.now() - startedAt;
3016
+ if (elapsedMs >= timeoutMs) {
3017
+ return makeToolResult(
3018
+ `Issue ${issue.id} queued but still ${progress.status} after ${elapsedMs}ms`,
3019
+ {
3020
+ issue,
3021
+ timedOut: true,
3022
+ elapsedMs,
3023
+ progress
3024
+ }
3025
+ );
3026
+ }
3027
+ await sleep(pollIntervalMs);
3028
+ }
3029
+ }
3030
+ if (toolName === "ai.generate_draft") {
3031
+ const publicationId = readRequiredString(args, "publicationId");
3032
+ const prompt = readRequiredString(args, "prompt");
3033
+ const tone = asOptionalString(args.tone);
3034
+ if (tone && tone !== "neutral" && tone !== "friendly" && tone !== "formal") {
3035
+ throw new Error("tone must be one of: neutral, friendly, formal");
3036
+ }
3037
+ const draft = await callTrpc(
3038
+ "ai.generateDraft",
3039
+ {
3040
+ publicationId,
3041
+ prompt,
3042
+ ...tone ? { tone } : {}
3043
+ },
3044
+ options
3045
+ );
3046
+ return makeToolResult("AI draft generated", draft);
3047
+ }
3048
+ if (toolName === "template.create") {
3049
+ const publicationId = readRequiredString(args, "publicationId");
3050
+ const name = readRequiredString(args, "name");
3051
+ const html = asOptionalString(args.html);
3052
+ const spec = args.spec;
3053
+ const description = asOptionalString(args.description);
3054
+ const subject = asOptionalString(args.subject);
3055
+ const variables = args.variables;
3056
+ if (!html && !spec) {
3057
+ throw new Error("Either 'html' or 'spec' must be provided");
3058
+ }
3059
+ const body = {
3060
+ publication_id: publicationId,
3061
+ name,
3062
+ ...html ? { html } : {},
3063
+ ...spec ? { spec } : {},
3064
+ ...description ? { description } : {},
3065
+ ...subject ? { subject } : {},
3066
+ ...variables ? { variables } : {}
3067
+ };
3068
+ const template = await callRestApi(
3069
+ "POST",
3070
+ "/v1/templates",
3071
+ body,
3072
+ options
3073
+ );
3074
+ return makeToolResult(
3075
+ `Template created: ${template.id} (${template.format})`,
3076
+ { template }
3077
+ );
3078
+ }
3079
+ if (toolName === "template.list") {
3080
+ const publicationId = readRequiredString(args, "publicationId");
3081
+ const limit = readOptionalNumber(args, "limit") ?? 20;
3082
+ const result = await callRestApi(
3083
+ "GET",
3084
+ `/v1/templates?publication_id=${encodeURIComponent(publicationId)}&limit=${limit}`,
3085
+ void 0,
3086
+ options
3087
+ );
3088
+ const names = result.data.map((t) => `${t.id}: ${t.name} (${t.format}, ${t.status})`);
3089
+ return makeToolResult(
3090
+ result.data.length > 0 ? `${result.data.length} template(s):
3091
+ ${names.join("\n")}` : "No templates found",
3092
+ result
3093
+ );
3094
+ }
3095
+ if (toolName === "template.get") {
3096
+ const publicationId = readRequiredString(args, "publicationId");
3097
+ const templateId = readRequiredString(args, "templateId");
3098
+ const template = await callRestApi(
3099
+ "GET",
3100
+ `/v1/templates/${encodeURIComponent(templateId)}?publication_id=${encodeURIComponent(publicationId)}`,
3101
+ void 0,
3102
+ options
3103
+ );
3104
+ return makeToolResult(
3105
+ `Template: ${template.name} (${template.format}, ${template.status})`,
3106
+ { template }
3107
+ );
3108
+ }
3109
+ if (toolName === "template.update") {
3110
+ const publicationId = readRequiredString(args, "publicationId");
3111
+ const templateId = readRequiredString(args, "templateId");
3112
+ const name = asOptionalString(args.name);
3113
+ const html = asOptionalString(args.html);
3114
+ const spec = args.spec;
3115
+ const description = asOptionalString(args.description);
3116
+ const text = asOptionalString(args.text);
3117
+ const subject = asOptionalString(args.subject);
3118
+ const from = asOptionalString(args.from);
3119
+ const replyTo = asOptionalString(args.reply_to);
3120
+ const variables = args.variables;
3121
+ const body = {
3122
+ ...name ? { name } : {},
3123
+ ...html ? { html } : {},
3124
+ ...spec ? { spec } : {},
3125
+ ...description ? { description } : {},
3126
+ ...text ? { text } : {},
3127
+ ...subject ? { subject } : {},
3128
+ ...from ? { from } : {},
3129
+ ...replyTo ? { reply_to: replyTo } : {},
3130
+ ...variables ? { variables } : {}
3131
+ };
3132
+ const template = await callRestApi(
3133
+ "PATCH",
3134
+ `/v1/templates/${encodeURIComponent(templateId)}?publication_id=${encodeURIComponent(publicationId)}`,
3135
+ body,
3136
+ options
3137
+ );
3138
+ return makeToolResult(`Template updated: ${template.id}`, { template });
3139
+ }
3140
+ if (toolName === "template.publish") {
3141
+ const publicationId = readRequiredString(args, "publicationId");
3142
+ const templateId = readRequiredString(args, "templateId");
3143
+ const template = await callRestApi(
3144
+ "POST",
3145
+ `/v1/templates/${encodeURIComponent(templateId)}/publish?publication_id=${encodeURIComponent(publicationId)}`,
3146
+ void 0,
3147
+ options
3148
+ );
3149
+ return makeToolResult(`Template published: ${template.id} (${template.status})`, { template });
3150
+ }
3151
+ if (toolName === "template.duplicate") {
3152
+ const publicationId = readRequiredString(args, "publicationId");
3153
+ const templateId = readRequiredString(args, "templateId");
3154
+ const template = await callRestApi(
3155
+ "POST",
3156
+ `/v1/templates/${encodeURIComponent(templateId)}/duplicate?publication_id=${encodeURIComponent(publicationId)}`,
3157
+ void 0,
3158
+ options
3159
+ );
3160
+ return makeToolResult(`Template duplicated: new template ${template.id}`, { template });
3161
+ }
3162
+ if (toolName === "template.delete") {
3163
+ const publicationId = readRequiredString(args, "publicationId");
3164
+ const templateId = readRequiredString(args, "templateId");
3165
+ const result = await callRestApi(
3166
+ "DELETE",
3167
+ `/v1/templates/${encodeURIComponent(templateId)}?publication_id=${encodeURIComponent(publicationId)}`,
3168
+ void 0,
3169
+ options
3170
+ );
3171
+ return makeToolResult(`Template deleted: ${result.id}`, result);
3172
+ }
3173
+ if (toolName === "email.send") {
3174
+ const body = {};
3175
+ for (const key of EMAIL_SEND_FIELDS) {
3176
+ if (args[key] !== void 0) body[key] = args[key];
3177
+ }
3178
+ for (const required of ["from", "to", "subject"]) {
3179
+ if (body[required] === void 0) {
3180
+ throw new Error(`Missing required field: ${required}`);
3181
+ }
3182
+ }
3183
+ const result = await callRestApi(
3184
+ "POST",
3185
+ "/v1/emails",
3186
+ body,
3187
+ options
3188
+ );
3189
+ return makeToolResult(
3190
+ body.scheduled_at ? `Email scheduled (${result.id}) for ${String(body.scheduled_at)}` : `Email queued for delivery (${result.id})`,
3191
+ result
3192
+ );
3193
+ }
3194
+ if (toolName === "email.batch") {
3195
+ const emails = args.emails;
3196
+ if (!Array.isArray(emails) || emails.length === 0) {
3197
+ throw new Error("'emails' must be a non-empty array of email objects.");
3198
+ }
3199
+ const result = await callRestApi(
3200
+ "POST",
3201
+ "/v1/emails/batch",
3202
+ emails,
3203
+ options
3204
+ );
3205
+ return makeToolResult(
3206
+ `Batch queued: ${result.data?.length ?? 0} email(s).`,
3207
+ result
3208
+ );
3209
+ }
3210
+ if (toolName === "email.get") {
3211
+ const id = readRequiredString(args, "id");
3212
+ const email = await callRestApi(
3213
+ "GET",
3214
+ `/v1/emails/${encodeURIComponent(id)}`,
3215
+ void 0,
3216
+ options
3217
+ );
3218
+ return makeToolResult(
3219
+ `Email ${id}: ${String(email.last_event ?? "unknown")} (opens ${String(
3220
+ email.open_count ?? 0
3221
+ )}, clicks ${String(email.click_count ?? 0)})`,
3222
+ { email }
3223
+ );
3224
+ }
3225
+ if (toolName === "email.reschedule") {
3226
+ const id = readRequiredString(args, "id");
3227
+ const scheduledAt = readRequiredString(args, "scheduled_at");
3228
+ const result = await callRestApi(
3229
+ "PATCH",
3230
+ `/v1/emails/${encodeURIComponent(id)}`,
3231
+ { scheduled_at: scheduledAt },
3232
+ options
3233
+ );
3234
+ return makeToolResult(`Email ${id} rescheduled for ${scheduledAt}.`, result);
3235
+ }
3236
+ if (toolName === "email.cancel") {
3237
+ const id = readRequiredString(args, "id");
3238
+ const result = await callRestApi(
3239
+ "POST",
3240
+ `/v1/emails/${encodeURIComponent(id)}/cancel`,
3241
+ void 0,
3242
+ options
3243
+ );
3244
+ return makeToolResult(`Email ${id} canceled.`, result);
3245
+ }
3246
+ if (toolName === "email.resend") {
3247
+ const id = readRequiredString(args, "id");
3248
+ const result = await callTrpc(
3249
+ "email.resend",
3250
+ { id },
3251
+ options
3252
+ );
3253
+ return makeToolResult(`Email ${id} resent as ${result.email.id}.`, result);
3254
+ }
3255
+ if (toolName === "email.list") {
3256
+ const params = new URLSearchParams();
3257
+ const limit = readOptionalNumber(args, "limit");
3258
+ const offset = readOptionalNumber(args, "offset");
3259
+ if (limit !== void 0) params.set("limit", String(limit));
3260
+ if (offset !== void 0) params.set("offset", String(offset));
3261
+ for (const key of ["status", "tag_name", "tag_value", "from_date", "to_date"]) {
3262
+ const value = asOptionalString(args[key]);
3263
+ if (value) params.set(key, value);
3264
+ }
3265
+ const qs = params.toString();
3266
+ const result = await callRestApi("GET", `/v1/emails${qs ? `?${qs}` : ""}`, void 0, options);
3267
+ const lines = result.data.map(
3268
+ (email) => `${String(email.id)}: ${String(email.last_event)} \u2014 ${String(email.subject)} \u2192 ${String(email.to)}`
3269
+ );
3270
+ return makeToolResult(
3271
+ result.data.length > 0 ? `${result.data.length} of ${result.total} email(s):
3272
+ ${lines.join("\n")}` : "No emails found.",
3273
+ result
3274
+ );
3275
+ }
3276
+ if (toolName === "email.analytics") {
3277
+ const params = new URLSearchParams();
3278
+ for (const key of ["from_date", "to_date"]) {
3279
+ const value = asOptionalString(args[key]);
3280
+ if (value) params.set(key, value);
3281
+ }
3282
+ const qs = params.toString();
3283
+ const result = await callRestApi("GET", `/v1/emails/analytics${qs ? `?${qs}` : ""}`, void 0, options);
3284
+ const pct = (rate) => `${(rate * 100).toFixed(1)}%`;
3285
+ return makeToolResult(
3286
+ `${result.sent} sent (of ${result.total}): delivered ${result.delivered} (${pct(result.rates.delivery_rate)}), opened ${result.opened} (${pct(result.rates.open_rate)}), clicked ${result.clicked} (${pct(result.rates.click_rate)}), bounced ${result.bounced} (${pct(result.rates.bounce_rate)})`,
3287
+ result
3288
+ );
3289
+ }
3290
+ if (toolName === "email.inbound_list") {
3291
+ const publicationId = readRequiredString(args, "publicationId");
3292
+ const limit = readOptionalNumber(args, "limit");
3293
+ const cursor = asOptionalString(args.cursor);
3294
+ const params = new URLSearchParams({ publication_id: publicationId });
3295
+ if (limit !== void 0) params.set("limit", String(limit));
3296
+ if (cursor) params.set("cursor", cursor);
3297
+ const result = await callRestApi(
3298
+ "GET",
3299
+ `/v1/emails/inbound?${params.toString()}`,
3300
+ void 0,
3301
+ options
3302
+ );
3303
+ return makeToolResult(`${result.data.length} inbound email(s).`, result);
3304
+ }
3305
+ if (toolName === "email.inbound_get") {
3306
+ const id = readRequiredString(args, "id");
3307
+ const result = await callRestApi(
3308
+ "GET",
3309
+ `/v1/emails/inbound/${encodeURIComponent(id)}`,
3310
+ void 0,
3311
+ options
3312
+ );
3313
+ return makeToolResult(
3314
+ `Inbound email ${id}: ${String(result.subject ?? "(no subject)")} from ${String(
3315
+ result.from ?? "unknown"
3316
+ )}.`,
3317
+ result
3318
+ );
3319
+ }
3320
+ if (toolName === "email.inbound_list_attachments") {
3321
+ const id = readRequiredString(args, "id");
3322
+ const result = await callRestApi(
3323
+ "GET",
3324
+ `/v1/emails/inbound/${encodeURIComponent(id)}/attachments`,
3325
+ void 0,
3326
+ options
3327
+ );
3328
+ return makeToolResult(`${result.data.length} attachment(s).`, result);
3329
+ }
3330
+ if (toolName === "email.inbound_get_attachment") {
3331
+ const id = readRequiredString(args, "id");
3332
+ const attachmentId = readRequiredString(args, "attachmentId");
3333
+ const result = await callRestApi(
3334
+ "GET",
3335
+ `/v1/emails/inbound/${encodeURIComponent(id)}/attachments/${encodeURIComponent(attachmentId)}`,
3336
+ void 0,
3337
+ options
3338
+ );
3339
+ return makeToolResult(`Attachment ${result.id} (${result.filename}).`, result);
3340
+ }
3341
+ if (toolName === "email.inbound_reply") {
3342
+ const id = readRequiredString(args, "id");
3343
+ const body = {};
3344
+ for (const key of ["from", "subject", "html", "text", "cc", "bcc", "idempotency_key"]) {
3345
+ if (args[key] !== void 0) body[key] = args[key];
3346
+ }
3347
+ if (body.html === void 0 && body.text === void 0) {
3348
+ throw new Error("Provide 'html' and/or 'text' for the reply body.");
3349
+ }
3350
+ const result = await callRestApi(
3351
+ "POST",
3352
+ `/v1/emails/inbound/${encodeURIComponent(id)}/reply`,
3353
+ body,
3354
+ options
3355
+ );
3356
+ return makeToolResult(`Reply queued as ${result.id} (${result.status}).`, result);
3357
+ }
3358
+ if (toolName === "issue.send_test") {
3359
+ const issueId = readRequiredString(args, "issueId");
3360
+ const from = readRequiredString(args, "from");
3361
+ const recipients = args.recipients;
3362
+ if (!recipients || typeof recipients !== "string" && !Array.isArray(recipients) || Array.isArray(recipients) && recipients.length === 0) {
3363
+ throw new Error("'recipients' must be an email address or a non-empty array of addresses.");
3364
+ }
3365
+ const recipientList = Array.isArray(recipients) ? recipients : [recipients];
3366
+ const result = await callRestApi(
3367
+ "POST",
3368
+ `/v1/posts/${encodeURIComponent(issueId)}/test`,
3369
+ { recipients: recipientList, from },
3370
+ options
3371
+ );
3372
+ const failedNote = result.failed_to.length ? `, ${result.failed_to.length} failed` : "";
3373
+ return makeToolResult(
3374
+ `Test of ${issueId} sent to ${result.sent_to.length} recipient(s)${failedNote}.`,
3375
+ result
3376
+ );
3377
+ }
3378
+ if (toolName === "domain.create") {
3379
+ const publicationId = readRequiredString(args, "publicationId");
3380
+ const name = readRequiredString(args, "name");
3381
+ const purpose = asOptionalString(args.purpose);
3382
+ const isPrimary = readOptionalBoolean(args, "is_primary");
3383
+ const result = await callRestApi(
3384
+ "POST",
3385
+ "/v1/domains",
3386
+ {
3387
+ publication_id: publicationId,
3388
+ name,
3389
+ ...purpose ? { purpose } : {},
3390
+ ...isPrimary !== void 0 ? { is_primary: isPrimary } : {}
3391
+ },
3392
+ options
3393
+ );
3394
+ return makeToolResult(
3395
+ `Domain ${result.name} created (${result.status}). Add the DNS records, then call domain.verify.`,
3396
+ result
3397
+ );
3398
+ }
3399
+ if (toolName === "domain.list") {
3400
+ const publicationId = readRequiredString(args, "publicationId");
3401
+ const limit = readOptionalNumber(args, "limit");
3402
+ const params = new URLSearchParams({ publication_id: publicationId });
3403
+ if (limit !== void 0) params.set("limit", String(limit));
3404
+ const result = await callRestApi(
3405
+ "GET",
3406
+ `/v1/domains?${params.toString()}`,
3407
+ void 0,
3408
+ options
3409
+ );
3410
+ return makeToolResult(`${result.data.length} domain(s).`, result);
3411
+ }
3412
+ if (toolName === "domain.get") {
3413
+ const publicationId = readRequiredString(args, "publicationId");
3414
+ const domainId = readRequiredString(args, "domainId");
3415
+ const result = await callRestApi(
3416
+ "GET",
3417
+ `/v1/domains/${encodeURIComponent(domainId)}?publication_id=${encodeURIComponent(publicationId)}`,
3418
+ void 0,
3419
+ options
3420
+ );
3421
+ return makeToolResult(`Domain ${result.name} (${result.status}).`, result);
3422
+ }
3423
+ if (toolName === "domain.verify") {
3424
+ const publicationId = readRequiredString(args, "publicationId");
3425
+ const domainId = readRequiredString(args, "domainId");
3426
+ const result = await callRestApi(
3427
+ "POST",
3428
+ `/v1/domains/${encodeURIComponent(domainId)}/verify?publication_id=${encodeURIComponent(publicationId)}`,
3429
+ void 0,
3430
+ options
3431
+ );
3432
+ return makeToolResult(`Domain ${result.name} is now ${result.status}.`, result);
3433
+ }
3434
+ if (toolName === "domain.update") {
3435
+ const publicationId = readRequiredString(args, "publicationId");
3436
+ const domainId = readRequiredString(args, "domainId");
3437
+ const purpose = asOptionalString(args.purpose);
3438
+ const isPrimary = readOptionalBoolean(args, "is_primary");
3439
+ const result = await callRestApi(
3440
+ "PATCH",
3441
+ `/v1/domains/${encodeURIComponent(domainId)}?publication_id=${encodeURIComponent(publicationId)}`,
3442
+ {
3443
+ ...purpose ? { purpose } : {},
3444
+ ...isPrimary !== void 0 ? { is_primary: isPrimary } : {}
3445
+ },
3446
+ options
3447
+ );
3448
+ return makeToolResult(`Domain ${result.name} updated (purpose: ${result.purpose}).`, result);
3449
+ }
3450
+ if (toolName === "domain.delete") {
3451
+ const publicationId = readRequiredString(args, "publicationId");
3452
+ const domainId = readRequiredString(args, "domainId");
3453
+ const result = await callRestApi(
3454
+ "DELETE",
3455
+ `/v1/domains/${encodeURIComponent(domainId)}?publication_id=${encodeURIComponent(publicationId)}`,
3456
+ void 0,
3457
+ options
3458
+ );
3459
+ return makeToolResult(`Domain ${result.id} deleted.`, result);
3460
+ }
3461
+ if (toolName === "domain.tracking_create") {
3462
+ const publicationId = readRequiredString(args, "publicationId");
3463
+ const domainId = readRequiredString(args, "domainId");
3464
+ const subdomain = readRequiredString(args, "subdomain");
3465
+ const result = await callRestApi(
3466
+ "POST",
3467
+ `/v1/domains/${encodeURIComponent(domainId)}/tracking-domains?publication_id=${encodeURIComponent(publicationId)}`,
3468
+ { subdomain },
3469
+ options
3470
+ );
3471
+ return makeToolResult(
3472
+ `Tracking domain ${result.full_name} created (${result.status}). Add the CNAME record, then call domain.tracking_verify.`,
3473
+ result
3474
+ );
3475
+ }
3476
+ if (toolName === "domain.tracking_list") {
3477
+ const publicationId = readRequiredString(args, "publicationId");
3478
+ const domainId = readRequiredString(args, "domainId");
3479
+ const result = await callRestApi(
3480
+ "GET",
3481
+ `/v1/domains/${encodeURIComponent(domainId)}/tracking-domains?publication_id=${encodeURIComponent(publicationId)}`,
3482
+ void 0,
3483
+ options
3484
+ );
3485
+ return makeToolResult(`${result.data.length} tracking domain(s).`, result);
3486
+ }
3487
+ if (toolName === "domain.tracking_verify") {
3488
+ const publicationId = readRequiredString(args, "publicationId");
3489
+ const domainId = readRequiredString(args, "domainId");
3490
+ const trackingDomainId = readRequiredString(args, "trackingDomainId");
3491
+ const result = await callRestApi(
3492
+ "POST",
3493
+ `/v1/domains/${encodeURIComponent(domainId)}/tracking-domains/${encodeURIComponent(trackingDomainId)}/verify?publication_id=${encodeURIComponent(publicationId)}`,
3494
+ void 0,
3495
+ options
3496
+ );
3497
+ return makeToolResult(`Tracking domain ${result.full_name} is now ${result.status}.`, result);
3498
+ }
3499
+ if (toolName === "domain.tracking_delete") {
3500
+ const publicationId = readRequiredString(args, "publicationId");
3501
+ const domainId = readRequiredString(args, "domainId");
3502
+ const trackingDomainId = readRequiredString(args, "trackingDomainId");
3503
+ const result = await callRestApi(
3504
+ "DELETE",
3505
+ `/v1/domains/${encodeURIComponent(domainId)}/tracking-domains/${encodeURIComponent(trackingDomainId)}?publication_id=${encodeURIComponent(publicationId)}`,
3506
+ void 0,
3507
+ options
3508
+ );
3509
+ return makeToolResult(`Tracking domain ${result.id} deleted.`, result);
3510
+ }
3511
+ if (toolName === "webhook.create") {
3512
+ const publicationId = readRequiredString(args, "publicationId");
3513
+ const endpoint = readRequiredString(args, "endpoint");
3514
+ const events = args.events;
3515
+ if (!Array.isArray(events) || events.length === 0 || !events.every((e) => typeof e === "string")) {
3516
+ throw new Error("'events' must be a non-empty array of event-type strings.");
3517
+ }
3518
+ const result = await callRestApi(
3519
+ "POST",
3520
+ "/v1/webhooks/endpoints",
3521
+ { publication_id: publicationId, endpoint, events },
3522
+ options
3523
+ );
3524
+ return makeToolResult(
3525
+ `Webhook ${result.id} created for ${result.endpoint}. Store the signing_secret now \u2014 it won't be shown again.`,
3526
+ result
3527
+ );
3528
+ }
3529
+ if (toolName === "webhook.list") {
3530
+ const publicationId = readRequiredString(args, "publicationId");
3531
+ const limit = readOptionalNumber(args, "limit");
3532
+ const params = new URLSearchParams({ publication_id: publicationId });
3533
+ if (limit !== void 0) params.set("limit", String(limit));
3534
+ const result = await callRestApi(
3535
+ "GET",
3536
+ `/v1/webhooks/endpoints?${params.toString()}`,
3537
+ void 0,
3538
+ options
3539
+ );
3540
+ return makeToolResult(`${result.data.length} webhook(s).`, result);
3541
+ }
3542
+ if (toolName === "webhook.get") {
3543
+ const publicationId = readRequiredString(args, "publicationId");
3544
+ const webhookId = readRequiredString(args, "webhookId");
3545
+ const result = await callRestApi(
3546
+ "GET",
3547
+ `/v1/webhooks/endpoints/${encodeURIComponent(webhookId)}?publication_id=${encodeURIComponent(publicationId)}`,
3548
+ void 0,
3549
+ options
3550
+ );
3551
+ return makeToolResult(`Webhook ${result.id} (${result.status}) -> ${result.endpoint}.`, result);
3552
+ }
3553
+ if (toolName === "webhook.update") {
3554
+ const publicationId = readRequiredString(args, "publicationId");
3555
+ const webhookId = readRequiredString(args, "webhookId");
3556
+ const endpoint = asOptionalString(args.endpoint);
3557
+ const status = asOptionalString(args.status);
3558
+ const events = args.events;
3559
+ if (events !== void 0 && (!Array.isArray(events) || !events.every((e) => typeof e === "string"))) {
3560
+ throw new Error("'events' must be an array of event-type strings.");
3561
+ }
3562
+ const result = await callRestApi(
3563
+ "PATCH",
3564
+ `/v1/webhooks/endpoints/${encodeURIComponent(webhookId)}?publication_id=${encodeURIComponent(publicationId)}`,
3565
+ {
3566
+ ...endpoint ? { endpoint } : {},
3567
+ ...events !== void 0 ? { events } : {},
3568
+ ...status ? { status } : {}
3569
+ },
3570
+ options
3571
+ );
3572
+ return makeToolResult(`Webhook ${result.id} updated (${result.status}).`, result);
3573
+ }
3574
+ if (toolName === "webhook.delete") {
3575
+ const publicationId = readRequiredString(args, "publicationId");
3576
+ const webhookId = readRequiredString(args, "webhookId");
3577
+ const result = await callRestApi(
3578
+ "DELETE",
3579
+ `/v1/webhooks/endpoints/${encodeURIComponent(webhookId)}?publication_id=${encodeURIComponent(publicationId)}`,
3580
+ void 0,
3581
+ options
3582
+ );
3583
+ return makeToolResult(`Webhook ${result.id} deleted.`, result);
3584
+ }
3585
+ if (toolName === "segment.create") {
3586
+ const publicationId = readRequiredString(args, "publicationId");
3587
+ const name = readRequiredString(args, "name");
3588
+ const description = asOptionalString(args.description);
3589
+ const statusFilter = asOptionalString(args.status_filter);
3590
+ const queryFilter = asOptionalString(args.query_filter);
3591
+ const result = await callRestApi(
3592
+ "POST",
3593
+ "/v1/segments",
3594
+ {
3595
+ publication_id: publicationId,
3596
+ name,
3597
+ ...description !== void 0 ? { description } : {},
3598
+ ...statusFilter ? { status_filter: statusFilter } : {},
3599
+ ...queryFilter !== void 0 ? { query_filter: queryFilter } : {}
3600
+ },
3601
+ options
3602
+ );
3603
+ return makeToolResult(`Segment ${result.name} created (${result.id}).`, result);
3604
+ }
3605
+ if (toolName === "segment.list") {
3606
+ const publicationId = readRequiredString(args, "publicationId");
3607
+ const limit = readOptionalNumber(args, "limit");
3608
+ const params = new URLSearchParams({ publication_id: publicationId });
3609
+ if (limit !== void 0) params.set("limit", String(limit));
3610
+ const result = await callRestApi(
3611
+ "GET",
3612
+ `/v1/segments?${params.toString()}`,
3613
+ void 0,
3614
+ options
3615
+ );
3616
+ return makeToolResult(`${result.data.length} segment(s).`, result);
3617
+ }
3618
+ if (toolName === "segment.get") {
3619
+ const publicationId = readRequiredString(args, "publicationId");
3620
+ const segmentId = readRequiredString(args, "segmentId");
3621
+ const result = await callRestApi(
3622
+ "GET",
3623
+ `/v1/segments/${encodeURIComponent(segmentId)}?publication_id=${encodeURIComponent(publicationId)}`,
3624
+ void 0,
3625
+ options
3626
+ );
3627
+ return makeToolResult(`Segment ${result.name} (${result.id}).`, result);
3628
+ }
3629
+ if (toolName === "segment.update") {
3630
+ const publicationId = readRequiredString(args, "publicationId");
3631
+ const segmentId = readRequiredString(args, "segmentId");
3632
+ const name = asOptionalString(args.name);
3633
+ const description = asOptionalString(args.description);
3634
+ const result = await callRestApi(
3635
+ "PATCH",
3636
+ `/v1/segments/${encodeURIComponent(segmentId)}?publication_id=${encodeURIComponent(publicationId)}`,
3637
+ {
3638
+ ...name ? { name } : {},
3639
+ ...description !== void 0 ? { description } : {},
3640
+ // status_filter / query_filter are nullable: forward an explicit null
3641
+ // to CLEAR the filter, a value to set it, and omit the key entirely
3642
+ // (absent from args) to leave it unchanged.
3643
+ ..."status_filter" in args ? { status_filter: args.status_filter } : {},
3644
+ ..."query_filter" in args ? { query_filter: args.query_filter } : {}
3645
+ },
3646
+ options
3647
+ );
3648
+ return makeToolResult(`Segment ${result.id} updated.`, result);
3649
+ }
3650
+ if (toolName === "segment.delete") {
3651
+ const publicationId = readRequiredString(args, "publicationId");
3652
+ const segmentId = readRequiredString(args, "segmentId");
3653
+ const result = await callRestApi(
3654
+ "DELETE",
3655
+ `/v1/segments/${encodeURIComponent(segmentId)}?publication_id=${encodeURIComponent(publicationId)}`,
3656
+ void 0,
3657
+ options
3658
+ );
3659
+ return makeToolResult(`Segment ${result.id} deleted.`, result);
3660
+ }
3661
+ if (toolName === "contact_property.create") {
3662
+ const key = readRequiredString(args, "key");
3663
+ const type = asOptionalString(args.type);
3664
+ if (type !== "string" && type !== "number") {
3665
+ throw new Error("'type' must be 'string' or 'number'.");
3666
+ }
3667
+ const description = asOptionalString(args.description);
3668
+ const fallback = args.fallback_value;
3669
+ const result = await callRestApi(
3670
+ "POST",
3671
+ "/v1/contact-properties",
3672
+ {
3673
+ key,
3674
+ type,
3675
+ ...fallback !== void 0 ? { fallback_value: fallback } : {},
3676
+ ...description !== void 0 ? { description } : {}
3677
+ },
3678
+ options
3679
+ );
3680
+ return makeToolResult(`Contact property ${result.key} created (${result.id}).`, result);
3681
+ }
3682
+ if (toolName === "contact_property.list") {
3683
+ const limit = readOptionalNumber(args, "limit");
3684
+ const qs = limit !== void 0 ? `?limit=${limit}` : "";
3685
+ const result = await callRestApi(
3686
+ "GET",
3687
+ `/v1/contact-properties${qs}`,
3688
+ void 0,
3689
+ options
3690
+ );
3691
+ return makeToolResult(`${result.data.length} contact propert(ies).`, result);
3692
+ }
3693
+ if (toolName === "contact_property.update") {
3694
+ const propertyId = readRequiredString(args, "propertyId");
3695
+ const description = asOptionalString(args.description);
3696
+ const fallback = args.fallback_value;
3697
+ const result = await callRestApi(
3698
+ "PATCH",
3699
+ `/v1/contact-properties/${encodeURIComponent(propertyId)}`,
3700
+ {
3701
+ ...fallback !== void 0 ? { fallback_value: fallback } : {},
3702
+ ...description !== void 0 ? { description } : {}
3703
+ },
3704
+ options
3705
+ );
3706
+ return makeToolResult(`Contact property ${result.id} updated.`, result);
3707
+ }
3708
+ if (toolName === "contact_property.delete") {
3709
+ const propertyId = readRequiredString(args, "propertyId");
3710
+ const result = await callRestApi(
3711
+ "DELETE",
3712
+ `/v1/contact-properties/${encodeURIComponent(propertyId)}`,
3713
+ void 0,
3714
+ options
3715
+ );
3716
+ return makeToolResult(`Contact property ${result.id} deleted.`, result);
3717
+ }
3718
+ if (toolName === "contact.get") {
3719
+ const publicationId = readRequiredString(args, "publicationId");
3720
+ const idOrEmail = readRequiredString(args, "idOrEmail");
3721
+ const result = await callRestApi(
3722
+ "GET",
3723
+ `/v1/contacts/${encodeURIComponent(idOrEmail)}?publication_id=${encodeURIComponent(publicationId)}`,
3724
+ void 0,
3725
+ options
3726
+ );
3727
+ return makeToolResult(`Contact ${result.email} (${result.status}).`, result);
3728
+ }
3729
+ if (toolName === "contact.delete") {
3730
+ const publicationId = readRequiredString(args, "publicationId");
3731
+ const idOrEmail = readRequiredString(args, "idOrEmail");
3732
+ const result = await callRestApi(
3733
+ "DELETE",
3734
+ `/v1/contacts/${encodeURIComponent(idOrEmail)}?publication_id=${encodeURIComponent(publicationId)}`,
3735
+ void 0,
3736
+ options
3737
+ );
3738
+ return makeToolResult(`Contact ${result.id} deleted.`, result);
3739
+ }
3740
+ if (toolName === "contact.get_properties") {
3741
+ const publicationId = readRequiredString(args, "publicationId");
3742
+ const contactId = readRequiredString(args, "contactId");
3743
+ const result = await callTrpc(
3744
+ "contact.getPropertyValues",
3745
+ { publicationId, contactId },
3746
+ options,
3747
+ "query"
3748
+ );
3749
+ return makeToolResult(`Property values for contact ${contactId}.`, result);
3750
+ }
3751
+ if (toolName === "contact.set_properties") {
3752
+ const publicationId = readRequiredString(args, "publicationId");
3753
+ const contactId = readRequiredString(args, "contactId");
3754
+ const values = args.values;
3755
+ if (!Array.isArray(values) || values.length === 0) {
3756
+ throw new Error("'values' must be a non-empty array of { propertyId, value }.");
3757
+ }
3758
+ const result = await callTrpc(
3759
+ "contact.setPropertyValues",
3760
+ { publicationId, contactId, values },
3761
+ options,
3762
+ "mutation"
3763
+ );
3764
+ return makeToolResult(
3765
+ `Set ${values.length} property value(s) for contact ${contactId}.`,
3766
+ result
3767
+ );
3768
+ }
3769
+ if (toolName === "tag.create") {
3770
+ const publicationId = readRequiredString(args, "publicationId");
3771
+ const name = readRequiredString(args, "name");
3772
+ const defaultSubscription = asOptionalString(args.default_subscription);
3773
+ if (defaultSubscription !== "opt_in" && defaultSubscription !== "opt_out") {
3774
+ throw new Error("'default_subscription' must be 'opt_in' or 'opt_out'.");
3775
+ }
3776
+ const description = asOptionalString(args.description);
3777
+ const visibility = asOptionalString(args.visibility);
3778
+ const result = await callRestApi(
3779
+ "POST",
3780
+ "/v1/tags",
3781
+ {
3782
+ publication_id: publicationId,
3783
+ name,
3784
+ default_subscription: defaultSubscription,
3785
+ ...description !== void 0 ? { description } : {},
3786
+ ...visibility ? { visibility } : {}
3787
+ },
3788
+ options
3789
+ );
3790
+ return makeToolResult(`Tag ${result.name} created (${result.id}).`, result);
3791
+ }
3792
+ if (toolName === "tag.list") {
3793
+ const publicationId = readRequiredString(args, "publicationId");
3794
+ const limit = readOptionalNumber(args, "limit");
3795
+ const params = new URLSearchParams({ publication_id: publicationId });
3796
+ if (limit !== void 0) params.set("limit", String(limit));
3797
+ const result = await callRestApi(
3798
+ "GET",
3799
+ `/v1/tags?${params.toString()}`,
3800
+ void 0,
3801
+ options
3802
+ );
3803
+ return makeToolResult(`${result.data.length} tag(s).`, result);
3804
+ }
3805
+ if (toolName === "tag.update") {
3806
+ const publicationId = readRequiredString(args, "publicationId");
3807
+ const tagId = readRequiredString(args, "tagId");
3808
+ const name = asOptionalString(args.name);
3809
+ const description = asOptionalString(args.description);
3810
+ const defaultSubscription = asOptionalString(args.default_subscription);
3811
+ const visibility = asOptionalString(args.visibility);
3812
+ const result = await callRestApi(
3813
+ "PATCH",
3814
+ `/v1/tags/${encodeURIComponent(tagId)}?publication_id=${encodeURIComponent(publicationId)}`,
3815
+ {
3816
+ ...name ? { name } : {},
3817
+ ...description !== void 0 ? { description } : {},
3818
+ ...defaultSubscription ? { default_subscription: defaultSubscription } : {},
3819
+ ...visibility ? { visibility } : {}
3820
+ },
3821
+ options
3822
+ );
3823
+ return makeToolResult(`Tag ${result.id} updated.`, result);
3824
+ }
3825
+ if (toolName === "tag.delete") {
3826
+ const publicationId = readRequiredString(args, "publicationId");
3827
+ const tagId = readRequiredString(args, "tagId");
3828
+ const result = await callRestApi(
3829
+ "DELETE",
3830
+ `/v1/tags/${encodeURIComponent(tagId)}?publication_id=${encodeURIComponent(publicationId)}`,
3831
+ void 0,
3832
+ options
3833
+ );
3834
+ return makeToolResult(`Tag ${result.id} deleted.`, result);
3835
+ }
3836
+ if (toolName === "api_key.create") {
3837
+ const name = readRequiredString(args, "name");
3838
+ const permission = asOptionalString(args.permission);
3839
+ const domainId = asOptionalString(args.domain_id);
3840
+ const result = await callRestApi(
3841
+ "POST",
3842
+ "/v1/api-keys",
3843
+ {
3844
+ name,
3845
+ ...permission ? { permission } : {},
3846
+ ...domainId ? { domain_id: domainId } : {}
3847
+ },
3848
+ options
3849
+ );
3850
+ return makeToolResult(
3851
+ `API key ${result.id} created. Store this token now \u2014 it won't be shown again: ${result.token}`,
3852
+ result
3853
+ );
3854
+ }
3855
+ if (toolName === "api_key.list") {
3856
+ const result = await callRestApi(
3857
+ "GET",
3858
+ "/v1/api-keys",
3859
+ void 0,
3860
+ options
3861
+ );
3862
+ return makeToolResult(`${result.data.length} API key(s).`, result);
3863
+ }
3864
+ if (toolName === "api_key.revoke") {
3865
+ const keyId = readRequiredString(args, "keyId");
3866
+ await callRestApi(
3867
+ "DELETE",
3868
+ `/v1/api-keys/${encodeURIComponent(keyId)}`,
3869
+ void 0,
3870
+ options
3871
+ );
3872
+ return makeToolResult(`API key ${keyId} revoked.`, { id: keyId, revoked: true });
3873
+ }
3874
+ throw new Error(`Unknown tool: ${toolName}`);
3875
+ }
3876
+ async function readResource(uri, options) {
3877
+ if (uri === "publication://current/brand-guidelines") {
3878
+ return {
3879
+ contents: [
3880
+ {
3881
+ uri,
3882
+ mimeType: "application/json",
3883
+ text: JSON.stringify(
3884
+ {
3885
+ tone: ["clear", "friendly", "direct"],
3886
+ style: "short paragraphs, concrete statements, one CTA",
3887
+ banned: ["vague hype", "unverifiable claims"]
3888
+ },
3889
+ null,
3890
+ 2
3891
+ )
3892
+ }
3893
+ ]
3894
+ };
3895
+ }
3896
+ if (uri === "mailtea://capabilities") {
3897
+ return {
3898
+ contents: [
3899
+ {
3900
+ uri,
3901
+ mimeType: "application/json",
3902
+ text: JSON.stringify(
3903
+ {
3904
+ apiBaseUrl: resolveApiBaseUrl(options),
3905
+ hasToken: Boolean(resolveToken(options)),
3906
+ tools: MCP_TOOLS.map((tool) => tool.name),
3907
+ notes: [
3908
+ "All tools call Mailtea API endpoints.",
3909
+ "Use a PAT or Better Auth session token in Authorization header."
3910
+ ]
3911
+ },
3912
+ null,
3913
+ 2
3914
+ )
3915
+ }
3916
+ ]
3917
+ };
3918
+ }
3919
+ const analyticsUri = parseAnalyticsSummaryUri(uri);
3920
+ if (analyticsUri) {
3921
+ const summary = await loadLatestAnalyticsSummary(options, {
3922
+ publicationId: analyticsUri.publicationId,
3923
+ range: analyticsUri.range
3924
+ });
3925
+ return {
3926
+ contents: [
3927
+ {
3928
+ uri,
3929
+ mimeType: "application/json",
3930
+ text: JSON.stringify(summary, null, 2)
3931
+ }
3932
+ ]
3933
+ };
3934
+ }
3935
+ throw new Error(`Unknown resource: ${uri}`);
3936
+ }
3937
+ function readPrompt(name) {
3938
+ if (name === "newsletter.draft_from_brief") {
3939
+ return {
3940
+ description: "Create a newsletter draft from a short brief",
3941
+ messages: [
3942
+ {
3943
+ role: "user",
3944
+ content: {
3945
+ type: "text",
3946
+ text: "Write a concise newsletter with clear sections and one CTA."
3947
+ }
3948
+ }
3949
+ ]
3950
+ };
3951
+ }
3952
+ if (name === "newsletter.subject_line_pack") {
3953
+ return {
3954
+ description: "Generate subject line variants",
3955
+ messages: [
3956
+ {
3957
+ role: "user",
3958
+ content: {
3959
+ type: "text",
3960
+ text: "Generate 10 subject lines with varied tone and urgency."
3961
+ }
3962
+ }
3963
+ ]
3964
+ };
3965
+ }
3966
+ throw new Error(`Unknown prompt: ${name}`);
3967
+ }
3968
+ async function handleMcpRequest(request, options = {}) {
3969
+ const id = request.id ?? null;
3970
+ try {
3971
+ switch (request.method) {
3972
+ case "initialize":
3973
+ return response(id, {
3974
+ protocolVersion: "2025-06-18",
3975
+ serverInfo: {
3976
+ name: "mailtea-mcp",
3977
+ version: "0.2.0"
3978
+ },
3979
+ capabilities: {
3980
+ tools: {},
3981
+ resources: {},
3982
+ prompts: {}
3983
+ }
3984
+ });
3985
+ case "tools/list":
3986
+ return response(id, { tools: MCP_TOOLS });
3987
+ case "resources/list":
3988
+ return response(id, { resources: MCP_RESOURCES });
3989
+ case "resources/read": {
3990
+ const uri = asOptionalString(request.params?.uri);
3991
+ if (!uri) {
3992
+ return error(id, -32602, "Missing required param: uri");
3993
+ }
3994
+ return response(id, await readResource(uri, options));
3995
+ }
3996
+ case "prompts/list":
3997
+ return response(id, { prompts: MCP_PROMPTS });
3998
+ case "prompts/get": {
3999
+ const promptName = asOptionalString(request.params?.name);
4000
+ if (!promptName) {
4001
+ return error(id, -32602, "Missing required param: name");
4002
+ }
4003
+ return response(id, readPrompt(promptName));
4004
+ }
4005
+ case "tools/call": {
4006
+ const params = asObject(request.params);
4007
+ const toolName = asOptionalString(params.name);
4008
+ if (!toolName) {
4009
+ return error(id, -32602, "Missing required param: name");
4010
+ }
4011
+ const argumentsValue = params.arguments;
4012
+ if (argumentsValue !== void 0 && (!argumentsValue || typeof argumentsValue !== "object" || Array.isArray(argumentsValue))) {
4013
+ return error(id, -32602, "Param arguments must be an object");
4014
+ }
4015
+ const args = argumentsValue ?? {};
4016
+ const result = await runTool(toolName, args, options);
4017
+ return response(id, result);
4018
+ }
4019
+ case "notifications/initialized":
4020
+ return response(id, {});
4021
+ default:
4022
+ return error(id, -32601, "Method not found", { method: request.method });
4023
+ }
4024
+ } catch (err) {
4025
+ return error(id, -32e3, toErrorMessage(err));
4026
+ }
4027
+ }
4028
+
4029
+ export {
4030
+ MCP_TOOLS,
4031
+ MCP_RESOURCES,
4032
+ MCP_PROMPTS,
4033
+ handleMcpRequest
4034
+ };