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/README.md +40 -3
- package/dist/cli.js +1202 -492
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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(
|
|
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.
|
|
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
|
|
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({
|
|
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/
|
|
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
|
-
|
|
830
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).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:
|
|
991
|
+
accounts: z2.array(accountSummaryOutputSchema)
|
|
833
992
|
};
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
866
|
-
status:
|
|
867
|
-
account:
|
|
868
|
-
email:
|
|
869
|
-
provider:
|
|
870
|
-
displayName:
|
|
871
|
-
tokens:
|
|
872
|
-
addedAt:
|
|
873
|
-
signature:
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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 =
|
|
901
|
-
status:
|
|
902
|
-
account:
|
|
903
|
-
email:
|
|
904
|
-
provider:
|
|
905
|
-
displayName:
|
|
906
|
-
tokens:
|
|
907
|
-
addedAt:
|
|
908
|
-
signature:
|
|
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:
|
|
1080
|
+
error: z2.string().optional()
|
|
912
1081
|
});
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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:
|
|
1110
|
+
signature: z2.string().nullable(),
|
|
938
1111
|
style: styleOutputSchema.nullable()
|
|
939
1112
|
};
|
|
940
|
-
|
|
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
|
-
"
|
|
1115
|
+
"get_account_settings",
|
|
1158
1116
|
{
|
|
1159
|
-
description: "
|
|
1160
|
-
inputSchema:
|
|
1161
|
-
outputSchema:
|
|
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
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
1177
|
-
id: z.string()
|
|
1178
|
-
};
|
|
1136
|
+
}
|
|
1137
|
+
if (shouldRegister("set_account_settings", tools)) {
|
|
1179
1138
|
server.registerTool(
|
|
1180
|
-
"
|
|
1139
|
+
"set_account_settings",
|
|
1181
1140
|
{
|
|
1182
|
-
description: "
|
|
1183
|
-
inputSchema:
|
|
1184
|
-
|
|
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
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
destination: z.string()
|
|
1178
|
+
const removeAccountOutputSchema = {
|
|
1179
|
+
removed: z2.boolean(),
|
|
1180
|
+
email: z2.string()
|
|
1203
1181
|
};
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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:
|
|
1294
|
-
id:
|
|
1749
|
+
sent: z6.literal(true),
|
|
1750
|
+
id: z6.string()
|
|
1295
1751
|
};
|
|
1296
|
-
if (
|
|
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:
|
|
1314
|
-
id:
|
|
1315
|
-
draftHtml:
|
|
1769
|
+
draft: z6.literal(true),
|
|
1770
|
+
id: z6.string(),
|
|
1771
|
+
draftHtml: z6.string().optional()
|
|
1316
1772
|
};
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
args
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
if (
|
|
1337
|
-
|
|
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
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/\n/g, "<br>");
|
|
1361
1930
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
|
1373
|
-
const
|
|
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,
|
|
1379
|
-
if (
|
|
1380
|
-
await startHttp(server,
|
|
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 "--
|
|
1454
|
-
out.
|
|
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
|
-
--
|
|
2183
|
+
--config <path> Path to hypermail-config.json
|
|
1480
2184
|
-h, --help Show this help
|
|
1481
2185
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
|
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);
|