@withpica/mcp-sdk 3.23.0 → 3.23.1

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 CHANGED
@@ -245,13 +245,20 @@ class BaseResource {
245
245
  * for routes that stream a generated file (e.g. `text/csv`) rather than
246
246
  * `{success, data}`. `request()` unconditionally calls `response.json()`,
247
247
  * which throws a SyntaxError parsing CSV text; every such route needs this
248
- * method instead (see `ImportResource.getTemplate()` for the prior
249
- * precedent this generalises).
248
+ * method instead (`ImportResource.getTemplate()`, `ExportResource.catalogCsv()`).
250
249
  *
251
250
  * `fetchWithRetry` already throws `ApiError` for non-2xx responses (reading
252
251
  * the body as text), so reaching here means `response.ok`. Still guard
253
252
  * against a 2xx JSON error envelope (`{success:false}`) via content-type,
254
253
  * since a route can commit to status 200 before a later validation step.
254
+ *
255
+ * An HTML page is refused. Neither route that uses this
256
+ * (`/admin/import/template`, `/admin/works/export` CSV formats) answers
257
+ * `text/html`, so one means something in front of PICA answered (on a
258
+ * protected Vercel preview, a missing bypass header ends on Vercel's
259
+ * login page with status 200). Returning that page as the file read as a
260
+ * success (2026-10-06, pica_import_template). Locked by
261
+ * `mcp-server/src/__tests__/sdk-request-text-and-bare-fetch.test.ts`.
255
262
  */
256
263
  async requestText(method, path) {
257
264
  const url = `${this.baseUrl}${path}`;
@@ -275,6 +282,9 @@ class BaseResource {
275
282
  }
276
283
  return typeof data === "string" ? data : JSON.stringify(data);
277
284
  }
285
+ if (contentType.includes("text/html")) {
286
+ throw new ApiError(`Expected a file from ${path} but got an HTML page; the request did not reach PICA`, response.status, false);
287
+ }
278
288
  return response.text();
279
289
  }
280
290
  /**
@@ -2548,7 +2558,7 @@ class ExportResource extends BaseResource {
2548
2558
  // request() unconditionally calls response.json(), which throws parsing
2549
2559
  // a CSV body — every pro/distributor/nro call 500'd with INTERNAL_ERROR
2550
2560
  // (pica_export_catalog_csv: 5/5 prod errors, ops audit 2026-07-04).
2551
- // requestText() mirrors the ImportResource.getTemplate() precedent for
2561
+ // requestText() serves ImportResource.getTemplate() too, for
2552
2562
  // the same response shape.
2553
2563
  if (params.format === "sync-pdf") {
2554
2564
  return this.request("GET", path);
@@ -2847,14 +2857,10 @@ class ImportResource extends BaseResource {
2847
2857
  });
2848
2858
  }
2849
2859
  async getTemplate(domain) {
2850
- // Template route returns CSV as text/csv — use raw fetch
2851
- const url = `${this.baseUrl}/admin/import/template?domain=${domain}`;
2852
- const response = await fetch(url, {
2853
- headers: { Authorization: `Bearer ${this.apiKey}` },
2854
- });
2855
- if (!response.ok)
2856
- throw new Error(`Template fetch failed: ${response.status}`);
2857
- return response.text();
2860
+ // The route answers text/csv, so requestText(), not request(). It was a
2861
+ // bare fetch() with no preview-protection bypass header, so on a Vercel
2862
+ // preview it never reached the route (2026-10-06).
2863
+ return this.requestText("GET", `/admin/import/template?domain=${encodeURIComponent(domain)}`);
2858
2864
  }
2859
2865
  /**
2860
2866
  * Emails that reached the organisation's forwarding address, newest first,
@@ -4346,24 +4352,15 @@ export class PicaClient {
4346
4352
  // Registration Integrity Floor (spec 2026-07-09, Task 8).
4347
4353
  integrity;
4348
4354
  async catalogStats(params) {
4349
- const baseUrl = this.works["baseUrl"];
4350
- const apiKey = this.works["apiKey"];
4351
4355
  const qp = new URLSearchParams();
4352
4356
  if (params?.entity)
4353
4357
  qp.set("entity", params.entity);
4354
4358
  if (params?.group_by)
4355
4359
  qp.set("group_by", params.group_by);
4356
4360
  const suffix = qp.toString() ? `?${qp.toString()}` : "";
4357
- const res = await fetch(`${baseUrl}/admin/catalog/stats${suffix}`, {
4358
- headers: {
4359
- Authorization: `Bearer ${apiKey}`,
4360
- "Content-Type": "application/json",
4361
- },
4362
- });
4363
- if (!res.ok)
4364
- throw new Error(`Catalog stats failed: ${res.status}`);
4365
- const json = (await res.json());
4366
- return (json.data || json);
4361
+ // Through the shared request path (bypass header, timeout, retry, typed
4362
+ // errors) rather than a bare fetch() that skipped all four (2026-10-06).
4363
+ return this.works["request"]("GET", `/admin/catalog/stats${suffix}`);
4367
4364
  }
4368
4365
  constructor(config) {
4369
4366
  if (!config.apiKey) {