@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.
- package/build/index.js +1464 -0
- 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
|
+
}
|