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 +21 -0
- package/README.md +68 -3
- package/cli.mjs +196 -0
- package/examples/openai-agent.mjs +22 -0
- package/index.mjs +92 -0
- package/mcp-stdio.mjs +97 -0
- package/package.json +52 -6
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
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|