paralith-mcp 0.2.0 → 0.3.1
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 +10 -2
- package/dist/chains.json +94 -43
- package/dist/client.mjs +1 -1
- package/dist/graph.json +2 -2
- package/dist/http.mjs +215 -31
- package/dist/server.mjs +644 -149
- package/package.json +1 -1
package/dist/http.mjs
CHANGED
|
@@ -5,8 +5,8 @@ import http2 from "node:http";
|
|
|
5
5
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
6
|
|
|
7
7
|
// src/runtime.mjs
|
|
8
|
-
import
|
|
9
|
-
import
|
|
8
|
+
import fs3 from "node:fs";
|
|
9
|
+
import path3 from "node:path";
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
11
|
|
|
12
12
|
// ../../backend/native/ans104.mjs
|
|
@@ -135,7 +135,7 @@ import { mnemonicToAccount } from "viem/accounts";
|
|
|
135
135
|
import { Keypair } from "@solana/web3.js";
|
|
136
136
|
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
137
137
|
import { getKeyFromMnemonic } from "arweave-mnemonic-keys";
|
|
138
|
-
var
|
|
138
|
+
var HOME = process.env.PARALITH_MCP_HOME || path.join(os.homedir(), ".paralith", "mcp");
|
|
139
139
|
function readPhrase(env = process.env) {
|
|
140
140
|
const raw = env.PARALITH_MNEMONIC ?? (env.PARALITH_MNEMONIC_FILE ? fs.readFileSync(env.PARALITH_MNEMONIC_FILE, "utf8") : null);
|
|
141
141
|
if (!raw) {
|
|
@@ -162,25 +162,27 @@ function slip10(seed, derivation) {
|
|
|
162
162
|
async function aoJwk(phrase, env) {
|
|
163
163
|
if (env.PARALITH_AO_JWK) return JSON.parse(fs.readFileSync(env.PARALITH_AO_JWK, "utf8"));
|
|
164
164
|
const id = crypto2.createHash("sha256").update(phrase).digest("hex").slice(0, 24);
|
|
165
|
-
const file = path.join(
|
|
165
|
+
const file = path.join(HOME, `ao-${id}.json`);
|
|
166
166
|
if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
167
167
|
const jwk = await getKeyFromMnemonic(phrase);
|
|
168
|
-
fs.mkdirSync(
|
|
168
|
+
fs.mkdirSync(HOME, { recursive: true, mode: 448 });
|
|
169
169
|
fs.writeFileSync(file, JSON.stringify(jwk), { mode: 384 });
|
|
170
170
|
return jwk;
|
|
171
171
|
}
|
|
172
|
-
|
|
172
|
+
function chainWallets(phrase) {
|
|
173
173
|
const evm = mnemonicToAccount(phrase, { path: "m/44'/60'/0'/0/0" });
|
|
174
174
|
const solana = Keypair.fromSeed(slip10(mnemonicToSeedSync(phrase), "m/44'/501'/0'/0'"));
|
|
175
175
|
const sui = Ed25519Keypair.deriveKeypair(phrase, "m/44'/784'/0'/0'/0'");
|
|
176
|
-
const jwk = await aoJwk(phrase, env);
|
|
177
176
|
return {
|
|
178
|
-
ao: { jwk, address: jwkToAddress(jwk) },
|
|
179
177
|
base: { account: evm, address: evm.address },
|
|
180
178
|
solana: { keypair: solana, address: solana.publicKey.toBase58() },
|
|
181
179
|
sui: { keypair: sui, address: sui.getPublicKey().toSuiAddress() }
|
|
182
180
|
};
|
|
183
181
|
}
|
|
182
|
+
async function deriveWallets(phrase, env = process.env) {
|
|
183
|
+
const jwk = await aoJwk(phrase, env);
|
|
184
|
+
return { ao: { jwk, address: jwkToAddress(jwk) }, ...chainWallets(phrase) };
|
|
185
|
+
}
|
|
184
186
|
var publicAddresses = (w) => ({
|
|
185
187
|
ao: w.ao.address,
|
|
186
188
|
base: w.base.address,
|
|
@@ -188,12 +190,32 @@ var publicAddresses = (w) => ({
|
|
|
188
190
|
sui: w.sui.address
|
|
189
191
|
});
|
|
190
192
|
|
|
193
|
+
// src/agents.mjs
|
|
194
|
+
import fs2 from "node:fs";
|
|
195
|
+
import path2 from "node:path";
|
|
196
|
+
var FILE = path2.join(HOME, "agents.json");
|
|
197
|
+
function readAgents() {
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(fs2.readFileSync(FILE, "utf8"));
|
|
200
|
+
} catch {
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function saveAgent(name, entry) {
|
|
205
|
+
const all = readAgents();
|
|
206
|
+
all[name] = { ...all[name], ...entry, addresses: { ...all[name]?.addresses, ...entry.addresses } };
|
|
207
|
+
fs2.mkdirSync(HOME, { recursive: true, mode: 448 });
|
|
208
|
+
fs2.writeFileSync(FILE, `${JSON.stringify(all, null, 2)}
|
|
209
|
+
`);
|
|
210
|
+
return all[name];
|
|
211
|
+
}
|
|
212
|
+
|
|
191
213
|
// src/runtime.mjs
|
|
192
|
-
var HERE =
|
|
193
|
-
var DIST =
|
|
194
|
-
var graph = JSON.parse(
|
|
214
|
+
var HERE = path3.dirname(fileURLToPath(import.meta.url));
|
|
215
|
+
var DIST = fs3.existsSync(path3.join(HERE, "graph.json")) ? HERE : path3.join(HERE, "..", "dist");
|
|
216
|
+
var graph = JSON.parse(fs3.readFileSync(path3.join(DIST, "graph.json"), "utf8"));
|
|
195
217
|
var NODE = graph.node.replace(/\/$/, "");
|
|
196
|
-
var chains = JSON.parse(
|
|
218
|
+
var chains = JSON.parse(fs3.readFileSync(path3.join(DIST, "chains.json"), "utf8"));
|
|
197
219
|
var PROCESS_ALIASES = {
|
|
198
220
|
game: graph.processes.game,
|
|
199
221
|
rune: graph.processes.rune,
|
|
@@ -215,7 +237,8 @@ var loaded = null;
|
|
|
215
237
|
function runtime() {
|
|
216
238
|
loaded ??= (async () => {
|
|
217
239
|
const wallets = READ_ONLY ? null : await deriveWallets(readPhrase());
|
|
218
|
-
|
|
240
|
+
if (wallets && process.env.PARALITH_AGENT) saveAgent(process.env.PARALITH_AGENT, { addresses: publicAddresses(wallets) });
|
|
241
|
+
const api = await import(pathToFileURL(path3.join(DIST, "client.mjs")).href);
|
|
219
242
|
if (wallets) api.swarmWalletSlot.wallet = walletShim(wallets.ao.jwk);
|
|
220
243
|
return { api, wallets };
|
|
221
244
|
})();
|
|
@@ -277,15 +300,31 @@ async function until(what, read, done, timeoutMs = 6e5, everyMs = 3e3) {
|
|
|
277
300
|
|
|
278
301
|
// src/build.mjs
|
|
279
302
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
280
|
-
import { z as
|
|
303
|
+
import { z as z9 } from "zod";
|
|
281
304
|
|
|
282
305
|
// src/tool.mjs
|
|
283
306
|
import { z } from "zod";
|
|
307
|
+
var migrationSeen = null;
|
|
308
|
+
async function migrationRefusal(read = readKey, now = Date.now()) {
|
|
309
|
+
if (!migrationSeen || now - migrationSeen.at > 15e3) {
|
|
310
|
+
migrationSeen = { at: now, value: await read("game", "migration").catch(() => null) };
|
|
311
|
+
}
|
|
312
|
+
const m = migrationSeen.value;
|
|
313
|
+
if (!m || typeof m !== "object" || m.phase === "live") return null;
|
|
314
|
+
if (m.phase === "retired") {
|
|
315
|
+
return `Paralith has moved to new processes (the game is now ${m.successor}). Nothing was sent. Update paralith-mcp to its latest version to use them.`;
|
|
316
|
+
}
|
|
317
|
+
return "A migration is in progress: Paralith is paused while every balance moves to new processes. Nothing was sent, and nothing is lost. Reads still work; try writes again later.";
|
|
318
|
+
}
|
|
284
319
|
function tool(server, name, description, inputSchema, run, annotations = {}, view = null) {
|
|
285
320
|
if (READ_ONLY && annotations !== READ) return;
|
|
286
321
|
const schema = view ? { ...inputSchema, full: z.boolean().optional().describe("Return the raw record instead of the compact view.") } : inputSchema;
|
|
287
322
|
server.registerTool(name, { description, inputSchema: schema, annotations }, async (args) => {
|
|
288
323
|
try {
|
|
324
|
+
if (annotations === WRITE) {
|
|
325
|
+
const refusal = await migrationRefusal();
|
|
326
|
+
if (refusal) return { isError: true, content: [{ type: "text", text: refusal }] };
|
|
327
|
+
}
|
|
289
328
|
const raw = await run(args ?? {});
|
|
290
329
|
const value = view && !args?.full ? await view(raw) : raw;
|
|
291
330
|
const text = typeof value === "string" ? value : JSON.stringify(value, bigints, 2);
|
|
@@ -539,7 +578,7 @@ function registerGame(server) {
|
|
|
539
578
|
tool(
|
|
540
579
|
server,
|
|
541
580
|
"game_daily",
|
|
542
|
-
"Claim the daily crate and Rune. Once per UTC day.",
|
|
581
|
+
"Claim the daily crate (and accrued Rune, pass holders only). Once per UTC day.",
|
|
543
582
|
{},
|
|
544
583
|
async () => (await me()).api.claimDaily(),
|
|
545
584
|
WRITE,
|
|
@@ -1153,6 +1192,19 @@ function suiChain(w) {
|
|
|
1153
1192
|
}
|
|
1154
1193
|
};
|
|
1155
1194
|
}
|
|
1195
|
+
async function gasOf(name, address) {
|
|
1196
|
+
if (name === "base") {
|
|
1197
|
+
const rpc = process.env.PARALITH_BASE_RPC || chains.base.rpc;
|
|
1198
|
+
const pub = createPublicClient({ chain: { ...base, id: chains.base.chainId }, transport: http(rpc) });
|
|
1199
|
+
return `${formatEther(await pub.getBalance({ address }))} ETH`;
|
|
1200
|
+
}
|
|
1201
|
+
if (name === "solana") {
|
|
1202
|
+
const conn = new Connection(process.env.PARALITH_SOLANA_RPC || chains.solana.rpc, "confirmed");
|
|
1203
|
+
return `${await conn.getBalance(new PublicKey(address)) / 1e9} SOL`;
|
|
1204
|
+
}
|
|
1205
|
+
const client = new SuiGrpcClient({ network: chains.sui.network, baseUrl: process.env.PARALITH_SUI_RPC || chains.sui.rpc });
|
|
1206
|
+
return `${Number((await client.getBalance({ owner: address })).balance.balance) / 1e9} SUI`;
|
|
1207
|
+
}
|
|
1156
1208
|
async function chainFor(name) {
|
|
1157
1209
|
const { wallets } = await runtime();
|
|
1158
1210
|
return { base: baseChain, solana: solanaChain, sui: suiChain }[name](wallets);
|
|
@@ -1627,18 +1679,19 @@ function registerVaults(server) {
|
|
|
1627
1679
|
}
|
|
1628
1680
|
|
|
1629
1681
|
// src/show.mjs
|
|
1630
|
-
import
|
|
1631
|
-
import
|
|
1682
|
+
import fs4 from "node:fs";
|
|
1683
|
+
import path4 from "node:path";
|
|
1632
1684
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1633
1685
|
import { z as z7 } from "zod";
|
|
1634
|
-
import {
|
|
1686
|
+
import { spawn } from "node:child_process";
|
|
1687
|
+
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, getUiCapability } from "@modelcontextprotocol/ext-apps/server";
|
|
1635
1688
|
var GAME = (process.env.PARALITH_GAME_URL || "https://game.paralithmarkets.com").replace(/\/$/, "");
|
|
1636
1689
|
var TRADE = (process.env.PARALITH_TRADE_URL || "https://trade.paralithmarkets.com").replace(/\/$/, "");
|
|
1637
1690
|
var URI = "ui://paralith/view";
|
|
1638
|
-
var HERE2 =
|
|
1691
|
+
var HERE2 = path4.dirname(fileURLToPath2(import.meta.url));
|
|
1639
1692
|
function viewHtml() {
|
|
1640
|
-
for (const file of [
|
|
1641
|
-
if (
|
|
1693
|
+
for (const file of [path4.join(HERE2, "view.html"), path4.join(HERE2, "..", "dist", "view.html")]) {
|
|
1694
|
+
if (fs4.existsSync(file)) return fs4.readFileSync(file, "utf8");
|
|
1642
1695
|
}
|
|
1643
1696
|
throw new Error("view.html is missing: run npm run build");
|
|
1644
1697
|
}
|
|
@@ -1664,6 +1717,16 @@ async function ownAddress() {
|
|
|
1664
1717
|
const { wallets } = await runtime();
|
|
1665
1718
|
return wallets?.ao.address;
|
|
1666
1719
|
}
|
|
1720
|
+
function openInBrowser(url) {
|
|
1721
|
+
const [cmd, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
1722
|
+
try {
|
|
1723
|
+
spawn(cmd, args, { detached: true, stdio: "ignore" }).on("error", () => {
|
|
1724
|
+
}).unref();
|
|
1725
|
+
return true;
|
|
1726
|
+
} catch {
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1667
1730
|
function registerShow(server) {
|
|
1668
1731
|
const csp = { frameDomains: [GAME, TRADE] };
|
|
1669
1732
|
registerAppResource(
|
|
@@ -1674,7 +1737,7 @@ function registerShow(server) {
|
|
|
1674
1737
|
async () => ({ contents: [{ uri: URI, mimeType: RESOURCE_MIME_TYPE, text: viewHtml(), _meta: { ui: { csp } } }] })
|
|
1675
1738
|
);
|
|
1676
1739
|
registerAppTool(server, "show", {
|
|
1677
|
-
description: "Show something with the game's and exchange's own visuals, live: battle (follows a player's arena fights, animated), companion (card), hunt, market (order book and chart), vault, position, bridge (transfers).
|
|
1740
|
+
description: "Show something with the game's and exchange's own visuals, live: battle (follows a player's arena fights, animated), companion (card), hunt, market (order book and chart), vault, position, bridge (transfers). Seeing it is a far better experience than reading about it: call show whenever you do something worth watching. It displays inline where the client supports that, and otherwise (local server) opens in the user's browser. Address defaults to yours.",
|
|
1678
1741
|
inputSchema: {
|
|
1679
1742
|
what: z7.enum(Object.keys(VIEWS)),
|
|
1680
1743
|
address: z7.string().optional().describe("AO address; omit for yours."),
|
|
@@ -1685,20 +1748,121 @@ function registerShow(server) {
|
|
|
1685
1748
|
market: z7.string().optional().describe("market/vault: the market, as exchange_markets lists it."),
|
|
1686
1749
|
vault: z7.enum(["amm", "active", "grid"]).optional(),
|
|
1687
1750
|
chain: z7.enum(["base", "solana", "sui"]).optional().describe("bridge: with tx, one transfer."),
|
|
1688
|
-
tx: z7.string().optional()
|
|
1751
|
+
tx: z7.string().optional(),
|
|
1752
|
+
open: z7.boolean().optional().describe("Open in the user's browser. Default: only when this client cannot display it inline.")
|
|
1689
1753
|
},
|
|
1690
1754
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1691
1755
|
_meta: { ui: { resourceUri: URI } }
|
|
1692
1756
|
}, async (args) => {
|
|
1693
1757
|
try {
|
|
1694
1758
|
const view = VIEWS[args.what]({ ...args, address: args.address ?? await ownAddress() });
|
|
1695
|
-
|
|
1759
|
+
const inline = !!getUiCapability(server.server.getClientCapabilities());
|
|
1760
|
+
const opened = !READ_ONLY && (args.open ?? !inline) && openInBrowser(view);
|
|
1761
|
+
const where = inline ? "Displayed inline." : opened ? "Opened in the user's browser." : "Give the user this link.";
|
|
1762
|
+
return { structuredContent: { what: args.what, view }, content: [{ type: "text", text: `${args.what}: ${view}
|
|
1763
|
+
${where}` }] };
|
|
1696
1764
|
} catch (error) {
|
|
1697
1765
|
return { isError: true, content: [{ type: "text", text: error.message }] };
|
|
1698
1766
|
}
|
|
1699
1767
|
});
|
|
1700
1768
|
}
|
|
1701
1769
|
|
|
1770
|
+
// src/progress.mjs
|
|
1771
|
+
import { z as z8 } from "zod";
|
|
1772
|
+
var VENUES = { game: graph.processes.internalVenue, token: graph.processes.externalVenue };
|
|
1773
|
+
var count = (x) => Array.isArray(x) ? x.length : Object.values(x ?? {}).reduce((n, v) => n + Number(v || 0), 0);
|
|
1774
|
+
async function targets(who) {
|
|
1775
|
+
const { wallets } = await runtime();
|
|
1776
|
+
const agents = readAgents();
|
|
1777
|
+
const self = wallets && { name: process.env.PARALITH_AGENT ?? "you", addresses: publicAddresses(wallets) };
|
|
1778
|
+
if (!who || who === "me") {
|
|
1779
|
+
if (!self) throw new Error("This server has no wallet; pass who: an AO address.");
|
|
1780
|
+
return [self];
|
|
1781
|
+
}
|
|
1782
|
+
if (who === "all") {
|
|
1783
|
+
const list = Object.entries(agents).map(([name, a]) => ({ name, addresses: a.addresses }));
|
|
1784
|
+
if (self && !list.some((t) => t.name === self.name)) list.unshift(self);
|
|
1785
|
+
if (!list.length) throw new Error("No agents are set up here. Run: npx -y paralith-mcp@latest setup");
|
|
1786
|
+
return list;
|
|
1787
|
+
}
|
|
1788
|
+
if (agents[who]) return [{ name: who, addresses: agents[who].addresses }];
|
|
1789
|
+
if (ID.test(who)) return [{ name: who, addresses: { ao: who } }];
|
|
1790
|
+
throw new Error(`"${who}" is not an agent here (${Object.keys(agents).join(", ") || "none"}) or an AO address.`);
|
|
1791
|
+
}
|
|
1792
|
+
async function report({ name, addresses: a }) {
|
|
1793
|
+
const { api } = await runtime();
|
|
1794
|
+
const out = { agent: name, addresses: a };
|
|
1795
|
+
const todo = [];
|
|
1796
|
+
const gas = Object.fromEntries(await Promise.all(["base", "solana", "sui"].filter((c) => a[c]).map(async (c) => [c, await gasOf(c, a[c]).catch(() => "unreadable")])));
|
|
1797
|
+
if (Object.keys(gas).length) out.gas = gas;
|
|
1798
|
+
if (!a.ao) {
|
|
1799
|
+
out.note = "AO key not derived yet: it appears once this agent's app has started the server.";
|
|
1800
|
+
return out;
|
|
1801
|
+
}
|
|
1802
|
+
const [p, tokens, venues, redemptions] = await Promise.all([
|
|
1803
|
+
api.readPlayer(a.ao).catch(() => null),
|
|
1804
|
+
Promise.all(Object.entries({ rune: graph.processes.rune, pusd: graph.processes.pusd, ...graph.processes.elements }).map(async ([t, id]) => [t, await readAtoms(id, `balance-${a.ao}`).catch(() => 0n)])),
|
|
1805
|
+
Promise.all(Object.entries(VENUES).map(async ([v, pid]) => [
|
|
1806
|
+
v,
|
|
1807
|
+
await api.readVenuePosition(pid, a.ao).catch(() => null),
|
|
1808
|
+
await api.readVenueVaultPositions(pid, a.ao).catch(() => null)
|
|
1809
|
+
])),
|
|
1810
|
+
readKey("pusd", "withdrawals").catch(() => null)
|
|
1811
|
+
]);
|
|
1812
|
+
if (!p) {
|
|
1813
|
+
out.game = { joined: false };
|
|
1814
|
+
todo.push("Join the game (game_join).");
|
|
1815
|
+
} else {
|
|
1816
|
+
const raw = p.monsters?.[p.activeId] ?? p.collection?.[p.activeId];
|
|
1817
|
+
const c = companion(raw);
|
|
1818
|
+
out.game = {
|
|
1819
|
+
faction: p.faction,
|
|
1820
|
+
gold: p.gold,
|
|
1821
|
+
rune: p.inventory?.rune ?? 0,
|
|
1822
|
+
companion: c && { name: c.name, level: c.level, exp: c.exp, energy: c.energy, happiness: c.happiness },
|
|
1823
|
+
arena: { wins: p.wins ?? 0, losses: p.losses ?? 0, rating: p.rating, sessionBattlesLeft: p.battlesRemaining ?? 0 },
|
|
1824
|
+
...p.battleFleet ? { inBattle: p.battleFleet.battleId } : {},
|
|
1825
|
+
...p.hunt ? { onHunt: p.hunt.runId } : {}
|
|
1826
|
+
};
|
|
1827
|
+
if (!p.dailyReadyAt || Number(p.dailyReadyAt) <= Date.now()) todo.push("Daily crate is ready (game_daily).");
|
|
1828
|
+
if (count(p.lootboxes)) todo.push(`${count(p.lootboxes)} loot box(es) to open (game_lootbox).`);
|
|
1829
|
+
if (raw && Number(raw.exp ?? 0) >= Number(raw.nextLevelExp ?? Infinity)) todo.push("Companion can level up (game_level_up).");
|
|
1830
|
+
if (c && (c.energy < 25 || c.happiness < 25)) todo.push("Companion is below the arena's 25 energy/happiness: feed or play.");
|
|
1831
|
+
}
|
|
1832
|
+
const held = Object.fromEntries(tokens.filter(([, v]) => v > 0n).map(([t, v]) => [t, formatAtoms(v)]));
|
|
1833
|
+
if (Object.keys(held).length) out.tokens = held;
|
|
1834
|
+
const trading = {};
|
|
1835
|
+
for (const [v, pos, vaults] of venues) {
|
|
1836
|
+
const row = {};
|
|
1837
|
+
if (pos?.orders?.length) row.openOrders = pos.orders.length;
|
|
1838
|
+
if (pos?.fills?.length) row.recentFills = pos.fills.length;
|
|
1839
|
+
const shares = Object.entries(vaults ?? {}).flatMap(([m, r]) => Object.entries(r).filter(([, x]) => BigInt(x?.shares ?? 0) > 0n).map(([s]) => `${m} ${s}`));
|
|
1840
|
+
if (shares.length) row.vaults = shares;
|
|
1841
|
+
if (Object.keys(row).length) trading[v] = row;
|
|
1842
|
+
}
|
|
1843
|
+
if (Object.keys(trading).length) out.trading = trading;
|
|
1844
|
+
const pending = (redemptions ?? []).filter((w) => w.account === a.ao && w.status !== "released");
|
|
1845
|
+
if (pending.length) out.bridge = { pendingRedemptions: pending.length };
|
|
1846
|
+
if (Object.keys(gas).length && Object.values(gas).every((g) => /^0 /.test(g))) {
|
|
1847
|
+
todo.push("No chain gas yet: fund an address above to bridge.");
|
|
1848
|
+
}
|
|
1849
|
+
if (todo.length) out.todo = todo;
|
|
1850
|
+
return out;
|
|
1851
|
+
}
|
|
1852
|
+
function registerProgress(server) {
|
|
1853
|
+
tool(
|
|
1854
|
+
server,
|
|
1855
|
+
"progress",
|
|
1856
|
+
'How an agent is doing, in one free read: game (companion, Gold, Rune, arena record, live battle or hunt), tokens, open orders and vault shares on both venues, pending bridge redemptions, chain gas, and what needs doing next. who: omit for yourself, "all" for every agent set up on this machine, an agent name (claude, codex, grok) or an AO address.',
|
|
1857
|
+
{ who: z8.string().optional() },
|
|
1858
|
+
async ({ who }) => {
|
|
1859
|
+
const list = await Promise.all((await targets(who)).map(report));
|
|
1860
|
+
return list.length === 1 ? list[0] : list;
|
|
1861
|
+
},
|
|
1862
|
+
READ
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1702
1866
|
// src/build.mjs
|
|
1703
1867
|
var DOCS = process.env.PARALITH_DOCS_URL || "https://docs.paralithmarkets.com";
|
|
1704
1868
|
var INSTRUCTIONS = `Paralith on AO (HyperBEAM): Rune Realm (a companion game), two order books, and the PUSD bridge.
|
|
@@ -1715,7 +1879,26 @@ Vaults (pooled liquidity, one amm/active/grid per market): vault_list, vault_pos
|
|
|
1715
1879
|
Rune: rune_move withdraw (game -> token) / deposit (token -> game). Bridge: bridge_balances, bridge_faucet, bridge_deposit, bridge_redeem.
|
|
1716
1880
|
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.
|
|
1717
1881
|
|
|
1718
|
-
|
|
1882
|
+
HOW RESOURCES FLOW. When asked for a resource, find its path here, check what you hold (progress), then say the route before acting.
|
|
1883
|
+
- Gold (in-game only, never leaves the game). Earn: companion play/quest/claim (capped at 60 Gold per 20 h), arena bot wins,
|
|
1884
|
+
selling berries/Scrolls/Rune to the shop (game_shop sell) or on the game venue. Spend: arena stakes, shop buys, game-venue buys.
|
|
1885
|
+
- Berries and Scrolls. Get: joining, crates (game_daily, game_lootbox), buying with Gold (game_shop, or exchange_order venue "game").
|
|
1886
|
+
Use: feed the companion, hunt escrow (2 of each berry), arena boosts; a Scroll plus 1-3 Rune for a hunt capture.
|
|
1887
|
+
- Rune exists twice: in-game Rune and the TEST-RUNE wallet token; rune_move converts 1:1 either way.
|
|
1888
|
+
Get Rune: (1) the shop, only if game_shop quote shows an ask (it often only bids); (2) sell a companion on companion_market;
|
|
1889
|
+
(3) the token route, which always works: element_claim (free daily, 1-10 of one element) -> exchange_order venue "token"
|
|
1890
|
+
sell that element for TEST-RUNE -> rune_move deposit. Public TEST accounts accrue no Rune from game_daily (pass holders only).
|
|
1891
|
+
Spend in-game Rune: game_level_up, hunt capture, companion_market buys. TEST-RUNE: token-venue buys, vaults, dao_stake.
|
|
1892
|
+
- Element tokens (TEST-EARTH/AIR/FIRE/WATER): element_claim daily; trade against TEST-RUNE on the token venue; vault liquidity.
|
|
1893
|
+
- PUSD: bridge in TEST-USDC from Base, Solana or Sui (needs that chain's gas, which the user funds; bridge_faucet gives TEST-USDC).
|
|
1894
|
+
No PUSD market is listed yet. Redeeming back out needs a release claim this server cannot submit yet: say so rather than start it.
|
|
1895
|
+
- Vaults: deposit both sides of a market to earn from its strategy; withdraw with vault_shares.
|
|
1896
|
+
|
|
1897
|
+
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.
|
|
1898
|
+
|
|
1899
|
+
SHOW IT. Watching beats reading: call show for anything worth seeing (a fight as it happens, the companion after a level-up, the book
|
|
1900
|
+
you are trading, a vault, a bridge transfer). It displays inline where the client supports it, otherwise it opens in the user's browser.
|
|
1901
|
+
Show once per thing, not once per round.
|
|
1719
1902
|
|
|
1720
1903
|
Rules: if a tool says a write WAS accepted but unconfirmed, never resend it; re-read state instead.
|
|
1721
1904
|
A refusal ({"error": ...}) costs nothing. Use game_catalog for exact costs and move stats; docs at ${DOCS}/llms-full.txt.`;
|
|
@@ -1723,7 +1906,7 @@ var READ_ONLY_NOTE = `This endpoint is READ-ONLY: it explains the protocol and r
|
|
|
1723
1906
|
To play, trade or bridge, run the local server with a wallet phrase: ${DOCS}/ai/mcp`;
|
|
1724
1907
|
function buildServer() {
|
|
1725
1908
|
const server = new McpServer(
|
|
1726
|
-
{ name: "paralith", version: "0.
|
|
1909
|
+
{ name: "paralith", version: "0.3.1" },
|
|
1727
1910
|
{ instructions: READ_ONLY ? `${READ_ONLY_NOTE}
|
|
1728
1911
|
|
|
1729
1912
|
${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
@@ -1754,7 +1937,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
|
1754
1937
|
server,
|
|
1755
1938
|
"read_state",
|
|
1756
1939
|
`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.`,
|
|
1757
|
-
{ process:
|
|
1940
|
+
{ process: z9.string(), key: z9.string() },
|
|
1758
1941
|
async ({ process: process2, key }) => readKey(process2, key),
|
|
1759
1942
|
READ
|
|
1760
1943
|
);
|
|
@@ -1763,10 +1946,10 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
|
1763
1946
|
"send_message",
|
|
1764
1947
|
"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.",
|
|
1765
1948
|
{
|
|
1766
|
-
process:
|
|
1767
|
-
tags:
|
|
1768
|
-
data:
|
|
1769
|
-
push_outbox:
|
|
1949
|
+
process: z9.string(),
|
|
1950
|
+
tags: z9.record(z9.string(), z9.string()).describe('e.g. {"Action": "Pass.Info"}'),
|
|
1951
|
+
data: z9.string().optional(),
|
|
1952
|
+
push_outbox: z9.boolean().optional().describe("Deliver the messages this one emits to other processes (e.g. a Transfer to a process).")
|
|
1770
1953
|
},
|
|
1771
1954
|
async ({ process: process2, tags: tags2, data, push_outbox }) => {
|
|
1772
1955
|
if (Object.keys(tags2).some((k) => k.toLowerCase() === "target")) throw new Error("Target is not a usable tag name.");
|
|
@@ -1796,6 +1979,7 @@ ${INSTRUCTIONS}` : INSTRUCTIONS }
|
|
|
1796
1979
|
registerDao(server);
|
|
1797
1980
|
registerVaults(server);
|
|
1798
1981
|
registerShow(server);
|
|
1982
|
+
registerProgress(server);
|
|
1799
1983
|
return server;
|
|
1800
1984
|
}
|
|
1801
1985
|
|