@mindstone-engineering/mcp-server-mixmax 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/dist/auth.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Mixmax authentication module.
3
+ *
4
+ * Manages the API token lifecycle — env var on startup, runtime update via
5
+ * configure tool, and bridge integration for host-app credential management.
6
+ */
7
+ /**
8
+ * Returns the current API token.
9
+ */
10
+ export declare function getApiToken(): string;
11
+ /**
12
+ * Returns true if an API token is configured.
13
+ */
14
+ export declare function isConfigured(): boolean;
15
+ /**
16
+ * Update the API token at runtime (e.g. after configure_mixmax_api_key).
17
+ */
18
+ export declare function setApiToken(token: string): void;
19
+ //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Mixmax authentication module.
3
+ *
4
+ * Manages the API token lifecycle — env var on startup, runtime update via
5
+ * configure tool, and bridge integration for host-app credential management.
6
+ */
7
+ let apiToken = process.env.MIXMAX_API_TOKEN || '';
8
+ /**
9
+ * Returns the current API token.
10
+ */
11
+ export function getApiToken() {
12
+ return apiToken;
13
+ }
14
+ /**
15
+ * Returns true if an API token is configured.
16
+ */
17
+ export function isConfigured() {
18
+ return apiToken.length > 0;
19
+ }
20
+ /**
21
+ * Update the API token at runtime (e.g. after configure_mixmax_api_key).
22
+ */
23
+ export function setApiToken(token) {
24
+ apiToken = token;
25
+ }
26
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Path to bridge state file, supporting both current and legacy env vars.
3
+ */
4
+ export declare const BRIDGE_STATE_PATH: string;
5
+ /**
6
+ * Send a request to the host app bridge.
7
+ *
8
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
9
+ * that handles credential management and other cross-process operations.
10
+ */
11
+ export declare const bridgeRequest: (urlPath: string, body: Record<string, unknown>) => Promise<{
12
+ success: boolean;
13
+ warning?: string;
14
+ error?: string;
15
+ }>;
16
+ //# sourceMappingURL=bridge.d.ts.map
package/dist/bridge.js ADDED
@@ -0,0 +1,43 @@
1
+ import * as fs from 'fs';
2
+ import { REQUEST_TIMEOUT_MS } from './types.js';
3
+ /**
4
+ * Path to bridge state file, supporting both current and legacy env vars.
5
+ */
6
+ export const BRIDGE_STATE_PATH = process.env.MCP_HOST_BRIDGE_STATE || process.env.MINDSTONE_REBEL_BRIDGE_STATE || '';
7
+ const loadBridgeState = () => {
8
+ if (!BRIDGE_STATE_PATH)
9
+ return null;
10
+ try {
11
+ const raw = fs.readFileSync(BRIDGE_STATE_PATH, 'utf8');
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ };
18
+ /**
19
+ * Send a request to the host app bridge.
20
+ *
21
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
22
+ * that handles credential management and other cross-process operations.
23
+ */
24
+ export const bridgeRequest = async (urlPath, body) => {
25
+ const bridge = loadBridgeState();
26
+ if (!bridge) {
27
+ return { success: false, error: 'Bridge not available' };
28
+ }
29
+ const response = await fetch(`http://127.0.0.1:${bridge.port}${urlPath}`, {
30
+ method: 'POST',
31
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Authorization: `Bearer ${bridge.token}`,
35
+ },
36
+ body: JSON.stringify(body),
37
+ });
38
+ if (response.status === 401 || response.status === 403) {
39
+ return { success: false, error: `Bridge returned ${response.status}: unauthorized. Check host app authentication.` };
40
+ }
41
+ return response.json();
42
+ };
43
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Mixmax API HTTP client.
3
+ *
4
+ * Centralises X-API-Token header injection, error handling, and rate-limit
5
+ * messaging for all Mixmax API calls.
6
+ */
7
+ /**
8
+ * Make an authenticated request to the Mixmax API.
9
+ *
10
+ * @param path API path relative to base, e.g. `/sequences`
11
+ * @param options Additional fetch options
12
+ * @returns Parsed JSON response
13
+ */
14
+ export declare function mixmaxFetch<T>(path: string, options?: RequestInit): Promise<T>;
15
+ //# sourceMappingURL=client.d.ts.map
package/dist/client.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Mixmax API HTTP client.
3
+ *
4
+ * Centralises X-API-Token header injection, error handling, and rate-limit
5
+ * messaging for all Mixmax API calls.
6
+ */
7
+ import { getApiToken } from './auth.js';
8
+ import { MixmaxError, MIXMAX_API_BASE, REQUEST_TIMEOUT_MS } from './types.js';
9
+ /**
10
+ * Make an authenticated request to the Mixmax API.
11
+ *
12
+ * @param path API path relative to base, e.g. `/sequences`
13
+ * @param options Additional fetch options
14
+ * @returns Parsed JSON response
15
+ */
16
+ export async function mixmaxFetch(path, options = {}) {
17
+ const token = getApiToken();
18
+ if (!token) {
19
+ throw new MixmaxError('Mixmax API token not configured', 'AUTH_REQUIRED', 'Use configure_mixmax_api_key to set your API token first.');
20
+ }
21
+ const url = `${MIXMAX_API_BASE}${path}`;
22
+ let response;
23
+ try {
24
+ response = await fetch(url, {
25
+ ...options,
26
+ signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
27
+ headers: {
28
+ 'X-API-Token': token,
29
+ 'Content-Type': 'application/json',
30
+ ...options.headers,
31
+ },
32
+ });
33
+ }
34
+ catch (error) {
35
+ if (error instanceof Error && error.name === 'TimeoutError') {
36
+ throw new MixmaxError('Request to Mixmax API timed out', 'TIMEOUT', 'The request took too long. Try again or check if the Mixmax API is available.');
37
+ }
38
+ throw error;
39
+ }
40
+ if (response.status === 401 || response.status === 403) {
41
+ throw new MixmaxError('Authentication failed', 'AUTH_FAILED', 'Your Mixmax API token is invalid or revoked. Use configure_mixmax_api_key to set a new token.');
42
+ }
43
+ if (response.status === 429) {
44
+ const retryAfter = response.headers.get('Retry-After');
45
+ const parsed = retryAfter ? parseInt(retryAfter, 10) : NaN;
46
+ const waitSeconds = Number.isFinite(parsed) ? parsed : 60;
47
+ throw new MixmaxError(`Rate limited by Mixmax API. Please wait ${waitSeconds} seconds before retrying.`, 'RATE_LIMITED', `Wait ${waitSeconds} seconds and try again. Mixmax limits to 120 requests per minute.`);
48
+ }
49
+ if (response.status === 404) {
50
+ throw new MixmaxError('Resource not found', 'NOT_FOUND', 'The requested resource does not exist or you do not have permission to access it.');
51
+ }
52
+ if (!response.ok) {
53
+ const errorText = await response.text().catch(() => 'Unknown error');
54
+ throw new MixmaxError(`Mixmax API error (${response.status}): ${errorText}`, 'API_ERROR', 'Check the request parameters and try again.');
55
+ }
56
+ // Some endpoints (e.g. POST with 204) may return empty body
57
+ const text = await response.text();
58
+ if (!text)
59
+ return {};
60
+ return JSON.parse(text);
61
+ }
62
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mixmax MCP Server
4
+ *
5
+ * Provides Mixmax email productivity integration via Model Context Protocol.
6
+ * Supports sequences (drip campaigns), email sending with tracking,
7
+ * templates (snippets), meeting scheduling, and user info.
8
+ *
9
+ * Environment variables:
10
+ * - MIXMAX_API_TOKEN: User's Mixmax API token — required for all operations
11
+ * - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
12
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mixmax MCP Server
4
+ *
5
+ * Provides Mixmax email productivity integration via Model Context Protocol.
6
+ * Supports sequences (drip campaigns), email sending with tracking,
7
+ * templates (snippets), meeting scheduling, and user info.
8
+ *
9
+ * Environment variables:
10
+ * - MIXMAX_API_TOKEN: User's Mixmax API token — required for all operations
11
+ * - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
12
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
13
+ */
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { createServer } from './server.js';
16
+ async function main() {
17
+ const server = createServer();
18
+ const transport = new StdioServerTransport();
19
+ await server.connect(transport);
20
+ console.error('Mixmax MCP server running on stdio');
21
+ }
22
+ main().catch((error) => {
23
+ console.error('Fatal error:', error);
24
+ process.exit(1);
25
+ });
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer(): McpServer;
3
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,16 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerConfigureTools, registerSequenceTools, registerMessageTools, registerSnippetTools, registerMeetingTools, registerUserTools, } from './tools/index.js';
3
+ export function createServer() {
4
+ const server = new McpServer({
5
+ name: 'mixmax-mcp-server',
6
+ version: '0.1.0',
7
+ });
8
+ registerConfigureTools(server);
9
+ registerSequenceTools(server);
10
+ registerMessageTools(server);
11
+ registerSnippetTools(server);
12
+ registerMeetingTools(server);
13
+ registerUserTools(server);
14
+ return server;
15
+ }
16
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerConfigureTools(server: McpServer): void;
3
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import { setApiToken } from '../auth.js';
3
+ import { bridgeRequest, BRIDGE_STATE_PATH } from '../bridge.js';
4
+ import { MixmaxError } from '../types.js';
5
+ import { withErrorHandling } from '../utils.js';
6
+ export function registerConfigureTools(server) {
7
+ server.registerTool('configure_mixmax_api_key', {
8
+ description: 'Configure the Mixmax API token. Call this tool when the user provides their Mixmax API token. ' +
9
+ 'Get your token from https://app.mixmax.com/dashboard/settings/personal/integrations — ' +
10
+ 'scroll to "API Key" section and click "Generate Token". ' +
11
+ 'Requires a Mixmax Growth or Enterprise annual plan.',
12
+ inputSchema: z.object({
13
+ api_key: z.string().min(1).describe('The Mixmax API token from Settings > Integrations'),
14
+ }),
15
+ annotations: { readOnlyHint: false, destructiveHint: false },
16
+ }, withErrorHandling(async (args) => {
17
+ const trimmedToken = args.api_key.trim();
18
+ // If bridge is available, persist via bridge
19
+ if (BRIDGE_STATE_PATH) {
20
+ try {
21
+ const result = await bridgeRequest('/bundled/mixmax/configure', { apiKey: trimmedToken });
22
+ if (result.success) {
23
+ setApiToken(trimmedToken);
24
+ const message = result.warning
25
+ ? `Mixmax API token configured successfully. Note: ${result.warning}`
26
+ : 'Mixmax API token configured successfully! You can now use Mixmax tools to manage sequences, emails, and templates.';
27
+ return JSON.stringify({ ok: true, message });
28
+ }
29
+ // Bridge returned failure — surface as error, do NOT fall through
30
+ throw new MixmaxError(result.error || 'Bridge configuration failed', 'BRIDGE_ERROR', 'The host app bridge rejected the configuration request. Check the host app logs.');
31
+ }
32
+ catch (error) {
33
+ if (error instanceof MixmaxError)
34
+ throw error;
35
+ // Bridge request failed (network, timeout, etc.) — surface as error
36
+ throw new MixmaxError(`Bridge request failed: ${error instanceof Error ? error.message : String(error)}`, 'BRIDGE_ERROR', 'Could not reach the host app bridge. Ensure the host app is running.');
37
+ }
38
+ }
39
+ // No bridge configured — configure in-memory only
40
+ setApiToken(trimmedToken);
41
+ return JSON.stringify({
42
+ ok: true,
43
+ message: 'Mixmax API token configured successfully! You can now use Mixmax tools to manage sequences, emails, and templates.',
44
+ });
45
+ }));
46
+ }
47
+ //# sourceMappingURL=configure.js.map
@@ -0,0 +1,7 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerSequenceTools } from './sequences.js';
3
+ export { registerMessageTools } from './messages.js';
4
+ export { registerSnippetTools } from './snippets.js';
5
+ export { registerMeetingTools } from './meetings.js';
6
+ export { registerUserTools } from './user.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,7 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerSequenceTools } from './sequences.js';
3
+ export { registerMessageTools } from './messages.js';
4
+ export { registerSnippetTools } from './snippets.js';
5
+ export { registerMeetingTools } from './meetings.js';
6
+ export { registerUserTools } from './user.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerMeetingTools(server: McpServer): void;
3
+ //# sourceMappingURL=meetings.d.ts.map
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod';
2
+ import { mixmaxFetch } from '../client.js';
3
+ import { withErrorHandling } from '../utils.js';
4
+ import { isConfigured } from '../auth.js';
5
+ function noApiTokenError() {
6
+ return JSON.stringify({
7
+ ok: false,
8
+ error: 'Mixmax API token not configured',
9
+ resolution: 'To use Mixmax, you need to configure an API token first.',
10
+ next_step: {
11
+ action: 'Ask the user for their Mixmax API token, then call configure_mixmax_api_key',
12
+ tool_to_call: 'configure_mixmax_api_key',
13
+ tool_parameters: { api_key: '<user_provided_token>' },
14
+ get_token_from: 'Mixmax Settings > Integrations > API Key section (requires Growth or Enterprise annual plan)',
15
+ },
16
+ });
17
+ }
18
+ export function registerMeetingTools(server) {
19
+ server.registerTool('list_mixmax_meeting_types', {
20
+ description: `List Mixmax meeting/scheduling link types configured by the user.
21
+
22
+ Returns for each meeting type:
23
+ - name: Meeting type label (e.g. "30 min intro call", "60 min deep dive")
24
+ - duration: Length in minutes
25
+ - location: Where the meeting happens (Zoom, Google Meet, phone, etc.)
26
+ - slug / link: The booking URL that can be shared with contacts
27
+
28
+ USE CASES:
29
+ - "Share my scheduling link" — find the meeting type, give the user the booking URL to share
30
+ - "What meeting types do I have?" — list them with durations and locations
31
+ - "Send Alice my 30-min call link" — find the right type, then use the URL in send_mixmax_email`,
32
+ inputSchema: z.object({}),
33
+ annotations: { readOnlyHint: true },
34
+ }, withErrorHandling(async () => {
35
+ if (!isConfigured())
36
+ return noApiTokenError();
37
+ const data = await mixmaxFetch('/meetingtypes');
38
+ return JSON.stringify({
39
+ ok: true,
40
+ meetingTypes: data.results || data,
41
+ count: Array.isArray(data.results) ? data.results.length : undefined,
42
+ });
43
+ }));
44
+ }
45
+ //# sourceMappingURL=meetings.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerMessageTools(server: McpServer): void;
3
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { z } from 'zod';
2
+ import { mixmaxFetch } from '../client.js';
3
+ import { withErrorHandling } from '../utils.js';
4
+ import { isConfigured } from '../auth.js';
5
+ function noApiTokenError() {
6
+ return JSON.stringify({
7
+ ok: false,
8
+ error: 'Mixmax API token not configured',
9
+ resolution: 'To use Mixmax, you need to configure an API token first.',
10
+ next_step: {
11
+ action: 'Ask the user for their Mixmax API token, then call configure_mixmax_api_key',
12
+ tool_to_call: 'configure_mixmax_api_key',
13
+ tool_parameters: { api_key: '<user_provided_token>' },
14
+ get_token_from: 'Mixmax Settings > Integrations > API Key section (requires Growth or Enterprise annual plan)',
15
+ },
16
+ });
17
+ }
18
+ export function registerMessageTools(server) {
19
+ server.registerTool('list_mixmax_messages', {
20
+ description: `List emails sent through Mixmax with open/click tracking data.
21
+
22
+ Returns for each message:
23
+ - _id, subject, recipients (to/cc/bcc)
24
+ - sentAt / scheduledAt: When it was sent or is scheduled
25
+ - opens: Number of times opened, with timestamps
26
+ - clicks: Links clicked, with URLs and timestamps
27
+ - state: "sent", "draft", or "scheduled"
28
+
29
+ USE CASES:
30
+ - "Show my recent emails" — call with no params
31
+ - "Did they open my email?" — find the message, check opens count/timestamps
32
+ - "What emails are scheduled?" — look for state: "scheduled"
33
+
34
+ PAGINATION: Cursor-based. If hasNext is true, pass the "next" value as the next parameter.`,
35
+ inputSchema: z.object({
36
+ limit: z.number().min(1).max(100).default(25).describe('Maximum results to return (default: 25)'),
37
+ next: z.string().optional().describe('Cursor for next page (from previous response)'),
38
+ }),
39
+ annotations: { readOnlyHint: true },
40
+ }, withErrorHandling(async (args) => {
41
+ if (!isConfigured())
42
+ return noApiTokenError();
43
+ let path = `/messages?limit=${args.limit}`;
44
+ if (args.next)
45
+ path += `&next=${encodeURIComponent(args.next)}`;
46
+ const data = await mixmaxFetch(path);
47
+ return JSON.stringify({
48
+ ok: true,
49
+ messages: data.results || [],
50
+ count: (data.results || []).length,
51
+ hasNext: data.hasNext ?? false,
52
+ ...(data.next ? { next: data.next } : {}),
53
+ });
54
+ }));
55
+ server.registerTool('send_mixmax_email', {
56
+ description: `Send an email via Mixmax through the user's connected Gmail account. Open and click tracking is enabled automatically.
57
+
58
+ IMPORTANT: Confirm with the user before sending — this sends a real email immediately.
59
+
60
+ BODY FORMAT: HTML is supported. Use <p>, <br>, <b>, <ul>, etc. for formatting. Plain text also works.
61
+
62
+ TRACKING: Mixmax automatically tracks opens and clicks. The user can check tracking data later via list_mixmax_messages.
63
+
64
+ NOTE: This sends through Mixmax, not raw Gmail. The email will appear in the user's Gmail sent folder with Mixmax tracking pixels.`,
65
+ inputSchema: z.object({
66
+ to: z.array(z.string().email()).min(1).describe('Recipient email addresses'),
67
+ subject: z.string().min(1).describe('Email subject line'),
68
+ body: z.string().min(1).describe('Email body (HTML supported — use <p>, <br>, <b>, <ul> for formatting)'),
69
+ cc: z.array(z.string().email()).optional().describe('CC recipient email addresses (optional)'),
70
+ bcc: z.array(z.string().email()).optional().describe('BCC recipient email addresses (optional)'),
71
+ }),
72
+ annotations: { readOnlyHint: false, destructiveHint: false },
73
+ }, withErrorHandling(async (args) => {
74
+ if (!isConfigured())
75
+ return noApiTokenError();
76
+ const payload = {
77
+ to: args.to.map((email) => ({ email })),
78
+ subject: args.subject,
79
+ body: args.body,
80
+ };
81
+ if (args.cc && args.cc.length > 0)
82
+ payload.cc = args.cc.map((email) => ({ email }));
83
+ if (args.bcc && args.bcc.length > 0)
84
+ payload.bcc = args.bcc.map((email) => ({ email }));
85
+ const data = await mixmaxFetch('/send', {
86
+ method: 'POST',
87
+ body: JSON.stringify(payload),
88
+ });
89
+ return JSON.stringify({
90
+ ok: true,
91
+ message: `Email sent to ${args.to.join(', ')}.`,
92
+ result: data,
93
+ });
94
+ }));
95
+ }
96
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerSequenceTools(server: McpServer): void;
3
+ //# sourceMappingURL=sequences.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { z } from 'zod';
2
+ import { mixmaxFetch } from '../client.js';
3
+ import { withErrorHandling } from '../utils.js';
4
+ import { isConfigured } from '../auth.js';
5
+ function noApiTokenError() {
6
+ return JSON.stringify({
7
+ ok: false,
8
+ error: 'Mixmax API token not configured',
9
+ resolution: 'To use Mixmax, you need to configure an API token first.',
10
+ next_step: {
11
+ action: 'Ask the user for their Mixmax API token, then call configure_mixmax_api_key',
12
+ tool_to_call: 'configure_mixmax_api_key',
13
+ tool_parameters: { api_key: '<user_provided_token>' },
14
+ get_token_from: 'Mixmax Settings > Integrations > API Key section (requires Growth or Enterprise annual plan)',
15
+ },
16
+ });
17
+ }
18
+ export function registerSequenceTools(server) {
19
+ server.registerTool('list_mixmax_sequences', {
20
+ description: `List Mixmax sequences (automated email drip campaigns).
21
+
22
+ Returns for each sequence:
23
+ - _id: Use with get_mixmax_sequence for full details or add_mixmax_sequence_recipients to enroll people
24
+ - name: Sequence name
25
+ - numStages: Number of email stages in the drip
26
+ - isPaused: Whether the sequence is currently active
27
+ - numRecipients / numFinished / numBounced: Recipient stats
28
+
29
+ TYPICAL WORKFLOW:
30
+ 1. list_mixmax_sequences to find the sequence
31
+ 2. get_mixmax_sequence with the _id to see stages/content
32
+ 3. add_mixmax_sequence_recipients to enroll contacts
33
+
34
+ PAGINATION: Cursor-based. If hasNext is true, pass the "next" value as the next parameter to fetch more.`,
35
+ inputSchema: z.object({
36
+ limit: z.number().min(1).max(100).default(25).describe('Maximum results to return (default: 25)'),
37
+ next: z.string().optional().describe('Cursor for next page (from previous response)'),
38
+ }),
39
+ annotations: { readOnlyHint: true },
40
+ }, withErrorHandling(async (args) => {
41
+ if (!isConfigured())
42
+ return noApiTokenError();
43
+ let path = `/sequences?limit=${args.limit}`;
44
+ if (args.next)
45
+ path += `&next=${encodeURIComponent(args.next)}`;
46
+ const data = await mixmaxFetch(path);
47
+ return JSON.stringify({
48
+ ok: true,
49
+ sequences: data.results || [],
50
+ count: (data.results || []).length,
51
+ hasNext: data.hasNext ?? false,
52
+ ...(data.next ? { next: data.next } : {}),
53
+ });
54
+ }));
55
+ server.registerTool('get_mixmax_sequence', {
56
+ description: `Get full details for a single Mixmax sequence including all stages.
57
+
58
+ Returns:
59
+ - name, isPaused, createdAt
60
+ - stages: Array of email steps, each with subject line, body content, delay settings, and send window
61
+ - Recipient statistics (total, finished, bounced, paused)
62
+ - Scheduling rules (send window, timezone, skip weekends)
63
+
64
+ USE list_mixmax_sequences FIRST to find the _id.`,
65
+ inputSchema: z.object({
66
+ sequenceId: z.string().min(1).describe('The _id of the sequence (from list_mixmax_sequences)'),
67
+ }),
68
+ annotations: { readOnlyHint: true },
69
+ }, withErrorHandling(async (args) => {
70
+ if (!isConfigured())
71
+ return noApiTokenError();
72
+ const data = await mixmaxFetch(`/sequences/${encodeURIComponent(args.sequenceId)}?expand=stages`);
73
+ return JSON.stringify({ ok: true, sequence: data });
74
+ }));
75
+ server.registerTool('add_mixmax_sequence_recipients', {
76
+ description: `Add recipients to a Mixmax sequence, enrolling them in the automated email drip campaign.
77
+
78
+ IMPORTANT: Confirm with the user before calling — this adds real people to a live sequence and they WILL receive emails starting from stage 1.
79
+
80
+ WORKFLOW:
81
+ 1. list_mixmax_sequences to find the sequence _id
82
+ 2. Optionally get_mixmax_sequence to review the stages/content with the user
83
+ 3. Confirm recipient list with user
84
+ 4. Call this tool
85
+
86
+ TEMPLATE VARIABLES: If the sequence stages use variables like {{first_name}}, pass them in the variables object for each recipient.`,
87
+ inputSchema: z.object({
88
+ sequenceId: z.string().min(1).describe('The _id of the sequence to add recipients to'),
89
+ recipients: z.array(z.object({
90
+ email: z.string().email().describe('Recipient email address'),
91
+ variables: z.record(z.unknown()).optional().describe('Template variables for personalisation'),
92
+ })).min(1).describe('Array of recipients to add (each must have an email)'),
93
+ }),
94
+ annotations: { readOnlyHint: false, destructiveHint: false },
95
+ }, withErrorHandling(async (args) => {
96
+ if (!isConfigured())
97
+ return noApiTokenError();
98
+ const data = await mixmaxFetch(`/sequences/${encodeURIComponent(args.sequenceId)}/recipients`, {
99
+ method: 'POST',
100
+ body: JSON.stringify({ recipients: args.recipients }),
101
+ });
102
+ return JSON.stringify({
103
+ ok: true,
104
+ message: `Added ${args.recipients.length} recipient(s) to sequence.`,
105
+ result: data,
106
+ });
107
+ }));
108
+ }
109
+ //# sourceMappingURL=sequences.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerSnippetTools(server: McpServer): void;
3
+ //# sourceMappingURL=snippets.d.ts.map
@@ -0,0 +1,92 @@
1
+ import { z } from 'zod';
2
+ import { mixmaxFetch } from '../client.js';
3
+ import { withErrorHandling } from '../utils.js';
4
+ import { isConfigured } from '../auth.js';
5
+ function noApiTokenError() {
6
+ return JSON.stringify({
7
+ ok: false,
8
+ error: 'Mixmax API token not configured',
9
+ resolution: 'To use Mixmax, you need to configure an API token first.',
10
+ next_step: {
11
+ action: 'Ask the user for their Mixmax API token, then call configure_mixmax_api_key',
12
+ tool_to_call: 'configure_mixmax_api_key',
13
+ tool_parameters: { api_key: '<user_provided_token>' },
14
+ get_token_from: 'Mixmax Settings > Integrations > API Key section (requires Growth or Enterprise annual plan)',
15
+ },
16
+ });
17
+ }
18
+ export function registerSnippetTools(server) {
19
+ server.registerTool('list_mixmax_snippets', {
20
+ description: `List Mixmax email templates (called "snippets" in Mixmax).
21
+
22
+ Returns for each snippet:
23
+ - _id: Use with send_mixmax_snippet to send it
24
+ - name: Template name
25
+ - subject: Email subject line the template uses
26
+ - body: HTML body content (check for template variables like {{first_name}})
27
+ - isShared: Whether it's shared with the team or personal
28
+
29
+ WORKFLOW FOR SENDING A TEMPLATE:
30
+ 1. list_mixmax_snippets to browse available templates
31
+ 2. Review the body for template variables (e.g. {{first_name}}, {{company}})
32
+ 3. send_mixmax_snippet with the _id, recipients, and matching variables
33
+
34
+ PAGINATION: Cursor-based. If hasNext is true, pass the "next" value as the next parameter.`,
35
+ inputSchema: z.object({
36
+ limit: z.number().min(1).max(100).default(25).describe('Maximum results to return (default: 25)'),
37
+ next: z.string().optional().describe('Cursor for next page (from previous response)'),
38
+ }),
39
+ annotations: { readOnlyHint: true },
40
+ }, withErrorHandling(async (args) => {
41
+ if (!isConfigured())
42
+ return noApiTokenError();
43
+ let path = `/snippets?limit=${args.limit}`;
44
+ if (args.next)
45
+ path += `&next=${encodeURIComponent(args.next)}`;
46
+ const data = await mixmaxFetch(path);
47
+ return JSON.stringify({
48
+ ok: true,
49
+ snippets: data.results || [],
50
+ count: (data.results || []).length,
51
+ hasNext: data.hasNext ?? false,
52
+ ...(data.next ? { next: data.next } : {}),
53
+ });
54
+ }));
55
+ server.registerTool('send_mixmax_snippet', {
56
+ description: `Send a Mixmax template (snippet) to one or more recipients.
57
+
58
+ IMPORTANT: Confirm with the user before sending — this sends a real email using the template content.
59
+
60
+ WORKFLOW:
61
+ 1. list_mixmax_snippets to find the template and its _id
62
+ 2. Check the snippet body for template variables (e.g. {{first_name}}, {{company}})
63
+ 3. Confirm recipients and variable values with user
64
+ 4. Call this tool with matching variables
65
+
66
+ NOTE: Variables are applied to ALL recipients equally. If you need different variables per recipient, send one at a time.`,
67
+ inputSchema: z.object({
68
+ snippetId: z.string().min(1).describe('The _id of the snippet/template (from list_mixmax_snippets)'),
69
+ to: z.array(z.string().email()).min(1).describe('Recipient email addresses'),
70
+ variables: z.record(z.unknown()).optional().describe('Template variables matching {{placeholders}} in the snippet body'),
71
+ }),
72
+ annotations: { readOnlyHint: false, destructiveHint: false },
73
+ }, withErrorHandling(async (args) => {
74
+ if (!isConfigured())
75
+ return noApiTokenError();
76
+ const payload = {
77
+ to: args.to.map((email) => ({ email })),
78
+ };
79
+ if (args.variables)
80
+ payload.variables = args.variables;
81
+ const data = await mixmaxFetch(`/snippets/${encodeURIComponent(args.snippetId)}/send`, {
82
+ method: 'POST',
83
+ body: JSON.stringify(payload),
84
+ });
85
+ return JSON.stringify({
86
+ ok: true,
87
+ message: `Snippet sent to ${args.to.join(', ')}.`,
88
+ result: data,
89
+ });
90
+ }));
91
+ }
92
+ //# sourceMappingURL=snippets.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerUserTools(server: McpServer): void;
3
+ //# sourceMappingURL=user.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ import { mixmaxFetch } from '../client.js';
3
+ import { withErrorHandling } from '../utils.js';
4
+ import { isConfigured } from '../auth.js';
5
+ function noApiTokenError() {
6
+ return JSON.stringify({
7
+ ok: false,
8
+ error: 'Mixmax API token not configured',
9
+ resolution: 'To use Mixmax, you need to configure an API token first.',
10
+ next_step: {
11
+ action: 'Ask the user for their Mixmax API token, then call configure_mixmax_api_key',
12
+ tool_to_call: 'configure_mixmax_api_key',
13
+ tool_parameters: { api_key: '<user_provided_token>' },
14
+ get_token_from: 'Mixmax Settings > Integrations > API Key section (requires Growth or Enterprise annual plan)',
15
+ },
16
+ });
17
+ }
18
+ export function registerUserTools(server) {
19
+ server.registerTool('get_mixmax_user', {
20
+ description: `Get the current Mixmax user's profile and account info.
21
+
22
+ Returns:
23
+ - name, email: User identity
24
+ - plan: Current Mixmax plan (Growth, Enterprise, etc.)
25
+ - integrations: Connected services (Gmail, Salesforce, etc.)
26
+
27
+ USE CASES:
28
+ - Verify which account is connected
29
+ - Check plan level (relevant if a feature requires Enterprise)
30
+ - See what integrations the user has active`,
31
+ inputSchema: z.object({}),
32
+ annotations: { readOnlyHint: true },
33
+ }, withErrorHandling(async () => {
34
+ if (!isConfigured())
35
+ return noApiTokenError();
36
+ const data = await mixmaxFetch('/users/me');
37
+ return JSON.stringify({ ok: true, user: data });
38
+ }));
39
+ }
40
+ //# sourceMappingURL=user.js.map
@@ -0,0 +1,71 @@
1
+ export declare const REQUEST_TIMEOUT_MS = 30000;
2
+ export declare const MIXMAX_API_BASE = "https://api.mixmax.com/v1";
3
+ export interface BridgeState {
4
+ port: number;
5
+ token: string;
6
+ }
7
+ export declare class MixmaxError extends Error {
8
+ readonly code: string;
9
+ readonly resolution: string;
10
+ constructor(message: string, code: string, resolution: string);
11
+ }
12
+ export interface SequenceItem {
13
+ _id: string;
14
+ name: string;
15
+ numStages?: number;
16
+ isPaused?: boolean;
17
+ numRecipients?: number;
18
+ numFinished?: number;
19
+ numBounced?: number;
20
+ createdAt?: string;
21
+ }
22
+ export interface SequencesResponse {
23
+ results: SequenceItem[];
24
+ hasNext?: boolean;
25
+ next?: string;
26
+ }
27
+ export interface MessagesResponse {
28
+ results: MessageItem[];
29
+ hasNext?: boolean;
30
+ next?: string;
31
+ }
32
+ export interface MessageItem {
33
+ _id: string;
34
+ subject?: string;
35
+ recipients?: {
36
+ to?: Array<{
37
+ email: string;
38
+ }>;
39
+ cc?: Array<{
40
+ email: string;
41
+ }>;
42
+ bcc?: Array<{
43
+ email: string;
44
+ }>;
45
+ };
46
+ sentAt?: string;
47
+ scheduledAt?: string;
48
+ opens?: number;
49
+ clicks?: number;
50
+ state?: string;
51
+ }
52
+ export interface SnippetsResponse {
53
+ results: SnippetItem[];
54
+ hasNext?: boolean;
55
+ next?: string;
56
+ }
57
+ export interface SnippetItem {
58
+ _id: string;
59
+ name?: string;
60
+ subject?: string;
61
+ body?: string;
62
+ isShared?: boolean;
63
+ }
64
+ export interface MeetingType {
65
+ name?: string;
66
+ duration?: number;
67
+ location?: string;
68
+ slug?: string;
69
+ link?: string;
70
+ }
71
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ export const REQUEST_TIMEOUT_MS = 30_000;
2
+ export const MIXMAX_API_BASE = 'https://api.mixmax.com/v1';
3
+ export class MixmaxError extends Error {
4
+ code;
5
+ resolution;
6
+ constructor(message, code, resolution) {
7
+ super(message);
8
+ this.code = code;
9
+ this.resolution = resolution;
10
+ this.name = 'MixmaxError';
11
+ }
12
+ }
13
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,14 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ type ToolHandler<T> = (args: T, extra: unknown) => Promise<CallToolResult>;
3
+ /**
4
+ * Wraps a tool handler with standard error handling.
5
+ *
6
+ * - On success: returns the string result as a text content block.
7
+ * - On MixmaxError: returns a structured JSON error with code and resolution.
8
+ * - On unknown error: returns a generic error message.
9
+ *
10
+ * Secrets are never exposed in error messages.
11
+ */
12
+ export declare function withErrorHandling<T>(fn: (args: T, extra: unknown) => Promise<string>): ToolHandler<T>;
13
+ export {};
14
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,42 @@
1
+ import { MixmaxError } from './types.js';
2
+ /**
3
+ * Wraps a tool handler with standard error handling.
4
+ *
5
+ * - On success: returns the string result as a text content block.
6
+ * - On MixmaxError: returns a structured JSON error with code and resolution.
7
+ * - On unknown error: returns a generic error message.
8
+ *
9
+ * Secrets are never exposed in error messages.
10
+ */
11
+ export function withErrorHandling(fn) {
12
+ return async (args, extra) => {
13
+ try {
14
+ const result = await fn(args, extra);
15
+ return { content: [{ type: 'text', text: result }] };
16
+ }
17
+ catch (error) {
18
+ if (error instanceof MixmaxError) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: JSON.stringify({
24
+ ok: false,
25
+ error: error.message,
26
+ code: error.code,
27
+ resolution: error.resolution,
28
+ }),
29
+ },
30
+ ],
31
+ isError: true,
32
+ };
33
+ }
34
+ const errorMessage = error instanceof Error ? error.message : String(error);
35
+ return {
36
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: errorMessage }) }],
37
+ isError: true,
38
+ };
39
+ }
40
+ };
41
+ }
42
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mindstone-engineering/mcp-server-mixmax",
3
+ "version": "0.1.0",
4
+ "description": "Mixmax email productivity MCP server for Model Context Protocol hosts",
5
+ "license": "FSL-1.1-MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-server-mixmax": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "!dist/**/*.map"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/nspr-io/mcp-servers.git",
17
+ "directory": "connectors/mixmax"
18
+ },
19
+ "homepage": "https://github.com/nspr-io/mcp-servers/tree/main/connectors/mixmax",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc && shx chmod +x dist/index.js",
25
+ "prepare": "npm run build",
26
+ "watch": "tsc --watch",
27
+ "start": "node dist/index.js",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:coverage": "vitest run --coverage"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.26.0",
34
+ "zod": "^3.23.0"
35
+ },
36
+ "devDependencies": {
37
+ "@mindstone-engineering/mcp-test-harness": "file:../../test-harness",
38
+ "@types/node": "^22",
39
+ "@vitest/coverage-v8": "^4.1.3",
40
+ "msw": "^2.13.2",
41
+ "shx": "^0.3.4",
42
+ "typescript": "^5.8.2",
43
+ "vitest": "^4.1.3"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ }
48
+ }