agent-data-tools 1.1.1 → 1.3.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 CHANGED
@@ -1,21 +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.
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
@@ -4,7 +4,7 @@ Data tools for AI agents, one HTTP call each: PDF to text, web page to Markdown,
4
4
 
5
5
  ## Use it in your AI client
6
6
 
7
- One command per client. Each connects the client to the MCP server at `https://agent-data-tools.revmesh2074.workers.dev/mcp`.
7
+ One command per client. Each connects the client to the compact MCP server at `https://agent-data-tools.revmesh2074.workers.dev/mcp/compact`: three tools (`find_tool`, `tool_details`, `call_tool`) that find, describe and run any tool in the catalog, so the assistant does not carry several hundred tool definitions. Add `--full` to `setup` or `mcp` for the server that lists every tool as its own definition (`https://agent-data-tools.revmesh2074.workers.dev/mcp`).
8
8
 
9
9
  ```sh
10
10
  npx agent-data-tools setup claude-code # prints the `claude mcp add --transport http ...` command to run
@@ -59,10 +59,11 @@ const answer = await adt.call(call.function.name, JSON.parse(call.function.argum
59
59
  |---|---|
60
60
  | Free calls | None. 25 successful calls per caller; AI media, rendering and search tools are paid only. |
61
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. |
62
+ | USDC per call from your wallet, in your AI client | Start the MCP server with `AGENT_DATA_TOOLS_WALLET_KEY` set to the private key of a wallet that holds USDC on Base: `npx agent-data-tools mcp`. No account and no key to buy. After the free calls each call is paid at its listed price; failed calls are not charged. Limits: $0.10 a call and $1 a day, changed with `AGENT_DATA_TOOLS_MAX_USD` and `AGENT_DATA_TOOLS_DAILY_USD`. Use a wallet that holds only what you are willing to spend. |
63
+ | USDC per call (x402), from code | 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
 
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
+ 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 reads a wallet key only from `AGENT_DATA_TOOLS_WALLET_KEY`, only in `mcp`, and only to sign USDC payments on Base for the exact amount the service quoted; it never writes, prints or sends the key. Your API key goes only to the service origin.
65
66
 
66
67
  ## Also
67
68
 
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).
69
+ 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 servers: `https://agent-data-tools.revmesh2074.workers.dev/mcp/compact` (three tools for the whole catalog) and `https://agent-data-tools.revmesh2074.workers.dev/mcp` (every tool). Terms: [`/terms`](https://agent-data-tools.revmesh2074.workers.dev/terms).
package/ai-sdk.mjs ADDED
@@ -0,0 +1,34 @@
1
+ // Agent Data Tools as Vercel AI SDK tools: `import { agentDataTools } from "agent-data-tools/ai-sdk"`.
2
+ // `ai` is a peer dependency of this entry point only; the rest of the package has no dependencies.
3
+ import { tool, jsonSchema } from "ai";
4
+ import { AgentDataTools } from "./index.mjs";
5
+
6
+ /**
7
+ * The named tools as AI SDK tools, ready for `generateText({ tools })`.
8
+ * @param {string[]} names Tool names (`weather`, `pdf_text`) or paths (`/weather`). Name the few a task needs: the catalog
9
+ * holds several hundred and a model works best with a handful.
10
+ * @param {object} [options] `apiKey` (prepaid key from /credits; default the ADT_API_KEY environment variable), `fetch`
11
+ * (a fetch that pays HTTP 402, such as @x402/fetch around your wallet), `origin`, or `client` (an AgentDataTools you made).
12
+ * Without a key or a paying fetch, calls use the free calls.
13
+ * @returns {Promise<Record<string, import("ai").Tool>>}
14
+ */
15
+ export async function agentDataTools(names, options = {}) {
16
+ if (!Array.isArray(names) || !names.length) throw new TypeError("agentDataTools(names): pass the tool names to load, for example ['weather', 'pdf_text']");
17
+ const client = options.client ?? new AgentDataTools(options);
18
+ const definitions = await client.openAiTools(names);
19
+ return Object.fromEntries(definitions.map(({ function: f }) => [f.name, tool({
20
+ description: f.description,
21
+ inputSchema: jsonSchema(f.parameters),
22
+ // A failed or unpaid call is returned to the model as text it can act on, not thrown: one bad tool call should not
23
+ // end the whole generation.
24
+ execute: async (input) => {
25
+ try {
26
+ return await client.call(f.name, input ?? {});
27
+ } catch (e) {
28
+ return { error: e?.message ?? String(e), ...(e?.priceUsd !== undefined ? { priceUsd: e.priceUsd, buyUrl: e.buyUrl } : {}) };
29
+ }
30
+ },
31
+ })]));
32
+ }
33
+
34
+ export { AgentDataTools };
package/cli.mjs CHANGED
@@ -1,196 +1,217 @@
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));
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
+ // The server a client is connected to. The default is the compact one: three tools (find_tool, tool_details, call_tool)
15
+ // that reach the whole catalog, so the assistant does not carry several hundred tool definitions. --full picks the
16
+ // server that lists every tool as its own definition.
17
+ const serverPath = (full) => (full ? "/mcp" : "/mcp/compact");
18
+ const stdio = (full) => ({ command: "npx", args: full ? ["-y", NAME, "mcp", "--full"] : ["-y", NAME, "mcp"] });
19
+ const TARGETS = ["claude-code", "claude-desktop", "cursor", "vscode"];
20
+
21
+ const origin = () => (process.env.AGENT_DATA_TOOLS_ORIGIN || ORIGIN).replace(/\/$/, "");
22
+ const configFile = () => join(homedir(), ".agent-data-tools", "config.json");
23
+ const tail = (key) => `…${key.slice(-4)}`;
24
+ const validKey = (key) => typeof key === "string" && /^adt_\S+$/.test(key);
25
+
26
+ function readConfig() {
27
+ try { const c = JSON.parse(readFileSync(configFile(), "utf8").replace(/^/, "")); return c && typeof c === "object" && !Array.isArray(c) ? c : {}; } catch { return {}; }
28
+ }
29
+
30
+ /** The key and where it came from: --key, AGENT_DATA_TOOLS_KEY, the config file, then ADT_API_KEY (the library's variable). */
31
+ function resolveKey(flag) {
32
+ if (flag) return { key: flag, source: "--key" };
33
+ if (process.env.AGENT_DATA_TOOLS_KEY) return { key: process.env.AGENT_DATA_TOOLS_KEY, source: "AGENT_DATA_TOOLS_KEY" };
34
+ const saved = readConfig().key;
35
+ if (typeof saved === "string" && saved) return { key: saved, source: configFile() };
36
+ if (process.env.ADT_API_KEY) return { key: process.env.ADT_API_KEY, source: "ADT_API_KEY" };
37
+ return { key: undefined, source: undefined };
38
+ }
39
+
40
+ /** Split `--key <value>`, `--key=<value>`, `--print` and `--full` from the other arguments. */
41
+ function parseArgs(args) {
42
+ const rest = [];
43
+ let key, print = false, full = false;
44
+ for (let i = 0; i < args.length; i++) {
45
+ const a = args[i];
46
+ if (a === "--print") print = true;
47
+ else if (a === "--full") full = true;
48
+ else if (a === "--key") { key = args[++i]; if (!key) throw new ToolError("--key needs a value (adt_...)", 400, null); }
49
+ else if (a.startsWith("--key=")) key = a.slice(6);
50
+ else rest.push(a);
51
+ }
52
+ if (key !== undefined && !validKey(key)) throw new ToolError("the key must start with adt_", 400, null);
53
+ return { rest, key, print, full };
54
+ }
55
+
56
+ const HELP = `Usage:
57
+ agent-data-tools setup [claude-code|claude-desktop|cursor|vscode] [--key adt_...] [--print] [--full]
58
+ add the tools to that client (--print shows the change without writing)
59
+ agent-data-tools mcp [--key adt_...] [--full]
60
+ stdio MCP server that forwards to ${ORIGIN}/mcp/compact
61
+ The client gets three tools that find, describe and run any tool in the catalog.
62
+ --full connects ${ORIGIN}/mcp instead: every tool as its own definition (several hundred).
63
+ agent-data-tools key [adt_...] save a prepaid key; without a key, show whether one is set and its balance
64
+ agent-data-tools credits where to get a prepaid key (card or USDC)
65
+ agent-data-tools list [words] tools and prices
66
+ agent-data-tools <tool> key=value ... call a tool by name or /path; prints its JSON
67
+
68
+ 25 free calls without any setup. A prepaid key is read from --key, then AGENT_DATA_TOOLS_KEY,
69
+ then ~/.agent-data-tools/config.json, then ADT_API_KEY.
70
+
71
+ Wallet mode for \`mcp\` (no account, no key): set AGENT_DATA_TOOLS_WALLET_KEY to the private key of a wallet holding
72
+ USDC on Base. After the free calls each call is paid over x402 at its listed price, at most $0.10 a call and $1 a day
73
+ unless AGENT_DATA_TOOLS_MAX_USD and AGENT_DATA_TOOLS_DAILY_USD say otherwise. Failed calls are not charged.`;
74
+
75
+ function menu() {
76
+ return `Add the tools to a client:
77
+ agent-data-tools setup claude-code prints the \`claude mcp add\` command to run
78
+ agent-data-tools setup claude-desktop writes claude_desktop_config.json
79
+ agent-data-tools setup cursor writes ~/.cursor/mcp.json
80
+ agent-data-tools setup vscode writes .vscode/mcp.json in the current folder
81
+
82
+ Add --print to see the change without writing, and --key adt_... to connect with a prepaid key.
83
+ The client gets three tools that reach the whole catalog; add --full for every tool as its own definition.
84
+ Without a key the server gives 25 free calls. Keys: agent-data-tools credits`;
85
+ }
86
+
87
+ function desktopConfigPath() {
88
+ if (process.platform === "win32") return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
89
+ if (process.platform === "darwin") return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
90
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
91
+ }
92
+
93
+ /** Add one server entry to a JSON config file, keeping everything else; a copy of the old file stays next to it as .bak. */
94
+ function mergeInto(file, section, entry, print) {
95
+ const snippet = JSON.stringify({ [section]: { [NAME]: entry } }, null, 2);
96
+ if (print) { console.log(`${file}\n${snippet}`); return false; }
97
+ let json = {};
98
+ const exists = existsSync(file);
99
+ if (exists) {
100
+ try { json = JSON.parse(readFileSync(file, "utf8").replace(/^/, "")); } catch { json = null; }
101
+ const usable = json && typeof json === "object" && !Array.isArray(json) && (json[section] === undefined || (json[section] && typeof json[section] === "object" && !Array.isArray(json[section])));
102
+ if (!usable) {
103
+ console.error(`${file} is not JSON this command can edit, so it was left unchanged. Add this to it yourself:\n${snippet}`);
104
+ process.exitCode = 1;
105
+ return false;
106
+ }
107
+ copyFileSync(file, `${file}.bak`);
108
+ } else mkdirSync(dirname(file), { recursive: true });
109
+ json[section] = { ...(json[section] ?? {}), [NAME]: entry };
110
+ writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
111
+ console.log(`${exists ? "Updated" : "Created"} ${file}${exists ? ` (previous version: ${file}.bak)` : ""}`);
112
+ return true;
113
+ }
114
+
115
+ function setup(target, { key, print, full }) {
116
+ if (!target) { console.log(menu()); return; }
117
+ if (!TARGETS.includes(target)) throw new ToolError(`unknown client "${target}"; use one of: ${TARGETS.join(", ")}`, 400, null);
118
+ const url = `${origin()}${serverPath(full)}`;
119
+ const STDIO = stdio(full);
120
+ const saved = key ? undefined : resolveKey().key;
121
+ const savedNote = `A key is already saved for this computer; pass it with --key to connect ${target} with it.`;
122
+ if (target === "claude-code") {
123
+ console.log(`claude mcp add --transport http ${NAME} ${url}${key ? ` --header "Authorization: Bearer ${key}"` : ""}`);
124
+ console.error(`Run the command above in a terminal; it registers the server with Claude Code.${saved ? `\n${savedNote}` : ""}`);
125
+ } else if (target === "claude-desktop") {
126
+ // Claude Desktop starts local commands; the stdio server reads the key from its environment or the config file.
127
+ const entry = key ? { ...STDIO, env: { AGENT_DATA_TOOLS_KEY: key } } : STDIO;
128
+ 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." : ""}`);
129
+ } else if (target === "cursor") {
130
+ const entry = key ? { url, headers: { Authorization: `Bearer ${key}` } } : { url };
131
+ 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}` : ""}`);
132
+ } else {
133
+ // 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.
134
+ const entry = key || saved ? { type: "stdio", ...STDIO } : { type: "http", url };
135
+ const done = mergeInto(resolve(".vscode", "mcp.json"), "servers", entry, print);
136
+ 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.`);
137
+ else if (saved && done) console.error("The command in the file reads your saved key; nothing secret is in the project file.");
138
+ }
139
+ }
140
+
141
+ async function keyCommand(value) {
142
+ if (value !== undefined) {
143
+ if (!validKey(value)) throw new ToolError("the key must start with adt_", 400, null);
144
+ const file = configFile();
145
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
146
+ writeFileSync(file, `${JSON.stringify({ ...readConfig(), key: value }, null, 2)}\n`, { mode: 0o600 });
147
+ try { chmodSync(file, 0o600); } catch { /* not supported on this file system */ }
148
+ console.log(`Saved the key ending in ${tail(value)} to ${file}`);
149
+ return;
150
+ }
151
+ const { key, source } = resolveKey();
152
+ if (!key) { console.log(`No key is set. The server gives 25 free calls without one. Get a key: ${NAME} credits`); return; }
153
+ console.log(`A key is set (ends in ${tail(key)}), from ${source}.`);
154
+ try {
155
+ const res = await fetch(`${origin()}/credits/balance`, { headers: { accept: "application/json", "x-api-key": key } });
156
+ const body = await res.json().catch(() => null);
157
+ if (res.ok && body && typeof body.balanceCredits === "number") console.log(`Balance: ${body.balanceCredits} credits (${body.calls ?? 0} calls so far).`);
158
+ else console.log(`Balance not available: ${typeof body?.error === "string" ? body.error : `HTTP ${res.status}`}`);
159
+ } catch (e) {
160
+ console.log(`Balance not available: ${e?.cause?.code ?? e?.message ?? "network error"}`);
161
+ }
162
+ }
163
+
164
+ function credits() {
165
+ const o = origin();
166
+ console.log(`Card: ${o}/credits
167
+ Open it in a browser to buy a prepaid key with a card.
168
+ USDC, $1: ${o}/credits/usdc/1
169
+ USDC, $5: ${o}/credits/usdc/5
170
+ USDC, $20: ${o}/credits/usdc/20
171
+ Each is an x402 route: an x402 client that pays it gets a key back. This command does not pay.
172
+
173
+ Then: ${NAME} key adt_... saves the key for \`${NAME} mcp\` and for tool calls.
174
+ Balance: ${NAME} key`);
175
+ }
176
+
177
+ const [command, ...rest] = process.argv.slice(2);
178
+ try {
179
+ if (!command || command === "help" || command === "--help" || command === "-h") {
180
+ console.log(HELP);
181
+ } else if (command === "mcp") {
182
+ // Nothing but JSON-RPC may reach stdout here.
183
+ const { key, full } = parseArgs(rest);
184
+ const { runStdioProxy } = await import("./mcp-stdio.mjs");
185
+ const prepaid = resolveKey(key).key;
186
+ // Wallet mode: no prepaid key, and a private key in the environment. The key never appears in arguments or output.
187
+ let payer;
188
+ if (!prepaid && process.env.AGENT_DATA_TOOLS_WALLET_KEY) {
189
+ const { walletPayer, limitsFromEnv } = await import("./wallet-pay.mjs");
190
+ payer = await walletPayer({ key: process.env.AGENT_DATA_TOOLS_WALLET_KEY.trim(), ...limitsFromEnv() });
191
191
  }
192
- }
193
- } catch (e) {
194
- console.error(e instanceof PaymentRequired || e instanceof ToolError ? e.message : e);
195
- process.exitCode = 1;
196
- }
192
+ await runStdioProxy({ origin: origin(), key: prepaid, payer, path: serverPath(full) });
193
+ } else if (command === "setup") {
194
+ const args = parseArgs(rest);
195
+ setup(args.rest[0], args);
196
+ } else if (command === "key") {
197
+ await keyCommand(rest[0]);
198
+ } else if (command === "credits") {
199
+ credits();
200
+ } else {
201
+ const adt = new AgentDataTools({ apiKey: resolveKey().key, origin: origin() });
202
+ if (command === "list") {
203
+ for (const t of await adt.tools({ search: rest.join(" ") })) console.log(`${t.name.padEnd(30)} ${t.price.padEnd(8)} ${t.summary}`);
204
+ } else {
205
+ const inputs = {};
206
+ for (const arg of rest) {
207
+ const eq = arg.indexOf("=");
208
+ if (eq < 1) throw new ToolError(`expected key=value, got "${arg}"`, 400, null);
209
+ inputs[arg.slice(0, eq)] = arg.slice(eq + 1);
210
+ }
211
+ console.log(JSON.stringify(await adt.call(command, inputs), null, 2));
212
+ }
213
+ }
214
+ } catch (e) {
215
+ console.error(e instanceof PaymentRequired || e instanceof ToolError ? e.message : e);
216
+ process.exitCode = 1;
217
+ }
package/mcp-stdio.mjs CHANGED
@@ -1,97 +1,127 @@
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.
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
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
- */
4
+ // Wallet mode (ctx.payer, see wallet-pay.mjs): a tools/call that comes back as an x402 payment request is paid from the
5
+ // user's wallet and sent once more with the payment in params._meta["x402/payment"]; the client sees only the answer.
6
+ import { paymentRequiredOf } from "./wallet-pay.mjs";
7
+
8
+ const log = (...a) => { try { process.stderr.write(`agent-data-tools mcp: ${a.join(" ")}\n`); } catch { /* stderr closed */ } };
9
+ const isRequest = (m) => m && typeof m === "object" && typeof m.method === "string" && m.id !== undefined && m.id !== null;
10
+
11
+ /** Parse an SSE body as it arrives and hand each `data:` payload (joined across lines of one event) to `onData`. */
12
+ async function readSse(body, onData) {
13
+ const decoder = new TextDecoder();
14
+ let buffer = "";
15
+ const flush = (block) => {
16
+ const data = block.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
17
+ if (data) onData(data);
18
+ };
19
+ for await (const chunk of body) {
20
+ buffer = (buffer + decoder.decode(chunk, { stream: true })).replace(/\r\n?/g, "\n");
21
+ let cut;
22
+ while ((cut = buffer.indexOf("\n\n")) >= 0) { flush(buffer.slice(0, cut)); buffer = buffer.slice(cut + 2); }
23
+ }
24
+ buffer = (buffer + decoder.decode()).replace(/\r\n?/g, "\n");
25
+ if (buffer.trim()) flush(buffer);
26
+ }
27
+
28
+ /**
29
+ * Forward one client message (or batch) and write whatever comes back.
30
+ * @param {{ url: string, key?: string, session: { id?: string, protocol?: string }, write: (m: unknown) => void, fetch?: typeof fetch }} ctx
31
+ */
29
32
  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;
33
+ const unpaid = [];
34
+ await forwardOnce(ctx, message, ctx.payer ? unpaid : null);
35
+ for (const { request, response, paymentRequired } of unpaid) {
36
+ let reserved;
37
+ try {
38
+ reserved = ctx.payer.guard.take(paymentRequired);
39
+ const payment = await ctx.payer.sign(paymentRequired, reserved.offer);
40
+ const paid = { ...request, params: { ...request.params, _meta: { ...(request.params?._meta ?? {}), "x402/payment": payment } } };
41
+ let failed = false;
42
+ const write = (m) => { if (m?.id === request.id && (m.error || m.result?.isError)) failed = true; ctx.write(m); };
43
+ await forwardOnce({ ...ctx, write }, paid, null);
44
+ if (failed) ctx.payer.guard.refund(reserved.usd); // a failed call is never settled by the server
45
+ else log(`paid $${reserved.usd} for ${request.params?.name ?? "a tool"}; $${ctx.payer.guard.spentToday.toFixed(4)} today`);
46
+ } catch (e) {
47
+ if (reserved) ctx.payer.guard.refund(reserved.usd);
48
+ log(`not paid: ${e?.message ?? e}`);
49
+ ctx.write(response); // the assistant sees the server's own payment request
55
50
  }
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
51
  }
69
52
  }
70
53
 
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
- }
54
+ async function forwardOnce(ctx, message, unpaid) {
55
+ const requests = (Array.isArray(message) ? message : [message]).filter(isRequest);
56
+ const answered = new Set();
57
+ const out = (m) => {
58
+ for (const one of Array.isArray(m) ? m : [m]) {
59
+ if (!one || typeof one !== "object") continue;
60
+ if (one.id !== undefined && one.id !== null && !one.method) answered.add(one.id);
61
+ const protocol = one.result?.protocolVersion;
62
+ if (typeof protocol === "string" && requests.some((r) => r.method === "initialize" && r.id === one.id)) ctx.session.protocol = protocol;
63
+ if (unpaid) {
64
+ const request = requests.find((r) => r.id === one.id && r.method === "tools/call");
65
+ const paymentRequired = request ? paymentRequiredOf(one.result) : null;
66
+ if (paymentRequired) { unpaid.push({ request, response: one, paymentRequired }); continue; }
67
+ }
68
+ ctx.write(one);
69
+ }
70
+ };
71
+ 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 } }); };
72
+ try {
73
+ const headers = { "content-type": "application/json", accept: "application/json, text/event-stream" };
74
+ if (ctx.key) headers.authorization = `Bearer ${ctx.key}`;
75
+ if (ctx.session.id) headers["mcp-session-id"] = ctx.session.id;
76
+ if (ctx.session.protocol) headers["mcp-protocol-version"] = ctx.session.protocol;
77
+ const res = await (ctx.fetch ?? globalThis.fetch)(ctx.url, { method: "POST", headers, body: JSON.stringify(message) });
78
+ const session = res.headers.get("mcp-session-id");
79
+ if (session) ctx.session.id = session;
80
+ const type = (res.headers.get("content-type") ?? "").toLowerCase();
81
+ if (res.ok && type.includes("text/event-stream") && res.body) {
82
+ await readSse(res.body, (data) => { try { out(JSON.parse(data)); } catch { log("ignored an event that is not JSON"); } });
83
+ if (requests.some((r) => !answered.has(r.id))) fail("the server closed the stream without an answer");
84
+ return;
85
+ }
86
+ const text = await res.text();
87
+ let body;
88
+ try { body = text ? JSON.parse(text) : undefined; } catch { body = undefined; }
89
+ const rpc = (Array.isArray(body) ? body : [body]).every((m) => m && typeof m === "object" && m.jsonrpc === "2.0");
90
+ if (body !== undefined && rpc) { out(body); if (res.ok) return; }
91
+ if (res.ok) { if (requests.length) fail("the server answered without a JSON-RPC message"); return; }
92
+ const detail = typeof body?.error === "string" ? body.error : text.replace(/\s+/g, " ").trim().slice(0, 200);
93
+ log(`HTTP ${res.status}`);
94
+ fail(`the server answered HTTP ${res.status}${detail ? `: ${detail}` : ""}`);
95
+ } catch (e) {
96
+ log(`request failed: ${e?.message ?? e}`);
97
+ fail(`could not reach the server: ${e?.cause?.code ?? e?.message ?? "network error"}`);
98
+ }
99
+ }
100
+
101
+ /** Run the proxy on stdin/stdout until stdin closes. */
102
+ export async function runStdioProxy({ origin, key, payer, path = "/mcp/compact", input = process.stdin, output = process.stdout }) {
103
+ const ctx = { url: `${origin.replace(/\/$/, "")}${path}`, key, payer, session: {}, write: (m) => output.write(`${JSON.stringify(m)}\n`) };
104
+ log(`forwarding to ${ctx.url}${key ? " with a key" : payer ? ` paying per call from wallet ${payer.address} (USDC on Base)` : " without a key (free calls)"}`);
105
+ const pending = new Set();
106
+ let gate = Promise.resolve(); // initialize finishes before anything after it is sent, so the session id is known
107
+ const handle = (line) => {
108
+ if (!line.trim()) return;
109
+ let message;
110
+ try { message = JSON.parse(line); } catch { ctx.write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } }); return; }
111
+ const first = Array.isArray(message) ? message.some((m) => m?.method === "initialize") : message?.method === "initialize";
112
+ const run = gate.then(() => forward(ctx, message)).catch((e) => log(`unexpected: ${e?.message ?? e}`));
113
+ if (first) gate = run;
114
+ pending.add(run);
115
+ run.finally(() => pending.delete(run));
116
+ };
117
+ let buffer = "";
118
+ input.setEncoding("utf8");
119
+ input.on("data", (chunk) => {
120
+ buffer += chunk;
121
+ let cut;
122
+ while ((cut = buffer.indexOf("\n")) >= 0) { handle(buffer.slice(0, cut)); buffer = buffer.slice(cut + 1); }
123
+ });
124
+ await new Promise((done) => { input.on("end", done); input.on("close", done); input.on("error", done); });
125
+ if (buffer.trim()) handle(buffer);
126
+ while (pending.size) await Promise.allSettled([...pending]);
127
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "agent-data-tools",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "mcpName": "io.github.Nutshell2000/agent-data-tools",
5
5
  "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.",
6
6
  "type": "module",
7
7
  "main": "./index.mjs",
8
8
  "exports": {
9
- ".": "./index.mjs"
9
+ ".": "./index.mjs",
10
+ "./ai-sdk": "./ai-sdk.mjs"
10
11
  },
11
12
  "bin": {
12
13
  "agent-data-tools": "cli.mjs"
@@ -14,13 +15,26 @@
14
15
  "files": [
15
16
  "index.mjs",
16
17
  "cli.mjs",
17
- "mcp-stdio.mjs",
18
+ "mcp-stdio.mjs",
19
+ "wallet-pay.mjs",
20
+ "ai-sdk.mjs",
18
21
  "examples",
19
22
  "README.md"
20
23
  ],
21
24
  "engines": {
22
25
  "node": ">=20"
23
26
  },
27
+ "peerDependencies": {
28
+ "ai": ">=5"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "ai": { "optional": true }
32
+ },
33
+ "optionalDependencies": {
34
+ "@x402/core": "^2.28.0",
35
+ "@x402/evm": "^2.28.0",
36
+ "viem": "^2.21.0"
37
+ },
24
38
  "license": "MIT",
25
39
  "homepage": "https://agent-data-tools.revmesh2074.workers.dev/",
26
40
  "repository": {
@@ -48,6 +62,8 @@
48
62
  "ofac",
49
63
  "sanctions-screening",
50
64
  "ip-geolocation",
51
- "llm-tools"
65
+ "llm-tools",
66
+ "ai-sdk",
67
+ "vercel-ai"
52
68
  ]
53
69
  }
package/wallet-pay.mjs ADDED
@@ -0,0 +1,96 @@
1
+ // Wallet mode for `agent-data-tools mcp`: pay each tool call over x402 from the user's own wallet, with no account and no
2
+ // prepaid key. The private key is read from the environment only (AGENT_DATA_TOOLS_WALLET_KEY), is never written, logged
3
+ // or sent anywhere, and signs nothing but EIP-3009 USDC transfer authorizations for the exact amount the server quoted.
4
+ // The three signing packages are optional dependencies and are loaded only in this mode.
5
+
6
+ /** USDC on Base mainnet: the only asset and network this client pays with. */
7
+ export const BASE = "eip155:8453";
8
+ export const BASE_USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
9
+ /** Base Sepolia and its faucet USDC, for testing against a staging server only (AGENT_DATA_TOOLS_TESTNET=1). */
10
+ export const TESTNET = { network: "eip155:84532", asset: "0x036cbd53842c5426634e7929541ec2318f3dcf7e" };
11
+ export const MAINNET = { network: BASE, asset: BASE_USDC };
12
+
13
+ const today = () => new Date().toISOString().slice(0, 10);
14
+
15
+ /** The payment request inside a tool result, as the x402 MCP transport carries it, or null. */
16
+ export function paymentRequiredOf(result) {
17
+ if (!result || typeof result !== "object" || !result.isError) return null;
18
+ const ok = (o) => (o && typeof o === "object" && Array.isArray(o.accepts) && o.accepts.length && o.x402Version !== undefined ? o : null);
19
+ const direct = ok(result.structuredContent);
20
+ if (direct) return direct;
21
+ const first = Array.isArray(result.content) ? result.content[0] : undefined;
22
+ if (first?.type !== "text" || typeof first.text !== "string") return null;
23
+ try { return ok(JSON.parse(first.text)); } catch { return null; }
24
+ }
25
+
26
+ /** The one offer this client may pay: exact scheme, USDC on Base. */
27
+ export function baseOffer(paymentRequired, chain = MAINNET) {
28
+ return paymentRequired.accepts.find((a) => a?.scheme === "exact" && a.network === chain.network && String(a.asset).toLowerCase() === chain.asset) ?? null;
29
+ }
30
+
31
+ /**
32
+ * Spending rules, kept apart from signing so they can be tested without the signing packages.
33
+ * @param {{ maxUsd: number, dailyUsd: number, payTo?: string, chain?: { network: string, asset: string } }} limits
34
+ */
35
+ export function spendGuard({ maxUsd, dailyUsd, payTo, chain = MAINNET }) {
36
+ let day = today(), spent = 0;
37
+ return {
38
+ /** Returns the offer to pay, or throws an Error whose message says why not. Reserves the amount. */
39
+ take(paymentRequired) {
40
+ const offer = baseOffer(paymentRequired, chain);
41
+ if (!offer) throw new Error("the server offered no USDC-on-Base payment");
42
+ if (!/^\d+$/.test(String(offer.amount))) throw new Error("the quoted amount is not a whole number of USDC units");
43
+ const usd = Number(offer.amount) / 1e6;
44
+ if (payTo && String(offer.payTo).toLowerCase() !== payTo.toLowerCase()) throw new Error("the quote pays an address other than the one this client was started for");
45
+ if (usd > maxUsd) throw new Error(`the call costs $${usd} and the per-call limit is $${maxUsd} (AGENT_DATA_TOOLS_MAX_USD)`);
46
+ if (day !== today()) { day = today(); spent = 0; }
47
+ if (spent + usd > dailyUsd + 1e-9) throw new Error(`the daily limit of $${dailyUsd} is used up (AGENT_DATA_TOOLS_DAILY_USD)`);
48
+ spent += usd;
49
+ return { offer, usd };
50
+ },
51
+ /** Gives a reserved amount back when the payment was not used (the call failed, so nothing settled). */
52
+ refund(usd) { spent = Math.max(0, spent - usd); },
53
+ get spentToday() { return day === today() ? spent : 0; },
54
+ };
55
+ }
56
+
57
+ const limit = (raw, fallback, name) => {
58
+ if (raw === undefined || raw === "") return fallback;
59
+ const n = Number(raw);
60
+ if (!Number.isFinite(n) || n <= 0 || n > 100) throw new Error(`${name} must be a dollar amount above 0 and at most 100`);
61
+ return n;
62
+ };
63
+
64
+ /** Limits from the environment: $0.10 a call and $1 a day unless the user sets others. */
65
+ export function limitsFromEnv(env = process.env) {
66
+ return {
67
+ maxUsd: limit(env.AGENT_DATA_TOOLS_MAX_USD, 0.1, "AGENT_DATA_TOOLS_MAX_USD"),
68
+ dailyUsd: limit(env.AGENT_DATA_TOOLS_DAILY_USD, 1, "AGENT_DATA_TOOLS_DAILY_USD"),
69
+ chain: env.AGENT_DATA_TOOLS_TESTNET === "1" ? TESTNET : MAINNET,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * A payer for the proxy. Loads the signing packages, derives the address and returns `{ address, guard, sign }`.
75
+ * @param {{ key: string, maxUsd: number, dailyUsd: number, chain?: { network: string, asset: string } }} options
76
+ */
77
+ export async function walletPayer({ key, maxUsd, dailyUsd, chain = MAINNET }) {
78
+ const hex = /^0x/i.test(key) ? key : `0x${key}`;
79
+ if (!/^0x[0-9a-fA-F]{64}$/.test(hex)) throw new Error("AGENT_DATA_TOOLS_WALLET_KEY must be a 32-byte hex private key");
80
+ let core, evm, accounts;
81
+ try {
82
+ [core, evm, accounts] = await Promise.all([import("@x402/core/client"), import("@x402/evm/exact/client"), import("viem/accounts")]);
83
+ } catch {
84
+ throw new Error("wallet mode needs the packages @x402/core, @x402/evm and viem; install them next to agent-data-tools (npm i @x402/core @x402/evm viem)");
85
+ }
86
+ const account = accounts.privateKeyToAccount(hex);
87
+ const client = new core.x402Client();
88
+ evm.registerExactEvmScheme(client, { signer: account });
89
+ const guard = spendGuard({ maxUsd, dailyUsd, chain });
90
+ return {
91
+ address: account.address,
92
+ guard,
93
+ /** Signs the Base USDC offer of one payment request; the caller has already passed the guard. */
94
+ sign: (paymentRequired, offer) => client.createPaymentPayload({ ...paymentRequired, accepts: [offer] }),
95
+ };
96
+ }