@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.
- 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 +39 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +21 -3
- 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.map +1 -1
- package/dist/tools/manage-subscription.js +19 -4
- package/dist/tools/manage-subscription.js.map +1 -1
- package/dist/tools/search-resources.d.ts.map +1 -1
- package/dist/tools/search-resources.js +2 -3
- package/dist/tools/search-resources.js.map +1 -1
- package/dist/tools/sync-resources.d.ts +9 -4
- package/dist/tools/sync-resources.d.ts.map +1 -1
- package/dist/tools/sync-resources.js +121 -7
- 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.map +1 -1
- package/dist/tools/upload-resource.js +49 -5
- package/dist/tools/upload-resource.js.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 +52 -3
- 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 +19 -4
- package/src/tools/search-resources.ts +2 -4
- package/src/tools/sync-resources.ts +131 -7
- package/src/tools/track-usage.ts +113 -0
- package/src/tools/uninstall-resource.ts +62 -4
- package/src/tools/upload-resource.ts +52 -5
- 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
|
|
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:
|
|
@@ -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
|
-
'
|
|
664
|
-
'
|
|
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
|
-
'\
|
|
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
|
-
'
|
|
377
|
-
'
|
|
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
|
+
}
|