web3-tools-mcp 3.0.0 → 3.2.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.
- package/CHANGELOG.md +115 -2
- package/README.md +96 -60
- package/dist/chain-meta.d.ts.map +1 -1
- package/dist/chain-meta.js +19 -8
- package/dist/chain-meta.js.map +1 -1
- package/dist/chains.d.ts +609 -65
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +89 -28
- package/dist/chains.js.map +1 -1
- package/dist/clear-signing.d.ts.map +1 -1
- package/dist/clear-signing.js +17 -6
- package/dist/clear-signing.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -4
- package/dist/client.js.map +1 -1
- package/dist/http-server.d.ts +1 -1
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +12 -5
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +5 -6
- package/dist/index.js.map +1 -1
- package/dist/kv-storage.d.ts +2 -1
- package/dist/kv-storage.d.ts.map +1 -1
- package/dist/kv-storage.js +5 -2
- package/dist/kv-storage.js.map +1 -1
- package/dist/oauth.d.ts +18 -4
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +62 -26
- package/dist/oauth.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +7 -5
- package/dist/preview.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +16 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read/advanced.d.ts +25 -20
- package/dist/tools/read/advanced.d.ts.map +1 -1
- package/dist/tools/read/advanced.js +45 -33
- package/dist/tools/read/advanced.js.map +1 -1
- package/dist/tools/read/balance.d.ts +6 -6
- package/dist/tools/read/balance.d.ts.map +1 -1
- package/dist/tools/read/balance.js +17 -32
- package/dist/tools/read/balance.js.map +1 -1
- package/dist/tools/read/contract-info.d.ts +14 -12
- package/dist/tools/read/contract-info.d.ts.map +1 -1
- package/dist/tools/read/contract-info.js +53 -122
- package/dist/tools/read/contract-info.js.map +1 -1
- package/dist/tools/read/contract.d.ts +10 -10
- package/dist/tools/read/contract.d.ts.map +1 -1
- package/dist/tools/read/contract.js +2 -1
- package/dist/tools/read/contract.js.map +1 -1
- package/dist/tools/read/defi.d.ts +86 -0
- package/dist/tools/read/defi.d.ts.map +1 -0
- package/dist/tools/read/defi.js +210 -0
- package/dist/tools/read/defi.js.map +1 -0
- package/dist/tools/read/ens.d.ts +20 -40
- package/dist/tools/read/ens.d.ts.map +1 -1
- package/dist/tools/read/ens.js +25 -33
- package/dist/tools/read/ens.js.map +1 -1
- package/dist/tools/read/gas.d.ts +16 -16
- package/dist/tools/read/gas.d.ts.map +1 -1
- package/dist/tools/read/gas.js +13 -48
- package/dist/tools/read/gas.js.map +1 -1
- package/dist/tools/read/logs.d.ts +65 -4
- package/dist/tools/read/logs.d.ts.map +1 -1
- package/dist/tools/read/logs.js +96 -115
- package/dist/tools/read/logs.js.map +1 -1
- package/dist/tools/read/signatures.d.ts.map +1 -1
- package/dist/tools/read/signatures.js +4 -3
- package/dist/tools/read/signatures.js.map +1 -1
- package/dist/tools/read/signing.d.ts.map +1 -1
- package/dist/tools/read/signing.js +8 -2
- package/dist/tools/read/signing.js.map +1 -1
- package/dist/tools/sign/transactions.d.ts +16 -16
- package/dist/tools/sign/transactions.d.ts.map +1 -1
- package/dist/tools/sign/transactions.js +54 -9
- package/dist/tools/sign/transactions.js.map +1 -1
- package/dist/tools/sign/wallet.d.ts +4 -4
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +43 -2
- package/dist/utils.js.map +1 -1
- package/dist/wallet-client.d.ts +1 -0
- package/dist/wallet-client.d.ts.map +1 -1
- package/dist/wallet-client.js +18 -5
- package/dist/wallet-client.js.map +1 -1
- package/package.json +4 -7
- package/src/chain-meta.ts +16 -7
- package/src/chains.ts +101 -31
- package/src/clear-signing.ts +15 -9
- package/src/client.ts +2 -3
- package/src/http-server.ts +11 -6
- package/src/index.ts +5 -6
- package/src/kv-storage.ts +6 -3
- package/src/oauth.ts +93 -34
- package/src/preview.ts +7 -5
- package/src/tools/index.ts +14 -1
- package/src/tools/read/advanced.ts +63 -34
- package/src/tools/read/balance.ts +18 -33
- package/src/tools/read/contract-info.ts +64 -144
- package/src/tools/read/contract.ts +2 -1
- package/src/tools/read/defi.ts +287 -0
- package/src/tools/read/ens.ts +26 -33
- package/src/tools/read/gas.ts +13 -50
- package/src/tools/read/logs.ts +131 -143
- package/src/tools/read/signatures.ts +4 -4
- package/src/tools/read/signing.ts +8 -2
- package/src/tools/sign/transactions.ts +60 -9
- package/src/types.ts +0 -1
- package/src/utils.ts +47 -2
- package/src/wallet-client.ts +26 -5
package/src/chain-meta.ts
CHANGED
|
@@ -8,15 +8,21 @@ import { whatsabi } from '@shazow/whatsabi'
|
|
|
8
8
|
import { type Abi, type Address, parseAbiItem } from 'viem'
|
|
9
9
|
import { getClientManager } from './client.js'
|
|
10
10
|
import type { ChainName } from './types.js'
|
|
11
|
+
import { TtlCache } from './utils.js'
|
|
11
12
|
|
|
12
13
|
const ERC20_META_ABI = [
|
|
13
14
|
parseAbiItem('function decimals() view returns (uint8)'),
|
|
14
15
|
parseAbiItem('function symbol() view returns (string)')
|
|
15
16
|
]
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
18
|
+
// An hour covers a session's repeats while still noticing an upgraded proxy.
|
|
19
|
+
const TTL_MS = 60 * 60 * 1000
|
|
20
|
+
const abiCache = new TtlCache<{ abi: Abi; source: 'verified' | 'guessed'; proxy?: string; name?: string }>(200, TTL_MS)
|
|
21
|
+
const labelCache = new TtlCache<{ label?: string }>(1000, TTL_MS)
|
|
22
|
+
const metaCache = new TtlCache<{ symbol?: string; decimals?: number }>(1000, TTL_MS)
|
|
23
|
+
|
|
24
|
+
/** A local node is reset and redeployed at will, so nothing learned from it stays true. */
|
|
25
|
+
const cacheable = (chain: ChainName) => chain !== 'localhost'
|
|
20
26
|
|
|
21
27
|
export async function loadAbi(chain: ChainName, address: string) {
|
|
22
28
|
const key = `${chain}:${address.toLowerCase()}`
|
|
@@ -49,7 +55,7 @@ export async function loadAbi(chain: ChainName, address: string) {
|
|
|
49
55
|
proxy: result.address !== address ? result.address : undefined,
|
|
50
56
|
name: result.contractResult?.name ?? undefined
|
|
51
57
|
}
|
|
52
|
-
abiCache.set(key, loaded)
|
|
58
|
+
if (cacheable(chain)) abiCache.set(key, loaded)
|
|
53
59
|
return loaded
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -71,7 +77,8 @@ export async function tokenMeta(chain: ChainName, token: string) {
|
|
|
71
77
|
decimals: decimals.status === 'success' ? Number(decimals.result) : undefined,
|
|
72
78
|
symbol: symbol.status === 'success' ? (symbol.result as string) : undefined
|
|
73
79
|
}
|
|
74
|
-
|
|
80
|
+
// A failed call may be a flaky RPC rather than a token without that method.
|
|
81
|
+
if (cacheable(chain) && decimals.status === 'success' && symbol.status === 'success') metaCache.set(key, meta)
|
|
75
82
|
return meta
|
|
76
83
|
}
|
|
77
84
|
|
|
@@ -82,7 +89,8 @@ export async function tokenMeta(chain: ChainName, token: string) {
|
|
|
82
89
|
*/
|
|
83
90
|
export async function addressLabel(chain: ChainName, address: string): Promise<string | undefined> {
|
|
84
91
|
const key = `${chain}:${address.toLowerCase()}`
|
|
85
|
-
|
|
92
|
+
const cached = labelCache.get(key)
|
|
93
|
+
if (cached) return cached.label
|
|
86
94
|
|
|
87
95
|
let label: string | undefined
|
|
88
96
|
try {
|
|
@@ -95,9 +103,10 @@ export async function addressLabel(chain: ChainName, address: string): Promise<s
|
|
|
95
103
|
}
|
|
96
104
|
} catch {
|
|
97
105
|
// Unknown address — show it bare rather than failing the preview.
|
|
106
|
+
return undefined
|
|
98
107
|
}
|
|
99
108
|
|
|
100
|
-
labelCache.set(key, label)
|
|
109
|
+
if (cacheable(chain)) labelCache.set(key, { label })
|
|
101
110
|
return label
|
|
102
111
|
}
|
|
103
112
|
|
package/src/chains.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import type { Chain } from 'viem'
|
|
2
2
|
import {
|
|
3
3
|
arbitrum,
|
|
4
|
+
arc,
|
|
4
5
|
avalanche,
|
|
5
6
|
base,
|
|
6
7
|
bsc,
|
|
8
|
+
celo,
|
|
7
9
|
gnosis,
|
|
10
|
+
hyperEvm,
|
|
11
|
+
ink,
|
|
8
12
|
linea,
|
|
9
13
|
localhost,
|
|
10
14
|
mainnet,
|
|
15
|
+
mantle,
|
|
16
|
+
monad,
|
|
11
17
|
optimism,
|
|
18
|
+
plasma,
|
|
12
19
|
polygon,
|
|
13
|
-
|
|
20
|
+
robinhood,
|
|
14
21
|
unichain,
|
|
15
22
|
zksync
|
|
16
23
|
} from 'viem/chains'
|
|
@@ -25,39 +32,41 @@ interface ChainInfo {
|
|
|
25
32
|
chain: Chain
|
|
26
33
|
/** Subdomain under g.alchemy.com; absent where Alchemy has no endpoint. */
|
|
27
34
|
alchemy?: string
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
/** Used when no
|
|
35
|
+
/** False where Alchemy serves the RPC but not its token API, which `get_portfolio` needs. */
|
|
36
|
+
alchemyTokenApi?: false
|
|
37
|
+
/** Used when no Alchemy key is configured, or Alchemy has no endpoint. */
|
|
31
38
|
fallback: string
|
|
32
39
|
/** Block explorer domain, for links. Absent where there is nothing to link to. */
|
|
33
40
|
explorer?: string
|
|
34
41
|
hypersync?: string
|
|
42
|
+
/** DefiLlama's two names for the chain: the prefix of a coin id, and the `chain` of a yield pool. */
|
|
43
|
+
llama?: { coins: string; yields: string }
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
const TABLE = {
|
|
38
47
|
mainnet: {
|
|
39
48
|
chain: mainnet,
|
|
40
49
|
alchemy: 'eth-mainnet',
|
|
41
|
-
infura: 'mainnet',
|
|
42
50
|
fallback: 'https://ethereum-rpc.publicnode.com',
|
|
43
51
|
explorer: 'etherscan.io',
|
|
44
|
-
hypersync: 'https://eth.hypersync.xyz'
|
|
52
|
+
hypersync: 'https://eth.hypersync.xyz',
|
|
53
|
+
llama: { coins: 'ethereum', yields: 'Ethereum' }
|
|
45
54
|
},
|
|
46
55
|
arbitrum: {
|
|
47
56
|
chain: arbitrum,
|
|
48
57
|
alchemy: 'arb-mainnet',
|
|
49
|
-
infura: 'arbitrum-mainnet',
|
|
50
58
|
fallback: 'https://arb1.arbitrum.io/rpc',
|
|
51
59
|
explorer: 'arbiscan.io',
|
|
52
|
-
hypersync: 'https://arbitrum.hypersync.xyz'
|
|
60
|
+
hypersync: 'https://arbitrum.hypersync.xyz',
|
|
61
|
+
llama: { coins: 'arbitrum', yields: 'Arbitrum' }
|
|
53
62
|
},
|
|
54
63
|
avalanche: {
|
|
55
64
|
chain: avalanche,
|
|
56
65
|
alchemy: 'avax-mainnet',
|
|
57
|
-
infura: 'avalanche-mainnet',
|
|
58
66
|
fallback: 'https://api.avax.network/ext/bc/C/rpc',
|
|
59
67
|
explorer: 'snowtrace.io',
|
|
60
|
-
hypersync: 'https://avalanche.hypersync.xyz'
|
|
68
|
+
hypersync: 'https://avalanche.hypersync.xyz',
|
|
69
|
+
llama: { coins: 'avax', yields: 'Avalanche' }
|
|
61
70
|
},
|
|
62
71
|
base: {
|
|
63
72
|
chain: base,
|
|
@@ -65,70 +74,131 @@ const TABLE = {
|
|
|
65
74
|
// publicnode refuses receipts and anything else it calls an archive request
|
|
66
75
|
fallback: 'https://mainnet.base.org',
|
|
67
76
|
explorer: 'basescan.org',
|
|
68
|
-
hypersync: 'https://base.hypersync.xyz'
|
|
77
|
+
hypersync: 'https://base.hypersync.xyz',
|
|
78
|
+
llama: { coins: 'base', yields: 'Base' }
|
|
69
79
|
},
|
|
70
80
|
bnb: {
|
|
71
81
|
chain: bsc,
|
|
72
82
|
alchemy: 'bnb-mainnet',
|
|
73
|
-
infura: 'bsc-mainnet',
|
|
74
83
|
fallback: 'https://bsc-dataseed.bnbchain.org',
|
|
75
84
|
explorer: 'bscscan.com',
|
|
76
|
-
hypersync: 'https://bsc.hypersync.xyz'
|
|
85
|
+
hypersync: 'https://bsc.hypersync.xyz',
|
|
86
|
+
llama: { coins: 'bsc', yields: 'BSC' }
|
|
77
87
|
},
|
|
78
88
|
gnosis: {
|
|
79
89
|
chain: gnosis,
|
|
80
90
|
alchemy: 'gnosis-mainnet',
|
|
81
91
|
fallback: 'https://rpc.gnosischain.com',
|
|
82
92
|
explorer: 'gnosisscan.io',
|
|
83
|
-
hypersync: 'https://gnosis.hypersync.xyz'
|
|
84
|
-
|
|
85
|
-
sonic: {
|
|
86
|
-
chain: sonic,
|
|
87
|
-
alchemy: 'sonic-mainnet',
|
|
88
|
-
fallback: 'https://rpc.soniclabs.com',
|
|
89
|
-
explorer: 'sonicscan.org',
|
|
90
|
-
hypersync: 'https://sonic.hypersync.xyz'
|
|
93
|
+
hypersync: 'https://gnosis.hypersync.xyz',
|
|
94
|
+
llama: { coins: 'xdai', yields: 'Gnosis' }
|
|
91
95
|
},
|
|
92
96
|
optimism: {
|
|
93
97
|
chain: optimism,
|
|
94
98
|
alchemy: 'opt-mainnet',
|
|
95
|
-
infura: 'optimism-mainnet',
|
|
96
99
|
fallback: 'https://mainnet.optimism.io',
|
|
97
100
|
explorer: 'optimistic.etherscan.io',
|
|
98
|
-
hypersync: 'https://optimism.hypersync.xyz'
|
|
101
|
+
hypersync: 'https://optimism.hypersync.xyz',
|
|
102
|
+
llama: { coins: 'optimism', yields: 'OP Mainnet' }
|
|
99
103
|
},
|
|
100
104
|
polygon: {
|
|
101
105
|
chain: polygon,
|
|
102
106
|
alchemy: 'polygon-mainnet',
|
|
103
|
-
infura: 'polygon-mainnet',
|
|
104
107
|
// polygon-rpc.com now answers 401 "API key disabled" to anonymous callers
|
|
105
108
|
fallback: 'https://polygon-bor-rpc.publicnode.com',
|
|
106
109
|
explorer: 'polygonscan.com',
|
|
107
|
-
hypersync: 'https://polygon.hypersync.xyz'
|
|
110
|
+
hypersync: 'https://polygon.hypersync.xyz',
|
|
111
|
+
llama: { coins: 'polygon', yields: 'Polygon' }
|
|
108
112
|
},
|
|
109
113
|
zksync: {
|
|
110
114
|
chain: zksync,
|
|
111
115
|
alchemy: 'zksync-mainnet',
|
|
112
|
-
infura: 'zksync-mainnet',
|
|
113
116
|
fallback: 'https://mainnet.era.zksync.io',
|
|
114
117
|
explorer: 'era.zksync.network',
|
|
115
|
-
hypersync: 'https://zksync.hypersync.xyz'
|
|
118
|
+
hypersync: 'https://zksync.hypersync.xyz',
|
|
119
|
+
llama: { coins: 'era', yields: 'ZKsync Era' }
|
|
116
120
|
},
|
|
117
121
|
linea: {
|
|
118
122
|
chain: linea,
|
|
119
123
|
alchemy: 'linea-mainnet',
|
|
120
|
-
infura: 'linea-mainnet',
|
|
121
124
|
fallback: 'https://rpc.linea.build',
|
|
122
125
|
explorer: 'lineascan.build',
|
|
123
|
-
hypersync: 'https://linea.hypersync.xyz'
|
|
126
|
+
hypersync: 'https://linea.hypersync.xyz',
|
|
127
|
+
llama: { coins: 'linea', yields: 'Linea' }
|
|
124
128
|
},
|
|
125
129
|
unichain: {
|
|
126
130
|
chain: unichain,
|
|
127
131
|
alchemy: 'unichain-mainnet',
|
|
128
|
-
infura: 'unichain-mainnet',
|
|
129
132
|
fallback: 'https://mainnet.unichain.org',
|
|
130
133
|
explorer: 'uniscan.xyz',
|
|
131
|
-
hypersync: 'https://unichain.hypersync.xyz'
|
|
134
|
+
hypersync: 'https://unichain.hypersync.xyz',
|
|
135
|
+
llama: { coins: 'unichain', yields: 'Unichain' }
|
|
136
|
+
},
|
|
137
|
+
monad: {
|
|
138
|
+
chain: monad,
|
|
139
|
+
alchemy: 'monad-mainnet',
|
|
140
|
+
fallback: 'https://rpc.monad.xyz',
|
|
141
|
+
explorer: 'monadscan.com',
|
|
142
|
+
hypersync: 'https://monad.hypersync.xyz',
|
|
143
|
+
llama: { coins: 'monad', yields: 'Monad' }
|
|
144
|
+
},
|
|
145
|
+
robinhood: {
|
|
146
|
+
chain: robinhood,
|
|
147
|
+
alchemy: 'robinhood-mainnet',
|
|
148
|
+
fallback: 'https://rpc.mainnet.chain.robinhood.com',
|
|
149
|
+
explorer: 'robinhoodchain.blockscout.com',
|
|
150
|
+
hypersync: 'https://robinhood.hypersync.xyz',
|
|
151
|
+
llama: { coins: 'robinhood', yields: 'Robinhood Chain' }
|
|
152
|
+
},
|
|
153
|
+
arc: {
|
|
154
|
+
chain: arc,
|
|
155
|
+
alchemy: 'arc-mainnet',
|
|
156
|
+
fallback: 'https://rpc.mainnet.arc.io',
|
|
157
|
+
explorer: 'explorer.arc.io',
|
|
158
|
+
hypersync: 'https://arc.hypersync.xyz',
|
|
159
|
+
llama: { coins: 'arc', yields: 'Arc' }
|
|
160
|
+
},
|
|
161
|
+
plasma: {
|
|
162
|
+
chain: plasma,
|
|
163
|
+
alchemy: 'plasma-mainnet',
|
|
164
|
+
alchemyTokenApi: false,
|
|
165
|
+
fallback: 'https://rpc.plasma.to',
|
|
166
|
+
explorer: 'plasmascan.to',
|
|
167
|
+
hypersync: 'https://plasma.hypersync.xyz',
|
|
168
|
+
llama: { coins: 'plasma', yields: 'Plasma' }
|
|
169
|
+
},
|
|
170
|
+
ink: {
|
|
171
|
+
chain: ink,
|
|
172
|
+
alchemy: 'ink-mainnet',
|
|
173
|
+
fallback: 'https://rpc-gel.inkonchain.com',
|
|
174
|
+
explorer: 'explorer.inkonchain.com',
|
|
175
|
+
hypersync: 'https://ink.hypersync.xyz',
|
|
176
|
+
llama: { coins: 'ink', yields: 'Ink' }
|
|
177
|
+
},
|
|
178
|
+
mantle: {
|
|
179
|
+
chain: mantle,
|
|
180
|
+
alchemy: 'mantle-mainnet',
|
|
181
|
+
alchemyTokenApi: false,
|
|
182
|
+
fallback: 'https://rpc.mantle.xyz',
|
|
183
|
+
explorer: 'mantlescan.xyz',
|
|
184
|
+
hypersync: 'https://mantle.hypersync.xyz',
|
|
185
|
+
llama: { coins: 'mantle', yields: 'Mantle' }
|
|
186
|
+
},
|
|
187
|
+
celo: {
|
|
188
|
+
chain: celo,
|
|
189
|
+
alchemy: 'celo-mainnet',
|
|
190
|
+
fallback: 'https://forno.celo.org',
|
|
191
|
+
explorer: 'celoscan.io',
|
|
192
|
+
hypersync: 'https://celo.hypersync.xyz',
|
|
193
|
+
llama: { coins: 'celo', yields: 'Celo' }
|
|
194
|
+
},
|
|
195
|
+
hyperevm: {
|
|
196
|
+
chain: hyperEvm,
|
|
197
|
+
alchemy: 'hyperliquid-mainnet',
|
|
198
|
+
fallback: 'https://rpc.hyperliquid.xyz/evm',
|
|
199
|
+
explorer: 'hyperevmscan.io',
|
|
200
|
+
hypersync: 'https://hyperliquid.hypersync.xyz',
|
|
201
|
+
llama: { coins: 'hyperliquid', yields: 'Hyperliquid L1' }
|
|
132
202
|
},
|
|
133
203
|
localhost: {
|
|
134
204
|
chain: localhost,
|
package/src/clear-signing.ts
CHANGED
|
@@ -138,13 +138,19 @@ async function formatField(chain: ChainName, field: IndexField, root: unknown, t
|
|
|
138
138
|
const token =
|
|
139
139
|
field.params?.tokenAddress ?? (field.params?.tokenPath ? resolvePath(root, tx, field.params.tokenPath) : undefined)
|
|
140
140
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
141
|
+
const tokenAddress = typeof token === 'string' && isAddress(token) ? getAddress(token) : undefined
|
|
142
|
+
const meta = tokenAddress
|
|
143
|
+
? await tokenMeta(chain, tokenAddress).catch(() => ({ symbol: undefined, decimals: undefined }))
|
|
144
|
+
: { symbol: undefined, decimals: undefined }
|
|
145
|
+
// `amount` is the native currency, which always has 18 decimals.
|
|
146
|
+
const decimals = meta.decimals ?? (format === 'amount' && !tokenAddress ? 18 : undefined)
|
|
147
|
+
|
|
148
|
+
// symbol() is whatever the token contract says it is, so the address always goes with it.
|
|
149
|
+
const symbol = `${meta.symbol ? ` ${meta.symbol}` : ''}${tokenAddress ? ` (${tokenAddress})` : ''}`
|
|
150
|
+
if (tokenAddress) out.address = tokenAddress
|
|
151
|
+
if (amount > UNLIMITED_THRESHOLD) out.value = `Unlimited${symbol}`
|
|
152
|
+
else if (decimals === undefined) out.value = `${amount}${symbol} (decimals unknown)`
|
|
153
|
+
else out.value = `${formatUnits(amount, decimals)}${symbol}`
|
|
148
154
|
return out
|
|
149
155
|
}
|
|
150
156
|
|
|
@@ -156,10 +162,10 @@ async function formatField(chain: ChainName, field: IndexField, root: unknown, t
|
|
|
156
162
|
return out
|
|
157
163
|
}
|
|
158
164
|
const address = getAddress(raw as string)
|
|
159
|
-
const name =
|
|
165
|
+
const name = lookupContract(chain, address)?.protocol
|
|
160
166
|
out.address = address
|
|
161
167
|
out.name = name ? protocolLabel(name) : await addressLabel(chain, address).catch(() => undefined)
|
|
162
|
-
out.value = out.name
|
|
168
|
+
out.value = out.name ? `${out.name} (${address})` : address
|
|
163
169
|
return out
|
|
164
170
|
}
|
|
165
171
|
|
package/src/client.ts
CHANGED
|
@@ -4,14 +4,13 @@ import type { Config } from './types.js'
|
|
|
4
4
|
|
|
5
5
|
export { SUPPORTED_CHAINS } from './chains.js'
|
|
6
6
|
|
|
7
|
-
/** Custom URL first, then
|
|
7
|
+
/** Custom URL first, then Alchemy when a key is configured, then the public endpoint. */
|
|
8
8
|
function getRpcUrl(chainName: ChainName, config: Config): string {
|
|
9
9
|
const custom = config.customRpcUrls?.[chainName]
|
|
10
10
|
if (custom) return custom
|
|
11
11
|
|
|
12
|
-
const { alchemy,
|
|
12
|
+
const { alchemy, fallback } = CHAINS[chainName]
|
|
13
13
|
if (alchemy && config.alchemyApiKey) return `https://${alchemy}.g.alchemy.com/v2/${config.alchemyApiKey}`
|
|
14
|
-
if (infura && config.infuraApiKey) return `https://${infura}.infura.io/v3/${config.infuraApiKey}`
|
|
15
14
|
return fallback
|
|
16
15
|
}
|
|
17
16
|
|
package/src/http-server.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { getOAuthProtectedResourceMetadataUrl, mcpAuthRouter } from '@modelconte
|
|
|
4
4
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
5
5
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
|
|
6
6
|
import express, { type Express } from 'express'
|
|
7
|
-
import type
|
|
7
|
+
import { SIGN_PATH, type WalletRelay } from 'web3-wallet-relay'
|
|
8
8
|
import { HOSTED } from './hosted.js'
|
|
9
9
|
import { SingleUserOAuthProvider } from './oauth.js'
|
|
10
10
|
|
|
@@ -56,7 +56,7 @@ export async function startHttpServer(options: HttpServerOptions): Promise<{ url
|
|
|
56
56
|
)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const port = options.port ?? Number(process.env.MCP_HTTP_PORT)
|
|
59
|
+
const port = options.port ?? (Number(process.env.MCP_HTTP_PORT) || DEFAULT_PORT)
|
|
60
60
|
const host = options.host ?? process.env.MCP_HTTP_HOST ?? '0.0.0.0'
|
|
61
61
|
const token = options.token ?? process.env.MCP_TOKEN
|
|
62
62
|
|
|
@@ -72,6 +72,11 @@ export async function startHttpServer(options: HttpServerOptions): Promise<{ url
|
|
|
72
72
|
const provider = options.provider ?? new SingleUserOAuthProvider(token as string)
|
|
73
73
|
|
|
74
74
|
const app = express()
|
|
75
|
+
// The auth routes rate-limit per req.ip, which behind a platform proxy is the proxy's own
|
|
76
|
+
// address — every caller would share one bucket. Only trust as many hops as are really
|
|
77
|
+
// in front, or anyone could pick their own bucket with a forged X-Forwarded-For.
|
|
78
|
+
const proxyHops = Number(process.env.MCP_TRUST_PROXY)
|
|
79
|
+
if (proxyHops) app.set('trust proxy', proxyHops)
|
|
75
80
|
app.use(express.json())
|
|
76
81
|
// The login form posts a token, so the authorize route needs form bodies too.
|
|
77
82
|
app.use(express.urlencoded({ extended: false }))
|
|
@@ -124,18 +129,18 @@ export async function startHttpServer(options: HttpServerOptions): Promise<{ url
|
|
|
124
129
|
})
|
|
125
130
|
|
|
126
131
|
const server = await new Promise<ReturnType<typeof app.listen>>((resolve, reject) => {
|
|
127
|
-
|
|
128
|
-
listening.
|
|
132
|
+
// Express 5 hands a listen error to this callback rather than leaving it to 'error'.
|
|
133
|
+
const listening = app.listen(port, host, (error?: Error) => (error ? reject(error) : resolve(listening)))
|
|
129
134
|
})
|
|
130
135
|
|
|
131
136
|
// The signing page rides on this same server, so a hosted deployment can offer browser
|
|
132
137
|
// signing without a second service: one platform port serves both /mcp and the page.
|
|
133
138
|
// Mounted last, so the routes above keep their paths — the relay only claims what is
|
|
134
|
-
// left, which is the static page at
|
|
139
|
+
// left, which is the static page at /sign/.
|
|
135
140
|
let walletUrl: string | undefined
|
|
136
141
|
if (options.walletRelay) {
|
|
137
142
|
options.walletRelay.attach(server, (relayApp) => app.use(relayApp))
|
|
138
|
-
walletUrl = `${publicUrl.replace(/\/$/, '')}
|
|
143
|
+
walletUrl = `${publicUrl.replace(/\/$/, '')}${SIGN_PATH}#t=${options.walletRelay.signerToken}`
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
return { url: `${publicUrl.replace(/\/$/, '')}/mcp`, port: (server.address() as { port: number }).port, walletUrl }
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,6 @@ OPTIONS:
|
|
|
27
27
|
--help, -h Show this help message
|
|
28
28
|
--etherscan-api-key <key> Etherscan API key (for contract ABI retrieval)
|
|
29
29
|
--alchemy-api-key <key> Alchemy API key (for enhanced RPC)
|
|
30
|
-
--infura-api-key <key> Infura API key (for alternative RPC)
|
|
31
30
|
--hypersync-api-key <key> Hypersync API key (for fast event queries)
|
|
32
31
|
--walletconnect-project-id <id>
|
|
33
32
|
WalletConnect project id, to sign from a phone
|
|
@@ -37,11 +36,12 @@ OPTIONS:
|
|
|
37
36
|
ENVIRONMENT VARIABLES:
|
|
38
37
|
ETHERSCAN_API_KEY Alternative to --etherscan-api-key
|
|
39
38
|
ALCHEMY_API_KEY Alternative to --alchemy-api-key
|
|
40
|
-
INFURA_API_KEY Alternative to --infura-api-key
|
|
41
39
|
HYPERSYNC_API_KEY Alternative to --hypersync-api-key
|
|
42
40
|
WALLETCONNECT_PROJECT_ID Alternative to --walletconnect-project-id
|
|
43
41
|
CUSTOM_RPC Alternative to --custom-rpc
|
|
44
42
|
MCP_HTTP_PORT Serve MCP over HTTP on this port instead of stdio
|
|
43
|
+
MCP_HTTP_HOST Interface the HTTP server binds to (default: 0.0.0.0)
|
|
44
|
+
MCP_TRUST_PROXY Proxy hops in front of the server, e.g. 1 on Fly/Render
|
|
45
45
|
MCP_HOSTED Apply the hosted guards without the CLI (library use)
|
|
46
46
|
MCP_TOKEN Bearer token required by the HTTP transport
|
|
47
47
|
MCP_PUBLIC_URL Public URL of this server, advertised in OAuth metadata
|
|
@@ -82,9 +82,6 @@ if (config.etherscanApiKey) {
|
|
|
82
82
|
if (config.alchemyApiKey) {
|
|
83
83
|
console.error('[MCP] Alchemy API key configured')
|
|
84
84
|
}
|
|
85
|
-
if (config.infuraApiKey) {
|
|
86
|
-
console.error('[MCP] Infura API key configured')
|
|
87
|
-
}
|
|
88
85
|
if (config.walletConnectProjectId) {
|
|
89
86
|
console.error('[MCP] WalletConnect project id configured — phone signing available')
|
|
90
87
|
}
|
|
@@ -153,7 +150,9 @@ async function main() {
|
|
|
153
150
|
await wallet.connect().catch((error) => {
|
|
154
151
|
console.error('[MCP] Could not join the wallet relay:', error.message)
|
|
155
152
|
})
|
|
156
|
-
|
|
153
|
+
// Without its #t= token: platform logs are read by more people than may sign. The full
|
|
154
|
+
// link comes from wallet_status, which already requires MCP_TOKEN.
|
|
155
|
+
if (walletUrl) console.error(`Wallet interface available at ${walletUrl.split('#')[0]} (link with token via wallet_status)`)
|
|
157
156
|
|
|
158
157
|
if (!config.walletConnectProjectId) {
|
|
159
158
|
console.error('[MCP] No WalletConnect project id — phone signing is unavailable on this server')
|
package/src/kv-storage.ts
CHANGED
|
@@ -15,7 +15,8 @@ export interface KeyValueStorage {
|
|
|
15
15
|
getKeys(): Promise<string[]>
|
|
16
16
|
getEntries<T = unknown>(): Promise<[string, T][]>
|
|
17
17
|
getItem<T = unknown>(key: string): Promise<T | undefined>
|
|
18
|
-
|
|
18
|
+
/** `ttl` (seconds) expires just this field, where the backend can. */
|
|
19
|
+
setItem<T = unknown>(key: string, value: T, ttl?: number): Promise<void>
|
|
19
20
|
removeItem(key: string): Promise<void>
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -30,7 +31,8 @@ class RedisKeyValueStorage implements KeyValueStorage {
|
|
|
30
31
|
const response = await fetch(this.url, {
|
|
31
32
|
method: 'POST',
|
|
32
33
|
headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' },
|
|
33
|
-
body: JSON.stringify(parts)
|
|
34
|
+
body: JSON.stringify(parts),
|
|
35
|
+
signal: AbortSignal.timeout(5000)
|
|
34
36
|
})
|
|
35
37
|
|
|
36
38
|
if (!response.ok) {
|
|
@@ -62,8 +64,9 @@ class RedisKeyValueStorage implements KeyValueStorage {
|
|
|
62
64
|
return value == null ? undefined : (JSON.parse(value) as T)
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
async setItem<T = unknown>(key: string, value: T): Promise<void> {
|
|
67
|
+
async setItem<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {
|
|
66
68
|
await this.command('HSET', HASH_KEY, key, JSON.stringify(value))
|
|
69
|
+
if (ttl) await this.command('HEXPIRE', HASH_KEY, Math.ceil(ttl), 'FIELDS', 1, key)
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
async removeItem(key: string): Promise<void> {
|