agent-data-tools 0.0.0-stage → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SpellzCode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,68 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # agent-data-tools
2
+
3
+ Data tools for AI agents, one HTTP call each: PDF to text, web page to Markdown, web search, news, token prices, OFAC sanctions checks, chain reads on 18 networks, IP geolocation, hosted pages and about 550 more. No dependencies. 25 free calls with no signup, then card credits or USDC per call; failed calls are not charged.
4
+
5
+ ## Use it in your AI client
6
+
7
+ One command per client. Each connects the client to the MCP server at `https://agent-data-tools.revmesh2074.workers.dev/mcp`.
8
+
9
+ ```sh
10
+ npx agent-data-tools setup claude-code # prints the `claude mcp add --transport http ...` command to run
11
+ npx agent-data-tools setup claude-desktop # adds the server to claude_desktop_config.json
12
+ npx agent-data-tools setup cursor # adds the server to ~/.cursor/mcp.json
13
+ npx agent-data-tools setup vscode # adds the server to .vscode/mcp.json in the current folder
14
+ ```
15
+
16
+ `--print` shows the change without writing anything. When a file is changed, other servers in it are kept and the previous version stays next to it as `.bak`; a file that is not plain JSON is left alone and the lines to add are printed.
17
+
18
+ Without a key the server gives 25 free calls. With a prepaid key:
19
+
20
+ ```sh
21
+ npx agent-data-tools credits # where to get a key (card or USDC)
22
+ npx agent-data-tools key adt_... # saves it to ~/.agent-data-tools/config.json
23
+ npx agent-data-tools key # shows the last 4 characters and the balance
24
+ npx agent-data-tools setup cursor --key adt_...
25
+ ```
26
+
27
+ `setup --key` puts the key into the client's own user-level configuration (a header for Claude Code and Cursor, the server's environment for Claude Desktop). It is never written to `.vscode/mcp.json`, which is a project file: there the entry is the local command below, which reads the saved key.
28
+
29
+ `npx agent-data-tools mcp` is a stdio MCP server that forwards to the remote one, for clients that only start local commands. It reads the key from `--key`, then `AGENT_DATA_TOOLS_KEY`, then `~/.agent-data-tools/config.json`. The package holds no payment or wallet code.
30
+
31
+ ## From the command line and from code
32
+
33
+ ```sh
34
+ npx agent-data-tools list pdf
35
+ npx agent-data-tools pdf_text url=https://arxiv.org/pdf/1706.03762 pages=2
36
+ ```
37
+
38
+ ```js
39
+ import { AgentDataTools } from "agent-data-tools";
40
+
41
+ const adt = new AgentDataTools();
42
+ const weather = await adt.call("/weather", { city: "London" });
43
+ const matches = await adt.tools({ search: "sanctions" });
44
+ ```
45
+
46
+ ## Give the tools to a model
47
+
48
+ `openAiTools()` returns OpenAI function-calling definitions; [examples/openai-agent.mjs](examples/openai-agent.mjs) is a complete loop in 30 lines.
49
+
50
+ ```js
51
+ const tools = await adt.openAiTools(["extract", "pdf_text", "search"]);
52
+ // When the model calls one:
53
+ const answer = await adt.call(call.function.name, JSON.parse(call.function.arguments));
54
+ ```
55
+
56
+ ## Paying
57
+
58
+ | How | Setup |
59
+ |---|---|
60
+ | Free calls | None. 25 successful calls per caller; AI media, rendering and search tools are paid only. |
61
+ | Card credits | Buy a key at [/credits](https://agent-data-tools.revmesh2074.workers.dev/credits), then `npx agent-data-tools key adt_...`, `new AgentDataTools({ apiKey })` or the `ADT_API_KEY` environment variable. |
62
+ | USDC per call (x402) | Pass a fetch that pays HTTP 402, such as `@x402/fetch` around your own wallet: `new AgentDataTools({ fetch: paying })`. Set your spending limits in that wrapper. |
63
+
64
+ When payment is needed and none is set up, `call()` throws `PaymentRequired` with `priceUsd` and `buyUrl`. Other failures throw `ToolError` with `status` and `body`. The package never reads a wallet key and sends your API key only to the service origin.
65
+
66
+ ## Also
67
+
68
+ Full catalog and prices: [`/tools`](https://agent-data-tools.revmesh2074.workers.dev/tools), [`/openapi.json`](https://agent-data-tools.revmesh2074.workers.dev/openapi.json). MCP server: `https://agent-data-tools.revmesh2074.workers.dev/mcp`. Terms: [`/terms`](https://agent-data-tools.revmesh2074.workers.dev/terms).
package/cli.mjs ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ // npx agent-data-tools setup [client] put the tools into Claude Code, Claude Desktop, Cursor or VS Code
3
+ // npx agent-data-tools mcp stdio MCP server that forwards to the remote one
4
+ // npx agent-data-tools key [adt_...] save a prepaid key, or show whether one is set and its balance
5
+ // npx agent-data-tools credits where to get a key
6
+ // npx agent-data-tools list [words] tools and prices
7
+ // npx agent-data-tools <tool> key=value … call a tool (name or /path); prints its JSON
8
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join, resolve } from "node:path";
11
+ import { AgentDataTools, ORIGIN, PaymentRequired, ToolError } from "./index.mjs";
12
+
13
+ const NAME = "agent-data-tools";
14
+ const STDIO = { command: "npx", args: ["-y", NAME, "mcp"] };
15
+ const TARGETS = ["claude-code", "claude-desktop", "cursor", "vscode"];
16
+
17
+ const origin = () => (process.env.AGENT_DATA_TOOLS_ORIGIN || ORIGIN).replace(/\/$/, "");
18
+ const configFile = () => join(homedir(), ".agent-data-tools", "config.json");
19
+ const tail = (key) => `…${key.slice(-4)}`;
20
+ const validKey = (key) => typeof key === "string" && /^adt_\S+$/.test(key);
21
+
22
+ function readConfig() {
23
+ try { const c = JSON.parse(readFileSync(configFile(), "utf8").replace(/^/, "")); return c && typeof c === "object" && !Array.isArray(c) ? c : {}; } catch { return {}; }
24
+ }
25
+
26
+ /** The key and where it came from: --key, AGENT_DATA_TOOLS_KEY, the config file, then ADT_API_KEY (the library's variable). */
27
+ function resolveKey(flag) {
28
+ if (flag) return { key: flag, source: "--key" };
29
+ if (process.env.AGENT_DATA_TOOLS_KEY) return { key: process.env.AGENT_DATA_TOOLS_KEY, source: "AGENT_DATA_TOOLS_KEY" };
30
+ const saved = readConfig().key;
31
+ if (typeof saved === "string" && saved) return { key: saved, source: configFile() };
32
+ if (process.env.ADT_API_KEY) return { key: process.env.ADT_API_KEY, source: "ADT_API_KEY" };
33
+ return { key: undefined, source: undefined };
34
+ }
35
+
36
+ /** Split `--key <value>`, `--key=<value>` and `--print` from the other arguments. */
37
+ function parseArgs(args) {
38
+ const rest = [];
39
+ let key, print = false;
40
+ for (let i = 0; i < args.length; i++) {
41
+ const a = args[i];
42
+ if (a === "--print") print = true;
43
+ else if (a === "--key") { key = args[++i]; if (!key) throw new ToolError("--key needs a value (adt_...)", 400, null); }
44
+ else if (a.startsWith("--key=")) key = a.slice(6);
45
+ else rest.push(a);
46
+ }
47
+ if (key !== undefined && !validKey(key)) throw new ToolError("the key must start with adt_", 400, null);
48
+ return { rest, key, print };
49
+ }
50
+
51
+ const HELP = `Usage:
52
+ agent-data-tools setup [claude-code|claude-desktop|cursor|vscode] [--key adt_...] [--print]
53
+ add the tools to that client (--print shows the change without writing)
54
+ agent-data-tools mcp [--key adt_...] stdio MCP server that forwards to ${ORIGIN}/mcp
55
+ agent-data-tools key [adt_...] save a prepaid key; without a key, show whether one is set and its balance
56
+ agent-data-tools credits where to get a prepaid key (card or USDC)
57
+ agent-data-tools list [words] tools and prices
58
+ agent-data-tools <tool> key=value ... call a tool by name or /path; prints its JSON
59
+
60
+ 25 free calls without any setup. A prepaid key is read from --key, then AGENT_DATA_TOOLS_KEY,
61
+ then ~/.agent-data-tools/config.json, then ADT_API_KEY.`;
62
+
63
+ function menu() {
64
+ return `Add the tools to a client:
65
+ agent-data-tools setup claude-code prints the \`claude mcp add\` command to run
66
+ agent-data-tools setup claude-desktop writes claude_desktop_config.json
67
+ agent-data-tools setup cursor writes ~/.cursor/mcp.json
68
+ agent-data-tools setup vscode writes .vscode/mcp.json in the current folder
69
+
70
+ Add --print to see the change without writing, and --key adt_... to connect with a prepaid key.
71
+ Without a key the server gives 25 free calls. Keys: agent-data-tools credits`;
72
+ }
73
+
74
+ function desktopConfigPath() {
75
+ if (process.platform === "win32") return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
76
+ if (process.platform === "darwin") return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
77
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
78
+ }
79
+
80
+ /** Add one server entry to a JSON config file, keeping everything else; a copy of the old file stays next to it as .bak. */
81
+ function mergeInto(file, section, entry, print) {
82
+ const snippet = JSON.stringify({ [section]: { [NAME]: entry } }, null, 2);
83
+ if (print) { console.log(`${file}\n${snippet}`); return false; }
84
+ let json = {};
85
+ const exists = existsSync(file);
86
+ if (exists) {
87
+ try { json = JSON.parse(readFileSync(file, "utf8").replace(/^/, "")); } catch { json = null; }
88
+ const usable = json && typeof json === "object" && !Array.isArray(json) && (json[section] === undefined || (json[section] && typeof json[section] === "object" && !Array.isArray(json[section])));
89
+ if (!usable) {
90
+ console.error(`${file} is not JSON this command can edit, so it was left unchanged. Add this to it yourself:\n${snippet}`);
91
+ process.exitCode = 1;
92
+ return false;
93
+ }
94
+ copyFileSync(file, `${file}.bak`);
95
+ } else mkdirSync(dirname(file), { recursive: true });
96
+ json[section] = { ...(json[section] ?? {}), [NAME]: entry };
97
+ writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
98
+ console.log(`${exists ? "Updated" : "Created"} ${file}${exists ? ` (previous version: ${file}.bak)` : ""}`);
99
+ return true;
100
+ }
101
+
102
+ function setup(target, { key, print }) {
103
+ if (!target) { console.log(menu()); return; }
104
+ if (!TARGETS.includes(target)) throw new ToolError(`unknown client "${target}"; use one of: ${TARGETS.join(", ")}`, 400, null);
105
+ const url = `${origin()}/mcp`;
106
+ const saved = key ? undefined : resolveKey().key;
107
+ const savedNote = `A key is already saved for this computer; pass it with --key to connect ${target} with it.`;
108
+ if (target === "claude-code") {
109
+ console.log(`claude mcp add --transport http ${NAME} ${url}${key ? ` --header "Authorization: Bearer ${key}"` : ""}`);
110
+ console.error(`Run the command above in a terminal; it registers the server with Claude Code.${saved ? `\n${savedNote}` : ""}`);
111
+ } else if (target === "claude-desktop") {
112
+ // Claude Desktop starts local commands; the stdio server reads the key from its environment or the config file.
113
+ const entry = key ? { ...STDIO, env: { AGENT_DATA_TOOLS_KEY: key } } : STDIO;
114
+ if (mergeInto(desktopConfigPath(), "mcpServers", entry, print)) console.error(`Restart Claude Desktop to load it.${key ? ` The key (${tail(key)}) is in the server's env.` : saved ? " The saved key is used." : ""}`);
115
+ } else if (target === "cursor") {
116
+ const entry = key ? { url, headers: { Authorization: `Bearer ${key}` } } : { url };
117
+ if (mergeInto(join(homedir(), ".cursor", "mcp.json"), "mcpServers", entry, print)) console.error(`Cursor reads it for every project.${key ? ` The key (${tail(key)}) is sent as a header.` : saved ? `\n${savedNote}` : ""}`);
118
+ } else {
119
+ // A project file may be committed, so a key never goes into it: with a key, the entry is the stdio command, which reads the key itself.
120
+ const entry = key || saved ? { type: "stdio", ...STDIO } : { type: "http", url };
121
+ const done = mergeInto(resolve(".vscode", "mcp.json"), "servers", entry, print);
122
+ if (key) console.error(`The key is not written to the project file. Save it once with \`${NAME} key <your key>\` (or set AGENT_DATA_TOOLS_KEY); the command in the file reads it from there.`);
123
+ else if (saved && done) console.error("The command in the file reads your saved key; nothing secret is in the project file.");
124
+ }
125
+ }
126
+
127
+ async function keyCommand(value) {
128
+ if (value !== undefined) {
129
+ if (!validKey(value)) throw new ToolError("the key must start with adt_", 400, null);
130
+ const file = configFile();
131
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
132
+ writeFileSync(file, `${JSON.stringify({ ...readConfig(), key: value }, null, 2)}\n`, { mode: 0o600 });
133
+ try { chmodSync(file, 0o600); } catch { /* not supported on this file system */ }
134
+ console.log(`Saved the key ending in ${tail(value)} to ${file}`);
135
+ return;
136
+ }
137
+ const { key, source } = resolveKey();
138
+ if (!key) { console.log(`No key is set. The server gives 25 free calls without one. Get a key: ${NAME} credits`); return; }
139
+ console.log(`A key is set (ends in ${tail(key)}), from ${source}.`);
140
+ try {
141
+ const res = await fetch(`${origin()}/credits/balance`, { headers: { accept: "application/json", "x-api-key": key } });
142
+ const body = await res.json().catch(() => null);
143
+ if (res.ok && body && typeof body.balanceCredits === "number") console.log(`Balance: ${body.balanceCredits} credits (${body.calls ?? 0} calls so far).`);
144
+ else console.log(`Balance not available: ${typeof body?.error === "string" ? body.error : `HTTP ${res.status}`}`);
145
+ } catch (e) {
146
+ console.log(`Balance not available: ${e?.cause?.code ?? e?.message ?? "network error"}`);
147
+ }
148
+ }
149
+
150
+ function credits() {
151
+ const o = origin();
152
+ console.log(`Card: ${o}/credits
153
+ Open it in a browser to buy a prepaid key with a card.
154
+ USDC, $1: ${o}/credits/usdc/1
155
+ USDC, $5: ${o}/credits/usdc/5
156
+ USDC, $20: ${o}/credits/usdc/20
157
+ Each is an x402 route: an x402 client that pays it gets a key back. This command does not pay.
158
+
159
+ Then: ${NAME} key adt_... saves the key for \`${NAME} mcp\` and for tool calls.
160
+ Balance: ${NAME} key`);
161
+ }
162
+
163
+ const [command, ...rest] = process.argv.slice(2);
164
+ try {
165
+ if (!command || command === "help" || command === "--help" || command === "-h") {
166
+ console.log(HELP);
167
+ } else if (command === "mcp") {
168
+ // Nothing but JSON-RPC may reach stdout here.
169
+ const { key } = parseArgs(rest);
170
+ const { runStdioProxy } = await import("./mcp-stdio.mjs");
171
+ await runStdioProxy({ origin: origin(), key: resolveKey(key).key });
172
+ } else if (command === "setup") {
173
+ const args = parseArgs(rest);
174
+ setup(args.rest[0], args);
175
+ } else if (command === "key") {
176
+ await keyCommand(rest[0]);
177
+ } else if (command === "credits") {
178
+ credits();
179
+ } else {
180
+ const adt = new AgentDataTools({ apiKey: resolveKey().key, origin: origin() });
181
+ if (command === "list") {
182
+ for (const t of await adt.tools({ search: rest.join(" ") })) console.log(`${t.name.padEnd(30)} ${t.price.padEnd(8)} ${t.summary}`);
183
+ } else {
184
+ const inputs = {};
185
+ for (const arg of rest) {
186
+ const eq = arg.indexOf("=");
187
+ if (eq < 1) throw new ToolError(`expected key=value, got "${arg}"`, 400, null);
188
+ inputs[arg.slice(0, eq)] = arg.slice(eq + 1);
189
+ }
190
+ console.log(JSON.stringify(await adt.call(command, inputs), null, 2));
191
+ }
192
+ }
193
+ } catch (e) {
194
+ console.error(e instanceof PaymentRequired || e instanceof ToolError ? e.message : e);
195
+ process.exitCode = 1;
196
+ }
@@ -0,0 +1,22 @@
1
+ // A minimal agent: any OpenAI-compatible chat API plus Agent Data Tools. Needs OPENAI_API_KEY (and optionally OPENAI_BASE_URL, OPENAI_MODEL).
2
+ // node examples/openai-agent.mjs "What does the first page of https://arxiv.org/pdf/1706.03762 say?"
3
+ import { AgentDataTools } from "agent-data-tools";
4
+
5
+ const adt = new AgentDataTools();
6
+ const tools = await adt.openAiTools(["extract", "pdf_text", "weather", "price_token"]);
7
+ const messages = [{ role: "user", content: process.argv[2] ?? "What is the weather in Stockholm?" }];
8
+
9
+ for (let turn = 0; turn < 6; turn++) {
10
+ const res = await fetch(`${process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"}/chat/completions`, {
11
+ method: "POST",
12
+ headers: { "content-type": "application/json", authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
13
+ body: JSON.stringify({ model: process.env.OPENAI_MODEL ?? "gpt-4o-mini", messages, tools }),
14
+ });
15
+ const message = (await res.json()).choices[0].message;
16
+ messages.push(message);
17
+ if (!message.tool_calls?.length) { console.log(message.content); break; }
18
+ for (const call of message.tool_calls) {
19
+ const result = await adt.call(call.function.name, JSON.parse(call.function.arguments)).catch((e) => ({ error: e.message }));
20
+ messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result).slice(0, 20000) });
21
+ }
22
+ }
package/index.mjs ADDED
@@ -0,0 +1,92 @@
1
+ // Client for Agent Data Tools: pay-per-call data tools for AI agents. No dependencies; Node 20+, Deno, Bun, Workers and browsers.
2
+ export const ORIGIN = "https://agent-data-tools.revmesh2074.workers.dev";
3
+ const MAX_URL = 6000;
4
+
5
+ /** A tool call that did not return data. `status` is the HTTP status; `body` is the parsed answer. */
6
+ export class ToolError extends Error {
7
+ constructor(message, status, body) { super(message); this.name = "ToolError"; this.status = status; this.body = body; }
8
+ }
9
+ /** HTTP 402: the free calls are used up (or the tool has none) and no way to pay was configured. */
10
+ export class PaymentRequired extends ToolError {
11
+ constructor(message, status, body, priceUsd, buyUrl) { super(message, status, body); this.name = "PaymentRequired"; this.priceUsd = priceUsd; this.buyUrl = buyUrl; }
12
+ }
13
+
14
+ const priceOf = (body) => { const a = Number(body?.accepts?.[0]?.amount); return Number.isFinite(a) ? a / 1e6 : null; };
15
+
16
+ export class AgentDataTools {
17
+ /**
18
+ * @param {object} [options]
19
+ * @param {string} [options.apiKey] Prepaid card-credit key (adt_…) from /credits; default: the ADT_API_KEY environment variable.
20
+ * @param {typeof fetch} [options.fetch] A fetch that pays HTTP 402 itself, such as @x402/fetch's wrapper around your wallet.
21
+ * @param {string} [options.origin] Service origin.
22
+ * Without a key or a paying fetch, calls use the free trial (25 successful calls per caller; AI media, rendering and search tools excluded).
23
+ */
24
+ constructor({ apiKey, fetch: paying, origin = ORIGIN } = {}) {
25
+ this.apiKey = apiKey ?? globalThis.process?.env?.ADT_API_KEY;
26
+ this.paying = paying;
27
+ this.fetch = paying ?? globalThis.fetch;
28
+ this.origin = origin.replace(/\/$/, "");
29
+ this.catalog = null;
30
+ }
31
+
32
+ /** Every tool: `{ name, path, price, summary, description, params }`. `search` keeps those whose text holds every word. */
33
+ async tools({ search } = {}) {
34
+ if (!this.catalog) {
35
+ const res = await globalThis.fetch(`${this.origin}/openapi.json`, { headers: { accept: "application/json" } });
36
+ if (!res.ok) throw new ToolError(`could not read the catalog: HTTP ${res.status}`, res.status, null);
37
+ const api = await res.json();
38
+ this.catalog = Object.entries(api.paths).flatMap(([path, ops]) => {
39
+ const op = ops.get;
40
+ const amount = op?.["x-payment-info"]?.price?.amount;
41
+ if (!op?.operationId || amount === undefined) return [];
42
+ return [{ name: op.operationId, path, price: `$${amount}`, summary: op.summary ?? "", description: op.description ?? "", params: (op.parameters ?? []).filter((p) => p.in === "query").map((p) => ({ name: p.name, required: Boolean(p.required), type: p.schema?.type ?? "string", description: p.description ?? "" })) }];
43
+ });
44
+ }
45
+ if (!search) return this.catalog;
46
+ const words = search.toLowerCase().split(/\s+/).filter(Boolean);
47
+ return this.catalog.filter((t) => { const text = `${t.name} ${t.summary} ${t.description}`.toLowerCase(); return words.every((w) => text.includes(w)); });
48
+ }
49
+
50
+ /** Call a tool by path (`/weather`) or name (`pdf_text`) with its inputs; resolves to the tool's JSON answer. */
51
+ async call(tool, inputs = {}) {
52
+ let path = tool;
53
+ if (!tool.startsWith("/")) {
54
+ const hit = (await this.tools()).find((t) => t.name === tool);
55
+ if (!hit) throw new ToolError(`no tool named "${tool}"; see tools()`, 404, null);
56
+ path = hit.path;
57
+ }
58
+ const headers = { accept: "application/json" };
59
+ if (this.apiKey) headers.authorization = `Bearer ${this.apiKey}`;
60
+ const query = new URLSearchParams();
61
+ for (const [k, v] of Object.entries(inputs)) if (v !== undefined && v !== null) query.set(k, typeof v === "object" ? JSON.stringify(v) : String(v));
62
+ if (!this.apiKey && !this.paying) query.set("trial", "1");
63
+ const url = `${this.origin}${path}?${query}`;
64
+ // Long inputs (a page to publish, a document to summarise) go as a flat JSON body, which the service reads the same way.
65
+ const res = url.length <= MAX_URL
66
+ ? await this.fetch(url, { headers })
67
+ : await this.fetch(`${this.origin}${path}`, { method: "POST", headers: { ...headers, "content-type": "application/json" }, body: JSON.stringify(Object.fromEntries(query)) });
68
+ const text = await res.text();
69
+ let body;
70
+ try { body = JSON.parse(text); } catch { body = text; }
71
+ if (res.status === 402) {
72
+ const price = priceOf(body);
73
+ throw new PaymentRequired(`${path} needs payment${price === null ? "" : ` ($${price})`}: pass apiKey (card credits from ${this.origin}/credits, from $1) or a paying fetch (x402)`, 402, body, price, `${this.origin}/credits`);
74
+ }
75
+ if (!res.ok) throw new ToolError(`${path} answered HTTP ${res.status}: ${body?.error ?? String(text).slice(0, 200)}`, res.status, body);
76
+ return body;
77
+ }
78
+
79
+ /** The named tools as OpenAI function-calling definitions; pass the model's tool call to `call(name, JSON.parse(arguments))`. */
80
+ async openAiTools(names) {
81
+ const all = await this.tools();
82
+ const picked = names ? names.map((n) => { const t = all.find((x) => x.name === n || x.path === n); if (!t) throw new ToolError(`no tool named "${n}"`, 404, null); return t; }) : all;
83
+ return picked.map((t) => ({
84
+ type: "function",
85
+ function: {
86
+ name: t.name,
87
+ description: t.description.slice(0, 1000),
88
+ parameters: { type: "object", properties: Object.fromEntries(t.params.map((p) => [p.name, { type: p.type, description: p.description }])), required: t.params.filter((p) => p.required).map((p) => p.name) },
89
+ },
90
+ }));
91
+ }
92
+ }
package/mcp-stdio.mjs ADDED
@@ -0,0 +1,97 @@
1
+ // `agent-data-tools mcp`: a stdio MCP server that forwards every JSON-RPC message to the remote server at <origin>/mcp
2
+ // (streamable HTTP) and writes the answers back, one JSON message per line. No dependencies.
3
+ // Only JSON-RPC lines go to stdout; everything else goes to stderr.
4
+
5
+ const log = (...a) => { try { process.stderr.write(`agent-data-tools mcp: ${a.join(" ")}\n`); } catch { /* stderr closed */ } };
6
+ const isRequest = (m) => m && typeof m === "object" && typeof m.method === "string" && m.id !== undefined && m.id !== null;
7
+
8
+ /** Parse an SSE body as it arrives and hand each `data:` payload (joined across lines of one event) to `onData`. */
9
+ async function readSse(body, onData) {
10
+ const decoder = new TextDecoder();
11
+ let buffer = "";
12
+ const flush = (block) => {
13
+ const data = block.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
14
+ if (data) onData(data);
15
+ };
16
+ for await (const chunk of body) {
17
+ buffer = (buffer + decoder.decode(chunk, { stream: true })).replace(/\r\n?/g, "\n");
18
+ let cut;
19
+ while ((cut = buffer.indexOf("\n\n")) >= 0) { flush(buffer.slice(0, cut)); buffer = buffer.slice(cut + 2); }
20
+ }
21
+ buffer = (buffer + decoder.decode()).replace(/\r\n?/g, "\n");
22
+ if (buffer.trim()) flush(buffer);
23
+ }
24
+
25
+ /**
26
+ * Forward one client message (or batch) and write whatever comes back.
27
+ * @param {{ url: string, key?: string, session: { id?: string, protocol?: string }, write: (m: unknown) => void, fetch?: typeof fetch }} ctx
28
+ */
29
+ export async function forward(ctx, message) {
30
+ const requests = (Array.isArray(message) ? message : [message]).filter(isRequest);
31
+ const answered = new Set();
32
+ const out = (m) => {
33
+ for (const one of Array.isArray(m) ? m : [m]) {
34
+ if (!one || typeof one !== "object") continue;
35
+ if (one.id !== undefined && one.id !== null && !one.method) answered.add(one.id);
36
+ const protocol = one.result?.protocolVersion;
37
+ if (typeof protocol === "string" && requests.some((r) => r.method === "initialize" && r.id === one.id)) ctx.session.protocol = protocol;
38
+ ctx.write(one);
39
+ }
40
+ };
41
+ const fail = (text) => { for (const r of requests) if (!answered.has(r.id)) ctx.write({ jsonrpc: "2.0", id: r.id, error: { code: -32000, message: text } }); };
42
+ try {
43
+ const headers = { "content-type": "application/json", accept: "application/json, text/event-stream" };
44
+ if (ctx.key) headers.authorization = `Bearer ${ctx.key}`;
45
+ if (ctx.session.id) headers["mcp-session-id"] = ctx.session.id;
46
+ if (ctx.session.protocol) headers["mcp-protocol-version"] = ctx.session.protocol;
47
+ const res = await (ctx.fetch ?? globalThis.fetch)(ctx.url, { method: "POST", headers, body: JSON.stringify(message) });
48
+ const session = res.headers.get("mcp-session-id");
49
+ if (session) ctx.session.id = session;
50
+ const type = (res.headers.get("content-type") ?? "").toLowerCase();
51
+ if (res.ok && type.includes("text/event-stream") && res.body) {
52
+ await readSse(res.body, (data) => { try { out(JSON.parse(data)); } catch { log("ignored an event that is not JSON"); } });
53
+ if (requests.some((r) => !answered.has(r.id))) fail("the server closed the stream without an answer");
54
+ return;
55
+ }
56
+ const text = await res.text();
57
+ let body;
58
+ try { body = text ? JSON.parse(text) : undefined; } catch { body = undefined; }
59
+ const rpc = (Array.isArray(body) ? body : [body]).every((m) => m && typeof m === "object" && m.jsonrpc === "2.0");
60
+ if (body !== undefined && rpc) { out(body); if (res.ok) return; }
61
+ if (res.ok) { if (requests.length) fail("the server answered without a JSON-RPC message"); return; }
62
+ const detail = typeof body?.error === "string" ? body.error : text.replace(/\s+/g, " ").trim().slice(0, 200);
63
+ log(`HTTP ${res.status}`);
64
+ fail(`the server answered HTTP ${res.status}${detail ? `: ${detail}` : ""}`);
65
+ } catch (e) {
66
+ log(`request failed: ${e?.message ?? e}`);
67
+ fail(`could not reach the server: ${e?.cause?.code ?? e?.message ?? "network error"}`);
68
+ }
69
+ }
70
+
71
+ /** Run the proxy on stdin/stdout until stdin closes. */
72
+ export async function runStdioProxy({ origin, key, input = process.stdin, output = process.stdout }) {
73
+ const ctx = { url: `${origin.replace(/\/$/, "")}/mcp`, key, session: {}, write: (m) => output.write(`${JSON.stringify(m)}\n`) };
74
+ log(`forwarding to ${ctx.url}${key ? " with a key" : " without a key (free calls)"}`);
75
+ const pending = new Set();
76
+ let gate = Promise.resolve(); // initialize finishes before anything after it is sent, so the session id is known
77
+ const handle = (line) => {
78
+ if (!line.trim()) return;
79
+ let message;
80
+ try { message = JSON.parse(line); } catch { ctx.write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } }); return; }
81
+ const first = Array.isArray(message) ? message.some((m) => m?.method === "initialize") : message?.method === "initialize";
82
+ const run = gate.then(() => forward(ctx, message)).catch((e) => log(`unexpected: ${e?.message ?? e}`));
83
+ if (first) gate = run;
84
+ pending.add(run);
85
+ run.finally(() => pending.delete(run));
86
+ };
87
+ let buffer = "";
88
+ input.setEncoding("utf8");
89
+ input.on("data", (chunk) => {
90
+ buffer += chunk;
91
+ let cut;
92
+ while ((cut = buffer.indexOf("\n")) >= 0) { handle(buffer.slice(0, cut)); buffer = buffer.slice(cut + 1); }
93
+ });
94
+ await new Promise((done) => { input.on("end", done); input.on("close", done); input.on("error", done); });
95
+ if (buffer.trim()) handle(buffer);
96
+ while (pending.size) await Promise.allSettled([...pending]);
97
+ }
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
- {
2
- "name": "agent-data-tools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "agent-data-tools",
3
+ "version": "1.1.0",
4
+ "description": "Data tools for AI agents, one call each: PDF to text, web page to Markdown, web search, news, token prices, OFAC sanctions checks, chain reads, IP geolocation, hosted pages. 25 free calls, then card credits or x402.",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "bin": {
11
+ "agent-data-tools": "cli.mjs"
12
+ },
13
+ "files": [
14
+ "index.mjs",
15
+ "cli.mjs",
16
+ "mcp-stdio.mjs",
17
+ "examples",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "license": "MIT",
24
+ "homepage": "https://agent-data-tools.revmesh2074.workers.dev/",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Nutshell2000/agent-data-tools.git",
28
+ "directory": "clients/agent-data-tools"
29
+ },
30
+ "keywords": [
31
+ "ai-agent",
32
+ "agent-tools",
33
+ "function-calling",
34
+ "openai-tools",
35
+ "mcp",
36
+ "mcp-server",
37
+ "claude",
38
+ "cursor",
39
+ "x402",
40
+ "pdf-to-text",
41
+ "web-scraping",
42
+ "html-to-markdown",
43
+ "web-search",
44
+ "news-api",
45
+ "token-price",
46
+ "crypto-price",
47
+ "ofac",
48
+ "sanctions-screening",
49
+ "ip-geolocation",
50
+ "llm-tools"
51
+ ]
52
+ }