Skip to content
Draft
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
24 changes: 24 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: tests

on:
push:
branches: [main]
pull_request:

jobs:
test:
name: node --test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ['20', '22']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Run test suite
run: node --test test/*.cjs
shell: bash
21 changes: 21 additions & 0 deletions docs/templates-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
name: outreach-v1-approach
description: First-touch outreach. Minimal — just approach + offer report. No bounty discussion, no severity, no legal preamble.
subject: Security finding for {{program}}
isHtml: false
vars: [contact_name, program, my_name]
---
Hi {{contact_name}},

I'm {{my_name}}. I'm reaching out about a security finding affecting {{program}}.

Could you point me at the right channel to share this responsibly?
A quick check on a couple of points before I send the report:

- Do you have a bug bounty / VDP I should follow?
- Is there a contract or NDA you'd want in place first?

Happy to send the report through whichever channel works for you.

Thanks,
{{my_name}}
95 changes: 95 additions & 0 deletions docs/templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Compose templates

The `listTemplates` and `renderTemplate` MCP tools read user-authored
templates from `<Thunderbird profile>/thunderbird-mcp/templates/`. The
directory does not exist by default — create it and drop `*.md` files
inside. The format is Jekyll-style YAML frontmatter followed by the
message body.

## Format

```
---
name: outreach-v1-approach
description: First-touch outreach. Minimal -- just approach + offer report.
subject: Security finding for {{program}}
isHtml: false
vars: [contact_name, program, my_name]
---
Hi {{contact_name}},

I'm {{my_name}}. I'm reaching out about a security finding affecting {{program}}.
...
```

### Frontmatter keys

| key | type | required | meaning |
| --- | --- | --- | --- |
| `name` | string | yes | Short ID. What you pass to `renderTemplate({ name, vars })`. Filename without `.md` is used if omitted. |
| `description` | string | no | One-line summary; shown in `listTemplates`. |
| `subject` | string | no | Subject line. Supports `{{var}}` substitution. |
| `isHtml` | boolean | no | Treat body as HTML. Default `false`. |
| `vars` | array of string | no | Names of `{{var}}` placeholders the caller must supply. `renderTemplate` errors if any are missing. |

### Variable substitution

Placeholders look like `{{name}}` and are replaced by the matching key
from the `vars` object passed to `renderTemplate`. Unknown placeholders
are left as literals so you notice rather than silently shipping an
empty value.

## Usage from an MCP client

```jsonc
// 1. Discover what's available
{ "method": "tools/call", "params": { "name": "listTemplates" } }

// 2. Render
{
"method": "tools/call",
"params": {
"name": "renderTemplate",
"arguments": {
"name": "outreach-v1-approach",
"vars": { "contact_name": "Alex", "program": "ExampleCorp", "my_name": "Jordan" }
}
}
}
// Returns { name, subject, body, isHtml, file }

// 3. Feed the rendered output into sendMail
{
"method": "tools/call",
"params": {
"name": "sendMail",
"arguments": {
"to": "security@example.com",
"subject": "<rendered subject>",
"body": "<rendered body>",
"isHtml": false,
"skipReview": false,
"idempotencyKey": "outreach-v1-examplecorp-2026-01"
}
}
}
```

Using `idempotencyKey` lets you re-run the same agent loop after a
crash without double-sending to the same target.

## Why this format

- One file per template, plain text. Editable in any tool, diff-able,
versionable in your own private dotfiles repo.
- Lives in `ProfD`, not in the extension bundle — your templates are
not shipped publicly when the extension is updated, and they survive
an `.xpi` reinstall.
- No template engine dependency. The substitution is `{{name}}` only;
no loops, no conditionals, no inline code. Keeps the surface
predictable for an LLM.
- Variable list is declared up-front so `renderTemplate` can fail
loudly when the caller forgot a placeholder, instead of producing
an output with a literal `{{name}}` in it.

An example template ships in `docs/templates-example.md`.
Loading