@contextvm/sdk 0.1.48 → 0.2.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/dist/esm/core/constants.d.ts +3 -0
- package/dist/esm/core/constants.d.ts.map +1 -1
- package/dist/esm/core/constants.js +3 -0
- package/dist/esm/core/constants.js.map +1 -1
- package/dist/esm/core/utils/logger.d.ts +1 -1
- package/dist/esm/core/utils/logger.d.ts.map +1 -1
- package/dist/esm/core/utils/lru-cache.d.ts.map +1 -1
- package/dist/esm/core/utils/lru-cache.js +9 -1
- package/dist/esm/core/utils/lru-cache.js.map +1 -1
- package/dist/esm/core/utils/task-queue.d.ts +5 -0
- package/dist/esm/core/utils/task-queue.d.ts.map +1 -1
- package/dist/esm/core/utils/task-queue.js +11 -0
- package/dist/esm/core/utils/task-queue.js.map +1 -1
- package/dist/esm/core/utils/utils.d.ts +8 -0
- package/dist/esm/core/utils/utils.d.ts.map +1 -1
- package/dist/esm/core/utils/utils.js +23 -0
- package/dist/esm/core/utils/utils.js.map +1 -1
- package/dist/esm/relay/applesauce-relay-pool.d.ts +37 -4
- package/dist/esm/relay/applesauce-relay-pool.d.ts.map +1 -1
- package/dist/esm/relay/applesauce-relay-pool.js +165 -11
- package/dist/esm/relay/applesauce-relay-pool.js.map +1 -1
- package/dist/esm/transport/base-nostr-transport.d.ts +1 -1
- package/dist/esm/transport/base-nostr-transport.d.ts.map +1 -1
- package/dist/esm/transport/base-nostr-transport.js +8 -6
- package/dist/esm/transport/base-nostr-transport.js.map +1 -1
- package/dist/esm/transport/nostr-client/correlation-store.d.ts +60 -0
- package/dist/esm/transport/nostr-client/correlation-store.d.ts.map +1 -0
- package/dist/esm/transport/nostr-client/correlation-store.js +61 -0
- package/dist/esm/transport/nostr-client/correlation-store.js.map +1 -0
- package/dist/esm/transport/nostr-client/stateless-mode-handler.d.ts +24 -0
- package/dist/esm/transport/nostr-client/stateless-mode-handler.d.ts.map +1 -0
- package/dist/esm/transport/nostr-client/stateless-mode-handler.js +61 -0
- package/dist/esm/transport/nostr-client/stateless-mode-handler.js.map +1 -0
- package/dist/esm/transport/nostr-client-transport.d.ts +48 -24
- package/dist/esm/transport/nostr-client-transport.d.ts.map +1 -1
- package/dist/esm/transport/nostr-client-transport.js +92 -131
- package/dist/esm/transport/nostr-client-transport.js.map +1 -1
- package/dist/esm/transport/nostr-server/announcement-manager.d.ts +116 -0
- package/dist/esm/transport/nostr-server/announcement-manager.d.ts.map +1 -0
- package/dist/esm/transport/nostr-server/announcement-manager.js +288 -0
- package/dist/esm/transport/nostr-server/announcement-manager.js.map +1 -0
- package/dist/esm/transport/nostr-server/authorization-policy.d.ts +74 -0
- package/dist/esm/transport/nostr-server/authorization-policy.d.ts.map +1 -0
- package/dist/esm/transport/nostr-server/authorization-policy.js +91 -0
- package/dist/esm/transport/nostr-server/authorization-policy.js.map +1 -0
- package/dist/esm/transport/nostr-server/correlation-store.d.ts +102 -0
- package/dist/esm/transport/nostr-server/correlation-store.d.ts.map +1 -0
- package/dist/esm/transport/nostr-server/correlation-store.js +167 -0
- package/dist/esm/transport/nostr-server/correlation-store.js.map +1 -0
- package/dist/esm/transport/nostr-server/session-store.d.ts +99 -0
- package/dist/esm/transport/nostr-server/session-store.d.ts.map +1 -0
- package/dist/esm/transport/nostr-server/session-store.js +123 -0
- package/dist/esm/transport/nostr-server/session-store.js.map +1 -0
- package/dist/esm/transport/nostr-server-transport.d.ts +18 -63
- package/dist/esm/transport/nostr-server-transport.d.ts.map +1 -1
- package/dist/esm/transport/nostr-server-transport.js +135 -378
- package/dist/esm/transport/nostr-server-transport.js.map +1 -1
- package/package.json +11 -12
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { InitializeResultSchema, isJSONRPCRequest, isJSONRPCNotification,
|
|
1
|
+
import { InitializeResultSchema, isJSONRPCRequest, isJSONRPCNotification, isJSONRPCResultResponse, isJSONRPCErrorResponse, } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { BaseNostrTransport, } from './base-nostr-transport.js';
|
|
3
|
-
import {
|
|
3
|
+
import { CTXVM_MESSAGES_KIND, GIFT_WRAP_KIND, NOSTR_TAGS, NOTIFICATIONS_INITIALIZED_METHOD, decryptMessage, } from '../core/index.js';
|
|
4
4
|
import { EncryptionMode } from '../core/interfaces.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { injectClientPubkey, withTimeout } from '../core/utils/utils.js';
|
|
6
|
+
import { CorrelationStore } from './nostr-server/correlation-store.js';
|
|
7
|
+
import { SessionStore } from './nostr-server/session-store.js';
|
|
8
|
+
import { AuthorizationPolicy, } from './nostr-server/authorization-policy.js';
|
|
9
|
+
import { AnnouncementManager, } from './nostr-server/announcement-manager.js';
|
|
8
10
|
/**
|
|
9
11
|
* A server-side transport layer for CTXVM that uses Nostr events for communication.
|
|
10
12
|
* This transport listens for incoming MCP requests via Nostr events and can send
|
|
@@ -15,55 +17,45 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
15
17
|
constructor(options) {
|
|
16
18
|
var _a;
|
|
17
19
|
super('nostr-server-transport', options);
|
|
18
|
-
this.eventIdToClient = new Map(); // eventId -> clientPubkey
|
|
19
|
-
this.maxSessions = 1000; // LRU cache limit
|
|
20
|
-
this.isInitialized = false;
|
|
21
|
-
this.serverInfo = options.serverInfo;
|
|
22
|
-
this.isPublicServer = options.isPublicServer;
|
|
23
|
-
this.allowedPublicKeys = options.allowedPublicKeys
|
|
24
|
-
? new Set(options.allowedPublicKeys)
|
|
25
|
-
: undefined;
|
|
26
|
-
this.excludedCapabilities = options.excludedCapabilities;
|
|
27
20
|
this.injectClientPubkey = (_a = options.injectClientPubkey) !== null && _a !== void 0 ? _a : false;
|
|
28
|
-
// Initialize
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
// Initialize authorization policy
|
|
22
|
+
this.authorizationPolicy = new AuthorizationPolicy({
|
|
23
|
+
allowedPublicKeys: options.allowedPublicKeys
|
|
24
|
+
? new Set(options.allowedPublicKeys)
|
|
25
|
+
: undefined,
|
|
26
|
+
excludedCapabilities: options.excludedCapabilities,
|
|
27
|
+
isPublicServer: options.isPublicServer,
|
|
28
|
+
});
|
|
29
|
+
// Initialize session store with eviction callback for correlation cleanup
|
|
30
|
+
this.sessionStore = new SessionStore({
|
|
31
|
+
maxSessions: 1000,
|
|
32
|
+
onSessionEvicted: (clientPubkey) => {
|
|
33
|
+
// Clean up all correlation data for evicted session
|
|
34
|
+
const removedCount = this.correlationStore.removeRoutesForClient(clientPubkey);
|
|
35
|
+
this.logger.info(`Evicted session for ${clientPubkey} (removed ${removedCount} routes)`);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
// Initialize correlation store with bounded caches
|
|
39
|
+
this.correlationStore = new CorrelationStore({
|
|
40
|
+
maxEventRoutes: 10000,
|
|
41
|
+
maxProgressTokens: 10000,
|
|
42
|
+
onEventRouteEvicted: (eventId, route) => {
|
|
43
|
+
this.logger.debug(`Evicted event route for ${eventId}`, {
|
|
44
|
+
clientPubkey: route.clientPubkey,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
// Initialize announcement manager
|
|
49
|
+
this.announcementManager = new AnnouncementManager({
|
|
50
|
+
serverInfo: options.serverInfo,
|
|
51
|
+
encryptionMode: this.encryptionMode,
|
|
52
|
+
onSendMessage: (message) => { var _a; return (_a = this.onmessage) === null || _a === void 0 ? void 0 : _a.call(this, message); },
|
|
53
|
+
onPublishEvent: (event) => this.publishEvent(event),
|
|
54
|
+
onSignEvent: (eventTemplate) => this.signer.signEvent(eventTemplate),
|
|
55
|
+
onGetPublicKey: () => this.getPublicKey(),
|
|
56
|
+
onSubscribe: (filters, onEvent) => this.relayHandler.subscribe(filters, onEvent),
|
|
57
|
+
logger: this.logger,
|
|
38
58
|
});
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Generates common tags from server information for use in Nostr events.
|
|
42
|
-
* @returns Array of tag arrays for Nostr events.
|
|
43
|
-
*/
|
|
44
|
-
generateCommonTags() {
|
|
45
|
-
var _a, _b, _c, _d;
|
|
46
|
-
if (this.cachedCommonTags) {
|
|
47
|
-
return this.cachedCommonTags;
|
|
48
|
-
}
|
|
49
|
-
const commonTags = [];
|
|
50
|
-
if ((_a = this.serverInfo) === null || _a === void 0 ? void 0 : _a.name) {
|
|
51
|
-
commonTags.push([NOSTR_TAGS.NAME, this.serverInfo.name]);
|
|
52
|
-
}
|
|
53
|
-
if ((_b = this.serverInfo) === null || _b === void 0 ? void 0 : _b.about) {
|
|
54
|
-
commonTags.push([NOSTR_TAGS.ABOUT, this.serverInfo.about]);
|
|
55
|
-
}
|
|
56
|
-
if ((_c = this.serverInfo) === null || _c === void 0 ? void 0 : _c.website) {
|
|
57
|
-
commonTags.push([NOSTR_TAGS.WEBSITE, this.serverInfo.website]);
|
|
58
|
-
}
|
|
59
|
-
if ((_d = this.serverInfo) === null || _d === void 0 ? void 0 : _d.picture) {
|
|
60
|
-
commonTags.push([NOSTR_TAGS.PICTURE, this.serverInfo.picture]);
|
|
61
|
-
}
|
|
62
|
-
if (this.encryptionMode !== EncryptionMode.DISABLED) {
|
|
63
|
-
commonTags.push([NOSTR_TAGS.SUPPORT_ENCRYPTION]);
|
|
64
|
-
}
|
|
65
|
-
this.cachedCommonTags = commonTags;
|
|
66
|
-
return commonTags;
|
|
67
59
|
}
|
|
68
60
|
/**
|
|
69
61
|
* Starts the transport, connecting to the relay and setting up event listeners
|
|
@@ -94,17 +86,13 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
94
86
|
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
95
87
|
}
|
|
96
88
|
});
|
|
97
|
-
if (this.isPublicServer) {
|
|
98
|
-
await this.getAnnouncementData();
|
|
89
|
+
if (this.authorizationPolicy.isPublicServer) {
|
|
90
|
+
await this.announcementManager.getAnnouncementData();
|
|
99
91
|
}
|
|
100
92
|
}
|
|
101
93
|
catch (error) {
|
|
102
|
-
this.logger.error('Error starting NostrServerTransport', {
|
|
103
|
-
error: error instanceof Error ? error.message : String(error),
|
|
104
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
105
|
-
});
|
|
106
94
|
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
107
|
-
|
|
95
|
+
this.logAndRethrowError('Error starting NostrServerTransport', error);
|
|
108
96
|
}
|
|
109
97
|
}
|
|
110
98
|
/**
|
|
@@ -113,17 +101,17 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
113
101
|
async close() {
|
|
114
102
|
var _a, _b;
|
|
115
103
|
try {
|
|
104
|
+
// Shutdown the task queue to prevent new tasks from being queued
|
|
105
|
+
// and clear pending tasks to avoid operating on stale state
|
|
106
|
+
this.taskQueue.shutdown();
|
|
116
107
|
await this.disconnect();
|
|
117
|
-
this.
|
|
108
|
+
this.sessionStore.clear();
|
|
109
|
+
this.correlationStore.clear();
|
|
118
110
|
(_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
119
111
|
}
|
|
120
112
|
catch (error) {
|
|
121
|
-
this.logger.error('Error closing NostrServerTransport', {
|
|
122
|
-
error: error instanceof Error ? error.message : String(error),
|
|
123
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
124
|
-
});
|
|
125
113
|
(_b = this.onerror) === null || _b === void 0 ? void 0 : _b.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
126
|
-
|
|
114
|
+
this.logAndRethrowError('Error closing NostrServerTransport', error);
|
|
127
115
|
}
|
|
128
116
|
}
|
|
129
117
|
/**
|
|
@@ -151,230 +139,46 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
151
139
|
* @returns Promise that resolves to an array of deletion events that were published.
|
|
152
140
|
*/
|
|
153
141
|
async deleteAnnouncement(reason = 'Service offline') {
|
|
154
|
-
|
|
155
|
-
const events = [];
|
|
156
|
-
const kinds = [
|
|
157
|
-
SERVER_ANNOUNCEMENT_KIND,
|
|
158
|
-
TOOLS_LIST_KIND,
|
|
159
|
-
RESOURCES_LIST_KIND,
|
|
160
|
-
RESOURCETEMPLATES_LIST_KIND,
|
|
161
|
-
PROMPTS_LIST_KIND,
|
|
162
|
-
];
|
|
163
|
-
for (const kind of kinds) {
|
|
164
|
-
const filter = {
|
|
165
|
-
kinds: [kind],
|
|
166
|
-
authors: [publicKey],
|
|
167
|
-
};
|
|
168
|
-
// Collect events using the subscribe method with onEvent hook
|
|
169
|
-
await this.relayHandler.subscribe([filter], (event) => {
|
|
170
|
-
var _a;
|
|
171
|
-
try {
|
|
172
|
-
events.push(event);
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
this.logger.error('Error in relay subscription event collection', {
|
|
176
|
-
error: error instanceof Error ? error.message : String(error),
|
|
177
|
-
eventId: event.id,
|
|
178
|
-
});
|
|
179
|
-
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
if (!events.length) {
|
|
183
|
-
this.logger.info(`No events found for kind ${kind} to delete`);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
const deletionEventTemplate = {
|
|
187
|
-
kind: EventDeletion,
|
|
188
|
-
pubkey: publicKey,
|
|
189
|
-
content: reason,
|
|
190
|
-
tags: events.map((ev) => ['e', ev.id]),
|
|
191
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
192
|
-
};
|
|
193
|
-
const deletionEvent = await this.signer.signEvent(deletionEventTemplate);
|
|
194
|
-
await this.relayHandler.publish(deletionEvent);
|
|
195
|
-
this.logger.info(`Published deletion event for kind ${kind} (${events.length} events)`);
|
|
196
|
-
}
|
|
197
|
-
return events;
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Initiates the process of fetching announcement data from the server's internal logic.
|
|
201
|
-
* This method now properly handles the initialization handshake by first sending
|
|
202
|
-
* the initialize request, waiting for the response, and then proceeding with other announcements.
|
|
203
|
-
*/
|
|
204
|
-
async getAnnouncementData() {
|
|
205
|
-
var _a, _b, _c;
|
|
206
|
-
try {
|
|
207
|
-
const initializeParams = {
|
|
208
|
-
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
209
|
-
capabilities: {},
|
|
210
|
-
clientInfo: {
|
|
211
|
-
name: 'DummyClient',
|
|
212
|
-
version: '1.0.0',
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
// Send the initialize request if not already initialized
|
|
216
|
-
if (!this.isInitialized) {
|
|
217
|
-
const initializeMessage = {
|
|
218
|
-
jsonrpc: '2.0',
|
|
219
|
-
id: 'announcement',
|
|
220
|
-
method: 'initialize',
|
|
221
|
-
params: initializeParams,
|
|
222
|
-
};
|
|
223
|
-
this.logger.info('Sending initialize request for announcement');
|
|
224
|
-
(_a = this.onmessage) === null || _a === void 0 ? void 0 : _a.call(this, initializeMessage);
|
|
225
|
-
}
|
|
226
|
-
try {
|
|
227
|
-
// Wait for initialization to complete
|
|
228
|
-
await this.waitForInitialization();
|
|
229
|
-
// Send all announcements now that we're initialized
|
|
230
|
-
for (const [key, methodValue] of Object.entries(announcementMethods)) {
|
|
231
|
-
this.logger.info('Sending announcement', { key, methodValue });
|
|
232
|
-
const message = {
|
|
233
|
-
jsonrpc: '2.0',
|
|
234
|
-
id: 'announcement',
|
|
235
|
-
method: methodValue,
|
|
236
|
-
params: key === 'server' ? initializeParams : {},
|
|
237
|
-
};
|
|
238
|
-
(_b = this.onmessage) === null || _b === void 0 ? void 0 : _b.call(this, message);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
catch (error) {
|
|
242
|
-
this.logger.warn('Server not initialized after waiting, skipping announcements', { error: error instanceof Error ? error.message : error });
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
this.logger.error('Error in getAnnouncementData', {
|
|
247
|
-
error: error instanceof Error ? error.message : String(error),
|
|
248
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
249
|
-
});
|
|
250
|
-
(_c = this.onerror) === null || _c === void 0 ? void 0 : _c.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Waits for the server to be initialized with a timeout.
|
|
255
|
-
* @returns Promise that resolves when initialized or after 10-second timeout.
|
|
256
|
-
* The method will always resolve, allowing announcements to proceed.
|
|
257
|
-
*/
|
|
258
|
-
async waitForInitialization() {
|
|
259
|
-
if (this.isInitialized)
|
|
260
|
-
return;
|
|
261
|
-
if (!this.initializationPromise) {
|
|
262
|
-
this.initializationPromise = new Promise((resolve) => {
|
|
263
|
-
this.initializationResolver = resolve;
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Initialization timeout')), 10000));
|
|
267
|
-
try {
|
|
268
|
-
await Promise.race([this.initializationPromise, timeout]);
|
|
269
|
-
}
|
|
270
|
-
catch (_a) {
|
|
271
|
-
this.logger.warn('Server initialization not completed within timeout, proceeding with announcements');
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Handles the JSON-RPC responses for public server announcements and publishes
|
|
276
|
-
* them as Nostr events to the configured relays.
|
|
277
|
-
* @param message The JSON-RPC response containing the announcement data.
|
|
278
|
-
*/
|
|
279
|
-
async announcer(message) {
|
|
280
|
-
var _a;
|
|
281
|
-
try {
|
|
282
|
-
// Only process successful responses with result data
|
|
283
|
-
if (!isJSONRPCResultResponse(message) || !message.result) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const recipientPubkey = await this.getPublicKey();
|
|
287
|
-
const commonTags = this.generateCommonTags();
|
|
288
|
-
const announcementMapping = [
|
|
289
|
-
{
|
|
290
|
-
schema: InitializeResultSchema,
|
|
291
|
-
kind: SERVER_ANNOUNCEMENT_KIND,
|
|
292
|
-
tags: commonTags,
|
|
293
|
-
},
|
|
294
|
-
{ schema: ListToolsResultSchema, kind: TOOLS_LIST_KIND, tags: [] },
|
|
295
|
-
{
|
|
296
|
-
schema: ListResourcesResultSchema,
|
|
297
|
-
kind: RESOURCES_LIST_KIND,
|
|
298
|
-
tags: [],
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
schema: ListResourceTemplatesResultSchema,
|
|
302
|
-
kind: RESOURCETEMPLATES_LIST_KIND,
|
|
303
|
-
tags: [],
|
|
304
|
-
},
|
|
305
|
-
{ schema: ListPromptsResultSchema, kind: PROMPTS_LIST_KIND, tags: [] },
|
|
306
|
-
];
|
|
307
|
-
for (const mapping of announcementMapping) {
|
|
308
|
-
if (mapping.schema.safeParse(message.result).success) {
|
|
309
|
-
await this.sendMcpMessage(message.result, recipientPubkey, mapping.kind, mapping.tags);
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
this.logger.error('Error in announcer', {
|
|
316
|
-
error: error instanceof Error ? error.message : String(error),
|
|
317
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
318
|
-
});
|
|
319
|
-
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
320
|
-
}
|
|
142
|
+
return await this.announcementManager.deleteAnnouncement(reason);
|
|
321
143
|
}
|
|
322
144
|
/**
|
|
323
145
|
* Gets or creates a client session with proper initialization.
|
|
324
146
|
* @param clientPubkey The client's public key.
|
|
325
|
-
* @param now Current timestamp.
|
|
326
147
|
* @param isEncrypted Whether the session uses encryption.
|
|
327
148
|
* @returns The client session.
|
|
328
149
|
*/
|
|
329
|
-
getOrCreateClientSession(clientPubkey,
|
|
330
|
-
const session = this.
|
|
150
|
+
getOrCreateClientSession(clientPubkey, isEncrypted) {
|
|
151
|
+
const session = this.sessionStore.getSession(clientPubkey);
|
|
331
152
|
if (!session) {
|
|
332
153
|
this.logger.info(`Session created for ${clientPubkey}`);
|
|
333
|
-
const newSession = {
|
|
334
|
-
isInitialized: false,
|
|
335
|
-
isEncrypted,
|
|
336
|
-
lastActivity: now,
|
|
337
|
-
pendingRequests: new Map(),
|
|
338
|
-
eventToProgressToken: new Map(),
|
|
339
|
-
};
|
|
340
|
-
this.clientSessions.set(clientPubkey, newSession);
|
|
341
|
-
return newSession;
|
|
342
154
|
}
|
|
343
|
-
|
|
344
|
-
return session;
|
|
155
|
+
return this.sessionStore.getOrCreateSession(clientPubkey, isEncrypted);
|
|
345
156
|
}
|
|
346
157
|
/**
|
|
347
158
|
* Handles incoming requests with correlation tracking.
|
|
348
|
-
* @param session The client session.
|
|
349
159
|
* @param eventId The Nostr event ID.
|
|
350
160
|
* @param request The request message.
|
|
161
|
+
* @param clientPubkey The client's public key.
|
|
351
162
|
*/
|
|
352
|
-
handleIncomingRequest(
|
|
163
|
+
handleIncomingRequest(eventId, request, clientPubkey) {
|
|
353
164
|
var _a, _b;
|
|
354
165
|
// Store the original request ID for later restoration
|
|
355
166
|
const originalRequestId = request.id;
|
|
356
167
|
// Use the unique Nostr event ID as the MCP request ID to avoid collisions
|
|
357
168
|
request.id = eventId;
|
|
358
|
-
//
|
|
359
|
-
session.pendingRequests.set(eventId, originalRequestId);
|
|
360
|
-
this.eventIdToClient.set(eventId, clientPubkey);
|
|
361
|
-
// Track progress tokens if provided
|
|
169
|
+
// Register the event route in the correlation store
|
|
362
170
|
const progressToken = (_b = (_a = request.params) === null || _a === void 0 ? void 0 : _a._meta) === null || _b === void 0 ? void 0 : _b.progressToken;
|
|
363
|
-
|
|
364
|
-
const tokenStr = String(progressToken);
|
|
365
|
-
session.pendingRequests.set(tokenStr, eventId);
|
|
366
|
-
session.eventToProgressToken.set(eventId, tokenStr);
|
|
367
|
-
}
|
|
171
|
+
this.correlationStore.registerEventRoute(eventId, clientPubkey, originalRequestId, progressToken ? String(progressToken) : undefined);
|
|
368
172
|
}
|
|
369
173
|
/**
|
|
370
174
|
* Handles incoming notifications.
|
|
371
|
-
* @param
|
|
175
|
+
* @param clientPubkey The client's public key.
|
|
372
176
|
* @param notification The notification message.
|
|
373
177
|
*/
|
|
374
|
-
handleIncomingNotification(
|
|
178
|
+
handleIncomingNotification(clientPubkey, notification) {
|
|
375
179
|
if (isJSONRPCNotification(notification) &&
|
|
376
|
-
notification.method ===
|
|
377
|
-
|
|
180
|
+
notification.method === NOTIFICATIONS_INITIALIZED_METHOD) {
|
|
181
|
+
this.sessionStore.markInitialized(clientPubkey);
|
|
378
182
|
}
|
|
379
183
|
}
|
|
380
184
|
/**
|
|
@@ -382,64 +186,44 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
382
186
|
* @param response The JSON-RPC response or error to send.
|
|
383
187
|
*/
|
|
384
188
|
async handleResponse(response) {
|
|
385
|
-
var _a, _b
|
|
189
|
+
var _a, _b;
|
|
386
190
|
// Handle special announcement responses
|
|
387
191
|
if (response.id === 'announcement') {
|
|
388
|
-
|
|
192
|
+
const wasHandled = await this.announcementManager.handleAnnouncementResponse(response);
|
|
193
|
+
if (wasHandled && isJSONRPCResultResponse(response)) {
|
|
389
194
|
if (InitializeResultSchema.safeParse(response.result).success) {
|
|
390
|
-
this.isInitialized = true;
|
|
391
|
-
(_a = this.initializationResolver) === null || _a === void 0 ? void 0 : _a.call(this); // Resolve waiting promise
|
|
392
|
-
// Send the initialized notification
|
|
393
|
-
const initializedNotification = {
|
|
394
|
-
jsonrpc: '2.0',
|
|
395
|
-
method: 'notifications/initialized',
|
|
396
|
-
};
|
|
397
|
-
(_b = this.onmessage) === null || _b === void 0 ? void 0 : _b.call(this, initializedNotification);
|
|
398
195
|
this.logger.info('Initialized');
|
|
399
196
|
}
|
|
400
|
-
await this.announcer(response);
|
|
401
197
|
}
|
|
402
198
|
return;
|
|
403
199
|
}
|
|
404
|
-
// Find the
|
|
200
|
+
// Find the event route using O(1) lookup
|
|
405
201
|
const nostrEventId = response.id;
|
|
406
|
-
const
|
|
407
|
-
if (!
|
|
408
|
-
(
|
|
202
|
+
const route = this.correlationStore.getEventRoute(nostrEventId);
|
|
203
|
+
if (!route) {
|
|
204
|
+
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, new Error(`No pending request found for response ID: ${response.id}`));
|
|
409
205
|
return;
|
|
410
206
|
}
|
|
411
|
-
const session = this.
|
|
207
|
+
const session = this.sessionStore.getSession(route.clientPubkey);
|
|
412
208
|
if (!session) {
|
|
413
|
-
(
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
const originalRequestId = session.pendingRequests.get(nostrEventId);
|
|
417
|
-
if (originalRequestId === undefined) {
|
|
418
|
-
(_e = this.onerror) === null || _e === void 0 ? void 0 : _e.call(this, new Error(`No original request ID found for response ID: ${response.id}`));
|
|
209
|
+
(_b = this.onerror) === null || _b === void 0 ? void 0 : _b.call(this, new Error(`No session found for client: ${route.clientPubkey}`));
|
|
419
210
|
return;
|
|
420
211
|
}
|
|
421
212
|
// Restore the original request ID in the response
|
|
422
|
-
response.id = originalRequestId;
|
|
213
|
+
response.id = route.originalRequestId;
|
|
423
214
|
// Send the response back to the original requester
|
|
424
|
-
const tags = this.createResponseTags(
|
|
215
|
+
const tags = this.createResponseTags(route.clientPubkey, nostrEventId);
|
|
216
|
+
// Add common tags for initialize responses (independent of encryption mode)
|
|
425
217
|
if (isJSONRPCResultResponse(response) &&
|
|
426
|
-
InitializeResultSchema.safeParse(response.result).success
|
|
427
|
-
|
|
428
|
-
const commonTags = this.generateCommonTags();
|
|
218
|
+
InitializeResultSchema.safeParse(response.result).success) {
|
|
219
|
+
const commonTags = this.announcementManager.getCommonTags();
|
|
429
220
|
commonTags.forEach((tag) => {
|
|
430
221
|
tags.push(tag);
|
|
431
222
|
});
|
|
432
223
|
}
|
|
433
|
-
await this.sendMcpMessage(response,
|
|
434
|
-
// Clean up the
|
|
435
|
-
|
|
436
|
-
this.eventIdToClient.delete(nostrEventId);
|
|
437
|
-
// Clean up progress token if it exists
|
|
438
|
-
const progressToken = session.eventToProgressToken.get(nostrEventId);
|
|
439
|
-
if (progressToken) {
|
|
440
|
-
session.pendingRequests.delete(progressToken);
|
|
441
|
-
session.eventToProgressToken.delete(nostrEventId);
|
|
442
|
-
}
|
|
224
|
+
await this.sendMcpMessage(response, route.clientPubkey, CTXVM_MESSAGES_KIND, tags, session.isEncrypted);
|
|
225
|
+
// Clean up the event route (this also cleans up progress token mapping)
|
|
226
|
+
this.correlationStore.removeEventRoute(nostrEventId);
|
|
443
227
|
}
|
|
444
228
|
/**
|
|
445
229
|
* Handles notification messages with routing.
|
|
@@ -454,28 +238,22 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
454
238
|
notification.method === 'notifications/progress' &&
|
|
455
239
|
((_b = (_a = notification.params) === null || _a === void 0 ? void 0 : _a._meta) === null || _b === void 0 ? void 0 : _b.progressToken)) {
|
|
456
240
|
const token = String(notification.params._meta.progressToken);
|
|
457
|
-
// Use
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
targetClientPubkey = clientPubkey;
|
|
465
|
-
break;
|
|
241
|
+
// Use O(1) lookup for progress token routing
|
|
242
|
+
const nostrEventId = this.correlationStore.getEventIdByProgressToken(token);
|
|
243
|
+
if (nostrEventId) {
|
|
244
|
+
const route = this.correlationStore.getEventRoute(nostrEventId);
|
|
245
|
+
if (route) {
|
|
246
|
+
await this.sendNotification(route.clientPubkey, notification, nostrEventId);
|
|
247
|
+
return;
|
|
466
248
|
}
|
|
467
249
|
}
|
|
468
|
-
if (targetClientPubkey && nostrEventId) {
|
|
469
|
-
await this.sendNotification(targetClientPubkey, notification, nostrEventId);
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
250
|
const error = new Error(`No client found for progress token: ${token}`);
|
|
473
251
|
this.logger.error('Progress token not found', { token });
|
|
474
252
|
(_c = this.onerror) === null || _c === void 0 ? void 0 : _c.call(this, error);
|
|
475
253
|
return;
|
|
476
254
|
}
|
|
477
255
|
// Use TaskQueue for outbound notification broadcasting to prevent event loop blocking
|
|
478
|
-
for (const [clientPubkey, session] of this.
|
|
256
|
+
for (const [clientPubkey, session,] of this.sessionStore.getAllSessions()) {
|
|
479
257
|
if (session.isInitialized) {
|
|
480
258
|
this.taskQueue.add(async () => {
|
|
481
259
|
try {
|
|
@@ -509,7 +287,7 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
509
287
|
* @returns Promise that resolves when the notification is sent.
|
|
510
288
|
*/
|
|
511
289
|
async sendNotification(clientPubkey, notification, correlatedEventId) {
|
|
512
|
-
const session = this.
|
|
290
|
+
const session = this.sessionStore.getSession(clientPubkey);
|
|
513
291
|
if (!session) {
|
|
514
292
|
throw new Error(`No active session found for client: ${clientPubkey}`);
|
|
515
293
|
}
|
|
@@ -557,7 +335,8 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
557
335
|
return;
|
|
558
336
|
}
|
|
559
337
|
try {
|
|
560
|
-
const decryptedJson = await decryptMessage(event, this.signer)
|
|
338
|
+
const decryptedJson = await withTimeout(decryptMessage(event, this.signer), 30000, // 30 seconds default timeout
|
|
339
|
+
'Decrypt message timed out');
|
|
561
340
|
const currentEvent = JSON.parse(decryptedJson);
|
|
562
341
|
this.authorizeAndProcessEvent(currentEvent, true);
|
|
563
342
|
}
|
|
@@ -584,34 +363,6 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
584
363
|
}
|
|
585
364
|
this.authorizeAndProcessEvent(event, false);
|
|
586
365
|
}
|
|
587
|
-
/**
|
|
588
|
-
* Checks if a capability is excluded from whitelisting requirements.
|
|
589
|
-
* @param method The JSON-RPC method (e.g., 'tools/call', 'tools/list')
|
|
590
|
-
* @param name Optional capability name for method-specific exclusions (e.g., 'get_weather')
|
|
591
|
-
* @returns true if the capability should bypass whitelisting, false otherwise
|
|
592
|
-
*/
|
|
593
|
-
isCapabilityExcluded(method, name) {
|
|
594
|
-
var _a;
|
|
595
|
-
// Always allow fundamental MCP methods for connection establishment
|
|
596
|
-
if (method === 'initialize' || method === 'notifications/initialized') {
|
|
597
|
-
return true;
|
|
598
|
-
}
|
|
599
|
-
if (!((_a = this.excludedCapabilities) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
return this.excludedCapabilities.some((exclusion) => {
|
|
603
|
-
// Check if method matches
|
|
604
|
-
if (exclusion.method !== method) {
|
|
605
|
-
return false;
|
|
606
|
-
}
|
|
607
|
-
// If exclusion has no name requirement, method match is sufficient
|
|
608
|
-
if (!exclusion.name) {
|
|
609
|
-
return true;
|
|
610
|
-
}
|
|
611
|
-
// If exclusion has a name requirement, check if it matches the provided name
|
|
612
|
-
return exclusion.name === name;
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
366
|
/**
|
|
616
367
|
* Authorizes and processes an incoming Nostr event, handling message validation,
|
|
617
368
|
* client authorization, session management, and optional client public key injection.
|
|
@@ -619,7 +370,7 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
619
370
|
* @param isEncrypted Whether the original event was encrypted.
|
|
620
371
|
*/
|
|
621
372
|
authorizeAndProcessEvent(event, isEncrypted) {
|
|
622
|
-
var _a, _b
|
|
373
|
+
var _a, _b;
|
|
623
374
|
try {
|
|
624
375
|
const mcpMessage = this.convertNostrEventToMcpMessage(event);
|
|
625
376
|
if (!mcpMessage) {
|
|
@@ -630,52 +381,48 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
630
381
|
});
|
|
631
382
|
return;
|
|
632
383
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
this.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
pubkey: event.pubkey,
|
|
656
|
-
eventId: event.id,
|
|
657
|
-
});
|
|
658
|
-
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, new Error(`Failed to send unauthorized response: ${err}`));
|
|
384
|
+
// Check authorization using the authorization policy
|
|
385
|
+
const authDecision = this.authorizationPolicy.authorize(event.pubkey, mcpMessage);
|
|
386
|
+
if (!authDecision.allowed) {
|
|
387
|
+
this.logger.error(`Unauthorized message from ${event.pubkey}, message: ${JSON.stringify(mcpMessage)}. Ignoring.`);
|
|
388
|
+
if ('shouldReplyUnauthorized' in authDecision &&
|
|
389
|
+
authDecision.shouldReplyUnauthorized &&
|
|
390
|
+
isJSONRPCRequest(mcpMessage)) {
|
|
391
|
+
const errorResponse = {
|
|
392
|
+
jsonrpc: '2.0',
|
|
393
|
+
id: mcpMessage.id,
|
|
394
|
+
error: {
|
|
395
|
+
code: -32000,
|
|
396
|
+
message: 'Unauthorized',
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
const tags = this.createResponseTags(event.pubkey, event.id);
|
|
400
|
+
this.sendMcpMessage(errorResponse, event.pubkey, CTXVM_MESSAGES_KIND, tags, isEncrypted).catch((err) => {
|
|
401
|
+
var _a;
|
|
402
|
+
this.logger.error('Failed to send unauthorized response', {
|
|
403
|
+
error: err instanceof Error ? err.message : String(err),
|
|
404
|
+
pubkey: event.pubkey,
|
|
405
|
+
eventId: event.id,
|
|
659
406
|
});
|
|
660
|
-
|
|
661
|
-
|
|
407
|
+
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, new Error(`Failed to send unauthorized response: ${err}`));
|
|
408
|
+
});
|
|
662
409
|
}
|
|
410
|
+
return;
|
|
663
411
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
session.lastActivity = now;
|
|
412
|
+
// Get or create session for this client (ensures session exists for authorized messages)
|
|
413
|
+
this.getOrCreateClientSession(event.pubkey, isEncrypted);
|
|
667
414
|
// Handle message routing and conditionally inject client pubkey
|
|
668
415
|
if (isJSONRPCRequest(mcpMessage)) {
|
|
669
|
-
this.handleIncomingRequest(
|
|
416
|
+
this.handleIncomingRequest(event.id, mcpMessage, event.pubkey);
|
|
670
417
|
// Inject client public key for enhanced server integration (in-place mutation)
|
|
671
418
|
if (this.injectClientPubkey) {
|
|
672
419
|
injectClientPubkey(mcpMessage, event.pubkey);
|
|
673
420
|
}
|
|
674
421
|
}
|
|
675
422
|
else if (isJSONRPCNotification(mcpMessage)) {
|
|
676
|
-
this.handleIncomingNotification(
|
|
423
|
+
this.handleIncomingNotification(event.pubkey, mcpMessage);
|
|
677
424
|
}
|
|
678
|
-
(
|
|
425
|
+
(_a = this.onmessage) === null || _a === void 0 ? void 0 : _a.call(this, mcpMessage);
|
|
679
426
|
}
|
|
680
427
|
catch (error) {
|
|
681
428
|
this.logger.error('Error in authorizeAndProcessEvent', {
|
|
@@ -684,8 +431,18 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
684
431
|
eventId: event.id,
|
|
685
432
|
pubkey: event.pubkey,
|
|
686
433
|
});
|
|
687
|
-
(
|
|
434
|
+
(_b = this.onerror) === null || _b === void 0 ? void 0 : _b.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
688
435
|
}
|
|
689
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Test-only accessor for internal state.
|
|
439
|
+
* @internal
|
|
440
|
+
*/
|
|
441
|
+
getInternalStateForTesting() {
|
|
442
|
+
return {
|
|
443
|
+
sessionStore: this.sessionStore,
|
|
444
|
+
correlationStore: this.correlationStore,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
690
447
|
}
|
|
691
448
|
//# sourceMappingURL=nostr-server-transport.js.map
|