@elliotding/ai-agent-mcp 0.1.5 → 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.
- package/ai-resource-telemetry.json +13 -1
- package/dist/tools/sync-resources.d.ts.map +1 -1
- package/dist/tools/sync-resources.js +148 -332
- package/dist/tools/sync-resources.js.map +1 -1
- package/dist/tools/uninstall-resource.d.ts.map +1 -1
- package/dist/tools/uninstall-resource.js +66 -150
- package/dist/tools/uninstall-resource.js.map +1 -1
- package/dist/types/tools.d.ts +53 -0
- package/dist/types/tools.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/tools/sync-resources.ts +162 -380
- package/src/tools/uninstall-resource.ts +70 -162
- 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:
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
//
|
|
242
|
-
|
|
146
|
+
// Remove from server subscription if requested
|
|
147
|
+
if (removeFromAccount) {
|
|
243
148
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
} catch {
|
|
247
|
-
|
|
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:
|
|
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,
|
|
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
|
-
'
|
|
288
|
-
'
|
|
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,
|
package/src/types/tools.ts
CHANGED
|
@@ -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
|
}
|