@drparadox05/lido-mcp-server 0.1.0

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.
@@ -0,0 +1,104 @@
1
+ import { getOptionalEnv } from '../config/env.js';
2
+ const UNISWAP_API_BASE_URL = 'https://trade-api.gateway.uniswap.org/v1';
3
+ function getUniswapApiKey() {
4
+ const value = getOptionalEnv('UNISWAP_API_KEY');
5
+ if (!value) {
6
+ throw new Error('No Uniswap API key configured. Set UNISWAP_API_KEY.');
7
+ }
8
+ return value;
9
+ }
10
+ export function hasUniswapApiKeyConfigured() {
11
+ return Boolean(getOptionalEnv('UNISWAP_API_KEY'));
12
+ }
13
+ export function getUniswapUniversalRouterVersion() {
14
+ return getOptionalEnv('UNISWAP_UNIVERSAL_ROUTER_VERSION') === '1.2' ? '1.2' : '2.0';
15
+ }
16
+ async function parseResponse(response) {
17
+ const text = await response.text();
18
+ if (!text) {
19
+ return null;
20
+ }
21
+ try {
22
+ return JSON.parse(text);
23
+ }
24
+ catch {
25
+ return text;
26
+ }
27
+ }
28
+ function buildHeaders(includeRouterHeader, includePermitHeader) {
29
+ const headers = {
30
+ accept: 'application/json',
31
+ 'content-type': 'application/json',
32
+ 'x-api-key': getUniswapApiKey(),
33
+ };
34
+ if (includeRouterHeader) {
35
+ headers['x-universal-router-version'] = getUniswapUniversalRouterVersion();
36
+ }
37
+ if (includePermitHeader) {
38
+ headers['x-permit2-disabled'] = 'true';
39
+ }
40
+ return headers;
41
+ }
42
+ async function requestJson(path, init, includeRouterHeader, includePermitHeader) {
43
+ const response = await fetch(`${UNISWAP_API_BASE_URL}${path}`, {
44
+ ...init,
45
+ headers: {
46
+ ...buildHeaders(includeRouterHeader, includePermitHeader),
47
+ ...(init.headers ?? {}),
48
+ },
49
+ });
50
+ const payload = await parseResponse(response);
51
+ if (!response.ok) {
52
+ const detail = typeof payload === 'string'
53
+ ? payload
54
+ : payload && typeof payload === 'object'
55
+ ? JSON.stringify(payload)
56
+ : response.statusText;
57
+ throw new Error(`Uniswap API ${response.status} ${response.statusText}: ${detail}`);
58
+ }
59
+ return payload;
60
+ }
61
+ export async function postUniswapApi(path, body, options) {
62
+ return requestJson(path, {
63
+ method: 'POST',
64
+ body: JSON.stringify(body),
65
+ }, options?.includeRouterHeader ?? false, options?.includePermitHeader ?? true);
66
+ }
67
+ export async function getUniswapApi(path, params, options) {
68
+ const query = new URLSearchParams();
69
+ for (const [key, value] of Object.entries(params)) {
70
+ query.set(key, value);
71
+ }
72
+ return requestJson(`${path}?${query.toString()}`, {
73
+ method: 'GET',
74
+ }, options?.includeRouterHeader ?? false, options?.includePermitHeader ?? false);
75
+ }
76
+ export async function checkUniswapApproval(body) {
77
+ return postUniswapApi('/check_approval', body, {
78
+ includePermitHeader: true,
79
+ });
80
+ }
81
+ export async function getUniswapQuote(body) {
82
+ return postUniswapApi('/quote', body, {
83
+ includeRouterHeader: true,
84
+ includePermitHeader: true,
85
+ });
86
+ }
87
+ export async function createUniswapSwap(body) {
88
+ return postUniswapApi('/swap', body, {
89
+ includeRouterHeader: true,
90
+ includePermitHeader: true,
91
+ });
92
+ }
93
+ export async function getUniswapSwapStatus(chainId, txHashes) {
94
+ return getUniswapApi('/swaps', {
95
+ chainId: String(chainId),
96
+ txHashes: txHashes.join(','),
97
+ });
98
+ }
99
+ export async function getUniswapBridgableTokens(tokenInChainId, tokenIn) {
100
+ return getUniswapApi('/swappable_tokens', {
101
+ tokenInChainId: String(tokenInChainId),
102
+ tokenIn,
103
+ });
104
+ }
@@ -0,0 +1,318 @@
1
+ import { erc20Abi, formatUnits, getAddress, parseUnits } from 'viem';
2
+ import { hasPrivateKeyConfigured } from '../config/env.js';
3
+ import { getAccount, getPublicClient, getWalletContext } from '../lido/clients.js';
4
+ import { getNetworkConfig } from '../lido/networks.js';
5
+ import { checkUniswapApproval, createUniswapSwap, getUniswapBridgableTokens as fetchUniswapBridgableTokens, getUniswapQuote, getUniswapSwapStatus as fetchUniswapSwapStatus, getUniswapUniversalRouterVersion, hasUniswapApiKeyConfigured } from './api.js';
6
+ import { NATIVE_TOKEN_ADDRESS, describeTokenReference, resolveTokenReference } from './tokens.js';
7
+ import { EXECUTABLE_UNISWAP_ROUTINGS } from './types.js';
8
+ function isNativeToken(address) {
9
+ return address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase();
10
+ }
11
+ function getExecutionContext(swapper) {
12
+ let configuredAddress = null;
13
+ let walletError = null;
14
+ if (hasPrivateKeyConfigured()) {
15
+ try {
16
+ configuredAddress = getAccount().address;
17
+ }
18
+ catch (error) {
19
+ walletError = error instanceof Error ? error.message : String(error);
20
+ }
21
+ }
22
+ const swapperAddress = swapper ? getAddress(swapper) : configuredAddress;
23
+ return {
24
+ wallet_configured: hasPrivateKeyConfigured(),
25
+ configured_address: configuredAddress,
26
+ wallet_error: walletError,
27
+ swapper: swapperAddress,
28
+ local_execution_available: Boolean(configuredAddress && swapperAddress && configuredAddress.toLowerCase() === swapperAddress.toLowerCase()),
29
+ };
30
+ }
31
+ async function getTokenDecimals(network, token) {
32
+ if (isNativeToken(token)) {
33
+ return 18;
34
+ }
35
+ const publicClient = getPublicClient(network);
36
+ return Number(await publicClient.readContract({
37
+ address: token,
38
+ abi: erc20Abi,
39
+ functionName: 'decimals',
40
+ }));
41
+ }
42
+ async function resolveTradeAmount(type, tokenInChain, tokenIn, tokenOutChain, tokenOut, amount) {
43
+ const amountTokenNetwork = type === 'EXACT_INPUT' ? tokenInChain : tokenOutChain;
44
+ const amountTokenAddress = type === 'EXACT_INPUT' ? tokenIn : tokenOut;
45
+ const decimals = await getTokenDecimals(amountTokenNetwork, amountTokenAddress);
46
+ const raw = parseUnits(amount, decimals);
47
+ return {
48
+ specified_token: type === 'EXACT_INPUT' ? 'token_in' : 'token_out',
49
+ decimals,
50
+ raw,
51
+ formatted: formatUnits(raw, decimals),
52
+ };
53
+ }
54
+ function getDefaultProtocols(tokenInChain, tokenOutChain, protocols) {
55
+ if (protocols && protocols.length > 0) {
56
+ return protocols;
57
+ }
58
+ if (tokenInChain !== tokenOutChain) {
59
+ return undefined;
60
+ }
61
+ return ['V4', 'V3', 'V2'];
62
+ }
63
+ async function prepareUniswapRoute(params) {
64
+ if (!hasUniswapApiKeyConfigured()) {
65
+ throw new Error('No Uniswap API key configured. Set UNISWAP_API_KEY.');
66
+ }
67
+ const execution = getExecutionContext(params.swapper);
68
+ if (!execution.swapper) {
69
+ throw new Error('Provide swapper explicitly or configure a wallet private key before requesting a Uniswap route.');
70
+ }
71
+ const inputToken = describeTokenReference(params.tokenInChain, params.tokenIn);
72
+ const outputToken = describeTokenReference(params.tokenOutChain, params.tokenOut);
73
+ const amount = await resolveTradeAmount(params.type, params.tokenInChain, inputToken.resolved, params.tokenOutChain, outputToken.resolved, params.amount);
74
+ const protocols = getDefaultProtocols(params.tokenInChain, params.tokenOutChain, params.protocols);
75
+ const quoteRequest = {
76
+ type: params.type,
77
+ amount: amount.raw.toString(),
78
+ tokenInChainId: getNetworkConfig(params.tokenInChain).chain.id,
79
+ tokenOutChainId: getNetworkConfig(params.tokenOutChain).chain.id,
80
+ tokenIn: inputToken.resolved,
81
+ tokenOut: outputToken.resolved,
82
+ swapper: execution.swapper,
83
+ routingPreference: params.routingPreference,
84
+ urgency: params.urgency,
85
+ ...(params.slippageTolerance !== undefined ? { slippageTolerance: params.slippageTolerance } : { autoSlippage: 'DEFAULT' }),
86
+ ...(protocols ? { protocols } : {}),
87
+ };
88
+ const quote = await getUniswapQuote(quoteRequest);
89
+ const routing = quote.routing;
90
+ const quotedInputAmount = quote.quote?.input?.amount ?? amount.raw.toString();
91
+ const approvalRequest = !isNativeToken(inputToken.resolved)
92
+ ? {
93
+ walletAddress: execution.swapper,
94
+ token: inputToken.resolved,
95
+ amount: quotedInputAmount,
96
+ chainId: getNetworkConfig(params.tokenInChain).chain.id,
97
+ urgency: params.urgency,
98
+ includeGasInfo: true,
99
+ tokenOut: outputToken.resolved,
100
+ tokenOutChainId: getNetworkConfig(params.tokenOutChain).chain.id,
101
+ }
102
+ : null;
103
+ const approval = approvalRequest ? await checkUniswapApproval(approvalRequest) : null;
104
+ return {
105
+ execution,
106
+ inputToken,
107
+ outputToken,
108
+ amount,
109
+ approvalRequest,
110
+ approval,
111
+ quoteRequest,
112
+ quote,
113
+ routing,
114
+ executable_routing: EXECUTABLE_UNISWAP_ROUTINGS.includes(routing),
115
+ };
116
+ }
117
+ function getQuoteSummary(prepared) {
118
+ const quote = prepared.quote.quote;
119
+ return {
120
+ request_id: prepared.quote.requestId,
121
+ routing: prepared.routing,
122
+ executable_via_swap_endpoint: prepared.executable_routing,
123
+ quote_id: quote.quoteId ?? null,
124
+ route_string: quote.routeString ?? null,
125
+ tx_failure_reasons: quote.txFailureReasons ?? [],
126
+ price_impact_pct: quote.priceImpact ?? null,
127
+ input: {
128
+ token: prepared.inputToken.resolved,
129
+ amount_raw: quote.input?.amount ?? (prepared.amount.specified_token === 'token_in' ? prepared.amount.raw.toString() : null),
130
+ amount_formatted: prepared.amount.specified_token === 'token_in' ? prepared.amount.formatted : null,
131
+ },
132
+ output: {
133
+ token: prepared.outputToken.resolved,
134
+ amount_raw: quote.output?.amount ?? null,
135
+ recipient: quote.output?.recipient ?? prepared.execution.swapper,
136
+ },
137
+ gas: {
138
+ gas_fee: quote.gasFee ?? null,
139
+ gas_fee_quote: quote.gasFeeQuote ?? null,
140
+ gas_fee_usd: quote.gasFeeUSD ?? null,
141
+ gas_use_estimate: quote.gasUseEstimate ?? null,
142
+ gas_price: quote.gasPrice ?? null,
143
+ max_fee_per_gas: quote.maxFeePerGas ?? null,
144
+ max_priority_fee_per_gas: quote.maxPriorityFeePerGas ?? null,
145
+ },
146
+ };
147
+ }
148
+ function createSwapRequest(prepared, deadline) {
149
+ const request = {
150
+ quote: prepared.quote.quote,
151
+ refreshGasPrice: true,
152
+ simulateTransaction: true,
153
+ safetyMode: 'SAFE',
154
+ urgency: prepared.quoteRequest.urgency,
155
+ deadline: deadline ?? Math.floor(Date.now() / 1000) + 1800,
156
+ };
157
+ if (prepared.quote.permitData) {
158
+ request.permitData = prepared.quote.permitData;
159
+ }
160
+ return request;
161
+ }
162
+ async function submitUniswapTransaction(network, transaction) {
163
+ const { account, publicClient, walletClient } = getWalletContext(network);
164
+ const hash = await walletClient.sendTransaction({
165
+ account,
166
+ to: getAddress(transaction.to),
167
+ data: transaction.data,
168
+ value: BigInt(transaction.value ?? '0'),
169
+ ...(transaction.gasLimit ? { gas: BigInt(transaction.gasLimit) } : {}),
170
+ ...(transaction.gasPrice ? { gasPrice: BigInt(transaction.gasPrice) } : {}),
171
+ ...(transaction.maxFeePerGas ? { maxFeePerGas: BigInt(transaction.maxFeePerGas) } : {}),
172
+ ...(transaction.maxPriorityFeePerGas ? { maxPriorityFeePerGas: BigInt(transaction.maxPriorityFeePerGas) } : {}),
173
+ });
174
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
175
+ return {
176
+ transaction_hash: hash,
177
+ block_number: receipt.blockNumber,
178
+ status: receipt.status,
179
+ };
180
+ }
181
+ export function getUniswapSetupSummary() {
182
+ return {
183
+ api_key_configured: hasUniswapApiKeyConfigured(),
184
+ universal_router_version: getUniswapUniversalRouterVersion(),
185
+ supported_networks: ['ethereum', 'base', 'optimism', 'arbitrum'],
186
+ supported_routing_execution_modes: [...EXECUTABLE_UNISWAP_ROUTINGS],
187
+ notes: [
188
+ 'This server uses x-permit2-disabled=true and follows a direct approval-then-swap flow.',
189
+ 'Uniswap API execution requires UNISWAP_API_KEY.',
190
+ 'Live execution is currently limited to routes returned as CLASSIC, BRIDGE, WRAP, or UNWRAP.',
191
+ ],
192
+ };
193
+ }
194
+ export async function preflightUniswapRoute(params) {
195
+ try {
196
+ const prepared = await prepareUniswapRoute(params);
197
+ const blockers = [];
198
+ const warnings = [];
199
+ const nextSteps = [];
200
+ if (prepared.approval?.cancel) {
201
+ warnings.push('Token approval likely needs a reset transaction before the new approval can be sent.');
202
+ }
203
+ if (prepared.approval?.approval && !params.approveIfNeeded) {
204
+ blockers.push('Approval is required but approve_if_needed=false.');
205
+ }
206
+ if (!prepared.executable_routing) {
207
+ blockers.push(`Returned routing ${prepared.routing} is not currently executable through this server's gasful /swap flow.`);
208
+ warnings.push('This usually means the route prefers UniswapX or a chained plan instead of a direct protocol swap or bridge transaction.');
209
+ }
210
+ if (!prepared.execution.local_execution_available) {
211
+ warnings.push('The server can preflight this route, but live execution requires the configured wallet to match the swapper address.');
212
+ }
213
+ if (prepared.quote.quote.txFailureReasons?.length) {
214
+ warnings.push('The quote contains transaction failure reasons from Uniswap simulation output. Review them before execution.');
215
+ }
216
+ nextSteps.push('Run lido_execute_uniswap_route with dry_run=true to inspect the approval and swap transactions.');
217
+ if (!blockers.length) {
218
+ nextSteps.push('If the dry run matches intent, rerun lido_execute_uniswap_route with dry_run=false to broadcast the transaction(s).');
219
+ }
220
+ return {
221
+ ready: blockers.length === 0,
222
+ wallet: prepared.execution,
223
+ amount: {
224
+ specified_token: prepared.amount.specified_token,
225
+ raw: prepared.amount.raw,
226
+ formatted: prepared.amount.formatted,
227
+ decimals: prepared.amount.decimals,
228
+ },
229
+ token_in: prepared.inputToken,
230
+ token_out: prepared.outputToken,
231
+ approval: prepared.approval,
232
+ quote_request: prepared.quoteRequest,
233
+ quote_summary: getQuoteSummary(prepared),
234
+ blockers,
235
+ warnings,
236
+ next_steps: nextSteps,
237
+ };
238
+ }
239
+ catch (error) {
240
+ return {
241
+ ready: false,
242
+ blockers: [error instanceof Error ? error.message : String(error)],
243
+ warnings: [],
244
+ next_steps: ['Fix the reported configuration or routing issue and rerun the preflight.'],
245
+ };
246
+ }
247
+ }
248
+ export async function executeUniswapRoute(params) {
249
+ const prepared = await prepareUniswapRoute(params);
250
+ if (!prepared.executable_routing) {
251
+ throw new Error(`Returned routing ${prepared.routing} is not currently executable by this server. Supported live routings: ${EXECUTABLE_UNISWAP_ROUTINGS.join(', ')}.`);
252
+ }
253
+ if (params.dryRun) {
254
+ const swapRequest = createSwapRequest(prepared, params.deadline);
255
+ const swap = await createUniswapSwap(swapRequest);
256
+ return {
257
+ mode: 'dry_run',
258
+ wallet: prepared.execution,
259
+ token_in: prepared.inputToken,
260
+ token_out: prepared.outputToken,
261
+ approval: prepared.approval,
262
+ quote_summary: getQuoteSummary(prepared),
263
+ swap_request: swap,
264
+ notes: prepared.approval?.approval
265
+ ? 'Approval is required before the swap or bridge transaction can succeed onchain.'
266
+ : 'Swap or bridge transaction is ready for execution.',
267
+ };
268
+ }
269
+ if (!prepared.execution.local_execution_available || !prepared.execution.configured_address) {
270
+ throw new Error('Live execution requires a configured wallet whose address matches the swapper parameter.');
271
+ }
272
+ if (prepared.approval?.approval && !params.approveIfNeeded) {
273
+ throw new Error('Approval is required but approve_if_needed=false.');
274
+ }
275
+ const sourceNetwork = params.tokenInChain;
276
+ const executedApprovals = [];
277
+ if (prepared.approval?.cancel) {
278
+ executedApprovals.push({
279
+ step: 'cancel_approval',
280
+ ...(await submitUniswapTransaction(sourceNetwork, prepared.approval.cancel)),
281
+ });
282
+ }
283
+ if (prepared.approval?.approval) {
284
+ executedApprovals.push({
285
+ step: 'approve',
286
+ ...(await submitUniswapTransaction(sourceNetwork, prepared.approval.approval)),
287
+ });
288
+ }
289
+ const refreshed = await prepareUniswapRoute(params);
290
+ if (!refreshed.executable_routing) {
291
+ throw new Error(`Refreshed routing ${refreshed.routing} is not currently executable by this server. Re-run preflight and review the new route.`);
292
+ }
293
+ const refreshedSwapRequest = createSwapRequest(refreshed, params.deadline);
294
+ const refreshedSwap = await createUniswapSwap(refreshedSwapRequest);
295
+ const swapExecution = await submitUniswapTransaction(sourceNetwork, refreshedSwap.swap);
296
+ return {
297
+ mode: 'executed',
298
+ routing: refreshed.routing,
299
+ source_network: sourceNetwork,
300
+ destination_network: params.tokenOutChain,
301
+ approvals: executedApprovals,
302
+ swap_request_id: refreshedSwap.requestId,
303
+ gas_fee: refreshedSwap.gasFee ?? null,
304
+ swap: swapExecution,
305
+ next_steps: refreshed.routing === 'BRIDGE'
306
+ ? ['Use lido_get_uniswap_route_status with the source chain and returned transaction hash to monitor bridge progress.']
307
+ : ['The route transaction has been broadcast and mined on the source chain.'],
308
+ };
309
+ }
310
+ export async function getUniswapRouteStatus(params) {
311
+ const chainId = getNetworkConfig(params.chain).chain.id;
312
+ return fetchUniswapSwapStatus(chainId, params.txHashes);
313
+ }
314
+ export async function getUniswapBridgableTokens(params) {
315
+ const token = resolveTokenReference(params.tokenInChain, params.tokenIn);
316
+ const chainId = getNetworkConfig(params.tokenInChain).chain.id;
317
+ return fetchUniswapBridgableTokens(chainId, token);
318
+ }
@@ -0,0 +1,44 @@
1
+ import { getAddress, zeroAddress } from 'viem';
2
+ import { getNetworkConfig } from '../lido/networks.js';
3
+ export const NATIVE_TOKEN_ADDRESS = zeroAddress;
4
+ const TOKEN_ALIASES = ['native', 'eth', 'steth', 'wsteth', 'ldo'];
5
+ export function isAddressLike(value) {
6
+ return /^(0x)?[0-9a-fA-F]{40}$/.test(value);
7
+ }
8
+ export function resolveTokenReference(network, value) {
9
+ if (isAddressLike(value)) {
10
+ return getAddress(value);
11
+ }
12
+ const normalized = value.trim().toLowerCase();
13
+ const config = getNetworkConfig(network);
14
+ if (normalized === 'native' || normalized === 'eth') {
15
+ return NATIVE_TOKEN_ADDRESS;
16
+ }
17
+ if (normalized === 'steth') {
18
+ if (!config.steth) {
19
+ throw new Error(`Token alias steth is not available on ${network}. Use a token address instead.`);
20
+ }
21
+ return getAddress(config.steth);
22
+ }
23
+ if (normalized === 'wsteth') {
24
+ if (!config.wsteth) {
25
+ throw new Error(`Token alias wsteth is not available on ${network}. Use a token address instead.`);
26
+ }
27
+ return getAddress(config.wsteth);
28
+ }
29
+ if (normalized === 'ldo') {
30
+ if (!config.ldo) {
31
+ throw new Error(`Token alias ldo is not available on ${network}. Use a token address instead.`);
32
+ }
33
+ return getAddress(config.ldo);
34
+ }
35
+ throw new Error(`Unsupported token reference ${value}. Use one of ${TOKEN_ALIASES.join(', ')} or provide an explicit token address.`);
36
+ }
37
+ export function describeTokenReference(network, value) {
38
+ const resolved = resolveTokenReference(network, value);
39
+ return {
40
+ input: value,
41
+ resolved,
42
+ kind: resolved.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase() ? 'native' : 'erc20',
43
+ };
44
+ }
@@ -0,0 +1,6 @@
1
+ export const UNISWAP_PROTOCOLS = ['V2', 'V3', 'V4', 'UNISWAPX', 'UNISWAPX_V2', 'UNISWAPX_V3'];
2
+ export const UNISWAP_ROUTING_PREFERENCES = ['BEST_PRICE', 'FASTEST'];
3
+ export const UNISWAP_ROUTINGS = ['CLASSIC', 'DUTCH_LIMIT', 'DUTCH_V2', 'DUTCH_V3', 'BRIDGE', 'LIMIT_ORDER', 'PRIORITY', 'WRAP', 'UNWRAP', 'CHAINED'];
4
+ export const EXECUTABLE_UNISWAP_ROUTINGS = ['CLASSIC', 'BRIDGE', 'WRAP', 'UNWRAP'];
5
+ export const UNISWAP_URGENCIES = ['normal', 'fast', 'urgent'];
6
+ export const UNISWAP_TRADE_TYPES = ['EXACT_INPUT', 'EXACT_OUTPUT'];
package/lido.skill.md ADDED
@@ -0,0 +1,131 @@
1
+ # Lido Skill
2
+
3
+ ## Mental model
4
+
5
+ Lido staking on Ethereum turns ETH into `stETH`.
6
+
7
+ `stETH` is a rebasing token.
8
+ Its balance changes as Lido oracle reports rewards and penalties.
9
+ The underlying accounting unit is shares, not displayed balance.
10
+ If you are reasoning about long-term position behavior, shares are the stable anchor and displayed `stETH` balance is the rebased surface.
11
+
12
+ `wstETH` is the wrapped, non-rebasing form of `stETH`.
13
+ Its token balance stays fixed, while each unit of `wstETH` becomes redeemable for more `stETH` over time.
14
+ That is why `wstETH` is the safer default for bridges, vaults, and DeFi integrations that do not handle rebasing tokens correctly.
15
+
16
+ ## When to use stETH vs wstETH
17
+
18
+ Use `stETH` when:
19
+ - you are staking directly on Ethereum and want the simplest canonical receipt token
20
+ - you want rebases reflected directly in wallet balance
21
+ - you may enter the withdrawal queue using `stETH`
22
+
23
+ Use `wstETH` when:
24
+ - you want a non-rebasing position representation
25
+ - you are moving exposure into DeFi or across L2s like Base, Optimism, or Arbitrum
26
+ - you need predictable token balances for automation logic
27
+
28
+ ## Safe operating patterns
29
+
30
+ Always start with `dry_run=true`.
31
+ Only switch to `dry_run=false` after the tool result clearly matches intent.
32
+
33
+ A safe agent workflow is:
34
+ - inspect setup
35
+ - inspect the aggregated portfolio summary
36
+ - run write preflight checks for the intended action
37
+ - dry-run the write
38
+ - execute with a small amount
39
+
40
+ For wrap and unstake requests, approvals matter.
41
+ If allowance is insufficient and `approve_if_needed=true`, the server may need to send an approval transaction before the main action.
42
+ Treat approval plus action as a two-step write path.
43
+
44
+ Use `lido_get_portfolio_summary` when you want a single view of balances, claimable withdrawals, and recent governance context.
45
+ Use `lido_preflight_write_action` before any write when you want the agent to explain blockers, approvals, and the next safest step.
46
+
47
+ ## Staking and unstaking specifics
48
+
49
+ Staking is immediate minting of `stETH` against ETH deposit.
50
+ Unstaking is not immediate ETH redemption.
51
+ On Lido, unstaking means creating withdrawal queue requests.
52
+ Those requests mint `unstETH` NFTs in the queue.
53
+ ETH is only claimable after finalization.
54
+ A safe agent should never promise instant exit liquidity from the queue.
55
+
56
+ ## Rewards reasoning
57
+
58
+ For `stETH`, balance growth comes from rebasing.
59
+ For `wstETH`, token count stays constant and value accrues through the `stETH per wstETH` exchange rate.
60
+ If calculating rewards over time, remember that pure on-chain balance deltas are only equal to rewards when there were no transfers in or out during the interval.
61
+ Otherwise the result is net position change, not pure staking income.
62
+
63
+ ## Governance reasoning
64
+
65
+ Lido DAO governance uses Aragon voting on Ethereum.
66
+ Before voting, verify:
67
+ - the target proposal id
68
+ - whether the wallet can vote at the proposal snapshot
69
+ - whether the intended action is yes or no
70
+ - whether auto-execution on decision is appropriate
71
+
72
+ Do not cast or execute governance actions without an explicit human instruction.
73
+
74
+ ## Swap and bridge reasoning
75
+
76
+ The server exposes Uniswap-powered routing for same-chain swaps and cross-chain bridges.
77
+
78
+ ### Safety model for Uniswap routes
79
+
80
+ The server uses a direct approval-then-swap flow with `x-permit2-disabled=true`.
81
+ It does not require or accept Permit2 signatures.
82
+
83
+ Always preflight first using `lido_preflight_uniswap_route`:
84
+ - confirms API key availability
85
+ - resolves token aliases (eth, steth, wsteth, or explicit addresses)
86
+ - checks approval requirements on the source chain
87
+ - reports the selected routing type and executability
88
+ - identifies blockers before any transaction is attempted
89
+
90
+ Execution defaults to `dry_run=true`:
91
+ - dry run returns approval calldata if needed
92
+ - dry run returns the swap/bridge transaction calldata for inspection
93
+ - only execute live after explicit human confirmation
94
+
95
+ ### Routing types and executability
96
+
97
+ Preflight surfaces the route type (CLASSIC, BRIDGE, WRAP, UNWRAP, UNISWAPX variants, etc.).
98
+ The server can execute these routing types live:
99
+ - CLASSIC (direct protocol pool swaps)
100
+ - BRIDGE (cross-chain via bridge partners)
101
+ - WRAP (wrapping native assets)
102
+ - UNWRAP (unwrapping wrapped assets)
103
+
104
+ UniswapX routes (DUTCH_LIMIT, DUTCH_V2, DUTCH_V3, PRIORITY) may appear in preflight results but are surfaced, not executed, by this server.
105
+
106
+ ### Approval handling
107
+
108
+ If the source token is not native ETH and allowance is insufficient:
109
+ - preflight reports `needs_approval: true`
110
+ - with `approve_if_needed=true`, live execution sends approval before the swap
111
+ - some tokens require a cancel-and-replace approval pattern; preflight surfaces this in the `cancel` field
112
+
113
+ ### Cross-chain bridge specifics
114
+
115
+ Bridging is initiated by a source-chain transaction.
116
+ After the source transaction mines, assets arrive on the destination chain asynchronously.
117
+
118
+ Use `lido_get_uniswap_route_status` with the source chain and transaction hash to monitor progress.
119
+ Do not assume instant destination-chain availability.
120
+
121
+ ### Key differences from Lido core actions
122
+
123
+ - Uniswap routes execute on the source chain provided; Lido core writes execute on Ethereum
124
+ - Uniswap routes use an external API for quote generation; Lido core actions use direct contract calls
125
+ - Bridge routes require monitoring; Lido staking/wrapping are single-transaction outcomes
126
+
127
+ ## Network boundaries
128
+
129
+ Ethereum mainnet is where core staking, wrapping, withdrawal queue actions, and governance live.
130
+ L2s such as Base, Optimism, and Arbitrum primarily expose bridged `wstETH` for balance monitoring and position awareness.
131
+ Do not assume L2 `wstETH` implies native staking or native Lido governance on that L2.
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@drparadox05/lido-mcp-server",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "description": "MCP server for Lido staking, wrapping, governance, and Uniswap-powered swaps/bridges with safety-first design.",
9
+ "main": "dist/index.js",
10
+ "bin": {
11
+ "lido-mcp-server": "dist/index.js"
12
+ },
13
+ "files": [
14
+ "dist/**/*",
15
+ "lido.skill.md",
16
+ "README.md",
17
+ "DEMO.md",
18
+ ".env.example"
19
+ ],
20
+ "keywords": [
21
+ "mcp",
22
+ "lido",
23
+ "ethereum",
24
+ "staking",
25
+ "defi",
26
+ "uniswap",
27
+ "bridge",
28
+ "cursor",
29
+ "claude"
30
+ ],
31
+ "author": "",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/drparadox05/Lido-MCP-Server.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/drparadox05/Lido-MCP-Server/issues"
39
+ },
40
+ "homepage": "https://github.com/drparadox05/Lido-MCP-Server#readme",
41
+ "engines": {
42
+ "node": ">=21"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.json",
46
+ "dev": "tsx src/index.ts",
47
+ "start": "node dist/index.js",
48
+ "prepublishOnly": "npm run build"
49
+ },
50
+ "dependencies": {
51
+ "@modelcontextprotocol/sdk": "1.27.1",
52
+ "viem": "2.47.6",
53
+ "zod": "4.3.6"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "22.12.0",
57
+ "tsx": "4.21.0",
58
+ "typescript": "5.9.3"
59
+ }
60
+ }