@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.
- package/README.md +45 -145
- package/esm/lib/src/agent-auth-http.d.ts +26 -0
- package/esm/lib/src/agent-auth-http.d.ts.map +1 -0
- package/esm/lib/src/agent-auth-http.js +76 -0
- package/esm/{mcp/src/credentials.d.ts → lib/src/agent-credentials-store.d.ts} +3 -2
- package/esm/lib/src/agent-credentials-store.d.ts.map +1 -0
- package/esm/{mcp/src/credentials.js → lib/src/agent-credentials-store.js} +19 -16
- package/esm/lib/src/agent-help-content.d.ts +4 -0
- package/esm/lib/src/agent-help-content.d.ts.map +1 -0
- package/esm/lib/src/agent-help-content.js +236 -0
- package/esm/lib/src/agent-jmap.d.ts +49 -0
- package/esm/lib/src/agent-jmap.d.ts.map +1 -0
- package/esm/lib/src/agent-jmap.js +130 -0
- package/esm/lib/src/agent-jwt.d.ts +14 -0
- package/esm/lib/src/agent-jwt.d.ts.map +1 -0
- package/esm/lib/src/agent-jwt.js +29 -0
- package/esm/lib/src/agent-pow.d.ts +5 -0
- package/esm/lib/src/agent-pow.d.ts.map +1 -0
- package/esm/lib/src/agent-pow.js +49 -0
- package/esm/lib/src/agent-resolve-config.d.ts +24 -0
- package/esm/lib/src/agent-resolve-config.d.ts.map +1 -0
- package/esm/lib/src/agent-resolve-config.js +70 -0
- package/esm/lib/src/agent-session.d.ts +62 -0
- package/esm/lib/src/agent-session.d.ts.map +1 -0
- package/esm/lib/src/agent-session.js +206 -0
- package/esm/lib/src/agent-vars.d.ts +23 -0
- package/esm/lib/src/agent-vars.d.ts.map +1 -0
- package/esm/lib/src/agent-vars.js +65 -0
- package/esm/mcp/src/main.js +31 -61
- package/esm/mcp/src/tools/help.d.ts +3 -0
- package/esm/mcp/src/tools/help.d.ts.map +1 -0
- package/esm/mcp/src/tools/help.js +22 -0
- package/esm/mcp/src/tools/jmap.d.ts +2 -2
- package/esm/mcp/src/tools/jmap.d.ts.map +1 -1
- package/esm/mcp/src/tools/jmap.js +53 -140
- package/esm/mcp/src/tools/register.d.ts +2 -2
- package/esm/mcp/src/tools/register.d.ts.map +1 -1
- package/esm/mcp/src/tools/register.js +9 -45
- package/package.json +1 -1
- package/esm/mcp/src/auth-session.d.ts +0 -88
- package/esm/mcp/src/auth-session.d.ts.map +0 -1
- package/esm/mcp/src/auth-session.js +0 -378
- package/esm/mcp/src/credentials.d.ts.map +0 -1
- package/esm/mcp/src/docs-content.d.ts +0 -4
- package/esm/mcp/src/docs-content.d.ts.map +0 -1
- package/esm/mcp/src/docs-content.js +0 -405
- package/esm/mcp/src/tools/docs.d.ts +0 -3
- package/esm/mcp/src/tools/docs.d.ts.map +0 -1
- 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
|
+
}
|
package/esm/mcp/src/main.js
CHANGED
|
@@ -1,88 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// AtomicMail MCP server —
|
|
2
|
+
// AtomicMail MCP server — stdio, PoW auth, JMAP (register / jmap_request / help).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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 {
|
|
24
|
-
import {
|
|
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
|
-
|
|
17
|
+
Atomic Mail MCP — programmable inbox for AI agents.
|
|
30
18
|
|
|
31
19
|
WORKFLOW
|
|
32
|
-
1.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
|
41
|
+
config = await resolveAgentConfigFromEnv();
|
|
72
42
|
}
|
|
73
43
|
catch (err) {
|
|
74
|
-
console.error("
|
|
44
|
+
console.error("Atomic Mail MCP: configuration error:", err instanceof Error ? err.message : err);
|
|
75
45
|
process.exit(1);
|
|
76
46
|
}
|
|
77
|
-
console.error(`
|
|
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(`
|
|
50
|
+
console.error(`Atomic Mail MCP: API key configured${config.inboxId ? ` (inbox ${config.inboxId})` : ""}.`);
|
|
81
51
|
}
|
|
82
52
|
else {
|
|
83
|
-
console.error("
|
|
53
|
+
console.error("Atomic Mail MCP: no API key — call register to create an account.");
|
|
84
54
|
}
|
|
85
|
-
const session = await
|
|
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
|
-
|
|
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("
|
|
81
|
+
console.error("Atomic Mail MCP: server running on stdio");
|
|
112
82
|
}
|
|
113
83
|
main().catch((err) => {
|
|
114
|
-
console.error("
|
|
84
|
+
console.error("Atomic Mail MCP: fatal error:", err);
|
|
115
85
|
process.exit(1);
|
|
116
86
|
});
|
|
@@ -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 {
|
|
3
|
-
export declare function registerJmapTool(server: McpServer, session:
|
|
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":"
|
|
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"}
|