@motorical/mcp 1.0.2
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 +87 -0
- package/package.json +44 -0
- package/src/client.js +236 -0
- package/src/index.js +20 -0
- package/src/server.js +367 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# @motorical/mcp
|
|
2
|
+
|
|
3
|
+
MCP tools for **transactional HTTP send and delivery inspection** on Motorical Sending SMTP (same job class as SendGrid/Postmark-style email APIs).
|
|
4
|
+
|
|
5
|
+
Agents can **execute** Motorical APIs (not only read docs): dry-run / send email, mint public tokens, list Motor Blocks, inspect message events. Discovery docs remain at [docs.motorical.com/llms.txt](https://docs.motorical.com/llms.txt).
|
|
6
|
+
|
|
7
|
+
A **Motorical SMTP Motor Block** is an isolated sending stream (similar to a per-app/per-tenant ESP project).
|
|
8
|
+
|
|
9
|
+
## Tools (v1)
|
|
10
|
+
|
|
11
|
+
| Tool | Auth | Purpose |
|
|
12
|
+
|------|------|---------|
|
|
13
|
+
| `motorical_get_send_status` | none | `GET /v1/status` |
|
|
14
|
+
| `motorical_mint_public_token` | `ak_live_…` | Mint Public API bearer |
|
|
15
|
+
| `motorical_list_motor_blocks` | bearer (auto-mint) | List Motor Blocks |
|
|
16
|
+
| `motorical_send_email` | `mk_live_…` | Transactional `POST /v1/send` (**default `dryRun: true`**) |
|
|
17
|
+
| `motorical_get_message` | bearer | Message by UUID |
|
|
18
|
+
| `motorical_get_message_events` | bearer | Delivery lifecycle events |
|
|
19
|
+
| `motorical_sandbox_status` | `MOTORICAL_JWT` | Developer sandbox status |
|
|
20
|
+
| `motorical_sandbox_provision` | `MOTORICAL_JWT` | Provision `*.sandbox.motorical.com` |
|
|
21
|
+
| `motorical_sandbox_convert` | `MOTORICAL_JWT` | Convert sandbox → verified domain |
|
|
22
|
+
|
|
23
|
+
**Resources:** `motorical://docs/llms.txt`, `motorical://docs/openapi.json`
|
|
24
|
+
**Prompt:** `motorical_integrate_send`
|
|
25
|
+
|
|
26
|
+
Safety: real sends require `dryRun: false` **and** `confirmRealSend: true`. Sandbox outbound is allowlist-locked until convert.
|
|
27
|
+
|
|
28
|
+
## Environment
|
|
29
|
+
|
|
30
|
+
| Variable | Required | Description |
|
|
31
|
+
|----------|----------|-------------|
|
|
32
|
+
| `MOTORICAL_MK_API_KEY` | for send | Motor Block key `mk_live_…` |
|
|
33
|
+
| `MOTORICAL_AK_API_KEY` | for public API tools | Account key `ak_live_…` |
|
|
34
|
+
| `MOTORICAL_JWT` | for sandbox tools | Dashboard JWT from signup/login |
|
|
35
|
+
| `MOTORICAL_MOTOR_BLOCK_ID` | recommended | UUID used when minting tokens |
|
|
36
|
+
| `MOTORICAL_DEFAULT_FROM` | optional | Default From address |
|
|
37
|
+
| `MOTORICAL_BEARER_TOKEN` | optional | Pre-minted public bearer |
|
|
38
|
+
| `MOTORICAL_API_BASE_URL` | optional | default `https://api.motorical.com` |
|
|
39
|
+
| `MOTORICAL_DOCS_BASE_URL` | optional | default `https://docs.motorical.com` |
|
|
40
|
+
|
|
41
|
+
## Cursor install
|
|
42
|
+
|
|
43
|
+
Add to MCP config (e.g. Cursor Settings → MCP):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"motorical": {
|
|
49
|
+
"command": "node",
|
|
50
|
+
"args": ["/root/motoric_smtp/packages/motorical-mcp/src/index.js"],
|
|
51
|
+
"env": {
|
|
52
|
+
"MOTORICAL_MK_API_KEY": "mk_live_…",
|
|
53
|
+
"MOTORICAL_AK_API_KEY": "ak_live_…",
|
|
54
|
+
"MOTORICAL_MOTOR_BLOCK_ID": "your-block-uuid",
|
|
55
|
+
"MOTORICAL_DEFAULT_FROM": "noreply@yourdomain.com"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
On a developer Mac after cloning:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
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"]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Develop / test
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cd packages/motorical-mcp
|
|
79
|
+
npm test # unit + in-memory MCP
|
|
80
|
+
npm run smoke # ovh24 live dry-run (creates/revokes temp keys)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Out of scope (v1)
|
|
84
|
+
|
|
85
|
+
- Communications Block tools (lists/campaigns) — later
|
|
86
|
+
- OAuth consent / SMTP / mTLS execution — use docs + out-of-band clients
|
|
87
|
+
- Hosted remote MCP HTTP
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@motorical/mcp",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "MCP server for Motorical transactional email API — dry-run/send, mint public tokens, list Motor Blocks, inspect delivery events",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"motorical-mcp": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/server.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/server.js",
|
|
12
|
+
"./client": "./src/client.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node src/index.js",
|
|
16
|
+
"test": "node --test test/*.test.js",
|
|
17
|
+
"smoke": "node test/live-smoke.js"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"motorical",
|
|
24
|
+
"mcp",
|
|
25
|
+
"email",
|
|
26
|
+
"smtp",
|
|
27
|
+
"transactional-email",
|
|
28
|
+
"email-api",
|
|
29
|
+
"model-context-protocol"
|
|
30
|
+
],
|
|
31
|
+
"license": "UNLICENSED",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.25.0",
|
|
34
|
+
"zod": "^3.25.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://docs.motorical.com/ai-mcp"
|
|
44
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin Motorical HTTP client for MCP tools.
|
|
3
|
+
* Auth rules match docs.motorical.com (mk_live_ → /v1/send; ak_live_ → mint bearer).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_API_BASE = 'https://api.motorical.com';
|
|
7
|
+
const DEFAULT_DOCS_BASE = 'https://docs.motorical.com';
|
|
8
|
+
|
|
9
|
+
export function loadConfig(env = process.env) {
|
|
10
|
+
return {
|
|
11
|
+
apiBaseUrl: (env.MOTORICAL_API_BASE_URL || DEFAULT_API_BASE).replace(/\/$/, ''),
|
|
12
|
+
docsBaseUrl: (env.MOTORICAL_DOCS_BASE_URL || DEFAULT_DOCS_BASE).replace(/\/$/, ''),
|
|
13
|
+
mkApiKey: env.MOTORICAL_MK_API_KEY || '',
|
|
14
|
+
akApiKey: env.MOTORICAL_AK_API_KEY || '',
|
|
15
|
+
bearerToken: env.MOTORICAL_BEARER_TOKEN || '',
|
|
16
|
+
dashboardJwt: env.MOTORICAL_JWT || '',
|
|
17
|
+
motorBlockId: env.MOTORICAL_MOTOR_BLOCK_ID || '',
|
|
18
|
+
defaultFrom: env.MOTORICAL_DEFAULT_FROM || ''
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class MotoricalClient {
|
|
23
|
+
constructor(config = loadConfig()) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
/** @type {string|null} */
|
|
26
|
+
this._cachedBearer = config.bearerToken || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
requireMk() {
|
|
30
|
+
if (!this.config.mkApiKey) {
|
|
31
|
+
throw new Error('MOTORICAL_MK_API_KEY is required (mk_live_... Motor Block API key for POST /v1/send)');
|
|
32
|
+
}
|
|
33
|
+
return this.config.mkApiKey;
|
|
34
|
+
}
|
|
35
|
+
|
|
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
|
+
async request(method, path, { headers = {}, body, apiKey, bearer } = {}) {
|
|
44
|
+
const url = `${this.config.apiBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
45
|
+
const h = { Accept: 'application/json', ...headers };
|
|
46
|
+
if (apiKey) h.Authorization = `ApiKey ${apiKey}`;
|
|
47
|
+
if (bearer) h.Authorization = `Bearer ${bearer}`;
|
|
48
|
+
if (body !== undefined) h['Content-Type'] = 'application/json';
|
|
49
|
+
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
method,
|
|
52
|
+
headers: h,
|
|
53
|
+
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const text = await res.text();
|
|
57
|
+
let data;
|
|
58
|
+
try {
|
|
59
|
+
data = text ? JSON.parse(text) : null;
|
|
60
|
+
} catch {
|
|
61
|
+
data = { raw: text };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const msg = data?.error || data?.message || data?.error_description || res.statusText;
|
|
66
|
+
const err = new Error(`${method} ${path} → ${res.status}: ${msg}`);
|
|
67
|
+
err.status = res.status;
|
|
68
|
+
err.data = data;
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async mintPublicToken({ motorBlockId, scopes, ttlSeconds = 900 } = {}) {
|
|
75
|
+
const blockId = motorBlockId || this.config.motorBlockId;
|
|
76
|
+
if (!blockId) {
|
|
77
|
+
throw new Error('motorBlockId is required (argument or MOTORICAL_MOTOR_BLOCK_ID)');
|
|
78
|
+
}
|
|
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
|
+
});
|
|
87
|
+
const token = data?.data?.token || data?.token || data?.access_token;
|
|
88
|
+
if (token) this._cachedBearer = token;
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getBearer({ motorBlockId, forceRefresh = false } = {}) {
|
|
93
|
+
if (this._cachedBearer && !forceRefresh) return this._cachedBearer;
|
|
94
|
+
const minted = await this.mintPublicToken({ motorBlockId });
|
|
95
|
+
const token = minted?.data?.token || minted?.token || minted?.access_token;
|
|
96
|
+
if (!token) throw new Error('Token mint succeeded but no token field in response');
|
|
97
|
+
this._cachedBearer = token;
|
|
98
|
+
return token;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async listMotorBlocks({ motorBlockId } = {}) {
|
|
102
|
+
const bearer = await this.getBearer({ motorBlockId });
|
|
103
|
+
return this.request('GET', '/api/public/v1/motor-blocks', { bearer });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async sendEmail(payload) {
|
|
107
|
+
const {
|
|
108
|
+
from,
|
|
109
|
+
fromName,
|
|
110
|
+
to,
|
|
111
|
+
subject,
|
|
112
|
+
text,
|
|
113
|
+
html,
|
|
114
|
+
dryRun = true,
|
|
115
|
+
confirmRealSend = false,
|
|
116
|
+
idempotencyKey,
|
|
117
|
+
headers: customHeaders,
|
|
118
|
+
...rest
|
|
119
|
+
} = payload;
|
|
120
|
+
|
|
121
|
+
if (dryRun === false && confirmRealSend !== true) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'Refusing real send: set dryRun:true (default) or pass confirmRealSend:true with dryRun:false'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const fromAddr = from || this.config.defaultFrom;
|
|
128
|
+
if (!fromAddr) throw new Error('from is required (or set MOTORICAL_DEFAULT_FROM)');
|
|
129
|
+
if (!to || (Array.isArray(to) && to.length === 0)) throw new Error('to is required');
|
|
130
|
+
if (!subject) throw new Error('subject is required');
|
|
131
|
+
if (!text && !html) throw new Error('At least one of text or html is required');
|
|
132
|
+
|
|
133
|
+
const headers = {};
|
|
134
|
+
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
|
|
135
|
+
|
|
136
|
+
return this.request('POST', '/v1/send', {
|
|
137
|
+
apiKey: this.requireMk(),
|
|
138
|
+
headers,
|
|
139
|
+
body: {
|
|
140
|
+
from: fromAddr,
|
|
141
|
+
...(fromName ? { fromName } : {}),
|
|
142
|
+
to: Array.isArray(to) ? to : [to],
|
|
143
|
+
subject,
|
|
144
|
+
text,
|
|
145
|
+
html,
|
|
146
|
+
dryRun: dryRun !== false,
|
|
147
|
+
...(customHeaders ? { headers: customHeaders } : {}),
|
|
148
|
+
...rest
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getMessage(messageId, { includePII = false, motorBlockId } = {}) {
|
|
154
|
+
if (!messageId) throw new Error('messageId is required');
|
|
155
|
+
const bearer = await this.getBearer({ motorBlockId });
|
|
156
|
+
const q = includePII ? '?includePII=true' : '';
|
|
157
|
+
return this.request('GET', `/api/public/v1/messages/${encodeURIComponent(messageId)}${q}`, { bearer });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async getMessageEvents(messageId, { includePII = false, motorBlockId } = {}) {
|
|
161
|
+
if (!messageId) throw new Error('messageId is required');
|
|
162
|
+
const bearer = await this.getBearer({ motorBlockId });
|
|
163
|
+
const q = includePII ? '?includePII=true' : '';
|
|
164
|
+
return this.request('GET', `/api/public/v1/messages/${encodeURIComponent(messageId)}/events${q}`, { bearer });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async getSendApiStatus() {
|
|
168
|
+
return this.request('GET', '/v1/status');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
requireDashboardJwt() {
|
|
172
|
+
if (!this.config.dashboardJwt) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
'MOTORICAL_JWT is required for developer sandbox tools (dashboard session from motorical login / set-password)'
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return this.config.dashboardJwt;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async sandboxStatus() {
|
|
181
|
+
return this.request('GET', '/api/developer/sandbox', { bearer: this.requireDashboardJwt() });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async sandboxProvision({ handle, channel = 'agent' } = {}) {
|
|
185
|
+
return this.request('POST', '/api/developer/sandbox/provision', {
|
|
186
|
+
bearer: this.requireDashboardJwt(),
|
|
187
|
+
body: { handle, channel }
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async sandboxConvert({ domainId }) {
|
|
192
|
+
if (!domainId) throw new Error('domainId is required');
|
|
193
|
+
return this.request('POST', '/api/developer/sandbox/convert', {
|
|
194
|
+
bearer: this.requireDashboardJwt(),
|
|
195
|
+
body: { domainId }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async domainAdd({ domain, verificationMethod = 'dns' } = {}) {
|
|
200
|
+
if (!domain) throw new Error('domain is required');
|
|
201
|
+
return this.request('POST', '/api/domains', {
|
|
202
|
+
bearer: this.requireDashboardJwt(),
|
|
203
|
+
body: { domain, verificationMethod }
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async domainVerify({ domainId, method = 'dns' } = {}) {
|
|
208
|
+
if (!domainId) throw new Error('domainId is required');
|
|
209
|
+
return this.request('POST', `/api/domains/${encodeURIComponent(domainId)}/verify`, {
|
|
210
|
+
bearer: this.requireDashboardJwt(),
|
|
211
|
+
body: { method }
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async domainCheckDns({ domainId, recordType } = {}) {
|
|
216
|
+
if (!domainId) throw new Error('domainId is required');
|
|
217
|
+
return this.request('POST', `/api/domains/${encodeURIComponent(domainId)}/check-dns`, {
|
|
218
|
+
bearer: this.requireDashboardJwt(),
|
|
219
|
+
body: recordType ? { recordType } : {}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async webHandoff({ path } = {}) {
|
|
224
|
+
return this.request('POST', '/api/auth/web-handoff', {
|
|
225
|
+
bearer: this.requireDashboardJwt(),
|
|
226
|
+
body: path ? { path } : {}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async fetchDocs(path = '/llms.txt') {
|
|
231
|
+
const url = `${this.config.docsBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
232
|
+
const res = await fetch(url);
|
|
233
|
+
if (!res.ok) throw new Error(`Docs fetch ${url} → ${res.status}`);
|
|
234
|
+
return res.text();
|
|
235
|
+
}
|
|
236
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Motorical MCP server — stdio transport for Cursor / Claude Desktop / etc.
|
|
4
|
+
* Credentials via env (see README). Log only to stderr.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { createMotoricalMcpServer } from './server.js';
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const { server } = createMotoricalMcpServer();
|
|
12
|
+
const transport = new StdioServerTransport();
|
|
13
|
+
await server.connect(transport);
|
|
14
|
+
console.error('[motorical-mcp] ready on stdio');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
main().catch((err) => {
|
|
18
|
+
console.error('[motorical-mcp] fatal:', err);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
package/src/server.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { MotoricalClient, loadConfig } from './client.js';
|
|
4
|
+
|
|
5
|
+
const PACKAGE_VERSION = '1.0.2';
|
|
6
|
+
|
|
7
|
+
function jsonResult(data, { isError = false } = {}) {
|
|
8
|
+
return {
|
|
9
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
10
|
+
structuredContent: typeof data === 'object' && data !== null ? data : { value: data },
|
|
11
|
+
isError
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function errorResult(err) {
|
|
16
|
+
return jsonResult(
|
|
17
|
+
{
|
|
18
|
+
error: err.message,
|
|
19
|
+
status: err.status || null,
|
|
20
|
+
details: err.data || null
|
|
21
|
+
},
|
|
22
|
+
{ isError: true }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a configured Motorical MCP server (stdio/HTTP transport attached by caller).
|
|
28
|
+
*/
|
|
29
|
+
export function createMotoricalMcpServer(options = {}) {
|
|
30
|
+
const client = options.client || new MotoricalClient(options.config || loadConfig());
|
|
31
|
+
|
|
32
|
+
const server = new McpServer({
|
|
33
|
+
name: 'motorical',
|
|
34
|
+
version: PACKAGE_VERSION
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.registerTool(
|
|
38
|
+
'motorical_get_send_status',
|
|
39
|
+
{
|
|
40
|
+
description:
|
|
41
|
+
'Check Motorical transactional email HTTP Send API status (GET /v1/status). No auth required.',
|
|
42
|
+
inputSchema: {}
|
|
43
|
+
},
|
|
44
|
+
async () => {
|
|
45
|
+
try {
|
|
46
|
+
return jsonResult(await client.getSendApiStatus());
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return errorResult(err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
server.registerTool(
|
|
54
|
+
'motorical_mint_public_token',
|
|
55
|
+
{
|
|
56
|
+
description:
|
|
57
|
+
'Mint a short-lived Public Analytics API bearer token using MOTORICAL_AK_API_KEY (ak_live_...). ' +
|
|
58
|
+
'Use for /api/public/v1 logs, analytics, webhooks, config. Not for POST /v1/send.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
motorBlockId: z.string().uuid().optional().describe('Defaults to MOTORICAL_MOTOR_BLOCK_ID'),
|
|
61
|
+
scopes: z.array(z.string()).optional(),
|
|
62
|
+
ttlSeconds: z.number().int().min(60).max(900).optional()
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async (args) => {
|
|
66
|
+
try {
|
|
67
|
+
return jsonResult(await client.mintPublicToken(args));
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return errorResult(err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
server.registerTool(
|
|
75
|
+
'motorical_list_motor_blocks',
|
|
76
|
+
{
|
|
77
|
+
description:
|
|
78
|
+
'List Motor Blocks (isolated sending streams) visible to a Public API bearer token (auto-mints with ak_live_ if needed).',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
motorBlockId: z.string().uuid().optional().describe('Block used when minting a token if none cached')
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
async (args) => {
|
|
84
|
+
try {
|
|
85
|
+
return jsonResult(await client.listMotorBlocks(args));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return errorResult(err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
server.registerTool(
|
|
93
|
+
'motorical_send_email',
|
|
94
|
+
{
|
|
95
|
+
description:
|
|
96
|
+
'Transactional email: send or validate via POST /v1/send using MOTORICAL_MK_API_KEY (mk_live_...). ' +
|
|
97
|
+
'Defaults to dryRun:true. Real sends require dryRun:false AND confirmRealSend:true. ' +
|
|
98
|
+
'Do not use OAuth access tokens or Bearer tokens here.',
|
|
99
|
+
inputSchema: {
|
|
100
|
+
from: z.string().email().optional().describe('Defaults to MOTORICAL_DEFAULT_FROM'),
|
|
101
|
+
fromName: z
|
|
102
|
+
.string()
|
|
103
|
+
.max(78)
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('Optional From display name (do not put From in headers — use this field)'),
|
|
106
|
+
to: z.union([z.string().email(), z.array(z.string().email())]),
|
|
107
|
+
subject: z.string().min(1),
|
|
108
|
+
text: z.string().optional(),
|
|
109
|
+
html: z.string().optional(),
|
|
110
|
+
dryRun: z.boolean().optional().describe('Default true — validate without queueing'),
|
|
111
|
+
confirmRealSend: z
|
|
112
|
+
.boolean()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('Required true when dryRun is false'),
|
|
115
|
+
idempotencyKey: z.string().optional()
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
async (args) => {
|
|
119
|
+
try {
|
|
120
|
+
return jsonResult(await client.sendEmail(args));
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return errorResult(err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
server.registerTool(
|
|
128
|
+
'motorical_get_message',
|
|
129
|
+
{
|
|
130
|
+
description: 'Get a message by send UUID (GET /api/public/v1/messages/{id}). Auto-mints bearer if needed.',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
messageId: z.string().uuid(),
|
|
133
|
+
includePII: z.boolean().optional(),
|
|
134
|
+
motorBlockId: z.string().uuid().optional()
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
async (args) => {
|
|
138
|
+
try {
|
|
139
|
+
return jsonResult(await client.getMessage(args.messageId, args));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return errorResult(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
server.registerTool(
|
|
147
|
+
'motorical_get_message_events',
|
|
148
|
+
{
|
|
149
|
+
description:
|
|
150
|
+
'Get delivery lifecycle events for a message (GET /api/public/v1/messages/{id}/events).',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
messageId: z.string().uuid(),
|
|
153
|
+
includePII: z.boolean().optional(),
|
|
154
|
+
motorBlockId: z.string().uuid().optional()
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
async (args) => {
|
|
158
|
+
try {
|
|
159
|
+
return jsonResult(await client.getMessageEvents(args.messageId, args));
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return errorResult(err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
server.registerTool(
|
|
167
|
+
'motorical_sandbox_status',
|
|
168
|
+
{
|
|
169
|
+
description:
|
|
170
|
+
'Get developer sandbox status (domain, Motor Block, outbound lock, allowlist). Requires MOTORICAL_JWT.',
|
|
171
|
+
inputSchema: {}
|
|
172
|
+
},
|
|
173
|
+
async () => {
|
|
174
|
+
try {
|
|
175
|
+
return jsonResult(await client.sandboxStatus());
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return errorResult(err);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
server.registerTool(
|
|
183
|
+
'motorical_sandbox_provision',
|
|
184
|
+
{
|
|
185
|
+
description:
|
|
186
|
+
'Provision unpaid developer sandbox (*.sandbox.motorical.com + outbound-locked Motor Block). ' +
|
|
187
|
+
'Returns mk_live_ once. Requires MOTORICAL_JWT from signup/login. channel defaults to agent.',
|
|
188
|
+
inputSchema: {
|
|
189
|
+
handle: z.string().optional(),
|
|
190
|
+
channel: z.enum(['cli', 'agent', 'web']).optional()
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
async (args) => {
|
|
194
|
+
try {
|
|
195
|
+
return jsonResult(await client.sandboxProvision(args));
|
|
196
|
+
} catch (err) {
|
|
197
|
+
return errorResult(err);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
server.registerTool(
|
|
203
|
+
'motorical_sandbox_convert',
|
|
204
|
+
{
|
|
205
|
+
description:
|
|
206
|
+
'Convert sandbox Motor Block onto a verified customer domain. Requires active Motorical Plan + MOTORICAL_JWT.',
|
|
207
|
+
inputSchema: {
|
|
208
|
+
domainId: z.string().uuid()
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
async (args) => {
|
|
212
|
+
try {
|
|
213
|
+
return jsonResult(await client.sandboxConvert(args));
|
|
214
|
+
} catch (err) {
|
|
215
|
+
return errorResult(err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
server.registerTool(
|
|
221
|
+
'motorical_domain_add',
|
|
222
|
+
{
|
|
223
|
+
description:
|
|
224
|
+
'Add a customer domain (POST /api/domains). Returns verification DNS instructions. ' +
|
|
225
|
+
'For cname_managed DKIM publish the CNAME in verification.records.dkim / dnsRecords (not a TXT p= key). Requires MOTORICAL_JWT.',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
domain: z.string().min(3),
|
|
228
|
+
verificationMethod: z.enum(['dns', 'email']).optional()
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
async (args) => {
|
|
232
|
+
try {
|
|
233
|
+
return jsonResult(await client.domainAdd(args));
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return errorResult(err);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
server.registerTool(
|
|
241
|
+
'motorical_domain_verify',
|
|
242
|
+
{
|
|
243
|
+
description:
|
|
244
|
+
'Verify domain ownership and refresh DKIM/SPF/DMARC send-readiness flags (POST /api/domains/{id}/verify). ' +
|
|
245
|
+
'Safe to re-call after ownership is done — returns sendReady. Requires MOTORICAL_JWT.',
|
|
246
|
+
inputSchema: {
|
|
247
|
+
domainId: z.string().uuid(),
|
|
248
|
+
method: z.enum(['dns', 'email']).optional()
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
async (args) => {
|
|
252
|
+
try {
|
|
253
|
+
return jsonResult(await client.domainVerify(args));
|
|
254
|
+
} catch (err) {
|
|
255
|
+
return errorResult(err);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
server.registerTool(
|
|
261
|
+
'motorical_domain_check_dns',
|
|
262
|
+
{
|
|
263
|
+
description:
|
|
264
|
+
'Live-check DKIM/SPF/DMARC and persist dkim_configured/spf_configured (POST /api/domains/{id}/check-dns). ' +
|
|
265
|
+
'Required before /v1/send when ownership is verified but send returns DOMAIN_DNS_INCOMPLETE. Requires MOTORICAL_JWT.',
|
|
266
|
+
inputSchema: {
|
|
267
|
+
domainId: z.string().uuid(),
|
|
268
|
+
recordType: z.enum(['dkim', 'spf', 'dmarc', 'mx']).optional()
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
async (args) => {
|
|
272
|
+
try {
|
|
273
|
+
return jsonResult(await client.domainCheckDns(args));
|
|
274
|
+
} catch (err) {
|
|
275
|
+
return errorResult(err);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
server.registerTool(
|
|
281
|
+
'motorical_web_handoff',
|
|
282
|
+
{
|
|
283
|
+
description:
|
|
284
|
+
'Mint a one-time CLI→browser handoff URL (POST /api/auth/web-handoff). ' +
|
|
285
|
+
'Open the URL so the human can set a dashboard password / use the UI. Requires MOTORICAL_JWT.',
|
|
286
|
+
inputSchema: {
|
|
287
|
+
path: z.string().optional().describe('Optional in-app path after handoff, e.g. /usage')
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
async (args) => {
|
|
291
|
+
try {
|
|
292
|
+
return jsonResult(await client.webHandoff(args));
|
|
293
|
+
} catch (err) {
|
|
294
|
+
return errorResult(err);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
server.registerResource(
|
|
300
|
+
'motorical-llms',
|
|
301
|
+
'motorical://docs/llms.txt',
|
|
302
|
+
{
|
|
303
|
+
description: 'Motorical llms.txt — transactional email API discovery index for agents',
|
|
304
|
+
mimeType: 'text/plain'
|
|
305
|
+
},
|
|
306
|
+
async () => ({
|
|
307
|
+
contents: [
|
|
308
|
+
{
|
|
309
|
+
uri: 'motorical://docs/llms.txt',
|
|
310
|
+
mimeType: 'text/plain',
|
|
311
|
+
text: await client.fetchDocs('/llms.txt')
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
server.registerResource(
|
|
318
|
+
'motorical-openapi',
|
|
319
|
+
'motorical://docs/openapi.json',
|
|
320
|
+
{
|
|
321
|
+
description: 'Motorical OpenAPI snapshot from docs.motorical.com',
|
|
322
|
+
mimeType: 'application/json'
|
|
323
|
+
},
|
|
324
|
+
async () => ({
|
|
325
|
+
contents: [
|
|
326
|
+
{
|
|
327
|
+
uri: 'motorical://docs/openapi.json',
|
|
328
|
+
mimeType: 'application/json',
|
|
329
|
+
text: await client.fetchDocs('/openapi.json')
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
server.registerPrompt(
|
|
336
|
+
'motorical_integrate_send',
|
|
337
|
+
{
|
|
338
|
+
description: 'Guidance for integrating Motorical transactional email (HTTP Send) safely',
|
|
339
|
+
argsSchema: {
|
|
340
|
+
language: z.string().optional().describe('e.g. node, python, curl')
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
async ({ language }) => ({
|
|
344
|
+
messages: [
|
|
345
|
+
{
|
|
346
|
+
role: 'user',
|
|
347
|
+
content: {
|
|
348
|
+
type: 'text',
|
|
349
|
+
text: [
|
|
350
|
+
`Help me integrate Motorical transactional email (HTTP Send API)${language ? ` in ${language}` : ''}.`,
|
|
351
|
+
'Motorical is a transactional email API and SMTP provider; Motor Blocks are isolated sending streams.',
|
|
352
|
+
'Rules: use mk_live_ ApiKey for POST /v1/send; start with dryRun:true;',
|
|
353
|
+
'use ak_live_ only to mint bearer tokens for /api/public/v1;',
|
|
354
|
+
'never put OAuth access tokens on /v1/send;',
|
|
355
|
+
'SMTP is mail.motorical.com:2587/2465 (password / OAuth / mTLS).',
|
|
356
|
+
'Canonical docs: https://docs.motorical.com/llms.txt'
|
|
357
|
+
].join(' ')
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
})
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
return { server, client };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export { loadConfig, MotoricalClient };
|