@elliotding/ai-agent-mcp 0.1.1 → 0.1.3

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.
@@ -21,12 +21,80 @@ import { MCPServerError, createValidationError } from '../types/errors';
21
21
  import type { UploadResourceParams, UploadResourceResult, ToolResult, FileEntry } from '../types/tools';
22
22
  import type { ResourceType } from '../types/resources';
23
23
 
24
+ type ResourceCategory = 'command' | 'skill' | 'rule' | 'mcp';
25
+
26
+ /**
27
+ * Infer the resource type from the uploaded file list ONLY when the user has
28
+ * not explicitly stated a type. If the user declared a type, that always wins.
29
+ *
30
+ * Auto-detection rules (in priority order, applied only when declaredType is absent):
31
+ * 1. Any file named "mcp-config.json" → mcp
32
+ * 2. Any file named "SKILL.md" → skill
33
+ * 3. Single file ending with ".mdc" → rule
34
+ * 4. Single file ending with ".md" → command
35
+ * 5. Cannot determine → throw validation error
36
+ */
37
+ function inferResourceType(
38
+ declaredType: ResourceCategory | undefined,
39
+ files: FileEntry[]
40
+ ): ResourceCategory {
41
+ // User explicitly specified the type — honour it unconditionally.
42
+ if (declaredType) return declaredType;
43
+
44
+ const names = files.map((f) => path.basename(f.path).toLowerCase());
45
+
46
+ if (names.includes('mcp-config.json')) return 'mcp';
47
+ if (names.includes('skill.md')) return 'skill';
48
+ if (files.length === 1) {
49
+ if (names[0]!.endsWith('.mdc')) return 'rule';
50
+ if (names[0]!.endsWith('.md')) return 'command';
51
+ }
52
+
53
+ throw createValidationError(
54
+ 'type',
55
+ 'required',
56
+ 'Cannot auto-detect the resource type from the provided files. ' +
57
+ 'Please specify "type" explicitly: "command" (single .md), "skill" (contains SKILL.md), ' +
58
+ '"rule" (single .mdc), or "mcp" (contains mcp-config.json).'
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Derive a human-readable resource name from the primary file in the upload list.
64
+ * The original filename (without extension) is used as-is — never renamed.
65
+ *
66
+ * - Single-file upload: strip extension from the filename.
67
+ * "code-review.md" → "code-review"
68
+ * "csp-agent.mdc" → "csp-agent"
69
+ * - Multi-file upload: use the top-level directory name when the first
70
+ * path contains a directory component.
71
+ * "code-review/SKILL.md" → "code-review"
72
+ * Falls back to the first file's base name otherwise.
73
+ *
74
+ * Returns undefined when the files array is empty (caller should error).
75
+ */
76
+ function deriveNameFromFiles(files: FileEntry[]): string | undefined {
77
+ if (!files || files.length === 0) return undefined;
78
+
79
+ const first = files[0]!.path;
80
+
81
+ // For paths like "code-review/SKILL.md", use the top-level directory.
82
+ const dir = path.dirname(first);
83
+ if (dir && dir !== '.') return dir;
84
+
85
+ // Strip extension from bare filename.
86
+ const base = path.basename(first, path.extname(first));
87
+ return base || undefined;
88
+ }
89
+
24
90
  /**
25
91
  * Validate and return the files[] array.
26
92
  * Each entry path must be a relative path with no traversal.
27
93
  * For MCP resources, mcp-config.json must be present.
94
+ *
95
+ * @param resolvedType The already-inferred resource type (never undefined here).
28
96
  */
29
- function collectFiles(typedParams: UploadResourceParams): FileEntry[] {
97
+ function collectFiles(typedParams: UploadResourceParams, resolvedType: string): FileEntry[] {
30
98
  if (!typedParams.files || typedParams.files.length === 0) {
31
99
  throw createValidationError(
32
100
  'files',
@@ -48,17 +116,34 @@ function collectFiles(typedParams: UploadResourceParams): FileEntry[] {
48
116
 
49
117
  // MCP resources must include mcp-config.json so the client can auto-register
50
118
  // the server in ~/.cursor/mcp.json after sync_resources installs it.
51
- if (typedParams.type === 'mcp') {
119
+ if (resolvedType === 'mcp') {
52
120
  const hasMcpConfig = typedParams.files.some(
53
121
  (f) => path.basename(f.path) === 'mcp-config.json'
54
122
  );
55
123
  if (!hasMcpConfig) {
124
+ // Look for other files that might already describe the server configuration
125
+ // (e.g. pyproject.toml, package.json, README.md, config.json, server.py).
126
+ // If found, surface a targeted hint so the user knows exactly what to create.
127
+ const configHints = typedParams.files
128
+ .map((f) => path.basename(f.path))
129
+ .filter((n) =>
130
+ /\.(toml|yaml|yml|json|cfg|ini|conf|py|js|ts|md)$/i.test(n) &&
131
+ n !== 'mcp-config.json'
132
+ );
133
+
134
+ const hintNote =
135
+ configHints.length > 0
136
+ ? `\nFound related files (${configHints.join(', ')}) that may already describe the server ` +
137
+ `configuration — please create "mcp-config.json" based on those files.`
138
+ : '';
139
+
56
140
  throw createValidationError(
57
141
  'files',
58
142
  'required',
59
143
  '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' +
144
+ 'This file tells the client how to start the MCP server after installation.' +
145
+ hintNote +
146
+ '\n\nRequired format:\n' +
62
147
  '{\n' +
63
148
  ' "name": "<server-name>",\n' +
64
149
  ' "command": "python3", // or "node", "uvx", etc.\n' +
@@ -79,11 +164,26 @@ export async function uploadResource(params: unknown): Promise<ToolResult<Upload
79
164
  logger.info({ tool: 'upload_resource', params }, 'upload_resource called');
80
165
 
81
166
  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;
167
+ const resourceId = typedParams.resource_id;
168
+ const userToken = typedParams.user_token;
169
+ const targetSource = typedParams.target_source ?? 'csp';
170
+ const force = (typedParams as any).force || false;
171
+
172
+ // User-declared type always wins; auto-detect only when omitted.
173
+ const resourceType = inferResourceType(typedParams.type, typedParams.files);
174
+
175
+ // Name: explicit user value > derived from filename (no extension).
176
+ // Never fall back to resource_id — that is an internal identifier, not a name.
177
+ const derivedName = typedParams.name ?? deriveNameFromFiles(typedParams.files);
178
+ if (!derivedName) {
179
+ throw createValidationError(
180
+ 'name',
181
+ 'required',
182
+ 'Could not determine a resource name from the provided files. ' +
183
+ 'Please provide a "name" field explicitly.'
184
+ );
185
+ }
186
+ const resourceName = derivedName;
87
187
 
88
188
  logger.info({ resourceId, resourceType, targetSource }, 'Upload target resolved');
89
189
 
@@ -124,25 +224,28 @@ export async function uploadResource(params: unknown): Promise<ToolResult<Upload
124
224
  }
125
225
 
126
226
  // ========== Step 4: Collect files ==========
127
- const fileEntries = collectFiles(typedParams);
227
+ const fileEntries = collectFiles(typedParams, resourceType);
128
228
  logger.info({ resourceId, fileCount: fileEntries.length }, 'Files collected for upload');
129
229
 
130
230
  // ========== Step 5: Call CSP API — upload (staging) ==========
131
231
  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
- });
232
+ const uploadResp = await apiClient.uploadResourceFiles(
233
+ {
234
+ type: resourceType,
235
+ name: resourceName,
236
+ files: fileEntries,
237
+ target_source: targetSource,
238
+ force,
239
+ },
240
+ userToken
241
+ );
139
242
 
140
243
  const uploadId = uploadResp.upload_id;
141
244
  logger.info({ uploadId, expiresAt: uploadResp.expires_at }, 'Upload staged successfully');
142
245
 
143
246
  // ========== Step 6: Call CSP API — finalize (Git commit) ==========
144
247
  logger.info({ uploadId }, 'Calling CSP finalize API...');
145
- const finalizeResp = await apiClient.finalizeResourceUpload(uploadId, typedParams.message);
248
+ const finalizeResp = await apiClient.finalizeResourceUpload(uploadId, typedParams.message, userToken);
146
249
 
147
250
  const finalResourceId = finalizeResp.resource_id;
148
251
  const version = finalizeResp.version ?? '1.0.0';
@@ -182,15 +285,31 @@ export const uploadResourceTool = {
182
285
  name: 'upload_resource',
183
286
  description:
184
287
  '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.',
288
+ 'The user selects files from their local machine — read each file and pass its content in files[]. ' +
289
+ 'ALWAYS confirm the target source repo with the user (e.g. "csp" (default) or "client-sdk-ai-hub"). ' +
290
+
291
+ '\n\nResource type rules:\n' +
292
+ ' If the user explicitly states the type, use it as-is no overriding.\n' +
293
+ ' If the user does NOT state a type, auto-detect from file structure:\n' +
294
+ ' - Contains mcp-config.json → type="mcp"\n' +
295
+ ' - Contains SKILL.md → type="skill"\n' +
296
+ ' - Single .mdc file type="rule"\n' +
297
+ ' - Single .md file → type="command"\n' +
298
+ ' • If the user says type="mcp" but mcp-config.json is missing, the tool will\n' +
299
+ ' return an error with a hint about creating mcp-config.json.\n' +
300
+
301
+ '\n\nResource name rules:\n' +
302
+ ' • If the user provides a name, use it.\n' +
303
+ ' • Otherwise derive the name from the filename WITHOUT its extension.\n' +
304
+ ' Keep the original filename — NEVER rename files (e.g. do not rename any .md file).\n' +
305
+ ' Examples: "code-review.md" → name="code-review"; "code-review/SKILL.md" → name="code-review".\n' +
306
+
307
+ '\n\nPass files[] — an array of {path, content} entries. ' +
308
+ 'path must be the original filename as-is (relative, no path traversal). ' +
309
+ 'No restriction on file extensions — mcp packages may include .py, .js, package.json, etc.\n' +
310
+
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.',
194
313
  inputSchema: {
195
314
  type: 'object' as const,
196
315
  properties: {
@@ -206,11 +325,11 @@ export const uploadResourceTool = {
206
325
  type: 'string',
207
326
  enum: ['command', 'skill', 'rule', 'mcp'],
208
327
  description:
209
- 'Resource category — MUST be confirmed with the user. ' +
328
+ 'Resource category. Auto-detected from file structure only set explicitly when detection is ambiguous. ' +
210
329
  'command: single .md slash-command file; ' +
211
- 'skill: directory with SKILL.md + supporting files; ' +
330
+ 'skill: directory or file set containing SKILL.md; ' +
212
331
  '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).',
332
+ 'mcp: MCP server package — MUST include mcp-config.json.',
214
333
  },
215
334
  message: {
216
335
  type: 'string',
@@ -251,8 +370,15 @@ export const uploadResourceTool = {
251
370
  description: 'Overwrite if a resource with the same name already exists',
252
371
  default: false,
253
372
  },
373
+ user_token: {
374
+ type: 'string',
375
+ 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.',
379
+ },
254
380
  },
255
- required: ['resource_id', 'type', 'message', 'files'],
381
+ required: ['resource_id', 'message', 'files'],
256
382
  },
257
383
  handler: uploadResource,
258
384
  };
@@ -35,6 +35,12 @@ export interface SyncResourcesParams {
35
35
  mode?: 'check' | 'incremental' | 'full';
36
36
  scope?: 'global' | 'workspace' | 'all';
37
37
  types?: string[];
38
+ /**
39
+ * CSP API token from the user's mcp.json env configuration.
40
+ * Overrides the server-level fallback token so that each user
41
+ * makes API calls with their own identity.
42
+ */
43
+ user_token?: string;
38
44
  }
39
45
 
40
46
  export interface McpSetupItem {
@@ -83,6 +89,8 @@ export interface ManageSubscriptionParams {
83
89
  auto_sync?: boolean;
84
90
  scope?: 'global' | 'workspace';
85
91
  notify?: boolean;
92
+ /** CSP API token from the user's mcp.json env configuration. */
93
+ user_token?: string;
86
94
  }
87
95
 
88
96
  export interface ManageSubscriptionResult {
@@ -106,6 +114,8 @@ export interface SearchResourcesParams {
106
114
  team?: string;
107
115
  type?: string;
108
116
  keyword: string;
117
+ /** CSP API token from the user's mcp.json env configuration. */
118
+ user_token?: string;
109
119
  }
110
120
 
111
121
  export interface SearchResourcesResult {
@@ -131,9 +141,10 @@ export interface FileEntry {
131
141
 
132
142
  export interface UploadResourceParams {
133
143
  resource_id: string;
134
- type: 'command' | 'skill' | 'rule' | 'mcp';
144
+ /** Resource category. Optional auto-detected from file structure when omitted. */
145
+ type?: 'command' | 'skill' | 'rule' | 'mcp';
135
146
  message: string;
136
- /** Human-readable resource name sent to the CSP API. Defaults to resource_id. */
147
+ /** Human-readable resource name sent to the CSP API. Defaults to the primary file name (without extension). */
137
148
  name?: string;
138
149
  /** Target source repo from ai-resources-config.json (e.g. "csp", "client-sdk-ai-hub"). Defaults to default_source. */
139
150
  target_source?: string;
@@ -147,6 +158,8 @@ export interface UploadResourceParams {
147
158
  // ---- Optional fields ----
148
159
  title?: string;
149
160
  metadata?: Record<string, unknown>;
161
+ /** CSP API token from the user's mcp.json env configuration. */
162
+ user_token?: string;
150
163
  }
151
164
 
152
165
  export interface UploadResourceResult {
@@ -161,6 +174,8 @@ export interface UploadResourceResult {
161
174
  export interface UninstallResourceParams {
162
175
  resource_id_or_name: string;
163
176
  remove_from_account?: boolean;
177
+ /** CSP API token from the user's mcp.json env configuration. */
178
+ user_token?: string;
164
179
  }
165
180
 
166
181
  export interface UninstallResourceResult {