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'; import WebSocket from 'ws'; // 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('WebSocket Nostr Integration Tests', () => { let relay: NostrRelay; const testPort = 9800; let relayUrl: string; let ws: WebSocket; let privateKey: string; let publicKey: string; beforeAll(async () => { privateKey = generatePrivateKey(); publicKey = getPublicKey(privateKey); // Start the ephemeral relay relay = new NostrRelay(testPort); await relay.start(); relayUrl = `ws://localhost:${testPort}`; }); beforeEach(async () => { // Create a new WebSocket connection before each test const connectPromise = new Promise<void>((resolve, reject) => { ws = new WebSocket(relayUrl); ws.on('open', () => resolve()); ws.on('error', reject); }); await connectPromise; }); afterEach(() => { // Close WebSocket connection after each test if (ws && ws.readyState === WebSocket.OPEN) { ws.close(); } }); afterAll(async () => { // Shutdown relay await relay.close(); }); // Helper function to send a message and wait for response const sendAndWait = (message: any): Promise<any[]> => { return new Promise((resolve, reject) => { const responses: any[] = []; const responseHandler = (data: WebSocket.Data) => { try { const response = JSON.parse(data.toString()); responses.push(response); // EOSE or OK messages indicate we can resolve if ( (response[0] === 'EOSE' && response[1] === 'test-sub') || (response[0] === 'OK') ) { resolve(responses); ws.off('message', responseHandler); } } catch (e) { reject(e); } }; ws.on('message', responseHandler); ws.send(JSON.stringify(message)); // Add a timeout setTimeout(() => { resolve(responses); ws.off('message', responseHandler); }, 2000); }); }; test('should connect to the relay', () => { expect(ws.readyState).toBe(WebSocket.OPEN); }); test('should publish an event and get OK response', async () => { // Create a test event const event = createSignedEvent(privateKey, 1, 'WebSocket test note'); // Send EVENT message const responses = await sendAndWait(['EVENT', event]); // Check for OK response const okResponse = responses.find(resp => resp[0] === 'OK' && resp[1] === event.id); expect(okResponse).toBeDefined(); expect(okResponse[2]).toBe(true); // Success flag }); test('should publish an event and retrieve it with REQ', async () => { // Create a test event with unique content const uniqueContent = `WebSocket REQ test note ${Date.now()}`; const event = createSignedEvent(privateKey, 1, uniqueContent); // Send EVENT message await sendAndWait(['EVENT', event]); // Now send a REQ to get this event const subId = 'test-sub'; const responses = await sendAndWait([ 'REQ', subId, { kinds: [1], authors: [publicKey], } ]); // Check that we got an EVENT response with our event const eventResponse = responses.find(resp => resp[0] === 'EVENT' && resp[1] === subId && resp[2].content === uniqueContent ); expect(eventResponse).toBeDefined(); expect(eventResponse[2].id).toBe(event.id); // Check that we got an EOSE response const eoseResponse = responses.find(resp => resp[0] === 'EOSE' && resp[1] === subId); expect(eoseResponse).toBeDefined(); }); test('should handle multiple subscriptions', async () => { // Create events of different kinds const profileEvent = createSignedEvent( privateKey, 0, JSON.stringify({ name: 'WebSocket Test' }) ); const noteEvent = createSignedEvent( privateKey, 1, 'WebSocket multi-subscription test' ); // Publish both events await sendAndWait(['EVENT', profileEvent]); await sendAndWait(['EVENT', noteEvent]); // Subscribe to profiles only const profileSubId = 'profile-sub'; const profileResponses = await sendAndWait([ 'REQ', profileSubId, { kinds: [0], authors: [publicKey], } ]); // Subscribe to notes only const noteSubId = 'note-sub'; const noteResponses = await sendAndWait([ 'REQ', noteSubId, { kinds: [1], authors: [publicKey], } ]); // Check profile subscription got profile event const profileEventResponse = profileResponses.find(resp => resp[0] === 'EVENT' && resp[1] === profileSubId && resp[2].kind === 0 ); expect(profileEventResponse).toBeDefined(); // Check note subscription got note event const noteEventResponse = noteResponses.find(resp => resp[0] === 'EVENT' && resp[1] === noteSubId && resp[2].kind === 1 ); expect(noteEventResponse).toBeDefined(); }); test('should support subscription closing', async () => { // Create a test event const event = createSignedEvent(privateKey, 1, 'Subscription close test'); // Publish the event await sendAndWait(['EVENT', event]); // Create a subscription const subId = 'close-test-sub'; await sendAndWait([ 'REQ', subId, { kinds: [1], authors: [publicKey], } ]); // Close the subscription ws.send(JSON.stringify(['CLOSE', subId])); // Create a new subscription with the same ID // This should work if the previous subscription was properly closed const newResponses = await sendAndWait([ 'REQ', subId, { kinds: [1], authors: [publicKey], } ]); // Verify we got an EOSE for the new subscription const eoseResponse = newResponses.find(resp => resp[0] === 'EOSE' && resp[1] === subId); expect(eoseResponse).toBeDefined(); }); test('should reject events with invalid signatures or silently ignore them', async () => { // Create an event with invalid signature const invalidEvent = { pubkey: publicKey, created_at: Math.floor(Date.now() / 1000), kind: 1, tags: [], content: 'Event with invalid signature', id: Buffer.from(sha256(JSON.stringify([0, publicKey, Math.floor(Date.now() / 1000), 1, [], 'Event with invalid signature']))).toString('hex'), sig: 'invalid_signature_0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' }; // Send EVENT message with invalid signature const responses = await sendAndWait(['EVENT', invalidEvent]); // Check for OK response with failure flag, or no response which means silent rejection const okResponse = responses.find(resp => resp[0] === 'OK' && resp[1] === invalidEvent.id ); // If the relay responds to invalid events, it should be with failure if (okResponse) { expect(okResponse[2]).toBe(false); // Success flag should be false } // Now verify that a valid event works properly const validEvent = createSignedEvent(privateKey, 1, 'Event with valid signature'); // Send EVENT message with valid signature const validResponses = await sendAndWait(['EVENT', validEvent]); // Check for OK response with success flag const validOkResponse = validResponses.find(resp => resp[0] === 'OK' && resp[1] === validEvent.id ); expect(validOkResponse).toBeDefined(); expect(validOkResponse[2]).toBe(true); // Success flag should be true // Verify the valid event made it to the relay's cache const eventInCache = relay.cache.find(e => e.id === validEvent.id); expect(eventInCache).toBeDefined(); // Verify the invalid event didn't make it to the relay's cache const invalidEventInCache = relay.cache.find(e => e.id === invalidEvent.id); expect(invalidEventInCache).toBeUndefined(); }); });

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