paralith-mcp 0.4.4 → 0.4.6
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/dist/chains.json +23 -7
- package/dist/client.mjs +22 -5
- package/dist/graph.json +22 -22
- package/dist/http.mjs +239 -86
- package/dist/server.mjs +248 -105
- package/package.json +1 -1
package/dist/http.mjs
CHANGED
|
@@ -305,8 +305,62 @@ import { z as z9 } from "zod";
|
|
|
305
305
|
// src/tool.mjs
|
|
306
306
|
import { z } from "zod";
|
|
307
307
|
|
|
308
|
+
// package.json
|
|
309
|
+
var package_default = {
|
|
310
|
+
name: "paralith-mcp",
|
|
311
|
+
version: "0.4.6",
|
|
312
|
+
description: "MCP server for Paralith on AO: an AI with one 24-word phrase plays Rune Realm, trades and bridges PUSD; plus paralith-bridge, the bridge operator CLI (TEST deployment).",
|
|
313
|
+
license: "UNLICENSED",
|
|
314
|
+
homepage: "https://docs.paralithmarkets.com/ai/mcp",
|
|
315
|
+
keywords: [
|
|
316
|
+
"mcp",
|
|
317
|
+
"model-context-protocol",
|
|
318
|
+
"ao",
|
|
319
|
+
"hyperbeam",
|
|
320
|
+
"arweave",
|
|
321
|
+
"game",
|
|
322
|
+
"agent"
|
|
323
|
+
],
|
|
324
|
+
type: "module",
|
|
325
|
+
bin: {
|
|
326
|
+
"paralith-mcp": "dist/server.mjs",
|
|
327
|
+
"paralith-bridge": "dist/bridge.mjs"
|
|
328
|
+
},
|
|
329
|
+
files: [
|
|
330
|
+
"dist",
|
|
331
|
+
"README.md"
|
|
332
|
+
],
|
|
333
|
+
publishConfig: {
|
|
334
|
+
access: "public"
|
|
335
|
+
},
|
|
336
|
+
scripts: {
|
|
337
|
+
build: "node scripts/build-client.mjs && node scripts/tools-doc.mjs",
|
|
338
|
+
"tools-doc": "node scripts/tools-doc.mjs",
|
|
339
|
+
start: "node src/server.mjs",
|
|
340
|
+
"start:http": "node src/http.mjs",
|
|
341
|
+
test: "node --test test/*.test.mjs"
|
|
342
|
+
},
|
|
343
|
+
engines: {
|
|
344
|
+
node: ">=20"
|
|
345
|
+
},
|
|
346
|
+
dependencies: {
|
|
347
|
+
"@modelcontextprotocol/ext-apps": "^2.0.3",
|
|
348
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
349
|
+
"@mysten/sui": "^2.34.0",
|
|
350
|
+
"@scure/bip39": "^2.4.0",
|
|
351
|
+
"@solana/web3.js": "^1.99.0",
|
|
352
|
+
"arweave-mnemonic-keys": "^0.0.9",
|
|
353
|
+
lodash: "^4.18.1",
|
|
354
|
+
viem: "^2.57.3",
|
|
355
|
+
zod: "^4.6.5"
|
|
356
|
+
},
|
|
357
|
+
devDependencies: {
|
|
358
|
+
esbuild: "^0.28.2"
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
308
362
|
// src/update.mjs
|
|
309
|
-
var VERSION =
|
|
363
|
+
var VERSION = package_default.version;
|
|
310
364
|
var CHECK_MS = 6 * 36e5;
|
|
311
365
|
var NOTE_MS = 36e5;
|
|
312
366
|
var latest = null;
|
|
@@ -443,13 +497,20 @@ function side(s) {
|
|
|
443
497
|
freeMovesUsed: Object.keys(s.freeUsed ?? {})
|
|
444
498
|
};
|
|
445
499
|
}
|
|
500
|
+
function sideOf(b, address) {
|
|
501
|
+
if (!address) return null;
|
|
502
|
+
if ((b.accepterAddress ?? b.accepter?.address) === address) return "accepter";
|
|
503
|
+
if ((b.challengerAddress ?? b.challenger?.address) === address) return "challenger";
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
446
506
|
function battle(b, address) {
|
|
447
507
|
if (!b || typeof b !== "object") return b;
|
|
448
|
-
const mine = b
|
|
508
|
+
const mine = sideOf(b, address);
|
|
449
509
|
const theirs = mine === "accepter" ? "challenger" : "accepter";
|
|
510
|
+
const label = (s) => !mine ? s : s === mine ? "you" : "opponent";
|
|
450
511
|
const turns = (b.turns ?? []).filter((t) => t.round === b.round - 1 || t.round === b.round).map((t) => ({
|
|
451
512
|
round: t.round,
|
|
452
|
-
by: t.attacker
|
|
513
|
+
by: label(t.attacker),
|
|
453
514
|
move: t.move,
|
|
454
515
|
damage: t.healthDamage,
|
|
455
516
|
shieldDamage: t.shieldDamage,
|
|
@@ -458,6 +519,7 @@ function battle(b, address) {
|
|
|
458
519
|
superEffective: t.superEffective,
|
|
459
520
|
notEffective: t.notEffective
|
|
460
521
|
}));
|
|
522
|
+
const sides = mine ? { you: side(b[mine]), opponent: side(b[theirs]) } : { challenger: side(b.challenger), accepter: side(b.accepter) };
|
|
461
523
|
return {
|
|
462
524
|
id: b.id,
|
|
463
525
|
kind: b.kind,
|
|
@@ -465,8 +527,7 @@ function battle(b, address) {
|
|
|
465
527
|
round: b.round,
|
|
466
528
|
...b.winner ? { winner: b.winner } : {},
|
|
467
529
|
...b.waitingOn?.length ? { waitingOn: b.waitingOn } : {},
|
|
468
|
-
|
|
469
|
-
opponent: side(b[theirs]),
|
|
530
|
+
...sides,
|
|
470
531
|
recentTurns: turns
|
|
471
532
|
};
|
|
472
533
|
}
|
|
@@ -483,22 +544,31 @@ function hunt(run) {
|
|
|
483
544
|
// src/game.mjs
|
|
484
545
|
var FACTIONS = ["Sky Nomads", "Aqua Guardians", "Inferno Blades", "Stone Titans"];
|
|
485
546
|
var BERRIES = ["air_berry", "fire_berry", "rock_berry", "water_berry"];
|
|
547
|
+
var SHOP_ITEMS = [...BERRIES, "scroll", "rune"];
|
|
486
548
|
async function me() {
|
|
487
549
|
const { api, wallets } = await runtime();
|
|
488
550
|
return { api, address: wallets?.ao.address };
|
|
489
551
|
}
|
|
490
|
-
async function player2() {
|
|
491
|
-
const { api, address } = await me();
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
552
|
+
async function player2(address) {
|
|
553
|
+
const { api, address: mine } = await me();
|
|
554
|
+
const who = address ?? mine;
|
|
555
|
+
if (!who) throw new Error("This server has no wallet; pass an address.");
|
|
556
|
+
const p = await api.readPlayer(who);
|
|
557
|
+
if (!p) throw new Error(address ? `${address} has no Rune Realm account.` : "This wallet has no Rune Realm account yet. Call game_join first.");
|
|
495
558
|
return p;
|
|
496
559
|
}
|
|
560
|
+
async function findBattle(id) {
|
|
561
|
+
const fleet = await readKey("game", "battlefleet").catch(() => null);
|
|
562
|
+
const workers = (fleet?.workers ?? []).map((w) => w.workerProcessId).filter(Boolean);
|
|
563
|
+
const found = await Promise.all([readKey("game", `battle-op-${id}`), ...workers.map((w) => readKey(w, `battle-${id}`))].map((read) => read.catch(() => null)));
|
|
564
|
+
return found.find((b) => b && typeof b === "object") ?? null;
|
|
565
|
+
}
|
|
566
|
+
var seenBy = (b, address) => b && typeof b === "object" ? Object.defineProperty(b, "viewer", { value: address, enumerable: false, configurable: true }) : b;
|
|
497
567
|
function registerGame(server) {
|
|
498
568
|
tool(
|
|
499
569
|
server,
|
|
500
570
|
"game_player",
|
|
501
|
-
"Read a Rune Realm player record (yours by default): gold,
|
|
571
|
+
"Read a Rune Realm player record (yours by default): gold, inventory, companions with energy/happiness/level/moves and pendingMoveOffer, loot boxes, arena record with arena.liveBattle, and liveHunt. Free, unsigned. Call this before deciding anything.",
|
|
502
572
|
{ address: z2.string().optional().describe("Another player's AO address; omit for yours.") },
|
|
503
573
|
async ({ address }) => {
|
|
504
574
|
const { api, address: mine } = await me();
|
|
@@ -514,7 +584,13 @@ function registerGame(server) {
|
|
|
514
584
|
"game_catalog",
|
|
515
585
|
"The game rules as data: activity costs/gains, arena terms, element effectiveness, free moves (Rally, Mend), hunt capture odds, items, level-up rules and every move pool. Free, unsigned.",
|
|
516
586
|
{},
|
|
517
|
-
async () =>
|
|
587
|
+
async () => {
|
|
588
|
+
const catalog = await (await me()).api.readCatalog();
|
|
589
|
+
if (catalog?.levelUp) {
|
|
590
|
+
catalog.levelUp = { ...catalog.levelUp, costDivisor: 32, cost: "ceil(targetLevel^2 / 32) Rune" };
|
|
591
|
+
}
|
|
592
|
+
return catalog;
|
|
593
|
+
},
|
|
518
594
|
READ
|
|
519
595
|
);
|
|
520
596
|
tool(
|
|
@@ -536,7 +612,11 @@ function registerGame(server) {
|
|
|
536
612
|
factions: factions?.map(({ members, ...f }) => f),
|
|
537
613
|
arenaTiers: tiers,
|
|
538
614
|
openChallenges: challenges,
|
|
539
|
-
leaderboard: leaderboard?.slice(0, 10)
|
|
615
|
+
leaderboard: leaderboard?.slice(0, 10).map(({ monster, ...row }) => {
|
|
616
|
+
if (!monster) return row;
|
|
617
|
+
const { exp, energy, happiness, ...companion2 } = companion(monster);
|
|
618
|
+
return { ...row, companion: companion2 };
|
|
619
|
+
})
|
|
540
620
|
};
|
|
541
621
|
},
|
|
542
622
|
READ
|
|
@@ -553,7 +633,7 @@ function registerGame(server) {
|
|
|
553
633
|
tool(
|
|
554
634
|
server,
|
|
555
635
|
"game_companion",
|
|
556
|
-
"Care for and manage companions. feed: 1 berry, +10
|
|
636
|
+
"Care for and manage companions. feed: 1 berry, +20 energy for a berry of its own element, +10 for any other. play: 10 energy + 1 berry, +25 happiness after 15 min. quest: 25 energy + 25 happiness, 1 h, then exp and Gold. claim: collect a finished play/quest. store (1 Rune; moves a companion into the collection) / retrieve / set_active / transfer manage the collection. adopt: once, if you have a faction but no companion.",
|
|
557
637
|
{
|
|
558
638
|
action: z2.enum(["feed", "play", "quest", "claim", "adopt", "store", "retrieve", "set_active", "transfer"]),
|
|
559
639
|
monster_id: z2.string().optional().describe("Companion id; omit for the active one."),
|
|
@@ -589,7 +669,7 @@ function registerGame(server) {
|
|
|
589
669
|
tool(
|
|
590
670
|
server,
|
|
591
671
|
"game_level_up",
|
|
592
|
-
"Level up the active companion when it has enough exp. Costs Rune. Allocate exactly the points the catalog says (levelUp.points), at most levelUp.maxPerStat in any one stat.",
|
|
672
|
+
"Level up the active companion when it has enough exp. Costs ceil(L^2/32) in-game Rune for target level L (1 up to level 5, 4 at 10, 13 at 20). Allocate exactly the points the catalog says (levelUp.points), at most levelUp.maxPerStat in any one stat. A pending move offer expires at the next level-up.",
|
|
593
673
|
{ attack: z2.number().int().min(0), defense: z2.number().int().min(0), speed: z2.number().int().min(0), health: z2.number().int().min(0) },
|
|
594
674
|
async (points) => (await me()).api.levelUp(points),
|
|
595
675
|
WRITE,
|
|
@@ -598,7 +678,7 @@ function registerGame(server) {
|
|
|
598
678
|
tool(
|
|
599
679
|
server,
|
|
600
680
|
"game_learn_move",
|
|
601
|
-
"Answer a pending move offer (every 5th level). Give replace to swap that move out; omit replace to decline. Final either way.",
|
|
681
|
+
"Answer a pending move offer (every 5th level). Give replace to swap that move out; omit replace to decline. Final either way. An unanswered offer expires at the next level-up.",
|
|
602
682
|
{ move: z2.string().describe("The offered move, exactly as in the player record."), replace: z2.string().optional() },
|
|
603
683
|
async ({ move, replace }) => (await me()).api.learnMove(move, replace),
|
|
604
684
|
WRITE,
|
|
@@ -622,27 +702,31 @@ function registerGame(server) {
|
|
|
622
702
|
WRITE,
|
|
623
703
|
asPlayer
|
|
624
704
|
);
|
|
705
|
+
tool(
|
|
706
|
+
server,
|
|
707
|
+
"game_shop_quote",
|
|
708
|
+
"The NPC realm shop's current Gold prices (bid/ask) and stock for berries, Scrolls and Rune. Free, unsigned.",
|
|
709
|
+
{},
|
|
710
|
+
async () => (await me()).api.readEconomy(),
|
|
711
|
+
READ
|
|
712
|
+
);
|
|
625
713
|
tool(
|
|
626
714
|
server,
|
|
627
715
|
"game_shop",
|
|
628
|
-
"
|
|
716
|
+
"Buy from or sell to the NPC realm shop for Gold (prices: game_shop_quote). Set max_total/min_total to bound slippage.",
|
|
629
717
|
{
|
|
630
|
-
action: z2.enum(["
|
|
631
|
-
item: z2.
|
|
632
|
-
quantity: z2.number().int().positive()
|
|
718
|
+
action: z2.enum(["buy", "sell"]),
|
|
719
|
+
item: z2.enum(SHOP_ITEMS),
|
|
720
|
+
quantity: z2.number().int().positive(),
|
|
633
721
|
max_total: z2.number().int().optional(),
|
|
634
722
|
min_total: z2.number().int().optional()
|
|
635
723
|
},
|
|
636
|
-
async ({ action, item, quantity, max_total, min_total }) =>
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
need(quantity, "quantity"),
|
|
643
|
-
{ maxTotal: max_total, minTotal: min_total }
|
|
644
|
-
);
|
|
645
|
-
},
|
|
724
|
+
async ({ action, item, quantity, max_total, min_total }) => (await me()).api.tradeGameShop(
|
|
725
|
+
action,
|
|
726
|
+
item,
|
|
727
|
+
quantity,
|
|
728
|
+
{ maxTotal: max_total, minTotal: min_total }
|
|
729
|
+
),
|
|
646
730
|
WRITE,
|
|
647
731
|
asPlayer
|
|
648
732
|
);
|
|
@@ -688,13 +772,23 @@ function registerGame(server) {
|
|
|
688
772
|
tool(
|
|
689
773
|
server,
|
|
690
774
|
"arena_battle",
|
|
691
|
-
"Read
|
|
692
|
-
{
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
775
|
+
"Read a battle: both sides' health, moves with uses left, round number and recent turns. Free. Defaults to your live battle; address reads that player's live battle; battle_id reads one battle directly (finished fights stay readable only briefly).",
|
|
776
|
+
{
|
|
777
|
+
address: z2.string().optional().describe("Whose live battle; omit for yours."),
|
|
778
|
+
battle_id: z2.string().optional().describe("A specific battle, e.g. from game_player arena.liveBattle.")
|
|
779
|
+
},
|
|
780
|
+
async ({ address, battle_id }) => {
|
|
781
|
+
const { api, address: mine } = await me();
|
|
782
|
+
const viewer = address ?? mine;
|
|
783
|
+
if (battle_id) {
|
|
784
|
+
const b = await findBattle(battle_id);
|
|
785
|
+
return b ? seenBy(b, viewer) : { battle: null, note: `No battle ${battle_id} on the game or any battle worker.` };
|
|
786
|
+
}
|
|
787
|
+
if (!viewer) throw new Error("This server has no wallet; pass address or battle_id.");
|
|
788
|
+
const p = await player2(viewer);
|
|
789
|
+
const id = p.battleFleet?.battleId ?? p.activeBattleId;
|
|
790
|
+
if (!id) return { battle: null, note: address ? `${address} has no live battle.` : "No live battle. Use arena_fight to start one." };
|
|
791
|
+
return seenBy(p.battle?.id === id ? p.battle : await api.readBattle(id), viewer);
|
|
698
792
|
},
|
|
699
793
|
READ,
|
|
700
794
|
battleView
|
|
@@ -732,7 +826,7 @@ function registerGame(server) {
|
|
|
732
826
|
tool(
|
|
733
827
|
server,
|
|
734
828
|
"hunt_begin",
|
|
735
|
-
"Start a hunt with a companion.
|
|
829
|
+
"Start a hunt with a companion. Entry fee: 2 of each berry, not refunded. Up to 3 Rune and 1 Scroll go with the run and come back when it ends, unless a capture spends them.",
|
|
736
830
|
{ monster_id: z2.string().describe("Companion to hunt with (see game_player).") },
|
|
737
831
|
async ({ monster_id }) => (await me()).api.beginHunt(monster_id),
|
|
738
832
|
WRITE,
|
|
@@ -741,14 +835,14 @@ function registerGame(server) {
|
|
|
741
835
|
tool(
|
|
742
836
|
server,
|
|
743
837
|
"hunt_state",
|
|
744
|
-
"Read
|
|
745
|
-
{},
|
|
746
|
-
async () => {
|
|
838
|
+
"Read a live hunt run (yours by default): encounter, wild creature, round, status. Free.",
|
|
839
|
+
{ address: z2.string().optional().describe("Another player's AO address; omit for yours.") },
|
|
840
|
+
async ({ address }) => {
|
|
747
841
|
const { api } = await me();
|
|
748
|
-
const route = (await player2()).hunt;
|
|
749
|
-
return route ? api.readHunt(route) : { hunt: null, note: "No live hunt. Use hunt_begin." };
|
|
842
|
+
const route = (await player2(address)).hunt;
|
|
843
|
+
return route ? api.readHunt(route) : { hunt: null, note: address ? `${address} has no live hunt.` : "No live hunt. Use hunt_begin." };
|
|
750
844
|
},
|
|
751
|
-
|
|
845
|
+
READ,
|
|
752
846
|
huntView
|
|
753
847
|
);
|
|
754
848
|
tool(
|
|
@@ -783,12 +877,24 @@ function registerGame(server) {
|
|
|
783
877
|
WRITE,
|
|
784
878
|
huntView
|
|
785
879
|
);
|
|
880
|
+
tool(
|
|
881
|
+
server,
|
|
882
|
+
"companion_market_browse",
|
|
883
|
+
"The companion market (priced in in-game Rune): current listings and recent sales. Free, unsigned.",
|
|
884
|
+
{},
|
|
885
|
+
async () => {
|
|
886
|
+
const { api } = await me();
|
|
887
|
+
const [listings, history] = await Promise.all([api.readMarket(), api.readMarketHistory()]);
|
|
888
|
+
return { listings, history };
|
|
889
|
+
},
|
|
890
|
+
READ
|
|
891
|
+
);
|
|
786
892
|
tool(
|
|
787
893
|
server,
|
|
788
894
|
"companion_market",
|
|
789
|
-
"
|
|
895
|
+
"Trade companions for in-game Rune (listings: companion_market_browse). list: sell a companion from your collection (never the active one; game_companion store moves it there for 1 Rune). buy / cancel: by listing_id.",
|
|
790
896
|
{
|
|
791
|
-
action: z2.enum(["
|
|
897
|
+
action: z2.enum(["list", "buy", "cancel"]),
|
|
792
898
|
monster_id: z2.string().optional(),
|
|
793
899
|
price: z2.number().int().min(1).max(1e6).optional(),
|
|
794
900
|
listing_id: z2.string().optional()
|
|
@@ -796,8 +902,6 @@ function registerGame(server) {
|
|
|
796
902
|
async ({ action, monster_id, price, listing_id }) => {
|
|
797
903
|
const { api } = await me();
|
|
798
904
|
switch (action) {
|
|
799
|
-
case "browse":
|
|
800
|
-
return { listings: await api.readMarket(), history: await api.readMarketHistory() };
|
|
801
905
|
case "list":
|
|
802
906
|
return api.listMonster(need(monster_id, "monster_id"), need(price, "price"));
|
|
803
907
|
case "buy":
|
|
@@ -825,7 +929,7 @@ function registerGame(server) {
|
|
|
825
929
|
tool(
|
|
826
930
|
server,
|
|
827
931
|
"element_claim",
|
|
828
|
-
"Claim today's free TEST element tokens
|
|
932
|
+
"Claim today's free TEST element tokens: 1-10 of one element (chosen by the contract) per wallet per UTC day, until that element's 2,000-token daily cap across all wallets is spent; after that, claims are refused until the next day.",
|
|
829
933
|
{},
|
|
830
934
|
async () => {
|
|
831
935
|
const { api, address } = await me();
|
|
@@ -836,9 +940,7 @@ function registerGame(server) {
|
|
|
836
940
|
);
|
|
837
941
|
}
|
|
838
942
|
var asPlayer = (r) => r?.address && r?.inventory ? player(r) : r;
|
|
839
|
-
|
|
840
|
-
return b?.id ? battle(b, (await me()).address) : b;
|
|
841
|
-
}
|
|
943
|
+
var battleView = (b) => b?.id ? battle(b, b.viewer) : b;
|
|
842
944
|
async function attackView(p) {
|
|
843
945
|
if (!p?.address) return p;
|
|
844
946
|
const { address, gold, battlesRemaining, wins, losses, arenaLast } = p;
|
|
@@ -879,10 +981,17 @@ function toWire(t, m, price, quantity) {
|
|
|
879
981
|
const baseAtoms = parseAmount(quantity, baseDen);
|
|
880
982
|
if (baseAtoms % lot !== 0n) throw new Error(`Quantity moves in steps of ${formatAtoms(lot, baseDen)} ${m.base}.`);
|
|
881
983
|
const perUnit = parseAmount(price, t.den(m.quote));
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
984
|
+
const unit = 10n ** BigInt(baseDen);
|
|
985
|
+
const tick = BigInt(Math.max(1, Number(m.tick ?? 1)));
|
|
986
|
+
const scaled = perUnit * lot;
|
|
987
|
+
if (scaled % unit !== 0n || scaled / unit % tick !== 0n) {
|
|
988
|
+
const step = tick * unit % lot === 0n ? formatAtoms(tick * unit / lot, t.den(m.quote)) : null;
|
|
989
|
+
throw new Error(step ? `Price moves in steps of ${step} ${m.quote} per ${m.base}.` : `Price must be a whole multiple of ${tick} ${m.quote} atoms per lot of ${formatAtoms(lot, baseDen)} ${m.base}.`);
|
|
990
|
+
}
|
|
991
|
+
return { lots: (baseAtoms / lot).toString(), perLot: (scaled / unit).toString() };
|
|
885
992
|
}
|
|
993
|
+
var unitPrice = (t, m, perLot) => formatAtoms(BigInt(perLot) * 10n ** BigInt(t.den(m.base)) / BigInt(m.lot), t.den(m.quote));
|
|
994
|
+
var units = (t, m, lots) => formatAtoms(BigInt(lots) * BigInt(m.lot), t.den(m.base));
|
|
886
995
|
var human = (t, free) => Object.fromEntries(Object.entries(free ?? {}).map(([asset, atoms]) => [asset, formatAtoms(atoms, t.den(asset))]));
|
|
887
996
|
function registerExchange(server) {
|
|
888
997
|
tool(
|
|
@@ -907,22 +1016,21 @@ function registerExchange(server) {
|
|
|
907
1016
|
const { api } = await runtime();
|
|
908
1017
|
const book = (await api.readVenueBook(t.process))?.[m.id];
|
|
909
1018
|
if (!book) return { market: m.id, book: null };
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
const level = (l) => ({ price: unitPrice(l.price), quantity: units(l.quantity), orders: l.orders });
|
|
1019
|
+
const price = (perLot) => unitPrice(t, m, perLot);
|
|
1020
|
+
const level = (l) => ({ price: price(l.price), quantity: units(t, m, l.quantity), orders: l.orders });
|
|
913
1021
|
return {
|
|
914
1022
|
market: m.id,
|
|
915
1023
|
units: `price in ${m.quote} per 1 ${m.base}; quantity in ${m.base}`,
|
|
916
|
-
bestBid: book.bestBid ?
|
|
917
|
-
bestAsk: book.bestAsk ?
|
|
1024
|
+
bestBid: book.bestBid ? price(book.bestBid) : null,
|
|
1025
|
+
bestAsk: book.bestAsk ? price(book.bestAsk) : null,
|
|
918
1026
|
bids: (book.depth?.bids ?? []).slice(0, 10).map(level),
|
|
919
1027
|
asks: (book.depth?.asks ?? []).slice(0, 10).map(level),
|
|
920
|
-
band: book.band && { low:
|
|
1028
|
+
band: book.band && { low: price(book.band.low), high: price(book.band.high) },
|
|
921
1029
|
today: book.candles?.at(-1) && {
|
|
922
|
-
open:
|
|
923
|
-
high:
|
|
924
|
-
low:
|
|
925
|
-
close:
|
|
1030
|
+
open: price(book.candles.at(-1).o),
|
|
1031
|
+
high: price(book.candles.at(-1).h),
|
|
1032
|
+
low: price(book.candles.at(-1).l),
|
|
1033
|
+
close: price(book.candles.at(-1).c),
|
|
926
1034
|
trades: book.candles.at(-1).n
|
|
927
1035
|
}
|
|
928
1036
|
};
|
|
@@ -932,7 +1040,7 @@ function registerExchange(server) {
|
|
|
932
1040
|
tool(
|
|
933
1041
|
server,
|
|
934
1042
|
"exchange_position",
|
|
935
|
-
"Balances at a venue (free,
|
|
1043
|
+
"Balances at a venue (free, and where each asset sits) with open orders and recent fills, all in human units; yours unless address is given. Free.",
|
|
936
1044
|
{ venue: VENUE, address: z3.string().optional() },
|
|
937
1045
|
async ({ venue, address }) => {
|
|
938
1046
|
const t = await terms(venue);
|
|
@@ -940,7 +1048,33 @@ function registerExchange(server) {
|
|
|
940
1048
|
const who = address ?? wallets?.ao.address;
|
|
941
1049
|
if (!who) throw new Error("This server has no wallet; pass an address.");
|
|
942
1050
|
const p = await api.readVenuePosition(t.process, who);
|
|
943
|
-
|
|
1051
|
+
const market = (row) => t.markets[row.market] ?? marketFor(t, row.item);
|
|
1052
|
+
const order = ({ lot, ...o }) => {
|
|
1053
|
+
const m = { ...market(o), ...lot ? { lot } : {} };
|
|
1054
|
+
return {
|
|
1055
|
+
...o,
|
|
1056
|
+
price: unitPrice(t, m, o.price),
|
|
1057
|
+
quantity: units(t, m, o.quantity),
|
|
1058
|
+
...o.remaining !== void 0 ? { remaining: units(t, m, o.remaining) } : {}
|
|
1059
|
+
};
|
|
1060
|
+
};
|
|
1061
|
+
const fill = (f) => {
|
|
1062
|
+
const m = market(f);
|
|
1063
|
+
return {
|
|
1064
|
+
...f,
|
|
1065
|
+
price: unitPrice(t, m, f.price),
|
|
1066
|
+
quantity: units(t, m, f.quantity),
|
|
1067
|
+
...f.fee !== void 0 ? { fee: formatAtoms(BigInt(f.fee), t.den(m.quote)) } : {}
|
|
1068
|
+
};
|
|
1069
|
+
};
|
|
1070
|
+
return {
|
|
1071
|
+
account: p.account ?? who,
|
|
1072
|
+
units: "price in quote per 1 base unit; quantity in base units; fee in quote",
|
|
1073
|
+
free: human(t, p.free),
|
|
1074
|
+
...p.locations ? { locations: Object.fromEntries(Object.entries(p.locations).map(([asset, at]) => [asset, Object.fromEntries(Object.entries(at).map(([where, atoms]) => [where, formatAtoms(atoms, t.den(asset))]))])) } : {},
|
|
1075
|
+
orders: (p.orders ?? []).map(order),
|
|
1076
|
+
fills: (p.fills ?? []).map(fill)
|
|
1077
|
+
};
|
|
944
1078
|
},
|
|
945
1079
|
READ
|
|
946
1080
|
);
|
|
@@ -2005,29 +2139,33 @@ var INSTRUCTIONS = `Paralith on AO (HyperBEAM): Rune Realm (a companion game), t
|
|
|
2005
2139
|
Everything is TEST-prefixed while unreleased. Reads are free; every write is a signed message (a few seconds round trip, longer under load).
|
|
2006
2140
|
|
|
2007
2141
|
Start: wallet -> game_player. No account? game_join (pick a faction).
|
|
2008
|
-
Core loop: game_daily once a day; game_companion feed/play
|
|
2009
|
-
game_level_up when exp allows; game_learn_move when an offer is pending.
|
|
2142
|
+
Core loop: game_daily once a day; game_companion feed/play keep energy and happiness up, quest then claim earns exp and Gold;
|
|
2143
|
+
game_level_up when exp allows; game_learn_move when an offer is pending (it expires at the next level-up).
|
|
2010
2144
|
Arena: arena_enter (25 energy + 25 happiness, 4 fights), arena_fight mode=bot, then arena_attack each round
|
|
2011
2145
|
(read arena_battle for moves and uses left; Rally and Mend are free once each), arena_end leave when done.
|
|
2012
|
-
Hunt: hunt_begin
|
|
2146
|
+
Hunt: hunt_begin (entry fee 2 of each berry, not refunded; up to 3 Rune + 1 Scroll set aside and returned unless a capture spends them)
|
|
2147
|
+
-> hunt_act search -> attack until defeated -> capture (1-3 Rune + a Scroll) or end. One encounter per hunt.
|
|
2013
2148
|
Markets: exchange_markets / exchange_book / exchange_order on venue "game" (items vs Gold) or "token" (elements vs TEST-RUNE).
|
|
2014
2149
|
Vaults (pooled liquidity, one amm/active/grid per market): vault_list, vault_position, vault_deposit, vault_shares (withdraw/lock/burn), vault_fund, vault_crank.
|
|
2015
2150
|
Rune: rune_move withdraw (game -> token) / deposit (token -> game). Bridge: bridge_balances, bridge_faucet, bridge_deposit, bridge_redeem.
|
|
2016
2151
|
DAO (stake TEST-RUNE; a passed proposal may send one attached message): dao_info, dao_proposals, dao_stake stake/unstake/withdraw, dao_propose, dao_vote.
|
|
2017
2152
|
|
|
2018
2153
|
HOW RESOURCES FLOW. When asked for a resource, find its path here, check what you hold (progress), then say the route before acting.
|
|
2019
|
-
- Gold (in-game only, never leaves the game). Earn:
|
|
2020
|
-
selling berries/Scrolls/Rune to the shop (game_shop sell) or on the game venue.
|
|
2154
|
+
- Gold (in-game only, never leaves the game). Earn: quest claims and arena bot wins, which share one 60 Gold allowance per 20 h
|
|
2155
|
+
(play pays happiness only); selling berries/Scrolls/Rune to the shop (game_shop sell) or on the game venue.
|
|
2156
|
+
Spend: arena stakes, shop buys, game-venue buys.
|
|
2021
2157
|
- Berries and Scrolls. Get: joining, crates (game_daily, game_lootbox), buying with Gold (game_shop, or exchange_order venue "game").
|
|
2022
|
-
Use: feed the companion, hunt
|
|
2158
|
+
Use: feed the companion, the hunt entry fee (2 of each berry), arena boosts; a Scroll plus 1-3 Rune for a hunt capture.
|
|
2023
2159
|
- Rune exists twice: in-game Rune and the TEST-RUNE wallet token; rune_move converts 1:1 either way.
|
|
2024
|
-
Get Rune: (1) the shop, only if
|
|
2025
|
-
(3) the token route
|
|
2026
|
-
sell that element for TEST-RUNE -> rune_move deposit.
|
|
2027
|
-
|
|
2160
|
+
Get Rune: (1) the shop, only if game_shop_quote shows an ask (it often only bids); (2) sell a companion on companion_market;
|
|
2161
|
+
(3) the token route: element_claim (free, 1-10 of one element per wallet per UTC day until that element's 2,000/day
|
|
2162
|
+
global cap is spent) -> exchange_order venue "token" sell that element for TEST-RUNE -> rune_move deposit.
|
|
2163
|
+
Public TEST accounts accrue no Rune from game_daily (pass holders only).
|
|
2164
|
+
Spend in-game Rune: game_level_up (ceil(L^2/32) for level L), hunt capture, companion_market buys, game_companion store (1 Rune).
|
|
2165
|
+
TEST-RUNE: token-venue buys, vaults, dao_stake.
|
|
2028
2166
|
- Element tokens (TEST-EARTH/AIR/FIRE/WATER): element_claim daily; trade against TEST-RUNE on the token venue; vault liquidity.
|
|
2029
2167
|
- PUSD: bridge in TEST-USDC from Base, Solana or Sui (needs that chain's gas, which the user funds; bridge_faucet gives TEST-USDC).
|
|
2030
|
-
No PUSD market is listed yet.
|
|
2168
|
+
No PUSD market is listed yet. bridge_redeem waits for k attestor signatures and submits the release (needs destination-chain gas).
|
|
2031
2169
|
- Vaults: deposit both sides of a market to earn from its strategy; withdraw with vault_shares.
|
|
2032
2170
|
|
|
2033
2171
|
To report how you (or every agent set up on this machine) are doing, call progress (who: "all" for every agent); it also lists what needs doing next.
|
|
@@ -2038,14 +2176,28 @@ Show once per thing, not once per round.
|
|
|
2038
2176
|
|
|
2039
2177
|
Rules: if a tool says a write WAS accepted but unconfirmed, never resend it; re-read state instead.
|
|
2040
2178
|
A refusal ({"error": ...}) costs nothing. Use game_catalog for exact costs and move stats; docs at ${DOCS}/llms-full.txt.`;
|
|
2041
|
-
var
|
|
2042
|
-
To play, trade or bridge, run the local server with a wallet phrase: ${DOCS}/ai/mcp
|
|
2179
|
+
var READ_ONLY_INSTRUCTIONS = `This endpoint is READ-ONLY: it explains the protocol and reads live state for any address, free.
|
|
2180
|
+
To play, trade or bridge, run the local server with a wallet phrase: ${DOCS}/ai/mcp
|
|
2181
|
+
|
|
2182
|
+
Paralith on AO (HyperBEAM): Rune Realm (a companion game), two order books, and the PUSD bridge.
|
|
2183
|
+
Everything is TEST-prefixed while unreleased. Pass an address wherever a tool takes one.
|
|
2184
|
+
|
|
2185
|
+
- Game: game_player (account, companions, live battle or hunt), game_catalog (rules, costs, move stats),
|
|
2186
|
+
game_world (factions, arena tiers, open challenges, leaderboard), arena_battle (address or battle_id), hunt_state (address),
|
|
2187
|
+
game_shop_quote (NPC shop prices and stock), companion_market_browse (listings and recent sales).
|
|
2188
|
+
- Markets: exchange_markets / exchange_book / exchange_position on venue "game" (items vs Gold) or "token" (elements vs TEST-RUNE);
|
|
2189
|
+
vault_list, vault_position.
|
|
2190
|
+
- Bridge and DAO: bridge_info, bridge_status, dao_info, dao_proposals, dao_stake_info.
|
|
2191
|
+
- Anything else: progress (one address across everything), read_state (any published key), protocol_docs.
|
|
2192
|
+
|
|
2193
|
+
SHOW IT. Watching beats reading: call show for anything worth seeing (a fight as it happens, a companion, a book, a vault,
|
|
2194
|
+
a bridge transfer). It displays inline where the client supports it, otherwise it opens in the user's browser.
|
|
2195
|
+
|
|
2196
|
+
Docs: ${DOCS}/llms-full.txt.`;
|
|
2043
2197
|
function buildServer() {
|
|
2044
2198
|
const server = new McpServer(
|
|
2045
2199
|
{ name: "paralith", version: VERSION },
|
|
2046
|
-
{ instructions: READ_ONLY ?
|
|
2047
|
-
|
|
2048
|
-
${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
2200
|
+
{ instructions: READ_ONLY ? READ_ONLY_INSTRUCTIONS : INSTRUCTIONS }
|
|
2049
2201
|
);
|
|
2050
2202
|
tool(
|
|
2051
2203
|
server,
|
|
@@ -2072,7 +2224,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
|
2072
2224
|
tool(
|
|
2073
2225
|
server,
|
|
2074
2226
|
"read_state",
|
|
2075
|
-
`Read any published key of any process, free: /<process>~process@1.0/now/<key>. process is an id or an alias (${Object.keys(PROCESS_ALIASES).join(", ")}). Returns null when absent.`,
|
|
2227
|
+
`Read any published key of any process, free: /<process>~process@1.0/now/<key>. process is an id or an alias (${Object.keys(PROCESS_ALIASES).join(", ")}). Returns null when absent. Common keys: game: player-<address>, catalogref (the catalog's id, not catalog), leaderboard, factions, arenatiers, challenges, economy, economybook, market, markethistory, battlefleet, huntconfig, access. rune and *-token: balance-<address>, tokeninfo (not info), totalsupply. pusd: balance-<address>, bridgeinfo, withdrawals. dao: daoinfo, proposals. game-venue and token-venue: balance-<address>, vaultinfo, vaultpairs.`,
|
|
2076
2228
|
{ process: z9.string(), key: z9.string() },
|
|
2077
2229
|
async ({ process: process2, key }) => readKey(process2, key),
|
|
2078
2230
|
READ
|
|
@@ -2080,7 +2232,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
|
2080
2232
|
tool(
|
|
2081
2233
|
server,
|
|
2082
2234
|
"send_message",
|
|
2083
|
-
"Escape hatch: sign and send a raw message to any process and return its reply. Prefer the specific tools; use this only for an action they do not cover. Tag values must be strings; never use a tag named Target.",
|
|
2235
|
+
"Escape hatch: sign and send a raw message to any process and return its reply. Prefer the specific tools; use this only for an action they do not cover. Tag values must be strings; never use a tag named Target, Signature or Epoch.",
|
|
2084
2236
|
{
|
|
2085
2237
|
process: z9.string(),
|
|
2086
2238
|
tags: z9.record(z9.string(), z9.string()).describe('e.g. {"Action": "Pass.Info"}'),
|
|
@@ -2088,7 +2240,8 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
|
2088
2240
|
push_outbox: z9.boolean().optional().describe("Deliver the messages this one emits to other processes (e.g. a Transfer to a process).")
|
|
2089
2241
|
},
|
|
2090
2242
|
async ({ process: process2, tags: tags2, data, push_outbox }) => {
|
|
2091
|
-
|
|
2243
|
+
const reserved = Object.keys(tags2).find((k) => ["target", "signature", "epoch"].includes(k.toLowerCase().replace(/[-_]/g, "")));
|
|
2244
|
+
if (reserved) throw new Error(`${reserved} is not a usable tag name (Target, Signature and Epoch are reserved).`);
|
|
2092
2245
|
const { api } = await runtime();
|
|
2093
2246
|
return api.rawSend(
|
|
2094
2247
|
Object.entries(tags2).map(([name, value]) => ({ name, value })),
|