@elliotding/ai-agent-mcp 0.1.21 → 0.1.23
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 +450 -0
- 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/tools/sync-resources.d.ts +4 -0
- package/dist/tools/sync-resources.d.ts.map +1 -1
- package/dist/tools/sync-resources.js +64 -20
- package/dist/tools/sync-resources.js.map +1 -1
- package/dist/types/tools.d.ts +11 -7
- package/dist/types/tools.d.ts.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/tools/sync-resources.ts +77 -26
- package/src/types/tools.ts +11 -7
package/src/prompts/manager.ts
CHANGED
|
@@ -433,6 +433,9 @@ export class PromptManager {
|
|
|
433
433
|
// Generate and write the intermediate cache file (shared across users since
|
|
434
434
|
// content is the same; only the in-memory registry is per-user).
|
|
435
435
|
try {
|
|
436
|
+
if (!meta.rawContent) {
|
|
437
|
+
throw new Error('rawContent is empty — skipping cache generation');
|
|
438
|
+
}
|
|
436
439
|
const tmpBase = promptCache.directory;
|
|
437
440
|
promptCache.ensureDir();
|
|
438
441
|
const rawExpanded = await generatePromptContentFromString(
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
|
|
19
19
|
import * as fs from 'fs/promises';
|
|
20
20
|
import * as path from 'path';
|
|
21
|
-
import { createHash } from 'crypto';
|
|
22
21
|
import { logger, logToolCall, logToolStep, logToolResult } from '../utils/logger';
|
|
23
22
|
import { apiClient } from '../api/client';
|
|
24
23
|
import { multiSourceGitManager } from '../git/multi-source-manager';
|
|
@@ -45,10 +44,11 @@ import { promptManager } from '../prompts/index.js';
|
|
|
45
44
|
*
|
|
46
45
|
* IMPORTANT — this cache ONLY skips the network download; it NEVER skips
|
|
47
46
|
* generating LocalAction instructions. Whether the user's local files are
|
|
48
|
-
* already up-to-date is determined client-side
|
|
49
|
-
*
|
|
50
|
-
* This ensures a manual sync always re-delivers
|
|
51
|
-
* recover deleted local files, even when the resource
|
|
47
|
+
* already up-to-date is determined client-side by direct content comparison
|
|
48
|
+
* (string equality check) in write_file actions and `skip_if_exists` checks
|
|
49
|
+
* on merge_mcp_json actions. This ensures a manual sync always re-delivers
|
|
50
|
+
* actions so the user can recover deleted local files, even when the resource
|
|
51
|
+
* content is unchanged.
|
|
52
52
|
*
|
|
53
53
|
* Key format: `${userToken}::${resourceId}`
|
|
54
54
|
* Value: the last downloadResource() response (hash + files).
|
|
@@ -65,11 +65,6 @@ function syncCacheKey(userToken: string, resourceId: string): string {
|
|
|
65
65
|
return `${userToken}::${resourceId}`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/** Compute SHA-256 hex digest of a UTF-8 string. */
|
|
69
|
-
function sha256(content: string): string {
|
|
70
|
-
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
68
|
/**
|
|
74
69
|
* Extract the `description` field from YAML frontmatter in a Markdown file.
|
|
75
70
|
* Frontmatter is delimited by leading `---` and closing `---` lines.
|
|
@@ -106,8 +101,14 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
106
101
|
const scope = typedParams.scope || 'global';
|
|
107
102
|
const types = typedParams.types;
|
|
108
103
|
const userToken = typedParams.user_token;
|
|
104
|
+
const configuredMcpServers = new Set(typedParams.configured_mcp_servers || []);
|
|
109
105
|
|
|
110
|
-
logToolStep('sync_resources', 'Parameters validated', {
|
|
106
|
+
logToolStep('sync_resources', 'Parameters validated', {
|
|
107
|
+
mode,
|
|
108
|
+
scope,
|
|
109
|
+
types,
|
|
110
|
+
configuredMcpCount: configuredMcpServers.size,
|
|
111
|
+
});
|
|
111
112
|
|
|
112
113
|
// ── Step 1: Fetch subscription list ────────────────────────────────────
|
|
113
114
|
logToolStep('sync_resources', 'Step 1: Fetching subscriptions from API', { scope, types });
|
|
@@ -420,6 +421,44 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
420
421
|
// ~/.cursor/mcp.json on the user's machine
|
|
421
422
|
const mcpJsonPath = `${getCursorRootDirForClient()}/mcp.json`;
|
|
422
423
|
|
|
424
|
+
// ── Optimization: skip if already configured (incremental mode only) ────
|
|
425
|
+
// In incremental mode, if the AI Agent reports this MCP server is already
|
|
426
|
+
// in ~/.cursor/mcp.json, skip downloading and generating write_file actions.
|
|
427
|
+
// This reduces API calls, network traffic, and AI Agent execution overhead.
|
|
428
|
+
// In full mode, always proceed to allow file recovery.
|
|
429
|
+
if (mode === 'incremental' && mcpConfigFile) {
|
|
430
|
+
let cfg: Record<string, unknown> = {};
|
|
431
|
+
try { cfg = JSON.parse(mcpConfigFile.content) as Record<string, unknown>; }
|
|
432
|
+
catch { /* ignore parse errors, proceed to download */ }
|
|
433
|
+
|
|
434
|
+
// Format A: check if the single server is configured
|
|
435
|
+
if (typeof cfg['command'] === 'string') {
|
|
436
|
+
const serverName = (cfg['name'] as string | undefined) ?? sub.name;
|
|
437
|
+
if (configuredMcpServers.has(serverName)) {
|
|
438
|
+
logger.info(
|
|
439
|
+
{ resourceId: sub.id, resourceName: sub.name, serverName },
|
|
440
|
+
'sync_resources: MCP server already configured — skipping download',
|
|
441
|
+
);
|
|
442
|
+
tally.cached++;
|
|
443
|
+
details.push({ id: sub.id, name: sub.name, action: 'cached', version: resourceVersion });
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Format B: check if all servers in the map are configured
|
|
448
|
+
else if (Object.keys(cfg).length > 0) {
|
|
449
|
+
const allConfigured = Object.keys(cfg).every((k) => configuredMcpServers.has(k));
|
|
450
|
+
if (allConfigured) {
|
|
451
|
+
logger.info(
|
|
452
|
+
{ resourceId: sub.id, resourceName: sub.name, serverKeys: Object.keys(cfg) },
|
|
453
|
+
'sync_resources: All MCP servers already configured — skipping download',
|
|
454
|
+
);
|
|
455
|
+
tally.cached++;
|
|
456
|
+
details.push({ id: sub.id, name: sub.name, action: 'cached', version: resourceVersion });
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
423
462
|
logger.info(
|
|
424
463
|
{
|
|
425
464
|
resourceId: sub.id,
|
|
@@ -449,13 +488,10 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
449
488
|
const normalised = path.normalize(file.path);
|
|
450
489
|
if (normalised.startsWith('..')) continue;
|
|
451
490
|
const fileDest = `${installDir}/${normalised}`;
|
|
452
|
-
// Carry content_hash so the AI can skip the write when the
|
|
453
|
-
// local file already has identical content.
|
|
454
491
|
localActions.push({
|
|
455
492
|
action: 'write_file',
|
|
456
493
|
path: fileDest,
|
|
457
494
|
content: file.content,
|
|
458
|
-
content_hash: sha256(file.content),
|
|
459
495
|
});
|
|
460
496
|
writeActions.push(fileDest);
|
|
461
497
|
}
|
|
@@ -542,7 +578,6 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
542
578
|
action: 'write_file',
|
|
543
579
|
path: fileDest,
|
|
544
580
|
content: file.content,
|
|
545
|
-
content_hash: sha256(file.content),
|
|
546
581
|
});
|
|
547
582
|
writeActions.push(fileDest);
|
|
548
583
|
}
|
|
@@ -580,12 +615,15 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
580
615
|
}
|
|
581
616
|
|
|
582
617
|
// ── Rule resource ─────────────────────────────────────────────────────
|
|
583
|
-
// Return write_file actions; the AI
|
|
584
|
-
//
|
|
585
|
-
// the local file
|
|
618
|
+
// Return write_file actions; the AI Agent executes them on the user's
|
|
619
|
+
// LOCAL machine. The AI compares file content directly (string equality)
|
|
620
|
+
// against the existing local file and skips the write when content is
|
|
621
|
+
// identical — avoiding unnecessary disk I/O. If the local file is missing
|
|
622
|
+
// or has different content, the AI writes it unconditionally, which also
|
|
623
|
+
// recovers files that were accidentally deleted by the user.
|
|
586
624
|
if (sub.type === 'rule') {
|
|
587
625
|
const typeDir = getCursorTypeDirForClient(sub.type);
|
|
588
|
-
const writeActions: string
|
|
626
|
+
const writeActions: Array<{ destPath: string; contentLength: number }> = [];
|
|
589
627
|
|
|
590
628
|
for (const file of resourceFiles) {
|
|
591
629
|
const normalised = path.normalize(file.path);
|
|
@@ -598,9 +636,8 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
598
636
|
action: 'write_file',
|
|
599
637
|
path: destPath,
|
|
600
638
|
content: file.content,
|
|
601
|
-
content_hash: sha256(file.content),
|
|
602
639
|
});
|
|
603
|
-
writeActions.push(destPath);
|
|
640
|
+
writeActions.push({ destPath, contentLength: file.content.length });
|
|
604
641
|
}
|
|
605
642
|
|
|
606
643
|
logger.info(
|
|
@@ -609,9 +646,10 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
609
646
|
resourceName: sub.name,
|
|
610
647
|
typeDir,
|
|
611
648
|
fileCount: writeActions.length,
|
|
612
|
-
|
|
649
|
+
files: writeActions,
|
|
650
|
+
clientSideNote: 'AI will compare file content directly; write is skipped if content is identical',
|
|
613
651
|
},
|
|
614
|
-
'sync_resources: Rule — write_file actions queued for AI',
|
|
652
|
+
'sync_resources: Rule — write_file actions queued for AI (client-side content comparison)',
|
|
615
653
|
);
|
|
616
654
|
|
|
617
655
|
tally.synced++;
|
|
@@ -735,10 +773,15 @@ export const syncResourcesTool = {
|
|
|
735
773
|
'write_file, merge_mcp_json, or other actions that the AI Agent MUST execute on the ' +
|
|
736
774
|
'USER\'S LOCAL MACHINE after receiving the response. ' +
|
|
737
775
|
'Execute every action in the list in order before reporting success to the user. ' +
|
|
776
|
+
'OPTIMIZATION: Before calling this tool in incremental mode, read ~/.cursor/mcp.json ' +
|
|
777
|
+
'and pass Object.keys(mcpServers || {}) as `configured_mcp_servers` parameter. ' +
|
|
778
|
+
'This allows the server to skip downloading MCP resources that are already configured, ' +
|
|
779
|
+
'significantly reducing API calls, network traffic, and local action overhead. ' +
|
|
738
780
|
'For write_file actions: ' +
|
|
739
|
-
' (1)
|
|
740
|
-
'
|
|
741
|
-
' (
|
|
781
|
+
' (1) Read the existing file at `path` (if it exists) using fs.readFile() or equivalent. ' +
|
|
782
|
+
' (2) Compare the file content directly (string equality) against the `content` field. ' +
|
|
783
|
+
' (3) SKIP the write if they are EXACTLY equal — the file is already up-to-date. ' +
|
|
784
|
+
' (4) Otherwise, create any missing parent directories and write the file. ' +
|
|
742
785
|
'For merge_mcp_json actions: ' +
|
|
743
786
|
' (1) If the action has `skip_if_exists: true` AND `mcpServers[server_name]` already exists ' +
|
|
744
787
|
' in ~/.cursor/mcp.json, SKIP this action entirely — the server is already configured ' +
|
|
@@ -770,6 +813,14 @@ export const syncResourcesTool = {
|
|
|
770
813
|
'DO NOT set this field — it is automatically injected by the MCP server from ' +
|
|
771
814
|
'the authenticated SSE connection. The server always provides the correct token.',
|
|
772
815
|
},
|
|
816
|
+
configured_mcp_servers: {
|
|
817
|
+
type: 'array',
|
|
818
|
+
description:
|
|
819
|
+
'List of MCP server names already configured in ~/.cursor/mcp.json. ' +
|
|
820
|
+
'In incremental mode, the server skips downloading these MCP resources to reduce overhead. ' +
|
|
821
|
+
'To populate this: read ~/.cursor/mcp.json and extract Object.keys(mcpServers || {}). ' +
|
|
822
|
+
'Example: ["github", "gitlab", "postgres"]. Ignored in full mode (always downloads).',
|
|
823
|
+
},
|
|
773
824
|
},
|
|
774
825
|
},
|
|
775
826
|
handler: syncResources,
|
package/src/types/tools.ts
CHANGED
|
@@ -22,13 +22,6 @@ export interface WriteFileAction {
|
|
|
22
22
|
path: string;
|
|
23
23
|
/** UTF-8 file content to write. */
|
|
24
24
|
content: string;
|
|
25
|
-
/**
|
|
26
|
-
* SHA-256 hex digest of `content` (without leading "sha256:").
|
|
27
|
-
* When present, the AI MUST read the existing file at `path` (if any),
|
|
28
|
-
* compute its SHA-256, and SKIP the write if the digests match.
|
|
29
|
-
* This prevents redundant writes on incremental syncs.
|
|
30
|
-
*/
|
|
31
|
-
content_hash?: string;
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
export interface DeleteFileAction {
|
|
@@ -113,6 +106,17 @@ export interface SyncResourcesParams {
|
|
|
113
106
|
* makes API calls with their own identity.
|
|
114
107
|
*/
|
|
115
108
|
user_token?: string;
|
|
109
|
+
/**
|
|
110
|
+
* List of MCP server names that are already configured in the user's
|
|
111
|
+
* ~/.cursor/mcp.json. The server will skip downloading and generating
|
|
112
|
+
* write_file actions for these MCP resources to reduce overhead.
|
|
113
|
+
*
|
|
114
|
+
* Set this to the keys from mcpServers in ~/.cursor/mcp.json:
|
|
115
|
+
* Object.keys(JSON.parse(fs.readFileSync('~/.cursor/mcp.json')).mcpServers || {})
|
|
116
|
+
*
|
|
117
|
+
* Only applies in 'incremental' mode; 'full' mode always downloads everything.
|
|
118
|
+
*/
|
|
119
|
+
configured_mcp_servers?: string[];
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
export interface McpSetupItem {
|