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.
- package/LICENSE +21 -0
- package/README.md +498 -0
- package/build/__tests__/basic.test.js +87 -0
- package/build/__tests__/error-handling.test.js +145 -0
- package/build/__tests__/format-conversion.test.js +137 -0
- package/build/__tests__/integration.test.js +163 -0
- package/build/__tests__/mocks.js +109 -0
- package/build/__tests__/nip19-conversion.test.js +268 -0
- package/build/__tests__/nips-search.test.js +109 -0
- package/build/__tests__/note-creation.test.js +148 -0
- package/build/__tests__/note-tools-functions.test.js +173 -0
- package/build/__tests__/note-tools-unit.test.js +97 -0
- package/build/__tests__/profile-notes-simple.test.js +78 -0
- package/build/__tests__/profile-postnote.test.js +120 -0
- package/build/__tests__/profile-tools.test.js +90 -0
- package/build/__tests__/relay-specification.test.js +136 -0
- package/build/__tests__/search-nips-simple.test.js +96 -0
- package/build/__tests__/websocket-integration.test.js +257 -0
- package/build/__tests__/zap-tools-simple.test.js +72 -0
- package/build/__tests__/zap-tools-tests.test.js +197 -0
- package/build/index.js +1285 -0
- package/build/nips/nips-tools.js +567 -0
- package/build/nips-tools.js +421 -0
- package/build/note/note-tools.js +296 -0
- package/build/note-tools.js +53 -0
- package/build/profile/profile-tools.js +260 -0
- package/build/utils/constants.js +27 -0
- package/build/utils/conversion.js +332 -0
- package/build/utils/ephemeral-relay.js +438 -0
- package/build/utils/formatting.js +34 -0
- package/build/utils/index.js +6 -0
- package/build/utils/nip19-tools.js +117 -0
- package/build/utils/pool.js +55 -0
- package/build/zap/zap-tools.js +980 -0
- package/build/zap-tools.js +989 -0
- 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
|
+
});
|