Skip to main content
Glama
theaidrill

EmailTemplateMCP

by theaidrill
README.md
# EmailTemplateMCP

A minimal [MCP](https://modelcontextprotocol.io) server that generates email templates with Claude
— one tool, one file, deployed as a single AWS Lambda function behind a public Function URL. No
framework, no MCP SDK dependency: the JSON-RPC dispatch is ~130 lines of plain JavaScript.

This is the reference implementation used in the *"Build an MCP server from scratch"* video series.

## What it does

Exposes a single tool, `create_email_template`, which takes a description of an email's purpose
(plus optional tone and brand name) and returns a subject line, a plain-text body, and an
inline-styled HTML body — generated by Claude in one call.

```json
{
  "name": "create_email_template",
  "arguments": {
    "description": "welcome email for new SaaS signups",
    "tone": "friendly",
    "brand_name": "Acme"
  }
}
```

## Why no MCP SDK?

MCP isn't a library you install — it's a protocol: JSON-RPC 2.0 over HTTP. This project implements
`initialize`, `tools/list`, and `tools/call` directly in the Lambda handler, so there's nothing to
learn beyond reading `index.mjs` top to bottom. The only real dependency is the
[Anthropic SDK](https://github.com/anthropics/anthropic-sdk-typescript), used to actually generate
the email content.

Output shape is enforced server-side via Anthropic's structured output
(`output_config.format.json_schema`), so there's no fragile hand-rolled JSON parsing of the model's
response.

## Why a Function URL, not API Gateway?

Generating an email is a real model call — it can take 10-20+ seconds. API Gateway's HTTP API has a
hard, non-configurable 30-second integration timeout. A Lambda Function URL has no such ceiling; the
only limit is the function's own configured timeout (set here to ~2 minutes).

## Deploying

### Option A — one command, via AWS SAM

`template.yaml` in this repo defines the whole thing — the function, its Function URL, and both
environment variables — so you don't have to click through the console at all.

```bash
sam build
sam deploy --guided
```

`--guided` walks you through stack name, region, and the two secret parameters (`AnthropicApiKey`,
`McpApiKey` — both `NoEcho`, never printed or stored in plain text in the template) on first run,
then remembers your answers in `samconfig.toml` for future deploys. On success it prints the
Function URL as an output.

Requires the [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html)
and valid AWS credentials configured locally.

### Option B — manual, via the Lambda console

1. `npm install` — pulls in the Anthropic SDK.
2. Zip the folder's **contents** (not the folder itself) — `index.mjs`, `node_modules`,
   `package.json`, `package-lock.json` should sit at the zip's top level.
3. In the Lambda console: **Create function** → author from scratch → Node.js 22+ → upload the zip.
4. **Configuration → General configuration**: raise Timeout (e.g. 2 min) and Memory (e.g. 256 MB) —
   the defaults (3s / 128MB) aren't enough for a real generation call.
5. **Configuration → Environment variables**:
   - `ANTHROPIC_API_KEY` — from the Anthropic console.
   - `MCP_API_KEY` — a secret you invent (any long random string). This is the shared secret every
     caller must present.
   - `ANTHROPIC_MODEL` — optional, defaults to `claude-opus-5`.
6. **Configuration → Function URL**: create one with Auth type `NONE`. This doesn't mean
   unauthenticated — it means AWS isn't checking IAM signatures. `MCP_API_KEY` is the actual gate,
   checked on every request inside the handler.

## Authentication

Every request must include the shared secret, either as a header:

```
x-api-key: <MCP_API_KEY>
```

or a query parameter:

```
?key=<MCP_API_KEY>
```

Requests without a valid key get a `401` before anything else runs — including before any call to
Anthropic, so a bad key never costs you a generation.

## Testing it directly

```bash
# List tools
curl -s https://<your-function-url> \
  -H "x-api-key: <MCP_API_KEY>" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# Call the tool
curl -s https://<your-function-url> \
  -H "x-api-key: <MCP_API_KEY>" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"create_email_template","arguments":{"description":"welcome email for new signups"}}}'
```

## Connecting an MCP client

For Claude Code:

```bash
claude mcp add --transport http email-template https://<your-function-url> \
  --header "x-api-key: <MCP_API_KEY>"
```

Any MCP-compatible client that supports plain header auth over Streamable HTTP works the same way.

## Project structure

```
index.mjs          — the entire server: tool definition, Anthropic call, JSON-RPC dispatch
package.json        — the one runtime dependency
```

## Security note

`MCP_API_KEY` and `ANTHROPIC_API_KEY` are both plain environment variables — never hardcode them in
`index.mjs`. If you've ever shown this function's configuration on screen (a demo, a screen
recording), rotate both values afterward.