@mailkite/mcp 0.2.0 → 0.3.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 +15 -0
- package/package.json +3 -3
- package/spec/api.json +44 -2
- package/spec/cases.json +102 -0
- package/spec/check-docs.mjs +182 -0
- package/spec/schemas/agent-request.json +17 -0
- package/spec/schemas/agent-response.json +13 -0
- package/spec/schemas/create-route-request.json +9 -4
- package/spec/schemas/create-template-request.json +16 -0
- package/spec/schemas/route-message-request.json +17 -0
- package/spec/schemas/route-response.json +12 -0
- package/spec/schemas/send-request.json +11 -2
package/README.md
CHANGED
|
@@ -10,6 +10,19 @@ from the shared SDK contract in [`../spec`](../spec); transport, auth, and error
|
|
|
10
10
|
handling come from the [MailKite Node SDK](../node). Nothing about the API is
|
|
11
11
|
duplicated here — update the spec and the MCP follows.
|
|
12
12
|
|
|
13
|
+
> ### Hosted vs local — which should I use?
|
|
14
|
+
>
|
|
15
|
+
> Most users should connect to the **hosted remote MCP** instead of running this
|
|
16
|
+
> package: `https://mcp.mailkite.dev/mcp` (Streamable HTTP, one-click **OAuth** — no
|
|
17
|
+
> key to copy, no local process). In Claude Code that's the **plugin**
|
|
18
|
+
> (`/plugin marketplace add mailkite/claude-code` → `/plugin install mailkite@mailkite`)
|
|
19
|
+
> or `claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp`. See
|
|
20
|
+
> <https://mailkite.dev/docs/ai-agents>.
|
|
21
|
+
>
|
|
22
|
+
> Run **this local server** when you want a **static key** (no browser OAuth), **offline /
|
|
23
|
+
> CI** use, a stdio-only client, a custom `MAILKITE_BASE_URL`, or `verifyWebhook` to run
|
|
24
|
+
> fully locally. Both expose the exact same tools (same `../spec`).
|
|
25
|
+
|
|
13
26
|
## Install / configure
|
|
14
27
|
|
|
15
28
|
Point your MCP client at the server and give it your MailKite credential. The
|
|
@@ -40,6 +53,8 @@ One tool per MailKite API operation (generated from [`../spec/api.json`](../spec
|
|
|
40
53
|
| Tool | Operation |
|
|
41
54
|
| --- | --- |
|
|
42
55
|
| `mailkite_send` | Send a message over a verified domain |
|
|
56
|
+
| `mailkite_agent` | Send a message to an inbox agent and get its reply |
|
|
57
|
+
| `mailkite_route` | Route a message to a registered route and run its action |
|
|
43
58
|
| `mailkite_list_domains` | List your domains |
|
|
44
59
|
| `mailkite_create_domain` | Add a domain (returns DNS records) |
|
|
45
60
|
| `mailkite_get_domain` | Get one domain with DNS + webhook |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mailkite/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Model Context Protocol server for MailKite — exposes the MailKite API to LLM agents as tools. A thin layer over the MailKite Node SDK and the shared sdks/spec contract.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"homepage": "https://mailkite.dev/docs/libraries",
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
|
-
"url": "git+https://github.com/
|
|
37
|
+
"url": "git+https://github.com/mailkite/mailkite.git",
|
|
38
38
|
"directory": "sdks/mcp"
|
|
39
39
|
},
|
|
40
40
|
"license": "MIT",
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
43
43
|
"ajv": "^8.17.1",
|
|
44
|
-
"mailkite": "^0.
|
|
44
|
+
"mailkite": "^0.3.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/spec/api.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailkite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Canonical interface contract for every MailKite SDK. One low-level request() plus one function per endpoint. All languages expose the same shape; only naming adapts to each language's convention (e.g. Go exports PascalCase).",
|
|
5
5
|
"baseUrl": "https://api.mailkite.dev",
|
|
6
6
|
"auth": {
|
|
@@ -11,11 +11,39 @@
|
|
|
11
11
|
"methods": [
|
|
12
12
|
{
|
|
13
13
|
"name": "send",
|
|
14
|
-
"summary": "Send a message over a verified domain.",
|
|
14
|
+
"summary": "Send a message over a verified domain. Pass `templateId` (+ optional `templateData`) to send from a saved or base template.",
|
|
15
15
|
"http": { "method": "POST", "path": "/v1/send" },
|
|
16
16
|
"args": [{ "name": "message", "in": "body", "schema": "send-request" }],
|
|
17
17
|
"returns": "send-response"
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
"name": "listTemplates",
|
|
21
|
+
"summary": "List your saved email templates (light metadata only — no body). Use getTemplate for the full template.",
|
|
22
|
+
"http": { "method": "GET", "path": "/api/templates" },
|
|
23
|
+
"args": [],
|
|
24
|
+
"returns": "any"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "listBaseTemplates",
|
|
28
|
+
"summary": "List the premade base templates (light metadata). Clone one with createTemplate({ baseId }) or send from it directly via send({ templateId }).",
|
|
29
|
+
"http": { "method": "GET", "path": "/api/templates/base" },
|
|
30
|
+
"args": [],
|
|
31
|
+
"returns": "any"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "getTemplate",
|
|
35
|
+
"summary": "Get one template (full: subject, html, text, theme). Works for your templates (tpl_…) and base templates (base_…).",
|
|
36
|
+
"http": { "method": "GET", "path": "/api/templates/{id}" },
|
|
37
|
+
"args": [{ "name": "id", "in": "path" }],
|
|
38
|
+
"returns": "any"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "createTemplate",
|
|
42
|
+
"summary": "Create a template. Pass `baseId` to clone a base template into your own, or provide name/subject/html/text/theme directly.",
|
|
43
|
+
"http": { "method": "POST", "path": "/api/templates" },
|
|
44
|
+
"args": [{ "name": "body", "in": "body", "schema": "create-template-request" }],
|
|
45
|
+
"returns": "any"
|
|
46
|
+
},
|
|
19
47
|
{
|
|
20
48
|
"name": "listDomains",
|
|
21
49
|
"summary": "List your domains, each with its webhook URL.",
|
|
@@ -104,6 +132,20 @@
|
|
|
104
132
|
"args": [{ "name": "body", "in": "body", "schema": "create-route-request" }],
|
|
105
133
|
"returns": "any"
|
|
106
134
|
},
|
|
135
|
+
{
|
|
136
|
+
"name": "agent",
|
|
137
|
+
"summary": "Send a message to one of your inbox agents and get its reply. Defaults to the account's default agent; pass `routeId` or `address` to target a specific agent, or `model` to override the model. This is separate from inbound routing — it does not match or override routes.",
|
|
138
|
+
"http": { "method": "POST", "path": "/v1/agent" },
|
|
139
|
+
"args": [{ "name": "message", "in": "body", "schema": "agent-request" }],
|
|
140
|
+
"returns": "agent-response"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "route",
|
|
144
|
+
"summary": "Route a message to one of your registered routes (by `routeId` or `address`), running that route's action — agent, webhook, or forward. The route must already exist on your account; arbitrary destinations are not allowed.",
|
|
145
|
+
"http": { "method": "POST", "path": "/v1/route" },
|
|
146
|
+
"args": [{ "name": "message", "in": "body", "schema": "route-message-request" }],
|
|
147
|
+
"returns": "route-response"
|
|
148
|
+
},
|
|
107
149
|
{
|
|
108
150
|
"name": "listMessages",
|
|
109
151
|
"summary": "List stored messages.",
|
package/spec/cases.json
CHANGED
|
@@ -218,6 +218,58 @@
|
|
|
218
218
|
"response": { "status": 200, "body": { "id": "rte_1" } },
|
|
219
219
|
"result": { "id": "rte_1" }
|
|
220
220
|
},
|
|
221
|
+
{
|
|
222
|
+
"name": "agent_default",
|
|
223
|
+
"method": "agent",
|
|
224
|
+
"args": { "text": "What's my current balance?" },
|
|
225
|
+
"request": {
|
|
226
|
+
"method": "POST",
|
|
227
|
+
"path": "/v1/agent",
|
|
228
|
+
"bodySchema": "agent-request",
|
|
229
|
+
"body": { "text": "What's my current balance?" }
|
|
230
|
+
},
|
|
231
|
+
"response": { "status": 200, "body": { "ok": true, "text": "Your balance is $0.00.", "messageId": "msg_agent" } },
|
|
232
|
+
"result": { "ok": true, "text": "Your balance is $0.00.", "messageId": "msg_agent" }
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"name": "agent_by_route",
|
|
236
|
+
"method": "agent",
|
|
237
|
+
"args": { "text": "ping", "routeId": "rte_1", "model": "claude-sonnet-4-6" },
|
|
238
|
+
"request": {
|
|
239
|
+
"method": "POST",
|
|
240
|
+
"path": "/v1/agent",
|
|
241
|
+
"bodySchema": "agent-request",
|
|
242
|
+
"body": { "text": "ping", "routeId": "rte_1", "model": "claude-sonnet-4-6" }
|
|
243
|
+
},
|
|
244
|
+
"response": { "status": 200, "body": { "ok": true, "text": "pong", "messageId": "msg_agent2" } },
|
|
245
|
+
"result": { "ok": true, "text": "pong", "messageId": "msg_agent2" }
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "route_by_id",
|
|
249
|
+
"method": "route",
|
|
250
|
+
"args": { "routeId": "rte_1", "from": "ops@example.com", "subject": "Process this", "text": "Please handle." },
|
|
251
|
+
"request": {
|
|
252
|
+
"method": "POST",
|
|
253
|
+
"path": "/v1/route",
|
|
254
|
+
"bodySchema": "route-message-request",
|
|
255
|
+
"body": { "routeId": "rte_1", "from": "ops@example.com", "subject": "Process this", "text": "Please handle." }
|
|
256
|
+
},
|
|
257
|
+
"response": { "status": 202, "body": { "id": "msg_r1", "routed": true, "action": "webhook" } },
|
|
258
|
+
"result": { "id": "msg_r1", "routed": true, "action": "webhook" }
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"name": "route_by_address",
|
|
262
|
+
"method": "route",
|
|
263
|
+
"args": { "address": "support@app.mailkite.dev", "from": "ops@example.com", "text": "hey there" },
|
|
264
|
+
"request": {
|
|
265
|
+
"method": "POST",
|
|
266
|
+
"path": "/v1/route",
|
|
267
|
+
"bodySchema": "route-message-request",
|
|
268
|
+
"body": { "address": "support@app.mailkite.dev", "from": "ops@example.com", "text": "hey there" }
|
|
269
|
+
},
|
|
270
|
+
"response": { "status": 202, "body": { "id": "msg_r2", "routed": true, "action": "agent" } },
|
|
271
|
+
"result": { "id": "msg_r2", "routed": true, "action": "agent" }
|
|
272
|
+
},
|
|
221
273
|
{
|
|
222
274
|
"name": "list_messages",
|
|
223
275
|
"method": "listMessages",
|
|
@@ -242,6 +294,56 @@
|
|
|
242
294
|
"response": { "status": 200, "body": { "ok": true } },
|
|
243
295
|
"result": { "ok": true }
|
|
244
296
|
},
|
|
297
|
+
{
|
|
298
|
+
"name": "send_with_template",
|
|
299
|
+
"method": "send",
|
|
300
|
+
"args": { "from": "hello@app.mailkite.dev", "to": "ada@example.com", "templateId": "base_welcome-dark", "templateData": { "name": "Ada" } },
|
|
301
|
+
"request": {
|
|
302
|
+
"method": "POST",
|
|
303
|
+
"path": "/v1/send",
|
|
304
|
+
"bodySchema": "send-request",
|
|
305
|
+
"body": { "from": "hello@app.mailkite.dev", "to": "ada@example.com", "templateId": "base_welcome-dark", "templateData": { "name": "Ada" } }
|
|
306
|
+
},
|
|
307
|
+
"response": { "status": 202, "body": { "id": "msg_tpl", "status": "queued" } },
|
|
308
|
+
"result": { "id": "msg_tpl", "status": "queued" }
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"name": "list_templates",
|
|
312
|
+
"method": "listTemplates",
|
|
313
|
+
"args": {},
|
|
314
|
+
"request": { "method": "GET", "path": "/api/templates" },
|
|
315
|
+
"response": { "status": 200, "body": [{ "id": "tpl_1", "name": "Welcome", "category": "", "subject": "Hi", "is_base": 0 }] },
|
|
316
|
+
"result": [{ "id": "tpl_1", "name": "Welcome", "category": "", "subject": "Hi", "is_base": 0 }]
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"name": "list_base_templates",
|
|
320
|
+
"method": "listBaseTemplates",
|
|
321
|
+
"args": {},
|
|
322
|
+
"request": { "method": "GET", "path": "/api/templates/base" },
|
|
323
|
+
"response": { "status": 200, "body": [{ "id": "base_welcome-dark", "name": "Welcome — Dark", "category": "Welcome", "subject": "Welcome to MailKite", "is_base": 1 }] },
|
|
324
|
+
"result": [{ "id": "base_welcome-dark", "name": "Welcome — Dark", "category": "Welcome", "subject": "Welcome to MailKite", "is_base": 1 }]
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"name": "get_template",
|
|
328
|
+
"method": "getTemplate",
|
|
329
|
+
"args": { "id": "tpl_1" },
|
|
330
|
+
"request": { "method": "GET", "path": "/api/templates/tpl_1" },
|
|
331
|
+
"response": { "status": 200, "body": { "id": "tpl_1", "name": "Welcome", "html": "<p>hi</p>" } },
|
|
332
|
+
"result": { "id": "tpl_1", "name": "Welcome", "html": "<p>hi</p>" }
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
"name": "create_template",
|
|
336
|
+
"method": "createTemplate",
|
|
337
|
+
"args": { "baseId": "base_welcome-dark", "name": "My Welcome" },
|
|
338
|
+
"request": {
|
|
339
|
+
"method": "POST",
|
|
340
|
+
"path": "/api/templates",
|
|
341
|
+
"bodySchema": "create-template-request",
|
|
342
|
+
"body": { "baseId": "base_welcome-dark", "name": "My Welcome" }
|
|
343
|
+
},
|
|
344
|
+
"response": { "status": 201, "body": { "id": "tpl_2", "name": "My Welcome" } },
|
|
345
|
+
"result": { "id": "tpl_2", "name": "My Welcome" }
|
|
346
|
+
},
|
|
245
347
|
{
|
|
246
348
|
"name": "get_message_not_found",
|
|
247
349
|
"method": "getMessage",
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// check-docs — validate that the website docs match our request SCHEMAS.
|
|
4
|
+
//
|
|
5
|
+
// "Our schema" = sdks/spec/schemas/*.json (the JSON Schemas every SDK + the MCP
|
|
6
|
+
// server validate against). The website docs (website/src/pages/docs/*.astro)
|
|
7
|
+
// are hand-written, so their field tables and request examples can drift from
|
|
8
|
+
// the schema. This script is the agent-runnable guard: run it, read the report.
|
|
9
|
+
//
|
|
10
|
+
// node sdks/spec/check-docs.mjs # human/agent: prints ✓/✗, exits 1 on drift
|
|
11
|
+
// import { runChecks } from './check-docs.mjs' # CI: see api/test/docs-schema.test.ts
|
|
12
|
+
//
|
|
13
|
+
// It checks, per schema-backed operation:
|
|
14
|
+
// 1. the doc "Request fields" table lists exactly the schema's properties,
|
|
15
|
+
// with the same required set;
|
|
16
|
+
// 2. every JSON request example in the docs uses only schema fields and
|
|
17
|
+
// includes the required ones;
|
|
18
|
+
// 3. the SDK code samples include the required fields and use no wrong alias.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
22
|
+
import { resolve, dirname } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const SPEC_DIR = dirname(fileURLToPath(import.meta.url)); // sdks/spec
|
|
26
|
+
const ROOT = resolve(SPEC_DIR, '../..'); // repo root
|
|
27
|
+
const read = (p) => readFileSync(p, 'utf8');
|
|
28
|
+
const readJson = (p) => JSON.parse(read(p));
|
|
29
|
+
|
|
30
|
+
const DOCS_DIR = resolve(ROOT, 'website/src/pages/docs');
|
|
31
|
+
const docText = Object.fromEntries(
|
|
32
|
+
readdirSync(DOCS_DIR)
|
|
33
|
+
.filter((f) => f.endsWith('.astro'))
|
|
34
|
+
.map((f) => [f, read(resolve(DOCS_DIR, f))]),
|
|
35
|
+
);
|
|
36
|
+
const samplesTs = read(resolve(ROOT, 'website/src/lib/samples.ts'));
|
|
37
|
+
|
|
38
|
+
const schema = (name) => readJson(resolve(SPEC_DIR, 'schemas', `${name}.json`));
|
|
39
|
+
const eqSet = (a, b) => a.size === b.size && [...a].every((x) => b.has(x));
|
|
40
|
+
|
|
41
|
+
// ---- helpers --------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
// All quoted keys (`"key":`) inside a JSON-ish block — includes nested keys,
|
|
44
|
+
// which is what we want (attachment sub-fields are valid send fields).
|
|
45
|
+
const jsonKeys = (block) => new Set([...block.matchAll(/"([a-zA-Z]+)"\s*:/g)].map((m) => m[1]));
|
|
46
|
+
|
|
47
|
+
// Every backtick template literal in a file (docs put JSON examples in these).
|
|
48
|
+
const templateLiterals = (src) => [...src.matchAll(/`([^`]*)`/g)].map((m) => m[1]);
|
|
49
|
+
|
|
50
|
+
// ---- the checks -----------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
// Each entry ties a doc-visible operation to its schema + how to spot its
|
|
53
|
+
// request examples among the docs' JSON blocks.
|
|
54
|
+
const OPS = [
|
|
55
|
+
{
|
|
56
|
+
label: 'send',
|
|
57
|
+
schema: 'send-request',
|
|
58
|
+
// The "Request fields" table lives in sending.astro under this heading.
|
|
59
|
+
fieldTable: { file: 'sending.astro', heading: 'Request fields' },
|
|
60
|
+
// A send body has from+subject. Exclude inbound webhook payloads (which also
|
|
61
|
+
// carry from/subject but add type/auth/threadId) and responses (status).
|
|
62
|
+
isRequestExample: (k) =>
|
|
63
|
+
k.has('from') &&
|
|
64
|
+
k.has('subject') &&
|
|
65
|
+
!k.has('status') &&
|
|
66
|
+
!k.has('type') &&
|
|
67
|
+
!k.has('auth') &&
|
|
68
|
+
!k.has('threadId'),
|
|
69
|
+
nestedSchemaKeys: ['filename', 'url', 'content', 'contentType'], // attachments[].*
|
|
70
|
+
sample: { source: samplesTs, slice: ['sendSamples', 'initSamples'] },
|
|
71
|
+
wrongAliases: ['reply_to', 'in_reply_to', 'html_body', 'text_body', 'from_addr', 'to_addr'],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: 'createRoute',
|
|
75
|
+
schema: 'create-route-request',
|
|
76
|
+
isRequestExample: (k) => k.has('match') && k.has('action'),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: 'createDomain',
|
|
80
|
+
schema: 'create-domain-request',
|
|
81
|
+
isRequestExample: (k) => k.has('domain') && k.size <= 2,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
function runChecks() {
|
|
86
|
+
const results = [];
|
|
87
|
+
const ok = (name) => results.push({ name, ok: true });
|
|
88
|
+
const fail = (name, detail) => results.push({ name, ok: false, detail });
|
|
89
|
+
|
|
90
|
+
for (const op of OPS) {
|
|
91
|
+
const s = schema(op.schema);
|
|
92
|
+
const props = new Set(Object.keys(s.properties));
|
|
93
|
+
const required = new Set(s.required ?? []);
|
|
94
|
+
const allowed = new Set([...props, ...(op.nestedSchemaKeys ?? [])]);
|
|
95
|
+
|
|
96
|
+
// ── Check 1: the "Request fields" table mirrors the schema exactly ──────
|
|
97
|
+
if (op.fieldTable) {
|
|
98
|
+
const src = docText[op.fieldTable.file] ?? '';
|
|
99
|
+
const table = src.match(
|
|
100
|
+
new RegExp(`<h2>${op.fieldTable.heading}</h2>([\\s\\S]*?)</table>`),
|
|
101
|
+
);
|
|
102
|
+
if (!table) {
|
|
103
|
+
fail(`${op.label}: fields table present`, `no "${op.fieldTable.heading}" table in ${op.fieldTable.file}`);
|
|
104
|
+
} else {
|
|
105
|
+
const docFields = new Set();
|
|
106
|
+
const docRequired = new Set();
|
|
107
|
+
for (const row of table[1].matchAll(/<tr>([\s\S]*?)<\/tr>/g)) {
|
|
108
|
+
const firstCell = row[1].match(/<td>([\s\S]*?)<\/td>/);
|
|
109
|
+
if (!firstCell) continue; // header row (uses <th>)
|
|
110
|
+
const names = [...firstCell[1].matchAll(/<code>([a-zA-Z]+)<\/code>/g)].map((m) => m[1]);
|
|
111
|
+
const isRequired = /Required\./.test(row[1]);
|
|
112
|
+
for (const n of names) {
|
|
113
|
+
docFields.add(n);
|
|
114
|
+
if (isRequired) docRequired.add(n);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (eqSet(docFields, props)) ok(`${op.label}: fields table = schema properties`);
|
|
118
|
+
else
|
|
119
|
+
fail(
|
|
120
|
+
`${op.label}: fields table = schema properties`,
|
|
121
|
+
`table=${[...docFields].sort()} schema=${[...props].sort()}`,
|
|
122
|
+
);
|
|
123
|
+
if (eqSet(docRequired, required)) ok(`${op.label}: fields table required = schema required`);
|
|
124
|
+
else
|
|
125
|
+
fail(
|
|
126
|
+
`${op.label}: fields table required = schema required`,
|
|
127
|
+
`table=${[...docRequired].sort()} schema=${[...required].sort()}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Check 2: every JSON request example uses only schema fields ─────────
|
|
133
|
+
let examplesSeen = 0;
|
|
134
|
+
for (const [file, src] of Object.entries(docText)) {
|
|
135
|
+
for (const block of templateLiterals(src)) {
|
|
136
|
+
const keys = jsonKeys(block);
|
|
137
|
+
if (keys.size === 0 || !op.isRequestExample(keys)) continue;
|
|
138
|
+
examplesSeen++;
|
|
139
|
+
const unknown = [...keys].filter((k) => !allowed.has(k));
|
|
140
|
+
if (unknown.length)
|
|
141
|
+
fail(`${op.label}: JSON example fields ⊆ schema (${file})`, `unknown: ${unknown}`);
|
|
142
|
+
const missing = [...required].filter((r) => !keys.has(r));
|
|
143
|
+
if (missing.length)
|
|
144
|
+
fail(`${op.label}: JSON example has required fields (${file})`, `missing: ${missing}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (examplesSeen) ok(`${op.label}: ${examplesSeen} JSON request example(s) validated`);
|
|
148
|
+
|
|
149
|
+
// ── Check 3: SDK code samples include required fields, no wrong aliases ──
|
|
150
|
+
if (op.sample) {
|
|
151
|
+
// Pull each named sample object's body out of samples.ts.
|
|
152
|
+
for (const name of op.sample.slice) {
|
|
153
|
+
const m = op.sample.source.match(new RegExp(`export const ${name}[\\s\\S]*?\\n};`));
|
|
154
|
+
if (!m) continue;
|
|
155
|
+
const body = m[0].toLowerCase();
|
|
156
|
+
for (const alias of op.wrongAliases ?? []) {
|
|
157
|
+
if (body.includes(alias))
|
|
158
|
+
fail(`${op.label}: sample uses wrong alias (${name})`, `found "${alias}" — use the schema field`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- CLI ------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
169
|
+
if (isMain) {
|
|
170
|
+
const results = runChecks();
|
|
171
|
+
const failures = results.filter((r) => !r.ok);
|
|
172
|
+
for (const r of results) {
|
|
173
|
+
console.log(`${r.ok ? '✓' : '✗'} ${r.name}${r.detail ? `\n ${r.detail}` : ''}`);
|
|
174
|
+
}
|
|
175
|
+
console.log(`\n${results.length - failures.length}/${results.length} checks passed.`);
|
|
176
|
+
if (failures.length) {
|
|
177
|
+
console.error(`\n${failures.length} doc/schema mismatch(es) — fix the .astro doc or the schema.`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export { runChecks };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "agent-request",
|
|
4
|
+
"title": "Send-to-agent request body",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["text"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"text": { "type": "string", "description": "The message for the agent — it reads this as the incoming email body and decides what to do." },
|
|
10
|
+
"subject": { "type": "string", "description": "Optional subject line the agent sees on the message." },
|
|
11
|
+
"from": { "type": "string", "description": "Optional sender address the agent sees as the originator. Defaults to the account's API caller address." },
|
|
12
|
+
"html": { "type": "string", "description": "Optional HTML body. `text` is still required — the agent reasons over the plain-text content." },
|
|
13
|
+
"routeId": { "type": "string", "description": "Target a specific agent by its route id (rte_…). Omit to use the account's default agent (its most recently created agent route)." },
|
|
14
|
+
"address": { "type": "string", "description": "Target the agent whose route matches this address. Alternative to routeId." },
|
|
15
|
+
"model": { "type": "string", "description": "Override the model the agent runs on for this call (e.g. claude-sonnet-4-6)." }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "agent-response",
|
|
4
|
+
"title": "Send-to-agent response body",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["ok"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"ok": { "type": "boolean", "description": "Whether the agent ran to completion." },
|
|
9
|
+
"text": { "type": "string", "description": "The agent's reply — its final message after running." },
|
|
10
|
+
"messageId": { "type": "string", "description": "Id of the stored message the agent processed (msg_…)." },
|
|
11
|
+
"error": { "type": "string", "description": "Error detail when `ok` is false." }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
"$id": "create-route-request",
|
|
4
4
|
"title": "Create route request body",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"required": ["match"
|
|
6
|
+
"required": ["match"],
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
|
-
"match": { "type": "string" },
|
|
10
|
-
"action": {
|
|
11
|
-
|
|
9
|
+
"match": { "type": "string", "description": "Address pattern: exact, *@domain, addr+*@domain, or /regex/." },
|
|
10
|
+
"action": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"enum": ["webhook", "forward", "store", "drop", "agent"],
|
|
13
|
+
"description": "What to do with matching mail. Defaults to webhook."
|
|
14
|
+
},
|
|
15
|
+
"destination": { "type": "string", "description": "Required for action webhook (URL) or forward (address)." },
|
|
16
|
+
"agentPrompt": { "type": "string", "description": "Required for action agent — instructions for the inbox agent." }
|
|
12
17
|
}
|
|
13
18
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "create-template-request",
|
|
4
|
+
"title": "Create template request body",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"baseId": { "type": "string", "description": "Clone this base template (base_…) into your own. When set, name is optional (defaults to the base's name)." },
|
|
9
|
+
"name": { "type": "string", "description": "Template name. Required unless baseId is given." },
|
|
10
|
+
"subject": { "type": "string", "description": "Default subject line for sends." },
|
|
11
|
+
"html": { "type": "string", "description": "Rendered, send-ready HTML." },
|
|
12
|
+
"text": { "type": "string", "description": "Plaintext fallback." },
|
|
13
|
+
"json": { "type": "string", "description": "Editor (TipTap) JSON source, for re-editing in the dashboard." },
|
|
14
|
+
"theme": { "type": "string", "description": "Brand tokens JSON (bg, surface, primary, text, logo, …)." }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "route-message-request",
|
|
4
|
+
"title": "Route-message request body",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["from"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"anyOf": [{ "required": ["routeId"] }, { "required": ["address"] }],
|
|
9
|
+
"properties": {
|
|
10
|
+
"routeId": { "type": "string", "description": "Target route by id (rte_…). One of routeId or address is required. The route must already be registered on this account." },
|
|
11
|
+
"address": { "type": "string", "description": "Target route by the address it matches. One of routeId or address is required." },
|
|
12
|
+
"from": { "type": "string", "description": "Sender address recorded on the message." },
|
|
13
|
+
"subject": { "type": "string", "description": "Optional subject line." },
|
|
14
|
+
"text": { "type": "string", "description": "Plain-text body." },
|
|
15
|
+
"html": { "type": "string", "description": "HTML body." }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "route-response",
|
|
4
|
+
"title": "Route-message response body",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["id", "routed", "action"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"id": { "type": "string", "description": "Stored message id (msg_…)." },
|
|
9
|
+
"routed": { "type": "boolean", "description": "Whether the message was handed to the route's action." },
|
|
10
|
+
"action": { "type": "string", "description": "The action the matched route performed: webhook, forward, agent, store, or drop." }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "send-request",
|
|
4
4
|
"title": "Send request body",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"required": ["from", "to"
|
|
6
|
+
"required": ["from", "to"],
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
9
|
"from": { "type": "string", "description": "An address on a verified domain." },
|
|
@@ -14,9 +14,18 @@
|
|
|
14
14
|
{ "type": "array", "items": { "type": "string" }, "minItems": 1 }
|
|
15
15
|
]
|
|
16
16
|
},
|
|
17
|
-
"subject": { "type": "string" },
|
|
17
|
+
"subject": { "type": "string", "description": "Required unless supplied by a template." },
|
|
18
18
|
"html": { "type": "string" },
|
|
19
19
|
"text": { "type": "string" },
|
|
20
|
+
"templateId": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Send using a saved template — a user template (tpl_…) or a base template (base_…). Its subject/html/text seed the message; explicit subject/html/text here override them."
|
|
23
|
+
},
|
|
24
|
+
"templateData": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"description": "Values substituted into the template's {{merge_tags}} (e.g. {\"name\":\"Ann\"} fills {{name}}). HTML values are auto-escaped.",
|
|
27
|
+
"additionalProperties": { "type": ["string", "number", "boolean", "null"] }
|
|
28
|
+
},
|
|
20
29
|
"cc": { "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] },
|
|
21
30
|
"bcc": { "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] },
|
|
22
31
|
"replyTo": { "type": "string" },
|