@relayrail/server 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/dist/index.js ADDED
@@ -0,0 +1,2213 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+
8
+ // ../database/dist/index.js
9
+ import { createClient } from "@supabase/supabase-js";
10
+ function createServiceClient(url, serviceRoleKey) {
11
+ return createClient(url, serviceRoleKey, {
12
+ auth: {
13
+ autoRefreshToken: false,
14
+ persistSession: false
15
+ }
16
+ });
17
+ }
18
+
19
+ // ../shared/src/index.ts
20
+ var TIER_LIMITS = {
21
+ free: {
22
+ emailsPerMonth: 100,
23
+ smsPerMonth: 0,
24
+ maxAgents: 1,
25
+ historyDays: 7,
26
+ smsEnabled: false
27
+ },
28
+ pro: {
29
+ emailsPerMonth: 1e3,
30
+ smsPerMonth: 100,
31
+ maxAgents: 5,
32
+ historyDays: 30,
33
+ smsEnabled: true
34
+ },
35
+ team: {
36
+ emailsPerMonth: 1e4,
37
+ smsPerMonth: 1e3,
38
+ maxAgents: 25,
39
+ historyDays: 90,
40
+ smsEnabled: true
41
+ }
42
+ };
43
+ function getTierLimits(tier) {
44
+ return TIER_LIMITS[tier] || TIER_LIMITS.free;
45
+ }
46
+ var API_KEY_PREFIX = "rr_";
47
+ var REQUEST_ID_LENGTH = 8;
48
+ var DEFAULT_TIMEOUT_MINUTES = 60;
49
+ var MAX_TIMEOUT_MINUTES = 1440;
50
+ var SMS_OVERAGE_CENTS = 2;
51
+ var EMAIL_OVERAGE_CENTS = 1;
52
+ function sanitizeBaseUrl(url) {
53
+ return url.replace(/\/+$/, "");
54
+ }
55
+ function joinUrl(base, ...parts) {
56
+ const cleanBase = sanitizeBaseUrl(base);
57
+ const cleanParts = parts.map((p) => p.replace(/^\/+|\/+$/g, ""));
58
+ return [cleanBase, ...cleanParts].join("/");
59
+ }
60
+ function buildUrl(base, path, params) {
61
+ const url = joinUrl(base, path);
62
+ if (!params || Object.keys(params).length === 0) {
63
+ return url;
64
+ }
65
+ const queryString = Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
66
+ return `${url}?${queryString}`;
67
+ }
68
+ function checkEmailQuota(tier, emailsSent, allowOverage, baseUrl) {
69
+ const limits = getTierLimits(tier);
70
+ const limit = limits.emailsPerMonth;
71
+ const remaining = Math.max(0, limit - emailsSent);
72
+ const withinIncluded = emailsSent < limit;
73
+ const isOverage = !withinIncluded;
74
+ const allowed = withinIncluded || isOverage && allowOverage;
75
+ const result = {
76
+ allowed,
77
+ withinIncluded,
78
+ isOverage,
79
+ current: emailsSent,
80
+ limit,
81
+ remaining,
82
+ tier
83
+ };
84
+ if (isOverage) {
85
+ result.overageRate = EMAIL_OVERAGE_CENTS / 100;
86
+ if (allowOverage) {
87
+ result.message = `Email sent as overage. You will be charged $${(EMAIL_OVERAGE_CENTS / 100).toFixed(2)} for this message.`;
88
+ } else {
89
+ result.message = `Email limit reached (${emailsSent}/${limit}). Overage charges are disabled in your settings.`;
90
+ const cleanBase = sanitizeBaseUrl(baseUrl);
91
+ result.enableOverageUrl = `${cleanBase}/settings?section=billing`;
92
+ result.upgradeUrl = `${cleanBase}/pricing?upgrade=true&reason=email_limit`;
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+ function checkSmsQuota(tier, smsSent, allowOverage, baseUrl) {
98
+ const limits = getTierLimits(tier);
99
+ const limit = limits.smsPerMonth;
100
+ if (tier === "free" || !limits.smsEnabled) {
101
+ const cleanBase = sanitizeBaseUrl(baseUrl);
102
+ return {
103
+ allowed: false,
104
+ withinIncluded: false,
105
+ isOverage: false,
106
+ current: smsSent,
107
+ limit: 0,
108
+ remaining: 0,
109
+ tier,
110
+ message: "SMS is not available on the Free tier. Upgrade to Pro for 100 SMS/month.",
111
+ upgradeUrl: `${cleanBase}/pricing?upgrade=true&reason=sms_required`
112
+ };
113
+ }
114
+ const remaining = Math.max(0, limit - smsSent);
115
+ const withinIncluded = smsSent < limit;
116
+ const isOverage = !withinIncluded;
117
+ const allowed = withinIncluded || isOverage && allowOverage;
118
+ const result = {
119
+ allowed,
120
+ withinIncluded,
121
+ isOverage,
122
+ current: smsSent,
123
+ limit,
124
+ remaining,
125
+ tier
126
+ };
127
+ if (isOverage) {
128
+ result.overageRate = SMS_OVERAGE_CENTS / 100;
129
+ if (allowOverage) {
130
+ result.message = `SMS sent as overage. You will be charged $${(SMS_OVERAGE_CENTS / 100).toFixed(2)} for this message.`;
131
+ } else {
132
+ result.message = `SMS limit reached (${smsSent}/${limit}). Overage charges are disabled in your settings.`;
133
+ const cleanBase = sanitizeBaseUrl(baseUrl);
134
+ result.enableOverageUrl = `${cleanBase}/settings?section=billing`;
135
+ result.upgradeUrl = `${cleanBase}/pricing?upgrade=true&reason=sms_limit`;
136
+ }
137
+ }
138
+ return result;
139
+ }
140
+
141
+ // src/auth.ts
142
+ import { createHash } from "crypto";
143
+ function hashApiKey(apiKey) {
144
+ return createHash("sha256").update(apiKey).digest("hex");
145
+ }
146
+ function getApiKeyPrefix(apiKey) {
147
+ if (!apiKey.startsWith(API_KEY_PREFIX)) {
148
+ return "";
149
+ }
150
+ return apiKey.substring(0, API_KEY_PREFIX.length + 8);
151
+ }
152
+ async function authenticateApiKey(supabase, apiKey) {
153
+ if (!apiKey || !apiKey.startsWith(API_KEY_PREFIX)) {
154
+ return {
155
+ success: false,
156
+ error: "Invalid API key format",
157
+ authError: {
158
+ code: "INVALID_FORMAT",
159
+ message: "Invalid API key format",
160
+ hint: `API keys must start with "${API_KEY_PREFIX}". Use the register_agent tool to get a valid API key, or check your Claude Desktop configuration.`
161
+ }
162
+ };
163
+ }
164
+ const expectedMinLength = 35;
165
+ if (apiKey.length < expectedMinLength) {
166
+ return {
167
+ success: false,
168
+ error: "API key appears truncated",
169
+ authError: {
170
+ code: "KEY_TRUNCATED",
171
+ message: "API key appears truncated",
172
+ hint: `Expected at least ${expectedMinLength} characters, but received ${apiKey.length}. Make sure you copied the complete API key. It should look like: rr_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`
173
+ }
174
+ };
175
+ }
176
+ const prefix = getApiKeyPrefix(apiKey);
177
+ const hash = hashApiKey(apiKey);
178
+ const agentQuery = supabase.from("agents").select("*").eq("api_key_prefix", prefix);
179
+ const agentResult = await agentQuery;
180
+ if (agentResult.error || !agentResult.data || agentResult.data.length === 0) {
181
+ return {
182
+ success: false,
183
+ error: "API key not found",
184
+ authError: {
185
+ code: "KEY_NOT_FOUND",
186
+ message: "API key not found",
187
+ hint: "This API key does not exist in our system. It may have been deleted, or was never created. Use the register_agent tool to create a new API key."
188
+ }
189
+ };
190
+ }
191
+ const agent = agentResult.data.find((a) => a.api_key_hash === hash);
192
+ if (!agent) {
193
+ return {
194
+ success: false,
195
+ error: "API key verification failed",
196
+ authError: {
197
+ code: "HASH_MISMATCH",
198
+ message: "API key verification failed",
199
+ hint: 'The API key prefix was recognized, but the full key verification failed. This usually means the key was corrupted or truncated. Please check that you copied the complete API key, including all characters after "rr_".'
200
+ }
201
+ };
202
+ }
203
+ if (!agent.is_active) {
204
+ return {
205
+ success: false,
206
+ error: "API key is deactivated",
207
+ authError: {
208
+ code: "KEY_INACTIVE",
209
+ message: "API key is deactivated",
210
+ hint: "This API key has been deactivated. Visit your RelayRail dashboard to reactivate this agent or create a new one."
211
+ }
212
+ };
213
+ }
214
+ const userQuery = supabase.from("users").select("*").eq("id", agent.user_id);
215
+ const userResult = await userQuery;
216
+ if (userResult.error || !userResult.data || userResult.data.length === 0) {
217
+ return {
218
+ success: false,
219
+ error: "User account not found",
220
+ authError: {
221
+ code: "USER_NOT_FOUND",
222
+ message: "User account not found",
223
+ hint: "The API key is valid, but the associated user account was not found. This may indicate a data synchronization issue. Please contact support or try creating a new account."
224
+ }
225
+ };
226
+ }
227
+ const user = userResult.data[0];
228
+ const updateData = { last_seen_at: (/* @__PURE__ */ new Date()).toISOString() };
229
+ const updateQuery = supabase.from("agents").update(updateData).eq("id", agent.id);
230
+ await updateQuery;
231
+ return {
232
+ success: true,
233
+ agent,
234
+ user
235
+ };
236
+ }
237
+ function generateApiKey() {
238
+ const randomBytes = new Uint8Array(24);
239
+ crypto.getRandomValues(randomBytes);
240
+ const keyPortion = Buffer.from(randomBytes).toString("base64url");
241
+ const key = `${API_KEY_PREFIX}${keyPortion}`;
242
+ const prefix = getApiKeyPrefix(key);
243
+ const hash = hashApiKey(key);
244
+ return { key, prefix, hash };
245
+ }
246
+
247
+ // src/tools/request-id.ts
248
+ import { nanoid } from "nanoid";
249
+ function generateRequestId() {
250
+ return nanoid(REQUEST_ID_LENGTH);
251
+ }
252
+ function generateResponseToken() {
253
+ return nanoid(32);
254
+ }
255
+
256
+ // src/tools/request-approval.ts
257
+ function createUserContext(user) {
258
+ return {
259
+ email: user.email,
260
+ tier: user.tier,
261
+ preferred_channel: user.notification_preferences?.preferred_channel || "email"
262
+ };
263
+ }
264
+ async function requestApproval(params, context) {
265
+ const { supabase, agent, user, router } = context;
266
+ const {
267
+ message,
268
+ options,
269
+ context: requestContext,
270
+ timeout_minutes = DEFAULT_TIMEOUT_MINUTES,
271
+ severity = "info"
272
+ } = params;
273
+ const requestId = generateRequestId();
274
+ const responseToken = generateResponseToken();
275
+ const expiresAt = new Date(Date.now() + timeout_minutes * 60 * 1e3);
276
+ const requestData = {
277
+ id: requestId,
278
+ agent_id: agent.id,
279
+ type: "approval",
280
+ status: "pending",
281
+ payload: {
282
+ message,
283
+ options,
284
+ context: requestContext,
285
+ timeout_minutes,
286
+ severity
287
+ },
288
+ response_token: responseToken,
289
+ expires_at: expiresAt.toISOString()
290
+ };
291
+ const insertResult = await supabase.from("requests").insert(requestData);
292
+ if (insertResult.error) {
293
+ throw new Error(`Failed to create request: ${insertResult.error.message}`);
294
+ }
295
+ if (router) {
296
+ const routeResult = await router.routeApproval(
297
+ {
298
+ requestId,
299
+ responseToken,
300
+ message,
301
+ options,
302
+ expiresAt: expiresAt.toISOString(),
303
+ severity
304
+ },
305
+ user,
306
+ agent
307
+ );
308
+ if (routeResult.quotaError) {
309
+ await supabase.from("requests").update({ status: "expired" }).eq("id", requestId);
310
+ return {
311
+ request_id: requestId,
312
+ status: "blocked",
313
+ expires_at: expiresAt.toISOString(),
314
+ user: createUserContext(user),
315
+ quota_error: routeResult.quotaError
316
+ };
317
+ }
318
+ if (!routeResult.success) {
319
+ console.warn(`[RelayRail] Failed to send approval notification:`, routeResult.error);
320
+ }
321
+ return {
322
+ request_id: requestId,
323
+ status: "pending",
324
+ expires_at: expiresAt.toISOString(),
325
+ user: createUserContext(user),
326
+ quota: routeResult.quota
327
+ };
328
+ }
329
+ console.log(`[RelayRail] Approval request created:`, {
330
+ requestId,
331
+ user: user.email,
332
+ agent: agent.name
333
+ });
334
+ return {
335
+ request_id: requestId,
336
+ status: "pending",
337
+ expires_at: expiresAt.toISOString(),
338
+ user: createUserContext(user)
339
+ };
340
+ }
341
+
342
+ // src/tools/send-notification.ts
343
+ function createUserContext2(user) {
344
+ return {
345
+ email: user.email,
346
+ tier: user.tier,
347
+ preferred_channel: user.notification_preferences?.preferred_channel || "email"
348
+ };
349
+ }
350
+ async function sendNotification(params, context) {
351
+ const { supabase, agent, user, router } = context;
352
+ const {
353
+ message,
354
+ context: notificationContext,
355
+ severity = "info"
356
+ } = params;
357
+ const requestId = generateRequestId();
358
+ const preferredChannel = user.notification_preferences?.preferred_channel || "email";
359
+ const requestData = {
360
+ id: requestId,
361
+ agent_id: agent.id,
362
+ type: "notification",
363
+ status: "pending",
364
+ payload: {
365
+ message,
366
+ context: notificationContext,
367
+ severity
368
+ },
369
+ // No response token needed for notifications
370
+ response_token: null,
371
+ expires_at: null
372
+ // Notifications don't expire
373
+ };
374
+ const insertResult = await supabase.from("requests").insert(requestData);
375
+ if (insertResult.error) {
376
+ throw new Error(`Failed to create notification: ${insertResult.error.message}`);
377
+ }
378
+ let channel = preferredChannel;
379
+ if (router) {
380
+ const routeResult = await router.routeNotification(
381
+ {
382
+ requestId,
383
+ message,
384
+ severity
385
+ },
386
+ user,
387
+ agent
388
+ );
389
+ channel = routeResult.channel;
390
+ if (routeResult.quotaError) {
391
+ await supabase.from("requests").update({ status: "expired" }).eq("id", requestId);
392
+ return {
393
+ request_id: requestId,
394
+ status: "blocked",
395
+ channel,
396
+ user: createUserContext2(user),
397
+ quota_error: routeResult.quotaError
398
+ };
399
+ }
400
+ if (!routeResult.success) {
401
+ console.warn(`[RelayRail] Failed to send notification:`, routeResult.error);
402
+ return {
403
+ request_id: requestId,
404
+ status: "failed",
405
+ channel,
406
+ user: createUserContext2(user)
407
+ };
408
+ }
409
+ return {
410
+ request_id: requestId,
411
+ status: "pending",
412
+ channel,
413
+ user: createUserContext2(user),
414
+ quota: routeResult.quota
415
+ };
416
+ }
417
+ console.log(`[RelayRail] Notification sent:`, {
418
+ requestId,
419
+ channel,
420
+ user: user.email,
421
+ agent: agent.name,
422
+ severity
423
+ });
424
+ return {
425
+ request_id: requestId,
426
+ status: "pending",
427
+ channel,
428
+ user: createUserContext2(user)
429
+ };
430
+ }
431
+
432
+ // src/tools/await-response.ts
433
+ var DEFAULT_TIMEOUT_SECONDS = 30;
434
+ var POLL_INTERVAL_MS = 1e3;
435
+ async function awaitResponse(params, context) {
436
+ const { supabase, agent } = context;
437
+ const {
438
+ request_id,
439
+ timeout_seconds = DEFAULT_TIMEOUT_SECONDS
440
+ } = params;
441
+ const startTime = Date.now();
442
+ const timeoutMs = timeout_seconds * 1e3;
443
+ while (Date.now() - startTime < timeoutMs) {
444
+ const requestResult = await supabase.from("requests").select("*").eq("id", request_id).eq("agent_id", agent.id);
445
+ const result = requestResult;
446
+ if (result.error || !result.data || result.data.length === 0) {
447
+ return {
448
+ request_id,
449
+ status: "pending",
450
+ timed_out: false
451
+ };
452
+ }
453
+ const request = result.data[0];
454
+ if (request.status !== "pending") {
455
+ return {
456
+ request_id,
457
+ status: request.status,
458
+ response: request.response,
459
+ timed_out: false
460
+ };
461
+ }
462
+ if (request.expires_at && new Date(request.expires_at) < /* @__PURE__ */ new Date()) {
463
+ await supabase.from("requests").update({ status: "expired" }).eq("id", request_id);
464
+ return {
465
+ request_id,
466
+ status: "expired",
467
+ timed_out: false
468
+ };
469
+ }
470
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
471
+ }
472
+ return {
473
+ request_id,
474
+ status: "pending",
475
+ timed_out: true
476
+ };
477
+ }
478
+
479
+ // src/tools/get-pending-commands.ts
480
+ var DEFAULT_LIMIT = 10;
481
+ async function getPendingCommands(params, context) {
482
+ const { supabase, agent } = context;
483
+ const { limit = DEFAULT_LIMIT } = params;
484
+ const requestResult = await supabase.from("requests").select("*, messages(*)").eq("agent_id", agent.id).eq("type", "command").eq("status", "pending").order("created_at", { ascending: false }).limit(limit);
485
+ const result = requestResult;
486
+ if (result.error || !result.data) {
487
+ return { commands: [] };
488
+ }
489
+ const commands = result.data.map((request) => ({
490
+ request_id: request.id,
491
+ message: request.payload.message,
492
+ received_at: request.created_at,
493
+ channel: request.messages?.[0]?.channel || "email"
494
+ }));
495
+ if (commands.length > 0) {
496
+ const commandIds = commands.map((c) => c.request_id);
497
+ await supabase.from("requests").update({ status: "delivered" }).in("id", commandIds);
498
+ }
499
+ return { commands };
500
+ }
501
+
502
+ // src/tools/register-agent.ts
503
+ async function registerAgent(params, context) {
504
+ const { supabase, baseUrl } = context;
505
+ const { name, user_email, auto_create = true } = params;
506
+ const cleanBaseUrl = sanitizeBaseUrl(baseUrl);
507
+ if (!name || name.trim().length === 0) {
508
+ return {
509
+ success: false,
510
+ error: "Agent name is required"
511
+ };
512
+ }
513
+ if (!user_email || !user_email.includes("@")) {
514
+ return {
515
+ success: false,
516
+ error: "Valid user email is required"
517
+ };
518
+ }
519
+ const normalizedEmail = user_email.toLowerCase().trim();
520
+ const userQuery = supabase.from("users").select("*").eq("email", normalizedEmail);
521
+ const userResult = await userQuery;
522
+ if (userResult.error) {
523
+ return {
524
+ success: false,
525
+ error: `Database error: ${userResult.error.message}`
526
+ };
527
+ }
528
+ let user;
529
+ let accountCreated = false;
530
+ if (!userResult.data || userResult.data.length === 0) {
531
+ if (!auto_create) {
532
+ const signupUrl = buildUrl(cleanBaseUrl, "signup", { email: normalizedEmail });
533
+ const docsUrl = buildUrl(cleanBaseUrl, "docs/troubleshooting");
534
+ return {
535
+ success: false,
536
+ error: `No account found for "${normalizedEmail}".`,
537
+ signup_url: signupUrl,
538
+ instructions: `To use RelayRail, you need an account:
539
+
540
+ 1. SIGN UP: Visit ${signupUrl}
541
+ 2. VERIFY: Check your email and click the verification link
542
+ 3. RETURN: Run register_agent again with the same email
543
+
544
+ If you already signed up:
545
+ - Check for typos in the email address
546
+ - Make sure you completed email verification
547
+ - Allow up to 60 seconds for account creation to complete
548
+
549
+ Need help? Visit ${docsUrl}`
550
+ };
551
+ }
552
+ const newUserId = crypto.randomUUID();
553
+ const createUserResult = await supabase.from("users").insert({
554
+ id: newUserId,
555
+ email: normalizedEmail,
556
+ tier: "free",
557
+ notification_preferences: { preferred_channel: "email" },
558
+ email_verified: false
559
+ }).select("*").single();
560
+ if (createUserResult.error) {
561
+ if (createUserResult.error.message.includes("duplicate") || createUserResult.error.message.includes("unique")) {
562
+ const retryQuery = supabase.from("users").select("*").eq("email", normalizedEmail).single();
563
+ const retryResult = await retryQuery;
564
+ if (retryResult.error || !retryResult.data) {
565
+ return {
566
+ success: false,
567
+ error: `Failed to create or find account: ${createUserResult.error.message}`
568
+ };
569
+ }
570
+ user = retryResult.data;
571
+ } else {
572
+ return {
573
+ success: false,
574
+ error: `Failed to create account: ${createUserResult.error.message}`
575
+ };
576
+ }
577
+ } else {
578
+ user = createUserResult.data;
579
+ accountCreated = true;
580
+ }
581
+ } else {
582
+ user = userResult.data[0];
583
+ }
584
+ const { key, prefix, hash } = generateApiKey();
585
+ const agentData = {
586
+ user_id: user.id,
587
+ name: name.trim(),
588
+ api_key_hash: hash,
589
+ api_key_prefix: prefix,
590
+ is_active: true,
591
+ metadata: {
592
+ registered_via: "mcp_tool",
593
+ registered_at: (/* @__PURE__ */ new Date()).toISOString(),
594
+ auto_created_account: accountCreated
595
+ }
596
+ };
597
+ const insertResult = await supabase.from("agents").insert(agentData).select("id").single();
598
+ if (insertResult.error) {
599
+ return {
600
+ success: false,
601
+ error: `Failed to create agent: ${insertResult.error.message}`
602
+ };
603
+ }
604
+ const agentId = insertResult.data.id;
605
+ const usageResult = await supabase.rpc("get_or_create_usage", {
606
+ p_user_id: user.id
607
+ });
608
+ const usage = usageResult.data;
609
+ const tier = user.tier;
610
+ const limits = getTierLimits(tier);
611
+ const emailsUsed = usage?.emails_sent ?? 0;
612
+ const smsUsed = usage?.sms_sent ?? 0;
613
+ const channelsAvailable = ["email"];
614
+ if (tier !== "free" && limits.smsEnabled && user.phone) {
615
+ channelsAvailable.push("sms");
616
+ }
617
+ const accountStatus = {
618
+ email: user.email,
619
+ tier,
620
+ email_verified: user.email_verified ?? false,
621
+ channels_available: channelsAvailable,
622
+ quota: {
623
+ email: {
624
+ used: emailsUsed,
625
+ limit: limits.emailsPerMonth,
626
+ remaining: Math.max(0, limits.emailsPerMonth - emailsUsed)
627
+ }
628
+ }
629
+ };
630
+ if (tier !== "free") {
631
+ accountStatus.quota.sms = {
632
+ used: smsUsed,
633
+ limit: limits.smsPerMonth,
634
+ remaining: Math.max(0, limits.smsPerMonth - smsUsed)
635
+ };
636
+ }
637
+ const nextSteps = [];
638
+ if (accountCreated) {
639
+ nextSteps.push(`New account created for ${normalizedEmail} on Free tier.`);
640
+ }
641
+ nextSteps.push(`Save your API key: ${key}`);
642
+ nextSteps.push(`Try: send_notification({message: "Hello from RelayRail!"})`);
643
+ if (tier === "free") {
644
+ nextSteps.push(`Upgrade to Pro for SMS and 1000 emails/month: ${cleanBaseUrl}/pricing`);
645
+ }
646
+ if (!user.email_verified) {
647
+ nextSteps.push(`Verify your email for full features: Check inbox for verification link.`);
648
+ }
649
+ const channelInfo = channelsAvailable.includes("sms") ? "email and SMS" : "email only (upgrade to Pro for SMS)";
650
+ const quotaInfo = tier === "free" ? `${limits.emailsPerMonth} emails/month` : `${limits.emailsPerMonth} emails + ${limits.smsPerMonth} SMS/month`;
651
+ return {
652
+ success: true,
653
+ api_key: key,
654
+ agent_id: agentId,
655
+ account_status: accountStatus,
656
+ account_created: accountCreated,
657
+ next_steps: nextSteps,
658
+ instructions: `Agent "${name}" registered successfully!
659
+
660
+ ACCOUNT: ${user.email} (${tier} tier)
661
+ CHANNELS: ${channelInfo}
662
+ QUOTA: ${quotaInfo}
663
+
664
+ IMPORTANT: Save this API key - it will NOT be shown again:
665
+ ${key}
666
+
667
+ QUICK START:
668
+ send_notification({message: "Test from RelayRail"})
669
+
670
+ MCP CONFIG:
671
+ {
672
+ "mcpServers": {
673
+ "relayrail": {
674
+ "command": "npx",
675
+ "args": ["@relayrail/server", "start"],
676
+ "env": { "RELAYRAIL_API_KEY": "${key}" }
677
+ }
678
+ }
679
+ }
680
+
681
+ AVAILABLE TOOLS:
682
+ - send_notification: Send one-way messages
683
+ - request_approval: Ask user to approve/reject
684
+ - await_response: Wait for user reply
685
+ - get_pending_commands: Get commands from user
686
+ - get_account_status: Check quota and channels`
687
+ };
688
+ }
689
+
690
+ // src/tools/get-account-status.ts
691
+ async function getAccountStatus(context) {
692
+ const { supabase, agent, user } = context;
693
+ const usageResult = await supabase.rpc("get_or_create_usage", {
694
+ p_user_id: user.id
695
+ });
696
+ const usage = usageResult.data;
697
+ const tier = user.tier;
698
+ const limits = getTierLimits(tier);
699
+ const emailsUsed = usage?.emails_sent ?? 0;
700
+ const smsUsed = usage?.sms_sent ?? 0;
701
+ const channels = ["email"];
702
+ if (tier !== "free" && limits.smsEnabled && user.phone) {
703
+ channels.push("sms");
704
+ }
705
+ const tips = [];
706
+ if (tier === "free") {
707
+ tips.push("Upgrade to Pro for SMS notifications and 1000 emails/month.");
708
+ }
709
+ if (!user.phone && tier !== "free") {
710
+ tips.push("Add a phone number in settings to enable SMS notifications.");
711
+ }
712
+ const preferredChannel = user.notification_preferences?.preferred_channel || "email";
713
+ if (preferredChannel === "sms" && !channels.includes("sms")) {
714
+ tips.push("Your preferred channel is SMS but it's not available. Messages will be sent via email.");
715
+ }
716
+ const emailRemaining = Math.max(0, limits.emailsPerMonth - emailsUsed);
717
+ if (emailRemaining < 20) {
718
+ tips.push(`Low email quota: ${emailRemaining} remaining this month.`);
719
+ }
720
+ if (tips.length === 0) {
721
+ tips.push("Your account is ready to send notifications!");
722
+ }
723
+ return {
724
+ email: user.email,
725
+ tier,
726
+ email_verified: user.email_verified ?? false,
727
+ phone_configured: !!user.phone,
728
+ channels_available: channels,
729
+ preferred_channel: preferredChannel,
730
+ quota: {
731
+ email: {
732
+ used: emailsUsed,
733
+ limit: limits.emailsPerMonth,
734
+ remaining: emailRemaining,
735
+ overage_enabled: user.allow_email_overage ?? true
736
+ },
737
+ sms: {
738
+ used: smsUsed,
739
+ limit: limits.smsPerMonth,
740
+ remaining: Math.max(0, limits.smsPerMonth - smsUsed),
741
+ overage_enabled: user.allow_sms_overage ?? true,
742
+ available: limits.smsEnabled && !!user.phone
743
+ }
744
+ },
745
+ agent: {
746
+ id: agent.id,
747
+ name: agent.name,
748
+ created_at: agent.created_at
749
+ },
750
+ tips
751
+ };
752
+ }
753
+
754
+ // src/services/email.ts
755
+ import { Resend } from "resend";
756
+ import { encode } from "html-entities";
757
+ function escapeHtml(text) {
758
+ return encode(text);
759
+ }
760
+ function sanitizeUrl(url) {
761
+ try {
762
+ const parsed = new URL(url);
763
+ if (!["http:", "https:"].includes(parsed.protocol)) {
764
+ return "#";
765
+ }
766
+ return url;
767
+ } catch {
768
+ return "#";
769
+ }
770
+ }
771
+ var EmailService = class {
772
+ resend;
773
+ config;
774
+ supabase;
775
+ constructor(config, supabase) {
776
+ this.config = config;
777
+ this.supabase = supabase;
778
+ this.resend = new Resend(config.resendApiKey);
779
+ }
780
+ /**
781
+ * Send an approval request email
782
+ */
783
+ async sendApprovalEmail(params) {
784
+ const {
785
+ to,
786
+ requestId,
787
+ responseToken,
788
+ message,
789
+ options,
790
+ agentName,
791
+ expiresAt,
792
+ severity = "info"
793
+ } = params;
794
+ const responseUrl = sanitizeUrl(`${this.config.baseUrl}/respond/${encodeURIComponent(requestId)}?token=${encodeURIComponent(responseToken)}`);
795
+ const expiresDate = new Date(expiresAt).toLocaleString();
796
+ const safeMessage = escapeHtml(message);
797
+ const safeAgentName = escapeHtml(agentName);
798
+ const safeRequestId = escapeHtml(requestId);
799
+ const severityColors = {
800
+ info: "#3B82F6",
801
+ warning: "#F59E0B",
802
+ critical: "#EF4444"
803
+ };
804
+ const severityColor = severityColors[severity];
805
+ const optionsHtml = options?.length ? `
806
+ <p style="margin: 16px 0 8px 0; color: #6B7280;">Quick responses:</p>
807
+ <div style="display: flex; gap: 8px; flex-wrap: wrap;">
808
+ ${options.map((opt) => {
809
+ const safeOpt = escapeHtml(opt);
810
+ const optUrl = sanitizeUrl(`${responseUrl}&choice=${encodeURIComponent(opt)}`);
811
+ return `
812
+ <a href="${optUrl}"
813
+ style="display: inline-block; padding: 8px 16px; background: #F3F4F6; color: #374151; text-decoration: none; border-radius: 6px; font-size: 14px;">
814
+ ${safeOpt}
815
+ </a>
816
+ `;
817
+ }).join("")}
818
+ </div>
819
+ ` : "";
820
+ const html = `
821
+ <!DOCTYPE html>
822
+ <html>
823
+ <head>
824
+ <meta charset="utf-8">
825
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
826
+ </head>
827
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #F9FAFB; padding: 40px 20px;">
828
+ <div style="max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
829
+ <div style="padding: 24px; border-bottom: 1px solid #E5E7EB;">
830
+ <div style="display: flex; align-items: center; gap: 12px;">
831
+ <span style="display: inline-block; padding: 4px 8px; background: ${severityColor}20; color: ${severityColor}; border-radius: 4px; font-size: 12px; font-weight: 600; text-transform: uppercase;">
832
+ ${severity}
833
+ </span>
834
+ <span style="color: #6B7280; font-size: 14px;">from ${safeAgentName}</span>
835
+ </div>
836
+ </div>
837
+
838
+ <div style="padding: 24px;">
839
+ <h1 style="margin: 0 0 16px 0; font-size: 20px; color: #111827;">Approval Required</h1>
840
+ <p style="margin: 0 0 24px 0; color: #374151; line-height: 1.6; white-space: pre-wrap;">${safeMessage}</p>
841
+
842
+ <a href="${responseUrl}" style="display: inline-block; padding: 12px 24px; background: #2563EB; color: white; text-decoration: none; border-radius: 6px; font-weight: 500;">
843
+ Respond Now
844
+ </a>
845
+
846
+ ${optionsHtml}
847
+ </div>
848
+
849
+ <div style="padding: 16px 24px; background: #F9FAFB; border-top: 1px solid #E5E7EB; border-radius: 0 0 8px 8px;">
850
+ <p style="margin: 0; color: #9CA3AF; font-size: 12px;">
851
+ Request ID: ${safeRequestId} \u2022 Expires: ${escapeHtml(expiresDate)}
852
+ </p>
853
+ </div>
854
+ </div>
855
+
856
+ <p style="text-align: center; margin-top: 24px; color: #9CA3AF; font-size: 12px;">
857
+ Sent by <a href="${sanitizeUrl(this.config.baseUrl)}" style="color: #6B7280;">RelayRail</a>
858
+ </p>
859
+ </body>
860
+ </html>
861
+ `;
862
+ try {
863
+ const emailOptions = {
864
+ from: `${this.config.fromName} <${this.config.fromEmail}>`,
865
+ to: [to],
866
+ subject: `[${severity.toUpperCase()}] Approval needed: ${message.substring(0, 50)}${message.length > 50 ? "..." : ""}`,
867
+ html
868
+ };
869
+ if (this.config.inboundDomain) {
870
+ const replyToAddress = `request-${requestId}@${this.config.inboundDomain}`;
871
+ emailOptions.replyTo = replyToAddress;
872
+ }
873
+ const result = await this.resend.emails.send(emailOptions);
874
+ await this.logMessage({
875
+ request_id: requestId,
876
+ channel: "email",
877
+ direction: "outbound",
878
+ external_id: result.data?.id || null,
879
+ status: result.data?.id ? "sent" : "failed",
880
+ content: { to, subject: "Approval Request", type: "approval" },
881
+ error_message: result.error?.message || null
882
+ });
883
+ if (result.error) {
884
+ return { success: false, error: result.error.message };
885
+ }
886
+ return { success: true, messageId: result.data?.id };
887
+ } catch (error) {
888
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
889
+ await this.logMessage({
890
+ request_id: requestId,
891
+ channel: "email",
892
+ direction: "outbound",
893
+ external_id: null,
894
+ status: "failed",
895
+ content: { to, subject: "Approval Request", type: "approval" },
896
+ error_message: errorMessage
897
+ });
898
+ return { success: false, error: errorMessage };
899
+ }
900
+ }
901
+ /**
902
+ * Send a notification email
903
+ */
904
+ async sendNotificationEmail(params) {
905
+ const { to, requestId, message, agentName, severity = "info" } = params;
906
+ const safeMessage = escapeHtml(message);
907
+ const safeAgentName = escapeHtml(agentName);
908
+ const safeRequestId = escapeHtml(requestId);
909
+ const severityColors = {
910
+ info: "#3B82F6",
911
+ warning: "#F59E0B",
912
+ critical: "#EF4444"
913
+ };
914
+ const severityColor = severityColors[severity];
915
+ const html = `
916
+ <!DOCTYPE html>
917
+ <html>
918
+ <head>
919
+ <meta charset="utf-8">
920
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
921
+ </head>
922
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #F9FAFB; padding: 40px 20px;">
923
+ <div style="max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
924
+ <div style="padding: 24px; border-bottom: 1px solid #E5E7EB;">
925
+ <div style="display: flex; align-items: center; gap: 12px;">
926
+ <span style="display: inline-block; padding: 4px 8px; background: ${severityColor}20; color: ${severityColor}; border-radius: 4px; font-size: 12px; font-weight: 600; text-transform: uppercase;">
927
+ ${severity}
928
+ </span>
929
+ <span style="color: #6B7280; font-size: 14px;">from ${safeAgentName}</span>
930
+ </div>
931
+ </div>
932
+
933
+ <div style="padding: 24px;">
934
+ <h1 style="margin: 0 0 16px 0; font-size: 20px; color: #111827;">Notification</h1>
935
+ <p style="margin: 0; color: #374151; line-height: 1.6; white-space: pre-wrap;">${safeMessage}</p>
936
+ </div>
937
+
938
+ <div style="padding: 16px 24px; background: #F9FAFB; border-top: 1px solid #E5E7EB; border-radius: 0 0 8px 8px;">
939
+ <p style="margin: 0; color: #9CA3AF; font-size: 12px;">
940
+ Request ID: ${safeRequestId}
941
+ </p>
942
+ </div>
943
+ </div>
944
+
945
+ <p style="text-align: center; margin-top: 24px; color: #9CA3AF; font-size: 12px;">
946
+ Sent by <a href="${sanitizeUrl(this.config.baseUrl)}" style="color: #6B7280;">RelayRail</a>
947
+ </p>
948
+ </body>
949
+ </html>
950
+ `;
951
+ try {
952
+ const result = await this.resend.emails.send({
953
+ from: `${this.config.fromName} <${this.config.fromEmail}>`,
954
+ to: [to],
955
+ subject: `[${severity.toUpperCase()}] ${agentName}: ${message.substring(0, 50)}${message.length > 50 ? "..." : ""}`,
956
+ html
957
+ });
958
+ await this.logMessage({
959
+ request_id: requestId,
960
+ channel: "email",
961
+ direction: "outbound",
962
+ external_id: result.data?.id || null,
963
+ status: result.data?.id ? "sent" : "failed",
964
+ content: { to, subject: "Notification", type: "notification" },
965
+ error_message: result.error?.message || null
966
+ });
967
+ if (result.data?.id) {
968
+ await this.supabase.from("requests").update({ status: "delivered" }).eq("id", requestId);
969
+ }
970
+ if (result.error) {
971
+ return { success: false, error: result.error.message };
972
+ }
973
+ return { success: true, messageId: result.data?.id };
974
+ } catch (error) {
975
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
976
+ await this.logMessage({
977
+ request_id: requestId,
978
+ channel: "email",
979
+ direction: "outbound",
980
+ external_id: null,
981
+ status: "failed",
982
+ content: { to, subject: "Notification", type: "notification" },
983
+ error_message: errorMessage
984
+ });
985
+ return { success: false, error: errorMessage };
986
+ }
987
+ }
988
+ /**
989
+ * Log a message to the database
990
+ */
991
+ async logMessage(data) {
992
+ try {
993
+ await this.supabase.from("messages").insert(data);
994
+ } catch (error) {
995
+ console.error("[RelayRail] Failed to log message:", error);
996
+ }
997
+ }
998
+ };
999
+
1000
+ // src/services/sms.ts
1001
+ import { Telnyx } from "telnyx";
1002
+ var SmsService = class {
1003
+ client;
1004
+ config;
1005
+ supabase;
1006
+ constructor(config, supabase) {
1007
+ this.config = config;
1008
+ this.supabase = supabase;
1009
+ this.client = new Telnyx({ apiKey: config.apiKey });
1010
+ }
1011
+ /**
1012
+ * Send an approval request via SMS
1013
+ */
1014
+ async sendApprovalSms(params) {
1015
+ const {
1016
+ to,
1017
+ requestId,
1018
+ responseToken,
1019
+ message,
1020
+ options,
1021
+ agentName,
1022
+ expiresAt,
1023
+ severity = "info"
1024
+ } = params;
1025
+ const responseUrl = `${this.config.baseUrl}/respond/${requestId}?token=${responseToken}`;
1026
+ const expiresDate = new Date(expiresAt).toLocaleString();
1027
+ const severityPrefix = severity === "critical" ? "!" : severity === "warning" ? "?" : "";
1028
+ let body = `${severityPrefix}[${agentName}] Approval needed:
1029
+
1030
+ ${message}
1031
+
1032
+ Respond: ${responseUrl}`;
1033
+ if (options?.length) {
1034
+ body += `
1035
+
1036
+ Reply with: ${options.join(" / ")}`;
1037
+ }
1038
+ body += `
1039
+
1040
+ Expires: ${expiresDate}`;
1041
+ if (body.length > 1500) {
1042
+ body = body.substring(0, 1450) + "...\n\nTap link to respond: " + responseUrl;
1043
+ }
1044
+ const normalizedTo = this.normalizePhoneNumber(to);
1045
+ console.log(`[RelayRail SMS] Sending approval SMS:`, {
1046
+ requestId,
1047
+ from: this.config.fromNumber,
1048
+ to: normalizedTo,
1049
+ bodyLength: body.length
1050
+ });
1051
+ try {
1052
+ const result = await this.client.messages.send({
1053
+ from: this.config.fromNumber,
1054
+ to: normalizedTo,
1055
+ text: body
1056
+ });
1057
+ const messageId = result.data?.id;
1058
+ console.log(`[RelayRail SMS] Telnyx response:`, {
1059
+ requestId,
1060
+ messageId,
1061
+ status: result.data?.to?.[0]?.status
1062
+ });
1063
+ await this.logMessage({
1064
+ request_id: requestId,
1065
+ channel: "sms",
1066
+ direction: "outbound",
1067
+ external_id: messageId || null,
1068
+ status: messageId ? "sent" : "pending",
1069
+ content: { to: normalizedTo, type: "approval", messageLength: body.length },
1070
+ error_message: null
1071
+ });
1072
+ return { success: true, messageId };
1073
+ } catch (error) {
1074
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1075
+ console.error(`[RelayRail SMS] Failed to send:`, {
1076
+ requestId,
1077
+ error: errorMessage,
1078
+ stack: error instanceof Error ? error.stack : void 0
1079
+ });
1080
+ await this.logMessage({
1081
+ request_id: requestId,
1082
+ channel: "sms",
1083
+ direction: "outbound",
1084
+ external_id: null,
1085
+ status: "failed",
1086
+ content: { to: normalizedTo, type: "approval" },
1087
+ error_message: errorMessage
1088
+ });
1089
+ return { success: false, error: errorMessage };
1090
+ }
1091
+ }
1092
+ /**
1093
+ * Send a notification via SMS
1094
+ */
1095
+ async sendNotificationSms(params) {
1096
+ const { to, requestId, message, agentName, severity = "info" } = params;
1097
+ const severityPrefix = severity === "critical" ? "!" : severity === "warning" ? "?" : "";
1098
+ let body = `${severityPrefix}[${agentName}] ${message}`;
1099
+ if (body.length > 1500) {
1100
+ body = body.substring(0, 1500) + "...";
1101
+ }
1102
+ try {
1103
+ const result = await this.client.messages.send({
1104
+ from: this.config.fromNumber,
1105
+ to: this.normalizePhoneNumber(to),
1106
+ text: body
1107
+ });
1108
+ const messageId = result.data?.id;
1109
+ await this.logMessage({
1110
+ request_id: requestId,
1111
+ channel: "sms",
1112
+ direction: "outbound",
1113
+ external_id: messageId || null,
1114
+ status: messageId ? "sent" : "pending",
1115
+ content: { to, type: "notification", messageLength: body.length },
1116
+ error_message: null
1117
+ });
1118
+ await this.supabase.from("requests").update({ status: "delivered" }).eq("id", requestId);
1119
+ return { success: true, messageId };
1120
+ } catch (error) {
1121
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1122
+ await this.logMessage({
1123
+ request_id: requestId,
1124
+ channel: "sms",
1125
+ direction: "outbound",
1126
+ external_id: null,
1127
+ status: "failed",
1128
+ content: { to, type: "notification" },
1129
+ error_message: errorMessage
1130
+ });
1131
+ return { success: false, error: errorMessage };
1132
+ }
1133
+ }
1134
+ /**
1135
+ * Normalize phone number to E.164 format
1136
+ */
1137
+ normalizePhoneNumber(phone) {
1138
+ let normalized = phone.replace(/[^\d+]/g, "");
1139
+ if (!normalized.startsWith("+") && normalized.length === 10) {
1140
+ normalized = "+1" + normalized;
1141
+ }
1142
+ if (!normalized.startsWith("+")) {
1143
+ normalized = "+" + normalized;
1144
+ }
1145
+ return normalized;
1146
+ }
1147
+ /**
1148
+ * Log a message to the database
1149
+ */
1150
+ async logMessage(data) {
1151
+ try {
1152
+ await this.supabase.from("messages").insert(data);
1153
+ } catch (error) {
1154
+ console.error("[RelayRail] Failed to log message:", error);
1155
+ }
1156
+ }
1157
+ };
1158
+
1159
+ // src/services/usage.ts
1160
+ var UsageService = class {
1161
+ supabase;
1162
+ baseUrl;
1163
+ constructor(supabase, baseUrl) {
1164
+ this.supabase = supabase;
1165
+ this.baseUrl = baseUrl;
1166
+ }
1167
+ /**
1168
+ * Get current usage for a user
1169
+ */
1170
+ async getUsage(userId) {
1171
+ const result = await this.supabase.rpc("get_or_create_usage", {
1172
+ p_user_id: userId
1173
+ });
1174
+ if (result.error) {
1175
+ console.error("Failed to get usage:", result.error);
1176
+ return null;
1177
+ }
1178
+ return result.data;
1179
+ }
1180
+ /**
1181
+ * Check email quota for a user
1182
+ */
1183
+ checkEmailQuota(user, currentUsage) {
1184
+ return checkEmailQuota(
1185
+ user.tier,
1186
+ currentUsage,
1187
+ user.allow_email_overage ?? true,
1188
+ this.baseUrl
1189
+ );
1190
+ }
1191
+ /**
1192
+ * Check SMS quota for a user
1193
+ */
1194
+ checkSmsQuota(user, currentUsage) {
1195
+ return checkSmsQuota(
1196
+ user.tier,
1197
+ currentUsage,
1198
+ user.allow_sms_overage ?? true,
1199
+ this.baseUrl
1200
+ );
1201
+ }
1202
+ /**
1203
+ * Increment email usage with overage tracking
1204
+ * Returns whether the send was allowed and if it was an overage
1205
+ */
1206
+ async incrementEmailUsage(userId, allowOverage) {
1207
+ const result = await this.supabase.rpc("increment_email_usage_with_overage", {
1208
+ p_user_id: userId,
1209
+ p_allow_overage: allowOverage
1210
+ });
1211
+ if (result.error) {
1212
+ console.error("Failed to increment email usage:", result.error);
1213
+ return { count: 0, isOverage: false, blocked: true };
1214
+ }
1215
+ const data = result.data;
1216
+ if (!data || data.length === 0) {
1217
+ return { count: 0, isOverage: false, blocked: true };
1218
+ }
1219
+ const row = data[0];
1220
+ return {
1221
+ count: row.new_count,
1222
+ isOverage: row.is_overage,
1223
+ blocked: row.was_blocked
1224
+ };
1225
+ }
1226
+ /**
1227
+ * Increment SMS usage with overage tracking
1228
+ * Returns whether the send was allowed and if it was an overage
1229
+ */
1230
+ async incrementSmsUsage(userId, allowOverage) {
1231
+ const result = await this.supabase.rpc("increment_sms_usage_with_overage", {
1232
+ p_user_id: userId,
1233
+ p_allow_overage: allowOverage
1234
+ });
1235
+ if (result.error) {
1236
+ console.error("Failed to increment SMS usage:", result.error);
1237
+ return { count: 0, isOverage: false, blocked: true };
1238
+ }
1239
+ const data = result.data;
1240
+ if (!data || data.length === 0) {
1241
+ return { count: 0, isOverage: false, blocked: true };
1242
+ }
1243
+ const row = data[0];
1244
+ return {
1245
+ count: row.new_count,
1246
+ isOverage: row.is_overage,
1247
+ blocked: row.was_blocked
1248
+ };
1249
+ }
1250
+ /**
1251
+ * Record an overage charge for billing
1252
+ */
1253
+ async recordOverageCharge(userId, channel, requestId) {
1254
+ const amountCents = channel === "email" ? EMAIL_OVERAGE_CENTS : SMS_OVERAGE_CENTS;
1255
+ const result = await this.supabase.rpc("record_overage_charge", {
1256
+ p_user_id: userId,
1257
+ p_channel: channel,
1258
+ p_amount_cents: amountCents,
1259
+ p_request_id: requestId ?? null
1260
+ });
1261
+ if (result.error) {
1262
+ console.error("Failed to record overage charge:", result.error);
1263
+ return null;
1264
+ }
1265
+ return result.data;
1266
+ }
1267
+ /**
1268
+ * Get email limit for a user's tier
1269
+ */
1270
+ async getEmailLimit(userId) {
1271
+ const result = await this.supabase.rpc("get_email_limit", {
1272
+ p_user_id: userId
1273
+ });
1274
+ if (result.error) {
1275
+ console.error("Failed to get email limit:", result.error);
1276
+ return 100;
1277
+ }
1278
+ return result.data;
1279
+ }
1280
+ /**
1281
+ * Get SMS limit for a user's tier
1282
+ */
1283
+ async getSmsLimit(userId) {
1284
+ const result = await this.supabase.rpc("get_sms_limit", {
1285
+ p_user_id: userId
1286
+ });
1287
+ if (result.error) {
1288
+ console.error("Failed to get SMS limit:", result.error);
1289
+ return 0;
1290
+ }
1291
+ return result.data;
1292
+ }
1293
+ };
1294
+
1295
+ // src/services/router.ts
1296
+ var RequestRouter = class {
1297
+ emailService;
1298
+ smsService;
1299
+ usageService;
1300
+ smsEnabled;
1301
+ baseUrl;
1302
+ constructor(config, supabase) {
1303
+ this.emailService = new EmailService(config.email, supabase);
1304
+ this.usageService = new UsageService(supabase, config.baseUrl);
1305
+ this.baseUrl = config.baseUrl;
1306
+ this.smsEnabled = !!config.sms?.apiKey;
1307
+ if (config.sms) {
1308
+ this.smsService = new SmsService(config.sms, supabase);
1309
+ }
1310
+ }
1311
+ /**
1312
+ * Route an approval request to the user
1313
+ */
1314
+ async routeApproval(params, user, agent) {
1315
+ const preferredChannel = user.notification_preferences?.preferred_channel || "email";
1316
+ const { channel, fallbackReason } = this.selectChannel(preferredChannel, user);
1317
+ console.log(`[RelayRail Router] Routing approval:`, {
1318
+ requestId: params.requestId,
1319
+ preferredChannel,
1320
+ selectedChannel: channel,
1321
+ fallbackReason,
1322
+ userPhone: user.phone ? `${user.phone.slice(0, 4)}...` : "none",
1323
+ userTier: user.tier,
1324
+ smsEnabled: this.smsEnabled,
1325
+ hasSmsService: !!this.smsService
1326
+ });
1327
+ const usage = await this.usageService.getUsage(user.id);
1328
+ const currentUsage = channel === "sms" ? usage?.sms_sent ?? 0 : usage?.emails_sent ?? 0;
1329
+ const quotaCheck = channel === "sms" ? this.usageService.checkSmsQuota(user, currentUsage) : this.usageService.checkEmailQuota(user, currentUsage);
1330
+ if (!quotaCheck.allowed) {
1331
+ return this.createQuotaErrorResult(channel, quotaCheck, user);
1332
+ }
1333
+ const allowOverage = channel === "sms" ? user.allow_sms_overage ?? true : user.allow_email_overage ?? true;
1334
+ const usageResult = channel === "sms" ? await this.usageService.incrementSmsUsage(user.id, allowOverage) : await this.usageService.incrementEmailUsage(user.id, allowOverage);
1335
+ if (usageResult.blocked) {
1336
+ return this.createQuotaErrorResult(channel, quotaCheck, user);
1337
+ }
1338
+ if (usageResult.isOverage) {
1339
+ await this.usageService.recordOverageCharge(user.id, channel, params.requestId);
1340
+ }
1341
+ let sendResult;
1342
+ if (channel === "sms" && this.smsService && user.phone) {
1343
+ sendResult = await this.smsService.sendApprovalSms({
1344
+ to: user.phone,
1345
+ requestId: params.requestId,
1346
+ responseToken: params.responseToken,
1347
+ message: params.message,
1348
+ options: params.options,
1349
+ agentName: agent.name,
1350
+ expiresAt: params.expiresAt,
1351
+ severity: params.severity
1352
+ });
1353
+ } else {
1354
+ sendResult = await this.emailService.sendApprovalEmail({
1355
+ to: user.email,
1356
+ requestId: params.requestId,
1357
+ responseToken: params.responseToken,
1358
+ message: params.message,
1359
+ options: params.options,
1360
+ agentName: agent.name,
1361
+ expiresAt: params.expiresAt,
1362
+ severity: params.severity
1363
+ });
1364
+ }
1365
+ return {
1366
+ success: sendResult.success,
1367
+ channel,
1368
+ channel_requested: preferredChannel,
1369
+ fallback_reason: fallbackReason,
1370
+ messageId: sendResult.messageId,
1371
+ error: sendResult.error,
1372
+ quota: this.createQuotaInfo(usageResult, quotaCheck, channel)
1373
+ };
1374
+ }
1375
+ /**
1376
+ * Route a notification to the user
1377
+ */
1378
+ async routeNotification(params, user, agent) {
1379
+ const preferredChannel = user.notification_preferences?.preferred_channel || "email";
1380
+ const { channel, fallbackReason } = this.selectChannel(preferredChannel, user);
1381
+ const usage = await this.usageService.getUsage(user.id);
1382
+ const currentUsage = channel === "sms" ? usage?.sms_sent ?? 0 : usage?.emails_sent ?? 0;
1383
+ const quotaCheck = channel === "sms" ? this.usageService.checkSmsQuota(user, currentUsage) : this.usageService.checkEmailQuota(user, currentUsage);
1384
+ if (!quotaCheck.allowed) {
1385
+ return this.createQuotaErrorResult(channel, quotaCheck, user);
1386
+ }
1387
+ const allowOverage = channel === "sms" ? user.allow_sms_overage ?? true : user.allow_email_overage ?? true;
1388
+ const usageResult = channel === "sms" ? await this.usageService.incrementSmsUsage(user.id, allowOverage) : await this.usageService.incrementEmailUsage(user.id, allowOverage);
1389
+ if (usageResult.blocked) {
1390
+ return this.createQuotaErrorResult(channel, quotaCheck, user);
1391
+ }
1392
+ if (usageResult.isOverage) {
1393
+ await this.usageService.recordOverageCharge(user.id, channel, params.requestId);
1394
+ }
1395
+ let sendResult;
1396
+ if (channel === "sms" && this.smsService && user.phone) {
1397
+ sendResult = await this.smsService.sendNotificationSms({
1398
+ to: user.phone,
1399
+ requestId: params.requestId,
1400
+ message: params.message,
1401
+ agentName: agent.name,
1402
+ severity: params.severity
1403
+ });
1404
+ } else {
1405
+ sendResult = await this.emailService.sendNotificationEmail({
1406
+ to: user.email,
1407
+ requestId: params.requestId,
1408
+ message: params.message,
1409
+ agentName: agent.name,
1410
+ severity: params.severity
1411
+ });
1412
+ }
1413
+ return {
1414
+ success: sendResult.success,
1415
+ channel,
1416
+ channel_requested: preferredChannel,
1417
+ fallback_reason: fallbackReason,
1418
+ messageId: sendResult.messageId,
1419
+ error: sendResult.error,
1420
+ quota: this.createQuotaInfo(usageResult, quotaCheck, channel)
1421
+ };
1422
+ }
1423
+ /**
1424
+ * Select the best available channel with fallback logic
1425
+ * Returns the channel to use and a reason if fallback occurred
1426
+ */
1427
+ selectChannel(preferred, user) {
1428
+ const tier = user.tier;
1429
+ const smsAvailable = tier !== "free" && !!user.phone && this.smsEnabled && !!this.smsService;
1430
+ if (preferred === "sms") {
1431
+ if (smsAvailable) {
1432
+ return { channel: "sms" };
1433
+ }
1434
+ let reason;
1435
+ if (tier === "free") {
1436
+ reason = "SMS requires Pro tier. Sent via email instead.";
1437
+ } else if (!user.phone) {
1438
+ reason = "No phone number configured. Sent via email instead.";
1439
+ } else if (!this.smsEnabled || !this.smsService) {
1440
+ reason = "SMS service not configured. Sent via email instead.";
1441
+ } else {
1442
+ reason = "SMS not available. Sent via email instead.";
1443
+ }
1444
+ return { channel: "email", fallbackReason: reason };
1445
+ }
1446
+ return { channel: "email" };
1447
+ }
1448
+ /**
1449
+ * Create quota info object for successful sends
1450
+ */
1451
+ createQuotaInfo(usageResult, quotaCheck, channel) {
1452
+ const overageRate = channel === "sms" ? SMS_OVERAGE_CENTS / 100 : EMAIL_OVERAGE_CENTS / 100;
1453
+ const info = {
1454
+ used: usageResult.count,
1455
+ limit: quotaCheck.limit,
1456
+ remaining: Math.max(0, quotaCheck.limit - usageResult.count)
1457
+ };
1458
+ if (usageResult.isOverage) {
1459
+ info.overage = true;
1460
+ info.overage_rate = overageRate;
1461
+ info.message = `${channel.toUpperCase()} sent as overage. You will be charged $${overageRate.toFixed(2)} for this message.`;
1462
+ }
1463
+ return info;
1464
+ }
1465
+ /**
1466
+ * Create quota error result when send is blocked
1467
+ */
1468
+ createQuotaErrorResult(channel, quotaCheck, user) {
1469
+ const tier = user.tier;
1470
+ const allowOverage = channel === "sms" ? user.allow_sms_overage ?? true : user.allow_email_overage ?? true;
1471
+ let message;
1472
+ if (tier === "free" && channel === "sms") {
1473
+ message = "SMS is not available on the Free tier. Upgrade to Pro for 100 SMS/month.";
1474
+ } else if (!allowOverage) {
1475
+ message = `${channel.toUpperCase()} limit reached (${quotaCheck.current}/${quotaCheck.limit}). Overage charges are disabled in your settings.`;
1476
+ } else {
1477
+ message = quotaCheck.message || `${channel.toUpperCase()} limit reached.`;
1478
+ }
1479
+ return {
1480
+ success: false,
1481
+ channel,
1482
+ error: message,
1483
+ quotaError: {
1484
+ exceeded: true,
1485
+ current: quotaCheck.current,
1486
+ limit: quotaCheck.limit,
1487
+ tier,
1488
+ overage_disabled: !allowOverage,
1489
+ message,
1490
+ enable_overage_url: !allowOverage ? `${sanitizeBaseUrl(this.baseUrl)}/settings?section=billing` : void 0,
1491
+ upgradeUrl: `${sanitizeBaseUrl(this.baseUrl)}/pricing?upgrade=true&reason=${channel}_limit`
1492
+ }
1493
+ };
1494
+ }
1495
+ };
1496
+ function createRouter(config, supabase) {
1497
+ return new RequestRouter(config, supabase);
1498
+ }
1499
+
1500
+ // src/server.ts
1501
+ var VERSION = "0.1.0";
1502
+ var RelayRailServer = class {
1503
+ server;
1504
+ supabase;
1505
+ config;
1506
+ context = null;
1507
+ router = null;
1508
+ constructor(config) {
1509
+ this.config = config;
1510
+ this.supabase = createServiceClient(config.supabaseUrl, config.supabaseServiceRoleKey);
1511
+ const hasEmail = !!config.resendApiKey;
1512
+ const hasSms = !!(config.telnyxApiKey && config.telnyxPhoneNumber);
1513
+ if (hasEmail || hasSms) {
1514
+ this.router = createRouter(
1515
+ {
1516
+ baseUrl: config.baseUrl,
1517
+ email: {
1518
+ resendApiKey: config.resendApiKey || "",
1519
+ fromEmail: "notifications@mail.relayrail.dev",
1520
+ fromName: "RelayRail",
1521
+ baseUrl: config.baseUrl,
1522
+ inboundDomain: "in.relayrail.dev"
1523
+ },
1524
+ sms: hasSms ? {
1525
+ apiKey: config.telnyxApiKey,
1526
+ fromNumber: config.telnyxPhoneNumber,
1527
+ baseUrl: config.baseUrl
1528
+ } : void 0
1529
+ },
1530
+ this.supabase
1531
+ );
1532
+ }
1533
+ this.server = new McpServer({
1534
+ name: "relayrail",
1535
+ version: VERSION
1536
+ });
1537
+ this.registerTools();
1538
+ }
1539
+ /**
1540
+ * Register all MCP tools
1541
+ */
1542
+ registerTools() {
1543
+ this.server.tool(
1544
+ "request_approval",
1545
+ "Request explicit approval from the user before proceeding with an action. The user will receive a notification and can approve or reject.",
1546
+ {
1547
+ message: z.string().describe("The message explaining what approval is being requested"),
1548
+ options: z.array(z.string()).optional().describe("Optional list of response options for the user"),
1549
+ context: z.record(z.unknown()).optional().describe("Additional context data to include"),
1550
+ timeout_minutes: z.number().min(1).max(MAX_TIMEOUT_MINUTES).optional().describe(`Timeout in minutes (default: ${DEFAULT_TIMEOUT_MINUTES}, max: ${MAX_TIMEOUT_MINUTES})`),
1551
+ severity: z.enum(["info", "warning", "critical"]).optional().describe("Severity level of the request")
1552
+ },
1553
+ async (params) => {
1554
+ if (!this.context) {
1555
+ return {
1556
+ content: [{ type: "text", text: JSON.stringify({ error: "Not authenticated" }) }]
1557
+ };
1558
+ }
1559
+ try {
1560
+ const result = await requestApproval(
1561
+ params,
1562
+ {
1563
+ supabase: this.supabase,
1564
+ agent: this.context.agent,
1565
+ user: this.context.user,
1566
+ baseUrl: this.config.baseUrl,
1567
+ router: this.router ?? void 0
1568
+ }
1569
+ );
1570
+ return {
1571
+ content: [{
1572
+ type: "text",
1573
+ text: JSON.stringify(result)
1574
+ }]
1575
+ };
1576
+ } catch (error) {
1577
+ return {
1578
+ content: [{
1579
+ type: "text",
1580
+ text: JSON.stringify({
1581
+ error: error instanceof Error ? error.message : "Unknown error"
1582
+ })
1583
+ }]
1584
+ };
1585
+ }
1586
+ }
1587
+ );
1588
+ this.server.tool(
1589
+ "send_notification",
1590
+ "Send a one-way notification to the user. No response is expected.",
1591
+ {
1592
+ message: z.string().describe("The notification message to send"),
1593
+ context: z.record(z.unknown()).optional().describe("Additional context data to include"),
1594
+ severity: z.enum(["info", "warning", "critical"]).optional().describe("Severity level")
1595
+ },
1596
+ async (params) => {
1597
+ if (!this.context) {
1598
+ return {
1599
+ content: [{ type: "text", text: JSON.stringify({ error: "Not authenticated" }) }]
1600
+ };
1601
+ }
1602
+ try {
1603
+ const result = await sendNotification(
1604
+ params,
1605
+ {
1606
+ supabase: this.supabase,
1607
+ agent: this.context.agent,
1608
+ user: this.context.user,
1609
+ router: this.router ?? void 0
1610
+ }
1611
+ );
1612
+ return {
1613
+ content: [{
1614
+ type: "text",
1615
+ text: JSON.stringify(result)
1616
+ }]
1617
+ };
1618
+ } catch (error) {
1619
+ return {
1620
+ content: [{
1621
+ type: "text",
1622
+ text: JSON.stringify({
1623
+ error: error instanceof Error ? error.message : "Unknown error"
1624
+ })
1625
+ }]
1626
+ };
1627
+ }
1628
+ }
1629
+ );
1630
+ this.server.tool(
1631
+ "await_response",
1632
+ "Wait for a user response to a previously sent approval request.",
1633
+ {
1634
+ request_id: z.string().describe("The request ID to wait for"),
1635
+ timeout_seconds: z.number().min(1).max(300).optional().describe("How long to wait for a response (default: 30, max: 300)")
1636
+ },
1637
+ async (params) => {
1638
+ if (!this.context) {
1639
+ return {
1640
+ content: [{ type: "text", text: JSON.stringify({ error: "Not authenticated" }) }]
1641
+ };
1642
+ }
1643
+ try {
1644
+ const result = await awaitResponse(
1645
+ params,
1646
+ {
1647
+ supabase: this.supabase,
1648
+ agent: this.context.agent
1649
+ }
1650
+ );
1651
+ return {
1652
+ content: [{
1653
+ type: "text",
1654
+ text: JSON.stringify(result)
1655
+ }]
1656
+ };
1657
+ } catch (error) {
1658
+ return {
1659
+ content: [{
1660
+ type: "text",
1661
+ text: JSON.stringify({
1662
+ error: error instanceof Error ? error.message : "Unknown error"
1663
+ })
1664
+ }]
1665
+ };
1666
+ }
1667
+ }
1668
+ );
1669
+ this.server.tool(
1670
+ "get_pending_commands",
1671
+ "Retrieve pending commands sent by the user via SMS or email.",
1672
+ {
1673
+ limit: z.number().min(1).max(100).optional().describe("Maximum number of commands to return (default: 10)")
1674
+ },
1675
+ async (params) => {
1676
+ if (!this.context) {
1677
+ return {
1678
+ content: [{ type: "text", text: JSON.stringify({ error: "Not authenticated" }) }]
1679
+ };
1680
+ }
1681
+ try {
1682
+ const result = await getPendingCommands(
1683
+ params,
1684
+ {
1685
+ supabase: this.supabase,
1686
+ agent: this.context.agent
1687
+ }
1688
+ );
1689
+ return {
1690
+ content: [{
1691
+ type: "text",
1692
+ text: JSON.stringify(result)
1693
+ }]
1694
+ };
1695
+ } catch (error) {
1696
+ return {
1697
+ content: [{
1698
+ type: "text",
1699
+ text: JSON.stringify({
1700
+ error: error instanceof Error ? error.message : "Unknown error"
1701
+ })
1702
+ }]
1703
+ };
1704
+ }
1705
+ }
1706
+ );
1707
+ this.server.tool(
1708
+ "register_agent",
1709
+ "Register this agent instance with RelayRail to enable human-in-the-loop communication. Use this tool first if you do not have an API key.",
1710
+ {
1711
+ name: z.string().min(1).max(100).describe("A descriptive name for this agent instance"),
1712
+ user_email: z.string().email().describe("Email address of the user who will own this agent")
1713
+ },
1714
+ async (params) => {
1715
+ try {
1716
+ const result = await registerAgent(
1717
+ params,
1718
+ {
1719
+ supabase: this.supabase,
1720
+ baseUrl: this.config.baseUrl
1721
+ }
1722
+ );
1723
+ return {
1724
+ content: [{
1725
+ type: "text",
1726
+ text: JSON.stringify(result)
1727
+ }]
1728
+ };
1729
+ } catch (error) {
1730
+ return {
1731
+ content: [{
1732
+ type: "text",
1733
+ text: JSON.stringify({
1734
+ success: false,
1735
+ error: error instanceof Error ? error.message : "Unknown error"
1736
+ })
1737
+ }]
1738
+ };
1739
+ }
1740
+ }
1741
+ );
1742
+ this.server.tool(
1743
+ "get_account_status",
1744
+ "Check your current account status including tier, available channels, quota usage, and helpful tips.",
1745
+ {},
1746
+ async () => {
1747
+ if (!this.context) {
1748
+ return {
1749
+ content: [{ type: "text", text: JSON.stringify({ error: "Not authenticated. Use register_agent first to get an API key." }) }]
1750
+ };
1751
+ }
1752
+ try {
1753
+ const result = await getAccountStatus({
1754
+ supabase: this.supabase,
1755
+ agent: this.context.agent,
1756
+ user: this.context.user
1757
+ });
1758
+ return {
1759
+ content: [{
1760
+ type: "text",
1761
+ text: JSON.stringify(result)
1762
+ }]
1763
+ };
1764
+ } catch (error) {
1765
+ return {
1766
+ content: [{
1767
+ type: "text",
1768
+ text: JSON.stringify({
1769
+ error: error instanceof Error ? error.message : "Unknown error"
1770
+ })
1771
+ }]
1772
+ };
1773
+ }
1774
+ }
1775
+ );
1776
+ }
1777
+ /**
1778
+ * Authenticate with an API key and set the context
1779
+ */
1780
+ async authenticate(apiKey) {
1781
+ const result = await authenticateApiKey(this.supabase, apiKey);
1782
+ if (result.success && result.agent && result.user) {
1783
+ this.context = {
1784
+ agent: result.agent,
1785
+ user: result.user
1786
+ };
1787
+ return true;
1788
+ }
1789
+ return false;
1790
+ }
1791
+ /**
1792
+ * Get the current authentication context
1793
+ */
1794
+ getContext() {
1795
+ return this.context;
1796
+ }
1797
+ /**
1798
+ * Get the underlying MCP server instance
1799
+ */
1800
+ getMcpServer() {
1801
+ return this.server;
1802
+ }
1803
+ /**
1804
+ * Get the Supabase client
1805
+ */
1806
+ getSupabase() {
1807
+ return this.supabase;
1808
+ }
1809
+ /**
1810
+ * Get the server configuration
1811
+ */
1812
+ getConfig() {
1813
+ return this.config;
1814
+ }
1815
+ /**
1816
+ * Start the server with stdio transport
1817
+ */
1818
+ async start() {
1819
+ const transport = new StdioServerTransport();
1820
+ await this.server.connect(transport);
1821
+ }
1822
+ };
1823
+ function createServer(config) {
1824
+ return new RelayRailServer(config);
1825
+ }
1826
+
1827
+ // src/transports/http.ts
1828
+ var MCP_ERRORS = {
1829
+ PARSE_ERROR: -32700,
1830
+ INVALID_REQUEST: -32600,
1831
+ METHOD_NOT_FOUND: -32601,
1832
+ INVALID_PARAMS: -32602,
1833
+ INTERNAL_ERROR: -32603,
1834
+ NOT_AUTHENTICATED: -32e3,
1835
+ RATE_LIMITED: -32001
1836
+ };
1837
+ var HTTPTransport = class {
1838
+ supabase;
1839
+ config;
1840
+ router = null;
1841
+ sessions = /* @__PURE__ */ new Map();
1842
+ sessionTTL = 24 * 60 * 60 * 1e3;
1843
+ // 24 hours
1844
+ constructor(transportConfig) {
1845
+ this.supabase = transportConfig.supabase;
1846
+ this.config = transportConfig.serverConfig;
1847
+ this.router = transportConfig.router ?? null;
1848
+ if (!this.router) {
1849
+ const hasEmail = !!this.config.resendApiKey;
1850
+ const hasSms = !!(this.config.telnyxApiKey && this.config.telnyxPhoneNumber);
1851
+ if (hasEmail || hasSms) {
1852
+ this.router = createRouter(
1853
+ {
1854
+ baseUrl: this.config.baseUrl,
1855
+ email: {
1856
+ resendApiKey: this.config.resendApiKey || "",
1857
+ fromEmail: "notifications@mail.relayrail.dev",
1858
+ fromName: "RelayRail",
1859
+ baseUrl: this.config.baseUrl,
1860
+ inboundDomain: "in.relayrail.dev"
1861
+ },
1862
+ sms: hasSms ? {
1863
+ apiKey: this.config.telnyxApiKey,
1864
+ fromNumber: this.config.telnyxPhoneNumber,
1865
+ baseUrl: this.config.baseUrl
1866
+ } : void 0
1867
+ },
1868
+ this.supabase
1869
+ );
1870
+ }
1871
+ }
1872
+ setInterval(() => this.cleanupSessions(), 60 * 60 * 1e3);
1873
+ }
1874
+ /**
1875
+ * Handle an incoming HTTP request
1876
+ */
1877
+ async handleRequest(body, apiKey, sessionId) {
1878
+ if (Array.isArray(body)) {
1879
+ const responses = await Promise.all(
1880
+ body.map((req) => this.handleSingleRequest(req, apiKey, sessionId))
1881
+ );
1882
+ return {
1883
+ response: responses.map((r) => r.response),
1884
+ newSessionId: responses[0]?.newSessionId
1885
+ };
1886
+ }
1887
+ return this.handleSingleRequest(body, apiKey, sessionId);
1888
+ }
1889
+ /**
1890
+ * Handle a single MCP request
1891
+ */
1892
+ async handleSingleRequest(request, apiKey, sessionId) {
1893
+ const { id, method, params } = request;
1894
+ if (request.jsonrpc !== "2.0") {
1895
+ return {
1896
+ response: this.errorResponse(id, MCP_ERRORS.INVALID_REQUEST, "Invalid JSON-RPC version")
1897
+ };
1898
+ }
1899
+ switch (method) {
1900
+ case "initialize":
1901
+ return { response: this.handleInitialize(id) };
1902
+ case "tools/list":
1903
+ return { response: this.handleListTools(id) };
1904
+ case "tools/call":
1905
+ return this.handleToolCall(id, params, apiKey, sessionId);
1906
+ case "ping":
1907
+ return { response: this.successResponse(id, { pong: true }) };
1908
+ default:
1909
+ return {
1910
+ response: this.errorResponse(id, MCP_ERRORS.METHOD_NOT_FOUND, `Unknown method: ${method}`)
1911
+ };
1912
+ }
1913
+ }
1914
+ /**
1915
+ * Handle initialize request
1916
+ */
1917
+ handleInitialize(id) {
1918
+ return this.successResponse(id, {
1919
+ protocolVersion: "2024-11-05",
1920
+ capabilities: {
1921
+ tools: {}
1922
+ },
1923
+ serverInfo: {
1924
+ name: "relayrail",
1925
+ version: VERSION
1926
+ }
1927
+ });
1928
+ }
1929
+ /**
1930
+ * Handle tools/list request
1931
+ */
1932
+ handleListTools(id) {
1933
+ const tools = [
1934
+ {
1935
+ name: "register_agent",
1936
+ description: "Register this agent instance with RelayRail to enable human-in-the-loop communication. Use this tool first if you do not have an API key.",
1937
+ inputSchema: {
1938
+ type: "object",
1939
+ properties: {
1940
+ name: {
1941
+ type: "string",
1942
+ description: "A descriptive name for this agent instance"
1943
+ },
1944
+ user_email: {
1945
+ type: "string",
1946
+ description: "Email address of the user who will own this agent"
1947
+ }
1948
+ },
1949
+ required: ["name", "user_email"]
1950
+ }
1951
+ },
1952
+ {
1953
+ name: "request_approval",
1954
+ description: "Request explicit approval from the user before proceeding with an action. The user will receive a notification and can approve or reject.",
1955
+ inputSchema: {
1956
+ type: "object",
1957
+ properties: {
1958
+ message: {
1959
+ type: "string",
1960
+ description: "The message explaining what approval is being requested"
1961
+ },
1962
+ options: {
1963
+ type: "array",
1964
+ items: { type: "string" },
1965
+ description: "Optional list of response options for the user"
1966
+ },
1967
+ context: {
1968
+ type: "object",
1969
+ description: "Additional context data to include"
1970
+ },
1971
+ timeout_minutes: {
1972
+ type: "number",
1973
+ description: "Timeout in minutes (default: 60, max: 1440)"
1974
+ },
1975
+ severity: {
1976
+ type: "string",
1977
+ enum: ["info", "warning", "critical"],
1978
+ description: "Severity level of the request"
1979
+ }
1980
+ },
1981
+ required: ["message"]
1982
+ }
1983
+ },
1984
+ {
1985
+ name: "send_notification",
1986
+ description: "Send a one-way notification to the user. No response is expected.",
1987
+ inputSchema: {
1988
+ type: "object",
1989
+ properties: {
1990
+ message: {
1991
+ type: "string",
1992
+ description: "The notification message to send"
1993
+ },
1994
+ context: {
1995
+ type: "object",
1996
+ description: "Additional context data to include"
1997
+ },
1998
+ severity: {
1999
+ type: "string",
2000
+ enum: ["info", "warning", "critical"],
2001
+ description: "Severity level"
2002
+ }
2003
+ },
2004
+ required: ["message"]
2005
+ }
2006
+ },
2007
+ {
2008
+ name: "await_response",
2009
+ description: "Wait for a user response to a previously sent approval request.",
2010
+ inputSchema: {
2011
+ type: "object",
2012
+ properties: {
2013
+ request_id: {
2014
+ type: "string",
2015
+ description: "The request ID to wait for"
2016
+ },
2017
+ timeout_seconds: {
2018
+ type: "number",
2019
+ description: "How long to wait for a response (default: 30, max: 300)"
2020
+ }
2021
+ },
2022
+ required: ["request_id"]
2023
+ }
2024
+ },
2025
+ {
2026
+ name: "get_pending_commands",
2027
+ description: "Retrieve pending commands sent by the user via SMS or email.",
2028
+ inputSchema: {
2029
+ type: "object",
2030
+ properties: {
2031
+ limit: {
2032
+ type: "number",
2033
+ description: "Maximum number of commands to return (default: 10)"
2034
+ }
2035
+ }
2036
+ }
2037
+ }
2038
+ ];
2039
+ return this.successResponse(id, { tools });
2040
+ }
2041
+ /**
2042
+ * Handle tools/call request
2043
+ */
2044
+ async handleToolCall(id, params, apiKey, sessionId) {
2045
+ const { name, arguments: args } = params;
2046
+ if (name === "register_agent") {
2047
+ try {
2048
+ const result = await registerAgent(
2049
+ args,
2050
+ {
2051
+ supabase: this.supabase,
2052
+ baseUrl: this.config.baseUrl
2053
+ }
2054
+ );
2055
+ return { response: this.successResponse(id, { content: [{ type: "text", text: JSON.stringify(result) }] }) };
2056
+ } catch (error) {
2057
+ return {
2058
+ response: this.successResponse(id, {
2059
+ content: [
2060
+ {
2061
+ type: "text",
2062
+ text: JSON.stringify({
2063
+ success: false,
2064
+ error: error instanceof Error ? error.message : "Unknown error"
2065
+ })
2066
+ }
2067
+ ]
2068
+ })
2069
+ };
2070
+ }
2071
+ }
2072
+ let context = null;
2073
+ let newSessionId;
2074
+ if (sessionId) {
2075
+ const session = this.sessions.get(sessionId);
2076
+ if (session && session.expiresAt > Date.now()) {
2077
+ context = session.context;
2078
+ }
2079
+ }
2080
+ if (!context && apiKey) {
2081
+ const authResult = await authenticateApiKey(this.supabase, apiKey);
2082
+ if (authResult.success && authResult.agent && authResult.user) {
2083
+ context = {
2084
+ agent: authResult.agent,
2085
+ user: authResult.user
2086
+ };
2087
+ newSessionId = crypto.randomUUID();
2088
+ this.sessions.set(newSessionId, {
2089
+ context,
2090
+ expiresAt: Date.now() + this.sessionTTL
2091
+ });
2092
+ }
2093
+ }
2094
+ if (!context) {
2095
+ return {
2096
+ response: this.errorResponse(
2097
+ id,
2098
+ MCP_ERRORS.NOT_AUTHENTICATED,
2099
+ "Authentication required. Provide API key via Authorization header or use register_agent tool first."
2100
+ )
2101
+ };
2102
+ }
2103
+ try {
2104
+ let result;
2105
+ switch (name) {
2106
+ case "request_approval":
2107
+ result = await requestApproval(args, {
2108
+ supabase: this.supabase,
2109
+ agent: context.agent,
2110
+ user: context.user,
2111
+ baseUrl: this.config.baseUrl,
2112
+ router: this.router ?? void 0
2113
+ });
2114
+ break;
2115
+ case "send_notification":
2116
+ result = await sendNotification(args, {
2117
+ supabase: this.supabase,
2118
+ agent: context.agent,
2119
+ user: context.user,
2120
+ router: this.router ?? void 0
2121
+ });
2122
+ break;
2123
+ case "await_response":
2124
+ result = await awaitResponse(args, {
2125
+ supabase: this.supabase,
2126
+ agent: context.agent
2127
+ });
2128
+ break;
2129
+ case "get_pending_commands":
2130
+ result = await getPendingCommands(args, {
2131
+ supabase: this.supabase,
2132
+ agent: context.agent
2133
+ });
2134
+ break;
2135
+ default:
2136
+ return {
2137
+ response: this.errorResponse(id, MCP_ERRORS.METHOD_NOT_FOUND, `Unknown tool: ${name}`),
2138
+ newSessionId
2139
+ };
2140
+ }
2141
+ return {
2142
+ response: this.successResponse(id, {
2143
+ content: [{ type: "text", text: JSON.stringify(result) }]
2144
+ }),
2145
+ newSessionId
2146
+ };
2147
+ } catch (error) {
2148
+ return {
2149
+ response: this.successResponse(id, {
2150
+ content: [
2151
+ {
2152
+ type: "text",
2153
+ text: JSON.stringify({
2154
+ error: error instanceof Error ? error.message : "Unknown error"
2155
+ })
2156
+ }
2157
+ ]
2158
+ }),
2159
+ newSessionId
2160
+ };
2161
+ }
2162
+ }
2163
+ /**
2164
+ * Create a success response
2165
+ */
2166
+ successResponse(id, result) {
2167
+ return {
2168
+ jsonrpc: "2.0",
2169
+ id,
2170
+ result
2171
+ };
2172
+ }
2173
+ /**
2174
+ * Create an error response
2175
+ */
2176
+ errorResponse(id, code, message, data) {
2177
+ return {
2178
+ jsonrpc: "2.0",
2179
+ id,
2180
+ error: {
2181
+ code,
2182
+ message,
2183
+ ...data ? { data } : {}
2184
+ }
2185
+ };
2186
+ }
2187
+ /**
2188
+ * Clean up expired sessions
2189
+ */
2190
+ cleanupSessions() {
2191
+ const now = Date.now();
2192
+ for (const [sessionId, session] of this.sessions) {
2193
+ if (session.expiresAt < now) {
2194
+ this.sessions.delete(sessionId);
2195
+ }
2196
+ }
2197
+ }
2198
+ };
2199
+ function createHTTPTransport(config) {
2200
+ return new HTTPTransport(config);
2201
+ }
2202
+ export {
2203
+ HTTPTransport,
2204
+ MCP_ERRORS,
2205
+ RelayRailServer,
2206
+ VERSION,
2207
+ authenticateApiKey,
2208
+ createHTTPTransport,
2209
+ createServer,
2210
+ generateApiKey,
2211
+ getApiKeyPrefix,
2212
+ hashApiKey
2213
+ };