nostr-mcp-server 2.0.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 (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +498 -0
  3. package/build/__tests__/basic.test.js +87 -0
  4. package/build/__tests__/error-handling.test.js +145 -0
  5. package/build/__tests__/format-conversion.test.js +137 -0
  6. package/build/__tests__/integration.test.js +163 -0
  7. package/build/__tests__/mocks.js +109 -0
  8. package/build/__tests__/nip19-conversion.test.js +268 -0
  9. package/build/__tests__/nips-search.test.js +109 -0
  10. package/build/__tests__/note-creation.test.js +148 -0
  11. package/build/__tests__/note-tools-functions.test.js +173 -0
  12. package/build/__tests__/note-tools-unit.test.js +97 -0
  13. package/build/__tests__/profile-notes-simple.test.js +78 -0
  14. package/build/__tests__/profile-postnote.test.js +120 -0
  15. package/build/__tests__/profile-tools.test.js +90 -0
  16. package/build/__tests__/relay-specification.test.js +136 -0
  17. package/build/__tests__/search-nips-simple.test.js +96 -0
  18. package/build/__tests__/websocket-integration.test.js +257 -0
  19. package/build/__tests__/zap-tools-simple.test.js +72 -0
  20. package/build/__tests__/zap-tools-tests.test.js +197 -0
  21. package/build/index.js +1285 -0
  22. package/build/nips/nips-tools.js +567 -0
  23. package/build/nips-tools.js +421 -0
  24. package/build/note/note-tools.js +296 -0
  25. package/build/note-tools.js +53 -0
  26. package/build/profile/profile-tools.js +260 -0
  27. package/build/utils/constants.js +27 -0
  28. package/build/utils/conversion.js +332 -0
  29. package/build/utils/ephemeral-relay.js +438 -0
  30. package/build/utils/formatting.js +34 -0
  31. package/build/utils/index.js +6 -0
  32. package/build/utils/nip19-tools.js +117 -0
  33. package/build/utils/pool.js +55 -0
  34. package/build/zap/zap-tools.js +980 -0
  35. package/build/zap-tools.js +989 -0
  36. package/package.json +59 -0
package/build/index.js ADDED
@@ -0,0 +1,1285 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import WebSocket from "ws";
6
+ import { searchNips, formatNipResult } from "./nips/nips-tools.js";
7
+ import { KINDS, DEFAULT_RELAYS, QUERY_TIMEOUT, getFreshPool, npubToHex, formatPubkey } from "./utils/index.js";
8
+ import { formatZapReceipt, processZapReceipt, validateZapReceipt, prepareAnonymousZap, sendAnonymousZapToolConfig, getReceivedZapsToolConfig, getSentZapsToolConfig, getAllZapsToolConfig } from "./zap/zap-tools.js";
9
+ import { formatProfile, formatNote, getProfileToolConfig, getKind1NotesToolConfig, getLongFormNotesToolConfig, postAnonymousNoteToolConfig, postAnonymousNote, createNote, signNote, publishNote, createNoteToolConfig, signNoteToolConfig, publishNoteToolConfig } from "./note/note-tools.js";
10
+ import { createKeypair, createProfile, updateProfile, postNote, createKeypairToolConfig, createProfileToolConfig, updateProfileToolConfig, postNoteToolConfig } from "./profile/profile-tools.js";
11
+ import { convertNip19, analyzeNip19, convertNip19ToolConfig, analyzeNip19ToolConfig, formatAnalysisResult } from "./utils/nip19-tools.js";
12
+ // Set WebSocket implementation for Node.js
13
+ globalThis.WebSocket = WebSocket;
14
+ // Create server instance
15
+ const server = new McpServer({
16
+ name: "nostr",
17
+ version: "1.0.0",
18
+ });
19
+ // Register Nostr tools
20
+ server.tool("getProfile", "Get a Nostr profile by public key", getProfileToolConfig, async ({ pubkey, relays }, extra) => {
21
+ // Convert npub to hex if needed
22
+ const hexPubkey = npubToHex(pubkey);
23
+ if (!hexPubkey) {
24
+ return {
25
+ content: [
26
+ {
27
+ type: "text",
28
+ text: "Invalid public key format. Please provide a valid hex pubkey or npub.",
29
+ },
30
+ ],
31
+ };
32
+ }
33
+ // Generate a friendly display version of the pubkey
34
+ const displayPubkey = formatPubkey(hexPubkey);
35
+ const relaysToUse = relays || DEFAULT_RELAYS;
36
+ // Create a fresh pool for this request
37
+ const pool = getFreshPool(relaysToUse);
38
+ try {
39
+ console.error(`Fetching profile for ${hexPubkey} from ${relaysToUse.join(", ")}`);
40
+ // Query for profile (kind 0) - snstr handles timeout internally
41
+ const profile = await pool.get(relaysToUse, {
42
+ kinds: [KINDS.Metadata],
43
+ authors: [hexPubkey],
44
+ });
45
+ if (!profile) {
46
+ return {
47
+ content: [
48
+ {
49
+ type: "text",
50
+ text: `No profile found for ${displayPubkey}`,
51
+ },
52
+ ],
53
+ };
54
+ }
55
+ const formatted = formatProfile(profile);
56
+ return {
57
+ content: [
58
+ {
59
+ type: "text",
60
+ text: `Profile for ${displayPubkey}:\n\n${formatted}`,
61
+ },
62
+ ],
63
+ };
64
+ }
65
+ catch (error) {
66
+ console.error("Error fetching profile:", error);
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: `Error fetching profile for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`,
72
+ },
73
+ ],
74
+ };
75
+ }
76
+ finally {
77
+ // Clean up any subscriptions and close the pool
78
+ await pool.close();
79
+ }
80
+ });
81
+ server.tool("getKind1Notes", "Get text notes (kind 1) by public key", getKind1NotesToolConfig, async ({ pubkey, limit, relays }, extra) => {
82
+ // Convert npub to hex if needed
83
+ const hexPubkey = npubToHex(pubkey);
84
+ if (!hexPubkey) {
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: "Invalid public key format. Please provide a valid hex pubkey or npub.",
90
+ },
91
+ ],
92
+ };
93
+ }
94
+ // Generate a friendly display version of the pubkey
95
+ const displayPubkey = formatPubkey(hexPubkey);
96
+ const relaysToUse = relays || DEFAULT_RELAYS;
97
+ // Create a fresh pool for this request
98
+ const pool = getFreshPool(relaysToUse);
99
+ try {
100
+ console.error(`Fetching kind 1 notes for ${hexPubkey} from ${relaysToUse.join(", ")}`);
101
+ // Query for text notes - snstr handles timeout internally
102
+ const notes = await pool.querySync(relaysToUse, {
103
+ kinds: [KINDS.Text],
104
+ authors: [hexPubkey],
105
+ limit,
106
+ }, { timeout: QUERY_TIMEOUT });
107
+ if (!notes || notes.length === 0) {
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: `No notes found for ${displayPubkey}`,
113
+ },
114
+ ],
115
+ };
116
+ }
117
+ // Sort notes by created_at in descending order (newest first)
118
+ notes.sort((a, b) => b.created_at - a.created_at);
119
+ const formattedNotes = notes.map(formatNote).join("\n");
120
+ return {
121
+ content: [
122
+ {
123
+ type: "text",
124
+ text: `Found ${notes.length} notes from ${displayPubkey}:\n\n${formattedNotes}`,
125
+ },
126
+ ],
127
+ };
128
+ }
129
+ catch (error) {
130
+ console.error("Error fetching notes:", error);
131
+ return {
132
+ content: [
133
+ {
134
+ type: "text",
135
+ text: `Error fetching notes for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`,
136
+ },
137
+ ],
138
+ };
139
+ }
140
+ finally {
141
+ // Clean up any subscriptions and close the pool
142
+ await pool.close();
143
+ }
144
+ });
145
+ server.tool("getReceivedZaps", "Get zaps received by a public key", getReceivedZapsToolConfig, async ({ pubkey, limit, relays, validateReceipts, debug }) => {
146
+ // Convert npub to hex if needed
147
+ const hexPubkey = npubToHex(pubkey);
148
+ if (!hexPubkey) {
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: "Invalid public key format. Please provide a valid hex pubkey or npub.",
154
+ },
155
+ ],
156
+ };
157
+ }
158
+ // Generate a friendly display version of the pubkey
159
+ const displayPubkey = formatPubkey(hexPubkey);
160
+ const relaysToUse = relays || DEFAULT_RELAYS;
161
+ // Create a fresh pool for this request
162
+ const pool = getFreshPool(relaysToUse);
163
+ try {
164
+ console.error(`Fetching zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`);
165
+ // Query for received zaps - snstr handles timeout internally
166
+ const zaps = await pool.querySync(relaysToUse, {
167
+ kinds: [KINDS.ZapReceipt],
168
+ "#p": [hexPubkey], // lowercase 'p' for recipient
169
+ limit: Math.ceil(limit * 1.5), // Fetch a bit more to account for potential invalid zaps
170
+ }, { timeout: QUERY_TIMEOUT });
171
+ if (!zaps || zaps.length === 0) {
172
+ return {
173
+ content: [
174
+ {
175
+ type: "text",
176
+ text: `No zaps found for ${displayPubkey}`,
177
+ },
178
+ ],
179
+ };
180
+ }
181
+ if (debug) {
182
+ console.error(`Retrieved ${zaps.length} raw zap receipts`);
183
+ }
184
+ // Process and optionally validate zaps
185
+ let processedZaps = [];
186
+ let invalidCount = 0;
187
+ for (const zap of zaps) {
188
+ try {
189
+ // Process the zap receipt with context of the target pubkey
190
+ const processedZap = processZapReceipt(zap, hexPubkey);
191
+ // Skip zaps that aren't actually received by this pubkey
192
+ if (processedZap.direction !== 'received' && processedZap.direction !== 'self') {
193
+ if (debug) {
194
+ console.error(`Skipping zap ${zap.id.slice(0, 8)}... with direction ${processedZap.direction}`);
195
+ }
196
+ continue;
197
+ }
198
+ // Validate if requested
199
+ if (validateReceipts) {
200
+ const validationResult = validateZapReceipt(zap);
201
+ if (!validationResult.valid) {
202
+ if (debug) {
203
+ console.error(`Invalid zap receipt ${zap.id.slice(0, 8)}...: ${validationResult.reason}`);
204
+ }
205
+ invalidCount++;
206
+ continue;
207
+ }
208
+ }
209
+ processedZaps.push(processedZap);
210
+ }
211
+ catch (error) {
212
+ if (debug) {
213
+ console.error(`Error processing zap ${zap.id.slice(0, 8)}...`, error);
214
+ }
215
+ }
216
+ }
217
+ if (processedZaps.length === 0) {
218
+ let message = `No valid zaps found for ${displayPubkey}`;
219
+ if (invalidCount > 0) {
220
+ message += ` (${invalidCount} invalid zaps were filtered out)`;
221
+ }
222
+ return {
223
+ content: [
224
+ {
225
+ type: "text",
226
+ text: message,
227
+ },
228
+ ],
229
+ };
230
+ }
231
+ // Sort zaps by created_at in descending order (newest first)
232
+ processedZaps.sort((a, b) => b.created_at - a.created_at);
233
+ // Limit to requested number
234
+ processedZaps = processedZaps.slice(0, limit);
235
+ // Calculate total sats received
236
+ const totalSats = processedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
237
+ const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n");
238
+ return {
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: `Found ${processedZaps.length} zaps received by ${displayPubkey}.\nTotal received: ${totalSats} sats\n\n${formattedZaps}`,
243
+ },
244
+ ],
245
+ };
246
+ }
247
+ catch (error) {
248
+ console.error("Error fetching zaps:", error);
249
+ return {
250
+ content: [
251
+ {
252
+ type: "text",
253
+ text: `Error fetching zaps for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`,
254
+ },
255
+ ],
256
+ };
257
+ }
258
+ finally {
259
+ // Clean up any subscriptions and close the pool
260
+ await pool.close();
261
+ }
262
+ });
263
+ server.tool("getSentZaps", "Get zaps sent by a public key", getSentZapsToolConfig, async ({ pubkey, limit, relays, validateReceipts, debug }) => {
264
+ // Convert npub to hex if needed
265
+ const hexPubkey = npubToHex(pubkey);
266
+ if (!hexPubkey) {
267
+ return {
268
+ content: [
269
+ {
270
+ type: "text",
271
+ text: "Invalid public key format. Please provide a valid hex pubkey or npub.",
272
+ },
273
+ ],
274
+ };
275
+ }
276
+ // Generate a friendly display version of the pubkey
277
+ const displayPubkey = formatPubkey(hexPubkey);
278
+ const relaysToUse = relays || DEFAULT_RELAYS;
279
+ // Create a fresh pool for this request
280
+ const pool = getFreshPool(relaysToUse);
281
+ try {
282
+ console.error(`Fetching sent zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`);
283
+ // First try the direct and correct approach: query with uppercase 'P' tag (NIP-57)
284
+ if (debug)
285
+ console.error("Trying direct query with #P tag...");
286
+ let potentialSentZaps = [];
287
+ try {
288
+ potentialSentZaps = await pool.querySync(relaysToUse, {
289
+ kinds: [KINDS.ZapReceipt],
290
+ "#P": [hexPubkey], // uppercase 'P' for sender
291
+ limit: Math.ceil(limit * 1.5), // Fetch a bit more to account for potential invalid zaps
292
+ }, { timeout: QUERY_TIMEOUT });
293
+ if (debug)
294
+ console.error(`Direct #P tag query returned ${potentialSentZaps.length} results`);
295
+ }
296
+ catch (e) {
297
+ if (debug)
298
+ console.error(`Direct #P tag query failed: ${e instanceof Error ? e.message : String(e)}`);
299
+ }
300
+ // If the direct query didn't return enough results, try the fallback method
301
+ if (!potentialSentZaps || potentialSentZaps.length < limit) {
302
+ if (debug)
303
+ console.error("Direct query yielded insufficient results, trying fallback approach...");
304
+ // Try a fallback approach - fetch a larger set of zap receipts
305
+ const additionalZaps = await pool.querySync(relaysToUse, {
306
+ kinds: [KINDS.ZapReceipt],
307
+ limit: Math.max(limit * 10, 100), // Get a larger sample
308
+ }, { timeout: QUERY_TIMEOUT });
309
+ if (debug) {
310
+ console.error(`Retrieved ${additionalZaps?.length || 0} additional zap receipts to analyze`);
311
+ }
312
+ if (additionalZaps && additionalZaps.length > 0) {
313
+ // Add these to our potential sent zaps
314
+ potentialSentZaps = [...potentialSentZaps, ...additionalZaps];
315
+ }
316
+ }
317
+ if (!potentialSentZaps || potentialSentZaps.length === 0) {
318
+ return {
319
+ content: [
320
+ {
321
+ type: "text",
322
+ text: "No zap receipts found to analyze",
323
+ },
324
+ ],
325
+ };
326
+ }
327
+ // Process and filter zaps
328
+ let processedZaps = [];
329
+ let invalidCount = 0;
330
+ let nonSentCount = 0;
331
+ if (debug) {
332
+ console.error(`Processing ${potentialSentZaps.length} potential sent zaps...`);
333
+ }
334
+ // Process each zap to determine if it was sent by the target pubkey
335
+ for (const zap of potentialSentZaps) {
336
+ try {
337
+ // Process the zap receipt with context of the target pubkey
338
+ const processedZap = processZapReceipt(zap, hexPubkey);
339
+ // Skip zaps that aren't sent by this pubkey
340
+ if (processedZap.direction !== 'sent' && processedZap.direction !== 'self') {
341
+ if (debug) {
342
+ console.error(`Skipping zap ${zap.id.slice(0, 8)}... with direction ${processedZap.direction}`);
343
+ }
344
+ nonSentCount++;
345
+ continue;
346
+ }
347
+ // Validate if requested
348
+ if (validateReceipts) {
349
+ const validationResult = validateZapReceipt(zap);
350
+ if (!validationResult.valid) {
351
+ if (debug) {
352
+ console.error(`Invalid zap receipt ${zap.id.slice(0, 8)}...: ${validationResult.reason}`);
353
+ }
354
+ invalidCount++;
355
+ continue;
356
+ }
357
+ }
358
+ processedZaps.push(processedZap);
359
+ }
360
+ catch (error) {
361
+ if (debug) {
362
+ console.error(`Error processing zap ${zap.id.slice(0, 8)}...`, error);
363
+ }
364
+ }
365
+ }
366
+ // Deduplicate by zap ID
367
+ const uniqueZaps = new Map();
368
+ processedZaps.forEach(zap => uniqueZaps.set(zap.id, zap));
369
+ processedZaps = Array.from(uniqueZaps.values());
370
+ if (processedZaps.length === 0) {
371
+ let message = `No zaps sent by ${displayPubkey} were found.`;
372
+ if (invalidCount > 0 || nonSentCount > 0) {
373
+ message += ` (${invalidCount} invalid zaps and ${nonSentCount} non-sent zaps were filtered out)`;
374
+ }
375
+ message += " This could be because:\n1. The user hasn't sent any zaps\n2. The zap receipts don't properly contain the sender's pubkey\n3. The relays queried don't have this data";
376
+ return {
377
+ content: [
378
+ {
379
+ type: "text",
380
+ text: message,
381
+ },
382
+ ],
383
+ };
384
+ }
385
+ // Sort zaps by created_at in descending order (newest first)
386
+ processedZaps.sort((a, b) => b.created_at - a.created_at);
387
+ // Limit to requested number
388
+ processedZaps = processedZaps.slice(0, limit);
389
+ // Calculate total sats sent
390
+ const totalSats = processedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
391
+ // For debugging, examine the first zap in detail
392
+ if (debug && processedZaps.length > 0) {
393
+ const firstZap = processedZaps[0];
394
+ console.error("Sample sent zap:", JSON.stringify(firstZap, null, 2));
395
+ }
396
+ const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n");
397
+ return {
398
+ content: [
399
+ {
400
+ type: "text",
401
+ text: `Found ${processedZaps.length} zaps sent by ${displayPubkey}.\nTotal sent: ${totalSats} sats\n\n${formattedZaps}`,
402
+ },
403
+ ],
404
+ };
405
+ }
406
+ catch (error) {
407
+ console.error("Error fetching sent zaps:", error);
408
+ return {
409
+ content: [
410
+ {
411
+ type: "text",
412
+ text: `Error fetching sent zaps for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`,
413
+ },
414
+ ],
415
+ };
416
+ }
417
+ finally {
418
+ // Clean up any subscriptions and close the pool
419
+ await pool.close();
420
+ }
421
+ });
422
+ server.tool("getAllZaps", "Get all zaps (sent and received) for a public key", getAllZapsToolConfig, async ({ pubkey, limit, relays, validateReceipts, debug }) => {
423
+ // Convert npub to hex if needed
424
+ const hexPubkey = npubToHex(pubkey);
425
+ if (!hexPubkey) {
426
+ return {
427
+ content: [
428
+ {
429
+ type: "text",
430
+ text: "Invalid public key format. Please provide a valid hex pubkey or npub.",
431
+ },
432
+ ],
433
+ };
434
+ }
435
+ // Generate a friendly display version of the pubkey
436
+ const displayPubkey = formatPubkey(hexPubkey);
437
+ const relaysToUse = relays || DEFAULT_RELAYS;
438
+ // Create a fresh pool for this request
439
+ const pool = getFreshPool(relaysToUse);
440
+ try {
441
+ console.error(`Fetching all zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`);
442
+ // Use a more efficient approach: fetch all potentially relevant zaps in parallel
443
+ // Prepare all required queries in parallel to reduce total time
444
+ const fetchPromises = [
445
+ // 1. Fetch received zaps (lowercase 'p' tag)
446
+ pool.querySync(relaysToUse, {
447
+ kinds: [KINDS.ZapReceipt],
448
+ "#p": [hexPubkey],
449
+ limit: Math.ceil(limit * 1.5),
450
+ }, { timeout: QUERY_TIMEOUT }),
451
+ // 2. Fetch sent zaps (uppercase 'P' tag)
452
+ pool.querySync(relaysToUse, {
453
+ kinds: [KINDS.ZapReceipt],
454
+ "#P": [hexPubkey],
455
+ limit: Math.ceil(limit * 1.5),
456
+ }, { timeout: QUERY_TIMEOUT })
457
+ ];
458
+ // Add a general query if we're in debug mode or need more comprehensive results
459
+ if (debug) {
460
+ fetchPromises.push(pool.querySync(relaysToUse, {
461
+ kinds: [KINDS.ZapReceipt],
462
+ limit: Math.max(limit * 5, 50),
463
+ }, { timeout: QUERY_TIMEOUT }));
464
+ }
465
+ // Execute all queries in parallel
466
+ const results = await Promise.allSettled(fetchPromises);
467
+ // Collect all zaps from successful queries
468
+ const allZaps = [];
469
+ results.forEach((result, index) => {
470
+ if (result.status === 'fulfilled') {
471
+ const zaps = result.value;
472
+ if (debug) {
473
+ const queryTypes = ['Received', 'Sent', 'General'];
474
+ console.error(`${queryTypes[index]} query returned ${zaps.length} results`);
475
+ }
476
+ allZaps.push(...zaps);
477
+ }
478
+ else if (debug) {
479
+ const queryTypes = ['Received', 'Sent', 'General'];
480
+ console.error(`${queryTypes[index]} query failed:`, result.reason);
481
+ }
482
+ });
483
+ if (allZaps.length === 0) {
484
+ return {
485
+ content: [
486
+ {
487
+ type: "text",
488
+ text: `No zaps found for ${displayPubkey}. Try specifying different relays that might have the data.`,
489
+ },
490
+ ],
491
+ };
492
+ }
493
+ if (debug) {
494
+ console.error(`Retrieved ${allZaps.length} total zaps before deduplication`);
495
+ }
496
+ // Deduplicate by zap ID
497
+ const uniqueZapsMap = new Map();
498
+ allZaps.forEach(zap => uniqueZapsMap.set(zap.id, zap));
499
+ const uniqueZaps = Array.from(uniqueZapsMap.values());
500
+ if (debug) {
501
+ console.error(`Deduplicated to ${uniqueZaps.length} unique zaps`);
502
+ }
503
+ // Process each zap to determine its relevance to the target pubkey
504
+ let processedZaps = [];
505
+ let invalidCount = 0;
506
+ let irrelevantCount = 0;
507
+ for (const zap of uniqueZaps) {
508
+ try {
509
+ // Process the zap with the target pubkey as context
510
+ const processedZap = processZapReceipt(zap, hexPubkey);
511
+ // Skip zaps that are neither sent nor received by this pubkey
512
+ if (processedZap.direction === 'unknown') {
513
+ if (debug) {
514
+ console.error(`Skipping irrelevant zap ${zap.id.slice(0, 8)}...`);
515
+ }
516
+ irrelevantCount++;
517
+ continue;
518
+ }
519
+ // Validate if requested
520
+ if (validateReceipts) {
521
+ const validationResult = validateZapReceipt(zap);
522
+ if (!validationResult.valid) {
523
+ if (debug) {
524
+ console.error(`Invalid zap receipt ${zap.id.slice(0, 8)}...: ${validationResult.reason}`);
525
+ }
526
+ invalidCount++;
527
+ continue;
528
+ }
529
+ }
530
+ processedZaps.push(processedZap);
531
+ }
532
+ catch (error) {
533
+ if (debug) {
534
+ console.error(`Error processing zap ${zap.id.slice(0, 8)}...`, error);
535
+ }
536
+ }
537
+ }
538
+ if (processedZaps.length === 0) {
539
+ let message = `No relevant zaps found for ${displayPubkey}.`;
540
+ if (invalidCount > 0 || irrelevantCount > 0) {
541
+ message += ` (${invalidCount} invalid zaps and ${irrelevantCount} irrelevant zaps were filtered out)`;
542
+ }
543
+ return {
544
+ content: [
545
+ {
546
+ type: "text",
547
+ text: message,
548
+ },
549
+ ],
550
+ };
551
+ }
552
+ // Sort zaps by created_at in descending order (newest first)
553
+ processedZaps.sort((a, b) => b.created_at - a.created_at);
554
+ // Calculate statistics: sent, received, and self zaps
555
+ const sentZaps = processedZaps.filter(zap => zap.direction === 'sent');
556
+ const receivedZaps = processedZaps.filter(zap => zap.direction === 'received');
557
+ const selfZaps = processedZaps.filter(zap => zap.direction === 'self');
558
+ // Calculate total sats
559
+ const totalSent = sentZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
560
+ const totalReceived = receivedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
561
+ const totalSelfZaps = selfZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
562
+ // Limit to requested number for display
563
+ processedZaps = processedZaps.slice(0, limit);
564
+ // Format the zaps with the pubkey context
565
+ const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n");
566
+ // Prepare summary statistics
567
+ const summary = [
568
+ `Zap Summary for ${displayPubkey}:`,
569
+ `- ${sentZaps.length} zaps sent (${totalSent} sats)`,
570
+ `- ${receivedZaps.length} zaps received (${totalReceived} sats)`,
571
+ `- ${selfZaps.length} self-zaps (${totalSelfZaps} sats)`,
572
+ `- Net balance: ${totalReceived - totalSent} sats`,
573
+ `\nShowing ${processedZaps.length} most recent zaps:\n`
574
+ ].join("\n");
575
+ return {
576
+ content: [
577
+ {
578
+ type: "text",
579
+ text: `${summary}\n${formattedZaps}`,
580
+ },
581
+ ],
582
+ };
583
+ }
584
+ catch (error) {
585
+ console.error("Error fetching all zaps:", error);
586
+ return {
587
+ content: [
588
+ {
589
+ type: "text",
590
+ text: `Error fetching all zaps for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`,
591
+ },
592
+ ],
593
+ };
594
+ }
595
+ finally {
596
+ // Clean up any subscriptions and close the pool
597
+ await pool.close();
598
+ }
599
+ });
600
+ server.tool("getLongFormNotes", "Get long-form notes (kind 30023) by public key", getLongFormNotesToolConfig, async ({ pubkey, limit, relays }, extra) => {
601
+ // Convert npub to hex if needed
602
+ const hexPubkey = npubToHex(pubkey);
603
+ if (!hexPubkey) {
604
+ return {
605
+ content: [
606
+ {
607
+ type: "text",
608
+ text: "Invalid public key format. Please provide a valid hex pubkey or npub.",
609
+ },
610
+ ],
611
+ };
612
+ }
613
+ // Generate a friendly display version of the pubkey
614
+ const displayPubkey = formatPubkey(hexPubkey);
615
+ const relaysToUse = relays || DEFAULT_RELAYS;
616
+ // Create a fresh pool for this request
617
+ const pool = getFreshPool(relaysToUse);
618
+ try {
619
+ console.error(`Fetching long-form notes for ${hexPubkey} from ${relaysToUse.join(", ")}`);
620
+ // Query for long-form notes - snstr handles timeout internally
621
+ const notes = await pool.querySync(relaysToUse, {
622
+ kinds: [30023], // NIP-23 long-form content
623
+ authors: [hexPubkey],
624
+ limit,
625
+ }, { timeout: QUERY_TIMEOUT });
626
+ if (!notes || notes.length === 0) {
627
+ return {
628
+ content: [
629
+ {
630
+ type: "text",
631
+ text: `No long-form notes found for ${displayPubkey}`,
632
+ },
633
+ ],
634
+ };
635
+ }
636
+ // Sort notes by created_at in descending order (newest first)
637
+ notes.sort((a, b) => b.created_at - a.created_at);
638
+ // Format each note with enhanced metadata
639
+ const formattedNotes = notes.map(note => {
640
+ // Extract metadata from tags
641
+ const title = note.tags.find(tag => tag[0] === "title")?.[1] || "Untitled";
642
+ const image = note.tags.find(tag => tag[0] === "image")?.[1];
643
+ const summary = note.tags.find(tag => tag[0] === "summary")?.[1];
644
+ const publishedAt = note.tags.find(tag => tag[0] === "published_at")?.[1];
645
+ const identifier = note.tags.find(tag => tag[0] === "d")?.[1];
646
+ // Format the output
647
+ const lines = [
648
+ `Title: ${title}`,
649
+ `Created: ${new Date(note.created_at * 1000).toLocaleString()}`,
650
+ publishedAt ? `Published: ${new Date(parseInt(publishedAt) * 1000).toLocaleString()}` : null,
651
+ image ? `Image: ${image}` : null,
652
+ summary ? `Summary: ${summary}` : null,
653
+ identifier ? `Identifier: ${identifier}` : null,
654
+ `Content:`,
655
+ note.content,
656
+ `---`,
657
+ ].filter(Boolean).join("\n");
658
+ return lines;
659
+ }).join("\n\n");
660
+ return {
661
+ content: [
662
+ {
663
+ type: "text",
664
+ text: `Found ${notes.length} long-form notes from ${displayPubkey}:\n\n${formattedNotes}`,
665
+ },
666
+ ],
667
+ };
668
+ }
669
+ catch (error) {
670
+ console.error("Error fetching long-form notes:", error);
671
+ return {
672
+ content: [
673
+ {
674
+ type: "text",
675
+ text: `Error fetching long-form notes for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`,
676
+ },
677
+ ],
678
+ };
679
+ }
680
+ finally {
681
+ // Clean up any subscriptions and close the pool
682
+ await pool.close();
683
+ }
684
+ });
685
+ server.tool("searchNips", "Search through Nostr Implementation Possibilities (NIPs)", {
686
+ query: z.string().describe("Search query to find relevant NIPs"),
687
+ limit: z.number().min(1).max(50).default(10).describe("Maximum number of results to return"),
688
+ includeContent: z.boolean().default(false).describe("Whether to include the full content of each NIP in the results"),
689
+ }, async ({ query, limit, includeContent }) => {
690
+ try {
691
+ console.error(`Searching NIPs for: "${query}"`);
692
+ const results = await searchNips(query, limit);
693
+ if (results.length === 0) {
694
+ return {
695
+ content: [
696
+ {
697
+ type: "text",
698
+ text: `No NIPs found matching "${query}". Try different search terms or check the NIPs repository for the latest updates.`,
699
+ },
700
+ ],
701
+ };
702
+ }
703
+ // Format results using the new formatter
704
+ const formattedResults = results.map(result => formatNipResult(result, includeContent)).join("\n\n");
705
+ return {
706
+ content: [
707
+ {
708
+ type: "text",
709
+ text: `Found ${results.length} matching NIPs:\n\n${formattedResults}`,
710
+ },
711
+ ],
712
+ };
713
+ }
714
+ catch (error) {
715
+ console.error("Error searching NIPs:", error);
716
+ return {
717
+ content: [
718
+ {
719
+ type: "text",
720
+ text: `Error searching NIPs: ${error instanceof Error ? error.message : "Unknown error"}`,
721
+ },
722
+ ],
723
+ };
724
+ }
725
+ });
726
+ server.tool("sendAnonymousZap", "Prepare an anonymous zap to a profile or event", sendAnonymousZapToolConfig, async ({ target, amountSats, comment, relays }) => {
727
+ // Use supplied relays or defaults
728
+ const relaysToUse = relays || DEFAULT_RELAYS;
729
+ try {
730
+ // console.error(`Preparing anonymous zap to ${target} for ${amountSats} sats`);
731
+ // Prepare the anonymous zap
732
+ const zapResult = await prepareAnonymousZap(target, amountSats, comment, relaysToUse);
733
+ if (!zapResult || !zapResult.success) {
734
+ return {
735
+ content: [
736
+ {
737
+ type: "text",
738
+ text: `Failed to prepare anonymous zap: ${zapResult?.message || "Unknown error"}`,
739
+ },
740
+ ],
741
+ };
742
+ }
743
+ return {
744
+ content: [
745
+ {
746
+ type: "text",
747
+ text: `Anonymous zap prepared successfully!\n\nAmount: ${amountSats} sats${comment ? `\nComment: "${comment}"` : ""}\nTarget: ${target}\n\nInvoice:\n${zapResult.invoice}\n\nCopy this invoice into your Lightning wallet to pay. After payment, the recipient will receive the zap anonymously.`,
748
+ },
749
+ ],
750
+ };
751
+ }
752
+ catch (error) {
753
+ console.error("Error in sendAnonymousZap tool:", error);
754
+ let errorMessage = error instanceof Error ? error.message : "Unknown error";
755
+ // Provide a more helpful message for common errors
756
+ if (errorMessage.includes("ENOTFOUND") || errorMessage.includes("ETIMEDOUT")) {
757
+ errorMessage = `Could not connect to the Lightning service. This might be a temporary network issue or the service might be down. Error: ${errorMessage}`;
758
+ }
759
+ else if (errorMessage.includes("Timeout")) {
760
+ errorMessage = "The operation timed out. This might be due to slow relays or network connectivity issues.";
761
+ }
762
+ return {
763
+ content: [
764
+ {
765
+ type: "text",
766
+ text: `Error preparing anonymous zap: ${errorMessage}`,
767
+ },
768
+ ],
769
+ };
770
+ }
771
+ });
772
+ // Register NIP-19 conversion tools
773
+ server.tool("convertNip19", "Convert any NIP-19 entity (npub, nsec, note, nprofile, nevent, naddr) to another format", convertNip19ToolConfig, async ({ input, targetType, relays, author, kind, identifier }) => {
774
+ try {
775
+ const result = await convertNip19(input, targetType, relays, author, kind, identifier);
776
+ if (result.success) {
777
+ let response = `Conversion successful!\n\n`;
778
+ response += `Original: ${result.originalType} entity\n`;
779
+ response += `Target: ${targetType}\n`;
780
+ response += `Result: ${result.result}\n`;
781
+ if (result.originalType && ['nprofile', 'nevent', 'naddr'].includes(result.originalType)) {
782
+ response += `\nOriginal entity data:\n${formatAnalysisResult(result.originalType, result.data)}`;
783
+ }
784
+ return {
785
+ content: [
786
+ {
787
+ type: "text",
788
+ text: response,
789
+ },
790
+ ],
791
+ };
792
+ }
793
+ else {
794
+ return {
795
+ content: [
796
+ {
797
+ type: "text",
798
+ text: `Conversion failed: ${result.message}`,
799
+ },
800
+ ],
801
+ };
802
+ }
803
+ }
804
+ catch (error) {
805
+ console.error("Error in convertNip19 tool:", error);
806
+ return {
807
+ content: [
808
+ {
809
+ type: "text",
810
+ text: `Error during conversion: ${error instanceof Error ? error.message : "Unknown error"}`,
811
+ },
812
+ ],
813
+ };
814
+ }
815
+ });
816
+ server.tool("analyzeNip19", "Analyze any NIP-19 entity or hex string to understand its type and contents", analyzeNip19ToolConfig, async ({ input }) => {
817
+ try {
818
+ const result = await analyzeNip19(input);
819
+ if (result.success) {
820
+ let response = `Analysis successful!\n\n`;
821
+ response += `Type: ${result.type}\n\n`;
822
+ response += formatAnalysisResult(result.type, result.data);
823
+ return {
824
+ content: [
825
+ {
826
+ type: "text",
827
+ text: response,
828
+ },
829
+ ],
830
+ };
831
+ }
832
+ else {
833
+ return {
834
+ content: [
835
+ {
836
+ type: "text",
837
+ text: `Analysis failed: ${result.message}`,
838
+ },
839
+ ],
840
+ };
841
+ }
842
+ }
843
+ catch (error) {
844
+ console.error("Error in analyzeNip19 tool:", error);
845
+ return {
846
+ content: [
847
+ {
848
+ type: "text",
849
+ text: `Error during analysis: ${error instanceof Error ? error.message : "Unknown error"}`,
850
+ },
851
+ ],
852
+ };
853
+ }
854
+ });
855
+ server.tool("postAnonymousNote", "Post an anonymous note to the Nostr network using a temporary keypair", postAnonymousNoteToolConfig, async ({ content, relays, tags }) => {
856
+ try {
857
+ const result = await postAnonymousNote(content, relays, tags);
858
+ if (result.success) {
859
+ let response = `Anonymous note posted successfully!\n\n`;
860
+ response += `${result.message}\n`;
861
+ if (result.noteId) {
862
+ response += `Note ID: ${result.noteId}\n`;
863
+ }
864
+ if (result.publicKey) {
865
+ response += `Anonymous Author: ${formatPubkey(result.publicKey)}\n`;
866
+ }
867
+ response += `Content: "${content}"\n`;
868
+ if (tags && tags.length > 0) {
869
+ response += `Tags: ${JSON.stringify(tags)}\n`;
870
+ }
871
+ if (relays && relays.length > 0) {
872
+ response += `Relays: ${relays.join(", ")}\n`;
873
+ }
874
+ return {
875
+ content: [
876
+ {
877
+ type: "text",
878
+ text: response,
879
+ },
880
+ ],
881
+ };
882
+ }
883
+ else {
884
+ return {
885
+ content: [
886
+ {
887
+ type: "text",
888
+ text: `Failed to post anonymous note: ${result.message}`,
889
+ },
890
+ ],
891
+ };
892
+ }
893
+ }
894
+ catch (error) {
895
+ console.error("Error in postAnonymousNote tool:", error);
896
+ return {
897
+ content: [
898
+ {
899
+ type: "text",
900
+ text: `Error posting anonymous note: ${error instanceof Error ? error.message : "Unknown error"}`,
901
+ },
902
+ ],
903
+ };
904
+ }
905
+ });
906
+ // Register profile management tools
907
+ server.tool("createKeypair", "Generate a new Nostr keypair", createKeypairToolConfig, async ({ format }) => {
908
+ try {
909
+ const result = await createKeypair(format);
910
+ let response = "New Nostr keypair generated:\n\n";
911
+ if (result.publicKey) {
912
+ response += `Public Key (hex): ${result.publicKey}\n`;
913
+ }
914
+ if (result.privateKey) {
915
+ response += `Private Key (hex): ${result.privateKey}\n`;
916
+ }
917
+ if (result.npub) {
918
+ response += `Public Key (npub): ${result.npub}\n`;
919
+ }
920
+ if (result.nsec) {
921
+ response += `Private Key (nsec): ${result.nsec}\n`;
922
+ }
923
+ response += "\n⚠️ IMPORTANT: Store your private key securely! This is the only copy and cannot be recovered if lost.";
924
+ return {
925
+ content: [
926
+ {
927
+ type: "text",
928
+ text: response,
929
+ },
930
+ ],
931
+ };
932
+ }
933
+ catch (error) {
934
+ console.error("Error in createKeypair tool:", error);
935
+ return {
936
+ content: [
937
+ {
938
+ type: "text",
939
+ text: `Error generating keypair: ${error instanceof Error ? error.message : "Unknown error"}`,
940
+ },
941
+ ],
942
+ };
943
+ }
944
+ });
945
+ server.tool("createProfile", "Create a new Nostr profile (kind 0 event)", createProfileToolConfig, async ({ privateKey, name, about, picture, nip05, lud16, lud06, website, relays }) => {
946
+ try {
947
+ const profileData = {
948
+ name,
949
+ about,
950
+ picture,
951
+ nip05,
952
+ lud16,
953
+ lud06,
954
+ website
955
+ };
956
+ const result = await createProfile(privateKey, profileData, relays);
957
+ if (result.success) {
958
+ let response = `Profile created successfully!\n\n`;
959
+ response += `${result.message}\n`;
960
+ if (result.eventId) {
961
+ response += `Event ID: ${result.eventId}\n`;
962
+ }
963
+ if (result.publicKey) {
964
+ response += `Public Key: ${formatPubkey(result.publicKey)}\n`;
965
+ }
966
+ // Show the profile data that was set
967
+ response += "\nProfile data:\n";
968
+ if (name)
969
+ response += `Name: ${name}\n`;
970
+ if (about)
971
+ response += `About: ${about}\n`;
972
+ if (picture)
973
+ response += `Picture: ${picture}\n`;
974
+ if (nip05)
975
+ response += `NIP-05: ${nip05}\n`;
976
+ if (lud16)
977
+ response += `Lightning Address: ${lud16}\n`;
978
+ if (lud06)
979
+ response += `LNURL: ${lud06}\n`;
980
+ if (website)
981
+ response += `Website: ${website}\n`;
982
+ return {
983
+ content: [
984
+ {
985
+ type: "text",
986
+ text: response,
987
+ },
988
+ ],
989
+ };
990
+ }
991
+ else {
992
+ return {
993
+ content: [
994
+ {
995
+ type: "text",
996
+ text: `Failed to create profile: ${result.message}`,
997
+ },
998
+ ],
999
+ };
1000
+ }
1001
+ }
1002
+ catch (error) {
1003
+ console.error("Error in createProfile tool:", error);
1004
+ return {
1005
+ content: [
1006
+ {
1007
+ type: "text",
1008
+ text: `Error creating profile: ${error instanceof Error ? error.message : "Unknown error"}`,
1009
+ },
1010
+ ],
1011
+ };
1012
+ }
1013
+ });
1014
+ server.tool("updateProfile", "Update an existing Nostr profile (kind 0 event)", updateProfileToolConfig, async ({ privateKey, name, about, picture, nip05, lud16, lud06, website, relays }) => {
1015
+ try {
1016
+ const profileData = {
1017
+ name,
1018
+ about,
1019
+ picture,
1020
+ nip05,
1021
+ lud16,
1022
+ lud06,
1023
+ website
1024
+ };
1025
+ const result = await updateProfile(privateKey, profileData, relays);
1026
+ if (result.success) {
1027
+ let response = `Profile updated successfully!\n\n`;
1028
+ response += `${result.message}\n`;
1029
+ if (result.eventId) {
1030
+ response += `Event ID: ${result.eventId}\n`;
1031
+ }
1032
+ if (result.publicKey) {
1033
+ response += `Public Key: ${formatPubkey(result.publicKey)}\n`;
1034
+ }
1035
+ // Show the profile data that was updated
1036
+ response += "\nUpdated profile data:\n";
1037
+ if (name)
1038
+ response += `Name: ${name}\n`;
1039
+ if (about)
1040
+ response += `About: ${about}\n`;
1041
+ if (picture)
1042
+ response += `Picture: ${picture}\n`;
1043
+ if (nip05)
1044
+ response += `NIP-05: ${nip05}\n`;
1045
+ if (lud16)
1046
+ response += `Lightning Address: ${lud16}\n`;
1047
+ if (lud06)
1048
+ response += `LNURL: ${lud06}\n`;
1049
+ if (website)
1050
+ response += `Website: ${website}\n`;
1051
+ return {
1052
+ content: [
1053
+ {
1054
+ type: "text",
1055
+ text: response,
1056
+ },
1057
+ ],
1058
+ };
1059
+ }
1060
+ else {
1061
+ return {
1062
+ content: [
1063
+ {
1064
+ type: "text",
1065
+ text: `Failed to update profile: ${result.message}`,
1066
+ },
1067
+ ],
1068
+ };
1069
+ }
1070
+ }
1071
+ catch (error) {
1072
+ console.error("Error in updateProfile tool:", error);
1073
+ return {
1074
+ content: [
1075
+ {
1076
+ type: "text",
1077
+ text: `Error updating profile: ${error instanceof Error ? error.message : "Unknown error"}`,
1078
+ },
1079
+ ],
1080
+ };
1081
+ }
1082
+ });
1083
+ server.tool("postNote", "Post a note using an existing private key (authenticated posting)", postNoteToolConfig, async ({ privateKey, content, tags, relays }) => {
1084
+ try {
1085
+ const result = await postNote(privateKey, content, tags, relays);
1086
+ if (result.success) {
1087
+ let response = `Note posted successfully!\n\n`;
1088
+ response += `${result.message}\n`;
1089
+ if (result.noteId) {
1090
+ response += `Note ID: ${result.noteId}\n`;
1091
+ }
1092
+ if (result.publicKey) {
1093
+ response += `Author: ${formatPubkey(result.publicKey)}\n`;
1094
+ }
1095
+ response += `Content: "${content}"\n`;
1096
+ if (tags && tags.length > 0) {
1097
+ response += `Tags: ${JSON.stringify(tags)}\n`;
1098
+ }
1099
+ if (relays && relays.length > 0) {
1100
+ response += `Relays: ${relays.join(", ")}\n`;
1101
+ }
1102
+ return {
1103
+ content: [
1104
+ {
1105
+ type: "text",
1106
+ text: response,
1107
+ },
1108
+ ],
1109
+ };
1110
+ }
1111
+ else {
1112
+ return {
1113
+ content: [
1114
+ {
1115
+ type: "text",
1116
+ text: `Failed to post note: ${result.message}`,
1117
+ },
1118
+ ],
1119
+ };
1120
+ }
1121
+ }
1122
+ catch (error) {
1123
+ console.error("Error in postNote tool:", error);
1124
+ return {
1125
+ content: [
1126
+ {
1127
+ type: "text",
1128
+ text: `Error posting note: ${error instanceof Error ? error.message : "Unknown error"}`,
1129
+ },
1130
+ ],
1131
+ };
1132
+ }
1133
+ });
1134
+ // Register note creation and publishing tools
1135
+ server.tool("createNote", "Create a new kind 1 note event (unsigned)", createNoteToolConfig, async ({ privateKey, content, tags }) => {
1136
+ try {
1137
+ const result = await createNote(privateKey, content, tags);
1138
+ if (result.success) {
1139
+ let response = `Note event created successfully!\n\n`;
1140
+ response += `${result.message}\n`;
1141
+ if (result.publicKey) {
1142
+ response += `Author: ${formatPubkey(result.publicKey)}\n`;
1143
+ }
1144
+ response += `Content: "${content}"\n`;
1145
+ if (tags && tags.length > 0) {
1146
+ response += `Tags: ${JSON.stringify(tags)}\n`;
1147
+ }
1148
+ response += `\nNote Event (unsigned):\n${JSON.stringify(result.noteEvent, null, 2)}`;
1149
+ return {
1150
+ content: [
1151
+ {
1152
+ type: "text",
1153
+ text: response,
1154
+ },
1155
+ ],
1156
+ };
1157
+ }
1158
+ else {
1159
+ return {
1160
+ content: [
1161
+ {
1162
+ type: "text",
1163
+ text: `Failed to create note: ${result.message}`,
1164
+ },
1165
+ ],
1166
+ };
1167
+ }
1168
+ }
1169
+ catch (error) {
1170
+ console.error("Error in createNote tool:", error);
1171
+ return {
1172
+ content: [
1173
+ {
1174
+ type: "text",
1175
+ text: `Error creating note: ${error instanceof Error ? error.message : "Unknown error"}`,
1176
+ },
1177
+ ],
1178
+ };
1179
+ }
1180
+ });
1181
+ server.tool("signNote", "Sign a note event with a private key", signNoteToolConfig, async ({ privateKey, noteEvent }) => {
1182
+ try {
1183
+ const result = await signNote(privateKey, noteEvent);
1184
+ if (result.success) {
1185
+ let response = `Note signed successfully!\n\n`;
1186
+ response += `${result.message}\n`;
1187
+ response += `Note ID: ${result.signedNote?.id}\n`;
1188
+ response += `Content: "${noteEvent.content}"\n`;
1189
+ response += `\nSigned Note Event:\n${JSON.stringify(result.signedNote, null, 2)}`;
1190
+ return {
1191
+ content: [
1192
+ {
1193
+ type: "text",
1194
+ text: response,
1195
+ },
1196
+ ],
1197
+ };
1198
+ }
1199
+ else {
1200
+ return {
1201
+ content: [
1202
+ {
1203
+ type: "text",
1204
+ text: `Failed to sign note: ${result.message}`,
1205
+ },
1206
+ ],
1207
+ };
1208
+ }
1209
+ }
1210
+ catch (error) {
1211
+ console.error("Error in signNote tool:", error);
1212
+ return {
1213
+ content: [
1214
+ {
1215
+ type: "text",
1216
+ text: `Error signing note: ${error instanceof Error ? error.message : "Unknown error"}`,
1217
+ },
1218
+ ],
1219
+ };
1220
+ }
1221
+ });
1222
+ server.tool("publishNote", "Publish a signed note to Nostr relays", publishNoteToolConfig, async ({ signedNote, relays }) => {
1223
+ try {
1224
+ const result = await publishNote(signedNote, relays);
1225
+ if (result.success) {
1226
+ let response = `Note published successfully!\n\n`;
1227
+ response += `${result.message}\n`;
1228
+ if (result.noteId) {
1229
+ response += `Note ID: ${result.noteId}\n`;
1230
+ }
1231
+ response += `Content: "${signedNote.content}"\n`;
1232
+ response += `Author: ${formatPubkey(signedNote.pubkey)}\n`;
1233
+ if (relays && relays.length > 0) {
1234
+ response += `Relays: ${relays.join(", ")}\n`;
1235
+ }
1236
+ return {
1237
+ content: [
1238
+ {
1239
+ type: "text",
1240
+ text: response,
1241
+ },
1242
+ ],
1243
+ };
1244
+ }
1245
+ else {
1246
+ return {
1247
+ content: [
1248
+ {
1249
+ type: "text",
1250
+ text: `Failed to publish note: ${result.message}`,
1251
+ },
1252
+ ],
1253
+ };
1254
+ }
1255
+ }
1256
+ catch (error) {
1257
+ console.error("Error in publishNote tool:", error);
1258
+ return {
1259
+ content: [
1260
+ {
1261
+ type: "text",
1262
+ text: `Error publishing note: ${error instanceof Error ? error.message : "Unknown error"}`,
1263
+ },
1264
+ ],
1265
+ };
1266
+ }
1267
+ });
1268
+ async function main() {
1269
+ const transport = new StdioServerTransport();
1270
+ await server.connect(transport);
1271
+ console.error("Nostr MCP Server running on stdio");
1272
+ }
1273
+ main().catch((error) => {
1274
+ console.error("Fatal error in main():", error);
1275
+ process.exit(1);
1276
+ });
1277
+ // Add handlers for unexpected termination
1278
+ process.on('uncaughtException', (error) => {
1279
+ console.error('Uncaught exception:', error);
1280
+ // Don't exit - keep the server running
1281
+ });
1282
+ process.on('unhandledRejection', (reason, promise) => {
1283
+ console.error('Unhandled rejection at:', promise, 'reason:', reason);
1284
+ // Don't exit - keep the server running
1285
+ });