@atomicmail/mcp 0.1.0 → 0.2.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.
Files changed (49) hide show
  1. package/README.md +45 -145
  2. package/esm/lib/src/agent-auth-http.d.ts +26 -0
  3. package/esm/lib/src/agent-auth-http.d.ts.map +1 -0
  4. package/esm/lib/src/agent-auth-http.js +76 -0
  5. package/esm/{mcp/src/credentials.d.ts → lib/src/agent-credentials-store.d.ts} +3 -2
  6. package/esm/lib/src/agent-credentials-store.d.ts.map +1 -0
  7. package/esm/{mcp/src/credentials.js → lib/src/agent-credentials-store.js} +19 -16
  8. package/esm/lib/src/agent-help-content.d.ts +4 -0
  9. package/esm/lib/src/agent-help-content.d.ts.map +1 -0
  10. package/esm/lib/src/agent-help-content.js +236 -0
  11. package/esm/lib/src/agent-jmap.d.ts +49 -0
  12. package/esm/lib/src/agent-jmap.d.ts.map +1 -0
  13. package/esm/lib/src/agent-jmap.js +130 -0
  14. package/esm/lib/src/agent-jwt.d.ts +14 -0
  15. package/esm/lib/src/agent-jwt.d.ts.map +1 -0
  16. package/esm/lib/src/agent-jwt.js +29 -0
  17. package/esm/lib/src/agent-pow.d.ts +5 -0
  18. package/esm/lib/src/agent-pow.d.ts.map +1 -0
  19. package/esm/lib/src/agent-pow.js +49 -0
  20. package/esm/lib/src/agent-resolve-config.d.ts +24 -0
  21. package/esm/lib/src/agent-resolve-config.d.ts.map +1 -0
  22. package/esm/lib/src/agent-resolve-config.js +70 -0
  23. package/esm/lib/src/agent-session.d.ts +62 -0
  24. package/esm/lib/src/agent-session.d.ts.map +1 -0
  25. package/esm/lib/src/agent-session.js +206 -0
  26. package/esm/lib/src/agent-vars.d.ts +23 -0
  27. package/esm/lib/src/agent-vars.d.ts.map +1 -0
  28. package/esm/lib/src/agent-vars.js +65 -0
  29. package/esm/mcp/src/main.js +31 -61
  30. package/esm/mcp/src/tools/help.d.ts +3 -0
  31. package/esm/mcp/src/tools/help.d.ts.map +1 -0
  32. package/esm/mcp/src/tools/help.js +22 -0
  33. package/esm/mcp/src/tools/jmap.d.ts +2 -2
  34. package/esm/mcp/src/tools/jmap.d.ts.map +1 -1
  35. package/esm/mcp/src/tools/jmap.js +53 -140
  36. package/esm/mcp/src/tools/register.d.ts +2 -2
  37. package/esm/mcp/src/tools/register.d.ts.map +1 -1
  38. package/esm/mcp/src/tools/register.js +9 -45
  39. package/package.json +1 -1
  40. package/esm/mcp/src/auth-session.d.ts +0 -88
  41. package/esm/mcp/src/auth-session.d.ts.map +0 -1
  42. package/esm/mcp/src/auth-session.js +0 -378
  43. package/esm/mcp/src/credentials.d.ts.map +0 -1
  44. package/esm/mcp/src/docs-content.d.ts +0 -4
  45. package/esm/mcp/src/docs-content.d.ts.map +0 -1
  46. package/esm/mcp/src/docs-content.js +0 -405
  47. package/esm/mcp/src/tools/docs.d.ts +0 -3
  48. package/esm/mcp/src/tools/docs.d.ts.map +0 -1
  49. package/esm/mcp/src/tools/docs.js +0 -22
@@ -0,0 +1,62 @@
1
+ import { type SkillFiles } from "./agent-credentials-store.js";
2
+ export interface AgentSessionConfig {
3
+ authUrl: string;
4
+ apiUrl: string;
5
+ scryptSalt: string;
6
+ apiKey?: string;
7
+ inboxId?: string;
8
+ credentialDir: string;
9
+ files: SkillFiles;
10
+ }
11
+ export interface RegisterResult {
12
+ inbox: string;
13
+ accountId: string;
14
+ /** Present only on first-time signup (not idempotent replay). */
15
+ apiKey?: string;
16
+ idempotent?: boolean;
17
+ }
18
+ /** Local-part of an inbox email, or the whole string if no @. */
19
+ export declare function inboxLocalPart(inboxId: string): string;
20
+ export declare class AgentSession {
21
+ private readonly authUrl;
22
+ readonly apiUrl: string;
23
+ private readonly scryptSalt;
24
+ private apiKey;
25
+ private inboxId;
26
+ readonly credentialDir: string;
27
+ readonly files: SkillFiles;
28
+ private sessionJWT;
29
+ private capabilityJWT;
30
+ private cachedMailAccountId;
31
+ constructor(cfg: AgentSessionConfig);
32
+ static create(cfg: AgentSessionConfig): Promise<AgentSession>;
33
+ get hasApiKey(): boolean;
34
+ get currentInboxId(): string | undefined;
35
+ private loadFromDisk;
36
+ /**
37
+ * Primary JMAP mail accountId from GET /.well-known/jmap (cached).
38
+ */
39
+ getPrimaryMailAccountId(): Promise<string>;
40
+ invalidateJmapSessionCache(): void;
41
+ /**
42
+ * Register or return existing inbox when username matches (idempotent).
43
+ * Different username replaces on-disk credentials and creates a new inbox.
44
+ */
45
+ register(username: string): Promise<RegisterResult>;
46
+ getCapabilityToken(): Promise<string>;
47
+ private ensureSession;
48
+ destroy(): void;
49
+ }
50
+ export interface PersistLoginWithApiKeyInput {
51
+ authUrl: string;
52
+ apiUrl: string;
53
+ scryptSalt: string;
54
+ apiKey: string;
55
+ files: SkillFiles;
56
+ onPowProgress?: (nonce: bigint) => void;
57
+ }
58
+ /** PoW login with an existing API key; writes credentials + JWT files. */
59
+ export declare function persistLoginWithApiKey(input: PersistLoginWithApiKeyInput): Promise<{
60
+ inboxId: string;
61
+ }>;
62
+ //# sourceMappingURL=agent-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session.d.ts","sourceRoot":"","sources":["../../../src/lib/src/agent-session.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,UAAU,EAMhB,MAAM,8BAA8B,CAAC;AAatC,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAMD,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAqB;IACpC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAE3B,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,mBAAmB,CAAqB;gBAEpC,GAAG,EAAE,kBAAkB;WAUtB,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC;IAMnE,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,cAAc,IAAI,MAAM,GAAG,SAAS,CAEvC;YAEa,YAAY;IAU1B;;OAEG;IACG,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC;IAShD,0BAA0B,IAAI,IAAI;IAIlC;;;OAGG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAuEnD,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC;YA4B7B,aAAa;IAyB3B,OAAO,IAAI,IAAI;CAGhB;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,UAAU,CAAC;IAClB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,0EAA0E;AAC1E,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,2BAA2B,GACjC,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAyB9B"}
@@ -0,0 +1,206 @@
1
+ // Stateful PoW + capability JWT + optional cached JMAP session (accountId).
2
+ import { tryReadCredentials, tryReadJwtFile, unlinkCredentialArtifacts, writeCredentials, writeJwtFile, } from "./agent-credentials-store.js";
3
+ import { CAPABILITY_SAFETY_MARGIN_MS, decodeJwtPayload, isJwtExpired, SESSION_SAFETY_MARGIN_MS, } from "./agent-jwt.js";
4
+ import { extractPrimaryMailAccountId, fetchJmapWellKnown, } from "./agent-jmap.js";
5
+ import { fetchCapability, performPoWAndSession } from "./agent-auth-http.js";
6
+ function normalizeUsername(u) {
7
+ return u.trim().toLowerCase();
8
+ }
9
+ /** Local-part of an inbox email, or the whole string if no @. */
10
+ export function inboxLocalPart(inboxId) {
11
+ const i = inboxId.indexOf("@");
12
+ return i === -1
13
+ ? normalizeUsername(inboxId)
14
+ : normalizeUsername(inboxId.slice(0, i));
15
+ }
16
+ export class AgentSession {
17
+ authUrl;
18
+ apiUrl;
19
+ scryptSalt;
20
+ apiKey;
21
+ inboxId;
22
+ credentialDir;
23
+ files;
24
+ sessionJWT;
25
+ capabilityJWT;
26
+ cachedMailAccountId;
27
+ constructor(cfg) {
28
+ this.authUrl = cfg.authUrl.replace(/\/+$/, "");
29
+ this.apiUrl = cfg.apiUrl.replace(/\/+$/, "");
30
+ this.scryptSalt = cfg.scryptSalt;
31
+ this.apiKey = cfg.apiKey;
32
+ this.inboxId = cfg.inboxId;
33
+ this.credentialDir = cfg.credentialDir;
34
+ this.files = cfg.files;
35
+ }
36
+ static async create(cfg) {
37
+ const session = new AgentSession(cfg);
38
+ await session.loadFromDisk();
39
+ return session;
40
+ }
41
+ get hasApiKey() {
42
+ return this.apiKey !== undefined && this.apiKey.length > 0;
43
+ }
44
+ get currentInboxId() {
45
+ return this.inboxId;
46
+ }
47
+ async loadFromDisk() {
48
+ this.sessionJWT = await tryReadJwtFile(this.files.sessionFile);
49
+ this.capabilityJWT = await tryReadJwtFile(this.files.capabilityFile);
50
+ const disk = await tryReadCredentials(this.files.credentialsFile);
51
+ if (disk) {
52
+ this.apiKey = this.apiKey ?? disk.apiKey;
53
+ this.inboxId = this.inboxId ?? disk.inboxId;
54
+ }
55
+ }
56
+ /**
57
+ * Primary JMAP mail accountId from GET /.well-known/jmap (cached).
58
+ */
59
+ async getPrimaryMailAccountId() {
60
+ if (this.cachedMailAccountId)
61
+ return this.cachedMailAccountId;
62
+ const cap = await this.getCapabilityToken();
63
+ const session = await fetchJmapWellKnown(this.apiUrl, cap);
64
+ const id = extractPrimaryMailAccountId(session);
65
+ this.cachedMailAccountId = id;
66
+ return id;
67
+ }
68
+ invalidateJmapSessionCache() {
69
+ this.cachedMailAccountId = undefined;
70
+ }
71
+ /**
72
+ * Register or return existing inbox when username matches (idempotent).
73
+ * Different username replaces on-disk credentials and creates a new inbox.
74
+ */
75
+ async register(username) {
76
+ const want = normalizeUsername(username);
77
+ if (this.hasApiKey && !this.inboxId) {
78
+ throw new Error("Cannot register: an API key is configured but inboxId is unknown. " +
79
+ "Fix credentials.json or unset ATOMIC_MAIL_API_KEY before registering.");
80
+ }
81
+ if (this.hasApiKey && this.inboxId) {
82
+ const have = inboxLocalPart(this.inboxId);
83
+ if (have === want) {
84
+ const accountId = await this.getPrimaryMailAccountId();
85
+ return {
86
+ inbox: this.inboxId,
87
+ accountId,
88
+ idempotent: true,
89
+ };
90
+ }
91
+ await unlinkCredentialArtifacts(this.files);
92
+ this.apiKey = undefined;
93
+ this.inboxId = undefined;
94
+ this.sessionJWT = undefined;
95
+ this.capabilityJWT = undefined;
96
+ this.cachedMailAccountId = undefined;
97
+ }
98
+ const result = await performPoWAndSession({
99
+ authUrl: this.authUrl,
100
+ scryptSalt: this.scryptSalt,
101
+ username,
102
+ });
103
+ if (!result.apiKey) {
104
+ throw new Error("Signup did not return an apiKey — this indicates a server bug.");
105
+ }
106
+ this.apiKey = result.apiKey;
107
+ this.sessionJWT = result.sessionJWT;
108
+ await writeJwtFile(this.files.sessionFile, this.sessionJWT);
109
+ const capability = await fetchCapability(this.authUrl, this.sessionJWT);
110
+ this.capabilityJWT = capability;
111
+ await writeJwtFile(this.files.capabilityFile, capability);
112
+ const claims = decodeJwtPayload(capability);
113
+ if (typeof claims.inboxId !== "string" || claims.inboxId.length === 0) {
114
+ throw new Error("Capability JWT missing inboxId claim after signup.");
115
+ }
116
+ this.inboxId = claims.inboxId;
117
+ this.cachedMailAccountId = undefined;
118
+ const creds = {
119
+ apiKey: this.apiKey,
120
+ inboxId: this.inboxId,
121
+ authUrl: this.authUrl,
122
+ apiUrl: this.apiUrl,
123
+ scryptSalt: this.scryptSalt,
124
+ };
125
+ await writeCredentials(this.files.credentialsFile, creds);
126
+ const accountId = await this.getPrimaryMailAccountId();
127
+ return {
128
+ inbox: this.inboxId,
129
+ accountId,
130
+ apiKey: this.apiKey,
131
+ };
132
+ }
133
+ async getCapabilityToken() {
134
+ if (this.capabilityJWT &&
135
+ !isJwtExpired(this.capabilityJWT, CAPABILITY_SAFETY_MARGIN_MS)) {
136
+ return this.capabilityJWT;
137
+ }
138
+ await this.ensureSession();
139
+ if (!this.sessionJWT) {
140
+ throw new Error("Internal: ensureSession() left sessionJWT unset.");
141
+ }
142
+ const cap = await fetchCapability(this.authUrl, this.sessionJWT);
143
+ this.capabilityJWT = cap;
144
+ await writeJwtFile(this.files.capabilityFile, cap);
145
+ try {
146
+ const claims = decodeJwtPayload(cap);
147
+ if (typeof claims.inboxId === "string" && claims.inboxId.length > 0) {
148
+ this.inboxId = claims.inboxId;
149
+ }
150
+ }
151
+ catch {
152
+ // non-fatal
153
+ }
154
+ return cap;
155
+ }
156
+ async ensureSession() {
157
+ if (this.sessionJWT &&
158
+ !isJwtExpired(this.sessionJWT, SESSION_SAFETY_MARGIN_MS)) {
159
+ return;
160
+ }
161
+ if (!this.apiKey) {
162
+ throw new Error("No API key configured and no valid session on disk. Run register " +
163
+ "first, set ATOMIC_MAIL_API_KEY, or place credentials.json in the " +
164
+ "credential directory.");
165
+ }
166
+ const result = await performPoWAndSession({
167
+ authUrl: this.authUrl,
168
+ scryptSalt: this.scryptSalt,
169
+ apiKey: this.apiKey,
170
+ });
171
+ this.sessionJWT = result.sessionJWT;
172
+ this.capabilityJWT = undefined;
173
+ this.cachedMailAccountId = undefined;
174
+ await writeJwtFile(this.files.sessionFile, this.sessionJWT);
175
+ }
176
+ destroy() {
177
+ // reserved
178
+ }
179
+ }
180
+ /** PoW login with an existing API key; writes credentials + JWT files. */
181
+ export async function persistLoginWithApiKey(input) {
182
+ const authUrl = input.authUrl.replace(/\/+$/, "");
183
+ const apiUrl = input.apiUrl.replace(/\/+$/, "");
184
+ const session = await performPoWAndSession({
185
+ authUrl,
186
+ scryptSalt: input.scryptSalt,
187
+ apiKey: input.apiKey,
188
+ onPowProgress: input.onPowProgress,
189
+ });
190
+ const capabilityJWT = await fetchCapability(authUrl, session.sessionJWT);
191
+ const claims = decodeJwtPayload(capabilityJWT);
192
+ const inboxId = claims.inboxId;
193
+ if (typeof inboxId !== "string" || inboxId.length === 0) {
194
+ throw new Error("Capability JWT did not contain an inboxId claim.");
195
+ }
196
+ await writeCredentials(input.files.credentialsFile, {
197
+ apiKey: input.apiKey,
198
+ inboxId,
199
+ authUrl,
200
+ apiUrl,
201
+ scryptSalt: input.scryptSalt,
202
+ });
203
+ await writeJwtFile(input.files.sessionFile, session.sessionJWT);
204
+ await writeJwtFile(input.files.capabilityFile, capabilityJWT);
205
+ return { inboxId };
206
+ }
@@ -0,0 +1,23 @@
1
+ /** Matches `$FOO_BAR`; excludes JMAP keywords like `$draft` (lowercase). */
2
+ export declare const VAR_PATTERN: RegExp;
3
+ /** Names substituted from JMAP session / credentials when not overridden in `vars`. */
4
+ export declare const SESSION_VAR_NAMES: Set<string>;
5
+ export interface SubstituteVarsInput {
6
+ raw: string;
7
+ /** Caller-supplied values; keys are names without `$` (e.g. `TO`, `SUBJECT`). */
8
+ vars?: Record<string, string>;
9
+ /** Invoked only when the name appears in `raw`, is absent from `vars`, and a resolver exists. */
10
+ autoResolvers?: Record<string, () => Promise<string> | string>;
11
+ }
12
+ export interface SubstituteVarsResult {
13
+ text: string;
14
+ }
15
+ /** Unique variable names in order of first occurrence (without leading `$`). */
16
+ export declare function findVarReferences(raw: string): string[];
17
+ /**
18
+ * Replaces every `$VAR_NAME` in `raw` with the corresponding string.
19
+ * Single pass — values are not scanned for further `$` tokens.
20
+ * Throws if any referenced variable has no value (after vars + autoResolvers).
21
+ */
22
+ export declare function substituteVars(input: SubstituteVarsInput): Promise<SubstituteVarsResult>;
23
+ //# sourceMappingURL=agent-vars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-vars.d.ts","sourceRoot":"","sources":["../../../src/lib/src/agent-vars.ts"],"names":[],"mappings":"AAEA,4EAA4E;AAC5E,eAAO,MAAM,WAAW,QAAyB,CAAC;AAMlD,uFAAuF;AACvF,eAAO,MAAM,iBAAiB,aAA2C,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,iGAAiG;IACjG,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;CAChE;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAWvD;AAeD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CA+B/B"}
@@ -0,0 +1,65 @@
1
+ // Variable substitution for JMAP presets / inline ops ($VAR_NAME tokens).
2
+ /** Matches `$FOO_BAR`; excludes JMAP keywords like `$draft` (lowercase). */
3
+ export const VAR_PATTERN = /\$([A-Z][A-Z0-9_]*)/g;
4
+ function varPattern() {
5
+ return new RegExp(VAR_PATTERN.source, VAR_PATTERN.flags);
6
+ }
7
+ /** Names substituted from JMAP session / credentials when not overridden in `vars`. */
8
+ export const SESSION_VAR_NAMES = new Set(["ACCOUNT_ID", "INBOX"]);
9
+ /** Unique variable names in order of first occurrence (without leading `$`). */
10
+ export function findVarReferences(raw) {
11
+ const seen = new Set();
12
+ const order = [];
13
+ for (const m of raw.matchAll(varPattern())) {
14
+ const name = m[1];
15
+ if (!seen.has(name)) {
16
+ seen.add(name);
17
+ order.push(name);
18
+ }
19
+ }
20
+ return order;
21
+ }
22
+ function formatMissingError(missing) {
23
+ const tokens = missing.map((n) => `$${n}`);
24
+ const hasSession = missing.some((n) => SESSION_VAR_NAMES.has(n));
25
+ let msg = `Missing values for variables: ${tokens.join(", ")}. ` +
26
+ "Pass custom placeholders in vars (MCP) or --vars (skill).";
27
+ if (hasSession) {
28
+ msg +=
29
+ " For $ACCOUNT_ID and $INBOX, ensure register completed and credentials are valid, " +
30
+ "or pass overrides in vars.";
31
+ }
32
+ return new Error(msg);
33
+ }
34
+ /**
35
+ * Replaces every `$VAR_NAME` in `raw` with the corresponding string.
36
+ * Single pass — values are not scanned for further `$` tokens.
37
+ * Throws if any referenced variable has no value (after vars + autoResolvers).
38
+ */
39
+ export async function substituteVars(input) {
40
+ const names = findVarReferences(input.raw);
41
+ if (names.length === 0) {
42
+ return { text: input.raw };
43
+ }
44
+ const userVars = input.vars ?? {};
45
+ const resolved = new Map();
46
+ for (const name of names) {
47
+ if (Object.prototype.hasOwnProperty.call(userVars, name)) {
48
+ resolved.set(name, userVars[name]);
49
+ continue;
50
+ }
51
+ const resolver = input.autoResolvers?.[name];
52
+ if (resolver) {
53
+ resolved.set(name, await resolver());
54
+ continue;
55
+ }
56
+ }
57
+ const missing = names.filter((n) => !resolved.has(n));
58
+ if (missing.length > 0) {
59
+ throw formatMissingError(missing);
60
+ }
61
+ const text = input.raw.replace(varPattern(), (_full, name) => {
62
+ return resolved.get(name);
63
+ });
64
+ return { text };
65
+ }
@@ -1,88 +1,58 @@
1
1
  #!/usr/bin/env node
2
- // AtomicMail MCP server — local stdio proxy with PoW auth and JMAP.
2
+ // AtomicMail MCP server — stdio, PoW auth, JMAP (register / jmap_request / help).
3
3
  //
4
- // Designed to be launched by an MCP-capable agent host (Cursor, Claude
5
- // Desktop, Continue, Cline, ...) as a child process and communicated with
6
- // over stdio.
7
- //
8
- // CONFIGURATION (priority order):
9
- // 1. credentials.json + session.jwt + capability.jwt in the credential
10
- // directory (default: ~/.atomicmail/, override with the env var
11
- // ATOMIC_MAIL_CREDENTIALS_DIR). This is the SAME on-disk layout
12
- // produced by the atomic-mail-signup CLI from @atomic-mail/agent-skill.
13
- // 2. ATOMIC_MAIL_AUTH_URL / ATOMIC_MAIL_API_URL / ATOMIC_MAIL_API_KEY —
14
- // overlay individual fields on top of (1). Optional: ATOMIC_MAIL_SCRYPT_SALT
15
- // overrides the built-in PoW salt (normally unnecessary).
16
- //
17
- // If auth and API URLs cannot be resolved, the server fails fast on startup.
18
- //
19
- // See README.md for installation and MCP host configuration examples.
4
+ // CONFIGURATION: credentials.json + session.jwt + capability.jwt in the
5
+ // credential directory (default ~/.atomicmail/, override ATOMIC_MAIL_CREDENTIALS_DIR),
6
+ // merged with ATOMIC_MAIL_AUTH_URL / ATOMIC_MAIL_API_URL / ATOMIC_MAIL_API_KEY.
20
7
  import process from "node:process";
21
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
- import { AuthSession, resolveConfig } from "./auth-session.js";
24
- import { registerDocsTool } from "./tools/docs.js";
10
+ import { AgentSession } from "../../lib/src/agent-session.js";
11
+ import { resolveAgentConfigFromEnv } from "../../lib/src/agent-resolve-config.js";
12
+ import { registerHelpTool } from "./tools/help.js";
25
13
  import { registerJmapTool } from "./tools/jmap.js";
26
14
  import { registerRegisterTool } from "./tools/register.js";
27
15
  const VERSION = "0.1.0";
28
16
  const INSTRUCTIONS = `\
29
- AtomicMail MCP server a programmable email inbox for AI agents.
17
+ Atomic Mail MCP — programmable inbox for AI agents.
30
18
 
31
19
  WORKFLOW
32
- 1. If you do not yet have an account, call the 'register' tool with a
33
- desired username. It performs a Proof-of-Work signup and persists
34
- credentials.json + session.jwt + capability.jwt into the credential
35
- directory so future invocations skip this step.
36
- 2. Call 'jmap_session' to fetch your accountId, capabilities, and the
37
- mailbox URL. Cache the accountId — every subsequent JMAP call needs it.
38
- 3. Call 'jmap_request' with one or more JMAP method calls. Auth (session
39
- + capability JWT rotation) is fully automatic. You can either pass
40
- 'methodCalls' inline or 'opsFile' (a path to a saved JSON preset).
41
- 4. Call 'get_docs' for in-depth topic guides. Topics include:
42
- overview, auth, jmap_cheatsheet, tools, installation, presets,
43
- troubleshooting.
20
+ 1. Call register with a desired username (PoW signup; credentials on disk).
21
+ 2. Call jmap_request with JMAP method calls (inline ops JSON or ops_file preset).
22
+ $VAR_NAME tokens: $ACCOUNT_ID / $INBOX from session; pass others in vars.
23
+ 3. Call help for full documentation (JMAP cheatsheet, presets, troubleshooting).
44
24
 
45
- CREDENTIAL FILES
46
- Credentials live in ~/.atomicmail/ by default (override with
47
- ATOMIC_MAIL_CREDENTIALS_DIR). The same files are produced by the
48
- atomic-mail-signup CLI from @atomic-mail/agent-skill so you can mix and
49
- match the two — e.g. bootstrap an account with the CLI, then point the
50
- MCP at the same directory.
25
+ CREDENTIAL DIRECTORY
26
+ Default ~/.atomicmail/ (override ATOMIC_MAIL_CREDENTIALS_DIR). Same files as
27
+ the @atomicmail/agent-skill CLI: credentials.json, session.jwt, capability.jwt.
51
28
 
52
- credentials.json { apiKey, inboxId, authUrl, apiUrl, scryptSalt }
53
- session.jwt 4-hour TTL, rotated automatically via PoW.
54
- capability.jwt 2-minute TTL, rotated automatically via /capability.
55
-
56
- ENVIRONMENT VARIABLES
57
- ATOMIC_MAIL_CREDENTIALS_DIR override default credential directory
58
- ATOMIC_MAIL_AUTH_URL auth-service base URL
59
- ATOMIC_MAIL_API_URL api-service / JMAP base URL
60
- ATOMIC_MAIL_SCRYPT_SALT optional PoW salt override (defaults match auth-service)
61
- ATOMIC_MAIL_API_KEY existing API key (skip signup)
29
+ ENVIRONMENT
30
+ ATOMIC_MAIL_CREDENTIALS_DIR credential directory
31
+ ATOMIC_MAIL_AUTH_URL auth-service base URL
32
+ ATOMIC_MAIL_API_URL JMAP / API base URL
33
+ ATOMIC_MAIL_SCRYPT_SALT optional PoW salt override
34
+ ATOMIC_MAIL_API_KEY optional existing API key
62
35
 
63
36
  SECURITY
64
- credentials.json contains your apiKey — treat it like a password. Files
65
- are written with mode 0600.
66
-
67
- For full reference call get_docs with the relevant topic.`;
37
+ credentials.json contains your apiKey — treat it as a secret (mode 0600).`;
68
38
  async function main() {
69
39
  let config;
70
40
  try {
71
- config = await resolveConfig();
41
+ config = await resolveAgentConfigFromEnv();
72
42
  }
73
43
  catch (err) {
74
- console.error("AtomicMail MCP: configuration error:", err instanceof Error ? err.message : err);
44
+ console.error("Atomic Mail MCP: configuration error:", err instanceof Error ? err.message : err);
75
45
  process.exit(1);
76
46
  }
77
- console.error(`AtomicMail MCP v${VERSION}: credential dir '${config.credentialDir}' ` +
47
+ console.error(`Atomic Mail MCP v${VERSION}: credential dir '${config.credentialDir}' ` +
78
48
  `(config source: ${config.source}).`);
79
49
  if (config.apiKey) {
80
- console.error(`AtomicMail MCP: API key found${config.inboxId ? ` (inbox ${config.inboxId})` : ""}.`);
50
+ console.error(`Atomic Mail MCP: API key configured${config.inboxId ? ` (inbox ${config.inboxId})` : ""}.`);
81
51
  }
82
52
  else {
83
- console.error("AtomicMail MCP: no API key configured — call the 'register' tool to create an account.");
53
+ console.error("Atomic Mail MCP: no API key — call register to create an account.");
84
54
  }
85
- const session = await AuthSession.create({
55
+ const session = await AgentSession.create({
86
56
  authUrl: config.authUrl,
87
57
  apiUrl: config.apiUrl,
88
58
  scryptSalt: config.scryptSalt,
@@ -94,7 +64,7 @@ async function main() {
94
64
  const server = new McpServer({ name: "atomicmail", version: VERSION }, { instructions: INSTRUCTIONS });
95
65
  registerRegisterTool(server, session);
96
66
  registerJmapTool(server, session);
97
- registerDocsTool(server);
67
+ registerHelpTool(server);
98
68
  const cleanup = () => {
99
69
  session.destroy();
100
70
  };
@@ -108,9 +78,9 @@ async function main() {
108
78
  });
109
79
  const transport = new StdioServerTransport();
110
80
  await server.connect(transport);
111
- console.error("AtomicMail MCP: server running on stdio");
81
+ console.error("Atomic Mail MCP: server running on stdio");
112
82
  }
113
83
  main().catch((err) => {
114
- console.error("AtomicMail MCP: fatal error:", err);
84
+ console.error("Atomic Mail MCP: fatal error:", err);
115
85
  process.exit(1);
116
86
  });
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
2
+ export declare function registerHelpTool(server: McpServer): void;
3
+ //# sourceMappingURL=help.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../../src/mcp/src/tools/help.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAOtE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA4BxD"}
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { getHelp, HELP_TOPIC_LIST, } from "../../../lib/src/agent-help-content.js";
3
+ export function registerHelpTool(server) {
4
+ server.registerTool("help", {
5
+ title: "Atomic Mail documentation",
6
+ description: "Return in-depth documentation: JMAP cheatsheet, presets, auth flow, " +
7
+ "troubleshooting. Optional topic. Topics: " +
8
+ HELP_TOPIC_LIST.join(", ") + ".",
9
+ inputSchema: z.object({
10
+ topic: z
11
+ .string()
12
+ .optional()
13
+ .describe(`Topic (e.g. ${HELP_TOPIC_LIST.slice(0, 4).join(", ")}, ...). Omit for overview.`),
14
+ }),
15
+ annotations: {
16
+ readOnlyHint: true,
17
+ idempotentHint: true,
18
+ },
19
+ }, ({ topic }) => ({
20
+ content: [{ type: "text", text: getHelp(topic) }],
21
+ }));
22
+ }
@@ -1,4 +1,4 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
2
- import type { AuthSession } from "../auth-session.js";
3
- export declare function registerJmapTool(server: McpServer, session: AuthSession): void;
2
+ import type { AgentSession } from "../../../lib/src/agent-session.js";
3
+ export declare function registerJmapTool(server: McpServer, session: AgentSession): void;
4
4
  //# sourceMappingURL=jmap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jmap.d.ts","sourceRoot":"","sources":["../../../../src/mcp/src/tools/jmap.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAEtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA2CtD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,WAAW,GACnB,IAAI,CAyMN"}
1
+ {"version":3,"file":"jmap.d.ts","sourceRoot":"","sources":["../../../../src/mcp/src/tools/jmap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAOtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAEtE,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,YAAY,GACpB,IAAI,CAmIN"}