mcpscraper-sdk 0.52.0 → 0.53.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/README.md CHANGED
@@ -41,11 +41,11 @@ Every non-2xx response throws a `ScraperApiError` with `status`, `code`, and the
41
41
  - `err.isConcurrencyLimitExceeded()` — narrows `err.body` to `{ active, limit, retryable, upgrade, ... }`.
42
42
  - `err.isVerificationChallenge()` / `err.isTimeout()` — preserve actionable, vendor-neutral retry and charge details returned by the service.
43
43
 
44
- Google Search light mode returns organic positions, URLs, titles, and descriptions for 20 Credits per delivered SERP page, or 35 Credits when the backup supplies the result. One-page full mode adds available same-page SERP features for 35 Credits. Both modes default to one page; an unfiltered two-page search costs 40 Credits when the normal provider returns both pages. Past-week and past-month searches use a managed browser for organic results at 35 Credits per delivered page, and unfiltered searches use it only as the final backup. If page 2 cannot be delivered, an available page 1 is returned as partial and billed at the one-page rate. A 35-Credit-per-requested-page hold is settled after delivery. PAA costs 400 Credits plus 10 Credits per returned question. One optional concurrency pack adds two browser slots for $5/month; quantity `n` adds `2n` slots for `$5n` without changing the base plan.
44
+ Google Search light mode returns organic positions, URLs, titles, and descriptions for 20 Credits per delivered SERP page, or 35 Credits when the backup supplies the result. One-page full mode adds available same-page SERP features for 35 Credits. Both modes default to one page; an unfiltered two-page search costs 40 Credits when the normal provider returns both pages. Past-week and past-month searches use a managed browser for organic results at 35 Credits per delivered page. Unfiltered light searches try the primary path twice, then one API fallback; an unfiltered one-page full search uses the feature-capable API directly. If page 2 cannot be delivered, an available page 1 is returned as partial and billed at the one-page rate. A 35-Credit-per-requested-page hold is settled after delivery. PAA costs 400 Credits plus 10 Credits per returned question. One optional concurrency pack adds two browser slots for $5/month; quantity `n` adds `2n` slots for `$5n` without changing the base plan.
45
45
 
46
46
  ## API surface
47
47
 
48
- `client.tools` is the generated, typed 364-tool MCP surface from `contracts/mcp.tools.json`, including provider-neutral X-Ray surveys, attribution-impact reporting, and truthful evidence-status operations.
48
+ `client.tools` is the generated, typed 367-tool MCP surface from `contracts/mcp.tools.json`, including provider-neutral X-Ray surveys, attribution-impact reporting, and truthful evidence-status operations.
49
49
 
50
50
  For multimodal results such as `meta_ad_creative_media`, call `client.tools.callToolResult(...)` to preserve native MCP image/audio/resource blocks. `callTool(...)` remains backward-compatible and returns the parsed structured or text value.
51
51
 
@@ -192,4 +192,10 @@ npm run verify:rest-contract
192
192
 
193
193
  [Repo README](../../README.md) (multi-language examples with real sample output) · [`mcpscraper-memory-sdk`](../memory) (Node, full 121-tool direct-memory surface) · [`mcpscraper-sdk` on PyPI](https://pypi.org/project/mcpscraper-sdk/) · [`mcpscraper-cli`](../cli)
194
194
 
195
- Release changes: [SDK 0.52.0 release notes](https://github.com/VilovietaSEO/mcpscraper-sdk/releases/tag/v0.52.0).
195
+ Release changes: [SDK 0.53.0 release notes](https://github.com/VilovietaSEO/mcpscraper-sdk/releases/tag/v0.53.0).
196
+
197
+ ## Client attribution and full results
198
+
199
+ Clients accept `clientAttribution: { installChannel?, aiClient?, aiModel? }` and default to calling surface `sdk`. Set only known caller labels; unspecified labels stay unknown. `onResponse({ requestId, status })` receives per-response correlation without changing results, and errors keep their own `requestId`. These options also apply to `McpToolsClient` and `MemoryClient`; labels are reporting declarations, not authorization.
200
+
201
+ Large tool outputs may be delivery envelopes instead of inline domain values. Preserve `delivery`, `scope` and `recovery`, then use `tools.artifacts.describe`, `readRetained` (follow `nextOffset` to null), or `download` with the returned artifact ID. The legacy `artifacts.read` method remains the report reader. `callToolResult` preserves native content/resource links. Download URL expiry differs from artifact retention; renew a URL through `download` without rerunning acquisition. Use the returned capability for binary files.
package/dist/index.cjs CHANGED
@@ -72,7 +72,29 @@ var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
72
72
  "site_export_not_found",
73
73
  "site_export_format_unavailable",
74
74
  "site_export_read_failed",
75
- "site_export_image_not_found"
75
+ "site_export_image_not_found",
76
+ "directory_job_not_found",
77
+ "idempotency_in_progress",
78
+ "idempotency_conflict",
79
+ "image_url_invalid",
80
+ "image_url_not_https",
81
+ "image_host_unresolvable",
82
+ "image_host_blocked",
83
+ "image_redirect_invalid",
84
+ "image_source_unavailable",
85
+ "image_too_large",
86
+ "image_redirect_limit",
87
+ "image_base64_invalid",
88
+ "image_source_missing",
89
+ "image_source_ambiguous",
90
+ "image_source_type_invalid",
91
+ "image_artifact_not_found",
92
+ "image_page_too_large",
93
+ "page_image_not_found",
94
+ "page_image_candidates_unusable",
95
+ "image_format_unsupported",
96
+ "image_storage_unconfigured",
97
+ "featured_image_unhostable"
76
98
  ]);
77
99
  var PUBLIC_ERROR_FIELDS = /* @__PURE__ */ new Set([
78
100
  "error",
@@ -103,13 +125,51 @@ var PUBLIC_DETAIL_FIELDS = /* @__PURE__ */ new Set([
103
125
  "slots_per_pack",
104
126
  "pack_price_usd",
105
127
  "balance_credits",
106
- "required_credits"
128
+ "required_credits",
129
+ "retry_guidance",
130
+ "http_status",
131
+ "job_id"
107
132
  ]);
133
+ function boundedString(value, limit) {
134
+ return typeof value === "string" && value.length > 0 && value.length <= limit;
135
+ }
136
+ function publicRecoveryFields(body) {
137
+ if (!body || typeof body !== "object" || Array.isArray(body)) return {};
138
+ const row = body;
139
+ const safe = {};
140
+ if (boundedString(row.idempotencyKey, 512)) safe.idempotencyKey = row.idempotencyKey;
141
+ if (typeof row.idempotencyFingerprint === "string" && /^[a-f0-9]{16}$/.test(row.idempotencyFingerprint)) safe.idempotencyFingerprint = row.idempotencyFingerprint;
142
+ const operation = row.operation;
143
+ if (operation && typeof operation === "object" && !Array.isArray(operation)) {
144
+ const op = operation;
145
+ if ((op.runId === null || boundedString(op.runId, 200)) && boundedString(op.idempotencyKey, 512) && boundedString(op.state, 100)) {
146
+ safe.operation = { runId: op.runId, idempotencyKey: op.idempotencyKey, state: op.state };
147
+ }
148
+ }
149
+ const recovery = row.recovery;
150
+ if (recovery && typeof recovery === "object" && !Array.isArray(recovery)) {
151
+ const receipt = recovery;
152
+ if (typeof receipt.jobId === "string" && /^[a-zA-Z0-9-]{1,100}$/.test(receipt.jobId) && typeof receipt.statusTool === "string" && /^[a-z][a-z0-9_]{0,99}$/.test(receipt.statusTool) && receipt.executionState === "unknown" && ["held", "unconfirmed"].includes(String(receipt.holdState))) {
153
+ safe.recovery = { jobId: receipt.jobId, statusTool: receipt.statusTool, executionState: receipt.executionState, holdState: receipt.holdState };
154
+ }
155
+ }
156
+ if (typeof row.job_id === "string" && /^[a-zA-Z0-9-]{1,100}$/.test(row.job_id)) safe.job_id = row.job_id;
157
+ if (typeof row.status_tool === "string" && /^[a-z][a-z0-9_]{0,99}$/.test(row.status_tool)) safe.status_tool = row.status_tool;
158
+ if (row.executionState === "unknown") safe.executionState = row.executionState;
159
+ if (row.hold_state === "held" || row.hold_state === "unconfirmed") safe.hold_state = row.hold_state;
160
+ if (typeof row.runId === "string" && /^[a-zA-Z0-9_-]{1,200}$/.test(row.runId)) {
161
+ safe.runId = row.runId;
162
+ if (["queued", "running", "interrupted", "complete", "partial", "failed", "cancelled"].includes(String(row.runStatus))) safe.runStatus = row.runStatus;
163
+ if (["unreserved", "held", "settling", "settled", "refunded", "pending_reconciliation"].includes(String(row.billingState))) safe.billingState = row.billingState;
164
+ if (row.statusPath === `/maps/place/runs/${row.runId}`) safe.statusPath = row.statusPath;
165
+ }
166
+ return safe;
167
+ }
108
168
  function recognizedPublicDetails(value) {
109
169
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
110
170
  const details = Object.fromEntries(
111
171
  Object.entries(value).filter(
112
- ([key, child]) => PUBLIC_DETAIL_FIELDS.has(key) && (child === null || ["string", "number", "boolean"].includes(typeof child))
172
+ ([key, child]) => PUBLIC_DETAIL_FIELDS.has(key) && (child === null || typeof child === "boolean" || typeof child === "number" && Number.isFinite(child) || typeof child === "string" && child.length <= (key === "job_id" ? 200 : 500))
113
173
  )
114
174
  );
115
175
  return Object.keys(details).length > 0 ? details : void 0;
@@ -119,10 +179,13 @@ function recognizedPublicErrorBody(body) {
119
179
  const record = body;
120
180
  const code = readString(record, "error_code");
121
181
  if (!code || !PUBLIC_ERROR_CODES.has(code)) return void 0;
122
- const safe = Object.fromEntries(Object.entries(record).filter(([key]) => PUBLIC_ERROR_FIELDS.has(key)));
182
+ const safe = Object.fromEntries(Object.entries(record).filter(([key, value]) => PUBLIC_ERROR_FIELDS.has(key) && (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.length <= 1e3)));
183
+ Object.assign(safe, publicRecoveryFields(record));
123
184
  const details = recognizedPublicDetails(record.details);
124
185
  if (details) safe.details = details;
125
186
  else delete safe.details;
187
+ const upgrade = recognizedPublicDetails(record.upgrade);
188
+ if (upgrade) safe.upgrade = upgrade;
126
189
  return safe;
127
190
  }
128
191
  function publicErrorBody(status, body) {
@@ -135,20 +198,23 @@ function publicErrorBody(status, body) {
135
198
  error_type: "service_unavailable",
136
199
  retryable: true,
137
200
  status,
138
- message: PUBLIC_SERVICE_UNAVAILABLE_MESSAGE
201
+ message: PUBLIC_SERVICE_UNAVAILABLE_MESSAGE,
202
+ ...publicRecoveryFields(body)
139
203
  };
140
204
  }
141
205
  var ScraperApiError = class extends Error {
142
206
  status;
143
207
  code;
144
208
  body;
145
- constructor(status, body) {
209
+ requestId;
210
+ constructor(status, body, details = {}) {
146
211
  const safeBody = publicErrorBody(status, body);
147
212
  super(readString(safeBody, "message") ?? `mcpscraper.dev API request failed with status ${status}`);
148
213
  this.name = "ScraperApiError";
149
214
  this.status = status;
150
215
  this.code = readString(safeBody, "error_code") ?? readString(safeBody, "error");
151
216
  this.body = safeBody;
217
+ this.requestId = details.requestId;
152
218
  }
153
219
  isInsufficientBalance() {
154
220
  return this.code === "insufficient_balance";
@@ -170,19 +236,29 @@ var ScraperApiError = class extends Error {
170
236
  // src/client.ts
171
237
  var import_mcpscraper_memory_sdk = require("mcpscraper-memory-sdk");
172
238
  var Requester = class {
173
- constructor(apiKey, baseUrl, fetchImpl, timeoutMs) {
239
+ constructor(apiKey, baseUrl, fetchImpl, timeoutMs, metadataOptions = {}) {
174
240
  this.apiKey = apiKey;
175
241
  this.baseUrl = baseUrl;
176
242
  this.fetchImpl = fetchImpl;
177
243
  this.timeoutMs = timeoutMs;
244
+ this.metadataOptions = metadataOptions;
178
245
  }
179
246
  apiKey;
180
247
  baseUrl;
181
248
  fetchImpl;
182
249
  timeoutMs;
250
+ metadataOptions;
251
+ headers(extra = {}) {
252
+ return { ...(0, import_mcpscraper_memory_sdk.clientAttributionHeaders)(this.metadataOptions.clientAttribution), ...extra, "x-api-key": this.apiKey };
253
+ }
254
+ received(res) {
255
+ (0, import_mcpscraper_memory_sdk.notifyResponse)(this.metadataOptions.onResponse, (0, import_mcpscraper_memory_sdk.responseMetadata)(res));
256
+ }
183
257
  async request(url, init) {
184
258
  try {
185
- return await this.fetchImpl(url, { ...init, signal: AbortSignal.timeout(this.timeoutMs) });
259
+ const response = await this.fetchImpl(url, { ...init, signal: AbortSignal.timeout(this.timeoutMs) });
260
+ this.received(response);
261
+ return response;
186
262
  } catch (error) {
187
263
  if (error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError")) {
188
264
  throw new ScraperApiError(408, {
@@ -207,14 +283,14 @@ var Requester = class {
207
283
  try {
208
284
  res = await this.fetchImpl(`${this.baseUrl}${path}`, {
209
285
  method,
210
- headers: {
211
- "x-api-key": this.apiKey,
286
+ headers: this.headers({
212
287
  ...body !== void 0 ? { "content-type": "application/json" } : {},
213
288
  ...extraHeaders
214
- },
289
+ }),
215
290
  body: body !== void 0 ? JSON.stringify(body) : void 0,
216
291
  signal
217
292
  });
293
+ this.received(res);
218
294
  } catch (error) {
219
295
  if (error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError")) {
220
296
  if (options.signal?.aborted && !timeoutSignal.aborted) throw error;
@@ -229,14 +305,14 @@ var Requester = class {
229
305
  throw error;
230
306
  }
231
307
  const data = await res.json().catch(() => void 0);
232
- if (!res.ok) throw new ScraperApiError(res.status, data);
308
+ if (!res.ok) throw new ScraperApiError(res.status, data, { requestId: (0, import_mcpscraper_memory_sdk.responseMetadata)(res).requestId });
233
309
  return { data, headers: res.headers };
234
310
  }
235
311
  async callRaw(method, path) {
236
- const res = await this.request(`${this.baseUrl}${path}`, { method, headers: { "x-api-key": this.apiKey } });
312
+ const res = await this.request(`${this.baseUrl}${path}`, { method, headers: this.headers() });
237
313
  if (!res.ok) {
238
314
  const data = await res.json().catch(() => void 0);
239
- throw new ScraperApiError(res.status, data);
315
+ throw new ScraperApiError(res.status, data, { requestId: (0, import_mcpscraper_memory_sdk.responseMetadata)(res).requestId });
240
316
  }
241
317
  return res.arrayBuffer();
242
318
  }
@@ -318,8 +394,14 @@ var RedditNamespace = class {
318
394
  this.r = r;
319
395
  }
320
396
  r;
321
- thread(params) {
322
- return this.r.call("POST", "/reddit/thread", params);
397
+ thread(params, options = {}) {
398
+ return this.r.call(
399
+ "POST",
400
+ "/reddit/thread",
401
+ params,
402
+ options.idempotencyKey === void 0 ? {} : { "Idempotency-Key": options.idempotencyKey },
403
+ options
404
+ );
323
405
  }
324
406
  };
325
407
  var VideoNamespace = class {
@@ -422,7 +504,8 @@ var SerpIntelligenceNamespace = class {
422
504
  );
423
505
  return {
424
506
  data: result.data,
425
- idempotencyKey: result.headers.get("Idempotency-Key")
507
+ idempotencyKey: result.headers.get("Idempotency-Key"),
508
+ ...result.headers.get("x-mcp-request-id") ? { requestId: result.headers.get("x-mcp-request-id") } : {}
426
509
  };
427
510
  }
428
511
  pageSnapshots(params) {
@@ -562,7 +645,7 @@ var ScraperClient = class {
562
645
  constructor(options) {
563
646
  const requestTimeoutMs = options.requestTimeoutMs ?? 59e4;
564
647
  if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) throw new TypeError("requestTimeoutMs must be a positive number");
565
- this.r = new Requester(options.apiKey, options.baseUrl ?? "https://mcpscraper.dev", options.fetch ?? globalThis.fetch, requestTimeoutMs);
648
+ this.r = new Requester(options.apiKey, options.baseUrl ?? "https://mcpscraper.dev", options.fetch ?? globalThis.fetch, requestTimeoutMs, options);
566
649
  this.youtube = new YoutubeNamespace(this.r);
567
650
  this.screenshot = new ScreenshotNamespace(this.r);
568
651
  this.facebook = new FacebookNamespace(this.r);
@@ -580,17 +663,19 @@ var ScraperClient = class {
580
663
  apiKey: options.apiKey,
581
664
  baseUrl: options.baseUrl ?? "https://mcpscraper.dev",
582
665
  fetch: options.fetch,
583
- requestTimeoutMs
666
+ requestTimeoutMs,
667
+ clientAttribution: options.clientAttribution,
668
+ onResponse: options.onResponse
584
669
  });
585
670
  }
586
671
  async callMemoryTool(toolName, args) {
587
- const result = await this.r.call("POST", "/memory/mcp-call", {
672
+ const { data: result, headers } = await this.r.callWithReceipt("POST", "/memory/mcp-call", {
588
673
  toolName,
589
674
  args: args ?? {}
590
675
  });
591
676
  if (result && typeof result === "object" && result.ok === false) {
592
677
  const failure = result;
593
- throw new ScraperApiError(200, { message: failure.error, ...failure });
678
+ throw new ScraperApiError(200, { message: failure.error, ...failure }, { requestId: headers.get("x-mcp-request-id") ?? void 0 });
594
679
  }
595
680
  return result;
596
681
  }