getregdata-mcp 1.1.0 → 1.2.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.
Files changed (4) hide show
  1. package/README.md +19 -2
  2. package/actors.js +1 -1
  3. package/index.js +107 -10
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -13,11 +13,27 @@ every beneficial owner, returned as a single verdict. The per-registry tools are
13
13
  depth behind that answer, and the way to run the same jobs elsewhere.
14
14
 
15
15
  Each registry is exposed as its own tool (e.g. `regdata_crbr_beneficial_owners`,
16
- `regdata_germany_handelsregister`, `regdata_adverse_media`), plus two helpers:
16
+ `regdata_germany_handelsregister`, `regdata_adverse_media`), plus three helpers:
17
17
 
18
18
  - `regdata_catalog` - list every registry tool and what it returns.
19
19
  - `regdata_describe` - fetch a specific actor's live input schema so the agent
20
20
  builds a correct call.
21
+ - `regdata_run_result` - collect a check that was still running when its tool call
22
+ returned.
23
+
24
+ ## Long checks return a handle, not a failure
25
+
26
+ Most calls return dataset items directly. If a run is still going after ~45 seconds,
27
+ the tool returns `{status, runId, datasetId, note}` instead. That run is still going
28
+ and is **already billed**, so call `regdata_run_result` with the `runId` to collect it -
29
+ re-running the check to "retry" charges the user twice.
30
+
31
+ This matters most for the Poland KYB composite: 20-30 seconds typically, up to ~2.5
32
+ minutes for a company with many beneficial owners. Set `"timeout": 600000` on the
33
+ server in your MCP config so the common case still returns inline.
34
+
35
+ Treat a `runId` as a secret: Apify resolves run and dataset IDs without checking
36
+ ownership, so anyone holding the ID can read that check's result.
21
37
 
22
38
  ## Setup
23
39
 
@@ -34,7 +50,8 @@ Add to your MCP config (`claude_desktop_config.json`, or `.mcp.json` for Claude
34
50
  "getregdata": {
35
51
  "command": "npx",
36
52
  "args": ["-y", "getregdata-mcp"],
37
- "env": { "APIFY_TOKEN": "apify_api_xxxxx" }
53
+ "env": { "APIFY_TOKEN": "apify_api_xxxxx" },
54
+ "timeout": 600000
38
55
  }
39
56
  }
40
57
  }
package/actors.js CHANGED
@@ -6,7 +6,7 @@ export const ACTORS = [
6
6
  "slug": "poland-kyb-check",
7
7
  "tool": "regdata_poland_kyb_check",
8
8
  "title": "Poland KYB Risk Check - one call, one verdict",
9
- "description": "Run a COMPLETE Polish KYB check from a single NIP or KRS - start here for Poland instead of chaining the individual registry tools. Resolves official company identity, pulls beneficial owners from the national UBO register, and screens the national debtor register for insolvency, restructuring and enforcement proceedings against the company AND every beneficial owner by exact identifier. Returns one normalized verdict - clear, findings, not_found or partial - plus machine-readable risk flags and the underlying registry entries (case signature, classification, court, dates, source). A \"partial\" verdict means a source could not be reached, not that the company is clean, and is not billed. Poland only; use the per-registry tools for other jurisdictions or for deeper single-source detail.",
9
+ "description": "Run a COMPLETE Polish KYB check from a single NIP or KRS - start here for Poland instead of chaining the individual registry tools. Resolves official company identity, pulls beneficial owners from the national UBO register, and screens the national debtor register for insolvency, restructuring and enforcement proceedings against the company AND every beneficial owner by exact identifier. Returns one normalized verdict - clear, findings, not_found or partial - plus machine-readable risk flags and the underlying registry entries (case signature, classification, court, dates, source). A \"partial\" verdict means a source could not be reached, not that the company is clean, and is not billed. Poland only; use the per-registry tools for other jurisdictions or for deeper single-source detail. TIMING: this check typically takes 20-30 seconds, but scales with the number of beneficial owners and has been measured up to ~2.5 minutes. If it has not finished in time, the tool returns {status, runId, note} instead of results - the check is running and already billed, so call regdata_run_result with that runId to collect it. Never re-run the check to 'retry': that bills twice.",
10
10
  "inputSchema": {
11
11
  "type": "object",
12
12
  "properties": {
package/index.js CHANGED
@@ -36,21 +36,89 @@ function requireToken() {
36
36
  return t;
37
37
  }
38
38
 
39
- // Run an actor synchronously and return its dataset items.
39
+ // How long we hold a tool call open before handing back a run handle. Must stay
40
+ // under the MCP client's default request timeout (60s) with margin: if the CLIENT
41
+ // gives up first, the caller sees an error for a run that is still billing.
42
+ const WAIT_SECS = 45;
43
+ const TERMINAL = new Set(["SUCCEEDED", "FAILED", "ABORTED", "TIMED-OUT"]);
44
+
45
+ // Retrying a pending check runs - and bills - a second time. This wording is
46
+ // deliberately blunt: it is the only thing between a slow check and a double charge.
47
+ const PENDING_NOTE =
48
+ "This check is STILL RUNNING and is already being billed. Do NOT retry it and do NOT call this tool again - " +
49
+ "call regdata_run_result with the runId above to collect the result. Retrying charges the user twice for the same check.";
50
+
51
+ async function apiJson(url, init) {
52
+ const res = await fetch(url, init);
53
+ const text = await res.text();
54
+ if (!res.ok) throw new Error(`Apify API ${res.status}: ${text.slice(0, 300)}`);
55
+ return JSON.parse(text).data;
56
+ }
57
+
58
+ async function datasetItems(datasetId, maxItems) {
59
+ const url = new URL(`${APIFY_BASE}/datasets/${datasetId}/items`);
60
+ url.searchParams.set("token", requireToken());
61
+ if (maxItems) url.searchParams.set("limit", String(maxItems));
62
+ const res = await fetch(url);
63
+ const text = await res.text();
64
+ if (!res.ok) throw new Error(`Could not read dataset ${datasetId} (${res.status})`);
65
+ return text; // JSON array of dataset items
66
+ }
67
+
68
+ // Either the items, or a redeemable handle. Never a lost run.
69
+ async function settle(run, label, maxItems) {
70
+ if (run.status === "SUCCEEDED") return await datasetItems(run.defaultDatasetId, maxItems);
71
+ if (TERMINAL.has(run.status)) {
72
+ throw new Error(
73
+ `regdata/${label} run ${run.id} ended as ${run.status}` +
74
+ (run.statusMessage ? `: ${run.statusMessage}` : "")
75
+ );
76
+ }
77
+ return JSON.stringify(
78
+ { status: run.status, runId: run.id, datasetId: run.defaultDatasetId, note: PENDING_NOTE },
79
+ null,
80
+ 2
81
+ );
82
+ }
83
+
84
+ // Start the actor and wait briefly.
85
+ //
86
+ // Started ASYNC on purpose. This previously used run-sync-get-dataset-items, which
87
+ // holds one connection open until the run ends. Per Apify's docs, "if the connection
88
+ // breaks, you will not receive any information about the run and its output" and
89
+ // "this won't abort the run itself" - so a client timeout destroyed the only handle
90
+ // to a run that then completed and BILLED. Measured durations for the Poland KYB
91
+ // composite were 21-148s against a 60s client default, so this was routine, not rare.
92
+ // Starting async means the runId exists from the first response and a disconnect is
93
+ // always recoverable.
40
94
  async function runActor(slug, input, { maxItems } = {}) {
41
- const url = new URL(`${APIFY_BASE}/acts/regdata~${slug}/run-sync-get-dataset-items`);
95
+ const url = new URL(`${APIFY_BASE}/acts/regdata~${slug}/runs`);
42
96
  url.searchParams.set("token", requireToken());
97
+ url.searchParams.set("waitForFinish", String(WAIT_SECS)); // Apify caps this at 60
43
98
  if (maxItems) url.searchParams.set("maxItems", String(maxItems));
44
- const res = await fetch(url, {
99
+ const run = await apiJson(url, {
45
100
  method: "POST",
46
101
  headers: { "Content-Type": "application/json" },
47
102
  body: JSON.stringify(input ?? {}),
48
103
  });
49
- const text = await res.text();
50
- if (!res.ok) {
51
- throw new Error(`Apify run failed (${res.status}) for regdata/${slug}: ${text.slice(0, 400)}`);
52
- }
53
- return text; // JSON array of dataset items
104
+ return await settle(run, slug, maxItems);
105
+ }
106
+
107
+ // Collect a run that was still going when its tool call returned.
108
+ //
109
+ // NOTE on access: we send the caller's token, but Apify does NOT scope runs or
110
+ // datasets to it - verified 2026-07-23, `GET /actor-runs/{id}` and
111
+ // `/datasets/{id}/items` both return 200 with a foreign token AND with no token
112
+ // at all, on a dataset whose isPublic is unset. The storage ID is itself the
113
+ // capability. So this tool grants nothing a runId holder could not already fetch
114
+ // straight from the public API; it is convenience, not an access boundary.
115
+ // Treat a runId as a secret and do not log or forward it.
116
+ async function collectRun(runId, { waitSecs, maxItems } = {}) {
117
+ const url = new URL(`${APIFY_BASE}/actor-runs/${runId}`);
118
+ url.searchParams.set("token", requireToken());
119
+ url.searchParams.set("waitForFinish", String(Math.max(0, Math.min(waitSecs ?? WAIT_SECS, 60))));
120
+ const run = await apiJson(url);
121
+ return await settle(run, run.actId ?? "run", maxItems);
54
122
  }
55
123
 
56
124
  // Fetch an actor's live input schema so the model can construct correct input.
@@ -70,7 +138,7 @@ async function describeActor(slug) {
70
138
  const bySlug = Object.fromEntries(ACTORS.map((a) => [a.slug, a]));
71
139
  const byTool = Object.fromEntries(ACTORS.map((a) => [a.tool, a]));
72
140
 
73
- // One tool per actor + two helpers (catalog, describe).
141
+ // One tool per actor + three helpers (catalog, run_result, describe).
74
142
  const MAX_ITEMS_PROP = {
75
143
  type: "integer",
76
144
  description: "Optional cap on billed dataset items returned.",
@@ -110,6 +178,30 @@ function buildToolList() {
110
178
  `Use this catalog to find the right single-registry tool for other jurisdictions, or when you need one specific source in depth.`,
111
179
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
112
180
  },
181
+ {
182
+ name: "regdata_run_result",
183
+ description:
184
+ "Collect the result of a regdata check that was still running when its tool call returned. " +
185
+ "When any regdata tool returns {status, runId, note} instead of results, the check is running and already billed - " +
186
+ "call THIS tool with that runId rather than re-running the check, which would charge the user twice. " +
187
+ "Returns the dataset items once the run finishes; if it is still going, returns the same handle so you can call again.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ runId: {
192
+ type: "string",
193
+ description: "The runId returned by the pending tool call, e.g. NAMNqpPch7RFSY1oQ",
194
+ },
195
+ waitSecs: {
196
+ type: "integer",
197
+ description: "Seconds to wait for the run to finish before returning (0-60, default 45).",
198
+ },
199
+ maxItems: MAX_ITEMS_PROP,
200
+ },
201
+ required: ["runId"],
202
+ additionalProperties: false,
203
+ },
204
+ },
113
205
  {
114
206
  name: "regdata_describe",
115
207
  description:
@@ -143,6 +235,11 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
143
235
  const list = ACTORS.map((a) => ({ tool: a.tool, slug: a.slug, title: a.title }));
144
236
  return { content: [{ type: "text", text: JSON.stringify(list, null, 2) }] };
145
237
  }
238
+ if (name === "regdata_run_result") {
239
+ if (!args.runId) throw new Error("runId is required - it is returned by the pending tool call.");
240
+ const out = await collectRun(args.runId, { waitSecs: args.waitSecs, maxItems: args.maxItems });
241
+ return { content: [{ type: "text", text: out }] };
242
+ }
146
243
  if (name === "regdata_describe") {
147
244
  const slug = args.slug || byTool[args.tool]?.slug;
148
245
  if (!slug || !bySlug[slug]) throw new Error(`Unknown actor: ${args.slug || args.tool}`);
@@ -164,4 +261,4 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
164
261
 
165
262
  const transport = new StdioServerTransport();
166
263
  await server.connect(transport);
167
- console.error(`getregdata MCP server ready - ${ACTORS.length} registry tools + catalog/describe.`);
264
+ console.error(`getregdata MCP server ready - ${ACTORS.length} registry tools + catalog/run_result/describe.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getregdata-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "mcpName": "io.github.Nolpak14/getregdata",
5
5
  "description": "MCP server for official business-registry data - KYC/AML, beneficial owners, credit-risk, adverse media. One-call Poland KYB verdict, plus per-registry tools for Germany, Italy, Spain, Austria, France, Belgium, Czechia, Slovakia, Cyprus, Ireland, Portugal, Nigeria, Colombia, the US (California) and the UAE.",
6
6
  "type": "module",