@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.
package/src/api/client.ts CHANGED
@@ -23,11 +23,21 @@ class APIClient {
23
23
  },
24
24
  });
25
25
 
26
- // Request interceptor for authentication and logging
26
+ // Request interceptor for authentication and logging.
27
+ // Every request MUST carry a per-request Authorization header supplied by
28
+ // the caller via authConfig(userToken). If none is present the request is
29
+ // rejected immediately with a clear error so the caller knows they must
30
+ // configure their token in mcp.json under env.CSP_API_TOKEN.
27
31
  this.client.interceptors.request.use(
28
32
  (requestConfig) => {
29
- if (config.csp.apiToken) {
30
- requestConfig.headers.Authorization = `Bearer ${config.csp.apiToken}`;
33
+ if (!requestConfig.headers.Authorization) {
34
+ return Promise.reject(
35
+ new Error(
36
+ 'CSP_API_TOKEN is not configured. ' +
37
+ 'Please add your personal CSP token to your mcp.json under ' +
38
+ 'mcpServers["csp-ai-agent"].env.CSP_API_TOKEN and restart Cursor.'
39
+ )
40
+ );
31
41
  }
32
42
 
33
43
  // Enhanced request logging
@@ -115,6 +125,27 @@ class APIClient {
115
125
  );
116
126
  }
117
127
 
128
+ /**
129
+ * Build an AxiosRequestConfig that carries a per-request user token.
130
+ * Pass the result as the `config` argument to get/post/put/delete or merge it
131
+ * into any existing request config so that the caller's token overrides the
132
+ * server-level fallback set in the interceptor.
133
+ *
134
+ * Usage:
135
+ * await apiClient.get('/some/path', apiClient.authConfig(userToken));
136
+ * await apiClient.post('/some/path', body, apiClient.authConfig(userToken));
137
+ */
138
+ authConfig(token: string | undefined, extra?: AxiosRequestConfig): AxiosRequestConfig {
139
+ if (!token) return extra ?? {};
140
+ return {
141
+ ...extra,
142
+ headers: {
143
+ ...(extra?.headers ?? {}),
144
+ Authorization: `Bearer ${token}`,
145
+ },
146
+ };
147
+ }
148
+
118
149
  /**
119
150
  * Sanitize headers to hide sensitive information
120
151
  */
@@ -241,12 +272,19 @@ class APIClient {
241
272
 
242
273
  /**
243
274
  * Get subscription list
275
+ *
276
+ * @param params Query parameters for filtering subscriptions.
277
+ * @param userToken Per-request token from the caller's mcp.json configuration.
278
+ * When provided it overrides the server-level fallback token.
244
279
  */
245
- async getSubscriptions(params?: {
246
- scope?: 'general' | 'team' | 'user' | 'all';
247
- types?: string[];
248
- detail?: boolean; // Added: include detailed metadata
249
- }): Promise<{
280
+ async getSubscriptions(
281
+ params?: {
282
+ scope?: 'general' | 'team' | 'user' | 'all';
283
+ types?: string[];
284
+ detail?: boolean;
285
+ },
286
+ userToken?: string
287
+ ): Promise<{
250
288
  total: number;
251
289
  subscriptions: Array<{
252
290
  id: string;
@@ -281,27 +319,29 @@ class APIClient {
281
319
  };
282
320
  }>;
283
321
  };
284
- }>('/csp/api/resources/subscriptions', { params });
285
-
286
- // Extract data from CSP API response format
322
+ }>('/csp/api/resources/subscriptions', this.authConfig(userToken, { params }));
323
+
287
324
  if (!response.data) {
288
325
  throw new Error('Invalid API response: missing data field');
289
326
  }
290
-
327
+
291
328
  return response.data;
292
329
  }
293
330
 
294
331
  /**
295
332
  * Subscribe to resource
333
+ *
334
+ * @param userToken Per-request token from the caller's mcp.json configuration.
296
335
  */
297
336
  async subscribe(
298
- resourceIds: string[],
337
+ resourceIds: string[],
299
338
  autoSync = true,
300
- scope?: 'general' | 'team' | 'user' // Added: subscription scope
339
+ scope?: 'general' | 'team' | 'user',
340
+ userToken?: string
301
341
  ): Promise<{
302
342
  success: boolean;
303
- subscriptions: Array<{
304
- id: string;
343
+ subscriptions: Array<{
344
+ id: string;
305
345
  name: string;
306
346
  type: string;
307
347
  subscribed_at: string;
@@ -312,44 +352,39 @@ class APIClient {
312
352
  result: string;
313
353
  data: {
314
354
  success?: boolean;
315
- subscriptions: Array<{
316
- id: string;
355
+ subscriptions: Array<{
356
+ id: string;
317
357
  name: string;
318
358
  type: string;
319
359
  subscribed_at: string;
320
360
  }>;
321
361
  };
322
- }>('/csp/api/resources/subscriptions/add', {
323
- resource_ids: resourceIds,
324
- auto_sync: autoSync,
325
- scope,
326
- });
327
-
362
+ }>(
363
+ '/csp/api/resources/subscriptions/add',
364
+ { resource_ids: resourceIds, auto_sync: autoSync, scope },
365
+ this.authConfig(userToken)
366
+ );
367
+
328
368
  if (!response.data) {
329
369
  throw new Error('Invalid API response: missing data field');
330
370
  }
331
-
332
- return {
333
- success: true,
334
- subscriptions: response.data.subscriptions
335
- };
371
+
372
+ return { success: true, subscriptions: response.data.subscriptions };
336
373
  }
337
374
 
338
375
  /**
339
376
  * Unsubscribe from resource
377
+ *
378
+ * @param userToken Per-request token from the caller's mcp.json configuration.
340
379
  */
341
- async unsubscribe(resourceIds: string | string[]): Promise<void> {
342
- // Support batch unsubscribe
380
+ async unsubscribe(resourceIds: string | string[], userToken?: string): Promise<void> {
343
381
  const ids = Array.isArray(resourceIds) ? resourceIds : [resourceIds];
344
382
  const response = await this.delete<{
345
383
  code: number;
346
384
  result: string;
347
385
  data: { removed_count: number };
348
- }>('/csp/api/resources/subscriptions/remove', {
349
- data: { resource_ids: ids }
350
- });
351
-
352
- // Just validate response, no need to return anything
386
+ }>('/csp/api/resources/subscriptions/remove', this.authConfig(userToken, { data: { resource_ids: ids } }));
387
+
353
388
  if (!response.data) {
354
389
  throw new Error('Invalid API response: missing data field');
355
390
  }
@@ -357,15 +392,20 @@ class APIClient {
357
392
 
358
393
  /**
359
394
  * Search resources
395
+ *
396
+ * @param userToken Per-request token from the caller's mcp.json configuration.
360
397
  */
361
- async searchResources(params: {
362
- keyword: string;
363
- team?: string;
364
- type?: string;
365
- detail?: boolean; // Added: include detailed metadata
366
- page?: number; // Added: pagination
367
- page_size?: number; // Added: page size
368
- }): Promise<{
398
+ async searchResources(
399
+ params: {
400
+ keyword: string;
401
+ team?: string;
402
+ type?: string;
403
+ detail?: boolean;
404
+ page?: number;
405
+ page_size?: number;
406
+ },
407
+ userToken?: string
408
+ ): Promise<{
369
409
  total: number;
370
410
  page?: number;
371
411
  page_size?: number;
@@ -415,22 +455,21 @@ class APIClient {
415
455
  };
416
456
  }>;
417
457
  };
418
- }>('/csp/api/resources/search', { params });
419
-
420
- // Extract data from CSP API response format
458
+ }>('/csp/api/resources/search', this.authConfig(userToken, { params }));
459
+
421
460
  if (!response.data) {
422
461
  throw new Error('Invalid API response: missing data field');
423
462
  }
424
-
463
+
425
464
  return {
426
465
  total: response.data.total,
427
466
  page: response.data.page,
428
467
  page_size: response.data.page_size,
429
- results: response.data.results.map(r => ({
468
+ results: response.data.results.map((r) => ({
430
469
  ...r,
431
470
  score: r.score || 0,
432
- is_subscribed: r.is_subscribed || false
433
- }))
471
+ is_subscribed: r.is_subscribed || false,
472
+ })),
434
473
  };
435
474
  }
436
475
 
@@ -443,8 +482,13 @@ class APIClient {
443
482
  * files[].path is the relative path within the resource directory.
444
483
  * Single-file resources (command, rule) have exactly one element.
445
484
  * Multi-file resources (skill, mcp) have all their files included.
485
+ *
486
+ * @param userToken Per-request token from the caller's mcp.json configuration.
446
487
  */
447
- async downloadResource(resourceId: string): Promise<{
488
+ async downloadResource(
489
+ resourceId: string,
490
+ userToken?: string
491
+ ): Promise<{
448
492
  resource_id: string;
449
493
  name: string;
450
494
  type: string;
@@ -463,14 +507,19 @@ class APIClient {
463
507
  hash: string;
464
508
  files: Array<{ path: string; content: string }>;
465
509
  };
466
- }>(`/csp/api/resources/download/${resourceId}`);
510
+ }>(`/csp/api/resources/download/${resourceId}`, this.authConfig(userToken));
467
511
  return response.data;
468
512
  }
469
513
 
470
514
  /**
471
515
  * Get resource detail
516
+ *
517
+ * @param userToken Per-request token from the caller's mcp.json configuration.
472
518
  */
473
- async getResourceDetail(resourceId: string): Promise<{
519
+ async getResourceDetail(
520
+ resourceId: string,
521
+ userToken?: string
522
+ ): Promise<{
474
523
  id: string;
475
524
  name: string;
476
525
  type: string;
@@ -489,7 +538,7 @@ class APIClient {
489
538
  };
490
539
  download_url: string;
491
540
  }> {
492
- return this.get(`/csp/api/resources/${resourceId}`);
541
+ return this.get(`/csp/api/resources/${resourceId}`, this.authConfig(userToken));
493
542
  }
494
543
 
495
544
  /**
@@ -501,22 +550,29 @@ class APIClient {
501
550
  *
502
551
  * The server validates path traversal, total size (< 10 MB), and name conflicts.
503
552
  * All file types are supported — mcp packages may include .py, .js, package.json, etc.
553
+ *
554
+ * @param userToken Per-request token from the caller's mcp.json configuration.
504
555
  */
505
- async uploadResourceFiles(params: {
506
- type: string;
507
- name: string;
508
- files: Array<{ path: string; content: string }>;
509
- target_source?: string;
510
- force?: boolean;
511
- }): Promise<{
556
+ async uploadResourceFiles(
557
+ params: {
558
+ type: string;
559
+ name: string;
560
+ files: Array<{ path: string; content: string }>;
561
+ target_source?: string;
562
+ force?: boolean;
563
+ },
564
+ userToken?: string
565
+ ): Promise<{
512
566
  upload_id: string;
513
567
  status: string;
514
568
  expires_at: string;
515
569
  preview_url?: string;
516
570
  }> {
517
- const resp = await this.post<{ code: number; result: string; data: { upload_id: string; status: string; expires_at: string; preview_url?: string } }>(
518
- '/csp/api/resources/upload', params
519
- );
571
+ const resp = await this.post<{
572
+ code: number;
573
+ result: string;
574
+ data: { upload_id: string; status: string; expires_at: string; preview_url?: string };
575
+ }>('/csp/api/resources/upload', params, this.authConfig(userToken));
520
576
  return resp.data;
521
577
  }
522
578
 
@@ -526,19 +582,34 @@ class APIClient {
526
582
  * POST /csp/api/resources/finalize
527
583
  * Body: { upload_id, commit_message }
528
584
  * Response: { resource_id, version, url, commit_hash, download_url }
585
+ *
586
+ * @param userToken Per-request token from the caller's mcp.json configuration.
529
587
  */
530
- async finalizeResourceUpload(uploadId: string, commitMessage: string): Promise<{
588
+ async finalizeResourceUpload(
589
+ uploadId: string,
590
+ commitMessage: string,
591
+ userToken?: string
592
+ ): Promise<{
531
593
  resource_id: string;
532
594
  version?: string;
533
595
  url?: string;
534
596
  commit_hash?: string;
535
597
  download_url?: string;
536
598
  }> {
537
- const resp = await this.post<{ code: number; result: string; data: { resource_id: string; version?: string; url?: string; commit_hash?: string; download_url?: string } }>(
538
- '/csp/api/resources/finalize', {
539
- upload_id: uploadId,
540
- commit_message: commitMessage,
541
- }
599
+ const resp = await this.post<{
600
+ code: number;
601
+ result: string;
602
+ data: {
603
+ resource_id: string;
604
+ version?: string;
605
+ url?: string;
606
+ commit_hash?: string;
607
+ download_url?: string;
608
+ };
609
+ }>(
610
+ '/csp/api/resources/finalize',
611
+ { upload_id: uploadId, commit_message: commitMessage },
612
+ this.authConfig(userToken)
542
613
  );
543
614
  return resp.data;
544
615
  }
@@ -37,7 +37,9 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
37
37
  // Subscribe to resources
38
38
  const subResult = await apiClient.subscribe(
39
39
  typedParams.resource_ids,
40
- typedParams.auto_sync
40
+ typedParams.auto_sync,
41
+ undefined,
42
+ typedParams.user_token
41
43
  );
42
44
 
43
45
  logger.info({ count: subResult.subscriptions.length }, 'Resources subscribed successfully');
@@ -50,7 +52,11 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
50
52
 
51
53
  if (shouldAutoSync && subResult.subscriptions.length > 0) {
52
54
  logger.info({ resourceIds: typedParams.resource_ids }, 'Auto-syncing newly subscribed resources...');
53
- const syncResult = await syncResources({ mode: 'incremental', scope: typedParams.scope || 'global' });
55
+ const syncResult = await syncResources({
56
+ mode: 'incremental',
57
+ scope: typedParams.scope || 'global',
58
+ user_token: typedParams.user_token,
59
+ });
54
60
  if (syncResult.success && syncResult.data) {
55
61
  const sd = syncResult.data;
56
62
  syncSummary = `Auto-sync: ${sd.summary.synced} synced, ${sd.summary.cached} cached, ${sd.summary.failed} failed`;
@@ -97,7 +103,7 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
97
103
  logger.debug({ resourceIds: typedParams.resource_ids }, 'Unsubscribing from resources...');
98
104
 
99
105
  // Cancel server-side subscription
100
- await apiClient.unsubscribe(typedParams.resource_ids);
106
+ await apiClient.unsubscribe(typedParams.resource_ids, typedParams.user_token);
101
107
  logger.info({ count: typedParams.resource_ids.length }, 'Server-side subscriptions removed');
102
108
 
103
109
  // Uninstall local files and MCP config for each resource
@@ -156,7 +162,7 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
156
162
  logger.debug({ scope: typedParams.scope || 'all' }, 'Listing subscriptions...');
157
163
 
158
164
  // Get subscriptions list
159
- const subs = await apiClient.getSubscriptions({});
165
+ const subs = await apiClient.getSubscriptions({}, typedParams.user_token);
160
166
 
161
167
  result = {
162
168
  action: 'list',
@@ -188,7 +194,9 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
188
194
 
189
195
  const batchSubResult = await apiClient.subscribe(
190
196
  typedParams.resource_ids,
191
- typedParams.auto_sync
197
+ typedParams.auto_sync,
198
+ undefined,
199
+ typedParams.user_token
192
200
  );
193
201
 
194
202
  logger.info({ count: batchSubResult.subscriptions.length }, 'Batch subscription completed');
@@ -201,7 +209,11 @@ export async function manageSubscription(params: unknown): Promise<ToolResult<Ma
201
209
 
202
210
  if (shouldBatchAutoSync && batchSubResult.subscriptions.length > 0) {
203
211
  logger.info({ count: batchSubResult.subscriptions.length }, 'Auto-syncing batch subscribed resources...');
204
- const batchSyncResult = await syncResources({ mode: 'incremental', scope: typedParams.scope || 'global' });
212
+ const batchSyncResult = await syncResources({
213
+ mode: 'incremental',
214
+ scope: typedParams.scope || 'global',
215
+ user_token: typedParams.user_token,
216
+ });
205
217
  if (batchSyncResult.success && batchSyncResult.data) {
206
218
  const sd = batchSyncResult.data;
207
219
  batchSyncSummary = `Auto-sync: ${sd.summary.synced} synced, ${sd.summary.cached} cached, ${sd.summary.failed} failed`;
@@ -325,6 +337,13 @@ export const manageSubscriptionTool = {
325
337
  description: 'Enable update notifications',
326
338
  default: true,
327
339
  },
340
+ user_token: {
341
+ type: 'string',
342
+ description:
343
+ 'CSP API token for the current user. Read this from the CSP_API_TOKEN environment ' +
344
+ 'variable configured in the user\'s mcp.json. When provided, this token is used ' +
345
+ 'for all CSP API calls in this request instead of the server-level fallback token.',
346
+ },
328
347
  },
329
348
  required: ['action'],
330
349
  },
@@ -84,11 +84,14 @@ export async function searchResources(params: unknown): Promise<ToolResult<Searc
84
84
  // Search via API
85
85
  logger.debug({ team: typedParams.team, type: typedParams.type, keyword: typedParams.keyword }, 'Searching resources...');
86
86
 
87
- const searchResults = await apiClient.searchResources({
88
- team: typedParams.team,
89
- type: typedParams.type,
90
- keyword: typedParams.keyword,
91
- });
87
+ const searchResults = await apiClient.searchResources(
88
+ {
89
+ team: typedParams.team,
90
+ type: typedParams.type,
91
+ keyword: typedParams.keyword,
92
+ },
93
+ typedParams.user_token
94
+ );
92
95
 
93
96
  // Check subscription and installation status for each result
94
97
  const enhancedResults = await Promise.all(
@@ -170,6 +173,13 @@ export const searchResourcesTool = {
170
173
  type: 'string',
171
174
  description: 'Search keyword (searches in name, description, tags)',
172
175
  },
176
+ user_token: {
177
+ type: 'string',
178
+ description:
179
+ 'CSP API token for the current user. Read this from the CSP_API_TOKEN environment ' +
180
+ 'variable configured in the user\'s mcp.json. When provided, this token is used ' +
181
+ 'for all CSP API calls in this request instead of the server-level fallback token.',
182
+ },
173
183
  },
174
184
  required: ['keyword'],
175
185
  },
@@ -303,9 +303,10 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
303
303
  logToolStep('sync_resources', 'Tool invocation started', { params: typedParams });
304
304
 
305
305
  try {
306
- const mode = typedParams.mode || 'incremental';
307
- const scope = typedParams.scope || 'global';
308
- const types = typedParams.types;
306
+ const mode = typedParams.mode || 'incremental';
307
+ const scope = typedParams.scope || 'global';
308
+ const types = typedParams.types;
309
+ const userToken = typedParams.user_token;
309
310
 
310
311
  logToolStep('sync_resources', 'Parameters validated', { mode, scope, types });
311
312
 
@@ -313,7 +314,7 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
313
314
  logToolStep('sync_resources', 'Step 1: Fetching subscriptions from API', { scope, types });
314
315
  const t1 = Date.now();
315
316
 
316
- const subscriptions = await apiClient.getSubscriptions({ types });
317
+ const subscriptions = await apiClient.getSubscriptions({ types }, userToken);
317
318
 
318
319
  logToolStep('sync_resources', 'Subscriptions fetched', {
319
320
  total: subscriptions.total,
@@ -405,7 +406,7 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
405
406
  resourceType: sub.type,
406
407
  });
407
408
  const tDl = Date.now();
408
- const downloadResult = await apiClient.downloadResource(sub.id);
409
+ const downloadResult = await apiClient.downloadResource(sub.id, userToken);
409
410
  logToolStep('sync_resources', 'Download complete', {
410
411
  resourceId: sub.id,
411
412
  fileCount: downloadResult.files.length,
@@ -656,6 +657,13 @@ export const syncResourcesTool = {
656
657
  type: 'array',
657
658
  description: 'Filter by resource types (empty = all types)',
658
659
  },
660
+ user_token: {
661
+ type: 'string',
662
+ 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.',
666
+ },
659
667
  },
660
668
  },
661
669
  handler: syncResources,