hypermail-mcp 0.4.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);
@@ -680,7 +696,67 @@ var OutlookProvider = class {
680
696
  const client = this.clients.get(account);
681
697
  await client.api(`/me/messages/${encodeURIComponent(id)}/move`).post({ destinationId });
682
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
+ }
683
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
+ }
684
760
  function mapRecipient(r) {
685
761
  return {
686
762
  name: r.emailAddress?.name,
@@ -744,12 +820,40 @@ var ImapProvider = class {
744
820
  async moveEmail(_account, _id, _destinationId) {
745
821
  throw new Error(NOT_IMPLEMENTED);
746
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
+ }
747
846
  };
748
847
 
749
848
  // src/providers/registry.ts
750
849
  function buildRegistry(opts) {
850
+ const outlookCfg = opts.providers?.outlook;
751
851
  const providers = /* @__PURE__ */ new Map();
752
- 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
+ }));
753
857
  providers.set("imap", new ImapProvider());
754
858
  function get(id) {
755
859
  const p = providers.get(id);
@@ -772,41 +876,11 @@ function buildRegistry(opts) {
772
876
  };
773
877
  }
774
878
 
775
- // src/tools/index.ts
879
+ // src/tools/shared.ts
776
880
  import { z } from "zod";
777
-
778
- // src/html-to-markdown.ts
779
- import TurndownService from "turndown";
780
- var turndown = new TurndownService();
781
- function htmlToMarkdown(html) {
782
- return turndown.turndown(html);
783
- }
784
- function selectBody(msg, format) {
785
- switch (format) {
786
- case "markdown": {
787
- if (msg.bodyHtml) return htmlToMarkdown(msg.bodyHtml);
788
- if (msg.bodyText) return msg.bodyText;
789
- return "";
790
- }
791
- case "html": {
792
- if (msg.bodyHtml) return msg.bodyHtml;
793
- if (msg.bodyText) return msg.bodyText;
794
- return "";
795
- }
796
- case "text": {
797
- if (msg.bodyText) return msg.bodyText;
798
- if (msg.bodyHtml) return msg.bodyHtml.replace(/<[^>]*>/g, "");
799
- return "";
800
- }
801
- }
802
- }
803
-
804
- // src/tools/index.ts
805
881
  function ok(data, structuredContent) {
806
882
  const result = {
807
- content: [
808
- { type: "text", text: JSON.stringify(data, null, 2) }
809
- ]
883
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
810
884
  };
811
885
  if (structuredContent !== void 0) {
812
886
  result.structuredContent = structuredContent;
@@ -819,6 +893,10 @@ function fail(message) {
819
893
  content: [{ type: "text", text: message }]
820
894
  };
821
895
  }
896
+ function errMsg(err) {
897
+ if (err instanceof Error) return err.message;
898
+ return String(err);
899
+ }
822
900
  var emailAddrSchema = z.object({
823
901
  address: z.string().email(),
824
902
  name: z.string().optional()
@@ -857,369 +935,239 @@ var styleOutputSchema = z.object({
857
935
  fontSize: z.string().optional(),
858
936
  fontColor: z.string().optional()
859
937
  });
860
- function registerTools(server, opts) {
861
- 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;
862
990
  const listAccountsOutputSchema = {
863
- accounts: z.array(accountSummaryOutputSchema)
991
+ accounts: z2.array(accountSummaryOutputSchema)
864
992
  };
865
- server.registerTool(
866
- "list_accounts",
867
- {
868
- description: "List all email accounts known to this server (no secrets). Use the returned `email` value as the `account` argument to other tools.",
869
- inputSchema: {},
870
- outputSchema: listAccountsOutputSchema
871
- },
872
- async () => {
873
- const rows = store.listAccounts().map((a) => ({
874
- email: a.email,
875
- provider: a.provider,
876
- displayName: a.displayName,
877
- addedAt: a.addedAt,
878
- hasSignature: !!a.signature,
879
- hasStyle: !!(a.style && (a.style.fontFamily || a.style.fontSize || a.style.fontColor))
880
- }));
881
- const data = { accounts: rows };
882
- return ok(data, data);
883
- }
884
- );
885
- const addAccountOutputSchema = z.discriminatedUnion("status", [
886
- z.object({
887
- status: z.literal("pending"),
888
- handle: z.string(),
889
- verification: z.object({
890
- userCode: z.string(),
891
- verificationUri: z.string(),
892
- expiresAt: z.string(),
893
- 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()
894
1024
  })
895
1025
  }),
896
- z.object({
897
- status: z.literal("ready"),
898
- account: z.object({
899
- email: z.string(),
900
- provider: z.enum(["outlook", "imap", "gmail"]),
901
- displayName: z.string().optional(),
902
- tokens: z.record(z.unknown()),
903
- addedAt: z.string(),
904
- 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(),
905
1035
  style: styleOutputSchema.optional()
906
1036
  })
907
1037
  })
908
1038
  ]);
909
- server.registerTool(
910
- "add_account",
911
- {
912
- 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.",
913
- inputSchema: {
914
- provider: z.enum(["outlook", "imap", "gmail"]).describe("Email backend. v1 only fully implements 'outlook'."),
915
- email: z.string().email().optional().describe("Optional hint \u2014 the provider will verify it against the auth result."),
916
- 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
917
1054
  },
918
- outputSchema: addAccountOutputSchema
919
- },
920
- async (args) => {
921
- if (readOnly) return fail("server is in --read-only mode; add_account is disabled");
922
- const provider = registry.get(args.provider);
923
- try {
924
- const res = await provider.addAccount({ email: args.email, config: args.config });
925
- return ok(res, res);
926
- } catch (err) {
927
- 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
+ }
928
1066
  }
929
- }
930
- );
931
- const completeAddAccountOutputSchema = z.object({
932
- status: z.enum(["pending", "ready", "expired", "error"]),
933
- account: z.object({
934
- email: z.string(),
935
- provider: z.enum(["outlook", "imap", "gmail"]),
936
- displayName: z.string().optional(),
937
- tokens: z.record(z.unknown()),
938
- addedAt: z.string(),
939
- 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(),
940
1078
  style: styleOutputSchema.optional()
941
1079
  }).optional(),
942
- error: z.string().optional()
1080
+ error: z2.string().optional()
943
1081
  });
944
- server.registerTool(
945
- "complete_add_account",
946
- {
947
- description: "Poll/finalize a pending add_account flow. Returns `pending` until the user completes the device-code step, then `ready` with the persisted account.",
948
- inputSchema: {
949
- provider: z.enum(["outlook", "imap", "gmail"]),
950
- 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
951
1092
  },
952
- outputSchema: completeAddAccountOutputSchema
953
- },
954
- async (args) => {
955
- const provider = registry.get(args.provider);
956
- if (!provider.completeAddAccount) {
957
- return fail(`provider ${args.provider} has no async add-account flow`);
958
- }
959
- try {
960
- const res = await provider.completeAddAccount(args.handle);
961
- return ok(res, res);
962
- } catch (err) {
963
- 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
+ }
964
1106
  }
965
- }
966
- );
1107
+ );
1108
+ }
967
1109
  const accountSettingsOutputSchema = {
968
- signature: z.string().nullable(),
1110
+ signature: z2.string().nullable(),
969
1111
  style: styleOutputSchema.nullable()
970
1112
  };
971
- server.registerTool(
972
- "get_account_settings",
973
- {
974
- description: "Get signature (HTML) and style preferences for an account.",
975
- inputSchema: { account: z.string().email() },
976
- outputSchema: accountSettingsOutputSchema
977
- },
978
- async (args) => {
979
- try {
980
- const acct = store.getAccount(args.account);
981
- if (!acct) return fail(`no account registered for "${args.account}"`);
982
- const data = { signature: acct.signature ?? null, style: acct.style ?? null };
983
- return ok(data, data);
984
- } catch (err) {
985
- return fail(errMsg(err));
986
- }
987
- }
988
- );
989
- server.registerTool(
990
- "set_account_settings",
991
- {
992
- description: "Set signature (HTML snippet) and/or style preferences for an account. Disabled in --read-only mode.",
993
- inputSchema: {
994
- account: z.string().email(),
995
- signature: z.string().optional().describe("HTML snippet \u2014 may contain formatting, images, links. Pass null to clear."),
996
- style: z.object({
997
- fontFamily: z.string().optional(),
998
- fontSize: z.string().optional(),
999
- fontColor: z.string().optional()
1000
- }).optional().describe("Font preferences applied to outgoing HTML emails. Pass null to clear.")
1001
- },
1002
- outputSchema: accountSettingsOutputSchema
1003
- },
1004
- async (args) => {
1005
- if (readOnly) return fail("server is in --read-only mode; set_account_settings is disabled");
1006
- try {
1007
- const acct = store.getAccount(args.account);
1008
- if (!acct) return fail(`no account registered for "${args.account}"`);
1009
- const updated = await store.upsertAccount({
1010
- ...acct,
1011
- signature: args.signature ?? acct.signature,
1012
- style: args.style ?? acct.style
1013
- });
1014
- const data = { signature: updated.signature ?? null, style: updated.style ?? null };
1015
- return ok(data, data);
1016
- } catch (err) {
1017
- return fail(errMsg(err));
1018
- }
1019
- }
1020
- );
1021
- const removeAccountOutputSchema = {
1022
- removed: z.boolean(),
1023
- email: z.string()
1024
- };
1025
- server.registerTool(
1026
- "remove_account",
1027
- {
1028
- description: "Forget an account and delete its stored tokens. Disabled in --read-only mode.",
1029
- inputSchema: { email: z.string().email() },
1030
- outputSchema: removeAccountOutputSchema
1031
- },
1032
- async (args) => {
1033
- if (readOnly) return fail("server is in --read-only mode; remove_account is disabled");
1034
- const removed = await store.removeAccount(args.email);
1035
- const data = { removed, email: args.email };
1036
- return ok(data, data);
1037
- }
1038
- );
1039
- const emailListOutputSchema = {
1040
- account: z.string(),
1041
- count: z.number(),
1042
- items: z.array(emailSummaryOutputSchema)
1043
- };
1044
- server.registerTool(
1045
- "list_emails",
1046
- {
1047
- 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.",
1048
- inputSchema: {
1049
- account: z.string().email(),
1050
- folder: z.string().default("inbox").optional(),
1051
- limit: z.number().int().positive().max(100).optional(),
1052
- unreadOnly: z.boolean().optional()
1053
- },
1054
- outputSchema: emailListOutputSchema
1055
- },
1056
- async (args) => {
1057
- try {
1058
- const { provider, account } = registry.resolveByEmail(args.account);
1059
- const items = await provider.listEmails(account, {
1060
- folder: args.folder,
1061
- limit: args.limit,
1062
- unreadOnly: args.unreadOnly
1063
- });
1064
- const data = { account: account.email, count: items.length, items };
1065
- return ok(data, data);
1066
- } catch (err) {
1067
- return fail(errMsg(err));
1068
- }
1069
- }
1070
- );
1071
- server.registerTool(
1072
- "search_emails",
1073
- {
1074
- description: "Search emails by free-text query (KQL on Outlook). Returns lightweight summaries.",
1075
- inputSchema: {
1076
- account: z.string().email(),
1077
- query: z.string().min(1),
1078
- limit: z.number().int().positive().max(100).optional()
1079
- },
1080
- outputSchema: emailListOutputSchema
1081
- },
1082
- async (args) => {
1083
- try {
1084
- const { provider, account } = registry.resolveByEmail(args.account);
1085
- const items = await provider.searchEmails(account, args.query, {
1086
- limit: args.limit
1087
- });
1088
- const data = { account: account.email, count: items.length, items };
1089
- return ok(data, data);
1090
- } catch (err) {
1091
- return fail(errMsg(err));
1092
- }
1093
- }
1094
- );
1095
- const readEmailOutputSchema = {
1096
- id: z.string(),
1097
- subject: z.string(),
1098
- from: emailAddrOutputSchema.optional(),
1099
- to: z.array(emailAddrOutputSchema).optional(),
1100
- cc: z.array(emailAddrOutputSchema).optional(),
1101
- bcc: z.array(emailAddrOutputSchema).optional(),
1102
- receivedAt: z.string().optional(),
1103
- preview: z.string().optional(),
1104
- isRead: z.boolean().optional(),
1105
- hasAttachments: z.boolean().optional(),
1106
- folder: z.string().optional(),
1107
- attachments: z.array(attachmentMetaOutputSchema).optional(),
1108
- body: z.string(),
1109
- bodyFormat: z.enum(["markdown", "html", "text"])
1110
- };
1111
- server.registerTool(
1112
- "read_email",
1113
- {
1114
- 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.",
1115
- inputSchema: {
1116
- account: z.string().email(),
1117
- id: z.string().min(1),
1118
- format: z.enum(["markdown", "html", "text"]).default("markdown").optional().describe(
1119
- "Output body format. 'markdown' converts HTML to Markdown (default), 'html' returns the raw HTML, 'text' returns plain text."
1120
- )
1121
- },
1122
- outputSchema: readEmailOutputSchema
1123
- },
1124
- async (args) => {
1125
- try {
1126
- const { provider, account } = registry.resolveByEmail(args.account);
1127
- const msg = await provider.readEmail(account, args.id);
1128
- const format = args.format ?? "markdown";
1129
- const body = selectBody(msg, format);
1130
- const data = {
1131
- id: msg.id,
1132
- subject: msg.subject,
1133
- from: msg.from,
1134
- to: msg.to,
1135
- cc: msg.cc,
1136
- bcc: msg.bcc,
1137
- receivedAt: msg.receivedAt,
1138
- preview: msg.preview,
1139
- isRead: msg.isRead,
1140
- hasAttachments: msg.hasAttachments,
1141
- folder: msg.folder,
1142
- attachments: msg.attachments,
1143
- body,
1144
- bodyFormat: format
1145
- };
1146
- return ok(data, data);
1147
- } catch (err) {
1148
- return fail(errMsg(err));
1149
- }
1150
- }
1151
- );
1152
- const readAttachmentOutputSchema = {
1153
- name: z.string(),
1154
- contentType: z.string().optional(),
1155
- path: z.string()
1156
- };
1157
- server.registerTool(
1158
- "read_attachment",
1159
- {
1160
- description: "Download an email attachment to a temporary file and return its path. Use messageId and attachmentId from a prior read_email call.",
1161
- inputSchema: {
1162
- account: z.string().email(),
1163
- messageId: z.string().min(1),
1164
- attachmentId: z.string().min(1)
1165
- },
1166
- outputSchema: readAttachmentOutputSchema
1167
- },
1168
- async (args) => {
1169
- try {
1170
- const { provider, account } = registry.resolveByEmail(args.account);
1171
- const res = await provider.readAttachment(account, args.messageId, args.attachmentId);
1172
- return ok(res, res);
1173
- } catch (err) {
1174
- return fail(errMsg(err));
1175
- }
1176
- }
1177
- );
1178
- {
1179
- const schema = {
1180
- account: z.string().email(),
1181
- id: z.string().min(1).describe("Message ID to move")
1182
- };
1183
- const archiveOutputSchema = {
1184
- archived: z.literal(true),
1185
- id: z.string()
1186
- };
1113
+ if (shouldRegister("get_account_settings", tools)) {
1187
1114
  server.registerTool(
1188
- "archive_email",
1115
+ "get_account_settings",
1189
1116
  {
1190
- description: "Move a message to the Archive folder. Disabled in --read-only mode.",
1191
- inputSchema: schema,
1192
- outputSchema: archiveOutputSchema
1117
+ description: "Get signature (HTML) and style preferences for an account.",
1118
+ inputSchema: { account: z2.string().email() },
1119
+ outputSchema: accountSettingsOutputSchema
1193
1120
  },
1194
1121
  async (args) => {
1195
- if (readOnly) return fail("server is in --read-only mode; archive_email is disabled");
1196
1122
  try {
1197
- const { provider, account } = registry.resolveByEmail(args.account);
1198
- await provider.moveEmail(account, args.id, "archive");
1199
- 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
+ };
1200
1130
  return ok(data, data);
1201
1131
  } catch (err) {
1202
1132
  return fail(errMsg(err));
1203
1133
  }
1204
1134
  }
1205
1135
  );
1206
- const trashOutputSchema = {
1207
- trashed: z.literal(true),
1208
- id: z.string()
1209
- };
1136
+ }
1137
+ if (shouldRegister("set_account_settings", tools)) {
1210
1138
  server.registerTool(
1211
- "trash_email",
1139
+ "set_account_settings",
1212
1140
  {
1213
- description: "Move a message to the Deleted Items (trash) folder. Disabled in --read-only mode.",
1214
- inputSchema: schema,
1215
- 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
1216
1156
  },
1217
1157
  async (args) => {
1218
- if (readOnly) return fail("server is in --read-only mode; trash_email is disabled");
1219
1158
  try {
1220
- const { provider, account } = registry.resolveByEmail(args.account);
1221
- await provider.moveEmail(account, args.id, "deleteditems");
1222
- 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
+ };
1223
1171
  return ok(data, data);
1224
1172
  } catch (err) {
1225
1173
  return fail(errMsg(err));
@@ -1227,59 +1175,536 @@ function registerTools(server, opts) {
1227
1175
  }
1228
1176
  );
1229
1177
  }
1230
- const moveEmailOutputSchema = {
1231
- moved: z.literal(true),
1232
- id: z.string(),
1233
- destination: z.string()
1178
+ const removeAccountOutputSchema = {
1179
+ removed: z2.boolean(),
1180
+ email: z2.string()
1234
1181
  };
1235
- server.registerTool(
1236
- "move_email",
1237
- {
1238
- 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.",
1239
- inputSchema: {
1240
- account: z.string().email(),
1241
- id: z.string().min(1).describe("Message ID to move"),
1242
- destination: z.string().min(1).describe(
1243
- "Destination folder \u2014 a well-known folder name ('archive', 'deleteditems', 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or a raw folder ID."
1244
- )
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
1245
1189
  },
1246
- outputSchema: moveEmailOutputSchema
1247
- },
1248
- async (args) => {
1249
- if (readOnly) return fail("server is in --read-only mode; move_email is disabled");
1250
- try {
1251
- const { provider, account } = registry.resolveByEmail(args.account);
1252
- await provider.moveEmail(account, args.id, args.destination);
1253
- 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 };
1254
1193
  return ok(data, data);
1255
- } catch (err) {
1256
- return fail(errMsg(err));
1257
1194
  }
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 "";
1258
1214
  }
1259
- );
1260
- const sendEmailSchema = z.object({
1261
- account: z.string().email(),
1262
- to: z.array(emailAddrSchema).min(1),
1263
- cc: z.array(emailAddrSchema).optional(),
1264
- bcc: z.array(emailAddrSchema).optional(),
1265
- subject: z.string(),
1266
- body: z.string(),
1267
- isHtml: z.boolean().optional(),
1268
- include_signature: z.boolean().describe(
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(
1269
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."
1270
1696
  ),
1271
- inReplyTo: z.string().optional().describe(
1697
+ inReplyTo: z6.string().optional().describe(
1272
1698
  "Message ID to reply to. When set, sends as a threaded reply which includes the quoted thread history automatically."
1273
1699
  ),
1274
- replyAll: z.boolean().default(false).optional().describe(
1700
+ replyAll: z6.boolean().default(false).optional().describe(
1275
1701
  "When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
1276
1702
  ),
1277
- forwardMessageId: z.string().optional().describe(
1703
+ forwardMessageId: z6.string().optional().describe(
1278
1704
  "Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
1279
1705
  )
1280
1706
  });
1281
1707
  async function handleSendOrDraft(args, action, resultKey, toolName) {
1282
- if (readOnly) return fail(`server is in --read-only mode; ${toolName} is disabled`);
1283
1708
  try {
1284
1709
  const { provider, account } = registry.resolveByEmail(args.account);
1285
1710
  if (args.include_signature && !account.signature) {
@@ -1321,10 +1746,10 @@ function registerTools(server, opts) {
1321
1746
  }
1322
1747
  }
1323
1748
  const sendEmailOutputSchema = {
1324
- sent: z.literal(true),
1325
- id: z.string()
1749
+ sent: z6.literal(true),
1750
+ id: z6.string()
1326
1751
  };
1327
- if (!draftOnly) {
1752
+ if (shouldRegister("send_email", tools)) {
1328
1753
  server.registerTool(
1329
1754
  "send_email",
1330
1755
  {
@@ -1341,142 +1766,323 @@ function registerTools(server, opts) {
1341
1766
  );
1342
1767
  }
1343
1768
  const draftEmailOutputSchema = {
1344
- draft: z.literal(true),
1345
- id: z.string(),
1346
- draftHtml: z.string().optional()
1769
+ draft: z6.literal(true),
1770
+ id: z6.string(),
1771
+ draftHtml: z6.string().optional()
1347
1772
  };
1348
- server.registerTool(
1349
- "draft_email",
1350
- {
1351
- 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.",
1352
- inputSchema: sendEmailSchema,
1353
- outputSchema: draftEmailOutputSchema
1354
- },
1355
- async (args) => handleSendOrDraft(
1356
- args,
1357
- (p, a, m) => p.saveDraft(a, m),
1358
- "draft",
1359
- "draft_email"
1360
- )
1361
- );
1362
- const editDraftSchema = z.object({
1363
- account: z.string().email(),
1364
- id: z.string().min(1).describe("Draft message ID to edit"),
1365
- to: z.array(emailAddrSchema).optional(),
1366
- cc: z.array(emailAddrSchema).optional(),
1367
- bcc: z.array(emailAddrSchema).optional(),
1368
- subject: z.string().optional(),
1369
- body: z.string().optional(),
1370
- isHtml: z.boolean().optional(),
1371
- include_signature: z.boolean().optional().describe(
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(
1372
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."
1373
1800
  )
1374
1801
  });
1375
1802
  const editDraftOutputSchema = {
1376
- edited: z.literal(true),
1377
- id: z.string(),
1378
- draftHtml: z.string().optional()
1803
+ edited: z6.literal(true),
1804
+ id: z6.string(),
1805
+ draftHtml: z6.string().optional()
1379
1806
  };
1380
- server.registerTool(
1381
- "edit_draft",
1382
- {
1383
- 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.",
1384
- inputSchema: editDraftSchema,
1385
- outputSchema: editDraftOutputSchema
1386
- },
1387
- async (args) => {
1388
- const a = args;
1389
- if (readOnly) return fail("server is in --read-only mode; edit_draft is disabled");
1390
- try {
1391
- const { provider, account } = registry.resolveByEmail(a.account);
1392
- if (a.include_signature && !account.signature) {
1393
- return fail(
1394
- "include_signature is true but no signature is configured for this account. Set up a signature first with set_account_settings."
1395
- );
1396
- }
1397
- let bodyPayload;
1398
- let isHtmlPayload;
1399
- if (a.body !== void 0) {
1400
- const composed = composeBody({
1401
- body: a.body,
1402
- isHtml: a.isHtml,
1403
- signature: account.signature,
1404
- style: account.style,
1405
- includeSignature: !!a.include_signature
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
1406
1844
  });
1407
- bodyPayload = composed.body;
1408
- isHtmlPayload = composed.isHtml;
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));
1409
1854
  }
1410
- const res = await provider.updateDraft(account, a.id, {
1411
- to: a.to,
1412
- cc: a.cc,
1413
- bcc: a.bcc,
1414
- subject: a.subject,
1415
- body: bodyPayload,
1416
- isHtml: isHtmlPayload
1417
- });
1418
- const draft = await provider.readEmail(account, res.id);
1419
- const result = {
1420
- edited: true,
1421
- id: res.id,
1422
- draftHtml: draft.bodyHtml
1423
- };
1424
- return ok(result, result);
1425
- } catch (err) {
1426
- return fail(errMsg(err));
1427
1855
  }
1428
- }
1429
- );
1430
- }
1431
- function composeBody(input) {
1432
- const { body, isHtml = false, signature, style, includeSignature } = input;
1433
- const hasSignature = includeSignature && !!signature;
1434
- const hasStyle = !!(style && (style.fontFamily || style.fontSize || style.fontColor));
1435
- if (!hasSignature && !hasStyle) {
1436
- return { body, isHtml };
1856
+ );
1437
1857
  }
1438
- const styleAttr = hasStyle ? buildStyleAttr(style) : "";
1439
- if (isHtml) {
1440
- let result2 = hasStyle ? `<div style="${styleAttr}">${body}</div>` : body;
1441
- if (hasSignature) result2 += `
1442
- <div class="signature">${signature}</div>`;
1443
- 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
+ );
1444
1929
  }
1445
- const escaped = escapeHtml(body);
1446
- let result = `<div style="${styleAttr}">${escaped}</div>`;
1447
- if (hasSignature) result += `
1448
- <div class="signature">${signature}</div>`;
1449
- return { body: result, isHtml: true };
1450
- }
1451
- function buildStyleAttr(style) {
1452
- const parts = [];
1453
- if (style.fontFamily) parts.push(`font-family: ${style.fontFamily}`);
1454
- if (style.fontSize) parts.push(`font-size: ${style.fontSize}`);
1455
- if (style.fontColor) parts.push(`color: ${style.fontColor}`);
1456
- return parts.join("; ");
1457
- }
1458
- function escapeHtml(text) {
1459
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/\n/g, "<br>");
1460
1930
  }
1461
- function errMsg(err) {
1462
- if (err instanceof Error) return err.message;
1463
- 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 });
1464
1940
  }
1465
1941
 
1466
1942
  // src/version.ts
1467
1943
  var VERSION = "0.3.0";
1468
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
+
1469
2073
  // src/server.ts
1470
- async function startServer(opts = {}) {
1471
- const store = await AccountStore.open({ dataDir: opts.dataDir });
1472
- 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);
1473
2079
  const server = new McpServer(
1474
2080
  { name: "hypermail-mcp", version: VERSION },
1475
2081
  { capabilities: { tools: {}, logging: {} } }
1476
2082
  );
1477
- registerTools(server, { store, registry, readOnly: !!opts.readOnly, draftOnly: !!opts.draftOnly });
1478
- if (opts.http) {
1479
- 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);
1480
2086
  } else {
1481
2087
  const transport = new StdioServerTransport();
1482
2088
  await server.connect(transport);
@@ -1531,7 +2137,6 @@ function parseArgs(argv) {
1531
2137
  http: false,
1532
2138
  port: 3e3,
1533
2139
  host: "127.0.0.1",
1534
- readOnly: false,
1535
2140
  help: false
1536
2141
  };
1537
2142
  for (let i = 0; i < argv.length; i++) {
@@ -1549,8 +2154,8 @@ function parseArgs(argv) {
1549
2154
  case "--data-dir":
1550
2155
  out.dataDir = String(argv[++i] ?? "");
1551
2156
  break;
1552
- case "--read-only":
1553
- out.readOnly = true;
2157
+ case "--config":
2158
+ out.config = String(argv[++i] ?? "");
1554
2159
  break;
1555
2160
  case "-h":
1556
2161
  case "--help":
@@ -1575,14 +2180,22 @@ Options:
1575
2180
  --host <addr> HTTP bind address (default: 127.0.0.1)
1576
2181
  --data-dir <path> Where to store the encrypted accounts file
1577
2182
  (default: $HYPERMAIL_MCP_DATA_DIR or ~/.hypermail-mcp)
1578
- --read-only Disable tools that modify state (send_email, remove_account, add_account)
2183
+ --config <path> Path to hypermail-config.json
1579
2184
  -h, --help Show this help
1580
2185
 
1581
- Environment:
1582
- HYPERMAIL_MCP_DATA_DIR Same as --data-dir
1583
- HYPERMAIL_MCP_KEY 32-byte key (base64 or hex) for at-rest encryption
1584
- MS_CLIENT_ID Azure AD public client (application) ID
1585
- 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
+ }
1586
2199
  `;
1587
2200
  process.stdout.write(msg);
1588
2201
  }
@@ -1592,15 +2205,13 @@ async function main() {
1592
2205
  printHelp();
1593
2206
  return;
1594
2207
  }
1595
- const draftOnly = process.env.HYPERMAIL_DRAFT_ONLY === "true";
1596
- await startServer({
2208
+ const config = loadConfig(opts.config, {
1597
2209
  http: opts.http,
1598
2210
  port: opts.port,
1599
2211
  host: opts.host,
1600
- dataDir: opts.dataDir,
1601
- readOnly: opts.readOnly,
1602
- draftOnly
2212
+ dataDir: opts.dataDir
1603
2213
  });
2214
+ await startServer({ config });
1604
2215
  }
1605
2216
  main().catch((err) => {
1606
2217
  console.error("[hypermail-mcp] fatal:", err);