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/README.md +35 -2
- package/dist/cli.js +1157 -546
- 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);
|
|
@@ -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({
|
|
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/
|
|
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
|
-
|
|
861
|
-
|
|
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;
|
|
862
990
|
const listAccountsOutputSchema = {
|
|
863
|
-
accounts:
|
|
991
|
+
accounts: z2.array(accountSummaryOutputSchema)
|
|
864
992
|
};
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
897
|
-
status:
|
|
898
|
-
account:
|
|
899
|
-
email:
|
|
900
|
-
provider:
|
|
901
|
-
displayName:
|
|
902
|
-
tokens:
|
|
903
|
-
addedAt:
|
|
904
|
-
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(),
|
|
905
1035
|
style: styleOutputSchema.optional()
|
|
906
1036
|
})
|
|
907
1037
|
})
|
|
908
1038
|
]);
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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 =
|
|
932
|
-
status:
|
|
933
|
-
account:
|
|
934
|
-
email:
|
|
935
|
-
provider:
|
|
936
|
-
displayName:
|
|
937
|
-
tokens:
|
|
938
|
-
addedAt:
|
|
939
|
-
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(),
|
|
940
1078
|
style: styleOutputSchema.optional()
|
|
941
1079
|
}).optional(),
|
|
942
|
-
error:
|
|
1080
|
+
error: z2.string().optional()
|
|
943
1081
|
});
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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:
|
|
1110
|
+
signature: z2.string().nullable(),
|
|
969
1111
|
style: styleOutputSchema.nullable()
|
|
970
1112
|
};
|
|
971
|
-
|
|
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
|
-
"
|
|
1115
|
+
"get_account_settings",
|
|
1189
1116
|
{
|
|
1190
|
-
description: "
|
|
1191
|
-
inputSchema:
|
|
1192
|
-
outputSchema:
|
|
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
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
|
|
1207
|
-
|
|
1208
|
-
id: z.string()
|
|
1209
|
-
};
|
|
1136
|
+
}
|
|
1137
|
+
if (shouldRegister("set_account_settings", tools)) {
|
|
1210
1138
|
server.registerTool(
|
|
1211
|
-
"
|
|
1139
|
+
"set_account_settings",
|
|
1212
1140
|
{
|
|
1213
|
-
description: "
|
|
1214
|
-
inputSchema:
|
|
1215
|
-
|
|
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
|
|
1221
|
-
|
|
1222
|
-
|
|
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
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
destination: z.string()
|
|
1178
|
+
const removeAccountOutputSchema = {
|
|
1179
|
+
removed: z2.boolean(),
|
|
1180
|
+
email: z2.string()
|
|
1234
1181
|
};
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1325
|
-
id:
|
|
1749
|
+
sent: z6.literal(true),
|
|
1750
|
+
id: z6.string()
|
|
1326
1751
|
};
|
|
1327
|
-
if (
|
|
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:
|
|
1345
|
-
id:
|
|
1346
|
-
draftHtml:
|
|
1769
|
+
draft: z6.literal(true),
|
|
1770
|
+
id: z6.string(),
|
|
1771
|
+
draftHtml: z6.string().optional()
|
|
1347
1772
|
};
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
args
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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:
|
|
1377
|
-
id:
|
|
1378
|
-
draftHtml:
|
|
1803
|
+
edited: z6.literal(true),
|
|
1804
|
+
id: z6.string(),
|
|
1805
|
+
draftHtml: z6.string().optional()
|
|
1379
1806
|
};
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1408
|
-
|
|
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
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/\n/g, "<br>");
|
|
1460
1930
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
|
1472
|
-
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);
|
|
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,
|
|
1478
|
-
if (
|
|
1479
|
-
await startHttp(server,
|
|
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 "--
|
|
1553
|
-
out.
|
|
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
|
-
--
|
|
2183
|
+
--config <path> Path to hypermail-config.json
|
|
1579
2184
|
-h, --help Show this help
|
|
1580
2185
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
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
|
|
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);
|