strapi-oauth-mcp-manager 0.1.3 → 0.1.5
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 +58 -182
- package/dist/server/index.mjs +58 -182
- 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/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,7 @@
|
|
|
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
5
|
const MCP_ENDPOINT_PATTERN = /^\/api\/[^/]+\/mcp(\/.*)?$/;
|
|
6
6
|
function extractBearerToken(authHeader) {
|
|
7
7
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
@@ -22,7 +22,7 @@ function buildWwwAuthenticateHeader(ctx, strapi) {
|
|
|
22
22
|
}
|
|
23
23
|
async function validateOAuthToken(token, strapi) {
|
|
24
24
|
try {
|
|
25
|
-
const tokenRecord = await strapi.documents(`${PLUGIN_UID$
|
|
25
|
+
const tokenRecord = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-token`).findFirst({
|
|
26
26
|
filters: { accessToken: token, revoked: false }
|
|
27
27
|
});
|
|
28
28
|
if (!tokenRecord) {
|
|
@@ -31,7 +31,7 @@ async function validateOAuthToken(token, strapi) {
|
|
|
31
31
|
if (new Date(tokenRecord.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
32
32
|
return { valid: false, error: "Token expired" };
|
|
33
33
|
}
|
|
34
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
34
|
+
const client = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-client`).findFirst({
|
|
35
35
|
filters: { clientId: tokenRecord.clientId }
|
|
36
36
|
});
|
|
37
37
|
if (!client) {
|
|
@@ -93,18 +93,18 @@ const config = {
|
|
|
93
93
|
validator() {
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
|
-
const kind$
|
|
97
|
-
const collectionName$
|
|
98
|
-
const info$
|
|
96
|
+
const kind$2 = "collectionType";
|
|
97
|
+
const collectionName$2 = "mcp_oauth_clients";
|
|
98
|
+
const info$2 = {
|
|
99
99
|
singularName: "mcp-oauth-client",
|
|
100
100
|
pluralName: "mcp-oauth-clients",
|
|
101
101
|
displayName: "MCP OAuth Client",
|
|
102
102
|
description: "OAuth 2.0 clients for MCP authentication"
|
|
103
103
|
};
|
|
104
|
-
const options$
|
|
104
|
+
const options$2 = {
|
|
105
105
|
draftAndPublish: false
|
|
106
106
|
};
|
|
107
|
-
const pluginOptions$
|
|
107
|
+
const pluginOptions$2 = {
|
|
108
108
|
"content-manager": {
|
|
109
109
|
visible: true
|
|
110
110
|
},
|
|
@@ -112,7 +112,7 @@ const pluginOptions$3 = {
|
|
|
112
112
|
visible: true
|
|
113
113
|
}
|
|
114
114
|
};
|
|
115
|
-
const attributes$
|
|
115
|
+
const attributes$2 = {
|
|
116
116
|
name: {
|
|
117
117
|
type: "string",
|
|
118
118
|
required: true
|
|
@@ -132,7 +132,7 @@ const attributes$3 = {
|
|
|
132
132
|
required: true
|
|
133
133
|
},
|
|
134
134
|
strapiApiToken: {
|
|
135
|
-
type: "
|
|
135
|
+
type: "text",
|
|
136
136
|
required: true,
|
|
137
137
|
"private": true
|
|
138
138
|
},
|
|
@@ -145,25 +145,25 @@ const attributes$3 = {
|
|
|
145
145
|
}
|
|
146
146
|
};
|
|
147
147
|
const mcpOauthClient = {
|
|
148
|
-
kind: kind$
|
|
149
|
-
collectionName: collectionName$
|
|
150
|
-
info: info$
|
|
151
|
-
options: options$
|
|
152
|
-
pluginOptions: pluginOptions$
|
|
153
|
-
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
|
|
154
154
|
};
|
|
155
|
-
const kind$
|
|
156
|
-
const collectionName$
|
|
157
|
-
const info$
|
|
155
|
+
const kind$1 = "collectionType";
|
|
156
|
+
const collectionName$1 = "mcp_oauth_codes";
|
|
157
|
+
const info$1 = {
|
|
158
158
|
singularName: "mcp-oauth-code",
|
|
159
159
|
pluralName: "mcp-oauth-codes",
|
|
160
160
|
displayName: "MCP OAuth Code",
|
|
161
161
|
description: "OAuth 2.0 authorization codes"
|
|
162
162
|
};
|
|
163
|
-
const options$
|
|
163
|
+
const options$1 = {
|
|
164
164
|
draftAndPublish: false
|
|
165
165
|
};
|
|
166
|
-
const pluginOptions$
|
|
166
|
+
const pluginOptions$1 = {
|
|
167
167
|
"content-manager": {
|
|
168
168
|
visible: false
|
|
169
169
|
},
|
|
@@ -171,7 +171,7 @@ const pluginOptions$2 = {
|
|
|
171
171
|
visible: false
|
|
172
172
|
}
|
|
173
173
|
};
|
|
174
|
-
const attributes$
|
|
174
|
+
const attributes$1 = {
|
|
175
175
|
code: {
|
|
176
176
|
type: "string",
|
|
177
177
|
required: true,
|
|
@@ -201,25 +201,25 @@ const attributes$2 = {
|
|
|
201
201
|
}
|
|
202
202
|
};
|
|
203
203
|
const mcpOauthCode = {
|
|
204
|
-
kind: kind$
|
|
205
|
-
collectionName: collectionName$
|
|
206
|
-
info: info$
|
|
207
|
-
options: options$
|
|
208
|
-
pluginOptions: pluginOptions$
|
|
209
|
-
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
|
|
210
210
|
};
|
|
211
|
-
const kind
|
|
212
|
-
const collectionName
|
|
213
|
-
const info
|
|
211
|
+
const kind = "collectionType";
|
|
212
|
+
const collectionName = "mcp_oauth_tokens";
|
|
213
|
+
const info = {
|
|
214
214
|
singularName: "mcp-oauth-token",
|
|
215
215
|
pluralName: "mcp-oauth-tokens",
|
|
216
216
|
displayName: "MCP OAuth Token",
|
|
217
217
|
description: "OAuth 2.0 access and refresh tokens"
|
|
218
218
|
};
|
|
219
|
-
const options
|
|
219
|
+
const options = {
|
|
220
220
|
draftAndPublish: false
|
|
221
221
|
};
|
|
222
|
-
const pluginOptions
|
|
222
|
+
const pluginOptions = {
|
|
223
223
|
"content-manager": {
|
|
224
224
|
visible: false
|
|
225
225
|
},
|
|
@@ -227,7 +227,7 @@ const pluginOptions$1 = {
|
|
|
227
227
|
visible: false
|
|
228
228
|
}
|
|
229
229
|
};
|
|
230
|
-
const attributes
|
|
230
|
+
const attributes = {
|
|
231
231
|
accessToken: {
|
|
232
232
|
type: "string",
|
|
233
233
|
required: true,
|
|
@@ -258,55 +258,6 @@ const attributes$1 = {
|
|
|
258
258
|
}
|
|
259
259
|
};
|
|
260
260
|
const mcpOauthToken = {
|
|
261
|
-
kind: kind$1,
|
|
262
|
-
collectionName: collectionName$1,
|
|
263
|
-
info: info$1,
|
|
264
|
-
options: options$1,
|
|
265
|
-
pluginOptions: pluginOptions$1,
|
|
266
|
-
attributes: attributes$1
|
|
267
|
-
};
|
|
268
|
-
const kind = "collectionType";
|
|
269
|
-
const collectionName = "mcp_endpoints";
|
|
270
|
-
const info = {
|
|
271
|
-
singularName: "mcp-endpoint",
|
|
272
|
-
pluralName: "mcp-endpoints",
|
|
273
|
-
displayName: "MCP Endpoint",
|
|
274
|
-
description: "Registered MCP endpoints protected by OAuth"
|
|
275
|
-
};
|
|
276
|
-
const options = {
|
|
277
|
-
draftAndPublish: false
|
|
278
|
-
};
|
|
279
|
-
const pluginOptions = {
|
|
280
|
-
"content-manager": {
|
|
281
|
-
visible: true
|
|
282
|
-
},
|
|
283
|
-
"content-type-builder": {
|
|
284
|
-
visible: true
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
const attributes = {
|
|
288
|
-
name: {
|
|
289
|
-
type: "string",
|
|
290
|
-
required: true
|
|
291
|
-
},
|
|
292
|
-
pluginId: {
|
|
293
|
-
type: "string",
|
|
294
|
-
required: true
|
|
295
|
-
},
|
|
296
|
-
path: {
|
|
297
|
-
type: "string",
|
|
298
|
-
required: true,
|
|
299
|
-
unique: true
|
|
300
|
-
},
|
|
301
|
-
description: {
|
|
302
|
-
type: "text"
|
|
303
|
-
},
|
|
304
|
-
active: {
|
|
305
|
-
type: "boolean",
|
|
306
|
-
"default": true
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
const mcpEndpoint = {
|
|
310
261
|
kind,
|
|
311
262
|
collectionName,
|
|
312
263
|
info,
|
|
@@ -317,10 +268,9 @@ const mcpEndpoint = {
|
|
|
317
268
|
const contentTypes = {
|
|
318
269
|
"mcp-oauth-client": { schema: mcpOauthClient },
|
|
319
270
|
"mcp-oauth-code": { schema: mcpOauthCode },
|
|
320
|
-
"mcp-oauth-token": { schema: mcpOauthToken }
|
|
321
|
-
"mcp-endpoint": { schema: mcpEndpoint }
|
|
271
|
+
"mcp-oauth-token": { schema: mcpOauthToken }
|
|
322
272
|
};
|
|
323
|
-
const PLUGIN_UID$
|
|
273
|
+
const PLUGIN_UID$1 = `plugin::${PLUGIN_ID}`;
|
|
324
274
|
function getBaseUrl(ctx, strapi) {
|
|
325
275
|
const forwardedProto = ctx.request.headers["x-forwarded-proto"] || ctx.protocol;
|
|
326
276
|
const forwardedHost = ctx.request.headers["x-forwarded-host"] || ctx.request.headers["host"];
|
|
@@ -357,17 +307,15 @@ const oauthController = ({ strapi }) => ({
|
|
|
357
307
|
/**
|
|
358
308
|
* OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
359
309
|
* GET /.well-known/oauth-protected-resource
|
|
310
|
+
*
|
|
311
|
+
* Convention-based protection: all /api/{plugin}/mcp endpoints are protected.
|
|
360
312
|
*/
|
|
361
313
|
async protectedResource(ctx) {
|
|
362
314
|
const baseUrl = getBaseUrl(ctx, strapi);
|
|
363
315
|
const pluginPath = `/api/${PLUGIN_ID}`;
|
|
364
316
|
const authServer = `${baseUrl}${pluginPath}`;
|
|
365
|
-
const endpoints = await strapi.documents(`${PLUGIN_UID$2}.mcp-endpoint`).findMany({
|
|
366
|
-
filters: { active: true }
|
|
367
|
-
});
|
|
368
|
-
const resources = endpoints.map((e) => `${baseUrl}${e.path}`);
|
|
369
317
|
ctx.body = {
|
|
370
|
-
resource:
|
|
318
|
+
resource: `${baseUrl}/api`,
|
|
371
319
|
authorization_servers: [authServer],
|
|
372
320
|
bearer_methods_supported: ["header"]
|
|
373
321
|
};
|
|
@@ -393,7 +341,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
393
341
|
ctx.body = { error: "unsupported_response_type", error_description: "Only code response type is supported" };
|
|
394
342
|
return;
|
|
395
343
|
}
|
|
396
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
344
|
+
const client = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-client`).findFirst({
|
|
397
345
|
filters: { clientId: client_id, active: true }
|
|
398
346
|
});
|
|
399
347
|
if (!client) {
|
|
@@ -422,7 +370,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
422
370
|
}
|
|
423
371
|
const code = node_crypto.randomBytes(32).toString("hex");
|
|
424
372
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
425
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
373
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).create({
|
|
426
374
|
data: {
|
|
427
375
|
code,
|
|
428
376
|
clientId: client_id,
|
|
@@ -460,7 +408,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
460
408
|
ctx.body = { error: "invalid_client", error_description: "Client authentication required" };
|
|
461
409
|
return;
|
|
462
410
|
}
|
|
463
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
411
|
+
const client = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-client`).findFirst({
|
|
464
412
|
filters: { clientId: authClientId, active: true }
|
|
465
413
|
});
|
|
466
414
|
if (!client || authClientSecret && client.clientSecret !== authClientSecret) {
|
|
@@ -484,7 +432,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
484
432
|
ctx.body = { error: "invalid_request", error_description: "code is required" };
|
|
485
433
|
return;
|
|
486
434
|
}
|
|
487
|
-
const authCode = await strapi.documents(`${PLUGIN_UID$
|
|
435
|
+
const authCode = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).findFirst({
|
|
488
436
|
filters: { code, clientId: client.clientId, used: false }
|
|
489
437
|
});
|
|
490
438
|
if (!authCode) {
|
|
@@ -502,7 +450,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
502
450
|
ctx.body = { error: "invalid_grant", error_description: "redirect_uri mismatch" };
|
|
503
451
|
return;
|
|
504
452
|
}
|
|
505
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
453
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).update({
|
|
506
454
|
documentId: authCode.documentId,
|
|
507
455
|
data: { used: true }
|
|
508
456
|
});
|
|
@@ -510,7 +458,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
510
458
|
const refreshToken = node_crypto.randomBytes(32).toString("hex");
|
|
511
459
|
const expiresIn = 3600;
|
|
512
460
|
const refreshExpiresIn = 30 * 24 * 3600;
|
|
513
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
461
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).create({
|
|
514
462
|
data: {
|
|
515
463
|
accessToken,
|
|
516
464
|
refreshToken,
|
|
@@ -533,7 +481,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
533
481
|
ctx.body = { error: "invalid_request", error_description: "refresh_token is required" };
|
|
534
482
|
return;
|
|
535
483
|
}
|
|
536
|
-
const token = await strapi.documents(`${PLUGIN_UID$
|
|
484
|
+
const token = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).findFirst({
|
|
537
485
|
filters: { refreshToken, clientId: client.clientId, revoked: false }
|
|
538
486
|
});
|
|
539
487
|
if (!token) {
|
|
@@ -546,7 +494,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
546
494
|
ctx.body = { error: "invalid_grant", error_description: "Refresh token expired" };
|
|
547
495
|
return;
|
|
548
496
|
}
|
|
549
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
497
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).update({
|
|
550
498
|
documentId: token.documentId,
|
|
551
499
|
data: { revoked: true }
|
|
552
500
|
});
|
|
@@ -554,7 +502,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
554
502
|
const newRefreshToken = node_crypto.randomBytes(32).toString("hex");
|
|
555
503
|
const expiresIn = 3600;
|
|
556
504
|
const refreshExpiresIn = 30 * 24 * 3600;
|
|
557
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
505
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).create({
|
|
558
506
|
data: {
|
|
559
507
|
accessToken: newAccessToken,
|
|
560
508
|
refreshToken: newRefreshToken,
|
|
@@ -631,14 +579,14 @@ const routes = {
|
|
|
631
579
|
routes: [...admin]
|
|
632
580
|
}
|
|
633
581
|
};
|
|
634
|
-
const PLUGIN_UID
|
|
582
|
+
const PLUGIN_UID = `plugin::${PLUGIN_ID}`;
|
|
635
583
|
const oauthService = ({ strapi }) => ({
|
|
636
584
|
/**
|
|
637
585
|
* Validate an OAuth access token
|
|
638
586
|
*/
|
|
639
587
|
async validateToken(accessToken) {
|
|
640
588
|
try {
|
|
641
|
-
const tokenRecord = await strapi.documents(`${PLUGIN_UID
|
|
589
|
+
const tokenRecord = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findFirst({
|
|
642
590
|
filters: { accessToken, revoked: false }
|
|
643
591
|
});
|
|
644
592
|
if (!tokenRecord) {
|
|
@@ -647,7 +595,7 @@ const oauthService = ({ strapi }) => ({
|
|
|
647
595
|
if (new Date(tokenRecord.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
648
596
|
return { valid: false, error: "Token expired" };
|
|
649
597
|
}
|
|
650
|
-
const client = await strapi.documents(`${PLUGIN_UID
|
|
598
|
+
const client = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-client`).findFirst({
|
|
651
599
|
filters: { clientId: tokenRecord.clientId }
|
|
652
600
|
});
|
|
653
601
|
if (!client) {
|
|
@@ -667,11 +615,11 @@ const oauthService = ({ strapi }) => ({
|
|
|
667
615
|
* Revoke all tokens for a client
|
|
668
616
|
*/
|
|
669
617
|
async revokeClientTokens(clientId) {
|
|
670
|
-
const tokens = await strapi.documents(`${PLUGIN_UID
|
|
618
|
+
const tokens = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findMany({
|
|
671
619
|
filters: { clientId, revoked: false }
|
|
672
620
|
});
|
|
673
621
|
for (const token of tokens) {
|
|
674
|
-
await strapi.documents(`${PLUGIN_UID
|
|
622
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).update({
|
|
675
623
|
documentId: token.documentId,
|
|
676
624
|
data: { revoked: true }
|
|
677
625
|
});
|
|
@@ -683,19 +631,19 @@ const oauthService = ({ strapi }) => ({
|
|
|
683
631
|
*/
|
|
684
632
|
async cleanupExpired() {
|
|
685
633
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
686
|
-
const expiredCodes = await strapi.documents(`${PLUGIN_UID
|
|
634
|
+
const expiredCodes = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-code`).findMany({
|
|
687
635
|
filters: { expiresAt: { $lt: now } }
|
|
688
636
|
});
|
|
689
637
|
for (const code of expiredCodes) {
|
|
690
|
-
await strapi.documents(`${PLUGIN_UID
|
|
638
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-code`).delete({
|
|
691
639
|
documentId: code.documentId
|
|
692
640
|
});
|
|
693
641
|
}
|
|
694
|
-
const expiredTokens = await strapi.documents(`${PLUGIN_UID
|
|
642
|
+
const expiredTokens = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findMany({
|
|
695
643
|
filters: { refreshExpiresAt: { $lt: now } }
|
|
696
644
|
});
|
|
697
645
|
for (const token of expiredTokens) {
|
|
698
|
-
await strapi.documents(`${PLUGIN_UID
|
|
646
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).delete({
|
|
699
647
|
documentId: token.documentId
|
|
700
648
|
});
|
|
701
649
|
}
|
|
@@ -705,80 +653,8 @@ const oauthService = ({ strapi }) => ({
|
|
|
705
653
|
};
|
|
706
654
|
}
|
|
707
655
|
});
|
|
708
|
-
const PLUGIN_UID = `plugin::${PLUGIN_ID}`;
|
|
709
|
-
const endpointService = ({ strapi }) => ({
|
|
710
|
-
/**
|
|
711
|
-
* Register an MCP endpoint for OAuth protection
|
|
712
|
-
*/
|
|
713
|
-
async register(endpoint) {
|
|
714
|
-
const existing = await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findFirst({
|
|
715
|
-
filters: { path: endpoint.path }
|
|
716
|
-
});
|
|
717
|
-
if (existing) {
|
|
718
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).update({
|
|
719
|
-
documentId: existing.documentId,
|
|
720
|
-
data: {
|
|
721
|
-
name: endpoint.name,
|
|
722
|
-
pluginId: endpoint.pluginId,
|
|
723
|
-
description: endpoint.description,
|
|
724
|
-
active: true
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).create({
|
|
729
|
-
data: {
|
|
730
|
-
name: endpoint.name,
|
|
731
|
-
pluginId: endpoint.pluginId,
|
|
732
|
-
path: endpoint.path,
|
|
733
|
-
description: endpoint.description,
|
|
734
|
-
active: true
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
},
|
|
738
|
-
/**
|
|
739
|
-
* Unregister an MCP endpoint
|
|
740
|
-
*/
|
|
741
|
-
async unregister(path) {
|
|
742
|
-
const endpoint = await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findFirst({
|
|
743
|
-
filters: { path }
|
|
744
|
-
});
|
|
745
|
-
if (!endpoint) {
|
|
746
|
-
return false;
|
|
747
|
-
}
|
|
748
|
-
await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).update({
|
|
749
|
-
documentId: endpoint.documentId,
|
|
750
|
-
data: { active: false }
|
|
751
|
-
});
|
|
752
|
-
return true;
|
|
753
|
-
},
|
|
754
|
-
/**
|
|
755
|
-
* Get all registered endpoints
|
|
756
|
-
*/
|
|
757
|
-
async getAll(activeOnly = true) {
|
|
758
|
-
const filters = activeOnly ? { active: true } : {};
|
|
759
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findMany({ filters });
|
|
760
|
-
},
|
|
761
|
-
/**
|
|
762
|
-
* Get endpoints for a specific plugin
|
|
763
|
-
*/
|
|
764
|
-
async getByPlugin(pluginId, activeOnly = true) {
|
|
765
|
-
const filters = { pluginId };
|
|
766
|
-
if (activeOnly) {
|
|
767
|
-
filters.active = true;
|
|
768
|
-
}
|
|
769
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findMany({ filters });
|
|
770
|
-
},
|
|
771
|
-
/**
|
|
772
|
-
* Check if a path is a protected endpoint
|
|
773
|
-
*/
|
|
774
|
-
async isProtected(path) {
|
|
775
|
-
const endpoints = await this.getAll(true);
|
|
776
|
-
return endpoints.some((e) => path.includes(e.path));
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
656
|
const services = {
|
|
780
|
-
oauth: oauthService
|
|
781
|
-
endpoint: endpointService
|
|
657
|
+
oauth: oauthService
|
|
782
658
|
};
|
|
783
659
|
const index = {
|
|
784
660
|
register,
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
4
|
const MCP_ENDPOINT_PATTERN = /^\/api\/[^/]+\/mcp(\/.*)?$/;
|
|
5
5
|
function extractBearerToken(authHeader) {
|
|
6
6
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
@@ -21,7 +21,7 @@ function buildWwwAuthenticateHeader(ctx, strapi) {
|
|
|
21
21
|
}
|
|
22
22
|
async function validateOAuthToken(token, strapi) {
|
|
23
23
|
try {
|
|
24
|
-
const tokenRecord = await strapi.documents(`${PLUGIN_UID$
|
|
24
|
+
const tokenRecord = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-token`).findFirst({
|
|
25
25
|
filters: { accessToken: token, revoked: false }
|
|
26
26
|
});
|
|
27
27
|
if (!tokenRecord) {
|
|
@@ -30,7 +30,7 @@ async function validateOAuthToken(token, strapi) {
|
|
|
30
30
|
if (new Date(tokenRecord.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
31
31
|
return { valid: false, error: "Token expired" };
|
|
32
32
|
}
|
|
33
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
33
|
+
const client = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-client`).findFirst({
|
|
34
34
|
filters: { clientId: tokenRecord.clientId }
|
|
35
35
|
});
|
|
36
36
|
if (!client) {
|
|
@@ -92,18 +92,18 @@ const config = {
|
|
|
92
92
|
validator() {
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
|
-
const kind$
|
|
96
|
-
const collectionName$
|
|
97
|
-
const info$
|
|
95
|
+
const kind$2 = "collectionType";
|
|
96
|
+
const collectionName$2 = "mcp_oauth_clients";
|
|
97
|
+
const info$2 = {
|
|
98
98
|
singularName: "mcp-oauth-client",
|
|
99
99
|
pluralName: "mcp-oauth-clients",
|
|
100
100
|
displayName: "MCP OAuth Client",
|
|
101
101
|
description: "OAuth 2.0 clients for MCP authentication"
|
|
102
102
|
};
|
|
103
|
-
const options$
|
|
103
|
+
const options$2 = {
|
|
104
104
|
draftAndPublish: false
|
|
105
105
|
};
|
|
106
|
-
const pluginOptions$
|
|
106
|
+
const pluginOptions$2 = {
|
|
107
107
|
"content-manager": {
|
|
108
108
|
visible: true
|
|
109
109
|
},
|
|
@@ -111,7 +111,7 @@ const pluginOptions$3 = {
|
|
|
111
111
|
visible: true
|
|
112
112
|
}
|
|
113
113
|
};
|
|
114
|
-
const attributes$
|
|
114
|
+
const attributes$2 = {
|
|
115
115
|
name: {
|
|
116
116
|
type: "string",
|
|
117
117
|
required: true
|
|
@@ -131,7 +131,7 @@ const attributes$3 = {
|
|
|
131
131
|
required: true
|
|
132
132
|
},
|
|
133
133
|
strapiApiToken: {
|
|
134
|
-
type: "
|
|
134
|
+
type: "text",
|
|
135
135
|
required: true,
|
|
136
136
|
"private": true
|
|
137
137
|
},
|
|
@@ -144,25 +144,25 @@ const attributes$3 = {
|
|
|
144
144
|
}
|
|
145
145
|
};
|
|
146
146
|
const mcpOauthClient = {
|
|
147
|
-
kind: kind$
|
|
148
|
-
collectionName: collectionName$
|
|
149
|
-
info: info$
|
|
150
|
-
options: options$
|
|
151
|
-
pluginOptions: pluginOptions$
|
|
152
|
-
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
|
|
153
153
|
};
|
|
154
|
-
const kind$
|
|
155
|
-
const collectionName$
|
|
156
|
-
const info$
|
|
154
|
+
const kind$1 = "collectionType";
|
|
155
|
+
const collectionName$1 = "mcp_oauth_codes";
|
|
156
|
+
const info$1 = {
|
|
157
157
|
singularName: "mcp-oauth-code",
|
|
158
158
|
pluralName: "mcp-oauth-codes",
|
|
159
159
|
displayName: "MCP OAuth Code",
|
|
160
160
|
description: "OAuth 2.0 authorization codes"
|
|
161
161
|
};
|
|
162
|
-
const options$
|
|
162
|
+
const options$1 = {
|
|
163
163
|
draftAndPublish: false
|
|
164
164
|
};
|
|
165
|
-
const pluginOptions$
|
|
165
|
+
const pluginOptions$1 = {
|
|
166
166
|
"content-manager": {
|
|
167
167
|
visible: false
|
|
168
168
|
},
|
|
@@ -170,7 +170,7 @@ const pluginOptions$2 = {
|
|
|
170
170
|
visible: false
|
|
171
171
|
}
|
|
172
172
|
};
|
|
173
|
-
const attributes$
|
|
173
|
+
const attributes$1 = {
|
|
174
174
|
code: {
|
|
175
175
|
type: "string",
|
|
176
176
|
required: true,
|
|
@@ -200,25 +200,25 @@ const attributes$2 = {
|
|
|
200
200
|
}
|
|
201
201
|
};
|
|
202
202
|
const mcpOauthCode = {
|
|
203
|
-
kind: kind$
|
|
204
|
-
collectionName: collectionName$
|
|
205
|
-
info: info$
|
|
206
|
-
options: options$
|
|
207
|
-
pluginOptions: pluginOptions$
|
|
208
|
-
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
|
|
209
209
|
};
|
|
210
|
-
const kind
|
|
211
|
-
const collectionName
|
|
212
|
-
const info
|
|
210
|
+
const kind = "collectionType";
|
|
211
|
+
const collectionName = "mcp_oauth_tokens";
|
|
212
|
+
const info = {
|
|
213
213
|
singularName: "mcp-oauth-token",
|
|
214
214
|
pluralName: "mcp-oauth-tokens",
|
|
215
215
|
displayName: "MCP OAuth Token",
|
|
216
216
|
description: "OAuth 2.0 access and refresh tokens"
|
|
217
217
|
};
|
|
218
|
-
const options
|
|
218
|
+
const options = {
|
|
219
219
|
draftAndPublish: false
|
|
220
220
|
};
|
|
221
|
-
const pluginOptions
|
|
221
|
+
const pluginOptions = {
|
|
222
222
|
"content-manager": {
|
|
223
223
|
visible: false
|
|
224
224
|
},
|
|
@@ -226,7 +226,7 @@ const pluginOptions$1 = {
|
|
|
226
226
|
visible: false
|
|
227
227
|
}
|
|
228
228
|
};
|
|
229
|
-
const attributes
|
|
229
|
+
const attributes = {
|
|
230
230
|
accessToken: {
|
|
231
231
|
type: "string",
|
|
232
232
|
required: true,
|
|
@@ -257,55 +257,6 @@ const attributes$1 = {
|
|
|
257
257
|
}
|
|
258
258
|
};
|
|
259
259
|
const mcpOauthToken = {
|
|
260
|
-
kind: kind$1,
|
|
261
|
-
collectionName: collectionName$1,
|
|
262
|
-
info: info$1,
|
|
263
|
-
options: options$1,
|
|
264
|
-
pluginOptions: pluginOptions$1,
|
|
265
|
-
attributes: attributes$1
|
|
266
|
-
};
|
|
267
|
-
const kind = "collectionType";
|
|
268
|
-
const collectionName = "mcp_endpoints";
|
|
269
|
-
const info = {
|
|
270
|
-
singularName: "mcp-endpoint",
|
|
271
|
-
pluralName: "mcp-endpoints",
|
|
272
|
-
displayName: "MCP Endpoint",
|
|
273
|
-
description: "Registered MCP endpoints protected by OAuth"
|
|
274
|
-
};
|
|
275
|
-
const options = {
|
|
276
|
-
draftAndPublish: false
|
|
277
|
-
};
|
|
278
|
-
const pluginOptions = {
|
|
279
|
-
"content-manager": {
|
|
280
|
-
visible: true
|
|
281
|
-
},
|
|
282
|
-
"content-type-builder": {
|
|
283
|
-
visible: true
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
const attributes = {
|
|
287
|
-
name: {
|
|
288
|
-
type: "string",
|
|
289
|
-
required: true
|
|
290
|
-
},
|
|
291
|
-
pluginId: {
|
|
292
|
-
type: "string",
|
|
293
|
-
required: true
|
|
294
|
-
},
|
|
295
|
-
path: {
|
|
296
|
-
type: "string",
|
|
297
|
-
required: true,
|
|
298
|
-
unique: true
|
|
299
|
-
},
|
|
300
|
-
description: {
|
|
301
|
-
type: "text"
|
|
302
|
-
},
|
|
303
|
-
active: {
|
|
304
|
-
type: "boolean",
|
|
305
|
-
"default": true
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
const mcpEndpoint = {
|
|
309
260
|
kind,
|
|
310
261
|
collectionName,
|
|
311
262
|
info,
|
|
@@ -316,10 +267,9 @@ const mcpEndpoint = {
|
|
|
316
267
|
const contentTypes = {
|
|
317
268
|
"mcp-oauth-client": { schema: mcpOauthClient },
|
|
318
269
|
"mcp-oauth-code": { schema: mcpOauthCode },
|
|
319
|
-
"mcp-oauth-token": { schema: mcpOauthToken }
|
|
320
|
-
"mcp-endpoint": { schema: mcpEndpoint }
|
|
270
|
+
"mcp-oauth-token": { schema: mcpOauthToken }
|
|
321
271
|
};
|
|
322
|
-
const PLUGIN_UID$
|
|
272
|
+
const PLUGIN_UID$1 = `plugin::${PLUGIN_ID}`;
|
|
323
273
|
function getBaseUrl(ctx, strapi) {
|
|
324
274
|
const forwardedProto = ctx.request.headers["x-forwarded-proto"] || ctx.protocol;
|
|
325
275
|
const forwardedHost = ctx.request.headers["x-forwarded-host"] || ctx.request.headers["host"];
|
|
@@ -356,17 +306,15 @@ const oauthController = ({ strapi }) => ({
|
|
|
356
306
|
/**
|
|
357
307
|
* OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
358
308
|
* GET /.well-known/oauth-protected-resource
|
|
309
|
+
*
|
|
310
|
+
* Convention-based protection: all /api/{plugin}/mcp endpoints are protected.
|
|
359
311
|
*/
|
|
360
312
|
async protectedResource(ctx) {
|
|
361
313
|
const baseUrl = getBaseUrl(ctx, strapi);
|
|
362
314
|
const pluginPath = `/api/${PLUGIN_ID}`;
|
|
363
315
|
const authServer = `${baseUrl}${pluginPath}`;
|
|
364
|
-
const endpoints = await strapi.documents(`${PLUGIN_UID$2}.mcp-endpoint`).findMany({
|
|
365
|
-
filters: { active: true }
|
|
366
|
-
});
|
|
367
|
-
const resources = endpoints.map((e) => `${baseUrl}${e.path}`);
|
|
368
316
|
ctx.body = {
|
|
369
|
-
resource:
|
|
317
|
+
resource: `${baseUrl}/api`,
|
|
370
318
|
authorization_servers: [authServer],
|
|
371
319
|
bearer_methods_supported: ["header"]
|
|
372
320
|
};
|
|
@@ -392,7 +340,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
392
340
|
ctx.body = { error: "unsupported_response_type", error_description: "Only code response type is supported" };
|
|
393
341
|
return;
|
|
394
342
|
}
|
|
395
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
343
|
+
const client = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-client`).findFirst({
|
|
396
344
|
filters: { clientId: client_id, active: true }
|
|
397
345
|
});
|
|
398
346
|
if (!client) {
|
|
@@ -421,7 +369,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
421
369
|
}
|
|
422
370
|
const code = randomBytes(32).toString("hex");
|
|
423
371
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
424
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
372
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).create({
|
|
425
373
|
data: {
|
|
426
374
|
code,
|
|
427
375
|
clientId: client_id,
|
|
@@ -459,7 +407,7 @@ const oauthController = ({ strapi }) => ({
|
|
|
459
407
|
ctx.body = { error: "invalid_client", error_description: "Client authentication required" };
|
|
460
408
|
return;
|
|
461
409
|
}
|
|
462
|
-
const client = await strapi.documents(`${PLUGIN_UID$
|
|
410
|
+
const client = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-client`).findFirst({
|
|
463
411
|
filters: { clientId: authClientId, active: true }
|
|
464
412
|
});
|
|
465
413
|
if (!client || authClientSecret && client.clientSecret !== authClientSecret) {
|
|
@@ -483,7 +431,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
483
431
|
ctx.body = { error: "invalid_request", error_description: "code is required" };
|
|
484
432
|
return;
|
|
485
433
|
}
|
|
486
|
-
const authCode = await strapi.documents(`${PLUGIN_UID$
|
|
434
|
+
const authCode = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).findFirst({
|
|
487
435
|
filters: { code, clientId: client.clientId, used: false }
|
|
488
436
|
});
|
|
489
437
|
if (!authCode) {
|
|
@@ -501,7 +449,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
501
449
|
ctx.body = { error: "invalid_grant", error_description: "redirect_uri mismatch" };
|
|
502
450
|
return;
|
|
503
451
|
}
|
|
504
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
452
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).update({
|
|
505
453
|
documentId: authCode.documentId,
|
|
506
454
|
data: { used: true }
|
|
507
455
|
});
|
|
@@ -509,7 +457,7 @@ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_
|
|
|
509
457
|
const refreshToken = randomBytes(32).toString("hex");
|
|
510
458
|
const expiresIn = 3600;
|
|
511
459
|
const refreshExpiresIn = 30 * 24 * 3600;
|
|
512
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
460
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).create({
|
|
513
461
|
data: {
|
|
514
462
|
accessToken,
|
|
515
463
|
refreshToken,
|
|
@@ -532,7 +480,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
532
480
|
ctx.body = { error: "invalid_request", error_description: "refresh_token is required" };
|
|
533
481
|
return;
|
|
534
482
|
}
|
|
535
|
-
const token = await strapi.documents(`${PLUGIN_UID$
|
|
483
|
+
const token = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).findFirst({
|
|
536
484
|
filters: { refreshToken, clientId: client.clientId, revoked: false }
|
|
537
485
|
});
|
|
538
486
|
if (!token) {
|
|
@@ -545,7 +493,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
545
493
|
ctx.body = { error: "invalid_grant", error_description: "Refresh token expired" };
|
|
546
494
|
return;
|
|
547
495
|
}
|
|
548
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
496
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).update({
|
|
549
497
|
documentId: token.documentId,
|
|
550
498
|
data: { revoked: true }
|
|
551
499
|
});
|
|
@@ -553,7 +501,7 @@ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
|
|
|
553
501
|
const newRefreshToken = randomBytes(32).toString("hex");
|
|
554
502
|
const expiresIn = 3600;
|
|
555
503
|
const refreshExpiresIn = 30 * 24 * 3600;
|
|
556
|
-
await strapi.documents(`${PLUGIN_UID$
|
|
504
|
+
await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).create({
|
|
557
505
|
data: {
|
|
558
506
|
accessToken: newAccessToken,
|
|
559
507
|
refreshToken: newRefreshToken,
|
|
@@ -630,14 +578,14 @@ const routes = {
|
|
|
630
578
|
routes: [...admin]
|
|
631
579
|
}
|
|
632
580
|
};
|
|
633
|
-
const PLUGIN_UID
|
|
581
|
+
const PLUGIN_UID = `plugin::${PLUGIN_ID}`;
|
|
634
582
|
const oauthService = ({ strapi }) => ({
|
|
635
583
|
/**
|
|
636
584
|
* Validate an OAuth access token
|
|
637
585
|
*/
|
|
638
586
|
async validateToken(accessToken) {
|
|
639
587
|
try {
|
|
640
|
-
const tokenRecord = await strapi.documents(`${PLUGIN_UID
|
|
588
|
+
const tokenRecord = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findFirst({
|
|
641
589
|
filters: { accessToken, revoked: false }
|
|
642
590
|
});
|
|
643
591
|
if (!tokenRecord) {
|
|
@@ -646,7 +594,7 @@ const oauthService = ({ strapi }) => ({
|
|
|
646
594
|
if (new Date(tokenRecord.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
647
595
|
return { valid: false, error: "Token expired" };
|
|
648
596
|
}
|
|
649
|
-
const client = await strapi.documents(`${PLUGIN_UID
|
|
597
|
+
const client = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-client`).findFirst({
|
|
650
598
|
filters: { clientId: tokenRecord.clientId }
|
|
651
599
|
});
|
|
652
600
|
if (!client) {
|
|
@@ -666,11 +614,11 @@ const oauthService = ({ strapi }) => ({
|
|
|
666
614
|
* Revoke all tokens for a client
|
|
667
615
|
*/
|
|
668
616
|
async revokeClientTokens(clientId) {
|
|
669
|
-
const tokens = await strapi.documents(`${PLUGIN_UID
|
|
617
|
+
const tokens = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findMany({
|
|
670
618
|
filters: { clientId, revoked: false }
|
|
671
619
|
});
|
|
672
620
|
for (const token of tokens) {
|
|
673
|
-
await strapi.documents(`${PLUGIN_UID
|
|
621
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).update({
|
|
674
622
|
documentId: token.documentId,
|
|
675
623
|
data: { revoked: true }
|
|
676
624
|
});
|
|
@@ -682,19 +630,19 @@ const oauthService = ({ strapi }) => ({
|
|
|
682
630
|
*/
|
|
683
631
|
async cleanupExpired() {
|
|
684
632
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
685
|
-
const expiredCodes = await strapi.documents(`${PLUGIN_UID
|
|
633
|
+
const expiredCodes = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-code`).findMany({
|
|
686
634
|
filters: { expiresAt: { $lt: now } }
|
|
687
635
|
});
|
|
688
636
|
for (const code of expiredCodes) {
|
|
689
|
-
await strapi.documents(`${PLUGIN_UID
|
|
637
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-code`).delete({
|
|
690
638
|
documentId: code.documentId
|
|
691
639
|
});
|
|
692
640
|
}
|
|
693
|
-
const expiredTokens = await strapi.documents(`${PLUGIN_UID
|
|
641
|
+
const expiredTokens = await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).findMany({
|
|
694
642
|
filters: { refreshExpiresAt: { $lt: now } }
|
|
695
643
|
});
|
|
696
644
|
for (const token of expiredTokens) {
|
|
697
|
-
await strapi.documents(`${PLUGIN_UID
|
|
645
|
+
await strapi.documents(`${PLUGIN_UID}.mcp-oauth-token`).delete({
|
|
698
646
|
documentId: token.documentId
|
|
699
647
|
});
|
|
700
648
|
}
|
|
@@ -704,80 +652,8 @@ const oauthService = ({ strapi }) => ({
|
|
|
704
652
|
};
|
|
705
653
|
}
|
|
706
654
|
});
|
|
707
|
-
const PLUGIN_UID = `plugin::${PLUGIN_ID}`;
|
|
708
|
-
const endpointService = ({ strapi }) => ({
|
|
709
|
-
/**
|
|
710
|
-
* Register an MCP endpoint for OAuth protection
|
|
711
|
-
*/
|
|
712
|
-
async register(endpoint) {
|
|
713
|
-
const existing = await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findFirst({
|
|
714
|
-
filters: { path: endpoint.path }
|
|
715
|
-
});
|
|
716
|
-
if (existing) {
|
|
717
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).update({
|
|
718
|
-
documentId: existing.documentId,
|
|
719
|
-
data: {
|
|
720
|
-
name: endpoint.name,
|
|
721
|
-
pluginId: endpoint.pluginId,
|
|
722
|
-
description: endpoint.description,
|
|
723
|
-
active: true
|
|
724
|
-
}
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).create({
|
|
728
|
-
data: {
|
|
729
|
-
name: endpoint.name,
|
|
730
|
-
pluginId: endpoint.pluginId,
|
|
731
|
-
path: endpoint.path,
|
|
732
|
-
description: endpoint.description,
|
|
733
|
-
active: true
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
},
|
|
737
|
-
/**
|
|
738
|
-
* Unregister an MCP endpoint
|
|
739
|
-
*/
|
|
740
|
-
async unregister(path) {
|
|
741
|
-
const endpoint = await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findFirst({
|
|
742
|
-
filters: { path }
|
|
743
|
-
});
|
|
744
|
-
if (!endpoint) {
|
|
745
|
-
return false;
|
|
746
|
-
}
|
|
747
|
-
await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).update({
|
|
748
|
-
documentId: endpoint.documentId,
|
|
749
|
-
data: { active: false }
|
|
750
|
-
});
|
|
751
|
-
return true;
|
|
752
|
-
},
|
|
753
|
-
/**
|
|
754
|
-
* Get all registered endpoints
|
|
755
|
-
*/
|
|
756
|
-
async getAll(activeOnly = true) {
|
|
757
|
-
const filters = activeOnly ? { active: true } : {};
|
|
758
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findMany({ filters });
|
|
759
|
-
},
|
|
760
|
-
/**
|
|
761
|
-
* Get endpoints for a specific plugin
|
|
762
|
-
*/
|
|
763
|
-
async getByPlugin(pluginId, activeOnly = true) {
|
|
764
|
-
const filters = { pluginId };
|
|
765
|
-
if (activeOnly) {
|
|
766
|
-
filters.active = true;
|
|
767
|
-
}
|
|
768
|
-
return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findMany({ filters });
|
|
769
|
-
},
|
|
770
|
-
/**
|
|
771
|
-
* Check if a path is a protected endpoint
|
|
772
|
-
*/
|
|
773
|
-
async isProtected(path) {
|
|
774
|
-
const endpoints = await this.getAll(true);
|
|
775
|
-
return endpoints.some((e) => path.includes(e.path));
|
|
776
|
-
}
|
|
777
|
-
});
|
|
778
655
|
const services = {
|
|
779
|
-
oauth: oauthService
|
|
780
|
-
endpoint: endpointService
|
|
656
|
+
oauth: oauthService
|
|
781
657
|
};
|
|
782
658
|
const index = {
|
|
783
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: {
|
|
@@ -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;
|