nostr-mcp-server 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,137 +0,0 @@
1
- import { describe, expect, test } from '@jest/globals';
2
- import * as nip19 from 'nostr-tools/nip19';
3
- // Import local conversion utilities
4
- import { npubToHex, hexToNpub } from '../utils/conversion.js';
5
- describe('Nostr format conversion', () => {
6
- // Known test vectors - generate these dynamically to ensure they match
7
- const testHexPubkeys = [
8
- '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
9
- '63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed'
10
- ];
11
- // Generate matching npubs using the library directly
12
- const knownPairs = testHexPubkeys.map(hex => ({
13
- hex,
14
- npub: nip19.npubEncode(hex)
15
- }));
16
- // Generate test event IDs and their encoded forms
17
- const testEventIds = [
18
- '5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36',
19
- '9ae37aa68f48645127299e9595f545f942a8314455cbafd9d913ed19c7fc0462'
20
- ];
21
- const eventIdPairs = testEventIds.map(hex => ({
22
- hex,
23
- note: nip19.noteEncode(hex)
24
- }));
25
- test('hex to npub conversion', () => {
26
- knownPairs.forEach(pair => {
27
- expect(hexToNpub(pair.hex)).toBe(pair.npub);
28
- });
29
- });
30
- test('npub to hex conversion', () => {
31
- knownPairs.forEach(pair => {
32
- const result = npubToHex(pair.npub);
33
- expect(result).toBe(pair.hex);
34
- });
35
- });
36
- test('hex to note conversion using nostr-tools', () => {
37
- eventIdPairs.forEach(pair => {
38
- expect(nip19.noteEncode(pair.hex)).toBe(pair.note);
39
- });
40
- });
41
- test('note to hex conversion using nostr-tools', () => {
42
- eventIdPairs.forEach(pair => {
43
- const decoded = nip19.decode(pair.note);
44
- expect(decoded.type).toBe('note');
45
- expect(decoded.data).toBe(pair.hex);
46
- });
47
- });
48
- test('nevent encoding and decoding using nostr-tools', () => {
49
- const eventPointer = {
50
- id: '5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36',
51
- relays: ['wss://relay.example.com', 'wss://relay.nostr.org'],
52
- author: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
53
- kind: 1
54
- };
55
- const nevent = nip19.neventEncode(eventPointer);
56
- expect(nevent).toMatch(/^nevent1/);
57
- const decoded = nip19.decode(nevent);
58
- expect(decoded.type).toBe('nevent');
59
- const data = decoded.data;
60
- expect(data.id).toBe(eventPointer.id);
61
- expect(data.author).toBe(eventPointer.author);
62
- expect(data.kind).toBe(eventPointer.kind);
63
- expect(data.relays).toEqual(eventPointer.relays);
64
- });
65
- test('naddr encoding and decoding using nostr-tools', () => {
66
- const addressPointer = {
67
- identifier: 'test-article',
68
- pubkey: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
69
- kind: 30023,
70
- relays: ['wss://relay.example.com']
71
- };
72
- const naddr = nip19.naddrEncode(addressPointer);
73
- expect(naddr).toMatch(/^naddr1/);
74
- const decoded = nip19.decode(naddr);
75
- expect(decoded.type).toBe('naddr');
76
- const data = decoded.data;
77
- expect(data.identifier).toBe(addressPointer.identifier);
78
- expect(data.pubkey).toBe(addressPointer.pubkey);
79
- expect(data.kind).toBe(addressPointer.kind);
80
- expect(data.relays).toEqual(addressPointer.relays);
81
- });
82
- test('validate Nostr identifiers using regex', () => {
83
- // Valid identifiers
84
- expect(/^npub1[a-z0-9]{58}$/.test(knownPairs[0].npub)).toBe(true);
85
- expect(/^note1[a-z0-9]{58}$/.test(eventIdPairs[0].note)).toBe(true);
86
- // Invalid identifiers with correct regex patterns
87
- expect(/^npub1[a-z0-9]{58}$/.test('npub1invalid')).toBe(false);
88
- expect(/^note1[a-z0-9]{58}$/.test('note1invalid')).toBe(false);
89
- // Fix the regex patterns to be more specific
90
- expect(/^nevent1[a-z0-9]{58,}$/.test('nevent1invalid')).toBe(false);
91
- expect(/^naddr1[a-z0-9]{58,}$/.test('naddr1invalid')).toBe(false);
92
- });
93
- test('error handling for npub conversion', () => {
94
- expect(npubToHex('npub1invalid')).toBeNull();
95
- expect(hexToNpub('invalidhex')).toBeNull();
96
- });
97
- test('partial nevent data', () => {
98
- // Test with minimal data
99
- const minimalPointer = {
100
- id: '5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36'
101
- };
102
- const nevent = nip19.neventEncode(minimalPointer);
103
- expect(nevent).toMatch(/^nevent1/);
104
- const decoded = nip19.decode(nevent);
105
- expect(decoded.type).toBe('nevent');
106
- const data = decoded.data;
107
- expect(data.id).toBe(minimalPointer.id);
108
- expect(data.relays).toEqual([]);
109
- expect(data.author).toBeUndefined();
110
- expect(data.kind).toBeUndefined();
111
- });
112
- test('bech32 conversion limits', () => {
113
- // Test with very long relay list
114
- const manyRelays = Array(20).fill(0).map((_, i) => `wss://relay${i}.example.com`);
115
- const eventPointer = {
116
- id: '5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36',
117
- relays: manyRelays
118
- };
119
- const nevent = nip19.neventEncode(eventPointer);
120
- expect(nevent).toMatch(/^nevent1/);
121
- const decoded = nip19.decode(nevent);
122
- expect(decoded.type).toBe('nevent');
123
- const data = decoded.data;
124
- expect(data.id).toBe(eventPointer.id);
125
- expect(data.relays.length).toBe(manyRelays.length);
126
- });
127
- test('edge case: nostr URI handling', () => {
128
- // Generate a fresh npub
129
- const hex = '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d';
130
- const npub = nip19.npubEncode(hex);
131
- // Test with nostr: prefix
132
- const nostrUriNpub = 'nostr:' + npub;
133
- // When handling the nostr: prefix in our conversion functions
134
- const result = npubToHex(nostrUriNpub.replace('nostr:', ''));
135
- expect(result).toBe(hex);
136
- });
137
- });
@@ -1,109 +0,0 @@
1
- import { describe, expect, test, jest } from '@jest/globals';
2
- // Mock NIP data for testing
3
- const mockNips = [
4
- {
5
- id: '01',
6
- title: 'Basic Protocol Flow Description',
7
- content: 'This NIP defines the basic protocol flow between clients and relays...',
8
- keywords: ['protocol', 'relay', 'client', 'basic']
9
- },
10
- {
11
- id: '19',
12
- title: 'bech32-encoded entities',
13
- content: 'This NIP defines how to encode and reference different Nostr entities...',
14
- keywords: ['encoding', 'bech32', 'npub', 'note', 'nevent', 'naddr', 'identifier']
15
- },
16
- {
17
- id: '57',
18
- title: 'Lightning Zaps',
19
- content: 'This NIP defines how users can send zaps to each other using lightning...',
20
- keywords: ['lightning', 'zap', 'payment', 'tip', 'lnurl']
21
- }
22
- ];
23
- // Mock data for testing
24
- const mockSearchResults = (query, limit = 10, includeContent = false) => {
25
- // Simple relevance scoring algorithm for testing
26
- const results = mockNips.map(nip => {
27
- const titleMatches = (nip.title.toLowerCase().includes(query.toLowerCase()) ? 2 : 0);
28
- const keywordMatches = nip.keywords.filter(kw => kw.includes(query.toLowerCase())).length;
29
- const contentMatches = (nip.content.toLowerCase().includes(query.toLowerCase()) ? 1 : 0);
30
- const relevance = titleMatches + keywordMatches + contentMatches;
31
- return {
32
- id: nip.id,
33
- title: nip.title,
34
- relevance: relevance,
35
- content: includeContent ? nip.content : undefined
36
- };
37
- })
38
- .filter(result => result.relevance > 0)
39
- .sort((a, b) => b.relevance - a.relevance)
40
- .slice(0, limit);
41
- return results;
42
- };
43
- // Mock search function
44
- const searchNips = jest.fn((query, options = {}) => {
45
- const { limit = 10, includeContent = false } = options;
46
- return Promise.resolve(mockSearchResults(query, limit, includeContent));
47
- });
48
- describe('NIP Search Functionality', () => {
49
- beforeEach(() => {
50
- searchNips.mockClear();
51
- });
52
- test('basic search returns relevant results', async () => {
53
- const results = await searchNips('lightning');
54
- expect(results.length).toBeGreaterThan(0);
55
- expect(results[0].id).toBe('57'); // NIP-57 is most relevant for 'lightning'
56
- expect(results[0].title).toBe('Lightning Zaps');
57
- expect(results[0].relevance).toBeGreaterThan(0);
58
- expect(results[0].content).toBeUndefined(); // Content shouldn't be included by default
59
- });
60
- test('search with includeContent option', async () => {
61
- const results = await searchNips('protocol', { includeContent: true });
62
- expect(results.length).toBeGreaterThan(0);
63
- expect(results[0].id).toBe('01'); // NIP-01 is most relevant for 'protocol'
64
- expect(results[0].content).toBeDefined();
65
- expect(results[0].content).toContain('protocol flow');
66
- });
67
- test('search respects limit parameter', async () => {
68
- // Create a search term that matches multiple NIPs
69
- const results = await searchNips('n', { limit: 2 });
70
- // Should return only two results even though more match
71
- expect(results.length).toBe(2);
72
- });
73
- test('search with no matches returns empty array', async () => {
74
- const results = await searchNips('nonexistentterm');
75
- expect(results).toEqual([]);
76
- });
77
- test('search relevance sorting', async () => {
78
- // 'encoding' appears in both NIP-19 title/keywords and NIP-57 content, but NIP-19 should rank higher
79
- const results = await searchNips('encoding');
80
- expect(results.length).toBeGreaterThan(0);
81
- expect(results[0].id).toBe('19');
82
- });
83
- test('search is case insensitive', async () => {
84
- const lowerResults = await searchNips('lightning');
85
- const upperResults = await searchNips('LIGHTNING');
86
- const mixedResults = await searchNips('LiGhTnInG');
87
- expect(lowerResults.length).toBeGreaterThan(0);
88
- expect(upperResults.length).toBeGreaterThan(0);
89
- expect(mixedResults.length).toBeGreaterThan(0);
90
- // All should find the same results
91
- expect(lowerResults[0].id).toBe(upperResults[0].id);
92
- expect(lowerResults[0].id).toBe(mixedResults[0].id);
93
- });
94
- test('search works with partial word matches', async () => {
95
- const results = await searchNips('light');
96
- expect(results.length).toBeGreaterThan(0);
97
- expect(results[0].id).toBe('57'); // Should find 'lightning' in NIP-57
98
- });
99
- test('very large limit is handled gracefully', async () => {
100
- // Even with a large limit, should only return matches
101
- const results = await searchNips('protocol', { limit: 1000 });
102
- // Should not break or return more than the available matches
103
- expect(results.length).toBeLessThanOrEqual(mockNips.length);
104
- });
105
- test('zero limit returns empty array', async () => {
106
- const results = await searchNips('protocol', { limit: 0 });
107
- expect(results).toEqual([]);
108
- });
109
- });
@@ -1,136 +0,0 @@
1
- import { describe, expect, test, jest, beforeEach } from '@jest/globals';
2
- import { NostrRelay } from '../utils/ephemeral-relay.js';
3
- // Mock the getRelayPool function
4
- const mockGetRelayPool = jest.fn();
5
- // Define a mock implementation
6
- const createMockPool = (relays) => {
7
- return {
8
- connect: jest.fn(),
9
- close: jest.fn(),
10
- relayUrls: relays,
11
- subscribeMany: jest.fn().mockImplementation(() => {
12
- return {
13
- on: jest.fn(),
14
- off: jest.fn()
15
- };
16
- }),
17
- };
18
- };
19
- // Set up the mock implementation
20
- mockGetRelayPool.mockImplementation((relays) => createMockPool(relays || []));
21
- // Mock the entire module
22
- jest.mock('../utils/pool.js', () => {
23
- return {
24
- getRelayPool: (relays) => mockGetRelayPool(relays)
25
- };
26
- });
27
- // Create a helper to simulate fetching profile data
28
- const fetchProfileWithRelays = (pubkey, customRelays) => {
29
- // This simulates how the actual code would use the pool
30
- const relayPool = mockGetRelayPool(customRelays);
31
- // Track if we've connected to the pool
32
- relayPool.connect();
33
- // Simulate the subscription
34
- const sub = relayPool.subscribeMany([{ kinds: [0], authors: [pubkey] }]);
35
- // Return the relays that were used
36
- return {
37
- relaysUsed: relayPool.relayUrls,
38
- pool: relayPool
39
- };
40
- };
41
- // Define default relays for comparison
42
- const DEFAULT_RELAYS = [
43
- 'wss://relay.damus.io',
44
- 'wss://relay.nostr.band',
45
- 'wss://relay.primal.net',
46
- 'wss://nos.lol',
47
- 'wss://relay.current.fyi',
48
- 'wss://nostr.bitcoiner.social'
49
- ];
50
- describe('Custom Relay Specification', () => {
51
- beforeEach(() => {
52
- // Clear all mocks before each test
53
- jest.clearAllMocks();
54
- // Set the default mockGetRelayPool implementation to use DEFAULT_RELAYS when no custom relays
55
- mockGetRelayPool.mockImplementation((relays) => {
56
- const trackedRelays = relays && relays.length > 0 ? relays : DEFAULT_RELAYS;
57
- return createMockPool(trackedRelays);
58
- });
59
- });
60
- test('uses default relays when no custom relays specified', () => {
61
- const { relaysUsed } = fetchProfileWithRelays('test-pubkey');
62
- // When no custom relays are specified, should use defaults
63
- expect(relaysUsed).toEqual(DEFAULT_RELAYS);
64
- });
65
- test('uses custom relays when specified', () => {
66
- const customRelays = ['wss://custom1.example.com', 'wss://custom2.example.com'];
67
- const { relaysUsed } = fetchProfileWithRelays('test-pubkey', customRelays);
68
- // Should use exactly the custom relays
69
- expect(relaysUsed).toEqual(customRelays);
70
- expect(relaysUsed).not.toEqual(DEFAULT_RELAYS);
71
- });
72
- test('empty custom relays array falls back to defaults', () => {
73
- const { relaysUsed } = fetchProfileWithRelays('test-pubkey', []);
74
- // When empty array is provided, should fall back to defaults
75
- expect(relaysUsed).toEqual(DEFAULT_RELAYS);
76
- });
77
- test('maintains relay connection pool during operation', () => {
78
- const customRelays = ['wss://custom1.example.com', 'wss://custom2.example.com'];
79
- const { pool } = fetchProfileWithRelays('test-pubkey', customRelays);
80
- // The connect method should be called
81
- expect(pool.connect).toHaveBeenCalled();
82
- // The subscribeMany method should be called with appropriate filters
83
- expect(pool.subscribeMany).toHaveBeenCalledWith(expect.arrayContaining([
84
- expect.objectContaining({
85
- kinds: expect.arrayContaining([0]),
86
- authors: expect.arrayContaining(['test-pubkey'])
87
- })
88
- ]));
89
- });
90
- test('invalid relay URLs are filtered out', () => {
91
- // Create a special mock implementation for this test
92
- mockGetRelayPool.mockImplementationOnce((relays) => {
93
- // Filter invalid URLs (this simulates what the real implementation should do)
94
- const filteredRelays = relays ? relays.filter((url) => url.startsWith('wss://') && url.includes('.')) : [];
95
- return createMockPool(filteredRelays);
96
- });
97
- // Include some invalid relay URLs
98
- const mixedRelays = [
99
- 'wss://valid.example.com',
100
- 'invalid-url',
101
- 'http://not-secure.example.com', // Not WSS
102
- 'wss://another-valid.example.com'
103
- ];
104
- const { relaysUsed } = fetchProfileWithRelays('test-pubkey', mixedRelays);
105
- // Only valid WSS URLs should remain
106
- expect(relaysUsed).toContain('wss://valid.example.com');
107
- expect(relaysUsed).toContain('wss://another-valid.example.com');
108
- expect(relaysUsed).not.toContain('invalid-url');
109
- expect(relaysUsed).not.toContain('http://not-secure.example.com');
110
- });
111
- test('relay fallback behavior with ephemeral relay', async () => {
112
- // Create an ephemeral relay for testing - using port 9000 for test
113
- const relay = new NostrRelay(9000);
114
- // Create a request that specifies two relays:
115
- // 1. A non-existent relay that will fail
116
- // 2. Our ephemeral relay that will work
117
- const testRelays = [
118
- 'wss://non-existent-relay.example.com',
119
- relay.url
120
- ];
121
- // This is a more advanced test that would require actual implementation
122
- // of relay fallback logic. We'll outline the concept here.
123
- // In a real test with actual implementation:
124
- // 1. Send a request to the specified relays
125
- // 2. The first relay fails to connect
126
- // 3. The code falls back to the second relay
127
- // 4. We get a successful response from the second relay
128
- // For now, we'll just verify that both relays are attempted
129
- const { relaysUsed } = fetchProfileWithRelays('test-pubkey', testRelays);
130
- expect(relaysUsed).toEqual(testRelays);
131
- // In a full implementation, we would:
132
- // 1. Set up a callback for events from the ephemeral relay
133
- // 2. Publish a test profile to the ephemeral relay
134
- // 3. Verify we receive it even though the first relay fails
135
- });
136
- });
@@ -1,96 +0,0 @@
1
- import { searchNips } from '../nips/nips-tools.js';
2
- // Since the NIPs tool uses a real cache, we'll test with the actual cache behavior
3
- describe('Search NIPs Tool - Simple Tests', () => {
4
- describe('basic search functionality', () => {
5
- it('should search NIPs by keyword', async () => {
6
- // This will use the real cache if available, or fetch from GitHub
7
- const result = await searchNips('protocol', 5);
8
- expect(Array.isArray(result)).toBe(true);
9
- // Should find NIP-01 when searching for "protocol"
10
- const nip01 = result.find(searchResult => searchResult.nip.number === 1);
11
- if (nip01) {
12
- expect(nip01.nip.title.toLowerCase()).toContain('protocol');
13
- }
14
- });
15
- it('should return limited results', async () => {
16
- const limit = 3;
17
- const result = await searchNips('event', limit);
18
- expect(Array.isArray(result)).toBe(true);
19
- expect(result.length).toBeLessThanOrEqual(limit);
20
- });
21
- it('should return results with correct structure', async () => {
22
- // Use a more common search term that's likely to return results
23
- const result = await searchNips('event', 5);
24
- expect(Array.isArray(result)).toBe(true);
25
- // Each result should have the expected structure
26
- if (result.length > 0) {
27
- result.forEach(searchResult => {
28
- expect(searchResult).toHaveProperty('nip');
29
- expect(searchResult).toHaveProperty('relevance');
30
- expect(searchResult).toHaveProperty('matchedTerms');
31
- // Check the nested nip structure
32
- expect(searchResult.nip).toHaveProperty('number');
33
- expect(searchResult.nip).toHaveProperty('title');
34
- expect(searchResult.nip).toHaveProperty('status');
35
- });
36
- }
37
- });
38
- it('should handle no results gracefully', async () => {
39
- const result = await searchNips('xyz123nonexistentterm456', 10);
40
- expect(Array.isArray(result)).toBe(true);
41
- expect(result).toEqual([]);
42
- });
43
- it('should handle case-insensitive search', async () => {
44
- const result1 = await searchNips('PROTOCOL', 5);
45
- const result2 = await searchNips('protocol', 5);
46
- expect(Array.isArray(result1)).toBe(true);
47
- expect(Array.isArray(result2)).toBe(true);
48
- // Both searches should return results
49
- if (result1.length > 0 && result2.length > 0) {
50
- // The results should be similar (may not be identical due to scoring)
51
- expect(result1.length).toBeGreaterThan(0);
52
- expect(result2.length).toBeGreaterThan(0);
53
- }
54
- });
55
- });
56
- describe('search relevance', () => {
57
- it('should return relevant results for common terms', async () => {
58
- // Test with terms we know exist in NIPs
59
- const result = await searchNips('event', 5);
60
- expect(Array.isArray(result)).toBe(true);
61
- if (result.length > 0) {
62
- // At least one result should have a relevance score
63
- const hasRelevantResult = result.some(searchResult => searchResult.relevance > 0);
64
- expect(hasRelevantResult).toBe(true);
65
- // Results should be sorted by relevance score
66
- expect(result[0].relevance).toBeGreaterThan(0);
67
- }
68
- });
69
- it('should rank results by relevance score', async () => {
70
- const result = await searchNips('encryption', 10);
71
- expect(Array.isArray(result)).toBe(true);
72
- if (result.length > 1) {
73
- // Results should be sorted by relevance score (descending)
74
- for (let i = 1; i < result.length; i++) {
75
- if (result[i - 1].relevance !== undefined && result[i].relevance !== undefined) {
76
- expect(result[i - 1].relevance).toBeGreaterThanOrEqual(result[i].relevance);
77
- }
78
- }
79
- }
80
- });
81
- });
82
- describe('error handling', () => {
83
- it('should handle empty search query', async () => {
84
- const result = await searchNips('', 10);
85
- // Empty query should return empty array
86
- expect(Array.isArray(result)).toBe(true);
87
- expect(result).toEqual([]);
88
- });
89
- it('should handle very long search queries gracefully', async () => {
90
- const longQuery = 'a'.repeat(1000);
91
- const result = await searchNips(longQuery, 10);
92
- // Should return an array (possibly empty)
93
- expect(Array.isArray(result)).toBe(true);
94
- });
95
- });
96
- });