Skip to main content
Glama

Nostr MCP Server

by AustinKelsay
// Helper functions that extract the zap handler logic for testing import { npubToHex, formatPubkey, getFreshPool, DEFAULT_RELAYS, KINDS, QUERY_TIMEOUT } from '../utils/index.js'; import { formatZapReceipt, processZapReceipt, validateZapReceipt, prepareAnonymousZap } from '../zap/zap-tools.js'; // Extracted handler for getReceivedZaps tool export const getReceivedZapsHandler = 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((_, 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 } ); const zaps = await Promise.race([zapsPromise, timeoutPromise]); 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 = []; let invalidCount = 0; for (const zap of zaps) { try { // Process the zap receipt with context of the target pubkey const processedZap = processZapReceipt(zap, 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); } }; // Extracted handler for getSentZaps tool export const getSentZapsHandler = 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((_, 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 } ); let potentialSentZaps = []; try { potentialSentZaps = await Promise.race([directSentZapsPromise, timeoutPromise]); if (debug) console.error(`Direct #P tag query returned ${potentialSentZaps.length} results`); } catch (e) { if (debug) console.error(`Direct #P tag query failed: ${e instanceof Error ? e.message : String(e)}`); } // Process and filter zaps let processedZaps = []; 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, 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); } } } if (processedZaps.length === 0) { return { content: [ { type: "text", text: `No zaps sent by ${displayPubkey} were found.`, }, ], }; } // 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); 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); } }; // Extracted handler for getAllZaps tool export const getAllZapsHandler = 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((_, 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), } ) ]; // Execute the query const results = await Promise.allSettled(fetchPromises); // Collect all zaps from successful queries const allZaps = []; results.forEach((result) => { if (result.status === 'fulfilled') { const zaps = result.value; allZaps.push(...zaps); } }); if (allZaps.length === 0) { return { content: [ { type: "text", text: `No zaps found for ${displayPubkey}. Try specifying different relays that might have the data.`, }, ], }; } // For testing purposes, we'll mock a simplified version const sentZaps = [{ amountSats: 50, direction: 'sent' }]; const receivedZaps = [{ amountSats: 100, direction: 'received' }]; const selfZaps = []; // 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); // 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 most recent zaps:\n` ].join("\n"); return { content: [ { type: "text", text: `${summary}\nMocked zap data for testing`, }, ], }; } 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); } }; // Extracted handler for sendAnonymousZap tool export const sendAnonymousZapHandler = async ({ target, amountSats, comment, relays }) => { // Use supplied relays or defaults const relaysToUse = relays || DEFAULT_RELAYS; try { console.error(`Preparing anonymous zap to ${target} for ${amountSats} sats`); // Prepare the anonymous zap const zapResult = await prepareAnonymousZap(target, amountSats, comment, relaysToUse); if (!zapResult || !zapResult.success) { return { content: [ { type: "text", text: `Failed to prepare anonymous zap: ${zapResult?.message || "Unknown error"}`, }, ], }; } return { content: [ { type: "text", 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.`, }, ], }; } catch (error) { console.error("Error in sendAnonymousZap tool:", error); let errorMessage = error instanceof Error ? error.message : "Unknown error"; return { content: [ { type: "text", text: `Error preparing anonymous zap: ${errorMessage}`, }, ], }; } }; // Default export for the module export default { getReceivedZapsHandler, getSentZapsHandler, getAllZapsHandler, sendAnonymousZapHandler };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AustinKelsay/nostr-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server