Nostr MCP Server

by AustinKelsay
Verified
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import WebSocket from "ws"; import { SimplePool } from "nostr-tools/pool"; import * as nip19 from "nostr-tools/nip19"; import { searchNips, formatNipResult } from "./nips-tools.js"; import { NostrEvent, NostrFilter, KINDS, ZapReceipt, ZapCache, zapCache, DEFAULT_RELAYS, QUERY_TIMEOUT, getFreshPool, npubToHex, hexToNpub, formatPubkey, formatZapReceipt, processZapReceipt, validateZapReceipt, parseZapRequestData, getReceivedZapsToolConfig, getSentZapsToolConfig, getAllZapsToolConfig } from "./zap-tools.js"; // Set WebSocket implementation for Node.js (globalThis as any).WebSocket = WebSocket; // Create server instance const server = new McpServer({ name: "nostr", version: "1.0.0", }); // Helper function to format profile data function formatProfile(profile: NostrEvent): string { if (!profile) return "No profile found"; let metadata: any = {}; try { metadata = profile.content ? JSON.parse(profile.content) : {}; } catch (e) { console.error("Error parsing profile metadata:", e); } return [ `Name: ${metadata.name || "Unknown"}`, `Display Name: ${metadata.display_name || metadata.displayName || metadata.name || "Unknown"}`, `About: ${metadata.about || "No about information"}`, `NIP-05: ${metadata.nip05 || "Not set"}`, `Picture: ${metadata.picture || "No picture"}`, `Website: ${metadata.website || "No website"}`, `Created At: ${new Date(profile.created_at * 1000).toISOString()}`, ].join("\n"); } // Helper function to format note content function formatNote(note: NostrEvent): string { if (!note) return ""; const created = new Date(note.created_at * 1000).toLocaleString(); return [ `ID: ${note.id}`, `Created: ${created}`, `Content: ${note.content}`, `---`, ].join("\n"); } // Register Nostr tools server.tool( "getProfile", "Get a Nostr profile by public key", { pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"), relays: z.array(z.string()).optional().describe("Optional list of relays to query"), }, async ({ pubkey, relays }) => { // Convert npub to hex if needed const hexPubkey = npubToHex(pubkey); if (!hexPubkey) { return { content: [ { type: "text", text: "Invalid public key format. Please provide a valid hex pubkey or npub.", }, ], }; } // Generate a friendly display version of the pubkey const displayPubkey = formatPubkey(hexPubkey); const relaysToUse = relays || DEFAULT_RELAYS; // Create a fresh pool for this request const pool = getFreshPool(); try { console.error(`Fetching profile for ${hexPubkey} from ${relaysToUse.join(", ")}`); // Create a timeout promise const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new Error("Timeout")), QUERY_TIMEOUT); }); // Create a query promise for profile (kind 0) const profilePromise = pool.get( relaysToUse, { kinds: [KINDS.Metadata], authors: [hexPubkey], } as NostrFilter ); // Race the promises const profile = await Promise.race([profilePromise, timeoutPromise]) as NostrEvent; if (!profile) { return { content: [ { type: "text", text: `No profile found for ${displayPubkey}`, }, ], }; } const formatted = formatProfile(profile); return { content: [ { type: "text", text: `Profile for ${displayPubkey}:\n\n${formatted}`, }, ], }; } catch (error) { console.error("Error fetching profile:", error); return { content: [ { type: "text", text: `Error fetching profile for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`, }, ], }; } finally { // Clean up any subscriptions and close the pool pool.close(relaysToUse); } }, ); server.tool( "getKind1Notes", "Get text notes (kind 1) by public key", { pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"), limit: z.number().min(1).max(100).default(10).describe("Maximum number of notes to fetch"), relays: z.array(z.string()).optional().describe("Optional list of relays to query"), }, async ({ pubkey, limit, relays }) => { // Convert npub to hex if needed const hexPubkey = npubToHex(pubkey); if (!hexPubkey) { return { content: [ { type: "text", text: "Invalid public key format. Please provide a valid hex pubkey or npub.", }, ], }; } // Generate a friendly display version of the pubkey const displayPubkey = formatPubkey(hexPubkey); const relaysToUse = relays || DEFAULT_RELAYS; // Create a fresh pool for this request const pool = getFreshPool(); try { console.error(`Fetching kind 1 notes for ${hexPubkey} from ${relaysToUse.join(", ")}`); // Use the querySync method with a timeout const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new Error("Timeout")), QUERY_TIMEOUT); }); const notesPromise = pool.querySync( relaysToUse, { kinds: [KINDS.Text], authors: [hexPubkey], limit, } as NostrFilter ); const notes = await Promise.race([notesPromise, timeoutPromise]) as NostrEvent[]; if (!notes || notes.length === 0) { return { content: [ { type: "text", text: `No notes found for ${displayPubkey}`, }, ], }; } // Sort notes by created_at in descending order (newest first) notes.sort((a, b) => b.created_at - a.created_at); const formattedNotes = notes.map(formatNote).join("\n"); return { content: [ { type: "text", text: `Found ${notes.length} notes from ${displayPubkey}:\n\n${formattedNotes}`, }, ], }; } catch (error) { console.error("Error fetching notes:", error); return { content: [ { type: "text", text: `Error fetching notes for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`, }, ], }; } finally { // Clean up any subscriptions and close the pool pool.close(relaysToUse); } }, ); server.tool( "getReceivedZaps", "Get zaps received by a public key", getReceivedZapsToolConfig, async ({ pubkey, limit, relays, validateReceipts, debug }) => { // Convert npub to hex if needed const hexPubkey = npubToHex(pubkey); if (!hexPubkey) { return { content: [ { type: "text", text: "Invalid public key format. Please provide a valid hex pubkey or npub.", }, ], }; } // Generate a friendly display version of the pubkey const displayPubkey = formatPubkey(hexPubkey); const relaysToUse = relays || DEFAULT_RELAYS; // Create a fresh pool for this request const pool = getFreshPool(); try { console.error(`Fetching zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`); // Use the querySync method with a timeout const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new Error("Timeout")), QUERY_TIMEOUT); }); // Use the proper filter with lowercase 'p' tag which indicates recipient const zapsPromise = pool.querySync( relaysToUse, { kinds: [KINDS.ZapReceipt], "#p": [hexPubkey], // lowercase 'p' for recipient limit: Math.ceil(limit * 1.5), // Fetch a bit more to account for potential invalid zaps } as NostrFilter ); const zaps = await Promise.race([zapsPromise, timeoutPromise]) as NostrEvent[]; if (!zaps || zaps.length === 0) { return { content: [ { type: "text", text: `No zaps found for ${displayPubkey}`, }, ], }; } if (debug) { console.error(`Retrieved ${zaps.length} raw zap receipts`); } // Process and optionally validate zaps let processedZaps: any[] = []; let invalidCount = 0; for (const zap of zaps) { try { // Process the zap receipt with context of the target pubkey const processedZap = processZapReceipt(zap as ZapReceipt, hexPubkey); // Skip zaps that aren't actually received by this pubkey if (processedZap.direction !== 'received' && processedZap.direction !== 'self') { if (debug) { console.error(`Skipping zap ${zap.id.slice(0, 8)}... with direction ${processedZap.direction}`); } continue; } // Validate if requested if (validateReceipts) { const validationResult = validateZapReceipt(zap); if (!validationResult.valid) { if (debug) { console.error(`Invalid zap receipt ${zap.id.slice(0, 8)}...: ${validationResult.reason}`); } invalidCount++; continue; } } processedZaps.push(processedZap); } catch (error) { if (debug) { console.error(`Error processing zap ${zap.id.slice(0, 8)}...`, error); } } } if (processedZaps.length === 0) { let message = `No valid zaps found for ${displayPubkey}`; if (invalidCount > 0) { message += ` (${invalidCount} invalid zaps were filtered out)`; } return { content: [ { type: "text", text: message, }, ], }; } // Sort zaps by created_at in descending order (newest first) processedZaps.sort((a, b) => b.created_at - a.created_at); // Limit to requested number processedZaps = processedZaps.slice(0, limit); // Calculate total sats received const totalSats = processedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0); const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n"); return { content: [ { type: "text", text: `Found ${processedZaps.length} zaps received by ${displayPubkey}.\nTotal received: ${totalSats} sats\n\n${formattedZaps}`, }, ], }; } catch (error) { console.error("Error fetching zaps:", error); return { content: [ { type: "text", text: `Error fetching zaps for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`, }, ], }; } finally { // Clean up any subscriptions and close the pool pool.close(relaysToUse); } }, ); server.tool( "getSentZaps", "Get zaps sent by a public key", getSentZapsToolConfig, async ({ pubkey, limit, relays, validateReceipts, debug }) => { // Convert npub to hex if needed const hexPubkey = npubToHex(pubkey); if (!hexPubkey) { return { content: [ { type: "text", text: "Invalid public key format. Please provide a valid hex pubkey or npub.", }, ], }; } // Generate a friendly display version of the pubkey const displayPubkey = formatPubkey(hexPubkey); const relaysToUse = relays || DEFAULT_RELAYS; // Create a fresh pool for this request const pool = getFreshPool(); try { console.error(`Fetching sent zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`); // Use the querySync method with a timeout const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new Error("Timeout")), QUERY_TIMEOUT); }); // First try the direct and correct approach: query with uppercase 'P' tag (NIP-57) if (debug) console.error("Trying direct query with #P tag..."); const directSentZapsPromise = pool.querySync( relaysToUse, { kinds: [KINDS.ZapReceipt], "#P": [hexPubkey], // uppercase 'P' for sender limit: Math.ceil(limit * 1.5), // Fetch a bit more to account for potential invalid zaps } as NostrFilter ); let potentialSentZaps: NostrEvent[] = []; try { potentialSentZaps = await Promise.race([directSentZapsPromise, timeoutPromise]) as NostrEvent[]; if (debug) console.error(`Direct #P tag query returned ${potentialSentZaps.length} results`); } catch (e: unknown) { if (debug) console.error(`Direct #P tag query failed: ${e instanceof Error ? e.message : String(e)}`); } // If the direct query didn't return enough results, try the fallback method if (!potentialSentZaps || potentialSentZaps.length < limit) { if (debug) console.error("Direct query yielded insufficient results, trying fallback approach..."); // Try a fallback approach - fetch a larger set of zap receipts const zapsPromise = pool.querySync( relaysToUse, { kinds: [KINDS.ZapReceipt], limit: Math.max(limit * 10, 100), // Get a larger sample } as NostrFilter ); const additionalZaps = await Promise.race([zapsPromise, timeoutPromise]) as NostrEvent[]; if (debug) { console.error(`Retrieved ${additionalZaps?.length || 0} additional zap receipts to analyze`); } if (additionalZaps && additionalZaps.length > 0) { // Add these to our potential sent zaps potentialSentZaps = [...potentialSentZaps, ...additionalZaps]; } } if (!potentialSentZaps || potentialSentZaps.length === 0) { return { content: [ { type: "text", text: "No zap receipts found to analyze", }, ], }; } // Process and filter zaps let processedZaps: any[] = []; let invalidCount = 0; let nonSentCount = 0; if (debug) { console.error(`Processing ${potentialSentZaps.length} potential sent zaps...`); } // Process each zap to determine if it was sent by the target pubkey for (const zap of potentialSentZaps) { try { // Process the zap receipt with context of the target pubkey const processedZap = processZapReceipt(zap as ZapReceipt, hexPubkey); // Skip zaps that aren't sent by this pubkey if (processedZap.direction !== 'sent' && processedZap.direction !== 'self') { if (debug) { console.error(`Skipping zap ${zap.id.slice(0, 8)}... with direction ${processedZap.direction}`); } nonSentCount++; continue; } // Validate if requested if (validateReceipts) { const validationResult = validateZapReceipt(zap); if (!validationResult.valid) { if (debug) { console.error(`Invalid zap receipt ${zap.id.slice(0, 8)}...: ${validationResult.reason}`); } invalidCount++; continue; } } processedZaps.push(processedZap); } catch (error) { if (debug) { console.error(`Error processing zap ${zap.id.slice(0, 8)}...`, error); } } } // Deduplicate by zap ID const uniqueZaps = new Map<string, any>(); processedZaps.forEach(zap => uniqueZaps.set(zap.id, zap)); processedZaps = Array.from(uniqueZaps.values()); if (processedZaps.length === 0) { let message = `No zaps sent by ${displayPubkey} were found.`; if (invalidCount > 0 || nonSentCount > 0) { message += ` (${invalidCount} invalid zaps and ${nonSentCount} non-sent zaps were filtered out)`; } 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"; return { content: [ { type: "text", text: message, }, ], }; } // Sort zaps by created_at in descending order (newest first) processedZaps.sort((a, b) => b.created_at - a.created_at); // Limit to requested number processedZaps = processedZaps.slice(0, limit); // Calculate total sats sent const totalSats = processedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0); // For debugging, examine the first zap in detail if (debug && processedZaps.length > 0) { const firstZap = processedZaps[0]; console.error("Sample sent zap:", JSON.stringify(firstZap, null, 2)); } const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n"); return { content: [ { type: "text", text: `Found ${processedZaps.length} zaps sent by ${displayPubkey}.\nTotal sent: ${totalSats} sats\n\n${formattedZaps}`, }, ], }; } catch (error) { console.error("Error fetching sent zaps:", error); return { content: [ { type: "text", text: `Error fetching sent zaps for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`, }, ], }; } finally { // Clean up any subscriptions and close the pool pool.close(relaysToUse); } }, ); server.tool( "getAllZaps", "Get all zaps (sent and received) for a public key", getAllZapsToolConfig, async ({ pubkey, limit, relays, validateReceipts, debug }) => { // Convert npub to hex if needed const hexPubkey = npubToHex(pubkey); if (!hexPubkey) { return { content: [ { type: "text", text: "Invalid public key format. Please provide a valid hex pubkey or npub.", }, ], }; } // Generate a friendly display version of the pubkey const displayPubkey = formatPubkey(hexPubkey); const relaysToUse = relays || DEFAULT_RELAYS; // Create a fresh pool for this request const pool = getFreshPool(); try { console.error(`Fetching all zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`); // Use a more efficient approach: fetch all potentially relevant zaps in parallel const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new Error("Timeout")), QUERY_TIMEOUT); }); // Prepare all required queries in parallel to reduce total time const fetchPromises = [ // 1. Fetch received zaps (lowercase 'p' tag) pool.querySync( relaysToUse, { kinds: [KINDS.ZapReceipt], "#p": [hexPubkey], limit: Math.ceil(limit * 1.5), } as NostrFilter ), // 2. Fetch sent zaps (uppercase 'P' tag) pool.querySync( relaysToUse, { kinds: [KINDS.ZapReceipt], "#P": [hexPubkey], limit: Math.ceil(limit * 1.5), } as NostrFilter ) ]; // Add a general query if we're in debug mode or need more comprehensive results if (debug) { fetchPromises.push( pool.querySync( relaysToUse, { kinds: [KINDS.ZapReceipt], limit: Math.max(limit * 5, 50), } as NostrFilter ) ); } // Execute all queries in parallel const results = await Promise.allSettled(fetchPromises); // Collect all zaps from successful queries const allZaps: NostrEvent[] = []; results.forEach((result, index) => { if (result.status === 'fulfilled') { const zaps = result.value as NostrEvent[]; if (debug) { const queryTypes = ['Received', 'Sent', 'General']; console.error(`${queryTypes[index]} query returned ${zaps.length} results`); } allZaps.push(...zaps); } else if (debug) { const queryTypes = ['Received', 'Sent', 'General']; console.error(`${queryTypes[index]} query failed:`, result.reason); } }); if (allZaps.length === 0) { return { content: [ { type: "text", text: `No zaps found for ${displayPubkey}. Try specifying different relays that might have the data.`, }, ], }; } if (debug) { console.error(`Retrieved ${allZaps.length} total zaps before deduplication`); } // Deduplicate by zap ID const uniqueZapsMap = new Map<string, NostrEvent>(); allZaps.forEach(zap => uniqueZapsMap.set(zap.id, zap)); const uniqueZaps = Array.from(uniqueZapsMap.values()); if (debug) { console.error(`Deduplicated to ${uniqueZaps.length} unique zaps`); } // Process each zap to determine its relevance to the target pubkey let processedZaps: any[] = []; let invalidCount = 0; let irrelevantCount = 0; for (const zap of uniqueZaps) { try { // Process the zap with the target pubkey as context const processedZap = processZapReceipt(zap as ZapReceipt, hexPubkey); // Skip zaps that are neither sent nor received by this pubkey if (processedZap.direction === 'unknown') { if (debug) { console.error(`Skipping irrelevant zap ${zap.id.slice(0, 8)}...`); } irrelevantCount++; continue; } // Validate if requested if (validateReceipts) { const validationResult = validateZapReceipt(zap); if (!validationResult.valid) { if (debug) { console.error(`Invalid zap receipt ${zap.id.slice(0, 8)}...: ${validationResult.reason}`); } invalidCount++; continue; } } processedZaps.push(processedZap); } catch (error) { if (debug) { console.error(`Error processing zap ${zap.id.slice(0, 8)}...`, error); } } } if (processedZaps.length === 0) { let message = `No relevant zaps found for ${displayPubkey}.`; if (invalidCount > 0 || irrelevantCount > 0) { message += ` (${invalidCount} invalid zaps and ${irrelevantCount} irrelevant zaps were filtered out)`; } return { content: [ { type: "text", text: message, }, ], }; } // Sort zaps by created_at in descending order (newest first) processedZaps.sort((a, b) => b.created_at - a.created_at); // Calculate statistics: sent, received, and self zaps const sentZaps = processedZaps.filter(zap => zap.direction === 'sent'); const receivedZaps = processedZaps.filter(zap => zap.direction === 'received'); const selfZaps = processedZaps.filter(zap => zap.direction === 'self'); // Calculate total sats const totalSent = sentZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0); const totalReceived = receivedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0); const totalSelfZaps = selfZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0); // Limit to requested number for display processedZaps = processedZaps.slice(0, limit); // Format the zaps with the pubkey context const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n"); // Prepare summary statistics const summary = [ `Zap Summary for ${displayPubkey}:`, `- ${sentZaps.length} zaps sent (${totalSent} sats)`, `- ${receivedZaps.length} zaps received (${totalReceived} sats)`, `- ${selfZaps.length} self-zaps (${totalSelfZaps} sats)`, `- Net balance: ${totalReceived - totalSent} sats`, `\nShowing ${processedZaps.length} most recent zaps:\n` ].join("\n"); return { content: [ { type: "text", text: `${summary}\n${formattedZaps}`, }, ], }; } catch (error) { console.error("Error fetching all zaps:", error); return { content: [ { type: "text", text: `Error fetching all zaps for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`, }, ], }; } finally { // Clean up any subscriptions and close the pool pool.close(relaysToUse); } }, ); server.tool( "getLongFormNotes", "Get long-form notes (kind 30023) by public key", { pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"), limit: z.number().min(1).max(100).default(10).describe("Maximum number of notes to fetch"), relays: z.array(z.string()).optional().describe("Optional list of relays to query"), }, async ({ pubkey, limit, relays }) => { // Convert npub to hex if needed const hexPubkey = npubToHex(pubkey); if (!hexPubkey) { return { content: [ { type: "text", text: "Invalid public key format. Please provide a valid hex pubkey or npub.", }, ], }; } // Generate a friendly display version of the pubkey const displayPubkey = formatPubkey(hexPubkey); const relaysToUse = relays || DEFAULT_RELAYS; // Create a fresh pool for this request const pool = getFreshPool(); try { console.error(`Fetching long-form notes for ${hexPubkey} from ${relaysToUse.join(", ")}`); // Use the querySync method with a timeout const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new Error("Timeout")), QUERY_TIMEOUT); }); const notesPromise = pool.querySync( relaysToUse, { kinds: [30023], // NIP-23 long-form content authors: [hexPubkey], limit, } as NostrFilter ); const notes = await Promise.race([notesPromise, timeoutPromise]) as NostrEvent[]; if (!notes || notes.length === 0) { return { content: [ { type: "text", text: `No long-form notes found for ${displayPubkey}`, }, ], }; } // Sort notes by created_at in descending order (newest first) notes.sort((a, b) => b.created_at - a.created_at); // Format each note with enhanced metadata const formattedNotes = notes.map(note => { // Extract metadata from tags const title = note.tags.find(tag => tag[0] === "title")?.[1] || "Untitled"; const image = note.tags.find(tag => tag[0] === "image")?.[1]; const summary = note.tags.find(tag => tag[0] === "summary")?.[1]; const publishedAt = note.tags.find(tag => tag[0] === "published_at")?.[1]; const identifier = note.tags.find(tag => tag[0] === "d")?.[1]; // Format the output const lines = [ `Title: ${title}`, `Created: ${new Date(note.created_at * 1000).toLocaleString()}`, publishedAt ? `Published: ${new Date(parseInt(publishedAt) * 1000).toLocaleString()}` : null, image ? `Image: ${image}` : null, summary ? `Summary: ${summary}` : null, identifier ? `Identifier: ${identifier}` : null, `Content:`, note.content, `---`, ].filter(Boolean).join("\n"); return lines; }).join("\n\n"); return { content: [ { type: "text", text: `Found ${notes.length} long-form notes from ${displayPubkey}:\n\n${formattedNotes}`, }, ], }; } catch (error) { console.error("Error fetching long-form notes:", error); return { content: [ { type: "text", text: `Error fetching long-form notes for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`, }, ], }; } finally { // Clean up any subscriptions and close the pool pool.close(relaysToUse); } }, ); server.tool( "searchNips", "Search through Nostr Implementation Possibilities (NIPs)", { query: z.string().describe("Search query to find relevant NIPs"), limit: z.number().min(1).max(50).default(10).describe("Maximum number of results to return"), includeContent: z.boolean().default(false).describe("Whether to include the full content of each NIP in the results"), }, async ({ query, limit, includeContent }) => { try { console.error(`Searching NIPs for: "${query}"`); const results = await searchNips(query, limit); if (results.length === 0) { return { content: [ { type: "text", text: `No NIPs found matching "${query}". Try different search terms or check the NIPs repository for the latest updates.`, }, ], }; } // Format results using the new formatter const formattedResults = results.map(result => formatNipResult(result, includeContent)).join("\n\n"); return { content: [ { type: "text", text: `Found ${results.length} matching NIPs:\n\n${formattedResults}`, }, ], }; } catch (error) { console.error("Error searching NIPs:", error); return { content: [ { type: "text", text: `Error searching NIPs: ${error instanceof Error ? error.message : "Unknown error"}`, }, ], }; } }, ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Nostr MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); }); // Add handlers for unexpected termination process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); // Don't exit - keep the server running }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); // Don't exit - keep the server running });