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.
Files changed (113) hide show
  1. package/CHANGELOG.md +115 -2
  2. package/README.md +96 -60
  3. package/dist/chain-meta.d.ts.map +1 -1
  4. package/dist/chain-meta.js +19 -8
  5. package/dist/chain-meta.js.map +1 -1
  6. package/dist/chains.d.ts +609 -65
  7. package/dist/chains.d.ts.map +1 -1
  8. package/dist/chains.js +89 -28
  9. package/dist/chains.js.map +1 -1
  10. package/dist/clear-signing.d.ts.map +1 -1
  11. package/dist/clear-signing.js +17 -6
  12. package/dist/clear-signing.js.map +1 -1
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +2 -4
  15. package/dist/client.js.map +1 -1
  16. package/dist/http-server.d.ts +1 -1
  17. package/dist/http-server.d.ts.map +1 -1
  18. package/dist/http-server.js +12 -5
  19. package/dist/http-server.js.map +1 -1
  20. package/dist/index.js +5 -6
  21. package/dist/index.js.map +1 -1
  22. package/dist/kv-storage.d.ts +2 -1
  23. package/dist/kv-storage.d.ts.map +1 -1
  24. package/dist/kv-storage.js +5 -2
  25. package/dist/kv-storage.js.map +1 -1
  26. package/dist/oauth.d.ts +18 -4
  27. package/dist/oauth.d.ts.map +1 -1
  28. package/dist/oauth.js +62 -26
  29. package/dist/oauth.js.map +1 -1
  30. package/dist/preview.d.ts.map +1 -1
  31. package/dist/preview.js +7 -5
  32. package/dist/preview.js.map +1 -1
  33. package/dist/tools/index.d.ts.map +1 -1
  34. package/dist/tools/index.js +16 -1
  35. package/dist/tools/index.js.map +1 -1
  36. package/dist/tools/read/advanced.d.ts +25 -20
  37. package/dist/tools/read/advanced.d.ts.map +1 -1
  38. package/dist/tools/read/advanced.js +45 -33
  39. package/dist/tools/read/advanced.js.map +1 -1
  40. package/dist/tools/read/balance.d.ts +6 -6
  41. package/dist/tools/read/balance.d.ts.map +1 -1
  42. package/dist/tools/read/balance.js +17 -32
  43. package/dist/tools/read/balance.js.map +1 -1
  44. package/dist/tools/read/contract-info.d.ts +14 -12
  45. package/dist/tools/read/contract-info.d.ts.map +1 -1
  46. package/dist/tools/read/contract-info.js +53 -122
  47. package/dist/tools/read/contract-info.js.map +1 -1
  48. package/dist/tools/read/contract.d.ts +10 -10
  49. package/dist/tools/read/contract.d.ts.map +1 -1
  50. package/dist/tools/read/contract.js +2 -1
  51. package/dist/tools/read/contract.js.map +1 -1
  52. package/dist/tools/read/defi.d.ts +86 -0
  53. package/dist/tools/read/defi.d.ts.map +1 -0
  54. package/dist/tools/read/defi.js +210 -0
  55. package/dist/tools/read/defi.js.map +1 -0
  56. package/dist/tools/read/ens.d.ts +20 -40
  57. package/dist/tools/read/ens.d.ts.map +1 -1
  58. package/dist/tools/read/ens.js +25 -33
  59. package/dist/tools/read/ens.js.map +1 -1
  60. package/dist/tools/read/gas.d.ts +16 -16
  61. package/dist/tools/read/gas.d.ts.map +1 -1
  62. package/dist/tools/read/gas.js +13 -48
  63. package/dist/tools/read/gas.js.map +1 -1
  64. package/dist/tools/read/logs.d.ts +65 -4
  65. package/dist/tools/read/logs.d.ts.map +1 -1
  66. package/dist/tools/read/logs.js +96 -115
  67. package/dist/tools/read/logs.js.map +1 -1
  68. package/dist/tools/read/signatures.d.ts.map +1 -1
  69. package/dist/tools/read/signatures.js +4 -3
  70. package/dist/tools/read/signatures.js.map +1 -1
  71. package/dist/tools/read/signing.d.ts.map +1 -1
  72. package/dist/tools/read/signing.js +8 -2
  73. package/dist/tools/read/signing.js.map +1 -1
  74. package/dist/tools/sign/transactions.d.ts +16 -16
  75. package/dist/tools/sign/transactions.d.ts.map +1 -1
  76. package/dist/tools/sign/transactions.js +54 -9
  77. package/dist/tools/sign/transactions.js.map +1 -1
  78. package/dist/tools/sign/wallet.d.ts +4 -4
  79. package/dist/types.d.ts +0 -1
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/utils.d.ts +18 -0
  82. package/dist/utils.d.ts.map +1 -1
  83. package/dist/utils.js +43 -2
  84. package/dist/utils.js.map +1 -1
  85. package/dist/wallet-client.d.ts +1 -0
  86. package/dist/wallet-client.d.ts.map +1 -1
  87. package/dist/wallet-client.js +18 -5
  88. package/dist/wallet-client.js.map +1 -1
  89. package/package.json +4 -7
  90. package/src/chain-meta.ts +16 -7
  91. package/src/chains.ts +101 -31
  92. package/src/clear-signing.ts +15 -9
  93. package/src/client.ts +2 -3
  94. package/src/http-server.ts +11 -6
  95. package/src/index.ts +5 -6
  96. package/src/kv-storage.ts +6 -3
  97. package/src/oauth.ts +93 -34
  98. package/src/preview.ts +7 -5
  99. package/src/tools/index.ts +14 -1
  100. package/src/tools/read/advanced.ts +63 -34
  101. package/src/tools/read/balance.ts +18 -33
  102. package/src/tools/read/contract-info.ts +64 -144
  103. package/src/tools/read/contract.ts +2 -1
  104. package/src/tools/read/defi.ts +287 -0
  105. package/src/tools/read/ens.ts +26 -33
  106. package/src/tools/read/gas.ts +13 -50
  107. package/src/tools/read/logs.ts +131 -143
  108. package/src/tools/read/signatures.ts +4 -4
  109. package/src/tools/read/signing.ts +8 -2
  110. package/src/tools/sign/transactions.ts +60 -9
  111. package/src/types.ts +0 -1
  112. package/src/utils.ts +47 -2
  113. 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
- const abiCache = new Map<string, { abi: Abi; source: 'verified' | 'guessed'; proxy?: string; name?: string }>()
18
- const labelCache = new Map<string, string | undefined>()
19
- const metaCache = new Map<string, { symbol?: string; decimals?: number }>()
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
- metaCache.set(key, meta)
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
- if (labelCache.has(key)) return labelCache.get(key)
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
- sonic,
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
- /** Subdomain under infura.io; absent where Infura has no endpoint. */
29
- infura?: string
30
- /** Used when no provider key is configured. */
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,
@@ -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 meta =
142
- typeof token === 'string' && isAddress(token)
143
- ? await tokenMeta(chain, token).catch(() => ({ symbol: undefined, decimals: undefined }))
144
- : { symbol: undefined, decimals: undefined }
145
-
146
- const symbol = meta.symbol ? ` ${meta.symbol}` : ''
147
- out.value = amount > UNLIMITED_THRESHOLD ? `Unlimited${symbol}` : `${formatUnits(amount, meta.decimals ?? 18)}${symbol}`
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 = registry()[address.toLowerCase()]?.protocol
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 ?? address
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 whichever provider key is configured, then the public endpoint. */
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, infura, fallback } = CHAINS[chainName]
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
 
@@ -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 { WalletRelay } from 'web3-wallet-relay'
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) ?? DEFAULT_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
- const listening = app.listen(port, host, () => resolve(listening))
128
- listening.once('error', reject)
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 the root.
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(/\/$/, '')}/#t=${options.walletRelay.signerToken}`
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
- if (walletUrl) console.error(`Wallet interface available at ${walletUrl}`)
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
- setItem<T = unknown>(key: string, value: T): Promise<void>
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> {