@yummybait/uniswap-tx-builder-mcp 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/dist/ticks.js ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Pure Uniswap v3 tick math — no RPC, no I/O. Converts human-readable prices
3
+ * into the aligned ticks a position uses, and live pool readings into
4
+ * range suggestions and mint amounts.
5
+ *
6
+ * A pool price is `token1` per `token0` expressed in each token's *smallest*
7
+ * unit. A human price `P` ("how many whole token1 per whole token0") relates
8
+ * to the raw price by `raw = P * 10^(decimals1 - decimals0)`, and
9
+ * `tick = log(raw) / log(1.0001)`.
10
+ */
11
+ // Uniswap v3 tick bounds.
12
+ export const MIN_TICK = -887272;
13
+ export const MAX_TICK = 887272;
14
+ const LOG_BASE = Math.log(1.0001);
15
+ /** Standard fee tier → tick spacing. */
16
+ export const FEE_TO_TICK_SPACING = {
17
+ 100: 1,
18
+ 500: 10,
19
+ 3000: 60,
20
+ 10000: 200,
21
+ };
22
+ export function feeToTickSpacing(fee) {
23
+ const spacing = FEE_TO_TICK_SPACING[fee];
24
+ if (!spacing) {
25
+ const known = Object.keys(FEE_TO_TICK_SPACING).join(", ");
26
+ throw new Error(`Unknown fee tier ${fee}. Supported: ${known}.`);
27
+ }
28
+ return spacing;
29
+ }
30
+ /**
31
+ * Convert a human price (token1 per token0) to the (unaligned) tick.
32
+ * `price` must be > 0.
33
+ */
34
+ export function priceToTick(price, decimals0, decimals1) {
35
+ if (!(price > 0)) {
36
+ throw new Error(`Price must be a positive number, got ${price}`);
37
+ }
38
+ const raw = price * 10 ** (decimals1 - decimals0);
39
+ return Math.log(raw) / LOG_BASE;
40
+ }
41
+ /** Snap a tick to the nearest usable tick for `spacing`, clamped to bounds. */
42
+ export function alignTick(tick, spacing) {
43
+ const snapped = Math.round(tick / spacing) * spacing;
44
+ const minAligned = Math.ceil(MIN_TICK / spacing) * spacing;
45
+ const maxAligned = Math.floor(MAX_TICK / spacing) * spacing;
46
+ return Math.min(Math.max(snapped, minAligned), maxAligned);
47
+ }
48
+ /**
49
+ * Convert a human price range to aligned [tickLower, tickUpper] for `fee`.
50
+ * Prices are reordered if given high→low, and the bounds are guaranteed to
51
+ * differ by at least one tick spacing.
52
+ */
53
+ export function priceRangeToTicks(priceLower, priceUpper, fee, decimals0, decimals1) {
54
+ const tickSpacing = feeToTickSpacing(fee);
55
+ const [lo, hi] = priceLower <= priceUpper
56
+ ? [priceLower, priceUpper]
57
+ : [priceUpper, priceLower];
58
+ let tickLower = alignTick(priceToTick(lo, decimals0, decimals1), tickSpacing);
59
+ let tickUpper = alignTick(priceToTick(hi, decimals0, decimals1), tickSpacing);
60
+ if (tickLower >= tickUpper) {
61
+ // Range collapsed after alignment — widen to one spacing, staying in bounds.
62
+ const maxAligned = Math.floor(MAX_TICK / tickSpacing) * tickSpacing;
63
+ if (tickLower >= maxAligned) {
64
+ tickLower = maxAligned - tickSpacing;
65
+ tickUpper = maxAligned;
66
+ }
67
+ else {
68
+ tickUpper = tickLower + tickSpacing;
69
+ }
70
+ }
71
+ return { tickLower, tickUpper, tickSpacing };
72
+ }
73
+ // ─── TickMath.getSqrtRatioAtTick (exact port of Uniswap v3-core) ─────
74
+ const Q96 = 1n << 96n;
75
+ const MAX_UINT256 = (1n << 256n) - 1n;
76
+ // One 128-bit multiplier per tick bit: sqrt(1.0001)^-(2^i) in Q128.
77
+ const TICK_MULTIPLIERS = [
78
+ 0xfffcb933bd6fad37aa2d162d1a594001n,
79
+ 0xfff97272373d413259a46990580e213an,
80
+ 0xfff2e50f5f656932ef12357cf3c7fdccn,
81
+ 0xffe5caca7e10e4e61c3624eaa0941cd0n,
82
+ 0xffcb9843d60f6159c9db58835c926644n,
83
+ 0xff973b41fa98c081472e6896dfb254c0n,
84
+ 0xff2ea16466c96a3843ec78b326b52861n,
85
+ 0xfe5dee046a99a2a811c461f1969c3053n,
86
+ 0xfcbe86c7900a88aedcffc83b479aa3a4n,
87
+ 0xf987a7253ac413176f2b074cf7815e54n,
88
+ 0xf3392b0822b70005940c7a398e4b70f3n,
89
+ 0xe7159475a2c29b7443b29c7fa6e889d9n,
90
+ 0xd097f3bdfd2022b8845ad8f792aa5825n,
91
+ 0xa9f746462d870fdf8a65dc1f90e061e5n,
92
+ 0x70d869a156d2a1b890bb3df62baf32f7n,
93
+ 0x31be135f97d08fd981231505542fcfa6n,
94
+ 0x9aa508b5b7a84e1c677de54f3e99bc9n,
95
+ 0x5d6af8dedb81196699c329225ee604n,
96
+ 0x2216e584f5fa1ea926041bedfe98n,
97
+ 0x48a170391f7dc42444e8fa2n,
98
+ ];
99
+ /**
100
+ * Q64.96 sqrt price at a tick — bit-for-bit identical to the on-chain
101
+ * `TickMath.getSqrtRatioAtTick`, so amounts derived from it match what the
102
+ * pool computes.
103
+ */
104
+ export function getSqrtRatioAtTick(tick) {
105
+ if (!Number.isInteger(tick) || tick < MIN_TICK || tick > MAX_TICK) {
106
+ throw new Error(`Tick out of range: ${tick}`);
107
+ }
108
+ const absTick = Math.abs(tick);
109
+ let ratio = (absTick & 1) !== 0 ? TICK_MULTIPLIERS[0] : 1n << 128n;
110
+ for (let i = 1; i < TICK_MULTIPLIERS.length; i++) {
111
+ if ((absTick >> i) & 1) {
112
+ ratio = (ratio * TICK_MULTIPLIERS[i]) >> 128n;
113
+ }
114
+ }
115
+ if (tick > 0)
116
+ ratio = MAX_UINT256 / ratio;
117
+ // Q128.128 → Q64.96, rounding up.
118
+ return (ratio >> 32n) + (ratio % (1n << 32n) === 0n ? 0n : 1n);
119
+ }
120
+ /**
121
+ * Ticks within ±`rangePct` of the spot tick, rounded INWARD to `tickSpacing`
122
+ * — outward rounding would exceed the caller's stated max width.
123
+ */
124
+ export function suggestRangeTicks(tick, tickSpacing, rangePct) {
125
+ if (!(rangePct > 0)) {
126
+ throw new Error(`rangePct must be positive, got ${rangePct}`);
127
+ }
128
+ const delta = Math.log(1 + rangePct / 100) / LOG_BASE;
129
+ const tickLower = Math.ceil((tick - delta) / tickSpacing) * tickSpacing;
130
+ const tickUpper = Math.floor((tick + delta) / tickSpacing) * tickSpacing;
131
+ if (tickLower >= tickUpper) {
132
+ throw new Error(`Range ±${rangePct}% is too narrow for tick spacing ${tickSpacing} — widen it.`);
133
+ }
134
+ return {
135
+ tickLower,
136
+ tickUpper,
137
+ pctBelow: (1.0001 ** (tickLower - tick) - 1) * 100,
138
+ pctAbove: (1.0001 ** (tickUpper - tick) - 1) * 100,
139
+ };
140
+ }
141
+ /**
142
+ * The largest both-sided deposit the balances allow at the LIVE pool price:
143
+ * per-side liquidity `L0`/`L1` from the full balances, `L = min(L0, L1)`,
144
+ * then back out the exact amounts that liquidity consumes. Feeding these to
145
+ * `mint` matches the pool's own math, so slippage mins can stay tight.
146
+ *
147
+ * Throws when the spot price is outside [tickLower, tickUpper] — such a mint
148
+ * is single-sided and the caller should recompute the range instead.
149
+ */
150
+ export function computeMintAmounts(sqrtPriceX96, tickLower, tickUpper, balance0, balance1) {
151
+ if (tickLower >= tickUpper) {
152
+ throw new Error(`tickLower must be < tickUpper (${tickLower} >= ${tickUpper})`);
153
+ }
154
+ const sqrtLower = getSqrtRatioAtTick(tickLower);
155
+ const sqrtUpper = getSqrtRatioAtTick(tickUpper);
156
+ if (!(sqrtLower < sqrtPriceX96 && sqrtPriceX96 < sqrtUpper)) {
157
+ throw new Error(`Spot price is outside [${tickLower}, ${tickUpper}] — a mint there would be ` +
158
+ "single-sided; recompute the range around the current tick.");
159
+ }
160
+ const l0 = (balance0 * ((sqrtPriceX96 * sqrtUpper) / Q96)) / (sqrtUpper - sqrtPriceX96);
161
+ const l1 = (balance1 * Q96) / (sqrtPriceX96 - sqrtLower);
162
+ const liquidity = l0 < l1 ? l0 : l1;
163
+ return {
164
+ amount0Desired: (liquidity * (sqrtUpper - sqrtPriceX96) * Q96) / (sqrtUpper * sqrtPriceX96),
165
+ amount1Desired: (liquidity * (sqrtPriceX96 - sqrtLower)) / Q96,
166
+ limitingSide: l0 < l1 ? "token0" : "token1",
167
+ };
168
+ }
169
+ /** Human price (token1 per token0) from a Q64.96 sqrt price. */
170
+ export function sqrtPriceX96ToPrice(sqrtPriceX96, decimals0, decimals1) {
171
+ const ratio = Number(sqrtPriceX96) / Number(Q96);
172
+ return ratio * ratio * 10 ** (decimals0 - decimals1);
173
+ }
174
+ //# sourceMappingURL=ticks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticks.js","sourceRoot":"","sources":["../src/ticks.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@yummybait/uniswap-tx-builder-mcp",
3
+ "version": "0.3.1",
4
+ "description": "Keyless MCP server that builds unsigned Uniswap v3 liquidity-position transactions (collect, close, mint, increase, wrap/swap via Universal Router) and simulates them — you sign with your own wallet.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "uniswap-tx-builder-mcp": "dist/mcp.js",
9
+ "uniswap-tx-builder-skill": "bin/install-skill.mjs"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "bin",
14
+ "skills",
15
+ ".claude-plugin"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/Yummybait-fin/uniswap-tx-builder-mcp.git"
20
+ },
21
+ "homepage": "https://github.com/Yummybait-fin/uniswap-tx-builder-mcp#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/Yummybait-fin/uniswap-tx-builder-mcp/issues"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "uniswap",
29
+ "defi",
30
+ "liquidity",
31
+ "ethereum",
32
+ "base",
33
+ "unsigned-transactions"
34
+ ],
35
+ "engines": {
36
+ "node": ">=20"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "provenance": true
41
+ },
42
+ "scripts": {
43
+ "dev": "tsx watch src/mcp.ts",
44
+ "build": "tsc",
45
+ "start": "node dist/mcp.js",
46
+ "typecheck": "tsc --noEmit",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "verify:pack": "npm run build && node scripts/verify-tarball.mjs",
50
+ "prepack": "npm run build",
51
+ "prepublishOnly": "npm run typecheck && npm test && npm run verify:pack"
52
+ },
53
+ "dependencies": {
54
+ "@modelcontextprotocol/sdk": "1.29.0",
55
+ "viem": "^2.31.3",
56
+ "zod": "3.25.76"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^22.15.21",
60
+ "tsx": "^4.19.4",
61
+ "typescript": "^5.8.3",
62
+ "vitest": "^4.1.0"
63
+ }
64
+ }
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: uniswap-tx-builder
3
+ description: Build unsigned Uniswap v3 liquidity-position transactions with the uniswap-tx-builder MCP — collect fees, close (remove liquidity + collect, optionally burn), mint a new position, increase liquidity, wrap native ETH / swap WETH via the Universal Router, read live pool state, and plan a position from a human price range — simulate them, then hand the calldata (or the ready-made unsigned RLP) to your own wallet to sign. Use whenever you need to manage, rebalance, open, or close a Uniswap v3 LP position through this MCP. Generic: no app- or wallet-specific knowledge.
4
+ ---
5
+
6
+ # uniswap-tx-builder
7
+
8
+ This skill ships with the **uniswap-tx-builder MCP** — a public, **keyless** server that builds
9
+ *unsigned* Uniswap v3 position transactions and (optionally) simulates them. **It never holds
10
+ keys and never signs.** You build calldata here, then sign + broadcast with *your own* wallet
11
+ (any signer — a CDP wallet MCP, viem, etc.).
12
+
13
+ ## Tools
14
+
15
+ All build tools return `tx = { to, data, value, chainId }`, `rlp`, and a `description`.
16
+ `value` is `"0"` except `build_wrap`/`build_swap` with native ETH (payable). Addresses are
17
+ `0x…40`; `positionId`/amounts are decimal **strings** (they exceed JS safe integers).
18
+
19
+ `rlp` is the **unsigned EIP-1559 (type-2)** serialization of `tx` with nonce, fees and gas
20
+ zeroed — pass it directly to signing services that populate those at signing time (e.g. a CDP
21
+ wallet MCP `send_transaction`). If your signer manages nonces itself, serialize `tx` instead.
22
+
23
+ | Tool | Args | Notes |
24
+ |------|------|-------|
25
+ | `build_collect` | `chainId, positionId, recipient, simulate?` | Collect all uncollected fees to `recipient`. |
26
+ | `build_close` | `chainId, positionId, recipient, burn?, simulate?` | Remove all liquidity **+** collect. `burn: true` also burns the now-empty NFT in the same multicall. Returns the read `position` (token0/1, fee, tickLower/Upper, liquidity). |
27
+ | `build_mint` | `chainId, token0, token1, fee, tickLower, tickUpper, amount0Desired, amount1Desired, recipient, slippageBps?, simulate?` | Mint a new position. Get the amounts from `get_pool_state` **immediately before** this call. |
28
+ | `build_increase` | `chainId, positionId, amount0Desired, amount1Desired, recipient, slippageBps?, simulate?` | Add liquidity to an **existing** position. |
29
+ | `build_wrap` | `chainId, amountWei, recipient?, sender?, deadline?, simulate?` | Native ETH → WETH via Universal Router `WRAP_ETH` (payable; works under UR-allowlisting wallet policies where `WETH.deposit()` doesn't). |
30
+ | `build_swap` | `chainId, amountInWei, tokenOut, fee, amountOutMin, recipient?, wrapWei?, sender?, deadline?, simulate?` | Exact-in single-hop WETH → `tokenOut`. With `wrapWei` (≥ `amountInWei`): wraps that much native ETH, swaps `amountInWei`, sweeps the WETH remainder — one payable tx. Without it: pays WETH via **Permit2** (needs a Permit2 approval). |
31
+ | `plan_position` | `chainId, token0, token1, fee, priceLower, priceUpper, amount0?, amount1?` | **Read-only.** Human price range + human amounts → aligned ticks + wei amounts. |
32
+ | `get_pool_state` | `chainId, token0, token1, fee, rangePct?, tickLower?, tickUpper?, balance0?, balance1?` | **Read-only.** Live `pool`, `tick`, `sqrtPriceX96`, `price` (token1 per token0, human), `tickSpacing`. See below. |
33
+
34
+ - **`simulate`** runs an `eth_call` dry-run. **On by default for collect/close** — keep it on so
35
+ you only ever sign txs that would succeed. **Off by default for mint/increase** (they need token
36
+ approvals + balances); pass `simulate: true` only if those are already in place. For
37
+ **wrap/swap**, pass `sender` (the wallet that will sign) — simulation then runs by default with
38
+ the correct `from` and `value`.
39
+ - A simulation failure comes back as an error — **do not sign** a tx that failed to simulate.
40
+ - `recipient` on wrap/swap **defaults to the tx sender** (the router's `MSG_SENDER` placeholder).
41
+ Omit it unless the output must go to a different address.
42
+
43
+ ### `get_pool_state` (live spot → range → mint amounts)
44
+
45
+ The one tool to call **right before every mint**:
46
+
47
+ 1. `rangePct: 2` → `suggested.tickLower/tickUpper` within ±2% of spot, **rounded inward** to the
48
+ tick spacing (the range never exceeds your stated width).
49
+ 2. `tickLower + tickUpper + balance0 + balance1` (raw wei balances) → `mintAmounts.
50
+ amount0Desired/amount1Desired` computed from the **live** sqrtPrice ratio, plus
51
+ `limitingSide` (which balance caps the position). Feed these straight into `build_mint`.
52
+ It errors if spot has moved outside the range — recompute the range, don't force the mint.
53
+
54
+ Amounts computed from a stale price revert the mint with **"Price slippage check"** — always
55
+ recompute in the same breath as `build_mint`.
56
+
57
+ ### `plan_position` (human price range → ticks)
58
+
59
+ When you start from a human price range instead of a ±pct width: `priceLower`/`priceUpper` are
60
+ **token1 per token0** in whole-token units, reordered if given high→low, snapped to the fee
61
+ tier's tick spacing. It reads `decimals` over RPC to convert `amount0`/`amount1` to wei, but does
62
+ **not** compute the range-optimal ratio — use `get_pool_state` with balances for that.
63
+
64
+ `token0` **must be `<` `token1`** by address (Uniswap's ordering) for both planning tools. If
65
+ they aren't, swap the pair and invert the prices — the tools error otherwise.
66
+
67
+ ## Signing (your wallet, not this MCP)
68
+
69
+ Take the returned `rlp` (for signing services that fill nonce/fees) or `tx` (for signers that
70
+ serialize themselves) and sign + broadcast with your own signer. This MCP is keyless, so the only
71
+ limits that apply are **your wallet's** (e.g. a CDP Wallet Policy). If your wallet rejects the
72
+ tx, report it — never try to route around the wallet's policy.
73
+
74
+ For `build_mint` and `build_increase`, your wallet must hold the input tokens **and have an
75
+ ERC-20 approval to the NonfungiblePositionManager** beforehand — that's the signer's
76
+ responsibility, not this MCP's. (This is also why their `simulate` is off by default.)
77
+ `build_wrap`/`build_swap` use the Universal Router v1.2 (`0x3fC91A3a…B2b7FAD`) — the address
78
+ wallet policies typically allowlist.
79
+
80
+ ## Position lifecycle
81
+
82
+ - **Collect fees:** `build_collect` → sign.
83
+ - **Add liquidity:** `build_increase` → sign (after approvals).
84
+ - **Close:** `build_close` → sign. Use the returned `position` (pair, range, liquidity) to inform
85
+ the decision. Pass `burn: true` if you want the empty NFT gone too.
86
+ - **Open around spot:** `get_pool_state` (rangePct → ticks, balances → amounts) → `build_mint` →
87
+ sign. If the wallet holds native ETH instead of WETH: `build_swap` with `wrapWei` (or
88
+ `build_wrap`) first, confirm, then recompute amounts and mint.
89
+ - **Open with a price range:** `plan_position` → `get_pool_state` (balances → amounts) →
90
+ `build_mint` → sign.
91
+ - **Rebalance** = **close → mint a recentered range.** The mint amounts depend on the tokens
92
+ freed by the close, so **do the close first**, then `get_pool_state` + `build_mint` the new
93
+ range once those balances are realized (typically a later step/cycle). Center the new range on
94
+ the current price; widen it for volatile pairs, tighten it for stable pairs.
95
+
96
+ ## Chains
97
+
98
+ Supports the chains configured in the MCP (NFPM/factory/Universal Router/WETH9 per chain):
99
+ Ethereum (1), Optimism (10), Polygon (137), Base (8453), Arbitrum (42161). On Polygon the
100
+ "wrapped native" is WMATIC. Calling an unconfigured chain returns an "Unsupported chain" error.