@motorical/mcp 1.0.5 → 1.1.1
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 +2 -2
- package/package.json +2 -2
- package/src/client.js +58 -19
- package/src/server.js +46 -2
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
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motorical/mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for Motorical transactional email API
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "MCP server for Motorical transactional email API — 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"
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
{
|