@ripplesdotrun/mcp 0.1.0 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ripples
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -28,7 +28,13 @@ charter published, and nothing else.
28
28
  ```
29
29
 
30
30
  `RIPPLES_NETWORK` is `robinhood` (chain 4663) or `robinhood-testnet`. `RIPPLES_TREASURY` names an
31
- agent that already exists; leave it out and `ripples_launch_agent` fills it in.
31
+ agent that already exists.
32
+
33
+ Leave `RIPPLES_TREASURY` out and `ripples_launch_agent` sets it for the rest of the session.
34
+ It is held in memory, so copy the `treasury` address it returns into the config before you
35
+ restart: nothing else can find it for you.
36
+
37
+ ESM only, Node 20 or newer.
32
38
 
33
39
  ## Tools
34
40
 
package/dist/server.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
5
  import { z } from "zod";
@@ -8,15 +9,20 @@ import { createRipplesAgent, DEPLOYMENTS, } from "@ripplesdotrun/agent-sdk";
8
9
  // chain by the charter the launch was created under, not by anything here: there is no owner, no
9
10
  // setter and no withdrawal on a treasury, so a key in the wrong hands reaches one day's budget
10
11
  // and the addresses the charter published, and nothing else.
12
+ // Read off the manifest so it cannot drift from what was published.
13
+ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
11
14
  const KEY = process.env.RIPPLES_PRIVATE_KEY;
12
15
  const NETWORK = (process.env.RIPPLES_NETWORK ?? "robinhood");
13
16
  const TREASURY = process.env.RIPPLES_TREASURY;
14
- if (!DEPLOYMENTS[NETWORK]) {
15
- throw new Error(`RIPPLES_NETWORK must be one of ${Object.keys(DEPLOYMENTS).join(", ")}.`);
16
- }
17
17
  let client;
18
18
  function agent() {
19
19
  if (!client) {
20
+ // Checked here rather than at startup. A server that exits during the handshake shows the
21
+ // user "Connection closed" and files the reason in a log; a tool that refuses shows them the
22
+ // sentence. `hasOwn` rather than a truthiness test, or `RIPPLES_NETWORK=constructor` passes.
23
+ if (!Object.hasOwn(DEPLOYMENTS, NETWORK)) {
24
+ throw new Error(`RIPPLES_NETWORK must be one of ${Object.keys(DEPLOYMENTS).join(", ")}.`);
25
+ }
20
26
  if (!KEY) {
21
27
  throw new Error("Set RIPPLES_PRIVATE_KEY to a wallet that can sign, then restart.");
22
28
  }
@@ -28,39 +34,58 @@ function agent() {
28
34
  }
29
35
  return client;
30
36
  }
31
- const server = new McpServer({ name: "ripples", version: "0.1.0" });
37
+ const server = new McpServer({ name: "ripples", version: VERSION });
32
38
  const say = (value) => ({
33
39
  content: [{ type: "text", text: JSON.stringify(value, bigints, 2) }],
34
40
  });
35
41
  const bigints = (_key, value) => typeof value === "bigint" ? value.toString() : value;
36
42
  const explorer = (hash) => `${DEPLOYMENTS[NETWORK].explorer}/tx/${hash}`;
43
+ /** A wallet address, checked for shape. Correctness is not something this can check. */
44
+ const address = z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Expected a 0x wallet address.");
45
+ /// Amounts cross the wire as decimal strings so nothing rounds through a float on the way.
46
+ const amount = (what) => z
47
+ .string()
48
+ .regex(/^\d+(\.\d+)?$/, `${what} is a decimal string, for example "0.05".`)
49
+ .refine((value) => Number(value) > 0, `${what} has to be above zero.`);
37
50
  server.tool("ripples_launch_agent", "Create an agent on Ripples: its token and market, its NFT collection, and the contract that "
38
51
  + "holds its money. The contract is the launch's creator, so the creator's 70% of every trade "
39
52
  + "fee becomes the agent's working capital. The spending limits are fixed in this call and "
40
53
  + "nobody, including the creator, can raise them afterwards.", {
41
54
  name: z.string().describe("The project's name, used for the token and the collection."),
42
- symbol: z.string().describe("Ticker, 2 to 10 characters."),
43
- image: z.string().url().describe("Public image URL. The token carries it on chain."),
44
- description: z.string().optional(),
45
- website: z.string().url().optional(),
46
- twitter: z.string().url().optional(),
47
- mintPrice: z.string().describe("Price per NFT, in the market's asset, e.g. \"0.01\"."),
55
+ symbol: z.string().min(2).max(10).describe("Ticker, 2 to 10 characters."),
56
+ image: z.string().url().max(512).describe("Public image URL. The token carries it on chain."),
57
+ description: z.string().max(2048).optional(),
58
+ website: z.string().url().max(256).optional(),
59
+ twitter: z.string().url().max(256).optional(),
60
+ mintPrice: amount("mintPrice").describe("Price per NFT, in the market's asset."),
48
61
  collectionSupply: z.number().int().positive(),
49
- artworkBaseUri: z.string().describe("Where the finished artwork lives, with a trailing slash."),
50
- runway: z.string().describe("What goes into the agent's contract, in the market's asset. Covers the launch fee and is "
62
+ artworkBaseUri: z
63
+ .string()
64
+ .url()
65
+ .refine((value) => value.endsWith("/"), "A base URI has to end in a slash.")
66
+ .describe("Where the finished artwork lives, with a trailing slash."),
67
+ runway: amount("runway").describe("What goes into the agent's contract, in the market's asset. Covers the launch fee and is "
51
68
  + "what the agent works with until its market earns."),
52
- operator: z.string().optional().describe("The wallet that works the agent. Defaults to this server's own wallet."),
53
- dailySpend: z.string().describe("Most it can spend in a day. Buys, mints and payouts share it."),
54
- perTransactionSpend: z.string().describe("Most it can spend in one transaction."),
55
- dailySell: z.string().optional().describe("Most of its own token it can sell in a day. Leave it out and it can never sell."),
69
+ operator: address.optional().describe("The wallet that works the agent. Defaults to this server's own wallet. It is fixed for "
70
+ + "the life of the launch, and an address that cannot sign leaves the agent inoperable."),
71
+ dailySpend: amount("dailySpend").describe("Most it can spend in a day. Buys, mints and payouts share it."),
72
+ perTransactionSpend: amount("perTransactionSpend").describe("Most it can spend in one transaction. At least the mint price, or it can never mint."),
73
+ dailySell: amount("dailySell").optional().describe("Most of its own token it can sell in a day. Leave it out and it can never sell."),
56
74
  dailyMint: z.number().int().min(0).optional().describe("Pieces it can mint a day. Default none."),
57
- payees: z.array(z.string()).optional().describe("The only addresses it will ever be able to pay. Leave it out and it can pay nobody."),
75
+ payees: z.array(address).max(8).optional().describe("The only addresses it will ever be able to pay. Leave it out and it can pay nobody."),
58
76
  }, async (input) => {
59
77
  const client = agent();
78
+ // The operator is the only key that will ever be able to spend, and there is no setter, so
79
+ // an agent created against an address that cannot sign is inoperable for good. Defaulting to
80
+ // this server's own wallet is the only default that can work.
81
+ const operator = (input.operator ?? client.address);
82
+ if (!operator) {
83
+ throw new Error("No operator. Pass one, or set RIPPLES_PRIVATE_KEY so this server has a wallet.");
84
+ }
60
85
  const result = await client.launch({
61
86
  runway: input.runway,
62
87
  charter: {
63
- operator: (input.operator ?? client.deployment.agentTreasuryFactory),
88
+ operator,
64
89
  dailySpend: input.dailySpend,
65
90
  perCallSpend: input.perTransactionSpend,
66
91
  ...(input.dailySell ? { dailySell: input.dailySell } : {}),
@@ -86,23 +111,52 @@ server.tool("ripples_launch_agent", "Create an agent on Ripples: its token and m
86
111
  return say({ ...result, transactionUrl: explorer(result.transaction) });
87
112
  });
88
113
  server.tool("ripples_agent_buy", "Buy the agent's own token on its own market, into its own contract. Counts against the daily "
89
- + "and per-transaction spending limits.", { amount: z.string().describe("How much of the market's asset to spend.") }, async ({ amount }) => say({ transactionUrl: explorer(await agent().buy(amount)) }));
114
+ + "and per-transaction spending limits.", { amount: amount("amount").describe("How much of the market's asset to spend.") }, async ({ amount }) => say({ transactionUrl: explorer(await agent().buy(amount)) }));
90
115
  server.tool("ripples_agent_sell", "Sell the agent's own token back into its market. Refused outright when the charter's daily "
91
- + "sell ceiling is zero, which is the default.", { amount: z.string().describe("How much of its own token to sell.") }, async ({ amount }) => say({ transactionUrl: explorer(await agent().sell(amount)) }));
116
+ + "sell ceiling is zero, which is the default.", { amount: amount("amount").describe("How much of its own token to sell.") }, async ({ amount }) => say({ transactionUrl: explorer(await agent().sell(amount)) }));
92
117
  server.tool("ripples_agent_mint", "Mint from the agent's own collection. The pieces stay with the agent, and the mint funds its "
93
- + "own market like anyone else's.", { quantity: z.number().int().positive() }, async ({ quantity }) => say({ transactionUrl: explorer(await agent().mint(quantity)) }));
94
- server.tool("ripples_agent_pay", "Pay one of the addresses the charter published. Any other address is refused on chain.", { to: z.string(), amount: z.string() }, async ({ to, amount }) => say({ transactionUrl: explorer(await agent().pay(to, amount)) }));
95
- server.tool("ripples_agent_note", "Write an entry in the agent's public run log, beside the transactions it describes.", { text: z.string(), uri: z.string().optional() }, async ({ text, uri }) => say({ transactionUrl: explorer(await agent().note(text, uri ?? "")) }));
118
+ + "own market like anyone else's.", { quantity: z.number().int().min(1).max(20).describe("1 to 20; the collection caps a mint at 20.") }, async ({ quantity }) => say({ transactionUrl: explorer(await agent().mint(quantity)) }));
119
+ server.tool("ripples_agent_pay", "Pay one of the addresses the charter published. Any other address is refused on chain.", { to: address, amount: amount("amount") }, async ({ to, amount }) => say({ transactionUrl: explorer(await agent().pay(to, amount)) }));
120
+ server.tool("ripples_agent_note", "Write an entry in the agent's public run log, beside the transactions it describes. The text "
121
+ + "is committed as a hash and the `uri` is published next to it, so an entry with no `uri` is "
122
+ + "a dated digest nobody can read back. Pass a link to what the entry is about.", { text: z.string(), uri: z.string().url().optional() }, async ({ text, uri }) => say({ transactionUrl: explorer(await agent().note(text, uri ?? "")) }));
96
123
  server.tool("ripples_agent_claim", "Take in what the launch has earned: the creator's share of every trade and the collection's "
97
124
  + "share of every mint. Permissionless, and it can only pay the agent.", {}, async () => say({ transactionUrl: explorer(await agent().claim()) }));
125
+ server.tool("ripples_agent_claim_vested", "Take in the token slice the agent's own mints earned it. Separate from the fee claim because "
126
+ + "it only pays out once the market has reached its target, and a claim with nothing vested "
127
+ + "would otherwise fail the other one.", {}, async () => say({ transactionUrl: explorer(await agent().claimVested()) }));
98
128
  server.tool("ripples_agent_status", "What the agent may do, what is left of today's limits, and the addresses of its launch.", {}, async () => {
99
129
  const client = agent();
130
+ // Answered before there is an agent too. This is where a new user finds the address their
131
+ // key loaded as, which is the address they have to fund before a launch will go through.
132
+ const base = {
133
+ network: client.network,
134
+ signer: client.address ?? null,
135
+ settlesIn: client.deployment.quoteSymbol,
136
+ treasury: client.treasury ?? null,
137
+ };
138
+ if (!client.treasury)
139
+ return say(base);
100
140
  const [charter, budget, launched] = await Promise.all([
101
141
  client.charter(),
102
142
  client.budget(),
103
143
  client.launched(),
104
144
  ]);
105
- return say({ treasury: client.treasury, network: client.network, charter, budget, launched });
145
+ return say({ ...base, charter, budget, launched });
106
146
  });
107
- server.tool("ripples_agent_activity", "Everything the agent has done, newest first, read from its own contract.", { limit: z.number().int().positive().max(200).optional() }, async ({ limit }) => say(await agent().activity(limit ?? 25)));
108
- await server.connect(new StdioServerTransport());
147
+ server.tool("ripples_agent_activity", "Everything the agent has done, newest first, read from its own contract.", {
148
+ limit: z.number().int().positive().max(200).optional(),
149
+ fromBlock: z.number().int().nonnegative().optional().describe("Providers cap how many blocks one log query may span. Defaults to the last 50,000."),
150
+ }, async ({ limit, fromBlock }) => say(await agent().activity({
151
+ limit: limit ?? 25,
152
+ ...(fromBlock === undefined ? {} : { fromBlock: BigInt(fromBlock) }),
153
+ })));
154
+ try {
155
+ await server.connect(new StdioServerTransport());
156
+ }
157
+ catch (reason) {
158
+ // stdout is the protocol channel, so a startup failure goes to stderr and takes the process
159
+ // with it rather than surfacing as an unhandled rejection.
160
+ console.error(reason instanceof Error ? reason.message : String(reason));
161
+ process.exit(1);
162
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ripplesdotrun/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "MCP server for Ripples agent launches: create an agent with its own market, collection and spending limits, then let it work itself.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -24,13 +24,14 @@
24
24
  "node": ">=20"
25
25
  },
26
26
  "scripts": {
27
- "build": "tsc -p tsconfig.json",
28
- "prepublishOnly": "npm run build",
29
- "start": "node dist/server.js"
27
+ "build": "rm -rf dist && tsc -p tsconfig.json",
28
+ "start": "node dist/server.js",
29
+ "prepack": "npm run build",
30
+ "test": "node --test test/*.test.mjs"
30
31
  },
31
32
  "dependencies": {
32
33
  "@modelcontextprotocol/sdk": "^1.12.0",
33
- "@ripplesdotrun/agent-sdk": "^0.1.0",
34
+ "@ripplesdotrun/agent-sdk": "^0.1.1",
34
35
  "viem": "^2.21.0",
35
36
  "zod": "^3.23.0"
36
37
  },
@@ -41,10 +42,8 @@
41
42
  "publishConfig": {
42
43
  "access": "public"
43
44
  },
44
- "repository": {
45
- "type": "git",
46
- "url": "git+https://github.com/ripples-dot-run/ripples.git",
47
- "directory": "packages/agent-mcp"
48
- },
49
- "homepage": "https://ripples.run/docs#rh-agents"
45
+ "homepage": "https://ripples.run/docs#rh-agents",
46
+ "bugs": {
47
+ "url": "https://ripples.run/docs#rh-agents"
48
+ }
50
49
  }