apimail-mcp 1.0.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 +52 -0
- package/package.json +34 -0
- package/server.js +185 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# ApiMail MCP (`apimail-mcp`)
|
|
2
|
+
|
|
3
|
+
Stdio MCP server that proxies ApiMail HTTP API. No business logic duplication: pricing and purchases stay on the API.
|
|
4
|
+
|
|
5
|
+
## End users (any machine, no local repo path)
|
|
6
|
+
|
|
7
|
+
Add to Cursor / Claude Desktop:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"apimail": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "apimail-mcp@latest"],
|
|
15
|
+
"env": {
|
|
16
|
+
"APIMAIL_TOKEN": "YOUR_API_TOKEN",
|
|
17
|
+
"APIMAIL_BASE_URL": "https://api.apimail.me"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Same pattern as other MCP servers: `npx` downloads and runs the package; **no filesystem path** in the config.
|
|
25
|
+
|
|
26
|
+
## Publish (maintainers only)
|
|
27
|
+
|
|
28
|
+
From this directory:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm login
|
|
32
|
+
npm publish
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Bump `version` in `package.json` for each release (`npm version patch`).
|
|
36
|
+
|
|
37
|
+
## Environment variables
|
|
38
|
+
|
|
39
|
+
- `APIMAIL_TOKEN` — required for authenticated tools.
|
|
40
|
+
- `APIMAIL_BASE_URL` — default `https://api.apimail.me`.
|
|
41
|
+
- `APIMAIL_TIMEOUT_MS` — HTTP timeout ms, default `20000`.
|
|
42
|
+
|
|
43
|
+
## Tools
|
|
44
|
+
|
|
45
|
+
- `apimail_user_balance`
|
|
46
|
+
- `apimail_domain_availability`
|
|
47
|
+
- `apimail_purchase_accounts`
|
|
48
|
+
- `apimail_purchased_emails`
|
|
49
|
+
|
|
50
|
+
## Security
|
|
51
|
+
|
|
52
|
+
Do not commit real API tokens. Use env in the MCP client only.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "apimail-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for ApiMail API (stdio, Model Context Protocol)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"server.js"
|
|
9
|
+
],
|
|
10
|
+
"bin": {
|
|
11
|
+
"apimail-mcp": "./server.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node server.js",
|
|
15
|
+
"prepublishOnly": "node --check server.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"apimail",
|
|
21
|
+
"stdio"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.17.5"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"license": "UNLICENSED",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/apimail/mcp"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
|
|
6
|
+
const API_BASE_URL = (process.env.APIMAIL_BASE_URL || "https://api.apimail.me").replace(/\/+$/, "");
|
|
7
|
+
const APIMAIL_TOKEN = process.env.APIMAIL_TOKEN || "";
|
|
8
|
+
const REQUEST_TIMEOUT_MS = Number(process.env.APIMAIL_TIMEOUT_MS || 20000);
|
|
9
|
+
|
|
10
|
+
const TOOL_DEFS = [
|
|
11
|
+
{
|
|
12
|
+
name: "apimail_user_balance",
|
|
13
|
+
description: "Get current user balance from ApiMail.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
token: { type: "string", description: "Optional API token override." }
|
|
18
|
+
},
|
|
19
|
+
additionalProperties: false
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "apimail_domain_availability",
|
|
24
|
+
description: "Get public domain availability with optional filters.",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
domain: { type: "string", description: "Optional domain filter, e.g. outlook.com." },
|
|
29
|
+
age: { type: "string", enum: ["fresh", "medium", "aged", "old"], description: "Optional age filter." }
|
|
30
|
+
},
|
|
31
|
+
additionalProperties: false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "apimail_purchase_accounts",
|
|
36
|
+
description: "Purchase Outlook accounts via ApiMail getEmail endpoint.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
token: { type: "string", description: "Optional API token override." },
|
|
41
|
+
count: { type: "integer", minimum: 1, maximum: 500, default: 1 },
|
|
42
|
+
domain: { type: "string", description: "Optional exact domain filter." },
|
|
43
|
+
age: { type: "string", enum: ["fresh", "medium", "aged", "old"], description: "Optional age tier." }
|
|
44
|
+
},
|
|
45
|
+
additionalProperties: false
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "apimail_purchased_emails",
|
|
50
|
+
description: "Fetch purchased emails by payment_id within retention period.",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
token: { type: "string", description: "Optional API token override." },
|
|
55
|
+
payment_id: { type: "integer", minimum: 1 }
|
|
56
|
+
},
|
|
57
|
+
required: ["payment_id"],
|
|
58
|
+
additionalProperties: false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function asText(text, isError = false) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text }],
|
|
66
|
+
isError
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getTokenFromArgs(args) {
|
|
71
|
+
const local = args && typeof args.token === "string" ? args.token.trim() : "";
|
|
72
|
+
const token = local || APIMAIL_TOKEN;
|
|
73
|
+
if (!token) {
|
|
74
|
+
throw new Error("ApiMail token is required. Set APIMAIL_TOKEN env or pass token argument.");
|
|
75
|
+
}
|
|
76
|
+
return token;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toQuery(params) {
|
|
80
|
+
const query = new URLSearchParams();
|
|
81
|
+
for (const [key, val] of Object.entries(params)) {
|
|
82
|
+
if (val === null || val === undefined || val === "") continue;
|
|
83
|
+
query.set(key, String(val));
|
|
84
|
+
}
|
|
85
|
+
return query.toString();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function callApi(endpoint, params) {
|
|
89
|
+
const query = toQuery(params);
|
|
90
|
+
const url = `${API_BASE_URL}/${endpoint}${query ? `?${query}` : ""}`;
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(url, {
|
|
96
|
+
method: "GET",
|
|
97
|
+
headers: { Accept: "application/json" },
|
|
98
|
+
signal: controller.signal
|
|
99
|
+
});
|
|
100
|
+
const text = await response.text();
|
|
101
|
+
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(text);
|
|
105
|
+
} catch {
|
|
106
|
+
parsed = { status: "ERROR", value: "INVALID_JSON_RESPONSE", raw: text };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
ok: response.ok,
|
|
111
|
+
statusCode: response.status,
|
|
112
|
+
url,
|
|
113
|
+
data: parsed
|
|
114
|
+
};
|
|
115
|
+
} finally {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleTool(name, args) {
|
|
121
|
+
if (name === "apimail_user_balance") {
|
|
122
|
+
const token = getTokenFromArgs(args || {});
|
|
123
|
+
const result = await callApi("userBalance", { token });
|
|
124
|
+
return asText(JSON.stringify(result, null, 2));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (name === "apimail_domain_availability") {
|
|
128
|
+
const params = {};
|
|
129
|
+
if (args && typeof args.domain === "string") params.domain = args.domain.trim();
|
|
130
|
+
if (args && typeof args.age === "string") params.age = args.age.trim();
|
|
131
|
+
const result = await callApi("domainAvailability", params);
|
|
132
|
+
return asText(JSON.stringify(result, null, 2));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (name === "apimail_purchase_accounts") {
|
|
136
|
+
const token = getTokenFromArgs(args || {});
|
|
137
|
+
const count = Number(args && args.count ? args.count : 1);
|
|
138
|
+
if (!Number.isInteger(count) || count < 1 || count > 500) {
|
|
139
|
+
throw new Error("count must be an integer from 1 to 500.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const params = { token, count };
|
|
143
|
+
if (args && typeof args.domain === "string" && args.domain.trim()) {
|
|
144
|
+
params.domain = args.domain.trim().toLowerCase();
|
|
145
|
+
}
|
|
146
|
+
if (args && typeof args.age === "string" && args.age.trim()) {
|
|
147
|
+
params.age = args.age.trim().toLowerCase();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const result = await callApi("getEmail", params);
|
|
151
|
+
return asText(JSON.stringify(result, null, 2));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (name === "apimail_purchased_emails") {
|
|
155
|
+
const token = getTokenFromArgs(args || {});
|
|
156
|
+
const paymentId = Number(args && args.payment_id);
|
|
157
|
+
if (!Number.isInteger(paymentId) || paymentId <= 0) {
|
|
158
|
+
throw new Error("payment_id must be a positive integer.");
|
|
159
|
+
}
|
|
160
|
+
const result = await callApi("purchasedEmails", { token, payment_id: paymentId });
|
|
161
|
+
return asText(JSON.stringify(result, null, 2));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const server = new Server(
|
|
168
|
+
{ name: "apimail-mcp", version: "1.1.0" },
|
|
169
|
+
{ capabilities: { tools: { listChanged: false } } }
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS }));
|
|
173
|
+
|
|
174
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
175
|
+
const name = request.params?.name;
|
|
176
|
+
const args = request.params?.arguments || {};
|
|
177
|
+
try {
|
|
178
|
+
return await handleTool(name, args);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return asText(String(error?.message || error), true);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const transport = new StdioServerTransport();
|
|
185
|
+
await server.connect(transport);
|