@mailloom/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/README.md CHANGED
@@ -1,3 +1,81 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @mailloom/mcp
2
+
3
+ Official [Model Context Protocol](https://modelcontextprotocol.io) server for [Mailloom](https://mailloom.app) — manage email templates, brand kits, and assets from Cursor, Claude, and other MCP hosts.
4
+
5
+ ## Setup
6
+
7
+ 1. Sign in to Mailloom → **Account settings**.
8
+ 2. Ensure **Allow MCP / API write access** is on (default) if agents should mutate data.
9
+ 3. **Create key** and copy the `ml_live_…` secret once.
10
+
11
+ API auth runs in the Supabase Edge Function `mailloom-api` (service role never leaves Supabase). Prefer the Next `/api/v1` base so `export_template` works; that route proxies CRUD to the edge function.
12
+
13
+ ### Cursor (`mcp.json`)
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "mailloom": {
19
+ "command": "npx",
20
+ "args": ["-y", "@mailloom/mcp"],
21
+ "env": {
22
+ "MAILLOOM_API_KEY": "ml_live_…",
23
+ "MAILLOOM_API_BASE": "http://localhost:3847/api/v1"
24
+ }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ Production Next: `https://your-host/api/v1`.
31
+ Edge Function only (CRUD, no HTML export): `https://<project>.supabase.co/functions/v1/mailloom-api`.
32
+
33
+ ### Local package (unpublished)
34
+
35
+ ```bash
36
+ cd packages/mailloom-mcp
37
+ npm install
38
+ npm run build
39
+ ```
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "mailloom": {
45
+ "command": "node",
46
+ "args": ["/absolute/path/to/packages/mailloom-mcp/dist/index.js"],
47
+ "env": {
48
+ "MAILLOOM_API_KEY": "ml_live_…",
49
+ "MAILLOOM_API_BASE": "http://localhost:3847/api/v1"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Environment
57
+
58
+ | Variable | Required | Default | Purpose |
59
+ | --- | --- | --- | --- |
60
+ | `MAILLOOM_API_KEY` | yes | — | `ml_live_…` from Account settings |
61
+ | `MAILLOOM_API_BASE` | no | `http://localhost:3847/api/v1` | API origin including `/api/v1` |
62
+
63
+ ## Tools
64
+
65
+ | Tool | Write? | Description |
66
+ | --- | --- | --- |
67
+ | `health_check` | | API liveness + write flag |
68
+ | `get_account` | | Email, write flag, key metadata |
69
+ | `list_templates` / `get_template` | | Templates (user + platform) |
70
+ | `create_template` / `update_template` / `duplicate_template` / `delete_template` | ✓ | Mutate user templates |
71
+ | `export_template` | | Render HTML (optional inline CSS) |
72
+ | `list_brand_kits` / `get_brand_kit` | | Brand kits |
73
+ | `create_brand_kit` / `update_brand_kit` / `delete_brand_kit` | ✓ | Mutate kits |
74
+ | `list_assets` | | Assets |
75
+ | `upload_asset` / `delete_asset` | ✓ | Upload (base64/url) / delete |
76
+
77
+ When write access is disabled in Account settings, write tools return `403 write_disabled`. Platform templates stay read-only; duplicate them to edit.
78
+
79
+ ## License
80
+
81
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/index.js'
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Thin Mailloom API client for MCP.
3
+ * Auth: Authorization Bearer ml_live_…
4
+ *
5
+ * Prefer Next /api/v1 (proxies to the mailloom-api Edge Function + HTML export).
6
+ * Or point at https://<project>.supabase.co/functions/v1/mailloom-api for CRUD only.
7
+ */
8
+ export type ApiConfig = {
9
+ baseUrl: string;
10
+ apiKey: string;
11
+ };
12
+ export declare function loadConfig(): ApiConfig;
13
+ export declare function apiRequest(config: ApiConfig, method: string, path: string, opts?: {
14
+ body?: unknown;
15
+ query?: Record<string, string | number | undefined | null>;
16
+ }): Promise<unknown>;
17
+ export declare function jsonResult(data: unknown): {
18
+ content: {
19
+ type: "text";
20
+ text: string;
21
+ }[];
22
+ };
23
+ export declare function errorResult(err: unknown): {
24
+ content: {
25
+ type: "text";
26
+ text: string;
27
+ }[];
28
+ isError: true;
29
+ };
package/dist/client.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Thin Mailloom API client for MCP.
3
+ * Auth: Authorization Bearer ml_live_…
4
+ *
5
+ * Prefer Next /api/v1 (proxies to the mailloom-api Edge Function + HTML export).
6
+ * Or point at https://<project>.supabase.co/functions/v1/mailloom-api for CRUD only.
7
+ */
8
+ const DEFAULT_BASE = 'http://localhost:3847/api/v1';
9
+ export function loadConfig() {
10
+ const apiKey = (process.env.MAILLOOM_API_KEY || '').trim();
11
+ if (!apiKey) {
12
+ throw new Error('MAILLOOM_API_KEY is required (ml_live_…). Create one in Mailloom → Account settings.');
13
+ }
14
+ if (!apiKey.startsWith('ml_live_')) {
15
+ throw new Error('MAILLOOM_API_KEY must start with ml_live_');
16
+ }
17
+ const baseUrl = (process.env.MAILLOOM_API_BASE || DEFAULT_BASE).replace(/\/$/, '');
18
+ return { baseUrl, apiKey };
19
+ }
20
+ export async function apiRequest(config, method, path, opts) {
21
+ const url = new URL(path.startsWith('http') ? path : `${config.baseUrl}${path.startsWith('/') ? path : `/${path}`}`);
22
+ if (opts?.query) {
23
+ for (const [k, v] of Object.entries(opts.query)) {
24
+ if (v !== undefined && v !== null && String(v) !== '') {
25
+ url.searchParams.set(k, String(v));
26
+ }
27
+ }
28
+ }
29
+ const headers = {
30
+ Authorization: `Bearer ${config.apiKey}`,
31
+ Accept: 'application/json',
32
+ };
33
+ if (opts?.body !== undefined) {
34
+ headers['Content-Type'] = 'application/json';
35
+ }
36
+ const res = await fetch(url, {
37
+ method,
38
+ headers,
39
+ body: opts?.body !== undefined ? JSON.stringify(opts.body) : undefined,
40
+ });
41
+ const text = await res.text();
42
+ let data = null;
43
+ if (text) {
44
+ try {
45
+ data = JSON.parse(text);
46
+ }
47
+ catch {
48
+ data = { raw: text };
49
+ }
50
+ }
51
+ if (!res.ok) {
52
+ const msg = data && typeof data === 'object' && 'error' in data
53
+ ? String(data.error)
54
+ : `HTTP ${res.status}`;
55
+ const code = data && typeof data === 'object' && 'code' in data
56
+ ? String(data.code)
57
+ : undefined;
58
+ throw new Error(code ? `${msg} (${code})` : msg);
59
+ }
60
+ return data;
61
+ }
62
+ export function jsonResult(data) {
63
+ return {
64
+ content: [
65
+ {
66
+ type: 'text',
67
+ text: JSON.stringify(data, null, 2),
68
+ },
69
+ ],
70
+ };
71
+ }
72
+ export function errorResult(err) {
73
+ const message = err instanceof Error ? err.message : String(err);
74
+ return {
75
+ content: [{ type: 'text', text: message }],
76
+ isError: true,
77
+ };
78
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Official Mailloom MCP server — templates, brand kits, assets for AI hosts.
4
+ *
5
+ * Env:
6
+ * MAILLOOM_API_KEY required — ml_live_…
7
+ * MAILLOOM_API_BASE optional — default http://localhost:3847/api/v1
8
+ *
9
+ * Write tools require Account settings → Allow MCP / API write access (default on).
10
+ * When disabled, mutating tools return 403 write_disabled; reads still work.
11
+ */
12
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { z } from 'zod';
15
+ import { apiRequest, errorResult, jsonResult, loadConfig } from './client.js';
16
+ const PACKAGE_VERSION = '0.1.0';
17
+ const config = loadConfig();
18
+ const server = new McpServer({
19
+ name: 'mailloom',
20
+ version: PACKAGE_VERSION,
21
+ });
22
+ function tool(name, description, schema, handler) {
23
+ server.tool(name, description, schema, async (args) => {
24
+ try {
25
+ const data = await handler(args);
26
+ return jsonResult(data);
27
+ }
28
+ catch (err) {
29
+ return errorResult(err);
30
+ }
31
+ });
32
+ }
33
+ // —— Account ——
34
+ tool('health_check', 'Check Mailloom API liveness and whether write access is enabled.', {}, async () => apiRequest(config, 'GET', '/health'));
35
+ tool('get_account', 'Get account email, MCP write-enabled flag, and current API key metadata (never the secret).', {}, async () => apiRequest(config, 'GET', '/me'));
36
+ // —— Templates ——
37
+ tool('list_templates', 'List user and platform email templates.', {}, async () => apiRequest(config, 'GET', '/templates'));
38
+ tool('get_template', 'Get one template by id (includes full email document).', { id: z.string().uuid().describe('Template UUID') }, async (args) => apiRequest(config, 'GET', `/templates/${args.id}`));
39
+ tool('create_template', 'Create a user template. Requires write access. document matches the editor shape (settings + blocks).', {
40
+ name: z.string().describe('Template name'),
41
+ document: z.record(z.unknown()).describe('Email document (settings + blocks)'),
42
+ brand_kit_id: z.string().uuid().nullable().optional().describe('Optional brand kit UUID'),
43
+ }, async (args) => apiRequest(config, 'POST', '/templates', {
44
+ body: {
45
+ name: args.name,
46
+ document: args.document,
47
+ brand_kit_id: args.brand_kit_id,
48
+ },
49
+ }));
50
+ tool('update_template', 'Update a user template name and/or document. Platform templates are read-only — duplicate first. Requires write access.', {
51
+ id: z.string().uuid().describe('Template UUID'),
52
+ name: z.string().optional().describe('New name'),
53
+ document: z.record(z.unknown()).optional().describe('Full email document'),
54
+ brand_kit_id: z.string().uuid().nullable().optional().describe('Brand kit UUID or null'),
55
+ }, async (args) => {
56
+ const { id, ...body } = args;
57
+ return apiRequest(config, 'PATCH', `/templates/${id}`, { body });
58
+ });
59
+ tool('duplicate_template', 'Copy a platform or user template into a new editable user template. Requires write access.', {
60
+ id: z.string().uuid().describe('Source template UUID'),
61
+ name: z.string().optional().describe('Name for the copy'),
62
+ }, async (args) => apiRequest(config, 'POST', `/templates/${args.id}/duplicate`, {
63
+ body: { name: args.name },
64
+ }));
65
+ tool('delete_template', 'Delete a user template. Platform templates cannot be deleted. Requires write access.', { id: z.string().uuid().describe('Template UUID') }, async (args) => apiRequest(config, 'DELETE', `/templates/${args.id}`));
66
+ tool('export_template', 'Render a template as email HTML (inline CSS by default). Read-only.', {
67
+ id: z.string().uuid().describe('Template UUID'),
68
+ inline: z.boolean().optional().describe('Inline CSS (default true)'),
69
+ }, async (args) => apiRequest(config, 'POST', `/templates/${args.id}/export`, {
70
+ body: { inline: args.inline },
71
+ }));
72
+ // —— Brand kits ——
73
+ tool('list_brand_kits', 'List brand kits for the account.', {}, async () => apiRequest(config, 'GET', '/brand-kits'));
74
+ tool('get_brand_kit', 'Get one brand kit by id.', { id: z.string().uuid().describe('Brand kit UUID') }, async (args) => apiRequest(config, 'GET', `/brand-kits/${args.id}`));
75
+ tool('create_brand_kit', 'Create a brand kit (colors, fonts, tone, logo_path). Requires write access.', {
76
+ name: z.string().describe('Kit name'),
77
+ data: z
78
+ .record(z.unknown())
79
+ .optional()
80
+ .describe('BrandKitData: colors, fonts, tone, company, socialDefaults, …'),
81
+ logo_path: z.string().nullable().optional().describe('Storage path in assets bucket'),
82
+ }, async (args) => apiRequest(config, 'POST', '/brand-kits', {
83
+ body: { name: args.name, data: args.data, logo_path: args.logo_path },
84
+ }));
85
+ tool('update_brand_kit', 'Update a brand kit. Requires write access.', {
86
+ id: z.string().uuid().describe('Brand kit UUID'),
87
+ name: z.string().optional(),
88
+ data: z.record(z.unknown()).optional(),
89
+ logo_path: z.string().nullable().optional(),
90
+ }, async (args) => {
91
+ const { id, ...body } = args;
92
+ return apiRequest(config, 'PATCH', `/brand-kits/${id}`, { body });
93
+ });
94
+ tool('delete_brand_kit', 'Delete a brand kit. Requires write access.', { id: z.string().uuid().describe('Brand kit UUID') }, async (args) => apiRequest(config, 'DELETE', `/brand-kits/${args.id}`));
95
+ // —— Assets ——
96
+ tool('list_assets', 'List uploaded assets for the account.', {}, async () => apiRequest(config, 'GET', '/assets'));
97
+ tool('upload_asset', 'Upload an asset from base64 or a public URL. Requires write access.', {
98
+ filename: z.string().optional().describe('Original filename (for extension)'),
99
+ mime: z.string().optional().describe('MIME type'),
100
+ base64: z.string().optional().describe('Base64 or data-URL payload'),
101
+ url: z.string().url().optional().describe('Public URL to fetch and store'),
102
+ }, async (args) => apiRequest(config, 'POST', '/assets', { body: args }));
103
+ tool('delete_asset', 'Delete an asset by id. Requires write access.', { id: z.string().uuid().describe('Asset UUID') }, async (args) => apiRequest(config, 'DELETE', `/assets/${args.id}`));
104
+ async function main() {
105
+ const transport = new StdioServerTransport();
106
+ await server.connect(transport);
107
+ }
108
+ main().catch((err) => {
109
+ console.error(err);
110
+ process.exit(1);
111
+ });
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
- {
2
- "name": "@mailloom/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@mailloom/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Official Mailloom MCP server — manage email templates, brand kits, and assets via the Mailloom API.",
5
+ "type": "module",
6
+ "bin": {
7
+ "mailloom-mcp": "bin/mailloom-mcp.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "start": "node dist/index.js",
20
+ "dev": "tsx src/index.ts",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.12.1",
28
+ "zod": "^3.24.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.13.10",
32
+ "tsx": "^4.19.3",
33
+ "typescript": "^5.8.2"
34
+ },
35
+ "files": [
36
+ "bin",
37
+ "dist",
38
+ "README.md"
39
+ ],
40
+ "keywords": [
41
+ "mailloom",
42
+ "mcp",
43
+ "model-context-protocol",
44
+ "cursor",
45
+ "claude",
46
+ "email",
47
+ "templates"
48
+ ],
49
+ "license": "MIT"
50
+ }