Skip to main content
Glama

Nostr MCP Server

by AustinKelsay
import { z } from "zod"; import { DEFAULT_RELAYS } from "../utils/index.js"; import { generateKeypair, createEvent, getEventHash, signEvent, decode as nip19decode, encodePublicKey, encodePrivateKey } from "snstr"; import { getFreshPool } from "../utils/index.js"; import { schnorr } from '@noble/curves/secp256k1'; // Schema for createKeypair tool export const createKeypairToolConfig = { format: z.enum(["both", "hex", "npub"]).default("both").describe("Format to return keys in: hex only, npub only, or both"), }; // Schema for createProfile tool export const createProfileToolConfig = { privateKey: z.string().describe("Private key to sign the profile with (hex format or nsec format)"), name: z.string().optional().describe("Display name for the profile"), about: z.string().optional().describe("About/bio text for the profile"), picture: z.string().optional().describe("URL to profile picture"), nip05: z.string().optional().describe("NIP-05 identifier (like email@domain.com)"), lud16: z.string().optional().describe("Lightning address for receiving payments"), lud06: z.string().optional().describe("LNURL for receiving payments"), website: z.string().optional().describe("Personal website URL"), relays: z.array(z.string()).optional().describe("Optional list of relays to publish to"), }; // Schema for updateProfile tool export const updateProfileToolConfig = { privateKey: z.string().describe("Private key to sign the profile with (hex format or nsec format)"), name: z.string().optional().describe("Display name for the profile"), about: z.string().optional().describe("About/bio text for the profile"), picture: z.string().optional().describe("URL to profile picture"), nip05: z.string().optional().describe("NIP-05 identifier (like email@domain.com)"), lud16: z.string().optional().describe("Lightning address for receiving payments"), lud06: z.string().optional().describe("LNURL for receiving payments"), website: z.string().optional().describe("Personal website URL"), relays: z.array(z.string()).optional().describe("Optional list of relays to publish to"), }; // Schema for postNote tool export const postNoteToolConfig = { privateKey: z.string().describe("Private key to sign the note with (hex format or nsec format)"), content: z.string().describe("Content of the note to post"), tags: z.array(z.array(z.string())).optional().describe("Optional tags to include with the note"), relays: z.array(z.string()).optional().describe("Optional list of relays to publish to"), }; // Helper function to convert private key to hex if nsec format function normalizePrivateKey(privateKey: string): string { if (privateKey.startsWith('nsec')) { // Validate nsec format before type assertion if (!/^nsec1[0-9a-z]+$/.test(privateKey)) { throw new Error('Invalid nsec format: must match pattern nsec1[0-9a-z]+'); } const decoded = nip19decode(privateKey as `${string}1${string}`); if (decoded.type !== 'nsec') { throw new Error('Invalid nsec format'); } return decoded.data; } // Validate hex format for non-nsec keys if (!/^[0-9a-f]{64}$/.test(privateKey)) { throw new Error('Invalid private key format: must be 64-character hex string or valid nsec format'); } return privateKey; } // Helper function to derive public key from private key function getPublicKeyFromPrivate(privateKey: string): string { return Buffer.from(schnorr.getPublicKey(privateKey)).toString('hex'); } /** * Generate a new Nostr keypair */ export async function createKeypair( format: "both" | "hex" | "npub" = "both" ): Promise<{ publicKey?: string, privateKey?: string, npub?: string, nsec?: string }> { try { // Generate a new keypair const keys = await generateKeypair(); const result: { publicKey?: string, privateKey?: string, npub?: string, nsec?: string } = {}; if (format === "hex" || format === "both") { result.publicKey = keys.publicKey; result.privateKey = keys.privateKey; } if (format === "npub" || format === "both") { result.npub = encodePublicKey(keys.publicKey); result.nsec = encodePrivateKey(keys.privateKey); } return result; } catch (error) { throw new Error(`Failed to generate keypair: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Create a new Nostr profile (kind 0 event) */ export async function createProfile( privateKey: string, profileData: { name?: string; about?: string; picture?: string; nip05?: string; lud16?: string; lud06?: string; website?: string; }, relays: string[] = DEFAULT_RELAYS ): Promise<{ success: boolean, message: string, eventId?: string, publicKey?: string }> { try { // Normalize private key const normalizedPrivateKey = normalizePrivateKey(privateKey); // Derive public key from private key const publicKey = getPublicKeyFromPrivate(normalizedPrivateKey); // Create profile metadata object const metadata: { name?: string; about?: string; picture?: string; nip05?: string; lud16?: string; lud06?: string; website?: string; } = {}; if (profileData.name) metadata.name = profileData.name; if (profileData.about) metadata.about = profileData.about; if (profileData.picture) metadata.picture = profileData.picture; if (profileData.nip05) metadata.nip05 = profileData.nip05; if (profileData.lud16) metadata.lud16 = profileData.lud16; if (profileData.lud06) metadata.lud06 = profileData.lud06; if (profileData.website) metadata.website = profileData.website; // Create a fresh pool for this request const pool = getFreshPool(relays); try { // Create the profile event template const profileTemplate = createEvent({ kind: 0, // kind 0 is profile metadata content: JSON.stringify(metadata), tags: [] }, publicKey); // Get event hash and sign it const eventId = await getEventHash(profileTemplate); const signature = await signEvent(eventId, normalizedPrivateKey); // Create complete signed event const signedProfile = { ...profileTemplate, id: eventId, sig: signature }; // If no relays specified, just return success with event creation if (relays.length === 0) { return { success: true, message: 'Profile event created successfully (no relays specified for publishing)', eventId: signedProfile.id, publicKey: publicKey, }; } // Publish to relays - pool.publish returns array of promises const pubPromises = pool.publish(relays, signedProfile); // Wait for all publish attempts to complete or timeout const results = await Promise.allSettled(pubPromises); // Check if at least one relay accepted the profile const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.success === true ).length; if (successCount === 0) { return { success: false, message: 'Failed to publish profile to any relay', }; } return { success: true, message: `Profile published to ${successCount}/${relays.length} relays`, eventId: signedProfile.id, publicKey: publicKey, }; } catch (error) { console.error("Error creating profile:", error); return { success: false, message: `Error creating profile: ${error instanceof Error ? error.message : "Unknown error"}`, }; } finally { // Clean up any subscriptions and close the pool await pool.close(); } } catch (error) { return { success: false, message: `Fatal error: ${error instanceof Error ? error.message : "Unknown error"}`, }; } } /** * Update an existing Nostr profile (kind 0 event) * This creates a new profile event that replaces the previous one */ export async function updateProfile( privateKey: string, profileData: { name?: string; about?: string; picture?: string; nip05?: string; lud16?: string; lud06?: string; website?: string; }, relays: string[] = DEFAULT_RELAYS ): Promise<{ success: boolean, message: string, eventId?: string, publicKey?: string }> { // For kind 0 events (profiles), updating is the same as creating // The newest event replaces the older one return createProfile(privateKey, profileData, relays); } /** * Post a note using an existing private key (authenticated posting) * This is a convenient all-in-one function that creates, signs, and publishes a note */ export async function postNote( privateKey: string, content: string, tags: string[][] = [], relays: string[] = DEFAULT_RELAYS ): Promise<{ success: boolean, message: string, noteId?: string, publicKey?: string }> { try { // console.log(`Preparing to post authenticated note to ${relays.join(", ")}`); // Normalize private key const normalizedPrivateKey = normalizePrivateKey(privateKey); // Derive public key from private key const publicKey = getPublicKeyFromPrivate(normalizedPrivateKey); // Create a fresh pool for this request const pool = getFreshPool(relays); try { // Create the note event template const noteTemplate = createEvent({ kind: 1, // kind 1 is a text note content, tags }, publicKey); // Get event hash and sign it const eventId = await getEventHash(noteTemplate); const signature = await signEvent(eventId, normalizedPrivateKey); // Create complete signed event const signedNote = { ...noteTemplate, id: eventId, sig: signature }; // If no relays specified, just return success with event creation if (relays.length === 0) { return { success: true, message: 'Note created and signed successfully (no relays specified for publishing)', noteId: signedNote.id, publicKey: publicKey, }; } // Publish to relays - pool.publish returns array of promises const pubPromises = pool.publish(relays, signedNote); // Wait for all publish attempts to complete or timeout const results = await Promise.allSettled(pubPromises); // Check if at least one relay accepted the note const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.success === true ).length; if (successCount === 0) { return { success: false, message: 'Failed to publish note to any relay', }; } return { success: true, message: `Note published to ${successCount}/${relays.length} relays`, noteId: signedNote.id, publicKey: publicKey, }; } catch (error) { console.error("Error posting note:", error); return { success: false, message: `Error posting note: ${error instanceof Error ? error.message : "Unknown error"}`, }; } finally { // Clean up any subscriptions and close the pool await pool.close(); } } catch (error) { return { success: false, message: `Fatal error: ${error instanceof Error ? error.message : "Unknown error"}`, }; } }

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