hypermail-mcp 0.6.0 → 0.6.2
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 +29 -0
- package/dist/cli.js +145 -30
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
A **Model Context Protocol** server that lets an agent operate any of the user's
|
|
4
4
|
inboxes through a single, unified tool surface.
|
|
5
5
|
|
|
6
|
+
> **v0.6.2** — Version source-of-truth fix: `version.ts` now imports directly
|
|
7
|
+
> from `package.json` instead of hardcoding, preventing version drift between
|
|
8
|
+
> the two files.
|
|
9
|
+
>
|
|
10
|
+
> **v0.6.1** — Docker deployment (standalone Dockerfile with HEALTHCHECK),
|
|
11
|
+
> email notification bug fixes (ID-based dedup, pagination cap, dynamic
|
|
12
|
+
> re-scan), Node 22 base image, dropped docker-compose.
|
|
13
|
+
>
|
|
6
14
|
> **v0.6.0** — Email watch notifications (polling-based), `signaturePath`
|
|
7
15
|
> support in `set_account_settings` for loading signatures from files,
|
|
8
16
|
> and a `check_notifications` tool for draining pending alerts.
|
|
@@ -72,6 +80,27 @@ hypermail-mcp --http --port 3000 --host 0.0.0.0
|
|
|
72
80
|
When hosted you **must** set `HYPERMAIL_MCP_KEY` so the account file is
|
|
73
81
|
reproducibly decryptable.
|
|
74
82
|
|
|
83
|
+
### Docker
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Build
|
|
87
|
+
docker build -t hypermail-mcp .
|
|
88
|
+
|
|
89
|
+
# Run
|
|
90
|
+
docker run -d \
|
|
91
|
+
--name hypermail-mcp \
|
|
92
|
+
-p 3000:3000 \
|
|
93
|
+
-e HYPERMAIL_MCP_KEY=<32-byte-key> \
|
|
94
|
+
-e MS_CLIENT_ID=<your-client-id> \
|
|
95
|
+
-e MS_TENANT_ID=<your-tenant-id> \
|
|
96
|
+
-v hypermail-data:/data \
|
|
97
|
+
hypermail-mcp
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The image runs the server in HTTP mode on port 3000 with a 30-second
|
|
101
|
+
HEALTHCHECK against `/mcp`. Data is persisted via a Docker volume at `/data`.
|
|
102
|
+
Pass `HYPERMAIL_AGENTS_CONFIG` and mount a config file for agent multi-tenancy.
|
|
103
|
+
|
|
75
104
|
### Development (HTTP mode + email watch)
|
|
76
105
|
|
|
77
106
|
To test the email watch feature locally:
|
package/dist/cli.js
CHANGED
|
@@ -3819,8 +3819,80 @@ function registerTools(server, opts) {
|
|
|
3819
3819
|
}
|
|
3820
3820
|
}
|
|
3821
3821
|
|
|
3822
|
+
// package.json
|
|
3823
|
+
var package_default = {
|
|
3824
|
+
name: "hypermail-mcp",
|
|
3825
|
+
version: "0.6.2",
|
|
3826
|
+
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
3827
|
+
type: "module",
|
|
3828
|
+
bin: {
|
|
3829
|
+
"hypermail-mcp": "dist/cli.js"
|
|
3830
|
+
},
|
|
3831
|
+
main: "dist/cli.js",
|
|
3832
|
+
files: [
|
|
3833
|
+
"dist",
|
|
3834
|
+
"README.md",
|
|
3835
|
+
"LICENSE"
|
|
3836
|
+
],
|
|
3837
|
+
scripts: {
|
|
3838
|
+
build: "tsup",
|
|
3839
|
+
dev: "tsup --watch",
|
|
3840
|
+
"dev:http": "tsup && node dist/cli.js --http --config hypermail-config.http.json",
|
|
3841
|
+
start: "node dist/cli.js",
|
|
3842
|
+
typecheck: "tsc --noEmit",
|
|
3843
|
+
test: "vitest run",
|
|
3844
|
+
"test:watch": "vitest",
|
|
3845
|
+
prepublishOnly: "pnpm build && pnpm test"
|
|
3846
|
+
},
|
|
3847
|
+
engines: {
|
|
3848
|
+
node: ">=20"
|
|
3849
|
+
},
|
|
3850
|
+
keywords: [
|
|
3851
|
+
"mcp",
|
|
3852
|
+
"model-context-protocol",
|
|
3853
|
+
"email",
|
|
3854
|
+
"outlook",
|
|
3855
|
+
"microsoft-graph",
|
|
3856
|
+
"imap"
|
|
3857
|
+
],
|
|
3858
|
+
license: "MIT",
|
|
3859
|
+
repository: {
|
|
3860
|
+
type: "git",
|
|
3861
|
+
url: "git+https://github.com/mateotiedra/hypermail-mcp.git"
|
|
3862
|
+
},
|
|
3863
|
+
bugs: {
|
|
3864
|
+
url: "https://github.com/mateotiedra/hypermail-mcp/issues"
|
|
3865
|
+
},
|
|
3866
|
+
dependencies: {
|
|
3867
|
+
"@azure/msal-node": "^2.16.2",
|
|
3868
|
+
"@microsoft/microsoft-graph-client": "^3.0.7",
|
|
3869
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
3870
|
+
"google-auth-library": "^9.15.1",
|
|
3871
|
+
googleapis: "^144.0.0",
|
|
3872
|
+
imapflow: "^1.3.3",
|
|
3873
|
+
"isomorphic-fetch": "^3.0.0",
|
|
3874
|
+
"js-yaml": "^4.2.0",
|
|
3875
|
+
marked: "^18.0.4",
|
|
3876
|
+
nodemailer: "^8.0.8",
|
|
3877
|
+
turndown: "^7.2.4",
|
|
3878
|
+
zod: "^4.4.3"
|
|
3879
|
+
},
|
|
3880
|
+
optionalDependencies: {
|
|
3881
|
+
keytar: "^7.9.0"
|
|
3882
|
+
},
|
|
3883
|
+
devDependencies: {
|
|
3884
|
+
"@types/isomorphic-fetch": "^0.0.39",
|
|
3885
|
+
"@types/js-yaml": "^4.0.9",
|
|
3886
|
+
"@types/node": "^22.10.2",
|
|
3887
|
+
"@types/nodemailer": "^8.0.0",
|
|
3888
|
+
tsup: "^8.3.5",
|
|
3889
|
+
typescript: "^5.7.2",
|
|
3890
|
+
vitest: "^2.1.8"
|
|
3891
|
+
}
|
|
3892
|
+
};
|
|
3893
|
+
|
|
3822
3894
|
// src/version.ts
|
|
3823
|
-
var VERSION =
|
|
3895
|
+
var VERSION = package_default.version;
|
|
3824
3896
|
|
|
3825
3897
|
// src/config.ts
|
|
3826
3898
|
import { readFileSync } from "fs";
|
|
@@ -3970,12 +4042,29 @@ var WatcherManager = class {
|
|
|
3970
4042
|
running = false;
|
|
3971
4043
|
/** Per-account inflight guards to prevent overlapping polls. */
|
|
3972
4044
|
inflight = /* @__PURE__ */ new Map();
|
|
4045
|
+
/** Accounts with active polling timers (lowercased email). */
|
|
4046
|
+
tracked = /* @__PURE__ */ new Set();
|
|
3973
4047
|
constructor(opts) {
|
|
3974
4048
|
this.opts = opts;
|
|
3975
4049
|
}
|
|
3976
4050
|
start() {
|
|
3977
4051
|
if (this.running) return;
|
|
3978
4052
|
this.running = true;
|
|
4053
|
+
this.scanAccounts();
|
|
4054
|
+
const rescanTimer = setInterval(() => {
|
|
4055
|
+
if (!this.running) return;
|
|
4056
|
+
this.scanAccounts();
|
|
4057
|
+
}, this.opts.pollIntervalSeconds * 1e3);
|
|
4058
|
+
this.timers.push(rescanTimer);
|
|
4059
|
+
}
|
|
4060
|
+
stop() {
|
|
4061
|
+
this.running = false;
|
|
4062
|
+
for (const t of this.timers) clearInterval(t);
|
|
4063
|
+
this.timers = [];
|
|
4064
|
+
this.tracked.clear();
|
|
4065
|
+
}
|
|
4066
|
+
// ── internals ──
|
|
4067
|
+
scanAccounts() {
|
|
3979
4068
|
let accounts = this.opts.store.listAccounts();
|
|
3980
4069
|
if (this.opts.accountFilter) {
|
|
3981
4070
|
const filter = new Set(this.opts.accountFilter.map((e) => e.toLowerCase()));
|
|
@@ -3985,13 +4074,10 @@ var WatcherManager = class {
|
|
|
3985
4074
|
this.schedulePoll(account);
|
|
3986
4075
|
}
|
|
3987
4076
|
}
|
|
3988
|
-
stop() {
|
|
3989
|
-
this.running = false;
|
|
3990
|
-
for (const t of this.timers) clearInterval(t);
|
|
3991
|
-
this.timers = [];
|
|
3992
|
-
}
|
|
3993
|
-
// ── internals ──
|
|
3994
4077
|
schedulePoll(account) {
|
|
4078
|
+
const key = account.email.toLowerCase();
|
|
4079
|
+
if (this.tracked.has(key)) return;
|
|
4080
|
+
this.tracked.add(key);
|
|
3995
4081
|
this.pollAccount(account).catch(() => {
|
|
3996
4082
|
});
|
|
3997
4083
|
const timer = setInterval(() => {
|
|
@@ -4007,21 +4093,25 @@ var WatcherManager = class {
|
|
|
4007
4093
|
this.inflight.set(key, true);
|
|
4008
4094
|
try {
|
|
4009
4095
|
const { provider } = this.opts.registry.resolveByEmail(account.email);
|
|
4010
|
-
const
|
|
4096
|
+
const seenIds = new Set(account.lastSeenIds ?? []);
|
|
4097
|
+
const isFirstPoll = !account.lastSeenAt && !account.lastSeenIds?.length;
|
|
4011
4098
|
const limit = 25;
|
|
4099
|
+
const MAX_PAGES = 5;
|
|
4012
4100
|
let skip = 0;
|
|
4101
|
+
let pageCount = 0;
|
|
4013
4102
|
const newEmails = [];
|
|
4014
|
-
let newestTimestamp =
|
|
4103
|
+
let newestTimestamp = account.lastSeenAt ?? "";
|
|
4015
4104
|
let hitBoundary = false;
|
|
4016
|
-
while (
|
|
4105
|
+
while (pageCount < MAX_PAGES) {
|
|
4017
4106
|
const { items, hasMore } = await provider.listEmails(account, {
|
|
4018
4107
|
folder: "inbox",
|
|
4019
4108
|
limit,
|
|
4020
4109
|
skip
|
|
4021
4110
|
});
|
|
4111
|
+
pageCount++;
|
|
4022
4112
|
for (const item of items) {
|
|
4023
4113
|
if (!item.receivedAt) continue;
|
|
4024
|
-
if (
|
|
4114
|
+
if (seenIds.has(item.id)) {
|
|
4025
4115
|
hitBoundary = true;
|
|
4026
4116
|
break;
|
|
4027
4117
|
}
|
|
@@ -4033,11 +4123,7 @@ var WatcherManager = class {
|
|
|
4033
4123
|
if (hitBoundary || !hasMore) break;
|
|
4034
4124
|
skip += limit;
|
|
4035
4125
|
}
|
|
4036
|
-
if (!
|
|
4037
|
-
if (newEmails.length > 0) {
|
|
4038
|
-
newestTimestamp = newEmails[0].receivedAt;
|
|
4039
|
-
}
|
|
4040
|
-
} else if (newEmails.length > 0) {
|
|
4126
|
+
if (!isFirstPoll && newEmails.length > 0) {
|
|
4041
4127
|
this.enqueue({
|
|
4042
4128
|
type: "new_emails",
|
|
4043
4129
|
account: account.email,
|
|
@@ -4045,11 +4131,27 @@ var WatcherManager = class {
|
|
|
4045
4131
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4046
4132
|
});
|
|
4047
4133
|
}
|
|
4048
|
-
if (
|
|
4134
|
+
if (isFirstPoll && newEmails.length === 0 && !newestTimestamp) {
|
|
4135
|
+
newestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4136
|
+
}
|
|
4137
|
+
const newIds = newEmails.map((e) => e.id);
|
|
4138
|
+
const updatedLastSeenIds = [
|
|
4139
|
+
...newIds,
|
|
4140
|
+
...account.lastSeenIds ?? []
|
|
4141
|
+
].slice(0, 200);
|
|
4142
|
+
try {
|
|
4049
4143
|
await this.opts.store.upsertAccount({
|
|
4050
4144
|
...account,
|
|
4051
|
-
lastSeenAt: newestTimestamp || void 0
|
|
4145
|
+
lastSeenAt: newestTimestamp || void 0,
|
|
4146
|
+
lastSeenIds: updatedLastSeenIds
|
|
4052
4147
|
});
|
|
4148
|
+
} catch (storeErr) {
|
|
4149
|
+
console.error(
|
|
4150
|
+
"[hypermail-mcp] failed to persist poll state for",
|
|
4151
|
+
account.email,
|
|
4152
|
+
":",
|
|
4153
|
+
storeErr instanceof Error ? storeErr.message : String(storeErr)
|
|
4154
|
+
);
|
|
4053
4155
|
}
|
|
4054
4156
|
} catch (err) {
|
|
4055
4157
|
this.enqueue({
|
|
@@ -4186,7 +4288,8 @@ async function startServer(opts) {
|
|
|
4186
4288
|
const store = await AccountStore.open({ dataDir: config.dataDir });
|
|
4187
4289
|
const registry = buildRegistry({ store, providers: config.providers });
|
|
4188
4290
|
const tools = resolveTools(config);
|
|
4189
|
-
const
|
|
4291
|
+
const watchEnabled = config.http.enabled && config.watch?.enabled !== false;
|
|
4292
|
+
const notificationBuffer = watchEnabled ? [] : void 0;
|
|
4190
4293
|
let agentStoreForFactory;
|
|
4191
4294
|
const createServer = (agentContext = null) => {
|
|
4192
4295
|
const s = new McpServer(
|
|
@@ -4211,18 +4314,30 @@ async function startServer(opts) {
|
|
|
4211
4314
|
);
|
|
4212
4315
|
}
|
|
4213
4316
|
const notifyTargets = /* @__PURE__ */ new Set();
|
|
4214
|
-
const
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
for (const fn of notifyTargets) {
|
|
4220
|
-
fn(notification);
|
|
4317
|
+
const accountFilter = agentStoreForFactory ? (() => {
|
|
4318
|
+
const all = /* @__PURE__ */ new Set();
|
|
4319
|
+
for (const agent of agentStoreForFactory.listAgents()) {
|
|
4320
|
+
for (const email of agent.accounts) {
|
|
4321
|
+
all.add(email.toLowerCase());
|
|
4221
4322
|
}
|
|
4222
|
-
}
|
|
4223
|
-
|
|
4224
|
-
});
|
|
4225
|
-
|
|
4323
|
+
}
|
|
4324
|
+
return all.size > 0 ? [...all] : void 0;
|
|
4325
|
+
})() : void 0;
|
|
4326
|
+
if (watchEnabled) {
|
|
4327
|
+
const watcher = new WatcherManager({
|
|
4328
|
+
registry,
|
|
4329
|
+
store,
|
|
4330
|
+
pollIntervalSeconds: config.watch?.pollIntervalSeconds ?? 60,
|
|
4331
|
+
accountFilter,
|
|
4332
|
+
onNotification: (notification) => {
|
|
4333
|
+
for (const fn of notifyTargets) {
|
|
4334
|
+
fn(notification);
|
|
4335
|
+
}
|
|
4336
|
+
},
|
|
4337
|
+
buffer: notificationBuffer
|
|
4338
|
+
});
|
|
4339
|
+
watcher.start();
|
|
4340
|
+
}
|
|
4226
4341
|
await startHttp(
|
|
4227
4342
|
createServer,
|
|
4228
4343
|
config.http.host,
|