@catchmexz/fedin-cms-mcp 0.1.1

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 (2) hide show
  1. package/build/index.js +1464 -0
  2. package/package.json +45 -0
package/build/index.js ADDED
@@ -0,0 +1,1464 @@
1
+ #!/usr/bin/env node
2
+ process.on('unhandledRejection', (reason, promise) => {
3
+ console.error('[FATAL] Unhandled Rejection at:', promise, 'reason:', reason);
4
+ });
5
+ process.on('uncaughtException', (error) => {
6
+ console.error('[FATAL] Uncaught Exception:', error);
7
+ process.exit(1); // Mandatory exit after uncaught exception
8
+ });
9
+ /**
10
+ * Strapi MCP Server
11
+ *
12
+ * This MCP server integrates with any Strapi CMS instance to provide:
13
+ * - Access to Strapi content types as resources
14
+ * - Tools to create and update content types in Strapi
15
+ * - Tools to manage content entries (create, read, update, delete)
16
+ * - Support for Strapi in development mode
17
+ *
18
+ * This server is designed to be generic and work with any Strapi instance,
19
+ * regardless of the content types defined in that instance.
20
+ */
21
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
24
+ import axios from "axios";
25
+ import dotenv from 'dotenv';
26
+ // Load environment variables from .env file
27
+ dotenv.config();
28
+ // Extended error codes to include additional ones we need
29
+ var ExtendedErrorCode;
30
+ (function (ExtendedErrorCode) {
31
+ // Original error codes from SDK
32
+ ExtendedErrorCode["InvalidRequest"] = "InvalidRequest";
33
+ ExtendedErrorCode["MethodNotFound"] = "MethodNotFound";
34
+ ExtendedErrorCode["InvalidParams"] = "InvalidParams";
35
+ ExtendedErrorCode["InternalError"] = "InternalError";
36
+ // Additional error codes
37
+ ExtendedErrorCode["ResourceNotFound"] = "ResourceNotFound";
38
+ ExtendedErrorCode["AccessDenied"] = "AccessDenied";
39
+ })(ExtendedErrorCode || (ExtendedErrorCode = {}));
40
+ // Custom error class extending McpError to support our extended error codes
41
+ class ExtendedMcpError extends McpError {
42
+ extendedCode;
43
+ constructor(code, message) {
44
+ // Map our extended codes to standard MCP error codes when needed
45
+ let mcpCode;
46
+ // Map custom error codes to standard MCP error codes
47
+ switch (code) {
48
+ case ExtendedErrorCode.ResourceNotFound:
49
+ case ExtendedErrorCode.AccessDenied:
50
+ // Map custom codes to InternalError for SDK compatibility
51
+ mcpCode = ErrorCode.InternalError;
52
+ break;
53
+ case ExtendedErrorCode.InvalidRequest:
54
+ mcpCode = ErrorCode.InvalidRequest;
55
+ break;
56
+ case ExtendedErrorCode.MethodNotFound:
57
+ mcpCode = ErrorCode.MethodNotFound;
58
+ break;
59
+ case ExtendedErrorCode.InvalidParams:
60
+ mcpCode = ErrorCode.InvalidParams;
61
+ break;
62
+ case ExtendedErrorCode.InternalError:
63
+ default:
64
+ mcpCode = ErrorCode.InternalError;
65
+ break;
66
+ }
67
+ // Call super before accessing 'this'
68
+ super(mcpCode, message);
69
+ // Store the extended code for reference
70
+ this.extendedCode = code;
71
+ }
72
+ }
73
+ // Configuration from environment variables
74
+ const STRAPI_URL = process.env.STRAPI_URL || "http://localhost:1337";
75
+ const STRAPI_API_TOKEN = process.env.STRAPI_API_TOKEN;
76
+ const STRAPI_DEV_MODE = process.env.STRAPI_DEV_MODE === "true";
77
+ const STRAPI_ADMIN_EMAIL = process.env.STRAPI_ADMIN_EMAIL;
78
+ const STRAPI_ADMIN_PASSWORD = process.env.STRAPI_ADMIN_PASSWORD;
79
+ // Supabase Edge Function URL for content type management
80
+ const SUPABASE_FUNCTION_URL = process.env.SUPABASE_FUNCTION_URL || "https://database.fedin.cn/functions/v1/cms_content-type-manager";
81
+ const SUPABASE_CONTENT_FUNCTION_URL = process.env.SUPABASE_CONTENT_FUNCTION_URL || "https://database.fedin.cn/functions/v1/cms_content-manager";
82
+ const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || "";
83
+ const WEBSITE_ID = process.env.WEBSITE_ID || "";
84
+ const USER_ID = process.env.USER_ID || "";
85
+ // Validate required environment variables for Fedin CMS
86
+ if (!WEBSITE_ID) {
87
+ console.error("[Error] Missing required environment variable: WEBSITE_ID");
88
+ console.error("[Error] Please set WEBSITE_ID in your environment variables");
89
+ }
90
+ // Axios instance for Strapi API
91
+ const strapiClient = axios.create({
92
+ baseURL: STRAPI_URL,
93
+ headers: {
94
+ "Content-Type": "application/json",
95
+ },
96
+ validateStatus: function (status) {
97
+ // Consider only 5xx as errors - for more robust error handling
98
+ return status < 500;
99
+ }
100
+ });
101
+ // Axios instance for Supabase Edge Function (content types)
102
+ const supabaseClient = axios.create({
103
+ baseURL: SUPABASE_FUNCTION_URL,
104
+ headers: {
105
+ "Content-Type": "application/json",
106
+ "apikey": SUPABASE_ANON_KEY,
107
+ },
108
+ validateStatus: function (status) {
109
+ return status < 500;
110
+ }
111
+ });
112
+ // Axios instance for Supabase Edge Function (content entries)
113
+ const supabaseContentClient = axios.create({
114
+ baseURL: SUPABASE_CONTENT_FUNCTION_URL,
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ "apikey": SUPABASE_ANON_KEY,
118
+ },
119
+ validateStatus: function (status) {
120
+ return status < 500;
121
+ }
122
+ });
123
+ // Store admin JWT token if we log in
124
+ let adminJwtToken = null;
125
+ /**
126
+ * Log in to the Strapi admin API using provided credentials
127
+ */
128
+ async function loginToStrapiAdmin() {
129
+ // Use process.env directly here to ensure latest values are used
130
+ const email = process.env.STRAPI_ADMIN_EMAIL;
131
+ const password = process.env.STRAPI_ADMIN_PASSWORD;
132
+ if (!email || !password) {
133
+ console.error("[Auth] No admin credentials found in process.env, skipping admin login");
134
+ return false;
135
+ }
136
+ try {
137
+ // Log the authentication attempt with more detail
138
+ console.error(`[Auth] Attempting login to Strapi admin at ${STRAPI_URL}/admin/login as ${email}`);
139
+ console.error(`[Auth] Full URL being used: ${STRAPI_URL}/admin/login`);
140
+ // Make the request with more detailed logging
141
+ console.error(`[Auth] Sending POST request with email and password`);
142
+ const response = await axios.post(`${STRAPI_URL}/admin/login`, {
143
+ email,
144
+ password
145
+ });
146
+ console.error(`[Auth] Response status: ${response.status}`);
147
+ console.error(`[Auth] Response headers:`, JSON.stringify(response.headers));
148
+ // Check if we got back valid data
149
+ if (response.data && response.data.data && response.data.data.token) {
150
+ adminJwtToken = response.data.data.token;
151
+ console.error("[Auth] Successfully logged in to Strapi admin");
152
+ console.error(`[Auth] Token received (first 20 chars): ${adminJwtToken?.substring(0, 20)}...`);
153
+ return true;
154
+ }
155
+ else {
156
+ console.error("[Auth] Login response missing token");
157
+ console.error(`[Auth] Response data:`, JSON.stringify(response.data));
158
+ return false;
159
+ }
160
+ }
161
+ catch (error) {
162
+ console.error("[Auth] Failed to log in to Strapi admin:");
163
+ if (axios.isAxiosError(error)) {
164
+ console.error(`[Auth] Status: ${error.response?.status}`);
165
+ console.error(`[Auth] Response data:`, error.response?.data);
166
+ console.error(`[Auth] Request URL: ${error.config?.url}`);
167
+ console.error(`[Auth] Request method: ${error.config?.method}`);
168
+ }
169
+ else {
170
+ console.error(error);
171
+ }
172
+ return false;
173
+ }
174
+ }
175
+ /**
176
+ * Make a request to the admin API using the admin JWT token
177
+ */
178
+ async function makeAdminApiRequest(endpoint, method = 'get', data, params) {
179
+ if (!adminJwtToken) {
180
+ // Try to log in first
181
+ console.error(`[Admin API] No token available, attempting login...`);
182
+ const success = await loginToStrapiAdmin();
183
+ if (!success) {
184
+ console.error(`[Admin API] Login failed. Cannot authenticate for admin API access.`);
185
+ throw new Error("Not authenticated for admin API access");
186
+ }
187
+ console.error(`[Admin API] Login successful, proceeding with request.`);
188
+ }
189
+ const fullUrl = `${STRAPI_URL}${endpoint}`;
190
+ console.error(`[Admin API] Making ${method.toUpperCase()} request to: ${fullUrl}`);
191
+ if (data) {
192
+ console.error(`[Admin API] Request payload: ${JSON.stringify(data, null, 2)}`);
193
+ }
194
+ try {
195
+ console.error(`[Admin API] Sending request with Authorization header using token: ${adminJwtToken?.substring(0, 20)}...`);
196
+ const response = await axios({
197
+ method,
198
+ url: fullUrl,
199
+ headers: {
200
+ 'Authorization': `Bearer ${adminJwtToken}`,
201
+ 'Content-Type': 'application/json'
202
+ },
203
+ data, // Used for POST, PUT, etc.
204
+ params // Used for GET requests query parameters
205
+ });
206
+ console.error(`[Admin API] Response status: ${response.status}`);
207
+ if (response.data) {
208
+ console.error(`[Admin API] Response received successfully`);
209
+ }
210
+ return response.data;
211
+ }
212
+ catch (error) {
213
+ console.error(`[Admin API] Request to ${endpoint} failed:`);
214
+ if (axios.isAxiosError(error)) {
215
+ console.error(`[Admin API] Status: ${error.response?.status}`);
216
+ console.error(`[Admin API] Error data: ${JSON.stringify(error.response?.data)}`);
217
+ console.error(`[Admin API] Error headers: ${JSON.stringify(error.response?.headers)}`);
218
+ // Check if it's an auth error (e.g., token expired)
219
+ if (error.response?.status === 401 && adminJwtToken) {
220
+ console.error("[Admin API] Admin token might be expired. Attempting re-login...");
221
+ adminJwtToken = null; // Clear expired token
222
+ const loginSuccess = await loginToStrapiAdmin();
223
+ if (loginSuccess) {
224
+ console.error("[Admin API] Re-login successful. Retrying original request...");
225
+ // Retry the request once after successful re-login
226
+ try {
227
+ const retryResponse = await axios({
228
+ method,
229
+ url: fullUrl,
230
+ headers: {
231
+ 'Authorization': `Bearer ${adminJwtToken}`,
232
+ 'Content-Type': 'application/json'
233
+ },
234
+ data,
235
+ params
236
+ });
237
+ console.error(`[Admin API] Retry successful, status: ${retryResponse.status}`);
238
+ return retryResponse.data;
239
+ }
240
+ catch (retryError) {
241
+ console.error(`[Admin API] Retry failed:`, retryError);
242
+ throw retryError;
243
+ }
244
+ }
245
+ else {
246
+ console.error("[Admin API] Re-login failed. Throwing original error.");
247
+ throw new Error("Admin re-authentication failed after token expiry.");
248
+ }
249
+ }
250
+ }
251
+ else {
252
+ console.error(`[Admin API] Non-Axios error:`, error);
253
+ }
254
+ // If not a 401 or re-login failed, throw the original error
255
+ throw error;
256
+ }
257
+ }
258
+ // Cache for content types
259
+ let contentTypesCache = [];
260
+ /**
261
+ * Create an MCP server with capabilities for resources and tools
262
+ */
263
+ const server = new Server({
264
+ name: "strapi-mcp",
265
+ version: "0.2.0",
266
+ }, {
267
+ capabilities: {
268
+ resources: {},
269
+ tools: {},
270
+ },
271
+ });
272
+ /**
273
+ * Fetch all content types from Fedin CMS (Supabase)
274
+ */
275
+ async function fetchContentTypes() {
276
+ try {
277
+ console.error("[API] Fetching content types from Fedin CMS");
278
+ if (!WEBSITE_ID) {
279
+ throw new Error("WEBSITE_ID environment variable is required to fetch content types");
280
+ }
281
+ const response = await supabaseClient.get('/content-types', {
282
+ params: { websiteId: WEBSITE_ID }
283
+ });
284
+ if (response.data && response.data.data && Array.isArray(response.data.data)) {
285
+ // Transform the response to match expected format
286
+ const contentTypes = response.data.data.map((ct) => ({
287
+ uid: ct.uid,
288
+ apiID: ct.uid,
289
+ info: {
290
+ displayName: ct.title || ct.uid,
291
+ description: ct.description || `${ct.uid} content type`,
292
+ },
293
+ attributes: ct.fields || {}
294
+ }));
295
+ contentTypesCache = contentTypes;
296
+ console.error(`[API] Successfully fetched ${contentTypes.length} content types`);
297
+ return contentTypes;
298
+ }
299
+ throw new Error("Invalid response format from Supabase Edge Function");
300
+ }
301
+ catch (error) {
302
+ console.error("[Error] Failed to fetch content types:", error);
303
+ let errorMessage = "Failed to fetch content types";
304
+ let errorCode = ExtendedErrorCode.InternalError;
305
+ if (axios.isAxiosError(error)) {
306
+ errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
307
+ if (error.response?.status === 400) {
308
+ errorCode = ExtendedErrorCode.InvalidParams;
309
+ errorMessage += ` (Bad Request - websiteId parameter is required)`;
310
+ }
311
+ else if (error.response?.status === 404) {
312
+ errorCode = ExtendedErrorCode.ResourceNotFound;
313
+ errorMessage += ` (Website not found)`;
314
+ }
315
+ else if (error.response?.status === 403) {
316
+ errorCode = ExtendedErrorCode.AccessDenied;
317
+ errorMessage += ` (Permission denied)`;
318
+ }
319
+ else if (error.response?.status === 401) {
320
+ errorCode = ExtendedErrorCode.AccessDenied;
321
+ errorMessage += ` (Unauthorized)`;
322
+ }
323
+ }
324
+ else if (error instanceof Error) {
325
+ errorMessage += `: ${error.message}`;
326
+ }
327
+ else {
328
+ errorMessage += `: ${String(error)}`;
329
+ }
330
+ throw new ExtendedMcpError(errorCode, errorMessage);
331
+ }
332
+ }
333
+ /**
334
+ * Fetch entries for a specific content type from Fedin CMS (Supabase)
335
+ */
336
+ async function fetchEntries(uid, queryParams) {
337
+ try {
338
+ console.error(`[API] Fetching entries for content type: ${uid}`);
339
+ if (!WEBSITE_ID) {
340
+ throw new Error("WEBSITE_ID environment variable is required to fetch entries");
341
+ }
342
+ // 构建查询参数
343
+ const params = {
344
+ websiteId: WEBSITE_ID,
345
+ uid: uid
346
+ };
347
+ // 支持分页
348
+ if (queryParams?.pagination) {
349
+ params.page = queryParams.pagination.page || 1;
350
+ params.pageSize = queryParams.pagination.pageSize || 20;
351
+ }
352
+ else {
353
+ params.page = 1;
354
+ params.pageSize = 20;
355
+ }
356
+ // 支持状态过滤
357
+ if (queryParams?.filters?.status) {
358
+ params.status = queryParams.filters.status;
359
+ }
360
+ // 支持排序
361
+ if (queryParams?.sort && queryParams.sort.length > 0) {
362
+ const sortStr = queryParams.sort[0];
363
+ if (sortStr.includes(':')) {
364
+ const [sortBy, sortOrder] = sortStr.split(':');
365
+ params.sortBy = sortBy;
366
+ params.sortOrder = sortOrder || 'desc';
367
+ }
368
+ }
369
+ const response = await supabaseContentClient.get('/contents', { params });
370
+ if (response.data && response.data.data) {
371
+ console.error(`[API] Successfully fetched ${response.data.data.length} entries`);
372
+ return {
373
+ data: response.data.data,
374
+ meta: response.data.pagination || {}
375
+ };
376
+ }
377
+ throw new Error("Invalid response format from Supabase Edge Function");
378
+ }
379
+ catch (error) {
380
+ console.error("[Error] Failed to fetch entries:", error);
381
+ let errorMessage = "Failed to fetch entries";
382
+ let errorCode = ExtendedErrorCode.InternalError;
383
+ if (axios.isAxiosError(error)) {
384
+ errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
385
+ if (error.response?.status === 400) {
386
+ errorCode = ExtendedErrorCode.InvalidParams;
387
+ errorMessage += ` (Bad Request - websiteId parameter is required)`;
388
+ }
389
+ else if (error.response?.status === 404) {
390
+ errorCode = ExtendedErrorCode.ResourceNotFound;
391
+ errorMessage += ` (Content type or website not found)`;
392
+ }
393
+ else if (error.response?.status === 403) {
394
+ errorCode = ExtendedErrorCode.AccessDenied;
395
+ errorMessage += ` (Permission denied)`;
396
+ }
397
+ else if (error.response?.status === 401) {
398
+ errorCode = ExtendedErrorCode.AccessDenied;
399
+ errorMessage += ` (Unauthorized)`;
400
+ }
401
+ }
402
+ else if (error instanceof Error) {
403
+ errorMessage += `: ${error.message}`;
404
+ }
405
+ else {
406
+ errorMessage += `: ${String(error)}`;
407
+ }
408
+ throw new ExtendedMcpError(errorCode, errorMessage);
409
+ }
410
+ }
411
+ /**
412
+ * Fetch a specific entry by ID from Fedin CMS (Supabase)
413
+ */
414
+ async function fetchEntry(uid, id, queryParams) {
415
+ try {
416
+ console.error(`[API] Fetching entry ${id} for content type: ${uid}`);
417
+ if (!WEBSITE_ID) {
418
+ throw new Error("WEBSITE_ID environment variable is required to fetch entry");
419
+ }
420
+ const response = await supabaseContentClient.get(`/contents/${id}`, {
421
+ params: { websiteId: WEBSITE_ID }
422
+ });
423
+ if (response.data && response.data.data) {
424
+ console.error(`[API] Successfully fetched entry ${id}`);
425
+ return response.data.data;
426
+ }
427
+ throw new Error("Invalid response format from Supabase Edge Function");
428
+ }
429
+ catch (error) {
430
+ console.error(`[Error] Failed to fetch entry ${id} for ${uid}:`, error);
431
+ let errorMessage = `Failed to fetch entry ${id} for ${uid}`;
432
+ let errorCode = ExtendedErrorCode.InternalError;
433
+ if (axios.isAxiosError(error)) {
434
+ errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
435
+ if (error.response?.status === 400) {
436
+ errorCode = ExtendedErrorCode.InvalidParams;
437
+ errorMessage += ` (Bad Request - websiteId parameter is required)`;
438
+ }
439
+ else if (error.response?.status === 404) {
440
+ errorCode = ExtendedErrorCode.ResourceNotFound;
441
+ errorMessage += ` (Entry not found)`;
442
+ }
443
+ else if (error.response?.status === 403) {
444
+ errorCode = ExtendedErrorCode.AccessDenied;
445
+ errorMessage += ` (Permission denied)`;
446
+ }
447
+ else if (error.response?.status === 401) {
448
+ errorCode = ExtendedErrorCode.AccessDenied;
449
+ errorMessage += ` (Unauthorized)`;
450
+ }
451
+ }
452
+ else if (error instanceof Error) {
453
+ errorMessage += `: ${error.message}`;
454
+ }
455
+ else {
456
+ errorMessage += `: ${String(error)}`;
457
+ }
458
+ throw new ExtendedMcpError(errorCode, errorMessage);
459
+ }
460
+ }
461
+ /**
462
+ * Create a new entry in Fedin CMS (Supabase)
463
+ */
464
+ async function createEntry(uid, data) {
465
+ try {
466
+ console.error(`[API] Creating new entry for content type: ${uid}`);
467
+ if (!WEBSITE_ID) {
468
+ throw new Error("WEBSITE_ID environment variable is required to create entry");
469
+ }
470
+ const payload = {
471
+ uid,
472
+ data,
473
+ status: data.status || 'published',
474
+ user_id: USER_ID || null,
475
+ is_ai_generated: data.is_ai_generated || false
476
+ };
477
+ const response = await supabaseContentClient.post('/contents', payload, {
478
+ params: { websiteId: WEBSITE_ID }
479
+ });
480
+ if (response.data && response.data.data) {
481
+ console.error(`[API] Successfully created entry`);
482
+ return response.data.data;
483
+ }
484
+ throw new Error("Invalid response format from Supabase Edge Function");
485
+ }
486
+ catch (error) {
487
+ console.error(`[Error] Failed to create entry for ${uid}:`, error);
488
+ let errorMessage = `Failed to create entry for ${uid}`;
489
+ let errorCode = ExtendedErrorCode.InternalError;
490
+ if (axios.isAxiosError(error)) {
491
+ errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
492
+ if (error.response?.status === 400) {
493
+ errorCode = ExtendedErrorCode.InvalidParams;
494
+ errorMessage += ` (Bad Request): ${JSON.stringify(error.response?.data)}`;
495
+ }
496
+ else if (error.response?.status === 404) {
497
+ errorCode = ExtendedErrorCode.ResourceNotFound;
498
+ errorMessage += ` (Website not found)`;
499
+ }
500
+ else if (error.response?.status === 403 || error.response?.status === 401) {
501
+ errorCode = ExtendedErrorCode.AccessDenied;
502
+ errorMessage += ` (Permission Denied)`;
503
+ }
504
+ }
505
+ else if (error instanceof Error) {
506
+ errorMessage += `: ${error.message}`;
507
+ }
508
+ else {
509
+ errorMessage += `: ${String(error)}`;
510
+ }
511
+ throw new ExtendedMcpError(errorCode, errorMessage);
512
+ }
513
+ }
514
+ /**
515
+ * Update an existing entry in Fedin CMS (Supabase)
516
+ */
517
+ async function updateEntry(uid, id, data) {
518
+ try {
519
+ console.error(`[API] Updating entry ${id} for content type: ${uid}`);
520
+ if (!WEBSITE_ID) {
521
+ throw new Error("WEBSITE_ID environment variable is required to update entry");
522
+ }
523
+ const payload = {
524
+ data: data.data !== undefined ? data.data : data,
525
+ };
526
+ // 支持更新其他字段
527
+ if (data.status !== undefined) {
528
+ payload.status = data.status;
529
+ }
530
+ if (USER_ID !== undefined) {
531
+ payload.user_id = USER_ID;
532
+ }
533
+ if (data.is_ai_generated !== undefined) {
534
+ payload.is_ai_generated = data.is_ai_generated;
535
+ }
536
+ if (data.uid !== undefined) {
537
+ payload.uid = data.uid;
538
+ }
539
+ const response = await supabaseContentClient.put(`/contents/${id}`, payload, {
540
+ params: { websiteId: WEBSITE_ID }
541
+ });
542
+ if (response.data && response.data.data) {
543
+ console.error(`[API] Successfully updated entry ${id}`);
544
+ return response.data.data;
545
+ }
546
+ throw new Error("Invalid response format from Supabase Edge Function");
547
+ }
548
+ catch (error) {
549
+ console.error(`[Error] Failed to update entry ${id} for ${uid}:`, error);
550
+ let errorMessage = `Failed to update entry ${id} for ${uid}`;
551
+ let errorCode = ExtendedErrorCode.InternalError;
552
+ if (axios.isAxiosError(error)) {
553
+ errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
554
+ if (error.response?.status === 400) {
555
+ errorCode = ExtendedErrorCode.InvalidParams;
556
+ errorMessage += ` (Bad Request): ${JSON.stringify(error.response?.data)}`;
557
+ }
558
+ else if (error.response?.status === 404) {
559
+ errorCode = ExtendedErrorCode.ResourceNotFound;
560
+ errorMessage += ` (Entry or website not found)`;
561
+ }
562
+ else if (error.response?.status === 403 || error.response?.status === 401) {
563
+ errorCode = ExtendedErrorCode.AccessDenied;
564
+ errorMessage += ` (Permission Denied)`;
565
+ }
566
+ }
567
+ else if (error instanceof Error) {
568
+ errorMessage += `: ${error.message}`;
569
+ }
570
+ else {
571
+ errorMessage += `: ${String(error)}`;
572
+ }
573
+ throw new ExtendedMcpError(errorCode, errorMessage);
574
+ }
575
+ }
576
+ /**
577
+ * Delete an entry from Fedin CMS (Supabase)
578
+ */
579
+ async function deleteEntry(uid, id) {
580
+ try {
581
+ console.error(`[API] Deleting entry ${id} for content type: ${uid}`);
582
+ if (!WEBSITE_ID) {
583
+ throw new Error("WEBSITE_ID environment variable is required to delete entry");
584
+ }
585
+ const response = await supabaseContentClient.delete(`/contents/${id}`, {
586
+ params: { websiteId: WEBSITE_ID }
587
+ });
588
+ if (response.status >= 200 && response.status < 300) {
589
+ console.error(`[API] Successfully deleted entry ${id}`);
590
+ return;
591
+ }
592
+ throw new Error(`Unexpected response status: ${response.status}`);
593
+ }
594
+ catch (error) {
595
+ console.error(`[Error] Failed to delete entry ${id} for ${uid}:`, error);
596
+ let errorMessage = `Failed to delete entry ${id} for ${uid}`;
597
+ let errorCode = ExtendedErrorCode.InternalError;
598
+ if (axios.isAxiosError(error)) {
599
+ errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
600
+ if (error.response?.status === 400) {
601
+ errorCode = ExtendedErrorCode.InvalidParams;
602
+ errorMessage += ` (Bad Request - websiteId parameter is required)`;
603
+ }
604
+ else if (error.response?.status === 404) {
605
+ errorCode = ExtendedErrorCode.ResourceNotFound;
606
+ errorMessage += ` (Entry or website not found)`;
607
+ }
608
+ else if (error.response?.status === 403 || error.response?.status === 401) {
609
+ errorCode = ExtendedErrorCode.AccessDenied;
610
+ errorMessage += ` (Permission Denied)`;
611
+ }
612
+ }
613
+ else if (error instanceof Error) {
614
+ errorMessage += `: ${error.message}`;
615
+ }
616
+ else {
617
+ errorMessage += `: ${String(error)}`;
618
+ }
619
+ throw new ExtendedMcpError(errorCode, errorMessage);
620
+ }
621
+ }
622
+ /**
623
+ * Upload media file to Strapi
624
+ */
625
+ /**
626
+ * Fetch the schema for a specific content type from Fedin CMS (Supabase)
627
+ */
628
+ async function fetchContentTypeSchema(contentType) {
629
+ try {
630
+ console.error(`[API] Fetching schema for content type: ${contentType}`);
631
+ if (!WEBSITE_ID) {
632
+ throw new Error("WEBSITE_ID environment variable is required to fetch content type schema");
633
+ }
634
+ const response = await supabaseClient.get(`/content-types/${contentType}`, {
635
+ params: { websiteId: WEBSITE_ID }
636
+ });
637
+ if (response.data && response.data.data) {
638
+ const schemaData = response.data.data;
639
+ console.error("[API] Successfully fetched schema via Supabase Edge Function");
640
+ // Transform to match expected format
641
+ return {
642
+ uid: schemaData.uid || contentType,
643
+ apiID: schemaData.uid || contentType,
644
+ info: {
645
+ displayName: schemaData.title || schemaData.uid || contentType,
646
+ description: schemaData.description || `${contentType} content type`,
647
+ },
648
+ attributes: schemaData.fields || {}
649
+ };
650
+ }
651
+ throw new Error("Invalid response format from Supabase Edge Function");
652
+ }
653
+ catch (error) {
654
+ let errorMessage = `Failed to fetch schema for ${contentType}`;
655
+ let errorCode = ExtendedErrorCode.InternalError;
656
+ if (axios.isAxiosError(error)) {
657
+ errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
658
+ if (error.response?.status === 400) {
659
+ errorCode = ExtendedErrorCode.InvalidParams;
660
+ errorMessage += ` (Bad Request - websiteId parameter is required)`;
661
+ }
662
+ else if (error.response?.status === 404) {
663
+ errorCode = ExtendedErrorCode.ResourceNotFound;
664
+ errorMessage += ` (Content type or website not found)`;
665
+ }
666
+ else if (error.response?.status === 403) {
667
+ errorCode = ExtendedErrorCode.AccessDenied;
668
+ errorMessage += ` (Permission denied)`;
669
+ }
670
+ else if (error.response?.status === 401) {
671
+ errorCode = ExtendedErrorCode.AccessDenied;
672
+ errorMessage += ` (Unauthorized)`;
673
+ }
674
+ }
675
+ else if (error instanceof Error) {
676
+ errorMessage += `: ${error.message}`;
677
+ }
678
+ else {
679
+ errorMessage += `: ${String(error)}`;
680
+ }
681
+ console.error(`[Error] ${errorMessage}`);
682
+ throw new ExtendedMcpError(errorCode, errorMessage);
683
+ }
684
+ }
685
+ /**
686
+ * Update an existing content type in Fedin CMS (Supabase)
687
+ */
688
+ /**
689
+ * Update a content type in Fedin CMS (Supabase)
690
+ * Only title and description can be updated. Fields are fixed and cannot be changed.
691
+ */
692
+ async function updateContentType(contentTypeUid, updateData) {
693
+ try {
694
+ console.error(`[API] Updating content type: ${contentTypeUid}`);
695
+ if (!contentTypeUid) {
696
+ throw new Error("Missing required field: contentTypeUid");
697
+ }
698
+ if (!WEBSITE_ID) {
699
+ throw new Error("WEBSITE_ID environment variable is required to update content type");
700
+ }
701
+ // Construct the payload - only title and description can be updated
702
+ // Fields are fixed and cannot be changed
703
+ const payload = {};
704
+ if (updateData.title !== undefined) {
705
+ payload.title = updateData.title;
706
+ }
707
+ if (updateData.description !== undefined) {
708
+ payload.description = updateData.description;
709
+ }
710
+ console.error(`[API] Update Payload for PUT /content-types/${contentTypeUid}: ${JSON.stringify(payload, null, 2)}`);
711
+ const response = await supabaseClient.put(`/content-types/${contentTypeUid}`, payload, {
712
+ params: { websiteId: WEBSITE_ID }
713
+ });
714
+ console.error(`[API] Content type update response:`, response.data);
715
+ if (response.data && response.data.data) {
716
+ return response.data.data;
717
+ }
718
+ return { message: `Content type ${contentTypeUid} updated successfully` };
719
+ }
720
+ catch (error) {
721
+ console.error(`[Error] Failed to update content type ${contentTypeUid}:`, error);
722
+ let errorMessage = `Failed to update content type ${contentTypeUid}`;
723
+ let errorCode = ExtendedErrorCode.InternalError;
724
+ if (axios.isAxiosError(error)) {
725
+ errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
726
+ const responseData = JSON.stringify(error.response?.data);
727
+ if (error.response?.status === 400) {
728
+ errorCode = ExtendedErrorCode.InvalidParams;
729
+ errorMessage += ` (Bad Request): ${responseData}`;
730
+ }
731
+ else if (error.response?.status === 404) {
732
+ errorCode = ExtendedErrorCode.ResourceNotFound;
733
+ errorMessage += ` (Content Type or Website Not Found)`;
734
+ }
735
+ else if (error.response?.status === 403 || error.response?.status === 401) {
736
+ errorCode = ExtendedErrorCode.AccessDenied;
737
+ errorMessage += ` (Permission Denied)`;
738
+ }
739
+ else {
740
+ errorMessage += `: ${responseData}`;
741
+ }
742
+ }
743
+ else if (error instanceof Error) {
744
+ errorMessage += `: ${error.message}`;
745
+ }
746
+ else {
747
+ errorMessage += `: ${String(error)}`;
748
+ }
749
+ throw new ExtendedMcpError(errorCode, errorMessage);
750
+ }
751
+ }
752
+ /**
753
+ * Create a new content type in Fedin CMS (Supabase)
754
+ * Fields are fixed and handled by the backend, so we don't pass them
755
+ */
756
+ async function createContentType(contentTypeData) {
757
+ try {
758
+ const { uid, title, description = "" } = contentTypeData;
759
+ if (!uid || !title) {
760
+ throw new Error("Missing required fields: uid, title");
761
+ }
762
+ if (!WEBSITE_ID) {
763
+ throw new Error("WEBSITE_ID environment variable is required to create content type");
764
+ }
765
+ // Construct the payload for Supabase Edge Function
766
+ // Fields are fixed and handled by the backend, so we don't pass them
767
+ const payload = {
768
+ uid,
769
+ title,
770
+ description
771
+ };
772
+ console.error(`[API] Creating new content type: ${title}`);
773
+ console.error(`[API] Attempting to create content type with payload: ${JSON.stringify(payload, null, 2)}`);
774
+ const response = await supabaseClient.post('/content-types', payload, {
775
+ params: { websiteId: WEBSITE_ID }
776
+ });
777
+ console.error(`[API] Content type creation response:`, response.data);
778
+ if (response.data && response.data.data) {
779
+ return response.data.data;
780
+ }
781
+ return { message: "Content type created successfully", uid };
782
+ }
783
+ catch (error) {
784
+ console.error(`[Error] Failed to create content type:`, error);
785
+ let errorMessage = `Failed to create content type`;
786
+ let errorCode = ExtendedErrorCode.InternalError;
787
+ if (axios.isAxiosError(error)) {
788
+ errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
789
+ if (error.response?.status === 400) {
790
+ errorCode = ExtendedErrorCode.InvalidParams;
791
+ errorMessage += ` (Bad Request): ${JSON.stringify(error.response?.data)}`;
792
+ }
793
+ else if (error.response?.status === 409) {
794
+ errorCode = ExtendedErrorCode.InvalidParams;
795
+ errorMessage += ` (Conflict - Content type already exists)`;
796
+ }
797
+ else if (error.response?.status === 404) {
798
+ errorCode = ExtendedErrorCode.ResourceNotFound;
799
+ errorMessage += ` (Website not found)`;
800
+ }
801
+ else if (error.response?.status === 403 || error.response?.status === 401) {
802
+ errorCode = ExtendedErrorCode.AccessDenied;
803
+ errorMessage += ` (Permission Denied)`;
804
+ }
805
+ }
806
+ else if (error instanceof Error) {
807
+ errorMessage += `: ${error.message}`;
808
+ }
809
+ else {
810
+ errorMessage += `: ${String(error)}`;
811
+ }
812
+ throw new ExtendedMcpError(errorCode, errorMessage);
813
+ }
814
+ }
815
+ /**
816
+ * Handler for listing available Strapi content as resources.
817
+ * Each content type and entry is exposed as a resource with:
818
+ * - A strapi:// URI scheme
819
+ * - JSON MIME type
820
+ * - Human readable name and description
821
+ */
822
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
823
+ try {
824
+ // Fetch all content types
825
+ const contentTypes = await fetchContentTypes();
826
+ // Create a resource for each content type
827
+ const contentTypeResources = contentTypes.map(ct => ({
828
+ uri: `strapi://content-type/${ct.uid}`,
829
+ mimeType: "application/json",
830
+ name: ct.info.displayName,
831
+ description: `Strapi content type: ${ct.info.displayName}`
832
+ }));
833
+ // Return the resources
834
+ return {
835
+ resources: contentTypeResources
836
+ };
837
+ }
838
+ catch (error) {
839
+ console.error("[Error] Failed to list resources:", error);
840
+ throw new McpError(ErrorCode.InternalError, `Failed to list resources: ${error instanceof Error ? error.message : String(error)}`);
841
+ }
842
+ });
843
+ /**
844
+ * Handler for reading the contents of a specific resource.
845
+ * Takes a strapi:// URI and returns the content as JSON.
846
+ *
847
+ * Supports URIs in the following formats:
848
+ * - strapi://content-type/[contentTypeUid] - Get all entries for a content type
849
+ * - strapi://content-type/[contentTypeUid]/[entryId] - Get a specific entry
850
+ * - strapi://content-type/[contentTypeUid]?[queryParams] - Get filtered entries
851
+ */
852
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
853
+ try {
854
+ const uri = request.params.uri;
855
+ // Parse the URI for content type
856
+ const contentTypeMatch = uri.match(/^strapi:\/\/content-type\/([^\/\?]+)(?:\/([^\/\?]+))?(?:\?(.+))?$/);
857
+ if (!contentTypeMatch) {
858
+ throw new McpError(ErrorCode.InvalidRequest, `Invalid URI format: ${uri}`);
859
+ }
860
+ const contentTypeUid = contentTypeMatch[1];
861
+ const entryId = contentTypeMatch[2];
862
+ const queryString = contentTypeMatch[3];
863
+ // Parse query parameters if present
864
+ let queryParams = {};
865
+ if (queryString) {
866
+ try {
867
+ // Parse the query string into an object
868
+ const parsedParams = new URLSearchParams(queryString);
869
+ // Extract filters
870
+ const filtersParam = parsedParams.get('filters');
871
+ if (filtersParam) {
872
+ queryParams.filters = JSON.parse(filtersParam);
873
+ }
874
+ // Extract pagination
875
+ const pageParam = parsedParams.get('page');
876
+ const pageSizeParam = parsedParams.get('pageSize');
877
+ if (pageParam || pageSizeParam) {
878
+ queryParams.pagination = {};
879
+ if (pageParam)
880
+ queryParams.pagination.page = parseInt(pageParam, 10);
881
+ if (pageSizeParam)
882
+ queryParams.pagination.pageSize = parseInt(pageSizeParam, 10);
883
+ }
884
+ // Extract sort
885
+ const sortParam = parsedParams.get('sort');
886
+ if (sortParam) {
887
+ queryParams.sort = sortParam.split(',');
888
+ }
889
+ // Extract populate
890
+ const populateParam = parsedParams.get('populate');
891
+ if (populateParam) {
892
+ try {
893
+ // Try to parse as JSON
894
+ queryParams.populate = JSON.parse(populateParam);
895
+ }
896
+ catch {
897
+ // If not valid JSON, treat as comma-separated string
898
+ queryParams.populate = populateParam.split(',');
899
+ }
900
+ }
901
+ // Extract fields
902
+ const fieldsParam = parsedParams.get('fields');
903
+ if (fieldsParam) {
904
+ queryParams.fields = fieldsParam.split(',');
905
+ }
906
+ }
907
+ catch (parseError) {
908
+ console.error("[Error] Failed to parse query parameters:", parseError);
909
+ throw new McpError(ErrorCode.InvalidRequest, `Invalid query parameters: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
910
+ }
911
+ }
912
+ // If an entry ID is provided, fetch that specific entry
913
+ if (entryId) {
914
+ const entry = await fetchEntry(contentTypeUid, entryId, queryParams);
915
+ return {
916
+ contents: [{
917
+ uri: request.params.uri,
918
+ mimeType: "application/json",
919
+ text: JSON.stringify(entry, null, 2)
920
+ }]
921
+ };
922
+ }
923
+ // Otherwise, fetch entries with query parameters
924
+ const entries = await fetchEntries(contentTypeUid, queryParams);
925
+ // Return the entries as JSON
926
+ return {
927
+ contents: [{
928
+ uri: request.params.uri,
929
+ mimeType: "application/json",
930
+ text: JSON.stringify(entries, null, 2)
931
+ }]
932
+ };
933
+ }
934
+ catch (error) {
935
+ console.error("[Error] Failed to read resource:", error);
936
+ throw new McpError(ErrorCode.InternalError, `Failed to read resource: ${error instanceof Error ? error.message : String(error)}`);
937
+ }
938
+ });
939
+ /**
940
+ * Handler that lists available tools.
941
+ * Exposes tools for working with Strapi content.
942
+ */
943
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
944
+ return {
945
+ tools: [
946
+ {
947
+ name: "list_content_types",
948
+ description: "List all available content types in Fedin CMS",
949
+ inputSchema: {
950
+ type: "object",
951
+ properties: {}
952
+ }
953
+ },
954
+ {
955
+ name: "get_entries",
956
+ description: "Get entries for a specific content type",
957
+ inputSchema: {
958
+ type: "object",
959
+ properties: {
960
+ uid: {
961
+ type: "string",
962
+ description: "The content type UID (e.g., 'blog-post')"
963
+ },
964
+ options: {
965
+ type: "string",
966
+ description: "JSON string with query options including filters, pagination, sort. Example: '{\"filters\":{\"status\":\"published\"},\"pagination\":{\"page\":1,\"pageSize\":10},\"sort\":[\"updated_at:desc\"]}'"
967
+ }
968
+ },
969
+ required: ["uid"]
970
+ }
971
+ },
972
+ {
973
+ name: "get_entry",
974
+ description: "Get a specific entry by ID",
975
+ inputSchema: {
976
+ type: "object",
977
+ properties: {
978
+ uid: {
979
+ type: "string",
980
+ description: "The content type UID (e.g., 'blog-post')"
981
+ },
982
+ id: {
983
+ type: "string",
984
+ description: "The ID of the entry"
985
+ }
986
+ },
987
+ required: ["uid", "id"]
988
+ }
989
+ },
990
+ {
991
+ name: "create_entry",
992
+ description: "Create a new entry for a content type",
993
+ inputSchema: {
994
+ type: "object",
995
+ properties: {
996
+ uid: {
997
+ type: "string",
998
+ description: "The content type UID (e.g., 'blog-post')"
999
+ },
1000
+ data: {
1001
+ type: "object",
1002
+ description: "The data for the new entry"
1003
+ },
1004
+ status: {
1005
+ type: "string",
1006
+ enum: ["draft", "published"],
1007
+ description: "Entry status (default: 'published')"
1008
+ }
1009
+ },
1010
+ required: ["uid", "data"]
1011
+ }
1012
+ },
1013
+ {
1014
+ name: "update_entry",
1015
+ description: "Update an existing entry",
1016
+ inputSchema: {
1017
+ type: "object",
1018
+ properties: {
1019
+ uid: {
1020
+ type: "string",
1021
+ description: "The content type UID (e.g., 'blog')"
1022
+ },
1023
+ id: {
1024
+ type: "string",
1025
+ description: "The ID of the entry to update"
1026
+ },
1027
+ data: {
1028
+ type: "object",
1029
+ description: "The updated data for the entry"
1030
+ },
1031
+ status: {
1032
+ type: "string",
1033
+ enum: ["draft", "published"],
1034
+ description: "Entry status (optional)"
1035
+ }
1036
+ },
1037
+ required: ["uid", "id", "data"]
1038
+ }
1039
+ },
1040
+ {
1041
+ name: "delete_entry",
1042
+ description: "Deletes a specific entry.",
1043
+ inputSchema: {
1044
+ type: "object",
1045
+ properties: {
1046
+ uid: {
1047
+ type: "string",
1048
+ description: "Content type UID (e.g., 'blog-post')",
1049
+ },
1050
+ id: {
1051
+ type: "string",
1052
+ description: "Entry ID.",
1053
+ },
1054
+ },
1055
+ required: ["uid", "id"]
1056
+ }
1057
+ },
1058
+ {
1059
+ name: "get_content_type_schema",
1060
+ description: "Get the schema for a specific content type.",
1061
+ inputSchema: {
1062
+ type: "object",
1063
+ properties: {
1064
+ contentType: {
1065
+ type: "string",
1066
+ description: "The UID of the content type (e.g., 'blog').",
1067
+ },
1068
+ },
1069
+ required: ["contentType"]
1070
+ }
1071
+ },
1072
+ {
1073
+ name: "create_content_type",
1074
+ description: "Creates a new content type in Fedin CMS. Fields are fixed and cannot be customized.",
1075
+ inputSchema: {
1076
+ type: "object",
1077
+ properties: {
1078
+ uid: { type: "string", description: "Unique identifier for the content type (e.g., 'blog')." },
1079
+ title: { type: "string", description: "Display title for content type." },
1080
+ description: { type: "string", description: "Optional description." }
1081
+ },
1082
+ required: ["uid", "title"]
1083
+ }
1084
+ },
1085
+ {
1086
+ name: "update_content_type",
1087
+ description: "Updates a content type in Fedin CMS. Only title and description can be updated. Fields are fixed and cannot be changed.",
1088
+ inputSchema: {
1089
+ type: "object",
1090
+ properties: {
1091
+ contentType: { type: "string", description: "UID of content type to update." },
1092
+ title: { type: "string", description: "Updated display title for content type." },
1093
+ description: { type: "string", description: "Updated description." }
1094
+ },
1095
+ required: ["contentType"]
1096
+ }
1097
+ },
1098
+ {
1099
+ name: "delete_content_type",
1100
+ description: "Deletes a content type from Fedin CMS.",
1101
+ inputSchema: {
1102
+ type: "object",
1103
+ properties: {
1104
+ contentType: { type: "string", description: "UID of content type to delete (e.g., 'blog-post')." }
1105
+ },
1106
+ required: ["contentType"]
1107
+ }
1108
+ },
1109
+ ]
1110
+ };
1111
+ });
1112
+ /**
1113
+ * Handler for tool calls.
1114
+ * Implements various tools for working with Strapi content.
1115
+ */
1116
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1117
+ try {
1118
+ switch (request.params.name) {
1119
+ case "list_content_types": {
1120
+ const contentTypes = await fetchContentTypes();
1121
+ return {
1122
+ content: [{
1123
+ type: "text",
1124
+ text: JSON.stringify(contentTypes.map(ct => ({
1125
+ uid: ct.uid,
1126
+ displayName: ct.info.displayName,
1127
+ description: ct.info.description
1128
+ })), null, 2)
1129
+ }]
1130
+ };
1131
+ }
1132
+ case "get_entries": {
1133
+ const { uid, options } = request.params.arguments;
1134
+ if (!uid) {
1135
+ throw new McpError(ErrorCode.InvalidParams, "uid is required");
1136
+ }
1137
+ // Parse the options string into a queryParams object
1138
+ let queryParams = {};
1139
+ if (options) {
1140
+ try {
1141
+ queryParams = JSON.parse(options);
1142
+ }
1143
+ catch (parseError) {
1144
+ console.error("[Error] Failed to parse query options:", parseError);
1145
+ throw new McpError(ErrorCode.InvalidParams, `Invalid query options: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
1146
+ }
1147
+ }
1148
+ // Fetch entries with query parameters
1149
+ const entries = await fetchEntries(String(uid), queryParams);
1150
+ return {
1151
+ content: [{
1152
+ type: "text",
1153
+ text: JSON.stringify(entries, null, 2)
1154
+ }]
1155
+ };
1156
+ }
1157
+ case "get_entry": {
1158
+ const { uid, id } = request.params.arguments;
1159
+ if (!uid || !id) {
1160
+ throw new McpError(ErrorCode.InvalidParams, "uid and id are required");
1161
+ }
1162
+ const entry = await fetchEntry(String(uid), String(id));
1163
+ return {
1164
+ content: [{
1165
+ type: "text",
1166
+ text: JSON.stringify(entry, null, 2)
1167
+ }]
1168
+ };
1169
+ }
1170
+ case "create_entry": {
1171
+ const uid = String(request.params.arguments?.uid);
1172
+ const data = request.params.arguments?.data;
1173
+ const status = request.params.arguments?.status;
1174
+ if (!uid || !data) {
1175
+ throw new McpError(ErrorCode.InvalidParams, "uid and data are required");
1176
+ }
1177
+ const entryData = {
1178
+ ...data,
1179
+ status: status || 'published'
1180
+ };
1181
+ const entry = await createEntry(uid, entryData);
1182
+ return {
1183
+ content: [{
1184
+ type: "text",
1185
+ text: JSON.stringify(entry, null, 2)
1186
+ }]
1187
+ };
1188
+ }
1189
+ case "update_entry": {
1190
+ const uid = String(request.params.arguments?.uid);
1191
+ const id = String(request.params.arguments?.id);
1192
+ const data = request.params.arguments?.data;
1193
+ const status = request.params.arguments?.status;
1194
+ if (!uid || !id || !data) {
1195
+ throw new McpError(ErrorCode.InvalidParams, "uid, id, and data are required");
1196
+ }
1197
+ const updateData = {
1198
+ data: data
1199
+ };
1200
+ if (status !== undefined) {
1201
+ updateData.status = status;
1202
+ }
1203
+ const entry = await updateEntry(uid, id, updateData);
1204
+ if (entry) {
1205
+ return {
1206
+ content: [{
1207
+ type: "text",
1208
+ text: JSON.stringify(entry, null, 2)
1209
+ }]
1210
+ };
1211
+ }
1212
+ else {
1213
+ console.warn(`[API] Update for ${uid} ${id} completed, but no updated entry data was returned by the API.`);
1214
+ return {
1215
+ content: [{
1216
+ type: "text",
1217
+ text: `Successfully updated entry ${id} for ${uid}, but no updated data was returned by the API.`
1218
+ }]
1219
+ };
1220
+ }
1221
+ }
1222
+ case "delete_entry": {
1223
+ const uid = String(request.params.arguments?.uid);
1224
+ const id = String(request.params.arguments?.id);
1225
+ if (!uid || !id) {
1226
+ throw new McpError(ErrorCode.InvalidParams, "uid and id are required");
1227
+ }
1228
+ await deleteEntry(uid, id);
1229
+ return {
1230
+ content: [{
1231
+ type: "text",
1232
+ text: `Successfully deleted entry ${id} from ${uid}`
1233
+ }]
1234
+ };
1235
+ }
1236
+ case "get_content_type_schema": {
1237
+ const contentType = String(request.params.arguments?.contentType);
1238
+ if (!contentType) {
1239
+ throw new McpError(ErrorCode.InvalidParams, "Content type is required");
1240
+ }
1241
+ const schema = await fetchContentTypeSchema(contentType);
1242
+ return {
1243
+ content: [{
1244
+ type: "text",
1245
+ text: JSON.stringify(schema, null, 2)
1246
+ }]
1247
+ };
1248
+ }
1249
+ case "create_content_type": {
1250
+ const contentTypeData = request.params.arguments;
1251
+ if (!contentTypeData || typeof contentTypeData !== 'object') {
1252
+ throw new McpError(ErrorCode.InvalidParams, "Content type data object is required.");
1253
+ }
1254
+ const creationResult = await createContentType(contentTypeData);
1255
+ return {
1256
+ content: [{
1257
+ type: "text",
1258
+ text: JSON.stringify(creationResult, null, 2)
1259
+ }]
1260
+ };
1261
+ }
1262
+ case "update_content_type": {
1263
+ const { contentType, title, description } = request.params.arguments;
1264
+ if (!contentType) {
1265
+ throw new McpError(ErrorCode.InvalidParams, "contentType (string) is required.");
1266
+ }
1267
+ const updateData = {};
1268
+ if (title !== undefined)
1269
+ updateData.title = String(title);
1270
+ if (description !== undefined)
1271
+ updateData.description = String(description);
1272
+ const updateResult = await updateContentType(String(contentType), updateData);
1273
+ return {
1274
+ content: [{
1275
+ type: "text",
1276
+ text: JSON.stringify(updateResult, null, 2)
1277
+ }]
1278
+ };
1279
+ }
1280
+ case "delete_content_type": {
1281
+ const contentTypeUid = String(request.params.arguments?.contentType);
1282
+ if (!contentTypeUid) {
1283
+ throw new McpError(ErrorCode.InvalidParams, "Content type UID is required");
1284
+ }
1285
+ const deletionResult = await deleteContentType(contentTypeUid);
1286
+ return {
1287
+ content: [{
1288
+ type: "text",
1289
+ text: JSON.stringify(deletionResult, null, 2)
1290
+ }]
1291
+ };
1292
+ }
1293
+ default:
1294
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
1295
+ }
1296
+ }
1297
+ catch (error) {
1298
+ console.error(`[Error] Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
1299
+ // Handle both McpError and regular errors the same way - return error response
1300
+ const errorMessage = error instanceof Error ? error.message : String(error);
1301
+ return {
1302
+ content: [{
1303
+ type: "text",
1304
+ text: `Error: ${errorMessage}`
1305
+ }],
1306
+ isError: true
1307
+ };
1308
+ }
1309
+ });
1310
+ /**
1311
+ * Start the server using stdio transport.
1312
+ */
1313
+ async function main() {
1314
+ console.error("[Setup] Starting Strapi MCP server");
1315
+ const transport = new StdioServerTransport();
1316
+ await server.connect(transport);
1317
+ console.error("[Setup] Strapi MCP server running");
1318
+ }
1319
+ main().catch((error) => {
1320
+ console.error("[Error] Server error:", error);
1321
+ process.exit(1);
1322
+ });
1323
+ /**
1324
+ * Delete a content type from Fedin CMS (Supabase)
1325
+ */
1326
+ async function deleteContentType(contentTypeUid) {
1327
+ try {
1328
+ console.error(`[API] Deleting content type: ${contentTypeUid}`);
1329
+ if (!contentTypeUid) {
1330
+ throw new Error(`Content type UID is required`);
1331
+ }
1332
+ if (!WEBSITE_ID) {
1333
+ throw new Error("WEBSITE_ID environment variable is required to delete content type");
1334
+ }
1335
+ const response = await supabaseClient.delete(`/content-types/${contentTypeUid}`, {
1336
+ params: { websiteId: WEBSITE_ID }
1337
+ });
1338
+ console.error(`[API] Content type deletion response:`, response.data);
1339
+ if (response.data && response.data.data) {
1340
+ return response.data.data;
1341
+ }
1342
+ return { message: `Content type ${contentTypeUid} deleted successfully` };
1343
+ }
1344
+ catch (error) {
1345
+ console.error(`[Error] Failed to delete content type ${contentTypeUid}:`, error);
1346
+ let errorMessage = `Failed to delete content type ${contentTypeUid}`;
1347
+ let errorCode = ExtendedErrorCode.InternalError;
1348
+ if (axios.isAxiosError(error)) {
1349
+ errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
1350
+ if (error.response?.status === 404) {
1351
+ errorCode = ExtendedErrorCode.ResourceNotFound;
1352
+ errorMessage += ` (Content type or website not found)`;
1353
+ }
1354
+ else if (error.response?.status === 400) {
1355
+ errorCode = ExtendedErrorCode.InvalidParams;
1356
+ errorMessage += ` (Bad Request): ${JSON.stringify(error.response?.data)}`;
1357
+ }
1358
+ else if (error.response?.status === 409) {
1359
+ errorCode = ExtendedErrorCode.InvalidParams;
1360
+ errorMessage += ` (Conflict - Content type is still in use)`;
1361
+ }
1362
+ else if (error.response?.status === 403 || error.response?.status === 401) {
1363
+ errorCode = ExtendedErrorCode.AccessDenied;
1364
+ errorMessage += ` (Permission Denied)`;
1365
+ }
1366
+ }
1367
+ else if (error instanceof Error) {
1368
+ errorMessage += `: ${error.message}`;
1369
+ }
1370
+ else {
1371
+ errorMessage += `: ${String(error)}`;
1372
+ }
1373
+ throw new ExtendedMcpError(errorCode, errorMessage);
1374
+ }
1375
+ }
1376
+ // Add connection validation flag
1377
+ let connectionValidated = false;
1378
+ /**
1379
+ * Test connection to Strapi and validate authentication
1380
+ */
1381
+ async function validateStrapiConnection() {
1382
+ if (connectionValidated)
1383
+ return; // Already validated
1384
+ try {
1385
+ console.error("[Setup] Validating connection to Strapi...");
1386
+ // Try a simple request to test connectivity - use a valid endpoint
1387
+ // Try the admin/users/me endpoint to test admin authentication
1388
+ // or fall back to a public content endpoint
1389
+ let response;
1390
+ let authMethod = "";
1391
+ // First try admin authentication if available
1392
+ if (STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD) {
1393
+ try {
1394
+ // Test admin login
1395
+ await loginToStrapiAdmin();
1396
+ const adminData = await makeAdminApiRequest('/admin/users/me');
1397
+ // makeAdminApiRequest returns data, not full response - if we get data, auth worked
1398
+ if (adminData) {
1399
+ authMethod = "admin credentials";
1400
+ console.error("[Setup] ✓ Admin authentication successful");
1401
+ console.error(`[Setup] ✓ Connection to Strapi successful using ${authMethod}`);
1402
+ connectionValidated = true;
1403
+ return; // Success - exit early
1404
+ }
1405
+ }
1406
+ catch (adminError) {
1407
+ console.error("[Setup] Admin authentication failed, trying API token...");
1408
+ // Fall through to API token test
1409
+ }
1410
+ }
1411
+ // If admin failed or not available, try API token
1412
+ try {
1413
+ // Try a simple endpoint that should exist - use upload/files to test API token
1414
+ response = await strapiClient.get('/api/upload/files?pagination[limit]=1');
1415
+ authMethod = "API token";
1416
+ console.error("[Setup] ✓ API token authentication successful");
1417
+ }
1418
+ catch (apiError) {
1419
+ console.error("[Setup] API token test failed, trying root endpoint...");
1420
+ try {
1421
+ // Last resort - try to hit the root to see if server is running
1422
+ response = await strapiClient.get('/');
1423
+ authMethod = "server connection";
1424
+ console.error("[Setup] ✓ Server is reachable");
1425
+ }
1426
+ catch (rootError) {
1427
+ throw new Error("All connection tests failed");
1428
+ }
1429
+ }
1430
+ // Check if we got a proper response from strapiClient calls
1431
+ if (response && response.status >= 200 && response.status < 300) {
1432
+ console.error(`[Setup] ✓ Connection to Strapi successful using ${authMethod}`);
1433
+ connectionValidated = true;
1434
+ }
1435
+ else {
1436
+ throw new Error(`Unexpected response status: ${response?.status}`);
1437
+ }
1438
+ }
1439
+ catch (error) {
1440
+ console.error("[Setup] ✗ Failed to connect to Strapi");
1441
+ let errorMessage = "Cannot connect to Strapi instance";
1442
+ if (axios.isAxiosError(error)) {
1443
+ if (error.code === 'ECONNREFUSED') {
1444
+ errorMessage += `: Connection refused. Is Strapi running at ${STRAPI_URL}?`;
1445
+ }
1446
+ else if (error.response?.status === 401) {
1447
+ errorMessage += `: Authentication failed. Check your API token or admin credentials.`;
1448
+ }
1449
+ else if (error.response?.status === 403) {
1450
+ errorMessage += `: Access forbidden. Your API token may lack necessary permissions.`;
1451
+ }
1452
+ else if (error.response?.status === 404) {
1453
+ errorMessage += `: Endpoint not found. Strapi server might be running but not properly configured.`;
1454
+ }
1455
+ else {
1456
+ errorMessage += `: ${error.message}`;
1457
+ }
1458
+ }
1459
+ else {
1460
+ errorMessage += `: ${error.message}`;
1461
+ }
1462
+ throw new ExtendedMcpError(ExtendedErrorCode.InternalError, errorMessage);
1463
+ }
1464
+ }