hypermail-mcp 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -182,9 +182,9 @@ var DEFAULT_SCOPES = [
182
182
  "Mail.ReadWrite",
183
183
  "Mail.Send"
184
184
  ];
185
- function makeConfig(prevCacheJson) {
186
- const clientId = process.env.MS_CLIENT_ID || DEFAULT_CLIENT_ID;
187
- const tenant = process.env.MS_TENANT_ID || "common";
185
+ function makeConfig(prevCacheJson, clientIdOverride, tenantOverride) {
186
+ const clientId = clientIdOverride || process.env.MS_CLIENT_ID || DEFAULT_CLIENT_ID;
187
+ const tenant = tenantOverride || process.env.MS_TENANT_ID || "common";
188
188
  return {
189
189
  auth: {
190
190
  clientId,
@@ -196,15 +196,15 @@ function makeConfig(prevCacheJson) {
196
196
  } : void 0
197
197
  };
198
198
  }
199
- function buildPca(prevCacheJson) {
200
- const pca = new PublicClientApplication(makeConfig(prevCacheJson));
199
+ function buildPca(prevCacheJson, clientIdOverride, tenantOverride) {
200
+ const pca = new PublicClientApplication(makeConfig(prevCacheJson, clientIdOverride, tenantOverride));
201
201
  if (prevCacheJson) {
202
202
  pca.getTokenCache().deserialize(prevCacheJson);
203
203
  }
204
204
  return pca;
205
205
  }
206
- function beginDeviceCode(scopes = DEFAULT_SCOPES) {
207
- const pca = buildPca();
206
+ function beginDeviceCode(scopes = DEFAULT_SCOPES, clientIdOverride, tenantOverride) {
207
+ const pca = buildPca(void 0, clientIdOverride, tenantOverride);
208
208
  let resolve;
209
209
  let reject;
210
210
  const result = new Promise(
@@ -284,8 +284,8 @@ async function awaitDeviceCodeReady(b) {
284
284
  const r = b._ready;
285
285
  await r;
286
286
  }
287
- async function acquireAccessToken(tokens, scopes = DEFAULT_SCOPES) {
288
- const pca = buildPca(tokens.msalCache);
287
+ async function acquireAccessToken(tokens, scopes = DEFAULT_SCOPES, clientIdOverride, tenantOverride) {
288
+ const pca = buildPca(tokens.msalCache, clientIdOverride, tenantOverride);
289
289
  const cache = pca.getTokenCache();
290
290
  const account = await cache.getAccountByHomeId(tokens.homeAccountId) ?? (await cache.getAllAccounts()).find((a) => a.username === tokens.username);
291
291
  if (!account) {
@@ -305,10 +305,14 @@ async function acquireAccessToken(tokens, scopes = DEFAULT_SCOPES) {
305
305
 
306
306
  // src/providers/outlook/client.ts
307
307
  var OutlookClientFactory = class {
308
- constructor(store) {
308
+ constructor(store, clientId, tenantId) {
309
309
  this.store = store;
310
+ this.clientId = clientId;
311
+ this.tenantId = tenantId;
310
312
  }
311
313
  store;
314
+ clientId;
315
+ tenantId;
312
316
  cache = /* @__PURE__ */ new Map();
313
317
  get(account) {
314
318
  const key = account.email.toLowerCase();
@@ -319,7 +323,12 @@ var OutlookClientFactory = class {
319
323
  getAccessToken: async () => {
320
324
  const fresh = store.getAccount(account.email) ?? account;
321
325
  const tokens = fresh.tokens;
322
- const { accessToken, tokens: nextTokens } = await acquireAccessToken(tokens);
326
+ const { accessToken, tokens: nextTokens } = await acquireAccessToken(
327
+ tokens,
328
+ void 0,
329
+ this.clientId,
330
+ this.tenantId
331
+ );
323
332
  if (nextTokens.msalCache !== tokens.msalCache) {
324
333
  store.upsertAccount({
325
334
  ...fresh,
@@ -362,15 +371,19 @@ function convertInlineImages(body) {
362
371
  var OutlookProvider = class {
363
372
  constructor(opts) {
364
373
  this.opts = opts;
365
- this.clients = new OutlookClientFactory(opts.store);
374
+ this.clientId = opts.clientId;
375
+ this.tenantId = opts.tenantId;
376
+ this.clients = new OutlookClientFactory(opts.store, opts.clientId, opts.tenantId);
366
377
  }
367
378
  opts;
368
379
  id = "outlook";
369
380
  clients;
370
381
  pending = /* @__PURE__ */ new Map();
382
+ clientId;
383
+ tenantId;
371
384
  // ---------- account lifecycle ----------
372
385
  async addAccount(input) {
373
- const begin = beginDeviceCode();
386
+ const begin = beginDeviceCode(void 0, this.clientId, this.tenantId);
374
387
  await awaitDeviceCodeReady(begin);
375
388
  const handle = randomUUID();
376
389
  const flow = {
@@ -440,7 +453,7 @@ var OutlookProvider = class {
440
453
  const folder = opts.folder ?? "inbox";
441
454
  const filterParts = [];
442
455
  if (opts.unreadOnly) filterParts.push("isRead eq false");
443
- let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).top(limit).select([
456
+ let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).top(limit).skip(opts.skip ?? 0).select([
444
457
  "id",
445
458
  "subject",
446
459
  "from",
@@ -452,7 +465,10 @@ var OutlookProvider = class {
452
465
  ].join(",")).orderby("receivedDateTime DESC");
453
466
  if (filterParts.length > 0) req = req.filter(filterParts.join(" and "));
454
467
  const res = await req.get();
455
- return res.value.map((m) => mapSummary(m, folder));
468
+ return {
469
+ items: res.value.map((m) => mapSummary(m, folder)),
470
+ hasMore: !!res["@odata.nextLink"]
471
+ };
456
472
  }
457
473
  async searchEmails(account, query, opts) {
458
474
  const client = this.clients.get(account);
@@ -648,11 +664,99 @@ var OutlookProvider = class {
648
664
  const draft = await client.api("/me/messages").post(draftPayload);
649
665
  return { id: draft.id };
650
666
  }
667
+ async updateDraft(account, id, update) {
668
+ const client = this.clients.get(account);
669
+ const payload = {};
670
+ if (update.subject !== void 0) {
671
+ payload.subject = update.subject;
672
+ }
673
+ if (update.to !== void 0) {
674
+ payload.toRecipients = update.to.map(toRecipient);
675
+ }
676
+ if (update.cc !== void 0) {
677
+ payload.ccRecipients = update.cc.map(toRecipient);
678
+ }
679
+ if (update.bcc !== void 0) {
680
+ payload.bccRecipients = update.bcc.map(toRecipient);
681
+ }
682
+ if (update.body !== void 0) {
683
+ const converted = convertInlineImages(update.body);
684
+ payload.body = {
685
+ contentType: update.isHtml ? "HTML" : "Text",
686
+ content: converted.body
687
+ };
688
+ if (converted.attachments.length > 0) {
689
+ payload.attachments = converted.attachments;
690
+ }
691
+ }
692
+ await client.api(`/me/messages/${encodeURIComponent(id)}`).patch(payload);
693
+ return { id };
694
+ }
651
695
  async moveEmail(account, id, destinationId) {
652
696
  const client = this.clients.get(account);
653
697
  await client.api(`/me/messages/${encodeURIComponent(id)}/move`).post({ destinationId });
654
698
  }
699
+ async sendDraft(account, id) {
700
+ const client = this.clients.get(account);
701
+ await client.api(`/me/messages/${encodeURIComponent(id)}/send`).post({});
702
+ return { id };
703
+ }
704
+ async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
705
+ const client = this.clients.get(account);
706
+ const att = await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post({
707
+ "@odata.type": "#microsoft.graph.fileAttachment",
708
+ name,
709
+ contentType: contentType ?? "application/octet-stream",
710
+ contentBytes
711
+ });
712
+ return {
713
+ id: draftId,
714
+ attachment: { id: att.id, name: att.name, contentType: att.contentType }
715
+ };
716
+ }
717
+ async markRead(account, id, isRead) {
718
+ const client = this.clients.get(account);
719
+ await client.api(`/me/messages/${encodeURIComponent(id)}`).patch({ isRead });
720
+ }
721
+ async listFolders(account, opts) {
722
+ const client = this.clients.get(account);
723
+ const endpoint = opts.parentFolderId ? `/me/mailFolders/${encodeURIComponent(opts.parentFolderId)}/childFolders` : "/me/mailFolders";
724
+ const res = await client.api(endpoint).select([
725
+ "id",
726
+ "displayName",
727
+ "parentFolderId",
728
+ "childFolderCount",
729
+ "totalItemCount",
730
+ "unreadItemCount"
731
+ ].join(",")).get();
732
+ return (res.value ?? []).map(mapFolder);
733
+ }
734
+ async createFolder(account, input) {
735
+ const client = this.clients.get(account);
736
+ const parentId = input.parentFolderId ?? "msgfolderroot";
737
+ const created = await client.api(`/me/mailFolders/${encodeURIComponent(parentId)}/childFolders`).post({ displayName: input.displayName });
738
+ return mapFolder(created);
739
+ }
740
+ async renameFolder(account, folderId, newName) {
741
+ const client = this.clients.get(account);
742
+ const updated = await client.api(`/me/mailFolders/${encodeURIComponent(folderId)}`).patch({ displayName: newName });
743
+ return mapFolder(updated);
744
+ }
745
+ async deleteFolder(account, folderId) {
746
+ const client = this.clients.get(account);
747
+ await client.api(`/me/mailFolders/${encodeURIComponent(folderId)}`).delete();
748
+ }
655
749
  };
750
+ function mapFolder(f) {
751
+ return {
752
+ id: f.id,
753
+ displayName: f.displayName,
754
+ parentFolderId: f.parentFolderId,
755
+ childFolderCount: f.childFolderCount,
756
+ totalItemCount: f.totalItemCount,
757
+ unreadItemCount: f.unreadItemCount
758
+ };
759
+ }
656
760
  function mapRecipient(r) {
657
761
  return {
658
762
  name: r.emailAddress?.name,
@@ -710,15 +814,46 @@ var ImapProvider = class {
710
814
  async saveDraft(_account, _msg) {
711
815
  throw new Error(NOT_IMPLEMENTED);
712
816
  }
817
+ async updateDraft(_account, _id, _update) {
818
+ throw new Error(NOT_IMPLEMENTED);
819
+ }
713
820
  async moveEmail(_account, _id, _destinationId) {
714
821
  throw new Error(NOT_IMPLEMENTED);
715
822
  }
823
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
824
+ async sendDraft(_account, _id) {
825
+ throw new Error(NOT_IMPLEMENTED);
826
+ }
827
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
828
+ async addAttachmentToDraft(_account, _draftId, _name, _contentBytes, _contentType) {
829
+ throw new Error(NOT_IMPLEMENTED);
830
+ }
831
+ async markRead(_account, _id, _isRead) {
832
+ throw new Error(NOT_IMPLEMENTED);
833
+ }
834
+ async listFolders(_account, _opts) {
835
+ throw new Error(NOT_IMPLEMENTED);
836
+ }
837
+ async createFolder(_account, _input) {
838
+ throw new Error(NOT_IMPLEMENTED);
839
+ }
840
+ async renameFolder(_account, _folderId, _newName) {
841
+ throw new Error(NOT_IMPLEMENTED);
842
+ }
843
+ async deleteFolder(_account, _folderId) {
844
+ throw new Error(NOT_IMPLEMENTED);
845
+ }
716
846
  };
717
847
 
718
848
  // src/providers/registry.ts
719
849
  function buildRegistry(opts) {
850
+ const outlookCfg = opts.providers?.outlook;
720
851
  const providers = /* @__PURE__ */ new Map();
721
- providers.set("outlook", new OutlookProvider({ store: opts.store }));
852
+ providers.set("outlook", new OutlookProvider({
853
+ store: opts.store,
854
+ clientId: outlookCfg?.clientId,
855
+ tenantId: outlookCfg?.tenantId
856
+ }));
722
857
  providers.set("imap", new ImapProvider());
723
858
  function get(id) {
724
859
  const p = providers.get(id);
@@ -741,41 +876,11 @@ function buildRegistry(opts) {
741
876
  };
742
877
  }
743
878
 
744
- // src/tools/index.ts
879
+ // src/tools/shared.ts
745
880
  import { z } from "zod";
746
-
747
- // src/html-to-markdown.ts
748
- import TurndownService from "turndown";
749
- var turndown = new TurndownService();
750
- function htmlToMarkdown(html) {
751
- return turndown.turndown(html);
752
- }
753
- function selectBody(msg, format) {
754
- switch (format) {
755
- case "markdown": {
756
- if (msg.bodyHtml) return htmlToMarkdown(msg.bodyHtml);
757
- if (msg.bodyText) return msg.bodyText;
758
- return "";
759
- }
760
- case "html": {
761
- if (msg.bodyHtml) return msg.bodyHtml;
762
- if (msg.bodyText) return msg.bodyText;
763
- return "";
764
- }
765
- case "text": {
766
- if (msg.bodyText) return msg.bodyText;
767
- if (msg.bodyHtml) return msg.bodyHtml.replace(/<[^>]*>/g, "");
768
- return "";
769
- }
770
- }
771
- }
772
-
773
- // src/tools/index.ts
774
881
  function ok(data, structuredContent) {
775
882
  const result = {
776
- content: [
777
- { type: "text", text: JSON.stringify(data, null, 2) }
778
- ]
883
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
779
884
  };
780
885
  if (structuredContent !== void 0) {
781
886
  result.structuredContent = structuredContent;
@@ -788,6 +893,10 @@ function fail(message) {
788
893
  content: [{ type: "text", text: message }]
789
894
  };
790
895
  }
896
+ function errMsg(err) {
897
+ if (err instanceof Error) return err.message;
898
+ return String(err);
899
+ }
791
900
  var emailAddrSchema = z.object({
792
901
  address: z.string().email(),
793
902
  name: z.string().optional()
@@ -826,369 +935,239 @@ var styleOutputSchema = z.object({
826
935
  fontSize: z.string().optional(),
827
936
  fontColor: z.string().optional()
828
937
  });
829
- function registerTools(server, opts) {
830
- const { store, registry, readOnly = false, draftOnly = false } = opts;
938
+ var folderInfoOutputSchema = z.object({
939
+ id: z.string(),
940
+ displayName: z.string(),
941
+ parentFolderId: z.string().optional(),
942
+ childFolderCount: z.number(),
943
+ totalItemCount: z.number(),
944
+ unreadItemCount: z.number()
945
+ });
946
+ function composeBody(input) {
947
+ const { body, isHtml = false, signature, style, includeSignature } = input;
948
+ const hasSignature = includeSignature && !!signature;
949
+ const hasStyle = !!(style && (style.fontFamily || style.fontSize || style.fontColor));
950
+ if (!hasSignature && !hasStyle) {
951
+ return { body, isHtml };
952
+ }
953
+ const styleAttr = hasStyle ? buildStyleAttr(style) : "";
954
+ if (isHtml) {
955
+ let result2 = hasStyle ? `<div style="${styleAttr}">${body}</div>` : body;
956
+ if (hasSignature) result2 += `
957
+ <div class="signature">${signature}</div>`;
958
+ return { body: result2, isHtml: true };
959
+ }
960
+ const escaped = escapeHtml(body);
961
+ let result = `<div style="${styleAttr}">${escaped}</div>`;
962
+ if (hasSignature) result += `
963
+ <div class="signature">${signature}</div>`;
964
+ return { body: result, isHtml: true };
965
+ }
966
+ function buildStyleAttr(style) {
967
+ const parts = [];
968
+ if (style.fontFamily) parts.push(`font-family: ${style.fontFamily}`);
969
+ if (style.fontSize) parts.push(`font-size: ${style.fontSize}`);
970
+ if (style.fontColor) parts.push(`color: ${style.fontColor}`);
971
+ return parts.join("; ");
972
+ }
973
+ function escapeHtml(text) {
974
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/\n/g, "<br>");
975
+ }
976
+ function shouldRegister(name, tools) {
977
+ if (tools.enabledTools) {
978
+ return tools.enabledTools.has(name);
979
+ }
980
+ if (tools.disabledTools) {
981
+ return !tools.disabledTools.has(name);
982
+ }
983
+ return true;
984
+ }
985
+
986
+ // src/tools/accounts.ts
987
+ import { z as z2 } from "zod";
988
+ function registerAccountTools(server, ctx) {
989
+ const { store, registry, tools } = ctx;
831
990
  const listAccountsOutputSchema = {
832
- accounts: z.array(accountSummaryOutputSchema)
991
+ accounts: z2.array(accountSummaryOutputSchema)
833
992
  };
834
- server.registerTool(
835
- "list_accounts",
836
- {
837
- description: "List all email accounts known to this server (no secrets). Use the returned `email` value as the `account` argument to other tools.",
838
- inputSchema: {},
839
- outputSchema: listAccountsOutputSchema
840
- },
841
- async () => {
842
- const rows = store.listAccounts().map((a) => ({
843
- email: a.email,
844
- provider: a.provider,
845
- displayName: a.displayName,
846
- addedAt: a.addedAt,
847
- hasSignature: !!a.signature,
848
- hasStyle: !!(a.style && (a.style.fontFamily || a.style.fontSize || a.style.fontColor))
849
- }));
850
- const data = { accounts: rows };
851
- return ok(data, data);
852
- }
853
- );
854
- const addAccountOutputSchema = z.discriminatedUnion("status", [
855
- z.object({
856
- status: z.literal("pending"),
857
- handle: z.string(),
858
- verification: z.object({
859
- userCode: z.string(),
860
- verificationUri: z.string(),
861
- expiresAt: z.string(),
862
- message: z.string()
993
+ if (shouldRegister("list_accounts", tools)) {
994
+ server.registerTool(
995
+ "list_accounts",
996
+ {
997
+ description: "List all email accounts known to this server (no secrets). Use the returned `email` value as the `account` argument to other tools.",
998
+ inputSchema: {},
999
+ outputSchema: listAccountsOutputSchema
1000
+ },
1001
+ async () => {
1002
+ const rows = store.listAccounts().map((a) => ({
1003
+ email: a.email,
1004
+ provider: a.provider,
1005
+ displayName: a.displayName,
1006
+ addedAt: a.addedAt,
1007
+ hasSignature: !!a.signature,
1008
+ hasStyle: !!(a.style && (a.style.fontFamily || a.style.fontSize || a.style.fontColor))
1009
+ }));
1010
+ const data = { accounts: rows };
1011
+ return ok(data, data);
1012
+ }
1013
+ );
1014
+ }
1015
+ const addAccountOutputSchema = z2.discriminatedUnion("status", [
1016
+ z2.object({
1017
+ status: z2.literal("pending"),
1018
+ handle: z2.string(),
1019
+ verification: z2.object({
1020
+ userCode: z2.string(),
1021
+ verificationUri: z2.string(),
1022
+ expiresAt: z2.string(),
1023
+ message: z2.string()
863
1024
  })
864
1025
  }),
865
- z.object({
866
- status: z.literal("ready"),
867
- account: z.object({
868
- email: z.string(),
869
- provider: z.enum(["outlook", "imap", "gmail"]),
870
- displayName: z.string().optional(),
871
- tokens: z.record(z.unknown()),
872
- addedAt: z.string(),
873
- signature: z.string().optional(),
1026
+ z2.object({
1027
+ status: z2.literal("ready"),
1028
+ account: z2.object({
1029
+ email: z2.string(),
1030
+ provider: z2.enum(["outlook", "imap", "gmail"]),
1031
+ displayName: z2.string().optional(),
1032
+ tokens: z2.record(z2.unknown()),
1033
+ addedAt: z2.string(),
1034
+ signature: z2.string().optional(),
874
1035
  style: styleOutputSchema.optional()
875
1036
  })
876
1037
  })
877
1038
  ]);
878
- server.registerTool(
879
- "add_account",
880
- {
881
- 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.",
882
- inputSchema: {
883
- provider: z.enum(["outlook", "imap", "gmail"]).describe("Email backend. v1 only fully implements 'outlook'."),
884
- email: z.string().email().optional().describe("Optional hint \u2014 the provider will verify it against the auth result."),
885
- config: z.record(z.unknown()).optional().describe("Provider-specific config (e.g. IMAP host/port). Unused for Outlook.")
1039
+ if (shouldRegister("add_account", tools)) {
1040
+ server.registerTool(
1041
+ "add_account",
1042
+ {
1043
+ 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.",
1044
+ inputSchema: {
1045
+ provider: z2.enum(["outlook", "imap", "gmail"]).describe("Email backend. v1 only fully implements 'outlook'."),
1046
+ email: z2.string().email().optional().describe(
1047
+ "Optional hint \u2014 the provider will verify it against the auth result."
1048
+ ),
1049
+ config: z2.record(z2.unknown()).optional().describe(
1050
+ "Provider-specific config (e.g. IMAP host/port). Unused for Outlook."
1051
+ )
1052
+ },
1053
+ outputSchema: addAccountOutputSchema
886
1054
  },
887
- outputSchema: addAccountOutputSchema
888
- },
889
- async (args) => {
890
- if (readOnly) return fail("server is in --read-only mode; add_account is disabled");
891
- const provider = registry.get(args.provider);
892
- try {
893
- const res = await provider.addAccount({ email: args.email, config: args.config });
894
- return ok(res, res);
895
- } catch (err) {
896
- return fail(errMsg(err));
1055
+ async (args) => {
1056
+ const provider = registry.get(args.provider);
1057
+ try {
1058
+ const res = await provider.addAccount({
1059
+ email: args.email,
1060
+ config: args.config
1061
+ });
1062
+ return ok(res, res);
1063
+ } catch (err) {
1064
+ return fail(errMsg(err));
1065
+ }
897
1066
  }
898
- }
899
- );
900
- const completeAddAccountOutputSchema = z.object({
901
- status: z.enum(["pending", "ready", "expired", "error"]),
902
- account: z.object({
903
- email: z.string(),
904
- provider: z.enum(["outlook", "imap", "gmail"]),
905
- displayName: z.string().optional(),
906
- tokens: z.record(z.unknown()),
907
- addedAt: z.string(),
908
- signature: z.string().optional(),
1067
+ );
1068
+ }
1069
+ const completeAddAccountOutputSchema = z2.object({
1070
+ status: z2.enum(["pending", "ready", "expired", "error"]),
1071
+ account: z2.object({
1072
+ email: z2.string(),
1073
+ provider: z2.enum(["outlook", "imap", "gmail"]),
1074
+ displayName: z2.string().optional(),
1075
+ tokens: z2.record(z2.unknown()),
1076
+ addedAt: z2.string(),
1077
+ signature: z2.string().optional(),
909
1078
  style: styleOutputSchema.optional()
910
1079
  }).optional(),
911
- error: z.string().optional()
1080
+ error: z2.string().optional()
912
1081
  });
913
- server.registerTool(
914
- "complete_add_account",
915
- {
916
- description: "Poll/finalize a pending add_account flow. Returns `pending` until the user completes the device-code step, then `ready` with the persisted account.",
917
- inputSchema: {
918
- provider: z.enum(["outlook", "imap", "gmail"]),
919
- handle: z.string().min(1)
1082
+ if (shouldRegister("complete_add_account", tools)) {
1083
+ server.registerTool(
1084
+ "complete_add_account",
1085
+ {
1086
+ description: "Poll/finalize a pending add_account flow. Returns `pending` until the user completes the device-code step, then `ready` with the persisted account.",
1087
+ inputSchema: {
1088
+ provider: z2.enum(["outlook", "imap", "gmail"]),
1089
+ handle: z2.string().min(1)
1090
+ },
1091
+ outputSchema: completeAddAccountOutputSchema
920
1092
  },
921
- outputSchema: completeAddAccountOutputSchema
922
- },
923
- async (args) => {
924
- const provider = registry.get(args.provider);
925
- if (!provider.completeAddAccount) {
926
- return fail(`provider ${args.provider} has no async add-account flow`);
927
- }
928
- try {
929
- const res = await provider.completeAddAccount(args.handle);
930
- return ok(res, res);
931
- } catch (err) {
932
- return fail(errMsg(err));
1093
+ async (args) => {
1094
+ const provider = registry.get(args.provider);
1095
+ if (!provider.completeAddAccount) {
1096
+ return fail(
1097
+ `provider ${args.provider} has no async add-account flow`
1098
+ );
1099
+ }
1100
+ try {
1101
+ const res = await provider.completeAddAccount(args.handle);
1102
+ return ok(res, res);
1103
+ } catch (err) {
1104
+ return fail(errMsg(err));
1105
+ }
933
1106
  }
934
- }
935
- );
1107
+ );
1108
+ }
936
1109
  const accountSettingsOutputSchema = {
937
- signature: z.string().nullable(),
1110
+ signature: z2.string().nullable(),
938
1111
  style: styleOutputSchema.nullable()
939
1112
  };
940
- server.registerTool(
941
- "get_account_settings",
942
- {
943
- description: "Get signature (HTML) and style preferences for an account.",
944
- inputSchema: { account: z.string().email() },
945
- outputSchema: accountSettingsOutputSchema
946
- },
947
- async (args) => {
948
- try {
949
- const acct = store.getAccount(args.account);
950
- if (!acct) return fail(`no account registered for "${args.account}"`);
951
- const data = { signature: acct.signature ?? null, style: acct.style ?? null };
952
- return ok(data, data);
953
- } catch (err) {
954
- return fail(errMsg(err));
955
- }
956
- }
957
- );
958
- server.registerTool(
959
- "set_account_settings",
960
- {
961
- description: "Set signature (HTML snippet) and/or style preferences for an account. Disabled in --read-only mode.",
962
- inputSchema: {
963
- account: z.string().email(),
964
- signature: z.string().optional().describe("HTML snippet \u2014 may contain formatting, images, links. Pass null to clear."),
965
- style: z.object({
966
- fontFamily: z.string().optional(),
967
- fontSize: z.string().optional(),
968
- fontColor: z.string().optional()
969
- }).optional().describe("Font preferences applied to outgoing HTML emails. Pass null to clear.")
970
- },
971
- outputSchema: accountSettingsOutputSchema
972
- },
973
- async (args) => {
974
- if (readOnly) return fail("server is in --read-only mode; set_account_settings is disabled");
975
- try {
976
- const acct = store.getAccount(args.account);
977
- if (!acct) return fail(`no account registered for "${args.account}"`);
978
- const updated = await store.upsertAccount({
979
- ...acct,
980
- signature: args.signature ?? acct.signature,
981
- style: args.style ?? acct.style
982
- });
983
- const data = { signature: updated.signature ?? null, style: updated.style ?? null };
984
- return ok(data, data);
985
- } catch (err) {
986
- return fail(errMsg(err));
987
- }
988
- }
989
- );
990
- const removeAccountOutputSchema = {
991
- removed: z.boolean(),
992
- email: z.string()
993
- };
994
- server.registerTool(
995
- "remove_account",
996
- {
997
- description: "Forget an account and delete its stored tokens. Disabled in --read-only mode.",
998
- inputSchema: { email: z.string().email() },
999
- outputSchema: removeAccountOutputSchema
1000
- },
1001
- async (args) => {
1002
- if (readOnly) return fail("server is in --read-only mode; remove_account is disabled");
1003
- const removed = await store.removeAccount(args.email);
1004
- const data = { removed, email: args.email };
1005
- return ok(data, data);
1006
- }
1007
- );
1008
- const emailListOutputSchema = {
1009
- account: z.string(),
1010
- count: z.number(),
1011
- items: z.array(emailSummaryOutputSchema)
1012
- };
1013
- server.registerTool(
1014
- "list_emails",
1015
- {
1016
- 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.",
1017
- inputSchema: {
1018
- account: z.string().email(),
1019
- folder: z.string().default("inbox").optional(),
1020
- limit: z.number().int().positive().max(100).optional(),
1021
- unreadOnly: z.boolean().optional()
1022
- },
1023
- outputSchema: emailListOutputSchema
1024
- },
1025
- async (args) => {
1026
- try {
1027
- const { provider, account } = registry.resolveByEmail(args.account);
1028
- const items = await provider.listEmails(account, {
1029
- folder: args.folder,
1030
- limit: args.limit,
1031
- unreadOnly: args.unreadOnly
1032
- });
1033
- const data = { account: account.email, count: items.length, items };
1034
- return ok(data, data);
1035
- } catch (err) {
1036
- return fail(errMsg(err));
1037
- }
1038
- }
1039
- );
1040
- server.registerTool(
1041
- "search_emails",
1042
- {
1043
- description: "Search emails by free-text query (KQL on Outlook). Returns lightweight summaries.",
1044
- inputSchema: {
1045
- account: z.string().email(),
1046
- query: z.string().min(1),
1047
- limit: z.number().int().positive().max(100).optional()
1048
- },
1049
- outputSchema: emailListOutputSchema
1050
- },
1051
- async (args) => {
1052
- try {
1053
- const { provider, account } = registry.resolveByEmail(args.account);
1054
- const items = await provider.searchEmails(account, args.query, {
1055
- limit: args.limit
1056
- });
1057
- const data = { account: account.email, count: items.length, items };
1058
- return ok(data, data);
1059
- } catch (err) {
1060
- return fail(errMsg(err));
1061
- }
1062
- }
1063
- );
1064
- const readEmailOutputSchema = {
1065
- id: z.string(),
1066
- subject: z.string(),
1067
- from: emailAddrOutputSchema.optional(),
1068
- to: z.array(emailAddrOutputSchema).optional(),
1069
- cc: z.array(emailAddrOutputSchema).optional(),
1070
- bcc: z.array(emailAddrOutputSchema).optional(),
1071
- receivedAt: z.string().optional(),
1072
- preview: z.string().optional(),
1073
- isRead: z.boolean().optional(),
1074
- hasAttachments: z.boolean().optional(),
1075
- folder: z.string().optional(),
1076
- attachments: z.array(attachmentMetaOutputSchema).optional(),
1077
- body: z.string(),
1078
- bodyFormat: z.enum(["markdown", "html", "text"])
1079
- };
1080
- server.registerTool(
1081
- "read_email",
1082
- {
1083
- 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.",
1084
- inputSchema: {
1085
- account: z.string().email(),
1086
- id: z.string().min(1),
1087
- format: z.enum(["markdown", "html", "text"]).default("markdown").optional().describe(
1088
- "Output body format. 'markdown' converts HTML to Markdown (default), 'html' returns the raw HTML, 'text' returns plain text."
1089
- )
1090
- },
1091
- outputSchema: readEmailOutputSchema
1092
- },
1093
- async (args) => {
1094
- try {
1095
- const { provider, account } = registry.resolveByEmail(args.account);
1096
- const msg = await provider.readEmail(account, args.id);
1097
- const format = args.format ?? "markdown";
1098
- const body = selectBody(msg, format);
1099
- const data = {
1100
- id: msg.id,
1101
- subject: msg.subject,
1102
- from: msg.from,
1103
- to: msg.to,
1104
- cc: msg.cc,
1105
- bcc: msg.bcc,
1106
- receivedAt: msg.receivedAt,
1107
- preview: msg.preview,
1108
- isRead: msg.isRead,
1109
- hasAttachments: msg.hasAttachments,
1110
- folder: msg.folder,
1111
- attachments: msg.attachments,
1112
- body,
1113
- bodyFormat: format
1114
- };
1115
- return ok(data, data);
1116
- } catch (err) {
1117
- return fail(errMsg(err));
1118
- }
1119
- }
1120
- );
1121
- const readAttachmentOutputSchema = {
1122
- name: z.string(),
1123
- contentType: z.string().optional(),
1124
- path: z.string()
1125
- };
1126
- server.registerTool(
1127
- "read_attachment",
1128
- {
1129
- description: "Download an email attachment to a temporary file and return its path. Use messageId and attachmentId from a prior read_email call.",
1130
- inputSchema: {
1131
- account: z.string().email(),
1132
- messageId: z.string().min(1),
1133
- attachmentId: z.string().min(1)
1134
- },
1135
- outputSchema: readAttachmentOutputSchema
1136
- },
1137
- async (args) => {
1138
- try {
1139
- const { provider, account } = registry.resolveByEmail(args.account);
1140
- const res = await provider.readAttachment(account, args.messageId, args.attachmentId);
1141
- return ok(res, res);
1142
- } catch (err) {
1143
- return fail(errMsg(err));
1144
- }
1145
- }
1146
- );
1147
- {
1148
- const schema = {
1149
- account: z.string().email(),
1150
- id: z.string().min(1).describe("Message ID to move")
1151
- };
1152
- const archiveOutputSchema = {
1153
- archived: z.literal(true),
1154
- id: z.string()
1155
- };
1113
+ if (shouldRegister("get_account_settings", tools)) {
1156
1114
  server.registerTool(
1157
- "archive_email",
1115
+ "get_account_settings",
1158
1116
  {
1159
- description: "Move a message to the Archive folder. Disabled in --read-only mode.",
1160
- inputSchema: schema,
1161
- outputSchema: archiveOutputSchema
1117
+ description: "Get signature (HTML) and style preferences for an account.",
1118
+ inputSchema: { account: z2.string().email() },
1119
+ outputSchema: accountSettingsOutputSchema
1162
1120
  },
1163
1121
  async (args) => {
1164
- if (readOnly) return fail("server is in --read-only mode; archive_email is disabled");
1165
1122
  try {
1166
- const { provider, account } = registry.resolveByEmail(args.account);
1167
- await provider.moveEmail(account, args.id, "archive");
1168
- const data = { archived: true, id: args.id };
1123
+ const acct = store.getAccount(args.account);
1124
+ if (!acct)
1125
+ return fail(`no account registered for "${args.account}"`);
1126
+ const data = {
1127
+ signature: acct.signature ?? null,
1128
+ style: acct.style ?? null
1129
+ };
1169
1130
  return ok(data, data);
1170
1131
  } catch (err) {
1171
1132
  return fail(errMsg(err));
1172
1133
  }
1173
1134
  }
1174
1135
  );
1175
- const trashOutputSchema = {
1176
- trashed: z.literal(true),
1177
- id: z.string()
1178
- };
1136
+ }
1137
+ if (shouldRegister("set_account_settings", tools)) {
1179
1138
  server.registerTool(
1180
- "trash_email",
1139
+ "set_account_settings",
1181
1140
  {
1182
- description: "Move a message to the Deleted Items (trash) folder. Disabled in --read-only mode.",
1183
- inputSchema: schema,
1184
- outputSchema: trashOutputSchema
1141
+ description: "Set signature (HTML snippet) and/or style preferences for an account. Disabled in --read-only mode.",
1142
+ inputSchema: {
1143
+ account: z2.string().email(),
1144
+ signature: z2.string().optional().describe(
1145
+ "HTML snippet \u2014 may contain formatting, images, links. Pass null to clear."
1146
+ ),
1147
+ style: z2.object({
1148
+ fontFamily: z2.string().optional(),
1149
+ fontSize: z2.string().optional(),
1150
+ fontColor: z2.string().optional()
1151
+ }).optional().describe(
1152
+ "Font preferences applied to outgoing HTML emails. Pass null to clear."
1153
+ )
1154
+ },
1155
+ outputSchema: accountSettingsOutputSchema
1185
1156
  },
1186
1157
  async (args) => {
1187
- if (readOnly) return fail("server is in --read-only mode; trash_email is disabled");
1188
1158
  try {
1189
- const { provider, account } = registry.resolveByEmail(args.account);
1190
- await provider.moveEmail(account, args.id, "deleteditems");
1191
- const data = { trashed: true, id: args.id };
1159
+ const acct = store.getAccount(args.account);
1160
+ if (!acct)
1161
+ return fail(`no account registered for "${args.account}"`);
1162
+ const updated = await store.upsertAccount({
1163
+ ...acct,
1164
+ signature: args.signature ?? acct.signature,
1165
+ style: args.style ?? acct.style
1166
+ });
1167
+ const data = {
1168
+ signature: updated.signature ?? null,
1169
+ style: updated.style ?? null
1170
+ };
1192
1171
  return ok(data, data);
1193
1172
  } catch (err) {
1194
1173
  return fail(errMsg(err));
@@ -1196,59 +1175,536 @@ function registerTools(server, opts) {
1196
1175
  }
1197
1176
  );
1198
1177
  }
1199
- const moveEmailOutputSchema = {
1200
- moved: z.literal(true),
1201
- id: z.string(),
1202
- destination: z.string()
1178
+ const removeAccountOutputSchema = {
1179
+ removed: z2.boolean(),
1180
+ email: z2.string()
1203
1181
  };
1204
- server.registerTool(
1205
- "move_email",
1206
- {
1207
- 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.",
1208
- inputSchema: {
1209
- account: z.string().email(),
1210
- id: z.string().min(1).describe("Message ID to move"),
1211
- destination: z.string().min(1).describe(
1212
- "Destination folder \u2014 a well-known folder name ('archive', 'deleteditems', 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or a raw folder ID."
1213
- )
1182
+ if (shouldRegister("remove_account", tools)) {
1183
+ server.registerTool(
1184
+ "remove_account",
1185
+ {
1186
+ description: "Forget an account and delete its stored tokens. Disabled in --read-only mode.",
1187
+ inputSchema: { email: z2.string().email() },
1188
+ outputSchema: removeAccountOutputSchema
1214
1189
  },
1215
- outputSchema: moveEmailOutputSchema
1216
- },
1217
- async (args) => {
1218
- if (readOnly) return fail("server is in --read-only mode; move_email is disabled");
1219
- try {
1220
- const { provider, account } = registry.resolveByEmail(args.account);
1221
- await provider.moveEmail(account, args.id, args.destination);
1222
- const data = { moved: true, id: args.id, destination: args.destination };
1190
+ async (args) => {
1191
+ const removed = await store.removeAccount(args.email);
1192
+ const data = { removed, email: args.email };
1223
1193
  return ok(data, data);
1224
- } catch (err) {
1225
- return fail(errMsg(err));
1226
1194
  }
1227
- }
1228
- );
1229
- const sendEmailSchema = z.object({
1230
- account: z.string().email(),
1231
- to: z.array(emailAddrSchema).min(1),
1232
- cc: z.array(emailAddrSchema).optional(),
1233
- bcc: z.array(emailAddrSchema).optional(),
1234
- subject: z.string(),
1235
- body: z.string(),
1236
- isHtml: z.boolean().optional(),
1237
- include_signature: z.boolean().describe(
1238
- "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."
1239
- ),
1240
- inReplyTo: z.string().optional().describe(
1241
- "Message ID to reply to. When set, sends as a threaded reply which includes the quoted thread history automatically."
1242
- ),
1243
- replyAll: z.boolean().default(false).optional().describe(
1244
- "When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
1245
- ),
1246
- forwardMessageId: z.string().optional().describe(
1195
+ );
1196
+ }
1197
+ }
1198
+
1199
+ // src/tools/browse.ts
1200
+ import { z as z3 } from "zod";
1201
+
1202
+ // src/html-to-markdown.ts
1203
+ import TurndownService from "turndown";
1204
+ var turndown = new TurndownService();
1205
+ function htmlToMarkdown(html) {
1206
+ return turndown.turndown(html);
1207
+ }
1208
+ function selectBody(msg, format) {
1209
+ switch (format) {
1210
+ case "markdown": {
1211
+ if (msg.bodyHtml) return htmlToMarkdown(msg.bodyHtml);
1212
+ if (msg.bodyText) return msg.bodyText;
1213
+ return "";
1214
+ }
1215
+ case "html": {
1216
+ if (msg.bodyHtml) return msg.bodyHtml;
1217
+ if (msg.bodyText) return msg.bodyText;
1218
+ return "";
1219
+ }
1220
+ case "text": {
1221
+ if (msg.bodyText) return msg.bodyText;
1222
+ if (msg.bodyHtml) return msg.bodyHtml.replace(/<[^>]*>/g, "");
1223
+ return "";
1224
+ }
1225
+ }
1226
+ }
1227
+
1228
+ // src/tools/browse.ts
1229
+ function registerBrowseTools(server, ctx) {
1230
+ const { registry, tools } = ctx;
1231
+ const emailListOutputSchema = {
1232
+ account: z3.string(),
1233
+ count: z3.number(),
1234
+ items: z3.array(emailSummaryOutputSchema),
1235
+ skip: z3.number(),
1236
+ hasMore: z3.boolean()
1237
+ };
1238
+ if (shouldRegister("list_emails", tools)) {
1239
+ server.registerTool(
1240
+ "list_emails",
1241
+ {
1242
+ 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.",
1243
+ inputSchema: {
1244
+ account: z3.string().email(),
1245
+ folder: z3.string().default("inbox").optional(),
1246
+ limit: z3.number().int().positive().max(100).optional(),
1247
+ unreadOnly: z3.boolean().optional(),
1248
+ skip: z3.number().int().min(0).optional()
1249
+ },
1250
+ outputSchema: emailListOutputSchema
1251
+ },
1252
+ async (args) => {
1253
+ try {
1254
+ const { provider, account } = registry.resolveByEmail(args.account);
1255
+ const { items, hasMore } = await provider.listEmails(account, {
1256
+ folder: args.folder,
1257
+ limit: args.limit,
1258
+ unreadOnly: args.unreadOnly,
1259
+ skip: args.skip
1260
+ });
1261
+ const data = {
1262
+ account: account.email,
1263
+ count: items.length,
1264
+ items,
1265
+ skip: args.skip ?? 0,
1266
+ hasMore
1267
+ };
1268
+ return ok(data, data);
1269
+ } catch (err) {
1270
+ return fail(errMsg(err));
1271
+ }
1272
+ }
1273
+ );
1274
+ }
1275
+ if (shouldRegister("search_emails", tools)) {
1276
+ server.registerTool(
1277
+ "search_emails",
1278
+ {
1279
+ description: "Search emails by free-text query (KQL on Outlook). Returns lightweight summaries.",
1280
+ inputSchema: {
1281
+ account: z3.string().email(),
1282
+ query: z3.string().min(1),
1283
+ limit: z3.number().int().positive().max(100).optional()
1284
+ },
1285
+ outputSchema: emailListOutputSchema
1286
+ },
1287
+ async (args) => {
1288
+ try {
1289
+ const { provider, account } = registry.resolveByEmail(args.account);
1290
+ const items = await provider.searchEmails(account, args.query, {
1291
+ limit: args.limit
1292
+ });
1293
+ const data = {
1294
+ account: account.email,
1295
+ count: items.length,
1296
+ items
1297
+ };
1298
+ return ok(data, data);
1299
+ } catch (err) {
1300
+ return fail(errMsg(err));
1301
+ }
1302
+ }
1303
+ );
1304
+ }
1305
+ const readEmailOutputSchema = {
1306
+ id: z3.string(),
1307
+ subject: z3.string(),
1308
+ from: emailAddrOutputSchema.optional(),
1309
+ to: z3.array(emailAddrOutputSchema).optional(),
1310
+ cc: z3.array(emailAddrOutputSchema).optional(),
1311
+ bcc: z3.array(emailAddrOutputSchema).optional(),
1312
+ receivedAt: z3.string().optional(),
1313
+ preview: z3.string().optional(),
1314
+ isRead: z3.boolean().optional(),
1315
+ hasAttachments: z3.boolean().optional(),
1316
+ folder: z3.string().optional(),
1317
+ attachments: z3.array(attachmentMetaOutputSchema).optional(),
1318
+ body: z3.string(),
1319
+ bodyFormat: z3.enum(["markdown", "html", "text"])
1320
+ };
1321
+ if (shouldRegister("read_email", tools)) {
1322
+ server.registerTool(
1323
+ "read_email",
1324
+ {
1325
+ 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.",
1326
+ inputSchema: {
1327
+ account: z3.string().email(),
1328
+ id: z3.string().min(1),
1329
+ format: z3.enum(["markdown", "html", "text"]).default("markdown").optional().describe(
1330
+ "Output body format. 'markdown' converts HTML to Markdown (default), 'html' returns the raw HTML, 'text' returns plain text."
1331
+ )
1332
+ },
1333
+ outputSchema: readEmailOutputSchema
1334
+ },
1335
+ async (args) => {
1336
+ try {
1337
+ const { provider, account } = registry.resolveByEmail(args.account);
1338
+ const msg = await provider.readEmail(account, args.id);
1339
+ const format = args.format ?? "markdown";
1340
+ const body = selectBody(msg, format);
1341
+ const data = {
1342
+ id: msg.id,
1343
+ subject: msg.subject,
1344
+ from: msg.from,
1345
+ to: msg.to,
1346
+ cc: msg.cc,
1347
+ bcc: msg.bcc,
1348
+ receivedAt: msg.receivedAt,
1349
+ preview: msg.preview,
1350
+ isRead: msg.isRead,
1351
+ hasAttachments: msg.hasAttachments,
1352
+ folder: msg.folder,
1353
+ attachments: msg.attachments,
1354
+ body,
1355
+ bodyFormat: format
1356
+ };
1357
+ return ok(data, data);
1358
+ } catch (err) {
1359
+ return fail(errMsg(err));
1360
+ }
1361
+ }
1362
+ );
1363
+ }
1364
+ const readAttachmentOutputSchema = {
1365
+ name: z3.string(),
1366
+ contentType: z3.string().optional(),
1367
+ path: z3.string()
1368
+ };
1369
+ if (shouldRegister("read_attachment", tools)) {
1370
+ server.registerTool(
1371
+ "read_attachment",
1372
+ {
1373
+ description: "Download an email attachment to a temporary file and return its path. Use messageId and attachmentId from a prior read_email call.",
1374
+ inputSchema: {
1375
+ account: z3.string().email(),
1376
+ messageId: z3.string().min(1),
1377
+ attachmentId: z3.string().min(1)
1378
+ },
1379
+ outputSchema: readAttachmentOutputSchema
1380
+ },
1381
+ async (args) => {
1382
+ try {
1383
+ const { provider, account } = registry.resolveByEmail(args.account);
1384
+ const res = await provider.readAttachment(
1385
+ account,
1386
+ args.messageId,
1387
+ args.attachmentId
1388
+ );
1389
+ return ok(res, res);
1390
+ } catch (err) {
1391
+ return fail(errMsg(err));
1392
+ }
1393
+ }
1394
+ );
1395
+ }
1396
+ }
1397
+
1398
+ // src/tools/folders.ts
1399
+ import { z as z4 } from "zod";
1400
+ function registerFolderTools(server, ctx) {
1401
+ const { registry, tools } = ctx;
1402
+ const listFoldersOutputSchema = {
1403
+ account: z4.string(),
1404
+ count: z4.number(),
1405
+ items: z4.array(folderInfoOutputSchema)
1406
+ };
1407
+ if (shouldRegister("list_folders", tools)) {
1408
+ server.registerTool(
1409
+ "list_folders",
1410
+ {
1411
+ description: "List available mail folders. Returns top-level folders by default, or child folders of the given parent when `parentFolderId` is provided.",
1412
+ inputSchema: {
1413
+ account: z4.string().email(),
1414
+ parentFolderId: z4.string().optional().describe(
1415
+ "When provided, lists child folders of this folder. When omitted, lists top-level folders (children of the root)."
1416
+ )
1417
+ },
1418
+ outputSchema: listFoldersOutputSchema
1419
+ },
1420
+ async (args) => {
1421
+ try {
1422
+ const { provider, account } = registry.resolveByEmail(args.account);
1423
+ const items = await provider.listFolders(account, {
1424
+ parentFolderId: args.parentFolderId
1425
+ });
1426
+ const data = {
1427
+ account: account.email,
1428
+ count: items.length,
1429
+ items
1430
+ };
1431
+ return ok(data, data);
1432
+ } catch (err) {
1433
+ return fail(errMsg(err));
1434
+ }
1435
+ }
1436
+ );
1437
+ }
1438
+ const createFolderOutputSchema = {
1439
+ created: z4.literal(true),
1440
+ folder: folderInfoOutputSchema
1441
+ };
1442
+ if (shouldRegister("create_folder", tools)) {
1443
+ server.registerTool(
1444
+ "create_folder",
1445
+ {
1446
+ 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.",
1447
+ inputSchema: {
1448
+ account: z4.string().email(),
1449
+ displayName: z4.string().min(1).describe("Name of the new folder"),
1450
+ parentFolderId: z4.string().optional().describe(
1451
+ "When provided, creates the folder as a child of this folder. When omitted, creates under the root folder."
1452
+ )
1453
+ },
1454
+ outputSchema: createFolderOutputSchema
1455
+ },
1456
+ async (args) => {
1457
+ try {
1458
+ const { provider, account } = registry.resolveByEmail(args.account);
1459
+ const folder = await provider.createFolder(account, {
1460
+ displayName: args.displayName,
1461
+ parentFolderId: args.parentFolderId
1462
+ });
1463
+ const data = { created: true, folder };
1464
+ return ok(data, data);
1465
+ } catch (err) {
1466
+ return fail(errMsg(err));
1467
+ }
1468
+ }
1469
+ );
1470
+ }
1471
+ const deleteFolderOutputSchema = {
1472
+ deleted: z4.literal(true),
1473
+ id: z4.string()
1474
+ };
1475
+ if (shouldRegister("delete_folder", tools)) {
1476
+ server.registerTool(
1477
+ "delete_folder",
1478
+ {
1479
+ description: "Delete a mail folder by ID. Disabled in --read-only mode.",
1480
+ inputSchema: {
1481
+ account: z4.string().email(),
1482
+ folderId: z4.string().min(1).describe("ID of the folder to delete")
1483
+ },
1484
+ outputSchema: deleteFolderOutputSchema
1485
+ },
1486
+ async (args) => {
1487
+ try {
1488
+ const { provider, account } = registry.resolveByEmail(args.account);
1489
+ await provider.deleteFolder(account, args.folderId);
1490
+ const data = { deleted: true, id: args.folderId };
1491
+ return ok(data, data);
1492
+ } catch (err) {
1493
+ return fail(errMsg(err));
1494
+ }
1495
+ }
1496
+ );
1497
+ }
1498
+ const renameFolderOutputSchema = {
1499
+ renamed: z4.literal(true),
1500
+ folder: folderInfoOutputSchema
1501
+ };
1502
+ if (shouldRegister("rename_folder", tools)) {
1503
+ server.registerTool(
1504
+ "rename_folder",
1505
+ {
1506
+ description: "Rename an existing mail folder. Disabled in --read-only mode.",
1507
+ inputSchema: {
1508
+ account: z4.string().email(),
1509
+ folderId: z4.string().min(1).describe("ID of the folder to rename"),
1510
+ newName: z4.string().min(1).describe("New display name for the folder")
1511
+ },
1512
+ outputSchema: renameFolderOutputSchema
1513
+ },
1514
+ async (args) => {
1515
+ try {
1516
+ const { provider, account } = registry.resolveByEmail(args.account);
1517
+ const folder = await provider.renameFolder(
1518
+ account,
1519
+ args.folderId,
1520
+ args.newName
1521
+ );
1522
+ const data = { renamed: true, folder };
1523
+ return ok(data, data);
1524
+ } catch (err) {
1525
+ return fail(errMsg(err));
1526
+ }
1527
+ }
1528
+ );
1529
+ }
1530
+ }
1531
+
1532
+ // src/tools/organize.ts
1533
+ import { z as z5 } from "zod";
1534
+ function registerOrganizeTools(server, ctx) {
1535
+ const { registry, tools } = ctx;
1536
+ const archiveMoveSchema = {
1537
+ account: z5.string().email(),
1538
+ id: z5.string().min(1).describe("Message ID to move")
1539
+ };
1540
+ const archiveOutputSchema = {
1541
+ archived: z5.literal(true),
1542
+ id: z5.string()
1543
+ };
1544
+ if (shouldRegister("archive_email", tools)) {
1545
+ server.registerTool(
1546
+ "archive_email",
1547
+ {
1548
+ description: "Move a message to the Archive folder. Disabled in --read-only mode.",
1549
+ inputSchema: archiveMoveSchema,
1550
+ outputSchema: archiveOutputSchema
1551
+ },
1552
+ async (args) => {
1553
+ try {
1554
+ const { provider, account } = registry.resolveByEmail(args.account);
1555
+ await provider.moveEmail(account, args.id, "archive");
1556
+ const data = { archived: true, id: args.id };
1557
+ return ok(data, data);
1558
+ } catch (err) {
1559
+ return fail(errMsg(err));
1560
+ }
1561
+ }
1562
+ );
1563
+ }
1564
+ const trashOutputSchema = {
1565
+ trashed: z5.literal(true),
1566
+ id: z5.string()
1567
+ };
1568
+ if (shouldRegister("trash_email", tools)) {
1569
+ server.registerTool(
1570
+ "trash_email",
1571
+ {
1572
+ description: "Move a message to the Deleted Items (trash) folder. Disabled in --read-only mode.",
1573
+ inputSchema: archiveMoveSchema,
1574
+ outputSchema: trashOutputSchema
1575
+ },
1576
+ async (args) => {
1577
+ try {
1578
+ const { provider, account } = registry.resolveByEmail(args.account);
1579
+ await provider.moveEmail(account, args.id, "deleteditems");
1580
+ const data = { trashed: true, id: args.id };
1581
+ return ok(data, data);
1582
+ } catch (err) {
1583
+ return fail(errMsg(err));
1584
+ }
1585
+ }
1586
+ );
1587
+ }
1588
+ const moveEmailOutputSchema = {
1589
+ moved: z5.literal(true),
1590
+ id: z5.string(),
1591
+ destination: z5.string()
1592
+ };
1593
+ if (shouldRegister("move_email", tools)) {
1594
+ server.registerTool(
1595
+ "move_email",
1596
+ {
1597
+ 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.",
1598
+ inputSchema: {
1599
+ account: z5.string().email(),
1600
+ id: z5.string().min(1).describe("Message ID to move"),
1601
+ destination: z5.string().min(1).describe(
1602
+ "Destination folder \u2014 a well-known folder name ('archive', 'deleteditems', 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or a raw folder ID."
1603
+ )
1604
+ },
1605
+ outputSchema: moveEmailOutputSchema
1606
+ },
1607
+ async (args) => {
1608
+ try {
1609
+ const { provider, account } = registry.resolveByEmail(args.account);
1610
+ await provider.moveEmail(account, args.id, args.destination);
1611
+ const data = {
1612
+ moved: true,
1613
+ id: args.id,
1614
+ destination: args.destination
1615
+ };
1616
+ return ok(data, data);
1617
+ } catch (err) {
1618
+ return fail(errMsg(err));
1619
+ }
1620
+ }
1621
+ );
1622
+ }
1623
+ const markReadInputSchema = {
1624
+ account: z5.string().email(),
1625
+ id: z5.string().min(1).describe("Message ID to mark as read")
1626
+ };
1627
+ const markReadOutputSchema = {
1628
+ marked: z5.literal(true),
1629
+ id: z5.string(),
1630
+ isRead: z5.boolean()
1631
+ };
1632
+ if (shouldRegister("mark_read", tools)) {
1633
+ server.registerTool(
1634
+ "mark_read",
1635
+ {
1636
+ description: "Mark a message as read. Disabled in --read-only mode.",
1637
+ inputSchema: markReadInputSchema,
1638
+ outputSchema: markReadOutputSchema
1639
+ },
1640
+ async (args) => {
1641
+ try {
1642
+ const { provider, account } = registry.resolveByEmail(args.account);
1643
+ await provider.markRead(account, args.id, true);
1644
+ const data = {
1645
+ marked: true,
1646
+ id: args.id,
1647
+ isRead: true
1648
+ };
1649
+ return ok(data, data);
1650
+ } catch (err) {
1651
+ return fail(errMsg(err));
1652
+ }
1653
+ }
1654
+ );
1655
+ }
1656
+ if (shouldRegister("mark_unread", tools)) {
1657
+ server.registerTool(
1658
+ "mark_unread",
1659
+ {
1660
+ description: "Mark a message as unread. Disabled in --read-only mode.",
1661
+ inputSchema: markReadInputSchema,
1662
+ outputSchema: markReadOutputSchema
1663
+ },
1664
+ async (args) => {
1665
+ try {
1666
+ const { provider, account } = registry.resolveByEmail(args.account);
1667
+ await provider.markRead(account, args.id, false);
1668
+ const data = {
1669
+ marked: true,
1670
+ id: args.id,
1671
+ isRead: false
1672
+ };
1673
+ return ok(data, data);
1674
+ } catch (err) {
1675
+ return fail(errMsg(err));
1676
+ }
1677
+ }
1678
+ );
1679
+ }
1680
+ }
1681
+
1682
+ // src/tools/compose.ts
1683
+ import { z as z6 } from "zod";
1684
+ function registerComposeTools(server, ctx) {
1685
+ const { store, registry, tools } = ctx;
1686
+ const sendEmailSchema = z6.object({
1687
+ account: z6.string().email(),
1688
+ to: z6.array(emailAddrSchema).min(1),
1689
+ cc: z6.array(emailAddrSchema).optional(),
1690
+ bcc: z6.array(emailAddrSchema).optional(),
1691
+ subject: z6.string(),
1692
+ body: z6.string(),
1693
+ isHtml: z6.boolean().optional(),
1694
+ include_signature: z6.boolean().describe(
1695
+ "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."
1696
+ ),
1697
+ inReplyTo: z6.string().optional().describe(
1698
+ "Message ID to reply to. When set, sends as a threaded reply which includes the quoted thread history automatically."
1699
+ ),
1700
+ replyAll: z6.boolean().default(false).optional().describe(
1701
+ "When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
1702
+ ),
1703
+ forwardMessageId: z6.string().optional().describe(
1247
1704
  "Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
1248
1705
  )
1249
1706
  });
1250
1707
  async function handleSendOrDraft(args, action, resultKey, toolName) {
1251
- if (readOnly) return fail(`server is in --read-only mode; ${toolName} is disabled`);
1252
1708
  try {
1253
1709
  const { provider, account } = registry.resolveByEmail(args.account);
1254
1710
  if (args.include_signature && !account.signature) {
@@ -1290,10 +1746,10 @@ function registerTools(server, opts) {
1290
1746
  }
1291
1747
  }
1292
1748
  const sendEmailOutputSchema = {
1293
- sent: z.literal(true),
1294
- id: z.string()
1749
+ sent: z6.literal(true),
1750
+ id: z6.string()
1295
1751
  };
1296
- if (!draftOnly) {
1752
+ if (shouldRegister("send_email", tools)) {
1297
1753
  server.registerTool(
1298
1754
  "send_email",
1299
1755
  {
@@ -1310,74 +1766,323 @@ function registerTools(server, opts) {
1310
1766
  );
1311
1767
  }
1312
1768
  const draftEmailOutputSchema = {
1313
- draft: z.literal(true),
1314
- id: z.string(),
1315
- draftHtml: z.string().optional()
1769
+ draft: z6.literal(true),
1770
+ id: z6.string(),
1771
+ draftHtml: z6.string().optional()
1316
1772
  };
1317
- server.registerTool(
1318
- "draft_email",
1319
- {
1320
- description: "Create a draft email from the given account without sending it. Works identically to send_email \u2014 appends signature when `include_signature` is true, applies style, and supports replies and forwards \u2014 but saves the message to the Drafts folder instead of sending. Returns the draft message ID and the draft's HTML body content (`draftHtml`). Before sending the draft, inspect `draftHtml` to verify the draft looks correct: no duplicate signature blocks, no broken or missing inline images, no malformed HTML, and no other formatting issues. Disabled in --read-only mode.",
1321
- inputSchema: sendEmailSchema,
1322
- outputSchema: draftEmailOutputSchema
1323
- },
1324
- async (args) => handleSendOrDraft(
1325
- args,
1326
- (p, a, m) => p.saveDraft(a, m),
1327
- "draft",
1328
- "draft_email"
1773
+ if (shouldRegister("draft_email", tools)) {
1774
+ server.registerTool(
1775
+ "draft_email",
1776
+ {
1777
+ description: "Create a draft email from the given account without sending it. Works identically to send_email \u2014 appends signature when `include_signature` is true, applies style, and supports replies and forwards \u2014 but saves the message to the Drafts folder instead of sending. Returns the draft message ID and the draft's HTML body content (`draftHtml`). Before sending the draft, inspect `draftHtml` to verify the draft looks correct: no duplicate signature blocks, no broken or missing inline images, no malformed HTML, and no other formatting issues. Disabled in --read-only mode.",
1778
+ inputSchema: sendEmailSchema,
1779
+ outputSchema: draftEmailOutputSchema
1780
+ },
1781
+ async (args) => handleSendOrDraft(
1782
+ args,
1783
+ (p, a, m) => p.saveDraft(a, m),
1784
+ "draft",
1785
+ "draft_email"
1786
+ )
1787
+ );
1788
+ }
1789
+ const editDraftSchema = z6.object({
1790
+ account: z6.string().email(),
1791
+ id: z6.string().min(1).describe("Draft message ID to edit"),
1792
+ to: z6.array(emailAddrSchema).optional(),
1793
+ cc: z6.array(emailAddrSchema).optional(),
1794
+ bcc: z6.array(emailAddrSchema).optional(),
1795
+ subject: z6.string().optional(),
1796
+ body: z6.string().optional(),
1797
+ isHtml: z6.boolean().optional(),
1798
+ include_signature: z6.boolean().optional().describe(
1799
+ "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."
1329
1800
  )
1330
- );
1331
- }
1332
- function composeBody(input) {
1333
- const { body, isHtml = false, signature, style, includeSignature } = input;
1334
- const hasSignature = includeSignature && !!signature;
1335
- const hasStyle = !!(style && (style.fontFamily || style.fontSize || style.fontColor));
1336
- if (!hasSignature && !hasStyle) {
1337
- return { body, isHtml };
1801
+ });
1802
+ const editDraftOutputSchema = {
1803
+ edited: z6.literal(true),
1804
+ id: z6.string(),
1805
+ draftHtml: z6.string().optional()
1806
+ };
1807
+ if (shouldRegister("edit_draft", tools)) {
1808
+ server.registerTool(
1809
+ "edit_draft",
1810
+ {
1811
+ description: "Edit an existing draft email by ID. Only the fields you provide are updated \u2014 unmentioned fields stay unchanged. When `body` is provided and `include_signature` is true, the account's signature is re-applied. Returns the draft ID and the draft's updated HTML body content (`draftHtml`). Before sending, inspect `draftHtml` to verify the draft looks correct. Does not support changing `inReplyTo` or `forwardMessageId` \u2014 those are set at creation time via `draft_email`. Disabled in --read-only mode.",
1812
+ inputSchema: editDraftSchema,
1813
+ outputSchema: editDraftOutputSchema
1814
+ },
1815
+ async (args) => {
1816
+ const a = args;
1817
+ try {
1818
+ const { provider, account } = registry.resolveByEmail(a.account);
1819
+ if (a.include_signature && !account.signature) {
1820
+ return fail(
1821
+ "include_signature is true but no signature is configured for this account. Set up a signature first with set_account_settings."
1822
+ );
1823
+ }
1824
+ let bodyPayload;
1825
+ let isHtmlPayload;
1826
+ if (a.body !== void 0) {
1827
+ const composed = composeBody({
1828
+ body: a.body,
1829
+ isHtml: a.isHtml,
1830
+ signature: account.signature,
1831
+ style: account.style,
1832
+ includeSignature: !!a.include_signature
1833
+ });
1834
+ bodyPayload = composed.body;
1835
+ isHtmlPayload = composed.isHtml;
1836
+ }
1837
+ const res = await provider.updateDraft(account, a.id, {
1838
+ to: a.to,
1839
+ cc: a.cc,
1840
+ bcc: a.bcc,
1841
+ subject: a.subject,
1842
+ body: bodyPayload,
1843
+ isHtml: isHtmlPayload
1844
+ });
1845
+ const draft = await provider.readEmail(account, res.id);
1846
+ const result = {
1847
+ edited: true,
1848
+ id: res.id,
1849
+ draftHtml: draft.bodyHtml
1850
+ };
1851
+ return ok(result, result);
1852
+ } catch (err) {
1853
+ return fail(errMsg(err));
1854
+ }
1855
+ }
1856
+ );
1338
1857
  }
1339
- const styleAttr = hasStyle ? buildStyleAttr(style) : "";
1340
- if (isHtml) {
1341
- let result2 = hasStyle ? `<div style="${styleAttr}">${body}</div>` : body;
1342
- if (hasSignature) result2 += `
1343
- <div class="signature">${signature}</div>`;
1344
- return { body: result2, isHtml: true };
1858
+ const sendDraftOutputSchema = {
1859
+ sent: z6.literal(true),
1860
+ id: z6.string()
1861
+ };
1862
+ if (shouldRegister("send_draft", tools)) {
1863
+ server.registerTool(
1864
+ "send_draft",
1865
+ {
1866
+ description: "Send an existing draft email by ID. Use this with draft IDs returned by `draft_email` or `edit_draft`. Disabled in --read-only mode.",
1867
+ inputSchema: {
1868
+ account: z6.string().email(),
1869
+ id: z6.string().min(1).describe("Draft message ID to send")
1870
+ },
1871
+ outputSchema: sendDraftOutputSchema
1872
+ },
1873
+ async (args) => {
1874
+ try {
1875
+ const { provider, account } = registry.resolveByEmail(args.account);
1876
+ const res = await provider.sendDraft(account, args.id);
1877
+ const data = { sent: true, id: res.id };
1878
+ return ok(data, data);
1879
+ } catch (err) {
1880
+ return fail(errMsg(err));
1881
+ }
1882
+ }
1883
+ );
1884
+ }
1885
+ const addAttachmentOutputSchema = {
1886
+ attached: z6.literal(true),
1887
+ id: z6.string(),
1888
+ attachment: z6.object({
1889
+ id: z6.string(),
1890
+ name: z6.string(),
1891
+ contentType: z6.string().optional()
1892
+ })
1893
+ };
1894
+ if (shouldRegister("add_attachment_to_draft", tools)) {
1895
+ server.registerTool(
1896
+ "add_attachment_to_draft",
1897
+ {
1898
+ description: "Add a file attachment to an existing draft email by ID. `contentBytes` must be base64-encoded file content. `contentType` is the MIME type (e.g. 'application/pdf'); defaults to 'application/octet-stream' if omitted. Disabled in --read-only mode.",
1899
+ inputSchema: {
1900
+ account: z6.string().email(),
1901
+ id: z6.string().min(1).describe("Draft message ID"),
1902
+ name: z6.string().min(1).describe("Attachment filename (e.g. 'report.pdf')"),
1903
+ contentBytes: z6.string().min(1).describe("Base64-encoded file content"),
1904
+ contentType: z6.string().optional().describe("MIME type (e.g. 'application/pdf')")
1905
+ },
1906
+ outputSchema: addAttachmentOutputSchema
1907
+ },
1908
+ async (args) => {
1909
+ try {
1910
+ const { provider, account } = registry.resolveByEmail(args.account);
1911
+ const res = await provider.addAttachmentToDraft(
1912
+ account,
1913
+ args.id,
1914
+ args.name,
1915
+ args.contentBytes,
1916
+ args.contentType
1917
+ );
1918
+ const data = {
1919
+ attached: true,
1920
+ id: res.id,
1921
+ attachment: res.attachment
1922
+ };
1923
+ return ok(data, data);
1924
+ } catch (err) {
1925
+ return fail(errMsg(err));
1926
+ }
1927
+ }
1928
+ );
1345
1929
  }
1346
- const escaped = escapeHtml(body);
1347
- let result = `<div style="${styleAttr}">${escaped}</div>`;
1348
- if (hasSignature) result += `
1349
- <div class="signature">${signature}</div>`;
1350
- return { body: result, isHtml: true };
1351
- }
1352
- function buildStyleAttr(style) {
1353
- const parts = [];
1354
- if (style.fontFamily) parts.push(`font-family: ${style.fontFamily}`);
1355
- if (style.fontSize) parts.push(`font-size: ${style.fontSize}`);
1356
- if (style.fontColor) parts.push(`color: ${style.fontColor}`);
1357
- return parts.join("; ");
1358
- }
1359
- function escapeHtml(text) {
1360
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/\n/g, "<br>");
1361
1930
  }
1362
- function errMsg(err) {
1363
- if (err instanceof Error) return err.message;
1364
- return String(err);
1931
+
1932
+ // src/tools/index.ts
1933
+ function registerTools(server, opts) {
1934
+ const { store, registry, tools } = opts;
1935
+ registerAccountTools(server, { store, registry, tools });
1936
+ registerBrowseTools(server, { store, registry, tools });
1937
+ registerFolderTools(server, { registry, tools });
1938
+ registerOrganizeTools(server, { registry, tools });
1939
+ registerComposeTools(server, { store, registry, tools });
1365
1940
  }
1366
1941
 
1367
1942
  // src/version.ts
1368
1943
  var VERSION = "0.3.0";
1369
1944
 
1945
+ // src/config.ts
1946
+ import { readFileSync } from "fs";
1947
+ import { z as z7 } from "zod";
1948
+ var httpConfigSchema = z7.object({
1949
+ enabled: z7.boolean().default(false),
1950
+ port: z7.number().int().min(1).max(65535).default(3e3),
1951
+ host: z7.string().default("127.0.0.1")
1952
+ });
1953
+ var toolsConfigSchema = z7.object({
1954
+ disabled: z7.array(z7.string()).optional(),
1955
+ enabled: z7.array(z7.string()).optional()
1956
+ });
1957
+ var outlookProviderSchema = z7.object({
1958
+ clientId: z7.string().optional(),
1959
+ tenantId: z7.string().optional()
1960
+ });
1961
+ var providersConfigSchema = z7.object({
1962
+ outlook: outlookProviderSchema.optional()
1963
+ });
1964
+ var rawConfigSchema = z7.object({
1965
+ dataDir: z7.string().optional(),
1966
+ http: httpConfigSchema.optional(),
1967
+ tools: toolsConfigSchema.optional(),
1968
+ providers: providersConfigSchema.optional()
1969
+ });
1970
+ var KNOWN_TOOLS = [
1971
+ "list_accounts",
1972
+ "add_account",
1973
+ "complete_add_account",
1974
+ "get_account_settings",
1975
+ "set_account_settings",
1976
+ "remove_account",
1977
+ "list_emails",
1978
+ "search_emails",
1979
+ "read_email",
1980
+ "read_attachment",
1981
+ "archive_email",
1982
+ "trash_email",
1983
+ "move_email",
1984
+ "mark_read",
1985
+ "mark_unread",
1986
+ "list_folders",
1987
+ "create_folder",
1988
+ "delete_folder",
1989
+ "rename_folder",
1990
+ "send_email",
1991
+ "draft_email",
1992
+ "edit_draft",
1993
+ "send_draft",
1994
+ "add_attachment_to_draft"
1995
+ ];
1996
+ var ENV_VAR_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
1997
+ function resolveEnvVars(value) {
1998
+ return value.replace(ENV_VAR_RE, (_match, name) => {
1999
+ return process.env[name] ?? "";
2000
+ });
2001
+ }
2002
+ function deepResolve(obj) {
2003
+ if (typeof obj === "string") return resolveEnvVars(obj);
2004
+ if (Array.isArray(obj)) return obj.map(deepResolve);
2005
+ if (obj && typeof obj === "object") {
2006
+ const out = {};
2007
+ for (const [key, val] of Object.entries(obj)) {
2008
+ out[key] = deepResolve(val);
2009
+ }
2010
+ return out;
2011
+ }
2012
+ return obj;
2013
+ }
2014
+ function validateToolNames(toolNames, listName) {
2015
+ if (!toolNames || toolNames.length === 0) return;
2016
+ const known = new Set(KNOWN_TOOLS);
2017
+ for (const name of toolNames) {
2018
+ if (!known.has(name)) {
2019
+ throw new Error(
2020
+ `Unknown tool "${name}" in ${listName}. Known tools: ${KNOWN_TOOLS.join(", ")}`
2021
+ );
2022
+ }
2023
+ }
2024
+ }
2025
+ function loadConfig(configPath, cliOverrides = {}) {
2026
+ let raw = {};
2027
+ if (configPath) {
2028
+ try {
2029
+ raw = JSON.parse(readFileSync(configPath, "utf-8"));
2030
+ } catch (err) {
2031
+ const detail = err instanceof SyntaxError ? "Invalid JSON" : err instanceof Error ? err.message : String(err);
2032
+ throw new Error(`Failed to read config file "${configPath}": ${detail}`);
2033
+ }
2034
+ }
2035
+ raw = deepResolve(raw);
2036
+ const parsed = rawConfigSchema.parse(raw);
2037
+ if (parsed.tools) {
2038
+ if (parsed.tools.disabled && parsed.tools.enabled) {
2039
+ throw new Error(
2040
+ "tools.disabled and tools.enabled are mutually exclusive \u2014 use one or the other"
2041
+ );
2042
+ }
2043
+ if (parsed.tools.enabled !== void 0 && parsed.tools.enabled.length === 0) {
2044
+ throw new Error(
2045
+ "tools.enabled is empty \u2014 at least one tool must be listed. To enable all tools, omit the tools section entirely."
2046
+ );
2047
+ }
2048
+ validateToolNames(parsed.tools.disabled, "tools.disabled");
2049
+ validateToolNames(parsed.tools.enabled, "tools.enabled");
2050
+ }
2051
+ const http = {
2052
+ enabled: cliOverrides.http ?? parsed.http?.enabled ?? false,
2053
+ port: cliOverrides.port ?? parsed.http?.port ?? 3e3,
2054
+ host: cliOverrides.host ?? parsed.http?.host ?? "127.0.0.1"
2055
+ };
2056
+ return {
2057
+ dataDir: cliOverrides.dataDir ?? parsed.dataDir ?? process.env.HYPERMAIL_MCP_DATA_DIR,
2058
+ http,
2059
+ tools: parsed.tools ? { disabled: parsed.tools.disabled, enabled: parsed.tools.enabled } : void 0,
2060
+ providers: parsed.providers
2061
+ };
2062
+ }
2063
+ function resolveTools(config) {
2064
+ if (!config.tools) {
2065
+ return { enabledTools: null, disabledTools: null };
2066
+ }
2067
+ return {
2068
+ enabledTools: config.tools.enabled ? new Set(config.tools.enabled) : null,
2069
+ disabledTools: config.tools.disabled ? new Set(config.tools.disabled) : null
2070
+ };
2071
+ }
2072
+
1370
2073
  // src/server.ts
1371
- async function startServer(opts = {}) {
1372
- const store = await AccountStore.open({ dataDir: opts.dataDir });
1373
- const registry = buildRegistry({ store });
2074
+ async function startServer(opts) {
2075
+ const { config } = opts;
2076
+ const store = await AccountStore.open({ dataDir: config.dataDir });
2077
+ const registry = buildRegistry({ store, providers: config.providers });
2078
+ const tools = resolveTools(config);
1374
2079
  const server = new McpServer(
1375
2080
  { name: "hypermail-mcp", version: VERSION },
1376
2081
  { capabilities: { tools: {}, logging: {} } }
1377
2082
  );
1378
- registerTools(server, { store, registry, readOnly: !!opts.readOnly, draftOnly: !!opts.draftOnly });
1379
- if (opts.http) {
1380
- await startHttp(server, opts.host ?? "127.0.0.1", opts.port ?? 3e3);
2083
+ registerTools(server, { store, registry, tools });
2084
+ if (config.http.enabled) {
2085
+ await startHttp(server, config.http.host, config.http.port);
1381
2086
  } else {
1382
2087
  const transport = new StdioServerTransport();
1383
2088
  await server.connect(transport);
@@ -1432,7 +2137,6 @@ function parseArgs(argv) {
1432
2137
  http: false,
1433
2138
  port: 3e3,
1434
2139
  host: "127.0.0.1",
1435
- readOnly: false,
1436
2140
  help: false
1437
2141
  };
1438
2142
  for (let i = 0; i < argv.length; i++) {
@@ -1450,8 +2154,8 @@ function parseArgs(argv) {
1450
2154
  case "--data-dir":
1451
2155
  out.dataDir = String(argv[++i] ?? "");
1452
2156
  break;
1453
- case "--read-only":
1454
- out.readOnly = true;
2157
+ case "--config":
2158
+ out.config = String(argv[++i] ?? "");
1455
2159
  break;
1456
2160
  case "-h":
1457
2161
  case "--help":
@@ -1476,14 +2180,22 @@ Options:
1476
2180
  --host <addr> HTTP bind address (default: 127.0.0.1)
1477
2181
  --data-dir <path> Where to store the encrypted accounts file
1478
2182
  (default: $HYPERMAIL_MCP_DATA_DIR or ~/.hypermail-mcp)
1479
- --read-only Disable tools that modify state (send_email, remove_account, add_account)
2183
+ --config <path> Path to hypermail-config.json
1480
2184
  -h, --help Show this help
1481
2185
 
1482
- Environment:
1483
- HYPERMAIL_MCP_DATA_DIR Same as --data-dir
1484
- HYPERMAIL_MCP_KEY 32-byte key (base64 or hex) for at-rest encryption
1485
- MS_CLIENT_ID Azure AD public client (application) ID
1486
- MS_TENANT_ID Tenant (default: "common")
2186
+ Configuration:
2187
+ All server settings (data dir, HTTP, tool filtering, provider credentials)
2188
+ live in hypermail-config.json. Pass it via --config.
2189
+
2190
+ Example hypermail-config.json:
2191
+ {
2192
+ "dataDir": "/path/to/data",
2193
+ "http": { "enabled": false },
2194
+ "tools": { "disabled": ["send_email"] },
2195
+ "providers": {
2196
+ "outlook": { "clientId": "\${MS_CLIENT_ID}", "tenantId": "\${MS_TENANT_ID}" }
2197
+ }
2198
+ }
1487
2199
  `;
1488
2200
  process.stdout.write(msg);
1489
2201
  }
@@ -1493,15 +2205,13 @@ async function main() {
1493
2205
  printHelp();
1494
2206
  return;
1495
2207
  }
1496
- const draftOnly = process.env.HYPERMAIL_DRAFT_ONLY === "true";
1497
- await startServer({
2208
+ const config = loadConfig(opts.config, {
1498
2209
  http: opts.http,
1499
2210
  port: opts.port,
1500
2211
  host: opts.host,
1501
- dataDir: opts.dataDir,
1502
- readOnly: opts.readOnly,
1503
- draftOnly
2212
+ dataDir: opts.dataDir
1504
2213
  });
2214
+ await startServer({ config });
1505
2215
  }
1506
2216
  main().catch((err) => {
1507
2217
  console.error("[hypermail-mcp] fatal:", err);