bsv-mcp 0.2.9 → 0.2.10

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/index.ts CHANGED
@@ -35,6 +35,7 @@ import { getBsvPriceWithCache } from "./tools/bsv/getPrice.ts";
35
35
  import { registerAllTools, type ToolsConfig } from "./tools/index.ts";
36
36
  import { IntegratedWallet } from "./tools/wallet/integratedWallet.ts";
37
37
  import { Wallet } from "./tools/wallet/wallet.ts";
38
+ import { initWallet, destroyWallet } from "./utils/walletInit.ts";
38
39
  import { BunSSEServerTransport } from "./transports/sse.ts";
39
40
  import {
40
41
  type BSVJWTPayload,
@@ -858,6 +859,8 @@ Authentication:
858
859
 
859
860
  let wallet: Wallet | undefined;
860
861
  let integratedWallet: IntegratedWallet | undefined;
862
+ let remoteCtx: import("@1sat/actions").OneSatContext | undefined;
863
+ let remoteServices: import("@1sat/wallet-remote").OneSatServices | undefined;
861
864
 
862
865
  if (CONFIG.loadTools) {
863
866
  // Check if we should use Droplet API mode
@@ -934,6 +937,21 @@ Authentication:
934
937
  effectiveConfig.loadMneeTools = false;
935
938
  effectiveConfig.loadBapTools = false;
936
939
  }
940
+
941
+ // Initialize remote BRC-100 wallet alongside the local wallet
942
+ try {
943
+ const chain = (process.env.BSV_CHAIN as "main" | "test") ?? "main";
944
+ const remoteResult = await initWallet(payPk.toWif(), chain);
945
+ remoteCtx = remoteResult.ctx;
946
+ remoteServices = remoteResult.services;
947
+ logFunc(
948
+ `\x1b[32mINFO: Remote BRC-100 wallet initialized. Deposit address: ${remoteResult.depositAddress}\x1b[0m`,
949
+ );
950
+ } catch (e) {
951
+ logFunc(
952
+ `\x1b[33mWARN: Remote BRC-100 wallet initialization failed: ${e instanceof Error ? e.message : String(e)}. Falling back to local wallet only.\x1b[0m`,
953
+ );
954
+ }
937
955
  }
938
956
 
939
957
  // Disable MNEE tools if wallet is not available
@@ -961,6 +979,8 @@ Authentication:
961
979
  wallet,
962
980
  integratedWallet,
963
981
  disableBroadcasting: effectiveConfig.disableBroadcasting,
982
+ ctx: remoteCtx,
983
+ services: remoteServices,
964
984
  };
965
985
 
966
986
  registerAllTools(server, toolsConfig);
@@ -979,6 +999,13 @@ Authentication:
979
999
  registerResources(server);
980
1000
  }
981
1001
 
1002
+ // Clean up remote wallet on shutdown
1003
+ for (const sig of ["SIGINT", "SIGTERM"] as const) {
1004
+ process.once(sig, () => {
1005
+ destroyWallet().catch(() => {});
1006
+ });
1007
+ }
1008
+
982
1009
  // Start the server based on transport mode
983
1010
  if (CONFIG.transportMode === "stdio") {
984
1011
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "bsv-mcp",
3
3
  "module": "dist/index.js",
4
4
  "type": "module",
5
- "version": "0.2.9",
5
+ "version": "0.2.10",
6
6
  "license": "MIT",
7
7
  "author": "satchmo",
8
8
  "description": "A collection of Bitcoin SV (BSV) tools for the Model Context Protocol (MCP) framework",
@@ -79,6 +79,8 @@
79
79
  "typescript": "^5.9.3"
80
80
  },
81
81
  "dependencies": {
82
+ "@1sat/actions": "0.0.25",
83
+ "@1sat/wallet-remote": "0.0.5",
82
84
  "@bsv/sdk": "^2.0.6",
83
85
  "@modelcontextprotocol/ext-apps": "^1.2.0",
84
86
  "@modelcontextprotocol/sdk": "^1.27.1",
package/tools/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { OneSatContext } from "@1sat/actions";
2
+ import type { OneSatServices } from "@1sat/wallet-remote";
1
3
  import type { PrivateKey } from "@bsv/sdk";
2
4
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
5
  import { registerA2bDiscoverTool } from "./a2b/discover";
@@ -41,6 +43,8 @@ export interface ToolsConfig {
41
43
  wallet?: Wallet;
42
44
  integratedWallet?: IntegratedWallet;
43
45
  disableBroadcasting?: boolean; // For wallet tools
46
+ ctx?: OneSatContext;
47
+ services?: OneSatServices;
44
48
  }
45
49
 
46
50
  /**
@@ -126,9 +130,10 @@ export function registerAllTools(
126
130
  } else if (config.wallet) {
127
131
  // Register normal wallet tools
128
132
  const walletToolOptions = {
129
- disableBroadcasting: config.disableBroadcasting === true, // Default to false if undefined
130
- enableA2bTools: enableA2bTools, // Use the already determined enableA2bTools value
133
+ disableBroadcasting: config.disableBroadcasting === true,
134
+ enableA2bTools: enableA2bTools,
131
135
  identityPk: config.identityPk,
136
+ ctx: config.ctx,
132
137
  };
133
138
  registerWalletTools(server, config.wallet, walletToolOptions);
134
139
  }
@@ -0,0 +1,572 @@
1
+ import type { OneSatContext } from '@1sat/actions'
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
3
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
4
+ import { z } from 'zod'
5
+
6
+ const noCtx: CallToolResult = {
7
+ content: [{ type: 'text', text: 'Wallet not initialized.' }],
8
+ isError: true,
9
+ }
10
+
11
+ function result(data: unknown): CallToolResult {
12
+ return { content: [{ type: 'text', text: JSON.stringify(data) }] }
13
+ }
14
+
15
+ function error(err: unknown): CallToolResult {
16
+ return {
17
+ content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
18
+ isError: true,
19
+ }
20
+ }
21
+
22
+ function parseJSON(s: string | undefined): any {
23
+ if (!s) return undefined
24
+ try { return JSON.parse(s) } catch { return s }
25
+ }
26
+
27
+ export function registerBrc100Tools(
28
+ server: McpServer,
29
+ ctx: OneSatContext | undefined,
30
+ ) {
31
+ // ── Transaction lifecycle ──────────────────────────────────────────
32
+
33
+ server.tool(
34
+ 'wallet_createAction',
35
+ 'Creates a new Bitcoin transaction. Handles funding, signing, and broadcasting based on options.',
36
+ {
37
+ description: z.string().describe('5-50 char description of the action'),
38
+ inputsJSON: z.string().optional().describe('JSON array of transaction inputs'),
39
+ outputsJSON: z.string().optional().describe('JSON array of transaction outputs [{lockingScript, satoshis, outputDescription, basket, tags, customInstructions}]'),
40
+ labelsJSON: z.string().optional().describe('JSON array of label strings'),
41
+ lockTime: z.number().optional(),
42
+ version: z.number().optional(),
43
+ optionsJSON: z.string().optional().describe('JSON CreateActionOptions: {noSend, sendWith, acceptDelayedBroadcast, signAndProcess, randomizeOutputs, noSendChange, knownTxids, trustSelf}'),
44
+ },
45
+ async ({ description, inputsJSON, outputsJSON, labelsJSON, lockTime, version, optionsJSON }) => {
46
+ if (!ctx) return noCtx
47
+ try {
48
+ return result(await ctx.wallet.createAction({
49
+ description,
50
+ inputs: parseJSON(inputsJSON),
51
+ outputs: parseJSON(outputsJSON),
52
+ labels: parseJSON(labelsJSON),
53
+ lockTime,
54
+ version,
55
+ options: parseJSON(optionsJSON),
56
+ } as any))
57
+ } catch (e) { return error(e) }
58
+ },
59
+ )
60
+
61
+ server.tool(
62
+ 'wallet_signAction',
63
+ 'Signs a transaction previously created with createAction (when signAndProcess was false).',
64
+ {
65
+ spendsJSON: z.string().describe('JSON map of input index to {unlockingScript, sequenceNumber}'),
66
+ reference: z.string().describe('Base64 reference from createAction result'),
67
+ optionsJSON: z.string().optional().describe('JSON SignActionOptions: {acceptDelayedBroadcast, sendWith}'),
68
+ },
69
+ async ({ spendsJSON, reference, optionsJSON }) => {
70
+ if (!ctx) return noCtx
71
+ try {
72
+ return result(await ctx.wallet.signAction({
73
+ spends: parseJSON(spendsJSON),
74
+ reference,
75
+ options: parseJSON(optionsJSON),
76
+ } as any))
77
+ } catch (e) { return error(e) }
78
+ },
79
+ )
80
+
81
+ server.tool(
82
+ 'wallet_abortAction',
83
+ 'Aborts a pending (nosend or unsigned) transaction, releasing consumed inputs back to spendable state.',
84
+ {
85
+ reference: z.string().describe('Base64 reference of the transaction to abort'),
86
+ },
87
+ async ({ reference }) => {
88
+ if (!ctx) return noCtx
89
+ try {
90
+ return result(await ctx.wallet.abortAction({ reference }))
91
+ } catch (e) { return error(e) }
92
+ },
93
+ )
94
+
95
+ server.tool(
96
+ 'wallet_internalizeAction',
97
+ 'Internalizes an external transaction, adding its outputs to wallet baskets.',
98
+ {
99
+ txJSON: z.string().describe('JSON array of AtomicBEEF bytes'),
100
+ outputsJSON: z.string().describe('JSON array of outputs to internalize'),
101
+ description: z.string().describe('5-50 char description'),
102
+ labelsJSON: z.string().optional().describe('JSON array of label strings'),
103
+ },
104
+ async ({ txJSON, outputsJSON, description, labelsJSON }) => {
105
+ if (!ctx) return noCtx
106
+ try {
107
+ return result(await ctx.wallet.internalizeAction({
108
+ tx: parseJSON(txJSON),
109
+ outputs: parseJSON(outputsJSON),
110
+ description,
111
+ labels: parseJSON(labelsJSON),
112
+ } as any))
113
+ } catch (e) { return error(e) }
114
+ },
115
+ )
116
+
117
+ // ── Queries ────────────────────────────────────────────────────────
118
+
119
+ server.tool(
120
+ 'wallet_listActions',
121
+ 'Lists wallet transactions filtered by labels, with optional input/output details.',
122
+ {
123
+ labelsJSON: z.string().default('[]').describe('JSON array of label strings'),
124
+ labelQueryMode: z.enum(['any', 'all']).default('any'),
125
+ includeLabels: z.boolean().default(true),
126
+ includeInputs: z.boolean().default(false),
127
+ includeInputSourceLockingScripts: z.boolean().default(false),
128
+ includeInputUnlockingScripts: z.boolean().default(false),
129
+ includeOutputs: z.boolean().default(false),
130
+ includeOutputLockingScripts: z.boolean().default(false),
131
+ limit: z.number().default(25),
132
+ offset: z.number().default(0),
133
+ },
134
+ async ({ labelsJSON, ...rest }) => {
135
+ if (!ctx) return noCtx
136
+ try {
137
+ return result(await ctx.wallet.listActions({
138
+ labels: parseJSON(labelsJSON),
139
+ ...rest,
140
+ } as any))
141
+ } catch (e) { return error(e) }
142
+ },
143
+ )
144
+
145
+ server.tool(
146
+ 'wallet_listOutputs',
147
+ 'Lists spendable outputs in a basket, optionally filtered by tags.',
148
+ {
149
+ basket: z.string().describe('Basket name (e.g. "default")'),
150
+ tagsJSON: z.string().optional().describe('JSON array of tag strings'),
151
+ tagQueryMode: z.enum(['all', 'any']).optional(),
152
+ include: z.enum(['locking scripts', 'entire transactions']).optional(),
153
+ includeCustomInstructions: z.boolean().default(false),
154
+ includeTags: z.boolean().default(false),
155
+ includeLabels: z.boolean().default(false),
156
+ limit: z.number().default(25),
157
+ offset: z.number().default(0),
158
+ },
159
+ async ({ tagsJSON, ...rest }) => {
160
+ if (!ctx) return noCtx
161
+ try {
162
+ return result(await ctx.wallet.listOutputs({
163
+ tags: parseJSON(tagsJSON),
164
+ ...rest,
165
+ } as any))
166
+ } catch (e) { return error(e) }
167
+ },
168
+ )
169
+
170
+ server.tool(
171
+ 'wallet_relinquishOutput',
172
+ 'Removes an output from a basket without spending it.',
173
+ {
174
+ basket: z.string().describe('Basket name'),
175
+ output: z.string().describe('Outpoint string (txid.vout)'),
176
+ },
177
+ async (args) => {
178
+ if (!ctx) return noCtx
179
+ try {
180
+ return result(await ctx.wallet.relinquishOutput(args as any))
181
+ } catch (e) { return error(e) }
182
+ },
183
+ )
184
+
185
+ // ── Keys & Crypto ──────────────────────────────────────────────────
186
+
187
+ server.tool(
188
+ 'wallet_getPublicKey',
189
+ 'Retrieves a public key by protocol/key derivation. Use identityKey:true for the root identity key. protocolID is a JSON array like [2,"1sat"].',
190
+ {
191
+ identityKey: z.boolean().optional().describe('If true, return the identity key (ignores other args)'),
192
+ protocolIDJSON: z.string().optional().describe('JSON array [securityLevel, protocolString] e.g. [2,"1sat"]'),
193
+ keyID: z.string().optional(),
194
+ counterparty: z.string().optional(),
195
+ forSelf: z.boolean().optional(),
196
+ privileged: z.boolean().optional(),
197
+ privilegedReason: z.string().optional(),
198
+ },
199
+ async ({ protocolIDJSON, ...rest }) => {
200
+ if (!ctx) return noCtx
201
+ try {
202
+ return result(await ctx.wallet.getPublicKey({
203
+ protocolID: parseJSON(protocolIDJSON),
204
+ ...rest,
205
+ } as any))
206
+ } catch (e) { return error(e) }
207
+ },
208
+ )
209
+
210
+ server.tool(
211
+ 'wallet_encrypt',
212
+ 'Encrypts data using wallet keys. protocolID is a JSON array like [2,"protocolName"].',
213
+ {
214
+ plaintext: z.array(z.number()).describe('Data bytes to encrypt'),
215
+ protocolIDJSON: z.string().describe('JSON array [securityLevel, protocolString]'),
216
+ keyID: z.string(),
217
+ counterparty: z.string().optional(),
218
+ privileged: z.boolean().optional(),
219
+ },
220
+ async ({ protocolIDJSON, ...rest }) => {
221
+ if (!ctx) return noCtx
222
+ try {
223
+ return result(await ctx.wallet.encrypt({
224
+ protocolID: parseJSON(protocolIDJSON),
225
+ ...rest,
226
+ } as any))
227
+ } catch (e) { return error(e) }
228
+ },
229
+ )
230
+
231
+ server.tool(
232
+ 'wallet_decrypt',
233
+ 'Decrypts data using wallet keys.',
234
+ {
235
+ ciphertext: z.array(z.number()).describe('Encrypted data bytes'),
236
+ protocolIDJSON: z.string().describe('JSON array [securityLevel, protocolString]'),
237
+ keyID: z.string(),
238
+ counterparty: z.string().optional(),
239
+ privileged: z.boolean().optional(),
240
+ },
241
+ async ({ protocolIDJSON, ...rest }) => {
242
+ if (!ctx) return noCtx
243
+ try {
244
+ return result(await ctx.wallet.decrypt({
245
+ protocolID: parseJSON(protocolIDJSON),
246
+ ...rest,
247
+ } as any))
248
+ } catch (e) { return error(e) }
249
+ },
250
+ )
251
+
252
+ server.tool(
253
+ 'wallet_createHmac',
254
+ 'Creates an HMAC using wallet keys.',
255
+ {
256
+ data: z.array(z.number()).describe('Data bytes'),
257
+ protocolIDJSON: z.string().describe('JSON array [securityLevel, protocolString]'),
258
+ keyID: z.string(),
259
+ counterparty: z.string().optional(),
260
+ privileged: z.boolean().optional(),
261
+ },
262
+ async ({ protocolIDJSON, ...rest }) => {
263
+ if (!ctx) return noCtx
264
+ try {
265
+ return result(await ctx.wallet.createHmac({
266
+ protocolID: parseJSON(protocolIDJSON),
267
+ ...rest,
268
+ } as any))
269
+ } catch (e) { return error(e) }
270
+ },
271
+ )
272
+
273
+ server.tool(
274
+ 'wallet_verifyHmac',
275
+ 'Verifies an HMAC using wallet keys.',
276
+ {
277
+ data: z.array(z.number()).describe('Data bytes'),
278
+ hmac: z.array(z.number()).describe('HMAC bytes to verify'),
279
+ protocolIDJSON: z.string().describe('JSON array [securityLevel, protocolString]'),
280
+ keyID: z.string(),
281
+ counterparty: z.string().optional(),
282
+ privileged: z.boolean().optional(),
283
+ },
284
+ async ({ protocolIDJSON, ...rest }) => {
285
+ if (!ctx) return noCtx
286
+ try {
287
+ return result(await ctx.wallet.verifyHmac({
288
+ protocolID: parseJSON(protocolIDJSON),
289
+ ...rest,
290
+ } as any))
291
+ } catch (e) { return error(e) }
292
+ },
293
+ )
294
+
295
+ server.tool(
296
+ 'wallet_createSignature',
297
+ 'Creates a digital signature using wallet keys.',
298
+ {
299
+ data: z.array(z.number()).describe('Data bytes to sign'),
300
+ protocolIDJSON: z.string().describe('JSON array [securityLevel, protocolString]'),
301
+ keyID: z.string(),
302
+ counterparty: z.string().optional(),
303
+ privileged: z.boolean().optional(),
304
+ },
305
+ async ({ protocolIDJSON, ...rest }) => {
306
+ if (!ctx) return noCtx
307
+ try {
308
+ return result(await ctx.wallet.createSignature({
309
+ protocolID: parseJSON(protocolIDJSON),
310
+ ...rest,
311
+ } as any))
312
+ } catch (e) { return error(e) }
313
+ },
314
+ )
315
+
316
+ server.tool(
317
+ 'wallet_verifySignature',
318
+ 'Verifies a digital signature using wallet keys.',
319
+ {
320
+ data: z.array(z.number()).describe('Data bytes that were signed'),
321
+ signature: z.array(z.number()).describe('Signature bytes'),
322
+ protocolIDJSON: z.string().describe('JSON array [securityLevel, protocolString]'),
323
+ keyID: z.string(),
324
+ counterparty: z.string().optional(),
325
+ forSelf: z.boolean().optional(),
326
+ privileged: z.boolean().optional(),
327
+ },
328
+ async ({ protocolIDJSON, ...rest }) => {
329
+ if (!ctx) return noCtx
330
+ try {
331
+ return result(await ctx.wallet.verifySignature({
332
+ protocolID: parseJSON(protocolIDJSON),
333
+ ...rest,
334
+ } as any))
335
+ } catch (e) { return error(e) }
336
+ },
337
+ )
338
+
339
+ // ── Key Linkage ────────────────────────────────────────────────────
340
+
341
+ server.tool(
342
+ 'wallet_revealCounterpartyKeyLinkage',
343
+ 'Reveals the linkage between the wallet identity and a counterparty to a verifier.',
344
+ {
345
+ counterparty: z.string().describe('Counterparty public key hex'),
346
+ verifier: z.string().describe('Verifier public key hex'),
347
+ privileged: z.boolean().optional(),
348
+ privilegedReason: z.string().optional(),
349
+ },
350
+ async (args) => {
351
+ if (!ctx) return noCtx
352
+ try {
353
+ return result(await ctx.wallet.revealCounterpartyKeyLinkage(args as any))
354
+ } catch (e) { return error(e) }
355
+ },
356
+ )
357
+
358
+ server.tool(
359
+ 'wallet_revealSpecificKeyLinkage',
360
+ 'Reveals linkage for a specific protocol/key combination to a verifier.',
361
+ {
362
+ counterparty: z.string().describe('Counterparty public key hex'),
363
+ verifier: z.string().describe('Verifier public key hex'),
364
+ protocolIDJSON: z.string().describe('JSON array [securityLevel, protocolString]'),
365
+ keyID: z.string(),
366
+ privileged: z.boolean().optional(),
367
+ privilegedReason: z.string().optional(),
368
+ },
369
+ async ({ protocolIDJSON, ...rest }) => {
370
+ if (!ctx) return noCtx
371
+ try {
372
+ return result(await ctx.wallet.revealSpecificKeyLinkage({
373
+ protocolID: parseJSON(protocolIDJSON),
374
+ ...rest,
375
+ } as any))
376
+ } catch (e) { return error(e) }
377
+ },
378
+ )
379
+
380
+ // ── Certificates ───────────────────────────────────────────────────
381
+
382
+ server.tool(
383
+ 'wallet_acquireCertificate',
384
+ 'Acquires an identity certificate from a certifier.',
385
+ {
386
+ type: z.string().describe('Certificate type (base64)'),
387
+ certifier: z.string().describe('Certifier public key hex'),
388
+ acquisitionProtocol: z.enum(['direct', 'issuance']),
389
+ fieldsJSON: z.string().describe('JSON object of certificate fields'),
390
+ serialNumber: z.string().optional(),
391
+ revocationOutpoint: z.string().optional(),
392
+ signature: z.string().optional(),
393
+ certifierUrl: z.string().optional(),
394
+ keyringRevealer: z.string().optional(),
395
+ keyringForSubjectJSON: z.string().optional().describe('JSON object of keyring for subject'),
396
+ privileged: z.boolean().optional(),
397
+ privilegedReason: z.string().optional(),
398
+ },
399
+ async ({ fieldsJSON, keyringForSubjectJSON, ...rest }) => {
400
+ if (!ctx) return noCtx
401
+ try {
402
+ return result(await ctx.wallet.acquireCertificate({
403
+ fields: parseJSON(fieldsJSON),
404
+ keyringForSubject: parseJSON(keyringForSubjectJSON),
405
+ ...rest,
406
+ } as any))
407
+ } catch (e) { return error(e) }
408
+ },
409
+ )
410
+
411
+ server.tool(
412
+ 'wallet_listCertificates',
413
+ 'Lists identity certificates filtered by certifiers and types.',
414
+ {
415
+ certifiersJSON: z.string().describe('JSON array of certifier public key hexes'),
416
+ typesJSON: z.string().describe('JSON array of certificate types (base64)'),
417
+ limit: z.number().default(25),
418
+ offset: z.number().default(0),
419
+ privileged: z.boolean().optional(),
420
+ privilegedReason: z.string().optional(),
421
+ },
422
+ async ({ certifiersJSON, typesJSON, ...rest }) => {
423
+ if (!ctx) return noCtx
424
+ try {
425
+ return result(await ctx.wallet.listCertificates({
426
+ certifiers: parseJSON(certifiersJSON),
427
+ types: parseJSON(typesJSON),
428
+ ...rest,
429
+ } as any))
430
+ } catch (e) { return error(e) }
431
+ },
432
+ )
433
+
434
+ server.tool(
435
+ 'wallet_proveCertificate',
436
+ 'Proves select fields of a certificate to a verifier.',
437
+ {
438
+ certificateJSON: z.string().describe('JSON object of the certificate to prove'),
439
+ fieldsToRevealJSON: z.string().describe('JSON array of field names to reveal'),
440
+ verifier: z.string().describe('Verifier public key hex'),
441
+ privileged: z.boolean().optional(),
442
+ privilegedReason: z.string().optional(),
443
+ },
444
+ async ({ certificateJSON, fieldsToRevealJSON, ...rest }) => {
445
+ if (!ctx) return noCtx
446
+ try {
447
+ return result(await ctx.wallet.proveCertificate({
448
+ certificate: parseJSON(certificateJSON),
449
+ fieldsToReveal: parseJSON(fieldsToRevealJSON),
450
+ ...rest,
451
+ } as any))
452
+ } catch (e) { return error(e) }
453
+ },
454
+ )
455
+
456
+ server.tool(
457
+ 'wallet_relinquishCertificate',
458
+ 'Removes a certificate from the wallet.',
459
+ {
460
+ type: z.string().describe('Certificate type'),
461
+ serialNumber: z.string().describe('Certificate serial number'),
462
+ certifier: z.string().describe('Certifier public key hex'),
463
+ },
464
+ async (args) => {
465
+ if (!ctx) return noCtx
466
+ try {
467
+ return result(await ctx.wallet.relinquishCertificate(args as any))
468
+ } catch (e) { return error(e) }
469
+ },
470
+ )
471
+
472
+ // ── Discovery ──────────────────────────────────────────────────────
473
+
474
+ server.tool(
475
+ 'wallet_discoverByIdentityKey',
476
+ 'Discovers certificates issued to a given identity key.',
477
+ {
478
+ identityKey: z.string().describe('Identity public key hex'),
479
+ limit: z.number().default(25),
480
+ offset: z.number().default(0),
481
+ },
482
+ async (args) => {
483
+ if (!ctx) return noCtx
484
+ try {
485
+ return result(await ctx.wallet.discoverByIdentityKey(args as any))
486
+ } catch (e) { return error(e) }
487
+ },
488
+ )
489
+
490
+ server.tool(
491
+ 'wallet_discoverByAttributes',
492
+ 'Discovers certificates matching specific attributes.',
493
+ {
494
+ attributesJSON: z.string().describe('JSON object of attribute key/value pairs to match'),
495
+ limit: z.number().default(25),
496
+ offset: z.number().default(0),
497
+ },
498
+ async ({ attributesJSON, ...rest }) => {
499
+ if (!ctx) return noCtx
500
+ try {
501
+ return result(await ctx.wallet.discoverByAttributes({
502
+ attributes: parseJSON(attributesJSON),
503
+ ...rest,
504
+ } as any))
505
+ } catch (e) { return error(e) }
506
+ },
507
+ )
508
+
509
+ // ── Info ────────────────────────────────────────────────────────────
510
+
511
+ server.tool(
512
+ 'wallet_isAuthenticated',
513
+ 'Checks if the wallet user is authenticated.',
514
+ {},
515
+ async () => {
516
+ if (!ctx) return noCtx
517
+ try {
518
+ return result(await ctx.wallet.isAuthenticated({}))
519
+ } catch (e) { return error(e) }
520
+ },
521
+ )
522
+
523
+ server.tool(
524
+ 'wallet_getHeight',
525
+ 'Gets the current blockchain height.',
526
+ {},
527
+ async () => {
528
+ if (!ctx) return noCtx
529
+ try {
530
+ return result(await ctx.wallet.getHeight({}))
531
+ } catch (e) { return error(e) }
532
+ },
533
+ )
534
+
535
+ server.tool(
536
+ 'wallet_getHeaderForHeight',
537
+ 'Gets the 80-byte block header at a given height.',
538
+ {
539
+ height: z.number().describe('Block height'),
540
+ },
541
+ async ({ height }) => {
542
+ if (!ctx) return noCtx
543
+ try {
544
+ return result(await ctx.wallet.getHeaderForHeight({ height }))
545
+ } catch (e) { return error(e) }
546
+ },
547
+ )
548
+
549
+ server.tool(
550
+ 'wallet_getNetwork',
551
+ 'Gets the network the wallet is connected to (mainnet or testnet).',
552
+ {},
553
+ async () => {
554
+ if (!ctx) return noCtx
555
+ try {
556
+ return result(await ctx.wallet.getNetwork({}))
557
+ } catch (e) { return error(e) }
558
+ },
559
+ )
560
+
561
+ server.tool(
562
+ 'wallet_getVersion',
563
+ 'Gets the wallet implementation version.',
564
+ {},
565
+ async () => {
566
+ if (!ctx) return noCtx
567
+ try {
568
+ return result(await ctx.wallet.getVersion({}))
569
+ } catch (e) { return error(e) }
570
+ },
571
+ )
572
+ }