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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
- run: bun run lint
- run: bun run test
- run: bun run docs:commands:check
- run: bun run generate:skills:check
- run: bun run build
- name: Bin smoke
run: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
- run: bun run lint
- run: bun run test
- run: bun run docs:commands:check
- run: bun run generate:skills:check
- run: bun run build
- name: Compile standalone binaries
run: bun run scripts/build-binaries.ts
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
# Changelog

## Unreleased

### Added

- **`brew-cli mcp install`** adds the Brew MCP server to Cursor
(`.cursor/mcp.json`), VS Code (`.vscode/mcp.json`), Claude Code
(`.mcp.json`), Gemini CLI (`.gemini/settings.json`), and, when named,
Devin Desktop, formerly Windsurf (`~/.config/devin/mcp_config.json`),
and Codex (`~/.codex/config.toml`). It merges a `brew` entry and keeps
other servers; an existing `brew` entry with other settings, a config
with comments, or a symbolic link whose target leaves the project comes
back `manual` with a snippet. `--client`, `--scope project|user`,
`--auth oauth|env` (env adds a header the client fills from
`BREW_API_KEY`, never a literal key), `--print`, `--dry-run`. JSON
output:
`{ mcpUrl, dryRun, clients: [{ client, path?, action, deeplink?, snippet?, note?, nextStep? }] }`
(`nextStep` is `codex mcp login brew` after a Codex OAuth entry).
Cursor and VS Code results carry one-click install deeplinks; the Cursor
link is byte-identical to the one brew.new serves.
- `BREW_CLI_INVOKED_AS`: a wrapper that runs the CLI through a package
runner (`npx -y @brew.new/cli@<version>`) sets it, and `confirmCommand`,
`retryCommand` and every "run …" hint start with it instead of
`brew-cli`. Only `npx`, `npx -y`, `pnpm dlx`,
`bunx`, `yarn dlx`, `npm exec` or `npm create` followed by one package
spec is accepted; anything else falls back to `brew-cli`.
- Project foundations for the coming `init` and `migrate` commands,
dependency-free under `src/lib/project/`: package-manager detection and
command builders (npm, pnpm, yarn classic and berry with PnP, bun, deno,
workspace filters), monorepo resolution (pnpm workspaces, npm/yarn/bun
workspaces, Turborepo, Nx, falling back to the git root when there is
no package.json), a create-only file writer (managed-file hashes,
marked-block upserts, atomic writes, dry-run plans; never writes a
secret through a symbolic link or follows one out of the project, and
tightens a secret file's mode to 0600), a dotenv editor that never
replaces, duplicates or prints a value and refuses browser-exposed
names for secrets, `brew.json` state (schema shipped in
`schemas/brew.json`), gitignore checks, a line diff, and vendor HTTP
that honours `--timeout` and Ctrl-C and never echoes a credential.
- The agent skills in `skills/` are embedded in the build
(`src/generated/skills.ts`, `bun run generate:skills`; CI runs
`generate:skills:check`).
- npm keywords for MCP, transactional and auth email, Resend and React
Email, so the package shows up where people look for them.

## 0.18.0

These import changes require `@brew.new/sdk` 13.3.0 or newer, which adds
Expand Down
51 changes: 49 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,49 @@ data on stdout and progress on stderr, and documents itself:
brew-cli emails send --help
```

## Connect your AI editor to Brew (MCP)

```bash
brew-cli mcp install # the editors this project uses
brew-cli mcp install --client cursor claude-code # or name them
brew-cli mcp install --client all --dry-run --json # plan only
```

`mcp install` adds the Brew MCP server (`https://brew.new/api/mcp`, or the
origin of `--api-url` + `/api/mcp`) to each client's config, merging a
`brew` entry and keeping every other server:

| Client | `--client` | Project file (`--scope project`, default) | User file (`--scope user`) |
| --- | --- | --- | --- |
| Cursor | `cursor` | `.cursor/mcp.json` | `~/.cursor/mcp.json` |
| VS Code | `vscode` | `.vscode/mcp.json` | the profile's `mcp.json` |
| Claude Code | `claude-code` | `.mcp.json` | prints `claude mcp add --scope user …` |
| Gemini CLI | `gemini` | `.gemini/settings.json` | `~/.gemini/settings.json` |
| Devin Desktop (Windsurf) | `windsurf` | (user only) | `~/.config/devin/mcp_config.json` (`$XDG_CONFIG_HOME/devin/`, Windows `%APPDATA%\devin\`) |
| Codex | `codex` | (user only) | `~/.codex/config.toml` (`$CODEX_HOME`) |

Without `--client` it configures the clients it detects (`.cursor/`,
`.vscode/`, `.claude/` or `CLAUDECODE=1`, `.gemini/`); a detected Devin
Desktop (`.windsurf/` or `.devin/`) or Codex is reported with its snippet
but only written when you name it, since its config lives in your home
directory. `--auth oauth` (default) writes the URL only and the client
signs in on first use; Codex signs in only through its own command, so its
result carries `nextStep: "codex mcp login brew"`. `--auth env` adds an
`Authorization` header that the client fills from `BREW_API_KEY` at
connect time: `${env:BREW_API_KEY}` for Cursor, VS Code and Devin Desktop,
`${BREW_API_KEY}` for Claude Code and Gemini CLI, `bearer_token_env_var`
for Codex. The client reads the variable from the environment it was
started with, so an editor opened from the Dock or Start menu may not see
a variable your shell exports. A literal key is never written.

An existing `brew` entry is never replaced: the same settings report
`unchanged`, other settings come back `manual` with the snippet to paste.
So does a config with comments, and a config that is a symbolic link
whose target leaves the project (or your home directory, for user
files). `--print` prints the snippets and writes nothing. Each result is
`created`, `updated`, `unchanged`, `manual` or `printed`; Cursor and
VS Code results carry a one-click install `deeplink`.

## Authentication

| Precedence | Source |
Expand Down Expand Up @@ -90,7 +133,11 @@ Designed for AI agents as first-class users:
trigger fires) never hang waiting for input: non-interactive callers
get exit `4` plus a JSON envelope containing a ready-to-run
`confirmCommand`; pass `--yes` to proceed. Humans on a TTY get a y/N
prompt. Test sends (`emails send --test`) skip the gate.
prompt. Test sends (`emails send --test`) skip the gate. Printed
commands (`confirmCommand`, `retryCommand`, "run … login") start the
way the CLI was invoked: a wrapper that runs it through a package
runner sets `BREW_CLI_INVOKED_AS` (for example
`npx -y @brew.new/cli@<version>`), so they re-run through it.
- **Saved email recovery** — `409 EMAIL_RESULT_UNAVAILABLE` means the
generation or edit committed. Do not repeat the write. Read the exact
saved result using `error.details.emailId` and `emailVersionId`:
Expand Down Expand Up @@ -121,7 +168,7 @@ The full generated reference lives in
`contacts`, `fields`, `emails` (+ `groups`, `comments`), `sends`,
`audiences`, `automations` (+ `triggers`, `runs`, `audience-runs`,
`trigger-instances`), `analytics`, `insights`, `brand`, `brands`, `content`,
`templates`, `flows`, `chats`, `notifications`, plus
`templates`, `flows`, `chats`, `notifications`, `mcp`, plus
`canvas sessions` and `login`/`logout`/`whoami`/`config`/`usage`/`health`/`docs`/`api`.

For an embedded editor, create brands with an organization-scoped key,
Expand Down
9 changes: 8 additions & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!dist", "!openapi", "!docs/commands", "!src/generated"]
"includes": [
"**",
"!dist",
"!openapi",
"!docs/commands",
"!src/generated",
"!tests/fixtures"
]
},
"formatter": {
"enabled": true,
Expand Down
22 changes: 21 additions & 1 deletion docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- GENERATED FILE — do not edit. Regenerate with `bun run docs:commands`. -->

149 commands. Classes: read (always safe), write
150 commands. Classes: read (always safe), write
(mutating, retry-safe), destructive (irreversible — the confirmation
protocol applies: interactive y/N on a TTY, exit 4 + JSON envelope with
a `confirmCommand` otherwise, `--yes` to proceed).
Expand Down Expand Up @@ -170,6 +170,7 @@ a `confirmCommand` otherwise, `--yes` to proceed).
| `brew-cli doctor` | read | — | Trust check: auth, API reachability, and installed-CLI vs live-API drift |
| `brew-cli docs` | read | — | Documentation pointers; --agent prints the command manifest |
| `brew-cli docs api` | read | `GET /v1/help` | Fetch the live machine-readable API catalog (GET /v1/help) |
| `brew-cli mcp install` | write | — | Add the Brew MCP server to your AI editors' config (Cursor, VS Code, Claude Code, Gemini CLI, Devin Desktop/Windsurf, Codex) |
| `brew-cli api` | destructive | — | Raw authenticated request against the Brew public API |

## Details
Expand Down Expand Up @@ -2685,6 +2686,25 @@ Fetch the live machine-readable API catalog (GET /v1/help)
brew-cli docs api --json
```

### brew-cli mcp install

Add the Brew MCP server to your AI editors' config (Cursor, VS Code, Claude Code, Gemini CLI, Devin Desktop/Windsurf, Codex)

- Class: write
- `--client <ids...>` — Clients to configure: cursor, vscode, claude-code, gemini, windsurf, codex, all (default: the ones this project uses; windsurf (Devin Desktop) and codex only when named)
- `--scope <scope>` — project (files in this repo) or user (your home directory)
- `--auth <mode>` — oauth (URL only; sign in from the client) or env (a header the client fills from BREW_API_KEY; never a literal key)
- `--print` — Print each config snippet instead of writing files
- `--dry-run` — Report what would change without writing anything

```bash
brew-cli mcp install
brew-cli mcp install --client cursor claude-code
brew-cli mcp install --client codex --auth env
brew-cli mcp install --client all --dry-run --json
brew-cli mcp install --client vscode --print
```

### brew-cli api

Raw authenticated request against the Brew public API
Expand Down
18 changes: 15 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,24 @@
"email",
"marketing",
"agent",
"api"
"api",
"mcp",
"model-context-protocol",
"transactional-email",
"resend",
"resend-alternative",
"react-email",
"ai-agent",
"agents",
"auth-emails"
],
"type": "module",
"bin": {
"brew-cli": "dist/bin.js"
},
"files": [
"dist",
"schemas",
"README.md",
"LICENSE"
],
Expand All @@ -45,11 +55,13 @@
"test": "vitest run",
"test:watch": "vitest",
"parity": "vitest run tests/parity-sdk.test.ts tests/parity-spec.test.ts",
"check": "bun run tsc && bun run lint && bun run test",
"check": "bun run tsc && bun run lint && bun run test && bun run docs:commands:check && bun run generate:skills:check",
"docs:commands": "bun run scripts/generate-command-docs.ts",
"docs:commands:check": "bun run scripts/generate-command-docs.ts --check",
"generate:types": "node scripts/generate-types.mjs",
"prepublishOnly": "bun run clean && bun run tsc && bun run lint && bun run test && bun run build",
"generate:skills": "bun run scripts/generate-skills.ts",
"generate:skills:check": "bun run scripts/generate-skills.ts --check",
"prepublishOnly": "bun run clean && bun run tsc && bun run lint && bun run test && bun run generate:skills:check && bun run build",
"smoke:signals": "bun run scripts/signal-smoke.ts"
},
"dependencies": {
Expand Down
88 changes: 88 additions & 0 deletions schemas/brew.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://unpkg.com/@brew.new/cli/schemas/brew.json",
"title": "brew.json",
"description": "Brew project file written by `brew-cli init`. Committed; holds identifiers and file hashes, never secrets.",
"type": "object",
"required": ["version", "projectId", "emails", "files"],
"properties": {
"$schema": {
"type": "string",
"minLength": 1
},
"version": {
"const": 1
},
"projectId": {
"type": "string",
"pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
},
"brandId": {
"type": "string",
"minLength": 1
},
"framework": {
"type": "string",
"minLength": 1
},
"auth": {
"type": "string",
"minLength": 1
},
"contractsPath": {
"type": "string",
"pattern": "^(?![A-Za-z]:)(?!/)(?!.*\\\\)(?!(?:.*/)?\\.\\.(?:/|$))(?!.*//)(?!.*/$).+$"
},
"emails": {
"type": "object",
"propertyNames": {
"pattern": "^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$"
},
"additionalProperties": {
"type": "object",
"required": ["triggerEventId", "automationId"],
"properties": {
"triggerEventId": {
"type": "string",
"minLength": 1
},
"automationId": {
"type": "string",
"minLength": 1
}
}
}
},
"files": {
"type": "object",
"propertyNames": {
"pattern": "^(?![A-Za-z]:)(?!/)(?!.*\\\\)(?!(?:.*/)?\\.\\.(?:/|$))(?!.*//)(?!.*/$).+$"
},
"additionalProperties": {
"type": "object",
"required": ["sha256", "generator"],
"properties": {
"sha256": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"generator": {
"type": "string",
"minLength": 1
}
}
}
},
"providerConfig": {
"type": "object"
},
"mcp": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
}
}
}
Loading
Loading