@mailkite/mcp 0.2.0 → 0.4.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 +277 -33
- package/spec/cases.json +657 -74
- 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/decrypt-request.json +19 -0
- package/spec/schemas/encrypt-request.json +19 -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
|
@@ -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,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "decrypt-request",
|
|
4
|
+
"title": "Decrypt request",
|
|
5
|
+
"description": "Inputs to decrypt — a local hybrid-envelope decryption, no API call.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["envelope", "privateKey"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"envelope": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "The at-rest envelope JSON string ({v,keyAlg,fp,enc,iv,wrappedKey,ciphertext}) from MailKite."
|
|
13
|
+
},
|
|
14
|
+
"privateKey": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Your RSA private key in PKCS8/PEM form (-----BEGIN PRIVATE KEY-----) — the match to the public key the envelope was wrapped to."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "encrypt-request",
|
|
4
|
+
"title": "Encrypt request",
|
|
5
|
+
"description": "Inputs to encrypt — a local hybrid-envelope encryption, no API call.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["plaintext", "publicKey"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"plaintext": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "The UTF-8 string to encrypt (e.g. a message body you're about to store)."
|
|
13
|
+
},
|
|
14
|
+
"publicKey": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "The recipient domain's RSA public key in SPKI/PEM form (-----BEGIN PUBLIC KEY-----), ≥2048-bit."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -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" },
|