paralith-mcp 0.4.5 → 0.4.7
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 +6 -6
- package/dist/client.mjs +1 -1
- package/dist/graph.json +22 -22
- package/dist/http.mjs +239 -86
- package/dist/server.mjs +248 -89
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -506,6 +506,65 @@ var init_runtime = __esm({
|
|
|
506
506
|
}
|
|
507
507
|
});
|
|
508
508
|
|
|
509
|
+
// package.json
|
|
510
|
+
var package_default;
|
|
511
|
+
var init_package = __esm({
|
|
512
|
+
"package.json"() {
|
|
513
|
+
package_default = {
|
|
514
|
+
name: "paralith-mcp",
|
|
515
|
+
version: "0.4.7",
|
|
516
|
+
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).",
|
|
517
|
+
license: "UNLICENSED",
|
|
518
|
+
homepage: "https://docs.paralithmarkets.com/ai/mcp",
|
|
519
|
+
keywords: [
|
|
520
|
+
"mcp",
|
|
521
|
+
"model-context-protocol",
|
|
522
|
+
"ao",
|
|
523
|
+
"hyperbeam",
|
|
524
|
+
"arweave",
|
|
525
|
+
"game",
|
|
526
|
+
"agent"
|
|
527
|
+
],
|
|
528
|
+
type: "module",
|
|
529
|
+
bin: {
|
|
530
|
+
"paralith-mcp": "dist/server.mjs",
|
|
531
|
+
"paralith-bridge": "dist/bridge.mjs"
|
|
532
|
+
},
|
|
533
|
+
files: [
|
|
534
|
+
"dist",
|
|
535
|
+
"README.md"
|
|
536
|
+
],
|
|
537
|
+
publishConfig: {
|
|
538
|
+
access: "public"
|
|
539
|
+
},
|
|
540
|
+
scripts: {
|
|
541
|
+
build: "node scripts/build-client.mjs && node scripts/tools-doc.mjs",
|
|
542
|
+
"tools-doc": "node scripts/tools-doc.mjs",
|
|
543
|
+
start: "node src/server.mjs",
|
|
544
|
+
"start:http": "node src/http.mjs",
|
|
545
|
+
test: "node --test test/*.test.mjs"
|
|
546
|
+
},
|
|
547
|
+
engines: {
|
|
548
|
+
node: ">=20"
|
|
549
|
+
},
|
|
550
|
+
dependencies: {
|
|
551
|
+
"@modelcontextprotocol/ext-apps": "^2.0.3",
|
|
552
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
553
|
+
"@mysten/sui": "^2.34.0",
|
|
554
|
+
"@scure/bip39": "^2.4.0",
|
|
555
|
+
"@solana/web3.js": "^1.99.0",
|
|
556
|
+
"arweave-mnemonic-keys": "^0.0.9",
|
|
557
|
+
lodash: "^4.18.1",
|
|
558
|
+
viem: "^2.57.3",
|
|
559
|
+
zod: "^4.6.5"
|
|
560
|
+
},
|
|
561
|
+
devDependencies: {
|
|
562
|
+
esbuild: "^0.28.2"
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
509
568
|
// src/update.mjs
|
|
510
569
|
function check(now) {
|
|
511
570
|
checkedAt = now;
|
|
@@ -523,7 +582,8 @@ function updateNote(now = Date.now()) {
|
|
|
523
582
|
var VERSION, CHECK_MS, NOTE_MS, latest, checkedAt, notedAt, newer;
|
|
524
583
|
var init_update = __esm({
|
|
525
584
|
"src/update.mjs"() {
|
|
526
|
-
|
|
585
|
+
init_package();
|
|
586
|
+
VERSION = package_default.version;
|
|
527
587
|
CHECK_MS = 6 * 36e5;
|
|
528
588
|
NOTE_MS = 36e5;
|
|
529
589
|
latest = null;
|
|
@@ -651,13 +711,20 @@ function side(s) {
|
|
|
651
711
|
freeMovesUsed: Object.keys(s.freeUsed ?? {})
|
|
652
712
|
};
|
|
653
713
|
}
|
|
714
|
+
function sideOf(b, address) {
|
|
715
|
+
if (!address) return null;
|
|
716
|
+
if ((b.accepterAddress ?? b.accepter?.address) === address) return "accepter";
|
|
717
|
+
if ((b.challengerAddress ?? b.challenger?.address) === address) return "challenger";
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
654
720
|
function battle(b, address) {
|
|
655
721
|
if (!b || typeof b !== "object") return b;
|
|
656
|
-
const mine = b
|
|
722
|
+
const mine = sideOf(b, address);
|
|
657
723
|
const theirs = mine === "accepter" ? "challenger" : "accepter";
|
|
724
|
+
const label = (s) => !mine ? s : s === mine ? "you" : "opponent";
|
|
658
725
|
const turns = (b.turns ?? []).filter((t) => t.round === b.round - 1 || t.round === b.round).map((t) => ({
|
|
659
726
|
round: t.round,
|
|
660
|
-
by: t.attacker
|
|
727
|
+
by: label(t.attacker),
|
|
661
728
|
move: t.move,
|
|
662
729
|
damage: t.healthDamage,
|
|
663
730
|
shieldDamage: t.shieldDamage,
|
|
@@ -666,6 +733,7 @@ function battle(b, address) {
|
|
|
666
733
|
superEffective: t.superEffective,
|
|
667
734
|
notEffective: t.notEffective
|
|
668
735
|
}));
|
|
736
|
+
const sides = mine ? { you: side(b[mine]), opponent: side(b[theirs]) } : { challenger: side(b.challenger), accepter: side(b.accepter) };
|
|
669
737
|
return {
|
|
670
738
|
id: b.id,
|
|
671
739
|
kind: b.kind,
|
|
@@ -673,8 +741,7 @@ function battle(b, address) {
|
|
|
673
741
|
round: b.round,
|
|
674
742
|
...b.winner ? { winner: b.winner } : {},
|
|
675
743
|
...b.waitingOn?.length ? { waitingOn: b.waitingOn } : {},
|
|
676
|
-
|
|
677
|
-
opponent: side(b[theirs]),
|
|
744
|
+
...sides,
|
|
678
745
|
recentTurns: turns
|
|
679
746
|
};
|
|
680
747
|
}
|
|
@@ -702,18 +769,25 @@ async function me() {
|
|
|
702
769
|
const { api, wallets } = await runtime();
|
|
703
770
|
return { api, address: wallets?.ao.address };
|
|
704
771
|
}
|
|
705
|
-
async function player2() {
|
|
706
|
-
const { api, address } = await me();
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
772
|
+
async function player2(address) {
|
|
773
|
+
const { api, address: mine } = await me();
|
|
774
|
+
const who = address ?? mine;
|
|
775
|
+
if (!who) throw new Error("This server has no wallet; pass an address.");
|
|
776
|
+
const p = await api.readPlayer(who);
|
|
777
|
+
if (!p) throw new Error(address ? `${address} has no Rune Realm account.` : "This wallet has no Rune Realm account yet. Call game_join first.");
|
|
710
778
|
return p;
|
|
711
779
|
}
|
|
780
|
+
async function findBattle(id) {
|
|
781
|
+
const fleet = await readKey("game", "battlefleet").catch(() => null);
|
|
782
|
+
const workers = (fleet?.workers ?? []).map((w) => w.workerProcessId).filter(Boolean);
|
|
783
|
+
const found = await Promise.all([readKey("game", `battle-op-${id}`), ...workers.map((w) => readKey(w, `battle-${id}`))].map((read) => read.catch(() => null)));
|
|
784
|
+
return found.find((b) => b && typeof b === "object") ?? null;
|
|
785
|
+
}
|
|
712
786
|
function registerGame(server) {
|
|
713
787
|
tool(
|
|
714
788
|
server,
|
|
715
789
|
"game_player",
|
|
716
|
-
"Read a Rune Realm player record (yours by default): gold,
|
|
790
|
+
"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.",
|
|
717
791
|
{ address: z2.string().optional().describe("Another player's AO address; omit for yours.") },
|
|
718
792
|
async ({ address }) => {
|
|
719
793
|
const { api, address: mine } = await me();
|
|
@@ -729,7 +803,13 @@ function registerGame(server) {
|
|
|
729
803
|
"game_catalog",
|
|
730
804
|
"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.",
|
|
731
805
|
{},
|
|
732
|
-
async () =>
|
|
806
|
+
async () => {
|
|
807
|
+
const catalog = await (await me()).api.readCatalog();
|
|
808
|
+
if (catalog?.levelUp) {
|
|
809
|
+
catalog.levelUp = { ...catalog.levelUp, costDivisor: 32, cost: "ceil(targetLevel^2 / 32) Rune" };
|
|
810
|
+
}
|
|
811
|
+
return catalog;
|
|
812
|
+
},
|
|
733
813
|
READ
|
|
734
814
|
);
|
|
735
815
|
tool(
|
|
@@ -751,7 +831,11 @@ function registerGame(server) {
|
|
|
751
831
|
factions: factions?.map(({ members, ...f }) => f),
|
|
752
832
|
arenaTiers: tiers,
|
|
753
833
|
openChallenges: challenges,
|
|
754
|
-
leaderboard: leaderboard?.slice(0, 10)
|
|
834
|
+
leaderboard: leaderboard?.slice(0, 10).map(({ monster, ...row }) => {
|
|
835
|
+
if (!monster) return row;
|
|
836
|
+
const { exp, energy, happiness, ...companion2 } = companion(monster);
|
|
837
|
+
return { ...row, companion: companion2 };
|
|
838
|
+
})
|
|
755
839
|
};
|
|
756
840
|
},
|
|
757
841
|
READ
|
|
@@ -768,7 +852,7 @@ function registerGame(server) {
|
|
|
768
852
|
tool(
|
|
769
853
|
server,
|
|
770
854
|
"game_companion",
|
|
771
|
-
"Care for and manage companions. feed: 1 berry, +10
|
|
855
|
+
"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.",
|
|
772
856
|
{
|
|
773
857
|
action: z2.enum(["feed", "play", "quest", "claim", "adopt", "store", "retrieve", "set_active", "transfer"]),
|
|
774
858
|
monster_id: z2.string().optional().describe("Companion id; omit for the active one."),
|
|
@@ -804,7 +888,7 @@ function registerGame(server) {
|
|
|
804
888
|
tool(
|
|
805
889
|
server,
|
|
806
890
|
"game_level_up",
|
|
807
|
-
"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.",
|
|
891
|
+
"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.",
|
|
808
892
|
{ attack: z2.number().int().min(0), defense: z2.number().int().min(0), speed: z2.number().int().min(0), health: z2.number().int().min(0) },
|
|
809
893
|
async (points) => (await me()).api.levelUp(points),
|
|
810
894
|
WRITE,
|
|
@@ -813,7 +897,7 @@ function registerGame(server) {
|
|
|
813
897
|
tool(
|
|
814
898
|
server,
|
|
815
899
|
"game_learn_move",
|
|
816
|
-
"Answer a pending move offer (every 5th level). Give replace to swap that move out; omit replace to decline. Final either way.",
|
|
900
|
+
"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.",
|
|
817
901
|
{ move: z2.string().describe("The offered move, exactly as in the player record."), replace: z2.string().optional() },
|
|
818
902
|
async ({ move, replace }) => (await me()).api.learnMove(move, replace),
|
|
819
903
|
WRITE,
|
|
@@ -837,27 +921,31 @@ function registerGame(server) {
|
|
|
837
921
|
WRITE,
|
|
838
922
|
asPlayer
|
|
839
923
|
);
|
|
924
|
+
tool(
|
|
925
|
+
server,
|
|
926
|
+
"game_shop_quote",
|
|
927
|
+
"The NPC realm shop's current Gold prices (bid/ask) and stock for berries, Scrolls and Rune. Free, unsigned.",
|
|
928
|
+
{},
|
|
929
|
+
async () => (await me()).api.readEconomy(),
|
|
930
|
+
READ
|
|
931
|
+
);
|
|
840
932
|
tool(
|
|
841
933
|
server,
|
|
842
934
|
"game_shop",
|
|
843
|
-
"
|
|
935
|
+
"Buy from or sell to the NPC realm shop for Gold (prices: game_shop_quote). Set max_total/min_total to bound slippage.",
|
|
844
936
|
{
|
|
845
|
-
action: z2.enum(["
|
|
846
|
-
item: z2.
|
|
847
|
-
quantity: z2.number().int().positive()
|
|
937
|
+
action: z2.enum(["buy", "sell"]),
|
|
938
|
+
item: z2.enum(SHOP_ITEMS),
|
|
939
|
+
quantity: z2.number().int().positive(),
|
|
848
940
|
max_total: z2.number().int().optional(),
|
|
849
941
|
min_total: z2.number().int().optional()
|
|
850
942
|
},
|
|
851
|
-
async ({ action, item, quantity, max_total, min_total }) =>
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
need(quantity, "quantity"),
|
|
858
|
-
{ maxTotal: max_total, minTotal: min_total }
|
|
859
|
-
);
|
|
860
|
-
},
|
|
943
|
+
async ({ action, item, quantity, max_total, min_total }) => (await me()).api.tradeGameShop(
|
|
944
|
+
action,
|
|
945
|
+
item,
|
|
946
|
+
quantity,
|
|
947
|
+
{ maxTotal: max_total, minTotal: min_total }
|
|
948
|
+
),
|
|
861
949
|
WRITE,
|
|
862
950
|
asPlayer
|
|
863
951
|
);
|
|
@@ -903,13 +991,23 @@ function registerGame(server) {
|
|
|
903
991
|
tool(
|
|
904
992
|
server,
|
|
905
993
|
"arena_battle",
|
|
906
|
-
"Read
|
|
907
|
-
{
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
994
|
+
"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).",
|
|
995
|
+
{
|
|
996
|
+
address: z2.string().optional().describe("Whose live battle; omit for yours."),
|
|
997
|
+
battle_id: z2.string().optional().describe("A specific battle, e.g. from game_player arena.liveBattle.")
|
|
998
|
+
},
|
|
999
|
+
async ({ address, battle_id }) => {
|
|
1000
|
+
const { api, address: mine } = await me();
|
|
1001
|
+
const viewer = address ?? mine;
|
|
1002
|
+
if (battle_id) {
|
|
1003
|
+
const b = await findBattle(battle_id);
|
|
1004
|
+
return b ? seenBy(b, viewer) : { battle: null, note: `No battle ${battle_id} on the game or any battle worker.` };
|
|
1005
|
+
}
|
|
1006
|
+
if (!viewer) throw new Error("This server has no wallet; pass address or battle_id.");
|
|
1007
|
+
const p = await player2(viewer);
|
|
1008
|
+
const id = p.battleFleet?.battleId ?? p.activeBattleId;
|
|
1009
|
+
if (!id) return { battle: null, note: address ? `${address} has no live battle.` : "No live battle. Use arena_fight to start one." };
|
|
1010
|
+
return seenBy(p.battle?.id === id ? p.battle : await api.readBattle(id), viewer);
|
|
913
1011
|
},
|
|
914
1012
|
READ,
|
|
915
1013
|
battleView
|
|
@@ -947,7 +1045,7 @@ function registerGame(server) {
|
|
|
947
1045
|
tool(
|
|
948
1046
|
server,
|
|
949
1047
|
"hunt_begin",
|
|
950
|
-
"Start a hunt with a companion.
|
|
1048
|
+
"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.",
|
|
951
1049
|
{ monster_id: z2.string().describe("Companion to hunt with (see game_player).") },
|
|
952
1050
|
async ({ monster_id }) => (await me()).api.beginHunt(monster_id),
|
|
953
1051
|
WRITE,
|
|
@@ -956,14 +1054,14 @@ function registerGame(server) {
|
|
|
956
1054
|
tool(
|
|
957
1055
|
server,
|
|
958
1056
|
"hunt_state",
|
|
959
|
-
"Read
|
|
960
|
-
{},
|
|
961
|
-
async () => {
|
|
1057
|
+
"Read a live hunt run (yours by default): encounter, wild creature, round, status. Free.",
|
|
1058
|
+
{ address: z2.string().optional().describe("Another player's AO address; omit for yours.") },
|
|
1059
|
+
async ({ address }) => {
|
|
962
1060
|
const { api } = await me();
|
|
963
|
-
const route = (await player2()).hunt;
|
|
964
|
-
return route ? api.readHunt(route) : { hunt: null, note: "No live hunt. Use hunt_begin." };
|
|
1061
|
+
const route = (await player2(address)).hunt;
|
|
1062
|
+
return route ? api.readHunt(route) : { hunt: null, note: address ? `${address} has no live hunt.` : "No live hunt. Use hunt_begin." };
|
|
965
1063
|
},
|
|
966
|
-
|
|
1064
|
+
READ,
|
|
967
1065
|
huntView
|
|
968
1066
|
);
|
|
969
1067
|
tool(
|
|
@@ -998,12 +1096,24 @@ function registerGame(server) {
|
|
|
998
1096
|
WRITE,
|
|
999
1097
|
huntView
|
|
1000
1098
|
);
|
|
1099
|
+
tool(
|
|
1100
|
+
server,
|
|
1101
|
+
"companion_market_browse",
|
|
1102
|
+
"The companion market (priced in in-game Rune): current listings and recent sales. Free, unsigned.",
|
|
1103
|
+
{},
|
|
1104
|
+
async () => {
|
|
1105
|
+
const { api } = await me();
|
|
1106
|
+
const [listings, history] = await Promise.all([api.readMarket(), api.readMarketHistory()]);
|
|
1107
|
+
return { listings, history };
|
|
1108
|
+
},
|
|
1109
|
+
READ
|
|
1110
|
+
);
|
|
1001
1111
|
tool(
|
|
1002
1112
|
server,
|
|
1003
1113
|
"companion_market",
|
|
1004
|
-
"
|
|
1114
|
+
"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.",
|
|
1005
1115
|
{
|
|
1006
|
-
action: z2.enum(["
|
|
1116
|
+
action: z2.enum(["list", "buy", "cancel"]),
|
|
1007
1117
|
monster_id: z2.string().optional(),
|
|
1008
1118
|
price: z2.number().int().min(1).max(1e6).optional(),
|
|
1009
1119
|
listing_id: z2.string().optional()
|
|
@@ -1011,8 +1121,6 @@ function registerGame(server) {
|
|
|
1011
1121
|
async ({ action, monster_id, price, listing_id }) => {
|
|
1012
1122
|
const { api } = await me();
|
|
1013
1123
|
switch (action) {
|
|
1014
|
-
case "browse":
|
|
1015
|
-
return { listings: await api.readMarket(), history: await api.readMarketHistory() };
|
|
1016
1124
|
case "list":
|
|
1017
1125
|
return api.listMonster(need(monster_id, "monster_id"), need(price, "price"));
|
|
1018
1126
|
case "buy":
|
|
@@ -1040,7 +1148,7 @@ function registerGame(server) {
|
|
|
1040
1148
|
tool(
|
|
1041
1149
|
server,
|
|
1042
1150
|
"element_claim",
|
|
1043
|
-
"Claim today's free TEST element tokens
|
|
1151
|
+
"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.",
|
|
1044
1152
|
{},
|
|
1045
1153
|
async () => {
|
|
1046
1154
|
const { api, address } = await me();
|
|
@@ -1050,9 +1158,6 @@ function registerGame(server) {
|
|
|
1050
1158
|
WRITE
|
|
1051
1159
|
);
|
|
1052
1160
|
}
|
|
1053
|
-
async function battleView(b) {
|
|
1054
|
-
return b?.id ? battle(b, (await me()).address) : b;
|
|
1055
|
-
}
|
|
1056
1161
|
async function attackView(p) {
|
|
1057
1162
|
if (!p?.address) return p;
|
|
1058
1163
|
const { address, gold, battlesRemaining, wins, losses, arenaLast } = p;
|
|
@@ -1069,7 +1174,7 @@ function need(value, name) {
|
|
|
1069
1174
|
if (value === void 0 || value === null || value === "") throw new Error(`${name} is required for this action`);
|
|
1070
1175
|
return value;
|
|
1071
1176
|
}
|
|
1072
|
-
var FACTIONS, BERRIES, asPlayer, huntView;
|
|
1177
|
+
var FACTIONS, BERRIES, SHOP_ITEMS, seenBy, asPlayer, battleView, huntView;
|
|
1073
1178
|
var init_game = __esm({
|
|
1074
1179
|
"src/game.mjs"() {
|
|
1075
1180
|
init_runtime();
|
|
@@ -1077,7 +1182,10 @@ var init_game = __esm({
|
|
|
1077
1182
|
init_views();
|
|
1078
1183
|
FACTIONS = ["Sky Nomads", "Aqua Guardians", "Inferno Blades", "Stone Titans"];
|
|
1079
1184
|
BERRIES = ["air_berry", "fire_berry", "rock_berry", "water_berry"];
|
|
1185
|
+
SHOP_ITEMS = [...BERRIES, "scroll", "rune"];
|
|
1186
|
+
seenBy = (b, address) => b && typeof b === "object" ? Object.defineProperty(b, "viewer", { value: address, enumerable: false, configurable: true }) : b;
|
|
1080
1187
|
asPlayer = (r) => r?.address && r?.inventory ? player(r) : r;
|
|
1188
|
+
battleView = (b) => b?.id ? battle(b, b.viewer) : b;
|
|
1081
1189
|
huntView = (r) => r?.runId ? hunt(r) : asPlayer(r);
|
|
1082
1190
|
}
|
|
1083
1191
|
});
|
|
@@ -1102,9 +1210,14 @@ function toWire(t, m, price, quantity) {
|
|
|
1102
1210
|
const baseAtoms = parseAmount(quantity, baseDen);
|
|
1103
1211
|
if (baseAtoms % lot !== 0n) throw new Error(`Quantity moves in steps of ${formatAtoms(lot, baseDen)} ${m.base}.`);
|
|
1104
1212
|
const perUnit = parseAmount(price, t.den(m.quote));
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1213
|
+
const unit = 10n ** BigInt(baseDen);
|
|
1214
|
+
const tick = BigInt(Math.max(1, Number(m.tick ?? 1)));
|
|
1215
|
+
const scaled = perUnit * lot;
|
|
1216
|
+
if (scaled % unit !== 0n || scaled / unit % tick !== 0n) {
|
|
1217
|
+
const step = tick * unit % lot === 0n ? formatAtoms(tick * unit / lot, t.den(m.quote)) : null;
|
|
1218
|
+
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}.`);
|
|
1219
|
+
}
|
|
1220
|
+
return { lots: (baseAtoms / lot).toString(), perLot: (scaled / unit).toString() };
|
|
1108
1221
|
}
|
|
1109
1222
|
function registerExchange(server) {
|
|
1110
1223
|
tool(
|
|
@@ -1129,22 +1242,21 @@ function registerExchange(server) {
|
|
|
1129
1242
|
const { api } = await runtime();
|
|
1130
1243
|
const book = (await api.readVenueBook(t.process))?.[m.id];
|
|
1131
1244
|
if (!book) return { market: m.id, book: null };
|
|
1132
|
-
const
|
|
1133
|
-
const
|
|
1134
|
-
const level = (l) => ({ price: unitPrice(l.price), quantity: units(l.quantity), orders: l.orders });
|
|
1245
|
+
const price = (perLot) => unitPrice(t, m, perLot);
|
|
1246
|
+
const level = (l) => ({ price: price(l.price), quantity: units(t, m, l.quantity), orders: l.orders });
|
|
1135
1247
|
return {
|
|
1136
1248
|
market: m.id,
|
|
1137
1249
|
units: `price in ${m.quote} per 1 ${m.base}; quantity in ${m.base}`,
|
|
1138
|
-
bestBid: book.bestBid ?
|
|
1139
|
-
bestAsk: book.bestAsk ?
|
|
1250
|
+
bestBid: book.bestBid ? price(book.bestBid) : null,
|
|
1251
|
+
bestAsk: book.bestAsk ? price(book.bestAsk) : null,
|
|
1140
1252
|
bids: (book.depth?.bids ?? []).slice(0, 10).map(level),
|
|
1141
1253
|
asks: (book.depth?.asks ?? []).slice(0, 10).map(level),
|
|
1142
|
-
band: book.band && { low:
|
|
1254
|
+
band: book.band && { low: price(book.band.low), high: price(book.band.high) },
|
|
1143
1255
|
today: book.candles?.at(-1) && {
|
|
1144
|
-
open:
|
|
1145
|
-
high:
|
|
1146
|
-
low:
|
|
1147
|
-
close:
|
|
1256
|
+
open: price(book.candles.at(-1).o),
|
|
1257
|
+
high: price(book.candles.at(-1).h),
|
|
1258
|
+
low: price(book.candles.at(-1).l),
|
|
1259
|
+
close: price(book.candles.at(-1).c),
|
|
1148
1260
|
trades: book.candles.at(-1).n
|
|
1149
1261
|
}
|
|
1150
1262
|
};
|
|
@@ -1154,7 +1266,7 @@ function registerExchange(server) {
|
|
|
1154
1266
|
tool(
|
|
1155
1267
|
server,
|
|
1156
1268
|
"exchange_position",
|
|
1157
|
-
"Balances at a venue (free,
|
|
1269
|
+
"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.",
|
|
1158
1270
|
{ venue: VENUE, address: z3.string().optional() },
|
|
1159
1271
|
async ({ venue, address }) => {
|
|
1160
1272
|
const t = await terms(venue);
|
|
@@ -1162,7 +1274,33 @@ function registerExchange(server) {
|
|
|
1162
1274
|
const who = address ?? wallets?.ao.address;
|
|
1163
1275
|
if (!who) throw new Error("This server has no wallet; pass an address.");
|
|
1164
1276
|
const p = await api.readVenuePosition(t.process, who);
|
|
1165
|
-
|
|
1277
|
+
const market = (row) => t.markets[row.market] ?? marketFor(t, row.item);
|
|
1278
|
+
const order = ({ lot, ...o }) => {
|
|
1279
|
+
const m = { ...market(o), ...lot ? { lot } : {} };
|
|
1280
|
+
return {
|
|
1281
|
+
...o,
|
|
1282
|
+
price: unitPrice(t, m, o.price),
|
|
1283
|
+
quantity: units(t, m, o.quantity),
|
|
1284
|
+
...o.remaining !== void 0 ? { remaining: units(t, m, o.remaining) } : {}
|
|
1285
|
+
};
|
|
1286
|
+
};
|
|
1287
|
+
const fill = (f) => {
|
|
1288
|
+
const m = market(f);
|
|
1289
|
+
return {
|
|
1290
|
+
...f,
|
|
1291
|
+
price: unitPrice(t, m, f.price),
|
|
1292
|
+
quantity: units(t, m, f.quantity),
|
|
1293
|
+
...f.fee !== void 0 ? { fee: formatAtoms(BigInt(f.fee), t.den(m.quote)) } : {}
|
|
1294
|
+
};
|
|
1295
|
+
};
|
|
1296
|
+
return {
|
|
1297
|
+
account: p.account ?? who,
|
|
1298
|
+
units: "price in quote per 1 base unit; quantity in base units; fee in quote",
|
|
1299
|
+
free: human(t, p.free),
|
|
1300
|
+
...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))]))])) } : {},
|
|
1301
|
+
orders: (p.orders ?? []).map(order),
|
|
1302
|
+
fills: (p.fills ?? []).map(fill)
|
|
1303
|
+
};
|
|
1166
1304
|
},
|
|
1167
1305
|
READ
|
|
1168
1306
|
);
|
|
@@ -1260,13 +1398,15 @@ function registerExchange(server) {
|
|
|
1260
1398
|
WRITE
|
|
1261
1399
|
);
|
|
1262
1400
|
}
|
|
1263
|
-
var VENUE, venueProcess, human;
|
|
1401
|
+
var VENUE, venueProcess, unitPrice, units, human;
|
|
1264
1402
|
var init_exchange = __esm({
|
|
1265
1403
|
"src/exchange.mjs"() {
|
|
1266
1404
|
init_runtime();
|
|
1267
1405
|
init_tool();
|
|
1268
1406
|
VENUE = z3.enum(["game", "token"]).describe("game: items vs Gold. token: element tokens vs TEST-RUNE.");
|
|
1269
1407
|
venueProcess = (venue) => venue === "game" ? graph.processes.internalVenue : graph.processes.externalVenue;
|
|
1408
|
+
unitPrice = (t, m, perLot) => formatAtoms(BigInt(perLot) * 10n ** BigInt(t.den(m.base)) / BigInt(m.lot), t.den(m.quote));
|
|
1409
|
+
units = (t, m, lots) => formatAtoms(BigInt(lots) * BigInt(m.lot), t.den(m.base));
|
|
1270
1410
|
human = (t, free) => Object.fromEntries(Object.entries(free ?? {}).map(([asset, atoms]) => [asset, formatAtoms(atoms, t.den(asset))]));
|
|
1271
1411
|
}
|
|
1272
1412
|
});
|
|
@@ -2279,9 +2419,7 @@ import { z as z9 } from "zod";
|
|
|
2279
2419
|
function buildServer() {
|
|
2280
2420
|
const server = new McpServer(
|
|
2281
2421
|
{ name: "paralith", version: VERSION },
|
|
2282
|
-
{ instructions: READ_ONLY ?
|
|
2283
|
-
|
|
2284
|
-
${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
2422
|
+
{ instructions: READ_ONLY ? READ_ONLY_INSTRUCTIONS : INSTRUCTIONS }
|
|
2285
2423
|
);
|
|
2286
2424
|
tool(
|
|
2287
2425
|
server,
|
|
@@ -2308,7 +2446,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
|
2308
2446
|
tool(
|
|
2309
2447
|
server,
|
|
2310
2448
|
"read_state",
|
|
2311
|
-
`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.`,
|
|
2449
|
+
`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.`,
|
|
2312
2450
|
{ process: z9.string(), key: z9.string() },
|
|
2313
2451
|
async ({ process: process2, key }) => readKey(process2, key),
|
|
2314
2452
|
READ
|
|
@@ -2316,7 +2454,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
|
2316
2454
|
tool(
|
|
2317
2455
|
server,
|
|
2318
2456
|
"send_message",
|
|
2319
|
-
"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.",
|
|
2457
|
+
"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.",
|
|
2320
2458
|
{
|
|
2321
2459
|
process: z9.string(),
|
|
2322
2460
|
tags: z9.record(z9.string(), z9.string()).describe('e.g. {"Action": "Pass.Info"}'),
|
|
@@ -2324,7 +2462,8 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
|
2324
2462
|
push_outbox: z9.boolean().optional().describe("Deliver the messages this one emits to other processes (e.g. a Transfer to a process).")
|
|
2325
2463
|
},
|
|
2326
2464
|
async ({ process: process2, tags: tags2, data, push_outbox }) => {
|
|
2327
|
-
|
|
2465
|
+
const reserved = Object.keys(tags2).find((k) => ["target", "signature", "epoch"].includes(k.toLowerCase().replace(/[-_]/g, "")));
|
|
2466
|
+
if (reserved) throw new Error(`${reserved} is not a usable tag name (Target, Signature and Epoch are reserved).`);
|
|
2328
2467
|
const { api } = await runtime();
|
|
2329
2468
|
return api.rawSend(
|
|
2330
2469
|
Object.entries(tags2).map(([name, value]) => ({ name, value })),
|
|
@@ -2354,7 +2493,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
|
2354
2493
|
registerProgress(server);
|
|
2355
2494
|
return server;
|
|
2356
2495
|
}
|
|
2357
|
-
var DOCS, INSTRUCTIONS,
|
|
2496
|
+
var DOCS, INSTRUCTIONS, READ_ONLY_INSTRUCTIONS;
|
|
2358
2497
|
var init_build = __esm({
|
|
2359
2498
|
"src/build.mjs"() {
|
|
2360
2499
|
init_runtime();
|
|
@@ -2373,29 +2512,33 @@ var init_build = __esm({
|
|
|
2373
2512
|
Everything is TEST-prefixed while unreleased. Reads are free; every write is a signed message (a few seconds round trip, longer under load).
|
|
2374
2513
|
|
|
2375
2514
|
Start: wallet -> game_player. No account? game_join (pick a faction).
|
|
2376
|
-
Core loop: game_daily once a day; game_companion feed/play
|
|
2377
|
-
game_level_up when exp allows; game_learn_move when an offer is pending.
|
|
2515
|
+
Core loop: game_daily once a day; game_companion feed/play keep energy and happiness up, quest then claim earns exp and Gold;
|
|
2516
|
+
game_level_up when exp allows; game_learn_move when an offer is pending (it expires at the next level-up).
|
|
2378
2517
|
Arena: arena_enter (25 energy + 25 happiness, 4 fights), arena_fight mode=bot, then arena_attack each round
|
|
2379
2518
|
(read arena_battle for moves and uses left; Rally and Mend are free once each), arena_end leave when done.
|
|
2380
|
-
Hunt: hunt_begin
|
|
2519
|
+
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)
|
|
2520
|
+
-> hunt_act search -> attack until defeated -> capture (1-3 Rune + a Scroll) or end. One encounter per hunt.
|
|
2381
2521
|
Markets: exchange_markets / exchange_book / exchange_order on venue "game" (items vs Gold) or "token" (elements vs TEST-RUNE).
|
|
2382
2522
|
Vaults (pooled liquidity, one amm/active/grid per market): vault_list, vault_position, vault_deposit, vault_shares (withdraw/lock/burn), vault_fund, vault_crank.
|
|
2383
2523
|
Rune: rune_move withdraw (game -> token) / deposit (token -> game). Bridge: bridge_balances, bridge_faucet, bridge_deposit, bridge_redeem.
|
|
2384
2524
|
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.
|
|
2385
2525
|
|
|
2386
2526
|
HOW RESOURCES FLOW. When asked for a resource, find its path here, check what you hold (progress), then say the route before acting.
|
|
2387
|
-
- Gold (in-game only, never leaves the game). Earn:
|
|
2388
|
-
selling berries/Scrolls/Rune to the shop (game_shop sell) or on the game venue.
|
|
2527
|
+
- Gold (in-game only, never leaves the game). Earn: quest claims and arena bot wins, which share one 60 Gold allowance per 20 h
|
|
2528
|
+
(play pays happiness only); selling berries/Scrolls/Rune to the shop (game_shop sell) or on the game venue.
|
|
2529
|
+
Spend: arena stakes, shop buys, game-venue buys.
|
|
2389
2530
|
- Berries and Scrolls. Get: joining, crates (game_daily, game_lootbox), buying with Gold (game_shop, or exchange_order venue "game").
|
|
2390
|
-
Use: feed the companion, hunt
|
|
2531
|
+
Use: feed the companion, the hunt entry fee (2 of each berry), arena boosts; a Scroll plus 1-3 Rune for a hunt capture.
|
|
2391
2532
|
- Rune exists twice: in-game Rune and the TEST-RUNE wallet token; rune_move converts 1:1 either way.
|
|
2392
|
-
Get Rune: (1) the shop, only if
|
|
2393
|
-
(3) the token route
|
|
2394
|
-
sell that element for TEST-RUNE -> rune_move deposit.
|
|
2395
|
-
|
|
2533
|
+
Get Rune: (1) the shop, only if game_shop_quote shows an ask (it often only bids); (2) sell a companion on companion_market;
|
|
2534
|
+
(3) the token route: element_claim (free, 1-10 of one element per wallet per UTC day until that element's 2,000/day
|
|
2535
|
+
global cap is spent) -> exchange_order venue "token" sell that element for TEST-RUNE -> rune_move deposit.
|
|
2536
|
+
Public TEST accounts accrue no Rune from game_daily (pass holders only).
|
|
2537
|
+
Spend in-game Rune: game_level_up (ceil(L^2/32) for level L), hunt capture, companion_market buys, game_companion store (1 Rune).
|
|
2538
|
+
TEST-RUNE: token-venue buys, vaults, dao_stake.
|
|
2396
2539
|
- Element tokens (TEST-EARTH/AIR/FIRE/WATER): element_claim daily; trade against TEST-RUNE on the token venue; vault liquidity.
|
|
2397
2540
|
- PUSD: bridge in TEST-USDC from Base, Solana or Sui (needs that chain's gas, which the user funds; bridge_faucet gives TEST-USDC).
|
|
2398
|
-
No PUSD market is listed yet.
|
|
2541
|
+
No PUSD market is listed yet. bridge_redeem waits for k attestor signatures and submits the release (needs destination-chain gas).
|
|
2399
2542
|
- Vaults: deposit both sides of a market to earn from its strategy; withdraw with vault_shares.
|
|
2400
2543
|
|
|
2401
2544
|
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.
|
|
@@ -2406,8 +2549,24 @@ Show once per thing, not once per round.
|
|
|
2406
2549
|
|
|
2407
2550
|
Rules: if a tool says a write WAS accepted but unconfirmed, never resend it; re-read state instead.
|
|
2408
2551
|
A refusal ({"error": ...}) costs nothing. Use game_catalog for exact costs and move stats; docs at ${DOCS}/llms-full.txt.`;
|
|
2409
|
-
|
|
2410
|
-
To play, trade or bridge, run the local server with a wallet phrase: ${DOCS}/ai/mcp
|
|
2552
|
+
READ_ONLY_INSTRUCTIONS = `This endpoint is READ-ONLY: it explains the protocol and reads live state for any address, free.
|
|
2553
|
+
To play, trade or bridge, run the local server with a wallet phrase: ${DOCS}/ai/mcp
|
|
2554
|
+
|
|
2555
|
+
Paralith on AO (HyperBEAM): Rune Realm (a companion game), two order books, and the PUSD bridge.
|
|
2556
|
+
Everything is TEST-prefixed while unreleased. Pass an address wherever a tool takes one.
|
|
2557
|
+
|
|
2558
|
+
- Game: game_player (account, companions, live battle or hunt), game_catalog (rules, costs, move stats),
|
|
2559
|
+
game_world (factions, arena tiers, open challenges, leaderboard), arena_battle (address or battle_id), hunt_state (address),
|
|
2560
|
+
game_shop_quote (NPC shop prices and stock), companion_market_browse (listings and recent sales).
|
|
2561
|
+
- Markets: exchange_markets / exchange_book / exchange_position on venue "game" (items vs Gold) or "token" (elements vs TEST-RUNE);
|
|
2562
|
+
vault_list, vault_position.
|
|
2563
|
+
- Bridge and DAO: bridge_info, bridge_status, dao_info, dao_proposals, dao_stake_info.
|
|
2564
|
+
- Anything else: progress (one address across everything), read_state (any published key), protocol_docs.
|
|
2565
|
+
|
|
2566
|
+
SHOW IT. Watching beats reading: call show for anything worth seeing (a fight as it happens, a companion, a book, a vault,
|
|
2567
|
+
a bridge transfer). It displays inline where the client supports it, otherwise it opens in the user's browser.
|
|
2568
|
+
|
|
2569
|
+
Docs: ${DOCS}/llms-full.txt.`;
|
|
2411
2570
|
}
|
|
2412
2571
|
});
|
|
2413
2572
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paralith-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"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).",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://docs.paralithmarkets.com/ai/mcp",
|