diff --git a/.env.example b/.env.example index 9d01f81..b47565c 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ RESEND_API_KEY= SENDER_EMAIL_ADDRESS= REPLY_TO_EMAIL_ADDRESSES= -MCP_PORT=3000 \ No newline at end of file +MCP_PORT=3000 +RESEND_OPENAPI_SPEC_URL= diff --git a/README.md b/README.md index 67e6421..7980439 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ An MCP server for the [Resend](https://resend.com/) platform. Send and receive e - **Contact Properties** — Create, list, get, update, and remove custom contact attributes. - **API Keys** — Create, list, and remove API keys. - **Webhooks** — Create, list, get, update, and remove webhooks for event notifications. +- **Code Mode** — Search REST method specs and execute sandboxed JavaScript that can orchestrate multi-step REST API flows. ## Setup @@ -118,6 +119,7 @@ You can pass additional arguments to configure the server: - `--sender`: Default sender email address from a verified domain - `--reply-to`: Default reply-to email address (can be specified multiple times) - `--http`: Use HTTP transport instead of stdio (default: stdio) +- `--code-mode-only`: Expose only Code Mode tools (`search-resend-api`, `execute-resend-code`) - `--port`: HTTP port when using `--http` (default: 3000, or `MCP_PORT` env var) Environment variables: @@ -126,6 +128,52 @@ Environment variables: - `SENDER_EMAIL_ADDRESS`: Default sender email address from a verified domain (optional) - `REPLY_TO_EMAIL_ADDRESSES`: Comma-separated reply-to email addresses (optional) - `MCP_PORT`: HTTP port when using `--http` (optional) +- `RESEND_OPENAPI_SPEC_URL`: Optional URL for the Resend OpenAPI spec (e.g. `https://raw.githubusercontent.com/resend/resend-openapi/refs/heads/main/resend.yaml`). When set, Code Mode loads the spec from this URL instead of the bundled file so you can use the latest spec from GitHub. + +### Code Mode + +Code Mode uses two tools and the Resend OpenAPI spec (with all `$ref` s pre-resolved) as the single source of truth. This keeps the tool footprint small no matter how many endpoints exist. + +- **`search-resend-api`**: Run JavaScript against the spec to discover endpoints. Your code runs as the body of an async function; `spec` is in scope. Use a top-level **return** for the result. Do not pass an arrow function—pass only statements. Right: `return Object.keys(spec.paths);` Wrong: `async (spec) => { return ... }`. +- **`execute-resend-code`**: Run JavaScript against the Resend API. Your code runs as the body of an async function; `resend` is in scope. Call `resend.request({ method, path, params?, body? })` and use a top-level **return** for the result. Do not pass an arrow function. Optional: `input`, `helpers`, `console`. + +Example search (discover email endpoints): + +```js +const results = []; +for (const [path, methods] of Object.entries(spec.paths)) { + if (path.startsWith('/emails') && path !== '/emails/batch') { + for (const [method, op] of Object.entries(methods)) { + if (op && op.summary) results.push({ method: method.toUpperCase(), path, summary: op.summary }); + } + } +} +return results; +``` + +Example execute (send an email): + +```js +return await resend.request({ + method: 'POST', + path: '/emails', + body: { from: 'you@example.com', to: 'user@example.com', subject: 'Hi', text: 'Hello' }, +}); +``` + +### Code Mode pattern and security + +This server uses the same **Code Mode** idea as [Cloudflare’s MCP post](https://blog.cloudflare.com/code-mode-mcp/) and [Anthropic’s code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp): two tools (search + execute), spec pre-resolved, single request API. There is no Cloudflare or Anthropic integration—only the Resend API. + +- **Sandbox**: Code runs in a Node.js `vm` with only `spec` (search) or `resend`, `input`, `helpers`, `console` (execute). No `process`, `require`, or timers. +- **Execute**: Only `resend.request()` can do I/O; it calls the Resend API only. Timeouts and `maxApiCalls` apply. +- **Note**: Node’s `vm` is [not a security boundary](https://nodejs.org/api/vm.html#vm-executing-javascript). Fine for normal MCP use (your API key, your agent). For untrusted code, use a real isolate (e.g. separate process). + +To test Code Mode as a full replacement for the granular tools: + +```bash +npx -y resend-mcp --code-mode-only +``` > [!NOTE] > If you don't provide a sender email address, the MCP server will ask you to provide one each time you call the tool. diff --git a/package.json b/package.json index 2e9b38d..97c57fd 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "dist" ], "scripts": { - "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"", + "clean": "rm -rf dist", + "build": "pnpm run clean && tsc && bash scripts/build-post.sh", "prepare": "npm run build", "inspector": "npx @modelcontextprotocol/inspector@latest", "lint": "biome check .", @@ -23,6 +24,7 @@ "express": "5.2.1", "minimist": "1.2.8", "resend": "6.9.2", + "yaml": "2.8.2", "zod": "4.3.6" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d23fb23..4b4bd71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: resend: specifier: 6.9.2 version: 6.9.2(@react-email/render@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + yaml: + specifier: 2.8.2 + version: 2.8.2 zod: specifier: 4.3.6 version: 4.3.6 @@ -41,7 +44,7 @@ importers: version: 5.9.3 vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@25.0.3) + version: 3.2.4(@types/node@25.0.3)(yaml@2.8.2) packages: @@ -1119,6 +1122,11 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -1385,13 +1393,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.0.3))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.0.3)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.0.3) + vite: 7.3.1(@types/node@25.0.3)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -2030,13 +2038,13 @@ snapshots: vary@1.1.2: {} - vite-node@3.2.4(@types/node@25.0.3): + vite-node@3.2.4(@types/node@25.0.3)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@25.0.3) + vite: 7.3.1(@types/node@25.0.3)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -2051,7 +2059,7 @@ snapshots: - tsx - yaml - vite@7.3.1(@types/node@25.0.3): + vite@7.3.1(@types/node@25.0.3)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -2062,12 +2070,13 @@ snapshots: optionalDependencies: '@types/node': 25.0.3 fsevents: 2.3.3 + yaml: 2.8.2 - vitest@3.2.4(@types/node@25.0.3): + vitest@3.2.4(@types/node@25.0.3)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.0.3)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.0.3)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -2085,8 +2094,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@25.0.3) - vite-node: 3.2.4(@types/node@25.0.3) + vite: 7.3.1(@types/node@25.0.3)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@25.0.3)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.0.3 @@ -2115,6 +2124,8 @@ snapshots: wrappy@1.0.2: {} + yaml@2.8.2: {} + zod-to-json-schema@3.25.1(zod@4.3.6): dependencies: zod: 4.3.6 diff --git a/scripts/build-post.sh b/scripts/build-post.sh new file mode 100644 index 0000000..2b215dc --- /dev/null +++ b/scripts/build-post.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(dirname "$SCRIPT_DIR")" + +mkdir -p "$ROOT/dist/openapi" +cp "$ROOT/src/openapi/resend-openapi.yaml" "$ROOT/dist/openapi/resend-openapi.yaml" + +BIN="$ROOT/dist/index.js" +if [[ ! -f "$BIN" ]]; then + echo "build-post: $BIN not found (tsc may have failed)" >&2 + exit 1 +fi +chmod 755 "$BIN" diff --git a/src/cli/help.ts b/src/cli/help.ts index 76fb68e..abec7e3 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -11,6 +11,7 @@ Options: --sender Default from address (or SENDER_EMAIL_ADDRESS) --reply-to Reply-to; repeat for multiple (or REPLY_TO_EMAIL_ADDRESSES) --http Run HTTP server (Streamable HTTP at /mcp) instead of stdio + --code-mode-only Expose only search-resend-api and execute-resend-code tools --port HTTP port when using --http (default: 3000, or MCP_PORT) -h, --help Show this help diff --git a/src/cli/parse.ts b/src/cli/parse.ts index 655f152..b0f741b 100644 --- a/src/cli/parse.ts +++ b/src/cli/parse.ts @@ -8,7 +8,7 @@ import { CLI_STRING_OPTIONS } from './constants.js'; export function parseArgs(argv: string[] = process.argv.slice(2)): ParsedArgs { return minimist(argv, { string: [...CLI_STRING_OPTIONS], - boolean: ['help', 'http'], + boolean: ['help', 'http', 'code-mode-only'], alias: { h: 'help' }, }); } diff --git a/src/cli/resolve.ts b/src/cli/resolve.ts index 8dad370..1b5bf7e 100644 --- a/src/cli/resolve.ts +++ b/src/cli/resolve.ts @@ -32,6 +32,7 @@ export function resolveConfig( null; const http = parsed.http === true; + const codeModeOnly = parsed['code-mode-only'] === true; // Stdio requires an API key at startup. HTTP mode is lenient because // each client provides their own key via the Authorization: Bearer header. @@ -54,6 +55,7 @@ export function resolveConfig( const base = { senderEmailAddress: senderEmailAddress ?? '', replierEmailAddresses: parseReplierAddresses(parsed, env), + codeModeOnly, port, }; diff --git a/src/cli/types.ts b/src/cli/types.ts index 90c5fd0..7252fa5 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -7,6 +7,7 @@ export interface StdioConfig { apiKey: string; senderEmailAddress: string; replierEmailAddresses: string[]; + codeModeOnly: boolean; transport: 'stdio'; port: number; } @@ -19,6 +20,7 @@ export interface HttpConfig { apiKey?: string; senderEmailAddress: string; replierEmailAddresses: string[]; + codeModeOnly: boolean; transport: 'http'; port: number; } diff --git a/src/index.ts b/src/index.ts index ec60ca3..433ed2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,8 +8,10 @@ import { runStdio } from './transports/stdio.js'; const parsed = parseArgs(process.argv.slice(2)); const config = resolveConfigOrExit(parsed, process.env); const serverOptions = { + apiKey: config.apiKey, senderEmailAddress: config.senderEmailAddress, replierEmailAddresses: config.replierEmailAddresses, + codeModeOnly: config.codeModeOnly, }; function onFatal(err: unknown): void { diff --git a/src/openapi/loader.ts b/src/openapi/loader.ts new file mode 100644 index 0000000..7a3ac8f --- /dev/null +++ b/src/openapi/loader.ts @@ -0,0 +1,132 @@ +/** + * Loads resend-openapi.yaml and returns the OpenAPI spec with all $refs + * pre-resolved inline (Code Mode standard: "All $refs are pre-resolved inline"). + * Used by both search (expose `spec` to agent code) and execute (drive request() from spec). + * + * Source (first match wins): + * - RESEND_OPENAPI_SPEC_URL env: fetch from this URL (e.g. latest from GitHub). + * - Else: bundled resend-openapi.yaml next to this file. + */ +import { readFileSync } from 'node:fs'; +import { parse as parseYaml } from 'yaml'; + +const OPENAPI_DOC_PATH = new URL('./resend-openapi.yaml', import.meta.url); + +/** Official Resend OpenAPI spec on GitHub (main branch). */ +export const RESEND_OPENAPI_SPEC_URL = + 'https://raw.githubusercontent.com/resend/resend-openapi/refs/heads/main/resend.yaml'; + +export type ResolvedSpec = Record; + +let cachedResolved: ResolvedSpec | null = null; + +function deepClone(value: T): T { + if (value === null || typeof value !== 'object') return value; + if (Array.isArray(value)) return value.map(deepClone) as T; + const out: Record = {}; + for (const [k, v] of Object.entries(value as Record)) + out[k] = deepClone(v); + return out as T; +} + +function resolveRef(spec: Record, ref: string): unknown { + if (typeof ref !== 'string' || !ref.startsWith('#/')) return null; + const parts = ref.slice(2).split('/'); + let current: unknown = spec; + for (const part of parts) { + if (current == null || typeof current !== 'object') return null; + current = (current as Record)[part]; + } + return current; +} + +/** + * Recursively resolve $ref in a value. Objects that are exactly { $ref: "#/..." } + * are replaced by the resolved reference. Nested $refs inside the referenced + * object are also resolved. + */ +function resolveRefsInValue( + spec: Record, + value: unknown, + seen: Set, +): unknown { + if ( + value === null || + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + if (Array.isArray(value)) { + return value.map((item) => resolveRefsInValue(spec, item, seen)); + } + if (typeof value === 'object') { + const obj = value as Record; + if (Object.keys(obj).length === 1 && typeof obj.$ref === 'string') { + const resolved = resolveRef(spec, obj.$ref); + if (resolved != null) { + const cloned = deepClone(resolved); + return resolveRefsInValue(spec, cloned, seen); + } + } + if (seen.has(value)) return value; + seen.add(value); + const out: Record = {}; + for (const [k, v] of Object.entries(obj)) { + if (k === '$ref') continue; + out[k] = resolveRefsInValue(spec, v, seen); + } + return out; + } + return value; +} + +function loadRawSpec(): string { + return readFileSync(OPENAPI_DOC_PATH, 'utf8'); +} + +async function loadRawSpecAsync(): Promise { + const url = + typeof process !== 'undefined' && + process.env?.RESEND_OPENAPI_SPEC_URL?.trim(); + if (url) { + const res = await fetch(url); + if (!res.ok) { + throw new Error( + `Failed to fetch OpenAPI spec from ${url}: ${res.status} ${res.statusText}`, + ); + } + return res.text(); + } + return readFileSync(OPENAPI_DOC_PATH, 'utf8'); +} + +function parseAndResolve(raw: string): ResolvedSpec { + const parsed = parseYaml(raw) as Record; + const seen = new Set(); + return resolveRefsInValue(parsed, deepClone(parsed), seen) as ResolvedSpec; +} + +/** + * Returns the Resend OpenAPI spec with all $refs resolved inline. + * Loads from RESEND_OPENAPI_SPEC_URL if set (e.g. GitHub raw URL), otherwise from the bundled file. + * Safe to expose to agent code (search) and use to drive request() (execute). + */ +export async function getResolvedOpenApiSpec(): Promise { + if (cachedResolved) return cachedResolved; + const raw = await loadRawSpecAsync(); + cachedResolved = parseAndResolve(raw); + return cachedResolved; +} + +/** + * Sync version: only works when RESEND_OPENAPI_SPEC_URL is not set (uses bundled file). + * Use getResolvedOpenApiSpec() when you support loading from URL. + */ +export function getResolvedOpenApiSpecSync(): ResolvedSpec { + if (cachedResolved) return cachedResolved; + const raw = loadRawSpec(); + cachedResolved = parseAndResolve(raw); + return cachedResolved; +} diff --git a/src/openapi/resend-openapi.yaml b/src/openapi/resend-openapi.yaml new file mode 100644 index 0000000..447835b --- /dev/null +++ b/src/openapi/resend-openapi.yaml @@ -0,0 +1,3712 @@ +# https://raw.githubusercontent.com/resend/resend-openapi/refs/heads/main/resend.yaml + +openapi: 3.0.3 +info: + title: Resend + version: 1.5.0 + description: 'Resend is the email platform for developers.' +servers: + - url: https://api.resend.com +security: + - bearerAuth: [] +tags: + - name: Emails + description: Start sending emails through the Resend API. + - name: Domains + description: Create and manage domains through the Resend API. + - name: API Keys + description: Create and manage API Keys through the Resend API. + - name: Audiences + description: 'Deprecated: Use Segments instead. Create and manage Audiences through the Resend API.' + - name: Contacts + description: Create and manage Contacts through the Resend API. + - name: Receiving Emails + description: Retrieve and manage received emails and attachments through the Resend API. + - name: Webhooks + description: Create and manage Webhooks through the Resend API. + - name: Templates + description: Create and manage Templates through the Resend API. + - name: Broadcasts + description: Create and manage Broadcasts through the Resend API. + - name: Segments + description: Create and manage Segments through the Resend API. + - name: Topics + description: Create and manage Topics through the Resend API. + - name: Contact Properties + description: Create and manage Contact Properties through the Resend API. +paths: + /emails: + post: + tags: + - Emails + summary: Send an email + parameters: + - in: header + name: Idempotency-Key + required: false + schema: + type: string + maxLength: 256 + description: A unique identifier for the request to ensure emails are only sent once. [Learn more](https://resend.com/docs/dashboard/emails/idempotency-keys) + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SendEmailRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SendEmailResponse' + get: + tags: + - Emails + summary: Retrieve a list of emails + parameters: + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListEmailsResponse' + /emails/{email_id}: + get: + tags: + - Emails + summary: Retrieve a single email + parameters: + - name: email_id + in: path + required: true + schema: + type: string + description: The ID of the email. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Email' + patch: + tags: + - Emails + summary: Update a single email + parameters: + - name: email_id + in: path + required: true + schema: + type: string + description: The ID of the email. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateEmailOptions' + /emails/{email_id}/cancel: + post: + tags: + - Emails + summary: Cancel the schedule of the e-mail. + parameters: + - name: email_id + in: path + required: true + schema: + type: string + description: The ID of the email. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Email' + /emails/batch: + post: + tags: + - Emails + summary: Trigger up to 100 batch emails at once. + parameters: + - in: header + name: Idempotency-Key + required: false + schema: + type: string + maxLength: 256 + description: A unique identifier for the request to ensure emails are only sent once. [Learn more](https://resend.com/docs/dashboard/emails/idempotency-keys) + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SendEmailRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateBatchEmailsResponse' + /emails/{email_id}/attachments: + get: + tags: + - Emails + summary: Retrieve a list of attachments for a sent email + parameters: + - name: email_id + in: path + required: true + schema: + type: string + format: uuid + description: The ID of the email. + - name: limit + in: query + required: false + schema: + type: integer + description: Maximum number of attachments to return. + - name: after + in: query + required: false + schema: + type: string + format: uuid + description: Pagination cursor to fetch results after this attachment ID. Cannot be used with 'before'. + - name: before + in: query + required: false + schema: + type: string + format: uuid + description: Pagination cursor to fetch results before this attachment ID. Cannot be used with 'after'. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListAttachmentsResponse' + /emails/{email_id}/attachments/{attachment_id}: + get: + tags: + - Emails + summary: Retrieve a single attachment for a sent email + parameters: + - name: email_id + in: path + required: true + schema: + type: string + format: uuid + description: The ID of the email. + - name: attachment_id + in: path + required: true + schema: + type: string + format: uuid + description: The ID of the attachment. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RetrievedAttachment' + /emails/receiving: + get: + tags: + - Receiving Emails + summary: Retrieve a list of received emails + parameters: + - name: limit + in: query + required: false + schema: + type: integer + description: Maximum number of received emails to return. + - name: after + in: query + required: false + schema: + type: string + format: uuid + description: Pagination cursor to fetch results after this email ID. Cannot be used with 'before'. + - name: before + in: query + required: false + schema: + type: string + format: uuid + description: Pagination cursor to fetch results before this email ID. Cannot be used with 'after'. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListReceivedEmailsResponse' + /emails/receiving/{email_id}: + get: + tags: + - Receiving Emails + summary: Retrieve a single received email + parameters: + - name: email_id + in: path + required: true + schema: + type: string + format: uuid + description: The ID of the received email. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetReceivedEmailResponse' + /emails/receiving/{email_id}/attachments: + get: + tags: + - Receiving Emails + summary: Retrieve a list of attachments for a received email + parameters: + - name: email_id + in: path + required: true + schema: + type: string + format: uuid + description: The ID of the received email. + - name: limit + in: query + required: false + schema: + type: integer + description: Maximum number of attachments to return. + - name: after + in: query + required: false + schema: + type: string + format: uuid + description: Pagination cursor to fetch results after this attachment ID. Cannot be used with 'before'. + - name: before + in: query + required: false + schema: + type: string + format: uuid + description: Pagination cursor to fetch results before this attachment ID. Cannot be used with 'after'. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListAttachmentsResponse' + /emails/receiving/{email_id}/attachments/{attachment_id}: + get: + tags: + - Receiving Emails + summary: Retrieve a single attachment for a received email + parameters: + - name: email_id + in: path + required: true + schema: + type: string + format: uuid + description: The ID of the received email. + - name: attachment_id + in: path + required: true + schema: + type: string + format: uuid + description: The ID of the attachment. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RetrievedAttachment' + /domains: + post: + tags: + - Domains + summary: Create a new domain + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateDomainRequest' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateDomainResponse' + get: + tags: + - Domains + summary: Retrieve a list of domains + parameters: + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListDomainsResponse' + /domains/{domain_id}: + get: + tags: + - Domains + summary: Retrieve a single domain + parameters: + - name: domain_id + in: path + required: true + schema: + type: string + description: The ID of the domain. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Domain' + patch: + tags: + - Domains + summary: Update an existing domain + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateDomainOptions' + parameters: + - name: domain_id + in: path + required: true + schema: + type: string + description: The ID of the domain. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateDomainResponseSuccess' + delete: + tags: + - Domains + summary: Remove an existing domain + parameters: + - name: domain_id + in: path + required: true + schema: + type: string + description: The ID of the domain. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteDomainResponse' + /domains/{domain_id}/verify: + post: + tags: + - Domains + summary: Verify an existing domain + parameters: + - name: domain_id + in: path + required: true + schema: + type: string + description: The ID of the domain. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VerifyDomainResponse' + /api-keys: + post: + tags: + - API Keys + summary: Create a new API key + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateApiKeyRequest' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateApiKeyResponse' + get: + tags: + - API Keys + summary: Retrieve a list of API keys + parameters: + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListApiKeysResponse' + /api-keys/{api_key_id}: + delete: + tags: + - API Keys + summary: Remove an existing API key + parameters: + - name: api_key_id + in: path + required: true + schema: + type: string + description: The API key ID. + responses: + '200': + description: OK + /templates: + post: + tags: + - Templates + summary: Create a template + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTemplateRequest' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTemplateResponseSuccess' + get: + tags: + - Templates + summary: Retrieve a list of templates + parameters: + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListTemplatesResponseSuccess' + /templates/{id}: + get: + tags: + - Templates + summary: Retrieve a single template + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Template ID or alias. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Template' + patch: + tags: + - Templates + summary: Update an existing template + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Template ID or alias. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateTemplateOptions' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateTemplateResponseSuccess' + delete: + tags: + - Templates + summary: Remove an existing template + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Template ID or alias. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RemoveTemplateResponseSuccess' + /templates/{id}/publish: + post: + tags: + - Templates + summary: Publish a template + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Template ID or alias. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PublishTemplateResponseSuccess' + /templates/{id}/duplicate: + post: + tags: + - Templates + summary: Duplicate a template + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Template ID or alias. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DuplicateTemplateResponseSuccess' + /audiences: + post: + tags: + - Audiences + summary: Create a list of contacts + deprecated: true + description: 'Deprecated: Use Segments instead. These endpoints still work, but will be removed in the future.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAudienceOptions' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAudienceResponseSuccess' + get: + tags: + - Audiences + summary: Retrieve a list of audiences + deprecated: true + description: 'Deprecated: Use Segments instead. These endpoints still work, but will be removed in the future.' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListAudiencesResponseSuccess' + /audiences/{id}: + delete: + tags: + - Audiences + summary: Remove an existing audience + deprecated: true + description: 'Deprecated: Use Segments instead. These endpoints still work, but will be removed in the future.' + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Audience ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RemoveAudienceResponseSuccess' + get: + tags: + - Audiences + summary: Retrieve a single audience + deprecated: true + description: 'Deprecated: Use Segments instead. These endpoints still work, but will be removed in the future.' + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Audience ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetAudienceResponseSuccess' + /contacts: + post: + tags: + - Contacts + summary: Create a new contact + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateContactOptions' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateContactResponseSuccess' + get: + tags: + - Contacts + summary: Retrieve a list of contacts + parameters: + - name: segment_id + in: query + required: false + schema: + type: string + description: Filter contacts by segment ID. + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListContactsResponseSuccess' + /contacts/{id}: + get: + tags: + - Contacts + summary: Retrieve a single contact by ID or email + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Contact ID or email address. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetContactResponseSuccess' + patch: + tags: + - Contacts + summary: Update a single contact by ID or email + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Contact ID or email address. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateContactOptions' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateContactResponseSuccess' + delete: + tags: + - Contacts + summary: Remove an existing contact by ID or email + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Contact ID or email address. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RemoveContactResponseSuccess' + /broadcasts: + post: + tags: + - Broadcasts + summary: Create a broadcast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateBroadcastOptions' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateBroadcastResponseSuccess' + get: + tags: + - Broadcasts + summary: Retrieve a list of broadcasts + parameters: + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListBroadcastsResponseSuccess' + /broadcasts/{id}: + delete: + tags: + - Broadcasts + summary: Remove an existing broadcast that is in the draft status + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Broadcast ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RemoveBroadcastResponseSuccess' + get: + tags: + - Broadcasts + summary: Retrieve a single broadcast + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Broadcast ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetBroadcastResponseSuccess' + patch: + tags: + - Broadcasts + summary: Update an existing broadcast + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Broadcast ID. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateBroadcastOptions' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateBroadcastResponseSuccess' + /broadcasts/{id}/send: + post: + tags: + - Broadcasts + summary: Send or schedule a broadcast + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Broadcast ID. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SendBroadcastOptions' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SendBroadcastResponseSuccess' + /webhooks: + post: + tags: + - Webhooks + summary: Create a new webhook + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateWebhookRequest' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/CreateWebhookResponse' + get: + tags: + - Webhooks + summary: Retrieve a list of webhooks + parameters: + - name: limit + in: query + required: false + schema: + type: integer + description: Maximum number of webhooks to return. + - name: after + in: query + required: false + schema: + type: string + format: uuid + description: Pagination cursor to fetch results after this webhook ID. Cannot be used with 'before'. + - name: before + in: query + required: false + schema: + type: string + format: uuid + description: Pagination cursor to fetch results before this webhook ID. Cannot be used with 'after'. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListWebhooksResponse' + /webhooks/{webhook_id}: + get: + tags: + - Webhooks + summary: Retrieve a single webhook + parameters: + - name: webhook_id + in: path + required: true + schema: + type: string + format: uuid + description: The Webhook ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetWebhookResponse' + patch: + tags: + - Webhooks + summary: Update an existing webhook + parameters: + - name: webhook_id + in: path + required: true + schema: + type: string + format: uuid + description: The Webhook ID. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateWebhookRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateWebhookResponse' + delete: + tags: + - Webhooks + summary: Remove an existing webhook + parameters: + - name: webhook_id + in: path + required: true + schema: + type: string + format: uuid + description: The Webhook ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteWebhookResponse' + /segments: + post: + tags: + - Segments + summary: Create a new segment + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSegmentOptions' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSegmentResponseSuccess' + get: + tags: + - Segments + summary: Retrieve a list of segments + parameters: + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListSegmentsResponseSuccess' + /segments/{id}: + get: + tags: + - Segments + summary: Retrieve a single segment + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Segment ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetSegmentResponseSuccess' + delete: + tags: + - Segments + summary: Remove an existing segment + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Segment ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RemoveSegmentResponseSuccess' + /topics: + post: + tags: + - Topics + summary: Create a new topic + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTopicOptions' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTopicResponseSuccess' + get: + tags: + - Topics + summary: Retrieve a list of topics + parameters: + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListTopicsResponseSuccess' + /topics/{id}: + get: + tags: + - Topics + summary: Retrieve a single topic + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Topic ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetTopicResponseSuccess' + patch: + tags: + - Topics + summary: Update an existing topic + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Topic ID. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateTopicOptions' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateTopicResponseSuccess' + delete: + tags: + - Topics + summary: Remove an existing topic + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Topic ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RemoveTopicResponseSuccess' + /contact-properties: + post: + tags: + - Contact Properties + summary: Create a new contact property + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateContactPropertyOptions' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateContactPropertyResponseSuccess' + get: + tags: + - Contact Properties + summary: Retrieve a list of contact properties + parameters: + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListContactPropertiesResponseSuccess' + /contact-properties/{id}: + get: + tags: + - Contact Properties + summary: Retrieve a single contact property + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Contact Property ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetContactPropertyResponseSuccess' + patch: + tags: + - Contact Properties + summary: Update an existing contact property + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Contact Property ID. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateContactPropertyOptions' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateContactPropertyResponseSuccess' + delete: + tags: + - Contact Properties + summary: Remove an existing contact property + parameters: + - name: id + in: path + required: true + schema: + type: string + description: The Contact Property ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RemoveContactPropertyResponseSuccess' + /contacts/{contact_id}/segments: + get: + tags: + - Contacts + summary: Retrieve a list of segments for a contact + parameters: + - name: contact_id + in: path + required: true + schema: + type: string + description: The Contact ID or email address. + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListContactSegmentsResponseSuccess' + /contacts/{contact_id}/segments/{segment_id}: + post: + tags: + - Contacts + summary: Add a contact to a segment + parameters: + - name: contact_id + in: path + required: true + schema: + type: string + description: The Contact ID or email address. + - name: segment_id + in: path + required: true + schema: + type: string + description: The Segment ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AddContactToSegmentResponseSuccess' + delete: + tags: + - Contacts + summary: Remove a contact from a segment + parameters: + - name: contact_id + in: path + required: true + schema: + type: string + description: The Contact ID or email address. + - name: segment_id + in: path + required: true + schema: + type: string + description: The Segment ID. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RemoveContactFromSegmentResponseSuccess' + /contacts/{contact_id}/topics: + get: + tags: + - Contacts + summary: Retrieve topics for a contact + parameters: + - name: contact_id + in: path + required: true + schema: + type: string + description: The Contact ID or email address. + - $ref: '#/components/parameters/PaginationLimit' + - $ref: '#/components/parameters/PaginationAfter' + - $ref: '#/components/parameters/PaginationBefore' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetContactTopicsResponseSuccess' + patch: + tags: + - Contacts + summary: Update topics for a contact + parameters: + - name: contact_id + in: path + required: true + schema: + type: string + description: The Contact ID or email address. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateContactTopicsOptions' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateContactTopicsResponseSuccess' +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + parameters: + PaginationLimit: + in: query + name: limit + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + description: Number of items to return. + PaginationAfter: + in: query + name: after + required: false + schema: + type: string + description: Return items after this cursor. + PaginationBefore: + in: query + name: before + required: false + schema: + type: string + description: Return items before this cursor. + schemas: + SendEmailRequest: + type: object + required: + - from + - to + - subject + properties: + from: + type: string + description: Sender email address. To include a friendly name, use the format "Your Name ". + to: + description: Recipient email address. For multiple addresses, send as an array of strings. Max 50. + oneOf: + - type: string + - type: array + items: + type: string + minItems: 1 + maxItems: 50 + subject: + type: string + description: Email subject. + bcc: + description: Bcc recipient email address. For multiple addresses, send as an array of strings. + oneOf: + - type: string + - type: array + items: + type: string + cc: + description: Cc recipient email address. For multiple addresses, send as an array of strings. + oneOf: + - type: string + - type: array + items: + type: string + reply_to: + description: Reply-to email address. For multiple addresses, send as an array of strings. + oneOf: + - type: string + - type: array + items: + type: string + html: + type: string + description: The HTML version of the message. + text: + type: string + description: The plain text version of the message. + template: + allOf: + - $ref: '#/components/schemas/EmailTemplateInput' + - description: Use a published template to send the email. If provided, do not include html or text. + headers: + type: object + description: Custom headers to add to the email. + scheduled_at: + type: string + description: Schedule email to be sent later. The date should be in ISO 8601 format. + attachments: + type: array + items: + $ref: '#/components/schemas/Attachment' + tags: + type: array + items: + $ref: '#/components/schemas/Tag' + topic_id: + type: string + description: The topic ID to scope the email to. If the recipient is a contact and opted-in to the topic, the email is sent. If opted-out, the email is not sent. If the recipient is not a contact, the email is sent if the topic's default subscription is opt_in. + Attachment: + type: object + properties: + content: + type: string + format: binary + description: Content of an attached file. + filename: + type: string + description: Name of attached file. + path: + type: string + description: Path where the attachment file is hosted + content_type: + type: string + description: Optional content type for the attachment, if not set it will be derived from the filename property + content_id: + type: string + description: Content ID for embedding inline images using cid references (e.g., cid:image001). + Tag: + type: object + properties: + name: + type: string + description: The name of the email tag. It can only contain ASCII letters (a–z, A–Z), numbers (0–9), underscores (_), or dashes (-). It can contain no more than 256 characters. + value: + type: string + description: The value of the email tag.It can only contain ASCII letters (a–z, A–Z), numbers (0–9), underscores (_), or dashes (-). It can contain no more than 256 characters. + EmailTemplateInput: + type: object + properties: + id: + type: string + description: The id of the published email template. + variables: + type: object + additionalProperties: + oneOf: + - type: string + - type: number + description: Template variables object with key/value pairs. + example: + variableName: 'Sign up now' + variableName2: 123 + required: + - id + SendEmailResponse: + type: object + properties: + id: + type: string + description: The ID of the sent email. + UpdateEmailOptions: + type: object + properties: + scheduled_at: + type: string + description: Schedule email to be sent later. The date should be in ISO 8601 format. + Email: + type: object + properties: + object: + type: string + description: The type of object. + example: 'email' + id: + type: string + description: The ID of the email. + example: '4ef9a417-02e9-4d39-ad75-9611e0fcc33c' + to: + type: array + items: + type: string + description: The email addresses of the recipients. + example: ['delivered@resend.dev'] + from: + type: string + description: The email address of the sender. + example: 'Acme ' + created_at: + type: string + format: date-time + description: The date and time the email was created. + example: '2023-04-03T22:13:42.674981+00:00' + subject: + type: string + description: The subject line of the email. + example: 'Hello World' + html: + type: string + description: The HTML body of the email. + example: 'Congrats on sending your first email!' + text: + type: string + description: The plain text body of the email. + bcc: + type: array + items: + type: string + description: The email addresses of the blind carbon copy recipients. + cc: + type: array + items: + type: string + description: The email addresses of the carbon copy recipients. + reply_to: + type: array + items: + type: string + description: The email addresses to which replies should be sent. + last_event: + type: string + description: The status of the email. + example: 'delivered' + ListEmailsResponse: + type: object + properties: + object: + type: string + description: Type of the response object. + example: 'list' + has_more: + type: boolean + description: Indicates if there are more results available. + example: false + data: + type: array + description: Array containing email information. + items: + $ref: '#/components/schemas/Email' + CreateBatchEmailsResponse: + type: object + properties: + data: + type: array + items: + type: object + properties: + id: + type: string + description: The ID of the sent email. + DomainCapabilities: + type: object + description: Configure the domain capabilities for sending and receiving emails. At least one capability must be enabled. + properties: + sending: + type: string + enum: + - enabled + - disabled + description: Enable or disable sending emails from this domain. + receiving: + type: string + enum: + - enabled + - disabled + description: Enable or disable receiving emails to this domain. + CreateDomainRequest: + type: object + required: + - name + properties: + name: + type: string + description: The name of the domain you want to create. + region: + type: string + enum: + - us-east-1 + - eu-west-1 + - sa-east-1 + - ap-northeast-1 + default: us-east-1 + description: The region where emails will be sent from. Possible values are us-east-1 | eu-west-1 | sa-east-1 | ap-northeast-1 + custom_return_path: + type: string + description: For advanced use cases, choose a subdomain for the Return-Path address. Defaults to 'send' (i.e., send.yourdomain.tld). + open_tracking: + type: boolean + description: Track the open rate of each email. + click_tracking: + type: boolean + description: Track clicks within the body of each HTML email. + tls: + type: string + enum: + - opportunistic + - enforced + default: opportunistic + description: TLS mode. Opportunistic attempts secure connection but falls back to unencrypted. Enforced requires TLS or email won't be sent. + capabilities: + $ref: '#/components/schemas/DomainCapabilities' + CreateDomainResponse: + type: object + properties: + id: + type: string + description: The ID of the domain. + name: + type: string + description: The name of the domain. + created_at: + type: string + format: date-time + description: The date and time the domain was created. + status: + type: string + description: The status of the domain. + capabilities: + $ref: '#/components/schemas/DomainCapabilities' + records: + type: array + items: + $ref: '#/components/schemas/DomainRecord' + region: + type: string + description: The region where the domain is hosted. + UpdateDomainOptions: + type: object + properties: + open_tracking: + type: boolean + description: Track the open rate of each email. + click_tracking: + type: boolean + description: Track clicks within the body of each HTML email. + tls: + type: string + description: enforced | opportunistic. + default: "opportunistic" + capabilities: + $ref: '#/components/schemas/DomainCapabilities' + DomainRecord: + type: object + properties: + record: + type: string + enum: + - SPF + - DKIM + - Receiving + description: The type of record (SPF for sending, DKIM for sending, Receiving for inbound emails). + name: + type: string + description: The name of the DNS record. + type: + type: string + enum: + - MX + - TXT + - CNAME + description: The DNS record type. + ttl: + type: string + description: The time to live for the record. + status: + type: string + enum: + - pending + - verified + - failed + - temporary_failure + - not_started + description: The status of the record. + value: + type: string + description: The value of the record. + priority: + type: integer + description: The priority of the record (only applicable for MX records). + Domain: + type: object + properties: + object: + type: string + description: The type of object. + example: 'domain' + id: + type: string + description: The ID of the domain. + example: 'd91cd9bd-1176-453e-8fc1-35364d380206' + name: + type: string + description: The name of the domain. + example: 'example.com' + status: + type: string + description: The status of the domain. + example: 'not_started' + created_at: + type: string + format: date-time + description: The date and time the domain was created. + example: '2023-04-26T20:21:26.347412+00:00' + region: + type: string + description: The region where the domain is hosted. + example: 'us-east-1' + capabilities: + $ref: '#/components/schemas/DomainCapabilities' + records: + type: array + items: + $ref: '#/components/schemas/DomainRecord' + VerifyDomainResponse: + type: object + properties: + object: + type: string + description: The type of object. + example: 'domain' + id: + type: string + description: The ID of the domain. + example: 'd91cd9bd-1176-453e-8fc1-35364d380206' + ListDomainsResponse: + type: object + properties: + object: + type: string + description: Type of the response object. + example: 'list' + has_more: + type: boolean + description: Indicates if there are more results available. + example: false + data: + type: array + items: + $ref: '#/components/schemas/ListDomainsItem' + ListDomainsItem: + type: object + properties: + id: + type: string + description: The ID of the domain. + example: 'd91cd9bd-1176-453e-8fc1-35364d380206' + name: + type: string + description: The name of the domain. + example: 'example.com' + status: + type: string + description: The status of the domain. + example: 'not_started' + created_at: + type: string + format: date-time + description: The date and time the domain was created. + example: '2023-04-26T20:21:26.347412+00:00' + region: + type: string + description: The region where the domain is hosted. + example: 'us-east-1' + capabilities: + $ref: '#/components/schemas/DomainCapabilities' + UpdateDomainResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the updated domain. + example: 'd91cd9bd-1176-453e-8fc1-35364d380206' + object: + type: string + description: The object type representing the updated domain. + example: 'domain' + DeleteDomainResponse: + type: object + properties: + object: + type: string + description: The type of object. + example: 'domain' + id: + type: string + description: The ID of the domain. + example: 'd91cd9bd-1176-453e-8fc1-35364d380206' + deleted: + type: boolean + description: Indicates whether the domain was deleted successfully. + example: true + CreateApiKeyRequest: + type: object + required: + - name + properties: + name: + type: string + description: The API key name. + permission: + type: string + enum: + - full_access + - sending_access + description: The API key can have full access to Resend’s API or be only restricted to send emails. * full_access - Can create, delete, get, and update any resource. * sending_access - Can only send emails. + domain_id: + type: string + description: Restrict an API key to send emails only from a specific domain. Only used when the permission is sending_acces. + CreateApiKeyResponse: + type: object + properties: + id: + type: string + description: The ID of the API key. + token: + type: string + description: The token of the API key. + ListApiKeysResponse: + type: object + properties: + object: + type: string + description: Type of the response object. + example: 'list' + has_more: + type: boolean + description: Indicates if there are more results available. + example: false + data: + type: array + items: + $ref: '#/components/schemas/ApiKey' + ApiKey: + type: object + properties: + id: + type: string + description: The ID of the API key. + name: + type: string + description: The name of the API key. + created_at: + type: string + format: date-time + description: The date and time the API key was created. + CreateAudienceOptions: + type: object + deprecated: true + required: + - name + properties: + name: + type: string + description: The name of the audience you want to create. + CreateAudienceResponseSuccess: + type: object + deprecated: true + properties: + id: + type: string + description: The ID of the audience. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object of the audience. + example: audience + name: + type: string + description: The name of the audience. + example: Registered Users + GetAudienceResponseSuccess: + type: object + deprecated: true + properties: + id: + type: string + description: The ID of the audience. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object of the audience. + example: audience + name: + type: string + description: The name of the audience. + example: Registered Users + created_at: + type: string + description: The date that the object was created. + example: 2023-10-06T22:59:55.977Z + RemoveAudienceResponseSuccess: + type: object + deprecated: true + properties: + id: + type: string + description: The ID of the audience. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object of the audience. + example: audience + deleted: + type: boolean + description: The deleted attribute indicates that the corresponding audience has been deleted. + example: true + ListAudiencesResponseSuccess: + type: object + deprecated: true + properties: + object: + type: string + description: Type of the response object. + example: list + data: + type: array + description: Array containing audience information. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the audience. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + name: + type: string + description: Name of the audience. + example: Registered Users + created_at: + type: string + format: date-time + description: Timestamp indicating when the audience was created. + example: "2023-10-06T22:59:55.977Z" + CreateContactOptions: + type: object + required: + - email + properties: + email: + type: string + description: Email address of the contact. + example: steve.wozniak@gmail.com + first_name: + type: string + description: First name of the contact. + example: Steve + last_name: + type: string + description: Last name of the contact. + example: Wozniak + unsubscribed: + type: boolean + description: The Contact's global subscription status. If set to true, the contact will be unsubscribed from all Broadcasts. + example: false + properties: + type: object + additionalProperties: true + description: A map of custom property keys and values to create. + segments: + type: array + items: + type: string + description: Array of segment IDs to add the contact to. + topics: + type: array + items: + type: object + properties: + id: + type: string + description: The topic ID. + subscription: + type: string + enum: + - opt_in + - opt_out + description: The subscription status for this topic. + description: Array of topic subscriptions for the contact. + audience_id: + type: string + description: Unique identifier of the audience to which the contact belongs. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + deprecated: true + CreateContactResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: contact + id: + type: string + description: Unique identifier for the created contact. + example: 479e3145-dd38-476b-932c-529ceb705947 + GetContactResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: contact + id: + type: string + description: Unique identifier for the contact. + example: e169aa45-1ecf-4183-9955-b1499d5701d3 + email: + type: string + description: Email address of the contact. + example: steve.wozniak@gmail.com + first_name: + type: string + description: First name of the contact. + example: Steve + last_name: + type: string + description: Last name of the contact. + example: Wozniak + created_at: + type: string + format: date-time + description: Timestamp indicating when the contact was created. + example: "2023-10-06T23:47:56.678Z" + unsubscribed: + type: boolean + description: Indicates if the contact is unsubscribed. + example: false + properties: + type: object + additionalProperties: true + description: A map of custom property keys and values. + UpdateContactOptions: + type: object + properties: + email: + type: string + description: Email address of the contact. + example: steve.wozniak@gmail.com + first_name: + type: string + description: First name of the contact. + example: Steve + last_name: + type: string + description: Last name of the contact. + example: Wozniak + unsubscribed: + type: boolean + description: The Contact's global subscription status. If set to true, the contact will be unsubscribed from all Broadcasts. + example: false + properties: + type: object + additionalProperties: true + description: A map of custom property keys and values to update. + UpdateContactResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: contact + id: + type: string + description: Unique identifier for the updated contact. + example: 479e3145-dd38-476b-932c-529ceb705947 + RemoveContactResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: contact + id: + type: string + description: Unique identifier for the removed contact. + example: 520784e2-887d-4c25-b53c-4ad46ad38100 + deleted: + type: boolean + description: Indicates whether the contact was successfully deleted. + example: true + ListContactsResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: list + data: + type: array + description: Array containing contact information. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the contact. + example: e169aa45-1ecf-4183-9955-b1499d5701d3 + email: + type: string + description: Email address of the contact. + example: steve.wozniak@gmail.com + first_name: + type: string + description: First name of the contact. + example: Steve + last_name: + type: string + description: Last name of the contact. + example: Wozniak + created_at: + type: string + format: date-time + description: Timestamp indicating when the contact was created. + example: "2023-10-06T23:47:56.678Z" + unsubscribed: + type: boolean + description: Indicates if the contact is unsubscribed. + example: false + CreateBroadcastOptions: + type: object + required: + - from + - subject + - segment_id + properties: + name: + type: string + description: Name of the broadcast. + segment_id: + type: string + description: Unique identifier of the segment this broadcast will be sent to. + audience_id: + type: string + description: Use `segment_id` instead. Unique identifier of the segment this broadcast will be sent to. + deprecated: true + from: + type: string + description: The email address of the sender. + subject: + type: string + description: The subject line of the email. + reply_to: + type: array + items: + type: string + description: The email addresses to which replies should be sent. + preview_text: + type: string + description: The preview text of the email. + example: 'Here are our announcements' + html: + type: string + description: The HTML version of the message. + text: + type: string + description: The plain text version of the message. + topic_id: + type: string + description: The topic ID that the broadcast will be scoped to. + send: + type: boolean + description: | + Whether to send the broadcast immediately or keep it as a draft. + scheduled_at: + type: string + description: | + Schedule time to send the broadcast. Can only be used if `send` is true. + CreateBroadcastResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the broadcast. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type of the response. + example: broadcast + ListBroadcastsResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: list + has_more: + type: boolean + description: Indicates if there are more results available. + example: false + data: + type: array + description: Array containing broadcast information. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the broadcast. + example: e169aa45-1ecf-4183-9955-b1499d5701d3 + name: + type: string + description: Name of the broadcast. + example: November announcements + audience_id: + type: string + description: Deprecated. Use segment_id instead. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + deprecated: true + segment_id: + type: string + description: Unique identifier of the segment this broadcast will be sent to. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + status: + type: string + description: The status of the broadcast. + example: 'draft' + created_at: + type: string + format: date-time + description: Timestamp indicating when the broadcast was created. + example: "2023-10-06T22:59:55.977Z" + scheduled_at: + type: string + format: date-time + description: Timestamp indicating when the broadcast is scheduled to be sent. + example: "2023-10-06T22:59:55.977Z" + sent_at: + type: string + format: date-time + description: Timestamp indicating when the broadcast was sent. + example: "2023-10-06T22:59:55.977Z" + topic_id: + type: string + description: The topic ID that the broadcast is scoped to. + example: b6d24b8e-af0b-4c3c-be0c-359bbd97381e + GetBroadcastResponseSuccess: + type: object + properties: + id: + type: string + description: Unique identifier for the broadcast. + example: e169aa45-1ecf-4183-9955-b1499d5701d3 + name: + type: string + description: Name of the broadcast. + example: November announcements + audience_id: + type: string + nullable: true + description: "Deprecated: use `segment_id` instead. Unique identifier of the segment this broadcast will be sent to." + deprecated: true + segment_id: + type: string + nullable: true + description: Unique identifier of the segment this broadcast will be sent to. + from: + type: string + description: The email address of the sender. + example: 'Acme ' + subject: + type: string + description: The subject line of the email. + example: 'Hello World' + reply_to: + type: array + items: + type: string + description: The email addresses to which replies should be sent. + preview_text: + type: string + description: The preview text of the email. + example: 'Here are our announcements' + status: + type: string + description: The status of the broadcast. + example: 'draft' + created_at: + type: string + format: date-time + description: Timestamp indicating when the broadcast was created. + example: "2023-10-06T22:59:55.977Z" + scheduled_at: + type: string + format: date-time + description: Timestamp indicating when the broadcast is scheduled to be sent. + example: "2023-10-06T22:59:55.977Z" + sent_at: + type: string + format: date-time + description: Timestamp indicating when the broadcast was sent. + example: "2023-10-06T22:59:55.977Z" + text: + type: string + nullable: true + description: The plain text version of the broadcast content. + example: 'Hello {{{FIRST_NAME|there}}}!' + html: + type: string + nullable: true + description: The HTML version of the broadcast content. + example: '

Hello {{{FIRST_NAME|there}}}!

' + topic_id: + type: string + nullable: true + description: The topic ID that the broadcast is scoped to. + example: b6d24b8e-af0b-4c3c-be0c-359bbd97381e + UpdateBroadcastOptions: + type: object + properties: + name: + type: string + description: Name of the broadcast. + audience_id: + type: string + description: Use `segment_id` instead. Unique identifier of the audience this broadcast will be sent to. + deprecated: true + segment_id: + type: string + description: Unique identifier of the segment this broadcast will be sent to. + from: + type: string + description: The email address of the sender. + subject: + type: string + description: The subject line of the email. + reply_to: + type: array + items: + type: string + description: The email addresses to which replies should be sent. + preview_text: + type: string + description: The preview text of the email. + html: + type: string + description: The HTML version of the message. + text: + type: string + description: The plain text version of the message. + topic_id: + type: string + description: The topic ID that the broadcast will be scoped to. + UpdateBroadcastResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the broadcast. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type of the response. + example: broadcast + RemoveBroadcastResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the broadcast. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: Type of the response object. + example: broadcast + deleted: + type: boolean + description: The deleted attribute indicates that the corresponding broadcast has been deleted. + example: true + SendBroadcastOptions: + type: object + properties: + scheduled_at: + type: string + description: Schedule email to be sent later. The date should be in ISO 8601 format. + SendBroadcastResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the broadcast. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + RetrievedAttachment: + type: object + properties: + object: + type: string + description: The type of object. + example: 'attachment' + id: + type: string + format: uuid + description: The ID of the attachment. + example: '660e8400-e29b-41d4-a716-446655440000' + filename: + type: string + description: The filename of the attachment. + example: 'document.pdf' + content_type: + type: string + description: The MIME type of the attachment. + example: 'application/pdf' + content_id: + type: string + description: The content ID for inline attachments. + example: 'img001' + content_disposition: + type: string + enum: + - inline + - attachment + description: How the attachment should be displayed. + example: 'attachment' + download_url: + type: string + description: Signed URL to download the attachment content. + example: 'https://cloudfront.example.com/path?Signature=...' + expires_at: + type: string + format: date-time + description: Timestamp when the download URL expires. + example: '2024-10-27T18:30:00.000Z' + size: + type: integer + description: Size of the attachment in bytes. + example: 2048 + ListAttachmentsResponse: + type: object + properties: + object: + type: string + description: Type of the response object. + example: 'list' + has_more: + type: boolean + description: Indicates if there are more results available. + example: false + data: + type: array + description: Array containing attachment information. + items: + type: object + properties: + id: + type: string + format: uuid + description: The ID of the attachment. + example: '660e8400-e29b-41d4-a716-446655440000' + filename: + type: string + description: The filename of the attachment. + example: 'document.pdf' + content_type: + type: string + description: The MIME type of the attachment. + example: 'application/pdf' + content_id: + type: string + description: The content ID for inline attachments. + example: 'img001' + content_disposition: + type: string + enum: + - inline + - attachment + description: How the attachment should be displayed. + example: 'attachment' + download_url: + type: string + description: Signed URL to download the attachment content. + example: 'https://cloudfront.example.com/path?Signature=...' + expires_at: + type: string + format: date-time + description: Timestamp when the download URL expires. + example: '2024-10-27T18:30:00.000Z' + size: + type: integer + description: Size of the attachment in bytes. + example: 2048 + GetReceivedEmailResponse: + type: object + properties: + object: + type: string + description: The type of object. + example: 'email' + id: + type: string + format: uuid + description: The ID of the received email. + example: '550e8400-e29b-41d4-a716-446655440000' + to: + type: array + items: + type: string + description: The recipient email addresses. + example: ['delivered@resend.dev'] + from: + type: string + description: The sender email address. + example: 'sender@example.com' + subject: + type: string + description: The email subject. + example: 'Hello World' + message_id: + type: string + description: The unique message ID from the email headers. + example: '' + bcc: + type: array + items: + type: string + nullable: true + description: The BCC recipients. + example: [] + cc: + type: array + items: + type: string + nullable: true + description: The CC recipients. + example: [] + reply_to: + type: array + items: + type: string + nullable: true + description: The reply-to addresses. + example: [] + html: + type: string + nullable: true + description: The HTML content of the email. + example: '

Email content

' + text: + type: string + nullable: true + description: The plain text content of the email. + example: 'Email content' + headers: + type: object + nullable: true + description: The email headers. + example: {'X-Custom-Header': 'value'} + created_at: + type: string + format: date-time + description: Timestamp when the email was received. + example: '2023-10-06:23:47:56.678Z' + attachments: + type: array + description: Array of attachments. + items: + type: object + properties: + id: + type: string + format: uuid + description: The ID of the attachment. + filename: + type: string + description: The filename of the attachment. + content_type: + type: string + description: The MIME type of the attachment. + content_id: + type: string + description: The content ID for inline attachments. + content_disposition: + type: string + enum: + - inline + - attachment + description: How the attachment should be displayed. + size: + type: integer + description: Size of the attachment in bytes. + ListReceivedEmailsResponse: + type: object + properties: + object: + type: string + description: Type of the response object. + example: 'list' + has_more: + type: boolean + description: Indicates if there are more results available. + example: false + data: + type: array + description: Array containing received email information. + items: + type: object + properties: + id: + type: string + format: uuid + description: The ID of the received email. + example: '550e8400-e29b-41d4-a716-446655440000' + to: + type: array + items: + type: string + description: The recipient email addresses. + example: ['delivered@resend.dev'] + from: + type: string + description: The sender email address. + example: 'sender@example.com' + subject: + type: string + nullable: true + description: The email subject. + example: 'Hello World' + message_id: + type: string + description: The unique message ID from the email headers. + example: '' + bcc: + type: array + items: + type: string + nullable: true + description: The BCC recipients. + cc: + type: array + items: + type: string + nullable: true + description: The CC recipients. + reply_to: + type: array + items: + type: string + nullable: true + description: The reply-to addresses. + created_at: + type: string + format: date-time + description: Timestamp when the email was received. + example: '2023-10-06T23:47:56.678Z' + attachments: + type: array + description: Array of attachments for this email. + items: + type: object + properties: + id: + type: string + format: uuid + description: The ID of the attachment. + filename: + type: string + description: The filename of the attachment. + content_type: + type: string + description: The MIME type of the attachment. + content_id: + type: string + description: The content ID for inline attachments. + content_disposition: + type: string + enum: + - inline + - attachment + description: How the attachment should be displayed. + size: + type: integer + description: Size of the attachment in bytes. + CreateWebhookRequest: + type: object + required: + - endpoint + - events + properties: + endpoint: + type: string + description: The URL where webhook events will be sent. + example: 'https://webhook.example.com/handler' + events: + type: array + items: + type: string + minItems: 1 + description: Array of event types to subscribe to. + example: ['email.sent', 'email.delivered', 'email.bounced'] + CreateWebhookResponse: + type: object + properties: + object: + type: string + description: The type of object. + example: 'webhook' + id: + type: string + format: uuid + description: The ID of the webhook. + example: '479e3145-dd38-476b-932c-529ceb705947' + signing_secret: + type: string + description: The secret key used to verify webhook payloads. + example: 'whsec_...' + GetWebhookResponse: + type: object + properties: + object: + type: string + description: The type of object. + example: 'webhook' + id: + type: string + format: uuid + description: The ID of the webhook. + example: '479e3145-dd38-476b-932c-529ceb705947' + endpoint: + type: string + description: The URL where webhook events are sent. + example: 'https://webhook.example.com/handler' + events: + type: array + items: + type: string + nullable: true + description: Array of event types subscribed to. + example: ['email.sent', 'email.delivered'] + status: + type: string + description: The status of the webhook. + example: 'enabled' + created_at: + type: string + format: date-time + description: Timestamp indicating when the webhook was created. + example: '2023-10-06T23:47:56.678Z' + signing_secret: + type: string + description: The secret key used to verify webhook payloads. + example: 'whsec_...' + ListWebhooksResponse: + type: object + properties: + object: + type: string + description: Type of the response object. + example: 'list' + has_more: + type: boolean + description: Indicates if there are more results available. + example: false + data: + type: array + description: Array containing webhook information. + items: + type: object + properties: + id: + type: string + format: uuid + description: The ID of the webhook. + example: '479e3145-dd38-476b-932c-529ceb705947' + endpoint: + type: string + description: The URL where webhook events are sent. + example: 'https://webhook.example.com/handler' + events: + type: array + items: + type: string + nullable: true + description: Array of event types subscribed to. + example: ['email.sent'] + status: + type: string + description: The status of the webhook. + example: 'enabled' + created_at: + type: string + format: date-time + description: Timestamp indicating when the webhook was created. + example: '2023-10-06T23:47:56.678Z' + UpdateWebhookRequest: + type: object + properties: + endpoint: + type: string + description: The URL where webhook events will be sent. + example: 'https://webhook.example.com/new-handler' + events: + type: array + items: + type: string + minItems: 1 + description: Array of event types to subscribe to. + example: ['email.sent', 'email.delivered'] + status: + type: string + enum: + - enabled + - disabled + description: The status of the webhook. + example: 'enabled' + UpdateWebhookResponse: + type: object + properties: + object: + type: string + description: The type of object. + example: 'webhook' + id: + type: string + format: uuid + description: The ID of the updated webhook. + example: '479e3145-dd38-476b-932c-529ceb705947' + DeleteWebhookResponse: + type: object + properties: + object: + type: string + description: The type of object. + example: 'webhook' + id: + type: string + format: uuid + description: The ID of the deleted webhook. + example: '479e3145-dd38-476b-932c-529ceb705947' + deleted: + type: boolean + description: Indicates whether the webhook was successfully deleted. + example: true + TemplateVariable: + type: object + properties: + id: + type: string + description: The ID of the template variable. + key: + type: string + description: The key of the variable. + type: + type: string + description: The type of the variable. + enum: [string, number, boolean, object, list] + fallback_value: + description: The fallback value of the variable. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + items: {} + created_at: + type: string + format: date-time + description: Timestamp indicating when the variable was created. + updated_at: + type: string + format: date-time + description: Timestamp indicating when the variable was last updated. + required: + - key + - type + TemplateVariableInput: + type: object + properties: + key: + type: string + description: The key of the variable. + type: + type: string + description: The type of the variable. + enum: [string, number, boolean, object, list] + fallback_value: + description: The fallback value of the variable. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + items: {} + required: + - key + - type + Template: + type: object + properties: + object: + type: string + description: The type of object. + example: template + id: + type: string + description: The ID of the template. + current_version_id: + type: string + description: The ID of the current version of the template. + name: + type: string + description: The name of the template. + alias: + type: string + description: The alias of the template. + from: + type: string + description: Sender email address. To include a friendly name, use the format "Your Name ". + subject: + type: string + description: Email subject. + reply_to: + type: array + items: + type: string + nullable: true + description: Reply-to email addresses. + html: + type: string + description: The HTML version of the template. + text: + type: string + description: The plain text version of the template. + variables: + type: array + items: + $ref: '#/components/schemas/TemplateVariable' + created_at: + type: string + format: date-time + description: Timestamp indicating when the template was created. + updated_at: + type: string + format: date-time + description: Timestamp indicating when the template was last updated. + status: + type: string + description: The publication status of the template. + enum: [draft, published] + published_at: + type: string + format: date-time + description: Timestamp indicating when the template was published. + nullable: true + has_unpublished_versions: + type: boolean + description: Indicates whether the template has unpublished versions. + TemplateListItem: + type: object + properties: + id: + type: string + description: The ID of the template. + name: + type: string + description: The name of the template. + status: + type: string + description: The publication status of the template. + enum: [draft, published] + published_at: + type: string + format: date-time + nullable: true + description: Timestamp indicating when the template was published. + created_at: + type: string + format: date-time + description: Timestamp indicating when the template was created. + updated_at: + type: string + format: date-time + description: Timestamp indicating when the template was last updated. + alias: + type: string + description: The alias of the template. + CreateTemplateRequest: + type: object + required: + - name + - html + properties: + name: + type: string + description: The name of the template. + alias: + type: string + description: The alias of the template. + from: + type: string + description: Sender email address. To include a friendly name, use the format "Your Name ". + subject: + type: string + description: Email subject. + reply_to: + type: array + items: + type: string + description: Reply-to email addresses. + html: + type: string + description: The HTML version of the template. + text: + type: string + description: The plain text version of the template. + variables: + type: array + items: + $ref: '#/components/schemas/TemplateVariableInput' + CreateTemplateResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the template. + object: + type: string + description: The object type of the response. + example: template + ListTemplatesResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: list + data: + type: array + description: Array containing templates information. + items: + $ref: '#/components/schemas/TemplateListItem' + has_more: + type: boolean + description: Indicates if there are more templates to retrieve. + UpdateTemplateOptions: + type: object + properties: + name: + type: string + description: The name of the template. + alias: + type: string + description: The alias of the template. + from: + type: string + description: Sender email address. To include a friendly name, use the format "Your Name ". + subject: + type: string + description: Email subject. + reply_to: + type: array + items: + type: string + description: Reply-to email addresses. + html: + type: string + description: The HTML version of the template. + text: + type: string + description: The plain text version of the template. + variables: + type: array + items: + $ref: '#/components/schemas/TemplateVariableInput' + UpdateTemplateResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the template. + object: + type: string + description: The object type of the response. + example: template + RemoveTemplateResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: template + id: + type: string + description: The ID of the template. + deleted: + type: boolean + description: Indicates whether the template was successfully deleted. + example: true + PublishTemplateResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the template. + object: + type: string + description: The object type of the response. + example: template + DuplicateTemplateResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the duplicated template. + object: + type: string + description: The object type of the response. + example: template + CreateSegmentOptions: + type: object + required: + - name + properties: + name: + type: string + description: The name of the segment. + audience_id: + type: string + description: The ID of the audience this segment belongs to. + deprecated: true + filter: + type: object + description: Filter conditions for the segment. + CreateSegmentResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the segment. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type of the response. + example: segment + GetSegmentResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the segment. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type. + example: segment + name: + type: string + description: The name of the segment. + example: Active Users + audience_id: + type: string + description: The ID of the audience this segment belongs to. + deprecated: true + filter: + type: object + description: Filter conditions for the segment. + created_at: + type: string + format: date-time + description: Timestamp indicating when the segment was created. + ListSegmentsResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: list + has_more: + type: boolean + description: Indicates if there are more results available. + data: + type: array + description: Array containing segment information. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the segment. + name: + type: string + description: Name of the segment. + audience_id: + type: string + description: The ID of the audience this segment belongs to. + deprecated: true + created_at: + type: string + format: date-time + description: Timestamp indicating when the segment was created. + RemoveSegmentResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the segment. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type. + example: segment + deleted: + type: boolean + description: Indicates whether the segment was successfully deleted. + example: true + CreateTopicOptions: + type: object + required: + - name + - default_subscription + properties: + name: + type: string + description: The name of the topic. Max 50 characters. + maxLength: 50 + default_subscription: + type: string + enum: + - opt_in + - opt_out + description: The default subscription status for the topic. Cannot be changed after creation. + description: + type: string + description: A description of the topic. Max 200 characters. + maxLength: 200 + visibility: + type: string + enum: + - public + - private + default: private + description: The visibility of the topic. Public topics are visible to all contacts on the unsubscribe page. Private topics are only visible to opted-in contacts. + CreateTopicResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the topic. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type of the response. + example: topic + GetTopicResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the topic. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type. + example: topic + name: + type: string + description: The name of the topic. + example: Newsletter + description: + type: string + description: A description of the topic. + default_subscription: + type: string + enum: + - opt_in + - opt_out + description: The default subscription status for the topic. + visibility: + type: string + enum: + - public + - private + description: The visibility of the topic. + created_at: + type: string + format: date-time + description: Timestamp indicating when the topic was created. + ListTopicsResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: list + has_more: + type: boolean + description: Indicates if there are more results available. + data: + type: array + description: Array containing topic information. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the topic. + name: + type: string + description: Name of the topic. + description: + type: string + description: A description of the topic. + default_subscription: + type: string + enum: + - opt_in + - opt_out + description: The default subscription status for the topic. + visibility: + type: string + enum: + - public + - private + description: The visibility of the topic. + created_at: + type: string + format: date-time + description: Timestamp indicating when the topic was created. + UpdateTopicOptions: + type: object + properties: + name: + type: string + description: The name of the topic. Max 50 characters. + maxLength: 50 + description: + type: string + description: A description of the topic. Max 200 characters. + maxLength: 200 + visibility: + type: string + enum: + - public + - private + description: The visibility of the topic. + UpdateTopicResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the topic. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type. + example: topic + RemoveTopicResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the topic. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type. + example: topic + deleted: + type: boolean + description: Indicates whether the topic was successfully deleted. + example: true + CreateContactPropertyOptions: + type: object + required: + - key + - type + properties: + key: + type: string + description: The property key. Max length is 50 characters. Only alphanumeric characters and underscores are allowed. + type: + type: string + enum: + - string + - number + description: The property type. + fallback_value: + oneOf: + - type: string + - type: number + description: The default value to use when the property is not set for a contact. Must match the type specified in the type field. + CreateContactPropertyResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the contact property. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type of the response. + example: contact_property + GetContactPropertyResponseSuccess: + type: object + properties: + object: + type: string + description: The object type. + example: contact_property + id: + type: string + description: The ID of the contact property. + example: b6d24b8e-af0b-4c3c-be0c-359bbd97381e + key: + type: string + description: The property key. + example: company_name + type: + type: string + description: The property type. + example: string + fallback_value: + oneOf: + - type: string + - type: number + description: The default value when the property is not set for a contact. + example: Acme Corp + created_at: + type: string + format: date-time + description: Timestamp indicating when the contact property was created. + ListContactPropertiesResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: list + has_more: + type: boolean + description: Indicates if there are more results available. + data: + type: array + description: Array containing contact property information. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the contact property. + key: + type: string + description: The property key. + type: + type: string + description: The property type. + fallback_value: + oneOf: + - type: string + - type: number + description: The default value when the property is not set for a contact. + created_at: + type: string + format: date-time + description: Timestamp indicating when the contact property was created. + UpdateContactPropertyOptions: + type: object + properties: + fallback_value: + oneOf: + - type: string + - type: number + description: The default value to use when the property is not set for a contact. Must match the type of the property. + UpdateContactPropertyResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the contact property. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type. + example: contact_property + RemoveContactPropertyResponseSuccess: + type: object + properties: + id: + type: string + description: The ID of the contact property. + example: 78261eea-8f8b-4381-83c6-79fa7120f1cf + object: + type: string + description: The object type. + example: contact_property + deleted: + type: boolean + description: Indicates whether the contact property was successfully deleted. + example: true + AddContactToSegmentResponseSuccess: + type: object + properties: + object: + type: string + description: The object type. + example: contact_segment + contact_id: + type: string + description: The ID of the contact. + segment_id: + type: string + description: The ID of the segment. + ListContactSegmentsResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: list + has_more: + type: boolean + description: Indicates if there are more results available. + data: + type: array + description: Array containing segment information for this contact. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the segment. + name: + type: string + description: Name of the segment. + created_at: + type: string + format: date-time + description: Timestamp indicating when the contact was added to the segment. + RemoveContactFromSegmentResponseSuccess: + type: object + properties: + object: + type: string + description: The object type. + example: contact_segment + contact_id: + type: string + description: The ID of the contact. + segment_id: + type: string + description: The ID of the segment. + deleted: + type: boolean + description: Indicates whether the contact was successfully removed from the segment. + example: true + GetContactTopicsResponseSuccess: + type: object + properties: + object: + type: string + description: Type of the response object. + example: list + has_more: + type: boolean + description: Indicates if there are more results available. + data: + type: array + description: Array containing topic subscriptions for this contact. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the topic. + name: + type: string + description: Name of the topic. + description: + type: string + description: Description of the topic. + subscription: + type: string + enum: + - opt_in + - opt_out + description: The subscription status for this topic. + UpdateContactTopicsOptions: + type: object + required: + - topics + properties: + topics: + type: array + items: + type: object + properties: + id: + type: string + description: The ID of the topic. + subscription: + type: string + enum: + - opt_in + - opt_out + description: The subscription status (opt_in or opt_out). + UpdateContactTopicsResponseSuccess: + type: object + properties: + object: + type: string + description: The object type. + example: contact_topics + contact_id: + type: string + description: The ID of the contact. + topics: + type: array + description: Array of updated topic subscriptions. + items: + type: object + properties: + id: + type: string + description: The ID of the topic. + subscription: + type: string + enum: + - opt_in + - opt_out + description: The subscription status. diff --git a/src/server.ts b/src/server.ts index 364b8d6..472141e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,6 +4,7 @@ import packageJson from '../package.json' with { type: 'json' }; import { addApiKeyTools, addBroadcastTools, + addCodeModeTools, addContactPropertyTools, addContactTools, addDomainTools, @@ -20,22 +21,33 @@ export function createMcpServer( resend: Resend, options: ServerOptions, ): McpServer { - const { senderEmailAddress, replierEmailAddresses } = options; + const { apiKey, senderEmailAddress, replierEmailAddresses, codeModeOnly } = + options; const server = new McpServer({ name: 'resend', version: packageJson.version, }); - addApiKeyTools(server, resend); - addBroadcastTools(server, resend, { + addCodeModeTools(server, { + apiKey, senderEmailAddress, replierEmailAddresses, }); - addContactPropertyTools(server, resend); - addContactTools(server, resend); - addDomainTools(server, resend); - addEmailTools(server, resend, { senderEmailAddress, replierEmailAddresses }); - addSegmentTools(server, resend); - addTopicTools(server, resend); - addWebhookTools(server, resend); + if (!codeModeOnly) { + addApiKeyTools(server, resend); + addBroadcastTools(server, resend, { + senderEmailAddress, + replierEmailAddresses, + }); + addContactPropertyTools(server, resend); + addContactTools(server, resend); + addDomainTools(server, resend); + addEmailTools(server, resend, { + senderEmailAddress, + replierEmailAddresses, + }); + addSegmentTools(server, resend); + addTopicTools(server, resend); + addWebhookTools(server, resend); + } return server; } diff --git a/src/tools/codeMode.ts b/src/tools/codeMode.ts new file mode 100644 index 0000000..11fb550 --- /dev/null +++ b/src/tools/codeMode.ts @@ -0,0 +1,651 @@ +import vm from 'node:vm'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; +import { getResolvedOpenApiSpec } from '../openapi/loader.js'; + +const RESEND_API_BASE = 'https://api.resend.com'; + +/** Operation metadata derived from resolved OpenAPI spec for request(). */ +interface OpMeta { + pathParamNames: string[]; + queryParamNames: string[]; + hasBody: boolean; + bodyIsArray: boolean; +} + +function toSnakeCase(input: string): string { + return input + .replace(/([a-z0-9])([A-Z])/g, '$1_$2') + .replace(/-/g, '_') + .toLowerCase(); +} + +function deepSnakeKeys(value: unknown): unknown { + if (Array.isArray(value)) return value.map((item) => deepSnakeKeys(item)); + if (!value || typeof value !== 'object') return value; + + const out: Record = {}; + for (const [key, next] of Object.entries(value as Record)) { + out[toSnakeCase(key)] = deepSnakeKeys(next); + } + return out; +} + +/** Build op meta map from resolved spec: key = "METHOD:path" e.g. "GET:/emails". */ +function getOpMetaMap(spec: Record): Map { + const map = new Map(); + const paths = spec.paths as + | Record>> + | undefined; + if (!paths) return map; + + const methods = ['get', 'post', 'patch', 'delete'] as const; + for (const [path, pathItem] of Object.entries(paths)) { + if (!pathItem || typeof pathItem !== 'object') continue; + for (const method of methods) { + const op = pathItem[method]; + if (!op || typeof op !== 'object') continue; + + const params = + (op.parameters as Array<{ in?: string; name?: string }>) ?? []; + const pathParamNames = params + .filter((p) => p.in === 'path') + .map((p) => p.name!) + .filter(Boolean); + const queryParamNames = params + .filter((p) => p.in === 'query') + .map((p) => p.name!) + .filter(Boolean); + + let hasBody = false; + let bodyIsArray = false; + const rb = op.requestBody as Record | undefined; + if (rb?.content) { + const json = (rb.content as Record)[ + 'application/json' + ] as Record | undefined; + if (json?.schema) { + hasBody = true; + const schema = json.schema as Record; + bodyIsArray = schema.type === 'array'; + } + } + + const key = `${method.toUpperCase()}:${path}`; + map.set(key, { pathParamNames, queryParamNames, hasBody, bodyIsArray }); + } + } + return map; +} + +/** Snake-case param key lookup: params.email_id, params.emailId, params.id for email_id. */ +function getParam(params: Record, specName: string): unknown { + const camel = specName.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); + const aliases: string[] = [specName, camel]; + if (specName === 'email_id') aliases.push('emailId', 'id'); + if (specName === 'domain_id') aliases.push('domainId', 'id'); + for (const k of aliases) { + if (Object.prototype.hasOwnProperty.call(params, k)) return params[k]; + } + return undefined; +} + +/** Apply sender/reply-to defaults to a body object for send/broadcast. */ +function applyEmailDefaultsToBody( + body: Record, + defaults: { senderEmailAddress?: string; replierEmailAddresses: string[] }, +): void { + if ( + body.from === undefined && + body.from_address === undefined && + defaults.senderEmailAddress + ) { + body.from = defaults.senderEmailAddress; + } + if ( + body.reply_to === undefined && + body.replyTo === undefined && + defaults.replierEmailAddresses.length > 0 + ) { + body.reply_to = defaults.replierEmailAddresses; + } +} + +const BATCH_MAX = 100; + +async function doRequest( + apiKey: string, + method: string, + pathTemplate: string, + params: Record, + body: unknown, + opMeta: OpMeta, + defaults: { senderEmailAddress?: string; replierEmailAddresses: string[] }, + requestTimeoutMs: number, +): Promise { + let path = pathTemplate; + const query = new URLSearchParams(); + let payload: unknown; + let idempotencyKey: string | undefined; + + const paramsCopy = { ...params }; + if (body && typeof body === 'object' && !Array.isArray(body)) { + Object.assign(paramsCopy, body as Record); + } + + if (opMeta.bodyIsArray && pathTemplate === '/emails/batch') { + let emails: unknown[]; + if (Array.isArray(body)) { + emails = body; + } else if (body && typeof body === 'object' && !Array.isArray(body)) { + const obj = body as Record; + const raw = obj.emails ?? obj.data ?? obj.batch; + if (!Array.isArray(raw)) { + throw new Error( + 'resend.request to /emails/batch expects body to be an array of email payloads or an object with "emails" array', + ); + } + emails = raw; + const key = obj.idempotencyKey ?? obj.idempotency_key; + idempotencyKey = + key !== undefined && key !== null && String(key).trim() !== '' + ? String(key).trim() + : undefined; + } else { + throw new Error( + 'resend.request to /emails/batch expects body to be an array or object with "emails"', + ); + } + if (emails.length > BATCH_MAX) { + throw new Error( + `Batch accepts at most ${BATCH_MAX} emails, got ${emails.length}`, + ); + } + if (emails.length === 0) { + throw new Error('Batch requires at least one email'); + } + payload = (emails as Record[]).map((item) => + deepSnakeKeys(item), + ); + } else { + for (const name of opMeta.pathParamNames) { + const value = getParam(paramsCopy, name); + if (value === undefined || value === null || `${value}`.trim() === '') { + throw new Error(`Missing path parameter: ${name}`); + } + path = path.replace(`{${name}}`, encodeURIComponent(String(value))); + } + for (const name of opMeta.queryParamNames) { + const value = getParam(paramsCopy, name); + if (value === undefined || value === null || value === '') continue; + query.set(name, String(value)); + } + idempotencyKey = (getParam(paramsCopy, 'idempotencyKey') ?? + getParam(paramsCopy, 'idempotency_key')) as string | undefined; + if (idempotencyKey != null && String(idempotencyKey).trim() === '') + idempotencyKey = undefined; + + if ( + opMeta.hasBody && + body && + typeof body === 'object' && + !Array.isArray(body) + ) { + const bodyObj = { ...(body as Record) }; + if ( + pathTemplate === '/emails' || + pathTemplate.startsWith('/broadcasts') + ) { + applyEmailDefaultsToBody(bodyObj, defaults); + } + delete bodyObj.idempotencyKey; + delete bodyObj.idempotency_key; + payload = deepSnakeKeys(bodyObj); + } + } + + const url = `${RESEND_API_BASE}${path}${query.toString() ? `?${query.toString()}` : ''}`; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), requestTimeoutMs); + + try { + const response = await fetch(url, { + method, + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + ...(idempotencyKey + ? { 'Idempotency-Key': String(idempotencyKey) } + : {}), + }, + body: payload !== undefined ? JSON.stringify(payload) : undefined, + signal: controller.signal, + }); + + const text = await response.text(); + let parsed: unknown; + try { + parsed = text.trim() ? (JSON.parse(text) as unknown) : {}; + } catch { + throw new Error( + `Invalid JSON in response: ${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`, + ); + } + + if (!response.ok) { + throw new Error( + `HTTP ${response.status} ${response.statusText}: ${typeof parsed === 'string' ? parsed : JSON.stringify(parsed)}`, + ); + } + + return parsed; + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`Request timed out after ${requestTimeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +function serializeResult(value: unknown): string { + if (typeof value === 'string') return value; + if (value === undefined) return 'undefined'; + + const seen = new WeakSet(); + return JSON.stringify( + value, + (_key, item) => { + if (typeof item === 'bigint') return item.toString(); + if (typeof item === 'function') return '[function]'; + if (item && typeof item === 'object') { + if (seen.has(item as object)) return '[circular]'; + seen.add(item as object); + } + return item; + }, + 2, + ); +} + +function deepFreezeObject(value: T, seen = new WeakSet()): T { + if (!value || typeof value !== 'object') return value; + const obj = value as object; + if (seen.has(obj)) return value; + seen.add(obj); + Object.freeze(obj); + for (const prop of Object.values(obj as Record)) { + if (prop && typeof prop === 'object') { + deepFreezeObject(prop as never, seen); + } + } + return value; +} + +/** Build resend.request({ method, path, params?, body? }) from resolved OpenAPI spec (Code Mode standard). */ +function buildResendRequest( + opMetaMap: Map, + ctx: { + apiKey: string; + defaults: { senderEmailAddress?: string; replierEmailAddresses: string[] }; + counters: { apiCalls: number; maxApiCalls: number }; + requestTimeoutMs: number; + }, +): { + request: (opts: { + method: string; + path: string; + params?: Record; + body?: unknown; + }) => Promise; +} { + return { + async request(opts) { + const { method, path, params = {}, body } = opts; + const upper = method.toUpperCase(); + const key = `${upper}:${path}`; + const opMeta = opMetaMap.get(key); + if (!opMeta) { + throw new Error( + `Unknown operation: ${upper} ${path}. Use search-resend-api to discover paths from the OpenAPI spec.`, + ); + } + ctx.counters.apiCalls += 1; + if (ctx.counters.apiCalls > ctx.counters.maxApiCalls) { + throw new Error( + `Maximum API calls exceeded (${ctx.counters.maxApiCalls}). Refine logic or increase limit.`, + ); + } + return doRequest( + ctx.apiKey, + upper, + path, + params, + body, + opMeta, + ctx.defaults, + ctx.requestTimeoutMs, + ); + }, + }; +} + +export function addCodeModeTools( + server: McpServer, + { + apiKey, + senderEmailAddress, + replierEmailAddresses, + }: { + apiKey?: string; + senderEmailAddress?: string; + replierEmailAddresses: string[]; + }, +) { + server.registerTool( + 'search-resend-api', + { + title: 'Search Resend OpenAPI (Code Mode)', + description: + 'Discover Resend API endpoints by running JavaScript against the OpenAPI spec. Your code runs as the body of an async function. The variable `spec` is in scope. Use a top-level return for the result. Do not pass an arrow function: pass only statements (e.g. return Object.keys(spec.paths);).', + inputSchema: { + code: z + .string() + .min(1) + .describe( + 'JavaScript: top-level statements only. `spec` is in scope. End with return . Wrong: async (spec) => { return x; }. Right: return Object.keys(spec.paths);', + ), + timeoutMs: z + .number() + .int() + .min(100) + .max(15_000) + .default(5_000) + .optional() + .describe('Execution timeout in milliseconds.'), + }, + }, + async ({ code, timeoutMs }) => { + const spec = await getResolvedOpenApiSpec(); + + const logs: string[] = []; + const safeConsole = { + log: (...args: unknown[]) => + logs.push( + args + .map((a) => + typeof a === 'object' ? JSON.stringify(a) : String(a), + ) + .join(' '), + ), + info: (...args: unknown[]) => + logs.push( + args + .map((a) => + typeof a === 'object' ? JSON.stringify(a) : String(a), + ) + .join(' '), + ), + warn: (...args: unknown[]) => + logs.push( + args + .map((a) => + typeof a === 'object' ? JSON.stringify(a) : String(a), + ) + .join(' '), + ), + error: (...args: unknown[]) => + logs.push( + args + .map((a) => + typeof a === 'object' ? JSON.stringify(a) : String(a), + ) + .join(' '), + ), + }; + + const context = vm.createContext({ + spec: deepFreezeObject(spec), + console: deepFreezeObject(safeConsole), + process: undefined, + require: undefined, + module: undefined, + setTimeout: undefined, + setInterval: undefined, + setImmediate: undefined, + clearTimeout: undefined, + clearInterval: undefined, + clearImmediate: undefined, + } as Record); + + const wrappedCode = `'use strict';\n(async () => {\n${code}\n})()`; + const script = new vm.Script(wrappedCode, { + filename: 'resend-search-code-mode.vm.js', + }); + + let result: unknown; + try { + const executionTimeoutMs = timeoutMs ?? 5_000; + const executionPromise = script.runInContext(context, { + timeout: executionTimeoutMs, + }) as Promise; + executionPromise.catch(() => {}); + + let timeoutHandle: NodeJS.Timeout | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeoutHandle = setTimeout( + () => + reject( + new Error(`Search timed out after ${executionTimeoutMs}ms`), + ), + executionTimeoutMs, + ); + }); + + try { + result = await Promise.race([executionPromise, timeoutPromise]); + } finally { + if (timeoutHandle) clearTimeout(timeoutHandle); + } + } catch (error) { + const message = + error instanceof Error ? error.message : 'Unknown error'; + return { + content: [ + { + type: 'text', + text: [ + 'Search failed.', + `Error: ${message}`, + logs.length > 0 ? `Logs:\n${logs.join('\n')}` : '', + ].join('\n\n'), + }, + ], + }; + } + + return { + content: [ + { + type: 'text', + text: [ + 'Search result:', + '', + serializeResult(result), + logs.length > 0 ? `\nLogs:\n${logs.join('\n')}` : '', + ].join('\n'), + }, + ], + }; + }, + ); + + server.registerTool( + 'execute-resend-code', + { + title: 'Execute Resend Code (Code Mode)', + description: + 'Execute JavaScript against the Resend API. Your code runs as the body of an async function. The variable `resend` is in scope (resend.request({ method, path, params?, body? })). Use a top-level return for the result. Do not pass an arrow function: pass only statements. Optional: input, helpers, console.', + inputSchema: { + code: z + .string() + .min(1) + .describe( + 'JavaScript: top-level statements only. `resend` is in scope. End with return . Wrong: async () => { return await resend.request(...); }. Right: return await resend.request({ method, path, body });', + ), + input: z + .record(z.string(), z.unknown()) + .optional() + .describe('Optional JSON object available to script as `input`.'), + timeoutMs: z + .number() + .int() + .min(100) + .max(30_000) + .default(10_000) + .optional() + .describe('VM execution timeout in milliseconds.'), + requestTimeoutMs: z + .number() + .int() + .min(100) + .max(30_000) + .default(15_000) + .optional() + .describe('Per HTTP request timeout in milliseconds.'), + maxApiCalls: z + .number() + .int() + .min(1) + .max(100) + .default(25) + .optional() + .describe('Maximum API calls this run can make.'), + }, + }, + async ({ code, input, timeoutMs, requestTimeoutMs, maxApiCalls }) => { + if (!apiKey || !apiKey.trim()) { + throw new Error( + 'No API key available for Code Mode. Provide RESEND_API_KEY (stdio) or Authorization Bearer token (HTTP).', + ); + } + + const spec = await getResolvedOpenApiSpec(); + const opMetaMap = getOpMetaMap(spec); + const counters = { apiCalls: 0, maxApiCalls: maxApiCalls ?? 25 }; + const resend = buildResendRequest(opMetaMap, { + apiKey, + defaults: { senderEmailAddress, replierEmailAddresses }, + counters, + requestTimeoutMs: requestTimeoutMs ?? 15_000, + }); + + const logs: string[] = []; + const safeConsole = { + log: (...args: unknown[]) => + logs.push(args.map(serializeResult).join(' ')), + info: (...args: unknown[]) => + logs.push(args.map(serializeResult).join(' ')), + warn: (...args: unknown[]) => + logs.push(args.map(serializeResult).join(' ')), + error: (...args: unknown[]) => + logs.push(args.map(serializeResult).join(' ')), + }; + + const helpers = { + nowIso: () => new Date().toISOString(), + assert: (condition: unknown, message = 'Assertion failed') => { + if (!condition) throw new Error(message); + }, + }; + + const sandboxResend = deepFreezeObject(resend); + const sandboxHelpers = deepFreezeObject(helpers); + const sandboxConsole = deepFreezeObject(safeConsole); + + const context = vm.createContext({ + // Restrict global to Code Mode surface only (no process, require, timers). + resend: sandboxResend, + input: input ?? {}, + helpers: sandboxHelpers, + console: sandboxConsole, + process: undefined, + require: undefined, + module: undefined, + setTimeout: undefined, + setInterval: undefined, + setImmediate: undefined, + clearTimeout: undefined, + clearInterval: undefined, + clearImmediate: undefined, + } as Record); + + const wrappedCode = `'use strict';\n(async () => {\n${code}\n})()`; + const script = new vm.Script(wrappedCode, { + filename: 'resend-execute-code-mode.vm.js', + }); + + let result: unknown; + try { + const executionTimeoutMs = timeoutMs ?? 10_000; + const executionPromise = script.runInContext(context, { + timeout: executionTimeoutMs, + }) as Promise; + executionPromise.catch(() => {}); + + let timeoutHandle: NodeJS.Timeout | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeoutHandle = setTimeout( + () => + reject( + new Error( + `Code execution timed out after ${executionTimeoutMs}ms`, + ), + ), + executionTimeoutMs, + ); + }); + + try { + result = await Promise.race([executionPromise, timeoutPromise]); + } finally { + if (timeoutHandle) clearTimeout(timeoutHandle); + } + } catch (error) { + const message = + error instanceof Error ? error.message : 'Unknown execution error'; + return { + content: [ + { + type: 'text', + text: [ + 'Code execution failed.', + `Error: ${message}`, + `API calls made: ${counters.apiCalls}`, + logs.length > 0 ? `Logs:\n${logs.join('\n')}` : 'Logs: (none)', + ].join('\n\n'), + }, + ], + }; + } + + return { + content: [ + { + type: 'text', + text: [ + 'Code executed successfully.', + `API calls made: ${counters.apiCalls}`, + '', + `Result:\n${serializeResult(result)}`, + '', + logs.length > 0 ? `Logs:\n${logs.join('\n')}` : 'Logs: (none)', + ].join('\n'), + }, + ], + }; + }, + ); +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 1115c4d..cf019d6 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,5 +1,6 @@ export * from './apiKeys.js'; export * from './broadcasts.js'; +export * from './codeMode.js'; export * from './contactProperties.js'; export * from './contacts.js'; export * from './domains.js'; diff --git a/src/transports/http.ts b/src/transports/http.ts index d083bbb..8c0e08c 100644 --- a/src/transports/http.ts +++ b/src/transports/http.ts @@ -66,8 +66,6 @@ export async function runHttp( req.method === 'POST' && isInitializeRequest(req.body) ) { - // New session: require a Bearer token so we can create a per-session - // Resend client scoped to this user's API key. const apiKey = extractBearerToken(req); if (!apiKey) { sendJsonRpcError( @@ -90,7 +88,7 @@ export async function runHttp( const sid = transport!.sessionId; if (sid && sessions[sid]) delete sessions[sid]; }; - const server = createMcpServer(resend, options); + const server = createMcpServer(resend, { ...options, apiKey }); await server.connect(transport); } else if (sessionId && !sessions[sessionId]) { res.statusCode = 404; @@ -124,9 +122,7 @@ export async function runHttp( for (const sid of Object.keys(sessions)) { try { await sessions[sid].close(); - } catch { - // ignore - } + } catch {} delete sessions[sid]; } server.close(); diff --git a/src/types.ts b/src/types.ts index 2ac9899..1da28d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,6 @@ export interface ServerOptions { + apiKey?: string; senderEmailAddress?: string; replierEmailAddresses: string[]; + codeModeOnly?: boolean; } diff --git a/tests/cli/help.test.ts b/tests/cli/help.test.ts index 5745f41..88a3afb 100644 --- a/tests/cli/help.test.ts +++ b/tests/cli/help.test.ts @@ -9,6 +9,7 @@ describe('help', () => { expect(HELP_TEXT).toContain('--sender'); expect(HELP_TEXT).toContain('--reply-to'); expect(HELP_TEXT).toContain('--http'); + expect(HELP_TEXT).toContain('--code-mode-only'); expect(HELP_TEXT).toContain('--port'); expect(HELP_TEXT).toContain('-h, --help'); expect(HELP_TEXT).toContain('RESEND_API_KEY'); diff --git a/tests/cli/parse.test.ts b/tests/cli/parse.test.ts index ca9f1ab..25c0ae4 100644 --- a/tests/cli/parse.test.ts +++ b/tests/cli/parse.test.ts @@ -45,6 +45,10 @@ describe('parseArgs', () => { expect(parseArgs(['--http']).http).toBe(true); }); + it('parses --code-mode-only as boolean', () => { + expect(parseArgs(['--code-mode-only'])['code-mode-only']).toBe(true); + }); + it('parses --port', () => { const parsed = parseArgs(['--port', '8080']); expect(parsed.port).toBe('8080'); diff --git a/tests/cli/resolve.test.ts b/tests/cli/resolve.test.ts index 7bb6c31..f2531b8 100644 --- a/tests/cli/resolve.test.ts +++ b/tests/cli/resolve.test.ts @@ -110,11 +110,21 @@ describe('resolveConfig', () => { const result = resolveConfig(parsed, { RESEND_API_KEY: 're_x' }); expect(result.ok).toBe(true); if (result.ok) { + expect(result.config.codeModeOnly).toBe(false); expect(result.config.transport).toBe('stdio'); expect(result.config.port).toBe(3000); } }); + it('enables code-mode-only with --code-mode-only', () => { + const parsed = parseArgs(['--key', 're_x', '--code-mode-only']); + const result = resolveConfig(parsed, { RESEND_API_KEY: 're_x' }); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.config.codeModeOnly).toBe(true); + } + }); + it('sets transport to http and uses default port when --http', () => { const parsed = parseArgs(['--key', 're_x', '--http']); const result = resolveConfig(parsed, { RESEND_API_KEY: 're_x' }); diff --git a/tests/cli/resolveConfigOrExit.test.ts b/tests/cli/resolveConfigOrExit.test.ts index 93d627c..adec66d 100644 --- a/tests/cli/resolveConfigOrExit.test.ts +++ b/tests/cli/resolveConfigOrExit.test.ts @@ -53,6 +53,7 @@ describe('resolveConfigOrExit', () => { apiKey: 're_abc', senderEmailAddress: 'x@r.dev', replierEmailAddresses: [], + codeModeOnly: false, transport: 'stdio', port: 3000, });