mcp-scraper 0.15.1 → 0.16.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-QPYYOTT2.js";
40
+ } from "./chunk-YIV4IKFG.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-EZAQWOKI.js";
76
+ import "./chunk-HQYIP5X3.js";
77
77
  import {
78
78
  completeExtractJob,
79
79
  countSuccessfulPages,
@@ -20633,6 +20633,7 @@ async function dbUsage(identity, plan) {
20633
20633
  }
20634
20634
 
20635
20635
  // src/api/nango-control.ts
20636
+ import { createHash as createHash5 } from "crypto";
20636
20637
  var DEFAULT_NANGO_CONTROL_URL = "https://mcp-scraper-scheduler.vercel.app";
20637
20638
  var DISABLED_NANGO_TOOLS = {
20638
20639
  "google-mail": /* @__PURE__ */ new Set(["list-filters"]),
@@ -20711,10 +20712,14 @@ function connectionSyncSelectionError(connections) {
20711
20712
  }
20712
20713
  var NangoControlError = class extends Error {
20713
20714
  status;
20714
- constructor(message, status = 502) {
20715
+ code;
20716
+ retryable;
20717
+ constructor(message, status = 502, code = status === 503 ? "connection_transport_unavailable" : "connection_control_failed", retryable = status === 429 || status >= 500) {
20715
20718
  super(message);
20716
20719
  this.name = "NangoControlError";
20717
20720
  this.status = status;
20721
+ this.code = code;
20722
+ this.retryable = retryable;
20718
20723
  }
20719
20724
  };
20720
20725
  var ScheduleConnectionValidationError = class extends Error {
@@ -20779,6 +20784,63 @@ function arrayFromPayload(value, keys) {
20779
20784
  }
20780
20785
  return [];
20781
20786
  }
20787
+ var SAFE_CONTROL_ERROR_CODES = /* @__PURE__ */ new Set([
20788
+ "invalid_request",
20789
+ "tool_not_allowed",
20790
+ "invalid_schema",
20791
+ "connection_not_found",
20792
+ "connection_inactive",
20793
+ "actions_disabled",
20794
+ "action_not_allowed",
20795
+ "upstream_rate_limited",
20796
+ "tool_discovery_failed",
20797
+ "live_tool_missing",
20798
+ "connection_transport_unavailable",
20799
+ "connection_control_failed"
20800
+ ]);
20801
+ var CONTROL_ERROR_CODE_ALIASES = /* @__PURE__ */ new Map([
20802
+ ["connection_not_active", "connection_inactive"],
20803
+ ["actions_not_enabled", "actions_disabled"],
20804
+ ["action_tool_not_allowed", "action_not_allowed"],
20805
+ ["no_action_for_provider", "action_not_allowed"],
20806
+ ["tool_not_available", "live_tool_missing"]
20807
+ ]);
20808
+ function controlErrorStatus(status) {
20809
+ if (status === 400 || status === 403 || status === 404 || status === 409 || status === 429 || status === 502 || status === 503) {
20810
+ return status;
20811
+ }
20812
+ return status >= 500 ? 503 : 502;
20813
+ }
20814
+ function defaultControlErrorCode(status) {
20815
+ if (status === 400) return "invalid_request";
20816
+ if (status === 403) return "actions_disabled";
20817
+ if (status === 404) return "connection_not_found";
20818
+ if (status === 409) return "connection_inactive";
20819
+ if (status === 429) return "upstream_rate_limited";
20820
+ if (status === 503) return "connection_transport_unavailable";
20821
+ return "connection_control_failed";
20822
+ }
20823
+ function defaultControlErrorMessage(status) {
20824
+ if (status === 400) return "The connection request was invalid.";
20825
+ if (status === 403) return "Actions are disabled for this service connection.";
20826
+ if (status === 404) return "The service connection was not found.";
20827
+ if (status === 409) return "The service connection is not currently usable.";
20828
+ if (status === 429) return "The connected service is temporarily rate limited.";
20829
+ if (status === 503) return "The service connection transport is temporarily unavailable.";
20830
+ return "The service connection control request failed.";
20831
+ }
20832
+ function safeControlErrorPayload(body, responseStatus) {
20833
+ const status = controlErrorStatus(responseStatus);
20834
+ const unwrapped = unwrapData(body);
20835
+ const record = isRecord(unwrapped) ? unwrapped : isRecord(body) ? body : null;
20836
+ const candidateCode = record ? firstString(record, ["code", "errorCode", "error_code"], 100) : null;
20837
+ const normalizedCandidateCode = candidateCode ? CONTROL_ERROR_CODE_ALIASES.get(candidateCode) ?? candidateCode : null;
20838
+ const code = normalizedCandidateCode && SAFE_CONTROL_ERROR_CODES.has(normalizedCandidateCode) ? normalizedCandidateCode : defaultControlErrorCode(status);
20839
+ const candidateMessage = record ? firstString(record, ["error", "message"], 500) : null;
20840
+ const message = normalizedCandidateCode && SAFE_CONTROL_ERROR_CODES.has(normalizedCandidateCode) && candidateMessage ? candidateMessage.replace(/[\u0000-\u001f\u007f]/g, " ") : defaultControlErrorMessage(status);
20841
+ const retryable = record && typeof record.retryable === "boolean" ? record.retryable : status === 429 || status >= 500;
20842
+ return { status, code, message, retryable };
20843
+ }
20782
20844
  async function controlRequest(path5, init, timeoutMs = 2e4) {
20783
20845
  const response = await fetch(`${controlBaseUrl()}${path5}`, {
20784
20846
  ...init,
@@ -20790,7 +20852,13 @@ async function controlRequest(path5, init, timeoutMs = 2e4) {
20790
20852
  },
20791
20853
  signal: AbortSignal.timeout(timeoutMs)
20792
20854
  }).catch((err) => {
20793
- throw new NangoControlError(`Scheduled service connection control is unavailable: ${err instanceof Error ? err.message : "network error"}`);
20855
+ const cause = err instanceof Error && err.name === "TimeoutError" ? "request timed out" : "network error";
20856
+ throw new NangoControlError(
20857
+ `Scheduled service connection control is unavailable: ${cause}.`,
20858
+ 503,
20859
+ "connection_transport_unavailable",
20860
+ true
20861
+ );
20794
20862
  });
20795
20863
  const text = await response.text();
20796
20864
  let body = {};
@@ -20800,8 +20868,8 @@ async function controlRequest(path5, init, timeoutMs = 2e4) {
20800
20868
  body = {};
20801
20869
  }
20802
20870
  if (!response.ok) {
20803
- const upstream = isRecord(body) ? cleanString(body.error, 300) : null;
20804
- throw new NangoControlError(upstream || `Scheduled service connection control failed (${response.status}).`);
20871
+ const error = safeControlErrorPayload(body, response.status);
20872
+ throw new NangoControlError(error.message, error.status, error.code, error.retryable);
20805
20873
  }
20806
20874
  return body;
20807
20875
  }
@@ -20896,6 +20964,9 @@ async function getNangoConnections(identity) {
20896
20964
  actionsEnabled: row.actionsEnabled === true || row.actions_enabled === true,
20897
20965
  readTools: cleanTools(row.readTools ?? row.read_tools),
20898
20966
  actionTools: cleanTools(row.actionTools ?? row.action_tools ?? row.writeTools ?? row.write_tools),
20967
+ mcpEndpoint: null,
20968
+ schemaDiscovery: "compatibility_describe",
20969
+ toolRevision: firstString(row, ["toolRevision", "tool_revision"], 200),
20899
20970
  vaultName: firstString(row, ["vaultName", "vault_name"], 100),
20900
20971
  tableName: firstString(row, ["tableName", "table_name"], 100),
20901
20972
  createdAt: firstString(row, ["createdAt", "created_at"], 100),
@@ -21028,6 +21099,145 @@ async function callScheduleConnectionRead(identity, connectionId, tool, args) {
21028
21099
  const data = unwrapData(body);
21029
21100
  return isRecord(data) ? data.result ?? data : data;
21030
21101
  }
21102
+ function sanitizeToolSchema(value) {
21103
+ if (!isRecord(value) || value.type !== "object") return null;
21104
+ try {
21105
+ const serialized = JSON.stringify(value);
21106
+ if (Buffer.byteLength(serialized, "utf8") > 256 * 1024) return null;
21107
+ const cloned = JSON.parse(serialized);
21108
+ return isRecord(cloned) ? cloned : null;
21109
+ } catch {
21110
+ return null;
21111
+ }
21112
+ }
21113
+ function sanitizeToolAnnotations(value) {
21114
+ if (!isRecord(value)) return void 0;
21115
+ const annotations = {};
21116
+ const title = cleanString(value.title, 200);
21117
+ if (title) annotations.title = title;
21118
+ for (const key of ["readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"]) {
21119
+ if (typeof value[key] === "boolean") annotations[key] = value[key];
21120
+ }
21121
+ return Object.keys(annotations).length > 0 ? annotations : void 0;
21122
+ }
21123
+ function sanitizeToolIconSource(value) {
21124
+ const candidate = cleanString(value, 1e4);
21125
+ if (!candidate) return null;
21126
+ if (/^data:image\/(?:png|jpeg|webp|gif|svg\+xml);base64,[a-z0-9+/=]+$/i.test(candidate)) return candidate;
21127
+ return cleanHttpsUrl(candidate);
21128
+ }
21129
+ function sanitizeToolIcons(value) {
21130
+ if (!Array.isArray(value)) return void 0;
21131
+ const icons = [];
21132
+ for (const item of value.slice(0, 16)) {
21133
+ if (!isRecord(item)) continue;
21134
+ const src = sanitizeToolIconSource(item.src);
21135
+ if (!src) continue;
21136
+ const mimeType = cleanString(item.mimeType ?? item.mime_type, 100);
21137
+ const sizes = cleanStringArray(item.sizes, 16, 32);
21138
+ const theme = item.theme === "light" || item.theme === "dark" ? item.theme : void 0;
21139
+ icons.push({
21140
+ src,
21141
+ ...mimeType ? { mimeType } : {},
21142
+ ...sizes.length > 0 ? { sizes } : {},
21143
+ ...theme ? { theme } : {}
21144
+ });
21145
+ }
21146
+ return icons.length > 0 ? icons : void 0;
21147
+ }
21148
+ function canonicalJson(value) {
21149
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
21150
+ if (isRecord(value)) {
21151
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
21152
+ }
21153
+ return JSON.stringify(value);
21154
+ }
21155
+ function projectedToolSchemaHash(tool) {
21156
+ return createHash5("sha256").update(canonicalJson(tool)).digest("hex");
21157
+ }
21158
+ async function describeNangoTool(identity, connectionId, tool, fresh) {
21159
+ const body = await controlRequest("/api/internal/nango/connections/actions/describe", {
21160
+ method: "POST",
21161
+ body: JSON.stringify({
21162
+ identity,
21163
+ connectionId,
21164
+ tool,
21165
+ ...fresh === void 0 ? {} : { fresh }
21166
+ })
21167
+ });
21168
+ const data = unwrapData(body);
21169
+ const rawTool = isRecord(data) && isRecord(data.tool) ? data.tool : data;
21170
+ if (!isRecord(rawTool)) {
21171
+ throw new NangoControlError(
21172
+ "The connection service returned an invalid live tool description.",
21173
+ 502,
21174
+ "tool_discovery_failed",
21175
+ true
21176
+ );
21177
+ }
21178
+ let serializedBytes = Number.POSITIVE_INFINITY;
21179
+ try {
21180
+ serializedBytes = Buffer.byteLength(JSON.stringify(rawTool), "utf8");
21181
+ } catch {
21182
+ }
21183
+ const name = cleanString(rawTool.name, 200);
21184
+ const classification = cleanString(rawTool.classification, 20);
21185
+ const transport = cleanString(rawTool.transport, 20);
21186
+ const providerConfigKey = cleanString(rawTool.providerConfigKey ?? rawTool.provider_config_key, 200);
21187
+ const schemaSource = cleanString(rawTool.schemaSource ?? rawTool.schema_source, 50);
21188
+ const upstreamSchemaHash = cleanString(rawTool.schemaHash ?? rawTool.schema_hash, 200);
21189
+ const fetchedAt = cleanString(rawTool.fetchedAt ?? rawTool.fetched_at, 100);
21190
+ const inputSchema = sanitizeToolSchema(rawTool.inputSchema ?? rawTool.input_schema);
21191
+ const outputSchemaValue = rawTool.outputSchema ?? rawTool.output_schema;
21192
+ const outputSchema = outputSchemaValue === void 0 ? void 0 : sanitizeToolSchema(outputSchemaValue);
21193
+ const blockedReasonValue = rawTool.blockedReason ?? rawTool.blocked_reason;
21194
+ const blockedReason = blockedReasonValue === null || blockedReasonValue === void 0 ? null : blockedReasonValue === "actions_disabled" || blockedReasonValue === "inactive_connection" ? blockedReasonValue : void 0;
21195
+ 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) {
21196
+ throw new NangoControlError(
21197
+ "The connection service returned an invalid live tool description.",
21198
+ 502,
21199
+ "tool_discovery_failed",
21200
+ true
21201
+ );
21202
+ }
21203
+ const protocolVersion = rawTool.protocolVersion === null || rawTool.protocol_version === null ? null : cleanString(rawTool.protocolVersion ?? rawTool.protocol_version, 100);
21204
+ const executionValue = rawTool.execution;
21205
+ const taskSupport = isRecord(executionValue) && (executionValue.taskSupport === "forbidden" || executionValue.taskSupport === "optional" || executionValue.taskSupport === "required") ? executionValue.taskSupport : null;
21206
+ const title = cleanString(rawTool.title, 200);
21207
+ const description = cleanString(rawTool.description, 12e3);
21208
+ const annotations = sanitizeToolAnnotations(rawTool.annotations);
21209
+ const icons = sanitizeToolIcons(rawTool.icons);
21210
+ const execution = taskSupport ? { taskSupport } : void 0;
21211
+ const schemaHash = projectedToolSchemaHash({
21212
+ name,
21213
+ inputSchema,
21214
+ ...title ? { title } : {},
21215
+ ...description ? { description } : {},
21216
+ ...outputSchema ? { outputSchema } : {},
21217
+ ...annotations ? { annotations } : {},
21218
+ ...icons ? { icons } : {},
21219
+ ...execution ? { execution } : {}
21220
+ });
21221
+ return {
21222
+ name,
21223
+ title,
21224
+ description,
21225
+ classification,
21226
+ callable: rawTool.callable,
21227
+ blockedReason,
21228
+ transport,
21229
+ providerConfigKey,
21230
+ protocolVersion,
21231
+ schemaSource: "live_tools_list",
21232
+ inputSchema,
21233
+ ...outputSchema ? { outputSchema } : {},
21234
+ ...annotations ? { annotations } : {},
21235
+ ...icons ? { icons } : {},
21236
+ ...execution ? { execution } : {},
21237
+ schemaHash,
21238
+ fetchedAt
21239
+ };
21240
+ }
21031
21241
  async function callScheduleConnectionExportPage(identity, input) {
21032
21242
  const body = await controlRequest("/api/internal/nango/connections/export-page", {
21033
21243
  method: "POST",
@@ -21247,6 +21457,9 @@ async function getResendConnections(identity) {
21247
21457
  readTools: cleanTools2(row.readTools ?? row.read_tools),
21248
21458
  actionTools: cleanTools2(row.actionTools ?? row.action_tools ?? row.writeTools ?? row.write_tools),
21249
21459
  adminBlockedTools: cleanTools2(row.adminBlockedTools ?? row.admin_blocked_tools).length > 0 ? cleanTools2(row.adminBlockedTools ?? row.admin_blocked_tools) : [...RESEND_ADMIN_BLOCKED_TOOLS],
21460
+ mcpEndpoint: null,
21461
+ schemaDiscovery: "compatibility_describe",
21462
+ toolRevision: firstString2(row, ["toolRevision", "tool_revision"], 200),
21250
21463
  vaultName: firstString2(row, ["vaultName", "vault_name"], 100),
21251
21464
  tableName: firstString2(row, ["tableName", "table_name"], 100),
21252
21465
  createdAt: firstString2(row, ["createdAt", "created_at"], 100),
@@ -21989,7 +22202,15 @@ app.post("/schedule-checkout", auth2, async (c) => {
21989
22202
  });
21990
22203
  function scheduleConnectionError(c, err, fallback) {
21991
22204
  if (err instanceof ScheduleConnectionValidationError) return c.json({ ok: false, error: err.message }, 400);
21992
- if (err instanceof NangoControlError) return c.json({ ok: false, error: err.message }, err.status);
22205
+ if (err instanceof NangoControlError) {
22206
+ return c.json({
22207
+ ok: false,
22208
+ error: err.message,
22209
+ code: err.code,
22210
+ errorCode: err.code,
22211
+ retryable: err.retryable
22212
+ }, err.status);
22213
+ }
21993
22214
  if (err instanceof ResendControlError) return c.json({ ok: false, error: err.message }, err.status);
21994
22215
  console.error("[schedule-connections]", err instanceof Error ? err.message : String(err));
21995
22216
  return c.json({ ok: false, error: fallback }, 502);
@@ -22243,11 +22464,12 @@ app.post("/schedule-connections/actions/describe", auth2, async (c) => {
22243
22464
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
22244
22465
  const tool = providerConfigKeyFrom(body.tool);
22245
22466
  if (!connectionId || !tool) return c.json({ ok: false, error: "connectionId and tool are required." }, 400);
22467
+ if (body.fresh !== void 0 && typeof body.fresh !== "boolean") {
22468
+ return c.json({ ok: false, error: "fresh must be a boolean when provided." }, 400);
22469
+ }
22246
22470
  try {
22247
- if (!await isResendConnection(user.email, connectionId, providerConfigKey)) {
22248
- return c.json({ ok: false, error: "Typed tool descriptions are not available for this connection transport yet." }, 501);
22249
- }
22250
- return c.json({ ok: true, tool: await describeResendTool(user.email, connectionId, tool) });
22471
+ const description = await isResendConnection(user.email, connectionId, providerConfigKey) ? await describeResendTool(user.email, connectionId, tool) : await describeNangoTool(user.email, connectionId, tool, body.fresh);
22472
+ return c.json({ ok: true, tool: description });
22251
22473
  } catch (err) {
22252
22474
  return scheduleConnectionError(c, err, "Unable to describe this connection tool.");
22253
22475
  }
@@ -23566,4 +23788,4 @@ app.get("/blog/:slug/", (c) => {
23566
23788
  export {
23567
23789
  app
23568
23790
  };
23569
- //# sourceMappingURL=server-FFNOTEQI.js.map
23791
+ //# sourceMappingURL=server-QEXOVJPR.js.map