@atomicmail/mcp 0.1.0 → 0.2.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 +77 -187
- package/esm/_dnt.polyfills.d.ts +101 -0
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +127 -0
- package/esm/lib/agent/auth/agent-auth-http.d.ts +26 -0
- package/esm/lib/agent/auth/agent-auth-http.d.ts.map +1 -0
- package/esm/lib/agent/auth/agent-auth-http.js +76 -0
- package/esm/lib/agent/auth/agent-jwt.d.ts +14 -0
- package/esm/lib/agent/auth/agent-jwt.d.ts.map +1 -0
- package/esm/lib/agent/auth/agent-jwt.js +29 -0
- package/esm/lib/agent/auth/agent-pow.d.ts +5 -0
- package/esm/lib/agent/auth/agent-pow.d.ts.map +1 -0
- package/esm/lib/agent/auth/agent-pow.js +49 -0
- package/esm/lib/agent/jmap/agent-help-content.d.ts +4 -0
- package/esm/lib/agent/jmap/agent-help-content.d.ts.map +1 -0
- package/esm/lib/agent/jmap/agent-help-content.js +244 -0
- package/esm/lib/agent/jmap/agent-jmap.d.ts +49 -0
- package/esm/lib/agent/jmap/agent-jmap.d.ts.map +1 -0
- package/esm/lib/agent/jmap/agent-jmap.js +174 -0
- package/esm/lib/agent/jmap/agent-vars.d.ts +23 -0
- package/esm/lib/agent/jmap/agent-vars.d.ts.map +1 -0
- package/esm/lib/agent/jmap/agent-vars.js +65 -0
- package/esm/{mcp/src/credentials.d.ts → lib/agent/session/agent-credentials-store.d.ts} +3 -2
- package/esm/lib/agent/session/agent-credentials-store.d.ts.map +1 -0
- package/esm/{mcp/src/credentials.js → lib/agent/session/agent-credentials-store.js} +19 -16
- package/esm/lib/agent/session/agent-resolve-config.d.ts +24 -0
- package/esm/lib/agent/session/agent-resolve-config.d.ts.map +1 -0
- package/esm/lib/agent/session/agent-resolve-config.js +70 -0
- package/esm/lib/agent/session/agent-session.d.ts +62 -0
- package/esm/lib/agent/session/agent-session.d.ts.map +1 -0
- package/esm/lib/agent/session/agent-session.js +206 -0
- package/esm/lib/core/consts.d.ts.map +1 -0
- package/esm/lib/core/types.d.ts +2 -0
- package/esm/lib/core/types.d.ts.map +1 -0
- package/esm/lib/core/types.js +1 -0
- package/esm/lib/core/utils.d.ts +10 -0
- package/esm/lib/core/utils.d.ts.map +1 -0
- package/esm/lib/core/utils.js +28 -0
- package/esm/lib/mod.d.ts +14 -0
- package/esm/lib/mod.d.ts.map +1 -0
- package/esm/lib/mod.js +13 -0
- package/esm/lib/network/auth-client.d.ts +57 -0
- package/esm/lib/network/auth-client.d.ts.map +1 -0
- package/esm/lib/network/auth-client.js +188 -0
- package/esm/mcp/main.d.ts +3 -0
- package/esm/mcp/main.d.ts.map +1 -0
- package/esm/mcp/main.js +86 -0
- package/esm/mcp/tools/help.d.ts +3 -0
- package/esm/mcp/tools/help.d.ts.map +1 -0
- package/esm/mcp/tools/help.js +22 -0
- package/esm/mcp/{src/tools → tools}/jmap.d.ts +2 -2
- package/esm/mcp/tools/jmap.d.ts.map +1 -0
- package/esm/mcp/tools/jmap.js +115 -0
- package/esm/mcp/{src/tools → tools}/register.d.ts +2 -2
- package/esm/mcp/tools/register.d.ts.map +1 -0
- package/esm/mcp/tools/register.js +43 -0
- package/package.json +5 -5
- package/presets/list_inbox.json +39 -0
- package/presets/reply.json +75 -0
- package/presets/send_mail.json +42 -0
- package/esm/lib/src/consts.d.ts.map +0 -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/main.d.ts +0 -3
- package/esm/mcp/src/main.d.ts.map +0 -1
- package/esm/mcp/src/main.js +0 -116
- 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
- package/esm/mcp/src/tools/jmap.d.ts.map +0 -1
- package/esm/mcp/src/tools/jmap.js +0 -202
- package/esm/mcp/src/tools/register.d.ts.map +0 -1
- package/esm/mcp/src/tools/register.js +0 -79
- /package/esm/lib/{src → core}/consts.d.ts +0 -0
- /package/esm/lib/{src → core}/consts.js +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// auth-client
|
|
2
|
+
//
|
|
3
|
+
// Thin HTTP client for services/auth-service. Encapsulates the full PoW
|
|
4
|
+
// challenge → session → capability flow so callers (integration tests, the
|
|
5
|
+
// future agent skill, etc.) don't have to reimplement scrypt grinding.
|
|
6
|
+
//
|
|
7
|
+
// The PoW digest is scrypt-based and uses the SAME salt the auth-service
|
|
8
|
+
// uses on the verify path (see services/auth-service/src/crypto.ts). The
|
|
9
|
+
// client must therefore be configured with that salt — there is no public
|
|
10
|
+
// hash function here, the salt is part of the protocol.
|
|
11
|
+
import { scrypt } from "node:crypto";
|
|
12
|
+
import { DEFAULT_POW_SCRYPT_SALT_HEX } from "../core/consts.js";
|
|
13
|
+
// Mirror services/auth-service/src/crypto.ts exactly. Changing any of these
|
|
14
|
+
// constants on either side breaks PoW interop.
|
|
15
|
+
const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };
|
|
16
|
+
const POW_HASH_BYTES = 64;
|
|
17
|
+
/** Thrown for any non-2xx HTTP response or malformed payload. */
|
|
18
|
+
export class AuthClientError extends Error {
|
|
19
|
+
status;
|
|
20
|
+
bodyText;
|
|
21
|
+
constructor(status, bodyText, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "AuthClientError";
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.bodyText = bodyText;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class AuthClient {
|
|
29
|
+
baseUrl;
|
|
30
|
+
scryptSaltHex;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
33
|
+
this.scryptSaltHex = options.scryptSaltHex ?? DEFAULT_POW_SCRYPT_SALT_HEX;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Register a new inbox under `username`. Returns the freshly minted API key
|
|
37
|
+
* (the server only ever returns it once — the caller MUST persist it) and
|
|
38
|
+
* a session JWT.
|
|
39
|
+
*/
|
|
40
|
+
async signup(username) {
|
|
41
|
+
const { challengeJWT, challenge, difficulty } = await this.fetchChallenge();
|
|
42
|
+
const { powHex, nonce } = await this.solvePoW(challenge, difficulty);
|
|
43
|
+
const data = await this.postSession({
|
|
44
|
+
challengeJWT,
|
|
45
|
+
powHex,
|
|
46
|
+
nonce: nonce.toString(),
|
|
47
|
+
username,
|
|
48
|
+
});
|
|
49
|
+
if (typeof data.apiKey !== "string" ||
|
|
50
|
+
typeof data.sessionJWT !== "string") {
|
|
51
|
+
throw new AuthClientError(200, JSON.stringify(data), "Signup response missing apiKey or sessionJWT.");
|
|
52
|
+
}
|
|
53
|
+
return { apiKey: data.apiKey, sessionJWT: data.sessionJWT };
|
|
54
|
+
}
|
|
55
|
+
/** Exchange an existing API key for a fresh session JWT. */
|
|
56
|
+
async login(apiKey) {
|
|
57
|
+
const { challengeJWT, challenge, difficulty } = await this.fetchChallenge();
|
|
58
|
+
const { powHex, nonce } = await this.solvePoW(challenge, difficulty);
|
|
59
|
+
const data = await this.postSession({
|
|
60
|
+
challengeJWT,
|
|
61
|
+
powHex,
|
|
62
|
+
nonce: nonce.toString(),
|
|
63
|
+
apiKey,
|
|
64
|
+
});
|
|
65
|
+
if (typeof data.sessionJWT !== "string") {
|
|
66
|
+
throw new AuthClientError(200, JSON.stringify(data), "Login response missing sessionJWT.");
|
|
67
|
+
}
|
|
68
|
+
return { sessionJWT: data.sessionJWT };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Exchange a session JWT for a short-lived capability JWT (audience:
|
|
72
|
+
* api-service).
|
|
73
|
+
*/
|
|
74
|
+
async renew(sessionJWT) {
|
|
75
|
+
const res = await fetch(`${this.baseUrl}/api/v1/capability`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { Authorization: `Bearer ${sessionJWT}` },
|
|
78
|
+
});
|
|
79
|
+
const data = await this.parseJsonOrThrow(res, "capability");
|
|
80
|
+
if (typeof data.capabilityJWT !== "string") {
|
|
81
|
+
throw new AuthClientError(res.status, JSON.stringify(data), "Capability response missing capabilityJWT.");
|
|
82
|
+
}
|
|
83
|
+
return { capabilityJWT: data.capabilityJWT };
|
|
84
|
+
}
|
|
85
|
+
async fetchChallenge() {
|
|
86
|
+
const res = await fetch(`${this.baseUrl}/api/v1/challenge`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
});
|
|
89
|
+
const data = await this.parseJsonOrThrow(res, "challenge");
|
|
90
|
+
if (typeof data.challengeJWT !== "string") {
|
|
91
|
+
throw new AuthClientError(res.status, JSON.stringify(data), "Challenge response missing challengeJWT.");
|
|
92
|
+
}
|
|
93
|
+
const payload = decodeJwtPayload(data.challengeJWT);
|
|
94
|
+
if (typeof payload.jti !== "string" ||
|
|
95
|
+
typeof payload.difficulty !== "number") {
|
|
96
|
+
throw new AuthClientError(res.status, data.challengeJWT, "Challenge JWT payload is malformed (missing jti or difficulty).");
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
challengeJWT: data.challengeJWT,
|
|
100
|
+
challenge: payload.jti,
|
|
101
|
+
difficulty: payload.difficulty,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async postSession(body) {
|
|
105
|
+
const res = await fetch(`${this.baseUrl}/api/v1/session`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
});
|
|
110
|
+
return await this.parseJsonOrThrow(res, "session");
|
|
111
|
+
}
|
|
112
|
+
async parseJsonOrThrow(res, endpoint) {
|
|
113
|
+
const text = await res.text();
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
throw new AuthClientError(res.status, text, `auth-service ${endpoint} returned ${res.status}: ${text}`);
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(text);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new AuthClientError(res.status, text, `auth-service ${endpoint} returned non-JSON body.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Brute-force a PoW nonce. Mirrors `generatePow` in
|
|
126
|
+
* services/auth-service/src/crypto.ts: scrypt(`${challenge}:${nonce}`, salt,
|
|
127
|
+
* 64) until `difficulty` leading bits of the digest are zero.
|
|
128
|
+
*
|
|
129
|
+
* Expected work at the server's POW_DIFFICULTY=6 is ~2^6 = 64 attempts; well
|
|
130
|
+
* within the challenge JWT's 3-minute TTL.
|
|
131
|
+
*/
|
|
132
|
+
async solvePoW(challenge, difficulty) {
|
|
133
|
+
let nonce = 0n;
|
|
134
|
+
while (true) {
|
|
135
|
+
const digest = await scryptHash(`${challenge}:${nonce}`, this.scryptSaltHex);
|
|
136
|
+
if (hasLeadingZeroBits(digest, difficulty)) {
|
|
137
|
+
return { powHex: bytesToHex(digest), nonce };
|
|
138
|
+
}
|
|
139
|
+
nonce++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function scryptHash(data, salt) {
|
|
144
|
+
const bytes = new TextEncoder().encode(data);
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
scrypt(bytes, salt, POW_HASH_BYTES, SCRYPT_PARAMS, (err, derived) => {
|
|
147
|
+
if (err)
|
|
148
|
+
return reject(err);
|
|
149
|
+
resolve(new Uint8Array(derived));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function hasLeadingZeroBits(hash, bits) {
|
|
154
|
+
if (bits > hash.length * 8)
|
|
155
|
+
return false;
|
|
156
|
+
const fullBytes = Math.floor(bits / 8);
|
|
157
|
+
const remainingBits = bits % 8;
|
|
158
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
159
|
+
if (hash[i] !== 0)
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (remainingBits > 0) {
|
|
163
|
+
const mask = (0xff << (8 - remainingBits)) & 0xff;
|
|
164
|
+
if ((hash[fullBytes] & mask) !== 0)
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
function bytesToHex(bytes) {
|
|
170
|
+
let hex = "";
|
|
171
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
172
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
173
|
+
}
|
|
174
|
+
return hex;
|
|
175
|
+
}
|
|
176
|
+
function decodeJwtPayload(jwt) {
|
|
177
|
+
const parts = jwt.split(".");
|
|
178
|
+
if (parts.length < 2) {
|
|
179
|
+
throw new Error("Malformed JWT: expected at least 2 dot-separated segments.");
|
|
180
|
+
}
|
|
181
|
+
const payloadB64Url = parts[1];
|
|
182
|
+
const padLen = (4 - (payloadB64Url.length % 4)) % 4;
|
|
183
|
+
const base64 = payloadB64Url
|
|
184
|
+
.replace(/-/g, "+")
|
|
185
|
+
.replace(/_/g, "/")
|
|
186
|
+
.padEnd(payloadB64Url.length + padLen, "=");
|
|
187
|
+
return JSON.parse(atob(base64));
|
|
188
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/mcp/main.ts"],"names":[],"mappings":";AAMA,OAAO,sBAAsB,CAAC"}
|
package/esm/mcp/main.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// AtomicMail MCP server — stdio, PoW auth, JMAP (register / jmap_request / help).
|
|
3
|
+
//
|
|
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.
|
|
7
|
+
import "../_dnt.polyfills.js";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { AgentSession, resolveAgentConfigFromEnv } from "../lib/mod.js";
|
|
12
|
+
import { registerHelpTool } from "./tools/help.js";
|
|
13
|
+
import { registerJmapTool } from "./tools/jmap.js";
|
|
14
|
+
import { registerRegisterTool } from "./tools/register.js";
|
|
15
|
+
const VERSION = "0.1.0";
|
|
16
|
+
const INSTRUCTIONS = `\
|
|
17
|
+
Atomic Mail MCP — programmable inbox for AI agents.
|
|
18
|
+
|
|
19
|
+
WORKFLOW
|
|
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).
|
|
24
|
+
|
|
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.
|
|
28
|
+
|
|
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
|
|
35
|
+
|
|
36
|
+
SECURITY
|
|
37
|
+
credentials.json contains your apiKey — treat it as a secret (mode 0600).`;
|
|
38
|
+
async function main() {
|
|
39
|
+
let config;
|
|
40
|
+
try {
|
|
41
|
+
config = await resolveAgentConfigFromEnv();
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error("Atomic Mail MCP: configuration error:", err instanceof Error ? err.message : err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
console.error(`Atomic Mail MCP v${VERSION}: credential dir '${config.credentialDir}' ` +
|
|
48
|
+
`(config source: ${config.source}).`);
|
|
49
|
+
if (config.apiKey) {
|
|
50
|
+
console.error(`Atomic Mail MCP: API key configured${config.inboxId ? ` (inbox ${config.inboxId})` : ""}.`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.error("Atomic Mail MCP: no API key — call register to create an account.");
|
|
54
|
+
}
|
|
55
|
+
const session = await AgentSession.create({
|
|
56
|
+
authUrl: config.authUrl,
|
|
57
|
+
apiUrl: config.apiUrl,
|
|
58
|
+
scryptSalt: config.scryptSalt,
|
|
59
|
+
apiKey: config.apiKey,
|
|
60
|
+
inboxId: config.inboxId,
|
|
61
|
+
credentialDir: config.credentialDir,
|
|
62
|
+
files: config.files,
|
|
63
|
+
});
|
|
64
|
+
const server = new McpServer({ name: "atomicmail", version: VERSION }, { instructions: INSTRUCTIONS });
|
|
65
|
+
registerRegisterTool(server, session);
|
|
66
|
+
registerJmapTool(server, session);
|
|
67
|
+
registerHelpTool(server);
|
|
68
|
+
const cleanup = () => {
|
|
69
|
+
session.destroy();
|
|
70
|
+
};
|
|
71
|
+
process.on("SIGINT", () => {
|
|
72
|
+
cleanup();
|
|
73
|
+
process.exit(0);
|
|
74
|
+
});
|
|
75
|
+
process.on("SIGTERM", () => {
|
|
76
|
+
cleanup();
|
|
77
|
+
process.exit(0);
|
|
78
|
+
});
|
|
79
|
+
const transport = new StdioServerTransport();
|
|
80
|
+
await server.connect(transport);
|
|
81
|
+
console.error("Atomic Mail MCP: server running on stdio");
|
|
82
|
+
}
|
|
83
|
+
main().catch((err) => {
|
|
84
|
+
console.error("Atomic Mail MCP: fatal error:", err);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/help.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAItE,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/mod.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/mod.js";
|
|
3
|
+
export declare function registerJmapTool(server: McpServer, session: AgentSession): void;
|
|
4
4
|
//# sourceMappingURL=jmap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jmap.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/jmap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAEtE,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,kBAAkB,CAAC;AAE1B,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,YAAY,GACpB,IAAI,CAmIN"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DEFAULT_JMAP_USING, readOpsFile, runJmapRequest, } from "../../lib/mod.js";
|
|
3
|
+
export function registerJmapTool(server, session) {
|
|
4
|
+
server.registerTool("jmap_request", {
|
|
5
|
+
title: "Send a JMAP request",
|
|
6
|
+
description: "Send a JMAP method-call batch. Auth and JWT rotation are automatic. " +
|
|
7
|
+
"Provide exactly one of: `ops` (JSON string — methodCalls array or full " +
|
|
8
|
+
"envelope) or `ops_file` (preset path; relative paths resolve against " +
|
|
9
|
+
"the credential directory). Tokens `$VAR_NAME` (uppercase `$FOO_BAR`) in " +
|
|
10
|
+
"either input are replaced: `$ACCOUNT_ID` and `$INBOX` come from the JMAP " +
|
|
11
|
+
"session; pass other names via `vars`.",
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
using: z
|
|
14
|
+
.array(z.string())
|
|
15
|
+
.default([...DEFAULT_JMAP_USING])
|
|
16
|
+
.describe("JMAP capability URNs when `ops` omits `using`. Ignored if the " +
|
|
17
|
+
"JSON body already sets `using`."),
|
|
18
|
+
ops: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Inline JSON: methodCalls array or { using, methodCalls }. " +
|
|
22
|
+
"Mutually exclusive with ops_file."),
|
|
23
|
+
ops_file: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Path to a preset JSON file. Mutually exclusive with ops."),
|
|
27
|
+
vars: z
|
|
28
|
+
.record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), z.string())
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Map of placeholder names (no `$`) to string values, e.g. " +
|
|
31
|
+
'{ "TO": "a@b.com", "SUBJECT": "Hi" } for `$TO` and `$SUBJECT` in ' +
|
|
32
|
+
"ops or ops_file. Overrides session values for `ACCOUNT_ID` / `INBOX` if set."),
|
|
33
|
+
}),
|
|
34
|
+
}, async ({ using, ops, ops_file, vars }) => {
|
|
35
|
+
try {
|
|
36
|
+
if (ops && ops_file) {
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: "ops and ops_file are mutually exclusive — provide one.",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!ops && !ops_file) {
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: "Provide either ops or ops_file.",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
isError: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
let raw;
|
|
59
|
+
let sourceLabel;
|
|
60
|
+
if (ops_file) {
|
|
61
|
+
try {
|
|
62
|
+
raw = await readOpsFile(session.credentialDir, ops_file);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: `Could not read ops_file: ${err.message}`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
sourceLabel = `ops_file '${ops_file}'`;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
raw = ops;
|
|
79
|
+
sourceLabel = "ops";
|
|
80
|
+
}
|
|
81
|
+
const { ok, status, bodyText } = await runJmapRequest({
|
|
82
|
+
session,
|
|
83
|
+
opsJson: raw,
|
|
84
|
+
defaultUsing: using,
|
|
85
|
+
sourceLabel,
|
|
86
|
+
vars,
|
|
87
|
+
});
|
|
88
|
+
if (!ok) {
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: "text",
|
|
93
|
+
text: `JMAP request failed (HTTP ${status}): ${bodyText}`,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: bodyText }],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: `JMAP request error: ${error instanceof Error ? error.message : String(error)}`,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
|
|
2
|
-
import type {
|
|
3
|
-
export declare function registerRegisterTool(server: McpServer, session:
|
|
2
|
+
import type { AgentSession } from "../../lib/mod.js";
|
|
3
|
+
export declare function registerRegisterTool(server: McpServer, session: AgentSession): void;
|
|
4
4
|
//# sourceMappingURL=register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/register.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAEtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,YAAY,GACpB,IAAI,CAiDN"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerRegisterTool(server, session) {
|
|
3
|
+
server.registerTool("register", {
|
|
4
|
+
title: "Register an Atomic Mail inbox",
|
|
5
|
+
description: "Proof-of-work signup; persists credentials. Idempotent when the same " +
|
|
6
|
+
"username matches the inbox already stored. A different username " +
|
|
7
|
+
"replaces credentials and creates a new inbox. Returns JSON with " +
|
|
8
|
+
"inbox, accountId, and apiKey on first signup only.",
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
username: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.describe("Desired username (local-part of your @atomicmail.ai address)."),
|
|
14
|
+
}),
|
|
15
|
+
annotations: {
|
|
16
|
+
idempotentHint: true,
|
|
17
|
+
destructiveHint: false,
|
|
18
|
+
},
|
|
19
|
+
}, async ({ username }) => {
|
|
20
|
+
try {
|
|
21
|
+
const result = await session.register(username);
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: JSON.stringify(result, null, 2),
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "text",
|
|
36
|
+
text: `Registration failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atomicmail/mcp",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Atomic Mail MCP server — local stdio proxy with PoW auth and JMAP, for AI agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atomic-mail",
|
|
7
7
|
"atomicmail",
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
],
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/atomic-mail/agentic-
|
|
19
|
+
"url": "git+https://github.com/atomic-mail/agentic-clients.git"
|
|
20
20
|
},
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"bugs": {
|
|
23
|
-
"url": "https://github.com/atomic-mail/agentic-
|
|
23
|
+
"url": "https://github.com/atomic-mail/agentic-clients/issues"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {},
|
|
26
26
|
"bin": {
|
|
27
|
-
"atomicmail-mcp": "./esm/mcp/
|
|
27
|
+
"atomicmail-mcp": "./esm/mcp/main.js"
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": ">=20"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"using": [
|
|
3
|
+
"urn:ietf:params:jmap:core",
|
|
4
|
+
"urn:ietf:params:jmap:mail"
|
|
5
|
+
],
|
|
6
|
+
"methodCalls": [
|
|
7
|
+
[
|
|
8
|
+
"Email/query",
|
|
9
|
+
{
|
|
10
|
+
"accountId": "$ACCOUNT_ID",
|
|
11
|
+
"filter": { "inMailbox": "$INBOX" },
|
|
12
|
+
"sort": [{ "property": "receivedAt", "isAscending": false }],
|
|
13
|
+
"limit": "$COUNT"
|
|
14
|
+
},
|
|
15
|
+
"q0"
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
"Email/get",
|
|
19
|
+
{
|
|
20
|
+
"accountId": "$ACCOUNT_ID",
|
|
21
|
+
"#ids": {
|
|
22
|
+
"resultOf": "q0",
|
|
23
|
+
"name": "Email/query",
|
|
24
|
+
"path": "/ids"
|
|
25
|
+
},
|
|
26
|
+
"properties": [
|
|
27
|
+
"id",
|
|
28
|
+
"threadId",
|
|
29
|
+
"receivedAt",
|
|
30
|
+
"from",
|
|
31
|
+
"to",
|
|
32
|
+
"subject",
|
|
33
|
+
"preview"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"g0"
|
|
37
|
+
]
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"using": [
|
|
3
|
+
"urn:ietf:params:jmap:core",
|
|
4
|
+
"urn:ietf:params:jmap:mail",
|
|
5
|
+
"urn:ietf:params:jmap:submission"
|
|
6
|
+
],
|
|
7
|
+
"methodCalls": [
|
|
8
|
+
[
|
|
9
|
+
"Email/get",
|
|
10
|
+
{
|
|
11
|
+
"accountId": "$ACCOUNT_ID",
|
|
12
|
+
"ids": ["$MAIL_ID"],
|
|
13
|
+
"properties": [
|
|
14
|
+
"id",
|
|
15
|
+
"threadId",
|
|
16
|
+
"from",
|
|
17
|
+
"replyTo",
|
|
18
|
+
"subject",
|
|
19
|
+
"messageId"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"g0"
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
"Email/set",
|
|
26
|
+
{
|
|
27
|
+
"accountId": "$ACCOUNT_ID",
|
|
28
|
+
"create": {
|
|
29
|
+
"d1": {
|
|
30
|
+
"from": [{ "email": "$INBOX" }],
|
|
31
|
+
"#to": {
|
|
32
|
+
"resultOf": "g0",
|
|
33
|
+
"name": "Email/get",
|
|
34
|
+
"path": "/list/0/replyTo"
|
|
35
|
+
},
|
|
36
|
+
"#subject": {
|
|
37
|
+
"resultOf": "g0",
|
|
38
|
+
"name": "Email/get",
|
|
39
|
+
"path": "/list/0/subject"
|
|
40
|
+
},
|
|
41
|
+
"#inReplyTo": {
|
|
42
|
+
"resultOf": "g0",
|
|
43
|
+
"name": "Email/get",
|
|
44
|
+
"path": "/list/0/messageId"
|
|
45
|
+
},
|
|
46
|
+
"textBody": [{ "partId": "b", "type": "text/plain" }],
|
|
47
|
+
"bodyValues": { "b": { "value": "$BODY" } },
|
|
48
|
+
"keywords": { "$draft": true }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"c0"
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"EmailSubmission/set",
|
|
56
|
+
{
|
|
57
|
+
"accountId": "$ACCOUNT_ID",
|
|
58
|
+
"create": {
|
|
59
|
+
"s1": {
|
|
60
|
+
"emailId": "#d1",
|
|
61
|
+
"envelope": {
|
|
62
|
+
"mailFrom": { "email": "$INBOX" },
|
|
63
|
+
"#rcptTo": {
|
|
64
|
+
"resultOf": "g0",
|
|
65
|
+
"name": "Email/get",
|
|
66
|
+
"path": "/list/0/replyTo"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"c1"
|
|
73
|
+
]
|
|
74
|
+
]
|
|
75
|
+
}
|