@elliotding/ai-agent-mcp 0.1.0

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 (191) hide show
  1. package/dist/api/cached-client.d.ts +48 -0
  2. package/dist/api/cached-client.d.ts.map +1 -0
  3. package/dist/api/cached-client.js +126 -0
  4. package/dist/api/cached-client.js.map +1 -0
  5. package/dist/api/client.d.ts +213 -0
  6. package/dist/api/client.d.ts.map +1 -0
  7. package/dist/api/client.js +326 -0
  8. package/dist/api/client.js.map +1 -0
  9. package/dist/auth/index.d.ts +8 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/index.js +26 -0
  12. package/dist/auth/index.js.map +1 -0
  13. package/dist/auth/middleware.d.ts +36 -0
  14. package/dist/auth/middleware.d.ts.map +1 -0
  15. package/dist/auth/middleware.js +194 -0
  16. package/dist/auth/middleware.js.map +1 -0
  17. package/dist/auth/permissions.d.ts +60 -0
  18. package/dist/auth/permissions.d.ts.map +1 -0
  19. package/dist/auth/permissions.js +256 -0
  20. package/dist/auth/permissions.js.map +1 -0
  21. package/dist/auth/token-validator.d.ts +52 -0
  22. package/dist/auth/token-validator.d.ts.map +1 -0
  23. package/dist/auth/token-validator.js +217 -0
  24. package/dist/auth/token-validator.js.map +1 -0
  25. package/dist/cache/cache-manager.d.ts +49 -0
  26. package/dist/cache/cache-manager.d.ts.map +1 -0
  27. package/dist/cache/cache-manager.js +191 -0
  28. package/dist/cache/cache-manager.js.map +1 -0
  29. package/dist/cache/index.d.ts +6 -0
  30. package/dist/cache/index.d.ts.map +1 -0
  31. package/dist/cache/index.js +12 -0
  32. package/dist/cache/index.js.map +1 -0
  33. package/dist/cache/redis-client.d.ts +45 -0
  34. package/dist/cache/redis-client.d.ts.map +1 -0
  35. package/dist/cache/redis-client.js +210 -0
  36. package/dist/cache/redis-client.js.map +1 -0
  37. package/dist/config/constants.d.ts +28 -0
  38. package/dist/config/constants.d.ts.map +1 -0
  39. package/dist/config/constants.js +31 -0
  40. package/dist/config/constants.js.map +1 -0
  41. package/dist/config/index.d.ts +54 -0
  42. package/dist/config/index.d.ts.map +1 -0
  43. package/dist/config/index.js +168 -0
  44. package/dist/config/index.js.map +1 -0
  45. package/dist/filesystem/manager.d.ts +45 -0
  46. package/dist/filesystem/manager.d.ts.map +1 -0
  47. package/dist/filesystem/manager.js +246 -0
  48. package/dist/filesystem/manager.js.map +1 -0
  49. package/dist/git/multi-source-manager.d.ts +62 -0
  50. package/dist/git/multi-source-manager.d.ts.map +1 -0
  51. package/dist/git/multi-source-manager.js +293 -0
  52. package/dist/git/multi-source-manager.js.map +1 -0
  53. package/dist/git/operations.d.ts +27 -0
  54. package/dist/git/operations.d.ts.map +1 -0
  55. package/dist/git/operations.js +83 -0
  56. package/dist/git/operations.js.map +1 -0
  57. package/dist/index.d.ts +6 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +109 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/monitoring/health.d.ts +35 -0
  62. package/dist/monitoring/health.d.ts.map +1 -0
  63. package/dist/monitoring/health.js +105 -0
  64. package/dist/monitoring/health.js.map +1 -0
  65. package/dist/resources/index.d.ts +6 -0
  66. package/dist/resources/index.d.ts.map +1 -0
  67. package/dist/resources/index.js +10 -0
  68. package/dist/resources/index.js.map +1 -0
  69. package/dist/resources/loader.d.ts +87 -0
  70. package/dist/resources/loader.d.ts.map +1 -0
  71. package/dist/resources/loader.js +452 -0
  72. package/dist/resources/loader.js.map +1 -0
  73. package/dist/server/http.d.ts +57 -0
  74. package/dist/server/http.d.ts.map +1 -0
  75. package/dist/server/http.js +336 -0
  76. package/dist/server/http.js.map +1 -0
  77. package/dist/server.d.ts +13 -0
  78. package/dist/server.d.ts.map +1 -0
  79. package/dist/server.js +157 -0
  80. package/dist/server.js.map +1 -0
  81. package/dist/session/manager.d.ts +91 -0
  82. package/dist/session/manager.d.ts.map +1 -0
  83. package/dist/session/manager.js +251 -0
  84. package/dist/session/manager.js.map +1 -0
  85. package/dist/tools/index.d.ts +11 -0
  86. package/dist/tools/index.d.ts.map +1 -0
  87. package/dist/tools/index.js +27 -0
  88. package/dist/tools/index.js.map +1 -0
  89. package/dist/tools/manage-subscription.d.ts +43 -0
  90. package/dist/tools/manage-subscription.d.ts.map +1 -0
  91. package/dist/tools/manage-subscription.js +268 -0
  92. package/dist/tools/manage-subscription.js.map +1 -0
  93. package/dist/tools/registry.d.ts +40 -0
  94. package/dist/tools/registry.d.ts.map +1 -0
  95. package/dist/tools/registry.js +85 -0
  96. package/dist/tools/registry.js.map +1 -0
  97. package/dist/tools/search-resources.d.ts +31 -0
  98. package/dist/tools/search-resources.d.ts.map +1 -0
  99. package/dist/tools/search-resources.js +154 -0
  100. package/dist/tools/search-resources.js.map +1 -0
  101. package/dist/tools/sync-resources.d.ts +41 -0
  102. package/dist/tools/sync-resources.d.ts.map +1 -0
  103. package/dist/tools/sync-resources.js +606 -0
  104. package/dist/tools/sync-resources.js.map +1 -0
  105. package/dist/tools/uninstall-resource.d.ts +30 -0
  106. package/dist/tools/uninstall-resource.d.ts.map +1 -0
  107. package/dist/tools/uninstall-resource.js +259 -0
  108. package/dist/tools/uninstall-resource.js.map +1 -0
  109. package/dist/tools/upload-resource.d.ts +77 -0
  110. package/dist/tools/upload-resource.d.ts.map +1 -0
  111. package/dist/tools/upload-resource.js +252 -0
  112. package/dist/tools/upload-resource.js.map +1 -0
  113. package/dist/transport/sse.d.ts +29 -0
  114. package/dist/transport/sse.d.ts.map +1 -0
  115. package/dist/transport/sse.js +271 -0
  116. package/dist/transport/sse.js.map +1 -0
  117. package/dist/types/errors.d.ts +60 -0
  118. package/dist/types/errors.d.ts.map +1 -0
  119. package/dist/types/errors.js +112 -0
  120. package/dist/types/errors.js.map +1 -0
  121. package/dist/types/index.d.ts +7 -0
  122. package/dist/types/index.d.ts.map +1 -0
  123. package/dist/types/index.js +23 -0
  124. package/dist/types/index.js.map +1 -0
  125. package/dist/types/mcp.d.ts +50 -0
  126. package/dist/types/mcp.d.ts.map +1 -0
  127. package/dist/types/mcp.js +6 -0
  128. package/dist/types/mcp.js.map +1 -0
  129. package/dist/types/resources.d.ts +109 -0
  130. package/dist/types/resources.d.ts.map +1 -0
  131. package/dist/types/resources.js +7 -0
  132. package/dist/types/resources.js.map +1 -0
  133. package/dist/types/tools.d.ts +147 -0
  134. package/dist/types/tools.d.ts.map +1 -0
  135. package/dist/types/tools.js +6 -0
  136. package/dist/types/tools.js.map +1 -0
  137. package/dist/utils/cursor-paths.d.ts +49 -0
  138. package/dist/utils/cursor-paths.d.ts.map +1 -0
  139. package/dist/utils/cursor-paths.js +116 -0
  140. package/dist/utils/cursor-paths.js.map +1 -0
  141. package/dist/utils/log-cleaner.d.ts +18 -0
  142. package/dist/utils/log-cleaner.d.ts.map +1 -0
  143. package/dist/utils/log-cleaner.js +112 -0
  144. package/dist/utils/log-cleaner.js.map +1 -0
  145. package/dist/utils/logger.d.ts +59 -0
  146. package/dist/utils/logger.d.ts.map +1 -0
  147. package/dist/utils/logger.js +292 -0
  148. package/dist/utils/logger.js.map +1 -0
  149. package/dist/utils/validation.d.ts +58 -0
  150. package/dist/utils/validation.d.ts.map +1 -0
  151. package/dist/utils/validation.js +214 -0
  152. package/dist/utils/validation.js.map +1 -0
  153. package/package.json +58 -0
  154. package/src/api/cached-client.ts +144 -0
  155. package/src/api/client.ts +578 -0
  156. package/src/auth/index.ts +11 -0
  157. package/src/auth/middleware.ts +244 -0
  158. package/src/auth/permissions.ts +317 -0
  159. package/src/auth/token-validator.ts +294 -0
  160. package/src/cache/cache-manager.ts +243 -0
  161. package/src/cache/index.ts +6 -0
  162. package/src/cache/redis-client.ts +249 -0
  163. package/src/config/constants.ts +33 -0
  164. package/src/config/index.ts +228 -0
  165. package/src/filesystem/manager.ts +235 -0
  166. package/src/git/multi-source-manager.ts +333 -0
  167. package/src/git/operations.ts +93 -0
  168. package/src/index.ts +139 -0
  169. package/src/monitoring/health.ts +132 -0
  170. package/src/resources/index.ts +13 -0
  171. package/src/resources/loader.ts +530 -0
  172. package/src/server/http.ts +427 -0
  173. package/src/server.ts +191 -0
  174. package/src/session/manager.ts +296 -0
  175. package/src/tools/index.ts +11 -0
  176. package/src/tools/manage-subscription.ts +332 -0
  177. package/src/tools/registry.ts +97 -0
  178. package/src/tools/search-resources.ts +177 -0
  179. package/src/tools/sync-resources.ts +662 -0
  180. package/src/tools/uninstall-resource.ts +248 -0
  181. package/src/tools/upload-resource.ts +258 -0
  182. package/src/transport/sse.ts +308 -0
  183. package/src/types/errors.ts +146 -0
  184. package/src/types/index.ts +7 -0
  185. package/src/types/mcp.ts +61 -0
  186. package/src/types/resources.ts +141 -0
  187. package/src/types/tools.ts +175 -0
  188. package/src/utils/cursor-paths.ts +83 -0
  189. package/src/utils/log-cleaner.ts +92 -0
  190. package/src/utils/logger.ts +333 -0
  191. package/src/utils/validation.ts +262 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * uninstall_resource Tool
3
+ * Uninstall a resource from local filesystem and clean up related configuration.
4
+ *
5
+ * For MCP resources this also removes the mcpServers entry from ~/.cursor/mcp.json.
6
+ * For directory-based resources (skill, mcp) the entire install directory is removed.
7
+ */
8
+
9
+ import * as fs from 'fs/promises';
10
+ import * as fsSync from 'fs';
11
+ import * as path from 'path';
12
+ import { logger, logToolCall } from '../utils/logger';
13
+ import { filesystemManager } from '../filesystem/manager';
14
+ import { apiClient } from '../api/client';
15
+ import { getCursorTypeDir, getCursorRootDir } from '../utils/cursor-paths.js';
16
+ import { MCPServerError, createValidationError } from '../types/errors';
17
+ import type { UninstallResourceParams, UninstallResourceResult, ToolResult } from '../types/tools';
18
+
19
+ /** Resource install entry — may be a file or a directory. */
20
+ interface InstalledResource {
21
+ id: string;
22
+ name: string;
23
+ path: string;
24
+ isDirectory: boolean;
25
+ }
26
+
27
+ /**
28
+ * Find installed resource files/directories by pattern.
29
+ * - File-based types (rule, command): scan for matching .md/.mdc files
30
+ * - Directory-based types (skill, mcp): scan for matching subdirectories
31
+ */
32
+ async function findInstalledResources(pattern: string): Promise<InstalledResource[]> {
33
+ const results: InstalledResource[] = [];
34
+
35
+ const FILE_TYPES = ['rule', 'command'] as const;
36
+ const DIR_TYPES = ['skill', 'mcp'] as const;
37
+
38
+ // Scan file-based types
39
+ for (const type of FILE_TYPES) {
40
+ let typePath: string;
41
+ try { typePath = getCursorTypeDir(type); } catch { continue; }
42
+
43
+ try {
44
+ // listFiles returns relative names; build absolute paths here
45
+ const relNames = await filesystemManager.listFiles(typePath, /\.(md|mdc)$/);
46
+ for (const relName of relNames) {
47
+ const absPath = path.join(typePath, relName);
48
+ const baseName = path.basename(relName).replace(/\.(md|mdc)$/, '');
49
+ if (baseName === pattern || baseName.includes(pattern) || relName.includes(pattern)) {
50
+ results.push({ id: baseName, name: baseName, path: absPath, isDirectory: false });
51
+ }
52
+ }
53
+ } catch {
54
+ logger.debug({ type, typePath: typePath! }, 'Cursor resource type directory not found, skipping');
55
+ }
56
+ }
57
+
58
+ // Scan directory-based types
59
+ for (const type of DIR_TYPES) {
60
+ let typePath: string;
61
+ try { typePath = getCursorTypeDir(type); } catch { continue; }
62
+
63
+ try {
64
+ const entries = await fs.readdir(typePath, { withFileTypes: true });
65
+ for (const entry of entries) {
66
+ if (!entry.isDirectory()) continue;
67
+ if (entry.name === pattern || entry.name.includes(pattern)) {
68
+ results.push({
69
+ id: entry.name,
70
+ name: entry.name,
71
+ path: path.join(typePath, entry.name),
72
+ isDirectory: true,
73
+ });
74
+ }
75
+ }
76
+ } catch {
77
+ logger.debug({ type, typePath: typePath! }, 'Cursor resource type directory not found, skipping');
78
+ }
79
+ }
80
+
81
+ return results;
82
+ }
83
+
84
+ /**
85
+ * Remove the mcpServers entry whose key matches `serverName` from ~/.cursor/mcp.json.
86
+ * Writes back atomically. No-op if the file or entry does not exist.
87
+ */
88
+ async function removeMcpJsonEntry(serverName: string): Promise<boolean> {
89
+ const mcpJsonPath = path.join(getCursorRootDir(), 'mcp.json');
90
+ if (!fsSync.existsSync(mcpJsonPath)) return false;
91
+
92
+ try {
93
+ const raw = await fs.readFile(mcpJsonPath, 'utf-8');
94
+ const config = JSON.parse(raw) as { mcpServers?: Record<string, unknown> };
95
+
96
+ if (!config.mcpServers) return false;
97
+
98
+ // Case-insensitive search for the server entry
99
+ const matchedKey = Object.keys(config.mcpServers).find(
100
+ k => k === serverName || k.toLowerCase() === serverName.toLowerCase()
101
+ );
102
+ if (!matchedKey) return false;
103
+
104
+ delete config.mcpServers[matchedKey];
105
+
106
+ const tempPath = `${mcpJsonPath}.tmp`;
107
+ await fs.writeFile(tempPath, JSON.stringify(config, null, 2), 'utf-8');
108
+ await fs.rename(tempPath, mcpJsonPath);
109
+
110
+ logger.info({ serverName: matchedKey, mcpJsonPath }, 'Removed mcpServers entry from mcp.json');
111
+ return true;
112
+ } catch (error) {
113
+ logger.warn({ serverName, mcpJsonPath, error }, 'Failed to update mcp.json');
114
+ return false;
115
+ }
116
+ }
117
+
118
+ /** Recursively delete a directory and all its contents. */
119
+ async function removeDirectory(dirPath: string): Promise<void> {
120
+ await fs.rm(dirPath, { recursive: true, force: true });
121
+ }
122
+
123
+ export async function uninstallResource(params: unknown): Promise<ToolResult<UninstallResourceResult>> {
124
+ const startTime = Date.now();
125
+ const typedParams = params as UninstallResourceParams;
126
+
127
+ logger.info({ tool: 'uninstall_resource', params }, 'uninstall_resource called');
128
+
129
+ try {
130
+ const pattern = typedParams.resource_id_or_name;
131
+ const removeFromAccount = typedParams.remove_from_account || false;
132
+
133
+ logger.debug({ pattern }, 'Finding installed resources...');
134
+ const matched = await findInstalledResources(pattern);
135
+
136
+ if (matched.length === 0) {
137
+ throw createValidationError(
138
+ pattern,
139
+ 'resource_id_or_name',
140
+ 'No installed resources found matching pattern. Use search_resources to find available resources'
141
+ );
142
+ }
143
+
144
+ logger.info({ pattern, count: matched.length }, 'Found matching installed resources');
145
+
146
+ const removedResources: Array<{ id: string; name: string; path: string }> = [];
147
+ let subscriptionRemoved = false;
148
+ let mcpJsonCleaned = false;
149
+
150
+ for (const resource of matched) {
151
+ try {
152
+ if (resource.isDirectory) {
153
+ // Directory-based resource (skill / mcp): remove entire directory
154
+ await removeDirectory(resource.path);
155
+ logger.debug({ resourceId: resource.id, path: resource.path }, 'Resource directory deleted');
156
+
157
+ // For MCP: also clean up mcp.json entry
158
+ const cleaned = await removeMcpJsonEntry(resource.name);
159
+ if (cleaned) mcpJsonCleaned = true;
160
+ } else {
161
+ // File-based resource (rule / command): remove single file
162
+ await filesystemManager.deleteResource(resource.path);
163
+ logger.debug({ resourceId: resource.id, path: resource.path }, 'Resource file deleted');
164
+ }
165
+
166
+ removedResources.push({ id: resource.id, name: resource.name, path: resource.path });
167
+
168
+ // Remove from server subscription if requested
169
+ if (removeFromAccount) {
170
+ try {
171
+ await apiClient.unsubscribe(resource.id);
172
+ subscriptionRemoved = true;
173
+ logger.debug({ resourceId: resource.id }, 'Resource unsubscribed from account');
174
+ } catch (error) {
175
+ logger.warn({ resourceId: resource.id, error }, 'Failed to unsubscribe resource from account');
176
+ }
177
+ }
178
+ } catch (error) {
179
+ logger.error({ resourceId: resource.id, path: resource.path, error }, 'Failed to delete resource');
180
+ }
181
+ }
182
+
183
+ // Clean up leftover empty directories
184
+ for (const type of ['command', 'skill', 'rule', 'mcp']) {
185
+ try {
186
+ const typePath = getCursorTypeDir(type);
187
+ await filesystemManager.removeEmptyDirs(typePath);
188
+ } catch {
189
+ // Directory may not exist — ignore
190
+ }
191
+ }
192
+
193
+ const messageParts = [
194
+ `Successfully uninstalled ${removedResources.length} resource${removedResources.length > 1 ? 's' : ''}.`,
195
+ mcpJsonCleaned ? 'MCP server entry removed from ~/.cursor/mcp.json.' : null,
196
+ subscriptionRemoved ? 'Subscription removed from account.' : null,
197
+ ];
198
+
199
+ const result: UninstallResourceResult = {
200
+ success: true,
201
+ removed_resources: removedResources,
202
+ subscription_removed: subscriptionRemoved,
203
+ message: messageParts.filter(Boolean).join(' '),
204
+ };
205
+
206
+ const duration = Date.now() - startTime;
207
+ logToolCall('uninstall_resource', 'user-id', params as Record<string, unknown>, duration);
208
+ logger.info({ pattern, removedCount: removedResources.length, mcpJsonCleaned, subscriptionRemoved, duration }, 'uninstall_resource completed');
209
+
210
+ return { success: true, data: result };
211
+
212
+ } catch (error) {
213
+ logger.error({ error, pattern: typedParams.resource_id_or_name }, 'uninstall_resource failed');
214
+ return {
215
+ success: false,
216
+ error: {
217
+ code: error instanceof MCPServerError ? error.code : 'UNKNOWN_ERROR',
218
+ message: error instanceof Error ? error.message : String(error),
219
+ },
220
+ };
221
+ }
222
+ }
223
+
224
+ // Tool definition for registry
225
+ export const uninstallResourceTool = {
226
+ name: 'uninstall_resource',
227
+ description:
228
+ 'Uninstall a resource from the local machine. ' +
229
+ 'Deletes installed files (rules/commands) or entire install directories (skills/mcp). ' +
230
+ 'For MCP resources, also removes the mcpServers entry from ~/.cursor/mcp.json. ' +
231
+ 'Set remove_from_account: true to also cancel the server-side subscription.',
232
+ inputSchema: {
233
+ type: 'object' as const,
234
+ properties: {
235
+ resource_id_or_name: {
236
+ type: 'string',
237
+ description: 'Resource ID, name, or pattern (supports fuzzy matching)',
238
+ },
239
+ remove_from_account: {
240
+ type: 'boolean',
241
+ description: 'Also remove from subscription list (default: false)',
242
+ default: false,
243
+ },
244
+ },
245
+ required: ['resource_id_or_name'],
246
+ },
247
+ handler: uninstallResource,
248
+ };
@@ -0,0 +1,258 @@
1
+ /**
2
+ * upload_resource Tool
3
+ *
4
+ * Uploads resource files to a CSP source repository via the two-step API:
5
+ * Step 1: POST /csp/api/resources/upload → returns upload_id (server-side staging)
6
+ * Step 2: POST /csp/api/resources/finalize → triggers Git commit, returns permanent resource_id
7
+ *
8
+ * The user selects files from anywhere on their local machine. The AI reads the file
9
+ * content and passes it directly in files[]. The MCP server forwards everything to the
10
+ * CSP API — no local path resolution or server-side filesystem access is needed.
11
+ *
12
+ * target_source is passed through to the CSP API as-is; the CSP server decides
13
+ * which Git repo to commit to based on that value.
14
+ */
15
+
16
+ import * as path from 'path';
17
+ import { logger, logToolCall } from '../utils/logger';
18
+ import { apiClient } from '../api/client';
19
+ import { resourceLoader } from '../resources';
20
+ import { MCPServerError, createValidationError } from '../types/errors';
21
+ import type { UploadResourceParams, UploadResourceResult, ToolResult, FileEntry } from '../types/tools';
22
+ import type { ResourceType } from '../types/resources';
23
+
24
+ /**
25
+ * Validate and return the files[] array.
26
+ * Each entry path must be a relative path with no traversal.
27
+ * For MCP resources, mcp-config.json must be present.
28
+ */
29
+ function collectFiles(typedParams: UploadResourceParams): FileEntry[] {
30
+ if (!typedParams.files || typedParams.files.length === 0) {
31
+ throw createValidationError(
32
+ 'files',
33
+ 'required',
34
+ '"files" must be a non-empty array of {path, content} entries.'
35
+ );
36
+ }
37
+
38
+ for (const entry of typedParams.files) {
39
+ const norm = path.normalize(entry.path);
40
+ if (norm.startsWith('..') || path.isAbsolute(norm)) {
41
+ throw createValidationError(
42
+ 'files[].path',
43
+ 'relative_path',
44
+ `Path traversal or absolute path not allowed: "${entry.path}"`
45
+ );
46
+ }
47
+ }
48
+
49
+ // MCP resources must include mcp-config.json so the client can auto-register
50
+ // the server in ~/.cursor/mcp.json after sync_resources installs it.
51
+ if (typedParams.type === 'mcp') {
52
+ const hasMcpConfig = typedParams.files.some(
53
+ (f) => path.basename(f.path) === 'mcp-config.json'
54
+ );
55
+ if (!hasMcpConfig) {
56
+ throw createValidationError(
57
+ 'files',
58
+ 'required',
59
+ 'MCP resources must include a "mcp-config.json" file. ' +
60
+ 'This file tells the client how to start the MCP server after installation. ' +
61
+ 'Required format:\n' +
62
+ '{\n' +
63
+ ' "name": "<server-name>",\n' +
64
+ ' "command": "python3", // or "node", "uvx", etc.\n' +
65
+ ' "args": ["<entry-file.py>"], // relative path resolved against install dir\n' +
66
+ ' "env": { "ENV_VAR": "" } // optional; empty string = user must fill in\n' +
67
+ '}'
68
+ );
69
+ }
70
+ }
71
+
72
+ return typedParams.files;
73
+ }
74
+
75
+ export async function uploadResource(params: unknown): Promise<ToolResult<UploadResourceResult>> {
76
+ const startTime = Date.now();
77
+ const typedParams = params as UploadResourceParams;
78
+
79
+ logger.info({ tool: 'upload_resource', params }, 'upload_resource called');
80
+
81
+ try {
82
+ const resourceType = typedParams.type;
83
+ const resourceId = typedParams.resource_id;
84
+ const resourceName = typedParams.name ?? resourceId; // API uses "name" field
85
+ const targetSource = typedParams.target_source ?? 'csp'; // default to "csp" repo
86
+ const force = (typedParams as any).force || false;
87
+
88
+ logger.info({ resourceId, resourceType, targetSource }, 'Upload target resolved');
89
+
90
+ // ========== Step 1: Duplicate-name check ==========
91
+ try {
92
+ if (!resourceLoader.getStats()) {
93
+ await resourceLoader.loadConfig();
94
+ await resourceLoader.scanResources();
95
+ }
96
+ const existing = resourceLoader.searchResourcesByName(resourceName, resourceType as ResourceType | undefined);
97
+ if (existing.length > 0 && !force) {
98
+ const conflictInfo = existing.map((r) => ({ name: r.name, type: r.type, source: r.source }));
99
+ logger.warn({ resourceName, resourceType, conflictInfo }, 'Resource name conflict detected');
100
+ return {
101
+ success: false,
102
+ error: {
103
+ code: 'RESOURCE_NAME_CONFLICT',
104
+ message:
105
+ `Resource "${resourceName}" already exists. Add "force": true to overwrite.\n` +
106
+ conflictInfo.map((c) => ` - ${c.name} (${c.type}, source: ${c.source})`).join('\n'),
107
+ details: conflictInfo,
108
+ } as any,
109
+ };
110
+ }
111
+ } catch (err) {
112
+ logger.warn({ error: (err as Error).message }, 'Duplicate check failed, continuing upload');
113
+ }
114
+
115
+ // ========== Step 3: Validate commit message ==========
116
+ if (!typedParams.message || typeof typedParams.message !== 'string') {
117
+ throw createValidationError('message', 'string', 'Commit message is required');
118
+ }
119
+ if (typedParams.message.length < 5 || typedParams.message.length > 200) {
120
+ throw createValidationError(
121
+ 'message', 'string',
122
+ `Commit message must be 5-200 characters, got ${typedParams.message.length}`
123
+ );
124
+ }
125
+
126
+ // ========== Step 4: Collect files ==========
127
+ const fileEntries = collectFiles(typedParams);
128
+ logger.info({ resourceId, fileCount: fileEntries.length }, 'Files collected for upload');
129
+
130
+ // ========== Step 5: Call CSP API — upload (staging) ==========
131
+ logger.info({ resourceName, resourceType, targetSource }, 'Calling CSP upload API...');
132
+ const uploadResp = await apiClient.uploadResourceFiles({
133
+ type: resourceType,
134
+ name: resourceName,
135
+ files: fileEntries,
136
+ target_source: targetSource,
137
+ force,
138
+ });
139
+
140
+ const uploadId = uploadResp.upload_id;
141
+ logger.info({ uploadId, expiresAt: uploadResp.expires_at }, 'Upload staged successfully');
142
+
143
+ // ========== Step 6: Call CSP API — finalize (Git commit) ==========
144
+ logger.info({ uploadId }, 'Calling CSP finalize API...');
145
+ const finalizeResp = await apiClient.finalizeResourceUpload(uploadId, typedParams.message);
146
+
147
+ const finalResourceId = finalizeResp.resource_id;
148
+ const version = finalizeResp.version ?? '1.0.0';
149
+ const resourceUrl = finalizeResp.url ?? '';
150
+ const commitHash = finalizeResp.commit_hash ?? '';
151
+
152
+ logger.info({ finalResourceId, version, commitHash }, 'Upload finalized successfully');
153
+
154
+ const result: UploadResourceResult = {
155
+ resource_id: finalResourceId,
156
+ version,
157
+ url: resourceUrl,
158
+ commit_hash: commitHash,
159
+ message: `Resource '${resourceName}' (${resourceType}) uploaded to source '${targetSource}' (v${version}). ${resourceUrl ? `URL: ${resourceUrl}` : ''}`.trim(),
160
+ };
161
+
162
+ const duration = Date.now() - startTime;
163
+ logToolCall('upload_resource', 'user-id', params as Record<string, unknown>, duration);
164
+ logger.info({ finalResourceId, version, source: targetSource, duration }, 'upload_resource completed');
165
+
166
+ return { success: true, data: result };
167
+
168
+ } catch (error) {
169
+ logger.error({ error, resourceId: typedParams.resource_id }, 'upload_resource failed');
170
+ return {
171
+ success: false,
172
+ error: {
173
+ code: error instanceof MCPServerError ? error.code : 'UNKNOWN_ERROR',
174
+ message: error instanceof Error ? error.message : String(error),
175
+ },
176
+ };
177
+ }
178
+ }
179
+
180
+ // Tool definition for registry
181
+ export const uploadResourceTool = {
182
+ name: 'upload_resource',
183
+ description:
184
+ 'Upload a new AI resource (command, skill, rule, or mcp) to a CSP source repository. ' +
185
+ 'The user selects files from anywhere on their local machine — read each file and pass its content in files[]. ' +
186
+ 'ALWAYS confirm two things with the user before uploading: ' +
187
+ '(1) the resource type (command/skill/rule/mcp), and ' +
188
+ '(2) the target source repo (e.g. "csp" (default) or "client-sdk-ai-hub"). ' +
189
+ 'The tool uses a two-step CSP API: first stages files and gets an upload_id, then finalizes the Git commit. ' +
190
+ 'Pass files[] — an array of {path, content} entries. ' +
191
+ 'path is the filename the resource should be stored as (e.g. "csp-ai-agent.mdc", "SKILL.md"). ' +
192
+ 'Single-file upload: one entry in files[]. Multi-file upload: multiple entries. ' +
193
+ 'No restriction on file extensions — mcp packages may include .py, .js, package.json, etc.',
194
+ inputSchema: {
195
+ type: 'object' as const,
196
+ properties: {
197
+ resource_id: {
198
+ type: 'string',
199
+ description: 'Unique resource identifier used as commit label and for duplicate detection',
200
+ },
201
+ name: {
202
+ type: 'string',
203
+ description: 'Human-readable resource name sent to the CSP API (defaults to resource_id if omitted)',
204
+ },
205
+ type: {
206
+ type: 'string',
207
+ enum: ['command', 'skill', 'rule', 'mcp'],
208
+ description:
209
+ 'Resource category — MUST be confirmed with the user. ' +
210
+ 'command: single .md slash-command file; ' +
211
+ 'skill: directory with SKILL.md + supporting files; ' +
212
+ 'rule: single .mdc Cursor rule file; ' +
213
+ 'mcp: MCP server package — MUST include mcp-config.json (defines command/args/env for auto-registration into ~/.cursor/mcp.json).',
214
+ },
215
+ message: {
216
+ type: 'string',
217
+ description: 'Git commit message (5-200 characters)',
218
+ },
219
+ target_source: {
220
+ type: 'string',
221
+ description:
222
+ 'Target source repo name on the CSP server. ' +
223
+ 'Ask the user which repo to target. Typical values: "csp" (default), "client-sdk-ai-hub". ' +
224
+ 'The CSP server will commit the resource to the corresponding Git repo.',
225
+ },
226
+ files: {
227
+ type: 'array',
228
+ description:
229
+ 'List of files to upload. Read each file from the user\'s local machine and pass content here. ' +
230
+ 'path is the filename the resource should be stored as on the server. ' +
231
+ 'Examples (type="rule"): [{path: "csp-ai-agent.mdc", content: "..."}]. ' +
232
+ 'Examples (type="skill"): [{path: "SKILL.md", content: "..."}, {path: "examples.md", content: "..."}]. ' +
233
+ 'Examples (type="mcp"): [{path: "server.py", content: "..."}, {path: "mcp-config.json", content: "..."}]. ' +
234
+ 'No restriction on file extension.',
235
+ items: {
236
+ type: 'object',
237
+ properties: {
238
+ path: { type: 'string', description: 'Relative path under the type subdirectory' },
239
+ content: { type: 'string', description: 'Full text content of the file' },
240
+ },
241
+ required: ['path', 'content'],
242
+ },
243
+ },
244
+ team: {
245
+ type: 'string',
246
+ description: 'Team / group name (defaults to Client-Public)',
247
+ default: 'Client-Public',
248
+ },
249
+ force: {
250
+ type: 'boolean',
251
+ description: 'Overwrite if a resource with the same name already exists',
252
+ default: false,
253
+ },
254
+ },
255
+ required: ['resource_id', 'type', 'message', 'files'],
256
+ },
257
+ handler: uploadResource,
258
+ };