@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 +21 -0
- package/README.md +128 -2
- package/dist/client.js +69 -0
- package/dist/index.js +12 -0
- package/dist/server.js +46 -0
- package/dist/tools.js +247 -0
- package/package.json +59 -4
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
|
-
#
|
|
1
|
+
# purelymail-mcp
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|