nostr-bray 1.27.2 → 1.27.4
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/dist/catalog.d.ts +21 -0
- package/dist/catalog.js +81 -4
- package/dist/relay-pool.d.ts +19 -0
- package/dist/relay-pool.js +27 -4
- package/dist/social/scheduled.js +6 -1
- package/dist/social/tools.js +1 -1
- package/dist/types.d.ts +11 -0
- package/package.json +1 -1
package/dist/catalog.d.ts
CHANGED
|
@@ -32,4 +32,25 @@ export declare class ActionCatalog {
|
|
|
32
32
|
* Promoted tools go to the real server; everything else goes to the catalog.
|
|
33
33
|
*/
|
|
34
34
|
export declare function createCatalogProxy(server: McpServer, catalog: ActionCatalog, promoted: Set<string>): McpServer;
|
|
35
|
+
/**
|
|
36
|
+
* Coerce execute-action's `params`/`parameters` argument into a plain object.
|
|
37
|
+
*
|
|
38
|
+
* Accepts either:
|
|
39
|
+
* - an object (the canonical shape), passed through unchanged
|
|
40
|
+
* - a JSON string literal that parses to an object (some MCP clients serialise
|
|
41
|
+
* record-typed fields this way because the compiled JSON Schema has no
|
|
42
|
+
* fixed properties list)
|
|
43
|
+
* - undefined (returns an empty object)
|
|
44
|
+
*
|
|
45
|
+
* Rejects malformed JSON strings and JSON that does not parse to a plain object
|
|
46
|
+
* (e.g. arrays, numbers, null), returning a structured error the caller can
|
|
47
|
+
* surface to the client without crashing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function coerceActionParams(raw: unknown): {
|
|
50
|
+
ok: true;
|
|
51
|
+
value: Record<string, unknown>;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
error: string;
|
|
55
|
+
};
|
|
35
56
|
export {};
|
package/dist/catalog.js
CHANGED
|
@@ -104,21 +104,48 @@ export class ActionCatalog {
|
|
|
104
104
|
}],
|
|
105
105
|
};
|
|
106
106
|
});
|
|
107
|
+
// Accept either an object (the natural shape) or a JSON string literal.
|
|
108
|
+
// Some MCP clients (notably the Claude Code harness as of April 2026)
|
|
109
|
+
// serialise schema fields typed as z.record(z.unknown()) as a JSON string
|
|
110
|
+
// rather than a nested object, because the compiled JSON Schema has no
|
|
111
|
+
// fixed `properties` list. The handler below coerces strings by parsing
|
|
112
|
+
// them, returning a clean error on malformed JSON.
|
|
113
|
+
const paramsSchema = z.union([
|
|
114
|
+
z.record(z.string(), z.unknown()),
|
|
115
|
+
z.string(),
|
|
116
|
+
]);
|
|
107
117
|
server.registerTool('execute-action', {
|
|
108
118
|
description: 'Execute an action found via search-actions. Pass the exact action name and its parameters.',
|
|
109
119
|
inputSchema: {
|
|
110
120
|
action: z.string().describe('Action name from search-actions results'),
|
|
111
|
-
params:
|
|
121
|
+
params: paramsSchema.optional().describe('Parameters for the action (see search-actions results for expected params). ' +
|
|
122
|
+
'Alias: parameters. Accepts a JSON object or a JSON string literal.'),
|
|
112
123
|
// Alias for clients that naturally reach for "parameters" rather than "params".
|
|
113
124
|
// Without this, MCP SDK strips unknown keys from the validated args and the
|
|
114
125
|
// inner action handler receives an empty object, causing spurious
|
|
115
126
|
// "expected string, received undefined" validation failures.
|
|
116
|
-
parameters:
|
|
127
|
+
parameters: paramsSchema.optional().describe('Alias for params. Either field is accepted. ' +
|
|
128
|
+
'Accepts a JSON object or a JSON string literal.'),
|
|
117
129
|
},
|
|
118
130
|
annotations: { openWorldHint: true },
|
|
119
131
|
}, async ({ action, params, parameters }) => {
|
|
120
|
-
const
|
|
121
|
-
|
|
132
|
+
const raw = params ?? parameters;
|
|
133
|
+
const coerced = coerceActionParams(raw);
|
|
134
|
+
if (!coerced.ok) {
|
|
135
|
+
return {
|
|
136
|
+
content: [{
|
|
137
|
+
type: 'text',
|
|
138
|
+
text: JSON.stringify({
|
|
139
|
+
error: coerced.error,
|
|
140
|
+
action,
|
|
141
|
+
hint: 'Pass params (or parameters) as a JSON object like ' +
|
|
142
|
+
'{"pubkeyHex":"..."}, or as a JSON string literal of the ' +
|
|
143
|
+
'same shape. Malformed JSON strings are rejected.',
|
|
144
|
+
}, null, 2),
|
|
145
|
+
}],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return catalog.execute(action, coerced.value);
|
|
122
149
|
});
|
|
123
150
|
}
|
|
124
151
|
}
|
|
@@ -152,3 +179,53 @@ function extractParams(schema) {
|
|
|
152
179
|
}
|
|
153
180
|
return params;
|
|
154
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Coerce execute-action's `params`/`parameters` argument into a plain object.
|
|
184
|
+
*
|
|
185
|
+
* Accepts either:
|
|
186
|
+
* - an object (the canonical shape), passed through unchanged
|
|
187
|
+
* - a JSON string literal that parses to an object (some MCP clients serialise
|
|
188
|
+
* record-typed fields this way because the compiled JSON Schema has no
|
|
189
|
+
* fixed properties list)
|
|
190
|
+
* - undefined (returns an empty object)
|
|
191
|
+
*
|
|
192
|
+
* Rejects malformed JSON strings and JSON that does not parse to a plain object
|
|
193
|
+
* (e.g. arrays, numbers, null), returning a structured error the caller can
|
|
194
|
+
* surface to the client without crashing.
|
|
195
|
+
*/
|
|
196
|
+
export function coerceActionParams(raw) {
|
|
197
|
+
if (raw === undefined || raw === null) {
|
|
198
|
+
return { ok: true, value: {} };
|
|
199
|
+
}
|
|
200
|
+
if (typeof raw === 'string') {
|
|
201
|
+
const trimmed = raw.trim();
|
|
202
|
+
if (trimmed === '') {
|
|
203
|
+
return { ok: true, value: {} };
|
|
204
|
+
}
|
|
205
|
+
let parsed;
|
|
206
|
+
try {
|
|
207
|
+
parsed = JSON.parse(trimmed);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
error: `Invalid parameters: could not parse JSON string (${message})`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
error: 'Invalid parameters: JSON string must decode to an object, not an array or primitive',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return { ok: true, value: parsed };
|
|
223
|
+
}
|
|
224
|
+
if (typeof raw === 'object' && !Array.isArray(raw)) {
|
|
225
|
+
return { ok: true, value: raw };
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
error: `Invalid parameters: expected an object or JSON string, received ${typeof raw}`,
|
|
230
|
+
};
|
|
231
|
+
}
|
package/dist/relay-pool.d.ts
CHANGED
|
@@ -6,6 +6,25 @@ export interface PoolLike {
|
|
|
6
6
|
querySync(relays: string[], filter: Filter): Promise<NostrEvent[]>;
|
|
7
7
|
destroy(): void;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Summarise a publish attempt into the two boolean flags on {@link PublishResult}.
|
|
11
|
+
*
|
|
12
|
+
* `success` is true when the event reached the network as a majority: at least
|
|
13
|
+
* one relay accepted AND at least 50% of attempted relays accepted. This is the
|
|
14
|
+
* default "did my publish work" signal. Paywalled, whitelisted, or transient
|
|
15
|
+
* failures routinely leave some relays in the rejected list, so requiring every
|
|
16
|
+
* relay to accept produces far too many false negatives.
|
|
17
|
+
*
|
|
18
|
+
* `allAccepted` preserves the strict "every attempted relay accepted" semantic
|
|
19
|
+
* for callers that genuinely need it (e.g. high-assurance publishing to a
|
|
20
|
+
* small curated list of private relays).
|
|
21
|
+
*
|
|
22
|
+
* Zero attempted relays is treated as failure on both flags.
|
|
23
|
+
*/
|
|
24
|
+
export declare function summarisePublish(acceptedCount: number, attempted: number): {
|
|
25
|
+
success: boolean;
|
|
26
|
+
allAccepted: boolean;
|
|
27
|
+
};
|
|
9
28
|
export interface RelayPoolConfig {
|
|
10
29
|
torProxy?: string;
|
|
11
30
|
allowClearnet: boolean;
|
package/dist/relay-pool.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarise a publish attempt into the two boolean flags on {@link PublishResult}.
|
|
3
|
+
*
|
|
4
|
+
* `success` is true when the event reached the network as a majority: at least
|
|
5
|
+
* one relay accepted AND at least 50% of attempted relays accepted. This is the
|
|
6
|
+
* default "did my publish work" signal. Paywalled, whitelisted, or transient
|
|
7
|
+
* failures routinely leave some relays in the rejected list, so requiring every
|
|
8
|
+
* relay to accept produces far too many false negatives.
|
|
9
|
+
*
|
|
10
|
+
* `allAccepted` preserves the strict "every attempted relay accepted" semantic
|
|
11
|
+
* for callers that genuinely need it (e.g. high-assurance publishing to a
|
|
12
|
+
* small curated list of private relays).
|
|
13
|
+
*
|
|
14
|
+
* Zero attempted relays is treated as failure on both flags.
|
|
15
|
+
*/
|
|
16
|
+
export function summarisePublish(acceptedCount, attempted) {
|
|
17
|
+
if (attempted <= 0)
|
|
18
|
+
return { success: false, allAccepted: false };
|
|
19
|
+
return {
|
|
20
|
+
success: acceptedCount >= 1 && acceptedCount * 2 >= attempted,
|
|
21
|
+
allAccepted: acceptedCount === attempted,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
1
24
|
/** Initialise WebSocket and create a real SimplePool — lazy-loaded to avoid side effects at import */
|
|
2
25
|
async function createRealPool(torProxy) {
|
|
3
26
|
const { useWebSocketImplementation, SimplePool } = await import('nostr-tools/pool');
|
|
@@ -72,7 +95,7 @@ export class RelayPool {
|
|
|
72
95
|
const relays = this.getRelays(npub);
|
|
73
96
|
const writeRelays = relays.write;
|
|
74
97
|
if (writeRelays.length === 0) {
|
|
75
|
-
return { success: false, accepted: [], rejected: [], errors: ['no write relays configured'] };
|
|
98
|
+
return { success: false, allAccepted: false, accepted: [], rejected: [], errors: ['no write relays configured'] };
|
|
76
99
|
}
|
|
77
100
|
const promises = pool.publish(writeRelays, event);
|
|
78
101
|
const accepted = [];
|
|
@@ -91,7 +114,7 @@ export class RelayPool {
|
|
|
91
114
|
}
|
|
92
115
|
}
|
|
93
116
|
return {
|
|
94
|
-
|
|
117
|
+
...summarisePublish(accepted.length, writeRelays.length),
|
|
95
118
|
accepted,
|
|
96
119
|
rejected,
|
|
97
120
|
errors,
|
|
@@ -101,7 +124,7 @@ export class RelayPool {
|
|
|
101
124
|
async publishDirect(relays, event) {
|
|
102
125
|
const pool = await this.poolReady;
|
|
103
126
|
if (relays.length === 0) {
|
|
104
|
-
return { success: false, accepted: [], rejected: [], errors: ['no relays specified'] };
|
|
127
|
+
return { success: false, allAccepted: false, accepted: [], rejected: [], errors: ['no relays specified'] };
|
|
105
128
|
}
|
|
106
129
|
const promises = pool.publish(relays, event);
|
|
107
130
|
const accepted = [];
|
|
@@ -120,7 +143,7 @@ export class RelayPool {
|
|
|
120
143
|
}
|
|
121
144
|
}
|
|
122
145
|
return {
|
|
123
|
-
|
|
146
|
+
...summarisePublish(accepted.length, relays.length),
|
|
124
147
|
accepted,
|
|
125
148
|
rejected,
|
|
126
149
|
errors,
|
package/dist/social/scheduled.js
CHANGED
|
@@ -123,7 +123,12 @@ export async function handlePublishScheduled(pool, npub, queueDir) {
|
|
|
123
123
|
published++;
|
|
124
124
|
}
|
|
125
125
|
else {
|
|
126
|
-
|
|
126
|
+
// success is the majority-quorum flag: treat as failure only when
|
|
127
|
+
// we did not reach at least 50% of attempted relays.
|
|
128
|
+
const reason = result.accepted.length === 0
|
|
129
|
+
? 'rejected by all relays'
|
|
130
|
+
: `only ${result.accepted.length}/${result.accepted.length + result.rejected.length} relays accepted`;
|
|
131
|
+
console.error(`Failed to publish ${data.event.id}: ${reason}`);
|
|
127
132
|
failed++;
|
|
128
133
|
}
|
|
129
134
|
}
|
package/dist/social/tools.js
CHANGED
|
@@ -25,7 +25,7 @@ export function registerSocialTools(server, deps) {
|
|
|
25
25
|
maxEntries: deps.veilCacheMax ?? 500,
|
|
26
26
|
});
|
|
27
27
|
server.registerTool('social-post', {
|
|
28
|
-
description: 'Post a text note (kind 1) signed by the active identity and publish to relays. Returns { id, pubkey, publish: { success, accepted, rejected } }. The most common social action.',
|
|
28
|
+
description: 'Post a text note (kind 1) signed by the active identity and publish to relays. Returns { id, pubkey, publish: { success, allAccepted, accepted, rejected } } where success is the majority-quorum signal (true when at least one relay accepted and at least half of attempted relays accepted) and allAccepted is true only when every attempted relay accepted. The most common social action.',
|
|
29
29
|
inputSchema: {
|
|
30
30
|
content: z.string().describe('Text content of the note'),
|
|
31
31
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -21,7 +21,18 @@ export interface RelaySet {
|
|
|
21
21
|
}
|
|
22
22
|
/** Result of a publish operation */
|
|
23
23
|
export interface PublishResult {
|
|
24
|
+
/**
|
|
25
|
+
* True when the event is reliably published: at least one relay accepted
|
|
26
|
+
* AND at least 50% of attempted relays accepted. Captures both "reached
|
|
27
|
+
* the network" and "reached a majority", which matches real-world
|
|
28
|
+
* expectations where paywalled or whitelisted relays routinely reject.
|
|
29
|
+
*/
|
|
24
30
|
success: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* True when every attempted relay accepted. Use this for high-assurance
|
|
33
|
+
* publishing to a small curated list of private relays.
|
|
34
|
+
*/
|
|
35
|
+
allAccepted: boolean;
|
|
25
36
|
accepted: string[];
|
|
26
37
|
rejected: string[];
|
|
27
38
|
errors: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nostr-bray",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.4",
|
|
4
4
|
"description": "Trust-aware Nostr for AI and humans. Three dimensions of trust -- verification, proximity, and access -- woven into every interaction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|