@1sat/cli 0.0.12 → 0.0.13
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/package.json +1 -1
- package/src/commands/mcp-proxy.ts +82 -28
package/package.json
CHANGED
|
@@ -1,39 +1,103 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 1sat mcp-proxy — stdio-to-HTTP bridge for the wallet-desktop MCP server.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* at :3322, writing responses to stdout.
|
|
7
|
-
*
|
|
8
|
-
* Used by Claude Code's .mcp.json to connect agents to the running
|
|
9
|
-
* 1Sat desktop wallet.
|
|
4
|
+
* Performs BRC-31 handshake, then proxies JSON-RPC messages from stdin
|
|
5
|
+
* to the MCP server at :3322 with signed auth headers on each request.
|
|
10
6
|
*/
|
|
11
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
-
import {
|
|
8
|
+
import { Hash, KeyDeriver, PrivateKey, Signature, Utils } from '@bsv/sdk'
|
|
9
|
+
import type { WalletProtocol } from '@bsv/sdk'
|
|
10
|
+
|
|
11
|
+
const { toBase64, toArray } = Utils
|
|
13
12
|
|
|
14
13
|
const MCP_URL = process.env.ONESAT_MCP_URL ?? 'http://127.0.0.1:3322'
|
|
15
14
|
const KEY_DIR = `${process.env.HOME}/.1sat-wallet`
|
|
16
15
|
const CLIENT_KEY_PATH = `${KEY_DIR}/mcp-agent.key`
|
|
16
|
+
const AUTH_PROTOCOL_ID: WalletProtocol = [2, 'authrite message signature']
|
|
17
17
|
|
|
18
18
|
function log(msg: string): void {
|
|
19
19
|
process.stderr.write(`[1sat mcp-proxy] ${msg}\n`)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function generateNonce(): string {
|
|
23
|
+
const bytes = new Uint8Array(32)
|
|
24
|
+
crypto.getRandomValues(bytes)
|
|
25
|
+
return toBase64(Array.from(bytes))
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
function getClientKey(): PrivateKey {
|
|
23
29
|
mkdirSync(KEY_DIR, { recursive: true })
|
|
24
|
-
|
|
25
30
|
if (existsSync(CLIENT_KEY_PATH)) {
|
|
26
31
|
return PrivateKey.fromWif(readFileSync(CLIENT_KEY_PATH, 'utf-8').trim())
|
|
27
32
|
}
|
|
28
|
-
|
|
29
33
|
const key = PrivateKey.fromRandom()
|
|
30
34
|
writeFileSync(CLIENT_KEY_PATH, key.toWif(), { mode: 0o600 })
|
|
31
35
|
log('Generated new agent identity key')
|
|
32
36
|
return key
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
interface Session {
|
|
40
|
+
serverIdentityKey: string
|
|
41
|
+
serverNonce: string
|
|
42
|
+
clientKey: PrivateKey
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handshake(key: PrivateKey): Promise<Session> {
|
|
46
|
+
const pubkey = key.toPublicKey().toString()
|
|
47
|
+
const clientNonce = generateNonce()
|
|
48
|
+
|
|
49
|
+
const res = await fetch(`${MCP_URL}/.well-known/auth`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
authrite: '0.1',
|
|
54
|
+
messageType: 'initialRequest',
|
|
55
|
+
identityKey: pubkey,
|
|
56
|
+
nonce: clientNonce,
|
|
57
|
+
}),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (!res.ok) throw new Error(`Handshake failed: HTTP ${res.status}`)
|
|
61
|
+
|
|
62
|
+
const data = await res.json()
|
|
63
|
+
if (data.messageType !== 'initialResponse') throw new Error(`Unexpected: ${data.messageType}`)
|
|
64
|
+
|
|
65
|
+
const serverNonce = data.nonce as string
|
|
66
|
+
const deriver = new KeyDeriver(key)
|
|
67
|
+
const serverPub = deriver.derivePublicKey(
|
|
68
|
+
AUTH_PROTOCOL_ID, `${serverNonce} ${clientNonce}`, data.identityKey as string, false,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const clientNonceBytes = toArray(clientNonce, 'base64')
|
|
72
|
+
const serverNonceBytes = toArray(serverNonce, 'base64')
|
|
73
|
+
const msgHash = Hash.sha256(Array.from(new Uint8Array([...clientNonceBytes, ...serverNonceBytes])))
|
|
74
|
+
|
|
75
|
+
const sig = Signature.fromDER(data.signature as string, 'hex')
|
|
76
|
+
if (!serverPub.verify(Array.from(msgHash), sig)) throw new Error('Server signature verification failed')
|
|
77
|
+
|
|
78
|
+
log(`Authenticated with ${(data.identityKey as string).slice(0, 12)}...`)
|
|
79
|
+
return { serverIdentityKey: data.identityKey as string, serverNonce, clientKey: key }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function signHeaders(session: Session, pathname: string): Record<string, string> {
|
|
83
|
+
const { serverIdentityKey, serverNonce, clientKey: key } = session
|
|
84
|
+
const nonce = generateNonce()
|
|
85
|
+
const deriver = new KeyDeriver(key)
|
|
86
|
+
const derivedKey = deriver.derivePrivateKey(AUTH_PROTOCOL_ID, `${nonce} ${serverNonce}`, serverIdentityKey)
|
|
87
|
+
const payload = new TextEncoder().encode(pathname)
|
|
88
|
+
const msgHash = Hash.sha256(Array.from(payload))
|
|
89
|
+
const sig = derivedKey.sign(Array.from(msgHash))
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
'x-bsv-auth-version': '0.1',
|
|
93
|
+
'x-bsv-auth-identity-key': key.toPublicKey().toString(),
|
|
94
|
+
'x-bsv-auth-nonce': nonce,
|
|
95
|
+
'x-bsv-auth-your-nonce': serverNonce,
|
|
96
|
+
'x-bsv-auth-signature': sig.toDER('hex') as string,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
35
100
|
export async function handleMcpProxyCommand(): Promise<void> {
|
|
36
|
-
// Health check before doing anything else
|
|
37
101
|
try {
|
|
38
102
|
await fetch(MCP_URL, { signal: AbortSignal.timeout(2000) })
|
|
39
103
|
} catch {
|
|
@@ -42,13 +106,9 @@ export async function handleMcpProxyCommand(): Promise<void> {
|
|
|
42
106
|
}
|
|
43
107
|
|
|
44
108
|
const key = getClientKey()
|
|
45
|
-
const
|
|
46
|
-
const authFetch = new AuthFetch(wallet)
|
|
47
|
-
|
|
48
|
-
log(`Connecting to ${MCP_URL}/mcp`)
|
|
109
|
+
const session = await handshake(key)
|
|
49
110
|
|
|
50
111
|
let mcpSessionId: string | null = null
|
|
51
|
-
|
|
52
112
|
const decoder = new TextDecoder()
|
|
53
113
|
const reader = Bun.stdin.stream().getReader()
|
|
54
114
|
let buffer = ''
|
|
@@ -63,12 +123,12 @@ export async function handleMcpProxyCommand(): Promise<void> {
|
|
|
63
123
|
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
64
124
|
const line = buffer.slice(0, newlineIdx).trim()
|
|
65
125
|
buffer = buffer.slice(newlineIdx + 1)
|
|
66
|
-
|
|
67
126
|
if (!line) continue
|
|
68
127
|
|
|
69
128
|
const headers: Record<string, string> = {
|
|
70
129
|
'Content-Type': 'application/json',
|
|
71
130
|
Accept: 'application/json, text/event-stream',
|
|
131
|
+
...signHeaders(session, '/mcp'),
|
|
72
132
|
}
|
|
73
133
|
|
|
74
134
|
if (mcpSessionId) {
|
|
@@ -76,11 +136,7 @@ export async function handleMcpProxyCommand(): Promise<void> {
|
|
|
76
136
|
}
|
|
77
137
|
|
|
78
138
|
try {
|
|
79
|
-
const res = await
|
|
80
|
-
method: 'POST',
|
|
81
|
-
body: line,
|
|
82
|
-
headers,
|
|
83
|
-
})
|
|
139
|
+
const res = await fetch(`${MCP_URL}/mcp`, { method: 'POST', headers, body: line })
|
|
84
140
|
|
|
85
141
|
const sessionHeader = res.headers.get('mcp-session-id')
|
|
86
142
|
if (sessionHeader) mcpSessionId = sessionHeader
|
|
@@ -102,13 +158,11 @@ export async function handleMcpProxyCommand(): Promise<void> {
|
|
|
102
158
|
} catch (err) {
|
|
103
159
|
const msg = err instanceof Error ? err.message : String(err)
|
|
104
160
|
log(`Request failed: ${msg}`)
|
|
105
|
-
process.stdout.write(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
})}\n`,
|
|
111
|
-
)
|
|
161
|
+
process.stdout.write(`${JSON.stringify({
|
|
162
|
+
jsonrpc: '2.0',
|
|
163
|
+
error: { code: -32000, message: `MCP proxy error: ${msg}` },
|
|
164
|
+
id: null,
|
|
165
|
+
})}\n`)
|
|
112
166
|
}
|
|
113
167
|
}
|
|
114
168
|
}
|