@pipeworx/mcp-cbs-nl 0.1.1 → 0.1.3

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
@@ -4,7 +4,7 @@ Statistics Netherlands (CBS) StatLine — the Dutch national statistics office's
4
4
  open-data catalogue, over OData v3. Population, labour, energy, prices, trade and
5
5
  several thousand other official tables, in Dutch and English.
6
6
 
7
- Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1679+ live data sources.
7
+ Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1684+ live data sources.
8
8
 
9
9
  ## Tools
10
10
 
@@ -82,7 +82,7 @@ directly, instead of just this one's:
82
82
  }
83
83
  ```
84
84
 
85
- Both URLs reach the same gateway and the same 1679+ data sources. The
85
+ Both URLs reach the same gateway and the same 1684+ data sources. The
86
86
  only difference is which pack's tools are listed **directly**; `ask_pipeworx`
87
87
  reaches all of them from either one.
88
88
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipeworx/mcp-cbs-nl",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Statistics Netherlands (CBS / StatLine) OData MCP.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -26,7 +26,7 @@
26
26
  "@cloudflare/workers-types": "^4.20260405.1"
27
27
  },
28
28
  "pipeworx": {
29
- "sourceHash": "v1-46e09e644b80bcb40b905088be867d5527e237f7218a8e75295732b0128424f1",
30
- "sourceCommit": "60f04b68c347e970c6be94a53439d1364061fc06"
29
+ "sourceHash": "v1-1f4013c99a6c89d86557fa722f1ed30372a6b8f380159c808b093354aeb4e965",
30
+ "sourceCommit": "a00767651388cfd017fdeac82188622721db2051"
31
31
  }
32
32
  }
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.pipeworx-io/cbs-nl",
4
4
  "title": "Cbs Nl",
5
5
  "description": "Statistics Netherlands (CBS / StatLine) OData MCP.",
6
- "version": "0.1.1",
6
+ "version": "0.1.3",
7
7
  "websiteUrl": "https://pipeworx.io/packs/cbs-nl",
8
8
  "repository": {
9
9
  "url": "https://github.com/pipeworx-io/mcp-cbs-nl",
package/src/index.ts CHANGED
@@ -452,7 +452,42 @@ async function fetchWithTimeout(
452
452
  ),
453
453
  );
454
454
  }
455
- throw err;
455
+ // Fleet #2382. Everything that isn't a timeout/abort here is a genuine
456
+ // NETWORK-LEVEL failure — DNS resolution, connection refused, TLS handshake,
457
+ // Cloudflare's own "Network connection lost." — meaning `fetch()` itself
458
+ // threw and no HTTP response of any kind was ever received. Until this fix
459
+ // that raw exception was rethrown VERBATIM: a bare `TypeError: fetch failed`
460
+ // (or the Workers-runtime equivalent) names no upstream, carries no class
461
+ // token, and reads exactly like a defect in OUR code — because it says
462
+ // nothing about the call at all. It landed in `error`, the tier that means
463
+ // "Pipeworx has a defect", for every one of the (at the time of writing)
464
+ // ~470 packs that call this helper directly with no wrapper of their own.
465
+ //
466
+ // `dexscreener` hit this independently (fleet #1579) and fixed it with a
467
+ // bespoke per-pack try/catch around `fetchWithTimeout`. That fix is correct
468
+ // but only covers one pack; every other caller of this shared helper still
469
+ // leaked the raw exception. Moving the same fix HERE — the one place that
470
+ // already carries the timeout case — covers every pack that uses
471
+ // `fetchWithTimeout` without a wrapper, for free, and without widening
472
+ // `classifyToolError`'s regex list: the fix is giving the message a proper
473
+ // `upstream_down:` token at the point the two facts (no response was ever
474
+ // received, and which host we were trying to reach) are actually in hand,
475
+ // not teaching the classifier to guess from prose after the fact.
476
+ //
477
+ // Safe on the same grounds as the timeout branch above: no argument a
478
+ // caller passes can make `fetch()` itself throw a connection-level error,
479
+ // so this is always an availability failure, never a caller mistake. Same
480
+ // `markInternalOrigin` treatment — an origin we run that never answered is
481
+ // still ours, not a third party's outage.
482
+ const raw = err instanceof Error ? err.message : String(err);
483
+ throw new Error(
484
+ markInternalOrigin(
485
+ `upstream_down: could not reach ${name} at all (${raw.slice(0, 160)}). ` +
486
+ `No request reached ${name}, so this says NOTHING about whether the arguments you passed ` +
487
+ 'are valid — do not re-check them on the strength of this error. Retry shortly.',
488
+ url,
489
+ ),
490
+ );
456
491
  }
457
492
  }
458
493
 
@@ -729,6 +764,32 @@ const tools: McpToolExport['tools'] = [
729
764
  ];
730
765
 
731
766
  async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
767
+ try {
768
+ return await dispatch(name, args);
769
+ } catch (err) {
770
+ // CBS answers an unknown table id (and an unknown dimension endpoint) with
771
+ // a 404 ASP.NET page titled "The resource cannot be found." That threw as a
772
+ // bare `CBS: 404 …` and booked as a Pipeworx defect with "retry the same
773
+ // tool" (fleet #2496). It is the caller naming a thing CBS does not have.
774
+ if (err instanceof CbsHttpError && err.status === 404 && /resource cannot be found/i.test(err.body)) {
775
+ const table = typeof args.table === 'string' ? args.table : '';
776
+ const what =
777
+ name === 'dimension_values'
778
+ ? `no table "${table}" or no dimension "${String(args.dimension ?? '')}" in it`
779
+ : `no table "${table}"`;
780
+ return {
781
+ error: 'not_found',
782
+ message:
783
+ `CBS StatLine has ${what}. Call search_tables({keyword: "..."}) to find the table id` +
784
+ `${name === 'dimension_values' ? ', then table_dimensions for its dimension names' : ''}.`,
785
+ table,
786
+ };
787
+ }
788
+ throw err;
789
+ }
790
+ }
791
+
792
+ async function dispatch(name: string, args: Record<string, unknown>): Promise<unknown> {
732
793
  PROXY =
733
794
  typeof args._proxyUrl === 'string' && typeof args._proxyToken === 'string'
734
795
  ? { url: args._proxyUrl, token: args._proxyToken }
@@ -763,7 +824,23 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
763
824
  if (typeof args.select === 'string' && args.select.trim()) params.set('$select', args.select);
764
825
  if (typeof args.filter === 'string' && args.filter.trim()) params.set('$filter', args.filter);
765
826
  if (args.skip !== undefined) params.set('$skip', String(numArg(args.skip, 0)));
766
- return cbsGet(`${TABLE}/${tableId(args)}/TypedDataSet?${params.toString()}`);
827
+ const out = (await cbsGet(`${TABLE}/${tableId(args)}/TypedDataSet?${params.toString()}`)) as {
828
+ value?: unknown[];
829
+ };
830
+ // `value: []` is an honest no-match (the filter or skip matched no rows)
831
+ // and used to go out as a bare empty success (fleet #2496).
832
+ if (Array.isArray(out?.value) && out.value.length === 0) {
833
+ return {
834
+ ...out,
835
+ empty_reason: 'no_match',
836
+ note:
837
+ `CBS table "${tableId(args)}" has no rows matching` +
838
+ `${typeof args.filter === 'string' && args.filter.trim() ? ` filter ${args.filter}` : ' this request'}` +
839
+ `${args.skip !== undefined ? ` at skip ${String(args.skip)}` : ''}. ` +
840
+ `Check the codes with dimension_values (e.g. dimension "Periods") before filtering.`,
841
+ };
842
+ }
843
+ return out;
767
844
  }
768
845
  default:
769
846
  throw new Error(`Unknown tool: ${name}`);
@@ -786,7 +863,9 @@ async function cbsGet(url: string): Promise<unknown> {
786
863
  if (!res.ok) {
787
864
  const body = await res.text().catch(() => '');
788
865
  const title = body.match(/<title>([\s\S]*?)<\/title>/i)?.[1]?.trim();
789
- throw new Error(
866
+ throw new CbsHttpError(
867
+ res.status,
868
+ body,
790
869
  `CBS: ${res.status}${title ? ` ${title}` : ''} — ${url} — ${body.replace(/\s+/g, ' ').slice(0, 300)}`,
791
870
  );
792
871
  }
@@ -799,6 +878,16 @@ async function cbsGet(url: string): Promise<unknown> {
799
878
  }
800
879
  }
801
880
 
881
+ class CbsHttpError extends Error {
882
+ readonly status: number;
883
+ readonly body: string;
884
+ constructor(status: number, body: string, message: string) {
885
+ super(message);
886
+ this.status = status;
887
+ this.body = body;
888
+ }
889
+ }
890
+
802
891
  function tableId(args: Record<string, unknown>): string {
803
892
  return reqStr(args, 'table', '"37296eng"').trim().replace(/[^A-Za-z0-9_]/g, '');
804
893
  }
package/src/server.ts CHANGED
@@ -9,7 +9,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
9
9
  import pack from './index.js';
10
10
 
11
11
  const server = new Server(
12
- { name: '@pipeworx/mcp-cbs-nl', version: '0.1.1' },
12
+ { name: '@pipeworx/mcp-cbs-nl', version: '0.1.3' },
13
13
  { capabilities: { tools: {} } },
14
14
  );
15
15