@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.
@@ -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 via the `content_hash` field
49
- * on write_file actions and `skip_if_exists` on merge_mcp_json actions.
50
- * This ensures a manual sync always re-delivers actions so the user can
51
- * recover deleted local files, even when the resource content is unchanged.
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', { mode, scope, types });
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 writes the files locally.
584
- // Each action carries content_hash so the AI can skip the write when
585
- // the local file already has identical content.
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
- destPaths: writeActions,
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) If the action has a `content_hash` field, read the existing file (if any), compute its SHA-256, ' +
740
- ' and SKIP the write if the digests match the file is already up-to-date. ' +
741
- ' (2) Otherwise create any missing parent directories then write the file. ' +
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,
@@ -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 {