hypermail-mcp 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,15 @@
3
3
  A **Model Context Protocol** server that lets an agent operate any of the user's
4
4
  inboxes through a single, unified tool surface.
5
5
 
6
+ > **v0.5.0** — Replaced optional `isHtml` boolean with required `format`
7
+ > parameter (`"html"` | `"markdown"`) on `send_email`, `draft_email`, and
8
+ > `edit_draft`. Markdown bodies are converted to HTML via `marked` so
9
+ > recipients always see clean HTML.
10
+ >
11
+ > **v0.4.3** — Upgraded Zod to v4.4.3. Fixed MCP SDK v1.29.0 compatibility
12
+ > by wrapping all tool schemas in `z.object()` and replacing discriminated
13
+ > union output schemas that caused `validateToolOutput` crashes.
14
+
6
15
  The agent doesn't care whether an address is a work Outlook account, a personal
7
16
  Microsoft account, or (soon) a personal IMAP mailbox — it just calls
8
17
  `list_emails`, `search_emails`, `read_email`, `send_email` and passes the email
@@ -117,9 +126,9 @@ account store.
117
126
  | `archive_email` | `account`, `id` | Move a message to the Archive folder. Disabled under `--read-only`. |
118
127
  | `trash_email` | `account`, `id` | Move a message to Deleted Items (trash). Disabled under `--read-only`. |
119
128
  | `move_email` | `account`, `id`, `destination` | Move to any folder by well-known name (`inbox`, `drafts`, etc.) or custom folder ID. Disabled under `--read-only`. |
120
- | `send_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `isHtml?`, `include_signature?`, `inReplyTo?`, `replyAll?`, `forwardMessageId?` | Send an email. Appends signature when `include_signature` is true. `inReplyTo` sends as threaded reply; `forwardMessageId` sends as forward. Disabled under `--read-only`. |
121
- | `draft_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `isHtml?`, `include_signature?`, `inReplyTo?`, `replyAll?`, `forwardMessageId?` | Save as draft instead of sending. Returns the draft message ID and HTML body (`draftHtml`). Disabled under `--read-only`. |
122
- | `edit_draft` | `account`, `id`, `to?`, `cc?`, `bcc?`, `subject?`, `body?`, `isHtml?`, `include_signature?` | Edit an existing draft by ID. Only provided fields are updated. Returns the updated draft ID and HTML body (`draftHtml`). Disabled under `--read-only`. |
129
+ | `send_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature?`, `inReplyTo?`, `replyAll?`, `forwardMessageId?` | Send an email. `format` (`"html"` or `"markdown"`) controls body format — Markdown is converted to HTML via `marked`. Appends signature when `include_signature` is true. `inReplyTo` sends as threaded reply; `forwardMessageId` sends as forward. Disabled under `--read-only`. |
130
+ | `draft_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature?`, `inReplyTo?`, `replyAll?`, `forwardMessageId?` | Save as draft instead of sending. `format` (`"html"` or `"markdown"`) controls body format — Markdown is converted to HTML via `marked`. Returns the draft message ID and HTML body (`draftHtml`). Disabled under `--read-only`. |
131
+ | `edit_draft` | `account`, `id`, `to?`, `cc?`, `bcc?`, `subject?`, `body?`, `format?`, `include_signature?` | Edit an existing draft by ID. Only provided fields are updated. `format` only meaningful when `body` is provided. Returns the updated draft ID and HTML body (`draftHtml`). Disabled under `--read-only`. |
123
132
  | `send_draft` | `account`, `id` | Send an existing draft email by ID. Use with draft IDs returned by `draft_email` or `edit_draft`. Disabled under `--read-only`. |
124
133
  | `add_attachment_to_draft` | `account`, `id`, `path` | Attach a local file to an existing draft email. Disabled under `--read-only`. |
125
134
  | `list_folders` | `account`, `parentFolderId?` | List available mail folders. Returns top-level folders by default, or children of `parentFolderId`. |
package/dist/cli.js CHANGED
@@ -2459,6 +2459,14 @@ function buildRegistry(opts) {
2459
2459
 
2460
2460
  // src/tools/shared.ts
2461
2461
  import { z } from "zod";
2462
+
2463
+ // src/markdown-to-html.ts
2464
+ import { marked } from "marked";
2465
+ function markdownToHtml(md) {
2466
+ return marked.parse(md, { async: false });
2467
+ }
2468
+
2469
+ // src/tools/shared.ts
2462
2470
  function ok(data, structuredContent) {
2463
2471
  const result = {
2464
2472
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
@@ -2504,7 +2512,7 @@ var accountFullOutputSchema = z.object({
2504
2512
  email: z.string(),
2505
2513
  provider: providerIdEnum,
2506
2514
  displayName: z.string().optional(),
2507
- tokens: z.record(z.unknown()),
2515
+ tokens: z.record(z.string(), z.unknown()),
2508
2516
  addedAt: z.string(),
2509
2517
  signature: z.string().optional(),
2510
2518
  style: styleOutputSchema.optional()
@@ -2535,21 +2543,15 @@ var folderInfoOutputSchema = z.object({
2535
2543
  unreadItemCount: z.number()
2536
2544
  });
2537
2545
  function composeBody(input) {
2538
- const { body, isHtml = false, signature, style, includeSignature } = input;
2546
+ const { body, format, signature, style, includeSignature } = input;
2547
+ const htmlBody = format === "markdown" ? markdownToHtml(body) : body;
2539
2548
  const hasSignature = includeSignature && !!signature;
2540
2549
  const hasStyle = !!(style && (style.fontFamily || style.fontSize || style.fontColor));
2541
2550
  if (!hasSignature && !hasStyle) {
2542
- return { body, isHtml };
2551
+ return { body: htmlBody, isHtml: true };
2543
2552
  }
2544
2553
  const styleAttr = hasStyle ? buildStyleAttr(style) : "";
2545
- if (isHtml) {
2546
- let result2 = hasStyle ? `<div style="${styleAttr}">${body}</div>` : body;
2547
- if (hasSignature) result2 += `
2548
- <div class="signature">${signature}</div>`;
2549
- return { body: result2, isHtml: true };
2550
- }
2551
- const escaped = escapeHtml(body);
2552
- let result = `<div style="${styleAttr}">${escaped}</div>`;
2554
+ let result = hasStyle ? `<div style="${styleAttr}">${htmlBody}</div>` : htmlBody;
2553
2555
  if (hasSignature) result += `
2554
2556
  <div class="signature">${signature}</div>`;
2555
2557
  return { body: result, isHtml: true };
@@ -2561,9 +2563,6 @@ function buildStyleAttr(style) {
2561
2563
  if (style.fontColor) parts.push(`color: ${style.fontColor}`);
2562
2564
  return parts.join("; ");
2563
2565
  }
2564
- function escapeHtml(text) {
2565
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/\n/g, "<br>");
2566
- }
2567
2566
  function shouldRegister(name, tools) {
2568
2567
  if (tools.enabledTools) {
2569
2568
  return tools.enabledTools.has(name);
@@ -2578,15 +2577,15 @@ function shouldRegister(name, tools) {
2578
2577
  import { z as z2 } from "zod";
2579
2578
  function registerAccountTools(server, ctx) {
2580
2579
  const { store, registry, tools } = ctx;
2581
- const listAccountsOutputSchema = {
2580
+ const listAccountsOutputSchema = z2.object({
2582
2581
  accounts: z2.array(accountSummaryOutputSchema)
2583
- };
2582
+ });
2584
2583
  if (shouldRegister("list_accounts", tools)) {
2585
2584
  server.registerTool(
2586
2585
  "list_accounts",
2587
2586
  {
2588
2587
  description: "List all email accounts known to this server (no secrets). Use the returned `email` value as the `account` argument to other tools.",
2589
- inputSchema: {},
2588
+ inputSchema: z2.object({}),
2590
2589
  outputSchema: listAccountsOutputSchema
2591
2590
  },
2592
2591
  async () => {
@@ -2603,36 +2602,31 @@ function registerAccountTools(server, ctx) {
2603
2602
  }
2604
2603
  );
2605
2604
  }
2606
- const addAccountOutputSchema = z2.discriminatedUnion("status", [
2607
- z2.object({
2608
- status: z2.literal("pending"),
2609
- handle: z2.string(),
2610
- verification: z2.object({
2611
- userCode: z2.string(),
2612
- verificationUri: z2.string(),
2613
- expiresAt: z2.string(),
2614
- message: z2.string()
2615
- })
2616
- }),
2617
- z2.object({
2618
- status: z2.literal("ready"),
2619
- account: accountFullOutputSchema
2620
- })
2621
- ]);
2605
+ const addAccountOutputSchema = z2.object({
2606
+ status: z2.enum(["pending", "ready"]),
2607
+ handle: z2.string().optional(),
2608
+ verification: z2.object({
2609
+ userCode: z2.string(),
2610
+ verificationUri: z2.string(),
2611
+ expiresAt: z2.string(),
2612
+ message: z2.string()
2613
+ }).optional(),
2614
+ account: accountFullOutputSchema.optional()
2615
+ });
2622
2616
  if (shouldRegister("add_account", tools)) {
2623
2617
  server.registerTool(
2624
2618
  "add_account",
2625
2619
  {
2626
2620
  description: "Start adding an email account. For Outlook this returns a device code the user must enter at the verification URL; then call `complete_add_account` with the returned `handle` to finalize. Disabled in --read-only mode.",
2627
- inputSchema: {
2621
+ inputSchema: z2.object({
2628
2622
  provider: providerIdEnum.describe("Email backend. 'outlook' (Microsoft Graph) and 'imap' are fully implemented."),
2629
2623
  email: z2.string().email().optional().describe(
2630
2624
  "Optional hint \u2014 the provider will verify it against the auth result."
2631
2625
  ),
2632
- config: z2.record(z2.unknown()).optional().describe(
2626
+ config: z2.record(z2.string(), z2.unknown()).optional().describe(
2633
2627
  "Provider-specific config (e.g. IMAP host/port). Unused for Outlook."
2634
2628
  )
2635
- },
2629
+ }),
2636
2630
  outputSchema: addAccountOutputSchema
2637
2631
  },
2638
2632
  async (args) => {
@@ -2659,10 +2653,10 @@ function registerAccountTools(server, ctx) {
2659
2653
  "complete_add_account",
2660
2654
  {
2661
2655
  description: "Poll/finalize a pending add_account flow. Returns `pending` until the user completes the device-code step, then `ready` with the persisted account.",
2662
- inputSchema: {
2656
+ inputSchema: z2.object({
2663
2657
  provider: providerIdEnum,
2664
2658
  handle: z2.string().min(1)
2665
- },
2659
+ }),
2666
2660
  outputSchema: completeAddAccountOutputSchema
2667
2661
  },
2668
2662
  async (args) => {
@@ -2681,16 +2675,16 @@ function registerAccountTools(server, ctx) {
2681
2675
  }
2682
2676
  );
2683
2677
  }
2684
- const accountSettingsOutputSchema = {
2678
+ const accountSettingsOutputSchema = z2.object({
2685
2679
  signature: z2.string().nullable(),
2686
2680
  style: styleOutputSchema.nullable()
2687
- };
2681
+ });
2688
2682
  if (shouldRegister("get_account_settings", tools)) {
2689
2683
  server.registerTool(
2690
2684
  "get_account_settings",
2691
2685
  {
2692
2686
  description: "Get signature (HTML) and style preferences for an account.",
2693
- inputSchema: { account: z2.string().email() },
2687
+ inputSchema: z2.object({ account: z2.string().email() }),
2694
2688
  outputSchema: accountSettingsOutputSchema
2695
2689
  },
2696
2690
  async (args) => {
@@ -2714,7 +2708,7 @@ function registerAccountTools(server, ctx) {
2714
2708
  "set_account_settings",
2715
2709
  {
2716
2710
  description: "Set signature (HTML snippet) and/or style preferences for an account. Disabled in --read-only mode.",
2717
- inputSchema: {
2711
+ inputSchema: z2.object({
2718
2712
  account: z2.string().email(),
2719
2713
  signature: z2.string().optional().describe(
2720
2714
  "HTML snippet \u2014 may contain formatting, images, links. Pass null to clear."
@@ -2726,7 +2720,7 @@ function registerAccountTools(server, ctx) {
2726
2720
  }).optional().describe(
2727
2721
  "Font preferences applied to outgoing HTML emails. Pass null to clear."
2728
2722
  )
2729
- },
2723
+ }),
2730
2724
  outputSchema: accountSettingsOutputSchema
2731
2725
  },
2732
2726
  async (args) => {
@@ -2750,16 +2744,16 @@ function registerAccountTools(server, ctx) {
2750
2744
  }
2751
2745
  );
2752
2746
  }
2753
- const removeAccountOutputSchema = {
2747
+ const removeAccountOutputSchema = z2.object({
2754
2748
  removed: z2.boolean(),
2755
2749
  email: z2.string()
2756
- };
2750
+ });
2757
2751
  if (shouldRegister("remove_account", tools)) {
2758
2752
  server.registerTool(
2759
2753
  "remove_account",
2760
2754
  {
2761
2755
  description: "Forget an account and delete its stored tokens. Disabled in --read-only mode.",
2762
- inputSchema: { email: z2.string().email() },
2756
+ inputSchema: z2.object({ email: z2.string().email() }),
2763
2757
  outputSchema: removeAccountOutputSchema
2764
2758
  },
2765
2759
  async (args) => {
@@ -2803,30 +2797,30 @@ function selectBody(msg, format) {
2803
2797
  // src/tools/browse.ts
2804
2798
  function registerBrowseTools(server, ctx) {
2805
2799
  const { registry, tools } = ctx;
2806
- const emailListOutputSchema = {
2800
+ const emailListOutputSchema = z3.object({
2807
2801
  account: z3.string(),
2808
2802
  count: z3.number(),
2809
2803
  items: z3.array(emailSummaryOutputSchema),
2810
2804
  skip: z3.number(),
2811
2805
  hasMore: z3.boolean()
2812
- };
2813
- const searchEmailsOutputSchema = {
2806
+ });
2807
+ const searchEmailsOutputSchema = z3.object({
2814
2808
  account: z3.string(),
2815
2809
  count: z3.number(),
2816
2810
  items: z3.array(emailSummaryOutputSchema)
2817
- };
2811
+ });
2818
2812
  if (shouldRegister("list_emails", tools)) {
2819
2813
  server.registerTool(
2820
2814
  "list_emails",
2821
2815
  {
2822
2816
  description: "List recent emails in a folder of the given account. Pass the user's email address as `account`; the server routes to the correct backend automatically.",
2823
- inputSchema: {
2817
+ inputSchema: z3.object({
2824
2818
  account: z3.string().email(),
2825
2819
  folder: z3.string().default("inbox").optional(),
2826
2820
  limit: z3.number().int().positive().max(100).optional(),
2827
2821
  unreadOnly: z3.boolean().optional(),
2828
2822
  skip: z3.number().int().min(0).optional()
2829
- },
2823
+ }),
2830
2824
  outputSchema: emailListOutputSchema
2831
2825
  },
2832
2826
  async (args) => {
@@ -2857,11 +2851,11 @@ function registerBrowseTools(server, ctx) {
2857
2851
  "search_emails",
2858
2852
  {
2859
2853
  description: "Search emails by free-text query (KQL on Outlook). Returns lightweight summaries.",
2860
- inputSchema: {
2854
+ inputSchema: z3.object({
2861
2855
  account: z3.string().email(),
2862
2856
  query: z3.string().min(1),
2863
2857
  limit: z3.number().int().positive().max(100).optional()
2864
- },
2858
+ }),
2865
2859
  outputSchema: searchEmailsOutputSchema
2866
2860
  },
2867
2861
  async (args) => {
@@ -2882,7 +2876,7 @@ function registerBrowseTools(server, ctx) {
2882
2876
  }
2883
2877
  );
2884
2878
  }
2885
- const readEmailOutputSchema = {
2879
+ const readEmailOutputSchema = z3.object({
2886
2880
  id: z3.string(),
2887
2881
  subject: z3.string(),
2888
2882
  from: emailAddrOutputSchema.optional(),
@@ -2897,19 +2891,19 @@ function registerBrowseTools(server, ctx) {
2897
2891
  attachments: z3.array(attachmentMetaOutputSchema).optional(),
2898
2892
  body: z3.string(),
2899
2893
  bodyFormat: z3.enum(["markdown", "html", "text"])
2900
- };
2894
+ });
2901
2895
  if (shouldRegister("read_email", tools)) {
2902
2896
  server.registerTool(
2903
2897
  "read_email",
2904
2898
  {
2905
2899
  description: "Fetch a single email with full body and recipients by id. Body is returned as `body` with `bodyFormat` indicating the format. Default format is 'markdown' \u2014 HTML is automatically converted to save context tokens.",
2906
- inputSchema: {
2900
+ inputSchema: z3.object({
2907
2901
  account: z3.string().email(),
2908
2902
  id: z3.string().min(1),
2909
2903
  format: z3.enum(["markdown", "html", "text"]).default("markdown").optional().describe(
2910
2904
  "Output body format. 'markdown' converts HTML to Markdown (default), 'html' returns the raw HTML, 'text' returns plain text."
2911
2905
  )
2912
- },
2906
+ }),
2913
2907
  outputSchema: readEmailOutputSchema
2914
2908
  },
2915
2909
  async (args) => {
@@ -2941,21 +2935,21 @@ function registerBrowseTools(server, ctx) {
2941
2935
  }
2942
2936
  );
2943
2937
  }
2944
- const readAttachmentOutputSchema = {
2938
+ const readAttachmentOutputSchema = z3.object({
2945
2939
  name: z3.string(),
2946
2940
  contentType: z3.string().optional(),
2947
2941
  path: z3.string()
2948
- };
2942
+ });
2949
2943
  if (shouldRegister("read_attachment", tools)) {
2950
2944
  server.registerTool(
2951
2945
  "read_attachment",
2952
2946
  {
2953
2947
  description: "Download an email attachment to a temporary file and return its path. Use messageId and attachmentId from a prior read_email call.",
2954
- inputSchema: {
2948
+ inputSchema: z3.object({
2955
2949
  account: z3.string().email(),
2956
2950
  messageId: z3.string().min(1),
2957
2951
  attachmentId: z3.string().min(1)
2958
- },
2952
+ }),
2959
2953
  outputSchema: readAttachmentOutputSchema
2960
2954
  },
2961
2955
  async (args) => {
@@ -2979,22 +2973,22 @@ function registerBrowseTools(server, ctx) {
2979
2973
  import { z as z4 } from "zod";
2980
2974
  function registerFolderTools(server, ctx) {
2981
2975
  const { registry, tools } = ctx;
2982
- const listFoldersOutputSchema = {
2976
+ const listFoldersOutputSchema = z4.object({
2983
2977
  account: z4.string(),
2984
2978
  count: z4.number(),
2985
2979
  items: z4.array(folderInfoOutputSchema)
2986
- };
2980
+ });
2987
2981
  if (shouldRegister("list_folders", tools)) {
2988
2982
  server.registerTool(
2989
2983
  "list_folders",
2990
2984
  {
2991
2985
  description: "List available mail folders. Returns top-level folders by default, or child folders of the given parent when `parentFolderId` is provided.",
2992
- inputSchema: {
2986
+ inputSchema: z4.object({
2993
2987
  account: z4.string().email(),
2994
2988
  parentFolderId: z4.string().optional().describe(
2995
2989
  "When provided, lists child folders of this folder. When omitted, lists top-level folders (children of the root)."
2996
2990
  )
2997
- },
2991
+ }),
2998
2992
  outputSchema: listFoldersOutputSchema
2999
2993
  },
3000
2994
  async (args) => {
@@ -3015,22 +3009,22 @@ function registerFolderTools(server, ctx) {
3015
3009
  }
3016
3010
  );
3017
3011
  }
3018
- const createFolderOutputSchema = {
3012
+ const createFolderOutputSchema = z4.object({
3019
3013
  created: z4.literal(true),
3020
3014
  folder: folderInfoOutputSchema
3021
- };
3015
+ });
3022
3016
  if (shouldRegister("create_folder", tools)) {
3023
3017
  server.registerTool(
3024
3018
  "create_folder",
3025
3019
  {
3026
3020
  description: "Create a new mail folder. Creates under the root folder by default, or under the specified parent when `parentFolderId` is provided. Disabled in --read-only mode.",
3027
- inputSchema: {
3021
+ inputSchema: z4.object({
3028
3022
  account: z4.string().email(),
3029
3023
  displayName: z4.string().min(1).describe("Name of the new folder"),
3030
3024
  parentFolderId: z4.string().optional().describe(
3031
3025
  "When provided, creates the folder as a child of this folder. When omitted, creates under the root folder."
3032
3026
  )
3033
- },
3027
+ }),
3034
3028
  outputSchema: createFolderOutputSchema
3035
3029
  },
3036
3030
  async (args) => {
@@ -3048,19 +3042,19 @@ function registerFolderTools(server, ctx) {
3048
3042
  }
3049
3043
  );
3050
3044
  }
3051
- const deleteFolderOutputSchema = {
3045
+ const deleteFolderOutputSchema = z4.object({
3052
3046
  deleted: z4.literal(true),
3053
3047
  id: z4.string()
3054
- };
3048
+ });
3055
3049
  if (shouldRegister("delete_folder", tools)) {
3056
3050
  server.registerTool(
3057
3051
  "delete_folder",
3058
3052
  {
3059
3053
  description: "Delete a mail folder by ID. Disabled in --read-only mode.",
3060
- inputSchema: {
3054
+ inputSchema: z4.object({
3061
3055
  account: z4.string().email(),
3062
3056
  folderId: z4.string().min(1).describe("ID of the folder to delete")
3063
- },
3057
+ }),
3064
3058
  outputSchema: deleteFolderOutputSchema
3065
3059
  },
3066
3060
  async (args) => {
@@ -3075,20 +3069,20 @@ function registerFolderTools(server, ctx) {
3075
3069
  }
3076
3070
  );
3077
3071
  }
3078
- const renameFolderOutputSchema = {
3072
+ const renameFolderOutputSchema = z4.object({
3079
3073
  renamed: z4.literal(true),
3080
3074
  folder: folderInfoOutputSchema
3081
- };
3075
+ });
3082
3076
  if (shouldRegister("rename_folder", tools)) {
3083
3077
  server.registerTool(
3084
3078
  "rename_folder",
3085
3079
  {
3086
3080
  description: "Rename an existing mail folder. Disabled in --read-only mode.",
3087
- inputSchema: {
3081
+ inputSchema: z4.object({
3088
3082
  account: z4.string().email(),
3089
3083
  folderId: z4.string().min(1).describe("ID of the folder to rename"),
3090
3084
  newName: z4.string().min(1).describe("New display name for the folder")
3091
- },
3085
+ }),
3092
3086
  outputSchema: renameFolderOutputSchema
3093
3087
  },
3094
3088
  async (args) => {
@@ -3126,14 +3120,14 @@ function registerOrganizeTools(server, ctx) {
3126
3120
  const data = { marked: true, id: args.id, isRead };
3127
3121
  return ok(data, data);
3128
3122
  }
3129
- const archiveMoveSchema = {
3123
+ const archiveMoveSchema = z5.object({
3130
3124
  account: z5.string().email(),
3131
3125
  id: z5.string().min(1).describe("Message ID to move")
3132
- };
3133
- const archiveOutputSchema = {
3126
+ });
3127
+ const archiveOutputSchema = z5.object({
3134
3128
  archived: z5.literal(true),
3135
3129
  id: z5.string()
3136
- };
3130
+ });
3137
3131
  if (shouldRegister("archive_email", tools)) {
3138
3132
  server.registerTool(
3139
3133
  "archive_email",
@@ -3151,10 +3145,10 @@ function registerOrganizeTools(server, ctx) {
3151
3145
  }
3152
3146
  );
3153
3147
  }
3154
- const trashOutputSchema = {
3148
+ const trashOutputSchema = z5.object({
3155
3149
  trashed: z5.literal(true),
3156
3150
  id: z5.string()
3157
- };
3151
+ });
3158
3152
  if (shouldRegister("trash_email", tools)) {
3159
3153
  server.registerTool(
3160
3154
  "trash_email",
@@ -3172,23 +3166,23 @@ function registerOrganizeTools(server, ctx) {
3172
3166
  }
3173
3167
  );
3174
3168
  }
3175
- const moveEmailOutputSchema = {
3169
+ const moveEmailOutputSchema = z5.object({
3176
3170
  moved: z5.literal(true),
3177
3171
  id: z5.string(),
3178
3172
  destination: z5.string()
3179
- };
3173
+ });
3180
3174
  if (shouldRegister("move_email", tools)) {
3181
3175
  server.registerTool(
3182
3176
  "move_email",
3183
3177
  {
3184
3178
  description: "Move a message to any folder by well-known name (e.g. 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or custom folder ID. Disabled in --read-only mode.",
3185
- inputSchema: {
3179
+ inputSchema: z5.object({
3186
3180
  account: z5.string().email(),
3187
3181
  id: z5.string().min(1).describe("Message ID to move"),
3188
3182
  destination: z5.string().min(1).describe(
3189
3183
  "Destination folder \u2014 a well-known folder name ('archive', 'deleteditems', 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or a raw folder ID."
3190
3184
  )
3191
- },
3185
+ }),
3192
3186
  outputSchema: moveEmailOutputSchema
3193
3187
  },
3194
3188
  async (args) => {
@@ -3207,15 +3201,15 @@ function registerOrganizeTools(server, ctx) {
3207
3201
  }
3208
3202
  );
3209
3203
  }
3210
- const markReadInputSchema = {
3204
+ const markReadInputSchema = z5.object({
3211
3205
  account: z5.string().email(),
3212
3206
  id: z5.string().min(1).describe("Message ID to mark as read")
3213
- };
3214
- const markReadOutputSchema = {
3207
+ });
3208
+ const markReadOutputSchema = z5.object({
3215
3209
  marked: z5.literal(true),
3216
3210
  id: z5.string(),
3217
3211
  isRead: z5.boolean()
3218
- };
3212
+ });
3219
3213
  if (shouldRegister("mark_read", tools)) {
3220
3214
  server.registerTool(
3221
3215
  "mark_read",
@@ -3263,7 +3257,9 @@ function registerComposeTools(server, ctx) {
3263
3257
  bcc: z6.array(emailAddrSchema).optional(),
3264
3258
  subject: z6.string(),
3265
3259
  body: z6.string(),
3266
- isHtml: z6.boolean().optional(),
3260
+ format: z6.enum(["html", "markdown"]).describe(
3261
+ "Body format. 'html' sends the body as-is (must be valid HTML). 'markdown' converts the body from Markdown to HTML for clean rendering on the recipient side."
3262
+ ),
3267
3263
  include_signature: z6.boolean().describe(
3268
3264
  "Whether to append the account's saved HTML signature to the email. If true, don't include a signature in the body param to avoid double signature. Returns an error if true but no signature is configured for this account."
3269
3265
  ),
@@ -3287,7 +3283,7 @@ function registerComposeTools(server, ctx) {
3287
3283
  }
3288
3284
  const composed = composeBody({
3289
3285
  body: args.body,
3290
- isHtml: args.isHtml,
3286
+ format: args.format,
3291
3287
  signature: account.signature,
3292
3288
  style: account.style,
3293
3289
  includeSignature: args.include_signature
@@ -3367,7 +3363,9 @@ function registerComposeTools(server, ctx) {
3367
3363
  bcc: z6.array(emailAddrSchema).optional(),
3368
3364
  subject: z6.string().optional(),
3369
3365
  body: z6.string().optional(),
3370
- isHtml: z6.boolean().optional(),
3366
+ format: z6.enum(["html", "markdown"]).optional().describe(
3367
+ "Body format. Only meaningful when `body` is also provided. 'html' sends the body as-is (must be valid HTML). 'markdown' converts the body from Markdown to HTML for clean rendering on the recipient side."
3368
+ ),
3371
3369
  include_signature: z6.boolean().optional().describe(
3372
3370
  "Whether to re-apply the account's saved HTML signature to the body. If true, don't include a signature in the body param. Only meaningful when `body` is also provided. Returns an error if true but no signature is configured for this account."
3373
3371
  )
@@ -3399,7 +3397,7 @@ function registerComposeTools(server, ctx) {
3399
3397
  if (a.body !== void 0) {
3400
3398
  const composed = composeBody({
3401
3399
  body: a.body,
3402
- isHtml: a.isHtml,
3400
+ format: a.format ?? "html",
3403
3401
  signature: account.signature,
3404
3402
  style: account.style,
3405
3403
  includeSignature: !!a.include_signature