strapi-oauth-mcp-manager 0.1.2 → 0.1.4
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/dist/server/index.js +63 -203
- package/dist/server/index.mjs +63 -203
- package/dist/server/src/content-types/index.d.ts +0 -45
- package/dist/server/src/controllers/oauth.d.ts +2 -0
- package/dist/server/src/index.d.ts +0 -54
- package/dist/server/src/middlewares/mcp-oauth.d.ts +3 -1
- package/dist/server/src/services/index.d.ts +0 -9
- package/package.json +1 -1
- package/dist/server/src/services/endpoint.d.ts +0 -37
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const node_crypto = require("node:crypto");
|
|
3
3
|
const PLUGIN_ID = "strapi-oauth-mcp-manager";
|
|
4
|
-
const PLUGIN_UID$
|
|
4
|
+
const PLUGIN_UID$2 = `plugin::${PLUGIN_ID}`;
|
|
5
|
+
const MCP_ENDPOINT_PATTERN = /^\/api\/[^/]+\/mcp(\/.*)?$/;
|
|
5
6
|
function extractBearerToken(authHeader) {
|
|
6
7
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
7
8
|
return null;
|
|
@@ -21,7 +22,7 @@ function buildWwwAuthenticateHeader(ctx, strapi) {
|
|
|
21
22
|
}
|
|
22
23
|
async function validateOAuthToken(token, strapi) {
|
|
23
24
|
try {
|
|
24
|
-
const tokenRecord = await strapi.documents(`${PLUGIN_UID$
|
|
25
|
+
const tokenRecord = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-token`).findFirst({
|
|
25
26
|
filters: { accessToken: token, revoked: false }
|
|
26
27
|
});
|
|
27
28
|
if (!tokenRecord) {
|
|
@@ -30,7 +31,7 @@ async function validateOAuthToken(token, strapi) {
|
|
|
30
31
|
if (new Date(tokenRecord.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
31
32
|
return { valid: false, error: "Token expired" };
|
|
32
33
|
}
|
|
33
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
34
|
+
const client = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-client`).findFirst({
|
|
34
35
|
filters: { clientId: tokenRecord.clientId }
|
|
35
36
|
});
|
|
36
37
|
if (!client) {
|
|
@@ -45,32 +46,15 @@ async function validateOAuthToken(token, strapi) {
|
|
|
45
46
|
return { valid: false, error: "Token validation failed" };
|
|
46
47
|
}
|
|
47
48
|
}
|
|
49
|
+
function isMcpEndpoint(path) {
|
|
50
|
+
return MCP_ENDPOINT_PATTERN.test(path);
|
|
51
|
+
}
|
|
48
52
|
const mcpOauthMiddleware = (config2, { strapi }) => {
|
|
49
|
-
let endpointCache = [];
|
|
50
|
-
let lastCacheUpdate = 0;
|
|
51
|
-
const CACHE_TTL = 6e4;
|
|
52
|
-
async function refreshEndpointCache() {
|
|
53
|
-
const now = Date.now();
|
|
54
|
-
if (now - lastCacheUpdate > CACHE_TTL) {
|
|
55
|
-
try {
|
|
56
|
-
const endpoints = await strapi.documents(`${PLUGIN_UID$3}.mcp-endpoint`).findMany({
|
|
57
|
-
filters: { active: true }
|
|
58
|
-
});
|
|
59
|
-
endpointCache = endpoints.map((e) => e.path);
|
|
60
|
-
lastCacheUpdate = now;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
strapi.log.error(`[${PLUGIN_ID}] Error refreshing endpoint cache`, { error });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
function isProtectedPath(path) {
|
|
67
|
-
return endpointCache.some((endpoint) => path.includes(endpoint));
|
|
68
|
-
}
|
|
69
53
|
return async (ctx, next) => {
|
|
70
|
-
|
|
71
|
-
if (!isProtectedPath(ctx.path)) {
|
|
54
|
+
if (!isMcpEndpoint(ctx.path)) {
|
|
72
55
|
return next();
|
|
73
56
|
}
|
|
57
|
+
strapi.log.debug(`[${PLUGIN_ID}] Protecting MCP endpoint: ${ctx.path}`);
|
|
74
58
|
const authHeader = ctx.request.headers.authorization;
|
|
75
59
|
const token = extractBearerToken(authHeader);
|
|
76
60
|
if (!token) {
|
|
@@ -109,18 +93,18 @@ const config = {
|
|
|
109
93
|
validator() {
|
|
110
94
|
}
|
|
111
95
|
};
|
|
112
|
-
const kind$
|
|
113
|
-
const collectionName$
|
|
114
|
-
const info$
|
|
96
|
+
const kind$2 = "collectionType";
|
|
97
|
+
const collectionName$2 = "mcp_oauth_clients";
|
|
98
|
+
const info$2 = {
|
|
115
99
|
singularName: "mcp-oauth-client",
|
|
116
100
|
pluralName: "mcp-oauth-clients",
|
|
117
101
|
displayName: "MCP OAuth Client",
|
|
118
102
|
description: "OAuth 2.0 clients for MCP authentication"
|
|
119
103
|
};
|
|
120
|
-
const options$
|
|
104
|
+
const options$2 = {
|
|
121
105
|
draftAndPublish: false
|
|
122
106
|
};
|
|
123
|
-
const pluginOptions$
|
|
107
|
+
const pluginOptions$2 = {
|
|
124
108
|
"content-manager": {
|
|
125
109
|
visible: true
|
|
126
110
|
},
|
|
@@ -128,7 +112,7 @@ const pluginOptions$3 = {
|
|
|
128
112
|
visible: true
|
|
129
113
|
}
|
|
130
114
|
};
|
|
131
|
-
const attributes$
|
|
115
|
+
const attributes$2 = {
|
|
132
116
|
name: {
|
|
133
117
|
type: "string",
|
|
134
118
|
required: true
|
|
@@ -161,25 +145,25 @@ const attributes$3 = {
|
|
|
161
145
|
}
|
|
162
146
|
};
|
|
163
147
|
const mcpOauthClient = {
|
|
164
|
-
kind: kind$
|
|
165
|
-
collectionName: collectionName$
|
|
166
|
-
info: info$
|
|
167
|
-
options: options$
|
|
168
|
-
pluginOptions: pluginOptions$
|
|
169
|
-
attributes: attributes$
|
|
148
|
+
kind: kind$2,
|
|
149
|
+
collectionName: collectionName$2,
|
|
150
|
+
info: info$2,
|
|
151
|
+
options: options$2,
|
|
152
|
+
pluginOptions: pluginOptions$2,
|
|
153
|
+
attributes: attributes$2
|
|
170
154
|
};
|
|
171
|
-
const kind$
|
|
172
|
-
const collectionName$
|
|
173
|
-
const info$
|
|
155
|
+
const kind$1 = "collectionType";
|
|
156
|
+
const collectionName$1 = "mcp_oauth_codes";
|
|
157
|
+
const info$1 = {
|
|
174
158
|
singularName: "mcp-oauth-code",
|
|
175
159
|
pluralName: "mcp-oauth-codes",
|
|
176
160
|
displayName: "MCP OAuth Code",
|
|
177
161
|
description: "OAuth 2.0 authorization codes"
|
|
178
162
|
};
|
|
179
|
-
const options$
|
|
163
|
+
const options$1 = {
|
|
180
164
|
draftAndPublish: false
|
|
181
165
|
};
|
|
182
|
-
const pluginOptions$
|
|
166
|
+
const pluginOptions$1 = {
|
|
183
167
|
"content-manager": {
|
|
184
168
|
visible: false
|
|
185
169
|
},
|
|
@@ -187,7 +171,7 @@ const pluginOptions$2 = {
|
|
|
187
171
|
visible: false
|
|
188
172
|
}
|
|
189
173
|
};
|
|
190
|
-
const attributes$
|
|
174
|
+
const attributes$1 = {
|
|
191
175
|
code: {
|
|
192
176
|
type: "string",
|
|
193
177
|
required: true,
|
|
@@ -217,25 +201,25 @@ const attributes$2 = {
|
|
|
217
201
|
}
|
|
218
202
|
};
|
|
219
203
|
const mcpOauthCode = {
|
|
220
|
-
kind: kind$
|
|
221
|
-
collectionName: collectionName$
|
|
222
|
-
info: info$
|
|
223
|
-
options: options$
|
|
224
|
-
pluginOptions: pluginOptions$
|
|
225
|
-
attributes: attributes$
|
|
204
|
+
kind: kind$1,
|
|
205
|
+
collectionName: collectionName$1,
|
|
206
|
+
info: info$1,
|
|
207
|
+
options: options$1,
|
|
208
|
+
pluginOptions: pluginOptions$1,
|
|
209
|
+
attributes: attributes$1
|
|
226
210
|
};
|
|
227
|
-
const kind
|
|
228
|
-
const collectionName
|
|
229
|
-
const info
|
|
211
|
+
const kind = "collectionType";
|
|
212
|
+
const collectionName = "mcp_oauth_tokens";
|
|
213
|
+
const info = {
|
|
230
214
|
singularName: "mcp-oauth-token",
|
|
231
215
|
pluralName: "mcp-oauth-tokens",
|
|
232
216
|
displayName: "MCP OAuth Token",
|
|
233
217
|
description: "OAuth 2.0 access and refresh tokens"
|
|
234
218
|
};
|
|
235
|
-
const options
|
|
219
|
+
const options = {
|
|
236
220
|
draftAndPublish: false
|
|
237
221
|
};
|
|
238
|
-
const pluginOptions
|
|
222
|
+
const pluginOptions = {
|
|
239
223
|
"content-manager": {
|
|
240
224
|
visible: false
|
|
241
225
|
},
|
|
@@ -243,7 +227,7 @@ const pluginOptions$1 = {
|
|
|
243
227
|
visible: false
|
|
244
228
|
}
|
|
245
229
|
};
|
|
246
|
-
const attributes
|
|
230
|
+
const attributes = {
|
|
247
231
|
accessToken: {
|
|
248
232
|
type: "string",
|
|
249
233
|
required: true,
|
|
@@ -274,55 +258,6 @@ const attributes$1 = {
|
|
|
274
258
|
}
|
|
275
259
|
};
|
|
276
260
|
const mcpOauthToken = {
|
|
277
|
-
kind: kind$1,
|
|
278
|
-
collectionName: collectionName$1,
|
|
279
|
-
info: info$1,
|
|
280
|
-
options: options$1,
|
|
281
|
-
pluginOptions: pluginOptions$1,
|
|
282
|
-
attributes: attributes$1
|
|
283
|
-
};
|
|
284
|
-
const kind = "collectionType";
|
|
285
|
-
const collectionName = "mcp_endpoints";
|
|
286
|
-
const info = {
|
|
287
|
-
singularName: "mcp-endpoint",
|
|
288
|
-
pluralName: "mcp-endpoints",
|
|
289
|
-
displayName: "MCP Endpoint",
|
|
290
|
-
description: "Registered MCP endpoints protected by OAuth"
|
|
291
|
-
};
|
|
292
|
-
const options = {
|
|
293
|
-
draftAndPublish: false
|
|
294
|
-
};
|
|
295
|
-
const pluginOptions = {
|
|
296
|
-
"content-manager": {
|
|
297
|
-
visible: true
|
|
298
|
-
},
|
|
299
|
-
"content-type-builder": {
|
|
300
|
-
visible: true
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
const attributes = {
|
|
304
|
-
name: {
|
|
305
|
-
type: "string",
|
|
306
|
-
required: true
|
|
307
|
-
},
|
|
308
|
-
pluginId: {
|
|
309
|
-
type: "string",
|
|
310
|
-
required: true
|
|
311
|
-
},
|
|
312
|
-
path: {
|
|
313
|
-
type: "string",
|
|
314
|
-
required: true,
|
|
315
|
-
unique: true
|
|
316
|
-
},
|
|
317
|
-
description: {
|
|
318
|
-
type: "text"
|
|
319
|
-
},
|
|
320
|
-
active: {
|
|
321
|
-
type: "boolean",
|
|
322
|
-
"default": true
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
const mcpEndpoint = {
|
|
326
261
|
kind,
|
|
327
262
|
collectionName,
|
|
328
263
|
info,
|
|
@@ -333,10 +268,9 @@ const mcpEndpoint = {
|
|
|
333
268
|
const contentTypes = {
|
|
334
269
|
"mcp-oauth-client": { schema: mcpOauthClient },
|
|
335
270
|
"mcp-oauth-code": { schema: mcpOauthCode },
|
|
336
|
-
"mcp-oauth-token": { schema: mcpOauthToken }
|
|
337
|
-
"mcp-endpoint": { schema: mcpEndpoint }
|
|
271
|
+
"mcp-oauth-token": { schema: mcpOauthToken }
|
|
338
272
|
};
|
|
339
|
-
const PLUGIN_UID$
|
|
273
|
+
const PLUGIN_UID$1 = `plugin::${PLUGIN_ID}`;
|
|
340
274
|
function getBaseUrl(ctx, strapi) {
|
|
341
275
|
const forwardedProto = ctx.request.headers["x-forwarded-proto"] || ctx.protocol;
|
|
342
276
|
const forwardedHost = ctx.request.headers["x-forwarded-host"] || ctx.request.headers["host"];
|
|
@@ -373,17 +307,15 @@ const oauthController = ({ strapi }) => ({
|
|
|
373
307
|
/**
|
|
374
308
|
* OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
375
309
|
* GET /.well-known/oauth-protected-resource
|
|
310
|
+
*
|
|
311
|
+
* Convention-based protection: all /api/{plugin}/mcp endpoints are protected.
|
|
376
312
|
*/
|
|
377
313
|
async protectedResource(ctx) {
|
|
378
314
|
const baseUrl = getBaseUrl(ctx, strapi);
|
|
379
315
|
const pluginPath = `/api/${PLUGIN_ID}`;
|
|
380
316
|
const authServer = `${baseUrl}${pluginPath}`;
|
|
381
|
-
const endpoints = await strapi.documents(`${PLUGIN_UID$2}.mcp-endpoint`).findMany({
|
|
382
|
-
filters: { active: true }
|
|
383
|
-
});
|
|
384
|
-
const resources = endpoints.map((e) => `${baseUrl}${e.path}`);
|
|
385
317
|
ctx.body = {
|
|
386
|
-
resource:
|
|
318
|
+
resource: `${baseUrl}/api`,
|
|
387
319
|
authorization_servers: [authServer],
|
|
388
320
|
bearer_methods_supported: ["header"]
|
|
389
321
|
};
|
|
@@ -409,7 +341,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
409
341
|
ctx.body = { error: "unsupported_response_type", error_description: "Only code response type is supported" };
|
|
410
342
|
return;
|
|
411
343
|
}
|
|
412
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
344
|
+
const client = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-client`).findFirst({
|
|
413
345
|
filters: { clientId: client_id, active: true }
|
|
414
346
|
});
|
|
415
347
|
if (!client) {
|
|
@@ -438,7 +370,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
438
370
|
}
|
|
439
371
|
const code = node_crypto.randomBytes(32).toString("hex");
|
|
440
372
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
441
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
373
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).create({
|
|
442
374
|
data: {
|
|
443
375
|
code,
|
|
444
376
|
clientId: client_id,
|
|
@@ -476,7 +408,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
476
408
|
ctx.body = { error: "invalid_client", error_description: "Client authentication required" };
|
|
477
409
|
return;
|
|
478
410
|
}
|
|
479
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
411
|
+
const client = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-client`).findFirst({
|
|
480
412
|
filters: { clientId: authClientId, active: true }
|
|
481
413
|
});
|
|
482
414
|
if (!client || authClientSecret && client.clientSecret !== authClientSecret) {
|
|
@@ -500,7 +432,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
500
432
|
ctx.body = { error: "invalid_request", error_description: "code is required" };
|
|
501
433
|
return;
|
|
502
434
|
}
|
|
503
|
-
const authCode = await strapi.documents(`${PLUGIN_UID$
|
|
435
|
+
const authCode = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).findFirst({
|
|
504
436
|
filters: { code, clientId: client.clientId, used: false }
|
|
505
437
|
});
|
|
506
438
|
if (!authCode) {
|
|
@@ -518,7 +450,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
518
450
|
ctx.body = { error: "invalid_grant", error_description: "redirect_uri mismatch" };
|
|
519
451
|
return;
|
|
520
452
|
}
|
|
521
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
453
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).update({
|
|
522
454
|
documentId: authCode.documentId,
|
|
523
455
|
data: { used: true }
|
|
524
456
|
});
|
|
@@ -526,7 +458,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
526
458
|
const refreshToken = node_crypto.randomBytes(32).toString("hex");
|
|
527
459
|
const expiresIn = 3600;
|
|
528
460
|
const refreshExpiresIn = 30 * 24 * 3600;
|
|
529
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
461
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).create({
|
|
530
462
|
data: {
|
|
531
463
|
accessToken,
|
|
532
464
|
refreshToken,
|
|
@@ -549,7 +481,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
549
481
|
ctx.body = { error: "invalid_request", error_description: "refresh_token is required" };
|
|
550
482
|
return;
|
|
551
483
|
}
|
|
552
|
-
const token = await strapi.documents(`${PLUGIN_UID$
|
|
484
|
+
const token = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).findFirst({
|
|
553
485
|
filters: { refreshToken, clientId: client.clientId, revoked: false }
|
|
554
486
|
});
|
|
555
487
|
if (!token) {
|
|
@@ -562,7 +494,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
562
494
|
ctx.body = { error: "invalid_grant", error_description: "Refresh token expired" };
|
|
563
495
|
return;
|
|
564
496
|
}
|
|
565
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
497
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).update({
|
|
566
498
|
documentId: token.documentId,
|
|
567
499
|
data: { revoked: true }
|
|
568
500
|
});
|
|
@@ -570,7 +502,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
570
502
|
const newRefreshToken = node_crypto.randomBytes(32).toString("hex");
|
|
571
503
|
const expiresIn = 3600;
|
|
572
504
|
const refreshExpiresIn = 30 * 24 * 3600;
|
|
573
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
505
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).create({
|
|
574
506
|
data: {
|
|
575
507
|
accessToken: newAccessToken,
|
|
576
508
|
refreshToken: newRefreshToken,
|
|
@@ -647,14 +579,14 @@ const routes = {
|
|
|
647
579
|
routes: [...admin]
|
|
648
580
|
}
|
|
649
581
|
};
|
|
650
|
-
const PLUGIN_UID
|
|
582
|
+
const PLUGIN_UID = `plugin::${PLUGIN_ID}`;
|
|
651
583
|
const oauthService = ({ strapi }) => ({
|
|
652
584
|
/**
|
|
653
585
|
* Validate an OAuth access token
|
|
654
586
|
*/
|
|
655
587
|
async validateToken(accessToken) {
|
|
656
588
|
try {
|
|
657
|
-
const tokenRecord = await strapi.documents(`${PLUGIN_UID
|
|
589
|
+
const tokenRecord = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findFirst({
|
|
658
590
|
filters: { accessToken, revoked: false }
|
|
659
591
|
});
|
|
660
592
|
if (!tokenRecord) {
|
|
@@ -663,7 +595,7 @@ const oauthService = ({ strapi }) => ({
|
|
|
663
595
|
if (new Date(tokenRecord.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
664
596
|
return { valid: false, error: "Token expired" };
|
|
665
597
|
}
|
|
666
|
-
const client = await strapi.documents(`${PLUGIN_UID
|
|
598
|
+
const client = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-client`).findFirst({
|
|
667
599
|
filters: { clientId: tokenRecord.clientId }
|
|
668
600
|
});
|
|
669
601
|
if (!client) {
|
|
@@ -683,11 +615,11 @@ const oauthService = ({ strapi }) => ({
|
|
|
683
615
|
* Revoke all tokens for a client
|
|
684
616
|
*/
|
|
685
617
|
async revokeClientTokens(clientId) {
|
|
686
|
-
const tokens = await strapi.documents(`${PLUGIN_UID
|
|
618
|
+
const tokens = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findMany({
|
|
687
619
|
filters: { clientId, revoked: false }
|
|
688
620
|
});
|
|
689
621
|
for (const token of tokens) {
|
|
690
|
-
await strapi.documents(`${PLUGIN_UID
|
|
622
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).update({
|
|
691
623
|
documentId: token.documentId,
|
|
692
624
|
data: { revoked: true }
|
|
693
625
|
});
|
|
@@ -699,19 +631,19 @@ const oauthService = ({ strapi }) => ({
|
|
|
699
631
|
*/
|
|
700
632
|
async cleanupExpired() {
|
|
701
633
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
702
|
-
const expiredCodes = await strapi.documents(`${PLUGIN_UID
|
|
634
|
+
const expiredCodes = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-code`).findMany({
|
|
703
635
|
filters: { expiresAt: { $lt: now } }
|
|
704
636
|
});
|
|
705
637
|
for (const code of expiredCodes) {
|
|
706
|
-
await strapi.documents(`${PLUGIN_UID
|
|
638
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-code`).delete({
|
|
707
639
|
documentId: code.documentId
|
|
708
640
|
});
|
|
709
641
|
}
|
|
710
|
-
const expiredTokens = await strapi.documents(`${PLUGIN_UID
|
|
642
|
+
const expiredTokens = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findMany({
|
|
711
643
|
filters: { refreshExpiresAt: { $lt: now } }
|
|
712
644
|
});
|
|
713
645
|
for (const token of expiredTokens) {
|
|
714
|
-
await strapi.documents(`${PLUGIN_UID
|
|
646
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).delete({
|
|
715
647
|
documentId: token.documentId
|
|
716
648
|
});
|
|
717
649
|
}
|
|
@@ -721,80 +653,8 @@ const oauthService = ({ strapi }) => ({
|
|
|
721
653
|
};
|
|
722
654
|
}
|
|
723
655
|
});
|
|
724
|
-
const PLUGIN_UID = `plugin::${PLUGIN_ID}`;
|
|
725
|
-
const endpointService = ({ strapi }) => ({
|
|
726
|
-
/**
|
|
727
|
-
* Register an MCP endpoint for OAuth protection
|
|
728
|
-
*/
|
|
729
|
-
async register(endpoint) {
|
|
730
|
-
const existing = await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findFirst({
|
|
731
|
-
filters: { path: endpoint.path }
|
|
732
|
-
});
|
|
733
|
-
if (existing) {
|
|
734
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).update({
|
|
735
|
-
documentId: existing.documentId,
|
|
736
|
-
data: {
|
|
737
|
-
name: endpoint.name,
|
|
738
|
-
pluginId: endpoint.pluginId,
|
|
739
|
-
description: endpoint.description,
|
|
740
|
-
active: true
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).create({
|
|
745
|
-
data: {
|
|
746
|
-
name: endpoint.name,
|
|
747
|
-
pluginId: endpoint.pluginId,
|
|
748
|
-
path: endpoint.path,
|
|
749
|
-
description: endpoint.description,
|
|
750
|
-
active: true
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
},
|
|
754
|
-
/**
|
|
755
|
-
* Unregister an MCP endpoint
|
|
756
|
-
*/
|
|
757
|
-
async unregister(path) {
|
|
758
|
-
const endpoint = await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findFirst({
|
|
759
|
-
filters: { path }
|
|
760
|
-
});
|
|
761
|
-
if (!endpoint) {
|
|
762
|
-
return false;
|
|
763
|
-
}
|
|
764
|
-
await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).update({
|
|
765
|
-
documentId: endpoint.documentId,
|
|
766
|
-
data: { active: false }
|
|
767
|
-
});
|
|
768
|
-
return true;
|
|
769
|
-
},
|
|
770
|
-
/**
|
|
771
|
-
* Get all registered endpoints
|
|
772
|
-
*/
|
|
773
|
-
async getAll(activeOnly = true) {
|
|
774
|
-
const filters = activeOnly ? { active: true } : {};
|
|
775
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findMany({ filters });
|
|
776
|
-
},
|
|
777
|
-
/**
|
|
778
|
-
* Get endpoints for a specific plugin
|
|
779
|
-
*/
|
|
780
|
-
async getByPlugin(pluginId, activeOnly = true) {
|
|
781
|
-
const filters = { pluginId };
|
|
782
|
-
if (activeOnly) {
|
|
783
|
-
filters.active = true;
|
|
784
|
-
}
|
|
785
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findMany({ filters });
|
|
786
|
-
},
|
|
787
|
-
/**
|
|
788
|
-
* Check if a path is a protected endpoint
|
|
789
|
-
*/
|
|
790
|
-
async isProtected(path) {
|
|
791
|
-
const endpoints = await this.getAll(true);
|
|
792
|
-
return endpoints.some((e) => path.includes(e.path));
|
|
793
|
-
}
|
|
794
|
-
});
|
|
795
656
|
const services = {
|
|
796
|
-
oauth: oauthService
|
|
797
|
-
endpoint: endpointService
|
|
657
|
+
oauth: oauthService
|
|
798
658
|
};
|
|
799
659
|
const index = {
|
|
800
660
|
register,
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
const PLUGIN_ID = "strapi-oauth-mcp-manager";
|
|
3
|
-
const PLUGIN_UID$
|
|
3
|
+
const PLUGIN_UID$2 = `plugin::${PLUGIN_ID}`;
|
|
4
|
+
const MCP_ENDPOINT_PATTERN = /^\/api\/[^/]+\/mcp(\/.*)?$/;
|
|
4
5
|
function extractBearerToken(authHeader) {
|
|
5
6
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
6
7
|
return null;
|
|
@@ -20,7 +21,7 @@ function buildWwwAuthenticateHeader(ctx, strapi) {
|
|
|
20
21
|
}
|
|
21
22
|
async function validateOAuthToken(token, strapi) {
|
|
22
23
|
try {
|
|
23
|
-
const tokenRecord = await strapi.documents(`${PLUGIN_UID$
|
|
24
|
+
const tokenRecord = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-token`).findFirst({
|
|
24
25
|
filters: { accessToken: token, revoked: false }
|
|
25
26
|
});
|
|
26
27
|
if (!tokenRecord) {
|
|
@@ -29,7 +30,7 @@ async function validateOAuthToken(token, strapi) {
|
|
|
29
30
|
if (new Date(tokenRecord.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
30
31
|
return { valid: false, error: "Token expired" };
|
|
31
32
|
}
|
|
32
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
33
|
+
const client = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-client`).findFirst({
|
|
33
34
|
filters: { clientId: tokenRecord.clientId }
|
|
34
35
|
});
|
|
35
36
|
if (!client) {
|
|
@@ -44,32 +45,15 @@ async function validateOAuthToken(token, strapi) {
|
|
|
44
45
|
return { valid: false, error: "Token validation failed" };
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
function isMcpEndpoint(path) {
|
|
49
|
+
return MCP_ENDPOINT_PATTERN.test(path);
|
|
50
|
+
}
|
|
47
51
|
const mcpOauthMiddleware = (config2, { strapi }) => {
|
|
48
|
-
let endpointCache = [];
|
|
49
|
-
let lastCacheUpdate = 0;
|
|
50
|
-
const CACHE_TTL = 6e4;
|
|
51
|
-
async function refreshEndpointCache() {
|
|
52
|
-
const now = Date.now();
|
|
53
|
-
if (now - lastCacheUpdate > CACHE_TTL) {
|
|
54
|
-
try {
|
|
55
|
-
const endpoints = await strapi.documents(`${PLUGIN_UID$3}.mcp-endpoint`).findMany({
|
|
56
|
-
filters: { active: true }
|
|
57
|
-
});
|
|
58
|
-
endpointCache = endpoints.map((e) => e.path);
|
|
59
|
-
lastCacheUpdate = now;
|
|
60
|
-
} catch (error) {
|
|
61
|
-
strapi.log.error(`[${PLUGIN_ID}] Error refreshing endpoint cache`, { error });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
function isProtectedPath(path) {
|
|
66
|
-
return endpointCache.some((endpoint) => path.includes(endpoint));
|
|
67
|
-
}
|
|
68
52
|
return async (ctx, next) => {
|
|
69
|
-
|
|
70
|
-
if (!isProtectedPath(ctx.path)) {
|
|
53
|
+
if (!isMcpEndpoint(ctx.path)) {
|
|
71
54
|
return next();
|
|
72
55
|
}
|
|
56
|
+
strapi.log.debug(`[${PLUGIN_ID}] Protecting MCP endpoint: ${ctx.path}`);
|
|
73
57
|
const authHeader = ctx.request.headers.authorization;
|
|
74
58
|
const token = extractBearerToken(authHeader);
|
|
75
59
|
if (!token) {
|
|
@@ -108,18 +92,18 @@ const config = {
|
|
|
108
92
|
validator() {
|
|
109
93
|
}
|
|
110
94
|
};
|
|
111
|
-
const kind$
|
|
112
|
-
const collectionName$
|
|
113
|
-
const info$
|
|
95
|
+
const kind$2 = "collectionType";
|
|
96
|
+
const collectionName$2 = "mcp_oauth_clients";
|
|
97
|
+
const info$2 = {
|
|
114
98
|
singularName: "mcp-oauth-client",
|
|
115
99
|
pluralName: "mcp-oauth-clients",
|
|
116
100
|
displayName: "MCP OAuth Client",
|
|
117
101
|
description: "OAuth 2.0 clients for MCP authentication"
|
|
118
102
|
};
|
|
119
|
-
const options$
|
|
103
|
+
const options$2 = {
|
|
120
104
|
draftAndPublish: false
|
|
121
105
|
};
|
|
122
|
-
const pluginOptions$
|
|
106
|
+
const pluginOptions$2 = {
|
|
123
107
|
"content-manager": {
|
|
124
108
|
visible: true
|
|
125
109
|
},
|
|
@@ -127,7 +111,7 @@ const pluginOptions$3 = {
|
|
|
127
111
|
visible: true
|
|
128
112
|
}
|
|
129
113
|
};
|
|
130
|
-
const attributes$
|
|
114
|
+
const attributes$2 = {
|
|
131
115
|
name: {
|
|
132
116
|
type: "string",
|
|
133
117
|
required: true
|
|
@@ -160,25 +144,25 @@ const attributes$3 = {
|
|
|
160
144
|
}
|
|
161
145
|
};
|
|
162
146
|
const mcpOauthClient = {
|
|
163
|
-
kind: kind$
|
|
164
|
-
collectionName: collectionName$
|
|
165
|
-
info: info$
|
|
166
|
-
options: options$
|
|
167
|
-
pluginOptions: pluginOptions$
|
|
168
|
-
attributes: attributes$
|
|
147
|
+
kind: kind$2,
|
|
148
|
+
collectionName: collectionName$2,
|
|
149
|
+
info: info$2,
|
|
150
|
+
options: options$2,
|
|
151
|
+
pluginOptions: pluginOptions$2,
|
|
152
|
+
attributes: attributes$2
|
|
169
153
|
};
|
|
170
|
-
const kind$
|
|
171
|
-
const collectionName$
|
|
172
|
-
const info$
|
|
154
|
+
const kind$1 = "collectionType";
|
|
155
|
+
const collectionName$1 = "mcp_oauth_codes";
|
|
156
|
+
const info$1 = {
|
|
173
157
|
singularName: "mcp-oauth-code",
|
|
174
158
|
pluralName: "mcp-oauth-codes",
|
|
175
159
|
displayName: "MCP OAuth Code",
|
|
176
160
|
description: "OAuth 2.0 authorization codes"
|
|
177
161
|
};
|
|
178
|
-
const options$
|
|
162
|
+
const options$1 = {
|
|
179
163
|
draftAndPublish: false
|
|
180
164
|
};
|
|
181
|
-
const pluginOptions$
|
|
165
|
+
const pluginOptions$1 = {
|
|
182
166
|
"content-manager": {
|
|
183
167
|
visible: false
|
|
184
168
|
},
|
|
@@ -186,7 +170,7 @@ const pluginOptions$2 = {
|
|
|
186
170
|
visible: false
|
|
187
171
|
}
|
|
188
172
|
};
|
|
189
|
-
const attributes$
|
|
173
|
+
const attributes$1 = {
|
|
190
174
|
code: {
|
|
191
175
|
type: "string",
|
|
192
176
|
required: true,
|
|
@@ -216,25 +200,25 @@ const attributes$2 = {
|
|
|
216
200
|
}
|
|
217
201
|
};
|
|
218
202
|
const mcpOauthCode = {
|
|
219
|
-
kind: kind$
|
|
220
|
-
collectionName: collectionName$
|
|
221
|
-
info: info$
|
|
222
|
-
options: options$
|
|
223
|
-
pluginOptions: pluginOptions$
|
|
224
|
-
attributes: attributes$
|
|
203
|
+
kind: kind$1,
|
|
204
|
+
collectionName: collectionName$1,
|
|
205
|
+
info: info$1,
|
|
206
|
+
options: options$1,
|
|
207
|
+
pluginOptions: pluginOptions$1,
|
|
208
|
+
attributes: attributes$1
|
|
225
209
|
};
|
|
226
|
-
const kind
|
|
227
|
-
const collectionName
|
|
228
|
-
const info
|
|
210
|
+
const kind = "collectionType";
|
|
211
|
+
const collectionName = "mcp_oauth_tokens";
|
|
212
|
+
const info = {
|
|
229
213
|
singularName: "mcp-oauth-token",
|
|
230
214
|
pluralName: "mcp-oauth-tokens",
|
|
231
215
|
displayName: "MCP OAuth Token",
|
|
232
216
|
description: "OAuth 2.0 access and refresh tokens"
|
|
233
217
|
};
|
|
234
|
-
const options
|
|
218
|
+
const options = {
|
|
235
219
|
draftAndPublish: false
|
|
236
220
|
};
|
|
237
|
-
const pluginOptions
|
|
221
|
+
const pluginOptions = {
|
|
238
222
|
"content-manager": {
|
|
239
223
|
visible: false
|
|
240
224
|
},
|
|
@@ -242,7 +226,7 @@ const pluginOptions$1 = {
|
|
|
242
226
|
visible: false
|
|
243
227
|
}
|
|
244
228
|
};
|
|
245
|
-
const attributes
|
|
229
|
+
const attributes = {
|
|
246
230
|
accessToken: {
|
|
247
231
|
type: "string",
|
|
248
232
|
required: true,
|
|
@@ -273,55 +257,6 @@ const attributes$1 = {
|
|
|
273
257
|
}
|
|
274
258
|
};
|
|
275
259
|
const mcpOauthToken = {
|
|
276
|
-
kind: kind$1,
|
|
277
|
-
collectionName: collectionName$1,
|
|
278
|
-
info: info$1,
|
|
279
|
-
options: options$1,
|
|
280
|
-
pluginOptions: pluginOptions$1,
|
|
281
|
-
attributes: attributes$1
|
|
282
|
-
};
|
|
283
|
-
const kind = "collectionType";
|
|
284
|
-
const collectionName = "mcp_endpoints";
|
|
285
|
-
const info = {
|
|
286
|
-
singularName: "mcp-endpoint",
|
|
287
|
-
pluralName: "mcp-endpoints",
|
|
288
|
-
displayName: "MCP Endpoint",
|
|
289
|
-
description: "Registered MCP endpoints protected by OAuth"
|
|
290
|
-
};
|
|
291
|
-
const options = {
|
|
292
|
-
draftAndPublish: false
|
|
293
|
-
};
|
|
294
|
-
const pluginOptions = {
|
|
295
|
-
"content-manager": {
|
|
296
|
-
visible: true
|
|
297
|
-
},
|
|
298
|
-
"content-type-builder": {
|
|
299
|
-
visible: true
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
const attributes = {
|
|
303
|
-
name: {
|
|
304
|
-
type: "string",
|
|
305
|
-
required: true
|
|
306
|
-
},
|
|
307
|
-
pluginId: {
|
|
308
|
-
type: "string",
|
|
309
|
-
required: true
|
|
310
|
-
},
|
|
311
|
-
path: {
|
|
312
|
-
type: "string",
|
|
313
|
-
required: true,
|
|
314
|
-
unique: true
|
|
315
|
-
},
|
|
316
|
-
description: {
|
|
317
|
-
type: "text"
|
|
318
|
-
},
|
|
319
|
-
active: {
|
|
320
|
-
type: "boolean",
|
|
321
|
-
"default": true
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
const mcpEndpoint = {
|
|
325
260
|
kind,
|
|
326
261
|
collectionName,
|
|
327
262
|
info,
|
|
@@ -332,10 +267,9 @@ const mcpEndpoint = {
|
|
|
332
267
|
const contentTypes = {
|
|
333
268
|
"mcp-oauth-client": { schema: mcpOauthClient },
|
|
334
269
|
"mcp-oauth-code": { schema: mcpOauthCode },
|
|
335
|
-
"mcp-oauth-token": { schema: mcpOauthToken }
|
|
336
|
-
"mcp-endpoint": { schema: mcpEndpoint }
|
|
270
|
+
"mcp-oauth-token": { schema: mcpOauthToken }
|
|
337
271
|
};
|
|
338
|
-
const PLUGIN_UID$
|
|
272
|
+
const PLUGIN_UID$1 = `plugin::${PLUGIN_ID}`;
|
|
339
273
|
function getBaseUrl(ctx, strapi) {
|
|
340
274
|
const forwardedProto = ctx.request.headers["x-forwarded-proto"] || ctx.protocol;
|
|
341
275
|
const forwardedHost = ctx.request.headers["x-forwarded-host"] || ctx.request.headers["host"];
|
|
@@ -372,17 +306,15 @@ const oauthController = ({ strapi }) => ({
|
|
|
372
306
|
/**
|
|
373
307
|
* OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
374
308
|
* GET /.well-known/oauth-protected-resource
|
|
309
|
+
*
|
|
310
|
+
* Convention-based protection: all /api/{plugin}/mcp endpoints are protected.
|
|
375
311
|
*/
|
|
376
312
|
async protectedResource(ctx) {
|
|
377
313
|
const baseUrl = getBaseUrl(ctx, strapi);
|
|
378
314
|
const pluginPath = `/api/${PLUGIN_ID}`;
|
|
379
315
|
const authServer = `${baseUrl}${pluginPath}`;
|
|
380
|
-
const endpoints = await strapi.documents(`${PLUGIN_UID$2}.mcp-endpoint`).findMany({
|
|
381
|
-
filters: { active: true }
|
|
382
|
-
});
|
|
383
|
-
const resources = endpoints.map((e) => `${baseUrl}${e.path}`);
|
|
384
316
|
ctx.body = {
|
|
385
|
-
resource:
|
|
317
|
+
resource: `${baseUrl}/api`,
|
|
386
318
|
authorization_servers: [authServer],
|
|
387
319
|
bearer_methods_supported: ["header"]
|
|
388
320
|
};
|
|
@@ -408,7 +340,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
408
340
|
ctx.body = { error: "unsupported_response_type", error_description: "Only code response type is supported" };
|
|
409
341
|
return;
|
|
410
342
|
}
|
|
411
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
343
|
+
const client = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-client`).findFirst({
|
|
412
344
|
filters: { clientId: client_id, active: true }
|
|
413
345
|
});
|
|
414
346
|
if (!client) {
|
|
@@ -437,7 +369,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
437
369
|
}
|
|
438
370
|
const code = randomBytes(32).toString("hex");
|
|
439
371
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
440
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
372
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).create({
|
|
441
373
|
data: {
|
|
442
374
|
code,
|
|
443
375
|
clientId: client_id,
|
|
@@ -475,7 +407,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
475
407
|
ctx.body = { error: "invalid_client", error_description: "Client authentication required" };
|
|
476
408
|
return;
|
|
477
409
|
}
|
|
478
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
410
|
+
const client = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-client`).findFirst({
|
|
479
411
|
filters: { clientId: authClientId, active: true }
|
|
480
412
|
});
|
|
481
413
|
if (!client || authClientSecret && client.clientSecret !== authClientSecret) {
|
|
@@ -499,7 +431,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
499
431
|
ctx.body = { error: "invalid_request", error_description: "code is required" };
|
|
500
432
|
return;
|
|
501
433
|
}
|
|
502
|
-
const authCode = await strapi.documents(`${PLUGIN_UID$
|
|
434
|
+
const authCode = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).findFirst({
|
|
503
435
|
filters: { code, clientId: client.clientId, used: false }
|
|
504
436
|
});
|
|
505
437
|
if (!authCode) {
|
|
@@ -517,7 +449,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
517
449
|
ctx.body = { error: "invalid_grant", error_description: "redirect_uri mismatch" };
|
|
518
450
|
return;
|
|
519
451
|
}
|
|
520
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
452
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).update({
|
|
521
453
|
documentId: authCode.documentId,
|
|
522
454
|
data: { used: true }
|
|
523
455
|
});
|
|
@@ -525,7 +457,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
525
457
|
const refreshToken = randomBytes(32).toString("hex");
|
|
526
458
|
const expiresIn = 3600;
|
|
527
459
|
const refreshExpiresIn = 30 * 24 * 3600;
|
|
528
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
460
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).create({
|
|
529
461
|
data: {
|
|
530
462
|
accessToken,
|
|
531
463
|
refreshToken,
|
|
@@ -548,7 +480,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
548
480
|
ctx.body = { error: "invalid_request", error_description: "refresh_token is required" };
|
|
549
481
|
return;
|
|
550
482
|
}
|
|
551
|
-
const token = await strapi.documents(`${PLUGIN_UID$
|
|
483
|
+
const token = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).findFirst({
|
|
552
484
|
filters: { refreshToken, clientId: client.clientId, revoked: false }
|
|
553
485
|
});
|
|
554
486
|
if (!token) {
|
|
@@ -561,7 +493,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
561
493
|
ctx.body = { error: "invalid_grant", error_description: "Refresh token expired" };
|
|
562
494
|
return;
|
|
563
495
|
}
|
|
564
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
496
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).update({
|
|
565
497
|
documentId: token.documentId,
|
|
566
498
|
data: { revoked: true }
|
|
567
499
|
});
|
|
@@ -569,7 +501,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
569
501
|
const newRefreshToken = randomBytes(32).toString("hex");
|
|
570
502
|
const expiresIn = 3600;
|
|
571
503
|
const refreshExpiresIn = 30 * 24 * 3600;
|
|
572
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
504
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).create({
|
|
573
505
|
data: {
|
|
574
506
|
accessToken: newAccessToken,
|
|
575
507
|
refreshToken: newRefreshToken,
|
|
@@ -646,14 +578,14 @@ const routes = {
|
|
|
646
578
|
routes: [...admin]
|
|
647
579
|
}
|
|
648
580
|
};
|
|
649
|
-
const PLUGIN_UID
|
|
581
|
+
const PLUGIN_UID = `plugin::${PLUGIN_ID}`;
|
|
650
582
|
const oauthService = ({ strapi }) => ({
|
|
651
583
|
/**
|
|
652
584
|
* Validate an OAuth access token
|
|
653
585
|
*/
|
|
654
586
|
async validateToken(accessToken) {
|
|
655
587
|
try {
|
|
656
|
-
const tokenRecord = await strapi.documents(`${PLUGIN_UID
|
|
588
|
+
const tokenRecord = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findFirst({
|
|
657
589
|
filters: { accessToken, revoked: false }
|
|
658
590
|
});
|
|
659
591
|
if (!tokenRecord) {
|
|
@@ -662,7 +594,7 @@ const oauthService = ({ strapi }) => ({
|
|
|
662
594
|
if (new Date(tokenRecord.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
663
595
|
return { valid: false, error: "Token expired" };
|
|
664
596
|
}
|
|
665
|
-
const client = await strapi.documents(`${PLUGIN_UID
|
|
597
|
+
const client = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-client`).findFirst({
|
|
666
598
|
filters: { clientId: tokenRecord.clientId }
|
|
667
599
|
});
|
|
668
600
|
if (!client) {
|
|
@@ -682,11 +614,11 @@ const oauthService = ({ strapi }) => ({
|
|
|
682
614
|
* Revoke all tokens for a client
|
|
683
615
|
*/
|
|
684
616
|
async revokeClientTokens(clientId) {
|
|
685
|
-
const tokens = await strapi.documents(`${PLUGIN_UID
|
|
617
|
+
const tokens = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findMany({
|
|
686
618
|
filters: { clientId, revoked: false }
|
|
687
619
|
});
|
|
688
620
|
for (const token of tokens) {
|
|
689
|
-
await strapi.documents(`${PLUGIN_UID
|
|
621
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).update({
|
|
690
622
|
documentId: token.documentId,
|
|
691
623
|
data: { revoked: true }
|
|
692
624
|
});
|
|
@@ -698,19 +630,19 @@ const oauthService = ({ strapi }) => ({
|
|
|
698
630
|
*/
|
|
699
631
|
async cleanupExpired() {
|
|
700
632
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
701
|
-
const expiredCodes = await strapi.documents(`${PLUGIN_UID
|
|
633
|
+
const expiredCodes = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-code`).findMany({
|
|
702
634
|
filters: { expiresAt: { $lt: now } }
|
|
703
635
|
});
|
|
704
636
|
for (const code of expiredCodes) {
|
|
705
|
-
await strapi.documents(`${PLUGIN_UID
|
|
637
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-code`).delete({
|
|
706
638
|
documentId: code.documentId
|
|
707
639
|
});
|
|
708
640
|
}
|
|
709
|
-
const expiredTokens = await strapi.documents(`${PLUGIN_UID
|
|
641
|
+
const expiredTokens = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findMany({
|
|
710
642
|
filters: { refreshExpiresAt: { $lt: now } }
|
|
711
643
|
});
|
|
712
644
|
for (const token of expiredTokens) {
|
|
713
|
-
await strapi.documents(`${PLUGIN_UID
|
|
645
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).delete({
|
|
714
646
|
documentId: token.documentId
|
|
715
647
|
});
|
|
716
648
|
}
|
|
@@ -720,80 +652,8 @@ const oauthService = ({ strapi }) => ({
|
|
|
720
652
|
};
|
|
721
653
|
}
|
|
722
654
|
});
|
|
723
|
-
const PLUGIN_UID = `plugin::${PLUGIN_ID}`;
|
|
724
|
-
const endpointService = ({ strapi }) => ({
|
|
725
|
-
/**
|
|
726
|
-
* Register an MCP endpoint for OAuth protection
|
|
727
|
-
*/
|
|
728
|
-
async register(endpoint) {
|
|
729
|
-
const existing = await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findFirst({
|
|
730
|
-
filters: { path: endpoint.path }
|
|
731
|
-
});
|
|
732
|
-
if (existing) {
|
|
733
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).update({
|
|
734
|
-
documentId: existing.documentId,
|
|
735
|
-
data: {
|
|
736
|
-
name: endpoint.name,
|
|
737
|
-
pluginId: endpoint.pluginId,
|
|
738
|
-
description: endpoint.description,
|
|
739
|
-
active: true
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).create({
|
|
744
|
-
data: {
|
|
745
|
-
name: endpoint.name,
|
|
746
|
-
pluginId: endpoint.pluginId,
|
|
747
|
-
path: endpoint.path,
|
|
748
|
-
description: endpoint.description,
|
|
749
|
-
active: true
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
},
|
|
753
|
-
/**
|
|
754
|
-
* Unregister an MCP endpoint
|
|
755
|
-
*/
|
|
756
|
-
async unregister(path) {
|
|
757
|
-
const endpoint = await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findFirst({
|
|
758
|
-
filters: { path }
|
|
759
|
-
});
|
|
760
|
-
if (!endpoint) {
|
|
761
|
-
return false;
|
|
762
|
-
}
|
|
763
|
-
await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).update({
|
|
764
|
-
documentId: endpoint.documentId,
|
|
765
|
-
data: { active: false }
|
|
766
|
-
});
|
|
767
|
-
return true;
|
|
768
|
-
},
|
|
769
|
-
/**
|
|
770
|
-
* Get all registered endpoints
|
|
771
|
-
*/
|
|
772
|
-
async getAll(activeOnly = true) {
|
|
773
|
-
const filters = activeOnly ? { active: true } : {};
|
|
774
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findMany({ filters });
|
|
775
|
-
},
|
|
776
|
-
/**
|
|
777
|
-
* Get endpoints for a specific plugin
|
|
778
|
-
*/
|
|
779
|
-
async getByPlugin(pluginId, activeOnly = true) {
|
|
780
|
-
const filters = { pluginId };
|
|
781
|
-
if (activeOnly) {
|
|
782
|
-
filters.active = true;
|
|
783
|
-
}
|
|
784
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findMany({ filters });
|
|
785
|
-
},
|
|
786
|
-
/**
|
|
787
|
-
* Check if a path is a protected endpoint
|
|
788
|
-
*/
|
|
789
|
-
async isProtected(path) {
|
|
790
|
-
const endpoints = await this.getAll(true);
|
|
791
|
-
return endpoints.some((e) => path.includes(e.path));
|
|
792
|
-
}
|
|
793
|
-
});
|
|
794
655
|
const services = {
|
|
795
|
-
oauth: oauthService
|
|
796
|
-
endpoint: endpointService
|
|
656
|
+
oauth: oauthService
|
|
797
657
|
};
|
|
798
658
|
const index = {
|
|
799
659
|
register,
|
|
@@ -159,50 +159,5 @@ declare const _default: {
|
|
|
159
159
|
};
|
|
160
160
|
};
|
|
161
161
|
};
|
|
162
|
-
'mcp-endpoint': {
|
|
163
|
-
schema: {
|
|
164
|
-
kind: string;
|
|
165
|
-
collectionName: string;
|
|
166
|
-
info: {
|
|
167
|
-
singularName: string;
|
|
168
|
-
pluralName: string;
|
|
169
|
-
displayName: string;
|
|
170
|
-
description: string;
|
|
171
|
-
};
|
|
172
|
-
options: {
|
|
173
|
-
draftAndPublish: boolean;
|
|
174
|
-
};
|
|
175
|
-
pluginOptions: {
|
|
176
|
-
"content-manager": {
|
|
177
|
-
visible: boolean;
|
|
178
|
-
};
|
|
179
|
-
"content-type-builder": {
|
|
180
|
-
visible: boolean;
|
|
181
|
-
};
|
|
182
|
-
};
|
|
183
|
-
attributes: {
|
|
184
|
-
name: {
|
|
185
|
-
type: string;
|
|
186
|
-
required: boolean;
|
|
187
|
-
};
|
|
188
|
-
pluginId: {
|
|
189
|
-
type: string;
|
|
190
|
-
required: boolean;
|
|
191
|
-
};
|
|
192
|
-
path: {
|
|
193
|
-
type: string;
|
|
194
|
-
required: boolean;
|
|
195
|
-
unique: boolean;
|
|
196
|
-
};
|
|
197
|
-
description: {
|
|
198
|
-
type: string;
|
|
199
|
-
};
|
|
200
|
-
active: {
|
|
201
|
-
type: string;
|
|
202
|
-
default: boolean;
|
|
203
|
-
};
|
|
204
|
-
};
|
|
205
|
-
};
|
|
206
|
-
};
|
|
207
162
|
};
|
|
208
163
|
export default _default;
|
|
@@ -15,6 +15,8 @@ declare const oauthController: ({ strapi }: {
|
|
|
15
15
|
/**
|
|
16
16
|
* OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
17
17
|
* GET /.well-known/oauth-protected-resource
|
|
18
|
+
*
|
|
19
|
+
* Convention-based protection: all /api/{plugin}/mcp endpoints are protected.
|
|
18
20
|
*/
|
|
19
21
|
protectedResource(ctx: any): Promise<void>;
|
|
20
22
|
/**
|
|
@@ -51,15 +51,6 @@ declare const _default: {
|
|
|
51
51
|
codes: number;
|
|
52
52
|
}>;
|
|
53
53
|
};
|
|
54
|
-
endpoint: ({ strapi }: {
|
|
55
|
-
strapi: import("@strapi/types/dist/core").Strapi;
|
|
56
|
-
}) => {
|
|
57
|
-
register(endpoint: import("./services/endpoint").EndpointRegistration): Promise<any>;
|
|
58
|
-
unregister(path: string): Promise<boolean>;
|
|
59
|
-
getAll(activeOnly?: boolean): Promise<any[]>;
|
|
60
|
-
getByPlugin(pluginId: string, activeOnly?: boolean): Promise<any[]>;
|
|
61
|
-
isProtected(path: string): Promise<boolean>;
|
|
62
|
-
};
|
|
63
54
|
};
|
|
64
55
|
contentTypes: {
|
|
65
56
|
'mcp-oauth-client': {
|
|
@@ -222,51 +213,6 @@ declare const _default: {
|
|
|
222
213
|
};
|
|
223
214
|
};
|
|
224
215
|
};
|
|
225
|
-
'mcp-endpoint': {
|
|
226
|
-
schema: {
|
|
227
|
-
kind: string;
|
|
228
|
-
collectionName: string;
|
|
229
|
-
info: {
|
|
230
|
-
singularName: string;
|
|
231
|
-
pluralName: string;
|
|
232
|
-
displayName: string;
|
|
233
|
-
description: string;
|
|
234
|
-
};
|
|
235
|
-
options: {
|
|
236
|
-
draftAndPublish: boolean;
|
|
237
|
-
};
|
|
238
|
-
pluginOptions: {
|
|
239
|
-
"content-manager": {
|
|
240
|
-
visible: boolean;
|
|
241
|
-
};
|
|
242
|
-
"content-type-builder": {
|
|
243
|
-
visible: boolean;
|
|
244
|
-
};
|
|
245
|
-
};
|
|
246
|
-
attributes: {
|
|
247
|
-
name: {
|
|
248
|
-
type: string;
|
|
249
|
-
required: boolean;
|
|
250
|
-
};
|
|
251
|
-
pluginId: {
|
|
252
|
-
type: string;
|
|
253
|
-
required: boolean;
|
|
254
|
-
};
|
|
255
|
-
path: {
|
|
256
|
-
type: string;
|
|
257
|
-
required: boolean;
|
|
258
|
-
unique: boolean;
|
|
259
|
-
};
|
|
260
|
-
description: {
|
|
261
|
-
type: string;
|
|
262
|
-
};
|
|
263
|
-
active: {
|
|
264
|
-
type: string;
|
|
265
|
-
default: boolean;
|
|
266
|
-
};
|
|
267
|
-
};
|
|
268
|
-
};
|
|
269
|
-
};
|
|
270
216
|
};
|
|
271
217
|
policies: {};
|
|
272
218
|
middlewares: {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP OAuth Authentication Middleware
|
|
3
3
|
*
|
|
4
|
-
* This middleware provides OAuth 2.0 authentication for all
|
|
4
|
+
* This middleware provides OAuth 2.0 authentication for all MCP endpoints.
|
|
5
|
+
* Uses convention-based protection: any route matching /api/{plugin}/mcp is protected.
|
|
6
|
+
*
|
|
5
7
|
* It supports dual authentication:
|
|
6
8
|
* - OAuth 2.0 tokens (for ChatGPT and other OAuth clients)
|
|
7
9
|
* - Direct Strapi API tokens (for Claude Desktop and scripts)
|
|
@@ -9,14 +9,5 @@ declare const _default: {
|
|
|
9
9
|
codes: number;
|
|
10
10
|
}>;
|
|
11
11
|
};
|
|
12
|
-
endpoint: ({ strapi }: {
|
|
13
|
-
strapi: import("@strapi/types/dist/core").Strapi;
|
|
14
|
-
}) => {
|
|
15
|
-
register(endpoint: import("./endpoint").EndpointRegistration): Promise<any>;
|
|
16
|
-
unregister(path: string): Promise<boolean>;
|
|
17
|
-
getAll(activeOnly?: boolean): Promise<any[]>;
|
|
18
|
-
getByPlugin(pluginId: string, activeOnly?: boolean): Promise<any[]>;
|
|
19
|
-
isProtected(path: string): Promise<boolean>;
|
|
20
|
-
};
|
|
21
12
|
};
|
|
22
13
|
export default _default;
|
package/package.json
CHANGED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Endpoint Registration Service
|
|
3
|
-
*
|
|
4
|
-
* Allows MCP plugins to register their endpoints for OAuth protection.
|
|
5
|
-
*/
|
|
6
|
-
import type { Core } from '@strapi/strapi';
|
|
7
|
-
export interface EndpointRegistration {
|
|
8
|
-
name: string;
|
|
9
|
-
pluginId: string;
|
|
10
|
-
path: string;
|
|
11
|
-
description?: string;
|
|
12
|
-
}
|
|
13
|
-
declare const endpointService: ({ strapi }: {
|
|
14
|
-
strapi: Core.Strapi;
|
|
15
|
-
}) => {
|
|
16
|
-
/**
|
|
17
|
-
* Register an MCP endpoint for OAuth protection
|
|
18
|
-
*/
|
|
19
|
-
register(endpoint: EndpointRegistration): Promise<any>;
|
|
20
|
-
/**
|
|
21
|
-
* Unregister an MCP endpoint
|
|
22
|
-
*/
|
|
23
|
-
unregister(path: string): Promise<boolean>;
|
|
24
|
-
/**
|
|
25
|
-
* Get all registered endpoints
|
|
26
|
-
*/
|
|
27
|
-
getAll(activeOnly?: boolean): Promise<any[]>;
|
|
28
|
-
/**
|
|
29
|
-
* Get endpoints for a specific plugin
|
|
30
|
-
*/
|
|
31
|
-
getByPlugin(pluginId: string, activeOnly?: boolean): Promise<any[]>;
|
|
32
|
-
/**
|
|
33
|
-
* Check if a path is a protected endpoint
|
|
34
|
-
*/
|
|
35
|
-
isProtected(path: string): Promise<boolean>;
|
|
36
|
-
};
|
|
37
|
-
export default endpointService;
|