@motorical/mcp 1.0.4 → 1.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
@@ -13,7 +13,7 @@ A **Motorical SMTP Motor Block** is an isolated sending stream (similar to a per
13
13
  | `motorical_get_send_status` | none | `GET /v1/status` |
14
14
  | `motorical_mint_public_token` | `ak_live_…` | Mint Public API bearer |
15
15
  | `motorical_list_motor_blocks` | bearer (auto-mint) | List Motor Blocks |
16
- | `motorical_send_email` | `mk_live_…` | Transactional `POST /v1/send` (**default `dryRun: true`**) |
16
+ | `motorical_send_email` | `mk_live_…` | Transactional `POST /v1/send` (**default `dryRun: true`**; optional **`fromName`**) |
17
17
  | `motorical_get_message` | bearer | Message by UUID |
18
18
  | `motorical_get_message_events` | bearer | Delivery lifecycle events |
19
19
  | `motorical_sandbox_status` | `MOTORICAL_JWT` | Developer sandbox status |
@@ -23,7 +23,7 @@ A **Motorical SMTP Motor Block** is an isolated sending stream (similar to a per
23
23
  **Resources:** `motorical://docs/llms.txt`, `motorical://docs/openapi.json`
24
24
  **Prompt:** `motorical_integrate_send`
25
25
 
26
- Safety: real sends require `dryRun: false` **and** `confirmRealSend: true`. Sandbox outbound is allowlist-locked until convert.
26
+ Safety: real sends require `dryRun: false` **and** `confirmRealSend: true`. Sandbox outbound is allowlist-locked until convert. Optional `fromName` sets the inbox display name (same as HTTP `/v1/send` / CLI `--from-name`); do not put `From` in custom headers.
27
27
 
28
28
  ## Environment
29
29
 
@@ -46,30 +46,25 @@ Add to MCP config (e.g. Cursor Settings → MCP):
46
46
  {
47
47
  "mcpServers": {
48
48
  "motorical": {
49
- "command": "node",
50
- "args": ["/root/motoric_smtp/packages/motorical-mcp/src/index.js"],
49
+ "command": "npx",
50
+ "args": ["-y", "@motorical/mcp"],
51
51
  "env": {
52
52
  "MOTORICAL_MK_API_KEY": "mk_live_…",
53
53
  "MOTORICAL_AK_API_KEY": "ak_live_…",
54
54
  "MOTORICAL_MOTOR_BLOCK_ID": "your-block-uuid",
55
- "MOTORICAL_DEFAULT_FROM": "noreply@yourdomain.com"
55
+ "MOTORICAL_DEFAULT_FROM": "noreply@yourdomain.com",
56
+ "MOTORICAL_JWT": "optional-dashboard-jwt-for-sandbox-tools"
56
57
  }
57
58
  }
58
59
  }
59
60
  }
60
61
  ```
61
62
 
62
- On a developer Mac after cloning:
63
+ From a clone of this repo:
63
64
 
64
65
  ```bash
65
66
  cd packages/motorical-mcp && npm ci
66
- # point args at the absolute path to src/index.js
67
- ```
68
-
69
- Or via npx once published:
70
-
71
- ```json
72
- "args": ["-y", "@motorical/mcp"]
67
+ # point command/args at node + absolute path to src/index.js
73
68
  ```
74
69
 
75
70
  ## Develop / test
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@motorical/mcp",
3
- "version": "1.0.4",
4
- "description": "MCP server for Motorical transactional email API dry-run/send, mint public tokens, list Motor Blocks, inspect delivery events",
3
+ "version": "1.1.0",
4
+ "description": "MCP server for Motorical transactional email API \u2014 dry-run/send, mint public tokens, list Motor Blocks, inspect delivery events",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "motorical-mcp": "./src/index.js"
@@ -42,6 +42,11 @@
42
42
  },
43
43
  "homepage": "https://docs.motorical.com/ai-mcp",
44
44
  "bugs": {
45
- "url": "https://docs.motorical.com/ai-mcp"
45
+ "url": "https://github.com/motorical-smtp/motorical-packages/issues"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/motorical-smtp/motorical-packages.git",
50
+ "directory": "packages/motorical-mcp"
46
51
  }
47
52
  }
package/src/client.js CHANGED
@@ -24,22 +24,18 @@ export class MotoricalClient {
24
24
  this.config = config;
25
25
  /** @type {string|null} */
26
26
  this._cachedBearer = config.bearerToken || null;
27
+ /** @type {string|null} */
28
+ this._cachedMkKey = null;
27
29
  }
28
30
 
29
31
  requireMk() {
32
+ if (this._cachedMkKey) return this._cachedMkKey;
30
33
  if (!this.config.mkApiKey) {
31
34
  throw new Error('MOTORICAL_MK_API_KEY is required (mk_live_... Motor Block API key for POST /v1/send)');
32
35
  }
33
36
  return this.config.mkApiKey;
34
37
  }
35
38
 
36
- requireAk() {
37
- if (!this.config.akApiKey) {
38
- throw new Error('MOTORICAL_AK_API_KEY is required (ak_live_... Account API key to mint public tokens)');
39
- }
40
- return this.config.akApiKey;
41
- }
42
-
43
39
  async request(method, path, { headers = {}, body, apiKey, bearer } = {}) {
44
40
  const url = `${this.config.apiBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;
45
41
  const h = { Accept: 'application/json', ...headers };
@@ -73,17 +69,28 @@ export class MotoricalClient {
73
69
 
74
70
  async mintPublicToken({ motorBlockId, scopes, ttlSeconds = 900 } = {}) {
75
71
  const blockId = motorBlockId || this.config.motorBlockId;
76
- if (!blockId) {
77
- throw new Error('motorBlockId is required (argument or MOTORICAL_MOTOR_BLOCK_ID)');
72
+ const hasAk = !!this.config.akApiKey;
73
+ const hasJwt = !!this.config.dashboardJwt;
74
+
75
+ const missing = [];
76
+ if (!blockId) missing.push('motorBlockId (argument or MOTORICAL_MOTOR_BLOCK_ID)');
77
+ if (!hasAk && !hasJwt) {
78
+ missing.push('MOTORICAL_AK_API_KEY (ak_live_... Account API key) or MOTORICAL_JWT (dashboard session)');
78
79
  }
79
- const data = await this.request('POST', '/api/public/token/account-key', {
80
- apiKey: this.requireAk(),
81
- body: {
82
- motorBlockId: blockId,
83
- scopes: scopes || ['logs.read', 'analytics.read', 'webhooks.manage', 'config.read'],
84
- ttlSeconds
85
- }
86
- });
80
+ if (missing.length) {
81
+ throw new Error(`Missing required to mint a public token: ${missing.join('; ')}`);
82
+ }
83
+
84
+ const body = {
85
+ motorBlockId: blockId,
86
+ scopes: scopes || ['logs.read', 'analytics.read', 'webhooks.manage', 'config.read'],
87
+ ttlSeconds
88
+ };
89
+
90
+ const data = hasAk
91
+ ? await this.request('POST', '/api/public/token/account-key', { apiKey: this.config.akApiKey, body })
92
+ : await this.request('POST', '/api/public/token', { bearer: this.config.dashboardJwt, body });
93
+
87
94
  const token = data?.data?.token || data?.token || data?.access_token;
88
95
  if (token) this._cachedBearer = token;
89
96
  return data;
@@ -178,14 +185,29 @@ export class MotoricalClient {
178
185
  }
179
186
 
180
187
  async sandboxStatus() {
181
- return this.request('GET', '/api/developer/sandbox', { bearer: this.requireDashboardJwt() });
188
+ const result = await this.request('GET', '/api/developer/sandbox', {
189
+ bearer: this.requireDashboardJwt()
190
+ });
191
+ // A returning agent that calls status instead of re-provisioning still
192
+ // needs motorBlockId for mintPublicToken()/getBearer() (message lookup).
193
+ const blockId = result?.data?.motorBlock?.id;
194
+ if (blockId && !this.config.motorBlockId) this.config.motorBlockId = blockId;
195
+ return result;
182
196
  }
183
197
 
184
198
  async sandboxProvision({ handle, channel = 'agent' } = {}) {
185
- return this.request('POST', '/api/developer/sandbox/provision', {
199
+ const result = await this.request('POST', '/api/developer/sandbox/provision', {
186
200
  bearer: this.requireDashboardJwt(),
187
201
  body: { handle, channel }
188
202
  });
203
+ const mkKey = result?.data?.credentials?.mkApiKey;
204
+ if (mkKey && !this.config.mkApiKey) this._cachedMkKey = mkKey;
205
+ // Without this a cold JWT-only agent can send but never track: getBearer()
206
+ // throws 'motorBlockId is required'. Same precedence rule as mkApiKey —
207
+ // an explicit MOTORICAL_MOTOR_BLOCK_ID always wins.
208
+ const blockId = result?.data?.motorBlock?.id;
209
+ if (blockId && !this.config.motorBlockId) this.config.motorBlockId = blockId;
210
+ return result;
189
211
  }
190
212
 
191
213
  async sandboxConvert({ domainId }) {
@@ -220,6 +242,23 @@ export class MotoricalClient {
220
242
  });
221
243
  }
222
244
 
245
+ async sandboxAllowlistRequest({ email } = {}) {
246
+ if (!email) throw new Error('email is required');
247
+ return this.request('POST', '/api/developer/sandbox/allowlist/request', {
248
+ bearer: this.requireDashboardJwt(),
249
+ body: { email }
250
+ });
251
+ }
252
+
253
+ async sandboxAllowlistConfirm({ email, code } = {}) {
254
+ if (!email) throw new Error('email is required');
255
+ if (!code) throw new Error('code is required');
256
+ return this.request('POST', '/api/developer/sandbox/allowlist/confirm', {
257
+ bearer: this.requireDashboardJwt(),
258
+ body: { email, code }
259
+ });
260
+ }
261
+
223
262
  async webHandoff({ path } = {}) {
224
263
  return this.request('POST', '/api/auth/web-handoff', {
225
264
  bearer: this.requireDashboardJwt(),
package/src/server.js CHANGED
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import { MotoricalClient, loadConfig } from './client.js';
4
4
 
5
- const PACKAGE_VERSION = '1.0.4';
5
+ const PACKAGE_VERSION = '1.0.5';
6
6
 
7
7
  function jsonResult(data, { isError = false } = {}) {
8
8
  return {
@@ -54,7 +54,8 @@ export function createMotoricalMcpServer(options = {}) {
54
54
  'motorical_mint_public_token',
55
55
  {
56
56
  description:
57
- 'Mint a short-lived Public Analytics API bearer token using MOTORICAL_AK_API_KEY (ak_live_...). ' +
57
+ 'Mint a short-lived Public Analytics API bearer token using MOTORICAL_AK_API_KEY (ak_live_...) ' +
58
+ 'or, when none is configured, MOTORICAL_JWT. ' +
58
59
  'Use for /api/public/v1 logs, analytics, webhooks, config. Not for POST /v1/send.',
59
60
  inputSchema: {
60
61
  motorBlockId: z.string().uuid().optional().describe('Defaults to MOTORICAL_MOTOR_BLOCK_ID'),
@@ -95,7 +96,9 @@ export function createMotoricalMcpServer(options = {}) {
95
96
  description:
96
97
  'Transactional email: send or validate via POST /v1/send using MOTORICAL_MK_API_KEY (mk_live_...). ' +
97
98
  'Defaults to dryRun:true. Real sends require dryRun:false AND confirmRealSend:true. ' +
98
- 'Do not use OAuth access tokens or Bearer tokens here.',
99
+ 'Do not use OAuth access tokens or Bearer tokens here. ' +
100
+ 'For developer-sandbox accounts, a non-allowlisted recipient is redirected to the account ' +
101
+ 'email rather than rejected — check the response\'s sandboxRedirect field.',
99
102
  inputSchema: {
100
103
  from: z.string().email().optional().describe('Defaults to MOTORICAL_DEFAULT_FROM'),
101
104
  fromName: z
@@ -179,6 +182,47 @@ export function createMotoricalMcpServer(options = {}) {
179
182
  }
180
183
  );
181
184
 
185
+ server.registerTool(
186
+ 'motorical_sandbox_allowlist_request',
187
+ {
188
+ description:
189
+ 'Request to add a new recipient to the developer sandbox outbound allowlist. ' +
190
+ 'Sends a 6-digit confirmation code to that address (not the account owner) — ' +
191
+ 'call motorical_sandbox_allowlist_confirm with the code the recipient receives. Requires MOTORICAL_JWT.',
192
+ inputSchema: {
193
+ email: z.string().email()
194
+ }
195
+ },
196
+ async (args) => {
197
+ try {
198
+ return jsonResult(await client.sandboxAllowlistRequest(args));
199
+ } catch (err) {
200
+ return errorResult(err);
201
+ }
202
+ }
203
+ );
204
+
205
+ server.registerTool(
206
+ 'motorical_sandbox_allowlist_confirm',
207
+ {
208
+ description:
209
+ 'Confirm a sandbox allowlist recipient using the 6-digit code sent by ' +
210
+ 'motorical_sandbox_allowlist_request. On success the address is added to the ' +
211
+ 'allowlist and can receive real (non-dryRun) sandbox sends. Requires MOTORICAL_JWT.',
212
+ inputSchema: {
213
+ email: z.string().email(),
214
+ code: z.string().length(6)
215
+ }
216
+ },
217
+ async (args) => {
218
+ try {
219
+ return jsonResult(await client.sandboxAllowlistConfirm(args));
220
+ } catch (err) {
221
+ return errorResult(err);
222
+ }
223
+ }
224
+ );
225
+
182
226
  server.registerTool(
183
227
  'motorical_sandbox_provision',
184
228
  {