@elliotding/ai-agent-mcp 0.1.4 → 0.1.6

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.
Files changed (38) hide show
  1. package/ai-resource-telemetry.json +19 -1
  2. package/dist/api/client.d.ts.map +1 -1
  3. package/dist/api/client.js +3 -4
  4. package/dist/api/client.js.map +1 -1
  5. package/dist/auth/token-validator.d.ts.map +1 -1
  6. package/dist/auth/token-validator.js +1 -3
  7. package/dist/auth/token-validator.js.map +1 -1
  8. package/dist/index.js +3 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/prompts/manager.d.ts.map +1 -1
  11. package/dist/prompts/manager.js +2 -3
  12. package/dist/prompts/manager.js.map +1 -1
  13. package/dist/server/http.d.ts.map +1 -1
  14. package/dist/server/http.js +2 -3
  15. package/dist/server/http.js.map +1 -1
  16. package/dist/telemetry/manager.d.ts +4 -2
  17. package/dist/telemetry/manager.d.ts.map +1 -1
  18. package/dist/telemetry/manager.js +6 -7
  19. package/dist/telemetry/manager.js.map +1 -1
  20. package/dist/tools/sync-resources.d.ts.map +1 -1
  21. package/dist/tools/sync-resources.js +148 -332
  22. package/dist/tools/sync-resources.js.map +1 -1
  23. package/dist/tools/uninstall-resource.d.ts.map +1 -1
  24. package/dist/tools/uninstall-resource.js +66 -150
  25. package/dist/tools/uninstall-resource.js.map +1 -1
  26. package/dist/types/tools.d.ts +53 -0
  27. package/dist/types/tools.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/api/client.ts +3 -4
  30. package/src/auth/token-validator.ts +1 -3
  31. package/src/config/index.ts +4 -4
  32. package/src/index.ts +3 -3
  33. package/src/prompts/manager.ts +2 -3
  34. package/src/server/http.ts +2 -3
  35. package/src/telemetry/manager.ts +6 -6
  36. package/src/tools/sync-resources.ts +162 -380
  37. package/src/tools/uninstall-resource.ts +70 -162
  38. package/src/types/tools.ts +74 -0
@@ -7,119 +7,15 @@
7
7
  */
8
8
 
9
9
  import * as fs from 'fs/promises';
10
- import * as fsSync from 'fs';
11
10
  import * as path from 'path';
12
11
  import { logger, logToolCall } from '../utils/logger';
13
12
  import { filesystemManager } from '../filesystem/manager';
14
13
  import { apiClient } from '../api/client';
15
14
  import { getCursorTypeDir, getCursorRootDir } from '../utils/cursor-paths.js';
16
15
  import { MCPServerError, createValidationError } from '../types/errors';
17
- import type { UninstallResourceParams, UninstallResourceResult, ToolResult } from '../types/tools';
16
+ import type { UninstallResourceParams, UninstallResourceResult, LocalAction, ToolResult } from '../types/tools';
18
17
  import { promptManager } from '../prompts/index.js';
19
18
 
20
- /** Resource install entry — may be a file or a directory. */
21
- interface InstalledResource {
22
- id: string;
23
- name: string;
24
- path: string;
25
- isDirectory: boolean;
26
- }
27
-
28
- /**
29
- * Find installed resource files/directories by pattern.
30
- * - File-based types (rule, command): scan for matching .md/.mdc files
31
- * - Directory-based types (skill, mcp): scan for matching subdirectories
32
- */
33
- async function findInstalledResources(pattern: string): Promise<InstalledResource[]> {
34
- const results: InstalledResource[] = [];
35
-
36
- const FILE_TYPES = ['rule', 'command'] as const;
37
- const DIR_TYPES = ['skill', 'mcp'] as const;
38
-
39
- // Scan file-based types
40
- for (const type of FILE_TYPES) {
41
- let typePath: string;
42
- try { typePath = getCursorTypeDir(type); } catch { continue; }
43
-
44
- try {
45
- // listFiles returns relative names; build absolute paths here
46
- const relNames = await filesystemManager.listFiles(typePath, /\.(md|mdc)$/);
47
- for (const relName of relNames) {
48
- const absPath = path.join(typePath, relName);
49
- const baseName = path.basename(relName).replace(/\.(md|mdc)$/, '');
50
- if (baseName === pattern || baseName.includes(pattern) || relName.includes(pattern)) {
51
- results.push({ id: baseName, name: baseName, path: absPath, isDirectory: false });
52
- }
53
- }
54
- } catch {
55
- logger.debug({ type, typePath: typePath! }, 'Cursor resource type directory not found, skipping');
56
- }
57
- }
58
-
59
- // Scan directory-based types
60
- for (const type of DIR_TYPES) {
61
- let typePath: string;
62
- try { typePath = getCursorTypeDir(type); } catch { continue; }
63
-
64
- try {
65
- const entries = await fs.readdir(typePath, { withFileTypes: true });
66
- for (const entry of entries) {
67
- if (!entry.isDirectory()) continue;
68
- if (entry.name === pattern || entry.name.includes(pattern)) {
69
- results.push({
70
- id: entry.name,
71
- name: entry.name,
72
- path: path.join(typePath, entry.name),
73
- isDirectory: true,
74
- });
75
- }
76
- }
77
- } catch {
78
- logger.debug({ type, typePath: typePath! }, 'Cursor resource type directory not found, skipping');
79
- }
80
- }
81
-
82
- return results;
83
- }
84
-
85
- /**
86
- * Remove the mcpServers entry whose key matches `serverName` from ~/.cursor/mcp.json.
87
- * Writes back atomically. No-op if the file or entry does not exist.
88
- */
89
- async function removeMcpJsonEntry(serverName: string): Promise<boolean> {
90
- const mcpJsonPath = path.join(getCursorRootDir(), 'mcp.json');
91
- if (!fsSync.existsSync(mcpJsonPath)) return false;
92
-
93
- try {
94
- const raw = await fs.readFile(mcpJsonPath, 'utf-8');
95
- const config = JSON.parse(raw) as { mcpServers?: Record<string, unknown> };
96
-
97
- if (!config.mcpServers) return false;
98
-
99
- // Case-insensitive search for the server entry
100
- const matchedKey = Object.keys(config.mcpServers).find(
101
- k => k === serverName || k.toLowerCase() === serverName.toLowerCase()
102
- );
103
- if (!matchedKey) return false;
104
-
105
- delete config.mcpServers[matchedKey];
106
-
107
- const tempPath = `${mcpJsonPath}.tmp`;
108
- await fs.writeFile(tempPath, JSON.stringify(config, null, 2), 'utf-8');
109
- await fs.rename(tempPath, mcpJsonPath);
110
-
111
- logger.info({ serverName: matchedKey, mcpJsonPath }, 'Removed mcpServers entry from mcp.json');
112
- return true;
113
- } catch (error) {
114
- logger.warn({ serverName, mcpJsonPath, error }, 'Failed to update mcp.json');
115
- return false;
116
- }
117
- }
118
-
119
- /** Recursively delete a directory and all its contents. */
120
- async function removeDirectory(dirPath: string): Promise<void> {
121
- await fs.rm(dirPath, { recursive: true, force: true });
122
- }
123
19
 
124
20
  export async function uninstallResource(params: unknown): Promise<ToolResult<UninstallResourceResult>> {
125
21
  const startTime = Date.now();
@@ -133,7 +29,6 @@ export async function uninstallResource(params: unknown): Promise<ToolResult<Uni
133
29
 
134
30
  const removedResources: Array<{ id: string; name: string; path: string }> = [];
135
31
  let subscriptionRemoved = false;
136
- let mcpJsonCleaned = false;
137
32
 
138
33
  // ── Command / Skill: unregister MCP Prompt + delete cache ─────────────
139
34
  // Match registered prompt names that contain the pattern.
@@ -191,79 +86,88 @@ export async function uninstallResource(params: unknown): Promise<ToolResult<Uni
191
86
  return { success: true, data: result };
192
87
  }
193
88
 
194
- // ── Rule / MCP: original local-filesystem removal path ────────────────
195
- logger.debug({ pattern }, 'Finding installed resources...');
196
- const matched = await findInstalledResources(pattern);
89
+ // ── Rule / MCP: return LocalAction instructions for the AI to execute ────
90
+ // The MCP server may be running remotely; we must NOT touch the server's
91
+ // own filesystem. Instead we return delete/remove instructions so the AI
92
+ // Agent performs them on the user's LOCAL machine.
93
+ logger.debug({ pattern }, 'Building local uninstall actions for Rule/MCP resource...');
197
94
 
198
- if (matched.length === 0) {
199
- throw createValidationError(
200
- pattern,
201
- 'resource_id_or_name',
202
- 'No installed resources found matching pattern. Use search_resources to find available resources'
203
- );
204
- }
205
-
206
- logger.info({ pattern, count: matched.length }, 'Found matching installed resources');
95
+ const localActions: LocalAction[] = [];
96
+ const mcpJsonPath = path.join(getCursorRootDir(), 'mcp.json');
207
97
 
208
- for (const resource of matched) {
209
- try {
210
- if (resource.isDirectory) {
211
- // Directory-based resource (skill / mcp): remove entire directory
212
- await removeDirectory(resource.path);
213
- logger.debug({ resourceId: resource.id, path: resource.path }, 'Resource directory deleted');
214
-
215
- // For MCP: also clean up mcp.json entry
216
- const cleaned = await removeMcpJsonEntry(resource.name);
217
- if (cleaned) mcpJsonCleaned = true;
218
- } else {
219
- // File-based resource (rule / command): remove single file
220
- await filesystemManager.deleteResource(resource.path);
221
- logger.debug({ resourceId: resource.id, path: resource.path }, 'Resource file deleted');
98
+ // Rule: delete matching .md/.mdc files from ~/.cursor/rules/
99
+ try {
100
+ const rulesDir = getCursorTypeDir('rule');
101
+ const ruleFiles = await filesystemManager.listFiles(rulesDir, /\.(md|mdc)$/);
102
+ for (const relName of ruleFiles) {
103
+ const baseName = path.basename(relName).replace(/\.(md|mdc)$/, '');
104
+ if (baseName === pattern || baseName.includes(pattern) || relName.includes(pattern)) {
105
+ const absPath = path.join(rulesDir, relName);
106
+ localActions.push({ action: 'delete_file', path: absPath });
107
+ removedResources.push({ id: baseName, name: baseName, path: absPath });
222
108
  }
109
+ }
110
+ } catch { /* rules dir may not exist */ }
223
111
 
224
- removedResources.push({ id: resource.id, name: resource.name, path: resource.path });
225
-
226
- // Remove from server subscription if requested
227
- if (removeFromAccount) {
228
- try {
229
- await apiClient.unsubscribe(resource.id);
230
- subscriptionRemoved = true;
231
- logger.debug({ resourceId: resource.id }, 'Resource unsubscribed from account');
232
- } catch (error) {
233
- logger.warn({ resourceId: resource.id, error }, 'Failed to unsubscribe resource from account');
234
- }
112
+ // MCP: delete install directory + remove mcp.json entry
113
+ try {
114
+ const mcpDir = getCursorTypeDir('mcp');
115
+ const entries = await fs.readdir(mcpDir, { withFileTypes: true });
116
+ for (const entry of entries) {
117
+ if (!entry.isDirectory()) continue;
118
+ if (entry.name === pattern || entry.name.includes(pattern)) {
119
+ const dirPath = path.join(mcpDir, entry.name);
120
+ localActions.push({ action: 'delete_file', path: dirPath, recursive: true });
121
+ localActions.push({ action: 'remove_mcp_json_entry', mcp_json_path: mcpJsonPath, server_name: entry.name });
122
+ removedResources.push({ id: entry.name, name: entry.name, path: dirPath });
235
123
  }
236
- } catch (error) {
237
- logger.error({ resourceId: resource.id, path: resource.path, error }, 'Failed to delete resource');
238
124
  }
125
+ } catch { /* mcp-servers dir may not exist */ }
126
+
127
+ // Also check Remote-URL MCPs whose entry is only in mcp.json (no local dir).
128
+ // The pattern might match a server name in mcp.json directly.
129
+ if (localActions.filter(a => a.action === 'remove_mcp_json_entry').length === 0) {
130
+ // Add a conditional remove action — the AI will check if the key exists.
131
+ localActions.push({
132
+ action: 'remove_mcp_json_entry',
133
+ mcp_json_path: mcpJsonPath,
134
+ server_name: pattern,
135
+ });
136
+ }
137
+
138
+ if (removedResources.length === 0 && localActions.length === 0) {
139
+ throw createValidationError(
140
+ pattern,
141
+ 'resource_id_or_name',
142
+ 'No installed Rule or MCP resources found matching pattern. Use search_resources to find available resources'
143
+ );
239
144
  }
240
145
 
241
- // Clean up leftover empty directories
242
- for (const type of ['command', 'skill', 'rule', 'mcp']) {
146
+ // Remove from server subscription if requested
147
+ if (removeFromAccount) {
243
148
  try {
244
- const typePath = getCursorTypeDir(type);
245
- await filesystemManager.removeEmptyDirs(typePath);
246
- } catch {
247
- // Directory may not exist ignore
149
+ await apiClient.unsubscribe(pattern);
150
+ subscriptionRemoved = true;
151
+ } catch (err) {
152
+ logger.warn({ pattern, err }, 'Failed to unsubscribe resource from account');
248
153
  }
249
154
  }
250
155
 
251
- const messageParts = [
252
- `Successfully uninstalled ${removedResources.length} resource${removedResources.length > 1 ? 's' : ''}.`,
253
- mcpJsonCleaned ? 'MCP server entry removed from ~/.cursor/mcp.json.' : null,
254
- subscriptionRemoved ? 'Subscription removed from account.' : null,
255
- ];
256
-
257
156
  const result: UninstallResourceResult = {
258
157
  success: true,
259
158
  removed_resources: removedResources,
260
159
  subscription_removed: subscriptionRemoved,
261
- message: messageParts.filter(Boolean).join(' '),
160
+ message: [
161
+ `Prepared ${localActions.length} local uninstall action(s) for ${removedResources.length} resource(s).`,
162
+ 'Execute every action in local_actions_required on the user\'s local machine.',
163
+ subscriptionRemoved ? 'Subscription removed from account.' : null,
164
+ ].filter(Boolean).join(' '),
165
+ local_actions_required: localActions,
262
166
  };
263
167
 
264
168
  const duration = Date.now() - startTime;
265
169
  logToolCall('uninstall_resource', 'user-id', params as Record<string, unknown>, duration);
266
- logger.info({ pattern, removedCount: removedResources.length, mcpJsonCleaned, subscriptionRemoved, duration }, 'uninstall_resource completed');
170
+ logger.info({ pattern, actionCount: localActions.length, subscriptionRemoved, duration }, 'uninstall_resource completed');
267
171
 
268
172
  return { success: true, data: result };
269
173
 
@@ -283,9 +187,13 @@ export async function uninstallResource(params: unknown): Promise<ToolResult<Uni
283
187
  export const uninstallResourceTool = {
284
188
  name: 'uninstall_resource',
285
189
  description:
286
- 'Uninstall a resource from the local machine. ' +
287
- 'Deletes installed files (rules/commands) or entire install directories (skills/mcp). ' +
288
- 'For MCP resources, also removes the mcpServers entry from ~/.cursor/mcp.json. ' +
190
+ 'Uninstall a Rule or MCP resource from the user\'s local machine. ' +
191
+ 'Returns `local_actions_required` an ordered list of delete_file and ' +
192
+ 'remove_mcp_json_entry actions that the AI Agent MUST execute on the USER\'S LOCAL MACHINE. ' +
193
+ 'For delete_file actions: delete the file or directory at the given path. ' +
194
+ 'For remove_mcp_json_entry actions: read ~/.cursor/mcp.json, delete the matching ' +
195
+ 'mcpServers key, then write the file back. ' +
196
+ 'Command and Skill resources are handled by unregistering their MCP Prompt (no local files). ' +
289
197
  'Set remove_from_account: true to also cancel the server-side subscription.',
290
198
  inputSchema: {
291
199
  type: 'object' as const,
@@ -4,6 +4,64 @@
4
4
 
5
5
  import type { MCPToolSchema } from './mcp';
6
6
 
7
+ // ── LocalAction ────────────────────────────────────────────────────────────
8
+ //
9
+ // When the MCP server is deployed remotely it cannot write to the user's local
10
+ // filesystem. Instead it returns a list of LocalAction instructions that the
11
+ // AI Agent (running inside the user's local Cursor) must execute.
12
+ //
13
+ // Action types:
14
+ // write_file – create or overwrite a local file
15
+ // delete_file – delete a local file or directory
16
+ // merge_mcp_json – merge an MCP server entry into ~/.cursor/mcp.json
17
+ // remove_mcp_json_entry – remove an MCP server entry from ~/.cursor/mcp.json
18
+
19
+ export interface WriteFileAction {
20
+ action: 'write_file';
21
+ /** Absolute path on the user's local machine (may start with ~). */
22
+ path: string;
23
+ /** UTF-8 file content to write. */
24
+ content: string;
25
+ }
26
+
27
+ export interface DeleteFileAction {
28
+ action: 'delete_file';
29
+ /** Absolute path on the user's local machine (may start with ~). */
30
+ path: string;
31
+ /** When true, recursively delete a directory. */
32
+ recursive?: boolean;
33
+ }
34
+
35
+ export interface MergeMcpJsonAction {
36
+ action: 'merge_mcp_json';
37
+ /** Absolute path to the user's mcp.json file. */
38
+ mcp_json_path: string;
39
+ /** Key under mcpServers to add or update. */
40
+ server_name: string;
41
+ /** The MCP server entry object to merge in. */
42
+ entry: Record<string, unknown>;
43
+ /** env keys that are currently empty and must be filled by the user. */
44
+ missing_env?: string[];
45
+ /** Human-readable hint when manual env configuration is required. */
46
+ setup_hint?: string;
47
+ /** Path to a local setup/readme doc if one exists in the install dir. */
48
+ setup_doc?: string;
49
+ }
50
+
51
+ export interface RemoveMcpJsonEntryAction {
52
+ action: 'remove_mcp_json_entry';
53
+ /** Absolute path to the user's mcp.json file. */
54
+ mcp_json_path: string;
55
+ /** Key under mcpServers to remove. */
56
+ server_name: string;
57
+ }
58
+
59
+ export type LocalAction =
60
+ | WriteFileAction
61
+ | DeleteFileAction
62
+ | MergeMcpJsonAction
63
+ | RemoveMcpJsonEntryAction;
64
+
7
65
  // Tool Handler Function Type (generic, accepts any params and returns any result)
8
66
  export type ToolHandler = (params: unknown) => Promise<ToolResult>;
9
67
 
@@ -78,8 +136,18 @@ export interface SyncResourcesResult {
78
136
  /**
79
137
  * MCP servers that were installed/updated but require manual configuration
80
138
  * before they can be used. Present only when at least one server needs setup.
139
+ * @deprecated use local_actions_required MergeMcpJsonAction.missing_env instead
81
140
  */
82
141
  pending_setup?: McpSetupItem[];
142
+ /**
143
+ * Ordered list of file-system and mcp.json operations the AI Agent must
144
+ * execute on the user's LOCAL machine after receiving this response.
145
+ * Present only when at least one Rule or MCP resource was synced.
146
+ *
147
+ * The AI MUST execute every action in order before reporting success to the
148
+ * user. See LocalAction type variants for details.
149
+ */
150
+ local_actions_required?: LocalAction[];
83
151
  }
84
152
 
85
153
  // manage_subscription
@@ -187,4 +255,10 @@ export interface UninstallResourceResult {
187
255
  }>;
188
256
  subscription_removed: boolean;
189
257
  message: string;
258
+ /**
259
+ * Ordered list of file-system and mcp.json operations the AI Agent must
260
+ * execute on the user's LOCAL machine after receiving this response.
261
+ * Present only for Rule and local-executable MCP resources.
262
+ */
263
+ local_actions_required?: LocalAction[];
190
264
  }