bsv-mcp 0.0.35 → 0.2.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 (71) hide show
  1. package/CHANGELOG.md +112 -1
  2. package/README.md +233 -4
  3. package/dist/index.js +111762 -0
  4. package/index.ts +804 -86
  5. package/next-env.d.ts +6 -0
  6. package/package.json +57 -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/bap/friend.ts +267 -0
  16. package/tools/bap/generate.test.ts +350 -0
  17. package/tools/bap/generate.ts +433 -0
  18. package/tools/bap/getCurrentAddress.ts +76 -0
  19. package/tools/bap/getId.test.ts +121 -0
  20. package/tools/bap/getId.ts +169 -0
  21. package/tools/bap/index.ts +111 -0
  22. package/tools/bap/types.ts +66 -0
  23. package/tools/bap/utils.ts +33 -0
  24. package/tools/bigblocks/components.ts +304 -0
  25. package/tools/bigblocks/docs.ts +488 -0
  26. package/tools/bigblocks/examples.ts +527 -0
  27. package/tools/bigblocks/generator.ts +485 -0
  28. package/tools/bigblocks/index.ts +23 -0
  29. package/tools/bsocial/bigblocksApiClient.ts +189 -0
  30. package/tools/bsocial/bmapFollow.ts +125 -0
  31. package/tools/bsocial/bmapLikes.ts +111 -0
  32. package/tools/bsocial/bmapReadPosts.ts +148 -0
  33. package/tools/bsocial/createPost.ts +136 -0
  34. package/tools/bsocial/index.ts +45 -0
  35. package/tools/bsocial/readPosts.ts +255 -0
  36. package/tools/bsv/decodeTransaction.ts +5 -1
  37. package/tools/constants.ts +21 -0
  38. package/tools/index.ts +103 -17
  39. package/tools/mnee/index.ts +8 -0
  40. package/tools/mnee/parseTx.ts +1 -1
  41. package/tools/mnee/sendMnee.ts +1 -1
  42. package/tools/utils/aip.ts +46 -0
  43. package/tools/utils/conversion.ts +1 -0
  44. package/tools/utils/errorHandler.ts +76 -0
  45. package/tools/utils/index.ts +45 -1
  46. package/tools/utils/installAgentMaster.ts +84 -0
  47. package/tools/utils/logger.ts +20 -0
  48. package/tools/utils/toolConfig.ts +80 -0
  49. package/tools/utils/toolRegistration.ts +66 -0
  50. package/tools/utils/transactionBuilder.ts +196 -0
  51. package/tools/wallet/a2bPublishAgent.ts +8 -4
  52. package/tools/wallet/a2bPublishMcp.ts +28 -24
  53. package/tools/wallet/createOrdinals.ts +4 -2
  54. package/tools/wallet/fetchPaymentUtxos.ts +44 -43
  55. package/tools/wallet/gatherCollectionInfo.ts +374 -0
  56. package/tools/wallet/getAddress.ts +13 -21
  57. package/tools/wallet/getBalance.ts +66 -0
  58. package/tools/wallet/getBalanceDroplet.ts +59 -0
  59. package/tools/wallet/integratedWallet.ts +120 -0
  60. package/tools/wallet/mintCollection.ts +378 -0
  61. package/tools/wallet/purchaseListing.ts +4 -4
  62. package/tools/wallet/schemas.ts +18 -249
  63. package/tools/wallet/sendOrdinals.ts +4 -2
  64. package/tools/wallet/sendToAddress.ts +5 -1
  65. package/tools/wallet/setupDroplet.ts +172 -0
  66. package/tools/wallet/tools.ts +71 -156
  67. package/tools/wallet/transactionHelper.ts +121 -0
  68. package/tools/wallet/transferOrdToken.ts +7 -4
  69. package/tools/wallet/utxo.ts +62 -0
  70. package/tools/wallet/wallet.ts +237 -234
  71. package/tools/a2b/call.ts +0 -32
package/index.ts CHANGED
@@ -1,13 +1,27 @@
1
1
  #!/usr/bin/env bun
2
+ import os from "node:os";
3
+ import path from "node:path";
2
4
  import { PrivateKey } from "@bsv/sdk";
3
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import packageJson from "./package.json";
5
8
  import { registerAllPrompts } from "./prompts/index.ts";
6
9
  import { registerResources } from "./resources/resources.ts";
7
- import { registerAllTools } from "./tools/index.ts";
10
+ import { registerAllTools, type ToolsConfig } from "./tools/index.ts";
8
11
  import { registerMneeTools } from "./tools/mnee/index.ts";
9
- import { registerWalletTools } from "./tools/wallet/tools.ts";
12
+ import { IntegratedWallet } from "./tools/wallet/integratedWallet.ts";
10
13
  import { Wallet } from "./tools/wallet/wallet.ts";
14
+ import { BunSSEServerTransport } from "./transports/sse.ts";
15
+ import {
16
+ type BSVJWTPayload,
17
+ createMCPJWTValidator,
18
+ generateWWWAuthenticate,
19
+ } from "./utils/jwtValidator.ts";
20
+ import { SecureKeyManager } from "./utils/keyManager.ts";
21
+ import { setServerInstance } from "./utils/passphrasePrompt.ts";
22
+
23
+ // Initialize server variable
24
+ let server: McpServer | undefined;
11
25
 
12
26
  /**
13
27
  * Configuration options from environment variables
@@ -18,119 +32,823 @@ const CONFIG = {
18
32
  loadResources: process.env.DISABLE_RESOURCES !== "true",
19
33
  loadTools: process.env.DISABLE_TOOLS !== "true",
20
34
 
21
- // Fine-grained tool category control
35
+ // Fine-grained tool category control (dependent on key availability)
22
36
  loadWalletTools: process.env.DISABLE_WALLET_TOOLS !== "true",
23
37
  loadMneeTools: process.env.DISABLE_MNEE_TOOLS !== "true",
24
38
  loadBsvTools: process.env.DISABLE_BSV_TOOLS !== "true",
25
39
  loadOrdinalsTools: process.env.DISABLE_ORDINALS_TOOLS !== "true",
26
40
  loadUtilsTools: process.env.DISABLE_UTILS_TOOLS !== "true",
27
41
  loadA2bTools: process.env.ENABLE_A2B_TOOLS === "true",
42
+ loadBapTools: process.env.DISABLE_BAP_TOOLS !== "true",
43
+ loadBsocialTools: process.env.DISABLE_BSOCIAL_TOOLS !== "true",
44
+ loadBigBlocksTools: process.env.DISABLE_BIGBLOCKS_TOOLS !== "true",
28
45
 
29
46
  // Transaction broadcasting control
30
47
  disableBroadcasting: process.env.DISABLE_BROADCASTING === "true",
48
+
49
+ // --- Transport Mode ---
50
+ transportMode: process.env.TRANSPORT?.toLowerCase() || "http", // 'stdio' or 'http'/default
51
+ port: Number.parseInt(process.env.PORT || "3000", 10),
52
+
53
+ // --- Droplet API Configuration ---
54
+ useDropletApi: process.env.USE_DROPLET_API === "true",
55
+ dropletApiUrl: process.env.DROPLET_API_URL || "http://127.0.0.1:4000",
56
+ dropletFaucetName: process.env.DROPLET_FAUCET_NAME || "",
57
+
58
+ // --- OAuth Configuration ---
59
+ enableOAuth: process.env.ENABLE_OAUTH !== "false", // Enabled by default
60
+ oauthIssuer: process.env.OAUTH_ISSUER || "https://auth.sigmaidentity.com",
61
+ resourceUrl: process.env.RESOURCE_URL || "", // Will be set based on port
31
62
  };
32
63
 
64
+ const logFunc = console.error;
65
+ const KEY_DIR = path.join(os.homedir(), ".bsv-mcp");
66
+ const KEY_FILE_PATH = path.join(KEY_DIR, "keys.json");
67
+
33
68
  /**
34
- * Try to initialize the private key from environment variables
35
- * Returns the private key if valid, or undefined if not present or invalid
69
+ * Initializes payment and identity private keys using SecureKeyManager.
70
+ * Priorities:
71
+ * 1. Valid PRIVATE_KEY_WIF environment variable (provides payPk, identityPk remains undefined).
72
+ * 2. Valid keys found in encrypted ~/.bsv-mcp/keys.bep or legacy ~/.bsv-mcp/keys.json file.
73
+ * 3. Generate only payPk, save it to the file, and log warnings. identityPk/xprv only created via bap_generate.
74
+ * Returns the keys and their source ('env', 'file', 'encrypted', 'generated', or 'none').
36
75
  */
37
- function initializePrivateKey(): PrivateKey | undefined {
38
- const privateKeyWif = process.env.PRIVATE_KEY_WIF;
76
+ async function initializeKeys(): Promise<{
77
+ payPk?: PrivateKey;
78
+ identityPk?: PrivateKey;
79
+ xprv?: string;
80
+ source: "env" | "file" | "encrypted" | "generated" | "none";
81
+ }> {
82
+ const keyManager = new SecureKeyManager({ keyDir: KEY_DIR });
39
83
 
40
- // Check if private key is set
41
- if (!privateKeyWif) {
42
- console.warn(
43
- "\x1b[33mWarning: PRIVATE_KEY_WIF environment variable is not set\x1b[0m",
44
- );
45
- console.warn(
46
- "The server will run, but wallet operations requiring a private key will return errors.",
47
- );
48
- console.warn(
49
- "Set this variable with a valid Bitcoin SV private key in WIF format to enable all features:",
50
- );
51
- console.warn(
52
- "Example: PRIVATE_KEY_WIF=your_private_key_wif bun run index.ts",
53
- );
54
- return undefined;
84
+ const privateKeyWifEnv = process.env.PRIVATE_KEY_WIF;
85
+
86
+ // 1. Try environment variable for payPk
87
+ if (privateKeyWifEnv) {
88
+ try {
89
+ const payPk = PrivateKey.fromWif(privateKeyWifEnv);
90
+ console.error(
91
+ "\x1b[32mINFO: Using valid PRIVATE_KEY_WIF from environment for payment key.\x1b[0m",
92
+ );
93
+
94
+ // Also check for encrypted/legacy keys to get xprv if available
95
+ let xprvFromFile: string | undefined;
96
+ try {
97
+ const { keys } = await keyManager.loadKeys();
98
+ xprvFromFile = keys.xprv;
99
+ if (xprvFromFile) {
100
+ console.error(
101
+ "\x1b[32mINFO: Loaded BAP HD Master Key (xprv) from secure storage alongside ENV payPk.\x1b[0m",
102
+ );
103
+ }
104
+ } catch (_e) {
105
+ /* Ignore file read errors here */
106
+ }
107
+
108
+ if (!xprvFromFile) {
109
+ console.error(
110
+ "\x1b[33mWARN: No identity key (identityPk or xprv) found. BAP/A2B tools needing identity will be limited.\x1b[0m",
111
+ );
112
+ }
113
+
114
+ return {
115
+ payPk,
116
+ identityPk: undefined,
117
+ xprv: xprvFromFile,
118
+ source: "env",
119
+ };
120
+ } catch (_error) {
121
+ console.error(
122
+ "\x1b[33mWARN: Invalid PRIVATE_KEY_WIF format in environment variable. Checking secure storage next.\x1b[0m",
123
+ );
124
+ }
55
125
  }
56
126
 
57
- // Validate the private key format
127
+ // 2. Try reading from secure storage (encrypted or legacy)
58
128
  try {
59
- return PrivateKey.fromWif(privateKeyWif);
129
+ const { keys, source } = await keyManager.loadKeys();
130
+
131
+ if (keys.payPk) {
132
+ if (source === "encrypted") {
133
+ console.error(
134
+ `\x1b[32mINFO: Using encrypted keys from: ${KEY_DIR}/keys.bep\x1b[0m`,
135
+ );
136
+ } else if (source === "legacy") {
137
+ console.error(
138
+ `\x1b[32mINFO: Using legacy keys from: ${KEY_FILE_PATH}\x1b[0m`,
139
+ );
140
+ console.error(
141
+ "\x1b[33mWARN: Using unencrypted keys. Run the server again to encrypt them.\x1b[0m",
142
+ );
143
+ }
144
+
145
+ if (keys.identityPk) {
146
+ console.error(
147
+ "\x1b[32mINFO: Loaded Identity Key (identityPk) from secure storage.\x1b[0m",
148
+ );
149
+ }
150
+ if (keys.xprv) {
151
+ console.error(
152
+ "\x1b[32mINFO: Loaded BAP HD Master Key (xprv) from secure storage.\x1b[0m",
153
+ );
154
+ }
155
+
156
+ return {
157
+ payPk: keys.payPk,
158
+ identityPk: keys.identityPk,
159
+ xprv: keys.xprv,
160
+ source: source === "encrypted" ? "encrypted" : "file",
161
+ };
162
+ }
163
+
164
+ console.error("\x1b[33mINFO: No keys found in secure storage.\x1b[0m");
60
165
  } catch (error) {
61
- console.warn("\x1b[33mWarning: Invalid private key format\x1b[0m");
62
- console.warn(
63
- "The PRIVATE_KEY_WIF provided is not a valid Bitcoin SV private key in WIF format.",
166
+ console.error(
167
+ `\x1b[33mWARN: Error reading keys from secure storage: ${error}\x1b[0m`,
168
+ );
169
+ }
170
+
171
+ // 3. Generate new payment key (but not identityPk/xprv)
172
+ console.error("\x1b[33mINFO: Generating new payment key ONLY.\x1b[0m");
173
+ const payPk = PrivateKey.fromRandom();
174
+ const keyStore = {
175
+ payPk,
176
+ identityPk: undefined,
177
+ xprv: undefined,
178
+ };
179
+
180
+ try {
181
+ // Save unencrypted for now - user can encrypt on next run
182
+ await keyManager.saveKeys(keyStore, { forceUnencrypted: true });
183
+ const status = keyManager.getStatus();
184
+
185
+ if (status.hasEncrypted) {
186
+ console.error(
187
+ `\x1b[33mWARN: Saved newly generated payment key (encrypted) to: ${KEY_DIR}/keys.bep\x1b[0m`,
188
+ );
189
+ } else {
190
+ console.error(
191
+ `\x1b[33mWARN: Saved newly generated payment key (unencrypted) to: ${KEY_FILE_PATH}\x1b[0m`,
192
+ );
193
+ console.error(
194
+ "\x1b[33mWARN: Run the server again to encrypt your keys.\x1b[0m",
195
+ );
196
+ }
197
+ console.error(
198
+ "\x1b[33mWARN: Identity key (identityPk/xprv) not found or generated. Use 'bap_generate' tool to create one.\x1b[0m",
199
+ );
200
+ } catch (writeError) {
201
+ console.error(
202
+ `\x1b[31mERROR: Failed to save newly generated payment key: ${writeError}\x1b[0m`,
64
203
  );
65
- console.warn(
66
- "The server will run, but wallet operations requiring a private key will return errors.",
204
+ console.error(
205
+ "\x1b[31mERROR: Using the generated payment key for this session only. No identity key available.\x1b[0m",
67
206
  );
68
- return undefined;
69
207
  }
208
+
209
+ return { payPk, identityPk: undefined, xprv: undefined, source: "generated" };
70
210
  }
71
211
 
72
- // Try to initialize private key but don't stop the server if missing or invalid
73
- const privKey = initializePrivateKey();
74
-
75
- const server = new McpServer(
76
- { name: "Bitcoin SV", version: "0.0.35" },
77
- // {
78
- // // Advertise only what you actually implement
79
- // capabilities: {
80
- // completions: {},
81
- // experimental: {},
82
- // logging: {},
83
- // prompts: {},
84
- // resources: {},
85
- // tools: {},
86
- // },
87
- // // Optional instructions banner for clients
88
- // instructions: `
89
- // This server exposes Bitcoin SV helpers.
90
- // Tools are idempotent unless marked destructive.
91
- // `,
92
- // },
93
- );
94
-
95
- // Initialize wallet and register tools based on configuration
96
- let wallet: Wallet | null = null;
97
-
98
- if (CONFIG.loadTools) {
99
- if (privKey) {
100
- // Register MNEE tools if enabled
101
- if (CONFIG.loadMneeTools) {
102
- registerMneeTools(server);
212
+ // --- Main Server Setup ---
213
+ async function main() {
214
+ // Check for help or info commands that don't need authentication
215
+ const args = process.argv.slice(2);
216
+ if (args.includes("--help") || args.includes("-h") || args.includes("help")) {
217
+ console.log(`
218
+ BSV MCP Server v${packageJson.version}
219
+
220
+ Usage: bun run index.ts [options]
221
+
222
+ Options:
223
+ --help, -h Show this help message
224
+ --version, -v Show version information
225
+
226
+ Environment Variables:
227
+ TRANSPORT Transport mode: 'stdio' or 'http' (default: http)
228
+ PORT HTTP server port (default: 3000)
229
+ PRIVATE_KEY_WIF Payment private key in WIF format
230
+ DISABLE_TOOLS Disable all tools (default: false)
231
+ DISABLE_WALLET_TOOLS Disable wallet tools (default: false)
232
+ DISABLE_BSV_TOOLS Disable BSV tools (default: false)
233
+ DISABLE_ORDINALS_TOOLS Disable ordinals tools (default: false)
234
+ DISABLE_UTILS_TOOLS Disable utility tools (default: false)
235
+ DISABLE_BAP_TOOLS Disable BAP tools (default: false)
236
+ DISABLE_BSOCIAL_TOOLS Disable BSocial tools (default: false)
237
+ DISABLE_BIGBLOCKS_TOOLS Disable BigBlocks tools (default: false)
238
+ ENABLE_A2B_TOOLS Enable A2B tools (default: false)
239
+ DISABLE_BROADCASTING Disable transaction broadcasting (default: false)
240
+ USE_DROPLET_API Use Droplet API for transactions (default: false)
241
+
242
+ Tool Categories:
243
+ BSV Tools: Price lookup, transaction decoding, validation
244
+ Wallet Tools: Send payments, manage UTXOs (requires payment key)
245
+ Ordinals Tools: Search listings, market data
246
+ Utils Tools: General utilities, conversions
247
+ BAP Tools: Identity management (requires identity key)
248
+ BSocial Tools: Social posts, likes, follows
249
+ BigBlocks Tools: React component registry, code generation, examples
250
+ A2B Tools: Advanced BSV operations (requires identity key)
251
+
252
+ Authentication:
253
+ - Most tools work without authentication
254
+ - Wallet operations require PRIVATE_KEY_WIF or generated keys
255
+ - BAP/A2B tools require identity keys (generated via bap_generate tool)
256
+ `);
257
+ process.exit(0);
258
+ }
259
+
260
+ if (args.includes("--version") || args.includes("-v")) {
261
+ console.log(`${packageJson.name} v${packageJson.version}`);
262
+ process.exit(0);
263
+ }
264
+
265
+ // --- Initialize Keys ---
266
+ const { payPk, identityPk, xprv, source: keySource } = await initializeKeys();
267
+
268
+ // Define persistence based on source
269
+ const hasPersistentPayKey =
270
+ keySource === "env" || keySource === "file" || keySource === "encrypted";
271
+ const hasPersistentIdentityKey =
272
+ !!identityPk && (keySource === "file" || keySource === "encrypted");
273
+ const hasXprv =
274
+ !!xprv &&
275
+ (keySource === "file" || keySource === "env" || keySource === "encrypted");
276
+
277
+ const effectiveConfig = { ...CONFIG };
278
+
279
+ // --- Configuration Logging ---
280
+ logFunc("\n--- BSV MCP Server Configuration ---");
281
+ logFunc(`Server Version: ${packageJson.version}`);
282
+ logFunc(`Server Name: ${packageJson.name}`);
283
+ logFunc("\nEnvironment Variables:");
284
+ logFunc(
285
+ ` TRANSPORT: ${process.env.TRANSPORT || "Not Set (http default)"}`,
286
+ );
287
+ if (CONFIG.transportMode === "http") {
288
+ logFunc(
289
+ ` PORT: ${process.env.PORT || "Not Set (3000 default)"}`,
290
+ );
291
+ }
292
+ logFunc(
293
+ ` PRIVATE_KEY_WIF: ${process.env.PRIVATE_KEY_WIF ? "Set (using env key)" : "Not Set (using file/generating)"}`,
294
+ );
295
+ logFunc(
296
+ ` IDENTITY_KEY_WIF: ${process.env.IDENTITY_KEY_WIF ? "Set (using env key)" : "Not Set (using file/generating)"}`,
297
+ );
298
+ if (process.env.BSV_MCP_PASSPHRASE) {
299
+ logFunc(" BSV_MCP_PASSPHRASE: \x1b[31mDEPRECATED - Remove this!\x1b[0m");
300
+ }
301
+ logFunc(
302
+ ` DISABLE_PROMPTS: ${process.env.DISABLE_PROMPTS === "true" ? "Set (true)" : "Not Set/false"}`,
303
+ );
304
+ logFunc(
305
+ ` DISABLE_RESOURCES: ${process.env.DISABLE_RESOURCES === "true" ? "Set (true)" : "Not Set/false"}`,
306
+ );
307
+ logFunc(
308
+ ` DISABLE_TOOLS: ${process.env.DISABLE_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
309
+ );
310
+ logFunc(
311
+ ` DISABLE_WALLET_TOOLS: ${process.env.DISABLE_WALLET_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
312
+ );
313
+ logFunc(
314
+ ` DISABLE_MNEE_TOOLS: ${process.env.DISABLE_MNEE_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
315
+ );
316
+ logFunc(
317
+ ` DISABLE_BSV_TOOLS: ${process.env.DISABLE_BSV_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
318
+ );
319
+ logFunc(
320
+ ` DISABLE_ORDINALS_TOOLS: ${process.env.DISABLE_ORDINALS_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
321
+ );
322
+ logFunc(
323
+ ` DISABLE_UTILS_TOOLS: ${process.env.DISABLE_UTILS_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
324
+ );
325
+ logFunc(
326
+ ` ENABLE_A2B_TOOLS: ${process.env.ENABLE_A2B_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
327
+ );
328
+ logFunc(
329
+ ` DISABLE_BAP_TOOLS: ${process.env.DISABLE_BAP_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
330
+ );
331
+ logFunc(
332
+ ` DISABLE_BIGBLOCKS_TOOLS: ${process.env.DISABLE_BIGBLOCKS_TOOLS === "true" ? "Set (true)" : "Not Set/false"}`,
333
+ );
334
+ logFunc(
335
+ ` DISABLE_BROADCASTING: ${process.env.DISABLE_BROADCASTING === "true" ? "Set (true)" : "Not Set/false"}`,
336
+ );
337
+ logFunc(
338
+ ` USE_DROPLET_API: ${CONFIG.useDropletApi ? "Set (true)" : "Not Set/false"}`,
339
+ );
340
+ if (CONFIG.useDropletApi) {
341
+ logFunc(` DROPLET_API_URL: ${CONFIG.dropletApiUrl}`);
342
+ logFunc(` DROPLET_FAUCET_NAME: ${CONFIG.dropletFaucetName || "Not Set"}`);
343
+ }
344
+
345
+ logFunc("\nKey Source:");
346
+ let payKeySourceInfo = `Payment Key (payPk): ${keySource}`;
347
+ if (keySource === "env") payKeySourceInfo = "Payment Key (payPk): env";
348
+ if (keySource === "file")
349
+ payKeySourceInfo += ` (Loaded from ${KEY_FILE_PATH})`;
350
+ if (keySource === "encrypted")
351
+ payKeySourceInfo += ` (Loaded from ${KEY_DIR}/keys.bep)`;
352
+ if (keySource === "generated") payKeySourceInfo += " (Generated & saved)";
353
+
354
+ let identityKeySourceInfo = "Identity Key (identityPk): Not Loaded";
355
+ if (identityPk && (keySource === "file" || keySource === "encrypted")) {
356
+ identityKeySourceInfo = `Identity Key (identityPk): ${keySource} (Loaded from secure storage)`;
357
+ } else if (identityPk) {
358
+ identityKeySourceInfo = `Identity Key (identityPk): ${keySource} (Unexpected Source)`;
359
+ }
360
+
361
+ let xprvSourceInfo = "BAP Master Key (xprv): Not found";
362
+ if (hasXprv) {
363
+ xprvSourceInfo = "BAP Master Key (xprv): Loaded from secure storage";
364
+ }
365
+
366
+ logFunc(` ${payKeySourceInfo}`);
367
+ logFunc(` ${identityKeySourceInfo}`);
368
+ logFunc(` ${xprvSourceInfo}`);
369
+
370
+ logFunc("\nEffective Component Status:");
371
+ logFunc(` Transport Mode: ${CONFIG.transportMode.toUpperCase()}`);
372
+ logFunc(
373
+ ` Prompts: ${effectiveConfig.loadPrompts ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
374
+ );
375
+ logFunc(
376
+ ` Resources: ${effectiveConfig.loadResources ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
377
+ );
378
+ logFunc(
379
+ ` Tools (Overall): ${effectiveConfig.loadTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
380
+ );
381
+ if (effectiveConfig.loadTools) {
382
+ const walletStatus = effectiveConfig.loadWalletTools
383
+ ? "\x1b[32mEnabled\x1b[0m"
384
+ : "\x1b[31mDisabled\x1b[0m";
385
+ const mneeStatus = effectiveConfig.loadMneeTools
386
+ ? "\x1b[32mEnabled\x1b[0m"
387
+ : "\x1b[31mDisabled\x1b[0m";
388
+ const a2bStatus = effectiveConfig.loadA2bTools
389
+ ? "\x1b[32mEnabled\x1b[0m"
390
+ : "\x1b[31mDisabled\x1b[0m";
391
+ const bapStatus = effectiveConfig.loadBapTools
392
+ ? "\x1b[32mEnabled\x1b[0m"
393
+ : "\x1b[31mDisabled\x1b[0m";
394
+
395
+ let payKeyNote = "";
396
+ if (!hasPersistentPayKey) {
397
+ payKeyNote = " \x1b[33m(Using generated payPk)\x1b[0m";
398
+ }
399
+ let identityKeyNote = "";
400
+ if (!hasPersistentIdentityKey) {
401
+ identityKeyNote = " \x1b[33m(Using generated identityPk)\x1b[0m";
103
402
  }
104
403
 
105
- // Initialize wallet with the private key if wallet tools are enabled
106
- if (CONFIG.loadWalletTools) {
107
- wallet = new Wallet(privKey);
108
- registerWalletTools(server, wallet, {
109
- disableBroadcasting: CONFIG.disableBroadcasting,
110
- enableA2bTools: CONFIG.loadA2bTools,
111
- });
404
+ logFunc(` Wallet: ${walletStatus}${payKeyNote}`);
405
+ logFunc(` MNEE: ${mneeStatus}${payKeyNote}`);
406
+ logFunc(
407
+ ` BSV: ${effectiveConfig.loadBsvTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
408
+ );
409
+ logFunc(
410
+ ` Ordinals: ${effectiveConfig.loadOrdinalsTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
411
+ );
412
+ logFunc(
413
+ ` Utils: ${effectiveConfig.loadUtilsTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
414
+ );
415
+ logFunc(` A2B: ${a2bStatus}${identityKeyNote}`);
416
+ logFunc(` BAP: ${bapStatus}${identityKeyNote}`);
417
+ logFunc(
418
+ ` BSocial: ${effectiveConfig.loadBsocialTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
419
+ );
420
+ logFunc(
421
+ ` BigBlocks: ${effectiveConfig.loadBigBlocksTools ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
422
+ );
423
+
424
+ if (effectiveConfig.loadWalletTools) {
425
+ logFunc(
426
+ ` Broadcasting: ${!effectiveConfig.disableBroadcasting ? "\x1b[32mEnabled\x1b[0m" : "\x1b[31mDisabled\x1b[0m"}`,
427
+ );
112
428
  }
113
429
  }
430
+ logFunc("------------------------------------\n");
431
+ // --- End of Logging Block ---
114
432
 
115
- // Register all other tools based on configuration
116
- registerAllTools(server, {
117
- enableBsvTools: CONFIG.loadBsvTools,
118
- enableOrdinalsTools: CONFIG.loadOrdinalsTools,
119
- enableUtilsTools: CONFIG.loadUtilsTools,
120
- enableA2bTools: CONFIG.loadA2bTools,
121
- });
122
- }
433
+ server = new McpServer(
434
+ { name: packageJson.name, version: packageJson.version },
435
+ {
436
+ capabilities: {
437
+ prompts: {},
438
+ resources: {},
439
+ tools: {},
440
+ },
441
+ instructions: `
442
+ This server exposes Bitcoin SV helpers.
443
+ Tools are idempotent unless marked destructive.
444
+ `,
445
+ },
446
+ );
123
447
 
124
- // Register resources if enabled
125
- if (CONFIG.loadResources) {
126
- registerResources(server);
127
- }
448
+ // Set server instance for transport detection
449
+ setServerInstance(server);
450
+
451
+ let wallet: Wallet | undefined;
452
+ let integratedWallet: IntegratedWallet | undefined;
453
+
454
+ if (CONFIG.loadTools) {
455
+ // Check if we should use Droplet API mode
456
+ if (CONFIG.useDropletApi && CONFIG.dropletFaucetName) {
457
+ // Initialize IntegratedWallet in Droplet mode
458
+ if (CONFIG.loadWalletTools) {
459
+ try {
460
+ integratedWallet = new IntegratedWallet({
461
+ useDropletApi: true,
462
+ dropletConfig: {
463
+ apiUrl: CONFIG.dropletApiUrl,
464
+ faucetName: CONFIG.dropletFaucetName,
465
+ // Note: authKey will be set by IntegratedWallet if paymentKey is provided
466
+ },
467
+ paymentKey: payPk, // Pass payment key for auth
468
+ identityKey: identityPk,
469
+ });
470
+ logFunc(
471
+ `\x1b[32mINFO: Droplet API mode initialized successfully (Faucet: ${CONFIG.dropletFaucetName}).\x1b[0m`,
472
+ );
473
+ logFunc(
474
+ `\x1b[33mNOTE: Using Droplet API at ${CONFIG.dropletApiUrl}\x1b[0m`,
475
+ );
476
+ logFunc(
477
+ "\x1b[33mNOTE: Local keys are ignored in Droplet API mode\x1b[0m",
478
+ );
479
+ // Create a compatibility wrapper for existing code
480
+ wallet = integratedWallet.getLocalWallet(); // This will be undefined in Droplet mode
481
+
482
+ // In Droplet mode, we need to disable tools that require local keys
483
+ effectiveConfig.loadMneeTools = false; // MNEE requires local wallet
484
+ effectiveConfig.loadBapTools = false; // BAP requires identity key
485
+ effectiveConfig.loadA2bTools = false; // A2B requires identity key
486
+ effectiveConfig.loadBsocialTools = false; // BSocial requires local wallet
487
+ } catch (e) {
488
+ logFunc(
489
+ `\x1b[31mERROR: Failed to initialize Droplet API mode: ${e instanceof Error ? e.message : String(e)}. Wallet-dependent tools will be unavailable.\x1b[0m`,
490
+ );
491
+ integratedWallet = undefined;
492
+ effectiveConfig.loadWalletTools = false;
493
+ effectiveConfig.loadMneeTools = false;
494
+ effectiveConfig.loadBapTools = false;
495
+ }
496
+ }
497
+ } else if (payPk) {
498
+ // Initialize wallet with the payPk if wallet tools are enabled
499
+ if (CONFIG.loadWalletTools) {
500
+ try {
501
+ wallet = new Wallet(payPk, identityPk);
502
+ integratedWallet = new IntegratedWallet({
503
+ paymentKey: payPk,
504
+ identityKey: identityPk,
505
+ });
506
+ logFunc(
507
+ "\x1b[32mINFO: Custom Wallet initialized successfully.\x1b[0m",
508
+ );
509
+ if (!hasPersistentPayKey) {
510
+ logFunc(
511
+ "\x1b[33mWARN: Wallet is using a generated payment key for this session only. It will not persist.\x1b[0m",
512
+ );
513
+ }
514
+ if (identityPk && !hasPersistentIdentityKey && !hasXprv) {
515
+ logFunc(
516
+ "\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",
517
+ );
518
+ }
519
+ } catch (e) {
520
+ logFunc(
521
+ `\x1b[31mERROR: Failed to initialize custom wallet: ${e instanceof Error ? e.message : String(e)}. Wallet-dependent tools will be unavailable.\x1b[0m`,
522
+ );
523
+ wallet = undefined;
524
+ integratedWallet = undefined;
525
+ effectiveConfig.loadWalletTools = false;
526
+ effectiveConfig.loadMneeTools = false;
527
+ effectiveConfig.loadBapTools = false;
528
+ }
529
+ }
530
+
531
+ // Register MNEE tools if enabled and wallet is available
532
+ if (effectiveConfig.loadMneeTools && wallet) {
533
+ registerMneeTools(server);
534
+ } else if (
535
+ effectiveConfig.loadMneeTools &&
536
+ !wallet &&
537
+ CONFIG.loadWalletTools
538
+ ) {
539
+ logFunc(
540
+ "\x1b[33mWARN: MNEE tools require a wallet but wallet initialization failed. MNEE tools disabled.\x1b[0m",
541
+ );
542
+ effectiveConfig.loadMneeTools = false;
543
+ }
544
+ }
545
+
546
+ // Register all other tools based on configuration
547
+ const toolsConfig: ToolsConfig = {
548
+ enableBsvTools: effectiveConfig.loadBsvTools,
549
+ enableOrdinalsTools: effectiveConfig.loadOrdinalsTools,
550
+ enableUtilsTools: effectiveConfig.loadUtilsTools,
551
+ enableA2bTools: effectiveConfig.loadA2bTools,
552
+ enableBapTools: effectiveConfig.loadBapTools,
553
+ enableBsocialTools: effectiveConfig.loadBsocialTools,
554
+ enableBigBlocksTools: effectiveConfig.loadBigBlocksTools,
555
+ enableWalletTools: effectiveConfig.loadWalletTools,
556
+ enableMneeTools: effectiveConfig.loadMneeTools,
557
+ identityPk,
558
+ payPk,
559
+ xprv,
560
+ wallet,
561
+ integratedWallet,
562
+ disableBroadcasting: effectiveConfig.disableBroadcasting,
563
+ };
564
+
565
+ registerAllTools(server, toolsConfig);
566
+ }
567
+
568
+ // Register prompts if enabled
569
+ if (CONFIG.loadPrompts) {
570
+ registerAllPrompts(server);
571
+ }
572
+
573
+ // Register resources if enabled
574
+ if (CONFIG.loadResources) {
575
+ registerResources(server);
576
+ }
577
+
578
+ // Start the server based on transport mode
579
+ if (CONFIG.transportMode === "stdio") {
580
+ const transport = new StdioServerTransport();
581
+ await server.connect(transport);
582
+ logFunc("BSV MCP Server running on stdio");
583
+ } else {
584
+ const port = CONFIG.port;
585
+ const messageEndpoint = "/messages";
586
+ const activeTransports = new Map<string, BunSSEServerTransport>();
587
+
588
+ // Set up resource URL for OAuth
589
+ const resourceUrl = CONFIG.resourceUrl || `http://localhost:${port}`;
128
590
 
129
- // Register prompts if enabled
130
- if (CONFIG.loadPrompts) {
131
- registerAllPrompts(server);
591
+ // Initialize JWT validator if OAuth is enabled
592
+ const jwtValidator = CONFIG.enableOAuth
593
+ ? createMCPJWTValidator(resourceUrl)
594
+ : null;
595
+
596
+ logFunc(`Starting BSV MCP Server in HTTP/SSE mode on port ${port}...`);
597
+ if (CONFIG.enableOAuth) {
598
+ logFunc(`OAuth 2.1 authentication enabled`);
599
+ logFunc(` Issuer: ${CONFIG.oauthIssuer}`);
600
+ logFunc(` Resource: ${resourceUrl}`);
601
+ }
602
+
603
+ Bun.serve({
604
+ port: port,
605
+ async fetch(req: Request): Promise<Response> {
606
+ const url = new URL(req.url);
607
+
608
+ // Skip auth for discovery endpoints
609
+ const isDiscoveryEndpoint =
610
+ url.pathname === "/.well-known/oauth-protected-resource" ||
611
+ url.pathname === "/.well-known/oauth-authorization-server";
612
+
613
+ // Validate JWT for protected endpoints (if OAuth enabled)
614
+ let userContext: BSVJWTPayload | null = null;
615
+ if (CONFIG.enableOAuth && jwtValidator && !isDiscoveryEndpoint) {
616
+ try {
617
+ userContext = await jwtValidator.validateFromRequest(req);
618
+
619
+ if (!userContext) {
620
+ // No token provided - return 401 with WWW-Authenticate
621
+ return new Response(
622
+ JSON.stringify({
623
+ error: "unauthorized",
624
+ message: "Authentication required",
625
+ }),
626
+ {
627
+ status: 401,
628
+ headers: {
629
+ "Content-Type": "application/json",
630
+ "WWW-Authenticate": generateWWWAuthenticate(
631
+ resourceUrl,
632
+ "invalid_token",
633
+ "Authentication required",
634
+ ),
635
+ "Access-Control-Expose-Headers": "WWW-Authenticate",
636
+ "Access-Control-Allow-Origin": "*",
637
+ },
638
+ },
639
+ );
640
+ }
641
+
642
+ // Token validated successfully
643
+ logFunc(
644
+ `Authenticated request from user: ${userContext.sub} (pubkey: ${userContext.pubkey?.substring(0, 20)}...)`,
645
+ );
646
+ } catch (error) {
647
+ // Token provided but invalid
648
+ const errorMessage =
649
+ error instanceof Error
650
+ ? error.message
651
+ : "Token validation failed";
652
+
653
+ logFunc(`JWT validation error: ${errorMessage}`);
654
+
655
+ return new Response(
656
+ JSON.stringify({
657
+ error: "invalid_token",
658
+ message: errorMessage,
659
+ }),
660
+ {
661
+ status: 401,
662
+ headers: {
663
+ "Content-Type": "application/json",
664
+ "WWW-Authenticate": generateWWWAuthenticate(
665
+ resourceUrl,
666
+ "invalid_token",
667
+ errorMessage,
668
+ ),
669
+ "Access-Control-Expose-Headers": "WWW-Authenticate",
670
+ "Access-Control-Allow-Origin": "*",
671
+ },
672
+ },
673
+ );
674
+ }
675
+ }
676
+
677
+ // Handle SSE connection endpoint
678
+ if (req.headers.get("accept") === "text/event-stream") {
679
+ const transport = new BunSSEServerTransport(messageEndpoint);
680
+ activeTransports.set(transport.sessionId, transport);
681
+ logFunc(`New SSE connection: ${transport.sessionId}`);
682
+
683
+ try {
684
+ // Connect server instance to THIS transport
685
+ if (!server) throw new Error("Server not initialized");
686
+ await server.connect(transport);
687
+ logFunc(
688
+ `Server connected to transport for session: ${transport.sessionId}`,
689
+ );
690
+
691
+ return await transport.createResponse();
692
+ } catch (connectError) {
693
+ logFunc(
694
+ `Error connecting server to transport ${transport.sessionId}: ${connectError}`,
695
+ );
696
+ activeTransports.delete(transport.sessionId); // Clean up if connect fails
697
+ return new Response("Failed to establish SSE connection", {
698
+ status: 500,
699
+ });
700
+ }
701
+ }
702
+
703
+ // Handle message POST endpoint
704
+ if (req.method === "POST" && url.pathname === messageEndpoint) {
705
+ const sessionId = url.searchParams.get("sessionId");
706
+ if (!sessionId) {
707
+ return new Response("Missing sessionId query parameter", {
708
+ status: 400,
709
+ });
710
+ }
711
+ const transport = activeTransports.get(sessionId);
712
+ if (!transport) {
713
+ return new Response(`Invalid or expired sessionId: ${sessionId}`, {
714
+ status: 404,
715
+ });
716
+ }
717
+ // Let the specific transport instance handle the message
718
+ return await transport.handlePostMessage(req);
719
+ }
720
+
721
+ // Handle OAuth 2.1 Authorization Server Metadata (MCP spec requirement)
722
+ // MCP clients will request this to discover sigma-auth endpoints
723
+ if (
724
+ req.method === "GET" &&
725
+ url.pathname === "/.well-known/oauth-authorization-server"
726
+ ) {
727
+ const authServer =
728
+ process.env.OAUTH_ISSUER || "https://auth.sigmaidentity.com";
729
+
730
+ return new Response(
731
+ JSON.stringify(
732
+ {
733
+ issuer: authServer,
734
+ authorization_endpoint: `${authServer}/api/oauth/authorize`,
735
+ token_endpoint: `${authServer}/api/oauth/token`,
736
+ userinfo_endpoint: `${authServer}/api/oauth/userinfo`,
737
+ jwks_uri: `${authServer}/.well-known/jwks.json`,
738
+ registration_endpoint: `${authServer}/api/oauth/register`,
739
+ scopes_supported: [
740
+ "openid",
741
+ "profile",
742
+ "email",
743
+ "offline_access",
744
+ "bsv:tools",
745
+ "bsv:wallet",
746
+ "bsv:ordinals",
747
+ "bsv:tokens",
748
+ ],
749
+ response_types_supported: ["code"],
750
+ grant_types_supported: ["authorization_code", "refresh_token"],
751
+ token_endpoint_auth_methods_supported: ["none"],
752
+ code_challenge_methods_supported: ["S256"],
753
+ },
754
+ null,
755
+ 2,
756
+ ),
757
+ {
758
+ status: 200,
759
+ headers: {
760
+ "Content-Type": "application/json",
761
+ "Access-Control-Allow-Origin": "*",
762
+ "Cache-Control": "public, max-age=3600",
763
+ },
764
+ },
765
+ );
766
+ }
767
+
768
+ // Handle OAuth 2.1 Protected Resource Metadata (RFC 9728)
769
+ if (
770
+ req.method === "GET" &&
771
+ url.pathname === "/.well-known/oauth-protected-resource"
772
+ ) {
773
+ const resourceUrl =
774
+ process.env.RESOURCE_URL || `http://localhost:${port}`;
775
+ const authServer =
776
+ process.env.OAUTH_ISSUER || "https://auth.sigmaidentity.com";
777
+
778
+ return new Response(
779
+ JSON.stringify(
780
+ {
781
+ resource: resourceUrl,
782
+ authorization_servers: [authServer],
783
+ scopes_supported: [
784
+ "openid",
785
+ "profile",
786
+ "email",
787
+ "bsv:tools",
788
+ "bsv:wallet",
789
+ "bsv:ordinals",
790
+ "bsv:tokens",
791
+ ],
792
+ bearer_methods_supported: ["header"],
793
+ resource_signing_alg_values_supported: ["RS256", "ES256"],
794
+ },
795
+ null,
796
+ 2,
797
+ ),
798
+ {
799
+ status: 200,
800
+ headers: {
801
+ "Content-Type": "application/json",
802
+ "Access-Control-Allow-Origin": "*",
803
+ "Access-Control-Allow-Methods": "GET",
804
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
805
+ "Access-Control-Expose-Headers": "WWW-Authenticate",
806
+ "Cache-Control": "public, max-age=3600",
807
+ },
808
+ },
809
+ );
810
+ }
811
+
812
+ // Handle CORS preflight for OAuth discovery
813
+ if (
814
+ req.method === "OPTIONS" &&
815
+ (url.pathname === "/.well-known/oauth-protected-resource" ||
816
+ url.pathname === "/.well-known/oauth-authorization-server")
817
+ ) {
818
+ return new Response(null, {
819
+ status: 204,
820
+ headers: {
821
+ "Access-Control-Allow-Origin": "*",
822
+ "Access-Control-Allow-Methods": "GET",
823
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
824
+ },
825
+ });
826
+ }
827
+
828
+ // Default response for other paths/methods
829
+ return new Response("Not Found", { status: 404 });
830
+ },
831
+ error(error: Error): Response {
832
+ logFunc(`Bun server error: ${error}\n${error.stack}`);
833
+ return new Response("Internal Server Error", { status: 500 });
834
+ },
835
+ });
836
+ logFunc(
837
+ ` Bun server started successfully. Listening on http://localhost:${port} `,
838
+ );
839
+ logFunc(" SSE Endpoint: /sse");
840
+ logFunc(
841
+ ` Message Endpoint: ${messageEndpoint} (POST with ?sessionId=...)`,
842
+ );
843
+ }
132
844
  }
133
845
 
134
- // Connect to the transport
135
- const transport = new StdioServerTransport();
136
- await server.connect(transport);
846
+ main().catch((error) => {
847
+ logFunc(`\x1b[31mFATAL: Server initialization failed: ${error}\x1b[0m`);
848
+ if (error instanceof Error && error.stack) {
849
+ logFunc(error.stack);
850
+ }
851
+ process.exit(1);
852
+ });
853
+
854
+ export { server };