carlyemail-toolkit 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 SWH Labs LLC
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,70 @@
1
+ # carlyemail-toolkit
2
+
3
+ CarlyEmail's email tools for the Vercel AI SDK, LangChain, and any other agent
4
+ framework. A real inbox your agent can send, receive and reply from, without
5
+ writing the tools yourself.
6
+
7
+ ```bash
8
+ npm install carlyemail-toolkit
9
+ export CARLYEMAIL_API_KEY=ce_us_...
10
+ ```
11
+
12
+ ```typescript
13
+ import { openai } from "@ai-sdk/openai";
14
+ import { ToolLoopAgent } from "ai";
15
+ import { CarlyEmailToolkit } from "carlyemail-toolkit/ai-sdk";
16
+
17
+ const agent = new ToolLoopAgent({
18
+ model: openai("gpt-5-mini"),
19
+ instructions: "Read the thread before replying. Draft when unsure.",
20
+ tools: new CarlyEmailToolkit().getTools(),
21
+ });
22
+ ```
23
+
24
+ ```typescript
25
+ import { createAgent } from "langchain";
26
+ import { CarlyEmailToolkit } from "carlyemail-toolkit/langchain";
27
+
28
+ const agent = createAgent({ model: "openai:gpt-5-mini", tools: new CarlyEmailToolkit().getTools() });
29
+ ```
30
+
31
+ ```typescript
32
+ import { CarlyEmailToolkit } from "carlyemail-toolkit"; // no framework
33
+
34
+ for (const tool of new CarlyEmailToolkit().getTools()) {
35
+ tool.name, tool.description, tool.inputSchema, tool.annotations.readOnlyHint;
36
+ await tool.func({ thread_id: "..." }); // runs it
37
+ }
38
+ ```
39
+
40
+ Plain ES modules with no build step; types ship with it. `ai` and
41
+ `@langchain/core` are optional peers — install the one you use.
42
+
43
+ ## What is in it
44
+
45
+ The 25 tools the hosted [MCP server](https://docs.carlyemail.com/mcp) serves —
46
+ inboxes, threads, messages, drafts, attachments, labels, and account
47
+ verification — with the same names, descriptions and parameter schemas, called
48
+ over the REST API. Pass names to take only some:
49
+
50
+ ```typescript
51
+ const tools = new CarlyEmailToolkit().getTools(["list_messages", "get_thread", "reply_to_message"]);
52
+ ```
53
+
54
+ A name that is not a tool throws rather than being dropped.
55
+
56
+ ## Which inbox
57
+
58
+ Every tool takes `inbox_id`, and most work without it. A key scoped to one
59
+ inbox means that inbox. An organization with one inbox means that one. With
60
+ several, the tools that read threads and drafts look across all of them, and
61
+ every other tool refuses and names the inboxes so the agent can ask which —
62
+ the same behaviour as the hosted server.
63
+
64
+ ## Errors
65
+
66
+ A failed call throws one bounded line carrying the API's message and the fix
67
+ it suggests. The AI SDK reports that as a `tool-error` part; LangChain's tool
68
+ node hands it back to the model as an error message.
69
+
70
+ See <https://docs.carlyemail.com/integrations/toolkit>.
package/ai-sdk.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { Tool } from "ai";
2
+
3
+ import { MapToolkit, ToolDefinition } from "./index.js";
4
+
5
+ export declare class CarlyEmailToolkit extends MapToolkit<Tool> {
6
+ protected buildTool(tool: ToolDefinition): Tool;
7
+ }
package/ai-sdk.js ADDED
@@ -0,0 +1,27 @@
1
+ // CarlyEmail tools for the Vercel AI SDK.
2
+ //
3
+ // import { CarlyEmailToolkit } from "carlyemail-toolkit/ai-sdk";
4
+ // const agent = new ToolLoopAgent({ model, tools: new CarlyEmailToolkit().getTools() });
5
+ //
6
+ // A failed call throws, which the AI SDK reports as a `tool-error` part
7
+ // rather than a successful result that happens to describe a failure.
8
+
9
+ import { jsonSchema, tool } from "ai";
10
+
11
+ import { MapToolkit, errorMessage } from "./core.js";
12
+
13
+ export class CarlyEmailToolkit extends MapToolkit {
14
+ buildTool(definition) {
15
+ return tool({
16
+ description: definition.description,
17
+ inputSchema: jsonSchema(definition.inputSchema),
18
+ execute: async (args) => {
19
+ try {
20
+ return await definition.func(this.client, args ?? {});
21
+ } catch (error) {
22
+ throw new Error(errorMessage(error), { cause: error });
23
+ }
24
+ },
25
+ });
26
+ }
27
+ }
package/core.js ADDED
@@ -0,0 +1,422 @@
1
+ // The tools, the SDK call behind each one, and the toolkit shape every adapter
2
+ // shares.
3
+ //
4
+ // Names, descriptions, parameter schemas and annotations come from `tools.js`,
5
+ // written from the hosted MCP server's registry by `tools/gen_toolkit_tools.py`
6
+ // in the API repository. Nothing in this file describes a tool; it only says
7
+ // what to do when one is called.
8
+ //
9
+ // The one piece of behaviour here that is not a straight SDK call is what
10
+ // happens when a tool is called without `inbox_id`. The hosted server resolves
11
+ // that for the model — an inbox-scoped key means that inbox, an organization
12
+ // with one inbox means that one, and several means the server refuses and
13
+ // names them rather than picking on somebody's behalf. The toolkit does the
14
+ // same, with the same words, so an agent behaves identically whichever way it
15
+ // is connected.
16
+
17
+ import { CarlyEmail, CarlyEmailError } from "carlyemail";
18
+
19
+ import definitions from "./tools.js";
20
+
21
+ /** What to tell a fresh account that has no inbox yet. The server's words. */
22
+ export const FIRST_INBOX = definitions.first_inbox;
23
+
24
+ // ------------------------------------------------------------------ errors
25
+
26
+ /** A validation error body can list every field it disliked. That is useful
27
+ * to a person and useless to a model, which reads the first sentence and
28
+ * acts. */
29
+ const MAX_MESSAGE_LENGTH = 500;
30
+
31
+ /** A refusal made on this side of the wire, before any request went out.
32
+ * Carries `code` and `fix` the way the API's own errors do. */
33
+ export class ToolkitError extends Error {
34
+ constructor(message, { code, fix }) {
35
+ super(message);
36
+ this.name = "ToolkitError";
37
+ this.code = code;
38
+ this.fix = fix;
39
+ }
40
+ }
41
+
42
+ /** One bounded sentence or three: what went wrong, what clears it, where to
43
+ * read more. The API answers errors with `message`, `fix` and `docs`, and the
44
+ * SDK keeps all three on the exception. */
45
+ export function errorMessage(error) {
46
+ let text;
47
+ if (error instanceof ToolkitError) {
48
+ text = `${error.message} ${error.fix}`;
49
+ } else if (error instanceof CarlyEmailError) {
50
+ const parts = [error.message];
51
+ if (error.fix) parts.push(String(error.fix));
52
+ if (error.docs) parts.push(`See ${error.docs}`);
53
+ const tag = error.code ? `${error.code}, HTTP ${error.status}` : `HTTP ${error.status}`;
54
+ text = `${parts.join(" ")} (${tag})`;
55
+ } else if (error instanceof Error) {
56
+ text = `${error.name}: ${error.message}`;
57
+ } else {
58
+ text = String(error);
59
+ }
60
+ return text.length > MAX_MESSAGE_LENGTH ? `${text.slice(0, MAX_MESSAGE_LENGTH)}…` : text;
61
+ }
62
+
63
+ // ------------------------------------------------------------------- scope
64
+
65
+ // The identity behind a key never changes for the life of the key, so it is
66
+ // asked for once per client — the promise is stored, so two tools starting
67
+ // together share one request. The inbox list is not cached: `create_inbox` is
68
+ // one of the tools, and the call after it should see what it made.
69
+ const identities = new WeakMap();
70
+
71
+ function identity(client) {
72
+ let pending = identities.get(client);
73
+ if (!pending) {
74
+ pending = client.auth.whoami();
75
+ identities.set(client, pending);
76
+ }
77
+ return pending;
78
+ }
79
+
80
+ /** Work out what a call is addressed to, without deciding what to do about
81
+ * it. Zero inboxes and several inboxes are both legitimate states; reads
82
+ * answer them by looking across the organization and writes refuse, so this
83
+ * never throws over the organization's shape. */
84
+ export async function resolveScope(client, inboxId) {
85
+ if (inboxId) return { inbox: inboxId, candidates: [], empty: false };
86
+ const keyed = (await identity(client)).inbox_id;
87
+ if (keyed) return { inbox: keyed, candidates: [], empty: false };
88
+ // Two would be enough to tell "one" from "several", but the refusal names
89
+ // the candidates, so fetch enough to list them.
90
+ const { inboxes } = await client.inboxes.list({ limit: 11 });
91
+ const ids = inboxes.map((row) => row.inbox_id);
92
+ if (ids.length === 1) return { inbox: ids[0], candidates: [], empty: false };
93
+ if (ids.length) return { inbox: null, candidates: ids, empty: false };
94
+ return { inbox: null, candidates: [], empty: true };
95
+ }
96
+
97
+ /** The single inbox to act on, or a refusal that says how to name one. Which
98
+ * address a message is sent from is the person's decision, so when an
99
+ * organization has several the candidates are reported rather than chosen
100
+ * between. */
101
+ export async function oneInbox(client, inboxId) {
102
+ const scope = await resolveScope(client, inboxId);
103
+ if (scope.inbox) return scope.inbox;
104
+ if (scope.empty) {
105
+ throw new ToolkitError(
106
+ "This organization has no inboxes yet, so there is no mailbox to act on.",
107
+ { code: "no_inbox", fix: FIRST_INBOX },
108
+ );
109
+ }
110
+ const shown = scope.candidates.slice(0, 10).join(", ");
111
+ const more = scope.candidates.length > 10 ? " and others" : "";
112
+ throw new ToolkitError(
113
+ `This organization has ${scope.candidates.length} inboxes, so inbox_id is required.`,
114
+ {
115
+ code: "inbox_id_required",
116
+ fix: `Ask the person which one to use — ${shown}${more} — and pass it as inbox_id. Do not pick for them.`,
117
+ },
118
+ );
119
+ }
120
+
121
+ /** Attach the first-run instruction when there was nowhere to read from. An
122
+ * empty organization is not a failed call; what must not be lost is the
123
+ * instruction. */
124
+ function guided(payload, scope) {
125
+ if (scope.empty && payload && typeof payload === "object") payload.next_step = FIRST_INBOX;
126
+ return payload;
127
+ }
128
+
129
+ /** The named arguments that were actually given. */
130
+ function pick(args, ...names) {
131
+ const out = {};
132
+ for (const name of names) {
133
+ if (args[name] !== undefined && args[name] !== null) out[name] = args[name];
134
+ }
135
+ return out;
136
+ }
137
+
138
+ const camel = (name) => name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
139
+
140
+ /** Query parameters, in the SDK's camelCase. Bodies stay as the API spells
141
+ * them; only the generated query option names are camelCased. */
142
+ function query(args, ...names) {
143
+ const out = {};
144
+ for (const [name, value] of Object.entries(pick(args, ...names))) out[camel(name)] = value;
145
+ return out;
146
+ }
147
+
148
+ const PAGE = ["limit", "page_token"];
149
+ const WINDOW = ["before", "after"];
150
+ const OUTBOUND = ["cc", "bcc", "subject", "text", "html", "labels", "reply_to", "headers", "attachments"];
151
+
152
+ // -------------------------------------------------------------- functions
153
+
154
+ async function listInboxes(client, a) {
155
+ const out = await client.inboxes.list(query(a, ...PAGE));
156
+ if (!out.inboxes?.length) out.next_step = FIRST_INBOX;
157
+ return out;
158
+ }
159
+
160
+ async function getInbox(client, a) {
161
+ return client.inboxes.get(await oneInbox(client, a.inbox_id));
162
+ }
163
+
164
+ async function createInbox(client, a) {
165
+ return client.inboxes.create(pick(a, "username", "domain", "display_name", "client_id", "metadata"));
166
+ }
167
+
168
+ async function updateInbox(client, a) {
169
+ const inbox = await oneInbox(client, a.inbox_id);
170
+ return client.inboxes.update(inbox, pick(a, "display_name", "metadata"));
171
+ }
172
+
173
+ async function deleteInbox(client, a) {
174
+ const inbox = await oneInbox(client, a.inbox_id);
175
+ await client.inboxes.delete(inbox);
176
+ return { deleted: true, inbox_id: inbox };
177
+ }
178
+
179
+ async function listThreads(client, a) {
180
+ const scope = await resolveScope(client, a.inbox_id);
181
+ const q = query(a, ...PAGE, ...WINDOW, "labels", "ascending", "senders", "recipients", "subject");
182
+ if (scope.inbox) return client.threads.list(scope.inbox, q);
183
+ return guided(await client.threads.listOrg(q), scope);
184
+ }
185
+
186
+ async function searchThreads(client, a) {
187
+ const scope = await resolveScope(client, a.inbox_id);
188
+ const q = { q: a.query, ...query(a, ...PAGE, ...WINDOW) };
189
+ if (scope.inbox) return client.threads.search(scope.inbox, q);
190
+ return guided(await client.threads.searchOrg(q), scope);
191
+ }
192
+
193
+ async function getThread(client, a) {
194
+ const scope = await resolveScope(client, a.inbox_id);
195
+ if (scope.inbox) return client.threads.get(scope.inbox, a.thread_id);
196
+ return client.threads.getOrg(a.thread_id);
197
+ }
198
+
199
+ async function updateThread(client, a) {
200
+ const scope = await resolveScope(client, a.inbox_id);
201
+ const body = pick(a, "add_labels", "remove_labels");
202
+ if (scope.inbox) return client.threads.update(scope.inbox, a.thread_id, body);
203
+ return client.threads.updateOrg(a.thread_id, body);
204
+ }
205
+
206
+ async function deleteThread(client, a) {
207
+ const scope = await resolveScope(client, a.inbox_id);
208
+ if (scope.inbox) await client.threads.delete(scope.inbox, a.thread_id);
209
+ else await client.threads.deleteOrg(a.thread_id);
210
+ return { deleted: true, thread_id: a.thread_id };
211
+ }
212
+
213
+ async function listMessages(client, a) {
214
+ const inbox = await oneInbox(client, a.inbox_id);
215
+ const q = query(a, ...PAGE, ...WINDOW, "labels", "ascending", "include_spam", "from", "to", "subject");
216
+ return client.messages.list(inbox, q);
217
+ }
218
+
219
+ async function searchMessages(client, a) {
220
+ const inbox = await oneInbox(client, a.inbox_id);
221
+ return client.messages.search(inbox, { q: a.query, ...query(a, ...PAGE, ...WINDOW) });
222
+ }
223
+
224
+ async function sendMessage(client, a) {
225
+ const inbox = await oneInbox(client, a.inbox_id);
226
+ return client.messages.send(inbox, pick(a, "to", ...OUTBOUND));
227
+ }
228
+
229
+ async function replyToMessage(client, a) {
230
+ const inbox = await oneInbox(client, a.inbox_id);
231
+ return client.messages.reply(inbox, a.message_id, pick(a, "to", "reply_all", ...OUTBOUND));
232
+ }
233
+
234
+ async function forwardMessage(client, a) {
235
+ const inbox = await oneInbox(client, a.inbox_id);
236
+ return client.messages.forward(inbox, a.message_id, pick(a, "to", ...OUTBOUND));
237
+ }
238
+
239
+ async function updateMessage(client, a) {
240
+ const inbox = await oneInbox(client, a.inbox_id);
241
+ return client.messages.update(inbox, a.message_id, pick(a, "add_labels", "remove_labels"));
242
+ }
243
+
244
+ async function getAttachment(client, a) {
245
+ const inbox = await oneInbox(client, a.inbox_id);
246
+ return client.messages.getAttachment(inbox, a.message_id, a.attachment_id);
247
+ }
248
+
249
+ async function createDraft(client, a) {
250
+ const inbox = await oneInbox(client, a.inbox_id);
251
+ const body = pick(a, "to", ...OUTBOUND, "in_reply_to", "forward_of", "reply_all", "send_at", "client_id");
252
+ delete body.headers;
253
+ return client.drafts.create(inbox, body);
254
+ }
255
+
256
+ async function listDrafts(client, a) {
257
+ const scope = await resolveScope(client, a.inbox_id);
258
+ const q = query(a, ...PAGE, ...WINDOW, "labels", "ascending");
259
+ if (scope.inbox) return client.drafts.list(scope.inbox, q);
260
+ return guided(await client.drafts.listOrg(q), scope);
261
+ }
262
+
263
+ async function getDraft(client, a) {
264
+ const scope = await resolveScope(client, a.inbox_id);
265
+ if (scope.inbox) return client.drafts.get(scope.inbox, a.draft_id);
266
+ return client.drafts.getOrg(a.draft_id);
267
+ }
268
+
269
+ /** The inbox a draft lives in, for the routes that need it in the path. A
270
+ * draft id is unique across the organization, so when several inboxes make
271
+ * the inbox ambiguous the draft itself can say. */
272
+ async function draftInbox(client, a) {
273
+ const scope = await resolveScope(client, a.inbox_id);
274
+ if (scope.inbox) return scope.inbox;
275
+ if (scope.empty) return oneInbox(client, null); // throws with the first-inbox guidance
276
+ return (await client.drafts.getOrg(a.draft_id)).inbox_id;
277
+ }
278
+
279
+ async function updateDraft(client, a) {
280
+ const inbox = await draftInbox(client, a);
281
+ const body = pick(
282
+ a,
283
+ "to", "cc", "bcc", "subject", "text", "html", "reply_to", "send_at",
284
+ "add_labels", "remove_labels", "add_attachments", "remove_attachments",
285
+ );
286
+ return client.drafts.update(inbox, a.draft_id, body);
287
+ }
288
+
289
+ async function sendDraft(client, a) {
290
+ const inbox = await draftInbox(client, a);
291
+ return client.drafts.send(inbox, a.draft_id, {});
292
+ }
293
+
294
+ async function deleteDraft(client, a) {
295
+ const inbox = await draftInbox(client, a);
296
+ await client.drafts.delete(inbox, a.draft_id);
297
+ return { deleted: true, draft_id: a.draft_id };
298
+ }
299
+
300
+ async function authMe(client) {
301
+ const out = { ...(await client.auth.whoami()) };
302
+ // The first call a connected client makes, and so the earliest place a
303
+ // missing mailbox can be mentioned.
304
+ if (!out.inbox_id && !(await client.inboxes.list({ limit: 1 })).inboxes?.length) {
305
+ out.inbox_count = 0;
306
+ out.next_step = FIRST_INBOX;
307
+ }
308
+ return out;
309
+ }
310
+
311
+ async function verifyAccount(client, a) {
312
+ return client.agent.verify({ otp_code: String(a.code).trim() });
313
+ }
314
+
315
+ export const FUNCTIONS = {
316
+ auth_me: authMe,
317
+ create_draft: createDraft,
318
+ create_inbox: createInbox,
319
+ delete_draft: deleteDraft,
320
+ delete_inbox: deleteInbox,
321
+ delete_thread: deleteThread,
322
+ forward_message: forwardMessage,
323
+ get_attachment: getAttachment,
324
+ get_draft: getDraft,
325
+ get_inbox: getInbox,
326
+ get_thread: getThread,
327
+ list_drafts: listDrafts,
328
+ list_inboxes: listInboxes,
329
+ list_messages: listMessages,
330
+ list_threads: listThreads,
331
+ reply_to_message: replyToMessage,
332
+ search_messages: searchMessages,
333
+ search_threads: searchThreads,
334
+ send_draft: sendDraft,
335
+ send_message: sendMessage,
336
+ update_draft: updateDraft,
337
+ update_inbox: updateInbox,
338
+ update_message: updateMessage,
339
+ update_thread: updateThread,
340
+ verify_account: verifyAccount,
341
+ };
342
+
343
+ function assemble() {
344
+ const defined = new Set(definitions.tools.map((t) => t.name));
345
+ const implemented = new Set(Object.keys(FUNCTIONS));
346
+ const undefinedOnes = [...implemented].filter((n) => !defined.has(n));
347
+ const unimplemented = [...defined].filter((n) => !implemented.has(n));
348
+ // Loud at import, not at the first call. A tool with no function would
349
+ // otherwise be advertised to the model and fail when chosen.
350
+ if (undefinedOnes.length || unimplemented.length) {
351
+ throw new Error(
352
+ `tools.js and FUNCTIONS disagree: undefined ${JSON.stringify(undefinedOnes)}, ` +
353
+ `unimplemented ${JSON.stringify(unimplemented)}`,
354
+ );
355
+ }
356
+ return definitions.tools.map((t) => ({
357
+ name: t.name,
358
+ title: t.title,
359
+ description: t.description,
360
+ inputSchema: t.input_schema,
361
+ annotations: {
362
+ title: t.title,
363
+ readOnlyHint: t.read_only,
364
+ destructiveHint: t.destructive,
365
+ idempotentHint: t.idempotent,
366
+ openWorldHint: t.open_world,
367
+ },
368
+ func: FUNCTIONS[t.name],
369
+ }));
370
+ }
371
+
372
+ /** Every tool, in the order the server lists them. */
373
+ export const TOOLS = assemble();
374
+
375
+ // ----------------------------------------------------------------- toolkit
376
+
377
+ /** Builds one framework-native tool per entry in `TOOLS`. Subclasses say how
378
+ * in `buildTool`; the client and the filter by name are shared. */
379
+ export class BaseToolkit {
380
+ constructor(clientOrOptions) {
381
+ this.client =
382
+ clientOrOptions instanceof CarlyEmail ? clientOrOptions : new CarlyEmail(clientOrOptions ?? {});
383
+ this.tools = {};
384
+ for (const tool of TOOLS) this.tools[tool.name] = this.buildTool(tool);
385
+ }
386
+
387
+ buildTool() {
388
+ throw new Error("buildTool is what a subclass supplies");
389
+ }
390
+
391
+ /** The named tools, checked. A name that is not a tool is an error, not an
392
+ * omission: silently dropping it is how an agent ships with `send_message`
393
+ * misspelled and nobody finds out until it cannot send. */
394
+ select(names) {
395
+ if (!names) return Object.keys(this.tools);
396
+ const unknown = names.filter((name) => !(name in this.tools));
397
+ if (unknown.length) {
398
+ throw new Error(
399
+ `Unknown tool(s): ${unknown.join(", ")}. Available: ${Object.keys(this.tools).join(", ")}.`,
400
+ );
401
+ }
402
+ return names;
403
+ }
404
+
405
+ names() {
406
+ return Object.keys(this.tools);
407
+ }
408
+ }
409
+
410
+ /** `getTools()` is an array, for frameworks that take a list. */
411
+ export class ListToolkit extends BaseToolkit {
412
+ getTools(names) {
413
+ return this.select(names).map((name) => this.tools[name]);
414
+ }
415
+ }
416
+
417
+ /** `getTools()` is an object keyed by name, for frameworks that take a map. */
418
+ export class MapToolkit extends BaseToolkit {
419
+ getTools(names) {
420
+ return Object.fromEntries(this.select(names).map((name) => [name, this.tools[name]]));
421
+ }
422
+ }
package/index.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { CarlyEmail } from "carlyemail";
2
+
3
+ /** What to tell a fresh account that has no inbox yet. */
4
+ export declare const FIRST_INBOX: string;
5
+
6
+ /** MCP's tool annotations, which is what every framework that has an opinion
7
+ * about tool safety has converged on. */
8
+ export interface ToolAnnotations {
9
+ title: string;
10
+ readOnlyHint: boolean;
11
+ destructiveHint: boolean;
12
+ idempotentHint: boolean;
13
+ openWorldHint: boolean;
14
+ }
15
+
16
+ export type JsonSchema = Record<string, unknown>;
17
+ export type ToolArguments = Record<string, unknown>;
18
+
19
+ /** One tool as every framework sees it, plus the function that runs it. */
20
+ export interface ToolDefinition {
21
+ name: string;
22
+ title: string;
23
+ description: string;
24
+ inputSchema: JsonSchema;
25
+ annotations: ToolAnnotations;
26
+ func: (client: CarlyEmail, args: ToolArguments) => Promise<unknown>;
27
+ }
28
+
29
+ /** A tool tied to one client: call `func` with the model's arguments. */
30
+ export interface Tool {
31
+ name: string;
32
+ title: string;
33
+ description: string;
34
+ inputSchema: JsonSchema;
35
+ annotations: ToolAnnotations;
36
+ func: (args?: ToolArguments) => Promise<unknown>;
37
+ }
38
+
39
+ /** Every tool, in the order the server lists them. */
40
+ export declare const TOOLS: ToolDefinition[];
41
+
42
+ /** A refusal made before any request went out, with `code` and `fix` the way
43
+ * the API's own errors carry them. */
44
+ export declare class ToolkitError extends Error {
45
+ code: string;
46
+ fix: string;
47
+ constructor(message: string, options: { code: string; fix: string });
48
+ }
49
+
50
+ /** What a failed tool call says to the framework that made it. */
51
+ export declare function errorMessage(error: unknown): string;
52
+
53
+ export interface Scope {
54
+ inbox: string | null;
55
+ candidates: string[];
56
+ empty: boolean;
57
+ }
58
+
59
+ export declare function resolveScope(client: CarlyEmail, inboxId?: string | null): Promise<Scope>;
60
+ export declare function oneInbox(client: CarlyEmail, inboxId?: string | null): Promise<string>;
61
+
62
+ export interface ClientOptions {
63
+ apiKey?: string;
64
+ baseUrl?: string;
65
+ fetch?: typeof fetch;
66
+ }
67
+
68
+ export declare abstract class BaseToolkit<T> {
69
+ readonly client: CarlyEmail;
70
+ protected readonly tools: Record<string, T>;
71
+ constructor(clientOrOptions?: CarlyEmail | ClientOptions);
72
+ protected abstract buildTool(tool: ToolDefinition): T;
73
+ names(): string[];
74
+ }
75
+
76
+ export declare abstract class ListToolkit<T> extends BaseToolkit<T> {
77
+ /** All tools, or the named ones in the order named. An unknown name throws. */
78
+ getTools(names?: string[]): T[];
79
+ }
80
+
81
+ export declare abstract class MapToolkit<T> extends BaseToolkit<T> {
82
+ /** All tools, or the named ones, keyed by name. An unknown name throws. */
83
+ getTools(names?: string[]): Record<string, T>;
84
+ }
85
+
86
+ export declare class CarlyEmailToolkit extends ListToolkit<Tool> {
87
+ protected buildTool(tool: ToolDefinition): Tool;
88
+ }
package/index.js ADDED
@@ -0,0 +1,42 @@
1
+ // The tools with no framework around them.
2
+ //
3
+ // import { CarlyEmailToolkit } from "carlyemail-toolkit";
4
+ // const tools = new CarlyEmailToolkit().getTools();
5
+ //
6
+ // Each has `name`, `title`, `description`, `inputSchema` (JSON Schema),
7
+ // `annotations` (MCP's vocabulary) and `func(args)`, which is enough to hand
8
+ // it to any framework this package has no adapter for.
9
+
10
+ import { ListToolkit, errorMessage } from "./core.js";
11
+
12
+ export {
13
+ FIRST_INBOX,
14
+ TOOLS,
15
+ ToolkitError,
16
+ errorMessage,
17
+ oneInbox,
18
+ resolveScope,
19
+ BaseToolkit,
20
+ ListToolkit,
21
+ MapToolkit,
22
+ } from "./core.js";
23
+
24
+ export class CarlyEmailToolkit extends ListToolkit {
25
+ buildTool(tool) {
26
+ return {
27
+ name: tool.name,
28
+ title: tool.title,
29
+ description: tool.description,
30
+ inputSchema: tool.inputSchema,
31
+ annotations: tool.annotations,
32
+ func: async (args = {}) => {
33
+ try {
34
+ return await tool.func(this.client, args);
35
+ } catch (error) {
36
+ // One concise line, not the SDK's whole error object.
37
+ throw new Error(errorMessage(error), { cause: error });
38
+ }
39
+ },
40
+ };
41
+ }
42
+ }
package/langchain.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { StructuredToolInterface } from "@langchain/core/tools";
2
+
3
+ import { ListToolkit, ToolDefinition } from "./index.js";
4
+
5
+ export declare class CarlyEmailToolkit extends ListToolkit<StructuredToolInterface> {
6
+ protected buildTool(tool: ToolDefinition): StructuredToolInterface;
7
+ }