@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.
Files changed (104) hide show
  1. package/.prompt-cache/cmd-cmd-client-sdk-ai-hub-generate-testcase.md +101 -0
  2. package/.prompt-cache/cmd-cmd-client-sdk-ai-hub-submit_zct_job.md +158 -0
  3. package/.prompt-cache/skill-skill-client-sdk-ai-hub-analyze-conf-status.md +311 -0
  4. package/.prompt-cache/skill-skill-client-sdk-ai-hub-analyze-sdk-log.md +64 -0
  5. package/.prompt-cache/skill-skill-client-sdk-ai-hub-analyze-zmb-log-errors.md +84 -0
  6. package/ai-resource-telemetry.json +22 -0
  7. package/dist/api/client.d.ts +76 -8
  8. package/dist/api/client.d.ts.map +1 -1
  9. package/dist/api/client.js +86 -40
  10. package/dist/api/client.js.map +1 -1
  11. package/dist/auth/permissions.d.ts.map +1 -1
  12. package/dist/auth/permissions.js +6 -0
  13. package/dist/auth/permissions.js.map +1 -1
  14. package/dist/config/index.d.ts +6 -1
  15. package/dist/config/index.d.ts.map +1 -1
  16. package/dist/config/index.js +1 -3
  17. package/dist/config/index.js.map +1 -1
  18. package/dist/index.js +12 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/prompts/cache.d.ts +69 -0
  21. package/dist/prompts/cache.d.ts.map +1 -0
  22. package/dist/prompts/cache.js +163 -0
  23. package/dist/prompts/cache.js.map +1 -0
  24. package/dist/prompts/generator.d.ts +49 -0
  25. package/dist/prompts/generator.d.ts.map +1 -0
  26. package/dist/prompts/generator.js +158 -0
  27. package/dist/prompts/generator.js.map +1 -0
  28. package/dist/prompts/index.d.ts +13 -0
  29. package/dist/prompts/index.d.ts.map +1 -0
  30. package/dist/prompts/index.js +24 -0
  31. package/dist/prompts/index.js.map +1 -0
  32. package/dist/prompts/manager.d.ts +106 -0
  33. package/dist/prompts/manager.d.ts.map +1 -0
  34. package/dist/prompts/manager.js +263 -0
  35. package/dist/prompts/manager.js.map +1 -0
  36. package/dist/server/http.d.ts.map +1 -1
  37. package/dist/server/http.js +61 -17
  38. package/dist/server/http.js.map +1 -1
  39. package/dist/server.d.ts.map +1 -1
  40. package/dist/server.js +43 -0
  41. package/dist/server.js.map +1 -1
  42. package/dist/telemetry/index.d.ts +3 -0
  43. package/dist/telemetry/index.d.ts.map +1 -0
  44. package/dist/telemetry/index.js +7 -0
  45. package/dist/telemetry/index.js.map +1 -0
  46. package/dist/telemetry/manager.d.ts +149 -0
  47. package/dist/telemetry/manager.d.ts.map +1 -0
  48. package/dist/telemetry/manager.js +368 -0
  49. package/dist/telemetry/manager.js.map +1 -0
  50. package/dist/tools/index.d.ts +1 -0
  51. package/dist/tools/index.d.ts.map +1 -1
  52. package/dist/tools/index.js +1 -0
  53. package/dist/tools/index.js.map +1 -1
  54. package/dist/tools/manage-subscription.d.ts +4 -0
  55. package/dist/tools/manage-subscription.d.ts.map +1 -1
  56. package/dist/tools/manage-subscription.js +36 -7
  57. package/dist/tools/manage-subscription.js.map +1 -1
  58. package/dist/tools/search-resources.d.ts +4 -0
  59. package/dist/tools/search-resources.d.ts.map +1 -1
  60. package/dist/tools/search-resources.js +6 -1
  61. package/dist/tools/search-resources.js.map +1 -1
  62. package/dist/tools/sync-resources.d.ts +13 -4
  63. package/dist/tools/sync-resources.d.ts.map +1 -1
  64. package/dist/tools/sync-resources.js +127 -6
  65. package/dist/tools/sync-resources.js.map +1 -1
  66. package/dist/tools/track-usage.d.ts +63 -0
  67. package/dist/tools/track-usage.d.ts.map +1 -0
  68. package/dist/tools/track-usage.js +90 -0
  69. package/dist/tools/track-usage.js.map +1 -0
  70. package/dist/tools/uninstall-resource.d.ts.map +1 -1
  71. package/dist/tools/uninstall-resource.js +53 -3
  72. package/dist/tools/uninstall-resource.js.map +1 -1
  73. package/dist/tools/upload-resource.d.ts +4 -0
  74. package/dist/tools/upload-resource.d.ts.map +1 -1
  75. package/dist/tools/upload-resource.js +164 -23
  76. package/dist/tools/upload-resource.js.map +1 -1
  77. package/dist/types/tools.d.ts +17 -2
  78. package/dist/types/tools.d.ts.map +1 -1
  79. package/dist/utils/cursor-paths.d.ts +10 -0
  80. package/dist/utils/cursor-paths.d.ts.map +1 -1
  81. package/dist/utils/cursor-paths.js +13 -0
  82. package/dist/utils/cursor-paths.js.map +1 -1
  83. package/package.json +1 -1
  84. package/src/api/client.ts +191 -71
  85. package/src/auth/permissions.ts +6 -0
  86. package/src/config/index.ts +11 -5
  87. package/src/index.ts +18 -0
  88. package/src/prompts/cache.ts +140 -0
  89. package/src/prompts/generator.ts +142 -0
  90. package/src/prompts/index.ts +20 -0
  91. package/src/prompts/manager.ts +342 -0
  92. package/src/server/http.ts +69 -17
  93. package/src/server.ts +13 -0
  94. package/src/telemetry/index.ts +10 -0
  95. package/src/telemetry/manager.ts +419 -0
  96. package/src/tools/index.ts +1 -0
  97. package/src/tools/manage-subscription.ts +41 -7
  98. package/src/tools/search-resources.ts +14 -6
  99. package/src/tools/sync-resources.ts +141 -9
  100. package/src/tools/track-usage.ts +113 -0
  101. package/src/tools/uninstall-resource.ts +62 -4
  102. package/src/tools/upload-resource.ts +204 -31
  103. package/src/types/tools.ts +17 -2
  104. 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({ mode: 'incremental', scope: typedParams.scope || 'global' });
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({ mode: 'incremental', scope: typedParams.scope || 'global' });
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
- team: typedParams.team,
89
- type: typedParams.type,
90
- keyword: typedParams.keyword,
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 to the local Cursor directories:
5
- * macOS/Linux : ~/.cursor/skills/ ~/.cursor/commands/ ~/.cursor/rules/ ~/.cursor/mcp-servers/
6
- * Windows : %APPDATA%\Cursor\User\<type>\
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: download content via REST API → write to Cursor directory.
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 = typedParams.mode || 'incremental';
307
- const scope = typedParams.scope || 'global';
308
- const types = typedParams.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) {