diff --git a/SKILLS.md b/SKILLS.md index 333da40..29c049e 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -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) @@ -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 diff --git a/package.json b/package.json index 48eb2cd..5c77601 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "node": ">=18.0.0" }, "scripts": { - "start": "node bin/index.js" + "start": "node bin/index.js", + "test": "node --test" }, "keywords": [ "mailinator", diff --git a/src/api/endpoints.js b/src/api/endpoints.js index 8f41db7..9ecca3c 100644 --- a/src/api/endpoints.js +++ b/src/api/endpoints.js @@ -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)}`; } /** @@ -22,7 +22,7 @@ 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') { @@ -30,5 +30,5 @@ export function getEmailUrl(domain, messageId, format = 'text') { } // Other formats use the format query parameter - return `${baseUrl}?format=${format}`; + return `${baseUrl}?format=${encodeURIComponent(format)}`; } diff --git a/src/mcp/server.js b/src/mcp/server.js index 3d2000f..63faf1f 100644 --- a/src/mcp/server.js +++ b/src/mcp/server.js @@ -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() diff --git a/src/mcp/tools/list-inbox-tool.js b/src/mcp/tools/list-inbox-tool.js index b31e30e..71c78a1 100644 --- a/src/mcp/tools/list-inbox-tool.js +++ b/src/mcp/tools/list-inbox-tool.js @@ -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() @@ -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', diff --git a/src/validators/input-validator.js b/src/validators/input-validator.js index c35d6a6..d43fa37 100644 --- a/src/validators/input-validator.js +++ b/src/validators/input-validator.js @@ -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 = [ @@ -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.' ); } } diff --git a/test/endpoints.test.js b/test/endpoints.test.js new file mode 100644 index 0000000..a0956a1 --- /dev/null +++ b/test/endpoints.test.js @@ -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', + ); +}); diff --git a/test/input-validator.test.js b/test/input-validator.test.js new file mode 100644 index 0000000..edebdfa --- /dev/null +++ b/test/input-validator.test.js @@ -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', + }, + ); +});