@koderlabs/inbox-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 +21 -0
- package/README.md +91 -0
- package/dist/chunk-T7SFANFP.js +969 -0
- package/dist/chunk-T7SFANFP.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +255 -0
- package/dist/index.js.map +1 -0
- package/dist/stdio.d.ts +1 -0
- package/dist/stdio.js +45 -0
- package/dist/stdio.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
// @koderlabs/inbox-mcp — generated build, do not edit
|
|
2
|
+
|
|
3
|
+
// ../../packages/shared/src/scopes.ts
|
|
4
|
+
var SCOPE = {
|
|
5
|
+
INBOX_READ: "inbox:read",
|
|
6
|
+
INBOX_WRITE: "inbox:write",
|
|
7
|
+
EMAIL_READ: "email:read",
|
|
8
|
+
EMAIL_WRITE: "email:write"
|
|
9
|
+
};
|
|
10
|
+
var ALL_SCOPES = Object.values(SCOPE);
|
|
11
|
+
|
|
12
|
+
// ../../packages/shared/src/types/organization.ts
|
|
13
|
+
var OrgRole = /* @__PURE__ */ ((OrgRole2) => {
|
|
14
|
+
OrgRole2["OWNER"] = "OWNER";
|
|
15
|
+
OrgRole2["ADMIN"] = "ADMIN";
|
|
16
|
+
OrgRole2["MEMBER"] = "MEMBER";
|
|
17
|
+
return OrgRole2;
|
|
18
|
+
})(OrgRole || {});
|
|
19
|
+
|
|
20
|
+
// ../../packages/shared/src/constants.ts
|
|
21
|
+
var MAX_EMAIL_SIZE_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
var PASSWORD_MIN_LENGTH = 8;
|
|
23
|
+
var PASSWORD_MAX_LENGTH = 128;
|
|
24
|
+
var NAME_MAX_LENGTH = 100;
|
|
25
|
+
var ORG_NAME_MIN_LENGTH = 2;
|
|
26
|
+
var ORG_NAME_MAX_LENGTH = 255;
|
|
27
|
+
var LABEL_MAX_LENGTH = 255;
|
|
28
|
+
var MAX_ADDRESS_LENGTH = 64;
|
|
29
|
+
var ADDRESS_REGEX = /^[a-zA-Z0-9._-]+$/;
|
|
30
|
+
|
|
31
|
+
// ../../packages/shared/src/schemas/auth.schema.ts
|
|
32
|
+
import { z } from "zod";
|
|
33
|
+
var passwordField = z.string().min(PASSWORD_MIN_LENGTH, `Password must be at least ${PASSWORD_MIN_LENGTH} characters.`).max(PASSWORD_MAX_LENGTH, `Password must be at most ${PASSWORD_MAX_LENGTH} characters.`);
|
|
34
|
+
var emailField = z.string().email("Enter a valid email address.");
|
|
35
|
+
var nameField = z.string().trim().max(NAME_MAX_LENGTH, `Must be at most ${NAME_MAX_LENGTH} characters.`).optional().or(z.literal(""));
|
|
36
|
+
var loginSchema = z.object({
|
|
37
|
+
email: emailField,
|
|
38
|
+
password: z.string().min(1, "Password is required.")
|
|
39
|
+
});
|
|
40
|
+
var registerSchema = z.object({
|
|
41
|
+
firstName: nameField,
|
|
42
|
+
lastName: nameField,
|
|
43
|
+
email: emailField,
|
|
44
|
+
password: passwordField,
|
|
45
|
+
confirmPassword: z.string().min(1, "Confirm your password."),
|
|
46
|
+
organizationName: z.string().trim().min(ORG_NAME_MIN_LENGTH, `Organization name must be at least ${ORG_NAME_MIN_LENGTH} characters.`).max(ORG_NAME_MAX_LENGTH, `Organization name must be at most ${ORG_NAME_MAX_LENGTH} characters.`).optional().or(z.literal(""))
|
|
47
|
+
}).refine((d) => d.password === d.confirmPassword, {
|
|
48
|
+
path: ["confirmPassword"],
|
|
49
|
+
message: "Passwords do not match."
|
|
50
|
+
});
|
|
51
|
+
var changePasswordSchema = z.object({
|
|
52
|
+
currentPassword: z.string().min(1, "Current password is required."),
|
|
53
|
+
newPassword: passwordField,
|
|
54
|
+
confirmNewPassword: z.string().min(1, "Confirm your new password.")
|
|
55
|
+
}).refine((d) => d.newPassword === d.confirmNewPassword, {
|
|
56
|
+
path: ["confirmNewPassword"],
|
|
57
|
+
message: "Passwords do not match."
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ../../packages/shared/src/schemas/organization.schema.ts
|
|
61
|
+
import { z as z2 } from "zod";
|
|
62
|
+
var orgNameField = z2.string().trim().min(ORG_NAME_MIN_LENGTH, `Organization name must be at least ${ORG_NAME_MIN_LENGTH} characters.`).max(ORG_NAME_MAX_LENGTH, `Organization name must be at most ${ORG_NAME_MAX_LENGTH} characters.`);
|
|
63
|
+
var createOrgSchema = z2.object({
|
|
64
|
+
name: orgNameField
|
|
65
|
+
});
|
|
66
|
+
var updateOrgSchema = z2.object({
|
|
67
|
+
name: orgNameField
|
|
68
|
+
});
|
|
69
|
+
var inviteMemberSchema = z2.object({
|
|
70
|
+
email: z2.string().email("Enter a valid email address."),
|
|
71
|
+
role: z2.enum(["ADMIN" /* ADMIN */, "MEMBER" /* MEMBER */])
|
|
72
|
+
});
|
|
73
|
+
var updateMemberRoleSchema = z2.object({
|
|
74
|
+
role: z2.nativeEnum(OrgRole)
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ../../packages/shared/src/schemas/inbox.schema.ts
|
|
78
|
+
import { z as z3 } from "zod";
|
|
79
|
+
var createInboxSchema = z3.object({
|
|
80
|
+
address: z3.string().trim().max(MAX_ADDRESS_LENGTH, `Address must be at most ${MAX_ADDRESS_LENGTH} characters.`).regex(ADDRESS_REGEX, "Address may only contain letters, numbers, dots, dashes, and underscores.").optional().or(z3.literal("")),
|
|
81
|
+
label: z3.string().trim().max(LABEL_MAX_LENGTH, `Label must be at most ${LABEL_MAX_LENGTH} characters.`).optional().or(z3.literal(""))
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// src/server.ts
|
|
85
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
86
|
+
import {
|
|
87
|
+
CallToolRequestSchema,
|
|
88
|
+
ListToolsRequestSchema,
|
|
89
|
+
McpError as McpError2,
|
|
90
|
+
ErrorCode as ErrorCode2
|
|
91
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
92
|
+
|
|
93
|
+
// src/tools/discover/whoami.ts
|
|
94
|
+
import { z as z4 } from "zod";
|
|
95
|
+
|
|
96
|
+
// src/tools/define.ts
|
|
97
|
+
import { zodToJsonSchema as convertZodToJsonSchema } from "zod-to-json-schema";
|
|
98
|
+
function defineTool(definition) {
|
|
99
|
+
return definition;
|
|
100
|
+
}
|
|
101
|
+
function zodToJsonSchema(schema) {
|
|
102
|
+
const out = convertZodToJsonSchema(schema, {
|
|
103
|
+
target: "openApi3",
|
|
104
|
+
$refStrategy: "none"
|
|
105
|
+
});
|
|
106
|
+
delete out.$schema;
|
|
107
|
+
delete out.definitions;
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/tools/discover/whoami.ts
|
|
112
|
+
var DESCRIPTION = `
|
|
113
|
+
USE WHEN:
|
|
114
|
+
- You need to confirm the InstantInbox token is valid before running any
|
|
115
|
+
other tool.
|
|
116
|
+
- The user asks "who am I" or "am I logged in to InstantInbox".
|
|
117
|
+
- As a pre-flight before a long agentic workflow that creates inboxes
|
|
118
|
+
and waits for email.
|
|
119
|
+
|
|
120
|
+
DO NOT USE:
|
|
121
|
+
- More than once per session unless the token changes.
|
|
122
|
+
- To list inboxes \u2014 use list_inboxes for that.
|
|
123
|
+
|
|
124
|
+
TRIGGER PATTERNS:
|
|
125
|
+
- "check my token"
|
|
126
|
+
- "what inbox access do I have"
|
|
127
|
+
- "verify InstantInbox auth"
|
|
128
|
+
|
|
129
|
+
<examples>
|
|
130
|
+
<example>
|
|
131
|
+
<user>Am I logged in?</user>
|
|
132
|
+
<tool>whoami</tool>
|
|
133
|
+
<result>Authenticated as alice@example.com (user id: u_123). Granted scopes: inbox:read, inbox:write, email:read, email:write.</result>
|
|
134
|
+
</example>
|
|
135
|
+
</examples>
|
|
136
|
+
`.trim();
|
|
137
|
+
var whoamiTool = defineTool({
|
|
138
|
+
name: "whoami",
|
|
139
|
+
title: "Identify Current User",
|
|
140
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
141
|
+
skills: ["discover"],
|
|
142
|
+
requiredScopes: [],
|
|
143
|
+
description: DESCRIPTION,
|
|
144
|
+
inputSchema: z4.object({}),
|
|
145
|
+
async handler(_params, ctx) {
|
|
146
|
+
const me = await ctx.apiClient.whoami();
|
|
147
|
+
const scopes = ctx.scopes.toArray().join(", ") || "(none)";
|
|
148
|
+
if (!me) {
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: `Token valid but /auth/me returned no profile. Scopes: ${scopes}` }]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const lines = [
|
|
154
|
+
`Authenticated as ${me.email ?? "(unknown email)"} (user id: ${me.id})`,
|
|
155
|
+
`Scopes: ${scopes}`
|
|
156
|
+
];
|
|
157
|
+
if (me.isEmailVerified === false) {
|
|
158
|
+
lines.push("Warning: email is NOT verified \u2014 some write operations may be rejected.");
|
|
159
|
+
}
|
|
160
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// src/tools/inboxes/list-inboxes.ts
|
|
165
|
+
import { z as z5 } from "zod";
|
|
166
|
+
|
|
167
|
+
// src/errors.ts
|
|
168
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
169
|
+
var UserInputError = class extends McpError {
|
|
170
|
+
constructor(message) {
|
|
171
|
+
super(ErrorCode.InvalidParams, message);
|
|
172
|
+
this.name = "UserInputError";
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var ApiNotFoundError = class extends McpError {
|
|
176
|
+
constructor(resource, id) {
|
|
177
|
+
super(ErrorCode.InvalidParams, `${resource} not found: ${id}`);
|
|
178
|
+
this.name = "ApiNotFoundError";
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var ScopeError = class extends McpError {
|
|
182
|
+
constructor(requiredScope) {
|
|
183
|
+
super(ErrorCode.InvalidRequest, `Token missing required scope: ${requiredScope}`);
|
|
184
|
+
this.name = "ScopeError";
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var ApiError = class extends McpError {
|
|
188
|
+
constructor(message, statusCode) {
|
|
189
|
+
super(ErrorCode.InternalError, `InstantInbox API error: ${message}`);
|
|
190
|
+
this.statusCode = statusCode;
|
|
191
|
+
this.name = "ApiError";
|
|
192
|
+
}
|
|
193
|
+
statusCode;
|
|
194
|
+
};
|
|
195
|
+
var AuthError = class extends McpError {
|
|
196
|
+
constructor(reason, detail) {
|
|
197
|
+
super(ErrorCode.InvalidRequest, detail ? `${reason}: ${detail}` : reason);
|
|
198
|
+
this.reason = reason;
|
|
199
|
+
this.name = "AuthError";
|
|
200
|
+
}
|
|
201
|
+
reason;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/auth/scopes.ts
|
|
205
|
+
var ScopeSet = class {
|
|
206
|
+
set;
|
|
207
|
+
constructor(scopes) {
|
|
208
|
+
this.set = new Set(scopes);
|
|
209
|
+
}
|
|
210
|
+
has(scope) {
|
|
211
|
+
return this.set.has(scope);
|
|
212
|
+
}
|
|
213
|
+
hasAll(scopes) {
|
|
214
|
+
return scopes.every((s) => this.set.has(s));
|
|
215
|
+
}
|
|
216
|
+
toArray() {
|
|
217
|
+
return [...this.set];
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
function assertScope(scopes, required) {
|
|
221
|
+
if (!scopes.has(required)) {
|
|
222
|
+
throw new ScopeError(required);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/formatting/inbox.ts
|
|
227
|
+
function formatInbox(inbox) {
|
|
228
|
+
const meta = inbox;
|
|
229
|
+
const lines = [
|
|
230
|
+
`Inbox: ${inbox.address}`,
|
|
231
|
+
` id: ${inbox.id}`
|
|
232
|
+
];
|
|
233
|
+
if (inbox.label) lines.push(` label: ${inbox.label}`);
|
|
234
|
+
if (typeof meta.emailCount === "number") lines.push(` emails: ${meta.emailCount}`);
|
|
235
|
+
if (meta.lastReceivedAt) {
|
|
236
|
+
lines.push(` last: ${meta.lastSubject ?? "(no subject)"} <${meta.lastFrom ?? "?"}> @ ${meta.lastReceivedAt}`);
|
|
237
|
+
}
|
|
238
|
+
lines.push(` created: ${inbox.createdAt}`);
|
|
239
|
+
return lines.join("\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/tools/inboxes/list-inboxes.ts
|
|
243
|
+
var DESCRIPTION2 = `
|
|
244
|
+
USE WHEN:
|
|
245
|
+
- You need to find an existing disposable inbox the caller already owns.
|
|
246
|
+
- The user asks "what inboxes do I have" or "show my test inboxes".
|
|
247
|
+
- You want to confirm an inbox exists before sending email to it.
|
|
248
|
+
|
|
249
|
+
DO NOT USE:
|
|
250
|
+
- To create a new inbox \u2014 use create_inbox.
|
|
251
|
+
- To read messages \u2014 use list_emails or wait_for_email.
|
|
252
|
+
|
|
253
|
+
REQUIRED SCOPE: inbox:read
|
|
254
|
+
|
|
255
|
+
<examples>
|
|
256
|
+
<example>
|
|
257
|
+
<user>List my inboxes</user>
|
|
258
|
+
<tool>list_inboxes</tool>
|
|
259
|
+
<result>Inbox: ab12cd@inbox.koderlabs.net (emails: 3, last: "Verify your email" ...)</result>
|
|
260
|
+
</example>
|
|
261
|
+
</examples>
|
|
262
|
+
`.trim();
|
|
263
|
+
var listInboxesTool = defineTool({
|
|
264
|
+
name: "list_inboxes",
|
|
265
|
+
title: "List Inboxes",
|
|
266
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
267
|
+
skills: ["inboxes"],
|
|
268
|
+
requiredScopes: [SCOPE.INBOX_READ],
|
|
269
|
+
description: DESCRIPTION2,
|
|
270
|
+
inputSchema: z5.object({
|
|
271
|
+
organizationId: z5.string().optional().describe("Filter to inboxes in this organization"),
|
|
272
|
+
search: z5.string().optional().describe("Substring match on address or label"),
|
|
273
|
+
limit: z5.number().int().min(1).max(100).optional().describe("Default 20, max 100")
|
|
274
|
+
}),
|
|
275
|
+
async handler(params, ctx) {
|
|
276
|
+
assertScope(ctx.scopes, SCOPE.INBOX_READ);
|
|
277
|
+
const { items, meta } = await ctx.apiClient.listInboxes({
|
|
278
|
+
organizationId: params.organizationId,
|
|
279
|
+
search: params.search,
|
|
280
|
+
limit: params.limit ?? 20
|
|
281
|
+
});
|
|
282
|
+
if (items.length === 0) {
|
|
283
|
+
return { content: [{ type: "text", text: "No inboxes found. Use create_inbox to make one." }] };
|
|
284
|
+
}
|
|
285
|
+
const text = [
|
|
286
|
+
`Found ${meta.total} inbox(es), showing ${items.length}:`,
|
|
287
|
+
"",
|
|
288
|
+
...items.map(formatInbox)
|
|
289
|
+
].join("\n");
|
|
290
|
+
return { content: [{ type: "text", text }] };
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// src/tools/inboxes/create-inbox.ts
|
|
295
|
+
import { z as z6 } from "zod";
|
|
296
|
+
var DESCRIPTION3 = `
|
|
297
|
+
USE WHEN:
|
|
298
|
+
- You are about to test an email-driven flow (signup verification, password
|
|
299
|
+
reset, magic link, etc.) and need a fresh disposable inbox to plug into
|
|
300
|
+
the app under test.
|
|
301
|
+
- The user asks to "create a test inbox" or "give me a throwaway email".
|
|
302
|
+
|
|
303
|
+
WHAT YOU GET BACK:
|
|
304
|
+
- The full email address (e.g. ab12cd@inbox.koderlabs.net) \u2014 this is the
|
|
305
|
+
string you plug into the signup form, profile, etc.
|
|
306
|
+
- The inbox id (UUID) \u2014 use this with list_emails / wait_for_email.
|
|
307
|
+
|
|
308
|
+
NEXT STEP after calling this tool:
|
|
309
|
+
- Trigger the flow that should send mail to this address.
|
|
310
|
+
- Then call wait_for_email with this inbox's id.
|
|
311
|
+
|
|
312
|
+
REQUIRED SCOPE: inbox:write
|
|
313
|
+
|
|
314
|
+
<examples>
|
|
315
|
+
<example>
|
|
316
|
+
<user>Create a disposable inbox for testing the signup flow.</user>
|
|
317
|
+
<tool>create_inbox {}</tool>
|
|
318
|
+
<result>Created inbox: ab12cd@inbox.koderlabs.net (id u_abc). Use this address in the app, then call wait_for_email.</result>
|
|
319
|
+
</example>
|
|
320
|
+
</examples>
|
|
321
|
+
`.trim();
|
|
322
|
+
var createInboxTool = defineTool({
|
|
323
|
+
name: "create_inbox",
|
|
324
|
+
title: "Create Disposable Inbox",
|
|
325
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
326
|
+
skills: ["inboxes"],
|
|
327
|
+
requiredScopes: [SCOPE.INBOX_WRITE],
|
|
328
|
+
description: DESCRIPTION3,
|
|
329
|
+
inputSchema: z6.object({
|
|
330
|
+
address: z6.string().optional().describe('Local-part only (e.g. "alice"). Omit to auto-generate a random address.'),
|
|
331
|
+
label: z6.string().optional().describe("Human-readable label for this inbox"),
|
|
332
|
+
organizationId: z6.string().optional().describe("Scope to an organization; omit for personal inbox")
|
|
333
|
+
}),
|
|
334
|
+
async handler(params, ctx) {
|
|
335
|
+
assertScope(ctx.scopes, SCOPE.INBOX_WRITE);
|
|
336
|
+
const inbox = await ctx.apiClient.createInbox({
|
|
337
|
+
address: params.address,
|
|
338
|
+
label: params.label,
|
|
339
|
+
organizationId: params.organizationId
|
|
340
|
+
});
|
|
341
|
+
const text = [
|
|
342
|
+
`Inbox created: ${inbox.address}`,
|
|
343
|
+
` id: ${inbox.id}`,
|
|
344
|
+
params.label ? ` label: ${params.label}` : null,
|
|
345
|
+
"",
|
|
346
|
+
`Next step: trigger the email flow with this address, then call wait_for_email with inboxId=${inbox.id}.`
|
|
347
|
+
].filter(Boolean).join("\n");
|
|
348
|
+
return {
|
|
349
|
+
content: [{ type: "text", text }],
|
|
350
|
+
_meta: {
|
|
351
|
+
"instantinbox/inboxId": inbox.id,
|
|
352
|
+
"instantinbox/address": inbox.address
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// src/tools/inboxes/get-inbox.ts
|
|
359
|
+
import { z as z7 } from "zod";
|
|
360
|
+
var DESCRIPTION4 = `
|
|
361
|
+
USE WHEN:
|
|
362
|
+
- You have an inbox id OR address and want its current state (label, counts,
|
|
363
|
+
last received email).
|
|
364
|
+
- You need to translate an address back into an inbox id before calling
|
|
365
|
+
list_emails / wait_for_email.
|
|
366
|
+
|
|
367
|
+
DO NOT USE to list multiple inboxes (use list_inboxes) or to fetch emails
|
|
368
|
+
(use list_emails or get_email).
|
|
369
|
+
|
|
370
|
+
REQUIRED SCOPE: inbox:read
|
|
371
|
+
`.trim();
|
|
372
|
+
var getInboxTool = defineTool({
|
|
373
|
+
name: "get_inbox",
|
|
374
|
+
title: "Get Inbox Details",
|
|
375
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
376
|
+
skills: ["inboxes"],
|
|
377
|
+
requiredScopes: [SCOPE.INBOX_READ],
|
|
378
|
+
description: DESCRIPTION4,
|
|
379
|
+
inputSchema: z7.object({
|
|
380
|
+
inboxId: z7.string().optional().describe("Inbox UUID. One of inboxId or address required."),
|
|
381
|
+
address: z7.string().optional().describe("Full email address. One of inboxId or address required.")
|
|
382
|
+
}),
|
|
383
|
+
async handler(params, ctx) {
|
|
384
|
+
assertScope(ctx.scopes, SCOPE.INBOX_READ);
|
|
385
|
+
if (!params.inboxId && !params.address) {
|
|
386
|
+
throw new UserInputError("Provide either inboxId or address.");
|
|
387
|
+
}
|
|
388
|
+
const inbox = params.inboxId ? await ctx.apiClient.getInboxById(params.inboxId) : await ctx.apiClient.getInboxByAddress(params.address);
|
|
389
|
+
if (!inbox) throw new ApiNotFoundError("Inbox", params.inboxId ?? params.address);
|
|
390
|
+
return {
|
|
391
|
+
content: [{ type: "text", text: formatInbox(inbox) }],
|
|
392
|
+
_meta: { "instantinbox/inboxId": inbox.id, "instantinbox/address": inbox.address }
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// src/tools/inboxes/delete-inbox.ts
|
|
398
|
+
import { z as z8 } from "zod";
|
|
399
|
+
var DESCRIPTION5 = `
|
|
400
|
+
USE WHEN:
|
|
401
|
+
- A test run is complete and the disposable inbox is no longer needed.
|
|
402
|
+
- The user asks to "delete this inbox" or "clean up the test inbox".
|
|
403
|
+
|
|
404
|
+
DESTRUCTIVE: This permanently deletes the inbox AND every email it ever
|
|
405
|
+
received. There is no recovery. Idempotent \u2014 re-calling with the same id
|
|
406
|
+
after deletion returns 404 from the API which this tool reports cleanly.
|
|
407
|
+
|
|
408
|
+
REQUIRED SCOPE: inbox:write
|
|
409
|
+
`.trim();
|
|
410
|
+
var deleteInboxTool = defineTool({
|
|
411
|
+
name: "delete_inbox",
|
|
412
|
+
title: "Delete Inbox",
|
|
413
|
+
annotations: { destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
414
|
+
skills: ["inboxes"],
|
|
415
|
+
requiredScopes: [SCOPE.INBOX_WRITE],
|
|
416
|
+
description: DESCRIPTION5,
|
|
417
|
+
inputSchema: z8.object({
|
|
418
|
+
inboxId: z8.string().describe("Inbox UUID")
|
|
419
|
+
}),
|
|
420
|
+
async handler(params, ctx) {
|
|
421
|
+
assertScope(ctx.scopes, SCOPE.INBOX_WRITE);
|
|
422
|
+
await ctx.apiClient.deleteInbox(params.inboxId);
|
|
423
|
+
return {
|
|
424
|
+
content: [{ type: "text", text: `Inbox ${params.inboxId} deleted (and all its emails).` }]
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// src/tools/emails/list-emails.ts
|
|
430
|
+
import { z as z9 } from "zod";
|
|
431
|
+
|
|
432
|
+
// src/formatting/email.ts
|
|
433
|
+
function snippet(text, max = 240) {
|
|
434
|
+
if (!text) return "";
|
|
435
|
+
const t = text.replace(/\s+/g, " ").trim();
|
|
436
|
+
return t.length > max ? `${t.slice(0, max)}\u2026` : t;
|
|
437
|
+
}
|
|
438
|
+
function formatEmailHeader(email) {
|
|
439
|
+
const read = email.readAt ? " " : "*";
|
|
440
|
+
return `${read} [${email.receivedAt}] ${email.fromAddress} \u2192 ${email.subject || "(no subject)"} (${email.id})`;
|
|
441
|
+
}
|
|
442
|
+
function formatEmail(email) {
|
|
443
|
+
const lines = [
|
|
444
|
+
`Email ${email.id}`,
|
|
445
|
+
`From: ${email.fromAddress}`,
|
|
446
|
+
`To: ${email.toAddress}`,
|
|
447
|
+
`Subject: ${email.subject || "(no subject)"}`,
|
|
448
|
+
`Date: ${email.receivedAt}`,
|
|
449
|
+
`Read: ${email.readAt ?? "no"}`,
|
|
450
|
+
`Attachments: ${email.attachmentsCount}`,
|
|
451
|
+
""
|
|
452
|
+
];
|
|
453
|
+
if (email.textBody) {
|
|
454
|
+
lines.push("--- text body ---", email.textBody);
|
|
455
|
+
} else if (email.htmlBody) {
|
|
456
|
+
lines.push("--- html body (text fallback unavailable) ---", snippet(email.htmlBody, 4e3));
|
|
457
|
+
} else {
|
|
458
|
+
lines.push("(no body)");
|
|
459
|
+
}
|
|
460
|
+
return lines.join("\n");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/tools/emails/list-emails.ts
|
|
464
|
+
var DESCRIPTION6 = `
|
|
465
|
+
USE WHEN:
|
|
466
|
+
- You want to see what mail has already arrived in an inbox.
|
|
467
|
+
- The user asks "what emails did I receive" or "show me the inbox".
|
|
468
|
+
|
|
469
|
+
DO NOT USE:
|
|
470
|
+
- To poll for a NOT-YET-ARRIVED email \u2014 use wait_for_email which long-polls
|
|
471
|
+
efficiently. Repeatedly calling list_emails in a tight loop wastes tokens
|
|
472
|
+
and load on the API.
|
|
473
|
+
- To get the full body \u2014 use get_email; this tool only returns headers
|
|
474
|
+
(subject / from / date).
|
|
475
|
+
|
|
476
|
+
REQUIRED SCOPE: email:read
|
|
477
|
+
`.trim();
|
|
478
|
+
var listEmailsTool = defineTool({
|
|
479
|
+
name: "list_emails",
|
|
480
|
+
title: "List Emails in Inbox",
|
|
481
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
482
|
+
skills: ["emails"],
|
|
483
|
+
requiredScopes: [SCOPE.EMAIL_READ, SCOPE.INBOX_READ],
|
|
484
|
+
description: DESCRIPTION6,
|
|
485
|
+
inputSchema: z9.object({
|
|
486
|
+
inboxId: z9.string().optional().describe("Inbox UUID. One of inboxId or address required."),
|
|
487
|
+
address: z9.string().optional().describe("Full inbox email address. One of inboxId or address required."),
|
|
488
|
+
limit: z9.number().int().min(1).max(100).optional().describe("Default 20, max 100"),
|
|
489
|
+
unreadOnly: z9.boolean().optional().describe("Filter to unread emails only"),
|
|
490
|
+
since: z9.string().optional().describe("ISO timestamp \u2014 only emails received after this time")
|
|
491
|
+
}),
|
|
492
|
+
async handler(params, ctx) {
|
|
493
|
+
assertScope(ctx.scopes, SCOPE.EMAIL_READ);
|
|
494
|
+
if (!params.inboxId && !params.address) {
|
|
495
|
+
throw new UserInputError("Provide either inboxId or address.");
|
|
496
|
+
}
|
|
497
|
+
let inboxId = params.inboxId;
|
|
498
|
+
if (!inboxId) {
|
|
499
|
+
const inbox = await ctx.apiClient.getInboxByAddress(params.address);
|
|
500
|
+
if (!inbox) throw new ApiNotFoundError("Inbox", params.address);
|
|
501
|
+
inboxId = inbox.id;
|
|
502
|
+
}
|
|
503
|
+
const { items, meta } = await ctx.apiClient.listEmails(inboxId, {
|
|
504
|
+
limit: params.limit ?? 20,
|
|
505
|
+
unreadOnly: params.unreadOnly,
|
|
506
|
+
since: params.since
|
|
507
|
+
});
|
|
508
|
+
if (items.length === 0) {
|
|
509
|
+
return {
|
|
510
|
+
content: [{
|
|
511
|
+
type: "text",
|
|
512
|
+
text: "No emails in this inbox yet. If you are waiting for one, use wait_for_email to long-poll efficiently."
|
|
513
|
+
}]
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const text = [
|
|
517
|
+
`Found ${meta.total} email(s), showing ${items.length} (\`*\` = unread):`,
|
|
518
|
+
...items.map(formatEmailHeader)
|
|
519
|
+
].join("\n");
|
|
520
|
+
return { content: [{ type: "text", text }] };
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// src/tools/emails/get-email.ts
|
|
525
|
+
import { z as z10 } from "zod";
|
|
526
|
+
var DESCRIPTION7 = `
|
|
527
|
+
USE WHEN:
|
|
528
|
+
- You have an email id (from list_emails or wait_for_email) and need the
|
|
529
|
+
full body to extract a verification link, OTP code, or password reset
|
|
530
|
+
token.
|
|
531
|
+
|
|
532
|
+
DO NOT USE to scan the inbox (use list_emails).
|
|
533
|
+
|
|
534
|
+
REQUIRED SCOPE: email:read
|
|
535
|
+
`.trim();
|
|
536
|
+
var getEmailTool = defineTool({
|
|
537
|
+
name: "get_email",
|
|
538
|
+
title: "Get Email",
|
|
539
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
540
|
+
skills: ["emails"],
|
|
541
|
+
requiredScopes: [SCOPE.EMAIL_READ],
|
|
542
|
+
description: DESCRIPTION7,
|
|
543
|
+
inputSchema: z10.object({
|
|
544
|
+
emailId: z10.string().describe("Email UUID")
|
|
545
|
+
}),
|
|
546
|
+
async handler(params, ctx) {
|
|
547
|
+
assertScope(ctx.scopes, SCOPE.EMAIL_READ);
|
|
548
|
+
const email = await ctx.apiClient.getEmail(params.emailId);
|
|
549
|
+
if (!email) throw new ApiNotFoundError("Email", params.emailId);
|
|
550
|
+
return {
|
|
551
|
+
content: [{ type: "text", text: formatEmail(email) }],
|
|
552
|
+
_meta: { "instantinbox/emailId": email.id, "instantinbox/inboxId": email.inboxId }
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// src/tools/emails/wait-for-email.ts
|
|
558
|
+
import { z as z11 } from "zod";
|
|
559
|
+
var DESCRIPTION8 = `
|
|
560
|
+
USE WHEN:
|
|
561
|
+
- You have just triggered an action that should send mail (signup, password
|
|
562
|
+
reset, magic link, OTP) and you need to wait for it to arrive.
|
|
563
|
+
- This is the canonical "did the email arrive?" tool \u2014 long-polls the API
|
|
564
|
+
every second up to \`timeoutSec\` (default 30s, max 120s) so the agent
|
|
565
|
+
blocks exactly as long as needed and no longer.
|
|
566
|
+
|
|
567
|
+
ARGS:
|
|
568
|
+
- inboxId OR address \u2014 required.
|
|
569
|
+
- timeoutSec \u2014 how long to wait (1\u2013120, default 30).
|
|
570
|
+
- subjectContains / fromContains \u2014 optional case-insensitive filters; the
|
|
571
|
+
first email matching both is returned.
|
|
572
|
+
- sinceMs \u2014 only consider emails received after Date.now() - sinceMs.
|
|
573
|
+
Defaults to (timeoutSec + 5) seconds. Use this to avoid matching emails
|
|
574
|
+
that arrived before the trigger.
|
|
575
|
+
|
|
576
|
+
RETURNS:
|
|
577
|
+
- The full email (subject, from, body, headers) when one arrives.
|
|
578
|
+
- \`{ timedOut: true, matched: 0 }\` if nothing arrives in time.
|
|
579
|
+
|
|
580
|
+
DO NOT USE for general inbox browsing (use list_emails) \u2014 wait_for_email
|
|
581
|
+
is meant for the "trigger \u2192 wait \u2192 extract" loop in E2E tests.
|
|
582
|
+
|
|
583
|
+
REQUIRED SCOPE: email:read
|
|
584
|
+
|
|
585
|
+
<examples>
|
|
586
|
+
<example>
|
|
587
|
+
<user>I signed up. Wait for the verification email.</user>
|
|
588
|
+
<tool>wait_for_email { inboxId, subjectContains: "verify", timeoutSec: 60 }</tool>
|
|
589
|
+
<result>Email arrived from no-reply@example.com \u2014 "Verify your email". Body contains link https://...</result>
|
|
590
|
+
</example>
|
|
591
|
+
</examples>
|
|
592
|
+
`.trim();
|
|
593
|
+
var waitForEmailTool = defineTool({
|
|
594
|
+
name: "wait_for_email",
|
|
595
|
+
title: "Wait For Email",
|
|
596
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
597
|
+
skills: ["emails"],
|
|
598
|
+
requiredScopes: [SCOPE.EMAIL_READ, SCOPE.INBOX_READ],
|
|
599
|
+
description: DESCRIPTION8,
|
|
600
|
+
inputSchema: z11.object({
|
|
601
|
+
inboxId: z11.string().optional().describe("Inbox UUID. One of inboxId or address required."),
|
|
602
|
+
address: z11.string().optional().describe("Full inbox email address. One of inboxId or address required."),
|
|
603
|
+
timeoutSec: z11.number().int().min(1).max(120).optional().describe("Max wait, default 30s, hard cap 120s"),
|
|
604
|
+
subjectContains: z11.string().optional().describe("Case-insensitive substring match on subject"),
|
|
605
|
+
fromContains: z11.string().optional().describe("Case-insensitive substring match on from address"),
|
|
606
|
+
sinceMs: z11.number().int().min(0).optional().describe("Only match emails received within the last N ms (default: timeoutSec + 5s)")
|
|
607
|
+
}),
|
|
608
|
+
async handler(params, ctx) {
|
|
609
|
+
assertScope(ctx.scopes, SCOPE.EMAIL_READ);
|
|
610
|
+
if (!params.inboxId && !params.address) {
|
|
611
|
+
throw new UserInputError("Provide either inboxId or address.");
|
|
612
|
+
}
|
|
613
|
+
let inboxId = params.inboxId;
|
|
614
|
+
if (!inboxId) {
|
|
615
|
+
const inbox = await ctx.apiClient.getInboxByAddress(params.address);
|
|
616
|
+
if (!inbox) throw new ApiNotFoundError("Inbox", params.address);
|
|
617
|
+
inboxId = inbox.id;
|
|
618
|
+
}
|
|
619
|
+
const timeoutSec = Math.min(params.timeoutSec ?? 30, 120);
|
|
620
|
+
const sinceMs = params.sinceMs ?? (timeoutSec + 5) * 1e3;
|
|
621
|
+
const startedAt = Date.now();
|
|
622
|
+
const sinceIso = new Date(startedAt - sinceMs).toISOString();
|
|
623
|
+
const deadline = startedAt + timeoutSec * 1e3;
|
|
624
|
+
const subjectMatch = params.subjectContains?.toLowerCase();
|
|
625
|
+
const fromMatch = params.fromContains?.toLowerCase();
|
|
626
|
+
const pollIntervalMs = 1e3;
|
|
627
|
+
while (Date.now() < deadline) {
|
|
628
|
+
const { items } = await ctx.apiClient.listEmails(inboxId, {
|
|
629
|
+
limit: 50,
|
|
630
|
+
since: sinceIso
|
|
631
|
+
});
|
|
632
|
+
const match = items.find((e) => {
|
|
633
|
+
if (subjectMatch && !(e.subject ?? "").toLowerCase().includes(subjectMatch)) return false;
|
|
634
|
+
if (fromMatch && !(e.fromAddress ?? "").toLowerCase().includes(fromMatch)) return false;
|
|
635
|
+
return true;
|
|
636
|
+
});
|
|
637
|
+
if (match) {
|
|
638
|
+
const full = await ctx.apiClient.getEmail(match.id) ?? match;
|
|
639
|
+
const elapsedSec = ((Date.now() - startedAt) / 1e3).toFixed(1);
|
|
640
|
+
return {
|
|
641
|
+
content: [{
|
|
642
|
+
type: "text",
|
|
643
|
+
text: `Matched after ${elapsedSec}s.
|
|
644
|
+
|
|
645
|
+
${formatEmail(full)}`
|
|
646
|
+
}],
|
|
647
|
+
_meta: {
|
|
648
|
+
"instantinbox/emailId": full.id,
|
|
649
|
+
"instantinbox/inboxId": inboxId,
|
|
650
|
+
"instantinbox/elapsedSec": Number(elapsedSec)
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const remaining = deadline - Date.now();
|
|
655
|
+
if (remaining <= 0) break;
|
|
656
|
+
await new Promise((r) => setTimeout(r, Math.min(pollIntervalMs, remaining)));
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
content: [{
|
|
660
|
+
type: "text",
|
|
661
|
+
text: `Timed out after ${timeoutSec}s. No matching email arrived. Inbox: ${inboxId}.`
|
|
662
|
+
}],
|
|
663
|
+
_meta: { "instantinbox/timedOut": true, "instantinbox/matched": 0 }
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// src/tools/emails/delete-email.ts
|
|
669
|
+
import { z as z12 } from "zod";
|
|
670
|
+
var DESCRIPTION9 = `
|
|
671
|
+
USE WHEN:
|
|
672
|
+
- A test run is complete and you want to clear specific emails from the
|
|
673
|
+
inbox before the next iteration.
|
|
674
|
+
|
|
675
|
+
DESTRUCTIVE: deletes a single email permanently. Idempotent \u2014 re-calling
|
|
676
|
+
with the same id after deletion is safe.
|
|
677
|
+
|
|
678
|
+
DO NOT USE to delete an entire inbox (use delete_inbox).
|
|
679
|
+
|
|
680
|
+
REQUIRED SCOPE: email:write
|
|
681
|
+
`.trim();
|
|
682
|
+
var deleteEmailTool = defineTool({
|
|
683
|
+
name: "delete_email",
|
|
684
|
+
title: "Delete Email",
|
|
685
|
+
annotations: { destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
686
|
+
skills: ["emails"],
|
|
687
|
+
requiredScopes: [SCOPE.EMAIL_WRITE],
|
|
688
|
+
description: DESCRIPTION9,
|
|
689
|
+
inputSchema: z12.object({
|
|
690
|
+
emailId: z12.string().describe("Email UUID")
|
|
691
|
+
}),
|
|
692
|
+
async handler(params, ctx) {
|
|
693
|
+
assertScope(ctx.scopes, SCOPE.EMAIL_WRITE);
|
|
694
|
+
await ctx.apiClient.deleteEmail(params.emailId);
|
|
695
|
+
return { content: [{ type: "text", text: `Email ${params.emailId} deleted.` }] };
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// src/tools/emails/mark-email-read.ts
|
|
700
|
+
import { z as z13 } from "zod";
|
|
701
|
+
var DESCRIPTION10 = `
|
|
702
|
+
USE WHEN:
|
|
703
|
+
- You want to mark an email as read or unread (e.g. so list_emails with
|
|
704
|
+
unreadOnly returns the expected set after you've processed one).
|
|
705
|
+
|
|
706
|
+
Idempotent. Not destructive.
|
|
707
|
+
|
|
708
|
+
REQUIRED SCOPE: email:write
|
|
709
|
+
`.trim();
|
|
710
|
+
var markEmailReadTool = defineTool({
|
|
711
|
+
name: "mark_email_read",
|
|
712
|
+
title: "Mark Email Read/Unread",
|
|
713
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
714
|
+
skills: ["emails"],
|
|
715
|
+
requiredScopes: [SCOPE.EMAIL_WRITE],
|
|
716
|
+
description: DESCRIPTION10,
|
|
717
|
+
inputSchema: z13.object({
|
|
718
|
+
emailId: z13.string().describe("Email UUID"),
|
|
719
|
+
read: z13.boolean().describe("true \u2192 mark read; false \u2192 mark unread")
|
|
720
|
+
}),
|
|
721
|
+
async handler(params, ctx) {
|
|
722
|
+
assertScope(ctx.scopes, SCOPE.EMAIL_WRITE);
|
|
723
|
+
const updated = params.read ? await ctx.apiClient.markRead(params.emailId) : await ctx.apiClient.markUnread(params.emailId);
|
|
724
|
+
return {
|
|
725
|
+
content: [{
|
|
726
|
+
type: "text",
|
|
727
|
+
text: `Email ${params.emailId} marked ${params.read ? "read" : "unread"} (readAt=${updated.readAt ?? "null"}).`
|
|
728
|
+
}]
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// src/tools/index.ts
|
|
734
|
+
var ALL_TOOLS = [
|
|
735
|
+
whoamiTool,
|
|
736
|
+
listInboxesTool,
|
|
737
|
+
createInboxTool,
|
|
738
|
+
getInboxTool,
|
|
739
|
+
deleteInboxTool,
|
|
740
|
+
listEmailsTool,
|
|
741
|
+
getEmailTool,
|
|
742
|
+
waitForEmailTool,
|
|
743
|
+
deleteEmailTool,
|
|
744
|
+
markEmailReadTool
|
|
745
|
+
];
|
|
746
|
+
function toolsForScopes(scopeList) {
|
|
747
|
+
const set = new Set(scopeList);
|
|
748
|
+
return ALL_TOOLS.filter((t) => t.requiredScopes.every((s) => set.has(s)));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/server.ts
|
|
752
|
+
var SERVER_NAME = "instantinbox-mcp";
|
|
753
|
+
var SERVER_VERSION = "0.1.0";
|
|
754
|
+
function createMcpServer(getContext) {
|
|
755
|
+
const server = new Server(
|
|
756
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
757
|
+
{ capabilities: { tools: { listChanged: false } } }
|
|
758
|
+
);
|
|
759
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
760
|
+
const ctx = await getContext();
|
|
761
|
+
const available = toolsForScopes(ctx.scopes.toArray());
|
|
762
|
+
return {
|
|
763
|
+
tools: available.map((tool) => ({
|
|
764
|
+
name: tool.name,
|
|
765
|
+
title: tool.title,
|
|
766
|
+
description: tool.description,
|
|
767
|
+
inputSchema: zodToJsonSchema(tool.inputSchema),
|
|
768
|
+
...tool.annotations && { annotations: tool.annotations },
|
|
769
|
+
...tool.meta && { _meta: tool.meta }
|
|
770
|
+
}))
|
|
771
|
+
};
|
|
772
|
+
});
|
|
773
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
774
|
+
const { name, arguments: rawArgs } = request.params;
|
|
775
|
+
const tool = ALL_TOOLS.find((t) => t.name === name);
|
|
776
|
+
if (!tool) {
|
|
777
|
+
throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
|
|
778
|
+
}
|
|
779
|
+
const ctx = await getContext();
|
|
780
|
+
for (const scope of tool.requiredScopes) {
|
|
781
|
+
if (!ctx.scopes.has(scope)) {
|
|
782
|
+
throw new McpError2(
|
|
783
|
+
ErrorCode2.InvalidRequest,
|
|
784
|
+
`Token missing required scope '${scope}' for tool '${name}'`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const parseResult = tool.inputSchema.safeParse(rawArgs ?? {});
|
|
789
|
+
if (!parseResult.success) {
|
|
790
|
+
throw new McpError2(
|
|
791
|
+
ErrorCode2.InvalidParams,
|
|
792
|
+
`Invalid parameters for tool '${name}': ${parseResult.error.message}`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
const result = await tool.handler(parseResult.data, {
|
|
796
|
+
apiClient: ctx.apiClient,
|
|
797
|
+
scopes: ctx.scopes
|
|
798
|
+
});
|
|
799
|
+
return {
|
|
800
|
+
content: result.content,
|
|
801
|
+
isError: result.isError ?? false,
|
|
802
|
+
...result._meta && { _meta: result._meta }
|
|
803
|
+
};
|
|
804
|
+
});
|
|
805
|
+
return server;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/api-client.ts
|
|
809
|
+
var ApiClient = class {
|
|
810
|
+
baseUrl;
|
|
811
|
+
headers;
|
|
812
|
+
constructor(opts) {
|
|
813
|
+
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
814
|
+
this.headers = {
|
|
815
|
+
Authorization: `Bearer ${opts.token}`,
|
|
816
|
+
"Content-Type": "application/json",
|
|
817
|
+
Accept: "application/json"
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/** Unwrap `{ success, data, meta }` → either `data` or `{ items, meta }`. */
|
|
821
|
+
unwrap(json) {
|
|
822
|
+
if (json && typeof json === "object" && "success" in json && "data" in json) {
|
|
823
|
+
const data = json.data;
|
|
824
|
+
if (Array.isArray(data)) {
|
|
825
|
+
return { items: data, meta: json.meta };
|
|
826
|
+
}
|
|
827
|
+
return data;
|
|
828
|
+
}
|
|
829
|
+
return json;
|
|
830
|
+
}
|
|
831
|
+
async request(method, path, body, query) {
|
|
832
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
833
|
+
if (query) {
|
|
834
|
+
for (const [k, v] of Object.entries(query)) {
|
|
835
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
let res;
|
|
839
|
+
try {
|
|
840
|
+
res = await fetch(url.toString(), {
|
|
841
|
+
method,
|
|
842
|
+
headers: this.headers,
|
|
843
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
844
|
+
signal: AbortSignal.timeout(15e3)
|
|
845
|
+
});
|
|
846
|
+
} catch (err) {
|
|
847
|
+
throw new ApiError(`Network error: ${err.message}`);
|
|
848
|
+
}
|
|
849
|
+
if (method === "GET" && res.status === 404) return null;
|
|
850
|
+
if (!res.ok) {
|
|
851
|
+
const text = await res.text().catch(() => "");
|
|
852
|
+
throw new ApiError(`${res.status} ${res.statusText}: ${text}`, res.status);
|
|
853
|
+
}
|
|
854
|
+
if (res.status === 204) return null;
|
|
855
|
+
return this.unwrap(await res.json());
|
|
856
|
+
}
|
|
857
|
+
// ── Auth / identity ─────────────────────────────────────────────────────
|
|
858
|
+
async whoami() {
|
|
859
|
+
return this.request("GET", "/auth/me");
|
|
860
|
+
}
|
|
861
|
+
// ── Inboxes ─────────────────────────────────────────────────────────────
|
|
862
|
+
async listInboxes(query = {}) {
|
|
863
|
+
const result = await this.request(
|
|
864
|
+
"GET",
|
|
865
|
+
"/inboxes",
|
|
866
|
+
void 0,
|
|
867
|
+
query
|
|
868
|
+
);
|
|
869
|
+
return result ?? { items: [], meta: { total: 0, page: 1, limit: 20, totalPages: 0 } };
|
|
870
|
+
}
|
|
871
|
+
async createInbox(body) {
|
|
872
|
+
const result = await this.request("POST", "/inboxes", body);
|
|
873
|
+
if (!result) throw new ApiError("createInbox returned no body");
|
|
874
|
+
return result;
|
|
875
|
+
}
|
|
876
|
+
async getInboxById(id) {
|
|
877
|
+
return this.request("GET", `/inboxes/${id}`);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* The API has no GET /inboxes?address=... filter; emulate by listing and
|
|
881
|
+
* matching client-side. Good enough at our volumes.
|
|
882
|
+
*/
|
|
883
|
+
async getInboxByAddress(address) {
|
|
884
|
+
const target = address.toLowerCase();
|
|
885
|
+
const { items } = await this.listInboxes({ limit: 100 });
|
|
886
|
+
return items.find((i) => i.address.toLowerCase() === target) ?? null;
|
|
887
|
+
}
|
|
888
|
+
async deleteInbox(id) {
|
|
889
|
+
await this.request("DELETE", `/inboxes/${id}`);
|
|
890
|
+
}
|
|
891
|
+
// ── Emails ──────────────────────────────────────────────────────────────
|
|
892
|
+
async listEmails(inboxId, query = {}) {
|
|
893
|
+
const result = await this.request(
|
|
894
|
+
"GET",
|
|
895
|
+
`/inboxes/${inboxId}/emails`,
|
|
896
|
+
void 0,
|
|
897
|
+
query
|
|
898
|
+
);
|
|
899
|
+
return result ?? { items: [], meta: { total: 0, page: 1, limit: 20, totalPages: 0 } };
|
|
900
|
+
}
|
|
901
|
+
async getEmail(id) {
|
|
902
|
+
return this.request("GET", `/emails/${id}`);
|
|
903
|
+
}
|
|
904
|
+
async deleteEmail(id) {
|
|
905
|
+
await this.request("DELETE", `/emails/${id}`);
|
|
906
|
+
}
|
|
907
|
+
async markRead(id) {
|
|
908
|
+
const r = await this.request("PATCH", `/emails/${id}/read`, {});
|
|
909
|
+
if (!r) throw new ApiError("markRead returned no body");
|
|
910
|
+
return r;
|
|
911
|
+
}
|
|
912
|
+
async markUnread(id) {
|
|
913
|
+
const r = await this.request("PATCH", `/emails/${id}/unread`, {});
|
|
914
|
+
if (!r) throw new ApiError("markUnread returned no body");
|
|
915
|
+
return r;
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// src/auth/secret-key.ts
|
|
920
|
+
function extractBearerToken(authHeader) {
|
|
921
|
+
if (!authHeader?.startsWith("Bearer ")) return null;
|
|
922
|
+
const token = authHeader.slice(7).trim();
|
|
923
|
+
return token || null;
|
|
924
|
+
}
|
|
925
|
+
async function validateSecretKey(token, apiBaseUrl) {
|
|
926
|
+
const url = `${apiBaseUrl}/auth/me`;
|
|
927
|
+
let res;
|
|
928
|
+
try {
|
|
929
|
+
res = await fetch(url, {
|
|
930
|
+
method: "GET",
|
|
931
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
932
|
+
signal: AbortSignal.timeout(5e3)
|
|
933
|
+
});
|
|
934
|
+
} catch (err) {
|
|
935
|
+
throw new ApiError(`Cannot reach InstantInbox API: ${err.message}`);
|
|
936
|
+
}
|
|
937
|
+
if (!res.ok) {
|
|
938
|
+
throw new AuthError("invalid_token", `/auth/me returned ${res.status}`);
|
|
939
|
+
}
|
|
940
|
+
let body;
|
|
941
|
+
try {
|
|
942
|
+
body = await res.json();
|
|
943
|
+
} catch (err) {
|
|
944
|
+
throw new ApiError(`Malformed /auth/me response: ${err.message}`);
|
|
945
|
+
}
|
|
946
|
+
const data = body.data ?? body;
|
|
947
|
+
if (!data?.id) {
|
|
948
|
+
throw new AuthError("invalid_token", "/auth/me response missing user id");
|
|
949
|
+
}
|
|
950
|
+
void SCOPE;
|
|
951
|
+
return {
|
|
952
|
+
userId: data.id,
|
|
953
|
+
email: data.email,
|
|
954
|
+
scopes: new ScopeSet([...ALL_SCOPES]),
|
|
955
|
+
kind: token.startsWith("sk_") ? "secret-key" : "jwt"
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export {
|
|
960
|
+
ALL_SCOPES,
|
|
961
|
+
ApiError,
|
|
962
|
+
AuthError,
|
|
963
|
+
ScopeSet,
|
|
964
|
+
createMcpServer,
|
|
965
|
+
ApiClient,
|
|
966
|
+
extractBearerToken,
|
|
967
|
+
validateSecretKey
|
|
968
|
+
};
|
|
969
|
+
//# sourceMappingURL=chunk-T7SFANFP.js.map
|