web3-tools-mcp 1.5.0 → 1.6.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/README.md +90 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -2
- package/dist/client.js.map +1 -1
- package/dist/http-server.d.ts +12 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +74 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.js +37 -13
- package/dist/index.js.map +1 -1
- package/dist/kv-storage.d.ts +23 -0
- package/dist/kv-storage.d.ts.map +1 -0
- package/dist/kv-storage.js +69 -0
- package/dist/kv-storage.js.map +1 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +12 -2
- package/dist/preview.js.map +1 -1
- package/dist/tools/transactions.d.ts +14 -0
- package/dist/tools/transactions.d.ts.map +1 -1
- package/dist/tools/transactions.js +128 -18
- package/dist/tools/transactions.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +2 -7
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -0
- package/dist/utils.js.map +1 -1
- package/dist/walletconnect.d.ts +31 -0
- package/dist/walletconnect.d.ts.map +1 -0
- package/dist/walletconnect.js +128 -0
- package/dist/walletconnect.js.map +1 -0
- package/package.json +7 -3
- package/src/client.ts +4 -2
- package/src/http-server.ts +94 -0
- package/src/index.ts +38 -13
- package/src/kv-storage.ts +83 -0
- package/src/preview.ts +12 -2
- package/src/tools/transactions.ts +146 -20
- package/src/types.ts +1 -0
- package/src/utils.ts +4 -17
- package/src/walletconnect.ts +157 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { getClientManager } from './client.js'
|
|
5
|
+
import { getKeyValueStorage } from './kv-storage.js'
|
|
6
|
+
import type { ChainName } from './types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Signing from a phone, with no page to load and nothing to host: the MCP server acts as
|
|
10
|
+
* the dApp, the wallet app is the signer, and WalletConnect's relay carries the (end-to-end
|
|
11
|
+
* encrypted) messages between them. Pair once by scanning a QR; the session is stored on
|
|
12
|
+
* disk, so it survives restarts.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type SignClientType = Awaited<ReturnType<typeof importSignClient>>['prototype']
|
|
16
|
+
|
|
17
|
+
async function importSignClient() {
|
|
18
|
+
// Imported lazily: several MB of SDK that is pointless to load without a project ID.
|
|
19
|
+
const { SignClient } = await import('@walletconnect/sign-client')
|
|
20
|
+
return SignClient
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Session {
|
|
24
|
+
topic: string
|
|
25
|
+
accounts: string[]
|
|
26
|
+
chains: number[]
|
|
27
|
+
peer?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const METHODS = ['eth_sendTransaction', 'personal_sign', 'eth_signTypedData_v4']
|
|
31
|
+
const EVENTS = ['chainChanged', 'accountsChanged']
|
|
32
|
+
const APPROVAL_TIMEOUT = 300_000
|
|
33
|
+
|
|
34
|
+
function storePath(): string {
|
|
35
|
+
const dir = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'web3-tools-mcp')
|
|
36
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
37
|
+
return join(dir, 'walletconnect.db')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class PhoneSigner {
|
|
41
|
+
private client: SignClientType | undefined
|
|
42
|
+
private starting: Promise<void> | undefined
|
|
43
|
+
private pairing: { uri: string; expiresAt: number } | undefined
|
|
44
|
+
|
|
45
|
+
constructor(private projectId: string) {}
|
|
46
|
+
|
|
47
|
+
private async start(): Promise<void> {
|
|
48
|
+
if (this.client) return
|
|
49
|
+
if (this.starting) return this.starting
|
|
50
|
+
|
|
51
|
+
this.starting = (async () => {
|
|
52
|
+
const SignClient = await importSignClient()
|
|
53
|
+
// Redis when hosted (a free-tier filesystem is ephemeral, and losing the store means
|
|
54
|
+
// re-pairing by QR after every sleep); a file next to the other config locally.
|
|
55
|
+
const storage = getKeyValueStorage()
|
|
56
|
+
this.client = (await SignClient.init({
|
|
57
|
+
projectId: this.projectId,
|
|
58
|
+
...(storage ? { storage } : { storageOptions: { database: storePath() } }),
|
|
59
|
+
metadata: {
|
|
60
|
+
name: 'web3-tools-mcp',
|
|
61
|
+
description: 'Blockchain tools for AI agents',
|
|
62
|
+
url: 'https://github.com/hajnalben/web3-tools-mcp',
|
|
63
|
+
icons: []
|
|
64
|
+
}
|
|
65
|
+
})) as SignClientType
|
|
66
|
+
})().finally(() => {
|
|
67
|
+
this.starting = undefined
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return this.starting
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The most recent still-valid session, if the phone is already paired. */
|
|
74
|
+
async session(): Promise<Session | undefined> {
|
|
75
|
+
await this.start()
|
|
76
|
+
const sessions = this.client?.session.getAll() ?? []
|
|
77
|
+
const live = sessions.filter((s) => s.expiry * 1000 > Date.now())
|
|
78
|
+
const session = live[live.length - 1]
|
|
79
|
+
if (!session) return undefined
|
|
80
|
+
|
|
81
|
+
const accounts = session.namespaces.eip155?.accounts ?? []
|
|
82
|
+
return {
|
|
83
|
+
topic: session.topic,
|
|
84
|
+
// "eip155:8453:0xabc…" → address / chain id
|
|
85
|
+
accounts: [...new Set(accounts.map((a) => a.split(':')[2] as string))],
|
|
86
|
+
chains: [...new Set(accounts.map((a) => Number(a.split(':')[1])))],
|
|
87
|
+
peer: session.peer?.metadata?.name
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async isPaired(): Promise<boolean> {
|
|
92
|
+
return Boolean(await this.session())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Start a pairing and return its URI immediately, resolving the wallet's approval in the
|
|
97
|
+
* background — a tool result only reaches the user once it returns, so blocking here
|
|
98
|
+
* would hide the QR they are supposed to scan.
|
|
99
|
+
*/
|
|
100
|
+
async pair(chains: ChainName[]): Promise<string> {
|
|
101
|
+
await this.start()
|
|
102
|
+
if (!this.client) throw new Error('WalletConnect failed to start')
|
|
103
|
+
|
|
104
|
+
const clientManager = getClientManager()
|
|
105
|
+
const eip155 = chains.map((chain) => `eip155:${clientManager.getChainId(chain)}`)
|
|
106
|
+
|
|
107
|
+
const { uri, approval } = await this.client.connect({
|
|
108
|
+
// Optional rather than required: wallets reject a session outright when they cannot
|
|
109
|
+
// serve every required chain, and few support all thirteen.
|
|
110
|
+
optionalNamespaces: { eip155: { chains: eip155, methods: METHODS, events: EVENTS } }
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
if (!uri) throw new Error('WalletConnect did not return a pairing URI')
|
|
114
|
+
this.pairing = { uri, expiresAt: Date.now() + APPROVAL_TIMEOUT }
|
|
115
|
+
|
|
116
|
+
approval()
|
|
117
|
+
.then((session) => console.error(`[WalletConnect] Paired with ${session.peer?.metadata?.name ?? 'a wallet'}`))
|
|
118
|
+
.catch((error) => console.error('[WalletConnect] Pairing was not completed:', error.message))
|
|
119
|
+
|
|
120
|
+
return uri
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Send a request to the paired wallet. Throws if the chain is outside the session. */
|
|
124
|
+
async request(chain: ChainName, method: string, params: unknown[]): Promise<unknown> {
|
|
125
|
+
const session = await this.session()
|
|
126
|
+
if (!session || !this.client) throw new Error('No phone wallet paired')
|
|
127
|
+
|
|
128
|
+
const chainId = getClientManager().getChainId(chain)
|
|
129
|
+
if (!session.chains.includes(chainId)) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`The paired wallet did not approve ${chain} (chain ${chainId}). It approved: ${session.chains.join(', ')}. Re-pair to add it.`
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return this.client.request({
|
|
136
|
+
topic: session.topic,
|
|
137
|
+
chainId: `eip155:${chainId}`,
|
|
138
|
+
request: { method, params }
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async unpair(): Promise<void> {
|
|
143
|
+
const session = await this.session()
|
|
144
|
+
if (!session || !this.client) return
|
|
145
|
+
await this.client.disconnect({ topic: session.topic, reason: { code: 6000, message: 'User disconnected' } })
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let signer: PhoneSigner | null = null
|
|
150
|
+
|
|
151
|
+
/** Configured only when a WalletConnect project id is present. */
|
|
152
|
+
export function getPhoneSigner(): PhoneSigner | null {
|
|
153
|
+
const projectId = getClientManager().getConfig().walletConnectProjectId
|
|
154
|
+
if (!projectId) return null
|
|
155
|
+
if (!signer) signer = new PhoneSigner(projectId)
|
|
156
|
+
return signer
|
|
157
|
+
}
|