@amplia-labs/purelymail-mcp 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amplia Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,129 @@
1
- # Temporary Holding Version
1
+ # purelymail-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ MCP server for the [Purelymail API](https://news.purelymail.com/api/index.html).
4
+ It covers all 19 API operations: users, password reset methods, app passwords,
5
+ routing rules, domains and account credit.
6
+
7
+ ## Usage
8
+
9
+ Requires Node.js 20 or later. There is nothing to install: MCP clients run the
10
+ package through `npx`.
11
+
12
+ | Variable | Required | Description |
13
+ | ---------------------- | -------- | ----------------------------------------------- |
14
+ | `PURELYMAIL_API_TOKEN` | yes | API token from your Purelymail account settings |
15
+ | `PURELYMAIL_READ_ONLY` | no | `true` or `1` exposes only the 7 read tools |
16
+
17
+ ### Claude Code
18
+
19
+ ```bash
20
+ claude mcp add purelymail \
21
+ -e PURELYMAIL_API_TOKEN=your-token \
22
+ -- npx -y @amplia-labs/purelymail-mcp
23
+ ```
24
+
25
+ ### Other MCP clients
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "purelymail": {
31
+ "command": "npx",
32
+ "args": ["-y", "@amplia-labs/purelymail-mcp"],
33
+ "env": { "PURELYMAIL_API_TOKEN": "your-token" }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## Tools
40
+
41
+ | Area | Read | Write | Destructive |
42
+ | -------------- | ------------------------------------ | -------------------------------------- | -------------------------------------------------------------- |
43
+ | Users | `list_users`, `get_user` | `create_user` | `modify_user`, `delete_user` |
44
+ | Password reset | `list_password_reset_methods` | | `upsert_password_reset_method`, `delete_password_reset_method` |
45
+ | App passwords | | `create_app_password` | `delete_app_password` |
46
+ | Routing | `list_routing_rules` | `create_routing_rule` | `delete_routing_rule` |
47
+ | Domains | `list_domains`, `get_ownership_code` | `add_domain`, `update_domain_settings` | `delete_domain` |
48
+ | Billing | `check_account_credit` | | |
49
+
50
+ Tools carry the MCP annotations `readOnlyHint` and `destructiveHint`, which
51
+ clients can use to ask for confirmation.
52
+
53
+ ## Security
54
+
55
+ - The token grants administrative access to the whole account. Use
56
+ `PURELYMAIL_READ_ONLY=true` when you only need to look things up.
57
+ - `delete_domain` deletes the domain **and all of its users**.
58
+ - Passwords given to `create_user` and `modify_user`, and those returned by
59
+ `create_app_password`, pass through the model's context and the MCP client's
60
+ conversation history. Rotate any that are sensitive afterwards.
61
+
62
+ ## API behavior
63
+
64
+ - Errors come back as **HTTP 200** with
65
+ `{"type":"error","code":...,"message":...}`, which the spec does not document.
66
+ The client inspects the response body and reports these as `isError`.
67
+ - `list_password_reset_methods` returns the methods under a key named `users`.
68
+ That name comes from the API itself.
69
+ - `delete_password_reset_method` requires `target`, even though the spec marks it
70
+ optional, because the spec does not say what happens without it.
71
+ - Requests are never retried automatically. After a timeout, check the current
72
+ state before repeating a write.
73
+
74
+ ## Development
75
+
76
+ ```bash
77
+ git clone https://github.com/amplia-labs/purelymail-mcp.git
78
+ cd purelymail-mcp
79
+ npm install
80
+ npm run build
81
+ ```
82
+
83
+ To run a local build from an MCP client, use
84
+ `node /path/to/purelymail-mcp/dist/index.js` as the command.
85
+
86
+ ### Quality checks
87
+
88
+ ```bash
89
+ npm run check
90
+ ```
91
+
92
+ Runs these in order, stopping at the first failure:
93
+
94
+ | Command | Tool | What it checks |
95
+ | ---------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
96
+ | `npx tsc --noEmit` | TypeScript | Types |
97
+ | `npm run lint` | [oxlint](https://oxc.rs/docs/guide/usage/linter.html) | `correctness`, `suspicious` and `perf` rules, with type-aware analysis; warnings also fail |
98
+ | `npm run format:check` | [oxfmt](https://oxc.rs/docs/guide/usage/formatter.html) | Formatting |
99
+ | `npm run knip` | [knip](https://knip.dev) | Unused exports, files and dependencies |
100
+ | `npm run cpd` | [jscpd](https://github.com/kucherenko/jscpd) | Duplicated code in `src` and `test`, with zero tolerance |
101
+ | `npm test` | `node:test` | Build and tests |
102
+
103
+ To fix issues automatically, run `npm run lint:fix` and `npm run format`.
104
+
105
+ The tests use a fake HTTP server and never call the real API. The development
106
+ tools require Node.js 20.19 or later.
107
+
108
+ ### Releasing
109
+
110
+ Releases are published to npm by the
111
+ [publish workflow](.github/workflows/publish.yml), using
112
+ [trusted publishing](https://docs.npmjs.com/trusted-publishers). No npm token is
113
+ stored in the repository.
114
+
115
+ 1. Update the version in `package.json` and in both `version` fields of
116
+ `server.json`. A test fails if they disagree.
117
+ 2. Merge the change to `main`.
118
+ 3. Tag that commit and push the tag. The tag must match the version:
119
+
120
+ ```bash
121
+ git tag v1.2.3
122
+ git push origin v1.2.3
123
+ ```
124
+
125
+ The workflow runs `npm run check` before uploading anything.
126
+
127
+ ## License
128
+
129
+ [MIT](LICENSE)
package/dist/client.js ADDED
@@ -0,0 +1,69 @@
1
+ const DEFAULT_BASE_URL = "https://purelymail.com";
2
+ const DEFAULT_TIMEOUT_MS = 30_000;
3
+ /** An error reported by the Purelymail API itself, e.g. `invalidToken`. */
4
+ export class PurelymailApiError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.name = "PurelymailApiError";
9
+ this.code = code;
10
+ }
11
+ }
12
+ export class PurelymailClient {
13
+ #token;
14
+ #baseUrl;
15
+ #timeoutMs;
16
+ constructor(options) {
17
+ this.#token = options.token;
18
+ this.#baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
19
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
20
+ }
21
+ /** POSTs to an API path and returns the unwrapped `result` payload. */
22
+ async call(path, body) {
23
+ let response;
24
+ try {
25
+ response = await fetch(new URL(path, this.#baseUrl), {
26
+ method: "POST",
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ "Purelymail-Api-Token": this.#token,
30
+ },
31
+ body: JSON.stringify(body),
32
+ // Never replay the token (or a write) against another location.
33
+ redirect: "error",
34
+ signal: AbortSignal.timeout(this.#timeoutMs),
35
+ });
36
+ }
37
+ catch (error) {
38
+ if (error instanceof Error && error.name === "TimeoutError") {
39
+ throw new Error(`Request to ${path} timed out after ${this.#timeoutMs}ms. ` +
40
+ "A write may or may not have been applied; check before retrying.", { cause: error });
41
+ }
42
+ throw new Error(`Request to ${path} failed: ${String(error)}`, {
43
+ cause: error,
44
+ });
45
+ }
46
+ const text = await response.text();
47
+ let payload;
48
+ try {
49
+ payload = JSON.parse(text);
50
+ }
51
+ catch {
52
+ throw new Error(`Non-JSON response from ${path} (HTTP ${response.status}): ${text.slice(0, 200)}`);
53
+ }
54
+ // The API reports failures as HTTP 200 with `type: "error"` in the body,
55
+ // so the status code alone says nothing about success.
56
+ if (isRecord(payload)) {
57
+ if (payload.type === "error") {
58
+ throw new PurelymailApiError(typeof payload.code === "string" ? payload.code : "unknown", typeof payload.message === "string" ? payload.message : "Unknown error");
59
+ }
60
+ if (response.ok && "result" in payload) {
61
+ return payload.result;
62
+ }
63
+ }
64
+ throw new Error(`Unexpected response from ${path} (HTTP ${response.status}): ${text.slice(0, 200)}`);
65
+ }
66
+ }
67
+ function isRecord(value) {
68
+ return typeof value === "object" && value !== null;
69
+ }
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { PurelymailClient } from "./client.js";
4
+ import { createServer } from "./server.js";
5
+ const token = process.env.PURELYMAIL_API_TOKEN?.trim();
6
+ if (!token) {
7
+ console.error("PURELYMAIL_API_TOKEN is not set. Get your API token from your Purelymail account settings.");
8
+ process.exit(1);
9
+ }
10
+ const readOnly = ["1", "true"].includes(process.env.PURELYMAIL_READ_ONLY?.trim().toLowerCase() ?? "");
11
+ const server = createServer({ client: new PurelymailClient({ token }), readOnly });
12
+ await server.connect(new StdioServerTransport());
package/dist/server.js ADDED
@@ -0,0 +1,46 @@
1
+ import { createRequire } from "node:module";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { PurelymailApiError } from "./client.js";
4
+ import { tools } from "./tools.js";
5
+ const packageJson = createRequire(import.meta.url)("../package.json");
6
+ export function createServer({ client, readOnly = false }) {
7
+ const server = new McpServer({ name: "purelymail", version: packageJson.version });
8
+ for (const tool of tools) {
9
+ if (readOnly && tool.kind !== "read")
10
+ continue;
11
+ server.registerTool(tool.name, {
12
+ title: tool.title,
13
+ description: tool.description,
14
+ inputSchema: tool.inputSchema,
15
+ annotations: annotationsFor(tool),
16
+ }, (args) => runTool(client, tool, args));
17
+ }
18
+ return server;
19
+ }
20
+ function annotationsFor(tool) {
21
+ return {
22
+ title: tool.title,
23
+ readOnlyHint: tool.kind === "read",
24
+ destructiveHint: tool.kind === "destructive",
25
+ openWorldHint: true,
26
+ };
27
+ }
28
+ async function runTool(client, tool, args) {
29
+ try {
30
+ const result = await client.call(tool.path, args);
31
+ return { content: [{ type: "text", text: formatResult(result) }] };
32
+ }
33
+ catch (error) {
34
+ const text = error instanceof PurelymailApiError
35
+ ? `Purelymail API error (${error.code}): ${error.message}`
36
+ : error instanceof Error
37
+ ? error.message
38
+ : String(error);
39
+ return { isError: true, content: [{ type: "text", text }] };
40
+ }
41
+ }
42
+ /** Write operations return `{}`, which reads as nothing having happened. */
43
+ function formatResult(result) {
44
+ const isEmpty = typeof result === "object" && result !== null && Object.keys(result).length === 0;
45
+ return JSON.stringify(isEmpty ? { success: true } : result, null, 2);
46
+ }
package/dist/tools.js ADDED
@@ -0,0 +1,247 @@
1
+ import { z } from "zod";
2
+ const fullUserName = z.string().min(1).describe('Full username, e.g. "user@domain.com"');
3
+ export const tools = [
4
+ // Users
5
+ {
6
+ name: "list_users",
7
+ title: "List users",
8
+ description: "Lists all users under the account (up to 1000).",
9
+ path: "/api/v0/listUser",
10
+ kind: "read",
11
+ inputSchema: {},
12
+ },
13
+ {
14
+ name: "get_user",
15
+ title: "Get user",
16
+ description: "Retrieves a user's settings: search indexing, recovery, 2FA requirement, spam filtering and password reset methods.",
17
+ path: "/api/v0/getUser",
18
+ kind: "read",
19
+ inputSchema: { userName: fullUserName },
20
+ },
21
+ {
22
+ name: "create_user",
23
+ title: "Create user",
24
+ description: "Creates a new mailbox user on a domain.",
25
+ path: "/api/v0/createUser",
26
+ kind: "write",
27
+ inputSchema: {
28
+ userName: z.string().min(1).describe('Local part only, e.g. "user" in "user@domain.com"'),
29
+ domainName: z.string().min(1),
30
+ password: z.string().min(1),
31
+ enablePasswordReset: z.boolean().optional().describe("Defaults to true"),
32
+ recoveryEmail: z.string().optional(),
33
+ recoveryEmailDescription: z.string().optional(),
34
+ recoveryPhone: z.string().optional(),
35
+ recoveryPhoneDescription: z.string().optional(),
36
+ enableSearchIndexing: z.boolean().optional().describe("Defaults to true"),
37
+ sendWelcomeEmail: z.boolean().optional().describe("Defaults to true"),
38
+ },
39
+ },
40
+ {
41
+ name: "modify_user",
42
+ title: "Modify user",
43
+ description: "Changes a user's address, password or settings. Only the fields provided are changed.",
44
+ path: "/api/v0/modifyUser",
45
+ kind: "destructive",
46
+ inputSchema: {
47
+ userName: fullUserName,
48
+ newUserName: z
49
+ .string()
50
+ .min(1)
51
+ .optional()
52
+ .describe('Full new username, e.g. "user@domain.com"'),
53
+ newPassword: z.string().min(1).optional(),
54
+ enableSearchIndexing: z
55
+ .boolean()
56
+ .optional()
57
+ .describe("Indexes may take some time to be created after enabling."),
58
+ enablePasswordReset: z
59
+ .boolean()
60
+ .optional()
61
+ .describe("Whether this user can have their password reset."),
62
+ requireTwoFactorAuthentication: z
63
+ .boolean()
64
+ .optional()
65
+ .describe("Whether this user requires 2FA for login."),
66
+ },
67
+ },
68
+ {
69
+ name: "delete_user",
70
+ title: "Delete user",
71
+ description: "Permanently deletes a user.",
72
+ path: "/api/v0/deleteUser",
73
+ kind: "destructive",
74
+ inputSchema: { userName: fullUserName },
75
+ },
76
+ // Password reset methods
77
+ {
78
+ name: "list_password_reset_methods",
79
+ title: "List password reset methods",
80
+ description: "Lists a user's password reset methods. The API returns them under a key named `users`.",
81
+ path: "/api/v0/listPasswordReset",
82
+ kind: "read",
83
+ inputSchema: { userName: fullUserName },
84
+ },
85
+ {
86
+ name: "upsert_password_reset_method",
87
+ title: "Create or update password reset method",
88
+ description: "Creates a password reset method, or updates an existing one when existingTarget is given. A reset method can take over the account, so make sure the target belongs to the user.",
89
+ path: "/api/v0/upsertPasswordReset",
90
+ kind: "destructive",
91
+ inputSchema: {
92
+ userName: fullUserName,
93
+ existingTarget: z
94
+ .string()
95
+ .optional()
96
+ .describe("Target from list_password_reset_methods. If provided, updates the method matching it instead of creating a new one."),
97
+ type: z.enum(["email", "phone"]),
98
+ target: z.string().min(1).describe("Email address or phone number"),
99
+ description: z.string().optional().describe("Human-readable description"),
100
+ allowMfaReset: z
101
+ .boolean()
102
+ .optional()
103
+ .describe("Whether this method can reset the user's MFA as well. Defaults to true."),
104
+ },
105
+ },
106
+ {
107
+ name: "delete_password_reset_method",
108
+ title: "Delete password reset method",
109
+ description: "Deletes one of a user's password reset methods.",
110
+ path: "/api/v0/deletePasswordReset",
111
+ kind: "destructive",
112
+ inputSchema: {
113
+ userName: fullUserName,
114
+ // Optional in the API spec, which does not say what omitting it does.
115
+ // Required here so a delete always names what it removes.
116
+ target: z.string().min(1).describe("Target from list_password_reset_methods"),
117
+ },
118
+ },
119
+ // App passwords
120
+ {
121
+ name: "create_app_password",
122
+ title: "Create app password",
123
+ description: "Creates an app password for a user and returns it. The result is a credential: show it to the user once and do not store or repeat it.",
124
+ path: "/api/v0/createAppPassword",
125
+ kind: "write",
126
+ inputSchema: {
127
+ userHandle: fullUserName,
128
+ name: z.string().optional().describe("Optional friendly description"),
129
+ },
130
+ },
131
+ {
132
+ name: "delete_app_password",
133
+ title: "Delete app password",
134
+ description: "Deletes an app password. Requires the full password value; the API has no way to list them.",
135
+ path: "/api/v0/deleteAppPassword",
136
+ kind: "destructive",
137
+ inputSchema: {
138
+ userName: fullUserName,
139
+ appPassword: z.string().min(1).describe("Full app password"),
140
+ },
141
+ },
142
+ // Routing
143
+ {
144
+ name: "list_routing_rules",
145
+ title: "List routing rules",
146
+ description: "Lists all routing rules active under the account.",
147
+ path: "/api/v0/listRoutingRules",
148
+ kind: "read",
149
+ inputSchema: {},
150
+ },
151
+ {
152
+ name: "create_routing_rule",
153
+ title: "Create routing rule",
154
+ description: "Creates a routing rule for a domain. It must not have the same user/prefix as another rule on that domain.",
155
+ path: "/api/v0/createRoutingRule",
156
+ kind: "write",
157
+ inputSchema: {
158
+ domainName: z.string().min(1),
159
+ prefix: z.boolean().describe("Whether matchUser is matched as a prefix of the local part"),
160
+ matchUser: z.string().describe('Local part to match, i.e. "user" in "user@domain.org"'),
161
+ targetAddresses: z
162
+ .array(z.string().min(1))
163
+ .min(1)
164
+ .describe("Full email addresses this mail will be rerouted to"),
165
+ catchall: z
166
+ .boolean()
167
+ .optional()
168
+ .describe("Whether this rule will not fire if the address maps to a user. Defaults to false."),
169
+ },
170
+ },
171
+ {
172
+ name: "delete_routing_rule",
173
+ title: "Delete routing rule",
174
+ description: "Deletes a routing rule.",
175
+ path: "/api/v0/deleteRoutingRule",
176
+ kind: "destructive",
177
+ inputSchema: {
178
+ routingRuleId: z.number().int().describe("Rule id from list_routing_rules"),
179
+ },
180
+ },
181
+ // Domains
182
+ {
183
+ name: "list_domains",
184
+ title: "List domains",
185
+ description: "Lists domains accessible to the account, with their settings and a DNS summary (MX, SPF, DKIM, DMARC).",
186
+ path: "/api/v0/listDomains",
187
+ kind: "read",
188
+ inputSchema: {
189
+ includeShared: z
190
+ .boolean()
191
+ .optional()
192
+ .describe("Whether to include Purelymail shared domains. Defaults to false."),
193
+ },
194
+ },
195
+ {
196
+ name: "get_ownership_code",
197
+ title: "Get ownership code",
198
+ description: "Gets the value of the DNS ownership proof record required before add_domain will succeed.",
199
+ path: "/api/v0/getOwnershipCode",
200
+ kind: "read",
201
+ inputSchema: {},
202
+ },
203
+ {
204
+ name: "add_domain",
205
+ title: "Add domain",
206
+ description: "Adds a domain, provided it passes DNS checks. See get_ownership_code.",
207
+ path: "/api/v0/addDomain",
208
+ kind: "write",
209
+ inputSchema: { domainName: z.string().min(1) },
210
+ },
211
+ {
212
+ name: "update_domain_settings",
213
+ title: "Update domain settings",
214
+ description: "Updates settings of an owned domain, and can trigger a DNS recheck. Only the fields provided are changed.",
215
+ path: "/api/v0/updateDomainSettings",
216
+ kind: "write",
217
+ inputSchema: {
218
+ name: z.string().min(1).describe("Domain name"),
219
+ allowAccountReset: z
220
+ .boolean()
221
+ .optional()
222
+ .describe("Whether this domain can be used to reset an account's password"),
223
+ symbolicSubaddressing: z
224
+ .boolean()
225
+ .optional()
226
+ .describe("Whether symbolic subaddressing is enabled on this domain"),
227
+ recheckDns: z.boolean().optional().describe("Defaults to false"),
228
+ },
229
+ },
230
+ {
231
+ name: "delete_domain",
232
+ title: "Delete domain",
233
+ description: "Permanently deletes a domain AND all of its dependent settings and users. Confirm with the user first, listing what will be lost.",
234
+ path: "/api/v0/deleteDomain",
235
+ kind: "destructive",
236
+ inputSchema: { name: z.string().min(1).describe("Domain name") },
237
+ },
238
+ // Billing
239
+ {
240
+ name: "check_account_credit",
241
+ title: "Check account credit",
242
+ description: "Returns the current account credit as a decimal string with up to 64 digits of precision.",
243
+ path: "/api/v0/checkAccountCredit",
244
+ kind: "read",
245
+ inputSchema: {},
246
+ },
247
+ ];
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@amplia-labs/purelymail-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the Purelymail API (users, domains, routing, app passwords, billing)",
5
+ "keywords": [
6
+ "email",
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "purelymail"
10
+ ],
11
+ "homepage": "https://github.com/amplia-labs/purelymail-mcp#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/amplia-labs/purelymail-mcp/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": "Amplia Labs",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/amplia-labs/purelymail-mcp.git"
20
+ },
21
+ "bin": {
22
+ "purelymail-mcp": "dist/index.js"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "start": "node dist/index.js",
34
+ "test": "npm run build && node --test",
35
+ "lint": "oxlint --deny-warnings",
36
+ "lint:fix": "oxlint --fix",
37
+ "format": "oxfmt",
38
+ "format:check": "oxfmt --check",
39
+ "knip": "knip",
40
+ "cpd": "jscpd",
41
+ "check": "tsc --noEmit && npm run lint && npm run format:check && npm run knip && npm run cpd && npm test",
42
+ "prepublishOnly": "npm run check"
43
+ },
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.32.2",
46
+ "zod": "^4.6.7"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^26.6.6",
50
+ "jscpd": "^5.4.2",
51
+ "knip": "^6.44.1",
52
+ "oxfmt": "^0.76.0",
53
+ "oxlint": "^1.91.0",
54
+ "oxlint-tsgolint": "^7.0.2006",
55
+ "typescript": "^7.0.2"
56
+ },
57
+ "engines": {
58
+ "node": ">=20"
59
+ },
60
+ "mcpName": "io.github.amplia-labs/purelymail-mcp"
61
+ }