@mailsac/mcp 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 Mailsac
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 ADDED
@@ -0,0 +1,106 @@
1
+ # Mailsac MCP server
2
+
3
+ The official [Model Context Protocol](https://modelcontextprotocol.io) server for [Mailsac](https://mailsac.com).
4
+ It gives AI coding agents (Claude Code, Cursor, VS Code, Claude Desktop and other MCP clients) disposable
5
+ test inboxes, so an agent can check the email your application sends: sign-up confirmations,
6
+ password resets, magic links and one-time codes.
7
+
8
+ A typical conversation:
9
+
10
+ > Sign up for a new account on http://localhost:3000 and confirm it by email.
11
+
12
+ The agent creates a unique address with Mailsac, fills in your sign-up form, waits for the confirmation
13
+ email, follows the link, and tells you what it found. The same tools help it write and debug end-to-end
14
+ tests for those flows.
15
+
16
+ ## Tools
17
+
18
+ | Tool | What it does | Mailsac operations |
19
+ |---|---|---|
20
+ | `create_test_address` | A new unique address, e.g. `signup-mf3k2a1b-9c41d2@mailsac.com`. Nothing to create first. | none |
21
+ | `wait_for_email` | Waits until a matching email arrives (optionally by subject, sender or time), then returns the subject, text, links, the most likely confirm/reset/login link (`actionLink`) and candidate one-time codes. | 1 per check, plus 1 to read |
22
+ | `list_emails` | Emails stored at an address, newest first. | 1 |
23
+ | `read_email` | One email as text (with links and codes), HTML, raw MIME or headers. | 1 |
24
+ | `delete_emails` | Delete one email, or all emails at an address. | 1 per call or message |
25
+ | `list_domains` | Your account's private domains, to use with `create_test_address`. | 1 |
26
+
27
+ ## Set up
28
+
29
+ You need a Mailsac API key. The free plan works: [create an account](https://mailsac.com/register), then
30
+ [create a key](https://mailsac.com/api-keys).
31
+
32
+ **Claude Code**
33
+
34
+ ```bash
35
+ claude mcp add mailsac -e MAILSAC_API_KEY=your-key -- npx -y @mailsac/mcp
36
+ ```
37
+
38
+ **Cursor** (`.cursor/mcp.json`), **Claude Desktop** (`claude_desktop_config.json`) and other clients that use the
39
+ same format:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "mailsac": {
45
+ "command": "npx",
46
+ "args": ["-y", "@mailsac/mcp"],
47
+ "env": { "MAILSAC_API_KEY": "your-key" }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ **VS Code** (`.vscode/mcp.json`):
54
+
55
+ ```json
56
+ {
57
+ "servers": {
58
+ "mailsac": {
59
+ "type": "stdio",
60
+ "command": "npx",
61
+ "args": ["-y", "@mailsac/mcp"],
62
+ "env": { "MAILSAC_API_KEY": "your-key" }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### Settings
69
+
70
+ | Variable | Default | Purpose |
71
+ |---|---|---|
72
+ | `MAILSAC_API_KEY` | (required) | Your Mailsac API key |
73
+ | `MAILSAC_DOMAIN` | `mailsac.com` | Domain for new test addresses. Set it to your private domain. |
74
+ | `MAILSAC_API_URL` | `https://mailsac.com/api` | API base URL |
75
+
76
+ ## Public inboxes and private domains
77
+
78
+ Addresses at `@mailsac.com` are public: anyone who guesses an address can read its mail. They are ideal for
79
+ made-up test data and need no setup. For anything real, such as a staging environment that sends real
80
+ customer names, use a private domain: your own subdomain (for example `test.example.com`) or a zero-setup
81
+ `yourteam.msdc.co` subdomain from the [domains page](https://mailsac.com/domains). Set `MAILSAC_DOMAIN` and every
82
+ new address uses it.
83
+
84
+ ## Operations and limits
85
+
86
+ Each API call uses one [Mailsac operation](https://mailsac.com/pricing). `wait_for_email` checks every 3 seconds
87
+ by default, so a message that arrives within a few seconds costs about 2 to 5 operations. The free plan
88
+ includes 1,500 operations a month; paid plans start at 25,000.
89
+
90
+ ## How Mailsac knows this is the MCP server
91
+
92
+ Requests from this server carry `Mailsac-Client: mcp` and a `mailsac-mcp/<version>` user agent. Mailsac
93
+ counts these in aggregate to see how many people use Mailsac through AI agents. Email content is never part of that
94
+ count.
95
+
96
+ ## Develop
97
+
98
+ ```bash
99
+ npm install
100
+ npm test # builds, then runs the tests with a fake Mailsac API
101
+ MAILSAC_API_KEY=your-key node dist/index.js # runs the server over stdio
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,37 @@
1
+ // Pull the things a test usually needs out of an email: its links, the one that most likely
2
+ // confirms or resets something, and one-time codes.
3
+ const LINK_PATTERN = /https?:\/\/[^\s"'<>)\]]+/g;
4
+ const ACTION_WORDS = /(verif|confirm|activat|reset|magic|login|log-in|sign-?in|sign-?up|invite|token|auth|validate|unsubscribe)/i;
5
+ const CODE_CONTEXT = /(code|otp|one[- ]time|passcode|pin|verification|security|2fa|mfa|token)/i;
6
+ export function extractLinks(text, known = []) {
7
+ const found = [...known, ...(text.match(LINK_PATTERN) || [])]
8
+ .map((link) => link.replace(/[.,;:!?]+$/, ''));
9
+ return [...new Set(found)];
10
+ }
11
+ /** The link most likely to be the one a signup or reset test should follow, if any. */
12
+ export function pickActionLink(links) {
13
+ const scored = links
14
+ .filter((link) => !/unsubscribe|preferences|privacy|terms/i.test(link))
15
+ .map((link) => ({ link, score: ACTION_WORDS.test(link) ? 2 : 0 }))
16
+ .filter((item) => item.score > 0);
17
+ return scored.length ? scored[0].link : null;
18
+ }
19
+ /**
20
+ * Candidate one-time codes: standalone 4–8 digit numbers, plus short uppercase letter/digit codes
21
+ * near words like "code" or "OTP". Codes on a line that mentions a code come first.
22
+ */
23
+ export function extractCodes(text) {
24
+ const withContext = [];
25
+ const without = [];
26
+ for (const line of text.split(/\r?\n/)) {
27
+ const hasContext = CODE_CONTEXT.test(line);
28
+ const digits = line.match(/(?<![\d.,/:#$-])\d{4,8}(?!\d|[.,/:-]\d)/g) || [];
29
+ const mixed = hasContext ? line.match(/\b(?=[A-Z0-9-]*\d)(?=[A-Z0-9-]*[A-Z])[A-Z0-9]{3,4}-?[A-Z0-9]{3,4}\b/g) || [] : [];
30
+ for (const code of [...digits, ...mixed]) {
31
+ if (/^(19|20)\d{2}$/.test(code) && !hasContext)
32
+ continue; // likely a year
33
+ (hasContext ? withContext : without).push(code);
34
+ }
35
+ }
36
+ return [...new Set([...withContext, ...without])].slice(0, 10);
37
+ }
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { Mailsac, VERSION } from './mailsac.js';
4
+ import { createServer } from './server.js';
5
+ const apiKey = process.env.MAILSAC_API_KEY;
6
+ if (process.argv.includes('--version')) {
7
+ console.log(VERSION);
8
+ process.exit(0);
9
+ }
10
+ if (!apiKey) {
11
+ console.error('Set MAILSAC_API_KEY to a Mailsac API key (free at https://mailsac.com/register, then https://mailsac.com/api-keys).');
12
+ process.exit(1);
13
+ }
14
+ const server = createServer({
15
+ client: new Mailsac(apiKey, process.env.MAILSAC_API_URL || 'https://mailsac.com/api'),
16
+ defaultDomain: (process.env.MAILSAC_DOMAIN || 'mailsac.com').toLowerCase(),
17
+ });
18
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,91 @@
1
+ // A small Mailsac REST client. Every request identifies itself as the official MCP server
2
+ // (Mailsac-Client: mcp), which Mailsac counts in aggregate, never alongside message content.
3
+ import { randomBytes } from 'node:crypto';
4
+ export const VERSION = '0.1.0';
5
+ export class MailsacError extends Error {
6
+ status;
7
+ constructor(message, status) {
8
+ super(message);
9
+ this.status = status;
10
+ }
11
+ }
12
+ export class Mailsac {
13
+ apiKey;
14
+ fetchImpl;
15
+ baseUrl;
16
+ constructor(apiKey, baseUrl = 'https://mailsac.com/api', fetchImpl = fetch) {
17
+ this.apiKey = apiKey;
18
+ this.fetchImpl = fetchImpl;
19
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
20
+ }
21
+ async request(path, init = {}) {
22
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
23
+ ...init,
24
+ headers: {
25
+ 'Mailsac-Key': this.apiKey,
26
+ 'Mailsac-Client': 'mcp',
27
+ 'User-Agent': `mailsac-mcp/${VERSION} (+https://github.com/mailsac/mailsac-mcp)`,
28
+ Accept: 'application/json',
29
+ ...(init.headers || {}),
30
+ },
31
+ });
32
+ if (!res.ok) {
33
+ let detail = '';
34
+ try {
35
+ const body = await res.json();
36
+ detail = body?.message || '';
37
+ }
38
+ catch {
39
+ /* not JSON */
40
+ }
41
+ throw new MailsacError(explain(res.status, detail), res.status);
42
+ }
43
+ return res;
44
+ }
45
+ async listMessages(address) {
46
+ const res = await this.request(`/addresses/${encodeURIComponent(address)}/messages`);
47
+ return (await res.json());
48
+ }
49
+ async readText(address, messageId, format = 'text') {
50
+ const route = { text: 'text', html: 'body', raw: 'raw' }[format];
51
+ const res = await this.request(`/${route}/${encodeURIComponent(address)}/${encodeURIComponent(messageId)}`, {
52
+ headers: { Accept: 'text/plain, text/html, */*' },
53
+ });
54
+ return res.text();
55
+ }
56
+ async readHeaders(address, messageId) {
57
+ const res = await this.request(`/addresses/${encodeURIComponent(address)}/messages/${encodeURIComponent(messageId)}/headers`);
58
+ return res.json();
59
+ }
60
+ async deleteMessage(address, messageId) {
61
+ await this.request(`/addresses/${encodeURIComponent(address)}/messages/${encodeURIComponent(messageId)}`, {
62
+ method: 'DELETE',
63
+ });
64
+ }
65
+ async deleteAllMessages(address) {
66
+ await this.request(`/addresses/${encodeURIComponent(address)}/messages`, { method: 'DELETE' });
67
+ }
68
+ async listDomains() {
69
+ const res = await this.request('/domains');
70
+ return (await res.json());
71
+ }
72
+ }
73
+ function explain(status, detail) {
74
+ const suffix = detail ? ` Mailsac said: ${detail}` : '';
75
+ if ((status === 401 || status === 403) && detail)
76
+ return `Not allowed (HTTP ${status}): ${detail}`;
77
+ if (status === 401 || status === 403) {
78
+ return `Mailsac rejected the API key (HTTP ${status}). Check MAILSAC_API_KEY; create a key at https://mailsac.com/api-keys.`;
79
+ }
80
+ if (status === 404)
81
+ return `Not found (HTTP 404). The address or message may not exist, or the message was already deleted.${suffix}`;
82
+ if (status === 429) {
83
+ return `Mailsac is limiting requests (HTTP 429): the account may have used its monthly operations, or is sending requests too fast. See https://mailsac.com/pricing.${suffix}`;
84
+ }
85
+ return `Mailsac API error (HTTP ${status}).${suffix}`;
86
+ }
87
+ /** A unique, lower-case address such as signup-mf3k2a1b-9c41d2@mailsac.com. */
88
+ export function uniqueAddress(domain, prefix = 'test') {
89
+ const clean = prefix.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, '').slice(0, 30) || 'test';
90
+ return `${clean}-${Date.now().toString(36)}-${randomBytes(3).toString('hex')}@${domain.toLowerCase()}`;
91
+ }
package/dist/server.js ADDED
@@ -0,0 +1,220 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { extractCodes, extractLinks, pickActionLink } from './extract.js';
4
+ import { MailsacError, uniqueAddress, VERSION } from './mailsac.js';
5
+ const MAX_TEXT = 12_000;
6
+ const PUBLIC_DOMAIN = 'mailsac.com';
7
+ const text = (value) => ({
8
+ content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
9
+ });
10
+ const failure = (err) => ({
11
+ isError: true,
12
+ content: [{ type: 'text', text: err instanceof MailsacError ? err.message : `Error: ${err?.message || err}` }],
13
+ });
14
+ const truncate = (value) => value.length > MAX_TEXT ? `${value.slice(0, MAX_TEXT)}\n…[truncated ${value.length - MAX_TEXT} characters]` : value;
15
+ function summarize(message) {
16
+ return {
17
+ messageId: message._id,
18
+ subject: message.subject,
19
+ from: message.from?.[0]?.address || null,
20
+ received: message.received,
21
+ attachments: message.attachments?.length || 0,
22
+ };
23
+ }
24
+ function matches(message, subjectContains, fromContains, receivedAfter) {
25
+ if (subjectContains && !(message.subject || '').toLowerCase().includes(subjectContains.toLowerCase()))
26
+ return false;
27
+ if (fromContains && !(message.from || []).some((f) => f.address?.toLowerCase().includes(fromContains.toLowerCase()))) {
28
+ return false;
29
+ }
30
+ if (receivedAfter && new Date(message.received).getTime() < receivedAfter)
31
+ return false;
32
+ return true;
33
+ }
34
+ async function withContent(client, message) {
35
+ const body = await client.readText(message.inbox, message._id, 'text');
36
+ const links = extractLinks(body, message.links || []);
37
+ return {
38
+ ...summarize(message),
39
+ address: message.inbox,
40
+ actionLink: pickActionLink(links),
41
+ codes: extractCodes(body),
42
+ links,
43
+ text: truncate(body),
44
+ };
45
+ }
46
+ export function createServer({ client, defaultDomain, sleep, now }) {
47
+ const wait = sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
48
+ const clock = now || Date.now;
49
+ const server = new McpServer({ name: 'mailsac', version: VERSION }, {
50
+ instructions: 'Mailsac test inboxes for checking the email an application sends. Typical flow: create_test_address, ' +
51
+ 'use the address in the app (sign up, reset a password, request a code), wait_for_email, then follow ' +
52
+ 'actionLink or enter one of codes, and delete_emails when done. Addresses on mailsac.com are public: ' +
53
+ 'anyone who guesses the address can read it, so only use made-up data there, or use a private domain ' +
54
+ '(see list_domains). Every API call, including each poll while waiting, uses Mailsac operations.',
55
+ });
56
+ server.registerTool('create_test_address', {
57
+ title: 'Create a test email address',
58
+ description: 'Return a new, unique address that can receive email immediately, with nothing to set up. ' +
59
+ 'Use one address per test so tests never read each other\'s mail. Makes no API call.',
60
+ inputSchema: {
61
+ prefix: z.string().max(30).optional().describe('Readable start of the address, e.g. "signup"'),
62
+ domain: z
63
+ .string()
64
+ .optional()
65
+ .describe(`Domain to use. Defaults to ${defaultDomain}. Use a private domain for real or sensitive data.`),
66
+ },
67
+ }, async ({ prefix, domain }) => {
68
+ const chosen = (domain || defaultDomain).toLowerCase();
69
+ const address = uniqueAddress(chosen, prefix);
70
+ return text({
71
+ address,
72
+ public: chosen === PUBLIC_DOMAIN,
73
+ ...(chosen === PUBLIC_DOMAIN
74
+ ? {
75
+ webInbox: `https://mailsac.com/inbox/${address}`,
76
+ note: 'Public inbox: anyone who knows the address can read it. Use made-up data only.',
77
+ }
78
+ : {}),
79
+ });
80
+ });
81
+ server.registerTool('wait_for_email', {
82
+ title: 'Wait for an email',
83
+ description: 'Poll an address until a matching email arrives, then return its subject, sender, text, links, the ' +
84
+ 'most likely confirm/reset/login link (actionLink) and candidate one-time codes. Each poll uses one ' +
85
+ 'Mailsac operation.',
86
+ inputSchema: {
87
+ address: z.string().email().describe('Address to watch'),
88
+ subjectContains: z.string().optional().describe('Only match emails whose subject contains this text'),
89
+ fromContains: z.string().optional().describe('Only match emails whose sender contains this text'),
90
+ receivedAfter: z
91
+ .string()
92
+ .optional()
93
+ .describe('ISO timestamp; ignore emails received before it (useful when an address is reused)'),
94
+ timeoutSeconds: z.number().int().min(5).max(300).default(60).describe('Give up after this long'),
95
+ pollIntervalSeconds: z.number().int().min(2).max(30).default(3),
96
+ },
97
+ }, async ({ address, subjectContains, fromContains, receivedAfter, timeoutSeconds, pollIntervalSeconds }) => {
98
+ try {
99
+ const after = receivedAfter ? Date.parse(receivedAfter) : undefined;
100
+ if (receivedAfter && Number.isNaN(after))
101
+ throw new Error('receivedAfter must be an ISO date-time');
102
+ const deadline = clock() + timeoutSeconds * 1000;
103
+ let polls = 0;
104
+ for (;;) {
105
+ polls++;
106
+ const messages = await client.listMessages(address);
107
+ const found = messages
108
+ .filter((m) => matches(m, subjectContains, fromContains, after))
109
+ .sort((a, b) => Date.parse(b.received) - Date.parse(a.received))[0];
110
+ if (found)
111
+ return text({ ...(await withContent(client, found)), polls });
112
+ if (clock() + pollIntervalSeconds * 1000 > deadline)
113
+ break;
114
+ await wait(pollIntervalSeconds * 1000);
115
+ }
116
+ return {
117
+ isError: true,
118
+ content: [
119
+ {
120
+ type: 'text',
121
+ text: `No matching email reached ${address} within ${timeoutSeconds} seconds (${polls} checks). ` +
122
+ 'Check that the app sent to exactly this address, that its mail provider is configured, ' +
123
+ 'and try a longer timeout for real delivery.',
124
+ },
125
+ ],
126
+ };
127
+ }
128
+ catch (err) {
129
+ return failure(err);
130
+ }
131
+ });
132
+ server.registerTool('list_emails', {
133
+ title: 'List emails at an address',
134
+ description: 'List the emails currently stored for an address, newest first (no bodies). One operation.',
135
+ inputSchema: { address: z.string().email() },
136
+ }, async ({ address }) => {
137
+ try {
138
+ const messages = await client.listMessages(address);
139
+ return text(messages
140
+ .sort((a, b) => Date.parse(b.received) - Date.parse(a.received))
141
+ .map(summarize));
142
+ }
143
+ catch (err) {
144
+ return failure(err);
145
+ }
146
+ });
147
+ server.registerTool('read_email', {
148
+ title: 'Read an email',
149
+ description: 'Read one email. format "text" (default) also returns links, actionLink and codes; "html" returns the ' +
150
+ 'HTML body; "raw" the full MIME source; "headers" the parsed headers.',
151
+ inputSchema: {
152
+ address: z.string().email(),
153
+ messageId: z.string().min(1),
154
+ format: z.enum(['text', 'html', 'raw', 'headers']).default('text'),
155
+ },
156
+ }, async ({ address, messageId, format }) => {
157
+ try {
158
+ if (format === 'headers')
159
+ return text(await client.readHeaders(address, messageId));
160
+ const body = await client.readText(address, messageId, format);
161
+ if (format !== 'text')
162
+ return text(truncate(body));
163
+ const links = extractLinks(body);
164
+ return text({ messageId, actionLink: pickActionLink(links), codes: extractCodes(body), links, text: truncate(body) });
165
+ }
166
+ catch (err) {
167
+ return failure(err);
168
+ }
169
+ });
170
+ server.registerTool('delete_emails', {
171
+ title: 'Delete emails',
172
+ description: 'Delete one email (messageId) or every email at an address (omit messageId). Clean up after a test.',
173
+ inputSchema: { address: z.string().email(), messageId: z.string().optional() },
174
+ annotations: { destructiveHint: true },
175
+ }, async ({ address, messageId }) => {
176
+ try {
177
+ if (messageId) {
178
+ await client.deleteMessage(address, messageId);
179
+ return text({ deleted: 1, address });
180
+ }
181
+ try {
182
+ await client.deleteAllMessages(address);
183
+ return text({ deleted: 'all', address });
184
+ }
185
+ catch (err) {
186
+ // Deleting a whole inbox needs ownership (e.g. a public address); delete message by message instead.
187
+ if (!(err instanceof MailsacError) || (err.status !== 401 && err.status !== 403))
188
+ throw err;
189
+ const messages = await client.listMessages(address);
190
+ for (const message of messages)
191
+ await client.deleteMessage(address, message._id);
192
+ return text({ deleted: messages.length, address });
193
+ }
194
+ }
195
+ catch (err) {
196
+ return failure(err);
197
+ }
198
+ });
199
+ server.registerTool('list_domains', {
200
+ title: 'List private domains',
201
+ description: 'List the private domains on this Mailsac account. Mail to a private domain is visible only to the ' +
202
+ 'account; pass one as `domain` to create_test_address.',
203
+ inputSchema: {},
204
+ }, async () => {
205
+ try {
206
+ const domains = await client.listDomains();
207
+ return text({
208
+ defaultDomain,
209
+ privateDomains: domains.map((d) => d._id || d.domain),
210
+ ...(domains.length
211
+ ? {}
212
+ : { note: 'No private domains. Add one, or a free yourteam.msdc.co subdomain, at https://mailsac.com/domains' }),
213
+ });
214
+ }
215
+ catch (err) {
216
+ return failure(err);
217
+ }
218
+ });
219
+ return server;
220
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@mailsac/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Official Mailsac MCP server: give AI coding agents disposable test inboxes to check signup, password-reset and one-time-code emails.",
5
+ "license": "MIT",
6
+ "author": "Mailsac <support@team.mailsac.com>",
7
+ "homepage": "https://github.com/mailsac/mailsac-mcp#readme",
8
+ "repository": { "type": "git", "url": "git+https://github.com/mailsac/mailsac-mcp.git" },
9
+ "bugs": { "url": "https://github.com/mailsac/mailsac-mcp/issues" },
10
+ "keywords": ["mcp", "model-context-protocol", "mailsac", "email-testing", "test-email", "disposable-email", "claude", "cursor", "ai-agents", "qa"],
11
+ "type": "module",
12
+ "bin": { "mailsac-mcp": "dist/index.js" },
13
+ "main": "dist/index.js",
14
+ "files": ["dist", "README.md", "LICENSE"],
15
+ "engines": { "node": ">=18" },
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json && chmod +x dist/index.js",
18
+ "test": "npm run build && node --test test/*.test.mjs",
19
+ "prepublishOnly": "npm test"
20
+ },
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "1.31.0",
23
+ "zod": "3.25.76"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "22.18.6",
27
+ "typescript": "5.9.3"
28
+ }
29
+ }