Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions SKILLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ This server exposes two tools for active operations:
Lists all emails in a Mailinator inbox.

**Parameters:**
- `inbox_name` (required): Inbox to query (max 50 chars, alphanumeric with dots)
- `inbox_name` (required): Inbox to query (max 50 chars; letters, numbers, dots, and hyphens)
- Supports wildcards: `*` (all inboxes) or `prefix*` (pattern match) with API token
- `domain` (optional): "public", "private", or custom domain (auto-detected if omitted)

Expand Down Expand Up @@ -236,7 +236,7 @@ When configured as an MCP server in Claude Desktop:

## Validation Rules

- **Inbox Names:** Max 50 chars, alphanumeric + dots, no leading/trailing dots
- **Inbox Names:** Max 50 chars; letters, numbers, dots, and hyphens; must begin and end with a letter or number
- **Wildcards:** Only `*` or `prefix*`, only in private domains, requires API token
- **Domains:** "public", "private", or valid custom domain names

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"node": ">=18.0.0"
},
"scripts": {
"start": "node bin/index.js"
"start": "node bin/index.js",
"test": "node --test"
},
"keywords": [
"mailinator",
Expand Down
6 changes: 3 additions & 3 deletions src/api/endpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const BASE_URL = 'https://api.mailinator.com/cli/v3';
* @returns {string} Full API URL
*/
export function getInboxUrl(domain, inboxName) {
return `${BASE_URL}/domains/${domain}/inboxes/${inboxName}`;
return `${BASE_URL}/domains/${encodeURIComponent(domain)}/inboxes/${encodeURIComponent(inboxName)}`;
}

/**
Expand All @@ -22,13 +22,13 @@ export function getInboxUrl(domain, inboxName) {
* @returns {string} Full API URL
*/
export function getEmailUrl(domain, messageId, format = 'text') {
const baseUrl = `${BASE_URL}/domains/${domain}/messages/${messageId}`;
const baseUrl = `${BASE_URL}/domains/${encodeURIComponent(domain)}/messages/${encodeURIComponent(messageId)}`;

// smtplog is a separate endpoint path, not a format parameter
if (format === 'smtplog') {
return `${baseUrl}/smtplog`;
}

// Other formats use the format query parameter
return `${baseUrl}?format=${format}`;
return `${baseUrl}?format=${encodeURIComponent(format)}`;
}
2 changes: 1 addition & 1 deletion src/mcp/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function createMcpServer() {
.string()
.min(1)
.max(50)
.describe('Inbox name to query (max 50 characters, alphanumeric with dots). Can use * for all inboxes or prefix* for wildcard search in private domain with API token.'),
.describe('Inbox name to query (max 50 characters; letters, numbers, dots, and hyphens). Can use * for all inboxes or prefix* for wildcard search in private domain with API token.'),
domain: z
.string()
.optional()
Expand Down
6 changes: 3 additions & 3 deletions src/mcp/tools/list-inbox-tool.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export const listInboxSchema = z.object({
.string()
.min(1)
.max(50)
.regex(/^[a-zA-Z0-9*]([a-zA-Z0-9.*]*[a-zA-Z0-9*])?$/,
'Inbox name must be alphanumeric with optional dots and wildcards'),
.regex(/^[a-zA-Z0-9*](?:[a-zA-Z0-9.*-]*[a-zA-Z0-9*])?$/,
'Inbox name may contain letters, numbers, dots, hyphens, and wildcards'),
domain: z
.string()
.optional()
Expand All @@ -29,7 +29,7 @@ export const listInboxTool = {
properties: {
inbox_name: {
type: 'string',
description: 'Inbox name to query (max 50 characters, alphanumeric with dots). Can use * for all inboxes or prefix* for wildcard search in private domain with API token.',
description: 'Inbox name to query (max 50 characters; letters, numbers, dots, and hyphens). Can use * for all inboxes or prefix* for wildcard search in private domain with API token.',
},
domain: {
type: 'string',
Expand Down
4 changes: 2 additions & 2 deletions src/validators/input-validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { ValidationError } from '../utils/errors.js';

const INBOX_NAME_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9.]*[a-zA-Z0-9])?$/;
const INBOX_NAME_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?$/;
const MAX_INBOX_LENGTH = 50;

const VALID_FORMATS = [
Expand Down Expand Up @@ -38,7 +38,7 @@ export function validateInboxName(inboxName) {

if (!INBOX_NAME_PATTERN.test(inboxName)) {
throw new ValidationError(
'Inbox name must be alphanumeric with optional dots (not at the beginning or end).'
'Inbox name may contain letters, numbers, dots, and hyphens, and must begin and end with a letter or number.'
);
}
}
Expand Down
22 changes: 22 additions & 0 deletions test/endpoints.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import { getEmailUrl, getInboxUrl } from '../src/api/endpoints.js';

test('builds an inbox URL for a hyphenated name', () => {
assert.equal(
getInboxUrl('public', 'kenst-vibium-0722'),
'https://api.mailinator.com/cli/v3/domains/public/inboxes/kenst-vibium-0722',
);
});

test('encodes dynamic API path segments', () => {
assert.equal(
getInboxUrl('custom/domain', 'kenst/inbox'),
'https://api.mailinator.com/cli/v3/domains/custom%2Fdomain/inboxes/kenst%2Finbox',
);
assert.equal(
getEmailUrl('custom/domain', 'message/id', 'text/plain'),
'https://api.mailinator.com/cli/v3/domains/custom%2Fdomain/messages/message%2Fid?format=text%2Fplain',
);
});
47 changes: 47 additions & 0 deletions test/input-validator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import { validateInboxName } from '../src/validators/input-validator.js';
import { listInboxSchema } from '../src/mcp/tools/list-inbox-tool.js';
import { ValidationError } from '../src/utils/errors.js';

test('accepts inbox names containing internal hyphens', () => {
for (const inboxName of [
'jango555',
'kenst.vibium',
'kenst-vibium-0722',
'test-inbox.example',
]) {
assert.doesNotThrow(() => validateInboxName(inboxName), inboxName);
}
});

test('rejects inbox names with punctuation at either end', () => {
for (const inboxName of ['-kenst', 'kenst-', '.kenst', 'kenst.']) {
assert.throws(
() => validateInboxName(inboxName),
ValidationError,
inboxName,
);
}
});

test('rejects unsupported inbox-name characters', () => {
assert.throws(
() => validateInboxName('kenst/inbox'),
ValidationError,
);
});

test('MCP input schema accepts a hyphenated inbox name', () => {
assert.deepEqual(
listInboxSchema.parse({
inbox_name: 'kenst-vibium-0722',
domain: 'public',
}),
{
inbox_name: 'kenst-vibium-0722',
domain: 'public',
},
);
});