@mailkite/mcp 0.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 ADDED
@@ -0,0 +1,88 @@
1
+ # @mailkite/mcp
2
+
3
+ [Model Context Protocol](https://modelcontextprotocol.io) server for
4
+ [MailKite](https://mailkite.dev). It exposes the MailKite API to LLM agents
5
+ (Claude Desktop, Claude Code, Cursor, …) as tools — send mail, manage domains,
6
+ webhooks, routes, and inbound messages, all from a chat.
7
+
8
+ It's a **thin layer**. The tools, their input schemas, and validation all come
9
+ from the shared SDK contract in [`../spec`](../spec); transport, auth, and error
10
+ handling come from the [MailKite Node SDK](../node). Nothing about the API is
11
+ duplicated here — update the spec and the MCP follows.
12
+
13
+ ## Install / configure
14
+
15
+ Point your MCP client at the server and give it your MailKite credential. The
16
+ token is the same one the SDKs take: an `mk_live_…` API key (for sending) or a
17
+ management session token.
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "mailkite": {
23
+ "command": "npx",
24
+ "args": ["-y", "@mailkite/mcp"],
25
+ "env": { "MAILKITE_API_KEY": "mk_live_…" }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ | Env var | Required | Default | Notes |
32
+ | --- | --- | --- | --- |
33
+ | `MAILKITE_API_KEY` | yes | — | Bearer credential (`mk_live_…` API key or session token) |
34
+ | `MAILKITE_BASE_URL` | no | `https://api.mailkite.dev` | Override for local/staging |
35
+
36
+ ## Tools
37
+
38
+ One tool per MailKite API operation (generated from [`../spec/api.json`](../spec/api.json)):
39
+
40
+ | Tool | Operation |
41
+ | --- | --- |
42
+ | `mailkite_send` | Send a message over a verified domain |
43
+ | `mailkite_list_domains` | List your domains |
44
+ | `mailkite_create_domain` | Add a domain (returns DNS records) |
45
+ | `mailkite_get_domain` | Get one domain with DNS + webhook |
46
+ | `mailkite_delete_domain` | Remove a domain |
47
+ | `mailkite_verify_domain` | Check DNS and update status |
48
+ | `mailkite_set_webhook` | Set/replace the domain catch-all webhook |
49
+ | `mailkite_delete_webhook` | Remove the domain webhook |
50
+ | `mailkite_test_webhook` | Send a signed test event to the webhook |
51
+ | `mailkite_list_routes` | List inbound routing rules |
52
+ | `mailkite_create_route` | Create a route (match, action, destination) |
53
+ | `mailkite_list_messages` | List stored messages |
54
+ | `mailkite_get_message` | Get a message with deliveries + attachments |
55
+ | `mailkite_retry_delivery` | Re-deliver a stored message to its webhook |
56
+ | `mailkite_verify_webhook` | Verify an `x-mailkite-signature` header (local — no API call) |
57
+
58
+ `mailkite_verify_webhook` is a **local** tool: it runs the SDK's `verifyWebhook`
59
+ in-process (no credential, no network) and returns `{ "valid": true | false }`.
60
+ Every other tool is one MailKite API operation.
61
+
62
+ Each tool's input is a flat object with the real field names (e.g.
63
+ `mailkite_send` takes `from`, `to`, `subject`, `html`, `text`, …). Required
64
+ fields and types are enforced with [`ajv`](https://ajv.js.org) against the same
65
+ JSON Schemas in [`../spec/schemas`](../spec/schemas) that the SDKs and
66
+ conformance harness use — invalid calls are rejected before any HTTP request.
67
+
68
+ ## How it works
69
+
70
+ ```
71
+ api.json ──▶ one MCP tool per method (name, description, input schema)
72
+ schemas/* ──▶ ajv validators (validate the call before dispatch)
73
+ Node SDK ──▶ MailKite.request(method, path, body) (auth, HTTP, errors)
74
+ local methods (verifyWebhook) dispatch in-process — no HTTP
75
+ ```
76
+
77
+ See [`PLAN.md`](./PLAN.md) for the full design.
78
+
79
+ ## Develop
80
+
81
+ ```bash
82
+ npm install
83
+ npm test # boots the server over stdio, lists tools, checks validation + wire bytes
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@mailkite/mcp",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "bin": {
7
+ "mailkite-mcp": "server.mjs"
8
+ },
9
+ "files": [
10
+ "server.mjs",
11
+ "spec",
12
+ "README.md"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "scripts": {
18
+ "prepack": "node prepack.mjs",
19
+ "postpack": "node -e \"require('node:fs').rmSync('spec',{recursive:true,force:true})\"",
20
+ "test": "node smoke.mjs && node wire-test.mjs"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "keywords": [
26
+ "mailkite",
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "email",
30
+ "ai",
31
+ "agent",
32
+ "tools"
33
+ ],
34
+ "homepage": "https://mailkite.dev/docs/libraries",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/fijiwebdesign/mailkite.git",
38
+ "directory": "sdks/mcp"
39
+ },
40
+ "license": "MIT",
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.0.0",
43
+ "ajv": "^8.17.1",
44
+ "mailkite": "^0.1.0"
45
+ }
46
+ }
package/server.mjs ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ // MailKite MCP server.
3
+ //
4
+ // This is *only* the MCP layer. Everything else is reused:
5
+ // • Operation ↔ endpoint mapping → ../spec/api.json (the canonical contract)
6
+ // • Input validation → ../spec/schemas/*.json compiled with ajv
7
+ // (the same files + validator as conformance)
8
+ // • Transport / auth / errors → the MailKite Node SDK (../node)
9
+ //
10
+ // We read api.json at startup and register one MCP tool per method. A call is
11
+ // validated against the shared JSON Schema, then dispatched through the SDK's
12
+ // low-level request(). No endpoint list, schema, or HTTP logic is duplicated.
13
+
14
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
15
+ import { fileURLToPath } from "node:url";
16
+ import path from "node:path";
17
+
18
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
21
+ import Ajv from "ajv";
22
+ import { MailKite, MailKiteError } from "mailkite";
23
+
24
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
25
+ // Prefer a spec bundled into the package (copied in at publish time by prepack);
26
+ // fall back to the sibling sdks/spec when running straight from the repo.
27
+ const SPEC = existsSync(path.join(HERE, "spec")) ? path.join(HERE, "spec") : path.resolve(HERE, "..", "spec");
28
+
29
+ // ---- load the canonical contract --------------------------------------------
30
+ const api = JSON.parse(readFileSync(path.join(SPEC, "api.json"), "utf8"));
31
+
32
+ // Compile one ajv validator per body schema — same config + files as
33
+ // sdks/conformance/run.mjs, so the MCP rejects exactly what the SDKs reject.
34
+ const ajv = new Ajv({ allErrors: true, strict: false });
35
+ const schemas = {}; // id -> parsed schema
36
+ const validators = {}; // id -> compiled validator
37
+ for (const file of readdirSync(path.join(SPEC, "schemas"))) {
38
+ if (!file.endsWith(".json")) continue;
39
+ const id = file.replace(/\.json$/, "");
40
+ const schema = JSON.parse(readFileSync(path.join(SPEC, "schemas", file), "utf8"));
41
+ schemas[id] = schema;
42
+ validators[id] = ajv.compile(schema);
43
+ }
44
+
45
+ // ---- turn each api.json method into an MCP tool -----------------------------
46
+ function toSnake(name) {
47
+ return name.replace(/([A-Z])/g, "_$1").toLowerCase();
48
+ }
49
+ const toolName = (method) => `mailkite_${toSnake(method.name)}`;
50
+
51
+ // Build the flat input schema for a method: path params (as strings) merged with
52
+ // the referenced body schema's properties, so the model sees real field names.
53
+ function buildInputSchema(method) {
54
+ const properties = {};
55
+ const required = [];
56
+
57
+ for (const arg of method.args || []) {
58
+ if (arg.in === "path") {
59
+ properties[arg.name] = { type: "string", description: `Path parameter \`${arg.name}\`.` };
60
+ required.push(arg.name);
61
+ } else if (arg.in === "body" && arg.schema && schemas[arg.schema]) {
62
+ const body = schemas[arg.schema];
63
+ Object.assign(properties, body.properties || {});
64
+ for (const r of body.required || []) required.push(r);
65
+ }
66
+ }
67
+
68
+ return { type: "object", properties, required, additionalProperties: false };
69
+ }
70
+
71
+ const credentialNote = (method) => {
72
+ if (method.local) return "Runs locally (no API call) — no credentials needed.";
73
+ return method.http.path.startsWith("/v1/")
74
+ ? "Requires an API key (mk_live_…)."
75
+ : "Requires a management session token.";
76
+ };
77
+
78
+ const tools = api.methods.map((method) => ({
79
+ name: toolName(method),
80
+ description: `${method.summary} ${credentialNote(method)}`,
81
+ inputSchema: buildInputSchema(method),
82
+ _method: method, // kept server-side for dispatch; not sent to the client
83
+ }));
84
+
85
+ const byToolName = new Map(tools.map((t) => [t.name, t]));
86
+
87
+ // ---- dispatch ---------------------------------------------------------------
88
+ const apiKey = process.env.MAILKITE_API_KEY;
89
+ const baseUrl = process.env.MAILKITE_BASE_URL;
90
+ const mk = new MailKite(apiKey || "", baseUrl || undefined);
91
+
92
+ // Split a tool's flat input into (resolved path, validated body).
93
+ function resolveCall(method, input) {
94
+ input = input || {};
95
+ let urlPath = method.http.path;
96
+ let bodySchemaId = null;
97
+
98
+ for (const arg of method.args || []) {
99
+ if (arg.in === "path") {
100
+ const value = input[arg.name];
101
+ if (value == null || value === "") throw new Error(`Missing required path parameter: ${arg.name}`);
102
+ urlPath = urlPath.replace(`{${arg.name}}`, encodeURIComponent(String(value)));
103
+ } else if (arg.in === "body" && arg.schema) {
104
+ bodySchemaId = arg.schema;
105
+ }
106
+ }
107
+
108
+ let body;
109
+ if (bodySchemaId) {
110
+ // The body is exactly the schema's own properties (path params are excluded,
111
+ // since every body schema is additionalProperties:false).
112
+ const keys = Object.keys(schemas[bodySchemaId].properties || {});
113
+ body = {};
114
+ for (const k of keys) if (input[k] !== undefined) body[k] = input[k];
115
+
116
+ const validate = validators[bodySchemaId];
117
+ if (!validate(body)) {
118
+ throw new Error(`Invalid input for ${bodySchemaId}: ${ajv.errorsText(validate.errors)}`);
119
+ }
120
+ }
121
+
122
+ return { method: method.http.method, urlPath, body };
123
+ }
124
+
125
+ // ---- MCP server -------------------------------------------------------------
126
+ const server = new Server(
127
+ { name: "mailkite", version: api.version || "0.1.0" },
128
+ { capabilities: { tools: {} } }
129
+ );
130
+
131
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
132
+ tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
133
+ }));
134
+
135
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
136
+ const tool = byToolName.get(req.params.name);
137
+ if (!tool) {
138
+ return { isError: true, content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }] };
139
+ }
140
+
141
+ // Local methods (e.g. verifyWebhook) run in-process — no API key, no network.
142
+ // Validate against the same JSON Schema, then dispatch through the SDK.
143
+ if (tool._method.local) {
144
+ try {
145
+ const input = req.params.arguments || {};
146
+ const bodyArg = (tool._method.args || []).find((a) => a.in === "body");
147
+ if (bodyArg?.schema) {
148
+ const validate = validators[bodyArg.schema];
149
+ if (!validate(input)) {
150
+ return {
151
+ isError: true,
152
+ content: [{ type: "text", text: `Invalid input for ${bodyArg.schema}: ${ajv.errorsText(validate.errors)}` }],
153
+ };
154
+ }
155
+ }
156
+ if (tool._method.name !== "verifyWebhook") {
157
+ throw new Error(`No local handler for ${tool._method.name}`);
158
+ }
159
+ const valid = mk.verifyWebhook(input.signature, input.payload, input.secret, input.toleranceMs);
160
+ return { content: [{ type: "text", text: JSON.stringify({ valid }, null, 2) }] };
161
+ } catch (err) {
162
+ return { isError: true, content: [{ type: "text", text: `Error: ${err.message}` }] };
163
+ }
164
+ }
165
+
166
+ if (!apiKey) {
167
+ return {
168
+ isError: true,
169
+ content: [{ type: "text", text: "MAILKITE_API_KEY is not set. Provide it in the MCP server env." }],
170
+ };
171
+ }
172
+
173
+ try {
174
+ const { method, urlPath, body } = resolveCall(tool._method, req.params.arguments);
175
+ const result = await mk.request(method, urlPath, body);
176
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
177
+ } catch (err) {
178
+ const text =
179
+ err instanceof MailKiteError
180
+ ? `MailKite API error ${err.status}: ${err.message}`
181
+ : `Error: ${err.message}`;
182
+ return { isError: true, content: [{ type: "text", text }] };
183
+ }
184
+ });
185
+
186
+ const transport = new StdioServerTransport();
187
+ await server.connect(transport);
188
+ // Note: stdout is the MCP transport — never console.log here. Use stderr.
189
+ console.error(`mailkite mcp server ready — ${tools.length} tools, base ${mk.baseUrl}`);
package/spec/api.json ADDED
@@ -0,0 +1,121 @@
1
+ {
2
+ "name": "mailkite",
3
+ "version": "0.1.0",
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
+ "baseUrl": "https://api.mailkite.dev",
6
+ "auth": {
7
+ "scheme": "bearer",
8
+ "header": "Authorization",
9
+ "note": "Send uses an API key (mk_live_…); management endpoints use a session token. Both are Bearer credentials, so the client takes one token."
10
+ },
11
+ "methods": [
12
+ {
13
+ "name": "send",
14
+ "summary": "Send a message over a verified domain.",
15
+ "http": { "method": "POST", "path": "/v1/send" },
16
+ "args": [{ "name": "message", "in": "body", "schema": "send-request" }],
17
+ "returns": "send-response"
18
+ },
19
+ {
20
+ "name": "listDomains",
21
+ "summary": "List your domains, each with its webhook URL.",
22
+ "http": { "method": "GET", "path": "/api/domains" },
23
+ "args": [],
24
+ "returns": "any"
25
+ },
26
+ {
27
+ "name": "createDomain",
28
+ "summary": "Add a domain. Returns the domain + DNS records.",
29
+ "http": { "method": "POST", "path": "/api/domains" },
30
+ "args": [{ "name": "body", "in": "body", "schema": "create-domain-request" }],
31
+ "returns": "any"
32
+ },
33
+ {
34
+ "name": "getDomain",
35
+ "summary": "Get one domain with DNS records + webhook.",
36
+ "http": { "method": "GET", "path": "/api/domains/{id}" },
37
+ "args": [{ "name": "id", "in": "path" }],
38
+ "returns": "any"
39
+ },
40
+ {
41
+ "name": "deleteDomain",
42
+ "summary": "Remove a domain.",
43
+ "http": { "method": "DELETE", "path": "/api/domains/{id}" },
44
+ "args": [{ "name": "id", "in": "path" }],
45
+ "returns": "any"
46
+ },
47
+ {
48
+ "name": "verifyDomain",
49
+ "summary": "Check DNS and update status.",
50
+ "http": { "method": "POST", "path": "/api/domains/{id}/verify" },
51
+ "args": [{ "name": "id", "in": "path" }],
52
+ "returns": "any"
53
+ },
54
+ {
55
+ "name": "setWebhook",
56
+ "summary": "Set or replace the domain's catch-all webhook.",
57
+ "http": { "method": "PUT", "path": "/api/domains/{id}/webhook" },
58
+ "args": [
59
+ { "name": "id", "in": "path" },
60
+ { "name": "body", "in": "body", "schema": "set-webhook-request" }
61
+ ],
62
+ "returns": "any"
63
+ },
64
+ {
65
+ "name": "deleteWebhook",
66
+ "summary": "Remove the domain's webhook.",
67
+ "http": { "method": "DELETE", "path": "/api/domains/{id}/webhook" },
68
+ "args": [{ "name": "id", "in": "path" }],
69
+ "returns": "any"
70
+ },
71
+ {
72
+ "name": "testWebhook",
73
+ "summary": "Send a signed test event to the domain's webhook.",
74
+ "http": { "method": "POST", "path": "/api/domains/{id}/webhook/test" },
75
+ "args": [{ "name": "id", "in": "path" }],
76
+ "returns": "any"
77
+ },
78
+ {
79
+ "name": "listRoutes",
80
+ "summary": "List inbound routing rules.",
81
+ "http": { "method": "GET", "path": "/api/routes" },
82
+ "args": [],
83
+ "returns": "any"
84
+ },
85
+ {
86
+ "name": "createRoute",
87
+ "summary": "Create a route (match, action, destination).",
88
+ "http": { "method": "POST", "path": "/api/routes" },
89
+ "args": [{ "name": "body", "in": "body", "schema": "create-route-request" }],
90
+ "returns": "any"
91
+ },
92
+ {
93
+ "name": "listMessages",
94
+ "summary": "List stored messages.",
95
+ "http": { "method": "GET", "path": "/api/messages" },
96
+ "args": [],
97
+ "returns": "any"
98
+ },
99
+ {
100
+ "name": "getMessage",
101
+ "summary": "Get a message with deliveries + attachments.",
102
+ "http": { "method": "GET", "path": "/api/messages/{id}" },
103
+ "args": [{ "name": "id", "in": "path" }],
104
+ "returns": "any"
105
+ },
106
+ {
107
+ "name": "retryDelivery",
108
+ "summary": "Re-deliver a stored message to its webhook.",
109
+ "http": { "method": "POST", "path": "/api/deliveries/{id}/retry" },
110
+ "args": [{ "name": "id", "in": "path" }],
111
+ "returns": "any"
112
+ },
113
+ {
114
+ "name": "verifyWebhook",
115
+ "summary": "Verify the `x-mailkite-signature` header on an inbound webhook delivery. Runs entirely locally (HMAC-SHA256 over `${t}.${payload}`) — no network call. Returns true only when the signature matches and the event is within the freshness window.",
116
+ "local": true,
117
+ "args": [{ "name": "body", "in": "body", "schema": "verify-webhook-request" }],
118
+ "returns": "boolean"
119
+ }
120
+ ]
121
+ }
@@ -0,0 +1,244 @@
1
+ {
2
+ "comment": "Shared cross-language conformance cases. Each SDK's conformance runner reads this file, calls the named method with `args`, and reports the parsed result (or error). The Node harness in sdks/conformance asserts the outgoing HTTP request matches `request` (and validates the body against the referenced JSON Schema) and that the SDK's result matches `result`/`error`. Cases with `\"local\": true` make no HTTP call (e.g. verifyWebhook); for those the harness validates `args` against `argsSchema` and only checks the returned `result`.",
3
+ "apiKey": "test_key",
4
+ "cases": [
5
+ {
6
+ "name": "send_minimal",
7
+ "method": "send",
8
+ "args": { "from": "hello@app.mailkite.dev", "to": "ada@example.com", "subject": "Hi", "text": "It works." },
9
+ "request": {
10
+ "method": "POST",
11
+ "path": "/v1/send",
12
+ "bodySchema": "send-request",
13
+ "body": { "from": "hello@app.mailkite.dev", "to": "ada@example.com", "subject": "Hi", "text": "It works." }
14
+ },
15
+ "response": { "status": 202, "body": { "id": "msg_minimal", "status": "queued" } },
16
+ "result": { "id": "msg_minimal", "status": "queued" }
17
+ },
18
+ {
19
+ "name": "send_full",
20
+ "method": "send",
21
+ "args": {
22
+ "from": "hello@app.mailkite.dev",
23
+ "to": ["ada@example.com", "grace@example.com"],
24
+ "cc": "cc@example.com",
25
+ "subject": "Your invoice #1042",
26
+ "html": "<p>Thanks!</p>",
27
+ "text": "Thanks!",
28
+ "replyTo": "support@app.mailkite.dev",
29
+ "inReplyTo": "<a1b2@mail.example.com>",
30
+ "attachments": [{ "filename": "receipt.pdf", "url": "https://files.app.com/receipt.pdf" }]
31
+ },
32
+ "request": {
33
+ "method": "POST",
34
+ "path": "/v1/send",
35
+ "bodySchema": "send-request",
36
+ "body": {
37
+ "from": "hello@app.mailkite.dev",
38
+ "to": ["ada@example.com", "grace@example.com"],
39
+ "cc": "cc@example.com",
40
+ "subject": "Your invoice #1042",
41
+ "html": "<p>Thanks!</p>",
42
+ "text": "Thanks!",
43
+ "replyTo": "support@app.mailkite.dev",
44
+ "inReplyTo": "<a1b2@mail.example.com>",
45
+ "attachments": [{ "filename": "receipt.pdf", "url": "https://files.app.com/receipt.pdf" }]
46
+ }
47
+ },
48
+ "response": { "status": 202, "body": { "id": "msg_full", "status": "queued" } },
49
+ "result": { "id": "msg_full", "status": "queued" }
50
+ },
51
+ {
52
+ "name": "list_domains",
53
+ "method": "listDomains",
54
+ "args": {},
55
+ "request": { "method": "GET", "path": "/api/domains" },
56
+ "response": { "status": 200, "body": [{ "id": "dom_1", "domain": "app.mailkite.dev" }] },
57
+ "result": [{ "id": "dom_1", "domain": "app.mailkite.dev" }]
58
+ },
59
+ {
60
+ "name": "create_domain",
61
+ "method": "createDomain",
62
+ "args": { "domain": "app.mailkite.dev" },
63
+ "request": {
64
+ "method": "POST",
65
+ "path": "/api/domains",
66
+ "bodySchema": "create-domain-request",
67
+ "body": { "domain": "app.mailkite.dev" }
68
+ },
69
+ "response": { "status": 200, "body": { "domain": { "id": "dom_1", "status": "pending" }, "dns": [] } },
70
+ "result": { "domain": { "id": "dom_1", "status": "pending" }, "dns": [] }
71
+ },
72
+ {
73
+ "name": "get_domain",
74
+ "method": "getDomain",
75
+ "args": { "id": "dom_1" },
76
+ "request": { "method": "GET", "path": "/api/domains/dom_1" },
77
+ "response": { "status": 200, "body": { "id": "dom_1", "domain": "app.mailkite.dev" } },
78
+ "result": { "id": "dom_1", "domain": "app.mailkite.dev" }
79
+ },
80
+ {
81
+ "name": "delete_domain",
82
+ "method": "deleteDomain",
83
+ "args": { "id": "dom_1" },
84
+ "request": { "method": "DELETE", "path": "/api/domains/dom_1" },
85
+ "response": { "status": 200, "body": { "ok": true } },
86
+ "result": { "ok": true }
87
+ },
88
+ {
89
+ "name": "verify_domain",
90
+ "method": "verifyDomain",
91
+ "args": { "id": "dom_1" },
92
+ "request": { "method": "POST", "path": "/api/domains/dom_1/verify" },
93
+ "response": { "status": 200, "body": { "id": "dom_1", "status": "verified" } },
94
+ "result": { "id": "dom_1", "status": "verified" }
95
+ },
96
+ {
97
+ "name": "set_webhook",
98
+ "method": "setWebhook",
99
+ "args": { "id": "dom_1", "url": "https://app.com/hooks/mailkite" },
100
+ "request": {
101
+ "method": "PUT",
102
+ "path": "/api/domains/dom_1/webhook",
103
+ "bodySchema": "set-webhook-request",
104
+ "body": { "url": "https://app.com/hooks/mailkite" }
105
+ },
106
+ "response": { "status": 200, "body": { "ok": true, "url": "https://app.com/hooks/mailkite" } },
107
+ "result": { "ok": true, "url": "https://app.com/hooks/mailkite" }
108
+ },
109
+ {
110
+ "name": "delete_webhook",
111
+ "method": "deleteWebhook",
112
+ "args": { "id": "dom_1" },
113
+ "request": { "method": "DELETE", "path": "/api/domains/dom_1/webhook" },
114
+ "response": { "status": 200, "body": { "ok": true } },
115
+ "result": { "ok": true }
116
+ },
117
+ {
118
+ "name": "test_webhook",
119
+ "method": "testWebhook",
120
+ "args": { "id": "dom_1" },
121
+ "request": { "method": "POST", "path": "/api/domains/dom_1/webhook/test" },
122
+ "response": { "status": 200, "body": { "ok": true } },
123
+ "result": { "ok": true }
124
+ },
125
+ {
126
+ "name": "list_routes",
127
+ "method": "listRoutes",
128
+ "args": {},
129
+ "request": { "method": "GET", "path": "/api/routes" },
130
+ "response": { "status": 200, "body": [] },
131
+ "result": []
132
+ },
133
+ {
134
+ "name": "create_route",
135
+ "method": "createRoute",
136
+ "args": { "match": "*@app.mailkite.dev", "action": "webhook", "destination": "https://app.com/hooks" },
137
+ "request": {
138
+ "method": "POST",
139
+ "path": "/api/routes",
140
+ "bodySchema": "create-route-request",
141
+ "body": { "match": "*@app.mailkite.dev", "action": "webhook", "destination": "https://app.com/hooks" }
142
+ },
143
+ "response": { "status": 200, "body": { "id": "rte_1" } },
144
+ "result": { "id": "rte_1" }
145
+ },
146
+ {
147
+ "name": "list_messages",
148
+ "method": "listMessages",
149
+ "args": {},
150
+ "request": { "method": "GET", "path": "/api/messages" },
151
+ "response": { "status": 200, "body": [{ "id": "msg_1" }] },
152
+ "result": [{ "id": "msg_1" }]
153
+ },
154
+ {
155
+ "name": "get_message",
156
+ "method": "getMessage",
157
+ "args": { "id": "msg_1" },
158
+ "request": { "method": "GET", "path": "/api/messages/msg_1" },
159
+ "response": { "status": 200, "body": { "id": "msg_1", "deliveries": [], "attachments": [] } },
160
+ "result": { "id": "msg_1", "deliveries": [], "attachments": [] }
161
+ },
162
+ {
163
+ "name": "retry_delivery",
164
+ "method": "retryDelivery",
165
+ "args": { "id": "dlv_1" },
166
+ "request": { "method": "POST", "path": "/api/deliveries/dlv_1/retry" },
167
+ "response": { "status": 200, "body": { "ok": true } },
168
+ "result": { "ok": true }
169
+ },
170
+ {
171
+ "name": "get_message_not_found",
172
+ "method": "getMessage",
173
+ "args": { "id": "msg_missing" },
174
+ "request": { "method": "GET", "path": "/api/messages/msg_missing" },
175
+ "response": { "status": 404, "body": { "error": "not found" } },
176
+ "error": { "status": 404, "message": "not found" }
177
+ },
178
+ {
179
+ "name": "send_upstream_error",
180
+ "method": "send",
181
+ "args": { "from": "hello@app.mailkite.dev", "to": "ada@example.com", "subject": "Hi", "text": "x" },
182
+ "request": {
183
+ "method": "POST",
184
+ "path": "/v1/send",
185
+ "bodySchema": "send-request",
186
+ "body": { "from": "hello@app.mailkite.dev", "to": "ada@example.com", "subject": "Hi", "text": "x" }
187
+ },
188
+ "response": { "status": 502, "body": { "error": "an upstream send failed" } },
189
+ "error": { "status": 502, "message": "an upstream send failed" }
190
+ },
191
+ {
192
+ "name": "verify_webhook_valid",
193
+ "method": "verifyWebhook",
194
+ "local": true,
195
+ "argsSchema": "verify-webhook-request",
196
+ "args": {
197
+ "signature": "t=1750000000000,v1=3d790f831e170ddba4d001f27532bf2c1fc68ebed52eef72fe453dfa1196b03c",
198
+ "payload": "{\"type\":\"email.received\",\"id\":\"evt_123\",\"message\":\"It works.\"}",
199
+ "secret": "whsec_mailkite_test",
200
+ "toleranceMs": 0
201
+ },
202
+ "result": true
203
+ },
204
+ {
205
+ "name": "verify_webhook_tampered_body",
206
+ "method": "verifyWebhook",
207
+ "local": true,
208
+ "argsSchema": "verify-webhook-request",
209
+ "args": {
210
+ "signature": "t=1750000000000,v1=3d790f831e170ddba4d001f27532bf2c1fc68ebed52eef72fe453dfa1196b03c",
211
+ "payload": "{\"type\":\"email.received\",\"id\":\"evt_123\",\"message\":\"It WORKS.\"}",
212
+ "secret": "whsec_mailkite_test",
213
+ "toleranceMs": 0
214
+ },
215
+ "result": false
216
+ },
217
+ {
218
+ "name": "verify_webhook_wrong_secret",
219
+ "method": "verifyWebhook",
220
+ "local": true,
221
+ "argsSchema": "verify-webhook-request",
222
+ "args": {
223
+ "signature": "t=1750000000000,v1=3d790f831e170ddba4d001f27532bf2c1fc68ebed52eef72fe453dfa1196b03c",
224
+ "payload": "{\"type\":\"email.received\",\"id\":\"evt_123\",\"message\":\"It works.\"}",
225
+ "secret": "whsec_wrong",
226
+ "toleranceMs": 0
227
+ },
228
+ "result": false
229
+ },
230
+ {
231
+ "name": "verify_webhook_malformed_header",
232
+ "method": "verifyWebhook",
233
+ "local": true,
234
+ "argsSchema": "verify-webhook-request",
235
+ "args": {
236
+ "signature": "not-a-valid-signature",
237
+ "payload": "{\"type\":\"email.received\",\"id\":\"evt_123\",\"message\":\"It works.\"}",
238
+ "secret": "whsec_mailkite_test",
239
+ "toleranceMs": 0
240
+ },
241
+ "result": false
242
+ }
243
+ ]
244
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "create-domain-request",
4
+ "title": "Create domain request body",
5
+ "type": "object",
6
+ "required": ["domain"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "domain": { "type": "string" }
10
+ }
11
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "create-route-request",
4
+ "title": "Create route request body",
5
+ "type": "object",
6
+ "required": ["match", "action", "destination"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "match": { "type": "string" },
10
+ "action": { "type": "string" },
11
+ "destination": { "type": "string" }
12
+ }
13
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "send-request",
4
+ "title": "Send request body",
5
+ "type": "object",
6
+ "required": ["from", "to", "subject"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "from": { "type": "string", "description": "An address on a verified domain." },
10
+ "to": {
11
+ "description": "One recipient or a list.",
12
+ "oneOf": [
13
+ { "type": "string" },
14
+ { "type": "array", "items": { "type": "string" }, "minItems": 1 }
15
+ ]
16
+ },
17
+ "subject": { "type": "string" },
18
+ "html": { "type": "string" },
19
+ "text": { "type": "string" },
20
+ "cc": { "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] },
21
+ "bcc": { "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] },
22
+ "replyTo": { "type": "string" },
23
+ "inReplyTo": { "type": "string" },
24
+ "attachments": {
25
+ "type": "array",
26
+ "items": {
27
+ "type": "object",
28
+ "required": ["filename"],
29
+ "properties": {
30
+ "filename": { "type": "string" },
31
+ "url": { "type": "string" },
32
+ "content": { "type": "string" },
33
+ "contentType": { "type": "string" }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "send-response",
4
+ "title": "Send response body",
5
+ "type": "object",
6
+ "required": ["id", "status"],
7
+ "properties": {
8
+ "id": { "type": "string" },
9
+ "status": { "type": "string" }
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "set-webhook-request",
4
+ "title": "Set webhook request body",
5
+ "type": "object",
6
+ "required": ["url"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "url": { "type": "string" }
10
+ }
11
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "verify-webhook-request",
4
+ "title": "Verify webhook request",
5
+ "description": "Inputs to verifyWebhook — a local HMAC check, no API call.",
6
+ "type": "object",
7
+ "required": ["payload", "signature", "secret"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "payload": {
11
+ "type": "string",
12
+ "description": "The raw, unparsed webhook request body — the exact bytes you received."
13
+ },
14
+ "signature": {
15
+ "type": "string",
16
+ "description": "The `x-mailkite-signature` header value, e.g. `t=1750000000000,v1=4f1a9c…`."
17
+ },
18
+ "secret": {
19
+ "type": "string",
20
+ "description": "Your webhook signing secret (from the dashboard)."
21
+ },
22
+ "toleranceMs": {
23
+ "type": "integer",
24
+ "minimum": 0,
25
+ "description": "Reject events whose timestamp is more than this many milliseconds old, to block replays. Defaults to 300000 (5 minutes). Pass 0 to disable the freshness check."
26
+ }
27
+ }
28
+ }