@xlmtools/cli 0.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.
Files changed (63) hide show
  1. package/README.md +99 -0
  2. package/dist/cli.js +214 -0
  3. package/dist/index.js +77 -0
  4. package/dist/lib/budget.js +54 -0
  5. package/dist/lib/cache.js +41 -0
  6. package/dist/lib/config.js +16 -0
  7. package/dist/lib/format.js +37 -0
  8. package/dist/lib/logger.js +2 -0
  9. package/dist/lib/wallet.js +106 -0
  10. package/dist/tools/budget.js +56 -0
  11. package/dist/tools/card.js +51 -0
  12. package/dist/tools/crypto.js +23 -0
  13. package/dist/tools/dex-candles.js +47 -0
  14. package/dist/tools/dex-orderbook.js +38 -0
  15. package/dist/tools/dex-trades.js +46 -0
  16. package/dist/tools/domain.js +22 -0
  17. package/dist/tools/image.js +42 -0
  18. package/dist/tools/oracle-price.js +36 -0
  19. package/dist/tools/reddit.js +40 -0
  20. package/dist/tools/research.js +39 -0
  21. package/dist/tools/scrape.js +32 -0
  22. package/dist/tools/screenshot.js +37 -0
  23. package/dist/tools/search.js +39 -0
  24. package/dist/tools/stellar-account.js +30 -0
  25. package/dist/tools/stellar-asset.js +31 -0
  26. package/dist/tools/stellar-pools.js +41 -0
  27. package/dist/tools/stocks.js +34 -0
  28. package/dist/tools/swap-quote.js +37 -0
  29. package/dist/tools/tools-list.js +16 -0
  30. package/dist/tools/wallet-tool.js +35 -0
  31. package/dist/tools/weather.js +22 -0
  32. package/dist/tools/youtube.js +42 -0
  33. package/package.json +28 -0
  34. package/src/cli.ts +240 -0
  35. package/src/index.ts +90 -0
  36. package/src/lib/budget.ts +78 -0
  37. package/src/lib/cache.ts +66 -0
  38. package/src/lib/config.ts +18 -0
  39. package/src/lib/format.ts +51 -0
  40. package/src/lib/logger.ts +6 -0
  41. package/src/lib/wallet.ts +143 -0
  42. package/src/tools/budget.ts +67 -0
  43. package/src/tools/crypto.ts +25 -0
  44. package/src/tools/dex-candles.ts +52 -0
  45. package/src/tools/dex-orderbook.ts +45 -0
  46. package/src/tools/dex-trades.ts +51 -0
  47. package/src/tools/domain.ts +24 -0
  48. package/src/tools/image.ts +49 -0
  49. package/src/tools/oracle-price.ts +42 -0
  50. package/src/tools/research.ts +47 -0
  51. package/src/tools/scrape.ts +41 -0
  52. package/src/tools/screenshot.ts +46 -0
  53. package/src/tools/search.ts +47 -0
  54. package/src/tools/stellar-account.ts +38 -0
  55. package/src/tools/stellar-asset.ts +39 -0
  56. package/src/tools/stellar-pools.ts +45 -0
  57. package/src/tools/stocks.ts +43 -0
  58. package/src/tools/swap-quote.ts +43 -0
  59. package/src/tools/tools-list.ts +22 -0
  60. package/src/tools/wallet-tool.ts +51 -0
  61. package/src/tools/weather.ts +24 -0
  62. package/src/tools/youtube.ts +49 -0
  63. package/tsconfig.json +13 -0
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { loadOrCreateWallet } from "../lib/wallet.js";
3
+ import { ok, err } from "../lib/format.js";
4
+ import { logger } from "../lib/logger.js";
5
+ export function registerStellarPoolsTool(server) {
6
+ server.registerTool("stellar-pools", {
7
+ title: "Stellar Liquidity Pools",
8
+ description: `Browse Stellar liquidity pools. Optionally filter by asset.\n` +
9
+ `Shows reserves, shares, trustlines, and fees.\nFree.`, inputSchema: z.object({
10
+ asset: z
11
+ .string()
12
+ .optional()
13
+ .describe('Filter by asset (e.g. "XLM", "USDC"). Omit for all pools.'),
14
+ limit: z
15
+ .number()
16
+ .int()
17
+ .min(1)
18
+ .max(200)
19
+ .default(10)
20
+ .describe("Number of pools (1-200)"),
21
+ }),
22
+ }, async ({ asset, limit }) => {
23
+ logger.debug({ asset, limit }, "stellar-pools invoked");
24
+ try {
25
+ const config = loadOrCreateWallet();
26
+ const params = new URLSearchParams({ limit: String(limit) });
27
+ if (asset)
28
+ params.set("asset", asset);
29
+ const res = await fetch(`${config.apiUrl}/stellar-pools?${params}`);
30
+ if (!res.ok) {
31
+ const body = await res.text();
32
+ return err(`Pools error ${res.status}: ${body}`);
33
+ }
34
+ return ok(await res.json());
35
+ }
36
+ catch (e) {
37
+ logger.error({ err: e }, "stellar-pools error");
38
+ return err(`Pools failed: ${String(e)}`);
39
+ }
40
+ });
41
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { loadOrCreateWallet } from "../lib/wallet.js";
3
+ import { okPaid, err } from "../lib/format.js";
4
+ import { logger } from "../lib/logger.js";
5
+ import { TOOL_PRICES } from "../lib/config.js";
6
+ import { withBudget } from "../lib/budget.js";
7
+ import { withCache } from "../lib/cache.js";
8
+ export function registerStocksTool(server) {
9
+ server.registerTool("stocks", {
10
+ title: "Stock Quotes",
11
+ description: `Get real-time stock price and market data for any ticker symbol.\nCost: $${TOOL_PRICES.stocks} USDC per query (paid via Stellar MPP).`, inputSchema: z.object({
12
+ symbol: z
13
+ .string()
14
+ .describe("Stock ticker symbol (e.g. AAPL, TSLA, MSFT)"),
15
+ }),
16
+ }, async ({ symbol }) => {
17
+ logger.debug({ symbol }, "stocks tool invoked");
18
+ return withCache("stocks", { symbol }, () => withBudget("stocks", async () => {
19
+ try {
20
+ const config = loadOrCreateWallet();
21
+ const res = await fetch(`${config.apiUrl}/stocks?symbol=${encodeURIComponent(symbol)}`);
22
+ if (!res.ok) {
23
+ const body = await res.text();
24
+ return err(`Stocks API error ${res.status}: ${body}`);
25
+ }
26
+ return okPaid(await res.json());
27
+ }
28
+ catch (e) {
29
+ logger.error({ err: e }, "stocks tool error");
30
+ return err(`Stocks query failed: ${String(e)}`);
31
+ }
32
+ }));
33
+ });
34
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import { loadOrCreateWallet } from "../lib/wallet.js";
3
+ import { ok, err } from "../lib/format.js";
4
+ import { logger } from "../lib/logger.js";
5
+ export function registerSwapQuoteTool(server) {
6
+ server.registerTool("swap-quote", {
7
+ title: "Stellar Swap Quote",
8
+ description: `Find the best swap path between any two Stellar assets using the native DEX.\n` +
9
+ `Shows rate, path hops, and slippage tips.\n` +
10
+ `Examples: swap 100 XLM to USDC, swap 50 USDC to EURC.\n` +
11
+ `Well-known assets: XLM, USDC, EURC, AQUA, yUSDC, BLND, SHX.\nFree.`, inputSchema: z.object({
12
+ from: z.string().describe('Source asset (e.g. "XLM", "USDC")'),
13
+ to: z.string().describe('Destination asset (e.g. "USDC", "EURC")'),
14
+ amount: z.string().describe("Amount to swap"),
15
+ mode: z
16
+ .enum(["send", "receive"])
17
+ .default("send")
18
+ .describe('"send" = you fix what you send; "receive" = you fix what you get'),
19
+ }),
20
+ }, async ({ from, to, amount, mode }) => {
21
+ logger.debug({ from, to, amount, mode }, "swap-quote invoked");
22
+ try {
23
+ const config = loadOrCreateWallet();
24
+ const params = new URLSearchParams({ from, to, amount, mode });
25
+ const res = await fetch(`${config.apiUrl}/swap-quote?${params}`);
26
+ if (!res.ok) {
27
+ const body = await res.text();
28
+ return err(`Swap quote error ${res.status}: ${body}`);
29
+ }
30
+ return ok(await res.json());
31
+ }
32
+ catch (e) {
33
+ logger.error({ err: e }, "swap-quote error");
34
+ return err(`Swap quote failed: ${String(e)}`);
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { TOOL_PRICES, FREE_TOOLS } from "../lib/config.js";
3
+ import { ok } from "../lib/format.js";
4
+ export function registerToolsListTool(server) {
5
+ server.registerTool("tools", {
6
+ title: "List Tools",
7
+ description: "List all XLMTools tools with their per-call cost in USDC.", inputSchema: z.object({}),
8
+ }, async () => {
9
+ return ok({
10
+ paid: Object.entries(TOOL_PRICES).map(([tool, cost]) => ({ tool, cost_usdc: cost })),
11
+ free: Array.from(FREE_TOOLS),
12
+ network: "stellar:testnet",
13
+ payment: "MPP charge mode — USDC auto-deducted per call",
14
+ });
15
+ });
16
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ import { Horizon } from "@stellar/stellar-sdk";
3
+ import { loadOrCreateWallet } from "../lib/wallet.js";
4
+ import { ok, err } from "../lib/format.js";
5
+ import { logger } from "../lib/logger.js";
6
+ export function registerWalletTool(server) {
7
+ server.registerTool("wallet", {
8
+ title: "XLMTools Wallet",
9
+ description: "Show your Stellar wallet address, current USDC balance, and how to fund it. Free.", inputSchema: z.object({}),
10
+ }, async () => {
11
+ logger.debug("wallet tool invoked");
12
+ try {
13
+ const config = loadOrCreateWallet();
14
+ const horizon = new Horizon.Server("https://horizon-testnet.stellar.org");
15
+ const account = await horizon.loadAccount(config.stellarPublicKey);
16
+ const xlm = account.balances.find((b) => b.asset_type === "native");
17
+ const usdc = account.balances.find((b) => b.asset_type === "credit_alphanum4" &&
18
+ b.asset_code === "USDC");
19
+ return ok({
20
+ address: config.stellarPublicKey,
21
+ xlm_balance: xlm?.balance ?? "0",
22
+ usdc_balance: usdc?.balance ?? "0 (no USDC trustline yet)",
23
+ fund_instructions: [
24
+ "1. Get testnet XLM: https://lab.stellar.org/account/fund",
25
+ "2. Get testnet USDC: https://faucet.circle.com (select Stellar Testnet)",
26
+ `3. Send USDC to: ${config.stellarPublicKey}`,
27
+ ],
28
+ });
29
+ }
30
+ catch (e) {
31
+ logger.error({ err: e }, "wallet tool error");
32
+ return err(`Failed to load account: ${String(e)}`);
33
+ }
34
+ });
35
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { loadOrCreateWallet } from "../lib/wallet.js";
3
+ import { ok, err } from "../lib/format.js";
4
+ export function registerWeatherTool(server) {
5
+ server.registerTool("weather", {
6
+ title: "Weather",
7
+ description: "Current weather and conditions for any city. Free.", inputSchema: z.object({
8
+ location: z.string().describe("City name (e.g. Lagos, London, New York)"),
9
+ }),
10
+ }, async ({ location }) => {
11
+ try {
12
+ const config = loadOrCreateWallet();
13
+ const res = await fetch(`${config.apiUrl}/weather?location=${encodeURIComponent(location)}`);
14
+ if (!res.ok)
15
+ return err(`Weather API error: ${res.status}`);
16
+ return ok(await res.json());
17
+ }
18
+ catch (e) {
19
+ return err(String(e));
20
+ }
21
+ });
22
+ }
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ import { loadOrCreateWallet } from "../lib/wallet.js";
3
+ import { okPaid, err } from "../lib/format.js";
4
+ import { logger } from "../lib/logger.js";
5
+ import { TOOL_PRICES } from "../lib/config.js";
6
+ import { withBudget } from "../lib/budget.js";
7
+ import { withCache } from "../lib/cache.js";
8
+ export function registerYoutubeTool(server) {
9
+ server.registerTool("youtube", {
10
+ title: "YouTube Search / Lookup",
11
+ description: `Search YouTube videos or look up a specific video by ID. Provide either query or id.\nCost: $${TOOL_PRICES.youtube} USDC per call (paid via Stellar MPP).`, inputSchema: z.object({
12
+ query: z.string().optional().describe("Search query to find videos"),
13
+ id: z.string().optional().describe("YouTube video ID for direct lookup"),
14
+ }),
15
+ }, async ({ query, id }) => {
16
+ logger.debug({ query, id }, "youtube tool invoked");
17
+ if (!query && !id) {
18
+ return err("Provide either query or id");
19
+ }
20
+ const params = { query, id };
21
+ return withCache("youtube", params, () => withBudget("youtube", async () => {
22
+ try {
23
+ const config = loadOrCreateWallet();
24
+ const qs = new URLSearchParams();
25
+ if (query)
26
+ qs.set("q", query);
27
+ if (id)
28
+ qs.set("id", id);
29
+ const res = await fetch(`${config.apiUrl}/youtube?${qs.toString()}`);
30
+ if (!res.ok) {
31
+ const body = await res.text();
32
+ return err(`YouTube API error ${res.status}: ${body}`);
33
+ }
34
+ return okPaid(await res.json());
35
+ }
36
+ catch (e) {
37
+ logger.error({ err: e }, "youtube tool error");
38
+ return err(`YouTube lookup failed: ${String(e)}`);
39
+ }
40
+ }));
41
+ });
42
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@xlmtools/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "xlmtools": "dist/index.js",
7
+ "xlm": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx watch src/index.ts",
11
+ "build": "tsc",
12
+ "test": "vitest run"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.29.0",
16
+ "@stellar/mpp": "^0.4.0",
17
+ "@stellar/stellar-sdk": "^15.0.1",
18
+ "mppx": "^0.5.5",
19
+ "pino": "^10.3.1",
20
+ "zod": "^4.3.6"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^25.5.1",
24
+ "tsx": "^4.21.0",
25
+ "typescript": "^6.0.2",
26
+ "vitest": "^4.1.2"
27
+ }
28
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Mppx } from "mppx/client";
4
+ import { stellar } from "@stellar/mpp/charge/client";
5
+ import { Horizon } from "@stellar/stellar-sdk";
6
+ import { loadOrCreateWallet, getKeypair } from "./lib/wallet.js";
7
+ import { TOOL_PRICES } from "./lib/config.js";
8
+ import { logger } from "./lib/logger.js";
9
+
10
+ // ── Arg parsing ──────────────────────────────────────────
11
+
12
+ function parseArgs(argv: string[]) {
13
+ const [tool, ...rest] = argv;
14
+ const positional: string[] = [];
15
+ const flags: Record<string, string> = {};
16
+
17
+ for (let i = 0; i < rest.length; i++) {
18
+ if (rest[i].startsWith("--") && i + 1 < rest.length) {
19
+ const key = rest[i].slice(2).replace(/-/g, "_");
20
+ flags[key] = rest[i + 1];
21
+ i++;
22
+ } else {
23
+ positional.push(rest[i]);
24
+ }
25
+ }
26
+
27
+ return { tool: tool ?? "", positional, flags };
28
+ }
29
+
30
+ // ── URL builder ──────────────────────────────────────────
31
+
32
+ function buildRequest(
33
+ base: string,
34
+ tool: string,
35
+ pos: string[],
36
+ flags: Record<string, string>,
37
+ ): { url: string; init: RequestInit } {
38
+ const p = new URLSearchParams();
39
+
40
+ // Map each tool to its API endpoint + params
41
+ const toolMap: Record<string, () => { path: string; method?: string; body?: string }> = {
42
+ search: () => { p.set("q", pos[0] ?? ""); if (flags.count) p.set("count", flags.count); return { path: "/search" }; },
43
+ research: () => { p.set("q", pos[0] ?? ""); if (flags.num_results) p.set("num_results", flags.num_results); return { path: "/research" }; },
44
+ youtube: () => { if (flags.id) p.set("id", flags.id); else p.set("q", pos[0] ?? flags.query ?? ""); return { path: "/youtube" }; },
45
+ screenshot: () => { p.set("url", pos[0] ?? ""); if (flags.format) p.set("format", flags.format); return { path: "/screenshot" }; },
46
+ scrape: () => { p.set("url", pos[0] ?? ""); return { path: "/scrape" }; },
47
+ image: () => ({ path: "/image", method: "POST", body: JSON.stringify({ prompt: pos[0] ?? "", size: flags.size ?? "1024x1024" }) }),
48
+ stocks: () => { p.set("symbol", pos[0] ?? ""); return { path: "/stocks" }; },
49
+ crypto: () => { p.set("ids", pos[0] ?? ""); if (flags.vs_currency) p.set("vs_currency", flags.vs_currency); return { path: "/crypto" }; },
50
+ weather: () => { p.set("location", pos[0] ?? ""); return { path: "/weather" }; },
51
+ domain: () => { p.set("name", pos[0] ?? ""); return { path: "/domain" }; },
52
+ "dex-orderbook": () => { p.set("pair", pos[0] ?? ""); if (flags.limit) p.set("limit", flags.limit); return { path: "/dex-orderbook" }; },
53
+ "dex-candles": () => { p.set("pair", pos[0] ?? ""); if (flags.resolution) p.set("resolution", flags.resolution); if (flags.limit) p.set("limit", flags.limit); return { path: "/dex-candles" }; },
54
+ "dex-trades": () => { p.set("pair", pos[0] ?? ""); if (flags.limit) p.set("limit", flags.limit); if (flags.trade_type) p.set("trade_type", flags.trade_type); return { path: "/dex-trades" }; },
55
+ "swap-quote": () => { p.set("from", pos[0] ?? flags.from ?? ""); p.set("to", pos[1] ?? flags.to ?? ""); p.set("amount", pos[2] ?? flags.amount ?? ""); if (flags.mode) p.set("mode", flags.mode); return { path: "/swap-quote" }; },
56
+ "stellar-asset": () => { p.set("asset", pos[0] ?? ""); return { path: "/stellar-asset" }; },
57
+ "stellar-account": () => { p.set("address", pos[0] ?? ""); return { path: "/stellar-account" }; },
58
+ "stellar-pools": () => { if (pos[0] || flags.asset) p.set("asset", pos[0] ?? flags.asset ?? ""); if (flags.limit) p.set("limit", flags.limit); return { path: "/stellar-pools" }; },
59
+ "oracle-price": () => { p.set("asset", pos[0] ?? ""); if (flags.feed) p.set("feed", flags.feed); return { path: "/oracle-price" }; },
60
+ };
61
+
62
+ const builder = toolMap[tool];
63
+ if (!builder) {
64
+ process.stderr.write(`Unknown tool: ${tool}\nRun xlm --help for available tools.\n`);
65
+ process.exit(1);
66
+ }
67
+
68
+ const { path, method, body } = builder();
69
+ const qs = p.toString();
70
+ const url = qs ? `${base}${path}?${qs}` : `${base}${path}`;
71
+ const init: RequestInit = { method: method ?? "GET" };
72
+ if (body) {
73
+ init.headers = { "Content-Type": "application/json" };
74
+ init.body = body;
75
+ }
76
+
77
+ return { url, init };
78
+ }
79
+
80
+ // ── Local tool handlers ──────────────────────────────────
81
+
82
+ async function handleWallet(publicKey: string) {
83
+ const server = new Horizon.Server("https://horizon-testnet.stellar.org");
84
+ try {
85
+ const account = await server.loadAccount(publicKey);
86
+ const xlm = account.balances.find(
87
+ (b: Horizon.HorizonApi.BalanceLine) => b.asset_type === "native",
88
+ );
89
+ const usdc = account.balances.find(
90
+ (b: Horizon.HorizonApi.BalanceLine) =>
91
+ "asset_code" in b && b.asset_code === "USDC",
92
+ );
93
+ return {
94
+ address: publicKey,
95
+ network: "stellar:testnet",
96
+ xlm_balance: xlm?.balance ?? "0",
97
+ usdc_balance: usdc && "balance" in usdc ? usdc.balance : "0",
98
+ };
99
+ } catch {
100
+ return {
101
+ address: publicKey,
102
+ network: "stellar:testnet",
103
+ xlm_balance: "0",
104
+ usdc_balance: "0",
105
+ note: "Account not funded yet",
106
+ };
107
+ }
108
+ }
109
+
110
+ function handleTools() {
111
+ const paid = Object.entries(TOOL_PRICES).map(([name, price]) => ({
112
+ name,
113
+ price: `$${price}`,
114
+ }));
115
+ // Note: `budget` is MCP-only — the CLI is a fresh process per invocation
116
+ // so a session-scoped cap is meaningless. Excluded from the CLI's free list.
117
+ const free = [
118
+ "crypto", "weather", "domain", "wallet", "tools",
119
+ "dex-orderbook", "dex-candles", "dex-trades", "swap-quote",
120
+ "stellar-asset", "stellar-account", "stellar-pools", "oracle-price",
121
+ ];
122
+ return { paid, free, network: "stellar:testnet", payment: "MPP / USDC" };
123
+ }
124
+
125
+ // ── Help text ────────────────────────────────────────────
126
+
127
+ const HELP = `XLMTools CLI — Stellar-native tools with pay-per-call
128
+
129
+ Usage: xlm <tool> [args] [--flag value]
130
+
131
+ Paid tools ($0.001-$0.04 USDC via Stellar MPP):
132
+ search <query> [--count N] Web search
133
+ research <query> [--num-results N] Deep research
134
+ youtube <query> | --id <id> YouTube search/lookup
135
+ screenshot <url> [--format png] Capture URL screenshot
136
+ scrape <url> Extract text from URL
137
+ image <prompt> [--size 1024x1024] AI image generation
138
+ stocks <symbol> Stock quotes
139
+
140
+ Free tools:
141
+ crypto <ids> [--vs-currency usd] Crypto prices
142
+ weather <location> Current weather
143
+ domain <name> Domain availability
144
+ dex-orderbook <pair> [--limit N] Stellar DEX orderbook
145
+ dex-candles <pair> OHLCV candlesticks
146
+ dex-trades <pair> Recent DEX trades
147
+ swap-quote <from> <to> <amount> DEX swap pathfinding
148
+ stellar-asset <asset> Asset info
149
+ stellar-account <address> Account lookup
150
+ stellar-pools [--asset X] Liquidity pools
151
+ oracle-price <asset> [--feed crypto] Oracle prices
152
+ wallet Your Stellar wallet
153
+ tools List all tools
154
+
155
+ Examples:
156
+ xlm search "Stellar MPP micropayments"
157
+ xlm crypto bitcoin,ethereum,stellar
158
+ xlm weather Lagos
159
+ xlm stocks AAPL
160
+ xlm dex-orderbook XLM/USDC --limit 5
161
+ xlm wallet
162
+ `;
163
+
164
+ // ── Main ─────────────────────────────────────────────────
165
+
166
+ async function main() {
167
+ const args = process.argv.slice(2);
168
+
169
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
170
+ process.stdout.write(HELP);
171
+ return;
172
+ }
173
+
174
+ const { tool, positional, flags } = parseArgs(args);
175
+
176
+ // Init wallet + mppx (payment handling)
177
+ const config = loadOrCreateWallet();
178
+ const keypair = getKeypair(config);
179
+
180
+ Mppx.create({
181
+ methods: [
182
+ stellar.charge({
183
+ keypair,
184
+ mode: "pull",
185
+ onProgress(event) {
186
+ logger.debug({ eventType: event.type }, "MPP payment event");
187
+ },
188
+ }),
189
+ ],
190
+ });
191
+
192
+ // ── Local tools (no API call needed) ──
193
+
194
+ if (tool === "wallet") {
195
+ const data = await handleWallet(config.stellarPublicKey);
196
+ console.log(JSON.stringify(data, null, 2));
197
+ return;
198
+ }
199
+
200
+ if (tool === "tools") {
201
+ console.log(JSON.stringify(handleTools(), null, 2));
202
+ return;
203
+ }
204
+
205
+ // ── API tools ──
206
+
207
+ const isPaid = tool in TOOL_PRICES;
208
+ const { url, init } = buildRequest(config.apiUrl, tool, positional, flags);
209
+
210
+ if (isPaid) {
211
+ process.stderr.write(` Tool: ${tool} · Cost: $${TOOL_PRICES[tool]} USDC\n`);
212
+ }
213
+
214
+ const res = await fetch(url, init);
215
+
216
+ if (!res.ok) {
217
+ const text = await res.text();
218
+ process.stderr.write(`Error ${res.status}: ${text}\n`);
219
+ process.exit(1);
220
+ }
221
+
222
+ const data = (await res.json()) as Record<string, unknown>;
223
+
224
+ // Print receipt footer for paid tools
225
+ if (isPaid && data.receipt) {
226
+ const { receipt, ...rest } = data;
227
+ const r = receipt as { tx_hash: string; amount: string; currency: string; network: string };
228
+ console.log(JSON.stringify(rest, null, 2));
229
+ const hash = r.tx_hash.length > 16 ? r.tx_hash.slice(0, 16) + "..." : r.tx_hash;
230
+ const network = r.network === "stellar:testnet" ? "stellar testnet" : r.network;
231
+ process.stderr.write(`\n Payment: $${r.amount} ${r.currency} · tx/${hash} · ${network}\n`);
232
+ } else {
233
+ console.log(JSON.stringify(data, null, 2));
234
+ }
235
+ }
236
+
237
+ main().catch((e) => {
238
+ logger.error({ err: e }, "CLI fatal error");
239
+ process.exit(1);
240
+ });
package/src/index.ts ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { Mppx } from "mppx/client";
5
+ import { stellar } from "@stellar/mpp/charge/client";
6
+ import { loadOrCreateWallet, getKeypair } from "./lib/wallet.js";
7
+ import { logger } from "./lib/logger.js";
8
+ import { registerCryptoTool } from "./tools/crypto.js";
9
+ import { registerWeatherTool } from "./tools/weather.js";
10
+ import { registerDomainTool } from "./tools/domain.js";
11
+ import { registerToolsListTool } from "./tools/tools-list.js";
12
+ import { registerWalletTool } from "./tools/wallet-tool.js";
13
+ import { registerSearchTool } from "./tools/search.js";
14
+ import { registerResearchTool } from "./tools/research.js";
15
+
16
+ import { registerYoutubeTool } from "./tools/youtube.js";
17
+ import { registerScreenshotTool } from "./tools/screenshot.js";
18
+ import { registerScrapeTool } from "./tools/scrape.js";
19
+ import { registerImageTool } from "./tools/image.js";
20
+ import { registerStocksTool } from "./tools/stocks.js";
21
+ import { registerDexOrderbookTool } from "./tools/dex-orderbook.js";
22
+ import { registerDexCandlesTool } from "./tools/dex-candles.js";
23
+ import { registerDexTradesTool } from "./tools/dex-trades.js";
24
+ import { registerSwapQuoteTool } from "./tools/swap-quote.js";
25
+ import { registerStellarAssetTool } from "./tools/stellar-asset.js";
26
+ import { registerStellarAccountTool } from "./tools/stellar-account.js";
27
+ import { registerStellarPoolsTool } from "./tools/stellar-pools.js";
28
+ import { registerOraclePriceTool } from "./tools/oracle-price.js";
29
+ import { registerBudgetTool } from "./tools/budget.js";
30
+
31
+ const config = loadOrCreateWallet();
32
+ const keypair = getKeypair(config);
33
+
34
+ // Mppx polyfills global fetch to auto-handle 402 payments
35
+ Mppx.create({
36
+ methods: [
37
+ stellar.charge({
38
+ keypair,
39
+ mode: "pull",
40
+ onProgress(event) {
41
+ logger.debug({ eventType: event.type }, "MPP payment event");
42
+ },
43
+ }),
44
+ ],
45
+ });
46
+
47
+ // { logging: {} } enables ctx.mcpReq.log() in tool handlers
48
+ const server = new McpServer(
49
+ { name: "xlmtools", version: "0.1.0" },
50
+ { capabilities: { tools: {}, logging: {} } }
51
+ );
52
+
53
+ // Free tools
54
+ registerCryptoTool(server);
55
+ registerWeatherTool(server);
56
+ registerDomainTool(server);
57
+ registerToolsListTool(server);
58
+ registerWalletTool(server);
59
+ registerBudgetTool(server);
60
+
61
+ // Paid tools
62
+ registerSearchTool(server);
63
+ registerResearchTool(server);
64
+
65
+ registerYoutubeTool(server);
66
+ registerScreenshotTool(server);
67
+ registerScrapeTool(server);
68
+ registerImageTool(server);
69
+ registerStocksTool(server);
70
+
71
+ // Stellar-native tools (free)
72
+ registerDexOrderbookTool(server);
73
+ registerDexCandlesTool(server);
74
+ registerDexTradesTool(server);
75
+ registerSwapQuoteTool(server);
76
+ registerStellarAssetTool(server);
77
+ registerStellarAccountTool(server);
78
+ registerStellarPoolsTool(server);
79
+ registerOraclePriceTool(server);
80
+
81
+ async function main() {
82
+ const transport = new StdioServerTransport();
83
+ await server.connect(transport);
84
+ logger.info("XLMTools MCP server running");
85
+ }
86
+
87
+ main().catch((error) => {
88
+ logger.error({ err: error }, "Fatal error");
89
+ process.exit(1);
90
+ });
@@ -0,0 +1,78 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ import { err } from "./format.js";
3
+ import { TOOL_PRICES } from "./config.js";
4
+ import { logger } from "./logger.js";
5
+
6
+ // Session-scoped budget state (resets on MCP server restart)
7
+ let maxBudget: number | null = null;
8
+ let totalSpent = 0;
9
+
10
+ export function setBudget(max: number): void {
11
+ maxBudget = max;
12
+ // Reset spent counter so the new cap starts fresh. Users expect
13
+ // "I just set a $1.00 budget" to mean they have $1.00 to spend
14
+ // from now — not "$1.00 minus whatever I spent before setting it".
15
+ totalSpent = 0;
16
+ logger.info({ max }, "session budget set");
17
+ }
18
+
19
+ export function clearBudget(): void {
20
+ maxBudget = null;
21
+ totalSpent = 0;
22
+ logger.info("session budget cleared");
23
+ }
24
+
25
+ export function getStatus(): {
26
+ max: number | null;
27
+ spent: number;
28
+ remaining: number | null;
29
+ } {
30
+ return {
31
+ max: maxBudget,
32
+ spent: Math.round(totalSpent * 1000) / 1000,
33
+ remaining:
34
+ maxBudget !== null
35
+ ? Math.round((maxBudget - totalSpent) * 1000) / 1000
36
+ : null,
37
+ };
38
+ }
39
+
40
+ function canSpend(amount: number): boolean {
41
+ if (maxBudget === null) return true;
42
+ // Round to 3 decimal places to avoid floating-point drift
43
+ const spent = Math.round(totalSpent * 1000) / 1000;
44
+ return spent + amount <= maxBudget;
45
+ }
46
+
47
+ function recordSpend(amount: number): void {
48
+ totalSpent += amount;
49
+ logger.debug({ amount, totalSpent }, "spend recorded");
50
+ }
51
+
52
+ /**
53
+ * Wrap a paid tool call with budget checking.
54
+ * If budget would be exceeded, returns an error without calling the API.
55
+ * On success, records the spend.
56
+ */
57
+ export async function withBudget(
58
+ toolName: string,
59
+ fn: () => Promise<CallToolResult>,
60
+ ): Promise<CallToolResult> {
61
+ const price = parseFloat(TOOL_PRICES[toolName] ?? "0");
62
+ if (!canSpend(price)) {
63
+ const status = getStatus();
64
+ return err(
65
+ `Budget limit reached. This call costs $${TOOL_PRICES[toolName]} ` +
66
+ `but only $${status.remaining?.toFixed(3)} remains. ` +
67
+ `Use the budget tool to check or adjust your limit.`,
68
+ );
69
+ }
70
+
71
+ const result = await fn();
72
+
73
+ if (!result.isError) {
74
+ recordSpend(price);
75
+ }
76
+
77
+ return result;
78
+ }