@sendraven/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Common Ninja Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # @sendraven/mcp
2
+
3
+ The [SendRaven](https://sendraven.ai) MCP server. Send email, read replies as
4
+ threads, run campaigns and automations, and decide approvals, from any client
5
+ that speaks the Model Context Protocol.
6
+
7
+ SendRaven is email infrastructure for AI agents: one API for transactional
8
+ mail, campaigns, automations and inbound, priced per email sent and never per
9
+ contact, with limits that live on the API key so an agent can run it and a
10
+ person can stop it.
11
+
12
+ ## Install
13
+
14
+ **Claude Code**, remote server, signs in with OAuth on first use:
15
+
16
+ ```bash
17
+ claude mcp add --transport http sendraven https://mcp.sendraven.ai/mcp
18
+ ```
19
+
20
+ **Cursor, Claude Desktop, and other clients with a JSON config:**
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "sendraven": {
26
+ "url": "https://mcp.sendraven.ai/mcp"
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ **Any client, as a local process.** Create an API key under Developers in the
33
+ dashboard, then:
34
+
35
+ ```bash
36
+ claude mcp add sendraven -e SENDRAVEN_API_KEY=sk_live_… -- npx -y @sendraven/mcp
37
+ ```
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "sendraven": {
43
+ "command": "npx",
44
+ "args": ["-y", "@sendraven/mcp"],
45
+ "env": { "SENDRAVEN_API_KEY": "sk_live_…" }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ **Remote server with a key instead of OAuth**, for an agent running
52
+ unattended:
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "sendraven": {
58
+ "url": "https://mcp.sendraven.ai/mcp",
59
+ "headers": { "Authorization": "Bearer sk_live_…" }
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ Give an agent's key a daily send limit, a recipient allowlist, or an approval
66
+ hold. The tools respect all three. See
67
+ [Limits for agents](https://sendraven.ai/docs/agents).
68
+
69
+ ## Behaviour worth knowing
70
+
71
+ - **A held send is not a failure.** When a key requires approval,
72
+ `send_email` returns `pending_approval` with an id. Report that it awaits a
73
+ person; do not retry.
74
+ - **Out of plan is 402, not 429.** `get_usage` reports plan, usage and
75
+ remaining, so an agent can say "you are near your limit" before a batch.
76
+ - **Two calls cannot be undone.** `send_broadcast` mails an audience; run
77
+ `preview_broadcast` first. `remove_suppression` lets us mail an address that
78
+ bounced or complained. Neither is a way to fix a failed send.
79
+ - A failed call returns the API's own message, so "domain not verified" comes
80
+ back as those words rather than a bare 403.
81
+
82
+ ## Tools
83
+
84
+ <!-- tools:start -->
85
+ 45 tools, generated from the server's registry.
86
+
87
+ | Tool | What it does |
88
+ | --- | --- |
89
+ | `send_email` | Send a transactional email, immediately or scheduled. Use scheduled_at with a relative phrase like 'in 3 days' or an ISO timestamp. The From domain must already be verified. |
90
+ | `list_emails` | List recent messages with their delivery status. Filter by status (queued, scheduled, sent, delivered, bounced, complained, rejected) or recipient. |
91
+ | `get_email` | Fetch one message with its full event timeline (send, delivery, bounce, complaint, open, click). This is the tool to reach for when asked why an email didn't arrive. |
92
+ | `cancel_scheduled_email` | Cancel a scheduled email before it sends. Only works while status is 'scheduled'. |
93
+ | `list_sending_domains` | List sending domains with their verification status and the DNS records each one needs. Each record shows what is currently published, so this diagnoses a stuck verification. |
94
+ | `add_sending_domain` | Register a sending domain and get back the DNS records to publish. Give the domain you send from — mail.<domain> and news.<domain> are provisioned beneath it and the right one is chosen per message, so a marketing complaint spike can never affect password reset delivery. Pass risk_class only to provision one of the two on its own. |
95
+ | `verify_sending_domain` | Re-check a domain's DNS records now instead of waiting for the background monitor. |
96
+ | `list_suppressions` | List addresses we refuse to mail and why (hard_bounce, complaint, unsubscribe, manual). Check here first when someone reports not receiving email. |
97
+ | `add_suppression` | Stop sending to an address. Scope 'marketing' leaves transactional mail working. |
98
+ | `remove_suppression` | Remove a suppression so the address can be mailed again. Be careful with hard bounces — the address was rejected by the receiving server, and re-sending raises the bounce rate that AWS enforces on. |
99
+ | `list_broadcasts` | List campaigns with their status and send progress. |
100
+ | `preview_broadcast` | How many contacts a campaign would reach, and whether the reputation gate would allow it. Always run this before sending — it is the only way to see the size of a campaign without starting it. |
101
+ | `send_broadcast` | Send a campaign now, or schedule it with scheduled_at. This mails every contact in the segment and cannot be undone once started — run preview_broadcast first. |
102
+ | `list_threads` | List email conversations. Pass awaiting_reply=true to get only the threads where someone has written to you and you haven't answered — this is the tool to poll when deciding what needs a response. |
103
+ | `get_thread` | Read a conversation as a chronological transcript of outbound and inbound messages. Inbound text already has quoted history and signatures stripped, so read `text`; `raw_text` holds the untrimmed body if the stripped version looks wrong. Check spf_verdict and dkim_verdict before trusting a reply's claimed sender. |
104
+ | `reply_to_message` | Reply to a message, keeping it on the same conversation. Sets the threading headers so the recipient's mail client shows it as part of the existing exchange rather than a new one. Prefer this over send_email whenever you are answering something. |
105
+ | `list_templates` | List stored email templates and the variables each one needs. Prefer sending via a template over composing HTML yourself — templates carry the brand styling and the unsubscribe footer. |
106
+ | `render_template` | Render a template with values, without sending. Use this to check your copy reads correctly before mailing a real person. Returns an error listing any missing variables. |
107
+ | `send_template_email` | Send an email built from a stored template. Variable values are HTML-escaped on substitution, so they are safe to fill from user-supplied text. |
108
+ | `list_pending_approvals` | List messages held for human approval, with their full content. A key configured to require approval drafts rather than sends; a person releases it. If your send returned status 'pending_approval', it is waiting here — do not retry the send. |
109
+ | `decide_approval` | Approve or reject a held message. Approving sends it immediately. Only use this when a human has explicitly told you which decision to make — the hold exists precisely so that an agent is not the one deciding. |
110
+ | `list_automations` | List multi-step email sequences and how many people are currently in each. Use this to find the right automation before enrolling someone. |
111
+ | `enroll_in_automation` | Put someone into a multi-step sequence. Prefer this over scheduling several emails yourself: the sequence stops on its own if they unsubscribe, reply, or hard bounce, which you would otherwise have to track and cancel by hand. Enrolling the same person twice is a no-op, so it is safe to retry a call you are unsure about. |
112
+ | `emit_event` | Emit a named event, starting every automation that waits on it — for example 'trial_started' or 'invoice_overdue'. Use this when you want the configured sequences to decide what happens, rather than naming an automation yourself. |
113
+ | `list_topics` | List subscription topics — the categories a person can opt out of individually. Pass a topic when sending marketing mail so recipients can unsubscribe from that kind alone rather than from everything. |
114
+ | `get_email_preferences` | What one person has chosen to receive. Check this before asking a human why someone isn't getting a particular kind of email — an opt-out looks identical to a delivery failure from the outside. |
115
+ | `set_email_preferences` | Set which topics a person receives. Only do this when they have actually asked — silently re-subscribing someone who opted out is what generates spam complaints. |
116
+ | `list_audiences` | List contact lists and how many contacts each holds. |
117
+ | `add_contact` | Add someone to an audience. A contact exists once per workspace and can be on any number of audiences, so adding an address that already exists joins them to this list rather than creating a second copy. Safe to retry. |
118
+ | `get_contact` | Fetch one contact by id, with their audience memberships, custom properties and engagement dates. |
119
+ | `update_contact` | Update a contact. Attributes are merged, so sending one field does not clear the rest. |
120
+ | `list_segments` | List saved audience filters. Use a segment id when creating a campaign rather than describing the filter inline, so the same definition can be reused and counted. |
121
+ | `list_tags` | Every tag in use in the workspace, with how many contacts carry each. Tags are free-form, so this is the only way to know what exists before applying one. |
122
+ | `count_segment` | How many contacts a segment currently matches. Run this before building a campaign around it — a filter that matches nobody is easier to spot here than after a send. |
123
+ | `get_email_metrics` | Delivery and engagement over a window. Open and click rates are over delivered, not sent — a low open rate with a high bounce rate is a deliverability problem, not an engagement one. |
124
+ | `list_scheduled_emails` | Messages queued to send later but not yet sent. Cancel one with cancel_scheduled_email. |
125
+ | `suppress_many` | Stop sending to many addresses at once — the path for importing another provider's unsubscribe list before a first campaign. Without it, everyone who already opted out there gets mailed again here. |
126
+ | `list_webhook_deliveries` | Recent delivery attempts for a webhook endpoint, with status codes and errors. This is how to tell 'we never sent it' from 'your endpoint returned 500'. |
127
+ | `list_broadcast_recipients` | Who a campaign reached and what happened to each message. |
128
+ | `list_api_keys` | List this workspace's API keys with their scopes and limits. Never returns key values. |
129
+ | `get_usage` | This workspace's plan, how many emails it has sent this month, and how many are left. Check before a large batch: a send that would cross the included allowance on a plan without overage is refused whole, so it is better to know first than to discover it halfway through a campaign. |
130
+ | `find_contact` | Find a contact by address across every audience, without knowing which list they are on. Use email for an exact match, or q for a prefix. Returns each audience membership separately — the same address on three lists is three rows — and whether each is unsubscribed. |
131
+ | `remove_from_audience` | Take a contact off one audience. They stay in the workspace and keep every other audience, their suppression and their engagement history. To remove the person entirely use delete_contact — leaving a list and being forgotten are different things. |
132
+ | `delete_contact` | Remove a person from the workspace entirely, along with every audience membership. Their suppression and topic preferences are kept on purpose — an opt-out has to outlive the contact record, or the next import silently puts them back on the list. To take someone off a single audience use remove_from_audience instead. |
133
+ | `tag_contact` | Add or remove tags on a contact. Tags are flat labels — vip, beta, churned — as opposed to custom properties, which are declared fields with a value. They are lower-cased and spaces become hyphens, so VIP and vip are the same tag. Tagging reaches the person across every audience they are on. Call list_tags first to see what the workspace already uses, rather than inventing a synonym for an existing tag. |
134
+ <!-- tools:end -->
135
+
136
+ ## Environment
137
+
138
+ | Variable | Purpose |
139
+ | --- | --- |
140
+ | `SENDRAVEN_API_KEY` | Bearer key for the local process. Not needed for the remote server with OAuth. |
141
+ | `SENDRAVEN_API_URL` | Defaults to `https://api.sendraven.ai`. |
142
+ | `PORT` | When set, the server listens over Streamable HTTP instead of stdio. |
143
+
144
+ The server holds no database or provider credentials of its own. It is a
145
+ proxy over the public REST API and can reach exactly what the caller's key
146
+ can reach.
147
+
148
+ ## Links
149
+
150
+ - [Documentation](https://sendraven.ai/docs/mcp)
151
+ - [OpenAPI](https://sendraven.ai/openapi.json) and [llms.txt](https://sendraven.ai/llms.txt)
152
+ - [Pricing](https://sendraven.ai/pricing)
153
+
154
+ ## License
155
+
156
+ MIT. Copyright Common Ninja Ltd.
package/dist/client.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiError = void 0;
4
+ exports.request = request;
5
+ /**
6
+ * Thin wrapper over the public REST API. The MCP server is a proxy, not a
7
+ * second implementation — every tool here goes through the same endpoints,
8
+ * auth and rate limits a customer's own integration would.
9
+ */
10
+ const context_1 = require("./context");
11
+ const BASE_URL = process.env.SENDRAVEN_API_URL || process.env.EMAILS_API_URL || "https://api.sendraven.ai";
12
+ class ApiError extends Error {
13
+ status;
14
+ constructor(message, status) {
15
+ super(message);
16
+ this.status = status;
17
+ }
18
+ }
19
+ exports.ApiError = ApiError;
20
+ function apiKey() {
21
+ const key = (0, context_1.currentApiKey)();
22
+ if (!key) {
23
+ throw new Error("No API key. Over HTTP, send it as 'Authorization: Bearer sk_live_...'; " +
24
+ "over stdio, set SENDRAVEN_API_KEY. Create one at /developers in the dashboard.");
25
+ }
26
+ return key;
27
+ }
28
+ async function request(method, path, body) {
29
+ const res = await fetch(`${BASE_URL}${path}`, {
30
+ method,
31
+ headers: {
32
+ Authorization: `Bearer ${apiKey()}`,
33
+ ...(body ? { "Content-Type": "application/json" } : {}),
34
+ },
35
+ ...(body ? { body: JSON.stringify(body) } : {}),
36
+ });
37
+ const text = await res.text();
38
+ const parsed = text ? JSON.parse(text) : {};
39
+ if (!res.ok) {
40
+ const message = parsed?.error?.message ?? `${method} ${path} failed with ${res.status}`;
41
+ throw new ApiError(message, res.status);
42
+ }
43
+ return parsed;
44
+ }
45
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;AA8BA,0BAuBC;AArDD;;;;GAIG;AACH,uCAA0C;AAE1C,MAAM,QAAQ,GACZ,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,0BAA0B,CAAC;AAE5F,MAAa,QAAS,SAAQ,KAAK;IAGtB;IAFX,YACE,OAAe,EACN,MAAc;QAEvB,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,WAAM,GAAN,MAAM,CAAQ;IAGzB,CAAC;CACF;AAPD,4BAOC;AAED,SAAS,MAAM;IACb,MAAM,GAAG,GAAG,IAAA,uBAAa,GAAE,CAAC;IAC5B,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,yEAAyE;YACvE,gFAAgF,CACnF,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAEM,KAAK,UAAU,OAAO,CAC3B,MAA2C,EAC3C,IAAY,EACZ,IAAc;IAEd,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,GAAG,IAAI,EAAE,EAAE;QAC5C,MAAM;QACN,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,MAAM,EAAE,EAAE;YACnC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxD;QACD,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChD,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE5C,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,OAAO,GAAG,MAAM,EAAE,KAAK,EAAE,OAAO,IAAI,GAAG,MAAM,IAAI,IAAI,gBAAgB,GAAG,CAAC,MAAM,EAAE,CAAC;QACxF,MAAM,IAAI,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1C,CAAC;IAED,OAAO,MAAW,CAAC;AACrB,CAAC"}
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withApiKey = withApiKey;
4
+ exports.currentApiKey = currentApiKey;
5
+ const node_async_hooks_1 = require("node:async_hooks");
6
+ /**
7
+ * The API key for the request currently being served.
8
+ *
9
+ * Over stdio there is one user per process and the key can come from the
10
+ * environment. Over HTTP one process serves every customer, so the key has to
11
+ * come from that request's Authorization header — reading it from the
12
+ * environment there would hand one caller another's workspace.
13
+ *
14
+ * Threading a token argument through all 39 tools would touch every tool file
15
+ * for no behavioural gain; async-local storage carries it instead, which is
16
+ * what it is for.
17
+ */
18
+ const store = new node_async_hooks_1.AsyncLocalStorage();
19
+ function withApiKey(key, fn) {
20
+ return store.run(key, fn);
21
+ }
22
+ /** The request's key, or the environment's when running over stdio. */
23
+ function currentApiKey() {
24
+ return store.getStore() ?? process.env.SENDRAVEN_API_KEY ?? process.env.EMAILS_API_KEY;
25
+ }
26
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":";;AAgBA,gCAEC;AAGD,sCAEC;AAvBD,uDAAqD;AAErD;;;;;;;;;;;GAWG;AACH,MAAM,KAAK,GAAG,IAAI,oCAAiB,EAAU,CAAC;AAE9C,SAAgB,UAAU,CAAI,GAAW,EAAE,EAAoB;IAC7D,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AAC5B,CAAC;AAED,uEAAuE;AACvE,SAAgB,aAAa;IAC3B,OAAO,KAAK,CAAC,QAAQ,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;AACzF,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
8
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
9
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
10
+ const express_1 = __importDefault(require("express"));
11
+ const context_1 = require("./context");
12
+ const client_1 = require("./client");
13
+ const tools_1 = require("./tools");
14
+ /**
15
+ * A server instance for one caller.
16
+ *
17
+ * Built per request rather than once at startup: over HTTP the tools act on
18
+ * whichever workspace the request's key belongs to, so a shared instance would
19
+ * be a shared identity.
20
+ */
21
+ function buildServer() {
22
+ const server = new mcp_js_1.McpServer({
23
+ name: "sendraven",
24
+ title: "SendRaven",
25
+ version: "0.1.0",
26
+ });
27
+ for (const tool of tools_1.TOOLS) {
28
+ server.tool(tool.name, tool.description,
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ tool.schema, async (args) => {
31
+ try {
32
+ const result = await tool.handler(args);
33
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
34
+ }
35
+ catch (e) {
36
+ // Surface the API's own message rather than a stack trace — an agent
37
+ // can usually act on "domain not verified" but not on a bare 403.
38
+ const message = e instanceof client_1.ApiError ? `${e.message} (HTTP ${e.status})` : e.message;
39
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
40
+ }
41
+ });
42
+ }
43
+ return server;
44
+ }
45
+ /* ---------------------------------------------------------------- stdio --- */
46
+ async function runStdio() {
47
+ const server = buildServer();
48
+ await server.connect(new stdio_js_1.StdioServerTransport());
49
+ }
50
+ /* ----------------------------------------------------------------- http --- */
51
+ const MCP_SERVER_URL = process.env.MCP_SERVER_URL || "https://mcp.sendraven.ai";
52
+ const APP_URL = process.env.APP_URL || "https://sendraven.ai";
53
+ const PUBLIC_API_URL = process.env.SENDRAVEN_API_URL || process.env.PUBLIC_API_URL || "https://api.sendraven.ai";
54
+ /** Kept in step with api-server/src/oauth/scopes.ts. */
55
+ const OAUTH_SCOPES = [
56
+ "emails:send",
57
+ "emails:read",
58
+ "domains:read",
59
+ "domains:write",
60
+ "contacts:read",
61
+ "contacts:write",
62
+ "broadcasts:read",
63
+ "broadcasts:write",
64
+ "webhooks:read",
65
+ "webhooks:write",
66
+ "threads:read",
67
+ "templates:read",
68
+ "templates:write",
69
+ ];
70
+ /**
71
+ * Does this request need a key?
72
+ *
73
+ * Only `tools/call` does. `initialize` and `tools/list` are answered
74
+ * anonymously on purpose: MCP registries and client directories probe with an
75
+ * unauthenticated `tools/list`, and a blanket 401 makes a server look like it
76
+ * exposes nothing at all. Every tool here still reaches the REST API, which
77
+ * rejects an absent or invalid key on its own — the check below only decides
78
+ * when to send the challenge.
79
+ */
80
+ function requiresAuth(body) {
81
+ const messages = Array.isArray(body) ? body : [body];
82
+ return messages.some((m) => m?.method === "tools/call");
83
+ }
84
+ async function handleMcp(req, res, token) {
85
+ const server = buildServer();
86
+ // Stateless: one transport per request, no session to resume.
87
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
88
+ res.on("close", () => {
89
+ void transport.close();
90
+ void server.close();
91
+ });
92
+ await server.connect(transport);
93
+ await (0, context_1.withApiKey)(token, async () => {
94
+ await transport.handleRequest(req, res, req.body);
95
+ });
96
+ }
97
+ function runHttp() {
98
+ const port = Number(process.env.PORT || 5100);
99
+ const app = (0, express_1.default)();
100
+ app.use(express_1.default.json({ limit: "5mb" }));
101
+ // RFC 9728. After a 401 an MCP client reads this to find out where to send
102
+ // the user to authorize.
103
+ app.get("/.well-known/oauth-protected-resource", (_req, res) => {
104
+ res.json({
105
+ resource: MCP_SERVER_URL,
106
+ authorization_servers: [PUBLIC_API_URL],
107
+ scopes_supported: OAUTH_SCOPES,
108
+ bearer_methods_supported: ["header"],
109
+ resource_name: "SendRaven",
110
+ resource_documentation: `${APP_URL}/docs`,
111
+ });
112
+ });
113
+ // RFC 8414. Some clients probe the resource host for this before following
114
+ // authorization_servers, so it is answered here as well as on the API.
115
+ app.get("/.well-known/oauth-authorization-server", (_req, res) => {
116
+ res.json({
117
+ issuer: PUBLIC_API_URL,
118
+ authorization_endpoint: `${APP_URL}/oauth/authorize`,
119
+ token_endpoint: `${PUBLIC_API_URL}/oauth/token`,
120
+ registration_endpoint: `${PUBLIC_API_URL}/oauth/register`,
121
+ revocation_endpoint: `${PUBLIC_API_URL}/oauth/revoke`,
122
+ token_endpoint_auth_methods_supported: ["none", "client_secret_post"],
123
+ scopes_supported: OAUTH_SCOPES,
124
+ response_types_supported: ["code"],
125
+ grant_types_supported: ["authorization_code", "refresh_token"],
126
+ // S256 only. "plain" offers no protection against an intercepted code,
127
+ // which is the entire reason PKCE exists.
128
+ code_challenge_methods_supported: ["S256"],
129
+ });
130
+ });
131
+ app.post("/mcp", async (req, res) => {
132
+ const auth = req.headers.authorization || "";
133
+ const token = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
134
+ if (!token && requiresAuth(req.body)) {
135
+ // Pointing at the protected-resource document is what starts the OAuth
136
+ // flow in an MCP client: it reads this header, fetches the metadata, and
137
+ // sends the user to the consent screen. A plain API key also works for
138
+ // anyone who would rather paste one.
139
+ res
140
+ .status(401)
141
+ .set("WWW-Authenticate", `Bearer resource_metadata="${MCP_SERVER_URL}/.well-known/oauth-protected-resource"`)
142
+ .json({
143
+ error: "Unauthorized",
144
+ message: `Authorize this client, or send an API key as 'Authorization: Bearer sk_live_...' from ${APP_URL}/developers.`,
145
+ });
146
+ return;
147
+ }
148
+ await handleMcp(req, res, token);
149
+ });
150
+ // Streamable HTTP says a server with no SSE stream on GET, and no session to
151
+ // DELETE, answers 405 — not 404. Registry health checks probe with GET, and
152
+ // Express's default 404 reads as the server being down.
153
+ const methodNotAllowed = (_req, res) => res.status(405).json({
154
+ jsonrpc: "2.0",
155
+ error: { code: -32000, message: "Method not allowed." },
156
+ id: null,
157
+ });
158
+ app.get("/mcp", methodNotAllowed);
159
+ app.delete("/mcp", methodNotAllowed);
160
+ app.get("/healthz", (_req, res) => res.json({ ok: true }));
161
+ app.listen(port, () => {
162
+ console.error(`SendRaven MCP server listening on :${port} (${MCP_SERVER_URL})`);
163
+ });
164
+ }
165
+ /* ----------------------------------------------------------------- main --- */
166
+ // stdio when asked for explicitly or when there is no PORT to bind, which is
167
+ // what an MCP client launching this as a subprocess looks like.
168
+ const useStdio = process.argv.includes("--stdio") || process.env.MCP_TRANSPORT === "stdio" || !process.env.PORT;
169
+ if (useStdio) {
170
+ runStdio().catch((e) => {
171
+ console.error("SendRaven MCP server failed to start:", e);
172
+ process.exit(1);
173
+ });
174
+ }
175
+ else {
176
+ runHttp();
177
+ }
178
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decideApprovalTool = exports.listApprovalsTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.listApprovalsTool = {
7
+ name: "list_pending_approvals",
8
+ description: "List messages held for human approval, with their full content. A key configured to " +
9
+ "require approval drafts rather than sends; a person releases it. If your send returned " +
10
+ "status 'pending_approval', it is waiting here — do not retry the send.",
11
+ schema: {},
12
+ handler: async () => (0, client_1.request)("GET", "/v1/approvals"),
13
+ };
14
+ exports.decideApprovalTool = {
15
+ name: "decide_approval",
16
+ description: "Approve or reject a held message. Approving sends it immediately. Only use this when a " +
17
+ "human has explicitly told you which decision to make — the hold exists precisely so that " +
18
+ "an agent is not the one deciding.",
19
+ schema: {
20
+ id: zod_1.z.string(),
21
+ decision: zod_1.z.enum(["approve", "reject"]),
22
+ decided_by: zod_1.z.string().describe("Who authorised this decision"),
23
+ reason: zod_1.z.string().optional(),
24
+ },
25
+ handler: async (args) => {
26
+ const { id, ...body } = args;
27
+ return (0, client_1.request)("POST", `/v1/approvals/${id}`, body);
28
+ },
29
+ };
30
+ //# sourceMappingURL=approvals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approvals.js","sourceRoot":"","sources":["../../src/tools/approvals.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,sCAAoC;AAEvB,QAAA,iBAAiB,GAAG;IAC/B,IAAI,EAAE,wBAAwB;IAC9B,WAAW,EACT,sFAAsF;QACtF,yFAAyF;QACzF,wEAAwE;IAC1E,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,eAAe,CAAC;CACrD,CAAC;AAEW,QAAA,kBAAkB,GAAG;IAChC,IAAI,EAAE,iBAAiB;IACvB,WAAW,EACT,yFAAyF;QACzF,2FAA2F;QAC3F,mCAAmC;IACrC,MAAM,EAAE;QACN,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE;QACd,QAAQ,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QACvC,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,8BAA8B,CAAC;QAC/D,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC9B;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE;QAC/C,MAAM,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;QAC7B,OAAO,IAAA,gBAAO,EAAC,MAAM,EAAE,iBAAiB,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IACtD,CAAC;CACF,CAAC"}
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listTagsTool = exports.tagContactTool = exports.deleteContactTool = exports.removeFromAudienceTool = exports.findContactTool = exports.countSegmentTool = exports.listSegmentsTool = exports.updateContactTool = exports.getContactTool = exports.addContactTool = exports.listAudiencesTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.listAudiencesTool = {
7
+ name: "list_audiences",
8
+ description: "List contact lists and how many contacts each holds.",
9
+ schema: {},
10
+ handler: async () => (0, client_1.request)("GET", "/v1/audiences"),
11
+ };
12
+ exports.addContactTool = {
13
+ name: "add_contact",
14
+ description: "Add someone to an audience. A contact exists once per workspace and can be on any number " +
15
+ "of audiences, so adding an address that already exists joins them to this list rather than " +
16
+ "creating a second copy. Safe to retry.",
17
+ schema: {
18
+ audience_id: zod_1.z.string(),
19
+ email: zod_1.z.string().email(),
20
+ first_name: zod_1.z.string().optional(),
21
+ last_name: zod_1.z.string().optional(),
22
+ },
23
+ handler: async (args) => {
24
+ const { audience_id, ...body } = args;
25
+ return (0, client_1.request)("POST", `/v1/audiences/${audience_id}/contacts`, body);
26
+ },
27
+ };
28
+ exports.getContactTool = {
29
+ name: "get_contact",
30
+ description: "Fetch one contact by id, with their audience memberships, custom properties and " +
31
+ "engagement dates.",
32
+ schema: { id: zod_1.z.string() },
33
+ handler: async (args) => (0, client_1.request)("GET", `/v1/contacts/${args.id}`),
34
+ };
35
+ exports.updateContactTool = {
36
+ name: "update_contact",
37
+ description: "Update a contact. Attributes are merged, so sending one field does not clear the rest.",
38
+ schema: {
39
+ id: zod_1.z.string(),
40
+ first_name: zod_1.z.string().optional(),
41
+ last_name: zod_1.z.string().optional(),
42
+ attributes: zod_1.z.record(zod_1.z.union([zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean()])).optional(),
43
+ },
44
+ handler: async (args) => {
45
+ const { id, ...body } = args;
46
+ return (0, client_1.request)("PATCH", `/v1/contacts/${id}`, body);
47
+ },
48
+ };
49
+ exports.listSegmentsTool = {
50
+ name: "list_segments",
51
+ description: "List saved audience filters. Use a segment id when creating a campaign rather than " +
52
+ "describing the filter inline, so the same definition can be reused and counted.",
53
+ schema: {},
54
+ handler: async () => (0, client_1.request)("GET", "/v1/segments"),
55
+ };
56
+ exports.countSegmentTool = {
57
+ name: "count_segment",
58
+ description: "How many contacts a segment currently matches. Run this before building a campaign " +
59
+ "around it — a filter that matches nobody is easier to spot here than after a send.",
60
+ schema: { id: zod_1.z.string() },
61
+ handler: async (args) => (0, client_1.request)("GET", `/v1/segments/${args.id}/metrics`),
62
+ };
63
+ exports.findContactTool = {
64
+ name: "find_contact",
65
+ description: "Find a contact by address across every audience, without knowing which list they are on. " +
66
+ "Use email for an exact match, or q for a prefix. Returns each audience membership " +
67
+ "separately — the same address on three lists is three rows — and whether each is " +
68
+ "unsubscribed.",
69
+ schema: {
70
+ email: zod_1.z.string().optional().describe("Exact address"),
71
+ q: zod_1.z.string().optional().describe("Address prefix, for a partial match"),
72
+ unsubscribed: zod_1.z.boolean().optional().describe("Filter to only opted-in or only opted-out"),
73
+ limit: zod_1.z.number().optional(),
74
+ cursor: zod_1.z.string().optional().describe("An address, from a previous call's next_cursor"),
75
+ },
76
+ handler: async (args) => {
77
+ const q = new URLSearchParams();
78
+ for (const k of ["email", "q", "limit", "cursor"]) {
79
+ if (args[k] !== undefined)
80
+ q.set(k, String(args[k]));
81
+ }
82
+ if (args.unsubscribed !== undefined)
83
+ q.set("unsubscribed", String(args.unsubscribed));
84
+ return (0, client_1.request)("GET", `/v1/contacts?${q.toString()}`);
85
+ },
86
+ };
87
+ exports.removeFromAudienceTool = {
88
+ name: "remove_from_audience",
89
+ description: "Take a contact off one audience. They stay in the workspace and keep every other audience, " +
90
+ "their suppression and their engagement history. To remove the person entirely use " +
91
+ "delete_contact — leaving a list and being forgotten are different things.",
92
+ schema: {
93
+ audience_id: zod_1.z.string(),
94
+ contact_id: zod_1.z.string(),
95
+ },
96
+ handler: async (args) => (0, client_1.request)("DELETE", `/v1/audiences/${args.audience_id}/contacts/${args.contact_id}`),
97
+ };
98
+ exports.deleteContactTool = {
99
+ name: "delete_contact",
100
+ description: "Remove a person from the workspace entirely, along with every audience membership. Their " +
101
+ "suppression and topic preferences are kept on purpose — an opt-out has to outlive the " +
102
+ "contact record, or the next import silently puts them back on the list. To take someone " +
103
+ "off a single audience use remove_from_audience instead.",
104
+ schema: { contact_id: zod_1.z.string() },
105
+ handler: async (args) => (0, client_1.request)("DELETE", `/v1/contacts/${args.contact_id}`),
106
+ };
107
+ exports.tagContactTool = {
108
+ name: "tag_contact",
109
+ description: "Add or remove tags on a contact. Tags are flat labels — vip, beta, churned — as opposed to " +
110
+ "custom properties, which are declared fields with a value. They are lower-cased and spaces " +
111
+ "become hyphens, so VIP and vip are the same tag. Tagging reaches the person across every " +
112
+ "audience they are on. Call list_tags first to see what the workspace already uses, rather " +
113
+ "than inventing a synonym for an existing tag.",
114
+ schema: {
115
+ contact_id: zod_1.z.string(),
116
+ add: zod_1.z.array(zod_1.z.string()).optional(),
117
+ remove: zod_1.z.array(zod_1.z.string()).optional(),
118
+ },
119
+ handler: async (args) => {
120
+ const { contact_id, ...body } = args;
121
+ return (0, client_1.request)("POST", `/v1/contacts/${contact_id}/tags`, body);
122
+ },
123
+ };
124
+ exports.listTagsTool = {
125
+ name: "list_tags",
126
+ description: "Every tag in use in the workspace, with how many contacts carry each. Tags are free-form, " +
127
+ "so this is the only way to know what exists before applying one.",
128
+ schema: {},
129
+ handler: async () => (0, client_1.request)("GET", "/v1/contacts/tags"),
130
+ };
131
+ //# sourceMappingURL=audiences.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audiences.js","sourceRoot":"","sources":["../../src/tools/audiences.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,sCAAoC;AAEvB,QAAA,iBAAiB,GAAG;IAC/B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,sDAAsD;IACnE,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,eAAe,CAAC;CACrD,CAAC;AAEW,QAAA,cAAc,GAAG;IAC5B,IAAI,EAAE,aAAa;IACnB,WAAW,EACT,2FAA2F;QAC3F,6FAA6F;QAC7F,wCAAwC;IAC1C,MAAM,EAAE;QACN,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE;QACvB,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACzB,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACjC,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KACjC;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE;QAC/C,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;QACtC,OAAO,IAAA,gBAAO,EAAC,MAAM,EAAE,iBAAiB,WAAW,WAAW,EAAE,IAAI,CAAC,CAAC;IACxE,CAAC;CACF,CAAC;AAEW,QAAA,cAAc,GAAG;IAC5B,IAAI,EAAE,aAAa;IACnB,WAAW,EACT,kFAAkF;QAClF,mBAAmB;IACrB,MAAM,EAAE,EAAE,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,EAAE;IAC1B,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,gBAAgB,IAAI,CAAC,EAAE,EAAE,CAAC;CAC5F,CAAC;AAEW,QAAA,iBAAiB,GAAG;IAC/B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EACT,wFAAwF;IAC1F,MAAM,EAAE;QACN,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE;QACd,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACjC,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAChC,UAAU,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,KAAK,CAAC,CAAC,OAAC,CAAC,MAAM,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;KAChF;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE;QAC/C,MAAM,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;QAC7B,OAAO,IAAA,gBAAO,EAAC,OAAO,EAAE,gBAAgB,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IACtD,CAAC;CACF,CAAC;AAEW,QAAA,gBAAgB,GAAG;IAC9B,IAAI,EAAE,eAAe;IACrB,WAAW,EACT,qFAAqF;QACrF,iFAAiF;IACnF,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,cAAc,CAAC;CACpD,CAAC;AAEW,QAAA,gBAAgB,GAAG;IAC9B,IAAI,EAAE,eAAe;IACrB,WAAW,EACT,qFAAqF;QACrF,oFAAoF;IACtF,MAAM,EAAE,EAAE,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,EAAE;IAC1B,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,gBAAgB,IAAI,CAAC,EAAE,UAAU,CAAC;CACpG,CAAC;AAEW,QAAA,eAAe,GAAG;IAC7B,IAAI,EAAE,cAAc;IACpB,WAAW,EACT,2FAA2F;QAC3F,oFAAoF;QACpF,mFAAmF;QACnF,eAAe;IACjB,MAAM,EAAE;QACN,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC;QACtD,CAAC,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;QACxE,YAAY,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2CAA2C,CAAC;QAC1F,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC5B,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gDAAgD,CAAC;KACzF;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE;QAC/C,MAAM,CAAC,GAAG,IAAI,eAAe,EAAE,CAAC;QAChC,KAAK,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC;YAClD,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;gBAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACvD,CAAC;QACD,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,CAAC,CAAC,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;QACtF,OAAO,IAAA,gBAAO,EAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACxD,CAAC;CACF,CAAC;AAEW,QAAA,sBAAsB,GAAG;IACpC,IAAI,EAAE,sBAAsB;IAC5B,WAAW,EACT,6FAA6F;QAC7F,oFAAoF;QACpF,2EAA2E;IAC7E,MAAM,EAAE;QACN,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE;QACvB,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE;KACvB;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAC/C,IAAA,gBAAO,EAAC,QAAQ,EAAE,iBAAiB,IAAI,CAAC,WAAW,aAAa,IAAI,CAAC,UAAU,EAAE,CAAC;CACrF,CAAC;AAEW,QAAA,iBAAiB,GAAG;IAC/B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EACT,2FAA2F;QAC3F,wFAAwF;QACxF,0FAA0F;QAC1F,yDAAyD;IAC3D,MAAM,EAAE,EAAE,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,EAAE;IAClC,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAC/C,IAAA,gBAAO,EAAC,QAAQ,EAAE,gBAAgB,IAAI,CAAC,UAAU,EAAE,CAAC;CACvD,CAAC;AAEW,QAAA,cAAc,GAAG;IAC5B,IAAI,EAAE,aAAa;IACnB,WAAW,EACT,6FAA6F;QAC7F,6FAA6F;QAC7F,2FAA2F;QAC3F,4FAA4F;QAC5F,+CAA+C;IACjD,MAAM,EAAE;QACN,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE;QACtB,GAAG,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;QACnC,MAAM,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;KACvC;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE;QAC/C,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;QACrC,OAAO,IAAA,gBAAO,EAAC,MAAM,EAAE,gBAAgB,UAAU,OAAO,EAAE,IAAI,CAAC,CAAC;IAClE,CAAC;CACF,CAAC;AAEW,QAAA,YAAY,GAAG;IAC1B,IAAI,EAAE,WAAW;IACjB,WAAW,EACT,4FAA4F;QAC5F,kEAAkE;IACpE,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,mBAAmB,CAAC;CACzD,CAAC"}
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.emitEventTool = exports.enrollTool = exports.listAutomationsTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.listAutomationsTool = {
7
+ name: "list_automations",
8
+ description: "List multi-step email sequences and how many people are currently in each. Use this " +
9
+ "to find the right automation before enrolling someone.",
10
+ schema: {},
11
+ handler: async () => (0, client_1.request)("GET", "/v1/automations"),
12
+ };
13
+ exports.enrollTool = {
14
+ name: "enroll_in_automation",
15
+ description: "Put someone into a multi-step sequence. Prefer this over scheduling several emails " +
16
+ "yourself: the sequence stops on its own if they unsubscribe, reply, or hard bounce, " +
17
+ "which you would otherwise have to track and cancel by hand. Enrolling the same person " +
18
+ "twice is a no-op, so it is safe to retry a call you are unsure about.",
19
+ schema: {
20
+ automation_id: zod_1.z.string(),
21
+ email: zod_1.z.string().email(),
22
+ variables: zod_1.z
23
+ .record(zod_1.z.string())
24
+ .optional()
25
+ .describe("Values for any templated steps in the sequence"),
26
+ },
27
+ handler: async (args) => {
28
+ const { automation_id, ...body } = args;
29
+ return (0, client_1.request)("POST", `/v1/automations/${automation_id}/enroll`, body);
30
+ },
31
+ };
32
+ exports.emitEventTool = {
33
+ name: "emit_event",
34
+ description: "Emit a named event, starting every automation that waits on it — for example " +
35
+ "'trial_started' or 'invoice_overdue'. Use this when you want the configured sequences " +
36
+ "to decide what happens, rather than naming an automation yourself.",
37
+ schema: {
38
+ name: zod_1.z.string().describe("Event name, e.g. trial_started"),
39
+ email: zod_1.z.string().email(),
40
+ variables: zod_1.z.record(zod_1.z.string()).optional(),
41
+ },
42
+ handler: async (args) => (0, client_1.request)("POST", "/v1/events", args),
43
+ };
44
+ //# sourceMappingURL=automations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"automations.js","sourceRoot":"","sources":["../../src/tools/automations.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,sCAAoC;AAEvB,QAAA,mBAAmB,GAAG;IACjC,IAAI,EAAE,kBAAkB;IACxB,WAAW,EACT,sFAAsF;QACtF,wDAAwD;IAC1D,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,iBAAiB,CAAC;CACvD,CAAC;AAEW,QAAA,UAAU,GAAG;IACxB,IAAI,EAAE,sBAAsB;IAC5B,WAAW,EACT,qFAAqF;QACrF,sFAAsF;QACtF,wFAAwF;QACxF,uEAAuE;IACzE,MAAM,EAAE;QACN,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE;QACzB,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACzB,SAAS,EAAE,OAAC;aACT,MAAM,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC;aAClB,QAAQ,EAAE;aACV,QAAQ,CAAC,gDAAgD,CAAC;KAC9D;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE;QAC/C,MAAM,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;QACxC,OAAO,IAAA,gBAAO,EAAC,MAAM,EAAE,mBAAmB,aAAa,SAAS,EAAE,IAAI,CAAC,CAAC;IAC1E,CAAC;CACF,CAAC;AAEW,QAAA,aAAa,GAAG;IAC3B,IAAI,EAAE,YAAY;IAClB,WAAW,EACT,+EAA+E;QAC/E,wFAAwF;QACxF,oEAAoE;IACtE,MAAM,EAAE;QACN,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,gCAAgC,CAAC;QAC3D,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACzB,SAAS,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;KAC3C;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC;CACtF,CAAC"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendBroadcastTool = exports.previewBroadcastTool = exports.listBroadcastsTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.listBroadcastsTool = {
7
+ name: "list_broadcasts",
8
+ description: "List campaigns with their status and send progress.",
9
+ schema: {},
10
+ handler: async () => (0, client_1.request)("GET", "/v1/broadcasts"),
11
+ };
12
+ exports.previewBroadcastTool = {
13
+ name: "preview_broadcast",
14
+ description: "How many contacts a campaign would reach, and whether the reputation gate would allow it. " +
15
+ "Always run this before sending — it is the only way to see the size of a campaign without " +
16
+ "starting it.",
17
+ schema: { id: zod_1.z.string() },
18
+ handler: async (args) => (0, client_1.request)("GET", `/v1/broadcasts/${args.id}/preview`),
19
+ };
20
+ exports.sendBroadcastTool = {
21
+ name: "send_broadcast",
22
+ description: "Send a campaign now, or schedule it with scheduled_at. This mails every contact in the " +
23
+ "segment and cannot be undone once started — run preview_broadcast first.",
24
+ schema: {
25
+ id: zod_1.z.string(),
26
+ scheduled_at: zod_1.z.string().optional().describe("ISO 8601 timestamp; omit to send now"),
27
+ },
28
+ handler: async (args) => {
29
+ const { id, ...body } = args;
30
+ return (0, client_1.request)("POST", `/v1/broadcasts/${id}/send`, body);
31
+ },
32
+ };
33
+ //# sourceMappingURL=broadcasts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broadcasts.js","sourceRoot":"","sources":["../../src/tools/broadcasts.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,sCAAoC;AAEvB,QAAA,kBAAkB,GAAG;IAChC,IAAI,EAAE,iBAAiB;IACvB,WAAW,EAAE,qDAAqD;IAClE,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,gBAAgB,CAAC;CACtD,CAAC;AAEW,QAAA,oBAAoB,GAAG;IAClC,IAAI,EAAE,mBAAmB;IACzB,WAAW,EACT,4FAA4F;QAC5F,4FAA4F;QAC5F,cAAc;IAChB,MAAM,EAAE,EAAE,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,EAAE;IAC1B,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAC/C,IAAA,gBAAO,EAAC,KAAK,EAAE,kBAAkB,IAAI,CAAC,EAAE,UAAU,CAAC;CACtD,CAAC;AAEW,QAAA,iBAAiB,GAAG;IAC/B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EACT,yFAAyF;QACzF,0EAA0E;IAC5E,MAAM,EAAE;QACN,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE;QACd,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sCAAsC,CAAC;KACrF;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE;QAC/C,MAAM,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;QAC7B,OAAO,IAAA,gBAAO,EAAC,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC5D,CAAC;CACF,CAAC"}
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyDomainTool = exports.addDomainTool = exports.listDomainsTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.listDomainsTool = {
7
+ name: "list_sending_domains",
8
+ description: "List sending domains with their verification status and the DNS records each one needs. " +
9
+ "Each record shows what is currently published, so this diagnoses a stuck verification.",
10
+ schema: {},
11
+ handler: async () => (0, client_1.request)("GET", "/v1/domains"),
12
+ };
13
+ exports.addDomainTool = {
14
+ name: "add_sending_domain",
15
+ description: "Register a sending domain and get back the DNS records to publish. Give the domain you " +
16
+ "send from — mail.<domain> and news.<domain> are provisioned beneath it and the right one " +
17
+ "is chosen per message, so a marketing complaint spike can never affect password reset " +
18
+ "delivery. Pass risk_class only to provision one of the two on its own.",
19
+ schema: {
20
+ domain: zod_1.z.string().describe("The domain you send from, e.g. example.com"),
21
+ risk_class: zod_1.z
22
+ .enum(["transactional", "marketing"])
23
+ .optional()
24
+ .describe("Omit to provision both, which is almost always what you want"),
25
+ },
26
+ handler: async (args) => (0, client_1.request)("POST", "/v1/domains", args),
27
+ };
28
+ exports.verifyDomainTool = {
29
+ name: "verify_sending_domain",
30
+ description: "Re-check a domain's DNS records now instead of waiting for the background monitor.",
31
+ schema: { id: zod_1.z.string() },
32
+ handler: async (args) => (0, client_1.request)("POST", `/v1/domains/${args.id}/verify`),
33
+ };
34
+ //# sourceMappingURL=domains.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domains.js","sourceRoot":"","sources":["../../src/tools/domains.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,sCAAoC;AAEvB,QAAA,eAAe,GAAG;IAC7B,IAAI,EAAE,sBAAsB;IAC5B,WAAW,EACT,0FAA0F;QAC1F,wFAAwF;IAC1F,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,aAAa,CAAC;CACnD,CAAC;AAEW,QAAA,aAAa,GAAG;IAC3B,IAAI,EAAE,oBAAoB;IAC1B,WAAW,EACT,yFAAyF;QACzF,2FAA2F;QAC3F,wFAAwF;QACxF,wEAAwE;IAC1E,MAAM,EAAE;QACN,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,4CAA4C,CAAC;QACzE,UAAU,EAAE,OAAC;aACV,IAAI,CAAC,CAAC,eAAe,EAAE,WAAW,CAAC,CAAC;aACpC,QAAQ,EAAE;aACV,QAAQ,CAAC,8DAA8D,CAAC;KAC5E;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,MAAM,EAAE,aAAa,EAAE,IAAI,CAAC;CACvF,CAAC;AAEW,QAAA,gBAAgB,GAAG;IAC9B,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EAAE,oFAAoF;IACjG,MAAM,EAAE,EAAE,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,EAAE;IAC1B,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAC/C,IAAA,gBAAO,EAAC,MAAM,EAAE,eAAe,IAAI,CAAC,EAAE,SAAS,CAAC;CACnD,CAAC"}
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cancelEmailTool = exports.getEmailTool = exports.listEmailsTool = exports.sendEmailTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.sendEmailTool = {
7
+ name: "send_email",
8
+ description: "Send a transactional email, immediately or scheduled. Use scheduled_at with a relative " +
9
+ "phrase like 'in 3 days' or an ISO timestamp. The From domain must already be verified.",
10
+ schema: {
11
+ from: zod_1.z.string().describe("Sender address on a verified domain, e.g. 'Team <team@mail.example.com>'"),
12
+ to: zod_1.z.string().email().describe("Recipient address"),
13
+ subject: zod_1.z.string(),
14
+ html: zod_1.z.string().optional().describe("HTML body; provide html, text, or both"),
15
+ text: zod_1.z.string().optional(),
16
+ scheduled_at: zod_1.z.string().optional().describe("'in 3 days' or an ISO 8601 timestamp"),
17
+ },
18
+ handler: async (args) => (0, client_1.request)("POST", "/v1/emails", args),
19
+ };
20
+ exports.listEmailsTool = {
21
+ name: "list_emails",
22
+ description: "List recent messages with their delivery status. Filter by status " +
23
+ "(queued, scheduled, sent, delivered, bounced, complained, rejected) or recipient.",
24
+ schema: {
25
+ status: zod_1.z.string().optional(),
26
+ to: zod_1.z.string().optional().describe("Filter to one recipient address"),
27
+ limit: zod_1.z.number().int().min(1).max(200).optional(),
28
+ },
29
+ handler: async (args) => {
30
+ const qs = new URLSearchParams(Object.entries(args)
31
+ .filter(([, v]) => v !== undefined)
32
+ .map(([k, v]) => [k, String(v)]));
33
+ return (0, client_1.request)("GET", `/v1/emails?${qs}`);
34
+ },
35
+ };
36
+ exports.getEmailTool = {
37
+ name: "get_email",
38
+ description: "Fetch one message with its full event timeline (send, delivery, bounce, complaint, " +
39
+ "open, click). This is the tool to reach for when asked why an email didn't arrive.",
40
+ schema: { id: zod_1.z.string().describe("Message id") },
41
+ handler: async (args) => (0, client_1.request)("GET", `/v1/emails/${args.id}`),
42
+ };
43
+ exports.cancelEmailTool = {
44
+ name: "cancel_scheduled_email",
45
+ description: "Cancel a scheduled email before it sends. Only works while status is 'scheduled'.",
46
+ schema: { id: zod_1.z.string() },
47
+ handler: async (args) => (0, client_1.request)("DELETE", `/v1/emails/${args.id}`),
48
+ };
49
+ //# sourceMappingURL=emails.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emails.js","sourceRoot":"","sources":["../../src/tools/emails.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,sCAAoC;AAEvB,QAAA,aAAa,GAAG;IAC3B,IAAI,EAAE,YAAY;IAClB,WAAW,EACT,yFAAyF;QACzF,wFAAwF;IAC1F,MAAM,EAAE;QACN,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,0EAA0E,CAAC;QACrG,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC;QACpD,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE;QACnB,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wCAAwC,CAAC;QAC9E,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC3B,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sCAAsC,CAAC;KACrF;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAC/C,IAAA,gBAAO,EAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC;CACtC,CAAC;AAEW,QAAA,cAAc,GAAG;IAC5B,IAAI,EAAE,aAAa;IACnB,WAAW,EACT,oEAAoE;QACpE,mFAAmF;IACrF,MAAM,EAAE;QACN,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC7B,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iCAAiC,CAAC;QACrE,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;KACnD;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE;QAC/C,MAAM,EAAE,GAAG,IAAI,eAAe,CAC5B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;aACjB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC;aAClC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAqB,CAAC,CACvD,CAAC;QACF,OAAO,IAAA,gBAAO,EAAC,KAAK,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC;IAC5C,CAAC;CACF,CAAC;AAEW,QAAA,YAAY,GAAG;IAC1B,IAAI,EAAE,WAAW;IACjB,WAAW,EACT,qFAAqF;QACrF,oFAAoF;IACtF,MAAM,EAAE,EAAE,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE;IACjD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,cAAc,IAAI,CAAC,EAAE,EAAE,CAAC;CAC1F,CAAC;AAEW,QAAA,eAAe,GAAG;IAC7B,IAAI,EAAE,wBAAwB;IAC9B,WAAW,EAAE,mFAAmF;IAChG,MAAM,EAAE,EAAE,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,EAAE;IAC1B,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,QAAQ,EAAE,cAAc,IAAI,CAAC,EAAE,EAAE,CAAC;CAC7F,CAAC"}
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TOOLS = void 0;
4
+ /**
5
+ * The registry: every tool the server exposes, in the order they are listed
6
+ * to clients.
7
+ *
8
+ * Kept in its own module so it can be read without starting a server —
9
+ * `scripts/export-tools.ts` writes the names and descriptions to
10
+ * `web/lib/content/mcp-tools.json`, which is what the marketing site renders.
11
+ * The site used to state the count by hand and was wrong within a month.
12
+ */
13
+ const emails_1 = require("./emails");
14
+ const domains_1 = require("./domains");
15
+ const suppressions_1 = require("./suppressions");
16
+ const broadcasts_1 = require("./broadcasts");
17
+ const threads_1 = require("./threads");
18
+ const templates_1 = require("./templates");
19
+ const approvals_1 = require("./approvals");
20
+ const automations_1 = require("./automations");
21
+ const topics_1 = require("./topics");
22
+ const audiences_1 = require("./audiences");
23
+ const operations_1 = require("./operations");
24
+ exports.TOOLS = [
25
+ emails_1.sendEmailTool,
26
+ emails_1.listEmailsTool,
27
+ emails_1.getEmailTool,
28
+ emails_1.cancelEmailTool,
29
+ domains_1.listDomainsTool,
30
+ domains_1.addDomainTool,
31
+ domains_1.verifyDomainTool,
32
+ suppressions_1.listSuppressionsTool,
33
+ suppressions_1.addSuppressionTool,
34
+ suppressions_1.removeSuppressionTool,
35
+ broadcasts_1.listBroadcastsTool,
36
+ broadcasts_1.previewBroadcastTool,
37
+ broadcasts_1.sendBroadcastTool,
38
+ threads_1.listThreadsTool,
39
+ threads_1.getThreadTool,
40
+ threads_1.replyToMessageTool,
41
+ templates_1.listTemplatesTool,
42
+ templates_1.renderTemplateTool,
43
+ templates_1.sendTemplateTool,
44
+ approvals_1.listApprovalsTool,
45
+ approvals_1.decideApprovalTool,
46
+ automations_1.listAutomationsTool,
47
+ automations_1.enrollTool,
48
+ automations_1.emitEventTool,
49
+ topics_1.listTopicsTool,
50
+ topics_1.getPreferencesTool,
51
+ topics_1.setPreferencesTool,
52
+ audiences_1.listAudiencesTool,
53
+ audiences_1.addContactTool,
54
+ audiences_1.getContactTool,
55
+ audiences_1.updateContactTool,
56
+ audiences_1.listSegmentsTool,
57
+ audiences_1.listTagsTool,
58
+ audiences_1.countSegmentTool,
59
+ operations_1.metricsTool,
60
+ operations_1.listScheduledTool,
61
+ operations_1.batchSuppressTool,
62
+ operations_1.listWebhookEventsTool,
63
+ operations_1.broadcastRecipientsTool,
64
+ operations_1.listApiKeysTool,
65
+ operations_1.getUsageTool,
66
+ audiences_1.findContactTool,
67
+ audiences_1.removeFromAudienceTool,
68
+ audiences_1.deleteContactTool,
69
+ audiences_1.tagContactTool,
70
+ ];
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,qCAKkB;AAClB,uCAA6E;AAC7E,iDAIwB;AACxB,6CAIsB;AACtB,uCAA+E;AAC/E,2CAAsF;AACtF,2CAAoE;AACpE,+CAA+E;AAC/E,qCAAkF;AAClF,2CAYqB;AACrB,6CAQsB;AAeT,QAAA,KAAK,GAAc;IAC9B,sBAAa;IACb,uBAAc;IACd,qBAAY;IACZ,wBAAe;IACf,yBAAe;IACf,uBAAa;IACb,0BAAgB;IAChB,mCAAoB;IACpB,iCAAkB;IAClB,oCAAqB;IACrB,+BAAkB;IAClB,iCAAoB;IACpB,8BAAiB;IACjB,yBAAe;IACf,uBAAa;IACb,4BAAkB;IAClB,6BAAiB;IACjB,8BAAkB;IAClB,4BAAgB;IAChB,6BAAiB;IACjB,8BAAkB;IAClB,iCAAmB;IACnB,wBAAU;IACV,2BAAa;IACb,uBAAc;IACd,2BAAkB;IAClB,2BAAkB;IAClB,6BAAiB;IACjB,0BAAc;IACd,0BAAc;IACd,6BAAiB;IACjB,4BAAgB;IAChB,wBAAY;IACZ,4BAAgB;IAChB,wBAAW;IACX,8BAAiB;IACjB,8BAAiB;IACjB,kCAAqB;IACrB,oCAAuB;IACvB,4BAAe;IACf,yBAAY;IACZ,2BAAe;IACf,kCAAsB;IACtB,6BAAiB;IACjB,0BAAc;CACf,CAAC"}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getUsageTool = exports.listApiKeysTool = exports.broadcastRecipientsTool = exports.listWebhookEventsTool = exports.batchSuppressTool = exports.listScheduledTool = exports.metricsTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.metricsTool = {
7
+ name: "get_email_metrics",
8
+ description: "Delivery and engagement over a window. Open and click rates are over delivered, not " +
9
+ "sent — a low open rate with a high bounce rate is a deliverability problem, not an " +
10
+ "engagement one.",
11
+ schema: { days: zod_1.z.number().int().min(1).max(365).optional().describe("Defaults to 30") },
12
+ handler: async (args) => (0, client_1.request)("GET", `/v1/emails/metrics?days=${args.days ?? 30}`),
13
+ };
14
+ exports.listScheduledTool = {
15
+ name: "list_scheduled_emails",
16
+ description: "Messages queued to send later but not yet sent. Cancel one with cancel_scheduled_email.",
17
+ schema: { limit: zod_1.z.number().int().min(1).max(200).optional() },
18
+ handler: async (args) => (0, client_1.request)("GET", `/v1/emails?status=scheduled&limit=${args.limit ?? 50}`),
19
+ };
20
+ exports.batchSuppressTool = {
21
+ name: "suppress_many",
22
+ description: "Stop sending to many addresses at once — the path for importing another provider's " +
23
+ "unsubscribe list before a first campaign. Without it, everyone who already opted out " +
24
+ "there gets mailed again here.",
25
+ schema: {
26
+ emails: zod_1.z.array(zod_1.z.string().email()).max(10000),
27
+ scope: zod_1.z.enum(["all", "transactional", "marketing"]).optional(),
28
+ reason: zod_1.z.enum(["manual", "list_hygiene", "unsubscribe"]).optional(),
29
+ },
30
+ handler: async (args) => (0, client_1.request)("POST", "/v1/suppressions/batch", args),
31
+ };
32
+ exports.listWebhookEventsTool = {
33
+ name: "list_webhook_deliveries",
34
+ description: "Recent delivery attempts for a webhook endpoint, with status codes and errors. This " +
35
+ "is how to tell 'we never sent it' from 'your endpoint returned 500'.",
36
+ schema: { endpoint_id: zod_1.z.string(), limit: zod_1.z.number().int().min(1).max(200).optional() },
37
+ handler: async (args) => (0, client_1.request)("GET", `/v1/webhook-endpoints/${args.endpoint_id}/events?limit=${args.limit ?? 50}`),
38
+ };
39
+ exports.broadcastRecipientsTool = {
40
+ name: "list_broadcast_recipients",
41
+ description: "Who a campaign reached and what happened to each message.",
42
+ schema: { id: zod_1.z.string(), limit: zod_1.z.number().int().min(1).max(1000).optional() },
43
+ handler: async (args) => (0, client_1.request)("GET", `/v1/broadcasts/${args.id}/recipients?limit=${args.limit ?? 200}`),
44
+ };
45
+ exports.listApiKeysTool = {
46
+ name: "list_api_keys",
47
+ description: "List this workspace's API keys with their scopes and limits. Never returns key values.",
48
+ schema: {},
49
+ handler: async () => (0, client_1.request)("GET", "/v1/api-keys"),
50
+ };
51
+ exports.getUsageTool = {
52
+ name: "get_usage",
53
+ description: "This workspace's plan, how many emails it has sent this month, and how many are " +
54
+ "left. Check before a large batch: a send that would cross the included allowance " +
55
+ "on a plan without overage is refused whole, so it is better to know first than to " +
56
+ "discover it halfway through a campaign.",
57
+ schema: {},
58
+ handler: async () => (0, client_1.request)("GET", "/v1/usage"),
59
+ };
60
+ //# sourceMappingURL=operations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.js","sourceRoot":"","sources":["../../src/tools/operations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.removeSuppressionTool = exports.addSuppressionTool = exports.listSuppressionsTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.listSuppressionsTool = {
7
+ name: "list_suppressions",
8
+ description: "List addresses we refuse to mail and why (hard_bounce, complaint, unsubscribe, manual). " +
9
+ "Check here first when someone reports not receiving email.",
10
+ schema: { limit: zod_1.z.number().int().min(1).max(500).optional() },
11
+ handler: async (args) => (0, client_1.request)("GET", `/v1/suppressions?limit=${args.limit ?? 100}`),
12
+ };
13
+ exports.addSuppressionTool = {
14
+ name: "add_suppression",
15
+ description: "Stop sending to an address. Scope 'marketing' leaves transactional mail working.",
16
+ schema: {
17
+ email: zod_1.z.string().email(),
18
+ scope: zod_1.z.enum(["all", "transactional", "marketing"]).optional(),
19
+ reason: zod_1.z.enum(["manual", "list_hygiene", "unsubscribe"]).optional(),
20
+ },
21
+ handler: async (args) => (0, client_1.request)("POST", "/v1/suppressions", args),
22
+ };
23
+ exports.removeSuppressionTool = {
24
+ name: "remove_suppression",
25
+ description: "Remove a suppression so the address can be mailed again. Be careful with hard bounces — " +
26
+ "the address was rejected by the receiving server, and re-sending raises the bounce rate " +
27
+ "that AWS enforces on.",
28
+ schema: {
29
+ email: zod_1.z.string().email(),
30
+ scope: zod_1.z.enum(["all", "transactional", "marketing"]).optional(),
31
+ },
32
+ handler: async (args) => (0, client_1.request)("DELETE", `/v1/suppressions/${encodeURIComponent(String(args.email))}?scope=${args.scope ?? "all"}`),
33
+ };
34
+ //# sourceMappingURL=suppressions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suppressions.js","sourceRoot":"","sources":["../../src/tools/suppressions.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,sCAAoC;AAEvB,QAAA,oBAAoB,GAAG;IAClC,IAAI,EAAE,mBAAmB;IACzB,WAAW,EACT,0FAA0F;QAC1F,4DAA4D;IAC9D,MAAM,EAAE,EAAE,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;IAC9D,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAC/C,IAAA,gBAAO,EAAC,KAAK,EAAE,0BAA0B,IAAI,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC;CAChE,CAAC;AAEW,QAAA,kBAAkB,GAAG;IAChC,IAAI,EAAE,iBAAiB;IACvB,WAAW,EAAE,kFAAkF;IAC/F,MAAM,EAAE;QACN,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACzB,KAAK,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,eAAe,EAAE,WAAW,CAAC,CAAC,CAAC,QAAQ,EAAE;QAC/D,MAAM,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,cAAc,EAAE,aAAa,CAAC,CAAC,CAAC,QAAQ,EAAE;KACrE;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,MAAM,EAAE,kBAAkB,EAAE,IAAI,CAAC;CAC5F,CAAC;AAEW,QAAA,qBAAqB,GAAG;IACnC,IAAI,EAAE,oBAAoB;IAC1B,WAAW,EACT,0FAA0F;QAC1F,0FAA0F;QAC1F,uBAAuB;IACzB,MAAM,EAAE;QACN,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACzB,KAAK,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,eAAe,EAAE,WAAW,CAAC,CAAC,CAAC,QAAQ,EAAE;KAChE;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAC/C,IAAA,gBAAO,EAAC,QAAQ,EAAE,oBAAoB,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,UAAU,IAAI,CAAC,KAAK,IAAI,KAAK,EAAE,CAAC;CAC/G,CAAC"}
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendTemplateTool = exports.renderTemplateTool = exports.listTemplatesTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.listTemplatesTool = {
7
+ name: "list_templates",
8
+ description: "List stored email templates and the variables each one needs. Prefer sending via a " +
9
+ "template over composing HTML yourself — templates carry the brand styling and the " +
10
+ "unsubscribe footer.",
11
+ schema: {},
12
+ handler: async () => (0, client_1.request)("GET", "/v1/templates"),
13
+ };
14
+ exports.renderTemplateTool = {
15
+ name: "render_template",
16
+ description: "Render a template with values, without sending. Use this to check your copy reads " +
17
+ "correctly before mailing a real person. Returns an error listing any missing variables.",
18
+ schema: {
19
+ slug: zod_1.z.string(),
20
+ variables: zod_1.z.record(zod_1.z.string()),
21
+ },
22
+ handler: async (args) => (0, client_1.request)("POST", `/v1/templates/${args.slug}/render`, { variables: args.variables }),
23
+ };
24
+ exports.sendTemplateTool = {
25
+ name: "send_template_email",
26
+ description: "Send an email built from a stored template. Variable values are HTML-escaped on " +
27
+ "substitution, so they are safe to fill from user-supplied text.",
28
+ schema: {
29
+ template: zod_1.z.string().describe("Template slug"),
30
+ variables: zod_1.z.record(zod_1.z.string()),
31
+ from: zod_1.z.string(),
32
+ to: zod_1.z.string().email(),
33
+ subject: zod_1.z.string().optional().describe("Overrides the template's subject"),
34
+ scheduled_at: zod_1.z.string().optional(),
35
+ },
36
+ handler: async (args) => (0, client_1.request)("POST", "/v1/emails", args),
37
+ };
38
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/tools/templates.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,sCAAoC;AAEvB,QAAA,iBAAiB,GAAG;IAC/B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EACT,qFAAqF;QACrF,oFAAoF;QACpF,qBAAqB;IACvB,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,eAAe,CAAC;CACrD,CAAC;AAEW,QAAA,kBAAkB,GAAG;IAChC,IAAI,EAAE,iBAAiB;IACvB,WAAW,EACT,oFAAoF;QACpF,yFAAyF;IAC3F,MAAM,EAAE;QACN,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE;QAChB,SAAS,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC;KAChC;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAC/C,IAAA,gBAAO,EAAC,MAAM,EAAE,iBAAiB,IAAI,CAAC,IAAI,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;CACtF,CAAC;AAEW,QAAA,gBAAgB,GAAG;IAC9B,IAAI,EAAE,qBAAqB;IAC3B,WAAW,EACT,kFAAkF;QAClF,iEAAiE;IACnE,MAAM,EAAE;QACN,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC;QAC9C,SAAS,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC;QAC/B,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE;QAChB,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACtB,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kCAAkC,CAAC;QAC3E,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KACpC;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC;CACtF,CAAC"}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.replyToMessageTool = exports.getThreadTool = exports.listThreadsTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.listThreadsTool = {
7
+ name: "list_threads",
8
+ description: "List email conversations. Pass awaiting_reply=true to get only the threads where someone " +
9
+ "has written to you and you haven't answered — this is the tool to poll when deciding what " +
10
+ "needs a response.",
11
+ schema: {
12
+ awaiting_reply: zod_1.z.boolean().optional(),
13
+ limit: zod_1.z.number().int().min(1).max(200).optional(),
14
+ },
15
+ handler: async (args) => {
16
+ const qs = new URLSearchParams();
17
+ if (args.awaiting_reply !== undefined)
18
+ qs.set("awaiting_reply", String(args.awaiting_reply));
19
+ if (args.limit !== undefined)
20
+ qs.set("limit", String(args.limit));
21
+ return (0, client_1.request)("GET", `/v1/threads?${qs}`);
22
+ },
23
+ };
24
+ exports.getThreadTool = {
25
+ name: "get_thread",
26
+ description: "Read a conversation as a chronological transcript of outbound and inbound messages. " +
27
+ "Inbound text already has quoted history and signatures stripped, so read `text`; " +
28
+ "`raw_text` holds the untrimmed body if the stripped version looks wrong. Check " +
29
+ "spf_verdict and dkim_verdict before trusting a reply's claimed sender.",
30
+ schema: { id: zod_1.z.string().describe("Thread id") },
31
+ handler: async (args) => (0, client_1.request)("GET", `/v1/threads/${args.id}`),
32
+ };
33
+ exports.replyToMessageTool = {
34
+ name: "reply_to_message",
35
+ description: "Reply to a message, keeping it on the same conversation. Sets the threading headers so " +
36
+ "the recipient's mail client shows it as part of the existing exchange rather than a new " +
37
+ "one. Prefer this over send_email whenever you are answering something.",
38
+ schema: {
39
+ reply_to_message_id: zod_1.z.string().describe("Id of the message being replied to"),
40
+ from: zod_1.z.string().describe("Sender address on a verified domain"),
41
+ to: zod_1.z.string().email(),
42
+ subject: zod_1.z.string(),
43
+ text: zod_1.z.string().optional(),
44
+ html: zod_1.z.string().optional(),
45
+ },
46
+ handler: async (args) => (0, client_1.request)("POST", "/v1/emails", args),
47
+ };
48
+ //# sourceMappingURL=threads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threads.js","sourceRoot":"","sources":["../../src/tools/threads.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,sCAAoC;AAEvB,QAAA,eAAe,GAAG;IAC7B,IAAI,EAAE,cAAc;IACpB,WAAW,EACT,2FAA2F;QAC3F,4FAA4F;QAC5F,mBAAmB;IACrB,MAAM,EAAE;QACN,cAAc,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;QACtC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;KACnD;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE;QAC/C,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;QACjC,IAAI,IAAI,CAAC,cAAc,KAAK,SAAS;YAAE,EAAE,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;QAC7F,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;YAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAClE,OAAO,IAAA,gBAAO,EAAC,KAAK,EAAE,eAAe,EAAE,EAAE,CAAC,CAAC;IAC7C,CAAC;CACF,CAAC;AAEW,QAAA,aAAa,GAAG;IAC3B,IAAI,EAAE,YAAY;IAClB,WAAW,EACT,sFAAsF;QACtF,mFAAmF;QACnF,iFAAiF;QACjF,wEAAwE;IAC1E,MAAM,EAAE,EAAE,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE;IAChD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,eAAe,IAAI,CAAC,EAAE,EAAE,CAAC;CAC3F,CAAC;AAEW,QAAA,kBAAkB,GAAG;IAChC,IAAI,EAAE,kBAAkB;IACxB,WAAW,EACT,yFAAyF;QACzF,0FAA0F;QAC1F,wEAAwE;IAC1E,MAAM,EAAE;QACN,mBAAmB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;QAC9E,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;QAChE,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACtB,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE;QACnB,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC3B,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC5B;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC;CACtF,CAAC"}
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setPreferencesTool = exports.getPreferencesTool = exports.listTopicsTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_1 = require("../client");
6
+ exports.listTopicsTool = {
7
+ name: "list_topics",
8
+ description: "List subscription topics — the categories a person can opt out of individually. " +
9
+ "Pass a topic when sending marketing mail so recipients can unsubscribe from that " +
10
+ "kind alone rather than from everything.",
11
+ schema: {},
12
+ handler: async () => (0, client_1.request)("GET", "/v1/topics"),
13
+ };
14
+ exports.getPreferencesTool = {
15
+ name: "get_email_preferences",
16
+ description: "What one person has chosen to receive. Check this before asking a human why someone " +
17
+ "isn't getting a particular kind of email — an opt-out looks identical to a delivery " +
18
+ "failure from the outside.",
19
+ schema: { email: zod_1.z.string().email() },
20
+ handler: async (args) => (0, client_1.request)("GET", `/v1/topics/preferences?email=${encodeURIComponent(String(args.email))}`),
21
+ };
22
+ exports.setPreferencesTool = {
23
+ name: "set_email_preferences",
24
+ description: "Set which topics a person receives. Only do this when they have actually asked — " +
25
+ "silently re-subscribing someone who opted out is what generates spam complaints.",
26
+ schema: {
27
+ email: zod_1.z.string().email(),
28
+ topics: zod_1.z.record(zod_1.z.boolean()).describe("Topic key to subscribed, e.g. {\"newsletter\": false}"),
29
+ },
30
+ handler: async (args) => (0, client_1.request)("POST", "/v1/topics/preferences", args),
31
+ };
32
+ //# sourceMappingURL=topics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topics.js","sourceRoot":"","sources":["../../src/tools/topics.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,sCAAoC;AAEvB,QAAA,cAAc,GAAG;IAC5B,IAAI,EAAE,aAAa;IACnB,WAAW,EACT,kFAAkF;QAClF,mFAAmF;QACnF,yCAAyC;IAC3C,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,IAAA,gBAAO,EAAC,KAAK,EAAE,YAAY,CAAC;CAClD,CAAC;AAEW,QAAA,kBAAkB,GAAG;IAChC,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EACT,sFAAsF;QACtF,sFAAsF;QACtF,2BAA2B;IAC7B,MAAM,EAAE,EAAE,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;IACrC,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAC/C,IAAA,gBAAO,EAAC,KAAK,EAAE,gCAAgC,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;CAC3F,CAAC;AAEW,QAAA,kBAAkB,GAAG;IAChC,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EACT,mFAAmF;QACnF,kFAAkF;IACpF,MAAM,EAAE;QACN,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACzB,MAAM,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,uDAAuD,CAAC;KAChG;IACD,OAAO,EAAE,KAAK,EAAE,IAA6B,EAAE,EAAE,CAAC,IAAA,gBAAO,EAAC,MAAM,EAAE,wBAAwB,EAAE,IAAI,CAAC;CAClG,CAAC"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@sendraven/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for SendRaven: send email, read replies as threads, run campaigns, and decide approvals, with per-key limits for agents.",
5
+ "license": "MIT",
6
+ "author": "Common Ninja Ltd.",
7
+ "homepage": "https://sendraven.ai/mcp",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/CommonNinja/sendraven-mcp-server.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/CommonNinja/sendraven-mcp-server/issues"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "email",
19
+ "email-api",
20
+ "ai-agents",
21
+ "claude",
22
+ "cursor",
23
+ "sendraven"
24
+ ],
25
+ "type": "commonjs",
26
+ "bin": {
27
+ "sendraven-mcp": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE",
33
+ "server.json"
34
+ ],
35
+ "engines": {
36
+ "node": ">=20"
37
+ },
38
+ "scripts": {
39
+ "dev": "PORT=5100 tsx watch src/index.ts",
40
+ "build": "tsc && tsx scripts/export-tools.ts",
41
+ "prepublishOnly": "npm run build",
42
+ "start": "node dist/index.js",
43
+ "typecheck": "tsc --noEmit",
44
+ "export-tools": "tsx scripts/export-tools.ts"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.12.0",
48
+ "express": "^4.21.0",
49
+ "zod": "^3.23.8"
50
+ },
51
+ "devDependencies": {
52
+ "@types/express": "^4.17.21",
53
+ "@types/node": "^20.14.12",
54
+ "tsx": "^4.23.1",
55
+ "typescript": "^5.5.4"
56
+ }
57
+ }
package/server.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
3
+ "name": "ai.sendraven/mcp",
4
+ "description": "Email for AI agents: send, read replies as threads, run campaigns, decide approvals, with per-key limits.",
5
+ "repository": {
6
+ "url": "https://github.com/CommonNinja/sendraven-mcp-server",
7
+ "source": "github"
8
+ },
9
+ "version": "0.1.0",
10
+ "websiteUrl": "https://sendraven.ai/mcp",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "registryBaseUrl": "https://registry.npmjs.org",
15
+ "identifier": "@sendraven/mcp",
16
+ "version": "0.1.0",
17
+ "transport": { "type": "stdio" },
18
+ "environmentVariables": [
19
+ {
20
+ "name": "SENDRAVEN_API_KEY",
21
+ "description": "API key from the SendRaven dashboard (Developers).",
22
+ "isRequired": true,
23
+ "isSecret": true
24
+ }
25
+ ]
26
+ }
27
+ ],
28
+ "remotes": [
29
+ {
30
+ "type": "streamable-http",
31
+ "url": "https://mcp.sendraven.ai/mcp"
32
+ }
33
+ ]
34
+ }