web3-tools-mcp 1.5.0 → 2.0.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 (109) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +346 -164
  3. package/dist/anvil.d.ts +1 -1
  4. package/dist/anvil.d.ts.map +1 -1
  5. package/dist/anvil.js +11 -9
  6. package/dist/anvil.js.map +1 -1
  7. package/dist/chain-meta.d.ts.map +1 -1
  8. package/dist/chain-meta.js +7 -3
  9. package/dist/chain-meta.js.map +1 -1
  10. package/dist/chains.d.ts +960 -0
  11. package/dist/chains.d.ts.map +1 -0
  12. package/dist/chains.js +108 -0
  13. package/dist/chains.js.map +1 -0
  14. package/dist/client.d.ts +10 -1717
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +33 -216
  17. package/dist/client.js.map +1 -1
  18. package/dist/http-server.d.ts +18 -0
  19. package/dist/http-server.d.ts.map +1 -0
  20. package/dist/http-server.js +89 -0
  21. package/dist/http-server.js.map +1 -0
  22. package/dist/index.js +77 -32
  23. package/dist/index.js.map +1 -1
  24. package/dist/kv-storage.d.ts +23 -0
  25. package/dist/kv-storage.d.ts.map +1 -0
  26. package/dist/kv-storage.js +69 -0
  27. package/dist/kv-storage.js.map +1 -0
  28. package/dist/oauth.d.ts +31 -0
  29. package/dist/oauth.d.ts.map +1 -0
  30. package/dist/oauth.js +227 -0
  31. package/dist/oauth.js.map +1 -0
  32. package/dist/preview.d.ts.map +1 -1
  33. package/dist/preview.js +14 -4
  34. package/dist/preview.js.map +1 -1
  35. package/dist/tools/advanced.d.ts +4 -4
  36. package/dist/tools/advanced.d.ts.map +1 -1
  37. package/dist/tools/advanced.js +228 -247
  38. package/dist/tools/advanced.js.map +1 -1
  39. package/dist/tools/balance.d.ts +1 -1
  40. package/dist/tools/balance.d.ts.map +1 -1
  41. package/dist/tools/balance.js +97 -103
  42. package/dist/tools/balance.js.map +1 -1
  43. package/dist/tools/contract-info.d.ts +3 -3
  44. package/dist/tools/contract-info.d.ts.map +1 -1
  45. package/dist/tools/contract-info.js +150 -161
  46. package/dist/tools/contract-info.js.map +1 -1
  47. package/dist/tools/contract.d.ts +4 -4
  48. package/dist/tools/contract.d.ts.map +1 -1
  49. package/dist/tools/contract.js +91 -110
  50. package/dist/tools/contract.js.map +1 -1
  51. package/dist/tools/ens.d.ts +6 -6
  52. package/dist/tools/ens.d.ts.map +1 -1
  53. package/dist/tools/ens.js +239 -297
  54. package/dist/tools/ens.js.map +1 -1
  55. package/dist/tools/gas.d.ts +4 -4
  56. package/dist/tools/gas.d.ts.map +1 -1
  57. package/dist/tools/gas.js +90 -138
  58. package/dist/tools/gas.js.map +1 -1
  59. package/dist/tools/index.d.ts +1 -1
  60. package/dist/tools/index.d.ts.map +1 -1
  61. package/dist/tools/logs.d.ts +1 -1
  62. package/dist/tools/logs.d.ts.map +1 -1
  63. package/dist/tools/logs.js +36 -37
  64. package/dist/tools/logs.js.map +1 -1
  65. package/dist/tools/signatures.d.ts.map +1 -1
  66. package/dist/tools/signatures.js +67 -89
  67. package/dist/tools/signatures.js.map +1 -1
  68. package/dist/tools/transactions.d.ts +48 -4
  69. package/dist/tools/transactions.d.ts.map +1 -1
  70. package/dist/tools/transactions.js +228 -44
  71. package/dist/tools/transactions.js.map +1 -1
  72. package/dist/types.d.ts +2 -1
  73. package/dist/types.d.ts.map +1 -1
  74. package/dist/utils.d.ts +3 -8
  75. package/dist/utils.d.ts.map +1 -1
  76. package/dist/utils.js +36 -98
  77. package/dist/utils.js.map +1 -1
  78. package/dist/wallet-client.d.ts +11 -1
  79. package/dist/wallet-client.d.ts.map +1 -1
  80. package/dist/wallet-client.js +27 -9
  81. package/dist/wallet-client.js.map +1 -1
  82. package/dist/walletconnect.d.ts +52 -0
  83. package/dist/walletconnect.d.ts.map +1 -0
  84. package/dist/walletconnect.js +221 -0
  85. package/dist/walletconnect.js.map +1 -0
  86. package/package.json +21 -9
  87. package/src/anvil.ts +15 -18
  88. package/src/chain-meta.ts +8 -4
  89. package/src/chains.ts +145 -0
  90. package/src/client.ts +39 -252
  91. package/src/http-server.ts +117 -0
  92. package/src/index.ts +98 -49
  93. package/src/kv-storage.ts +83 -0
  94. package/src/oauth.ts +270 -0
  95. package/src/preview.ts +15 -14
  96. package/src/tools/advanced.ts +195 -213
  97. package/src/tools/balance.ts +103 -108
  98. package/src/tools/contract-info.ts +163 -184
  99. package/src/tools/contract.ts +123 -144
  100. package/src/tools/ens.ts +290 -345
  101. package/src/tools/gas.ts +122 -170
  102. package/src/tools/index.ts +2 -2
  103. package/src/tools/logs.ts +45 -43
  104. package/src/tools/signatures.ts +68 -86
  105. package/src/tools/transactions.ts +283 -50
  106. package/src/types.ts +2 -15
  107. package/src/utils.ts +56 -127
  108. package/src/wallet-client.ts +31 -9
  109. package/src/walletconnect.ts +269 -0
package/src/client.ts CHANGED
@@ -1,278 +1,65 @@
1
- import { http, createPublicClient } from 'viem'
2
- import {
3
- arbitrum,
4
- avalanche,
5
- base,
6
- bsc,
7
- gnosis,
8
- linea,
9
- localhost,
10
- mainnet,
11
- optimism,
12
- polygon,
13
- sonic,
14
- unichain,
15
- zksync
16
- } from 'viem/chains'
17
- import type { ChainName, Config } from './types.js'
1
+ import { createPublicClient, http } from 'viem'
2
+ import { CHAINS, type ChainName } from './chains.js'
3
+ import type { Config } from './types.js'
18
4
 
19
- // Enhanced RPC configuration with API keys
20
- function getRpcUrl(chainName: string, config: Config): string {
21
- // Check for custom RPC URLs first
22
- if (config.customRpcUrls?.[chainName]) {
23
- return config.customRpcUrls[chainName]
24
- }
25
-
26
- // Enhanced RPC URLs with API keys
27
- const enhancedRpcs: Record<string, string> = {
28
- mainnet: config.alchemyApiKey
29
- ? `https://eth-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
30
- : config.infuraApiKey
31
- ? `https://mainnet.infura.io/v3/${config.infuraApiKey}`
32
- : 'https://ethereum-rpc.publicnode.com',
33
-
34
- base: config.alchemyApiKey
35
- ? `https://base-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
36
- : 'https://base-rpc.publicnode.com',
37
-
38
- arbitrum: config.alchemyApiKey
39
- ? `https://arb-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
40
- : config.infuraApiKey
41
- ? `https://arbitrum-mainnet.infura.io/v3/${config.infuraApiKey}`
42
- : 'https://arb1.arbitrum.io/rpc',
43
-
44
- polygon: config.alchemyApiKey
45
- ? `https://polygon-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
46
- : config.infuraApiKey
47
- ? `https://polygon-mainnet.infura.io/v3/${config.infuraApiKey}`
48
- : 'https://polygon-rpc.com',
49
-
50
- optimism: config.alchemyApiKey
51
- ? `https://opt-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
52
- : config.infuraApiKey
53
- ? `https://optimism-mainnet.infura.io/v3/${config.infuraApiKey}`
54
- : 'https://mainnet.optimism.io',
55
-
56
- avalanche: config.alchemyApiKey
57
- ? `https://avax-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
58
- : config.infuraApiKey
59
- ? `https://avalanche-mainnet.infura.io/v3/${config.infuraApiKey}`
60
- : 'https://api.avax.network/ext/bc/C/rpc',
5
+ export { SUPPORTED_CHAINS } from './chains.js'
61
6
 
62
- bnb: config.alchemyApiKey
63
- ? `https://bnb-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
64
- : config.infuraApiKey
65
- ? `https://bsc-mainnet.infura.io/v3/${config.infuraApiKey}`
66
- : 'https://bsc-dataseed.bnbchain.org',
7
+ /** Custom URL first, then whichever provider key is configured, then the public endpoint. */
8
+ function getRpcUrl(chainName: ChainName, config: Config): string {
9
+ const custom = config.customRpcUrls?.[chainName]
10
+ if (custom) return custom
67
11
 
68
- gnosis: config.alchemyApiKey
69
- ? `https://gnosis-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
70
- : 'https://rpc.gnosischain.com',
71
-
72
- sonic: config.alchemyApiKey
73
- ? `https://sonic-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
74
- : 'https://rpc.soniclabs.com',
75
-
76
- zksync: config.alchemyApiKey
77
- ? `https://zksync-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
78
- : config.infuraApiKey
79
- ? `https://zksync-mainnet.infura.io/v3/${config.infuraApiKey}`
80
- : 'https://mainnet.era.zksync.io',
81
-
82
- linea: config.alchemyApiKey
83
- ? `https://linea-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
84
- : config.infuraApiKey
85
- ? `https://linea-mainnet.infura.io/v3/${config.infuraApiKey}`
86
- : 'https://rpc.linea.build',
87
-
88
- unichain: config.alchemyApiKey
89
- ? `https://unichain-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
90
- : config.infuraApiKey
91
- ? `https://unichain-mainnet.infura.io/v3/${config.infuraApiKey}`
92
- : 'https://sepolia.unichain.org',
93
-
94
- localhost: 'http://localhost:8545'
95
- }
96
-
97
- return enhancedRpcs[chainName] ?? 'https://eth.llamarpc.com'
12
+ const { alchemy, infura, fallback } = CHAINS[chainName]
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
+ return fallback
98
16
  }
99
17
 
100
- // Supported chains with their RPC endpoints (now dynamic)
101
- export const CHAINS = (config: Config) => ({
102
- mainnet: {
103
- chain: mainnet,
104
- get rpc() {
105
- return getRpcUrl('mainnet', config)
106
- }
107
- },
108
- base: {
109
- chain: base,
110
- get rpc() {
111
- return getRpcUrl('base', config)
112
- }
113
- },
114
- arbitrum: {
115
- chain: arbitrum,
116
- get rpc() {
117
- return getRpcUrl('arbitrum', config)
118
- }
119
- },
120
- polygon: {
121
- chain: polygon,
122
- get rpc() {
123
- return getRpcUrl('polygon', config)
124
- }
125
- },
126
- optimism: {
127
- chain: optimism,
128
- get rpc() {
129
- return getRpcUrl('optimism', config)
130
- }
131
- },
132
- avalanche: {
133
- chain: avalanche,
134
- get rpc() {
135
- return getRpcUrl('avalanche', config)
136
- }
137
- },
138
- bnb: {
139
- chain: bsc,
140
- get rpc() {
141
- return getRpcUrl('bnb', config)
142
- }
143
- },
144
- gnosis: {
145
- chain: gnosis,
146
- get rpc() {
147
- return getRpcUrl('gnosis', config)
148
- }
149
- },
150
- sonic: {
151
- chain: sonic,
152
- get rpc() {
153
- return getRpcUrl('sonic', config)
154
- }
155
- },
156
- zksync: {
157
- chain: zksync,
158
- get rpc() {
159
- return getRpcUrl('zksync', config)
160
- }
161
- },
162
- linea: {
163
- chain: linea,
164
- get rpc() {
165
- return getRpcUrl('linea', config)
166
- }
167
- },
168
- unichain: {
169
- chain: unichain,
170
- get rpc() {
171
- return getRpcUrl('unichain', config)
172
- }
173
- },
174
- localhost: {
175
- chain: localhost,
176
- get rpc() {
177
- return getRpcUrl('localhost', config)
178
- }
179
- }
180
- })
18
+ /** Named so declaration emit can refer to it: node_modules is hoisted above this package. */
19
+ type PublicClient = ReturnType<typeof createPublicClient>
181
20
 
182
- // Hypersync URLs for supported chains (localhost is not supported)
183
- export const HYPERSYNC_URLS: Partial<Record<ChainName, string>> = {
184
- mainnet: 'https://eth.hypersync.xyz', // Chain ID 1
185
- arbitrum: 'https://arbitrum.hypersync.xyz', // Chain ID 42161
186
- avalanche: 'https://avalanche.hypersync.xyz', // Chain ID 43114
187
- base: 'https://base.hypersync.xyz', // Chain ID 8453
188
- bnb: 'https://bsc.hypersync.xyz', // Chain ID 56
189
- gnosis: 'https://gnosis.hypersync.xyz', // Chain ID 100
190
- sonic: 'https://sonic.hypersync.xyz', // Chain ID 146
191
- optimism: 'https://optimism.hypersync.xyz', // Chain ID 10
192
- polygon: 'https://polygon.hypersync.xyz', // Chain ID 137
193
- zksync: 'https://zksync.hypersync.xyz', // Chain ID 324
194
- linea: 'https://linea.hypersync.xyz', // Chain ID 59144
195
- unichain: 'https://unichain.hypersync.xyz' // Chain ID 130
196
- // localhost is not supported by hypersync
197
- }
21
+ export class ClientManager {
22
+ private clients = new Map<ChainName, PublicClient>()
198
23
 
199
- export const SUPPORTED_CHAINS = [
200
- 'mainnet',
201
- 'arbitrum',
202
- 'avalanche',
203
- 'base',
204
- 'bnb',
205
- 'gnosis',
206
- 'sonic',
207
- 'optimism',
208
- 'polygon',
209
- 'zksync',
210
- 'linea',
211
- 'unichain',
212
- 'localhost'
213
- ] as const
24
+ constructor(private config: Config) {}
214
25
 
215
- // Client manager
216
- export class ClientManager {
217
- private clients: Map<ChainName, unknown> = new Map()
218
- private config: Config
26
+ getClient(chainName: ChainName): PublicClient {
27
+ const cached = this.clients.get(chainName)
28
+ if (cached) return cached
219
29
 
220
- constructor(config: Config) {
221
- this.config = config
222
- }
30
+ // Cast because a client built for a concrete chain is not assignable to the generic one.
31
+ const client = createPublicClient({
32
+ chain: CHAINS[chainName].chain,
33
+ transport: http(this.getRpcUrl(chainName))
34
+ }) as PublicClient
223
35
 
224
- getClient(chainName: ChainName) {
225
- if (!this.clients.has(chainName)) {
226
- const chains = CHAINS(this.config)
227
- const { chain, rpc } = chains[chainName]
228
- const client = createPublicClient({
229
- chain,
230
- transport: http(rpc)
231
- })
232
- this.clients.set(chainName, client)
233
- }
234
- const client = this.clients.get(chainName)
235
- if (!client) {
236
- throw new Error(`Failed to create client for chain: ${chainName}`)
237
- }
238
- return client as ReturnType<typeof createPublicClient>
36
+ this.clients.set(chainName, client)
37
+ return client
239
38
  }
240
39
 
241
40
  getConfig(): Config {
242
41
  return this.config
243
42
  }
244
43
 
245
- getEtherscanDomain(chainName: ChainName): string {
246
- const domains: Record<ChainName, string> = {
247
- mainnet: 'etherscan.io',
248
- base: 'basescan.org',
249
- arbitrum: 'arbiscan.io',
250
- polygon: 'polygonscan.com',
251
- optimism: 'optimistic.etherscan.io',
252
- avalanche: 'snowtrace.io',
253
- bnb: 'bscscan.com',
254
- gnosis: 'gnosisscan.io',
255
- sonic: 'sonicscan.org',
256
- zksync: 'era.zksync.network',
257
- linea: 'lineascan.build',
258
- unichain: 'uniscan.xyz',
259
- localhost: 'etherscan.io'
260
- }
261
- return domains[chainName] || 'etherscan.io'
262
- }
263
-
264
44
  getRpcUrl(chainName: ChainName): string {
265
- const chains = CHAINS(this.config)
266
- return chains[chainName].rpc
45
+ return getRpcUrl(chainName, this.config)
267
46
  }
268
47
 
269
48
  getChainId(chainName: ChainName): number {
270
- const chains = CHAINS(this.config)
271
- return chains[chainName].chain.id
49
+ return CHAINS[chainName].chain.id
50
+ }
51
+
52
+ getHypersyncUrl(chainName: ChainName): string | undefined {
53
+ return CHAINS[chainName].hypersync
54
+ }
55
+
56
+ /** Undefined where the chain has no explorer — a local node, for one. */
57
+ explorerUrl(chainName: ChainName, path = ''): string | undefined {
58
+ const domain = CHAINS[chainName].explorer
59
+ return domain ? `https://${domain}${path}` : undefined
272
60
  }
273
61
  }
274
62
 
275
- // Export a global client manager instance
276
63
  let clientManager: ClientManager | null = null
277
64
 
278
65
  export function initializeClientManager(config: Config): ClientManager {
@@ -0,0 +1,117 @@
1
+ import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'
2
+ import { getOAuthProtectedResourceMetadataUrl, mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js'
3
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
5
+ import express from 'express'
6
+ import type { WalletRelay } from 'web3-wallet-relay'
7
+ import { SingleUserOAuthProvider } from './oauth.js'
8
+
9
+ /**
10
+ * Serve the MCP over HTTP instead of stdio, so the server can live somewhere other than the
11
+ * machine running the agent. Signing still reaches you: a paired phone over WalletConnect,
12
+ * or the signing page, which is mounted on this same server so one platform port carries
13
+ * both it and /mcp.
14
+ *
15
+ * Two ways in, because clients differ: a static `Authorization: Bearer <MCP_TOKEN>` header
16
+ * (Claude Code), and the OAuth flow the MCP spec defines (claude.ai connectors, which offer
17
+ * no way to send a header). Both end up at the same single credential.
18
+ *
19
+ * Stateless — a server per request — because every tool here is a one-shot call and the
20
+ * signing state lives in the WalletConnect session, not in the transport.
21
+ */
22
+
23
+ const DEFAULT_PORT = 3457
24
+
25
+ export interface HttpServerOptions {
26
+ port?: number
27
+ host?: string
28
+ token?: string
29
+ /** Public URL clients reach this server on; OAuth metadata must advertise it. */
30
+ publicUrl?: string
31
+ /** Serve the signing page on this same server, so browser signing works when hosted. */
32
+ walletRelay?: WalletRelay
33
+ createMcpServer: () => McpServer
34
+ }
35
+
36
+ export async function startHttpServer(options: HttpServerOptions): Promise<{ url: string; port: number; walletUrl?: string }> {
37
+ const port = options.port ?? Number(process.env.MCP_HTTP_PORT) ?? DEFAULT_PORT
38
+ const host = options.host ?? process.env.MCP_HTTP_HOST ?? '0.0.0.0'
39
+ const token = options.token ?? process.env.MCP_TOKEN
40
+
41
+ // A reachable MCP server with a paired phone can push signing prompts at that phone. It
42
+ // never runs without a token.
43
+ if (!token) {
44
+ throw new Error('MCP_TOKEN is required to serve MCP over HTTP — anyone reaching the URL could request signatures')
45
+ }
46
+
47
+ const publicUrl = options.publicUrl ?? process.env.MCP_PUBLIC_URL ?? `http://localhost:${port}`
48
+ const issuer = new URL(publicUrl)
49
+ const provider = new SingleUserOAuthProvider(token)
50
+
51
+ const app = express()
52
+ app.use(express.json())
53
+ // The login form posts a token, so the authorize route needs form bodies too.
54
+ app.use(express.urlencoded({ extended: false }))
55
+
56
+ app.get('/health', (_req, res) => {
57
+ res.json({ status: 'ok' })
58
+ })
59
+
60
+ // /authorize, /token, /register, /revoke and the metadata documents a client discovers
61
+ // after a 401.
62
+ app.use(
63
+ mcpAuthRouter({
64
+ provider,
65
+ issuerUrl: issuer,
66
+ baseUrl: issuer,
67
+ resourceServerUrl: new URL('/mcp', issuer),
68
+ resourceName: 'web3-tools-mcp',
69
+ scopesSupported: []
70
+ })
71
+ )
72
+
73
+ // The metadata document is mounted under the resource path, so the 401 has to advertise
74
+ // that exact URL — a bare /.well-known/oauth-protected-resource 404s, and a client that
75
+ // follows it gives up right where discovery should have started.
76
+ const authenticate = requireBearerAuth({
77
+ verifier: provider,
78
+ resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL('/mcp', issuer))
79
+ })
80
+
81
+ app.all('/mcp', authenticate, async (req, res) => {
82
+ const mcp = options.createMcpServer()
83
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true })
84
+
85
+ res.on('close', () => {
86
+ transport.close().catch(() => {})
87
+ mcp.close().catch(() => {})
88
+ })
89
+
90
+ try {
91
+ await mcp.connect(transport)
92
+ await transport.handleRequest(req, res, req.body)
93
+ } catch (error) {
94
+ console.error('[MCP] Request failed:', error)
95
+ if (!res.headersSent) {
96
+ res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' } })
97
+ }
98
+ }
99
+ })
100
+
101
+ const server = await new Promise<ReturnType<typeof app.listen>>((resolve, reject) => {
102
+ const listening = app.listen(port, host, () => resolve(listening))
103
+ listening.once('error', reject)
104
+ })
105
+
106
+ // The signing page rides on this same server, so a hosted deployment can offer browser
107
+ // signing without a second service: one platform port serves both /mcp and the page.
108
+ // Mounted last, so the routes above keep their paths — the relay only claims what is
109
+ // left, which is the static page at the root.
110
+ let walletUrl: string | undefined
111
+ if (options.walletRelay) {
112
+ options.walletRelay.attach(server, (relayApp) => app.use(relayApp))
113
+ walletUrl = `${publicUrl.replace(/\/$/, '')}/#t=${options.walletRelay.token}`
114
+ }
115
+
116
+ return { url: `${publicUrl.replace(/\/$/, '')}/mcp`, port: (server.address() as { port: number }).port, walletUrl }
117
+ }
package/src/index.ts CHANGED
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import packageJson from "../package.json" with { type: "json" };
4
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { initializeClientManager } from "./client.js";
7
- import { registerAllTools } from "./tools/index.js";
8
- import { parseCommandLineArgs } from "./utils.js";
9
- import { getWalletClient } from "./wallet-client.js";
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
+ import { WalletRelay } from 'web3-wallet-relay'
6
+ import packageJson from '../package.json' with { type: 'json' }
7
+ import { initializeClientManager, SUPPORTED_CHAINS } from './client.js'
8
+ import { startHttpServer } from './http-server.js'
9
+ import { registerAllTools } from './tools/index.js'
10
+ import { parseCommandLineArgs } from './utils.js'
11
+ import { getWalletClient } from './wallet-client.js'
10
12
 
11
13
  // Parse configuration
12
- const config = parseCommandLineArgs();
14
+ const config = parseCommandLineArgs()
13
15
 
14
16
  // Show help if requested
15
17
  if (config.showHelp) {
@@ -26,6 +28,8 @@ OPTIONS:
26
28
  --alchemy-api-key <key> Alchemy API key (for enhanced RPC)
27
29
  --infura-api-key <key> Infura API key (for alternative RPC)
28
30
  --hypersync-api-key <key> Hypersync API key (for fast event queries)
31
+ --walletconnect-project-id <id>
32
+ WalletConnect project id, to sign from a phone
29
33
  --custom-rpc <json> Custom RPC URLs as JSON object
30
34
  Example: '{"mainnet":"https://...", "base":"https://..."}'
31
35
 
@@ -34,11 +38,18 @@ ENVIRONMENT VARIABLES:
34
38
  ALCHEMY_API_KEY Alternative to --alchemy-api-key
35
39
  INFURA_API_KEY Alternative to --infura-api-key
36
40
  HYPERSYNC_API_KEY Alternative to --hypersync-api-key
41
+ WALLETCONNECT_PROJECT_ID Alternative to --walletconnect-project-id
42
+ CUSTOM_RPC Alternative to --custom-rpc
43
+ MCP_HTTP_PORT Serve MCP over HTTP on this port instead of stdio
44
+ MCP_TOKEN Bearer token required by the HTTP transport
45
+ MCP_PUBLIC_URL Public URL of this server, advertised in OAuth metadata
46
+ UPSTASH_REDIS_REST_URL Keep WalletConnect sessions in Redis, not on disk
47
+ UPSTASH_REDIS_REST_TOKEN
37
48
  WALLET_SERVER_URL URL of a hosted wallet relay (omit to run one locally)
38
49
  WALLET_TOKEN Shared pairing token for the wallet relay
39
50
 
40
51
  SUPPORTED CHAINS:
41
- mainnet, arbitrum, avalanche, base, bnb, gnosis, sonic, optimism, polygon, zksync, linea, unichain, localhost
52
+ ${SUPPORTED_CHAINS.join(', ')}
42
53
 
43
54
  EXAMPLES:
44
55
  # Use with npx (recommended)
@@ -56,70 +67,108 @@ DOCUMENTATION:
56
67
  Issues: ${packageJson.bugs.url}
57
68
 
58
69
  For MCP client configuration, see the README.md file.
59
- `);
60
- process.exit(0);
70
+ `)
71
+ process.exit(0)
61
72
  }
62
73
 
63
74
  // Log configuration info
64
75
  if (config.etherscanApiKey) {
65
- console.error("[MCP] Etherscan API key configured");
76
+ console.error('[MCP] Etherscan API key configured')
66
77
  }
67
78
  if (config.alchemyApiKey) {
68
- console.error("[MCP] Alchemy API key configured");
79
+ console.error('[MCP] Alchemy API key configured')
69
80
  }
70
81
  if (config.infuraApiKey) {
71
- console.error("[MCP] Infura API key configured");
82
+ console.error('[MCP] Infura API key configured')
83
+ }
84
+ if (config.walletConnectProjectId) {
85
+ console.error('[MCP] WalletConnect project id configured — phone signing available')
72
86
  }
73
87
  if (config.customRpcUrls) {
74
- console.error("[MCP] Custom RPC URLs:", Object.keys(config.customRpcUrls));
88
+ console.error('[MCP] Custom RPC URLs:', Object.keys(config.customRpcUrls))
75
89
  }
76
90
 
77
91
  // Initialize client manager
78
- initializeClientManager(config);
79
-
80
- // Create MCP server instance
81
- const server = new McpServer({
82
- name: "web3-tools-mcp",
83
- version: packageJson.version,
84
- });
85
-
86
- // Register all tools
87
- registerAllTools(server);
92
+ initializeClientManager(config)
93
+
94
+ function createMcpServer() {
95
+ const server = new McpServer({
96
+ name: 'web3-tools-mcp',
97
+ version: packageJson.version
98
+ })
99
+ registerAllTools(server)
100
+ return server
101
+ }
88
102
 
89
- // Connect to the wallet relay (hosted when WALLET_SERVER_URL is set, embedded otherwise)
90
- const wallet = getWalletClient();
91
- const walletReady = wallet.connect().catch((error) => {
92
- console.error("[MCP] Wallet relay unavailable:", error.message);
93
- console.error("[MCP] Transaction signing features will not be available");
94
- });
103
+ const httpMode = Boolean(process.env.MCP_HTTP_PORT)
104
+ const httpPort = Number(process.env.MCP_HTTP_PORT)
105
+
106
+ /**
107
+ * Hosted, the signing page rides on the same port as /mcp, so a browser wallet works from
108
+ * a deployment too — the platform gives one port and both fit on it.
109
+ *
110
+ * Its token is minted per boot rather than persisted. A hosted relay is reachable from
111
+ * anywhere, and the pairing URL is fetched through wallet_status, which already needs
112
+ * MCP_TOKEN — so nobody types this token, and a short-lived one costs nothing. Locally the
113
+ * token stays on disk, because several editor sessions have to find the same relay.
114
+ */
115
+ const hostedRelay = httpMode
116
+ ? new WalletRelay({ token: process.env.WALLET_TOKEN, publicUrl: process.env.MCP_PUBLIC_URL })
117
+ : undefined
118
+
119
+ const wallet = getWalletClient(hostedRelay ? { port: httpPort, token: hostedRelay.token } : undefined)
120
+
121
+ // Hosted, the relay is attached to the HTTP server, so connect only once that is listening.
122
+ const walletReady = httpMode
123
+ ? Promise.resolve()
124
+ : wallet.connect().catch((error) => {
125
+ console.error('[MCP] Wallet relay unavailable:', error.message)
126
+ console.error('[MCP] Transaction signing features will not be available')
127
+ })
95
128
 
96
129
  // The wallet relay's listening socket keeps the event loop alive, so this process would
97
130
  // outlive the client that spawned it and go on holding its port. Leave when the client does.
98
- let shuttingDown = false;
131
+ let shuttingDown = false
99
132
  async function shutdown(reason: string) {
100
- if (shuttingDown) return;
101
- shuttingDown = true;
102
- console.error(`[MCP] Shutting down (${reason})`);
103
- await wallet.stop().catch(() => {});
104
- process.exit(0);
133
+ if (shuttingDown) return
134
+ shuttingDown = true
135
+ console.error(`[MCP] Shutting down (${reason})`)
136
+ await wallet.stop().catch(() => {})
137
+ process.exit(0)
105
138
  }
106
139
 
107
- process.stdin.on("end", () => shutdown("client disconnected"));
108
- process.stdin.on("close", () => shutdown("client disconnected"));
109
- process.on("SIGTERM", () => shutdown("SIGTERM"));
110
- process.on("SIGINT", () => shutdown("SIGINT"));
140
+ process.stdin.on('end', () => shutdown('client disconnected'))
141
+ process.stdin.on('close', () => shutdown('client disconnected'))
142
+ process.on('SIGTERM', () => shutdown('SIGTERM'))
143
+ process.on('SIGINT', () => shutdown('SIGINT'))
111
144
 
112
145
  // Start server
113
146
  async function main() {
114
- const transport = new StdioServerTransport();
115
- await server.connect(transport);
116
- console.error("Web3 Tools MCP Server running on stdio");
147
+ if (httpMode) {
148
+ const { url, walletUrl } = await startHttpServer({ createMcpServer, walletRelay: hostedRelay })
149
+ console.error(`Web3 Tools MCP Server listening on ${url}`)
150
+
151
+ // Join the relay now that it is listening, so browser signing is available here too.
152
+ await wallet.connect().catch((error) => {
153
+ console.error('[MCP] Could not join the wallet relay:', error.message)
154
+ })
155
+ if (walletUrl) console.error(`Wallet interface available at ${walletUrl}`)
156
+
157
+ if (!config.walletConnectProjectId) {
158
+ console.error('[MCP] No WalletConnect project id — phone signing is unavailable on this server')
159
+ }
160
+ return
161
+ }
162
+
163
+ const transport = new StdioServerTransport()
164
+ await createMcpServer().connect(transport)
165
+ console.error('Web3 Tools MCP Server running on stdio')
117
166
  // The relay may still be picking a free port, and its URL carries the pairing token.
118
- await walletReady;
119
- console.error(`Wallet interface available at ${wallet.getUrl()}`);
167
+ await walletReady
168
+ console.error(`Wallet interface available at ${wallet.getUrl()}`)
120
169
  }
121
170
 
122
171
  main().catch((error) => {
123
- console.error("Fatal error:", error);
124
- process.exit(1);
125
- });
172
+ console.error('Fatal error:', error)
173
+ process.exit(1)
174
+ })