Skip to main content
Glama

Nostr MCP Server

by AustinKelsay
import { jest } from '@jest/globals'; import { NostrRelay } from '../utils/ephemeral-relay.js'; import { schnorr } from '@noble/curves/secp256k1'; import { randomBytes } from 'crypto'; import { sha256 } from '@noble/hashes/sha256'; // Generate a keypair for testing function generatePrivateKey(): string { return Buffer.from(randomBytes(32)).toString('hex'); } function getPublicKey(privateKey: string): string { return Buffer.from(schnorr.getPublicKey(privateKey)).toString('hex'); } // Create a signed event function createSignedEvent(privateKey: string, kind: number, content: string, tags: string[][] = []) { const pubkey = getPublicKey(privateKey); const created_at = Math.floor(Date.now() / 1000); // Create event const event = { pubkey, created_at, kind, tags, content, }; // Calculate event ID const eventData = JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content]); const id = Buffer.from(sha256(eventData)).toString('hex'); // Sign the event const sig = Buffer.from( schnorr.sign(id, privateKey) ).toString('hex'); return { ...event, id, sig }; } describe('Nostr Integration Tests', () => { let relay: NostrRelay; const testPort = 9700; let privateKey: string; let publicKey: string; beforeAll(async () => { privateKey = generatePrivateKey(); publicKey = getPublicKey(privateKey); // Start the ephemeral relay relay = new NostrRelay(testPort); await relay.start(); }); afterAll(async () => { // Shutdown relay await relay.close(); }); test('should publish and retrieve a profile', async () => { // Create a profile event (kind 0) const profileContent = JSON.stringify({ name: 'Test User', about: 'This is a test profile', picture: 'https://example.com/avatar.jpg' }); const profileEvent = createSignedEvent(privateKey, 0, profileContent); // Store it in the relay relay.store(profileEvent); // Verify it was stored expect(relay.cache.length).toBeGreaterThan(0); // Find the profile in the cache const retrievedProfile = relay.cache.find(event => event.kind === 0 && event.pubkey === publicKey ); // Verify profile data expect(retrievedProfile).toBeDefined(); expect(retrievedProfile?.id).toBe(profileEvent.id); // Parse the content const parsedContent = JSON.parse(retrievedProfile?.content || '{}'); expect(parsedContent.name).toBe('Test User'); expect(parsedContent.about).toBe('This is a test profile'); }); test('should publish and retrieve a text note', async () => { // Create a text note (kind 1) const noteContent = 'This is a test note posted from integration tests!'; const noteEvent = createSignedEvent(privateKey, 1, noteContent); // Store it in the relay relay.store(noteEvent); // Find the note in the cache const retrievedNote = relay.cache.find(event => event.kind === 1 && event.pubkey === publicKey && event.content === noteContent ); // Verify note data expect(retrievedNote).toBeDefined(); expect(retrievedNote?.id).toBe(noteEvent.id); expect(retrievedNote?.content).toBe(noteContent); }); test('should publish and retrieve a zap receipt', async () => { // Create a mock recipient public key const recipientKey = generatePrivateKey(); const recipientPubkey = getPublicKey(recipientKey); // Create zap receipt tags const zapTags = [ ['p', recipientPubkey], ['amount', '100000'], // 100 sats in millisats ['bolt11', 'lnbc100n...'], ['description', ''], ]; // Create a zap receipt (kind 9735) const zapEvent = createSignedEvent(privateKey, 9735, '', zapTags); // Store it in the relay relay.store(zapEvent); // Find the zap in the cache const retrievedZap = relay.cache.find(event => event.kind === 9735 && event.pubkey === publicKey ); // Verify zap data expect(retrievedZap).toBeDefined(); expect(retrievedZap?.id).toBe(zapEvent.id); // Verify zap tags const pTag = retrievedZap?.tags.find(tag => tag[0] === 'p'); const amountTag = retrievedZap?.tags.find(tag => tag[0] === 'amount'); expect(pTag?.[1]).toBe(recipientPubkey); expect(amountTag?.[1]).toBe('100000'); }); test('should filter events correctly', async () => { // Create multiple events of different kinds const profileEvent = createSignedEvent(privateKey, 0, JSON.stringify({ name: 'Filter Test' })); const textNote1 = createSignedEvent(privateKey, 1, 'Filter test note 1'); const textNote2 = createSignedEvent(privateKey, 1, 'Filter test note 2'); const reactionEvent = createSignedEvent(privateKey, 7, '+', [['e', 'fake-event-id']]); // Store all events relay.store(profileEvent); relay.store(textNote1); relay.store(textNote2); relay.store(reactionEvent); // Filter for just kind 1 events const textNotes = relay.cache.filter(event => event.kind === 1 && event.pubkey === publicKey ); // We should have at least 3 text notes (2 from this test plus 1 from earlier test) expect(textNotes.length).toBeGreaterThanOrEqual(3); // Filter for reaction events const reactions = relay.cache.filter(event => event.kind === 7 && event.pubkey === publicKey ); expect(reactions.length).toBeGreaterThanOrEqual(1); expect(reactions[0].content).toBe('+'); }); // The ephemeral-relay validates events during WebSocket communication, // but doesn't validate during direct store() calls - this test verifies this behavior test('should store events without validation when using direct store() method', () => { // Create a properly signed event const signedEvent = createSignedEvent(privateKey, 1, 'Verification test'); // Store it in the relay relay.store(signedEvent); // Create an event with invalid signature const invalidEvent = { pubkey: publicKey, created_at: Math.floor(Date.now() / 1000), kind: 1, tags: [], content: 'Invalid signature event', id: 'invalid_id_that_doesnt_match_content', sig: 'invalid_signature_0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' }; // Get the current cache size const cacheSizeBefore = relay.cache.length; // Store the invalid event (this should succeed since store() doesn't validate) relay.store(invalidEvent); // Cache size should increase since the invalid event should be added const cacheSizeAfter = relay.cache.length; // Verify the event was added (expected behavior for direct store calls) expect(cacheSizeAfter).toBe(cacheSizeBefore + 1); // Find the invalid event in the cache const invalidEventInCache = relay.cache.find(event => event.id === 'invalid_id_that_doesnt_match_content'); expect(invalidEventInCache).toBeDefined(); // Note: This confirms the current behavior, but in websocket-integration.test.ts we // verify that invalid events are properly rejected over WebSocket communication }); });

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