web3-tools-mcp 2.0.2 → 3.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.
Files changed (180) hide show
  1. package/CHANGELOG.md +182 -1
  2. package/README.md +76 -47
  3. package/dist/anvil.d.ts +17 -19
  4. package/dist/anvil.d.ts.map +1 -1
  5. package/dist/anvil.js +124 -228
  6. package/dist/anvil.js.map +1 -1
  7. package/dist/chain-meta.d.ts.map +1 -1
  8. package/dist/chain-meta.js +19 -8
  9. package/dist/chain-meta.js.map +1 -1
  10. package/dist/chains.d.ts +9 -1
  11. package/dist/chains.d.ts.map +1 -1
  12. package/dist/chains.js +11 -2
  13. package/dist/chains.js.map +1 -1
  14. package/dist/clear-signing.d.ts.map +1 -1
  15. package/dist/clear-signing.js +17 -6
  16. package/dist/clear-signing.js.map +1 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js.map +1 -1
  19. package/dist/context.d.ts +22 -0
  20. package/dist/context.d.ts.map +1 -0
  21. package/dist/context.js +24 -0
  22. package/dist/context.js.map +1 -0
  23. package/dist/hosted.d.ts +12 -0
  24. package/dist/hosted.d.ts.map +1 -0
  25. package/dist/hosted.js +12 -0
  26. package/dist/hosted.js.map +1 -0
  27. package/dist/http-server.d.ts +13 -0
  28. package/dist/http-server.d.ts.map +1 -1
  29. package/dist/http-server.js +22 -7
  30. package/dist/http-server.js.map +1 -1
  31. package/dist/index.js +20 -12
  32. package/dist/index.js.map +1 -1
  33. package/dist/kv-storage.d.ts +2 -1
  34. package/dist/kv-storage.d.ts.map +1 -1
  35. package/dist/kv-storage.js +5 -2
  36. package/dist/kv-storage.js.map +1 -1
  37. package/dist/lib.d.ts +24 -0
  38. package/dist/lib.d.ts.map +1 -0
  39. package/dist/lib.js +23 -0
  40. package/dist/lib.js.map +1 -0
  41. package/dist/mcp-server.d.ts +4 -0
  42. package/dist/mcp-server.d.ts.map +1 -0
  43. package/dist/mcp-server.js +43 -0
  44. package/dist/mcp-server.js.map +1 -0
  45. package/dist/oauth.d.ts +57 -8
  46. package/dist/oauth.d.ts.map +1 -1
  47. package/dist/oauth.js +133 -63
  48. package/dist/oauth.js.map +1 -1
  49. package/dist/preview.d.ts +7 -0
  50. package/dist/preview.d.ts.map +1 -1
  51. package/dist/preview.js +21 -7
  52. package/dist/preview.js.map +1 -1
  53. package/dist/signing-requests.d.ts +81 -0
  54. package/dist/signing-requests.d.ts.map +1 -0
  55. package/dist/signing-requests.js +138 -0
  56. package/dist/signing-requests.js.map +1 -0
  57. package/dist/tools/index.d.ts +27 -1
  58. package/dist/tools/index.d.ts.map +1 -1
  59. package/dist/tools/index.js +50 -15
  60. package/dist/tools/index.js.map +1 -1
  61. package/dist/tools/{advanced.d.ts → read/advanced.d.ts} +39 -30
  62. package/dist/tools/read/advanced.d.ts.map +1 -0
  63. package/dist/tools/{advanced.js → read/advanced.js} +53 -41
  64. package/dist/tools/read/advanced.js.map +1 -0
  65. package/dist/tools/{balance.d.ts → read/balance.d.ts} +8 -7
  66. package/dist/tools/read/balance.d.ts.map +1 -0
  67. package/dist/tools/{balance.js → read/balance.js} +18 -33
  68. package/dist/tools/read/balance.js.map +1 -0
  69. package/dist/tools/{contract-info.d.ts → read/contract-info.d.ts} +20 -15
  70. package/dist/tools/read/contract-info.d.ts.map +1 -0
  71. package/dist/tools/{contract-info.js → read/contract-info.js} +54 -123
  72. package/dist/tools/read/contract-info.js.map +1 -0
  73. package/dist/tools/{contract.d.ts → read/contract.d.ts} +14 -12
  74. package/dist/tools/read/contract.d.ts.map +1 -0
  75. package/dist/tools/{contract.js → read/contract.js} +3 -2
  76. package/dist/tools/read/contract.js.map +1 -0
  77. package/dist/tools/read/ens.d.ts +104 -0
  78. package/dist/tools/read/ens.d.ts.map +1 -0
  79. package/dist/tools/{ens.js → read/ens.js} +25 -33
  80. package/dist/tools/read/ens.js.map +1 -0
  81. package/dist/tools/read/gas.d.ts +171 -0
  82. package/dist/tools/read/gas.d.ts.map +1 -0
  83. package/dist/tools/read/gas.js +333 -0
  84. package/dist/tools/read/gas.js.map +1 -0
  85. package/dist/tools/read/logs.d.ts +101 -0
  86. package/dist/tools/read/logs.d.ts.map +1 -0
  87. package/dist/tools/read/logs.js +211 -0
  88. package/dist/tools/read/logs.js.map +1 -0
  89. package/dist/tools/{signatures.d.ts → read/signatures.d.ts} +8 -4
  90. package/dist/tools/read/signatures.d.ts.map +1 -0
  91. package/dist/tools/{signatures.js → read/signatures.js} +4 -3
  92. package/dist/tools/read/signatures.js.map +1 -0
  93. package/dist/tools/read/signing.d.ts +27 -0
  94. package/dist/tools/read/signing.d.ts.map +1 -0
  95. package/dist/tools/read/signing.js +81 -0
  96. package/dist/tools/read/signing.js.map +1 -0
  97. package/dist/tools/sign/errors.d.ts +9 -0
  98. package/dist/tools/sign/errors.d.ts.map +1 -0
  99. package/dist/tools/sign/errors.js +29 -0
  100. package/dist/tools/sign/errors.js.map +1 -0
  101. package/dist/tools/sign/transactions.d.ts +141 -0
  102. package/dist/tools/sign/transactions.d.ts.map +1 -0
  103. package/dist/tools/sign/transactions.js +361 -0
  104. package/dist/tools/sign/transactions.js.map +1 -0
  105. package/dist/tools/sign/wallet.d.ts +50 -0
  106. package/dist/tools/sign/wallet.d.ts.map +1 -0
  107. package/dist/tools/sign/wallet.js +180 -0
  108. package/dist/tools/sign/wallet.js.map +1 -0
  109. package/dist/utils.d.ts +23 -2
  110. package/dist/utils.d.ts.map +1 -1
  111. package/dist/utils.js +48 -3
  112. package/dist/utils.js.map +1 -1
  113. package/dist/wallet-client.d.ts +34 -4
  114. package/dist/wallet-client.d.ts.map +1 -1
  115. package/dist/wallet-client.js +106 -27
  116. package/dist/wallet-client.js.map +1 -1
  117. package/dist/walletconnect.d.ts +100 -13
  118. package/dist/walletconnect.d.ts.map +1 -1
  119. package/dist/walletconnect.js +215 -50
  120. package/dist/walletconnect.js.map +1 -1
  121. package/package.json +12 -9
  122. package/src/anvil.ts +150 -271
  123. package/src/chain-meta.ts +16 -7
  124. package/src/chains.ts +14 -2
  125. package/src/clear-signing.ts +15 -9
  126. package/src/client.ts +0 -1
  127. package/src/context.ts +27 -0
  128. package/src/hosted.ts +11 -0
  129. package/src/http-server.ts +38 -8
  130. package/src/index.ts +20 -13
  131. package/src/kv-storage.ts +6 -3
  132. package/src/lib.ts +24 -0
  133. package/src/mcp-server.ts +47 -0
  134. package/src/oauth.ts +181 -69
  135. package/src/preview.ts +28 -7
  136. package/src/signing-requests.ts +198 -0
  137. package/src/tools/index.ts +78 -16
  138. package/src/tools/{advanced.ts → read/advanced.ts} +74 -43
  139. package/src/tools/{balance.ts → read/balance.ts} +20 -35
  140. package/src/tools/{contract-info.ts → read/contract-info.ts} +66 -146
  141. package/src/tools/{contract.ts → read/contract.ts} +4 -3
  142. package/src/tools/{ens.ts → read/ens.ts} +27 -34
  143. package/src/tools/read/gas.ts +443 -0
  144. package/src/tools/read/logs.ts +281 -0
  145. package/src/tools/{signatures.ts → read/signatures.ts} +5 -5
  146. package/src/tools/read/signing.ts +92 -0
  147. package/src/tools/sign/errors.ts +28 -0
  148. package/src/tools/sign/transactions.ts +479 -0
  149. package/src/tools/sign/wallet.ts +216 -0
  150. package/src/utils.ts +52 -3
  151. package/src/wallet-client.ts +107 -24
  152. package/src/walletconnect.ts +250 -48
  153. package/dist/tools/advanced.d.ts.map +0 -1
  154. package/dist/tools/advanced.js.map +0 -1
  155. package/dist/tools/balance.d.ts.map +0 -1
  156. package/dist/tools/balance.js.map +0 -1
  157. package/dist/tools/contract-info.d.ts.map +0 -1
  158. package/dist/tools/contract-info.js.map +0 -1
  159. package/dist/tools/contract.d.ts.map +0 -1
  160. package/dist/tools/contract.js.map +0 -1
  161. package/dist/tools/ens.d.ts +0 -119
  162. package/dist/tools/ens.d.ts.map +0 -1
  163. package/dist/tools/ens.js.map +0 -1
  164. package/dist/tools/gas.d.ts +0 -99
  165. package/dist/tools/gas.d.ts.map +0 -1
  166. package/dist/tools/gas.js +0 -181
  167. package/dist/tools/gas.js.map +0 -1
  168. package/dist/tools/logs.d.ts +0 -39
  169. package/dist/tools/logs.d.ts.map +0 -1
  170. package/dist/tools/logs.js +0 -230
  171. package/dist/tools/logs.js.map +0 -1
  172. package/dist/tools/signatures.d.ts.map +0 -1
  173. package/dist/tools/signatures.js.map +0 -1
  174. package/dist/tools/transactions.d.ts +0 -155
  175. package/dist/tools/transactions.d.ts.map +0 -1
  176. package/dist/tools/transactions.js +0 -365
  177. package/dist/tools/transactions.js.map +0 -1
  178. package/src/tools/gas.ts +0 -219
  179. package/src/tools/logs.ts +0 -293
  180. package/src/tools/transactions.ts +0 -456
@@ -1,17 +1,33 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
2
  import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'
3
3
  import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js'
4
- import advancedTools from './advanced.js'
5
- import balanceTools from './balance.js'
6
- import contractTools from './contract.js'
7
- import contractInfoTools from './contract-info.js'
8
- import ensTools from './ens.js'
9
- import gasTools from './gas.js'
10
- import logTools from './logs.js'
11
- import signatureTools from './signatures.js'
12
- import transactionTools from './transactions.js'
13
-
14
- const allToolDefinitions = {
4
+ import { getClientManager } from '../client.js'
5
+ import { identityFrom } from '../context.js'
6
+ import type { ToolResult } from '../types.js'
7
+ import { redactSecrets } from '../utils.js'
8
+ import advancedTools from './read/advanced.js'
9
+ import balanceTools from './read/balance.js'
10
+ import contractTools from './read/contract.js'
11
+ import contractInfoTools from './read/contract-info.js'
12
+ import ensTools from './read/ens.js'
13
+ import gasTools from './read/gas.js'
14
+ import logTools from './read/logs.js'
15
+ import signatureTools from './read/signatures.js'
16
+ import signingRequestTools from './read/signing.js'
17
+ import transactionTools from './sign/transactions.js'
18
+ import walletTools from './sign/wallet.js'
19
+
20
+ /**
21
+ * What a tool does with a wallet, which is the line everything else divides along.
22
+ *
23
+ * `read` answers questions about a chain and costs an RPC call. `sign` puts a transaction
24
+ * in front of somebody's wallet, and is the reason identity and rooms exist. A host pricing
25
+ * or rate-limiting tool calls almost always wants this distinction, and taking it from the
26
+ * folder a tool lives in means it cannot drift from a hand-kept list.
27
+ */
28
+ export type ToolGroup = 'read' | 'sign'
29
+
30
+ const readTools = {
15
31
  ...signatureTools,
16
32
  ...contractTools,
17
33
  ...contractInfoTools,
@@ -20,12 +36,45 @@ const allToolDefinitions = {
20
36
  ...advancedTools,
21
37
  ...ensTools,
22
38
  ...gasTools,
23
- ...transactionTools
39
+ // A signature already asked for and paid for; collecting it is not a second request.
40
+ ...signingRequestTools
24
41
  } as const
25
42
 
26
- // Register all tools with the MCP server
27
- export function registerAllTools(server: McpServer) {
43
+ const signTools = {
44
+ ...transactionTools,
45
+ ...walletTools
46
+ } as const
47
+
48
+ const allToolDefinitions = { ...readTools, ...signTools } as const
49
+
50
+ const groups = new Map<string, ToolGroup>([
51
+ ...Object.keys(readTools).map((name) => [name, 'read'] as const),
52
+ ...Object.keys(signTools).map((name) => [name, 'sign'] as const)
53
+ ])
54
+
55
+ /** The group a tool belongs to, for a host deciding what it costs or who may call it. */
56
+ export function toolGroup(name: string): ToolGroup | undefined {
57
+ return groups.get(name)
58
+ }
59
+
60
+ /**
61
+ * Wraps every tool call, for a host that needs to say who may run what.
62
+ *
63
+ * Deliberately only the tool's name, its group and who is calling: enough to count calls,
64
+ * refuse them past a limit and record what was used, without this package taking a view on
65
+ * any of it. Throwing refuses the call, and the message reaches the agent — so it should
66
+ * say what to do about it. Whatever the returned promise settles as is what the caller
67
+ * sees, so a host can also time a call or meter it once it has succeeded.
68
+ */
69
+ export type ToolMiddleware = (
70
+ call: { tool: string; group: ToolGroup; identity: string },
71
+ run: () => Promise<ToolResult>
72
+ ) => Promise<ToolResult>
73
+
74
+ export function registerAllTools(server: McpServer, middleware?: ToolMiddleware) {
28
75
  Object.entries(allToolDefinitions).forEach(([name, tool]) => {
76
+ if (tool.available && !tool.available()) return
77
+
29
78
  server.registerTool(
30
79
  name,
31
80
  {
@@ -33,8 +82,21 @@ export function registerAllTools(server: McpServer) {
33
82
  description: tool.description,
34
83
  inputSchema: tool.schema.shape
35
84
  },
36
- async (args: any, _extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
37
- return await tool.handler(args)
85
+ async (args: any, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
86
+ // Passed to every handler; the ones that reach a signer must say whose it is.
87
+ const identity = identityFrom(extra.authInfo)
88
+ const run = () => tool.handler(args, identity)
89
+ const group = toolGroup(name) as ToolGroup
90
+ // Anything a tool returns or throws may quote an RPC URL, and those carry provider keys.
91
+ const config = getClientManager().getConfig()
92
+ try {
93
+ const result = await (middleware ? middleware({ tool: name, group, identity }, run) : run())
94
+ return { ...result, content: result.content.map((item) => ({ ...item, text: redactSecrets(item.text, config) })) }
95
+ } catch (error) {
96
+ if (!(error instanceof Error)) throw new Error(redactSecrets(String(error), config))
97
+ error.message = redactSecrets(error.message, config)
98
+ throw error
99
+ }
38
100
  }
39
101
  )
40
102
  })
@@ -1,9 +1,37 @@
1
- import { type Address, decodeAbiParameters, isAddress, parseAbiParameters } from 'viem'
1
+ import {
2
+ type AbiFunction,
3
+ type Address,
4
+ decodeAbiParameters,
5
+ encodeFunctionData,
6
+ isAddress,
7
+ parseAbiItem,
8
+ parseAbiParameters
9
+ } from 'viem'
2
10
  import { z } from 'zod'
3
- import { isAnvilInstalled, simulateCallWithTrace, traceTransactionWithAnvil } from '../anvil.js'
4
- import { getClientManager, SUPPORTED_CHAINS } from '../client.js'
5
- import type { ChainName } from '../types.js'
6
- import { createTool, formatResponse, summarizeTrace } from '../utils.js'
11
+ import { isDebugNodeAvailable, simulateCallWithTrace, traceTransactionWithAnvil } from '../../anvil.js'
12
+ import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
13
+ import type { ChainName } from '../../types.js'
14
+ import { convertArgumentsToTypes, createTool, formatResponse, rpcReason, summarizeTrace } from '../../utils.js'
15
+
16
+ /** Past these, a trace would crowd everything else out of the agent's context. */
17
+ const MAX_TRACE_CHARS = 50_000
18
+ const MAX_INPUT_CHARS = 10_000
19
+ const MAX_RECEIPT_LOGS = 100
20
+
21
+ const SUMMARIZE_DESCRIPTION =
22
+ 'Return a compact summary instead of the full trace: the path to the revert, or the top-level calls, with selectors only and no calldata.'
23
+
24
+ /** The trace itself if it fits, otherwise its JSON cut short. */
25
+ export function boundTrace(trace: unknown): { trace: unknown; truncated?: true } {
26
+ const json = JSON.stringify(trace) ?? ''
27
+ return json.length > MAX_TRACE_CHARS ? { trace: json.slice(0, MAX_TRACE_CHARS), truncated: true } : { trace }
28
+ }
29
+
30
+ const TRACERS = {
31
+ trace: { tracer: 'callTracer' },
32
+ prestate: { tracer: 'prestateTracer' },
33
+ stateDiff: { tracer: 'prestateTracer', tracerConfig: { diffMode: true } }
34
+ } as const
7
35
 
8
36
  export default {
9
37
  get_storage_at: createTool(
@@ -77,8 +105,8 @@ export default {
77
105
  const parsedTypes = parseAbiParameters([args.abiType])
78
106
  if (parsedTypes.length > 0) {
79
107
  const decoded = decodeAbiParameters(parsedTypes, storageValue) as readonly unknown[]
80
- decodedValue = decoded.length > 0 ? decoded[0] : null
81
- formattedValue = decodedValue ? String(decodedValue) : 'No data'
108
+ decodedValue = decoded[0]
109
+ formattedValue = decodedValue === undefined ? 'No data' : String(decodedValue)
82
110
  } else {
83
111
  decodedValue = null
84
112
  formattedValue = 'Invalid ABI type'
@@ -148,20 +176,20 @@ export default {
148
176
  chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
149
177
  transactionHash: z.string().describe('Transaction hash to trace (0x prefixed)'),
150
178
  traceType: z
151
- .enum(['trace', 'vmTrace', 'stateDiff'])
179
+ .enum(['trace', 'prestate', 'stateDiff'])
152
180
  .optional()
153
- .describe('Trace type: "trace" (call tree, recommended), "vmTrace" (VM execution), "stateDiff" (state changes)')
181
+ .describe(
182
+ 'Trace type: "trace" (call tree, recommended), "prestate" (state of every touched account before the tx), "stateDiff" (state before and after, changed fields only)'
183
+ )
154
184
  .default('trace'),
155
185
  useAnvil: z
156
186
  .boolean()
157
187
  .optional()
158
- .describe('Force using Anvil for tracing (requires Foundry installed). Auto-used as fallback when RPC tracing fails.')
188
+ .describe(
189
+ 'Force tracing through the debug node (ANVIL_RPC_URL). Used automatically as a fallback when the RPC cannot trace.'
190
+ )
159
191
  .default(false),
160
- summarize: z
161
- .boolean()
162
- .optional()
163
- .describe('Return a compact summary instead of full trace. Truncates hex data and flattens nested calls.')
164
- .default(false)
192
+ summarize: z.boolean().optional().describe(SUMMARIZE_DESCRIPTION).default(false)
165
193
  }),
166
194
  async (args) => {
167
195
  const clientManager = getClientManager()
@@ -174,24 +202,18 @@ export default {
174
202
  let traceResult: unknown = null
175
203
  let usedAnvil = false
176
204
 
177
- // Map trace type to tracer name
178
- const tracerMap: Record<string, 'callTracer' | 'prestateTracer' | 'stateDiffTracer'> = {
179
- trace: 'callTracer',
180
- vmTrace: 'prestateTracer',
181
- stateDiff: 'stateDiffTracer'
182
- }
183
- const tracer = tracerMap[args.traceType ?? 'trace'] ?? 'callTracer'
205
+ const tracerOptions = TRACERS[args.traceType ?? 'trace']
184
206
 
185
207
  // Try RPC tracing first (unless forceAnvil is true)
186
208
  if (!args.useAnvil) {
187
209
  try {
188
210
  traceResult = await client.request({
189
211
  method: 'debug_traceTransaction',
190
- params: [args.transactionHash, { tracer }]
212
+ params: [args.transactionHash, tracerOptions]
191
213
  })
192
214
  } catch (rpcError) {
193
215
  // RPC tracing failed, will try Anvil fallback
194
- const errorMessage = (rpcError as Error).message
216
+ const errorMessage = rpcReason(rpcError)
195
217
  if (
196
218
  errorMessage.includes('not supported') ||
197
219
  errorMessage.includes('not available') ||
@@ -213,18 +235,18 @@ export default {
213
235
  (traceResult && typeof traceResult === 'object' && 'rpcError' in traceResult) ||
214
236
  args.useAnvil
215
237
  ) {
216
- const anvilAvailable = await isAnvilInstalled()
238
+ const anvilAvailable = await isDebugNodeAvailable()
217
239
  if (anvilAvailable) {
218
240
  usedAnvil = true // Mark as used before attempting (even if it fails)
219
241
  try {
220
242
  const forkUrl = clientManager.getRpcUrl(args.chain as ChainName)
221
243
  const blockNumber = transaction.blockNumber ?? 0n
222
244
 
223
- traceResult = await traceTransactionWithAnvil(forkUrl, args.transactionHash, blockNumber, tracer, args.chain)
245
+ traceResult = await traceTransactionWithAnvil(forkUrl, args.transactionHash, blockNumber, tracerOptions.tracer)
224
246
  } catch (anvilError) {
225
247
  // Anvil tracing also failed
226
248
  traceResult = {
227
- error: `Anvil tracing failed: ${(anvilError as Error).message}`,
249
+ error: `Anvil tracing failed: ${rpcReason(anvilError)}`,
228
250
  rpcError:
229
251
  traceResult && typeof traceResult === 'object' && 'rpcError' in traceResult
230
252
  ? (traceResult as { rpcError: string }).rpcError
@@ -235,7 +257,7 @@ export default {
235
257
  // Anvil not available and RPC failed
236
258
  traceResult = {
237
259
  error:
238
- 'Tracing not available. Install Foundry (anvil) for local tracing: https://book.getfoundry.sh/getting-started/installation',
260
+ 'Tracing not available. Point ANVIL_RPC_URL at a node exposing debug_traceCall — a local Anvil on 127.0.0.1:8545 is picked up automatically.',
239
261
  rpcError:
240
262
  traceResult && typeof traceResult === 'object' && 'rpcError' in traceResult
241
263
  ? (traceResult as { rpcError: string }).rpcError
@@ -255,11 +277,20 @@ export default {
255
277
  const traceSummary =
256
278
  args.summarize && traceResult && typeof traceResult === 'object' && !isPlainError ? summarizeTrace(traceResult) : null
257
279
 
280
+ const replayNote =
281
+ usedAnvil && !isPlainError
282
+ ? 'Replayed on the debug node against the state at the end of the previous block: transactions before this one in the same block were not applied, so the trace can differ from what happened on chain.' +
283
+ (args.traceType === 'stateDiff' ? ' This replay shows the prestate only, not a diff.' : '')
284
+ : undefined
285
+ const bounded = boundTrace(traceResult)
286
+ const input = transaction.input ?? '0x'
287
+
258
288
  const result =
259
289
  args.summarize && traceSummary
260
290
  ? {
261
291
  chain: args.chain,
262
292
  transactionHash: args.transactionHash,
293
+ ...(replayNote && { note: replayNote }),
263
294
  status: receipt.status,
264
295
  gasUsed: receipt.gasUsed?.toString(),
265
296
  ...traceSummary // { hasError, errorPath, summary }
@@ -270,6 +301,10 @@ export default {
270
301
  transactionHash: args.transactionHash,
271
302
  traceType: args.traceType,
272
303
  usedAnvil,
304
+ ...(replayNote && { note: replayNote }),
305
+ ...((bounded.truncated || input.length > MAX_INPUT_CHARS || receipt.logs.length > MAX_RECEIPT_LOGS) && {
306
+ truncated: true
307
+ }),
273
308
  transaction: {
274
309
  blockNumber: transaction.blockNumber?.toString(),
275
310
  from: transaction.from,
@@ -278,19 +313,20 @@ export default {
278
313
  gas: transaction.gas?.toString() || '0',
279
314
  gasPrice: transaction.gasPrice?.toString() || '0',
280
315
  nonce: transaction.nonce?.toString() || '0',
281
- input: transaction.input
316
+ input: input.length > MAX_INPUT_CHARS ? `${input.slice(0, MAX_INPUT_CHARS)}…` : input
282
317
  },
283
318
  receipt: {
284
319
  status: receipt.status,
285
320
  gasUsed: receipt.gasUsed?.toString() || '0',
286
321
  effectiveGasPrice: receipt.effectiveGasPrice?.toString() || '0',
287
- logs: receipt.logs.map((log) => ({
322
+ logCount: receipt.logs.length,
323
+ logs: receipt.logs.slice(0, MAX_RECEIPT_LOGS).map((log) => ({
288
324
  address: log.address,
289
325
  topics: log.topics,
290
326
  data: log.data
291
327
  }))
292
328
  },
293
- trace: traceResult
329
+ trace: bounded.trace
294
330
  }
295
331
 
296
332
  return formatResponse(result)
@@ -299,7 +335,7 @@ export default {
299
335
 
300
336
  debug_call: createTool(
301
337
  'Debug Contract Call',
302
- 'Simulate a contract call with full trace output. Requires Foundry (anvil) installed. Useful for debugging reverts and understanding call execution.',
338
+ 'Simulate a contract call with full trace output. Needs a node exposing debug_traceCall (a local Anvil, or ANVIL_RPC_URL). Useful for debugging reverts and understanding call execution.',
303
339
  z.object({
304
340
  chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to fork'),
305
341
  to: z.string().describe('Contract address to call'),
@@ -320,17 +356,13 @@ export default {
320
356
  .optional()
321
357
  .default('callTracer')
322
358
  .describe('Trace type: callTracer (call tree) or prestateTracer (state before execution)'),
323
- summarize: z
324
- .boolean()
325
- .optional()
326
- .describe('Return a compact summary instead of full trace. Truncates hex data and flattens nested calls.')
327
- .default(false)
359
+ summarize: z.boolean().optional().describe(SUMMARIZE_DESCRIPTION).default(false)
328
360
  }),
329
361
  async (args) => {
330
362
  // Check if Anvil is installed
331
- const anvilAvailable = await isAnvilInstalled()
363
+ const anvilAvailable = await isDebugNodeAvailable()
332
364
  if (!anvilAvailable) {
333
- throw new Error('Anvil is not installed. Please install Foundry: https://book.getfoundry.sh/getting-started/installation')
365
+ throw new Error('No tracing node reachable. Run a local Anvil, or set ANVIL_RPC_URL to a node exposing debug_traceCall.')
334
366
  }
335
367
 
336
368
  const clientManager = getClientManager()
@@ -340,15 +372,14 @@ export default {
340
372
  let calldata = args.data
341
373
  if (args.functionAbi && !calldata) {
342
374
  try {
343
- const { encodeFunctionData, parseAbiItem } = await import('viem')
344
- const abiItem = parseAbiItem(args.functionAbi)
375
+ const abiItem = parseAbiItem(args.functionAbi) as AbiFunction
345
376
  if (abiItem.type !== 'function') {
346
377
  throw new Error('ABI must be a function signature')
347
378
  }
348
379
  calldata = encodeFunctionData({
349
380
  abi: [abiItem],
350
381
  functionName: abiItem.name,
351
- args: (args.args ?? []) as readonly unknown[]
382
+ args: convertArgumentsToTypes(args.args ?? [], abiItem.inputs)
352
383
  })
353
384
  } catch (encodeError) {
354
385
  throw new Error(`Failed to encode function call: ${(encodeError as Error).message}`)
@@ -404,7 +435,7 @@ export default {
404
435
  result: traceResult.result,
405
436
  gasUsed: traceResult.gasUsed.toString(),
406
437
  revertReason: traceResult.revertReason,
407
- trace: traceResult.trace
438
+ ...boundTrace(traceResult.trace)
408
439
  }
409
440
 
410
441
  return formatResponse(result)
@@ -1,8 +1,9 @@
1
1
  import { type Address, formatEther, formatUnits, isAddress } from 'viem'
2
2
  import { z } from 'zod'
3
- import { getClientManager, SUPPORTED_CHAINS } from '../client.js'
4
- import type { ChainName } from '../types.js'
5
- import { createTool, formatResponse } from '../utils.js'
3
+ import { tokenMeta } from '../../chain-meta.js'
4
+ import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
5
+ import type { ChainName } from '../../types.js'
6
+ import { createTool, formatResponse, MAX_BATCH, rpcReason } from '../../utils.js'
6
7
 
7
8
  const BalanceQuerySchema = z.object({
8
9
  chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
@@ -19,6 +20,7 @@ export default {
19
20
  z.object({
20
21
  queries: z
21
22
  .array(BalanceQuerySchema)
23
+ .max(MAX_BATCH)
22
24
  .describe('Array of balance queries. Omit tokenAddress for native balance, include for ERC20.')
23
25
  }),
24
26
  async (args) => {
@@ -42,7 +44,7 @@ export default {
42
44
  success: boolean
43
45
  balance?: unknown
44
46
  balanceFormatted?: string
45
- decimals?: number
47
+ decimals?: number | null
46
48
  error?: string
47
49
  }> = []
48
50
 
@@ -86,42 +88,24 @@ export default {
86
88
  throw new Error(`Invalid token address format: ${query.tokenAddress}`)
87
89
  }
88
90
 
89
- const balance = await client.readContract({
90
- address: query.tokenAddress as Address,
91
- abi: [
92
- {
93
- name: 'balanceOf',
94
- type: 'function',
95
- stateMutability: 'view',
96
- inputs: [{ name: 'account', type: 'address' }],
97
- outputs: [{ name: '', type: 'uint256' }]
98
- }
99
- ],
100
- functionName: 'balanceOf',
101
- args: [query.address as Address],
102
- blockNumber: blockTag === 'latest' ? undefined : blockTag
103
- })
104
-
105
- // Try to get decimals
106
- let decimals = 18
107
- try {
108
- decimals = (await client.readContract({
91
+ const [balance, { decimals }] = await Promise.all([
92
+ client.readContract({
109
93
  address: query.tokenAddress as Address,
110
94
  abi: [
111
95
  {
112
- name: 'decimals',
96
+ name: 'balanceOf',
113
97
  type: 'function',
114
98
  stateMutability: 'view',
115
- inputs: [],
116
- outputs: [{ name: '', type: 'uint8' }]
99
+ inputs: [{ name: 'account', type: 'address' }],
100
+ outputs: [{ name: '', type: 'uint256' }]
117
101
  }
118
102
  ],
119
- functionName: 'decimals',
103
+ functionName: 'balanceOf',
104
+ args: [query.address as Address],
120
105
  blockNumber: blockTag === 'latest' ? undefined : blockTag
121
- })) as number
122
- } catch {
123
- // Use default decimals if call fails
124
- }
106
+ }),
107
+ tokenMeta(chain as ChainName, query.tokenAddress).catch(() => ({ decimals: undefined }))
108
+ ])
125
109
 
126
110
  return {
127
111
  index: originalIndex,
@@ -132,8 +116,9 @@ export default {
132
116
  blockNumber: query.blockNumber || 'latest',
133
117
  success: true,
134
118
  balance,
135
- balanceFormatted: formatUnits(balance as bigint, decimals),
136
- decimals
119
+ // Unknown decimals leave only the raw balance: guessing 18 could be off by 10^12.
120
+ balanceFormatted: decimals === undefined ? undefined : formatUnits(balance, decimals),
121
+ decimals: decimals ?? null
137
122
  }
138
123
  })
139
124
  )
@@ -153,7 +138,7 @@ export default {
153
138
  chain: query.chain,
154
139
  blockNumber: query.blockNumber || 'latest',
155
140
  success: false,
156
- error: result.reason instanceof Error ? result.reason.message : String(result.reason)
141
+ error: rpcReason(result.reason)
157
142
  }
158
143
  }
159
144
  })