@marlinjai/email-mcp 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/index.js +315 -1
- package/dist/index.js.map +4 -4
- package/dist/setup/wizard.js +176 -1
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ npx @marlinjai/email-mcp setup
|
|
|
103
103
|
# Optionally enter SMTP host and port for sending
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
## Available Tools (
|
|
106
|
+
## Available Tools (30)
|
|
107
107
|
|
|
108
108
|
### Account Management (4)
|
|
109
109
|
|
|
@@ -157,6 +157,18 @@ npx @marlinjai/email-mcp setup
|
|
|
157
157
|
|
|
158
158
|
All batch tools accept a `sourceFolder` parameter for IMAP/iCloud and include a sequential fallback for maximum compatibility.
|
|
159
159
|
|
|
160
|
+
### Spam Moderation (5)
|
|
161
|
+
|
|
162
|
+
| Tool | Description |
|
|
163
|
+
|------|-------------|
|
|
164
|
+
| `email_report_spam` | Report an email as spam/junk, training the provider's own filter — the same signal the "Report Junk" button sends in Gmail/Outlook. This is different from `email_delete`, which removes the message but teaches the filter nothing. **Not** an abuse report to the provider's security team; it only trains this account's filter |
|
|
165
|
+
| `email_batch_report_spam` | Report multiple emails as spam/junk at once |
|
|
166
|
+
| `email_create_block_rule` | Create a standing rule that intercepts future mail matching a pattern (sender domain/address, subject, or arbitrary header content) and either deletes it or files it straight to Junk. Use `headerContains` (e.g. a Reply-To domain) to block a spam template family whose visible "From" domain rotates — matching the rotating domain directly stops working within days. **Not supported on iCloud/generic IMAP** (no standard server-side rule mechanism exists across IMAP servers). On Outlook, requires the `MailboxSettings.ReadWrite` scope — accounts authenticated before this tool existed need to re-run the setup wizard once to re-consent |
|
|
167
|
+
| `email_list_block_rules` | List the standing block rules on an account, for auditing or before deleting one |
|
|
168
|
+
| `email_delete_block_rule` | Delete a standing block rule — use to undo a rule that turned out too broad |
|
|
169
|
+
|
|
170
|
+
Gmail and Outlook only for the rule tools; `email_report_spam`/`email_batch_report_spam` work on every provider (iCloud/IMAP fall back to a best-effort move into the account's Junk-typed folder, with no vendor ML training signal since generic IMAP has none to train).
|
|
171
|
+
|
|
160
172
|
## Usage with Claude Code
|
|
161
173
|
|
|
162
174
|
Add the following to your `.mcp.json` file (project-level or global `~/.claude/.mcp.json`):
|
package/dist/index.js
CHANGED
|
@@ -7463,6 +7463,74 @@ var init_adapter = __esm({
|
|
|
7463
7463
|
messageCount: label.messagesTotal ?? 0
|
|
7464
7464
|
}));
|
|
7465
7465
|
}
|
|
7466
|
+
/**
|
|
7467
|
+
* Trains Gmail's own spam filter — the same signal the "Report spam" UI
|
|
7468
|
+
* button sends (add SPAM, drop INBOX). Not an abuse report to Google.
|
|
7469
|
+
*/
|
|
7470
|
+
async reportSpam(emailId, _sourceFolder) {
|
|
7471
|
+
const gmail = this.ensureConnected();
|
|
7472
|
+
await gmail.users.messages.modify({
|
|
7473
|
+
userId: "me",
|
|
7474
|
+
id: emailId,
|
|
7475
|
+
requestBody: { addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"] }
|
|
7476
|
+
});
|
|
7477
|
+
}
|
|
7478
|
+
async createBlockRule(rule) {
|
|
7479
|
+
const gmail = this.ensureConnected();
|
|
7480
|
+
const criteria = {};
|
|
7481
|
+
switch (rule.matchType) {
|
|
7482
|
+
case "senderDomain":
|
|
7483
|
+
criteria.from = `@${rule.value}`;
|
|
7484
|
+
break;
|
|
7485
|
+
case "senderAddress":
|
|
7486
|
+
criteria.from = rule.value;
|
|
7487
|
+
break;
|
|
7488
|
+
case "subjectContains":
|
|
7489
|
+
criteria.subject = rule.value;
|
|
7490
|
+
break;
|
|
7491
|
+
case "headerContains":
|
|
7492
|
+
criteria.query = rule.value;
|
|
7493
|
+
break;
|
|
7494
|
+
}
|
|
7495
|
+
const action = rule.action === "delete" ? { addLabelIds: ["TRASH"] } : { addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"] };
|
|
7496
|
+
const res = await gmail.users.settings.filters.create({
|
|
7497
|
+
userId: "me",
|
|
7498
|
+
requestBody: { criteria, action }
|
|
7499
|
+
});
|
|
7500
|
+
return { id: res.data.id || "" };
|
|
7501
|
+
}
|
|
7502
|
+
async listBlockRules() {
|
|
7503
|
+
const gmail = this.ensureConnected();
|
|
7504
|
+
const res = await gmail.users.settings.filters.list({ userId: "me" });
|
|
7505
|
+
return (res.data.filter || []).map((f) => {
|
|
7506
|
+
const criteria = f.criteria || {};
|
|
7507
|
+
const addLabelIds = f.action?.addLabelIds || [];
|
|
7508
|
+
let matchType = "headerContains";
|
|
7509
|
+
let value = criteria.query || "";
|
|
7510
|
+
if (typeof criteria.from === "string" && criteria.from.startsWith("@")) {
|
|
7511
|
+
matchType = "senderDomain";
|
|
7512
|
+
value = criteria.from.slice(1);
|
|
7513
|
+
} else if (criteria.from) {
|
|
7514
|
+
matchType = "senderAddress";
|
|
7515
|
+
value = criteria.from;
|
|
7516
|
+
} else if (criteria.subject) {
|
|
7517
|
+
matchType = "subjectContains";
|
|
7518
|
+
value = criteria.subject;
|
|
7519
|
+
}
|
|
7520
|
+
return {
|
|
7521
|
+
id: f.id || "",
|
|
7522
|
+
matchType,
|
|
7523
|
+
value,
|
|
7524
|
+
action: addLabelIds.includes("TRASH") ? "delete" : "moveToJunk",
|
|
7525
|
+
createdAt: ""
|
|
7526
|
+
// Gmail's filter API does not expose a creation timestamp
|
|
7527
|
+
};
|
|
7528
|
+
});
|
|
7529
|
+
}
|
|
7530
|
+
async deleteBlockRule(ruleId) {
|
|
7531
|
+
const gmail = this.ensureConnected();
|
|
7532
|
+
await gmail.users.settings.filters.delete({ userId: "me", id: ruleId });
|
|
7533
|
+
}
|
|
7466
7534
|
buildRfc2822(params) {
|
|
7467
7535
|
const lines = [];
|
|
7468
7536
|
lines.push(`From: ${this.email}`);
|
|
@@ -8059,6 +8127,86 @@ var init_adapter2 = __esm({
|
|
|
8059
8127
|
}
|
|
8060
8128
|
return result;
|
|
8061
8129
|
}
|
|
8130
|
+
/**
|
|
8131
|
+
* Trains Outlook's own spam filter — the same signal "Report Junk" sends
|
|
8132
|
+
* in the UI (move to the Junk Email well-known folder). Not an abuse
|
|
8133
|
+
* report to Microsoft's security team.
|
|
8134
|
+
*/
|
|
8135
|
+
async reportSpam(emailId, _sourceFolder) {
|
|
8136
|
+
const client = this.ensureClient();
|
|
8137
|
+
await client.api(`/me/messages/${encodeURIComponent(emailId)}/move`).post({
|
|
8138
|
+
destinationId: "junkemail"
|
|
8139
|
+
});
|
|
8140
|
+
}
|
|
8141
|
+
/**
|
|
8142
|
+
* Requires the `MailboxSettings.ReadWrite` delegated scope, confirmed by
|
|
8143
|
+
* Microsoft's own docs to be supported for personal Microsoft accounts
|
|
8144
|
+
* (outlook.com/hotmail), not just work/school. Accounts authenticated
|
|
8145
|
+
* before this scope was added need to re-run the setup wizard to
|
|
8146
|
+
* re-consent — old tokens will 403 on this call until then.
|
|
8147
|
+
*/
|
|
8148
|
+
async createBlockRule(rule) {
|
|
8149
|
+
const client = this.ensureClient();
|
|
8150
|
+
const conditions = {};
|
|
8151
|
+
switch (rule.matchType) {
|
|
8152
|
+
case "senderDomain":
|
|
8153
|
+
case "senderAddress":
|
|
8154
|
+
conditions.senderContains = [rule.value];
|
|
8155
|
+
break;
|
|
8156
|
+
case "subjectContains":
|
|
8157
|
+
conditions.subjectContains = [rule.value];
|
|
8158
|
+
break;
|
|
8159
|
+
case "headerContains":
|
|
8160
|
+
conditions.headerContains = [rule.value];
|
|
8161
|
+
break;
|
|
8162
|
+
}
|
|
8163
|
+
const actions = { stopProcessingRules: true };
|
|
8164
|
+
if (rule.action === "delete") {
|
|
8165
|
+
actions.delete = true;
|
|
8166
|
+
} else {
|
|
8167
|
+
actions.moveToFolder = await this.resolveFolder("junkemail");
|
|
8168
|
+
}
|
|
8169
|
+
const existing = await this.listBlockRules();
|
|
8170
|
+
const result = await client.api("/me/mailFolders/inbox/messageRules").post({
|
|
8171
|
+
displayName: `email-mcp: ${rule.matchType} "${rule.value}"`.slice(0, 255),
|
|
8172
|
+
sequence: existing.length + 1,
|
|
8173
|
+
isEnabled: true,
|
|
8174
|
+
conditions,
|
|
8175
|
+
actions
|
|
8176
|
+
});
|
|
8177
|
+
return { id: result.id };
|
|
8178
|
+
}
|
|
8179
|
+
async listBlockRules() {
|
|
8180
|
+
const client = this.ensureClient();
|
|
8181
|
+
const response = await client.api("/me/mailFolders/inbox/messageRules").get();
|
|
8182
|
+
return (response.value || []).map((r) => {
|
|
8183
|
+
const conditions = r.conditions || {};
|
|
8184
|
+
let matchType = "headerContains";
|
|
8185
|
+
let value = "";
|
|
8186
|
+
if (conditions.senderContains?.length) {
|
|
8187
|
+
value = conditions.senderContains[0];
|
|
8188
|
+
matchType = value.includes("@") ? "senderAddress" : "senderDomain";
|
|
8189
|
+
} else if (conditions.subjectContains?.length) {
|
|
8190
|
+
matchType = "subjectContains";
|
|
8191
|
+
value = conditions.subjectContains[0];
|
|
8192
|
+
} else if (conditions.headerContains?.length) {
|
|
8193
|
+
matchType = "headerContains";
|
|
8194
|
+
value = conditions.headerContains[0];
|
|
8195
|
+
}
|
|
8196
|
+
return {
|
|
8197
|
+
id: r.id,
|
|
8198
|
+
matchType,
|
|
8199
|
+
value,
|
|
8200
|
+
action: r.actions?.delete ? "delete" : "moveToJunk",
|
|
8201
|
+
createdAt: ""
|
|
8202
|
+
// Graph's messageRule resource has no creation timestamp field
|
|
8203
|
+
};
|
|
8204
|
+
});
|
|
8205
|
+
}
|
|
8206
|
+
async deleteBlockRule(ruleId) {
|
|
8207
|
+
const client = this.ensureClient();
|
|
8208
|
+
await client.api(`/me/mailFolders/inbox/messageRules/${encodeURIComponent(ruleId)}`).delete();
|
|
8209
|
+
}
|
|
8062
8210
|
async getCategories() {
|
|
8063
8211
|
const client = this.ensureClient();
|
|
8064
8212
|
const response = await client.api("/me/outlook/masterCategories").get();
|
|
@@ -8122,7 +8270,10 @@ var init_auth2 = __esm({
|
|
|
8122
8270
|
DEFAULT_CACHE_FILE = path2.join(DEFAULT_CACHE_DIR, "msal-cache.json");
|
|
8123
8271
|
OutlookAuth = class _OutlookAuth {
|
|
8124
8272
|
static AUTHORITY = "https://login.microsoftonline.com/consumers";
|
|
8125
|
-
|
|
8273
|
+
// MailboxSettings.ReadWrite is required for the messageRules API (block
|
|
8274
|
+
// rules) — confirmed by Microsoft's own docs to be supported for personal
|
|
8275
|
+
// Microsoft accounts (not just work/school), not just Mail.ReadWrite/Send.
|
|
8276
|
+
static SCOPES = ["Mail.ReadWrite", "Mail.Send", "MailboxSettings.ReadWrite", "offline_access"];
|
|
8126
8277
|
pca;
|
|
8127
8278
|
cryptoProvider;
|
|
8128
8279
|
constructor(clientId, cacheFilePath) {
|
|
@@ -8745,6 +8896,30 @@ var init_adapter3 = __esm({
|
|
|
8745
8896
|
lock.release();
|
|
8746
8897
|
}
|
|
8747
8898
|
}
|
|
8899
|
+
/**
|
|
8900
|
+
* Best-effort only: generic IMAP has no server-side ML classifier to
|
|
8901
|
+
* train, so this just moves the message to the account's Junk-typed
|
|
8902
|
+
* folder (via the same alias resolution `deleteEmail` uses for Trash).
|
|
8903
|
+
* No standing block-rule mechanism exists across IMAP servers (Sieve
|
|
8904
|
+
* exists on some but isn't universal), so `createBlockRule` is
|
|
8905
|
+
* intentionally left unimplemented for this provider.
|
|
8906
|
+
*/
|
|
8907
|
+
async reportSpam(emailId, sourceFolder) {
|
|
8908
|
+
if (!this.client) throw new Error("Not connected");
|
|
8909
|
+
const folder = sourceFolder || "INBOX";
|
|
8910
|
+
const junkFolder = await this.resolveFolder("junk");
|
|
8911
|
+
let lock;
|
|
8912
|
+
try {
|
|
8913
|
+
lock = await this.client.getMailboxLock(folder);
|
|
8914
|
+
} catch (error48) {
|
|
8915
|
+
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
8916
|
+
}
|
|
8917
|
+
try {
|
|
8918
|
+
await this.client.messageMove(emailId, junkFolder, { uid: true });
|
|
8919
|
+
} finally {
|
|
8920
|
+
lock.release();
|
|
8921
|
+
}
|
|
8922
|
+
}
|
|
8748
8923
|
async deleteEmail(emailId, permanent, sourceFolder) {
|
|
8749
8924
|
if (!this.client) throw new Error("Not connected");
|
|
8750
8925
|
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
@@ -33492,6 +33667,144 @@ function registerOrganizingTools(server, accountManager) {
|
|
|
33492
33667
|
);
|
|
33493
33668
|
}
|
|
33494
33669
|
|
|
33670
|
+
// src/tools/moderation.ts
|
|
33671
|
+
function jsonResult5(data) {
|
|
33672
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
33673
|
+
}
|
|
33674
|
+
var BLOCK_RULE_MATCH_TYPES = ["senderDomain", "senderAddress", "subjectContains", "headerContains"];
|
|
33675
|
+
var BLOCK_RULE_ACTIONS = ["delete", "moveToJunk"];
|
|
33676
|
+
function registerModerationTools(server, accountManager) {
|
|
33677
|
+
server.tool(
|
|
33678
|
+
"email_report_spam",
|
|
33679
|
+
`Report an email as spam/junk, training the provider's own spam filter (the same signal the "Report Junk" button sends in Gmail/Outlook). This is different from email_delete, which removes the message but teaches the filter nothing. Not an abuse report to the provider's security team \u2014 it only trains this account's filter.`,
|
|
33680
|
+
{
|
|
33681
|
+
accountId: external_exports3.string(),
|
|
33682
|
+
emailId: external_exports3.string(),
|
|
33683
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
|
|
33684
|
+
},
|
|
33685
|
+
async (args) => {
|
|
33686
|
+
try {
|
|
33687
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33688
|
+
if (!provider.reportSpam) {
|
|
33689
|
+
return jsonResult5({ success: false, error: "email_report_spam is not supported on this provider" });
|
|
33690
|
+
}
|
|
33691
|
+
await provider.reportSpam(args.emailId, args.sourceFolder);
|
|
33692
|
+
return jsonResult5({ success: true });
|
|
33693
|
+
} catch (error48) {
|
|
33694
|
+
return jsonResult5({ success: false, error: error48.message });
|
|
33695
|
+
}
|
|
33696
|
+
}
|
|
33697
|
+
);
|
|
33698
|
+
server.tool(
|
|
33699
|
+
"email_batch_report_spam",
|
|
33700
|
+
"Report multiple emails as spam/junk at once. Much faster than individual calls.",
|
|
33701
|
+
{
|
|
33702
|
+
accountId: external_exports3.string(),
|
|
33703
|
+
emailIds: external_exports3.array(external_exports3.string()).min(1).describe("Array of email IDs to report as spam"),
|
|
33704
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when emails are not in INBOX)")
|
|
33705
|
+
},
|
|
33706
|
+
async (args) => {
|
|
33707
|
+
try {
|
|
33708
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33709
|
+
if (!provider.reportSpam) {
|
|
33710
|
+
return jsonResult5({ success: false, error: "email_batch_report_spam is not supported on this provider" });
|
|
33711
|
+
}
|
|
33712
|
+
const result = { succeeded: [], failed: [] };
|
|
33713
|
+
for (const id of args.emailIds) {
|
|
33714
|
+
try {
|
|
33715
|
+
await provider.reportSpam(id, args.sourceFolder);
|
|
33716
|
+
result.succeeded.push(id);
|
|
33717
|
+
} catch (e) {
|
|
33718
|
+
result.failed.push({ id, error: e.message });
|
|
33719
|
+
}
|
|
33720
|
+
}
|
|
33721
|
+
return jsonResult5({ success: true, ...result });
|
|
33722
|
+
} catch (error48) {
|
|
33723
|
+
return jsonResult5({ success: false, error: error48.message });
|
|
33724
|
+
}
|
|
33725
|
+
}
|
|
33726
|
+
);
|
|
33727
|
+
server.tool(
|
|
33728
|
+
"email_create_block_rule",
|
|
33729
|
+
'Create a standing rule that intercepts future mail matching a pattern, before it reaches the inbox. Use headerContains (e.g. matching a Reply-To domain) to block a spam template family whose visible "From" domain rotates \u2014 matching the rotating domain directly stops working within days. Not supported on iCloud/generic IMAP (no standard server-side rule mechanism exists across IMAP servers). Outlook requires the MailboxSettings.ReadWrite scope \u2014 accounts authenticated before this tool existed need to re-run the setup wizard once to re-consent.',
|
|
33730
|
+
{
|
|
33731
|
+
accountId: external_exports3.string(),
|
|
33732
|
+
matchType: external_exports3.enum(BLOCK_RULE_MATCH_TYPES).describe(
|
|
33733
|
+
"senderDomain/senderAddress: match the From address. subjectContains: match the subject. headerContains: match any header's raw content \u2014 the right choice for a stable element (e.g. a Reply-To domain) when the From domain rotates."
|
|
33734
|
+
),
|
|
33735
|
+
value: external_exports3.string().describe("The domain, address, or text to match"),
|
|
33736
|
+
action: external_exports3.enum(BLOCK_RULE_ACTIONS).describe("delete: discard on arrival. moveToJunk: file straight to Junk/Spam without notifying.")
|
|
33737
|
+
},
|
|
33738
|
+
async (args) => {
|
|
33739
|
+
try {
|
|
33740
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33741
|
+
if (!provider.createBlockRule) {
|
|
33742
|
+
return jsonResult5({
|
|
33743
|
+
success: false,
|
|
33744
|
+
error: "email_create_block_rule is not supported on this provider",
|
|
33745
|
+
supportedProviders: ["gmail", "outlook"]
|
|
33746
|
+
});
|
|
33747
|
+
}
|
|
33748
|
+
const result = await provider.createBlockRule({
|
|
33749
|
+
matchType: args.matchType,
|
|
33750
|
+
value: args.value,
|
|
33751
|
+
action: args.action
|
|
33752
|
+
});
|
|
33753
|
+
return jsonResult5({ success: true, data: result });
|
|
33754
|
+
} catch (error48) {
|
|
33755
|
+
return jsonResult5({ success: false, error: error48.message });
|
|
33756
|
+
}
|
|
33757
|
+
}
|
|
33758
|
+
);
|
|
33759
|
+
server.tool(
|
|
33760
|
+
"email_list_block_rules",
|
|
33761
|
+
"List the standing block rules on an account, for auditing or before deleting one.",
|
|
33762
|
+
{
|
|
33763
|
+
accountId: external_exports3.string()
|
|
33764
|
+
},
|
|
33765
|
+
async (args) => {
|
|
33766
|
+
try {
|
|
33767
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33768
|
+
if (!provider.listBlockRules) {
|
|
33769
|
+
return jsonResult5({
|
|
33770
|
+
success: false,
|
|
33771
|
+
error: "email_list_block_rules is not supported on this provider",
|
|
33772
|
+
supportedProviders: ["gmail", "outlook"]
|
|
33773
|
+
});
|
|
33774
|
+
}
|
|
33775
|
+
const rules = await provider.listBlockRules();
|
|
33776
|
+
return jsonResult5({ success: true, data: rules });
|
|
33777
|
+
} catch (error48) {
|
|
33778
|
+
return jsonResult5({ success: false, error: error48.message });
|
|
33779
|
+
}
|
|
33780
|
+
}
|
|
33781
|
+
);
|
|
33782
|
+
server.tool(
|
|
33783
|
+
"email_delete_block_rule",
|
|
33784
|
+
"Delete a standing block rule \u2014 use this to undo a rule that turned out to be too broad (e.g. it blocked a domain a real correspondent also uses).",
|
|
33785
|
+
{
|
|
33786
|
+
accountId: external_exports3.string(),
|
|
33787
|
+
ruleId: external_exports3.string()
|
|
33788
|
+
},
|
|
33789
|
+
async (args) => {
|
|
33790
|
+
try {
|
|
33791
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33792
|
+
if (!provider.deleteBlockRule) {
|
|
33793
|
+
return jsonResult5({
|
|
33794
|
+
success: false,
|
|
33795
|
+
error: "email_delete_block_rule is not supported on this provider",
|
|
33796
|
+
supportedProviders: ["gmail", "outlook"]
|
|
33797
|
+
});
|
|
33798
|
+
}
|
|
33799
|
+
await provider.deleteBlockRule(args.ruleId);
|
|
33800
|
+
return jsonResult5({ success: true });
|
|
33801
|
+
} catch (error48) {
|
|
33802
|
+
return jsonResult5({ success: false, error: error48.message });
|
|
33803
|
+
}
|
|
33804
|
+
}
|
|
33805
|
+
);
|
|
33806
|
+
}
|
|
33807
|
+
|
|
33495
33808
|
// src/server.ts
|
|
33496
33809
|
async function createServer(accountManager) {
|
|
33497
33810
|
const mgr = accountManager ?? new AccountManager();
|
|
@@ -33503,6 +33816,7 @@ async function createServer(accountManager) {
|
|
|
33503
33816
|
registerReadingTools(server, mgr);
|
|
33504
33817
|
registerSendingTools(server, mgr);
|
|
33505
33818
|
registerOrganizingTools(server, mgr);
|
|
33819
|
+
registerModerationTools(server, mgr);
|
|
33506
33820
|
return { server, accountManager: mgr };
|
|
33507
33821
|
}
|
|
33508
33822
|
async function startServer() {
|