@elliotding/ai-agent-mcp 0.1.20 → 0.1.22
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/ai-resource-telemetry.json +1 -1
- package/dist/git/multi-source-manager.d.ts.map +1 -1
- package/dist/git/multi-source-manager.js +157 -28
- package/dist/git/multi-source-manager.js.map +1 -1
- package/dist/prompts/generator.d.ts +1 -1
- package/dist/prompts/generator.d.ts.map +1 -1
- package/dist/prompts/generator.js +2 -0
- package/dist/prompts/generator.js.map +1 -1
- package/dist/prompts/manager.d.ts.map +1 -1
- package/dist/prompts/manager.js +3 -0
- package/dist/prompts/manager.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +49 -17
- package/dist/server/http.js.map +1 -1
- package/dist/tools/sync-resources.d.ts.map +1 -1
- package/dist/tools/sync-resources.js +12 -7
- package/dist/tools/sync-resources.js.map +1 -1
- package/package.json +1 -1
- package/src/git/multi-source-manager.ts +164 -39
- package/src/prompts/generator.ts +2 -1
- package/src/prompts/manager.ts +3 -0
- package/src/server/http.ts +65 -19
- package/src/tools/sync-resources.ts +13 -8
package/src/server/http.ts
CHANGED
|
@@ -315,15 +315,69 @@ export class HTTPServer {
|
|
|
315
315
|
// See config/index.ts for details.
|
|
316
316
|
const basePath = config.http?.basePath ?? '';
|
|
317
317
|
const publicOrigin = config.http?.publicOrigin ?? `http://127.0.0.1:${config.http?.port ?? 3000}`;
|
|
318
|
-
|
|
319
|
-
// We pass only the path to SSEServerTransport so its internal URL
|
|
320
|
-
// parsing produces a clean relative URL. After connect() we re-send
|
|
321
|
-
// an absolute endpoint event so Cursor (which may sit behind a proxy
|
|
322
|
-
// with a different origin than its SSE connection) uses the correct
|
|
323
|
-
// public address for all subsequent JSON-RPC POST messages.
|
|
324
318
|
const messagePath = `${basePath}/message`;
|
|
325
|
-
|
|
319
|
+
|
|
320
|
+
// The MCP SDK SSEServerTransport.start() emits an `endpoint` SSE event
|
|
321
|
+
// whose data is a *relative* path (pathname + ?sessionId=...), stripping
|
|
322
|
+
// the origin. When deployed behind nginx, Cursor resolves this relative
|
|
323
|
+
// path against whatever origin it used to open the SSE connection, which
|
|
324
|
+
// may differ from our public API origin. The result is that GetPrompt /
|
|
325
|
+
// tools/call POST requests go to the wrong address and never arrive.
|
|
326
|
+
//
|
|
327
|
+
// Fix: intercept the raw response stream's write() method. When the SDK
|
|
328
|
+
// emits the relative endpoint event we replace it on-the-fly with the
|
|
329
|
+
// full absolute URL so Cursor always uses the correct public address.
|
|
330
|
+
// Only ONE endpoint event is ever written to the wire this way.
|
|
331
|
+
const rawRes = reply.raw;
|
|
332
|
+
const originalWrite = rawRes.write.bind(rawRes);
|
|
333
|
+
(rawRes as NodeJS.WritableStream & { write: typeof originalWrite }).write = (
|
|
334
|
+
chunk: unknown,
|
|
335
|
+
encodingOrCb?: unknown,
|
|
336
|
+
cb?: unknown,
|
|
337
|
+
): boolean => {
|
|
338
|
+
if (typeof chunk === 'string' && chunk.startsWith('event: endpoint\ndata:')) {
|
|
339
|
+
// The SDK wrote a relative endpoint event — replace with absolute URL.
|
|
340
|
+
// We know the sessionId from transport.sessionId (read after construction).
|
|
341
|
+
// Use a placeholder here; replaced below once we have the transport.
|
|
342
|
+
// (This interceptor is set before connect(), so the write happens during
|
|
343
|
+
// connect() → transport.start().)
|
|
344
|
+
chunk = chunk.replace(
|
|
345
|
+
/^(event: endpoint\ndata:).*/,
|
|
346
|
+
`$1 ${publicOrigin}${messagePath}?sessionId=__SESSION_ID__`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (typeof encodingOrCb === 'function') {
|
|
350
|
+
return originalWrite(chunk as string, encodingOrCb as () => void);
|
|
351
|
+
}
|
|
352
|
+
if (typeof cb === 'function') {
|
|
353
|
+
return originalWrite(chunk as string, encodingOrCb as BufferEncoding, cb as () => void);
|
|
354
|
+
}
|
|
355
|
+
return originalWrite(chunk as string);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const transport = new SSEServerTransport(messagePath, rawRes);
|
|
326
359
|
const sdkSessionId = transport.sessionId;
|
|
360
|
+
|
|
361
|
+
// Now patch the placeholder with the real sessionId that the SDK assigned.
|
|
362
|
+
// The write interceptor is still active during connect() → start(), so we
|
|
363
|
+
// swap it out for a version that knows the actual sessionId.
|
|
364
|
+
(rawRes as NodeJS.WritableStream & { write: typeof originalWrite }).write = (
|
|
365
|
+
chunk: unknown,
|
|
366
|
+
encodingOrCb?: unknown,
|
|
367
|
+
cb?: unknown,
|
|
368
|
+
): boolean => {
|
|
369
|
+
if (typeof chunk === 'string' && chunk.startsWith('event: endpoint\ndata:')) {
|
|
370
|
+
chunk = `event: endpoint\ndata: ${publicOrigin}${messagePath}?sessionId=${sdkSessionId}\n\n`;
|
|
371
|
+
}
|
|
372
|
+
if (typeof encodingOrCb === 'function') {
|
|
373
|
+
return originalWrite(chunk as string, encodingOrCb as () => void);
|
|
374
|
+
}
|
|
375
|
+
if (typeof cb === 'function') {
|
|
376
|
+
return originalWrite(chunk as string, encodingOrCb as BufferEncoding, cb as () => void);
|
|
377
|
+
}
|
|
378
|
+
return originalWrite(chunk as string);
|
|
379
|
+
};
|
|
380
|
+
|
|
327
381
|
this.sseTransports.set(sdkSessionId, transport);
|
|
328
382
|
|
|
329
383
|
transport.onclose = () => {
|
|
@@ -337,29 +391,21 @@ export class HTTPServer {
|
|
|
337
391
|
logger.warn({ sdkSessionId, error: err.message }, 'SSE transport error');
|
|
338
392
|
};
|
|
339
393
|
|
|
340
|
-
// Create a per-connection MCP Server and connect it to the transport.
|
|
341
|
-
// connect() calls transport.start() which writes the initial (relative)
|
|
342
|
-
// endpoint SSE event. We then immediately overwrite it with an absolute
|
|
343
|
-
// URL so Cursor uses the correct public address regardless of how nginx
|
|
344
|
-
// forwards the SSE connection.
|
|
345
394
|
const mcpServer = this.createMcpServer(
|
|
346
395
|
request.user?.userId,
|
|
347
396
|
request.user?.email,
|
|
348
397
|
request.user?.groups,
|
|
349
398
|
token,
|
|
350
399
|
);
|
|
400
|
+
|
|
401
|
+
// connect() calls transport.start() which triggers the intercepted write()
|
|
402
|
+
// above — emitting exactly ONE absolute endpoint event to the wire.
|
|
351
403
|
await mcpServer.connect(transport);
|
|
352
404
|
|
|
353
|
-
// Re-send the endpoint event with the full absolute URL.
|
|
354
|
-
// This overwrites the relative-path event emitted by the SDK and
|
|
355
|
-
// ensures Cursor POSTs subsequent JSON-RPC messages (prompts/get,
|
|
356
|
-
// tools/call, etc.) to the correct externally-reachable address.
|
|
357
405
|
const absoluteMessageUrl = `${publicOrigin}${messagePath}?sessionId=${sdkSessionId}`;
|
|
358
|
-
reply.raw.write(`event: endpoint\ndata: ${absoluteMessageUrl}\n\n`);
|
|
359
|
-
|
|
360
406
|
logger.info(
|
|
361
407
|
{ sdkSessionId, absoluteMessageUrl, publicOrigin },
|
|
362
|
-
'SSE stream established — absolute endpoint
|
|
408
|
+
'SSE stream established — absolute endpoint URL intercepted and sent',
|
|
363
409
|
);
|
|
364
410
|
|
|
365
411
|
// Handle client disconnect
|
|
@@ -580,12 +580,15 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
580
580
|
}
|
|
581
581
|
|
|
582
582
|
// ── Rule resource ─────────────────────────────────────────────────────
|
|
583
|
-
// Return write_file actions; the AI
|
|
584
|
-
// Each action carries content_hash so the AI
|
|
585
|
-
// the local file
|
|
583
|
+
// Return write_file actions; the AI Agent executes them on the user's
|
|
584
|
+
// LOCAL machine. Each action carries content_hash (SHA-256) so the AI
|
|
585
|
+
// can compare against the existing local file and skip the write when
|
|
586
|
+
// the digests match — avoiding unnecessary disk I/O. If the local file
|
|
587
|
+
// is missing or has different content, the AI writes it unconditionally,
|
|
588
|
+
// which also recovers files that were accidentally deleted by the user.
|
|
586
589
|
if (sub.type === 'rule') {
|
|
587
590
|
const typeDir = getCursorTypeDirForClient(sub.type);
|
|
588
|
-
const writeActions: string
|
|
591
|
+
const writeActions: Array<{ destPath: string; hash: string; contentLength: number }> = [];
|
|
589
592
|
|
|
590
593
|
for (const file of resourceFiles) {
|
|
591
594
|
const normalised = path.normalize(file.path);
|
|
@@ -594,13 +597,14 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
594
597
|
continue;
|
|
595
598
|
}
|
|
596
599
|
const destPath = `${typeDir}/${normalised}`;
|
|
600
|
+
const contentHash = sha256(file.content);
|
|
597
601
|
localActions.push({
|
|
598
602
|
action: 'write_file',
|
|
599
603
|
path: destPath,
|
|
600
604
|
content: file.content,
|
|
601
|
-
content_hash:
|
|
605
|
+
content_hash: contentHash,
|
|
602
606
|
});
|
|
603
|
-
writeActions.push(destPath);
|
|
607
|
+
writeActions.push({ destPath, hash: contentHash, contentLength: file.content.length });
|
|
604
608
|
}
|
|
605
609
|
|
|
606
610
|
logger.info(
|
|
@@ -609,9 +613,10 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
609
613
|
resourceName: sub.name,
|
|
610
614
|
typeDir,
|
|
611
615
|
fileCount: writeActions.length,
|
|
612
|
-
|
|
616
|
+
files: writeActions,
|
|
617
|
+
clientSideNote: 'AI will compare content_hash against local file SHA-256; write is skipped if equal, executed if different or file missing',
|
|
613
618
|
},
|
|
614
|
-
'sync_resources: Rule — write_file actions queued for AI',
|
|
619
|
+
'sync_resources: Rule — write_file actions queued for AI (client-side hash comparison)',
|
|
615
620
|
);
|
|
616
621
|
|
|
617
622
|
tally.synced++;
|