@elliotding/ai-agent-mcp 0.1.3 → 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 (98) 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 +39 -0
  8. package/dist/api/client.d.ts.map +1 -1
  9. package/dist/api/client.js +21 -3
  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.map +1 -1
  55. package/dist/tools/manage-subscription.js +19 -4
  56. package/dist/tools/manage-subscription.js.map +1 -1
  57. package/dist/tools/search-resources.d.ts.map +1 -1
  58. package/dist/tools/search-resources.js +2 -3
  59. package/dist/tools/search-resources.js.map +1 -1
  60. package/dist/tools/sync-resources.d.ts +9 -4
  61. package/dist/tools/sync-resources.d.ts.map +1 -1
  62. package/dist/tools/sync-resources.js +121 -7
  63. package/dist/tools/sync-resources.js.map +1 -1
  64. package/dist/tools/track-usage.d.ts +63 -0
  65. package/dist/tools/track-usage.d.ts.map +1 -0
  66. package/dist/tools/track-usage.js +90 -0
  67. package/dist/tools/track-usage.js.map +1 -0
  68. package/dist/tools/uninstall-resource.d.ts.map +1 -1
  69. package/dist/tools/uninstall-resource.js +53 -3
  70. package/dist/tools/uninstall-resource.js.map +1 -1
  71. package/dist/tools/upload-resource.d.ts.map +1 -1
  72. package/dist/tools/upload-resource.js +49 -5
  73. package/dist/tools/upload-resource.js.map +1 -1
  74. package/dist/utils/cursor-paths.d.ts +10 -0
  75. package/dist/utils/cursor-paths.d.ts.map +1 -1
  76. package/dist/utils/cursor-paths.js +13 -0
  77. package/dist/utils/cursor-paths.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/api/client.ts +52 -3
  80. package/src/auth/permissions.ts +6 -0
  81. package/src/config/index.ts +11 -5
  82. package/src/index.ts +18 -0
  83. package/src/prompts/cache.ts +140 -0
  84. package/src/prompts/generator.ts +142 -0
  85. package/src/prompts/index.ts +20 -0
  86. package/src/prompts/manager.ts +342 -0
  87. package/src/server/http.ts +69 -17
  88. package/src/server.ts +13 -0
  89. package/src/telemetry/index.ts +10 -0
  90. package/src/telemetry/manager.ts +419 -0
  91. package/src/tools/index.ts +1 -0
  92. package/src/tools/manage-subscription.ts +19 -4
  93. package/src/tools/search-resources.ts +2 -4
  94. package/src/tools/sync-resources.ts +131 -7
  95. package/src/tools/track-usage.ts +113 -0
  96. package/src/tools/uninstall-resource.ts +62 -4
  97. package/src/tools/upload-resource.ts +52 -5
  98. package/src/utils/cursor-paths.ts +13 -0
@@ -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:
@@ -397,6 +421,86 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
397
421
  continue;
398
422
  }
399
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
+
400
504
  // Download all files for this resource from the CSP server.
401
505
  // We always download first so we can inspect the payload and determine
402
506
  // whether this is a remote-URL-only MCP (Format B: config-only, no
@@ -608,6 +712,27 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
608
712
  timestamp: new Date().toISOString()
609
713
  }, 'sync_resources completed successfully');
610
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
+
611
736
  return { success: true, data: result };
612
737
 
613
738
  } catch (error) {
@@ -660,9 +785,8 @@ export const syncResourcesTool = {
660
785
  user_token: {
661
786
  type: 'string',
662
787
  description:
663
- 'CSP API token for the current user. Read this from the CSP_API_TOKEN environment ' +
664
- 'variable configured in the user\'s mcp.json. When provided, this token is used ' +
665
- 'for all CSP API calls in this request instead of the server-level fallback token.',
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.',
666
790
  },
667
791
  },
668
792
  },
@@ -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) {
@@ -18,11 +18,25 @@ import { logger, logToolCall } from '../utils/logger';
18
18
  import { apiClient } from '../api/client';
19
19
  import { resourceLoader } from '../resources';
20
20
  import { MCPServerError, createValidationError } from '../types/errors';
21
+ import { promptManager } from '../prompts/index.js';
21
22
  import type { UploadResourceParams, UploadResourceResult, ToolResult, FileEntry } from '../types/tools';
22
23
  import type { ResourceType } from '../types/resources';
23
24
 
24
25
  type ResourceCategory = 'command' | 'skill' | 'rule' | 'mcp';
25
26
 
27
+ /** Extract the `description` field from YAML frontmatter (--- ... ---) in Markdown content. */
28
+ function extractFrontmatterDescription(content: string): string | undefined {
29
+ if (!content.startsWith('---')) return undefined;
30
+ const end = content.indexOf('\n---', 3);
31
+ if (end === -1) return undefined;
32
+ const frontmatter = content.slice(3, end);
33
+ for (const line of frontmatter.split('\n')) {
34
+ const match = /^description:\s*(.+)$/.exec(line.trim());
35
+ if (match) return match[1]!.trim().replace(/^['"]|['"]$/g, '');
36
+ }
37
+ return undefined;
38
+ }
39
+
26
40
  /**
27
41
  * Infer the resource type from the uploaded file list ONLY when the user has
28
42
  * not explicitly stated a type. If the user declared a type, that always wins.
@@ -254,6 +268,41 @@ export async function uploadResource(params: unknown): Promise<ToolResult<Upload
254
268
 
255
269
  logger.info({ finalResourceId, version, commitHash }, 'Upload finalized successfully');
256
270
 
271
+ // For Command / Skill: register the uploaded content as an MCP Prompt immediately,
272
+ // so the user can invoke it via slash-command without a full sync cycle.
273
+ if (resourceType === 'command' || resourceType === 'skill') {
274
+ try {
275
+ const primaryFile = resourceType === 'skill'
276
+ ? (fileEntries.find((f) => path.basename(f.path) === 'SKILL.md') ??
277
+ fileEntries.find((f) => f.path.endsWith('.md')) ??
278
+ fileEntries[0])
279
+ : (fileEntries.find((f) => path.basename(f.path).replace(/\.md$/, '') === resourceName) ??
280
+ fileEntries.find((f) => f.path.endsWith('.md')) ??
281
+ fileEntries[0]);
282
+
283
+ const rawContent = primaryFile?.content ?? '';
284
+ const team = (typedParams as any).team ?? 'general';
285
+ const frontmatterDesc = extractFrontmatterDescription(rawContent);
286
+ const description = frontmatterDesc ?? typedParams.message ?? resourceName;
287
+
288
+ await promptManager.registerPrompt({
289
+ resource_id: finalResourceId,
290
+ resource_type: resourceType as 'command' | 'skill',
291
+ resource_name: resourceName,
292
+ team,
293
+ description,
294
+ rawContent,
295
+ });
296
+ logger.info({ finalResourceId, resourceType }, 'MCP Prompt registered after upload');
297
+ } catch (promptErr) {
298
+ // Non-fatal: the resource is uploaded; Prompt registration is best-effort.
299
+ logger.warn(
300
+ { finalResourceId, error: (promptErr as Error).message },
301
+ 'MCP Prompt registration after upload failed (non-fatal)',
302
+ );
303
+ }
304
+ }
305
+
257
306
  const result: UploadResourceResult = {
258
307
  resource_id: finalResourceId,
259
308
  version,
@@ -308,8 +357,7 @@ export const uploadResourceTool = {
308
357
  'path must be the original filename as-is (relative, no path traversal). ' +
309
358
  'No restriction on file extensions — mcp packages may include .py, .js, package.json, etc.\n' +
310
359
 
311
- '\nIMPORTANT: Always read the CSP_API_TOKEN from the user\'s environment and pass it as user_token ' +
312
- 'so that each user\'s API calls use their own identity.',
360
+ '\nThe user_token is injected automatically by the server do NOT read or pass it manually.',
313
361
  inputSchema: {
314
362
  type: 'object' as const,
315
363
  properties: {
@@ -373,9 +421,8 @@ export const uploadResourceTool = {
373
421
  user_token: {
374
422
  type: 'string',
375
423
  description:
376
- 'CSP API token for the current user. Read this from the CSP_API_TOKEN environment ' +
377
- 'variable configured in the user\'s mcp.json. When provided, this token is used ' +
378
- 'for all CSP API calls in this request instead of the server-level fallback token.',
424
+ 'DO NOT set this field it is automatically injected by the MCP server from ' +
425
+ 'the authenticated SSE connection. The server always provides the correct token.',
379
426
  },
380
427
  },
381
428
  required: ['resource_id', 'message', 'files'],
@@ -81,3 +81,16 @@ export function getCursorTypeDir(resourceType: string): string {
81
81
  export function getCursorResourcePath(resourceType: string, resourceName: string): string {
82
82
  return path.join(getCursorTypeDir(resourceType), resourceName);
83
83
  }
84
+
85
+ /**
86
+ * Returns the path to the local AI resource telemetry file.
87
+ *
88
+ * Stored at the Cursor root level (not inside a resource-type subdirectory)
89
+ * so it persists independently of individual resource installs/uninstalls.
90
+ *
91
+ * macOS / Linux : ~/.cursor/ai-resource-telemetry.json
92
+ * Windows : %APPDATA%\Cursor\User\ai-resource-telemetry.json
93
+ */
94
+ export function getTelemetryFilePath(): string {
95
+ return path.join(getCursorRootDir(), 'ai-resource-telemetry.json');
96
+ }