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.
- package/README.md +37 -33
- package/build/__tests__/basic.test.js +2 -2
- package/build/__tests__/integration.test.js +2 -1
- package/build/__tests__/mocks.js +0 -10
- package/build/__tests__/nip19-conversion.test.js +1 -0
- package/build/__tests__/note-creation.test.js +1 -0
- package/build/__tests__/note-tools-functions.test.js +10 -5
- package/build/__tests__/note-tools-unit.test.js +1 -0
- package/build/__tests__/profile-notes-simple.test.js +1 -1
- package/build/__tests__/profile-postnote.test.js +1 -0
- package/build/__tests__/profile-tools.test.js +1 -0
- package/build/__tests__/websocket-integration.test.js +2 -1
- package/build/__tests__/zap-tools-simple.test.js +1 -1
- package/build/__tests__/zap-tools-tests.test.js +1 -0
- package/build/bun.setup.js +3 -0
- package/build/index.js +4 -45
- package/build/zap/zap-tools.js +0 -1
- package/package.json +12 -15
- package/build/__tests__/error-handling.test.js +0 -145
- package/build/__tests__/format-conversion.test.js +0 -137
- package/build/__tests__/nips-search.test.js +0 -109
- package/build/__tests__/relay-specification.test.js +0 -136
- package/build/__tests__/search-nips-simple.test.js +0 -96
- package/build/nips/nips-tools.js +0 -567
- package/build/nips-tools.js +0 -421
- package/build/note-tools.js +0 -53
- package/build/zap-tools.js +0 -989
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# Nostr MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/nostr-mcp-server)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
3
6
|
A Model Context Protocol (MCP) server that provides Nostr capabilities to LLMs like Claude.
|
|
4
7
|
|
|
5
8
|
https://github.com/user-attachments/assets/1d2d47d0-c61b-44e2-85be-5985d2a81c64
|
|
6
9
|
|
|
7
10
|
## Features
|
|
8
11
|
|
|
9
|
-
This server implements
|
|
12
|
+
This server implements 17 tools for interacting with the Nostr network:
|
|
10
13
|
|
|
11
14
|
### Reading & Querying Tools
|
|
12
15
|
1. `getProfile`: Fetches a user's profile information by public key
|
|
@@ -15,26 +18,25 @@ This server implements 18 tools for interacting with the Nostr network:
|
|
|
15
18
|
4. `getReceivedZaps`: Fetches zaps received by a user, including detailed payment information
|
|
16
19
|
5. `getSentZaps`: Fetches zaps sent by a user, including detailed payment information
|
|
17
20
|
6. `getAllZaps`: Fetches both sent and received zaps for a user, clearly labeled with direction and totals
|
|
18
|
-
7. `searchNips`: Search through Nostr Implementation Possibilities (NIPs) with relevance scoring
|
|
19
21
|
|
|
20
22
|
### Identity & Profile Management Tools
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
7. `createKeypair`: Generate new Nostr keypairs in hex and/or npub/nsec format
|
|
24
|
+
8. `createProfile`: Create a new Nostr profile (kind 0 event) with metadata
|
|
25
|
+
9. `updateProfile`: Update an existing Nostr profile with new metadata
|
|
24
26
|
|
|
25
|
-
### Note Creation & Publishing Tools
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
### Note Creation & Publishing Tools
|
|
28
|
+
10. `createNote`: Create unsigned kind 1 note events with specified content and tags
|
|
29
|
+
11. `signNote`: Sign note events with a private key, generating cryptographically valid signatures
|
|
30
|
+
12. `publishNote`: Publish signed notes to specified Nostr relays
|
|
31
|
+
13. `postNote`: All-in-one authenticated note posting using an existing private key (nsec/hex)
|
|
30
32
|
|
|
31
33
|
### Anonymous Tools
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
14. `sendAnonymousZap`: Prepare an anonymous zap to a profile or event, generating a lightning invoice for payment
|
|
35
|
+
15. `postAnonymousNote`: Post an anonymous note using a randomly generated one-time keypair
|
|
34
36
|
|
|
35
37
|
### NIP-19 Entity Tools
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
16. `convertNip19`: Convert between different NIP-19 entity formats (hex, npub, nsec, note, nprofile, nevent, naddr)
|
|
39
|
+
17. `analyzeNip19`: Analyze and decode any NIP-19 entity to understand its type and contents
|
|
38
40
|
|
|
39
41
|
All tools fully support both hex public keys and npub format, with user-friendly display of Nostr identifiers.
|
|
40
42
|
|
|
@@ -46,7 +48,21 @@ All tools fully support both hex public keys and npub format, with user-friendly
|
|
|
46
48
|
npm install -g nostr-mcp-server
|
|
47
49
|
```
|
|
48
50
|
|
|
49
|
-
### Option 2: Install from source
|
|
51
|
+
### Option 2: Install from source (using Bun - Recommended)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Clone the repository
|
|
55
|
+
git clone https://github.com/austinkelsay/nostr-mcp-server.git
|
|
56
|
+
cd nostr-mcp-server
|
|
57
|
+
|
|
58
|
+
# Install dependencies
|
|
59
|
+
bun install
|
|
60
|
+
|
|
61
|
+
# Build the project
|
|
62
|
+
bun run build
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Option 3: Install from source (using npm)
|
|
50
66
|
|
|
51
67
|
```bash
|
|
52
68
|
# Clone the repository
|
|
@@ -214,9 +230,6 @@ Once configured, you can ask Claude to use the Nostr tools by making requests li
|
|
|
214
230
|
- "How many zaps has npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8 received?"
|
|
215
231
|
- "Show me the zaps sent by npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8"
|
|
216
232
|
- "Show me all zaps (both sent and received) for npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8"
|
|
217
|
-
- "Search for NIPs about zaps"
|
|
218
|
-
- "What NIPs are related to long-form content?"
|
|
219
|
-
- "Show me NIP-23 with full content"
|
|
220
233
|
|
|
221
234
|
### Identity & Profile Management
|
|
222
235
|
- "Generate a new Nostr keypair for me"
|
|
@@ -349,12 +362,6 @@ For zap queries, you can enable extra validation and debugging:
|
|
|
349
362
|
|
|
350
363
|
- "Show me all zaps for npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8 with validation and debug enabled"
|
|
351
364
|
|
|
352
|
-
For NIP searches, you can control the number of results and include full content:
|
|
353
|
-
|
|
354
|
-
- "Search for NIPs about zaps with full content"
|
|
355
|
-
- "Show me the top 5 NIPs about relays"
|
|
356
|
-
- "What NIPs are related to encryption? Show me 15 results"
|
|
357
|
-
|
|
358
365
|
## Limitations
|
|
359
366
|
|
|
360
367
|
- The server has a default 8-second timeout for queries to prevent hanging
|
|
@@ -418,7 +425,6 @@ To modify or extend this server:
|
|
|
418
425
|
- `profile/profile-tools.ts`: Identity management, keypair generation, profile creation ([Documentation](./profile/README.md))
|
|
419
426
|
- `note/note-tools.ts`: Note creation, signing, publishing, and reading functionality ([Documentation](./note/README.md))
|
|
420
427
|
- `zap/zap-tools.ts`: Zap-related functionality ([Documentation](./zap/README.md))
|
|
421
|
-
- `nips/nips-tools.ts`: Functions for searching NIPs ([Documentation](./nips/README.md))
|
|
422
428
|
- `utils/`: Shared utility functions
|
|
423
429
|
- `constants.ts`: Global constants and relay configurations
|
|
424
430
|
- `conversion.ts`: NIP-19 entity conversion utilities (hex/npub/nprofile/nevent/naddr)
|
|
@@ -427,23 +433,23 @@ To modify or extend this server:
|
|
|
427
433
|
- `pool.ts`: Nostr connection pool management
|
|
428
434
|
- `ephemeral-relay.ts`: In-memory Nostr relay for testing
|
|
429
435
|
|
|
430
|
-
2. Run `npm run build` to compile
|
|
436
|
+
2. Run `bun run build` (or `npm run build`) to compile
|
|
431
437
|
|
|
432
438
|
3. Restart Claude for Desktop or Cursor to pick up your changes
|
|
433
439
|
|
|
434
440
|
## Testing
|
|
435
441
|
|
|
436
|
-
We've implemented a comprehensive test suite using
|
|
442
|
+
We've implemented a comprehensive test suite using Bun's native test runner to test both basic functionality and integration with the Nostr protocol:
|
|
437
443
|
|
|
438
444
|
```bash
|
|
439
|
-
# Run all tests
|
|
440
|
-
|
|
445
|
+
# Run all tests (using Bun - Recommended)
|
|
446
|
+
bun test
|
|
441
447
|
|
|
442
448
|
# Run a specific test file
|
|
443
|
-
|
|
449
|
+
bun test __tests__/basic.test.ts
|
|
444
450
|
|
|
445
451
|
# Run integration tests
|
|
446
|
-
|
|
452
|
+
bun test __tests__/integration.test.ts
|
|
447
453
|
```
|
|
448
454
|
|
|
449
455
|
The test suite includes:
|
|
@@ -458,7 +464,6 @@ The test suite includes:
|
|
|
458
464
|
- `profile-postnote.test.ts` - Tests authenticated note posting with existing private keys
|
|
459
465
|
- `zap-tools-simple.test.ts` - Tests zap processing and anonymous zap preparation
|
|
460
466
|
- `zap-tools-tests.test.ts` - Tests zap validation, parsing, and direction determination
|
|
461
|
-
- `search-nips-simple.test.ts` - Tests NIPs search functionality with relevance scoring
|
|
462
467
|
- `nip19-conversion.test.ts` - Tests NIP-19 entity conversion and analysis (28 test cases)
|
|
463
468
|
|
|
464
469
|
### Integration Tests
|
|
@@ -492,7 +497,6 @@ The codebase is organized into modules:
|
|
|
492
497
|
- [`profile/`](./profile/README.md): Identity management, keypair generation, and profile creation
|
|
493
498
|
- [`note/`](./note/README.md): Note creation, signing, publishing, and reading functionality
|
|
494
499
|
- [`zap/`](./zap/README.md): Zap handling and anonymous zapping
|
|
495
|
-
- [`nips/`](./nips/README.md): NIPs search and caching functionality
|
|
496
500
|
- Common utilities in the `utils/` directory
|
|
497
501
|
|
|
498
502
|
This modular structure makes the codebase more maintainable, reduces duplication, and enables easier feature extensions. For detailed information about each module's features and implementation, see their respective documentation.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, test, expect, mock } from 'bun:test';
|
|
2
2
|
// Mock the formatProfile function
|
|
3
|
-
const mockFormatProfile =
|
|
3
|
+
const mockFormatProfile = mock((profile) => {
|
|
4
4
|
const content = typeof profile.content === 'string'
|
|
5
5
|
? JSON.parse(profile.content)
|
|
6
6
|
: profile.content;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
1
2
|
import { NostrRelay } from '../utils/ephemeral-relay.js';
|
|
2
3
|
import { schnorr } from '@noble/curves/secp256k1';
|
|
3
|
-
import { randomBytes } from 'crypto';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
4
5
|
import { sha256 } from '@noble/hashes/sha256';
|
|
5
6
|
// Generate a keypair for testing
|
|
6
7
|
function generatePrivateKey() {
|
package/build/__tests__/mocks.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Mock Nostr events and utility functions for testing
|
|
2
|
-
import { jest } from '@jest/globals';
|
|
3
1
|
export const MOCK_HEX_PUBKEY = '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e';
|
|
4
2
|
export const MOCK_NPUB = 'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6';
|
|
5
3
|
export const mockProfile = {
|
|
@@ -64,14 +62,6 @@ export const mockZapReceipt = {
|
|
|
64
62
|
content: '',
|
|
65
63
|
sig: 'mock_signature'
|
|
66
64
|
};
|
|
67
|
-
// Mock pool functions
|
|
68
|
-
export const mockPool = {
|
|
69
|
-
get: jest.fn(),
|
|
70
|
-
querySync: jest.fn(),
|
|
71
|
-
close: jest.fn()
|
|
72
|
-
};
|
|
73
|
-
// Mock for getFreshPool function
|
|
74
|
-
export const getFreshPoolMock = jest.fn().mockReturnValue(mockPool);
|
|
75
65
|
// Mock response for lightning service for anonymous zaps
|
|
76
66
|
export const mockLightningServiceResponse = {
|
|
77
67
|
callback: 'https://example.com/callback',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'bun:test';
|
|
1
2
|
import { convertNip19, analyzeNip19 } from '../utils/nip19-tools.js';
|
|
2
3
|
import { generateKeypair, encodePublicKey, encodePrivateKey, encodeNoteId, encodeProfile, encodeEvent, encodeAddress } from 'snstr';
|
|
3
4
|
describe('NIP-19 Conversion Tools', () => {
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mock, describe, it, expect, beforeAll, beforeEach } from 'bun:test';
|
|
2
2
|
import { generateKeypair } from 'snstr';
|
|
3
3
|
// Mock the pool to prevent real WebSocket connections
|
|
4
4
|
const mockPool = {
|
|
5
|
-
close:
|
|
6
|
-
publish:
|
|
5
|
+
close: mock(() => { }),
|
|
6
|
+
publish: mock(() => [
|
|
7
7
|
Promise.resolve({ success: true }),
|
|
8
8
|
Promise.resolve({ success: true })
|
|
9
9
|
])
|
|
10
10
|
};
|
|
11
11
|
// Mock the pool module directly
|
|
12
|
-
|
|
13
|
-
getFreshPool:
|
|
12
|
+
mock.module('../utils/pool.js', () => ({
|
|
13
|
+
getFreshPool: mock(() => mockPool)
|
|
14
14
|
}));
|
|
15
15
|
// Now import the functions that use the mocked module
|
|
16
16
|
import { formatProfile, formatNote, createNote, signNote, publishNote } from '../note/note-tools.js';
|
|
@@ -19,6 +19,11 @@ describe('Note Tools Functions', () => {
|
|
|
19
19
|
beforeAll(async () => {
|
|
20
20
|
testKeys = await generateKeypair();
|
|
21
21
|
});
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Reset mock state between tests
|
|
24
|
+
mockPool.close.mockClear();
|
|
25
|
+
mockPool.publish.mockClear();
|
|
26
|
+
});
|
|
22
27
|
describe('formatProfile', () => {
|
|
23
28
|
it('should format a complete profile', () => {
|
|
24
29
|
const profileEvent = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';
|
|
1
2
|
import { NostrRelay } from '../utils/ephemeral-relay.js';
|
|
2
3
|
import { schnorr } from '@noble/curves/secp256k1';
|
|
3
|
-
import { randomBytes } from 'crypto';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
4
5
|
import { sha256 } from '@noble/hashes/sha256';
|
|
5
6
|
import WebSocket from 'ws';
|
|
6
7
|
// Generate a keypair for testing
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
1
2
|
// Mock the processZapReceipt function
|
|
2
3
|
const processZapReceipt = (receipt, targetPubkey) => {
|
|
3
4
|
const targetTag = receipt.tags?.find(tag => tag[0] === 'p' && tag[1] === targetPubkey);
|
|
@@ -69,4 +70,3 @@ describe('Zap Tools Functions', () => {
|
|
|
69
70
|
expect(result.comment).toBe('');
|
|
70
71
|
});
|
|
71
72
|
});
|
|
72
|
-
export {};
|
package/build/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
4
|
import WebSocket from "ws";
|
|
6
|
-
import { searchNips, formatNipResult } from "./nips/nips-tools.js";
|
|
7
5
|
import { KINDS, DEFAULT_RELAYS, QUERY_TIMEOUT, getFreshPool, npubToHex, formatPubkey } from "./utils/index.js";
|
|
8
6
|
import { formatZapReceipt, processZapReceipt, validateZapReceipt, prepareAnonymousZap, sendAnonymousZapToolConfig, getReceivedZapsToolConfig, getSentZapsToolConfig, getAllZapsToolConfig } from "./zap/zap-tools.js";
|
|
9
7
|
import { formatProfile, formatNote, getProfileToolConfig, getKind1NotesToolConfig, getLongFormNotesToolConfig, postAnonymousNoteToolConfig, postAnonymousNote, createNote, signNote, publishNote, createNoteToolConfig, signNoteToolConfig, publishNoteToolConfig } from "./note/note-tools.js";
|
|
10
8
|
import { createKeypair, createProfile, updateProfile, postNote, createKeypairToolConfig, createProfileToolConfig, updateProfileToolConfig, postNoteToolConfig } from "./profile/profile-tools.js";
|
|
11
9
|
import { convertNip19, analyzeNip19, convertNip19ToolConfig, analyzeNip19ToolConfig, formatAnalysisResult } from "./utils/nip19-tools.js";
|
|
12
|
-
// Set WebSocket implementation for Node.js
|
|
13
|
-
globalThis.WebSocket
|
|
10
|
+
// Set WebSocket implementation for Node.js (Bun has native WebSocket)
|
|
11
|
+
if (typeof globalThis.WebSocket === 'undefined') {
|
|
12
|
+
globalThis.WebSocket = WebSocket;
|
|
13
|
+
}
|
|
14
14
|
// Create server instance
|
|
15
15
|
const server = new McpServer({
|
|
16
16
|
name: "nostr",
|
|
@@ -682,47 +682,6 @@ server.tool("getLongFormNotes", "Get long-form notes (kind 30023) by public key"
|
|
|
682
682
|
await pool.close();
|
|
683
683
|
}
|
|
684
684
|
});
|
|
685
|
-
server.tool("searchNips", "Search through Nostr Implementation Possibilities (NIPs)", {
|
|
686
|
-
query: z.string().describe("Search query to find relevant NIPs"),
|
|
687
|
-
limit: z.number().min(1).max(50).default(10).describe("Maximum number of results to return"),
|
|
688
|
-
includeContent: z.boolean().default(false).describe("Whether to include the full content of each NIP in the results"),
|
|
689
|
-
}, async ({ query, limit, includeContent }) => {
|
|
690
|
-
try {
|
|
691
|
-
console.error(`Searching NIPs for: "${query}"`);
|
|
692
|
-
const results = await searchNips(query, limit);
|
|
693
|
-
if (results.length === 0) {
|
|
694
|
-
return {
|
|
695
|
-
content: [
|
|
696
|
-
{
|
|
697
|
-
type: "text",
|
|
698
|
-
text: `No NIPs found matching "${query}". Try different search terms or check the NIPs repository for the latest updates.`,
|
|
699
|
-
},
|
|
700
|
-
],
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
// Format results using the new formatter
|
|
704
|
-
const formattedResults = results.map(result => formatNipResult(result, includeContent)).join("\n\n");
|
|
705
|
-
return {
|
|
706
|
-
content: [
|
|
707
|
-
{
|
|
708
|
-
type: "text",
|
|
709
|
-
text: `Found ${results.length} matching NIPs:\n\n${formattedResults}`,
|
|
710
|
-
},
|
|
711
|
-
],
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
catch (error) {
|
|
715
|
-
console.error("Error searching NIPs:", error);
|
|
716
|
-
return {
|
|
717
|
-
content: [
|
|
718
|
-
{
|
|
719
|
-
type: "text",
|
|
720
|
-
text: `Error searching NIPs: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
721
|
-
},
|
|
722
|
-
],
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
});
|
|
726
685
|
server.tool("sendAnonymousZap", "Prepare an anonymous zap to a profile or event", sendAnonymousZapToolConfig, async ({ target, amountSats, comment, relays }) => {
|
|
727
686
|
// Use supplied relays or defaults
|
|
728
687
|
const relaysToUse = relays || DEFAULT_RELAYS;
|
package/build/zap/zap-tools.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { decode } from "light-bolt11-decoder";
|
|
3
3
|
import { decode as nip19decode, generateKeypair, createEvent, getEventHash, signEvent } from "snstr";
|
|
4
|
-
import fetch from "node-fetch";
|
|
5
4
|
import { KINDS, DEFAULT_RELAYS, FALLBACK_RELAYS, QUERY_TIMEOUT, getFreshPool, npubToHex, hexToNpub } from "../utils/index.js";
|
|
6
5
|
// Simple cache implementation for zap receipts
|
|
7
6
|
export class ZapCache {
|
package/package.json
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nostr-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) server that provides Nostr capabilities to LLMs like Claude",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"nostr-mcp-server": "
|
|
8
|
+
"nostr-mcp-server": "build/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "bun test",
|
|
12
12
|
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
|
13
|
-
"start": "
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"release:
|
|
17
|
-
"release:
|
|
13
|
+
"start": "bun build/index.js",
|
|
14
|
+
"start:node": "node build/index.js",
|
|
15
|
+
"prerelease": "bun test && bun run build",
|
|
16
|
+
"release:patch": "bun run prerelease && npm version patch",
|
|
17
|
+
"release:minor": "bun run prerelease && npm version minor",
|
|
18
|
+
"release:major": "bun run prerelease && npm version major"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"build"
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"claude",
|
|
27
28
|
"llm"
|
|
28
29
|
],
|
|
29
|
-
"author": "Austin Kelsay <austinkelsay@
|
|
30
|
+
"author": "Austin Kelsay <austinkelsay@protonmail.com>",
|
|
30
31
|
"repository": {
|
|
31
32
|
"type": "git",
|
|
32
33
|
"url": "git+https://github.com/AustinKelsay/nostr-mcp-server.git"
|
|
@@ -35,25 +36,21 @@
|
|
|
35
36
|
"url": "https://github.com/AustinKelsay/nostr-mcp-server/issues"
|
|
36
37
|
},
|
|
37
38
|
"homepage": "https://github.com/AustinKelsay/nostr-mcp-server#readme",
|
|
38
|
-
"license": "
|
|
39
|
+
"license": "MIT",
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
41
42
|
"@noble/curves": "^1.8.2",
|
|
42
43
|
"@noble/hashes": "^1.7.2",
|
|
43
44
|
"@scure/base": "^1.2.4",
|
|
44
|
-
"@types/node-fetch": "^2.6.12",
|
|
45
45
|
"light-bolt11-decoder": "^3.2.0",
|
|
46
|
-
"node-fetch": "^3.3.2",
|
|
47
46
|
"snstr": "^0.1.0",
|
|
48
47
|
"ws": "^8.16.1",
|
|
49
48
|
"zod": "^3.24.2"
|
|
50
49
|
},
|
|
51
50
|
"devDependencies": {
|
|
52
|
-
"@types/
|
|
51
|
+
"@types/bun": "^1.1.14",
|
|
53
52
|
"@types/node": "^22.13.11",
|
|
54
53
|
"@types/ws": "^8.5.10",
|
|
55
|
-
"jest": "^29.7.0",
|
|
56
|
-
"ts-jest": "^29.3.2",
|
|
57
54
|
"typescript": "^5.8.2"
|
|
58
55
|
}
|
|
59
56
|
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, jest, beforeEach } from '@jest/globals';
|
|
2
|
-
// Mock the actual Nostr pool functionality
|
|
3
|
-
jest.mock('../utils/pool.js', () => {
|
|
4
|
-
return {
|
|
5
|
-
getRelayPool: jest.fn(() => ({
|
|
6
|
-
connect: jest.fn(),
|
|
7
|
-
close: jest.fn(),
|
|
8
|
-
subscribeMany: jest.fn(),
|
|
9
|
-
})),
|
|
10
|
-
};
|
|
11
|
-
});
|
|
12
|
-
// Mock setTimeout for testing timeouts
|
|
13
|
-
jest.useFakeTimers();
|
|
14
|
-
// Helper function for creating timed out promises
|
|
15
|
-
const createTimedOutPromise = () => new Promise((resolve, reject) => {
|
|
16
|
-
setTimeout(() => reject(new Error('Request timed out')), 10000);
|
|
17
|
-
});
|
|
18
|
-
// Mock getProfile that simulates various error scenarios
|
|
19
|
-
const mockGetProfile = jest.fn();
|
|
20
|
-
// Helpers for common error scenarios
|
|
21
|
-
const simulateTimeoutError = () => {
|
|
22
|
-
mockGetProfile.mockImplementationOnce(() => createTimedOutPromise());
|
|
23
|
-
};
|
|
24
|
-
const simulateNetworkError = () => {
|
|
25
|
-
mockGetProfile.mockImplementationOnce(() => Promise.reject(new Error('Failed to connect to relay')));
|
|
26
|
-
};
|
|
27
|
-
const simulateInvalidPubkey = () => {
|
|
28
|
-
mockGetProfile.mockImplementationOnce(() => Promise.reject(new Error('Invalid pubkey format')));
|
|
29
|
-
};
|
|
30
|
-
const simulateMalformedEvent = () => {
|
|
31
|
-
mockGetProfile.mockImplementationOnce(() => Promise.resolve({
|
|
32
|
-
error: 'Malformed event',
|
|
33
|
-
details: 'Event missing required signature'
|
|
34
|
-
}));
|
|
35
|
-
};
|
|
36
|
-
describe('Error Handling and Edge Cases', () => {
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
mockGetProfile.mockReset();
|
|
39
|
-
});
|
|
40
|
-
test('timeout handling', async () => {
|
|
41
|
-
simulateTimeoutError();
|
|
42
|
-
try {
|
|
43
|
-
jest.useFakeTimers();
|
|
44
|
-
const profilePromise = mockGetProfile('valid-pubkey');
|
|
45
|
-
// Fast-forward time to trigger timeout
|
|
46
|
-
jest.advanceTimersByTime(10000);
|
|
47
|
-
await profilePromise;
|
|
48
|
-
fail('Expected promise to reject with timeout error');
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
expect(error.message).toContain('timed out');
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
test('invalid pubkey format handling', async () => {
|
|
55
|
-
simulateInvalidPubkey();
|
|
56
|
-
try {
|
|
57
|
-
await mockGetProfile('invalid-pubkey-format');
|
|
58
|
-
fail('Expected promise to reject with invalid pubkey error');
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
expect(error.message).toContain('Invalid pubkey');
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
test('network error handling', async () => {
|
|
65
|
-
simulateNetworkError();
|
|
66
|
-
try {
|
|
67
|
-
await mockGetProfile('valid-pubkey');
|
|
68
|
-
fail('Expected promise to reject with network error');
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
expect(error.message).toContain('Failed to connect');
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
test('malformed event handling', async () => {
|
|
75
|
-
simulateMalformedEvent();
|
|
76
|
-
const result = await mockGetProfile('valid-pubkey');
|
|
77
|
-
expect(result.error).toBeDefined();
|
|
78
|
-
expect(result.error).toContain('Malformed event');
|
|
79
|
-
});
|
|
80
|
-
test('empty pubkey handling', async () => {
|
|
81
|
-
// Empty string pubkey
|
|
82
|
-
simulateInvalidPubkey();
|
|
83
|
-
try {
|
|
84
|
-
await mockGetProfile('');
|
|
85
|
-
fail('Expected promise to reject with invalid pubkey error');
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
expect(error.message).toContain('Invalid pubkey');
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
test('extremely long pubkey handling', async () => {
|
|
92
|
-
// Extremely long input should be rejected
|
|
93
|
-
simulateInvalidPubkey();
|
|
94
|
-
const veryLongPubkey = 'a'.repeat(1000);
|
|
95
|
-
try {
|
|
96
|
-
await mockGetProfile(veryLongPubkey);
|
|
97
|
-
fail('Expected promise to reject with invalid pubkey error');
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
expect(error.message).toContain('Invalid pubkey');
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
test('special character handling in pubkey', async () => {
|
|
104
|
-
// Pubkey with special characters
|
|
105
|
-
simulateInvalidPubkey();
|
|
106
|
-
try {
|
|
107
|
-
await mockGetProfile('npub1<script>alert("xss")</script>');
|
|
108
|
-
fail('Expected promise to reject with invalid pubkey error');
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
expect(error.message).toContain('Invalid pubkey');
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
test('null or undefined pubkey handling', async () => {
|
|
115
|
-
// Null pubkey
|
|
116
|
-
simulateInvalidPubkey();
|
|
117
|
-
try {
|
|
118
|
-
await mockGetProfile(null);
|
|
119
|
-
fail('Expected promise to reject with invalid pubkey error');
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
expect(error.message).toContain('Invalid pubkey');
|
|
123
|
-
}
|
|
124
|
-
// Undefined pubkey
|
|
125
|
-
simulateInvalidPubkey();
|
|
126
|
-
try {
|
|
127
|
-
await mockGetProfile(undefined);
|
|
128
|
-
fail('Expected promise to reject with invalid pubkey error');
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
expect(error.message).toContain('Invalid pubkey');
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
test('all relays failing scenario', async () => {
|
|
135
|
-
// Simulate all relays failing
|
|
136
|
-
simulateNetworkError();
|
|
137
|
-
try {
|
|
138
|
-
await mockGetProfile('valid-pubkey', { relays: ['wss://relay1.example.com', 'wss://relay2.example.com'] });
|
|
139
|
-
fail('Expected promise to reject when all relays fail');
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
expect(error.message).toContain('Failed to connect');
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
});
|