mcp-scraper 0.17.0 → 0.18.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.
@@ -37,7 +37,7 @@ import {
37
37
  renewConnectedDataArtifactDownload,
38
38
  sanitizeAttempts,
39
39
  sanitizeHarvestResult
40
- } from "./chunk-LENCALSN.js";
40
+ } from "./chunk-FMEDPBIV.js";
41
41
  import {
42
42
  auditImages,
43
43
  buildLinkReport,
@@ -73,7 +73,7 @@ import {
73
73
  RawMapsOverviewSchema,
74
74
  RawMapsReviewStatsSchema
75
75
  } from "./chunk-XGIPATLV.js";
76
- import "./chunk-L3FT4JBT.js";
76
+ import "./chunk-RLWCHLV3.js";
77
77
  import {
78
78
  completeExtractJob,
79
79
  countSuccessfulPages,
@@ -3506,7 +3506,7 @@ async function generateOgImage(post) {
3506
3506
  }
3507
3507
 
3508
3508
  // src/api/server.ts
3509
- import { Resend } from "resend";
3509
+ import { Resend as Resend2 } from "resend";
3510
3510
 
3511
3511
  // src/api/url-utils.ts
3512
3512
  import { isIP } from "net";
@@ -5321,7 +5321,7 @@ async function extractSite(opts) {
5321
5321
  }
5322
5322
 
5323
5323
  // src/api/server.ts
5324
- import { Hono as Hono22 } from "hono";
5324
+ import { Hono as Hono23 } from "hono";
5325
5325
  import { serve as serveInngest } from "inngest/hono";
5326
5326
 
5327
5327
  // src/inngest/client.ts
@@ -19976,6 +19976,238 @@ loadActions();
19976
19976
  </html>`;
19977
19977
  }
19978
19978
 
19979
+ // src/api/resend-inbound-routes.ts
19980
+ import { Hono as Hono22 } from "hono";
19981
+ import { Resend } from "resend";
19982
+ var SUPPORT_ADDRESS = "support@mcpscraper.dev";
19983
+ var DMARC_ADDRESS = "dmarc@mcpscraper.dev";
19984
+ var FORWARD_FROM = "MCP Scraper Support <support@updates.mcpscraper.dev>";
19985
+ function classifyInboundRecipients(recipients, ...receivingDomains) {
19986
+ const normalized = recipients.map((address) => address.trim().toLowerCase());
19987
+ const domains = receivingDomains.map((domain) => domain?.trim().toLowerCase()).filter((domain) => Boolean(domain));
19988
+ const supportAddresses = [SUPPORT_ADDRESS, ...domains.map((domain) => `support@${domain}`)];
19989
+ const dmarcAddresses = [DMARC_ADDRESS, ...domains.map((domain) => `dmarc@${domain}`)];
19990
+ if (supportAddresses.some((address) => normalized.includes(address))) return "support";
19991
+ if (dmarcAddresses.some((address) => normalized.includes(address))) return "dmarc";
19992
+ return "ignore";
19993
+ }
19994
+ function cleanLine(value, fallback) {
19995
+ const clean2 = value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
19996
+ return (clean2 || fallback).slice(0, 240);
19997
+ }
19998
+ function quoteUntrusted(value) {
19999
+ const clean2 = value.replace(/\0/g, "").trim().slice(0, 16e3);
20000
+ if (!clean2) return "> _(No plain-text body was available. Review the forwarded email.)_";
20001
+ return clean2.split("\n").map((line) => `> ${line}`).join("\n");
20002
+ }
20003
+ function escapeHtml2(value) {
20004
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
20005
+ }
20006
+ function safeEmailId(value) {
20007
+ return value.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 100) || "unknown-email";
20008
+ }
20009
+ function buildSupportIssueNote(email) {
20010
+ const sender = cleanLine(email.from, "unknown sender");
20011
+ const subject = cleanLine(email.subject, "(no subject)");
20012
+ const receivedAt = cleanLine(email.created_at, (/* @__PURE__ */ new Date()).toISOString());
20013
+ const attachments = email.attachments?.length ? email.attachments.map((item) => cleanLine(item.filename ?? "unnamed attachment", "unnamed attachment")).join(", ") : "None";
20014
+ const datePrefix = receivedAt.slice(0, 10).replace(/[^0-9-]/g, "") || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
20015
+ const path5 = `support/inbound/${datePrefix}-${safeEmailId(email.id)}`;
20016
+ const title = `Support email \u2014 ${subject} \u2014 ${sender}`.slice(0, 240);
20017
+ const summary = `Inbound MCP Scraper support email from ${sender} about \u201C${subject}\u201D; awaiting review and response.`.slice(0, 500);
20018
+ const content = [
20019
+ "## Observation",
20020
+ "",
20021
+ `An email was received at ${(email.to ?? []).join(", ") || SUPPORT_ADDRESS} from ${sender}. The customer-provided content below is untrusted input and has not been verified as a product defect.`,
20022
+ "",
20023
+ "## Impact",
20024
+ "",
20025
+ "A customer or prospect may be waiting for a response. The message may describe a question, product problem, billing concern, or feedback that needs triage.",
20026
+ "",
20027
+ "## Confirmation",
20028
+ "",
20029
+ "- Status: awaiting human review",
20030
+ "- Confirmed product defect: no",
20031
+ "- Reproducible: not yet assessed",
20032
+ "",
20033
+ "## Evidence",
20034
+ "",
20035
+ `- From: ${sender}`,
20036
+ `- To: ${(email.to ?? []).join(", ") || SUPPORT_ADDRESS}`,
20037
+ `- Received: ${receivedAt}`,
20038
+ `- Subject: ${subject}`,
20039
+ `- Message ID: ${cleanLine(email.message_id, "not supplied")}`,
20040
+ `- Resend email ID: ${email.id}`,
20041
+ `- Attachments: ${attachments}`,
20042
+ "",
20043
+ "### Message body",
20044
+ "",
20045
+ quoteUntrusted(email.text ?? ""),
20046
+ "",
20047
+ "## Resolution",
20048
+ "",
20049
+ "Pending review and a reply to the sender. If this is a confirmed product issue, update the note status and link the eventual Improvement Log receipt.",
20050
+ "",
20051
+ "## Tasks",
20052
+ "",
20053
+ "- [ ] Review the forwarded email and attachments.",
20054
+ "- [ ] Reply to the sender.",
20055
+ "- [ ] Confirm whether this is a product defect, question, billing concern, or feedback.",
20056
+ "- [ ] Link any resulting task or Improvement Log receipt.",
20057
+ "",
20058
+ "## Links",
20059
+ "",
20060
+ "_No automatic links were added; support messages should be linked during human triage to avoid false relationships._"
20061
+ ].join("\n");
20062
+ return { path: path5, title, content, summary };
20063
+ }
20064
+ async function captureSupportIssue(email, memoryKey) {
20065
+ const note = buildSupportIssueNote(email);
20066
+ const tagCandidates = [
20067
+ { tag: "customer-support", central: true, reusable: true, description: "Inbound and in-product MCP Scraper customer support requests." },
20068
+ { tag: "inbound-email", central: true, reusable: true, description: "Messages received through an inbound email integration." },
20069
+ { tag: "product-feedback", central: true, reusable: true, description: "Customer feedback and reported product problems." }
20070
+ ];
20071
+ const prepared = await memoryCall("prepareMemoryWriteTool", {
20072
+ title: note.title,
20073
+ content: note.content,
20074
+ source: `resend:email:${email.id}`,
20075
+ type: "user_friction",
20076
+ vault: "Issues",
20077
+ tagCandidates,
20078
+ maxLinks: 4
20079
+ }, memoryKey);
20080
+ if (!prepared.ok) throw new Error(prepared.error ?? "memory preparation failed");
20081
+ const resolvedTags = (prepared.tagResolutions ?? []).filter((item) => item.action !== "omit").map((item) => item.tag ?? item.candidate).filter((tag) => Boolean(tag));
20082
+ const capture = await memoryCall("memoryCaptureTool", {
20083
+ vault: "Issues",
20084
+ path: note.path,
20085
+ title: note.title,
20086
+ content: note.content,
20087
+ props: {
20088
+ status: "ai_observed",
20089
+ summary: note.summary,
20090
+ tags: resolvedTags,
20091
+ pinned: false,
20092
+ source_type: "channel",
20093
+ source_ref: `resend:email:${email.id}`,
20094
+ related: [],
20095
+ related_vault_notes: [],
20096
+ embed: true,
20097
+ embed_priority: "high",
20098
+ embedding_summary: note.summary,
20099
+ type: "user_friction",
20100
+ severity: "untriaged",
20101
+ system: "MCP Scraper customer support",
20102
+ observed_at: email.created_at,
20103
+ detected_by: "resend-email-webhook",
20104
+ reproducible: false
20105
+ },
20106
+ tagDecisions: tagCandidates
20107
+ }, memoryKey);
20108
+ if (capture.ok) return capture.note?.path ?? note.path;
20109
+ if (capture.conflict) {
20110
+ const existing = await memoryCall("getTool", { vault: "Issues", path: note.path }, memoryKey);
20111
+ if (existing.ok) return note.path;
20112
+ }
20113
+ throw new Error(capture.error ?? "memory capture failed");
20114
+ }
20115
+ async function forwardReceivedEmail(resend, email, destination, label) {
20116
+ const attachmentResult = await resend.emails.receiving.attachments.list({ emailId: email.id });
20117
+ if (attachmentResult.error) throw new Error(`attachment retrieval failed: ${attachmentResult.error.message}`);
20118
+ const attachments = (attachmentResult.data?.data ?? []).filter((item) => item.content_disposition !== "inline").map((item) => ({
20119
+ path: item.download_url,
20120
+ filename: item.filename ?? "attachment",
20121
+ contentId: item.content_id
20122
+ }));
20123
+ const sender = email.reply_to?.[0] || email.from;
20124
+ const subject = cleanLine(email.subject, "(no subject)");
20125
+ const headerText = [
20126
+ `${label} message received at ${(email.to ?? []).join(", ")}`,
20127
+ `From: ${email.from}`,
20128
+ `Reply to: ${sender}`,
20129
+ `Received: ${email.created_at}`,
20130
+ ""
20131
+ ].join("\n");
20132
+ const headerHtml = [
20133
+ '<div style="font-family:Arial,sans-serif;font-size:13px;line-height:1.5;color:#5f5a54;border-bottom:1px solid #e9e4db;padding-bottom:12px;margin-bottom:18px">',
20134
+ `<strong>${label} message received</strong><br>`,
20135
+ `From: ${escapeHtml2(email.from)}<br>`,
20136
+ `Reply to: ${escapeHtml2(sender)}<br>`,
20137
+ `Received: ${escapeHtml2(email.created_at)}`,
20138
+ "</div>"
20139
+ ].join("");
20140
+ const sent = await resend.emails.send({
20141
+ from: FORWARD_FROM,
20142
+ to: destination,
20143
+ replyTo: sender,
20144
+ subject: `[${label}] ${subject}`,
20145
+ text: `${headerText}${email.text || "(HTML-only message; view the HTML version.)"}`,
20146
+ html: email.html ? `${headerHtml}${email.html}` : void 0,
20147
+ attachments: attachments.length ? attachments : void 0,
20148
+ tags: [
20149
+ { name: "category", value: label === "Support" ? "support_forward" : "dmarc_report" },
20150
+ { name: "inbound_id", value: safeEmailId(email.id) }
20151
+ ]
20152
+ }, { idempotencyKey: `mcp-inbound-${label.toLowerCase()}-${safeEmailId(email.id)}` });
20153
+ if (sent.error) throw new Error(`forward failed: ${sent.error.message}`);
20154
+ return sent.data.id;
20155
+ }
20156
+ var resendInboundApp = new Hono22();
20157
+ resendInboundApp.post("/webhooks", async (c) => {
20158
+ const apiKey = process.env.RESEND_API_KEY?.trim();
20159
+ const webhookSecret = process.env.RESEND_WEBHOOK_SECRET?.trim();
20160
+ if (!apiKey || !webhookSecret) return c.json({ error: "inbound email is not configured" }, 503);
20161
+ const rawBody = await c.req.text();
20162
+ const resend = new Resend(apiKey);
20163
+ let event;
20164
+ try {
20165
+ event = resend.webhooks.verify({
20166
+ payload: rawBody,
20167
+ headers: {
20168
+ id: c.req.header("svix-id") ?? "",
20169
+ timestamp: c.req.header("svix-timestamp") ?? "",
20170
+ signature: c.req.header("svix-signature") ?? ""
20171
+ },
20172
+ webhookSecret
20173
+ });
20174
+ } catch {
20175
+ return c.json({ error: "invalid webhook signature" }, 400);
20176
+ }
20177
+ if (event.type !== "email.received") return c.json({ ok: true, ignored: true });
20178
+ const receivedEvent = event;
20179
+ const route = classifyInboundRecipients(
20180
+ receivedEvent.data.to ?? [],
20181
+ process.env.RESEND_RECEIVING_DOMAIN,
20182
+ process.env.RESEND_MANAGED_RECEIVING_DOMAIN
20183
+ );
20184
+ if (route === "ignore") return c.json({ ok: true, ignored: true });
20185
+ const received = await resend.emails.receiving.get(receivedEvent.data.email_id);
20186
+ if (received.error || !received.data) {
20187
+ console.error("[resend/inbound] could not retrieve received email", receivedEvent.data.email_id);
20188
+ return c.json({ error: "received email could not be retrieved" }, 502);
20189
+ }
20190
+ try {
20191
+ if (route === "support") {
20192
+ const destination2 = process.env.SUPPORT_FORWARD_TO?.trim();
20193
+ const memoryKey = process.env.SUPPORT_INBOX_MEMORY_KEY?.trim();
20194
+ if (!destination2 || !memoryKey) return c.json({ error: "support routing is not configured" }, 503);
20195
+ const [forwardedId2, memoryPath] = await Promise.all([
20196
+ forwardReceivedEmail(resend, received.data, destination2, "Support"),
20197
+ captureSupportIssue(received.data, memoryKey)
20198
+ ]);
20199
+ return c.json({ ok: true, route, forwardedId: forwardedId2, memoryPath });
20200
+ }
20201
+ const destination = process.env.DMARC_FORWARD_TO?.trim() || process.env.SUPPORT_FORWARD_TO?.trim();
20202
+ if (!destination) return c.json({ error: "DMARC routing is not configured" }, 503);
20203
+ const forwardedId = await forwardReceivedEmail(resend, received.data, destination, "DMARC");
20204
+ return c.json({ ok: true, route, forwardedId });
20205
+ } catch (err) {
20206
+ console.error("[resend/inbound] processing failed", receivedEvent.data.email_id, err instanceof Error ? err.message : String(err));
20207
+ return c.json({ error: "inbound email processing failed" }, 502);
20208
+ }
20209
+ });
20210
+
19979
20211
  // src/api/site-audit-worker.ts
19980
20212
  var MAX_CONCURRENT_SITE_AUDIT = 1;
19981
20213
  async function drainSiteAuditQueue(budget) {
@@ -20446,6 +20678,7 @@ var CONNECTED_DATA_DATASETS = [
20446
20678
  "calendar_events",
20447
20679
  "zoom_recordings",
20448
20680
  "zoom_transcripts",
20681
+ "meta_ads_insights",
20449
20682
  "resend_data",
20450
20683
  "resend_emails",
20451
20684
  "resend_received_emails",
@@ -20918,6 +21151,24 @@ var CONNECTION_SYNC_REQUIRED_TOOLS = {
20918
21151
  "get-broadcast",
20919
21152
  "list-templates",
20920
21153
  "get-template"
21154
+ ],
21155
+ "meta-marketing-api": [
21156
+ "list-ad-accounts",
21157
+ "list-campaigns",
21158
+ "list-ad-sets",
21159
+ "list-ads",
21160
+ "list-ad-creatives",
21161
+ "get-insights"
21162
+ ]
21163
+ };
21164
+ var CONNECTION_SYNC_OPTIONAL_TOOLS = {
21165
+ "meta-marketing-api": [
21166
+ "list-catalogs",
21167
+ "list-datasets",
21168
+ "list-custom-audiences",
21169
+ "list-ad-images",
21170
+ "list-ad-videos",
21171
+ "list-ad-rules"
20921
21172
  ]
20922
21173
  };
20923
21174
  function connectionSyncPolicyIssues(selections) {
@@ -20933,8 +21184,11 @@ function connectionSyncPolicyIssues(selections) {
20933
21184
  if (missingTools.length > 0) {
20934
21185
  issues.push({ providerConfigKey: selection.providerConfigKey, code: "missing_required_tools", missingTools });
20935
21186
  }
20936
- const requiredSet = new Set(required);
20937
- const unexpectedTools = [...allowed].filter((tool) => !requiredSet.has(tool));
21187
+ const permittedSet = /* @__PURE__ */ new Set([
21188
+ ...required,
21189
+ ...CONNECTION_SYNC_OPTIONAL_TOOLS[selection.providerConfigKey] ?? []
21190
+ ]);
21191
+ const unexpectedTools = [...allowed].filter((tool) => !permittedSet.has(tool));
20938
21192
  if (unexpectedTools.length > 0) {
20939
21193
  issues.push({ providerConfigKey: selection.providerConfigKey, code: "unexpected_tools", unexpectedTools });
20940
21194
  }
@@ -21043,7 +21297,10 @@ var SAFE_CONTROL_ERROR_CODES = /* @__PURE__ */ new Set([
21043
21297
  "tool_discovery_failed",
21044
21298
  "live_tool_missing",
21045
21299
  "connection_transport_unavailable",
21046
- "connection_control_failed"
21300
+ "connection_control_failed",
21301
+ "missing_provider_permission",
21302
+ "permission_verification_unavailable",
21303
+ "meta_app_feature_not_enabled"
21047
21304
  ]);
21048
21305
  var CONTROL_ERROR_CODE_ALIASES = /* @__PURE__ */ new Map([
21049
21306
  ["connection_not_active", "connection_inactive"],
@@ -21159,6 +21416,27 @@ async function getNangoCatalog() {
21159
21416
  const safeDefaultAllowedTools = cleanTools(
21160
21417
  row.safeDefaultAllowedTools ?? row.safe_default_allowed_tools ?? row.defaultAllowedTools ?? row.default_allowed_tools ?? row.allowedTools ?? row.allowed_tools
21161
21418
  ).filter((tool) => !disabledTools?.has(tool));
21419
+ const actionTools = cleanTools(
21420
+ row.actionTools ?? row.action_tools ?? row.writeTools ?? row.write_tools
21421
+ ).filter((tool) => !disabledTools?.has(tool));
21422
+ const requiredPermissionsByTool = {};
21423
+ const rawRequiredPermissions = row.requiredPermissionsByTool ?? row.required_permissions_by_tool;
21424
+ if (isRecord2(rawRequiredPermissions)) {
21425
+ for (const [tool, permissions] of Object.entries(rawRequiredPermissions).slice(0, 500)) {
21426
+ const cleanTool = cleanString(tool, 200);
21427
+ if (!cleanTool || !safeDefaultAllowedTools.includes(cleanTool) && !actionTools.includes(cleanTool)) continue;
21428
+ requiredPermissionsByTool[cleanTool] = cleanStringArray(permissions, 32, 200);
21429
+ }
21430
+ }
21431
+ const requiredFeaturesByTool = {};
21432
+ const rawRequiredFeatures = row.requiredFeaturesByTool ?? row.required_features_by_tool;
21433
+ if (isRecord2(rawRequiredFeatures)) {
21434
+ for (const [tool, features] of Object.entries(rawRequiredFeatures).slice(0, 500)) {
21435
+ const cleanTool = cleanString(tool, 200);
21436
+ if (!cleanTool) continue;
21437
+ requiredFeaturesByTool[cleanTool] = cleanStringArray(features, 32, 200);
21438
+ }
21439
+ }
21162
21440
  const platformSetupStatus = firstString(row, [
21163
21441
  "platformSetupStatus",
21164
21442
  "platform_setup_status",
@@ -21168,6 +21446,7 @@ async function getNangoCatalog() {
21168
21446
  const appReviewStatus = row.appReviewLimited === true || row.app_review_limited === true ? "limited" : firstString(row, ["appReviewStatus", "app_review_status", "reviewStatus", "review_status"], 100);
21169
21447
  const appReviewNote = firstString(row, ["appReviewNote", "app_review_note", "reviewNote", "review_note"], 500);
21170
21448
  const connectionSyncRequiredTools = [...CONNECTION_SYNC_REQUIRED_TOOLS[providerConfigKey] ?? []];
21449
+ const connectionSyncOptionalTools = [...CONNECTION_SYNC_OPTIONAL_TOOLS[providerConfigKey] ?? []];
21171
21450
  result.push({
21172
21451
  providerConfigKey,
21173
21452
  provider,
@@ -21178,8 +21457,13 @@ async function getNangoCatalog() {
21178
21457
  authMode,
21179
21458
  categories,
21180
21459
  safeDefaultAllowedTools,
21460
+ actionTools,
21461
+ requiredPermissionsByTool,
21462
+ requiredFeaturesByTool,
21463
+ enabledFeatures: cleanStringArray(row.enabledFeatures ?? row.enabled_features, 32, 200),
21181
21464
  connectionSyncSupported: connectionSyncRequiredTools.length > 0,
21182
21465
  connectionSyncRequiredTools,
21466
+ connectionSyncOptionalTools,
21183
21467
  platformSetupStatus,
21184
21468
  appReviewStatus,
21185
21469
  appReviewNote
@@ -21201,6 +21485,30 @@ async function getNangoConnections(identity) {
21201
21485
  const label = firstString(row, ["label", "accountLabel", "account_label", "displayName", "display_name", "endUserEmail", "end_user_email"]) || providerConfigKey;
21202
21486
  const rawStatus = firstString(row, ["status"], 64) || "connected";
21203
21487
  const reconnectRequired = row.reconnectRequired === true || row.reconnect_required === true || rawStatus === "needs_reauth" || rawStatus === "reauth_required" || rawStatus === "invalid" || rawStatus === "error" || rawStatus === "revoked" || rawStatus === "disabled";
21488
+ const toolCapabilities = [];
21489
+ const rawToolCapabilities = row.toolCapabilities ?? row.tool_capabilities;
21490
+ if (Array.isArray(rawToolCapabilities)) {
21491
+ for (const value of rawToolCapabilities.slice(0, 500)) {
21492
+ if (!isRecord2(value)) continue;
21493
+ const name = cleanString(value.name, 200);
21494
+ const classification = value.classification;
21495
+ const blockedValue = Object.prototype.hasOwnProperty.call(value, "blockedReason") ? value.blockedReason : value.blocked_reason;
21496
+ const blockedReason = blockedValue === null ? null : blockedValue === "missing_permission" || blockedValue === "permission_policy_missing" || blockedValue === "permission_verification_unavailable" || blockedValue === "missing_app_feature" ? blockedValue : void 0;
21497
+ if (!name || classification !== "read" && classification !== "action" || typeof value.available !== "boolean" || blockedReason === void 0) continue;
21498
+ toolCapabilities.push({
21499
+ name,
21500
+ classification,
21501
+ requiredPermissions: cleanStringArray(value.requiredPermissions ?? value.required_permissions, 32, 200),
21502
+ requiredFeatures: cleanStringArray(value.requiredFeatures ?? value.required_features, 32, 200),
21503
+ available: value.available,
21504
+ blockedReason,
21505
+ missingPermissions: cleanStringArray(value.missingPermissions ?? value.missing_permissions, 32, 200),
21506
+ missingFeatures: cleanStringArray(value.missingFeatures ?? value.missing_features, 32, 200)
21507
+ });
21508
+ }
21509
+ }
21510
+ const permissionVerificationValue = row.permissionVerification ?? row.permission_verification;
21511
+ const permissionVerification = permissionVerificationValue === "verified" || permissionVerificationValue === "unavailable" ? permissionVerificationValue : null;
21204
21512
  result.push({
21205
21513
  connectionId,
21206
21514
  providerConfigKey,
@@ -21211,6 +21519,10 @@ async function getNangoConnections(identity) {
21211
21519
  actionsEnabled: row.actionsEnabled === true || row.actions_enabled === true,
21212
21520
  readTools: cleanTools(row.readTools ?? row.read_tools),
21213
21521
  actionTools: cleanTools(row.actionTools ?? row.action_tools ?? row.writeTools ?? row.write_tools),
21522
+ toolCapabilities,
21523
+ grantedPermissions: cleanStringArray(row.grantedPermissions ?? row.granted_permissions, 64, 200),
21524
+ enabledFeatures: cleanStringArray(row.enabledFeatures ?? row.enabled_features, 32, 200),
21525
+ permissionVerification,
21214
21526
  mcpEndpoint: null,
21215
21527
  schemaDiscovery: "compatibility_describe",
21216
21528
  toolRevision: firstString(row, ["toolRevision", "tool_revision"], 200),
@@ -21438,7 +21750,7 @@ async function describeNangoTool(identity, connectionId, tool, fresh) {
21438
21750
  const outputSchemaValue = rawTool.outputSchema ?? rawTool.output_schema;
21439
21751
  const outputSchema = outputSchemaValue === void 0 ? void 0 : sanitizeToolSchema(outputSchemaValue);
21440
21752
  const blockedReasonValue = rawTool.blockedReason ?? rawTool.blocked_reason;
21441
- const blockedReason = blockedReasonValue === null || blockedReasonValue === void 0 ? null : blockedReasonValue === "actions_disabled" || blockedReasonValue === "inactive_connection" ? blockedReasonValue : void 0;
21753
+ const blockedReason = blockedReasonValue === null || blockedReasonValue === void 0 ? null : blockedReasonValue === "actions_disabled" || blockedReasonValue === "inactive_connection" || blockedReasonValue === "missing_permission" || blockedReasonValue === "missing_app_feature" || blockedReasonValue === "permission_policy_missing" || blockedReasonValue === "permission_verification_unavailable" ? blockedReasonValue : void 0;
21442
21754
  if (serializedBytes > 256 * 1024 || !name || name !== tool || classification !== "read" && classification !== "action" || typeof rawTool.callable !== "boolean" || blockedReason === void 0 || transport !== "nango" && transport !== "remote_mcp" || !providerConfigKey || schemaSource !== "live_tools_list" || !upstreamSchemaHash || !/^[a-f0-9]{64}$/i.test(upstreamSchemaHash) || !fetchedAt || Number.isNaN(Date.parse(fetchedAt)) || !inputSchema || outputSchemaValue !== void 0 && !outputSchema) {
21443
21755
  throw new NangoControlError(
21444
21756
  "The connection service returned an invalid live tool description.",
@@ -21455,6 +21767,10 @@ async function describeNangoTool(identity, connectionId, tool, fresh) {
21455
21767
  const annotations = sanitizeToolAnnotations(rawTool.annotations);
21456
21768
  const icons = sanitizeToolIcons(rawTool.icons);
21457
21769
  const execution = taskSupport ? { taskSupport } : void 0;
21770
+ const requiredPermissions = cleanStringArray(rawTool.requiredPermissions ?? rawTool.required_permissions, 32, 200);
21771
+ const missingPermissions = cleanStringArray(rawTool.missingPermissions ?? rawTool.missing_permissions, 32, 200);
21772
+ const requiredFeatures = cleanStringArray(rawTool.requiredFeatures ?? rawTool.required_features, 32, 200);
21773
+ const missingFeatures = cleanStringArray(rawTool.missingFeatures ?? rawTool.missing_features, 32, 200);
21458
21774
  const schemaHash = projectedToolSchemaHash({
21459
21775
  name,
21460
21776
  inputSchema,
@@ -21472,6 +21788,10 @@ async function describeNangoTool(identity, connectionId, tool, fresh) {
21472
21788
  classification,
21473
21789
  callable: rawTool.callable,
21474
21790
  blockedReason,
21791
+ requiredPermissions,
21792
+ missingPermissions,
21793
+ requiredFeatures,
21794
+ missingFeatures,
21475
21795
  transport,
21476
21796
  providerConfigKey,
21477
21797
  protocolVersion,
@@ -21912,7 +22232,18 @@ var sessionAuth = createMiddleware3(async (c, next) => {
21912
22232
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
21913
22233
  return next();
21914
22234
  });
21915
- var app = new Hono22();
22235
+ var requireIntegrationsTier = createMiddleware3(async (c, next) => {
22236
+ const user = c.get("user");
22237
+ if (!user.subscription_tier) {
22238
+ return c.json({
22239
+ ok: false,
22240
+ error: "Integrations require an active Starter plan or higher.",
22241
+ code: "subscription_required"
22242
+ }, 403);
22243
+ }
22244
+ return next();
22245
+ });
22246
+ var app = new Hono23();
21916
22247
  var STRIPE_API_VERSION = "2026-02-25.clover";
21917
22248
  function requireStripeSecret() {
21918
22249
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
@@ -22080,7 +22411,7 @@ app.post("/auth/forgot-password", requireAllowedOrigin, async (c) => {
22080
22411
  const resetUrl = `${appUrl}/reset-password?token=${token}`;
22081
22412
  if (process.env.RESEND_API_KEY) {
22082
22413
  try {
22083
- const resend = new Resend(process.env.RESEND_API_KEY);
22414
+ const resend = new Resend2(process.env.RESEND_API_KEY);
22084
22415
  const sent = await resend.emails.send({
22085
22416
  from: "MCP Scraper <noreply@updates.mcpscraper.dev>",
22086
22417
  to: normalizedEmail,
@@ -22136,9 +22467,11 @@ app.get("/me", async (c) => {
22136
22467
  return c.json({ authenticated: false });
22137
22468
  }
22138
22469
  const refreshed = await applyMonthlyFreeRefresh(foundUser);
22139
- const balanceMc = await reconcileBalanceMc(refreshed.id);
22470
+ const [balanceMc, stats] = await Promise.all([
22471
+ reconcileBalanceMc(refreshed.id),
22472
+ getUserStats(refreshed.id)
22473
+ ]);
22140
22474
  const user = { ...refreshed, balance_mc: balanceMc };
22141
- const stats = await getUserStats(user.id);
22142
22475
  return c.json({
22143
22476
  authenticated: true,
22144
22477
  id: user.id,
@@ -22508,7 +22841,34 @@ async function combinedConnections(identity) {
22508
22841
  if (nango.status === "rejected" && resend.status === "rejected") throw nango.reason;
22509
22842
  return [
22510
22843
  ...nango.status === "fulfilled" ? nango.value.map((connection) => ({ ...connection, transport: "nango", adminBlockedTools: [] })) : [],
22511
- ...resend.status === "fulfilled" ? resend.value : []
22844
+ ...resend.status === "fulfilled" ? resend.value.map((connection) => ({
22845
+ ...connection,
22846
+ toolCapabilities: [
22847
+ ...connection.readTools.map((name) => ({
22848
+ name,
22849
+ classification: "read",
22850
+ requiredPermissions: [],
22851
+ requiredFeatures: [],
22852
+ available: true,
22853
+ blockedReason: null,
22854
+ missingPermissions: [],
22855
+ missingFeatures: []
22856
+ })),
22857
+ ...connection.actionTools.map((name) => ({
22858
+ name,
22859
+ classification: "action",
22860
+ requiredPermissions: [],
22861
+ requiredFeatures: [],
22862
+ available: true,
22863
+ blockedReason: null,
22864
+ missingPermissions: [],
22865
+ missingFeatures: []
22866
+ }))
22867
+ ],
22868
+ grantedPermissions: [],
22869
+ enabledFeatures: [],
22870
+ permissionVerification: null
22871
+ })) : []
22512
22872
  ];
22513
22873
  }
22514
22874
  async function isResendConnection(identity, connectionId, providerHint) {
@@ -22554,7 +22914,7 @@ app.get("/schedule-connections", auth2, async (c) => {
22554
22914
  return scheduleConnectionError(c, err, "Unable to load service connections.");
22555
22915
  }
22556
22916
  });
22557
- app.post("/schedule-connections/session", auth2, async (c) => {
22917
+ app.post("/schedule-connections/session", auth2, requireIntegrationsTier, async (c) => {
22558
22918
  const user = c.get("user");
22559
22919
  const body = await c.req.json().catch(() => ({}));
22560
22920
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
@@ -22566,7 +22926,7 @@ app.post("/schedule-connections/session", auth2, async (c) => {
22566
22926
  return scheduleConnectionError(c, err, "Unable to start the service connection flow.");
22567
22927
  }
22568
22928
  });
22569
- app.post("/schedule-connections/:id/reconnect-session", auth2, async (c) => {
22929
+ app.post("/schedule-connections/:id/reconnect-session", auth2, requireIntegrationsTier, async (c) => {
22570
22930
  const user = c.get("user");
22571
22931
  const body = await c.req.json().catch(() => ({}));
22572
22932
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
@@ -22591,7 +22951,7 @@ app.delete("/schedule-connections/:id", auth2, async (c) => {
22591
22951
  return scheduleConnectionError(c, err, "Unable to disconnect this service connection.");
22592
22952
  }
22593
22953
  });
22594
- app.post("/schedule-connections/:id/enable-actions", auth2, async (c) => {
22954
+ app.post("/schedule-connections/:id/enable-actions", auth2, requireIntegrationsTier, async (c) => {
22595
22955
  const user = c.get("user");
22596
22956
  const body = await c.req.json().catch(() => ({}));
22597
22957
  if (typeof body.enabled !== "boolean") return c.json({ ok: false, error: "enabled must be a boolean." }, 400);
@@ -22627,7 +22987,7 @@ app.get("/oauth/resend/callback", async (c) => {
22627
22987
  return c.html(renderResendOAuthResult(false), 502);
22628
22988
  }
22629
22989
  });
22630
- app.post("/schedule-connections/actions/slack/send-message", auth2, async (c) => {
22990
+ app.post("/schedule-connections/actions/slack/send-message", auth2, requireIntegrationsTier, async (c) => {
22631
22991
  const user = c.get("user");
22632
22992
  const body = await c.req.json().catch(() => ({}));
22633
22993
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -22641,7 +23001,7 @@ app.post("/schedule-connections/actions/slack/send-message", auth2, async (c) =>
22641
23001
  return scheduleConnectionError(c, err, "Unable to send the Slack message.");
22642
23002
  }
22643
23003
  });
22644
- app.post("/schedule-connections/actions/gmail/send-message", auth2, async (c) => {
23004
+ app.post("/schedule-connections/actions/gmail/send-message", auth2, requireIntegrationsTier, async (c) => {
22645
23005
  const user = c.get("user");
22646
23006
  const body = await c.req.json().catch(() => ({}));
22647
23007
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -22655,7 +23015,7 @@ app.post("/schedule-connections/actions/gmail/send-message", auth2, async (c) =>
22655
23015
  return scheduleConnectionError(c, err, "Unable to send the email.");
22656
23016
  }
22657
23017
  });
22658
- app.post("/schedule-connections/actions/google-calendar/create-event", auth2, async (c) => {
23018
+ app.post("/schedule-connections/actions/google-calendar/create-event", auth2, requireIntegrationsTier, async (c) => {
22659
23019
  const user = c.get("user");
22660
23020
  const body = await c.req.json().catch(() => ({}));
22661
23021
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -22677,7 +23037,7 @@ app.post("/schedule-connections/actions/google-calendar/create-event", auth2, as
22677
23037
  return scheduleConnectionError(c, err, "Unable to create the calendar event.");
22678
23038
  }
22679
23039
  });
22680
- app.post("/schedule-connections/actions/zoom/create-meeting", auth2, async (c) => {
23040
+ app.post("/schedule-connections/actions/zoom/create-meeting", auth2, requireIntegrationsTier, async (c) => {
22681
23041
  const user = c.get("user");
22682
23042
  const body = await c.req.json().catch(() => ({}));
22683
23043
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -22697,7 +23057,7 @@ app.post("/schedule-connections/actions/zoom/create-meeting", auth2, async (c) =
22697
23057
  return scheduleConnectionError(c, err, "Unable to create the Zoom meeting.");
22698
23058
  }
22699
23059
  });
22700
- app.post("/schedule-connections/actions/read", auth2, async (c) => {
23060
+ app.post("/schedule-connections/actions/read", auth2, requireIntegrationsTier, async (c) => {
22701
23061
  const user = c.get("user");
22702
23062
  const body = await c.req.json().catch(() => ({}));
22703
23063
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -22714,7 +23074,7 @@ app.post("/schedule-connections/actions/read", auth2, async (c) => {
22714
23074
  return scheduleConnectionError(c, err, "Unable to read from this connection.");
22715
23075
  }
22716
23076
  });
22717
- app.post("/schedule-connections/actions/import-memory", auth2, async (c) => {
23077
+ app.post("/schedule-connections/actions/import-memory", auth2, requireIntegrationsTier, async (c) => {
22718
23078
  const user = c.get("user");
22719
23079
  const body = await c.req.json().catch(() => ({}));
22720
23080
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -22771,7 +23131,7 @@ app.post("/schedule-connections/actions/import-memory", auth2, async (c) => {
22771
23131
  return scheduleConnectionError(c, err, "Unable to import this connected-service snapshot into Memory.");
22772
23132
  }
22773
23133
  });
22774
- app.post("/schedule-connections/actions/describe", auth2, async (c) => {
23134
+ app.post("/schedule-connections/actions/describe", auth2, requireIntegrationsTier, async (c) => {
22775
23135
  const user = c.get("user");
22776
23136
  const body = await c.req.json().catch(() => ({}));
22777
23137
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -22788,7 +23148,7 @@ app.post("/schedule-connections/actions/describe", auth2, async (c) => {
22788
23148
  return scheduleConnectionError(c, err, "Unable to describe this connection tool.");
22789
23149
  }
22790
23150
  });
22791
- app.post("/schedule-connections/actions/export", auth2, async (c) => {
23151
+ app.post("/schedule-connections/actions/export", auth2, requireIntegrationsTier, async (c) => {
22792
23152
  const user = c.get("user");
22793
23153
  const body = await c.req.json().catch(() => ({}));
22794
23154
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -22848,7 +23208,7 @@ app.post("/schedule-connections/actions/export", auth2, async (c) => {
22848
23208
  return scheduleConnectionError(c, err, "Unable to export this connected service.");
22849
23209
  }
22850
23210
  });
22851
- app.post("/schedule-connections/actions/export-download", auth2, async (c) => {
23211
+ app.post("/schedule-connections/actions/export-download", auth2, requireIntegrationsTier, async (c) => {
22852
23212
  const user = c.get("user");
22853
23213
  const body = await c.req.json().catch(() => ({}));
22854
23214
  const artifactId = typeof body.artifactId === "string" ? body.artifactId.trim() : "";
@@ -22865,7 +23225,7 @@ app.post("/schedule-connections/actions/export-download", auth2, async (c) => {
22865
23225
  return c.json({ ok: false, error: "Unable to renew this artifact download." }, 502);
22866
23226
  }
22867
23227
  });
22868
- app.post("/schedule-connections/actions/call", auth2, async (c) => {
23228
+ app.post("/schedule-connections/actions/call", auth2, requireIntegrationsTier, async (c) => {
22869
23229
  const user = c.get("user");
22870
23230
  const body = await c.req.json().catch(() => ({}));
22871
23231
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -22985,7 +23345,7 @@ app.get("/schedule-actions/:id/connections", auth2, async (c) => {
22985
23345
  return scheduleConnectionError(c, err, "Unable to load scheduled action service connections.");
22986
23346
  }
22987
23347
  });
22988
- app.put("/schedule-actions/:id/connections", auth2, async (c) => {
23348
+ app.put("/schedule-actions/:id/connections", auth2, requireIntegrationsTier, async (c) => {
22989
23349
  const user = c.get("user");
22990
23350
  const body = await c.req.json().catch(() => ({}));
22991
23351
  let connections;
@@ -23270,8 +23630,7 @@ app.get("/history", auth2, async (c) => {
23270
23630
  location: job.options?.location ?? "",
23271
23631
  source: LedgerOperation.SERP,
23272
23632
  status: job.status,
23273
- result_count: job.result ? (job.result.flat?.length ?? 0) + (job.result.organicResults?.length ?? 0) : 0,
23274
- result: job.result ?? null
23633
+ result_count: job.result ? (job.result.flat?.length ?? 0) + (job.result.organicResults?.length ?? 0) : 0
23275
23634
  }));
23276
23635
  const eventRows = events.map((event) => ({
23277
23636
  id: event.id,
@@ -23281,7 +23640,6 @@ app.get("/history", auth2, async (c) => {
23281
23640
  source: event.source,
23282
23641
  status: event.status,
23283
23642
  result_count: event.result_count ?? 0,
23284
- result: event.result ?? null,
23285
23643
  error: event.error ?? null
23286
23644
  }));
23287
23645
  const rows = [...jobRows, ...eventRows].sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
@@ -23973,6 +24331,7 @@ app.route("/mcp", mcpApp);
23973
24331
  app.route("/agent", buildBrowserAgentRoutes());
23974
24332
  app.route("/chat", chatApp);
23975
24333
  app.route("/schedule", scheduleApp);
24334
+ app.route("/resend", resendInboundApp);
23976
24335
  app.get("/console", (c) => c.html(renderConsoleHtml()));
23977
24336
  app.get("/console/auth/:id", async (c) => {
23978
24337
  const id = c.req.param("id");
@@ -24102,4 +24461,4 @@ app.get("/blog/:slug/", (c) => {
24102
24461
  export {
24103
24462
  app
24104
24463
  };
24105
- //# sourceMappingURL=server-5KTVLL3L.js.map
24464
+ //# sourceMappingURL=server-MQDCAR6I.js.map