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.
- package/CHANGELOG.md +124 -0
- package/README.md +233 -4
- package/dist/index.js +120082 -54669
- package/index.ts +1196 -88
- package/next-env.d.ts +6 -0
- package/package.json +58 -15
- package/prompts/bsvSdk/index.ts +5 -5
- package/prompts/bsvSdk/transaction.ts +2 -2
- package/prompts/bsvSdk/wallet.ts +1 -1
- package/prompts/index.ts +1 -1
- package/resources/bitcom.ts +132 -0
- package/resources/resources.ts +7 -3
- package/smithery.yaml +3 -0
- package/test-mcp-server.ts +157 -0
- package/tools/a2b/discover.ts +13 -13
- package/tools/bap/friend.ts +266 -0
- package/tools/bap/generate.test.ts +350 -0
- package/tools/bap/generate.ts +431 -0
- package/tools/bap/getCurrentAddress.ts +69 -0
- package/tools/bap/getId.test.ts +121 -0
- package/tools/bap/getId.ts +169 -0
- package/tools/bap/index.ts +111 -0
- package/tools/bap/types.ts +66 -0
- package/tools/bap/utils.ts +33 -0
- package/tools/bsocial/bmapFollow.ts +121 -0
- package/tools/bsocial/bmapLikes.ts +111 -0
- package/tools/bsocial/bmapReadPosts.ts +148 -0
- package/tools/bsocial/createPost.ts +136 -0
- package/tools/bsocial/index.ts +45 -0
- package/tools/bsocial/readPosts.ts +255 -0
- package/tools/bsv/decodeTransaction.ts +7 -6
- package/tools/bsv/explore.ts +2 -3
- package/tools/bsv/getPrice.ts +1 -9
- package/tools/bsv/token.ts +14 -26
- package/tools/constants.ts +21 -0
- package/tools/index.ts +91 -18
- package/tools/mnee/getBalance.ts +2 -4
- package/tools/mnee/index.ts +8 -0
- package/tools/mnee/parseTx.ts +4 -4
- package/tools/mnee/sendMnee.ts +6 -6
- package/tools/ordinals/getInscription.ts +2 -3
- package/tools/ordinals/getTokenByIdOrTicker.ts +6 -3
- package/tools/ordinals/marketListings.ts +2 -18
- package/tools/ordinals/marketSales.ts +2 -4
- package/tools/ordinals/searchInscriptions.ts +2 -4
- package/tools/utils/aip.ts +46 -0
- package/tools/utils/conversion.ts +1 -0
- package/tools/utils/errorHandler.ts +76 -0
- package/tools/utils/index.ts +56 -14
- package/tools/utils/installAgentMaster.ts +84 -0
- package/tools/utils/logger.ts +20 -0
- package/tools/utils/toolConfig.ts +80 -0
- package/tools/utils/toolRegistration.ts +66 -0
- package/tools/utils/transactionBuilder.ts +196 -0
- package/tools/wallet/a2bPublishAgent.ts +25 -30
- package/tools/wallet/a2bPublishMcp.ts +43 -44
- package/tools/wallet/createOrdinals.ts +13 -20
- package/tools/wallet/fetchPaymentUtxos.ts +48 -39
- package/tools/wallet/gatherCollectionInfo.ts +370 -0
- package/tools/wallet/getAddress.ts +14 -30
- package/tools/wallet/getBalance.ts +54 -0
- package/tools/wallet/getBalanceDroplet.ts +48 -0
- package/tools/wallet/getPublicKey.ts +3 -16
- package/tools/wallet/integratedWallet.ts +120 -0
- package/tools/wallet/mintCollection.ts +373 -0
- package/tools/wallet/purchaseListing.ts +21 -31
- package/tools/wallet/refreshUtxos.ts +2 -14
- package/tools/wallet/schemas.ts +18 -249
- package/tools/wallet/sendOrdinals.ts +14 -21
- package/tools/wallet/sendToAddress.ts +2 -18
- package/tools/wallet/setupDroplet.ts +161 -0
- package/tools/wallet/tools.ts +55 -210
- package/tools/wallet/transactionHelper.ts +121 -0
- package/tools/wallet/transferOrdToken.ts +19 -24
- package/tools/wallet/utxo.ts +62 -0
- package/tools/wallet/wallet.ts +237 -234
- package/vite.config.ts +15 -0
- 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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
-
*
|
|
35
|
-
*
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
//
|
|
74
|
-
const
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
//
|
|
121
|
-
|
|
122
|
-
registerResources(server);
|
|
123
|
-
}
|
|
839
|
+
// Set server instance for transport detection
|
|
840
|
+
setServerInstance(server);
|
|
124
841
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 };
|