@elliotding/ai-agent-mcp 0.1.2 → 0.1.4
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/.prompt-cache/cmd-cmd-client-sdk-ai-hub-generate-testcase.md +101 -0
- package/.prompt-cache/cmd-cmd-client-sdk-ai-hub-submit_zct_job.md +158 -0
- package/.prompt-cache/skill-skill-client-sdk-ai-hub-analyze-conf-status.md +311 -0
- package/.prompt-cache/skill-skill-client-sdk-ai-hub-analyze-sdk-log.md +64 -0
- package/.prompt-cache/skill-skill-client-sdk-ai-hub-analyze-zmb-log-errors.md +84 -0
- package/ai-resource-telemetry.json +22 -0
- package/dist/api/client.d.ts +76 -8
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +86 -40
- package/dist/api/client.js.map +1 -1
- package/dist/auth/permissions.d.ts.map +1 -1
- package/dist/auth/permissions.js +6 -0
- package/dist/auth/permissions.js.map +1 -1
- package/dist/config/index.d.ts +6 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -3
- package/dist/config/index.js.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/prompts/cache.d.ts +69 -0
- package/dist/prompts/cache.d.ts.map +1 -0
- package/dist/prompts/cache.js +163 -0
- package/dist/prompts/cache.js.map +1 -0
- package/dist/prompts/generator.d.ts +49 -0
- package/dist/prompts/generator.d.ts.map +1 -0
- package/dist/prompts/generator.js +158 -0
- package/dist/prompts/generator.js.map +1 -0
- package/dist/prompts/index.d.ts +13 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +24 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/manager.d.ts +106 -0
- package/dist/prompts/manager.d.ts.map +1 -0
- package/dist/prompts/manager.js +263 -0
- package/dist/prompts/manager.js.map +1 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +61 -17
- package/dist/server/http.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +43 -0
- package/dist/server.js.map +1 -1
- package/dist/telemetry/index.d.ts +3 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +7 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/manager.d.ts +149 -0
- package/dist/telemetry/manager.d.ts.map +1 -0
- package/dist/telemetry/manager.js +368 -0
- package/dist/telemetry/manager.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/manage-subscription.d.ts +4 -0
- package/dist/tools/manage-subscription.d.ts.map +1 -1
- package/dist/tools/manage-subscription.js +36 -7
- package/dist/tools/manage-subscription.js.map +1 -1
- package/dist/tools/search-resources.d.ts +4 -0
- package/dist/tools/search-resources.d.ts.map +1 -1
- package/dist/tools/search-resources.js +6 -1
- package/dist/tools/search-resources.js.map +1 -1
- package/dist/tools/sync-resources.d.ts +13 -4
- package/dist/tools/sync-resources.d.ts.map +1 -1
- package/dist/tools/sync-resources.js +127 -6
- package/dist/tools/sync-resources.js.map +1 -1
- package/dist/tools/track-usage.d.ts +63 -0
- package/dist/tools/track-usage.d.ts.map +1 -0
- package/dist/tools/track-usage.js +90 -0
- package/dist/tools/track-usage.js.map +1 -0
- package/dist/tools/uninstall-resource.d.ts.map +1 -1
- package/dist/tools/uninstall-resource.js +53 -3
- package/dist/tools/uninstall-resource.js.map +1 -1
- package/dist/tools/upload-resource.d.ts +4 -0
- package/dist/tools/upload-resource.d.ts.map +1 -1
- package/dist/tools/upload-resource.js +164 -23
- package/dist/tools/upload-resource.js.map +1 -1
- package/dist/types/tools.d.ts +17 -2
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/cursor-paths.d.ts +10 -0
- package/dist/utils/cursor-paths.d.ts.map +1 -1
- package/dist/utils/cursor-paths.js +13 -0
- package/dist/utils/cursor-paths.js.map +1 -1
- package/package.json +1 -1
- package/src/api/client.ts +191 -71
- package/src/auth/permissions.ts +6 -0
- package/src/config/index.ts +11 -5
- package/src/index.ts +18 -0
- package/src/prompts/cache.ts +140 -0
- package/src/prompts/generator.ts +142 -0
- package/src/prompts/index.ts +20 -0
- package/src/prompts/manager.ts +342 -0
- package/src/server/http.ts +69 -17
- package/src/server.ts +13 -0
- package/src/telemetry/index.ts +10 -0
- package/src/telemetry/manager.ts +419 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/manage-subscription.ts +41 -7
- package/src/tools/search-resources.ts +14 -6
- package/src/tools/sync-resources.ts +141 -9
- package/src/tools/track-usage.ts +113 -0
- package/src/tools/uninstall-resource.ts +62 -4
- package/src/tools/upload-resource.ts +204 -31
- package/src/types/tools.ts +17 -2
- package/src/utils/cursor-paths.ts +13 -0
|
@@ -9,6 +9,7 @@ import { MCPServerError, createValidationError } from '../types/errors';
|
|
|
9
9
|
import type { ManageSubscriptionParams, ManageSubscriptionResult, ToolResult } from '../types/tools';
|
|
10
10
|
import { syncResources } from './sync-resources';
|
|
11
11
|
import { uninstallResource } from './uninstall-resource';
|
|
12
|
+
import { promptManager } from '../prompts/index.js';
|
|
12
13
|
|
|
13
14
|
export async function manageSubscription(params: unknown): Promise<ToolResult<ManageSubscriptionResult>> {
|
|
14
15
|
const startTime = Date.now();
|
|
@@ -37,7 +38,9 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
|
|
|
37
38
|
// Subscribe to resources
|
|
38
39
|
const subResult = await apiClient.subscribe(
|
|
39
40
|
typedParams.resource_ids,
|
|
40
|
-
typedParams.auto_sync
|
|
41
|
+
typedParams.auto_sync,
|
|
42
|
+
undefined,
|
|
43
|
+
typedParams.user_token
|
|
41
44
|
);
|
|
42
45
|
|
|
43
46
|
logger.info({ count: subResult.subscriptions.length }, 'Resources subscribed successfully');
|
|
@@ -50,7 +53,11 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
|
|
|
50
53
|
|
|
51
54
|
if (shouldAutoSync && subResult.subscriptions.length > 0) {
|
|
52
55
|
logger.info({ resourceIds: typedParams.resource_ids }, 'Auto-syncing newly subscribed resources...');
|
|
53
|
-
const syncResult = await syncResources({
|
|
56
|
+
const syncResult = await syncResources({
|
|
57
|
+
mode: 'incremental',
|
|
58
|
+
scope: typedParams.scope || 'global',
|
|
59
|
+
user_token: typedParams.user_token,
|
|
60
|
+
});
|
|
54
61
|
if (syncResult.success && syncResult.data) {
|
|
55
62
|
const sd = syncResult.data;
|
|
56
63
|
syncSummary = `Auto-sync: ${sd.summary.synced} synced, ${sd.summary.cached} cached, ${sd.summary.failed} failed`;
|
|
@@ -97,12 +104,27 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
|
|
|
97
104
|
logger.debug({ resourceIds: typedParams.resource_ids }, 'Unsubscribing from resources...');
|
|
98
105
|
|
|
99
106
|
// Cancel server-side subscription
|
|
100
|
-
await apiClient.unsubscribe(typedParams.resource_ids);
|
|
107
|
+
await apiClient.unsubscribe(typedParams.resource_ids, typedParams.user_token);
|
|
101
108
|
logger.info({ count: typedParams.resource_ids.length }, 'Server-side subscriptions removed');
|
|
102
109
|
|
|
103
|
-
// Uninstall local files and MCP config for each resource
|
|
110
|
+
// Uninstall local files and MCP config for each resource.
|
|
111
|
+
// For Command/Skill: unregister MCP Prompt instead of deleting local files.
|
|
104
112
|
const uninstallResults: Array<{ id: string; removed: boolean; detail: string }> = [];
|
|
105
113
|
for (const resourceId of typedParams.resource_ids) {
|
|
114
|
+
// Determine if this is a Command or Skill by checking the prompt registry.
|
|
115
|
+
// Resource IDs follow the pattern: cmd-<source>-<name> or skill-<source>-<name>.
|
|
116
|
+
const isCommand = resourceId.startsWith('cmd-');
|
|
117
|
+
const isSkill = resourceId.startsWith('skill-');
|
|
118
|
+
if (isCommand || isSkill) {
|
|
119
|
+
const resourceType = isCommand ? 'command' : 'skill';
|
|
120
|
+
// Extract resource name from the ID (cmd-<team>-<name> or skill-<team>-<name>).
|
|
121
|
+
const parts = resourceId.split('-');
|
|
122
|
+
const resourceName = parts.slice(2).join('-') || resourceId;
|
|
123
|
+
promptManager.unregisterPrompt(resourceId, resourceType as 'command' | 'skill', resourceName);
|
|
124
|
+
uninstallResults.push({ id: resourceId, removed: true, detail: `Unregistered MCP Prompt for "${resourceName}"` });
|
|
125
|
+
logger.info({ resourceId, resourceType }, 'MCP Prompt unregistered on unsubscribe');
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
106
128
|
// Use the last segment of the resource ID as the search pattern
|
|
107
129
|
// e.g. "mcp-client-sdk-ai-hub-jenkins" → "jenkins"
|
|
108
130
|
// "rule-csp-elliotTest" → "elliotTest"
|
|
@@ -156,7 +178,7 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
|
|
|
156
178
|
logger.debug({ scope: typedParams.scope || 'all' }, 'Listing subscriptions...');
|
|
157
179
|
|
|
158
180
|
// Get subscriptions list
|
|
159
|
-
const subs = await apiClient.getSubscriptions({});
|
|
181
|
+
const subs = await apiClient.getSubscriptions({}, typedParams.user_token);
|
|
160
182
|
|
|
161
183
|
result = {
|
|
162
184
|
action: 'list',
|
|
@@ -188,7 +210,9 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
|
|
|
188
210
|
|
|
189
211
|
const batchSubResult = await apiClient.subscribe(
|
|
190
212
|
typedParams.resource_ids,
|
|
191
|
-
typedParams.auto_sync
|
|
213
|
+
typedParams.auto_sync,
|
|
214
|
+
undefined,
|
|
215
|
+
typedParams.user_token
|
|
192
216
|
);
|
|
193
217
|
|
|
194
218
|
logger.info({ count: batchSubResult.subscriptions.length }, 'Batch subscription completed');
|
|
@@ -201,7 +225,11 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
|
|
|
201
225
|
|
|
202
226
|
if (shouldBatchAutoSync && batchSubResult.subscriptions.length > 0) {
|
|
203
227
|
logger.info({ count: batchSubResult.subscriptions.length }, 'Auto-syncing batch subscribed resources...');
|
|
204
|
-
const batchSyncResult = await syncResources({
|
|
228
|
+
const batchSyncResult = await syncResources({
|
|
229
|
+
mode: 'incremental',
|
|
230
|
+
scope: typedParams.scope || 'global',
|
|
231
|
+
user_token: typedParams.user_token,
|
|
232
|
+
});
|
|
205
233
|
if (batchSyncResult.success && batchSyncResult.data) {
|
|
206
234
|
const sd = batchSyncResult.data;
|
|
207
235
|
batchSyncSummary = `Auto-sync: ${sd.summary.synced} synced, ${sd.summary.cached} cached, ${sd.summary.failed} failed`;
|
|
@@ -325,6 +353,12 @@ export const manageSubscriptionTool = {
|
|
|
325
353
|
description: 'Enable update notifications',
|
|
326
354
|
default: true,
|
|
327
355
|
},
|
|
356
|
+
user_token: {
|
|
357
|
+
type: 'string',
|
|
358
|
+
description:
|
|
359
|
+
'DO NOT set this field — it is automatically injected by the MCP server from ' +
|
|
360
|
+
'the authenticated SSE connection. The server always provides the correct token.',
|
|
361
|
+
},
|
|
328
362
|
},
|
|
329
363
|
required: ['action'],
|
|
330
364
|
},
|
|
@@ -9,7 +9,6 @@ import { filesystemManager } from '../filesystem/manager';
|
|
|
9
9
|
import { getCursorResourcePath } from '../utils/cursor-paths.js';
|
|
10
10
|
import { MCPServerError } from '../types/errors';
|
|
11
11
|
import type { SearchResourcesParams, SearchResourcesResult, ToolResult } from '../types/tools';
|
|
12
|
-
|
|
13
12
|
// Simple in-memory cache
|
|
14
13
|
const searchCache = new Map<string, { results: SearchResourcesResult; timestamp: number }>();
|
|
15
14
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
@@ -84,11 +83,14 @@ export async function searchResources(params: unknown): Promise<ToolResult<Searc
|
|
|
84
83
|
// Search via API
|
|
85
84
|
logger.debug({ team: typedParams.team, type: typedParams.type, keyword: typedParams.keyword }, 'Searching resources...');
|
|
86
85
|
|
|
87
|
-
const searchResults = await apiClient.searchResources(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
const searchResults = await apiClient.searchResources(
|
|
87
|
+
{
|
|
88
|
+
team: typedParams.team,
|
|
89
|
+
type: typedParams.type,
|
|
90
|
+
keyword: typedParams.keyword,
|
|
91
|
+
},
|
|
92
|
+
typedParams.user_token
|
|
93
|
+
);
|
|
92
94
|
|
|
93
95
|
// Check subscription and installation status for each result
|
|
94
96
|
const enhancedResults = await Promise.all(
|
|
@@ -170,6 +172,12 @@ export const searchResourcesTool = {
|
|
|
170
172
|
type: 'string',
|
|
171
173
|
description: 'Search keyword (searches in name, description, tags)',
|
|
172
174
|
},
|
|
175
|
+
user_token: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
description:
|
|
178
|
+
'DO NOT set this field — it is automatically injected by the MCP server from ' +
|
|
179
|
+
'the authenticated SSE connection. The server always provides the correct token.',
|
|
180
|
+
},
|
|
173
181
|
},
|
|
174
182
|
required: ['keyword'],
|
|
175
183
|
},
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* sync_resources Tool
|
|
3
3
|
*
|
|
4
|
-
* Synchronises the user's subscribed AI resources
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Synchronises the user's subscribed AI resources.
|
|
5
|
+
*
|
|
6
|
+
* Resource delivery strategy (v1.5):
|
|
7
|
+
* - Command / Skill : registered as MCP Prompts (NOT written to local filesystem).
|
|
8
|
+
* Content is generated into .prompt-cache/ and registered via PromptManager.
|
|
9
|
+
* - Rule : downloaded to ~/.cursor/rules/ (Cursor engine requires local files).
|
|
10
|
+
* - MCP : downloaded to ~/.cursor/mcp-servers/ and registered in mcp.json.
|
|
7
11
|
*
|
|
8
12
|
* Flow:
|
|
9
13
|
* 1. Fetch subscription list from CSP server (REST API).
|
|
10
14
|
* 2. (non-check) Trigger Git sync on server side via multiSourceGitManager.
|
|
11
|
-
* 3. For each subscription:
|
|
15
|
+
* 3. For each subscription: handle per type as above.
|
|
16
|
+
* 4. Update telemetry: subscribed_rules + configured_mcps lists.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
import * as fs from 'fs/promises';
|
|
@@ -19,6 +24,25 @@ import { multiSourceGitManager } from '../git/multi-source-manager';
|
|
|
19
24
|
import { getCursorResourcePath, getCursorTypeDir, getCursorRootDir } from '../utils/cursor-paths';
|
|
20
25
|
import { MCPServerError } from '../types/errors';
|
|
21
26
|
import type { SyncResourcesParams, SyncResourcesResult, McpSetupItem, ToolResult } from '../types/tools';
|
|
27
|
+
import { telemetry } from '../telemetry/index.js';
|
|
28
|
+
import { promptManager } from '../prompts/index.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract the `description` field from YAML frontmatter in a Markdown file.
|
|
32
|
+
* Frontmatter is delimited by leading `---` and closing `---` lines.
|
|
33
|
+
* Returns undefined if no frontmatter or no description key is found.
|
|
34
|
+
*/
|
|
35
|
+
function extractFrontmatterDescription(content: string): string | undefined {
|
|
36
|
+
if (!content.startsWith('---')) return undefined;
|
|
37
|
+
const end = content.indexOf('\n---', 3);
|
|
38
|
+
if (end === -1) return undefined;
|
|
39
|
+
const frontmatter = content.slice(3, end);
|
|
40
|
+
for (const line of frontmatter.split('\n')) {
|
|
41
|
+
const match = /^description:\s*(.+)$/.exec(line.trim());
|
|
42
|
+
if (match) return match[1]!.trim().replace(/^['"]|['"]$/g, '');
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
22
46
|
|
|
23
47
|
/**
|
|
24
48
|
* Two supported mcp-config.json formats:
|
|
@@ -303,9 +327,10 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
303
327
|
logToolStep('sync_resources', 'Tool invocation started', { params: typedParams });
|
|
304
328
|
|
|
305
329
|
try {
|
|
306
|
-
const mode
|
|
307
|
-
const scope
|
|
308
|
-
const types
|
|
330
|
+
const mode = typedParams.mode || 'incremental';
|
|
331
|
+
const scope = typedParams.scope || 'global';
|
|
332
|
+
const types = typedParams.types;
|
|
333
|
+
const userToken = typedParams.user_token;
|
|
309
334
|
|
|
310
335
|
logToolStep('sync_resources', 'Parameters validated', { mode, scope, types });
|
|
311
336
|
|
|
@@ -313,7 +338,7 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
313
338
|
logToolStep('sync_resources', 'Step 1: Fetching subscriptions from API', { scope, types });
|
|
314
339
|
const t1 = Date.now();
|
|
315
340
|
|
|
316
|
-
const subscriptions = await apiClient.getSubscriptions({ types });
|
|
341
|
+
const subscriptions = await apiClient.getSubscriptions({ types }, userToken);
|
|
317
342
|
|
|
318
343
|
logToolStep('sync_resources', 'Subscriptions fetched', {
|
|
319
344
|
total: subscriptions.total,
|
|
@@ -396,6 +421,86 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
396
421
|
continue;
|
|
397
422
|
}
|
|
398
423
|
|
|
424
|
+
// ── Command / Skill: MCP Prompt mode (no local file write) ──────────
|
|
425
|
+
// Download content → generate intermediate cache → register as MCP Prompt.
|
|
426
|
+
if (sub.type === 'command' || sub.type === 'skill') {
|
|
427
|
+
logToolStep('sync_resources', `Registering ${sub.type} as MCP Prompt`, {
|
|
428
|
+
resourceId: sub.id,
|
|
429
|
+
resourceName: sub.name,
|
|
430
|
+
});
|
|
431
|
+
try {
|
|
432
|
+
const tDl = Date.now();
|
|
433
|
+
const downloadResult = await apiClient.downloadResource(sub.id, userToken);
|
|
434
|
+
logToolStep('sync_resources', 'Download complete (Prompt mode)', {
|
|
435
|
+
resourceId: sub.id,
|
|
436
|
+
fileCount: downloadResult.files.length,
|
|
437
|
+
duration: Date.now() - tDl,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Primary Markdown content selection:
|
|
441
|
+
// - skill: prefer SKILL.md (canonical entrypoint for all skill content)
|
|
442
|
+
// - command: prefer the file whose name matches the resource name
|
|
443
|
+
// - fallback: first .md file, then first file of any type
|
|
444
|
+
const isSkill = sub.type === 'skill';
|
|
445
|
+
const primaryFile = isSkill
|
|
446
|
+
? (downloadResult.files.find((f) => path.basename(f.path) === 'SKILL.md') ??
|
|
447
|
+
downloadResult.files.find((f) => f.path.endsWith('.md')) ??
|
|
448
|
+
downloadResult.files[0])
|
|
449
|
+
: (downloadResult.files.find((f) => path.basename(f.path).replace(/\.md$/, '') === sub.name) ??
|
|
450
|
+
downloadResult.files.find((f) => f.path.endsWith('.md')) ??
|
|
451
|
+
downloadResult.files[0]);
|
|
452
|
+
|
|
453
|
+
const rawContent = primaryFile?.content ?? '';
|
|
454
|
+
|
|
455
|
+
// Extract description from frontmatter (---\ndescription: ...\n---)
|
|
456
|
+
// falling back to the subscription's description field or resource name.
|
|
457
|
+
const frontmatterDesc = extractFrontmatterDescription(rawContent);
|
|
458
|
+
const description =
|
|
459
|
+
frontmatterDesc ??
|
|
460
|
+
(sub as any).description ??
|
|
461
|
+
sub.name;
|
|
462
|
+
|
|
463
|
+
await promptManager.registerPrompt({
|
|
464
|
+
resource_id: sub.id,
|
|
465
|
+
resource_type: sub.type as 'command' | 'skill',
|
|
466
|
+
resource_name: sub.name,
|
|
467
|
+
team: (sub as any).team ?? 'general',
|
|
468
|
+
description,
|
|
469
|
+
rawContent,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Clean up any legacy local files that may have been written by an
|
|
473
|
+
// older version of sync_resources. Command/Skill resources are now
|
|
474
|
+
// served exclusively as MCP Prompts; stale local files would cause
|
|
475
|
+
// the AI to read outdated content (without the track_usage header).
|
|
476
|
+
try {
|
|
477
|
+
const legacyPath = getCursorResourcePath(sub.type, `${sub.name}.md`);
|
|
478
|
+
await fs.unlink(legacyPath);
|
|
479
|
+
logger.info(
|
|
480
|
+
{ resourceId: sub.id, legacyPath },
|
|
481
|
+
'Removed legacy local file for Command/Skill resource',
|
|
482
|
+
);
|
|
483
|
+
} catch {
|
|
484
|
+
// File didn't exist — nothing to clean up.
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
tally.synced++;
|
|
488
|
+
details.push({ id: sub.id, name: sub.name, action: 'synced', version: resourceVersion });
|
|
489
|
+
logToolStep('sync_resources', `${sub.type} registered as MCP Prompt`, {
|
|
490
|
+
resourceId: sub.id,
|
|
491
|
+
promptCount: promptManager.size,
|
|
492
|
+
});
|
|
493
|
+
} catch (promptErr) {
|
|
494
|
+
logger.error(
|
|
495
|
+
{ resourceId: sub.id, error: (promptErr as Error).message },
|
|
496
|
+
'Failed to register Command/Skill as MCP Prompt',
|
|
497
|
+
);
|
|
498
|
+
tally.failed++;
|
|
499
|
+
details.push({ id: sub.id, name: sub.name, action: 'failed', version: resourceVersion });
|
|
500
|
+
}
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
399
504
|
// Download all files for this resource from the CSP server.
|
|
400
505
|
// We always download first so we can inspect the payload and determine
|
|
401
506
|
// whether this is a remote-URL-only MCP (Format B: config-only, no
|
|
@@ -405,7 +510,7 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
405
510
|
resourceType: sub.type,
|
|
406
511
|
});
|
|
407
512
|
const tDl = Date.now();
|
|
408
|
-
const downloadResult = await apiClient.downloadResource(sub.id);
|
|
513
|
+
const downloadResult = await apiClient.downloadResource(sub.id, userToken);
|
|
409
514
|
logToolStep('sync_resources', 'Download complete', {
|
|
410
515
|
resourceId: sub.id,
|
|
411
516
|
fileCount: downloadResult.files.length,
|
|
@@ -607,6 +712,27 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
607
712
|
timestamp: new Date().toISOString()
|
|
608
713
|
}, 'sync_resources completed successfully');
|
|
609
714
|
|
|
715
|
+
// Update telemetry snapshot lists (fire-and-forget).
|
|
716
|
+
// Rules: cannot track individual invocations; report subscription list only.
|
|
717
|
+
const subscribedRules = subscriptions.subscriptions
|
|
718
|
+
.filter((s) => s.type === 'rule')
|
|
719
|
+
.map((s) => ({
|
|
720
|
+
resource_id: s.id,
|
|
721
|
+
resource_name: s.name,
|
|
722
|
+
subscribed_at: (s as any).subscribed_at ?? new Date().toISOString(),
|
|
723
|
+
}));
|
|
724
|
+
if (userToken) telemetry.updateSubscribedRules(subscribedRules, userToken).catch(() => {});
|
|
725
|
+
|
|
726
|
+
// MCPs: individual invocation tracking is each MCP server's own responsibility.
|
|
727
|
+
const configuredMcps = subscriptions.subscriptions
|
|
728
|
+
.filter((s) => s.type === 'mcp')
|
|
729
|
+
.map((s) => ({
|
|
730
|
+
resource_id: s.id,
|
|
731
|
+
resource_name: s.name,
|
|
732
|
+
configured_at: (s as any).subscribed_at ?? new Date().toISOString(),
|
|
733
|
+
}));
|
|
734
|
+
if (userToken) telemetry.updateConfiguredMcps(configuredMcps, userToken).catch(() => {});
|
|
735
|
+
|
|
610
736
|
return { success: true, data: result };
|
|
611
737
|
|
|
612
738
|
} catch (error) {
|
|
@@ -656,6 +782,12 @@ export const syncResourcesTool = {
|
|
|
656
782
|
type: 'array',
|
|
657
783
|
description: 'Filter by resource types (empty = all types)',
|
|
658
784
|
},
|
|
785
|
+
user_token: {
|
|
786
|
+
type: 'string',
|
|
787
|
+
description:
|
|
788
|
+
'DO NOT set this field — it is automatically injected by the MCP server from ' +
|
|
789
|
+
'the authenticated SSE connection. The server always provides the correct token.',
|
|
790
|
+
},
|
|
659
791
|
},
|
|
660
792
|
},
|
|
661
793
|
handler: syncResources,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* track_usage Tool
|
|
3
|
+
*
|
|
4
|
+
* Records an AI Resource invocation for telemetry purposes.
|
|
5
|
+
*
|
|
6
|
+
* This tool is automatically invoked by the AI at the start of every
|
|
7
|
+
* Command or Skill execution. The Prompt content generated by
|
|
8
|
+
* PromptGenerator prepends a system instruction that asks the AI to
|
|
9
|
+
* call `track_usage` before doing anything else, so that the server
|
|
10
|
+
* can record the usage even though Cursor does not call `prompts/get`
|
|
11
|
+
* when a slash command is selected.
|
|
12
|
+
*
|
|
13
|
+
* The tool is intentionally lightweight:
|
|
14
|
+
* - No external API calls.
|
|
15
|
+
* - Fire-and-forget write to the local telemetry file.
|
|
16
|
+
* - Always returns a success response so the AI continues normally.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { logger } from '../utils/logger';
|
|
20
|
+
import { telemetry } from '../telemetry/index.js';
|
|
21
|
+
import type { ToolResult } from '../types/tools';
|
|
22
|
+
|
|
23
|
+
export interface TrackUsageParams {
|
|
24
|
+
resource_id: string;
|
|
25
|
+
resource_type: 'command' | 'skill';
|
|
26
|
+
resource_name: string;
|
|
27
|
+
/** Automatically injected by the MCP server from the SSE token. */
|
|
28
|
+
user_token?: string;
|
|
29
|
+
/** Optional Jira Issue ID for usage correlation (e.g. "PROJ-12345"). */
|
|
30
|
+
jira_id?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function trackUsage(params: unknown): Promise<ToolResult<{ recorded: boolean }>> {
|
|
34
|
+
const p = params as TrackUsageParams;
|
|
35
|
+
|
|
36
|
+
const resourceId = p.resource_id ?? '';
|
|
37
|
+
const resourceType = p.resource_type ?? 'command';
|
|
38
|
+
const resourceName = p.resource_name ?? '';
|
|
39
|
+
const userToken = p.user_token ?? '';
|
|
40
|
+
const jiraId = typeof p.jira_id === 'string' && p.jira_id.trim() !== ''
|
|
41
|
+
? p.jira_id.trim()
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
if (!resourceId || !userToken) {
|
|
45
|
+
// Missing required fields — log and return without recording so the AI
|
|
46
|
+
// is not blocked. This should not happen in normal operation.
|
|
47
|
+
logger.warn(
|
|
48
|
+
{ resourceId, userToken: !!userToken },
|
|
49
|
+
'track_usage called with missing resource_id or user_token — skipping',
|
|
50
|
+
);
|
|
51
|
+
return { success: true, data: { recorded: false } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Await the write to ensure the event is persisted before the periodic flush
|
|
55
|
+
// timer fires and clears pending_events. File write latency is negligible
|
|
56
|
+
// (< 1 ms) so this does not meaningfully delay the tool response.
|
|
57
|
+
await telemetry
|
|
58
|
+
.recordInvocation(resourceId, resourceType, resourceName, userToken, jiraId)
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
logger.warn({ resourceId, error: (err as Error).message }, 'track_usage: telemetry write failed (non-critical)');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
logger.info(
|
|
64
|
+
{ resourceId, resourceType, resourceName, jiraId: jiraId ?? '(none)' },
|
|
65
|
+
'track_usage: invocation recorded',
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return { success: true, data: { recorded: true } };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const trackUsageTool = {
|
|
72
|
+
name: 'track_usage',
|
|
73
|
+
description:
|
|
74
|
+
'Record the invocation of an AI Resource (Command or Skill) for telemetry. ' +
|
|
75
|
+
'MUST be called at the very beginning of every Command or Skill execution, ' +
|
|
76
|
+
'before performing any other action. ' +
|
|
77
|
+
'The resource_id, resource_type, and resource_name are provided in the prompt header — ' +
|
|
78
|
+
'copy them exactly as given. ' +
|
|
79
|
+
'user_token is injected automatically by the server; do NOT ask the user for it. ' +
|
|
80
|
+
'jira_id is optional — only include it if the user explicitly mentions a Jira issue number.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object' as const,
|
|
83
|
+
properties: {
|
|
84
|
+
resource_id: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'Canonical resource ID as shown in the prompt header (e.g. "cmd-client-sdk-ai-hub-generate-testcase").',
|
|
87
|
+
},
|
|
88
|
+
resource_type: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
enum: ['command', 'skill'],
|
|
91
|
+
description: 'Resource type: "command" or "skill".',
|
|
92
|
+
},
|
|
93
|
+
resource_name: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Human-readable resource name as shown in the prompt header (e.g. "generate-testcase").',
|
|
96
|
+
},
|
|
97
|
+
user_token: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description:
|
|
100
|
+
'DO NOT set this field — it is automatically injected by the MCP server from ' +
|
|
101
|
+
'the authenticated SSE connection.',
|
|
102
|
+
},
|
|
103
|
+
jira_id: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description:
|
|
106
|
+
'Optional Jira Issue ID for usage correlation (e.g. "PROJ-12345"). ' +
|
|
107
|
+
'Only include if the user explicitly mentioned a Jira issue in this conversation.',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: ['resource_id', 'resource_type', 'resource_name'],
|
|
111
|
+
},
|
|
112
|
+
handler: trackUsage,
|
|
113
|
+
};
|
|
@@ -15,6 +15,7 @@ import { apiClient } from '../api/client';
|
|
|
15
15
|
import { getCursorTypeDir, getCursorRootDir } from '../utils/cursor-paths.js';
|
|
16
16
|
import { MCPServerError, createValidationError } from '../types/errors';
|
|
17
17
|
import type { UninstallResourceParams, UninstallResourceResult, ToolResult } from '../types/tools';
|
|
18
|
+
import { promptManager } from '../prompts/index.js';
|
|
18
19
|
|
|
19
20
|
/** Resource install entry — may be a file or a directory. */
|
|
20
21
|
interface InstalledResource {
|
|
@@ -130,6 +131,67 @@ export async function uninstallResource(params: unknown): Promise<ToolResult<Uni
|
|
|
130
131
|
const pattern = typedParams.resource_id_or_name;
|
|
131
132
|
const removeFromAccount = typedParams.remove_from_account || false;
|
|
132
133
|
|
|
134
|
+
const removedResources: Array<{ id: string; name: string; path: string }> = [];
|
|
135
|
+
let subscriptionRemoved = false;
|
|
136
|
+
let mcpJsonCleaned = false;
|
|
137
|
+
|
|
138
|
+
// ── Command / Skill: unregister MCP Prompt + delete cache ─────────────
|
|
139
|
+
// Match registered prompt names that contain the pattern.
|
|
140
|
+
const matchedPromptNames = promptManager.promptNames().filter(
|
|
141
|
+
(name) => name === pattern || name.includes(pattern),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (matchedPromptNames.length > 0) {
|
|
145
|
+
for (const promptName of matchedPromptNames) {
|
|
146
|
+
// Prompt name format: <team>/<type>/<resource_name>
|
|
147
|
+
const parts = promptName.split('/');
|
|
148
|
+
const team = parts[0] ?? 'general';
|
|
149
|
+
const resourceType = parts[1] as 'command' | 'skill' | undefined;
|
|
150
|
+
const resourceName = parts.slice(2).join('/') || promptName;
|
|
151
|
+
|
|
152
|
+
// Find the resource_id from the registered prompt (best-effort via name).
|
|
153
|
+
// For unsubscription, we pass the promptName as id if no better source.
|
|
154
|
+
const resourceId = pattern.startsWith('cmd-') || pattern.startsWith('skill-')
|
|
155
|
+
? pattern
|
|
156
|
+
: promptName;
|
|
157
|
+
|
|
158
|
+
// Unregister from the in-memory prompt registry only.
|
|
159
|
+
// The server-side .prompt-cache/ files are intentionally NOT deleted here —
|
|
160
|
+
// they are shared across all users and will be regenerated on the next git pull.
|
|
161
|
+
promptManager.unregisterPrompt(resourceId, resourceType ?? 'command', resourceName);
|
|
162
|
+
|
|
163
|
+
removedResources.push({ id: resourceId, name: resourceName, path: `[MCP Prompt: ${promptName}]` });
|
|
164
|
+
logger.info({ promptName, team, resourceType, resourceName }, 'MCP Prompt unregistered via uninstall');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Remove from server subscription if requested
|
|
168
|
+
if (removeFromAccount) {
|
|
169
|
+
for (const r of removedResources) {
|
|
170
|
+
try {
|
|
171
|
+
await apiClient.unsubscribe(r.id);
|
|
172
|
+
subscriptionRemoved = true;
|
|
173
|
+
} catch (err) {
|
|
174
|
+
logger.warn({ resourceId: r.id, err }, 'Failed to unsubscribe Command/Skill Prompt from account');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Return early — Command/Skill resources have no local filesystem footprint.
|
|
180
|
+
const result: UninstallResourceResult = {
|
|
181
|
+
success: true,
|
|
182
|
+
removed_resources: removedResources,
|
|
183
|
+
subscription_removed: subscriptionRemoved,
|
|
184
|
+
message: [
|
|
185
|
+
`Successfully unregistered ${removedResources.length} MCP Prompt${removedResources.length > 1 ? 's' : ''}.`,
|
|
186
|
+
subscriptionRemoved ? 'Subscription removed from account.' : null,
|
|
187
|
+
].filter(Boolean).join(' '),
|
|
188
|
+
};
|
|
189
|
+
const duration = Date.now() - startTime;
|
|
190
|
+
logToolCall('uninstall_resource', 'user-id', params as Record<string, unknown>, duration);
|
|
191
|
+
return { success: true, data: result };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Rule / MCP: original local-filesystem removal path ────────────────
|
|
133
195
|
logger.debug({ pattern }, 'Finding installed resources...');
|
|
134
196
|
const matched = await findInstalledResources(pattern);
|
|
135
197
|
|
|
@@ -143,10 +205,6 @@ export async function uninstallResource(params: unknown): Promise<ToolResult<Uni
|
|
|
143
205
|
|
|
144
206
|
logger.info({ pattern, count: matched.length }, 'Found matching installed resources');
|
|
145
207
|
|
|
146
|
-
const removedResources: Array<{ id: string; name: string; path: string }> = [];
|
|
147
|
-
let subscriptionRemoved = false;
|
|
148
|
-
let mcpJsonCleaned = false;
|
|
149
|
-
|
|
150
208
|
for (const resource of matched) {
|
|
151
209
|
try {
|
|
152
210
|
if (resource.isDirectory) {
|