bsv-mcp 0.0.36 → 0.2.7

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 (78) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +233 -4
  3. package/dist/index.js +120082 -54669
  4. package/index.ts +1196 -88
  5. package/next-env.d.ts +6 -0
  6. package/package.json +58 -15
  7. package/prompts/bsvSdk/index.ts +5 -5
  8. package/prompts/bsvSdk/transaction.ts +2 -2
  9. package/prompts/bsvSdk/wallet.ts +1 -1
  10. package/prompts/index.ts +1 -1
  11. package/resources/bitcom.ts +132 -0
  12. package/resources/resources.ts +7 -3
  13. package/smithery.yaml +3 -0
  14. package/test-mcp-server.ts +157 -0
  15. package/tools/a2b/discover.ts +13 -13
  16. package/tools/bap/friend.ts +266 -0
  17. package/tools/bap/generate.test.ts +350 -0
  18. package/tools/bap/generate.ts +431 -0
  19. package/tools/bap/getCurrentAddress.ts +69 -0
  20. package/tools/bap/getId.test.ts +121 -0
  21. package/tools/bap/getId.ts +169 -0
  22. package/tools/bap/index.ts +111 -0
  23. package/tools/bap/types.ts +66 -0
  24. package/tools/bap/utils.ts +33 -0
  25. package/tools/bsocial/bmapFollow.ts +121 -0
  26. package/tools/bsocial/bmapLikes.ts +111 -0
  27. package/tools/bsocial/bmapReadPosts.ts +148 -0
  28. package/tools/bsocial/createPost.ts +136 -0
  29. package/tools/bsocial/index.ts +45 -0
  30. package/tools/bsocial/readPosts.ts +255 -0
  31. package/tools/bsv/decodeTransaction.ts +7 -6
  32. package/tools/bsv/explore.ts +2 -3
  33. package/tools/bsv/getPrice.ts +1 -9
  34. package/tools/bsv/token.ts +14 -26
  35. package/tools/constants.ts +21 -0
  36. package/tools/index.ts +91 -18
  37. package/tools/mnee/getBalance.ts +2 -4
  38. package/tools/mnee/index.ts +8 -0
  39. package/tools/mnee/parseTx.ts +4 -4
  40. package/tools/mnee/sendMnee.ts +6 -6
  41. package/tools/ordinals/getInscription.ts +2 -3
  42. package/tools/ordinals/getTokenByIdOrTicker.ts +6 -3
  43. package/tools/ordinals/marketListings.ts +2 -18
  44. package/tools/ordinals/marketSales.ts +2 -4
  45. package/tools/ordinals/searchInscriptions.ts +2 -4
  46. package/tools/utils/aip.ts +46 -0
  47. package/tools/utils/conversion.ts +1 -0
  48. package/tools/utils/errorHandler.ts +76 -0
  49. package/tools/utils/index.ts +56 -14
  50. package/tools/utils/installAgentMaster.ts +84 -0
  51. package/tools/utils/logger.ts +20 -0
  52. package/tools/utils/toolConfig.ts +80 -0
  53. package/tools/utils/toolRegistration.ts +66 -0
  54. package/tools/utils/transactionBuilder.ts +196 -0
  55. package/tools/wallet/a2bPublishAgent.ts +25 -30
  56. package/tools/wallet/a2bPublishMcp.ts +43 -44
  57. package/tools/wallet/createOrdinals.ts +13 -20
  58. package/tools/wallet/fetchPaymentUtxos.ts +48 -39
  59. package/tools/wallet/gatherCollectionInfo.ts +370 -0
  60. package/tools/wallet/getAddress.ts +14 -30
  61. package/tools/wallet/getBalance.ts +54 -0
  62. package/tools/wallet/getBalanceDroplet.ts +48 -0
  63. package/tools/wallet/getPublicKey.ts +3 -16
  64. package/tools/wallet/integratedWallet.ts +120 -0
  65. package/tools/wallet/mintCollection.ts +373 -0
  66. package/tools/wallet/purchaseListing.ts +21 -31
  67. package/tools/wallet/refreshUtxos.ts +2 -14
  68. package/tools/wallet/schemas.ts +18 -249
  69. package/tools/wallet/sendOrdinals.ts +14 -21
  70. package/tools/wallet/sendToAddress.ts +2 -18
  71. package/tools/wallet/setupDroplet.ts +161 -0
  72. package/tools/wallet/tools.ts +55 -210
  73. package/tools/wallet/transactionHelper.ts +121 -0
  74. package/tools/wallet/transferOrdToken.ts +19 -24
  75. package/tools/wallet/utxo.ts +62 -0
  76. package/tools/wallet/wallet.ts +237 -234
  77. package/vite.config.ts +15 -0
  78. package/tools/a2b/call.ts +0 -32
package/index.ts CHANGED
@@ -1,13 +1,51 @@
1
1
  #!/usr/bin/env bun
2
+
3
+ // Redirect console.log/warn/info/debug to stderr in stdio mode.
4
+ // MCP stdio transport uses stdout exclusively for JSON-RPC messages —
5
+ // any stray stdout output corrupts the protocol. Must run before any import.
6
+ const _isStdio =
7
+ process.argv.includes("--stdio") ||
8
+ process.env.TRANSPORT?.toLowerCase() === "stdio";
9
+ if (_isStdio) {
10
+ const _err = console.error.bind(console);
11
+ console.log = (...a: unknown[]) => _err("[log]", ...a);
12
+ console.warn = (...a: unknown[]) => _err("[warn]", ...a);
13
+ console.info = (...a: unknown[]) => _err("[info]", ...a);
14
+ console.debug = (...a: unknown[]) => _err("[debug]", ...a);
15
+ }
16
+
17
+ import { readFile } from "node:fs/promises";
18
+ import os from "node:os";
19
+ import path from "node:path";
20
+ import { dirname, join } from "node:path";
21
+ import { fileURLToPath } from "node:url";
2
22
  import { PrivateKey } from "@bsv/sdk";
23
+ import {
24
+ RESOURCE_MIME_TYPE,
25
+ registerAppResource,
26
+ registerAppTool,
27
+ } from "@modelcontextprotocol/ext-apps/server";
3
28
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
29
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
30
+ import { z } from "zod";
31
+ import packageJson from "./package.json";
5
32
  import { registerAllPrompts } from "./prompts/index.ts";
6
33
  import { registerResources } from "./resources/resources.ts";
7
- import { registerAllTools } from "./tools/index.ts";
8
- import { registerMneeTools } from "./tools/mnee/index.ts";
9
- import { registerWalletTools } from "./tools/wallet/tools.ts";
34
+ import { getBsvPriceWithCache } from "./tools/bsv/getPrice.ts";
35
+ import { registerAllTools, type ToolsConfig } from "./tools/index.ts";
36
+ import { IntegratedWallet } from "./tools/wallet/integratedWallet.ts";
10
37
  import { Wallet } from "./tools/wallet/wallet.ts";
38
+ import { BunSSEServerTransport } from "./transports/sse.ts";
39
+ import {
40
+ type BSVJWTPayload,
41
+ createMCPJWTValidator,
42
+ generateWWWAuthenticate,
43
+ } from "./utils/jwtValidator.ts";
44
+ import { SecureKeyManager } from "./utils/keyManager.ts";
45
+ import { setServerInstance } from "./utils/passphrasePrompt.ts";
46
+
47
+ // Initialize server variable
48
+ let server: McpServer | undefined;
11
49
 
12
50
  /**
13
51
  * Configuration options from environment variables
@@ -18,116 +56,1186 @@ const CONFIG = {
18
56
  loadResources: process.env.DISABLE_RESOURCES !== "true",
19
57
  loadTools: process.env.DISABLE_TOOLS !== "true",
20
58
 
21
- // Fine-grained tool category control
59
+ // Fine-grained tool category control (dependent on key availability)
22
60
  loadWalletTools: process.env.DISABLE_WALLET_TOOLS !== "true",
23
61
  loadMneeTools: process.env.DISABLE_MNEE_TOOLS !== "true",
24
62
  loadBsvTools: process.env.DISABLE_BSV_TOOLS !== "true",
25
63
  loadOrdinalsTools: process.env.DISABLE_ORDINALS_TOOLS !== "true",
26
64
  loadUtilsTools: process.env.DISABLE_UTILS_TOOLS !== "true",
27
65
  loadA2bTools: process.env.ENABLE_A2B_TOOLS === "true",
28
-
66
+ loadBapTools: process.env.DISABLE_BAP_TOOLS !== "true",
67
+ loadBsocialTools: process.env.DISABLE_BSOCIAL_TOOLS !== "true",
29
68
  // Transaction broadcasting control
30
69
  disableBroadcasting: process.env.DISABLE_BROADCASTING === "true",
70
+
71
+ // --- Transport Mode ---
72
+ // --stdio CLI flag takes precedence over TRANSPORT env var (matches neighborhood plugin pattern)
73
+ transportMode: process.argv.includes("--stdio")
74
+ ? "stdio"
75
+ : (process.env.TRANSPORT?.toLowerCase() || "http"), // 'stdio' or 'http'/default
76
+ port: Number.parseInt(process.env.PORT || "3000", 10),
77
+
78
+ // --- Droplet API Configuration ---
79
+ useDropletApi: process.env.USE_DROPLET_API === "true",
80
+ dropletApiUrl: process.env.DROPLET_API_URL || "http://127.0.0.1:4000",
81
+ dropletFaucetName: process.env.DROPLET_FAUCET_NAME || "",
82
+
83
+ // --- OAuth Configuration ---
84
+ enableOAuth: process.env.ENABLE_OAUTH !== "false", // Enabled by default
85
+ oauthIssuer: process.env.OAUTH_ISSUER || "https://auth.sigmaidentity.com",
86
+ resourceUrl: process.env.RESOURCE_URL || "", // Will be set based on port
31
87
  };
32
88
 
89
+ const logFunc = console.error;
90
+ const KEY_DIR = path.join(os.homedir(), ".bsv-mcp");
91
+ const KEY_FILE_PATH = path.join(KEY_DIR, "keys.json");
92
+
33
93
  /**
34
- * Try to initialize the private key from environment variables
35
- * Returns the private key if valid, or undefined if not present or invalid
94
+ * Initializes payment and identity private keys using SecureKeyManager.
95
+ * Priorities:
96
+ * 1. Valid PRIVATE_KEY_WIF environment variable (provides payPk, identityPk remains undefined).
97
+ * 2. Valid keys found in encrypted ~/.bsv-mcp/keys.bep or legacy ~/.bsv-mcp/keys.json file.
98
+ * 3. Generate only payPk, save it to the file, and log warnings. identityPk/xprv only created via bap_generate.
99
+ * Returns the keys and their source ('env', 'file', 'encrypted', 'generated', or 'none').
36
100
  */
37
- function initializePrivateKey(): PrivateKey | undefined {
38
- const privateKeyWif = process.env.PRIVATE_KEY_WIF;
39
-
40
- // Check if private key is set
41
- if (!privateKeyWif) {
42
- // stdio protocol you cant send anything to console!!
43
- // console.warn(
44
- // "\x1b[33mWarning: PRIVATE_KEY_WIF environment variable is not set\x1b[0m",
45
- // );
46
- // console.warn(
47
- // "The server will run, but wallet operations requiring a private key will return errors.",
48
- // );
49
- // console.warn(
50
- // "Set this variable with a valid Bitcoin SV private key in WIF format to enable all features:",
51
- // );
52
- // console.warn(
53
- // "Example: PRIVATE_KEY_WIF=your_private_key_wif bun run index.ts",
54
- // );
55
- return undefined;
56
- }
57
-
58
- // Validate the private key format
101
+ async function initializeKeys(): Promise<{
102
+ payPk?: PrivateKey;
103
+ identityPk?: PrivateKey;
104
+ xprv?: string;
105
+ source: "env" | "file" | "encrypted" | "generated" | "none";
106
+ }> {
107
+ const keyManager = new SecureKeyManager({ keyDir: KEY_DIR });
108
+
109
+ const privateKeyWifEnv = process.env.PRIVATE_KEY_WIF;
110
+
111
+ // 1. Try environment variable for payPk
112
+ if (privateKeyWifEnv) {
113
+ try {
114
+ const payPk = PrivateKey.fromWif(privateKeyWifEnv);
115
+ console.error(
116
+ "\x1b[32mINFO: Using valid PRIVATE_KEY_WIF from environment for payment key.\x1b[0m",
117
+ );
118
+
119
+ // Also check for encrypted/legacy keys to get xprv if available
120
+ let xprvFromFile: string | undefined;
121
+ try {
122
+ const { keys } = await keyManager.loadKeys();
123
+ xprvFromFile = keys.xprv;
124
+ if (xprvFromFile) {
125
+ console.error(
126
+ "\x1b[32mINFO: Loaded BAP HD Master Key (xprv) from secure storage alongside ENV payPk.\x1b[0m",
127
+ );
128
+ }
129
+ } catch (_e) {
130
+ /* Ignore file read errors here */
131
+ }
132
+
133
+ if (!xprvFromFile) {
134
+ console.error(
135
+ "\x1b[33mWARN: No identity key (identityPk or xprv) found. BAP/A2B tools needing identity will be limited.\x1b[0m",
136
+ );
137
+ }
138
+
139
+ return {
140
+ payPk,
141
+ identityPk: undefined,
142
+ xprv: xprvFromFile,
143
+ source: "env",
144
+ };
145
+ } catch (_error) {
146
+ console.error(
147
+ "\x1b[33mWARN: Invalid PRIVATE_KEY_WIF format in environment variable. Checking secure storage next.\x1b[0m",
148
+ );
149
+ }
150
+ }
151
+
152
+ // 2. Try reading from secure storage (encrypted or legacy)
59
153
  try {
60
- return PrivateKey.fromWif(privateKeyWif);
154
+ const { keys, source } = await keyManager.loadKeys();
155
+
156
+ if (keys.payPk) {
157
+ if (source === "encrypted") {
158
+ console.error(
159
+ `\x1b[32mINFO: Using encrypted keys from: ${KEY_DIR}/keys.bep\x1b[0m`,
160
+ );
161
+ } else if (source === "legacy") {
162
+ console.error(
163
+ `\x1b[32mINFO: Using legacy keys from: ${KEY_FILE_PATH}\x1b[0m`,
164
+ );
165
+ console.error(
166
+ "\x1b[33mWARN: Using unencrypted keys. Run the server again to encrypt them.\x1b[0m",
167
+ );
168
+ }
169
+
170
+ if (keys.identityPk) {
171
+ console.error(
172
+ "\x1b[32mINFO: Loaded Identity Key (identityPk) from secure storage.\x1b[0m",
173
+ );
174
+ }
175
+ if (keys.xprv) {
176
+ console.error(
177
+ "\x1b[32mINFO: Loaded BAP HD Master Key (xprv) from secure storage.\x1b[0m",
178
+ );
179
+ }
180
+
181
+ return {
182
+ payPk: keys.payPk,
183
+ identityPk: keys.identityPk,
184
+ xprv: keys.xprv,
185
+ source: source === "encrypted" ? "encrypted" : "file",
186
+ };
187
+ }
188
+
189
+ console.error("\x1b[33mINFO: No keys found in secure storage.\x1b[0m");
61
190
  } catch (error) {
62
- // console.warn("\x1b[33mWarning: Invalid private key format\x1b[0m");
63
- // console.warn(
64
- // "The PRIVATE_KEY_WIF provided is not a valid Bitcoin SV private key in WIF format.",
65
- // );
66
- // console.warn(
67
- // "The server will run, but wallet operations requiring a private key will return errors.",
68
- // );
69
- return undefined;
191
+ console.error(
192
+ `\x1b[33mWARN: Error reading keys from secure storage: ${error}\x1b[0m`,
193
+ );
70
194
  }
195
+
196
+ // 3. Generate new payment key (but not identityPk/xprv)
197
+ console.error("\x1b[33mINFO: Generating new payment key ONLY.\x1b[0m");
198
+ const payPk = PrivateKey.fromRandom();
199
+ const keyStore = {
200
+ payPk,
201
+ identityPk: undefined,
202
+ xprv: undefined,
203
+ };
204
+
205
+ try {
206
+ // Save unencrypted for now - user can encrypt on next run
207
+ await keyManager.saveKeys(keyStore, { forceUnencrypted: true });
208
+ const status = keyManager.getStatus();
209
+
210
+ if (status.hasEncrypted) {
211
+ console.error(
212
+ `\x1b[33mWARN: Saved newly generated payment key (encrypted) to: ${KEY_DIR}/keys.bep\x1b[0m`,
213
+ );
214
+ } else {
215
+ console.error(
216
+ `\x1b[33mWARN: Saved newly generated payment key (unencrypted) to: ${KEY_FILE_PATH}\x1b[0m`,
217
+ );
218
+ console.error(
219
+ "\x1b[33mWARN: Run the server again to encrypt your keys.\x1b[0m",
220
+ );
221
+ }
222
+ console.error(
223
+ "\x1b[33mWARN: Identity key (identityPk/xprv) not found or generated. Use 'bap_generate' tool to create one.\x1b[0m",
224
+ );
225
+ } catch (writeError) {
226
+ console.error(
227
+ `\x1b[31mERROR: Failed to save newly generated payment key: ${writeError}\x1b[0m`,
228
+ );
229
+ console.error(
230
+ "\x1b[31mERROR: Using the generated payment key for this session only. No identity key available.\x1b[0m",
231
+ );
232
+ }
233
+
234
+ return { payPk, identityPk: undefined, xprv: undefined, source: "generated" };
71
235
  }
72
236
 
73
- // Try to initialize private key but don't stop the server if missing or invalid
74
- const privKey = initializePrivateKey();
237
+ // --- MCP App Tools & Resource ---
238
+ const APP_RESOURCE_URI = "ui://bsv-mcp/app.html";
239
+ const __appDirname = dirname(fileURLToPath(import.meta.url));
240
+
241
+ function registerMcpAppTools(server: McpServer, wallet?: Wallet) {
242
+ // Primary dashboard tool — model calls this to open the UI
243
+ registerAppTool(
244
+ server,
245
+ "bsv_dashboard",
246
+ {
247
+ title: "BSV Dashboard",
248
+ description:
249
+ "Interactive BSV dashboard with Explorer, Wallet, and Ordinals tabs. Use this for any BSV-related query that benefits from visual display.",
250
+ inputSchema: {},
251
+ _meta: {
252
+ ui: { resourceUri: APP_RESOURCE_URI },
253
+ },
254
+ },
255
+ async () => {
256
+ return {
257
+ content: [{ type: "text" as const, text: "BSV Dashboard opened" }],
258
+ structuredContent: { view: "dashboard", ready: true },
259
+ };
260
+ },
261
+ );
262
+
263
+ // App-only: fetch explorer data (price, chain info, tx decode, address lookup)
264
+ registerAppTool(
265
+ server,
266
+ "app_explorer_data",
267
+ {
268
+ title: "Explorer Data",
269
+ description:
270
+ "App-only: fetches BSV price, chain info, decodes transactions, and looks up addresses.",
271
+ inputSchema: {
272
+ txid: z.string().optional().describe("Transaction ID to decode"),
273
+ address: z
274
+ .string()
275
+ .optional()
276
+ .describe("Address to look up balance/history"),
277
+ },
278
+ _meta: {
279
+ ui: { resourceUri: APP_RESOURCE_URI, visibility: ["app"] },
280
+ },
281
+ },
282
+ async (args) => {
283
+ const { txid, address } = args as {
284
+ txid?: string;
285
+ address?: string;
286
+ };
287
+
288
+ // If txid provided, decode transaction
289
+ if (txid) {
290
+ try {
291
+ const res = await fetch(
292
+ `https://junglebus.gorillapool.io/v1/transaction/get/${txid}`,
293
+ );
294
+ if (!res.ok)
295
+ throw new Error(`Transaction not found: ${res.status}`);
296
+ const jbData = (await res.json()) as Record<string, unknown>;
297
+
298
+ const { Transaction, Utils } = await import("@bsv/sdk");
299
+ const rawTx = jbData.transaction as string;
300
+ const isBase64 = /^[A-Za-z0-9+/=]+$/.test(rawTx);
301
+ const txBytes = isBase64
302
+ ? Utils.toArray(rawTx, "base64")
303
+ : Utils.toArray(rawTx, "hex");
304
+ const tx = Transaction.fromBinary(txBytes);
305
+
306
+ return {
307
+ content: [
308
+ { type: "text" as const, text: `Decoded transaction ${txid}` },
309
+ ],
310
+ structuredContent: {
311
+ transaction: {
312
+ txid,
313
+ version: tx.version,
314
+ lockTime: tx.lockTime,
315
+ size: tx.toBinary().length,
316
+ inputs: tx.inputs.map((inp) => ({
317
+ txid: inp.sourceTXID,
318
+ vout: inp.sourceOutputIndex,
319
+ script: inp.unlockingScript?.toHex() || "",
320
+ })),
321
+ outputs: tx.outputs.map((out, i) => ({
322
+ n: i,
323
+ value: out.satoshis,
324
+ scriptPubKey: {
325
+ hex: out.lockingScript.toHex(),
326
+ asm: out.lockingScript.toASM(),
327
+ },
328
+ })),
329
+ confirmations: jbData.block_height ? 1 : 0,
330
+ block: jbData.block_hash
331
+ ? {
332
+ hash: jbData.block_hash,
333
+ height: jbData.block_height,
334
+ }
335
+ : null,
336
+ },
337
+ },
338
+ };
339
+ } catch (err) {
340
+ return {
341
+ content: [
342
+ {
343
+ type: "text" as const,
344
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
345
+ },
346
+ ],
347
+ structuredContent: { error: String(err) },
348
+ };
349
+ }
350
+ }
351
+
352
+ // If address provided, look up balance and history
353
+ if (address) {
354
+ try {
355
+ const [balRes, histRes] = await Promise.all([
356
+ fetch(
357
+ `https://api.whatsonchain.com/v1/bsv/main/address/${address}/balance`,
358
+ ),
359
+ fetch(
360
+ `https://api.whatsonchain.com/v1/bsv/main/address/${address}/history`,
361
+ ),
362
+ ]);
363
+ const balance = balRes.ok
364
+ ? ((await balRes.json()) as Record<string, unknown>)
365
+ : null;
366
+ const history = histRes.ok
367
+ ? ((await histRes.json()) as Array<Record<string, unknown>>)
368
+ : [];
369
+
370
+ return {
371
+ content: [
372
+ { type: "text" as const, text: `Address info for ${address}` },
373
+ ],
374
+ structuredContent: {
375
+ addressInfo: { balance, history },
376
+ },
377
+ };
378
+ } catch (err) {
379
+ return {
380
+ content: [
381
+ {
382
+ type: "text" as const,
383
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
384
+ },
385
+ ],
386
+ structuredContent: { error: String(err) },
387
+ };
388
+ }
389
+ }
390
+
391
+ // Default: return price + chain info
392
+ try {
393
+ const [price, chainRes] = await Promise.all([
394
+ getBsvPriceWithCache(),
395
+ fetch("https://api.whatsonchain.com/v1/bsv/main/chain/info"),
396
+ ]);
397
+ const chainInfo = chainRes.ok
398
+ ? ((await chainRes.json()) as Record<string, unknown>)
399
+ : null;
400
+
401
+ return {
402
+ content: [
403
+ {
404
+ type: "text" as const,
405
+ text: `BSV price: $${price.toFixed(2)}`,
406
+ },
407
+ ],
408
+ structuredContent: { price, chainInfo },
409
+ };
410
+ } catch (err) {
411
+ return {
412
+ content: [
413
+ {
414
+ type: "text" as const,
415
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
416
+ },
417
+ ],
418
+ structuredContent: { error: String(err) },
419
+ };
420
+ }
421
+ },
422
+ );
423
+
424
+ // App-only: fetch wallet data
425
+ registerAppTool(
426
+ server,
427
+ "app_wallet_data",
428
+ {
429
+ title: "Wallet Data",
430
+ description: "App-only: fetches wallet balance, UTXOs, and address.",
431
+ inputSchema: {},
432
+ _meta: {
433
+ ui: { resourceUri: APP_RESOURCE_URI, visibility: ["app"] },
434
+ },
435
+ },
436
+ async () => {
437
+ if (!wallet) {
438
+ return {
439
+ content: [
440
+ {
441
+ type: "text" as const,
442
+ text: "No wallet configured",
443
+ },
444
+ ],
445
+ structuredContent: {
446
+ error: "No wallet configured. Set PRIVATE_KEY_WIF or generate keys.",
447
+ },
448
+ };
449
+ }
450
+
451
+ try {
452
+ const { paymentUtxos } = await wallet.getUtxos();
453
+ const address = wallet.getAddress();
454
+ let totalSatoshis = 0;
455
+ for (const utxo of paymentUtxos) {
456
+ totalSatoshis += utxo.satoshis || 0;
457
+ }
458
+
459
+ let price: number | undefined;
460
+ try {
461
+ price = await getBsvPriceWithCache();
462
+ } catch {
463
+ /* price fetch optional */
464
+ }
465
+
466
+ const { toBitcoin } = await import("satoshi-token");
467
+ const bsvAmount = toBitcoin(totalSatoshis);
75
468
 
76
- const server = new McpServer(
77
- { name: "Bitcoin SV", version: "0.0.36" },
78
- {
79
- capabilities: {
80
- prompts: {},
81
- resources: {},
82
- tools: {},
469
+ return {
470
+ content: [
471
+ {
472
+ type: "text" as const,
473
+ text: `Wallet balance: ${bsvAmount} BSV`,
474
+ },
475
+ ],
476
+ structuredContent: {
477
+ balance: {
478
+ satoshis: totalSatoshis,
479
+ bsv: bsvAmount,
480
+ utxoCount: paymentUtxos.length,
481
+ },
482
+ address,
483
+ utxos: paymentUtxos.slice(0, 50).map((u) => ({
484
+ txid: u.txid,
485
+ vout: u.vout,
486
+ satoshis: u.satoshis,
487
+ })),
488
+ price,
489
+ },
490
+ };
491
+ } catch (err) {
492
+ return {
493
+ content: [
494
+ {
495
+ type: "text" as const,
496
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
497
+ },
498
+ ],
499
+ structuredContent: { error: String(err) },
500
+ };
501
+ }
83
502
  },
84
- instructions: `
85
- This server exposes Bitcoin SV helpers.
86
- Tools are idempotent unless marked destructive.
87
- `,
88
- },
89
- );
90
-
91
- // Initialize wallet and register tools based on configuration
92
- let wallet: Wallet | null = null;
93
-
94
- if (CONFIG.loadTools) {
95
- if (privKey) {
96
- // Register MNEE tools if enabled
97
- if (CONFIG.loadMneeTools) {
98
- registerMneeTools(server);
503
+ );
504
+
505
+ // App-only: fetch ordinals data
506
+ registerAppTool(
507
+ server,
508
+ "app_ordinals_data",
509
+ {
510
+ title: "Ordinals Data",
511
+ description:
512
+ "App-only: fetches ordinals/NFT marketplace listings and search results.",
513
+ inputSchema: {
514
+ query: z.string().optional().describe("Search query"),
515
+ },
516
+ _meta: {
517
+ ui: { resourceUri: APP_RESOURCE_URI, visibility: ["app"] },
518
+ },
519
+ },
520
+ async (args) => {
521
+ const { query } = args as { query?: string };
522
+
523
+ try {
524
+ if (query) {
525
+ // Search inscriptions
526
+ const url = new URL(
527
+ "https://ordinals.gorillapool.io/api/inscriptions/search",
528
+ );
529
+ url.searchParams.set("limit", "20");
530
+ url.searchParams.set("offset", "0");
531
+ url.searchParams.set("dir", "desc");
532
+ url.searchParams.set("terms", query);
533
+
534
+ const res = await fetch(url.toString());
535
+ if (!res.ok) throw new Error(`Search failed: ${res.status}`);
536
+ const data = (await res.json()) as Record<string, unknown>;
537
+
538
+ return {
539
+ content: [
540
+ {
541
+ type: "text" as const,
542
+ text: `Found results for "${query}"`,
543
+ },
544
+ ],
545
+ structuredContent: {
546
+ results: data.results || [],
547
+ total: data.total || 0,
548
+ },
549
+ };
550
+ }
551
+
552
+ // Default: fetch marketplace listings
553
+ const res = await fetch(
554
+ "https://ordinals.gorillapool.io/api/market?limit=20&offset=0&sort=recent&dir=desc",
555
+ );
556
+ if (!res.ok) throw new Error(`Market fetch failed: ${res.status}`);
557
+ const data = (await res.json()) as Record<string, unknown>;
558
+
559
+ return {
560
+ content: [
561
+ { type: "text" as const, text: "Marketplace listings loaded" },
562
+ ],
563
+ structuredContent: {
564
+ listings: data.results || [],
565
+ total: data.total || 0,
566
+ },
567
+ };
568
+ } catch (err) {
569
+ return {
570
+ content: [
571
+ {
572
+ type: "text" as const,
573
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
574
+ },
575
+ ],
576
+ structuredContent: { error: String(err) },
577
+ };
578
+ }
579
+ },
580
+ );
581
+
582
+ // Register the HTML resource
583
+ registerAppResource(
584
+ server,
585
+ "BSV Dashboard",
586
+ APP_RESOURCE_URI,
587
+ { description: "Interactive BSV dashboard with Explorer, Wallet, and Ordinals tabs" },
588
+ async () => {
589
+ const distPath = join(__appDirname, "dist", "app.html");
590
+ let html: string;
591
+ try {
592
+ html = await readFile(distPath, "utf-8");
593
+ } catch {
594
+ html = "<html><body><p>Dashboard not built. Run <code>bun run build:view</code> to enable it.</p></body></html>";
595
+ }
596
+ return {
597
+ contents: [
598
+ {
599
+ uri: APP_RESOURCE_URI,
600
+ mimeType: RESOURCE_MIME_TYPE,
601
+ text: html,
602
+ },
603
+ ],
604
+ };
605
+ },
606
+ );
607
+ }
608
+
609
+ // --- Main Server Setup ---
610
+ async function main() {
611
+ // Check for help or info commands that don't need authentication
612
+ const args = process.argv.slice(2);
613
+ if (args.includes("--help") || args.includes("-h") || args.includes("help")) {
614
+ console.log(`
615
+ BSV MCP Server v${packageJson.version}
616
+
617
+ Usage: bun run index.ts [options]
618
+
619
+ Options:
620
+ --help, -h Show this help message
621
+ --version, -v Show version information
622
+
623
+ Environment Variables:
624
+ TRANSPORT Transport mode: 'stdio' or 'http' (default: http)
625
+ PORT HTTP server port (default: 3000)
626
+ PRIVATE_KEY_WIF Payment private key in WIF format
627
+ DISABLE_TOOLS Disable all tools (default: false)
628
+ DISABLE_WALLET_TOOLS Disable wallet tools (default: false)
629
+ DISABLE_BSV_TOOLS Disable BSV tools (default: false)
630
+ DISABLE_ORDINALS_TOOLS Disable ordinals tools (default: false)
631
+ DISABLE_UTILS_TOOLS Disable utility tools (default: false)
632
+ DISABLE_BAP_TOOLS Disable BAP tools (default: false)
633
+ DISABLE_BSOCIAL_TOOLS Disable BSocial tools (default: false)
634
+ ENABLE_A2B_TOOLS Enable A2B tools (default: false)
635
+ DISABLE_BROADCASTING Disable transaction broadcasting (default: false)
636
+ USE_DROPLET_API Use Droplet API for transactions (default: false)
637
+
638
+ Tool Categories:
639
+ BSV Tools: Price lookup, transaction decoding, validation
640
+ Wallet Tools: Send payments, manage UTXOs (requires payment key)
641
+ Ordinals Tools: Search listings, market data
642
+ Utils Tools: General utilities, conversions
643
+ BAP Tools: Identity management (requires identity key)
644
+ BSocial Tools: Social posts, likes, follows
645
+ A2B Tools: Advanced BSV operations (requires identity key)
646
+
647
+ Authentication:
648
+ - Most tools work without authentication
649
+ - Wallet operations require PRIVATE_KEY_WIF or generated keys
650
+ - BAP/A2B tools require identity keys (generated via bap_generate tool)
651
+ `);
652
+ process.exit(0);
653
+ }
654
+
655
+ if (args.includes("--version") || args.includes("-v")) {
656
+ console.log(`${packageJson.name} v${packageJson.version}`);
657
+ process.exit(0);
658
+ }
659
+
660
+ // --- Initialize Keys ---
661
+ const { payPk, identityPk, xprv, source: keySource } = await initializeKeys();
662
+
663
+ // Define persistence based on source
664
+ const hasPersistentPayKey =
665
+ keySource === "env" || keySource === "file" || keySource === "encrypted";
666
+ const hasPersistentIdentityKey =
667
+ !!identityPk && (keySource === "file" || keySource === "encrypted");
668
+ const hasXprv =
669
+ !!xprv &&
670
+ (keySource === "file" || keySource === "env" || keySource === "encrypted");
671
+
672
+ const effectiveConfig = { ...CONFIG };
673
+
674
+ // --- Configuration Logging ---
675
+ logFunc("\n--- BSV MCP Server Configuration ---");
676
+ logFunc(`Server Version: ${packageJson.version}`);
677
+ logFunc(`Server Name: ${packageJson.name}`);
678
+ logFunc("\nEnvironment Variables:");
679
+ logFunc(
680
+ ` TRANSPORT: ${process.env.TRANSPORT || "Not Set (http default)"}`,
681
+ );
682
+ if (CONFIG.transportMode === "http") {
683
+ logFunc(
684
+ ` PORT: ${process.env.PORT || "Not Set (3000 default)"}`,
685
+ );
686
+ }
687
+ logFunc(
688
+ ` PRIVATE_KEY_WIF: ${process.env.PRIVATE_KEY_WIF ? "Set (using env key)" : "Not Set (using file/generating)"}`,
689
+ );
690
+ logFunc(
691
+ ` IDENTITY_KEY_WIF: ${process.env.IDENTITY_KEY_WIF ? "Set (using env key)" : "Not Set (using file/generating)"}`,
692
+ );
693
+ if (process.env.BSV_MCP_PASSPHRASE) {
694
+ logFunc(" BSV_MCP_PASSPHRASE: \x1b[31mDEPRECATED - Remove this!\x1b[0m");
695
+ }
696
+ logFunc(
697
+ ` DISABLE_PROMPTS: ${process.env.DISABLE_PROMPTS === "true" ? "Set (true)" : "Not Set/false"}`,
698
+ );
699
+ logFunc(
700
+ ` DISABLE_RESOURCES: ${process.env.DISABLE_RESOURCES === "true" ? "Set (true)" : "Not Set/false"}`,
701
+ );
702
+ logFunc(
703
+ ` DISABLE_TOOLS: ${process.env.DISABLE_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
704
+ );
705
+ logFunc(
706
+ ` DISABLE_WALLET_TOOLS: ${process.env.DISABLE_WALLET_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
707
+ );
708
+ logFunc(
709
+ ` DISABLE_MNEE_TOOLS: ${process.env.DISABLE_MNEE_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
710
+ );
711
+ logFunc(
712
+ ` DISABLE_BSV_TOOLS: ${process.env.DISABLE_BSV_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
713
+ );
714
+ logFunc(
715
+ ` DISABLE_ORDINALS_TOOLS: ${process.env.DISABLE_ORDINALS_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
716
+ );
717
+ logFunc(
718
+ ` DISABLE_UTILS_TOOLS: ${process.env.DISABLE_UTILS_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
719
+ );
720
+ logFunc(
721
+ ` ENABLE_A2B_TOOLS: ${process.env.ENABLE_A2B_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
722
+ );
723
+ logFunc(
724
+ ` DISABLE_BAP_TOOLS: ${process.env.DISABLE_BAP_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
725
+ );
726
+ logFunc(
727
+ ` DISABLE_BROADCASTING: ${process.env.DISABLE_BROADCASTING === "true" ? "Set (true)" : "Not Set/false"}`,
728
+ );
729
+ logFunc(
730
+ ` USE_DROPLET_API: ${CONFIG.useDropletApi ? "Set (true)" : "Not Set/false"}`,
731
+ );
732
+ if (CONFIG.useDropletApi) {
733
+ logFunc(` DROPLET_API_URL: ${CONFIG.dropletApiUrl}`);
734
+ logFunc(` DROPLET_FAUCET_NAME: ${CONFIG.dropletFaucetName || "Not Set"}`);
735
+ }
736
+
737
+ logFunc("\nKey Source:");
738
+ let payKeySourceInfo = `Payment Key (payPk): ${keySource}`;
739
+ if (keySource === "env") payKeySourceInfo = "Payment Key (payPk): env";
740
+ if (keySource === "file")
741
+ payKeySourceInfo += ` (Loaded from ${KEY_FILE_PATH})`;
742
+ if (keySource === "encrypted")
743
+ payKeySourceInfo += ` (Loaded from ${KEY_DIR}/keys.bep)`;
744
+ if (keySource === "generated") payKeySourceInfo += " (Generated & saved)";
745
+
746
+ let identityKeySourceInfo = "Identity Key (identityPk): Not Loaded";
747
+ if (identityPk && (keySource === "file" || keySource === "encrypted")) {
748
+ identityKeySourceInfo = `Identity Key (identityPk): ${keySource} (Loaded from secure storage)`;
749
+ } else if (identityPk) {
750
+ identityKeySourceInfo = `Identity Key (identityPk): ${keySource} (Unexpected Source)`;
751
+ }
752
+
753
+ let xprvSourceInfo = "BAP Master Key (xprv): Not found";
754
+ if (hasXprv) {
755
+ xprvSourceInfo = "BAP Master Key (xprv): Loaded from secure storage";
756
+ }
757
+
758
+ logFunc(` ${payKeySourceInfo}`);
759
+ logFunc(` ${identityKeySourceInfo}`);
760
+ logFunc(` ${xprvSourceInfo}`);
761
+
762
+ logFunc("\nEffective Component Status:");
763
+ logFunc(` Transport Mode: ${CONFIG.transportMode.toUpperCase()}`);
764
+ logFunc(
765
+ ` Prompts: ${effectiveConfig.loadPrompts ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
766
+ );
767
+ logFunc(
768
+ ` Resources: ${effectiveConfig.loadResources ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
769
+ );
770
+ logFunc(
771
+ ` Tools (Overall): ${effectiveConfig.loadTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
772
+ );
773
+ if (effectiveConfig.loadTools) {
774
+ const walletStatus = effectiveConfig.loadWalletTools
775
+ ? "\x1b[32mEnabled\x1b[0m"
776
+ : "\x1b[31mDisabled\x1b[0m";
777
+ const mneeStatus = effectiveConfig.loadMneeTools
778
+ ? "\x1b[32mEnabled\x1b[0m"
779
+ : "\x1b[31mDisabled\x1b[0m";
780
+ const a2bStatus = effectiveConfig.loadA2bTools
781
+ ? "\x1b[32mEnabled\x1b[0m"
782
+ : "\x1b[31mDisabled\x1b[0m";
783
+ const bapStatus = effectiveConfig.loadBapTools
784
+ ? "\x1b[32mEnabled\x1b[0m"
785
+ : "\x1b[31mDisabled\x1b[0m";
786
+
787
+ let payKeyNote = "";
788
+ if (!hasPersistentPayKey) {
789
+ payKeyNote = " \x1b[33m(Using generated payPk)\x1b[0m";
790
+ }
791
+ let identityKeyNote = "";
792
+ if (!hasPersistentIdentityKey) {
793
+ identityKeyNote = " \x1b[33m(Using generated identityPk)\x1b[0m";
99
794
  }
100
795
 
101
- // Initialize wallet with the private key if wallet tools are enabled
102
- if (CONFIG.loadWalletTools) {
103
- wallet = new Wallet(privKey);
104
- registerWalletTools(server, wallet, {
105
- disableBroadcasting: CONFIG.disableBroadcasting,
106
- enableA2bTools: CONFIG.loadA2bTools,
107
- });
796
+ logFunc(` Wallet: ${walletStatus}${payKeyNote}`);
797
+ logFunc(` MNEE: ${mneeStatus}${payKeyNote}`);
798
+ logFunc(
799
+ ` BSV: ${effectiveConfig.loadBsvTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
800
+ );
801
+ logFunc(
802
+ ` Ordinals: ${effectiveConfig.loadOrdinalsTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
803
+ );
804
+ logFunc(
805
+ ` Utils: ${effectiveConfig.loadUtilsTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
806
+ );
807
+ logFunc(` A2B: ${a2bStatus}${identityKeyNote}`);
808
+ logFunc(` BAP: ${bapStatus}${identityKeyNote}`);
809
+ logFunc(
810
+ ` BSocial: ${effectiveConfig.loadBsocialTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
811
+ );
812
+ if (effectiveConfig.loadWalletTools) {
813
+ logFunc(
814
+ ` Broadcasting: ${!effectiveConfig.disableBroadcasting ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
815
+ );
108
816
  }
109
817
  }
818
+ logFunc("------------------------------------\n");
819
+ // --- End of Logging Block ---
110
820
 
111
- // Register all other tools based on configuration
112
- registerAllTools(server, {
113
- enableBsvTools: CONFIG.loadBsvTools,
114
- enableOrdinalsTools: CONFIG.loadOrdinalsTools,
115
- enableUtilsTools: CONFIG.loadUtilsTools,
116
- enableA2bTools: CONFIG.loadA2bTools,
117
- });
118
- }
821
+ server = new McpServer(
822
+ { name: packageJson.name, version: packageJson.version },
823
+ {
824
+ capabilities: {
825
+ prompts: {},
826
+ resources: {},
827
+ tools: {},
828
+ experimental: {
829
+ "io.modelcontextprotocol/ui": { version: "0.1" },
830
+ },
831
+ },
832
+ instructions: `
833
+ This server exposes Bitcoin SV helpers.
834
+ Tools are idempotent unless marked destructive.
835
+ `,
836
+ },
837
+ );
119
838
 
120
- // Register resources if enabled
121
- if (CONFIG.loadResources) {
122
- registerResources(server);
123
- }
839
+ // Set server instance for transport detection
840
+ setServerInstance(server);
124
841
 
125
- // Register prompts if enabled
126
- if (CONFIG.loadPrompts) {
127
- registerAllPrompts(server);
842
+ let wallet: Wallet | undefined;
843
+ let integratedWallet: IntegratedWallet | undefined;
844
+
845
+ if (CONFIG.loadTools) {
846
+ // Check if we should use Droplet API mode
847
+ if (CONFIG.useDropletApi && CONFIG.dropletFaucetName) {
848
+ // Initialize IntegratedWallet in Droplet mode
849
+ if (CONFIG.loadWalletTools) {
850
+ try {
851
+ integratedWallet = new IntegratedWallet({
852
+ useDropletApi: true,
853
+ dropletConfig: {
854
+ apiUrl: CONFIG.dropletApiUrl,
855
+ faucetName: CONFIG.dropletFaucetName,
856
+ // Note: authKey will be set by IntegratedWallet if paymentKey is provided
857
+ },
858
+ paymentKey: payPk, // Pass payment key for auth
859
+ identityKey: identityPk,
860
+ });
861
+ logFunc(
862
+ `\x1b[32mINFO: Droplet API mode initialized successfully (Faucet: ${CONFIG.dropletFaucetName}).\x1b[0m`,
863
+ );
864
+ logFunc(
865
+ `\x1b[33mNOTE: Using Droplet API at ${CONFIG.dropletApiUrl}\x1b[0m`,
866
+ );
867
+ logFunc(
868
+ "\x1b[33mNOTE: Local keys are ignored in Droplet API mode\x1b[0m",
869
+ );
870
+ // Create a compatibility wrapper for existing code
871
+ wallet = integratedWallet.getLocalWallet(); // This will be undefined in Droplet mode
872
+
873
+ // In Droplet mode, we need to disable tools that require local keys
874
+ effectiveConfig.loadMneeTools = false; // MNEE requires local wallet
875
+ effectiveConfig.loadBapTools = false; // BAP requires identity key
876
+ effectiveConfig.loadA2bTools = false; // A2B requires identity key
877
+ effectiveConfig.loadBsocialTools = false; // BSocial requires local wallet
878
+ } catch (e) {
879
+ logFunc(
880
+ `\x1b[31mERROR: Failed to initialize Droplet API mode: ${e instanceof Error ? e.message : String(e)}. Wallet-dependent tools will be unavailable.\x1b[0m`,
881
+ );
882
+ integratedWallet = undefined;
883
+ effectiveConfig.loadWalletTools = false;
884
+ effectiveConfig.loadMneeTools = false;
885
+ effectiveConfig.loadBapTools = false;
886
+ }
887
+ }
888
+ } else if (payPk) {
889
+ // Initialize wallet with the payPk if wallet tools are enabled
890
+ if (CONFIG.loadWalletTools) {
891
+ try {
892
+ wallet = new Wallet(payPk, identityPk);
893
+ integratedWallet = new IntegratedWallet({
894
+ paymentKey: payPk,
895
+ identityKey: identityPk,
896
+ });
897
+ logFunc(
898
+ "\x1b[32mINFO: Custom Wallet initialized successfully.\x1b[0m",
899
+ );
900
+ if (!hasPersistentPayKey) {
901
+ logFunc(
902
+ "\x1b[33mWARN: Wallet is using a generated payment key for this session only. It will not persist.\x1b[0m",
903
+ );
904
+ }
905
+ if (identityPk && !hasPersistentIdentityKey && !hasXprv) {
906
+ logFunc(
907
+ "\x1b[33mWARN: Wallet is using an identity key that might not be from a persistent file source (keys.json xprv or identityPk field) for BAP operations.\x1b[0m",
908
+ );
909
+ }
910
+ } catch (e) {
911
+ logFunc(
912
+ `\x1b[31mERROR: Failed to initialize custom wallet: ${e instanceof Error ? e.message : String(e)}. Wallet-dependent tools will be unavailable.\x1b[0m`,
913
+ );
914
+ wallet = undefined;
915
+ integratedWallet = undefined;
916
+ effectiveConfig.loadWalletTools = false;
917
+ effectiveConfig.loadMneeTools = false;
918
+ effectiveConfig.loadBapTools = false;
919
+ }
920
+ }
921
+
922
+ // Disable MNEE tools if wallet is not available
923
+ if (effectiveConfig.loadMneeTools && !wallet && CONFIG.loadWalletTools) {
924
+ logFunc(
925
+ "\x1b[33mWARN: MNEE tools require a wallet but wallet initialization failed. MNEE tools disabled.\x1b[0m",
926
+ );
927
+ effectiveConfig.loadMneeTools = false;
928
+ }
929
+ }
930
+
931
+ // Register all other tools based on configuration
932
+ const toolsConfig: ToolsConfig = {
933
+ enableBsvTools: effectiveConfig.loadBsvTools,
934
+ enableOrdinalsTools: effectiveConfig.loadOrdinalsTools,
935
+ enableUtilsTools: effectiveConfig.loadUtilsTools,
936
+ enableA2bTools: effectiveConfig.loadA2bTools,
937
+ enableBapTools: effectiveConfig.loadBapTools,
938
+ enableBsocialTools: effectiveConfig.loadBsocialTools,
939
+ enableWalletTools: effectiveConfig.loadWalletTools,
940
+ enableMneeTools: effectiveConfig.loadMneeTools,
941
+ identityPk,
942
+ payPk,
943
+ xprv,
944
+ wallet,
945
+ integratedWallet,
946
+ disableBroadcasting: effectiveConfig.disableBroadcasting,
947
+ };
948
+
949
+ registerAllTools(server, toolsConfig);
950
+ }
951
+
952
+ // Register MCP App tools and resource (interactive UI)
953
+ registerMcpAppTools(server, wallet);
954
+
955
+ // Register prompts if enabled
956
+ if (CONFIG.loadPrompts) {
957
+ registerAllPrompts(server);
958
+ }
959
+
960
+ // Register resources if enabled
961
+ if (CONFIG.loadResources) {
962
+ registerResources(server);
963
+ }
964
+
965
+ // Start the server based on transport mode
966
+ if (CONFIG.transportMode === "stdio") {
967
+ const transport = new StdioServerTransport();
968
+ await server.connect(transport);
969
+ logFunc("BSV MCP Server running on stdio");
970
+ } else {
971
+ const port = CONFIG.port;
972
+ const messageEndpoint = "/messages";
973
+ const activeTransports = new Map<string, BunSSEServerTransport>();
974
+
975
+ // Set up resource URL for OAuth
976
+ const resourceUrl = CONFIG.resourceUrl || `http://localhost:${port}`;
977
+
978
+ // Initialize JWT validator if OAuth is enabled
979
+ const jwtValidator = CONFIG.enableOAuth
980
+ ? createMCPJWTValidator(resourceUrl)
981
+ : null;
982
+
983
+ logFunc(`Starting BSV MCP Server in HTTP/SSE mode on port ${port}...`);
984
+ if (CONFIG.enableOAuth) {
985
+ logFunc(`OAuth 2.1 authentication enabled`);
986
+ logFunc(` Issuer: ${CONFIG.oauthIssuer}`);
987
+ logFunc(` Resource: ${resourceUrl}`);
988
+ }
989
+
990
+ Bun.serve({
991
+ port: port,
992
+ async fetch(req: Request): Promise<Response> {
993
+ const url = new URL(req.url);
994
+
995
+ // Skip auth for discovery endpoints
996
+ const isDiscoveryEndpoint =
997
+ url.pathname === "/.well-known/oauth-protected-resource" ||
998
+ url.pathname === "/.well-known/oauth-authorization-server";
999
+
1000
+ // Validate JWT for protected endpoints (if OAuth enabled)
1001
+ let userContext: BSVJWTPayload | null = null;
1002
+ if (CONFIG.enableOAuth && jwtValidator && !isDiscoveryEndpoint) {
1003
+ try {
1004
+ userContext = await jwtValidator.validateFromRequest(req);
1005
+
1006
+ if (!userContext) {
1007
+ // No token provided - return 401 with WWW-Authenticate
1008
+ return new Response(
1009
+ JSON.stringify({
1010
+ error: "unauthorized",
1011
+ message: "Authentication required",
1012
+ }),
1013
+ {
1014
+ status: 401,
1015
+ headers: {
1016
+ "Content-Type": "application/json",
1017
+ "WWW-Authenticate": generateWWWAuthenticate(
1018
+ resourceUrl,
1019
+ "invalid_token",
1020
+ "Authentication required",
1021
+ ),
1022
+ "Access-Control-Expose-Headers": "WWW-Authenticate",
1023
+ "Access-Control-Allow-Origin": "*",
1024
+ },
1025
+ },
1026
+ );
1027
+ }
1028
+
1029
+ // Token validated successfully
1030
+ logFunc(
1031
+ `Authenticated request from user: ${userContext.sub} (pubkey: ${userContext.pubkey?.substring(0, 20)}...)`,
1032
+ );
1033
+ } catch (error) {
1034
+ // Token provided but invalid
1035
+ const errorMessage =
1036
+ error instanceof Error
1037
+ ? error.message
1038
+ : "Token validation failed";
1039
+
1040
+ logFunc(`JWT validation error: ${errorMessage}`);
1041
+
1042
+ return new Response(
1043
+ JSON.stringify({
1044
+ error: "invalid_token",
1045
+ message: errorMessage,
1046
+ }),
1047
+ {
1048
+ status: 401,
1049
+ headers: {
1050
+ "Content-Type": "application/json",
1051
+ "WWW-Authenticate": generateWWWAuthenticate(
1052
+ resourceUrl,
1053
+ "invalid_token",
1054
+ errorMessage,
1055
+ ),
1056
+ "Access-Control-Expose-Headers": "WWW-Authenticate",
1057
+ "Access-Control-Allow-Origin": "*",
1058
+ },
1059
+ },
1060
+ );
1061
+ }
1062
+ }
1063
+
1064
+ // Handle SSE connection endpoint
1065
+ if (req.headers.get("accept") === "text/event-stream") {
1066
+ const transport = new BunSSEServerTransport(messageEndpoint);
1067
+ activeTransports.set(transport.sessionId, transport);
1068
+ logFunc(`New SSE connection: ${transport.sessionId}`);
1069
+
1070
+ try {
1071
+ // Connect server instance to THIS transport
1072
+ if (!server) throw new Error("Server not initialized");
1073
+ await server.connect(transport);
1074
+ logFunc(
1075
+ `Server connected to transport for session: ${transport.sessionId}`,
1076
+ );
1077
+
1078
+ return await transport.createResponse();
1079
+ } catch (connectError) {
1080
+ logFunc(
1081
+ `Error connecting server to transport ${transport.sessionId}: ${connectError}`,
1082
+ );
1083
+ activeTransports.delete(transport.sessionId); // Clean up if connect fails
1084
+ return new Response("Failed to establish SSE connection", {
1085
+ status: 500,
1086
+ });
1087
+ }
1088
+ }
1089
+
1090
+ // Handle message POST endpoint
1091
+ if (req.method === "POST" && url.pathname === messageEndpoint) {
1092
+ const sessionId = url.searchParams.get("sessionId");
1093
+ if (!sessionId) {
1094
+ return new Response("Missing sessionId query parameter", {
1095
+ status: 400,
1096
+ });
1097
+ }
1098
+ const transport = activeTransports.get(sessionId);
1099
+ if (!transport) {
1100
+ return new Response(`Invalid or expired sessionId: ${sessionId}`, {
1101
+ status: 404,
1102
+ });
1103
+ }
1104
+ // Let the specific transport instance handle the message
1105
+ return await transport.handlePostMessage(req);
1106
+ }
1107
+
1108
+ // Handle OAuth 2.1 Authorization Server Metadata (MCP spec requirement)
1109
+ // MCP clients will request this to discover sigma-auth endpoints
1110
+ if (
1111
+ req.method === "GET" &&
1112
+ url.pathname === "/.well-known/oauth-authorization-server"
1113
+ ) {
1114
+ const authServer =
1115
+ process.env.OAUTH_ISSUER || "https://auth.sigmaidentity.com";
1116
+
1117
+ return new Response(
1118
+ JSON.stringify(
1119
+ {
1120
+ issuer: authServer,
1121
+ authorization_endpoint: `${authServer}/api/oauth/authorize`,
1122
+ token_endpoint: `${authServer}/api/oauth/token`,
1123
+ userinfo_endpoint: `${authServer}/api/oauth/userinfo`,
1124
+ jwks_uri: `${authServer}/.well-known/jwks.json`,
1125
+ registration_endpoint: `${authServer}/api/oauth/register`,
1126
+ scopes_supported: [
1127
+ "openid",
1128
+ "profile",
1129
+ "email",
1130
+ "offline_access",
1131
+ "bsv:tools",
1132
+ "bsv:wallet",
1133
+ "bsv:ordinals",
1134
+ "bsv:tokens",
1135
+ ],
1136
+ response_types_supported: ["code"],
1137
+ grant_types_supported: ["authorization_code", "refresh_token"],
1138
+ token_endpoint_auth_methods_supported: ["none"],
1139
+ code_challenge_methods_supported: ["S256"],
1140
+ },
1141
+ null,
1142
+ 2,
1143
+ ),
1144
+ {
1145
+ status: 200,
1146
+ headers: {
1147
+ "Content-Type": "application/json",
1148
+ "Access-Control-Allow-Origin": "*",
1149
+ "Cache-Control": "public, max-age=3600",
1150
+ },
1151
+ },
1152
+ );
1153
+ }
1154
+
1155
+ // Handle OAuth 2.1 Protected Resource Metadata (RFC 9728)
1156
+ if (
1157
+ req.method === "GET" &&
1158
+ url.pathname === "/.well-known/oauth-protected-resource"
1159
+ ) {
1160
+ const resourceUrl =
1161
+ process.env.RESOURCE_URL || `http://localhost:${port}`;
1162
+ const authServer =
1163
+ process.env.OAUTH_ISSUER || "https://auth.sigmaidentity.com";
1164
+
1165
+ return new Response(
1166
+ JSON.stringify(
1167
+ {
1168
+ resource: resourceUrl,
1169
+ authorization_servers: [authServer],
1170
+ scopes_supported: [
1171
+ "openid",
1172
+ "profile",
1173
+ "email",
1174
+ "bsv:tools",
1175
+ "bsv:wallet",
1176
+ "bsv:ordinals",
1177
+ "bsv:tokens",
1178
+ ],
1179
+ bearer_methods_supported: ["header"],
1180
+ resource_signing_alg_values_supported: ["RS256", "ES256"],
1181
+ },
1182
+ null,
1183
+ 2,
1184
+ ),
1185
+ {
1186
+ status: 200,
1187
+ headers: {
1188
+ "Content-Type": "application/json",
1189
+ "Access-Control-Allow-Origin": "*",
1190
+ "Access-Control-Allow-Methods": "GET",
1191
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
1192
+ "Access-Control-Expose-Headers": "WWW-Authenticate",
1193
+ "Cache-Control": "public, max-age=3600",
1194
+ },
1195
+ },
1196
+ );
1197
+ }
1198
+
1199
+ // Handle CORS preflight for OAuth discovery
1200
+ if (
1201
+ req.method === "OPTIONS" &&
1202
+ (url.pathname === "/.well-known/oauth-protected-resource" ||
1203
+ url.pathname === "/.well-known/oauth-authorization-server")
1204
+ ) {
1205
+ return new Response(null, {
1206
+ status: 204,
1207
+ headers: {
1208
+ "Access-Control-Allow-Origin": "*",
1209
+ "Access-Control-Allow-Methods": "GET",
1210
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
1211
+ },
1212
+ });
1213
+ }
1214
+
1215
+ // Default response for other paths/methods
1216
+ return new Response("Not Found", { status: 404 });
1217
+ },
1218
+ error(error: Error): Response {
1219
+ logFunc(`Bun server error: ${error}\n${error.stack}`);
1220
+ return new Response("Internal Server Error", { status: 500 });
1221
+ },
1222
+ });
1223
+ logFunc(
1224
+ ` Bun server started successfully. Listening on http://localhost:${port} `,
1225
+ );
1226
+ logFunc(" SSE Endpoint: /sse");
1227
+ logFunc(
1228
+ ` Message Endpoint: ${messageEndpoint} (POST with ?sessionId=...)`,
1229
+ );
1230
+ }
128
1231
  }
129
1232
 
130
- // Connect to the transport
131
- const transport = new StdioServerTransport();
132
- await server.connect(transport);
133
- console.error("BSV MCP Server running on stdio");
1233
+ main().catch((error) => {
1234
+ logFunc(`\x1b[31mFATAL: Server initialization failed: ${error}\x1b[0m`);
1235
+ if (error instanceof Error && error.stack) {
1236
+ logFunc(error.stack);
1237
+ }
1238
+ process.exit(1);
1239
+ });
1240
+
1241
+ export { server };