nostr-bray 1.19.0 → 1.21.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 +123 -98
- package/dist/catalog.d.ts +35 -0
- package/dist/catalog.js +148 -0
- package/dist/cli.js +20 -5
- package/dist/identity/tools.js +11 -11
- package/dist/index.js +24 -13
- package/dist/relay/tools.js +6 -6
- package/dist/safety/tools.js +3 -3
- package/dist/social/tools.js +36 -36
- package/dist/trust/handlers.d.ts +3 -1
- package/dist/trust/handlers.js +7 -0
- package/dist/trust/tools.js +48 -22
- package/dist/util/tools.js +14 -14
- package/dist/zap/tools.js +26 -11
- package/llms-full.txt +95 -89
- package/llms.txt +15 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](./LICENSE)
|
|
9
9
|
[](./tsconfig.json)
|
|
10
10
|
|
|
11
|
-
An MCP server that gives AI agents a full Nostr identity — not just a key pair, but a hierarchical identity tree with personas, attestations, ring signatures, encrypted DMs, and duress detection.
|
|
11
|
+
An MCP server that gives AI agents a full Nostr identity — not just a key pair, but a hierarchical identity tree with personas, attestations, ring signatures, encrypted DMs, and duress detection. 77 tools across 10 groups.
|
|
12
12
|
|
|
13
13
|
## The Problem
|
|
14
14
|
|
|
@@ -16,6 +16,41 @@ AI agents interacting with Nostr today are handed a single key pair with no sepa
|
|
|
16
16
|
|
|
17
17
|
nostr-bray solves this with nsec-tree hierarchical derivation. A single master secret generates unlimited child identities, each with its own key pair, purpose, and relay set. Private keys are zeroed from memory on eviction. Agents can switch personas mid-conversation, prove they control the master without revealing the derivation path, and activate a duress identity if compromised.
|
|
18
18
|
|
|
19
|
+
## How It Works
|
|
20
|
+
|
|
21
|
+
Every tool operates as the "active identity." Derive a persona, switch to it, and everything you do is signed by that persona's key — cryptographically unlinkable to the master unless you publish a proof.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
identity-derive-persona("work") → npub1abc...
|
|
25
|
+
identity-switch("work") → now operating as work persona
|
|
26
|
+
social-post("Hello from work!") → signed by npub1abc...
|
|
27
|
+
identity-switch("master") → back to master
|
|
28
|
+
social-post("Back to main") → signed by master npub
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This is not just key management — it is context isolation. Each persona has its own relay set, its own contact list, and its own attestation chain. Compromise one and the others remain intact.
|
|
32
|
+
|
|
33
|
+
## Comparison
|
|
34
|
+
|
|
35
|
+
| Feature | nostr-bray | nak | nostr-mcp | nostr-tools |
|
|
36
|
+
|---------|-----------|-----|-----------|-------------|
|
|
37
|
+
| MCP server | 77 tools | [via `nak mcp`](https://github.com/fiatjaf/nak) | 5–23 tools | — |
|
|
38
|
+
| CLI | 77 commands | ✓ (mature) | — | — |
|
|
39
|
+
| Hierarchical identity (nsec-tree) | ✓ | — | — | — |
|
|
40
|
+
| Persona switching | ✓ | — | — | — |
|
|
41
|
+
| Ring signatures (anonymous proofs) | ✓ | — | — | — |
|
|
42
|
+
| Shamir backup (BIP-39 words) | ✓ | — | — | — |
|
|
43
|
+
| Duress detection | ✓ | — | — | — |
|
|
44
|
+
| NIP-VA attestations (kind 31000) | ✓ | — | — | — |
|
|
45
|
+
| Linkage proofs (blind/full) | ✓ | — | — | — |
|
|
46
|
+
| NIP-46 bunker (server + client) | ✓ | ✓ (client) | partial | — |
|
|
47
|
+
| NIP-17 encrypted DMs | ✓ | ✓ | ✓ | ✓ |
|
|
48
|
+
| NWC Lightning payments | ✓ | — | — | — |
|
|
49
|
+
| Blossom media | ✓ | ✓ | — | — |
|
|
50
|
+
| NIP-29 groups | ✓ | — | — | — |
|
|
51
|
+
| Tor routing (SOCKS5h) | ✓ | — | — | — |
|
|
52
|
+
| 329 tests, 96% coverage | ✓ | — | — | — |
|
|
53
|
+
|
|
19
54
|
## Quick Start — CLI
|
|
20
55
|
|
|
21
56
|
```bash
|
|
@@ -68,143 +103,133 @@ Or with a secret file (recommended):
|
|
|
68
103
|
|
|
69
104
|
## Tool Groups
|
|
70
105
|
|
|
71
|
-
### Identity (
|
|
106
|
+
### Identity (12 tools) — create, derive, switch, prove, backup, and migrate Nostr identities
|
|
72
107
|
|
|
73
108
|
| Tool | Description |
|
|
74
109
|
|------|-------------|
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
|
|
87
|
-
|
|
110
|
+
| `whoami` | Returns the active identity's npub |
|
|
111
|
+
| `identity-create` | Generate a fresh identity with BIP-39 mnemonic |
|
|
112
|
+
| `identity-derive` | Derive a child identity by purpose and index |
|
|
113
|
+
| `identity-derive-persona` | Derive a named persona (work, personal, anonymous) |
|
|
114
|
+
| `identity-switch` | Switch active identity — all tools operate as the new identity |
|
|
115
|
+
| `identity-list` | List all known identities (public info only) |
|
|
116
|
+
| `identity-prove` | Create blind/full linkage proof |
|
|
117
|
+
| `identity-backup-shamir` | Split master secret into Shamir shard files |
|
|
118
|
+
| `identity-restore-shamir` | Reconstruct secret from shard files |
|
|
119
|
+
| `identity-backup` | Fetch profile, contacts, relay list as portable bundle |
|
|
120
|
+
| `identity-restore` | Re-sign migratable events under the active identity |
|
|
121
|
+
| `identity-migrate` | Full migration with preview, confirmation, and linkage proof |
|
|
122
|
+
|
|
123
|
+
### Social (14 tools) — post, reply, react, DM, follow, and read feeds
|
|
88
124
|
|
|
89
125
|
| Tool | Description |
|
|
90
126
|
|------|-------------|
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
|
|
106
|
-
### Trust (11 tools)
|
|
127
|
+
| `social-post` | Post a text note (kind 1) |
|
|
128
|
+
| `social-reply` | Reply with correct e-tag and p-tag threading |
|
|
129
|
+
| `social-react` | React to an event (kind 7) |
|
|
130
|
+
| `social-delete` | Request deletion of your event (kind 5) |
|
|
131
|
+
| `social-repost` | Repost/boost an event (kind 6) |
|
|
132
|
+
| `social-profile-get` | Fetch and parse a kind 0 profile |
|
|
133
|
+
| `social-profile-set` | Set profile with overwrite safety guard |
|
|
134
|
+
| `dm-send` | Send encrypted DM (NIP-17 default, NIP-04 opt-in) |
|
|
135
|
+
| `dm-read` | Read and decrypt received DMs |
|
|
136
|
+
| `contacts-get` | Fetch contact list (kind 3 follows) |
|
|
137
|
+
| `contacts-follow` | Follow a pubkey (publishes updated kind 3) |
|
|
138
|
+
| `contacts-unfollow` | Unfollow a pubkey |
|
|
139
|
+
| `social-notifications` | Fetch mentions, replies, reactions, zap receipts |
|
|
140
|
+
| `social-feed` | Fetch kind 1 text note feed |
|
|
141
|
+
|
|
142
|
+
### Trust (11 tools) — attestations, ring signatures, linkage proofs, spoken verification
|
|
107
143
|
|
|
108
144
|
| Tool | Description |
|
|
109
145
|
|------|-------------|
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
|
|
122
|
-
### Relay (
|
|
146
|
+
| `trust-attest` | Create kind 31000 verifiable attestation |
|
|
147
|
+
| `trust-read` | Read attestations by subject/type/attestor |
|
|
148
|
+
| `trust-verify` | Validate attestation structure |
|
|
149
|
+
| `trust-revoke` | Revoke an attestation (identity check) |
|
|
150
|
+
| `trust-request` | Send attestation request via NIP-17 |
|
|
151
|
+
| `trust-request-list` | Scan DMs for attestation requests |
|
|
152
|
+
| `trust-proof-publish` | Publish linkage proof (kind 30078) with confirmation |
|
|
153
|
+
| `trust-ring-prove` | Anonymous group membership proof (ring signature) |
|
|
154
|
+
| `trust-ring-verify` | Verify ring signature proof |
|
|
155
|
+
| `trust-spoken-challenge` | Generate spoken verification token |
|
|
156
|
+
| `trust-spoken-verify` | Verify spoken token response |
|
|
157
|
+
|
|
158
|
+
### Relay (5 tools) — per-identity relay lists, NIP-65 management, direct queries
|
|
123
159
|
|
|
124
160
|
| Tool | Description |
|
|
125
161
|
|------|-------------|
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
162
|
+
| `relay-list` | List relays with shared-relay warnings |
|
|
163
|
+
| `relay-set` | Publish kind 10002 relay list |
|
|
164
|
+
| `relay-add` | Add relay to active identity (in-memory) |
|
|
165
|
+
| `relay-query` | Query events from relays by kind, author, tags, or time range |
|
|
166
|
+
| `relay-info` | Fetch NIP-11 relay information document |
|
|
130
167
|
|
|
131
|
-
### Zap (7 tools)
|
|
168
|
+
### Zap (7 tools) — Lightning payments and invoices via Nostr Wallet Connect
|
|
132
169
|
|
|
133
170
|
| Tool | Description |
|
|
134
171
|
|------|-------------|
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
172
|
+
| `zap-send` | Pay a Lightning invoice via NWC |
|
|
173
|
+
| `zap-balance` | Request wallet balance via NWC |
|
|
174
|
+
| `zap-make-invoice` | Generate a Lightning invoice via NWC |
|
|
175
|
+
| `zap-lookup-invoice` | Check invoice payment status via NWC |
|
|
176
|
+
| `zap-list-transactions` | List recent Lightning transactions |
|
|
177
|
+
| `zap-receipts` | Parse zap receipts (amount, sender, message) |
|
|
178
|
+
| `zap-decode` | Decode bolt11 invoice fields |
|
|
142
179
|
|
|
143
|
-
### Safety (2 tools)
|
|
180
|
+
### Safety (2 tools) — duress personas for coercion resistance
|
|
144
181
|
|
|
145
182
|
| Tool | Description |
|
|
146
183
|
|------|-------------|
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
184
|
+
| `safety-configure` | Configure an alternative identity persona |
|
|
185
|
+
| `safety-activate` | Switch to alternative identity |
|
|
149
186
|
|
|
150
|
-
### Blossom (3 tools)
|
|
187
|
+
### Blossom (3 tools) — media uploads and management
|
|
151
188
|
|
|
152
189
|
| Tool | Description |
|
|
153
190
|
|------|-------------|
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
191
|
+
| `blossom-upload` | Upload file to a blossom media server |
|
|
192
|
+
| `blossom-list` | List blobs for a pubkey |
|
|
193
|
+
| `blossom-delete` | Delete a blob by SHA-256 hash |
|
|
157
194
|
|
|
158
|
-
### Groups — NIP-29 (4 tools)
|
|
195
|
+
### Groups — NIP-29 (4 tools) — group chat, metadata, and membership
|
|
159
196
|
|
|
160
197
|
| Tool | Description |
|
|
161
198
|
|------|-------------|
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
199
|
+
| `group-info` | Fetch group metadata |
|
|
200
|
+
| `group-chat` | Read group chat messages |
|
|
201
|
+
| `group-send` | Send message to a group |
|
|
202
|
+
| `group-members` | List group members |
|
|
166
203
|
|
|
167
|
-
### Community NIPs (2 tools)
|
|
204
|
+
### Community NIPs (2 tools) — publish and read community-proposed NIPs
|
|
168
205
|
|
|
169
206
|
| Tool | Description |
|
|
170
207
|
|------|-------------|
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
208
|
+
| `nip-publish` | Publish a community NIP (kind 30817) |
|
|
209
|
+
| `nip-read` | Fetch community NIPs |
|
|
173
210
|
|
|
174
|
-
### Utility (
|
|
211
|
+
### Utility (18 tools) — encode, decode, encrypt, verify, filter, fetch, browse NIPs
|
|
175
212
|
|
|
176
213
|
| Tool | Description |
|
|
177
214
|
|------|-------------|
|
|
178
215
|
| `decode` | Decode npub/nsec/note/nevent/nprofile/naddr |
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
216
|
+
| `encode-npub` | Encode hex pubkey as npub |
|
|
217
|
+
| `encode-note` | Encode hex event ID as note |
|
|
218
|
+
| `encode-nprofile` | Encode pubkey + relays as nprofile |
|
|
219
|
+
| `encode-nevent` | Encode event ID + relays as nevent |
|
|
220
|
+
| `encode-naddr` | Encode addressable event as naddr |
|
|
221
|
+
| `encode-nsec` | Encode hex private key as nsec |
|
|
222
|
+
| `key-public` | Derive pubkey from secret key |
|
|
223
|
+
| `key-encrypt` | Encrypt a secret key with a password (NIP-49 ncryptsec) |
|
|
224
|
+
| `key-decrypt` | Decrypt an ncryptsec (NIP-49) with a password |
|
|
225
|
+
| `verify-event` | Verify event hash and signature |
|
|
187
226
|
| `filter` | Test if an event matches a filter |
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
227
|
+
| `nip44-encrypt` | NIP-44 encrypt for a recipient |
|
|
228
|
+
| `nip44-decrypt` | NIP-44 decrypt from a sender |
|
|
190
229
|
| `count` | Count events matching a filter |
|
|
191
230
|
| `fetch` | Fetch events by nip19 code |
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
|
|
195
|
-
## Identity Switching
|
|
196
|
-
|
|
197
|
-
The killer feature. Every tool operates as the "active identity." Switch with a single call:
|
|
198
|
-
|
|
199
|
-
```
|
|
200
|
-
identity_derive_persona("work") → npub1abc...
|
|
201
|
-
identity_switch("work") → now operating as work persona
|
|
202
|
-
social_post("Hello from work!") → signed by npub1abc...
|
|
203
|
-
identity_switch("master") → back to master
|
|
204
|
-
social_post("Back to main") → signed by master npub
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Derived identities are cryptographically unlinkable unless you publish a linkage proof.
|
|
231
|
+
| `nip-list` | List all official NIPs |
|
|
232
|
+
| `nip-show` | Show a specific NIP's content |
|
|
208
233
|
|
|
209
234
|
## Configuration
|
|
210
235
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
type ToolHandler = (args: any) => Promise<{
|
|
3
|
+
content: Array<{
|
|
4
|
+
type: 'text';
|
|
5
|
+
text: string;
|
|
6
|
+
}>;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Holds non-promoted tool definitions and exposes them via search-actions
|
|
10
|
+
* and execute-action meta-tools. This keeps the primary tool list lean
|
|
11
|
+
* (context-window economics) while still making every action discoverable.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ActionCatalog {
|
|
14
|
+
private readonly entries;
|
|
15
|
+
add(name: string, definition: any, handler: ToolHandler): void;
|
|
16
|
+
get size(): number;
|
|
17
|
+
search(intent: string): Array<{
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
parameters?: Record<string, string>;
|
|
21
|
+
}>;
|
|
22
|
+
execute(name: string, params: Record<string, unknown>): Promise<{
|
|
23
|
+
content: Array<{
|
|
24
|
+
type: 'text';
|
|
25
|
+
text: string;
|
|
26
|
+
}>;
|
|
27
|
+
}>;
|
|
28
|
+
registerMetaTools(server: McpServer): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a proxy around McpServer that intercepts registerTool calls.
|
|
32
|
+
* Promoted tools go to the real server; everything else goes to the catalog.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createCatalogProxy(server: McpServer, catalog: ActionCatalog, promoted: Set<string>): McpServer;
|
|
35
|
+
export {};
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Holds non-promoted tool definitions and exposes them via search-actions
|
|
4
|
+
* and execute-action meta-tools. This keeps the primary tool list lean
|
|
5
|
+
* (context-window economics) while still making every action discoverable.
|
|
6
|
+
*/
|
|
7
|
+
export class ActionCatalog {
|
|
8
|
+
entries = new Map();
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
add(name, definition, handler) {
|
|
11
|
+
this.entries.set(name, {
|
|
12
|
+
name,
|
|
13
|
+
description: definition.description ?? '',
|
|
14
|
+
inputSchema: definition.inputSchema,
|
|
15
|
+
annotations: definition.annotations,
|
|
16
|
+
handler,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
get size() {
|
|
20
|
+
return this.entries.size;
|
|
21
|
+
}
|
|
22
|
+
search(intent) {
|
|
23
|
+
const tokens = intent.toLowerCase().split(/[\s\-_]+/).filter(t => t.length > 1);
|
|
24
|
+
const scored = [];
|
|
25
|
+
for (const entry of this.entries.values()) {
|
|
26
|
+
const haystack = `${entry.name} ${entry.description}`.toLowerCase();
|
|
27
|
+
let score = 0;
|
|
28
|
+
for (const token of tokens) {
|
|
29
|
+
if (haystack.includes(token))
|
|
30
|
+
score++;
|
|
31
|
+
}
|
|
32
|
+
if (score > 0)
|
|
33
|
+
scored.push({ entry, score });
|
|
34
|
+
}
|
|
35
|
+
scored.sort((a, b) => b.score - a.score);
|
|
36
|
+
return scored.slice(0, 10).map(({ entry }) => ({
|
|
37
|
+
name: entry.name,
|
|
38
|
+
description: entry.description,
|
|
39
|
+
parameters: extractParams(entry.inputSchema),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
async execute(name, params) {
|
|
43
|
+
const entry = this.entries.get(name);
|
|
44
|
+
if (!entry) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{
|
|
47
|
+
type: 'text',
|
|
48
|
+
text: JSON.stringify({
|
|
49
|
+
error: `Unknown action "${name}". Use search-actions to find available actions.`,
|
|
50
|
+
}),
|
|
51
|
+
}],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (entry.inputSchema && Object.keys(entry.inputSchema).length > 0) {
|
|
55
|
+
const schema = z.object(entry.inputSchema);
|
|
56
|
+
const result = schema.safeParse(params);
|
|
57
|
+
if (!result.success) {
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: 'text',
|
|
61
|
+
text: JSON.stringify({
|
|
62
|
+
error: 'Invalid parameters',
|
|
63
|
+
action: name,
|
|
64
|
+
issues: result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`),
|
|
65
|
+
expected: extractParams(entry.inputSchema),
|
|
66
|
+
}, null, 2),
|
|
67
|
+
}],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return entry.handler(result.data);
|
|
71
|
+
}
|
|
72
|
+
return entry.handler(params);
|
|
73
|
+
}
|
|
74
|
+
registerMetaTools(server) {
|
|
75
|
+
const catalog = this;
|
|
76
|
+
server.registerTool('search-actions', {
|
|
77
|
+
description: `Search ${catalog.size} additional actions by describing what you want to do. ` +
|
|
78
|
+
'Returns matching actions with names, descriptions, and parameter schemas. ' +
|
|
79
|
+
'Use this when the promoted tools (whoami, social-post, social-reply, social-feed, ' +
|
|
80
|
+
'dm-send, dm-read, zap-send, zap-balance, identity-switch, relay-query) ' +
|
|
81
|
+
'do not cover what you need.',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
intent: z.string().describe('Describe what you want to do (e.g. "upload a file", "verify an attestation", ' +
|
|
84
|
+
'"encrypt a message", "manage contacts", "ring signature")'),
|
|
85
|
+
},
|
|
86
|
+
annotations: { readOnlyHint: true },
|
|
87
|
+
}, async ({ intent }) => {
|
|
88
|
+
const matches = catalog.search(intent);
|
|
89
|
+
if (matches.length === 0) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{
|
|
92
|
+
type: 'text',
|
|
93
|
+
text: JSON.stringify({
|
|
94
|
+
matches: [],
|
|
95
|
+
hint: 'No matches found. Try different keywords or broader terms.',
|
|
96
|
+
}, null, 2),
|
|
97
|
+
}],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
content: [{
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: JSON.stringify({ matches }, null, 2),
|
|
104
|
+
}],
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
server.registerTool('execute-action', {
|
|
108
|
+
description: 'Execute an action found via search-actions. Pass the exact action name and its parameters.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
action: z.string().describe('Action name from search-actions results'),
|
|
111
|
+
params: z.record(z.string(), z.unknown()).default({}).describe('Parameters for the action (see search-actions results for expected params)'),
|
|
112
|
+
},
|
|
113
|
+
annotations: { openWorldHint: true },
|
|
114
|
+
}, async ({ action, params }) => {
|
|
115
|
+
return catalog.execute(action, params);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Create a proxy around McpServer that intercepts registerTool calls.
|
|
121
|
+
* Promoted tools go to the real server; everything else goes to the catalog.
|
|
122
|
+
*/
|
|
123
|
+
export function createCatalogProxy(server, catalog, promoted) {
|
|
124
|
+
return new Proxy(server, {
|
|
125
|
+
get(target, prop, receiver) {
|
|
126
|
+
if (prop === 'registerTool') {
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
return (name, definition, handler) => {
|
|
129
|
+
if (promoted.has(name)) {
|
|
130
|
+
return target.registerTool(name, definition, handler);
|
|
131
|
+
}
|
|
132
|
+
catalog.add(name, definition, handler);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return Reflect.get(target, prop, receiver);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
function extractParams(schema) {
|
|
141
|
+
if (!schema || Object.keys(schema).length === 0)
|
|
142
|
+
return undefined;
|
|
143
|
+
const params = {};
|
|
144
|
+
for (const [key, zodType] of Object.entries(schema)) {
|
|
145
|
+
params[key] = zodType?.description ?? '(no description)';
|
|
146
|
+
}
|
|
147
|
+
return params;
|
|
148
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -130,7 +130,8 @@ Social:
|
|
|
130
130
|
group-members <group-id> List group members
|
|
131
131
|
|
|
132
132
|
Trust:
|
|
133
|
-
attest <
|
|
133
|
+
attest <event-id> Verify someone's assertion (assertion-first)
|
|
134
|
+
claim <type> Make a direct claim (endorsement, vouch, review)
|
|
134
135
|
trust-read [--subject X] [--type X] Read attestations
|
|
135
136
|
trust-verify <event-json> Validate attestation structure
|
|
136
137
|
trust-revoke <type> <identifier> Revoke an attestation
|
|
@@ -473,13 +474,27 @@ async function run(cmdArgs) {
|
|
|
473
474
|
out(await handleNotifications(ctx, pool, { limit: parseInt(flag('limit', '50'), 10) }), fmt.formatNotifications);
|
|
474
475
|
break;
|
|
475
476
|
// === Trust ===
|
|
476
|
-
case 'attest':
|
|
477
|
+
case 'attest': {
|
|
478
|
+
const assertionId = req(1, 'attest <assertion-event-id> [--subject <hex>] [--type <type>] [--summary <text>]');
|
|
477
479
|
out(await handleTrustAttest(ctx, pool, {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
480
|
+
assertionId,
|
|
481
|
+
subject: flag('subject'),
|
|
482
|
+
type: flag('type'),
|
|
483
|
+
summary: flag('summary'),
|
|
484
|
+
assertionRelay: flag('relay'),
|
|
481
485
|
}));
|
|
482
486
|
break;
|
|
487
|
+
}
|
|
488
|
+
case 'claim': {
|
|
489
|
+
const type = req(1, 'claim <type> [--subject <hex>] [--identifier <string>] [--summary <text>]');
|
|
490
|
+
out(await handleTrustAttest(ctx, pool, {
|
|
491
|
+
type,
|
|
492
|
+
subject: flag('subject'),
|
|
493
|
+
identifier: flag('identifier'),
|
|
494
|
+
summary: flag('summary'),
|
|
495
|
+
}));
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
483
498
|
case 'trust-read':
|
|
484
499
|
out(await handleTrustRead(pool, ctx.activeNpub, {
|
|
485
500
|
subject: flag('subject'),
|
package/dist/identity/tools.js
CHANGED
|
@@ -14,7 +14,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
14
14
|
content: [{ type: 'text', text: deps.ctx.activeNpub }],
|
|
15
15
|
};
|
|
16
16
|
});
|
|
17
|
-
server.registerTool('
|
|
17
|
+
server.registerTool('identity-create', {
|
|
18
18
|
description: 'Generate a fresh Nostr identity with a BIP-39 mnemonic seed. Returns { npub, mnemonic }. Store the mnemonic securely — it will not be shown again. Use this when the user needs a brand new identity unrelated to the current one.',
|
|
19
19
|
annotations: { readOnlyHint: false },
|
|
20
20
|
}, async () => {
|
|
@@ -23,7 +23,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
23
23
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
24
24
|
};
|
|
25
25
|
});
|
|
26
|
-
server.registerTool('
|
|
26
|
+
server.registerTool('identity-derive', {
|
|
27
27
|
description: 'Derive a child Nostr identity from the master key by purpose and index. Deterministic — same inputs always produce the same npub. Returns { npub, purpose, index }. Use identity_switch after deriving to operate as the new identity.',
|
|
28
28
|
inputSchema: {
|
|
29
29
|
purpose: z.string().describe('Purpose string for derivation (e.g. "messaging", "signing")'),
|
|
@@ -36,7 +36,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
36
36
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
37
37
|
};
|
|
38
38
|
});
|
|
39
|
-
server.registerTool('
|
|
39
|
+
server.registerTool('identity-derive-persona', {
|
|
40
40
|
description: 'Derive a named persona (e.g. "work", "personal", "anonymous"). Like identity_derive but uses a human-readable name. Returns { npub, personaName, index }. Follow with identity_switch to activate.',
|
|
41
41
|
inputSchema: {
|
|
42
42
|
name: z.string().describe('Persona name (e.g. "work", "personal", "anonymous")'),
|
|
@@ -49,7 +49,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
49
49
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
50
50
|
};
|
|
51
51
|
});
|
|
52
|
-
server.registerTool('
|
|
52
|
+
server.registerTool('identity-switch', {
|
|
53
53
|
description: 'Switch the active Nostr identity. ALL subsequent tool calls will sign events and query relays as the new identity. Pass "master" to return to root. Returns { npub } of the now-active identity. This is the key tool for multi-persona workflows.',
|
|
54
54
|
inputSchema: {
|
|
55
55
|
target: z.string().describe('Purpose, persona name, or "master" to switch to'),
|
|
@@ -62,7 +62,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
62
62
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
63
63
|
};
|
|
64
64
|
});
|
|
65
|
-
server.registerTool('
|
|
65
|
+
server.registerTool('identity-list', {
|
|
66
66
|
description: 'List all known identities (master + all derived). Returns array of { npub, purpose, index, personaName }. Never includes private keys. Use this to see what identities are available before switching.',
|
|
67
67
|
inputSchema: {
|
|
68
68
|
output: z.enum(['json', 'human']).default('human').describe('Response format'),
|
|
@@ -72,7 +72,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
72
72
|
const result = handleIdentityList(deps.ctx);
|
|
73
73
|
return toolResponse(result, output, fmt.formatIdentityList);
|
|
74
74
|
});
|
|
75
|
-
server.registerTool('
|
|
75
|
+
server.registerTool('identity-prove', {
|
|
76
76
|
description: 'Create a cryptographic linkage proof between the master key and the active identity. "blind" (default) proves they share a master without revealing how the child was derived. "full" also reveals the purpose and index. Returns a LinkageProof object that can be verified by anyone with nsec-tree. Use trust_proof_publish to make it permanent on relays.',
|
|
77
77
|
inputSchema: {
|
|
78
78
|
mode: z.enum(['blind', 'full']).default('blind').describe('Proof mode: "blind" hides derivation path, "full" reveals purpose and index'),
|
|
@@ -84,7 +84,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
84
84
|
content: [{ type: 'text', text: JSON.stringify(proof, null, 2) }],
|
|
85
85
|
};
|
|
86
86
|
});
|
|
87
|
-
server.registerTool('
|
|
87
|
+
server.registerTool('identity-backup-shamir', {
|
|
88
88
|
description: 'Split the active identity\'s private key into Shamir shard files using threshold secret sharing. Any "threshold" shards can reconstruct the key. Files written with 0600 permissions via atomic rename. Returns file paths only — shard content never appears in the response.',
|
|
89
89
|
inputSchema: {
|
|
90
90
|
threshold: z.number().int().min(2).describe('Minimum shards needed to reconstruct'),
|
|
@@ -106,7 +106,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
106
106
|
}, null, 2) }],
|
|
107
107
|
};
|
|
108
108
|
});
|
|
109
|
-
server.registerTool('
|
|
109
|
+
server.registerTool('identity-restore-shamir', {
|
|
110
110
|
description: 'Reconstruct a secret from Shamir shard files. Returns the restored npub for verification.',
|
|
111
111
|
inputSchema: {
|
|
112
112
|
files: z.array(z.string()).describe('Paths to shard files'),
|
|
@@ -129,7 +129,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
129
129
|
}, null, 2) }],
|
|
130
130
|
};
|
|
131
131
|
});
|
|
132
|
-
server.registerTool('
|
|
132
|
+
server.registerTool('identity-backup', {
|
|
133
133
|
description: 'Fetch profile, contacts, relay list, and attestations for a pubkey. Returns a portable JSON bundle (no private keys).',
|
|
134
134
|
inputSchema: {
|
|
135
135
|
pubkeyHex: hexId.describe('Hex pubkey to back up'),
|
|
@@ -147,7 +147,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
147
147
|
}, null, 2) }],
|
|
148
148
|
};
|
|
149
149
|
});
|
|
150
|
-
server.registerTool('
|
|
150
|
+
server.registerTool('identity-restore', {
|
|
151
151
|
description: 'Re-sign migratable events (profile, contacts, relay list) under the active identity. Skips attestations (trust chain protection).',
|
|
152
152
|
inputSchema: {
|
|
153
153
|
pubkeyHex: hexId.describe('Hex pubkey of the original identity to restore from'),
|
|
@@ -162,7 +162,7 @@ export function registerIdentityTools(server, deps) {
|
|
|
162
162
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
163
163
|
};
|
|
164
164
|
});
|
|
165
|
-
server.registerTool('
|
|
165
|
+
server.registerTool('identity-migrate', {
|
|
166
166
|
description: 'Migrate from an old identity to the active one. Shows preview first — set confirm: true to execute. Publishes linkage proof and re-signs migratable events.',
|
|
167
167
|
inputSchema: {
|
|
168
168
|
oldPubkeyHex: z.string().describe('Hex pubkey of the old identity'),
|