Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
RESEND_API_KEY=
SENDER_EMAIL_ADDRESS=
REPLY_TO_EMAIL_ADDRESSES=
MCP_PORT=3000
MCP_PORT=3000
RESEND_OPENAPI_SPEC_URL=
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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: '[email protected]', to: '[email protected]', 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.
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand All @@ -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": {
Expand Down
31 changes: 21 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions scripts/build-post.sh
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Options:
--sender <email> Default from address (or SENDER_EMAIL_ADDRESS)
--reply-to <email> 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 <number> HTTP port when using --http (default: 3000, or MCP_PORT)
-h, --help Show this help

Expand Down
2 changes: 1 addition & 1 deletion src/cli/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
});
}
Expand Down
2 changes: 2 additions & 0 deletions src/cli/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -54,6 +55,7 @@ export function resolveConfig(
const base = {
senderEmailAddress: senderEmailAddress ?? '',
replierEmailAddresses: parseReplierAddresses(parsed, env),
codeModeOnly,
port,
};

Expand Down
2 changes: 2 additions & 0 deletions src/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface StdioConfig {
apiKey: string;
senderEmailAddress: string;
replierEmailAddresses: string[];
codeModeOnly: boolean;
transport: 'stdio';
port: number;
}
Expand All @@ -19,6 +20,7 @@ export interface HttpConfig {
apiKey?: string;
senderEmailAddress: string;
replierEmailAddresses: string[];
codeModeOnly: boolean;
transport: 'http';
port: number;
}
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading