stelagent 0.0.1 → 0.0.3
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/README.md +63 -43
- package/dist/index.mjs +596 -227
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,87 +1,105 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Stelagent CLI
|
|
2
2
|
|
|
3
|
-
Modular, agent-first CLI for Stellar — wallet, payments, markets, and
|
|
3
|
+
Modular, agent-first CLI for Stellar — wallet, payments, markets, and monitoring.
|
|
4
4
|
|
|
5
|
-
## Quick
|
|
5
|
+
## Quick start for AI agents
|
|
6
|
+
|
|
7
|
+
Paste this prompt into any AI agent (Claude Code, Cursor, OpenCode, OpenClaw, Hermes Agent, etc.) and chat naturally:
|
|
6
8
|
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
+
```
|
|
10
|
+
Read https://stelagent.noval.me/AGENTS.md, then set it up for me.
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
The agent reads the skill definition, handles wallet creation, OTP verification, and every Stellar operation on your behalf.
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
### Install the skill
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
|
|
18
|
+
# Via flins (recommended)
|
|
19
|
+
npx flins@latest add stelagent.noval.me
|
|
20
|
+
|
|
21
|
+
# Via skills.sh
|
|
22
|
+
npx skills add stelagent.noval.me/stelagent-cli
|
|
17
23
|
```
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
Both commands download the skill into your project's skills directory and wire it into your agent's configuration automatically.
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
## Install
|
|
22
28
|
|
|
23
29
|
```bash
|
|
24
|
-
stelagent
|
|
25
|
-
|
|
30
|
+
npx stelagent@latest <command>
|
|
31
|
+
# or
|
|
32
|
+
bunx stelagent@latest <command>
|
|
26
33
|
```
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
| --------------- | ------------------------------------------ |
|
|
30
|
-
| `-e, --email` | Your email address (required) |
|
|
31
|
-
| `-n, --network` | `testnet` or `pubnet` (default: `testnet`) |
|
|
35
|
+
No install needed — `npx` always runs the latest published version.
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
## Commands
|
|
34
38
|
|
|
35
|
-
###
|
|
36
|
-
|
|
37
|
-
Show the wallet public key.
|
|
39
|
+
### Wallet
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
|
-
stelagent wallet
|
|
42
|
+
npx stelagent@latest wallet login -e you@example.com # Request OTP
|
|
43
|
+
npx stelagent@latest wallet verify -e you@example.com -o 123456 # Verify OTP
|
|
44
|
+
npx stelagent@latest wallet address # Show public key
|
|
45
|
+
npx stelagent@latest wallet balance # Check balances
|
|
46
|
+
npx stelagent@latest wallet transfer -t GDXXX... -a 10 # Send XLM
|
|
47
|
+
npx stelagent@latest wallet logout # Clear session
|
|
41
48
|
```
|
|
42
49
|
|
|
43
|
-
###
|
|
44
|
-
|
|
45
|
-
Show token balances.
|
|
50
|
+
### Payments
|
|
46
51
|
|
|
47
52
|
```bash
|
|
48
|
-
stelagent
|
|
53
|
+
npx stelagent@latest send GDXXX... 100 -a USDC:GAXYZ... # Custom asset
|
|
54
|
+
npx stelagent@latest send GDXXX... 50 -a native --memo text:ref-99 # XLM with memo
|
|
55
|
+
npx stelagent@latest pay https://api.example.com/premium # x402 payment
|
|
49
56
|
```
|
|
50
57
|
|
|
51
|
-
###
|
|
52
|
-
|
|
53
|
-
Send XLM to another Stellar address.
|
|
58
|
+
### Account queries
|
|
54
59
|
|
|
55
60
|
```bash
|
|
56
|
-
stelagent
|
|
61
|
+
npx stelagent@latest account details GDXXX... # Balances, signers, thresholds
|
|
62
|
+
npx stelagent@latest account transactions GDXXX... # Transaction history
|
|
63
|
+
npx stelagent@latest account payments GDXXX... # Payment history
|
|
64
|
+
npx stelagent@latest account effects GDXXX... # Effect history
|
|
57
65
|
```
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
| -------------- | --------------------------------- |
|
|
61
|
-
| `-t, --to` | Destination public key (required) |
|
|
62
|
-
| `-a, --amount` | Amount in XLM (required) |
|
|
67
|
+
### Assets & fees
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
```bash
|
|
70
|
+
npx stelagent@latest assets search --code USDC # Search assets
|
|
71
|
+
npx stelagent@latest assets orderbook --selling XLM --buying USDC:G... # Order book
|
|
72
|
+
npx stelagent@latest fee # Fee stats
|
|
73
|
+
```
|
|
65
74
|
|
|
66
|
-
|
|
75
|
+
### Streaming
|
|
67
76
|
|
|
68
77
|
```bash
|
|
69
|
-
stelagent
|
|
78
|
+
npx stelagent@latest monitor transactions GDXXX... # Stream transactions
|
|
79
|
+
npx stelagent@latest monitor payments GDXXX... # Stream payments
|
|
80
|
+
npx stelagent@latest monitor effects GDXXX... # Stream effects
|
|
70
81
|
```
|
|
71
82
|
|
|
72
|
-
###
|
|
73
|
-
|
|
74
|
-
Make an x402 payment to access a paywalled resource.
|
|
83
|
+
### MCP server
|
|
75
84
|
|
|
76
85
|
```bash
|
|
77
|
-
stelagent
|
|
86
|
+
npx stelagent@latest mcp # Start MCP server on stdio
|
|
78
87
|
```
|
|
79
88
|
|
|
80
|
-
|
|
89
|
+
All commands accept `-n testnet|pubnet` (default: `testnet`) and `-f json|text`. Account and asset queries support `--limit`, `--cursor`, and `--order asc|desc`.
|
|
90
|
+
|
|
91
|
+
## Authentication
|
|
92
|
+
|
|
93
|
+
Two-step OTP flow:
|
|
94
|
+
|
|
95
|
+
1. **`wallet login -e <email>`** — sends a one-time code to your email
|
|
96
|
+
2. **`wallet verify -e <email> -o <code>`** — verifies the code and creates (or recovers) your wallet
|
|
97
|
+
|
|
98
|
+
One email maps to one wallet, recoverable from any device.
|
|
81
99
|
|
|
82
100
|
## Output
|
|
83
101
|
|
|
84
|
-
All commands
|
|
102
|
+
All commands return structured JSON:
|
|
85
103
|
|
|
86
104
|
```json
|
|
87
105
|
{ "ok": true, "data": { ... } }
|
|
@@ -91,9 +109,11 @@ All commands output structured JSON:
|
|
|
91
109
|
{ "ok": false, "error": "..." }
|
|
92
110
|
```
|
|
93
111
|
|
|
112
|
+
Use `--format text` for human-readable output.
|
|
113
|
+
|
|
94
114
|
## Architecture
|
|
95
115
|
|
|
96
|
-
Wallets are stored server-side — one wallet per email, recoverable from any device. The CLI only holds a session token locally (`~/.stelagent/session.json`). Secret keys are fetched
|
|
116
|
+
Wallets are stored server-side — one wallet per email, recoverable from any device. The CLI only holds a session token locally (`~/.stelagent/session.json`). Secret keys are fetched over HTTPS when needed and never written to disk.
|
|
97
117
|
|
|
98
118
|
## Development
|
|
99
119
|
|
package/dist/index.mjs
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { defineCommand, runMain } from "citty";
|
|
4
4
|
import { Result, TaggedError, isTaggedError, matchError, matchErrorPartial } from "better-result";
|
|
5
|
-
import { cancel, confirm, isCancel, log, text } from "@clack/prompts";
|
|
6
|
-
import pc from "picocolors";
|
|
7
5
|
import { z } from "zod";
|
|
8
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
7
|
import { join } from "node:path";
|
|
10
8
|
import { homedir } from "node:os";
|
|
9
|
+
import pc from "picocolors";
|
|
11
10
|
import { Horizon } from "@stellar/stellar-sdk";
|
|
11
|
+
import { cancel, confirm, isCancel } from "@clack/prompts";
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
14
|
//#region src/lib/audit.ts
|
|
13
15
|
const STELAGENT_DIR$1 = join(homedir(), ".stelagent");
|
|
14
16
|
const AUDIT_FILE = join(STELAGENT_DIR$1, "audit.jsonl");
|
|
@@ -68,16 +70,6 @@ function printResult(result, format) {
|
|
|
68
70
|
if (Result.isOk(result)) printText(result.value);
|
|
69
71
|
else console.error(pc.red(`Error: ${result.error}`));
|
|
70
72
|
}
|
|
71
|
-
function printData(data, format) {
|
|
72
|
-
if (format === "json") {
|
|
73
|
-
console.log(JSON.stringify({
|
|
74
|
-
ok: true,
|
|
75
|
-
data
|
|
76
|
-
}, null, 2));
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
printText(data);
|
|
80
|
-
}
|
|
81
73
|
function formatSessionError(err) {
|
|
82
74
|
return matchErrorPartial(err, {
|
|
83
75
|
SessionNotFoundError: () => "No active session.",
|
|
@@ -293,6 +285,89 @@ function verifyOtp(email, otp) {
|
|
|
293
285
|
});
|
|
294
286
|
}
|
|
295
287
|
//#endregion
|
|
288
|
+
//#region src/lib/args.ts
|
|
289
|
+
const networkArg = {
|
|
290
|
+
type: "string",
|
|
291
|
+
alias: ["n"],
|
|
292
|
+
description: "Network: testnet or pubnet",
|
|
293
|
+
default: "testnet"
|
|
294
|
+
};
|
|
295
|
+
const formatArg = {
|
|
296
|
+
type: "string",
|
|
297
|
+
alias: ["f"],
|
|
298
|
+
description: "Output format: json or text",
|
|
299
|
+
default: "json"
|
|
300
|
+
};
|
|
301
|
+
function parseNetwork$4(value) {
|
|
302
|
+
if (value === "testnet" || value === "pubnet") return Result.ok(value);
|
|
303
|
+
return Result.err(new InvalidNetworkError({ provided: value }));
|
|
304
|
+
}
|
|
305
|
+
function parseFormat(value) {
|
|
306
|
+
if (value === "json" || value === "text") return value;
|
|
307
|
+
return "json";
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/domain/validators.ts
|
|
311
|
+
const stellarPublicKey = z.string().regex(/^G[A-Z2-7]{55}$/, "Invalid Stellar public key. Must start with 'G' followed by 55 alphanumeric characters.");
|
|
312
|
+
const emailSchema = z.string().email("Invalid email address.");
|
|
313
|
+
const amountSchema = z.string().regex(/^\d+(\.\d{1,7})?$/, "Invalid amount. Must be a number with up to 7 decimal places.");
|
|
314
|
+
const assetSchema = z.union([z.literal("native"), z.string().regex(/^[A-Za-z0-9]{1,12}:[A-Z2-7]{56}$/, "Invalid asset format. Use 'native' for XLM or 'CODE:ISSUER' for custom assets.")]);
|
|
315
|
+
const memoSchema = z.string().max(28, "Memo text must be 28 characters or fewer.");
|
|
316
|
+
z.string().min(1, "Cursor must be a non-empty string.");
|
|
317
|
+
const otpSchema = z.string().regex(/^\d{4,8}$/, "OTP must be 4-8 digits.");
|
|
318
|
+
const urlSchema = z.string().url("Invalid URL.");
|
|
319
|
+
z.enum(["testnet", "pubnet"]);
|
|
320
|
+
z.enum(["json", "text"]);
|
|
321
|
+
const limitSchema = z.coerce.number().int().min(1).max(200).default(10);
|
|
322
|
+
z.enum(["asc", "desc"]).default("desc");
|
|
323
|
+
z.coerce.number().int().min(6e4);
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/commands/wallet-login.ts
|
|
326
|
+
function formatZodError$11(e) {
|
|
327
|
+
return e.issues.map((issue) => issue.message).join(", ");
|
|
328
|
+
}
|
|
329
|
+
const walletLogin = defineCommand({
|
|
330
|
+
meta: {
|
|
331
|
+
name: "login",
|
|
332
|
+
description: "Request an OTP code be sent to your email"
|
|
333
|
+
},
|
|
334
|
+
args: {
|
|
335
|
+
email: {
|
|
336
|
+
type: "string",
|
|
337
|
+
alias: ["e"],
|
|
338
|
+
description: "Your email address",
|
|
339
|
+
required: true
|
|
340
|
+
},
|
|
341
|
+
network: networkArg,
|
|
342
|
+
format: formatArg
|
|
343
|
+
},
|
|
344
|
+
async run({ args }) {
|
|
345
|
+
const email = String(args.email ?? "");
|
|
346
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
347
|
+
const format = parseFormat(String(args.format ?? "json"));
|
|
348
|
+
if (Result.isError(networkResult)) {
|
|
349
|
+
printResult(Result.err(networkResult.error._tag), "json");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const validation = Result.try({
|
|
353
|
+
try: () => emailSchema.parse(email),
|
|
354
|
+
catch: (e) => e
|
|
355
|
+
});
|
|
356
|
+
if (Result.isError(validation)) {
|
|
357
|
+
const msg = validation.error instanceof z.ZodError ? formatZodError$11(validation.error) : validation.error instanceof Error ? validation.error.message : String(validation.error);
|
|
358
|
+
printResult(Result.err(msg), format);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
await runApp("wallet login", async () => {
|
|
362
|
+
const otpResponseResult = await requestOtp(email);
|
|
363
|
+
if (Result.isError(otpResponseResult)) throw new Error(`Could not reach auth server: ${otpResponseResult.error.cause}`);
|
|
364
|
+
const otpResponse = otpResponseResult.value;
|
|
365
|
+
if (!otpResponse || !otpResponse.ok) throw new Error("Failed to send OTP email");
|
|
366
|
+
return { message: otpResponse.message };
|
|
367
|
+
}, format);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
//#endregion
|
|
296
371
|
//#region src/services/session.ts
|
|
297
372
|
const STELAGENT_DIR = join(homedir(), ".stelagent");
|
|
298
373
|
const SESSION_FILE = join(STELAGENT_DIR, "session.json");
|
|
@@ -390,132 +465,6 @@ async function createWallet(network) {
|
|
|
390
465
|
});
|
|
391
466
|
}
|
|
392
467
|
//#endregion
|
|
393
|
-
//#region src/lib/args.ts
|
|
394
|
-
const networkArg = {
|
|
395
|
-
type: "string",
|
|
396
|
-
alias: ["n"],
|
|
397
|
-
description: "Network: testnet or pubnet",
|
|
398
|
-
default: "testnet"
|
|
399
|
-
};
|
|
400
|
-
const formatArg = {
|
|
401
|
-
type: "string",
|
|
402
|
-
alias: ["f"],
|
|
403
|
-
description: "Output format: json or text",
|
|
404
|
-
default: "json"
|
|
405
|
-
};
|
|
406
|
-
function parseNetwork(value) {
|
|
407
|
-
if (value === "testnet" || value === "pubnet") return Result.ok(value);
|
|
408
|
-
return Result.err(new InvalidNetworkError({ provided: value }));
|
|
409
|
-
}
|
|
410
|
-
function parseFormat(value) {
|
|
411
|
-
if (value === "json" || value === "text") return value;
|
|
412
|
-
return "json";
|
|
413
|
-
}
|
|
414
|
-
//#endregion
|
|
415
|
-
//#region src/domain/validators.ts
|
|
416
|
-
const stellarPublicKey = z.string().regex(/^G[A-Z2-7]{55}$/, "Invalid Stellar public key. Must start with 'G' followed by 55 alphanumeric characters.");
|
|
417
|
-
const emailSchema = z.string().email("Invalid email address.");
|
|
418
|
-
const amountSchema = z.string().regex(/^\d+(\.\d{1,7})?$/, "Invalid amount. Must be a number with up to 7 decimal places.");
|
|
419
|
-
const assetSchema = z.union([z.literal("native"), z.string().regex(/^[A-Za-z0-9]{1,12}:[A-Z2-7]{56}$/, "Invalid asset format. Use 'native' for XLM or 'CODE:ISSUER' for custom assets.")]);
|
|
420
|
-
const memoSchema = z.string().max(28, "Memo text must be 28 characters or fewer.");
|
|
421
|
-
z.string().min(1, "Cursor must be a non-empty string.");
|
|
422
|
-
z.string().regex(/^\d{4,8}$/, "OTP must be 4-8 digits.");
|
|
423
|
-
const urlSchema = z.string().url("Invalid URL.");
|
|
424
|
-
z.enum(["testnet", "pubnet"]);
|
|
425
|
-
z.enum(["json", "text"]);
|
|
426
|
-
const limitSchema = z.coerce.number().int().min(1).max(200).default(10);
|
|
427
|
-
z.enum(["asc", "desc"]).default("desc");
|
|
428
|
-
z.coerce.number().int().min(6e4);
|
|
429
|
-
//#endregion
|
|
430
|
-
//#region src/commands/wallet-login.ts
|
|
431
|
-
function formatZodError$11(e) {
|
|
432
|
-
return e.issues.map((issue) => issue.message).join(", ");
|
|
433
|
-
}
|
|
434
|
-
const walletLogin = defineCommand({
|
|
435
|
-
meta: {
|
|
436
|
-
name: "login",
|
|
437
|
-
description: "Sign in with email to create or recover your wallet"
|
|
438
|
-
},
|
|
439
|
-
args: {
|
|
440
|
-
email: {
|
|
441
|
-
type: "string",
|
|
442
|
-
alias: ["e"],
|
|
443
|
-
description: "Your email address",
|
|
444
|
-
required: true
|
|
445
|
-
},
|
|
446
|
-
network: networkArg,
|
|
447
|
-
format: formatArg
|
|
448
|
-
},
|
|
449
|
-
async run({ args }) {
|
|
450
|
-
const email = String(args.email ?? "");
|
|
451
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
452
|
-
const format = parseFormat(String(args.format ?? "json"));
|
|
453
|
-
if (Result.isError(networkResult)) {
|
|
454
|
-
printResult(Result.err(networkResult.error._tag), "json");
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
const network = networkResult.value;
|
|
458
|
-
const validation = Result.try({
|
|
459
|
-
try: () => emailSchema.parse(email),
|
|
460
|
-
catch: (e) => e
|
|
461
|
-
});
|
|
462
|
-
if (Result.isError(validation)) {
|
|
463
|
-
const msg = validation.error instanceof z.ZodError ? formatZodError$11(validation.error) : validation.error instanceof Error ? validation.error.message : String(validation.error);
|
|
464
|
-
printResult(Result.err(msg), format);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
log.message(pc.cyan(`Sending OTP to ${email}...`));
|
|
468
|
-
const otpResponseResult = await requestOtp(email);
|
|
469
|
-
if (Result.isError(otpResponseResult)) {
|
|
470
|
-
const msg = `AuthRequestError: ${otpResponseResult.error.cause}`;
|
|
471
|
-
printResult(Result.err(msg), format);
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
const otpResponse = otpResponseResult.value;
|
|
475
|
-
if (!otpResponse || !otpResponse.ok) return;
|
|
476
|
-
log.success(otpResponse.message);
|
|
477
|
-
const otpInput = await text({
|
|
478
|
-
message: `Enter the OTP sent to ${email}`,
|
|
479
|
-
placeholder: "123456",
|
|
480
|
-
validate: (value) => {
|
|
481
|
-
if (!value || value.trim().length === 0) return "OTP is required";
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
if (isCancel(otpInput)) {
|
|
485
|
-
cancel("Login cancelled.");
|
|
486
|
-
process.exit(0);
|
|
487
|
-
}
|
|
488
|
-
const otp = String(otpInput);
|
|
489
|
-
await runApp("wallet login verify", async () => {
|
|
490
|
-
const verifyResult = await verifyOtp(email, otp);
|
|
491
|
-
if (Result.isError(verifyResult)) {
|
|
492
|
-
printResult(Result.err(`OTP verification failed: ${verifyResult.error.cause}`), format);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
const verifyResponse = verifyResult.value;
|
|
496
|
-
if (!verifyResponse.verified) {
|
|
497
|
-
printResult(Result.err("OTP verification failed."), format);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
const sessionResult = saveSession(verifyResponse.token, verifyResponse.email);
|
|
501
|
-
if (Result.isError(sessionResult)) {
|
|
502
|
-
printResult(Result.err(formatSessionError(sessionResult.error)), format);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
log.message(pc.cyan("Setting up your wallet..."));
|
|
506
|
-
const walletResult = await createWallet(network);
|
|
507
|
-
if (Result.isError(walletResult)) {
|
|
508
|
-
printResult(Result.err(formatWalletError(walletResult.error)), format);
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
const wallet = walletResult.value;
|
|
512
|
-
log.success(pc.green(wallet.publicKey ? `Wallet ready: ${wallet.publicKey.slice(0, 8)}...` : "Wallet recovered!"));
|
|
513
|
-
printData({ wallet }, format);
|
|
514
|
-
return wallet;
|
|
515
|
-
}, format);
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
//#endregion
|
|
519
468
|
//#region src/commands/wallet-verify.ts
|
|
520
469
|
const walletVerify = defineCommand({
|
|
521
470
|
meta: {
|
|
@@ -541,7 +490,7 @@ const walletVerify = defineCommand({
|
|
|
541
490
|
async run({ args }) {
|
|
542
491
|
const email = String(args.email ?? "");
|
|
543
492
|
const otp = String(args.otp ?? "");
|
|
544
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
493
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
545
494
|
const format = parseFormat(String(args.format ?? "json"));
|
|
546
495
|
if (Result.isError(networkResult)) {
|
|
547
496
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1182,7 +1131,7 @@ const accountDetails = defineCommand({
|
|
|
1182
1131
|
format: formatArg
|
|
1183
1132
|
},
|
|
1184
1133
|
async run({ args }) {
|
|
1185
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1134
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1186
1135
|
const format = parseFormat(String(args.format ?? "json"));
|
|
1187
1136
|
if (Result.isError(networkResult)) {
|
|
1188
1137
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1243,7 +1192,7 @@ const accountTransactions = defineCommand({
|
|
|
1243
1192
|
}
|
|
1244
1193
|
},
|
|
1245
1194
|
async run({ args }) {
|
|
1246
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1195
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1247
1196
|
const format = parseFormat(String(args.format ?? "json"));
|
|
1248
1197
|
if (Result.isError(networkResult)) {
|
|
1249
1198
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1312,7 +1261,7 @@ const accountPayments = defineCommand({
|
|
|
1312
1261
|
}
|
|
1313
1262
|
},
|
|
1314
1263
|
async run({ args }) {
|
|
1315
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1264
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1316
1265
|
const format = parseFormat(String(args.format ?? "json"));
|
|
1317
1266
|
if (Result.isError(networkResult)) {
|
|
1318
1267
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1386,7 +1335,7 @@ const accountCommand = defineCommand({
|
|
|
1386
1335
|
}
|
|
1387
1336
|
},
|
|
1388
1337
|
async run({ args }) {
|
|
1389
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1338
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1390
1339
|
const format = parseFormat(String(args.format ?? "json"));
|
|
1391
1340
|
if (Result.isError(networkResult)) {
|
|
1392
1341
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1458,7 +1407,7 @@ const assetsCommand = defineCommand({
|
|
|
1458
1407
|
}
|
|
1459
1408
|
},
|
|
1460
1409
|
async run({ args }) {
|
|
1461
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1410
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1462
1411
|
const format = parseFormat(String(args.format ?? "json"));
|
|
1463
1412
|
if (Result.isError(networkResult)) {
|
|
1464
1413
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1516,7 +1465,7 @@ const assetsCommand = defineCommand({
|
|
|
1516
1465
|
}
|
|
1517
1466
|
},
|
|
1518
1467
|
async run({ args }) {
|
|
1519
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1468
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1520
1469
|
const format = parseFormat(String(args.format ?? "json"));
|
|
1521
1470
|
if (Result.isError(networkResult)) {
|
|
1522
1471
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1591,7 +1540,7 @@ const sendCommand = defineCommand({
|
|
|
1591
1540
|
format: formatArg
|
|
1592
1541
|
},
|
|
1593
1542
|
async run({ args }) {
|
|
1594
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1543
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1595
1544
|
const format = parseFormat(String(args.format ?? "json"));
|
|
1596
1545
|
if (Result.isError(networkResult)) {
|
|
1597
1546
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1644,7 +1593,7 @@ const feeCommand = defineCommand({
|
|
|
1644
1593
|
format: formatArg
|
|
1645
1594
|
},
|
|
1646
1595
|
async run({ args }) {
|
|
1647
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1596
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1648
1597
|
const format = parseFormat(String(args.format ?? "json"));
|
|
1649
1598
|
if (Result.isError(networkResult)) {
|
|
1650
1599
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1684,7 +1633,7 @@ const monitorTransactions = defineCommand({
|
|
|
1684
1633
|
}
|
|
1685
1634
|
},
|
|
1686
1635
|
async run({ args }) {
|
|
1687
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1636
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1688
1637
|
const format = parseFormat(String(args.format ?? "json"));
|
|
1689
1638
|
if (Result.isError(networkResult)) {
|
|
1690
1639
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1761,7 +1710,7 @@ const monitorPayments = defineCommand({
|
|
|
1761
1710
|
}
|
|
1762
1711
|
},
|
|
1763
1712
|
async run({ args }) {
|
|
1764
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1713
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1765
1714
|
const format = parseFormat(String(args.format ?? "json"));
|
|
1766
1715
|
if (Result.isError(networkResult)) {
|
|
1767
1716
|
printResult(Result.err(networkResult.error._tag), "json");
|
|
@@ -1818,6 +1767,491 @@ function formatZodError(e) {
|
|
|
1818
1767
|
return e.issues.map((issue) => issue.message).join(", ");
|
|
1819
1768
|
}
|
|
1820
1769
|
//#endregion
|
|
1770
|
+
//#region src/commands/monitor.ts
|
|
1771
|
+
const monitorCommand = defineCommand({
|
|
1772
|
+
meta: {
|
|
1773
|
+
name: "monitor",
|
|
1774
|
+
description: "Stream live data from Horizon via SSE"
|
|
1775
|
+
},
|
|
1776
|
+
subCommands: {
|
|
1777
|
+
transactions: monitorTransactions,
|
|
1778
|
+
payments: monitorPayments,
|
|
1779
|
+
effects: defineCommand({
|
|
1780
|
+
meta: {
|
|
1781
|
+
name: "effects",
|
|
1782
|
+
description: "Stream effects for an account in real-time"
|
|
1783
|
+
},
|
|
1784
|
+
args: {
|
|
1785
|
+
address: {
|
|
1786
|
+
type: "positional",
|
|
1787
|
+
description: "Stellar public key (G...)",
|
|
1788
|
+
required: true
|
|
1789
|
+
},
|
|
1790
|
+
network: networkArg,
|
|
1791
|
+
format: formatArg,
|
|
1792
|
+
cursor: {
|
|
1793
|
+
type: "string",
|
|
1794
|
+
alias: ["c"],
|
|
1795
|
+
description: "Start from this cursor (default: 'now' for new events)",
|
|
1796
|
+
default: "now"
|
|
1797
|
+
}
|
|
1798
|
+
},
|
|
1799
|
+
async run({ args }) {
|
|
1800
|
+
const networkResult = parseNetwork$4(String(args.network ?? "testnet"));
|
|
1801
|
+
const format = parseFormat(String(args.format ?? "json"));
|
|
1802
|
+
if (Result.isError(networkResult)) {
|
|
1803
|
+
printResult(Result.err(networkResult.error._tag), "json");
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const network = networkResult.value;
|
|
1807
|
+
const address = String(args.address ?? "");
|
|
1808
|
+
const validation = Result.try({
|
|
1809
|
+
try: () => stellarPublicKey.parse(address),
|
|
1810
|
+
catch: (e) => e
|
|
1811
|
+
});
|
|
1812
|
+
if (Result.isError(validation)) {
|
|
1813
|
+
const msg = validation.error instanceof z.ZodError ? formatZodError(validation.error) : validation.error instanceof Error ? validation.error.message : String(validation.error);
|
|
1814
|
+
printResult(Result.err(msg), format);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const cursor = args.cursor === "now" ? void 0 : String(args.cursor);
|
|
1818
|
+
let close;
|
|
1819
|
+
try {
|
|
1820
|
+
close = await runApp("monitor effects", async () => {
|
|
1821
|
+
return streamEffects(address, network, { cursor }, (record) => {
|
|
1822
|
+
console.log(JSON.stringify({
|
|
1823
|
+
ok: true,
|
|
1824
|
+
data: record
|
|
1825
|
+
}));
|
|
1826
|
+
}, (error) => {
|
|
1827
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1828
|
+
console.log(JSON.stringify({
|
|
1829
|
+
ok: false,
|
|
1830
|
+
error: `Stream error: ${message}`
|
|
1831
|
+
}));
|
|
1832
|
+
});
|
|
1833
|
+
}, format);
|
|
1834
|
+
} catch (e) {
|
|
1835
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1836
|
+
printResult(Result.err(`Failed to stream effects: ${message}`), "json");
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
console.log(JSON.stringify({
|
|
1840
|
+
ok: true,
|
|
1841
|
+
data: { message: `Streaming effects for ${address} on ${network}... Press Ctrl+C to stop.` }
|
|
1842
|
+
}, null, 2));
|
|
1843
|
+
await new Promise((resolve) => {
|
|
1844
|
+
process.on("SIGINT", () => {
|
|
1845
|
+
close();
|
|
1846
|
+
resolve();
|
|
1847
|
+
});
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
})
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
//#endregion
|
|
1854
|
+
//#region src/mcp/wallet-tools.ts
|
|
1855
|
+
function ok$3(data) {
|
|
1856
|
+
return { content: [{
|
|
1857
|
+
type: "text",
|
|
1858
|
+
text: JSON.stringify({
|
|
1859
|
+
ok: true,
|
|
1860
|
+
data
|
|
1861
|
+
}, null, 2)
|
|
1862
|
+
}] };
|
|
1863
|
+
}
|
|
1864
|
+
function err$3(message) {
|
|
1865
|
+
return { content: [{
|
|
1866
|
+
type: "text",
|
|
1867
|
+
text: JSON.stringify({
|
|
1868
|
+
ok: false,
|
|
1869
|
+
error: message
|
|
1870
|
+
}, null, 2)
|
|
1871
|
+
}] };
|
|
1872
|
+
}
|
|
1873
|
+
function parseNetwork$3(value) {
|
|
1874
|
+
return value === "pubnet" ? "pubnet" : "testnet";
|
|
1875
|
+
}
|
|
1876
|
+
function formatZodIssues(issues) {
|
|
1877
|
+
return issues.map((i) => i.message).join(", ");
|
|
1878
|
+
}
|
|
1879
|
+
function registerWalletTools(server) {
|
|
1880
|
+
server.registerTool("wallet_login", {
|
|
1881
|
+
description: "Request an OTP code be sent to the user's email. After calling this, use wallet_verify with the code the user provides. Does NOT require an active session.",
|
|
1882
|
+
inputSchema: { email: z.string().describe("User's email address") }
|
|
1883
|
+
}, async ({ email }) => {
|
|
1884
|
+
const validation = emailSchema.safeParse(email);
|
|
1885
|
+
if (!validation.success) return err$3(`Invalid email: ${formatZodIssues(validation.error.issues)}`);
|
|
1886
|
+
const result = await requestOtp(email);
|
|
1887
|
+
if (Result.isError(result)) return err$3(`Could not reach auth server: ${result.error.cause}`);
|
|
1888
|
+
const response = result.value;
|
|
1889
|
+
if (!response || !response.ok) return err$3("Failed to send OTP email");
|
|
1890
|
+
return ok$3({ message: response.message });
|
|
1891
|
+
});
|
|
1892
|
+
server.registerTool("wallet_verify", {
|
|
1893
|
+
description: "Verify an OTP code and create or recover the wallet. The session is saved locally so subsequent tools (wallet_address, wallet_balance, wallet_transfer, etc.) work without re-authentication.",
|
|
1894
|
+
inputSchema: {
|
|
1895
|
+
email: z.string().describe("Must match the email used in wallet_login"),
|
|
1896
|
+
otp: z.string().describe("OTP code from email"),
|
|
1897
|
+
network: z.enum(["testnet", "pubnet"]).default("testnet").describe("Stellar network")
|
|
1898
|
+
}
|
|
1899
|
+
}, async ({ email, otp, network }) => {
|
|
1900
|
+
const emailValidation = emailSchema.safeParse(email);
|
|
1901
|
+
if (!emailValidation.success) return err$3(`Invalid email: ${formatZodIssues(emailValidation.error.issues)}`);
|
|
1902
|
+
const otpValidation = otpSchema.safeParse(otp);
|
|
1903
|
+
if (!otpValidation.success) return err$3(`Invalid OTP: ${formatZodIssues(otpValidation.error.issues)}`);
|
|
1904
|
+
const verifyResult = await verifyOtp(email, otp);
|
|
1905
|
+
if (Result.isError(verifyResult)) return err$3(`OTP verification failed: ${verifyResult.error.cause}`);
|
|
1906
|
+
const verifyResponse = verifyResult.value;
|
|
1907
|
+
if (!verifyResponse.verified) return err$3("OTP verification failed.");
|
|
1908
|
+
const sessionResult = saveSession(verifyResponse.token, verifyResponse.email);
|
|
1909
|
+
if (Result.isError(sessionResult)) return err$3(formatSessionError(sessionResult.error));
|
|
1910
|
+
const walletResult = await createWallet(parseNetwork$3(network));
|
|
1911
|
+
if (Result.isError(walletResult)) return err$3(formatWalletError(walletResult.error));
|
|
1912
|
+
const wallet = walletResult.value;
|
|
1913
|
+
return ok$3({ wallet: {
|
|
1914
|
+
email: wallet.email,
|
|
1915
|
+
publicKey: wallet.publicKey,
|
|
1916
|
+
network: wallet.network,
|
|
1917
|
+
createdAt: wallet.createdAt
|
|
1918
|
+
} });
|
|
1919
|
+
});
|
|
1920
|
+
server.registerTool("wallet_address", {
|
|
1921
|
+
description: "Get the logged-in wallet's public key, network, and email. Requires an active session.",
|
|
1922
|
+
inputSchema: {}
|
|
1923
|
+
}, async () => {
|
|
1924
|
+
const addressResult = await fetchAddress();
|
|
1925
|
+
if (Result.isError(addressResult)) return err$3(formatWalletError(addressResult.error));
|
|
1926
|
+
const addr = addressResult.value;
|
|
1927
|
+
return ok$3({
|
|
1928
|
+
publicKey: addr.publicKey,
|
|
1929
|
+
network: addr.network,
|
|
1930
|
+
email: addr.email
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1933
|
+
server.registerTool("wallet_balance", {
|
|
1934
|
+
description: "Get all asset balances for the logged-in wallet. Requires an active session.",
|
|
1935
|
+
inputSchema: {}
|
|
1936
|
+
}, async () => {
|
|
1937
|
+
const walletResult = await fetchWallet();
|
|
1938
|
+
if (Result.isError(walletResult)) return err$3(formatWalletError(walletResult.error));
|
|
1939
|
+
const wallet = walletResult.value;
|
|
1940
|
+
const balancesResult = await getBalances(wallet.publicKey, wallet.network);
|
|
1941
|
+
if (Result.isError(balancesResult)) return err$3(formatStellarError(balancesResult.error));
|
|
1942
|
+
return ok$3({
|
|
1943
|
+
address: wallet.publicKey,
|
|
1944
|
+
email: wallet.email,
|
|
1945
|
+
balances: balancesResult.value
|
|
1946
|
+
});
|
|
1947
|
+
});
|
|
1948
|
+
server.registerTool("wallet_transfer", {
|
|
1949
|
+
description: "Send XLM from the logged-in wallet to another Stellar address. Requires an active session.",
|
|
1950
|
+
inputSchema: {
|
|
1951
|
+
destination: z.string().describe("Destination public key (G...)"),
|
|
1952
|
+
amount: z.string().describe("Amount in XLM (up to 7 decimal places)")
|
|
1953
|
+
}
|
|
1954
|
+
}, async ({ destination, amount }) => {
|
|
1955
|
+
const destValidation = stellarPublicKey.safeParse(destination);
|
|
1956
|
+
if (!destValidation.success) return err$3(`Invalid destination: ${formatZodIssues(destValidation.error.issues)}`);
|
|
1957
|
+
const amountValidation = amountSchema.safeParse(amount);
|
|
1958
|
+
if (!amountValidation.success) return err$3(`Invalid amount: ${formatZodIssues(amountValidation.error.issues)}`);
|
|
1959
|
+
const walletResult = await fetchWallet();
|
|
1960
|
+
if (Result.isError(walletResult)) return err$3(formatWalletError(walletResult.error));
|
|
1961
|
+
const result = await transferXlm(walletResult.value.secretKey, destination, amount, walletResult.value.network);
|
|
1962
|
+
if (Result.isError(result)) return err$3(formatStellarError(result.error));
|
|
1963
|
+
return ok$3(result.value);
|
|
1964
|
+
});
|
|
1965
|
+
server.registerTool("wallet_logout", {
|
|
1966
|
+
description: "Clear the local wallet session.",
|
|
1967
|
+
inputSchema: {}
|
|
1968
|
+
}, async () => {
|
|
1969
|
+
const sessionResult = loadSession();
|
|
1970
|
+
if (Result.isError(sessionResult)) return err$3(formatSessionError(sessionResult.error));
|
|
1971
|
+
const email = sessionResult.value.email;
|
|
1972
|
+
const clearResult = clearSession();
|
|
1973
|
+
if (Result.isError(clearResult)) return err$3(formatSessionError(clearResult.error));
|
|
1974
|
+
return ok$3({
|
|
1975
|
+
loggedOut: true,
|
|
1976
|
+
email
|
|
1977
|
+
});
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
//#endregion
|
|
1981
|
+
//#region src/mcp/account-tools.ts
|
|
1982
|
+
function ok$2(data) {
|
|
1983
|
+
return { content: [{
|
|
1984
|
+
type: "text",
|
|
1985
|
+
text: JSON.stringify({
|
|
1986
|
+
ok: true,
|
|
1987
|
+
data
|
|
1988
|
+
}, null, 2)
|
|
1989
|
+
}] };
|
|
1990
|
+
}
|
|
1991
|
+
function err$2(message) {
|
|
1992
|
+
return { content: [{
|
|
1993
|
+
type: "text",
|
|
1994
|
+
text: JSON.stringify({
|
|
1995
|
+
ok: false,
|
|
1996
|
+
error: message
|
|
1997
|
+
}, null, 2)
|
|
1998
|
+
}] };
|
|
1999
|
+
}
|
|
2000
|
+
function parseNetwork$2(value) {
|
|
2001
|
+
return value === "pubnet" ? "pubnet" : "testnet";
|
|
2002
|
+
}
|
|
2003
|
+
const addressParam = z.string().describe("Stellar public key (G...)");
|
|
2004
|
+
const networkParam$1 = z.enum(["testnet", "pubnet"]).default("testnet").describe("Stellar network");
|
|
2005
|
+
const limitParam$1 = z.number().int().min(1).max(200).default(10).describe("Max records to return");
|
|
2006
|
+
const cursorParam = z.string().optional().describe("Pagination cursor");
|
|
2007
|
+
const orderParam = z.enum(["asc", "desc"]).default("desc").describe("Sort order");
|
|
2008
|
+
function registerAccountTools(server) {
|
|
2009
|
+
server.registerTool("account_details", {
|
|
2010
|
+
description: "Get detailed account information from Horizon: balances, signers, thresholds, flags, and more.",
|
|
2011
|
+
inputSchema: {
|
|
2012
|
+
address: addressParam,
|
|
2013
|
+
network: networkParam$1
|
|
2014
|
+
}
|
|
2015
|
+
}, async ({ address, network }) => {
|
|
2016
|
+
const validation = stellarPublicKey.safeParse(address);
|
|
2017
|
+
if (!validation.success) return err$2(`Invalid address: ${validation.error.issues.map((i) => i.message).join(", ")}`);
|
|
2018
|
+
const result = await getAccountDetails(address, parseNetwork$2(network));
|
|
2019
|
+
if (Result.isError(result)) return err$2(formatHorizonError(result.error));
|
|
2020
|
+
return ok$2(result.value);
|
|
2021
|
+
});
|
|
2022
|
+
server.registerTool("account_transactions", {
|
|
2023
|
+
description: "Get transaction history for a Stellar account from Horizon.",
|
|
2024
|
+
inputSchema: {
|
|
2025
|
+
address: addressParam,
|
|
2026
|
+
network: networkParam$1,
|
|
2027
|
+
limit: limitParam$1,
|
|
2028
|
+
cursor: cursorParam,
|
|
2029
|
+
order: orderParam
|
|
2030
|
+
}
|
|
2031
|
+
}, async ({ address, network, limit, cursor, order }) => {
|
|
2032
|
+
const validation = stellarPublicKey.safeParse(address);
|
|
2033
|
+
if (!validation.success) return err$2(`Invalid address: ${validation.error.issues.map((i) => i.message).join(", ")}`);
|
|
2034
|
+
const result = await getTransactions(address, parseNetwork$2(network), {
|
|
2035
|
+
limit,
|
|
2036
|
+
cursor,
|
|
2037
|
+
order
|
|
2038
|
+
});
|
|
2039
|
+
if (Result.isError(result)) return err$2(formatHorizonError(result.error));
|
|
2040
|
+
return ok$2({
|
|
2041
|
+
address,
|
|
2042
|
+
count: result.value.length,
|
|
2043
|
+
transactions: result.value
|
|
2044
|
+
});
|
|
2045
|
+
});
|
|
2046
|
+
server.registerTool("account_payments", {
|
|
2047
|
+
description: "Get payment history for a Stellar account from Horizon.",
|
|
2048
|
+
inputSchema: {
|
|
2049
|
+
address: addressParam,
|
|
2050
|
+
network: networkParam$1,
|
|
2051
|
+
limit: limitParam$1,
|
|
2052
|
+
cursor: cursorParam,
|
|
2053
|
+
order: orderParam
|
|
2054
|
+
}
|
|
2055
|
+
}, async ({ address, network, limit, cursor, order }) => {
|
|
2056
|
+
const validation = stellarPublicKey.safeParse(address);
|
|
2057
|
+
if (!validation.success) return err$2(`Invalid address: ${validation.error.issues.map((i) => i.message).join(", ")}`);
|
|
2058
|
+
const result = await getPayments(address, parseNetwork$2(network), {
|
|
2059
|
+
limit,
|
|
2060
|
+
cursor,
|
|
2061
|
+
order
|
|
2062
|
+
});
|
|
2063
|
+
if (Result.isError(result)) return err$2(formatHorizonError(result.error));
|
|
2064
|
+
return ok$2({
|
|
2065
|
+
address,
|
|
2066
|
+
count: result.value.length,
|
|
2067
|
+
payments: result.value
|
|
2068
|
+
});
|
|
2069
|
+
});
|
|
2070
|
+
server.registerTool("account_effects", {
|
|
2071
|
+
description: "Get effects history for a Stellar account from Horizon.",
|
|
2072
|
+
inputSchema: {
|
|
2073
|
+
address: addressParam,
|
|
2074
|
+
network: networkParam$1,
|
|
2075
|
+
limit: limitParam$1,
|
|
2076
|
+
cursor: cursorParam
|
|
2077
|
+
}
|
|
2078
|
+
}, async ({ address, network, limit, cursor }) => {
|
|
2079
|
+
const validation = stellarPublicKey.safeParse(address);
|
|
2080
|
+
if (!validation.success) return err$2(`Invalid address: ${validation.error.issues.map((i) => i.message).join(", ")}`);
|
|
2081
|
+
const result = await getEffects(address, parseNetwork$2(network), {
|
|
2082
|
+
limit,
|
|
2083
|
+
cursor
|
|
2084
|
+
});
|
|
2085
|
+
if (Result.isError(result)) return err$2(formatHorizonError(result.error));
|
|
2086
|
+
return ok$2({
|
|
2087
|
+
address,
|
|
2088
|
+
count: result.value.length,
|
|
2089
|
+
effects: result.value
|
|
2090
|
+
});
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
//#endregion
|
|
2094
|
+
//#region src/mcp/asset-tools.ts
|
|
2095
|
+
function ok$1(data) {
|
|
2096
|
+
return { content: [{
|
|
2097
|
+
type: "text",
|
|
2098
|
+
text: JSON.stringify({
|
|
2099
|
+
ok: true,
|
|
2100
|
+
data
|
|
2101
|
+
}, null, 2)
|
|
2102
|
+
}] };
|
|
2103
|
+
}
|
|
2104
|
+
function err$1(message) {
|
|
2105
|
+
return { content: [{
|
|
2106
|
+
type: "text",
|
|
2107
|
+
text: JSON.stringify({
|
|
2108
|
+
ok: false,
|
|
2109
|
+
error: message
|
|
2110
|
+
}, null, 2)
|
|
2111
|
+
}] };
|
|
2112
|
+
}
|
|
2113
|
+
function parseNetwork$1(value) {
|
|
2114
|
+
return value === "pubnet" ? "pubnet" : "testnet";
|
|
2115
|
+
}
|
|
2116
|
+
const networkParam = z.enum(["testnet", "pubnet"]).default("testnet").describe("Stellar network");
|
|
2117
|
+
const limitParam = z.number().int().min(1).max(200).default(10).describe("Max records to return");
|
|
2118
|
+
function parseAssetInput(input) {
|
|
2119
|
+
if (input === "native" || input === "XLM") return { assetType: "native" };
|
|
2120
|
+
const parts = input.split(":");
|
|
2121
|
+
if (parts.length !== 2) throw new Error(`Invalid asset format: "${input}". Use "native" or "CODE:ISSUER".`);
|
|
2122
|
+
return {
|
|
2123
|
+
assetType: parts[0].length <= 4 ? "credit_alphanum4" : "credit_alphanum12",
|
|
2124
|
+
assetCode: parts[0],
|
|
2125
|
+
assetIssuer: parts[1]
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
function registerAssetTools(server) {
|
|
2129
|
+
server.registerTool("assets_search", {
|
|
2130
|
+
description: "Search for assets on Stellar. Filter by asset code and/or issuer.",
|
|
2131
|
+
inputSchema: {
|
|
2132
|
+
network: networkParam,
|
|
2133
|
+
code: z.string().optional().describe("Filter by asset code (e.g. USDC)"),
|
|
2134
|
+
issuer: z.string().optional().describe("Filter by asset issuer (G...)"),
|
|
2135
|
+
limit: limitParam
|
|
2136
|
+
}
|
|
2137
|
+
}, async ({ network, code, issuer, limit }) => {
|
|
2138
|
+
const result = await getAssets(parseNetwork$1(network), {
|
|
2139
|
+
limit,
|
|
2140
|
+
code,
|
|
2141
|
+
issuer
|
|
2142
|
+
});
|
|
2143
|
+
if (Result.isError(result)) return err$1(formatHorizonError(result.error));
|
|
2144
|
+
return ok$1({
|
|
2145
|
+
count: result.value.length,
|
|
2146
|
+
assets: result.value
|
|
2147
|
+
});
|
|
2148
|
+
});
|
|
2149
|
+
server.registerTool("assets_orderbook", {
|
|
2150
|
+
description: "View the order book (bids and asks) for a pair of assets on Stellar.",
|
|
2151
|
+
inputSchema: {
|
|
2152
|
+
selling: z.string().describe("Selling asset: 'native' for XLM, or 'CODE:ISSUER'"),
|
|
2153
|
+
buying: z.string().describe("Buying asset: 'native' for XLM, or 'CODE:ISSUER'"),
|
|
2154
|
+
network: networkParam,
|
|
2155
|
+
limit: limitParam
|
|
2156
|
+
}
|
|
2157
|
+
}, async ({ selling, buying, network, limit }) => {
|
|
2158
|
+
try {
|
|
2159
|
+
const result = await getOrderbook(parseAssetInput(selling), parseAssetInput(buying), parseNetwork$1(network), { limit });
|
|
2160
|
+
if (Result.isError(result)) return err$1(formatHorizonError(result.error));
|
|
2161
|
+
return ok$1(result.value);
|
|
2162
|
+
} catch (e) {
|
|
2163
|
+
return err$1(e instanceof Error ? e.message : String(e));
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
server.registerTool("fee_stats", {
|
|
2167
|
+
description: "Get current fee statistics from the Stellar network.",
|
|
2168
|
+
inputSchema: { network: networkParam }
|
|
2169
|
+
}, async ({ network }) => {
|
|
2170
|
+
const result = await getFeeStats(parseNetwork$1(network));
|
|
2171
|
+
if (Result.isError(result)) return err$1(formatHorizonError(result.error));
|
|
2172
|
+
return ok$1(result.value);
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
//#endregion
|
|
2176
|
+
//#region src/mcp/payment-tools.ts
|
|
2177
|
+
function ok(data) {
|
|
2178
|
+
return { content: [{
|
|
2179
|
+
type: "text",
|
|
2180
|
+
text: JSON.stringify({
|
|
2181
|
+
ok: true,
|
|
2182
|
+
data
|
|
2183
|
+
}, null, 2)
|
|
2184
|
+
}] };
|
|
2185
|
+
}
|
|
2186
|
+
function err(message) {
|
|
2187
|
+
return { content: [{
|
|
2188
|
+
type: "text",
|
|
2189
|
+
text: JSON.stringify({
|
|
2190
|
+
ok: false,
|
|
2191
|
+
error: message
|
|
2192
|
+
}, null, 2)
|
|
2193
|
+
}] };
|
|
2194
|
+
}
|
|
2195
|
+
function parseNetwork(value) {
|
|
2196
|
+
return value === "pubnet" ? "pubnet" : "testnet";
|
|
2197
|
+
}
|
|
2198
|
+
function registerPaymentTools(server) {
|
|
2199
|
+
server.registerTool("send_payment", {
|
|
2200
|
+
description: "Send a payment (XLM or custom asset) from the logged-in wallet to another Stellar address. Requires an active session.",
|
|
2201
|
+
inputSchema: {
|
|
2202
|
+
destination: z.string().describe("Destination public key (G...)"),
|
|
2203
|
+
amount: z.string().describe("Amount to send (up to 7 decimal places)"),
|
|
2204
|
+
asset: z.string().default("native").describe("Asset: 'native' for XLM, or 'CODE:ISSUER' for custom assets"),
|
|
2205
|
+
memo: z.string().optional().describe("Transaction memo text (max 28 chars)"),
|
|
2206
|
+
network: z.enum(["testnet", "pubnet"]).default("testnet").describe("Stellar network")
|
|
2207
|
+
}
|
|
2208
|
+
}, async ({ destination, amount, asset, memo, network }) => {
|
|
2209
|
+
const destValidation = stellarPublicKey.safeParse(destination);
|
|
2210
|
+
if (!destValidation.success) return err(`Invalid destination: ${destValidation.error.issues.map((i) => i.message).join(", ")}`);
|
|
2211
|
+
const amountValidation = amountSchema.safeParse(amount);
|
|
2212
|
+
if (!amountValidation.success) return err(`Invalid amount: ${amountValidation.error.issues.map((i) => i.message).join(", ")}`);
|
|
2213
|
+
const assetValidation = assetSchema.safeParse(asset);
|
|
2214
|
+
if (!assetValidation.success) return err(`Invalid asset: ${assetValidation.error.issues.map((i) => i.message).join(", ")}`);
|
|
2215
|
+
if (memo !== void 0) {
|
|
2216
|
+
const memoValidation = memoSchema.safeParse(memo);
|
|
2217
|
+
if (!memoValidation.success) return err(`Invalid memo: ${memoValidation.error.issues.map((i) => i.message).join(", ")}`);
|
|
2218
|
+
}
|
|
2219
|
+
const walletResult = await fetchWallet();
|
|
2220
|
+
if (Result.isError(walletResult)) return err(formatWalletError(walletResult.error));
|
|
2221
|
+
const result = await sendPayment(walletResult.value.secretKey, destination, amount, asset, parseNetwork(network), memo);
|
|
2222
|
+
if (Result.isError(result)) return err(formatStellarError(result.error));
|
|
2223
|
+
return ok(result.value);
|
|
2224
|
+
});
|
|
2225
|
+
server.registerTool("pay_url", {
|
|
2226
|
+
description: "Make an X402 micropayment to a URL. The CLI detects 402 responses, signs auth entries, and returns the paid content. Requires an active session.",
|
|
2227
|
+
inputSchema: { url: z.string().describe("URL that requires X402 payment") }
|
|
2228
|
+
}, async ({ url }) => {
|
|
2229
|
+
const result = await pay(url);
|
|
2230
|
+
if (Result.isError(result)) return err(formatPaymentError(result.error));
|
|
2231
|
+
return ok(result.value);
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
//#endregion
|
|
2235
|
+
//#region src/mcp/mod.ts
|
|
2236
|
+
async function startMcpServer() {
|
|
2237
|
+
const server = new McpServer({
|
|
2238
|
+
name: "stelagent",
|
|
2239
|
+
version: "0.1.0"
|
|
2240
|
+
}, { instructions: [
|
|
2241
|
+
"Stelagent MCP server for Stellar blockchain operations.",
|
|
2242
|
+
"Authentication flow: 1) wallet_login (sends OTP to email) → 2) wallet_verify (verifies OTP, creates session and wallet). After verify, all other wallet tools work.",
|
|
2243
|
+
"Account, asset, and fee tools are public and require no authentication — only a Stellar public key or network parameter.",
|
|
2244
|
+
"All tools return { ok: true, data } on success or { ok: false, error } on failure.",
|
|
2245
|
+
"Network defaults to 'testnet'. Use 'pubnet' for mainnet."
|
|
2246
|
+
].join("\n") });
|
|
2247
|
+
registerWalletTools(server);
|
|
2248
|
+
registerAccountTools(server);
|
|
2249
|
+
registerAssetTools(server);
|
|
2250
|
+
registerPaymentTools(server);
|
|
2251
|
+
const transport = new StdioServerTransport();
|
|
2252
|
+
await server.connect(transport);
|
|
2253
|
+
}
|
|
2254
|
+
//#endregion
|
|
1821
2255
|
//#region src/index.ts
|
|
1822
2256
|
runMain(defineCommand({
|
|
1823
2257
|
meta: {
|
|
@@ -1832,86 +2266,21 @@ runMain(defineCommand({
|
|
|
1832
2266
|
assets: assetsCommand,
|
|
1833
2267
|
send: sendCommand,
|
|
1834
2268
|
fee: feeCommand,
|
|
1835
|
-
monitor:
|
|
2269
|
+
monitor: monitorCommand,
|
|
2270
|
+
mcp: defineCommand({
|
|
1836
2271
|
meta: {
|
|
1837
|
-
name: "
|
|
1838
|
-
description: "
|
|
2272
|
+
name: "mcp",
|
|
2273
|
+
description: "Start the MCP server on stdio"
|
|
1839
2274
|
},
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
address: {
|
|
1850
|
-
type: "positional",
|
|
1851
|
-
description: "Stellar public key (G...)",
|
|
1852
|
-
required: true
|
|
1853
|
-
},
|
|
1854
|
-
network: networkArg,
|
|
1855
|
-
format: formatArg,
|
|
1856
|
-
cursor: {
|
|
1857
|
-
type: "string",
|
|
1858
|
-
alias: ["c"],
|
|
1859
|
-
description: "Start from this cursor (default: 'now' for new events)",
|
|
1860
|
-
default: "now"
|
|
1861
|
-
}
|
|
1862
|
-
},
|
|
1863
|
-
async run({ args }) {
|
|
1864
|
-
const networkResult = parseNetwork(String(args.network ?? "testnet"));
|
|
1865
|
-
const format = parseFormat(String(args.format ?? "json"));
|
|
1866
|
-
if (Result.isError(networkResult)) {
|
|
1867
|
-
printResult(Result.err(networkResult.error._tag), "json");
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1870
|
-
const network = networkResult.value;
|
|
1871
|
-
const address = String(args.address ?? "");
|
|
1872
|
-
const validation = Result.try({
|
|
1873
|
-
try: () => stellarPublicKey.parse(address),
|
|
1874
|
-
catch: (e) => e
|
|
1875
|
-
});
|
|
1876
|
-
if (Result.isError(validation)) {
|
|
1877
|
-
const msg = validation.error instanceof z.ZodError ? formatZodError(validation.error) : validation.error instanceof Error ? validation.error.message : String(validation.error);
|
|
1878
|
-
printResult(Result.err(msg), format);
|
|
1879
|
-
return;
|
|
1880
|
-
}
|
|
1881
|
-
const cursor = args.cursor === "now" ? void 0 : String(args.cursor);
|
|
1882
|
-
let close;
|
|
1883
|
-
try {
|
|
1884
|
-
close = await runApp("monitor effects", async () => {
|
|
1885
|
-
return streamEffects(address, network, { cursor }, (record) => {
|
|
1886
|
-
console.log(JSON.stringify({
|
|
1887
|
-
ok: true,
|
|
1888
|
-
data: record
|
|
1889
|
-
}));
|
|
1890
|
-
}, (error) => {
|
|
1891
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1892
|
-
console.log(JSON.stringify({
|
|
1893
|
-
ok: false,
|
|
1894
|
-
error: `Stream error: ${message}`
|
|
1895
|
-
}));
|
|
1896
|
-
});
|
|
1897
|
-
}, format);
|
|
1898
|
-
} catch (e) {
|
|
1899
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
1900
|
-
printResult(Result.err(`Failed to stream effects: ${message}`), "json");
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
console.log(JSON.stringify({
|
|
1904
|
-
ok: true,
|
|
1905
|
-
data: { message: `Streaming effects for ${address} on ${network}... Press Ctrl+C to stop.` }
|
|
1906
|
-
}, null, 2));
|
|
1907
|
-
await new Promise((resolve) => {
|
|
1908
|
-
process.on("SIGINT", () => {
|
|
1909
|
-
close();
|
|
1910
|
-
resolve();
|
|
1911
|
-
});
|
|
1912
|
-
});
|
|
1913
|
-
}
|
|
1914
|
-
})
|
|
2275
|
+
args: {},
|
|
2276
|
+
async run() {
|
|
2277
|
+
try {
|
|
2278
|
+
await startMcpServer();
|
|
2279
|
+
} catch (e) {
|
|
2280
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2281
|
+
console.error(`Failed to start MCP server: ${message}`);
|
|
2282
|
+
process.exit(1);
|
|
2283
|
+
}
|
|
1915
2284
|
}
|
|
1916
2285
|
})
|
|
1917
2286
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stelagent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Modular, agent-first CLI for Stellar — wallet, payments, markets, and DeFi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@clack/prompts": "^1.2.0",
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
29
|
"@stellar/stellar-sdk": "^14.6.1",
|
|
29
30
|
"@x402/core": "^2.9.0",
|
|
30
31
|
"@x402/stellar": "^2.9.0",
|