@payloadcms/plugin-mcp 4.0.0-internal.016994f → 4.0.0-internal.0731f09
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/bin.js +0 -7
- package/dist/endpoint/access.d.ts.map +1 -1
- package/dist/endpoint/access.js +17 -5
- package/dist/endpoint/access.js.map +1 -1
- package/dist/endpoint/index.d.ts.map +1 -1
- package/dist/endpoint/index.js +46 -4
- package/dist/endpoint/index.js.map +1 -1
- package/dist/endpoint/oauth.d.ts +39 -0
- package/dist/endpoint/oauth.d.ts.map +1 -0
- package/dist/endpoint/oauth.js +92 -0
- package/dist/endpoint/oauth.js.map +1 -0
- package/dist/endpoint/oauthServer.d.ts +11 -0
- package/dist/endpoint/oauthServer.d.ts.map +1 -0
- package/dist/endpoint/oauthServer.js +351 -0
- package/dist/endpoint/oauthServer.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/buildMcpServer.d.ts +2 -2
- package/dist/mcp/buildMcpServer.d.ts.map +1 -1
- package/dist/mcp/buildMcpServer.js +36 -24
- package/dist/mcp/buildMcpServer.js.map +1 -1
- package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/authTools.js +80 -56
- package/dist/mcp/builtin/collections/authTools.js.map +1 -1
- package/dist/mcp/builtin/collections/countTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/countTool.js +9 -14
- package/dist/mcp/builtin/collections/countTool.js.map +1 -1
- package/dist/mcp/builtin/collections/countVersionsTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/countVersionsTool.js +9 -13
- package/dist/mcp/builtin/collections/countVersionsTool.js.map +1 -1
- package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/createTool.js +48 -49
- package/dist/mcp/builtin/collections/createTool.js.map +1 -1
- package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/deleteTool.js +44 -43
- package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
- package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/duplicateTool.js +31 -37
- package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
- package/dist/mcp/builtin/collections/fileInput.d.ts +21 -21
- package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/fileInput.js +34 -70
- package/dist/mcp/builtin/collections/fileInput.js.map +1 -1
- package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/findDistinctTool.js +11 -26
- package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -1
- package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/findTool.js +28 -46
- package/dist/mcp/builtin/collections/findTool.js.map +1 -1
- package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/findVersionByIDTool.js +17 -26
- package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -1
- package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/findVersionsTool.js +16 -30
- package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -1
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +30 -14
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -1
- package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/restoreVersionTool.js +14 -26
- package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -1
- package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/updateTool.js +55 -87
- package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
- package/dist/mcp/builtin/collections/uploadInstructionsTool.js +11 -11
- package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -1
- package/dist/mcp/builtin/formatEntityError.d.ts +16 -0
- package/dist/mcp/builtin/formatEntityError.d.ts.map +1 -0
- package/dist/mcp/builtin/formatEntityError.js +55 -0
- package/dist/mcp/builtin/formatEntityError.js.map +1 -0
- package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -1
- package/dist/mcp/builtin/getConfigInfoTool.js +3 -2
- package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -1
- package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/countVersionsTool.js +9 -13
- package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -1
- package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/findTool.js +10 -16
- package/dist/mcp/builtin/globals/findTool.js.map +1 -1
- package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/findVersionByIDTool.js +13 -24
- package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -1
- package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/findVersionsTool.js +13 -29
- package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -1
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +29 -13
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -1
- package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/restoreVersionTool.js +15 -22
- package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -1
- package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/updateTool.js +27 -36
- package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
- package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
- package/dist/mcp/sanitizeMCPConfig.js +3 -0
- package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
- package/dist/stdio.js +4 -3
- package/dist/stdio.js.map +1 -1
- package/dist/types.d.ts +79 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/toStandardSchema.js.map +1 -1
- package/package.json +23 -28
- package/src/endpoint/access.ts +14 -6
- package/src/endpoint/index.ts +41 -3
- package/src/endpoint/oauth.ts +91 -0
- package/src/endpoint/oauthServer.ts +386 -0
- package/src/index.ts +38 -5
- package/src/mcp/buildMcpServer.ts +40 -30
- package/src/mcp/builtin/collections/authTools.ts +82 -71
- package/src/mcp/builtin/collections/countTool.ts +9 -21
- package/src/mcp/builtin/collections/countVersionsTool.ts +9 -18
- package/src/mcp/builtin/collections/createTool.ts +71 -96
- package/src/mcp/builtin/collections/deleteTool.ts +37 -65
- package/src/mcp/builtin/collections/duplicateTool.ts +36 -86
- package/src/mcp/builtin/collections/fileInput.spec.ts +2 -2
- package/src/mcp/builtin/collections/fileInput.ts +44 -81
- package/src/mcp/builtin/collections/findDistinctTool.ts +11 -63
- package/src/mcp/builtin/collections/findTool.ts +24 -106
- package/src/mcp/builtin/collections/findVersionByIDTool.ts +15 -58
- package/src/mcp/builtin/collections/findVersionsTool.ts +14 -85
- package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +23 -12
- package/src/mcp/builtin/collections/restoreVersionTool.ts +15 -57
- package/src/mcp/builtin/collections/updateTool.ts +55 -109
- package/src/mcp/builtin/collections/uploadInstructionsTool.ts +12 -12
- package/src/mcp/builtin/formatEntityError.ts +77 -0
- package/src/mcp/builtin/getConfigInfoTool.ts +2 -1
- package/src/mcp/builtin/globals/countVersionsTool.ts +9 -17
- package/src/mcp/builtin/globals/findTool.ts +13 -52
- package/src/mcp/builtin/globals/findVersionByIDTool.ts +13 -52
- package/src/mcp/builtin/globals/findVersionsTool.ts +14 -92
- package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +21 -11
- package/src/mcp/builtin/globals/restoreVersionTool.ts +14 -45
- package/src/mcp/builtin/globals/updateTool.ts +38 -61
- package/src/mcp/sanitizeMCPConfig.ts +1 -0
- package/src/stdio.ts +2 -2
- package/src/types.ts +79 -7
- package/src/utils/toStandardSchema.ts +1 -1
- package/dist/mcp/builtin/collections/fileInput.spec.js +0 -86
- package/dist/mcp/builtin/collections/fileInput.spec.js.map +0 -1
- package/dist/mcp/builtin/collections/formatCollectionError.d.ts +0 -9
- package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +0 -1
- package/dist/mcp/builtin/collections/formatCollectionError.js +0 -60
- package/dist/mcp/builtin/collections/formatCollectionError.js.map +0 -1
- package/dist/mcp/builtin/validateEntityData.d.ts +0 -14
- package/dist/mcp/builtin/validateEntityData.d.ts.map +0 -1
- package/dist/mcp/builtin/validateEntityData.js +0 -82
- package/dist/mcp/builtin/validateEntityData.js.map +0 -1
- package/dist/utils/getVirtualFieldNames.d.ts +0 -14
- package/dist/utils/getVirtualFieldNames.d.ts.map +0 -1
- package/dist/utils/getVirtualFieldNames.js +0 -35
- package/dist/utils/getVirtualFieldNames.js.map +0 -1
- package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts +0 -28
- package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts.map +0 -1
- package/dist/utils/schemaConversion/filterFieldsByAccess.js +0 -71
- package/dist/utils/schemaConversion/filterFieldsByAccess.js.map +0 -1
- package/dist/utils/schemaConversion/getEntityInputSchema.d.ts +0 -13
- package/dist/utils/schemaConversion/getEntityInputSchema.d.ts.map +0 -1
- package/dist/utils/schemaConversion/getEntityInputSchema.js +0 -66
- package/dist/utils/schemaConversion/getEntityInputSchema.js.map +0 -1
- package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +0 -10
- package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +0 -1
- package/dist/utils/schemaConversion/sanitizeEntitySchema.js +0 -384
- package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +0 -1
- package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +0 -133
- package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +0 -1
- package/dist/utils/transformPointDataToPayload.d.ts +0 -7
- package/dist/utils/transformPointDataToPayload.d.ts.map +0 -1
- package/dist/utils/transformPointDataToPayload.js +0 -28
- package/dist/utils/transformPointDataToPayload.js.map +0 -1
- package/dist/utils/whereSchema.d.ts +0 -9
- package/dist/utils/whereSchema.d.ts.map +0 -1
- package/dist/utils/whereSchema.js +0 -13
- package/dist/utils/whereSchema.js.map +0 -1
- package/src/mcp/builtin/collections/formatCollectionError.ts +0 -84
- package/src/mcp/builtin/validateEntityData.ts +0 -132
- package/src/utils/getVirtualFieldNames.ts +0 -53
- package/src/utils/schemaConversion/filterFieldsByAccess.ts +0 -123
- package/src/utils/schemaConversion/getEntityInputSchema.ts +0 -96
- package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +0 -96
- package/src/utils/schemaConversion/sanitizeEntitySchema.ts +0 -432
- package/src/utils/transformPointDataToPayload.ts +0 -40
- package/src/utils/whereSchema.ts +0 -24
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { OAuthErrorCode } from '@modelcontextprotocol/server';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { APIError, createAuthToken } from 'payload';
|
|
4
|
+
import { formatAdminURL } from 'payload/shared';
|
|
5
|
+
import { getPluginConfig } from '../utils/getPluginConfig.js';
|
|
6
|
+
import { getMCPURL } from './oauth.js';
|
|
7
|
+
/**
|
|
8
|
+
* Turns Payload into the OAuth authorization server for MCP clients when `oauth.type` is
|
|
9
|
+
* `'builtin'`. Clients register, the user allows them after logging in to the admin panel, and
|
|
10
|
+
* the client receives the access token `issueToken` creates.
|
|
11
|
+
*
|
|
12
|
+
* Clients, consent forms, and authorization codes are encrypted and handed to the client instead
|
|
13
|
+
* of being stored, so nothing is written to the database.
|
|
14
|
+
*/ export const oauthServerEndpoints = [
|
|
15
|
+
{
|
|
16
|
+
disableAuth: true,
|
|
17
|
+
handler: (req)=>withIssuer(req, (issuer)=>metadataResponse(issuer)),
|
|
18
|
+
method: 'get',
|
|
19
|
+
// OpenID discovery is the only metadata location MCP clients check below the issuer's path,
|
|
20
|
+
// which lets this work under /api without a route at the site root
|
|
21
|
+
path: '/mcp/oauth/.well-known/openid-configuration'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
disableAuth: true,
|
|
25
|
+
handler: (req)=>withIssuer(req, ()=>Response.json({
|
|
26
|
+
keys: []
|
|
27
|
+
})),
|
|
28
|
+
method: 'get',
|
|
29
|
+
path: '/mcp/oauth/jwks'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
disableAuth: true,
|
|
33
|
+
handler: (req)=>withIssuer(req, ()=>register(req)),
|
|
34
|
+
method: 'post',
|
|
35
|
+
path: '/mcp/oauth/register'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
handler: (req)=>withIssuer(req, ()=>authorize(req)),
|
|
39
|
+
method: 'get',
|
|
40
|
+
path: '/mcp/oauth/authorize'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
handler: (req)=>withIssuer(req, ()=>approve(req)),
|
|
44
|
+
method: 'post',
|
|
45
|
+
path: '/mcp/oauth/authorize'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
disableAuth: true,
|
|
49
|
+
handler: (req)=>withIssuer(req, ()=>token(req)),
|
|
50
|
+
method: 'post',
|
|
51
|
+
path: '/mcp/oauth/token'
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
/** The authorization server URL, or `undefined` when Payload isn't the authorization server. */ const getIssuer = (req)=>getPluginConfig({
|
|
55
|
+
config: req.payload.config
|
|
56
|
+
}).oauth?.type === 'builtin' ? `${getMCPURL({
|
|
57
|
+
config: req.payload.config,
|
|
58
|
+
url: req.url
|
|
59
|
+
})}/oauth` : undefined;
|
|
60
|
+
const withIssuer = (req, handler)=>{
|
|
61
|
+
const issuer = getIssuer(req);
|
|
62
|
+
return issuer ? handler(issuer) : new Response(null, {
|
|
63
|
+
status: 404
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const metadataResponse = (issuer)=>Response.json({
|
|
67
|
+
authorization_endpoint: `${issuer}/authorize`,
|
|
68
|
+
code_challenge_methods_supported: [
|
|
69
|
+
'S256'
|
|
70
|
+
],
|
|
71
|
+
grant_types_supported: [
|
|
72
|
+
'authorization_code'
|
|
73
|
+
],
|
|
74
|
+
// Required by OpenID discovery. No ID tokens are issued.
|
|
75
|
+
id_token_signing_alg_values_supported: [
|
|
76
|
+
'RS256'
|
|
77
|
+
],
|
|
78
|
+
issuer,
|
|
79
|
+
jwks_uri: `${issuer}/jwks`,
|
|
80
|
+
registration_endpoint: `${issuer}/register`,
|
|
81
|
+
response_types_supported: [
|
|
82
|
+
'code'
|
|
83
|
+
],
|
|
84
|
+
subject_types_supported: [
|
|
85
|
+
'public'
|
|
86
|
+
],
|
|
87
|
+
token_endpoint: `${issuer}/token`,
|
|
88
|
+
token_endpoint_auth_methods_supported: [
|
|
89
|
+
'none'
|
|
90
|
+
]
|
|
91
|
+
}, {
|
|
92
|
+
headers: {
|
|
93
|
+
'Access-Control-Allow-Origin': '*'
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const register = async (req)=>{
|
|
97
|
+
const { client_name, redirect_uris } = await req.json().catch(()=>({}));
|
|
98
|
+
if (!Array.isArray(redirect_uris) || !redirect_uris.length || !redirect_uris.every((uri)=>typeof uri === 'string' && URL.canParse(uri) && !uri.includes('#'))) {
|
|
99
|
+
return Response.json({
|
|
100
|
+
error: OAuthErrorCode.InvalidRedirectUri
|
|
101
|
+
}, {
|
|
102
|
+
status: 400
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const client = {
|
|
106
|
+
name: typeof client_name === 'string' ? client_name : 'An MCP client',
|
|
107
|
+
redirectURIs: redirect_uris
|
|
108
|
+
};
|
|
109
|
+
return Response.json({
|
|
110
|
+
client_id: seal({
|
|
111
|
+
type: 'client',
|
|
112
|
+
data: client,
|
|
113
|
+
req
|
|
114
|
+
}),
|
|
115
|
+
client_name: client.name,
|
|
116
|
+
grant_types: [
|
|
117
|
+
'authorization_code'
|
|
118
|
+
],
|
|
119
|
+
redirect_uris: client.redirectURIs,
|
|
120
|
+
response_types: [
|
|
121
|
+
'code'
|
|
122
|
+
],
|
|
123
|
+
token_endpoint_auth_method: 'none'
|
|
124
|
+
}, {
|
|
125
|
+
status: 201
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
const authorize = (req)=>{
|
|
129
|
+
const url = new URL(req.url);
|
|
130
|
+
const params = url.searchParams;
|
|
131
|
+
const clientID = params.get('client_id');
|
|
132
|
+
const client = unseal({
|
|
133
|
+
type: 'client',
|
|
134
|
+
req,
|
|
135
|
+
value: clientID
|
|
136
|
+
});
|
|
137
|
+
const redirectURI = params.get('redirect_uri');
|
|
138
|
+
// Never redirect to an address the client didn't register
|
|
139
|
+
if (!client || !redirectURI || !client.redirectURIs.includes(redirectURI)) {
|
|
140
|
+
return page({
|
|
141
|
+
content: '<p>Start the sign-in again from your MCP client.</p>',
|
|
142
|
+
status: 400,
|
|
143
|
+
title: 'This sign-in link is invalid'
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const state = params.get('state');
|
|
147
|
+
const codeChallenge = params.get('code_challenge');
|
|
148
|
+
if (params.get('response_type') !== 'code' || params.get('code_challenge_method') !== 'S256' || !codeChallenge) {
|
|
149
|
+
return redirect({
|
|
150
|
+
params: {
|
|
151
|
+
error: OAuthErrorCode.InvalidRequest,
|
|
152
|
+
state
|
|
153
|
+
},
|
|
154
|
+
redirectURI
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (!req.user) {
|
|
158
|
+
const loginURL = formatAdminURL({
|
|
159
|
+
adminRoute: req.payload.config.routes.admin,
|
|
160
|
+
path: '/login'
|
|
161
|
+
});
|
|
162
|
+
return new Response(null, {
|
|
163
|
+
headers: {
|
|
164
|
+
Location: `${loginURL}?redirect=${encodeURIComponent(url.pathname + url.search)}`
|
|
165
|
+
},
|
|
166
|
+
status: 302
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const consent = {
|
|
170
|
+
clientID: clientID,
|
|
171
|
+
codeChallenge,
|
|
172
|
+
collection: req.user.collection,
|
|
173
|
+
redirectURI,
|
|
174
|
+
state,
|
|
175
|
+
userID: req.user.id
|
|
176
|
+
};
|
|
177
|
+
return page({
|
|
178
|
+
content: `<p>It will be able to use this app's MCP tools as ${escapeHTML(String(req.user.email ?? req.user.id))}, and you will return to ${escapeHTML(new URL(redirectURI).host || redirectURI)}.</p>
|
|
179
|
+
<form method="post">
|
|
180
|
+
<input name="consent" type="hidden" value="${seal({
|
|
181
|
+
type: 'consent',
|
|
182
|
+
data: consent,
|
|
183
|
+
expiresIn: 20 * 60 * 60,
|
|
184
|
+
req
|
|
185
|
+
})}">
|
|
186
|
+
<button name="decision" type="submit" value="allow">Allow</button>
|
|
187
|
+
<button name="decision" type="submit" value="deny">Deny</button>
|
|
188
|
+
</form>`,
|
|
189
|
+
status: 200,
|
|
190
|
+
title: `Allow ${client.name} to use your account?`
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
const approve = async (req)=>{
|
|
194
|
+
const form = new URLSearchParams(await req.text());
|
|
195
|
+
const consent = unseal({
|
|
196
|
+
type: 'consent',
|
|
197
|
+
req,
|
|
198
|
+
value: form.get('consent')
|
|
199
|
+
});
|
|
200
|
+
// The form is only valid for the user it was shown to
|
|
201
|
+
if (!consent || req.user?.id !== consent.userID || req.user.collection !== consent.collection) {
|
|
202
|
+
return page({
|
|
203
|
+
content: '<p>Start the sign-in again from your MCP client.</p>',
|
|
204
|
+
status: 400,
|
|
205
|
+
title: 'This sign-in request expired'
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const { clientID, codeChallenge, redirectURI, state } = consent;
|
|
209
|
+
if (form.get('decision') !== 'allow') {
|
|
210
|
+
return redirect({
|
|
211
|
+
params: {
|
|
212
|
+
error: OAuthErrorCode.AccessDenied,
|
|
213
|
+
state
|
|
214
|
+
},
|
|
215
|
+
redirectURI
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const { issueToken = issueLoginToken } = getPluginConfig({
|
|
219
|
+
config: req.payload.config
|
|
220
|
+
}).oauth;
|
|
221
|
+
const { accessToken, expiresIn } = await issueToken({
|
|
222
|
+
req
|
|
223
|
+
});
|
|
224
|
+
const code = {
|
|
225
|
+
accessToken,
|
|
226
|
+
clientID,
|
|
227
|
+
codeChallenge,
|
|
228
|
+
expiresIn,
|
|
229
|
+
redirectURI
|
|
230
|
+
};
|
|
231
|
+
return redirect({
|
|
232
|
+
params: {
|
|
233
|
+
code: seal({
|
|
234
|
+
type: 'code',
|
|
235
|
+
data: code,
|
|
236
|
+
expiresIn: 60,
|
|
237
|
+
req
|
|
238
|
+
}),
|
|
239
|
+
state
|
|
240
|
+
},
|
|
241
|
+
redirectURI
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
const token = async (req)=>{
|
|
245
|
+
const form = new URLSearchParams(await req.text());
|
|
246
|
+
const code = unseal({
|
|
247
|
+
type: 'code',
|
|
248
|
+
req,
|
|
249
|
+
value: form.get('code')
|
|
250
|
+
});
|
|
251
|
+
if (form.get('grant_type') !== 'authorization_code' || !code || code.clientID !== form.get('client_id') || code.redirectURI !== form.get('redirect_uri') || // PKCE: only the client that started the sign-in knows the verifier
|
|
252
|
+
createHash('sha256').update(form.get('code_verifier') ?? '').digest('base64url') !== code.codeChallenge) {
|
|
253
|
+
return Response.json({
|
|
254
|
+
error: OAuthErrorCode.InvalidGrant
|
|
255
|
+
}, {
|
|
256
|
+
status: 400
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return Response.json({
|
|
260
|
+
access_token: code.accessToken,
|
|
261
|
+
expires_in: code.expiresIn,
|
|
262
|
+
token_type: 'Bearer'
|
|
263
|
+
}, {
|
|
264
|
+
headers: {
|
|
265
|
+
'Cache-Control': 'no-store'
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
/** The default `issueToken`: a Payload login token, which Payload's JWT strategy accepts. */ const issueLoginToken = async ({ req })=>{
|
|
270
|
+
const collection = req.payload.collections[req.user.collection];
|
|
271
|
+
if (collection.config.auth.disableLocalStrategy) {
|
|
272
|
+
throw new APIError(`The "${collection.config.slug}" collection has its local strategy disabled, so MCP clients can't get Payload login tokens for it. Pass oauth.issueToken to the MCP plugin.`);
|
|
273
|
+
}
|
|
274
|
+
// The full document, like the one a password login starts the session on
|
|
275
|
+
const user = await req.payload.db.findOne({
|
|
276
|
+
collection: collection.config.slug,
|
|
277
|
+
req,
|
|
278
|
+
where: {
|
|
279
|
+
id: {
|
|
280
|
+
equals: req.user.id
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const { token } = await createAuthToken({
|
|
285
|
+
collectionConfig: collection.config,
|
|
286
|
+
email: user.email,
|
|
287
|
+
req,
|
|
288
|
+
user
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
accessToken: token,
|
|
292
|
+
expiresIn: collection.config.auth.tokenExpiration
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* Encrypts data for the client to hold, so it doesn't need to be stored. `type` keeps one kind
|
|
297
|
+
* from being used as another, e.g. a client ID as an authorization code.
|
|
298
|
+
*/ const seal = ({ type, data, expiresIn, req })=>req.payload.encrypt(JSON.stringify({
|
|
299
|
+
...data,
|
|
300
|
+
type,
|
|
301
|
+
exp: expiresIn && Date.now() + expiresIn * 1000
|
|
302
|
+
}));
|
|
303
|
+
const unseal = ({ type, req, value })=>{
|
|
304
|
+
// Only the v1 format is authenticated. Older formats decrypt anything without an error.
|
|
305
|
+
if (!value?.startsWith('v1:')) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const data = JSON.parse(req.payload.decrypt(value));
|
|
310
|
+
return data.type === type && (!data.exp || data.exp > Date.now()) ? data : undefined;
|
|
311
|
+
} catch {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const redirect = ({ params, redirectURI })=>{
|
|
316
|
+
const location = new URL(redirectURI);
|
|
317
|
+
for (const [key, value] of Object.entries(params)){
|
|
318
|
+
if (value) {
|
|
319
|
+
location.searchParams.set(key, value);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return new Response(null, {
|
|
323
|
+
headers: {
|
|
324
|
+
Location: location.href
|
|
325
|
+
},
|
|
326
|
+
status: 302
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
const page = ({ content, status, title })=>new Response(`<!doctype html>
|
|
330
|
+
<html lang="en">
|
|
331
|
+
<head>
|
|
332
|
+
<meta charset="utf-8">
|
|
333
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
334
|
+
<title>${escapeHTML(title)}</title>
|
|
335
|
+
<style>
|
|
336
|
+
body { font-family: system-ui, sans-serif; margin: 4rem auto; max-width: 32rem; overflow-wrap: anywhere; padding: 0 1rem }
|
|
337
|
+
button { font: inherit; margin-inline-end: 0.5rem; padding: 0.5rem 1rem }
|
|
338
|
+
</style>
|
|
339
|
+
</head>
|
|
340
|
+
<body><h1>${escapeHTML(title)}</h1>${content}</body>
|
|
341
|
+
</html>`, {
|
|
342
|
+
headers: {
|
|
343
|
+
// Stop other sites from framing the page to trick users into clicking Allow
|
|
344
|
+
'Content-Security-Policy': "frame-ancestors 'none'",
|
|
345
|
+
'Content-Type': 'text/html; charset=utf-8'
|
|
346
|
+
},
|
|
347
|
+
status
|
|
348
|
+
});
|
|
349
|
+
const escapeHTML = (value)=>value.replace(/[&<>"']/g, (character)=>`&#${character.charCodeAt(0)};`);
|
|
350
|
+
|
|
351
|
+
//# sourceMappingURL=oauthServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/endpoint/oauthServer.ts"],"sourcesContent":["import type {\n OAuthClientInformationFull,\n OAuthTokens,\n OpenIdProviderDiscoveryMetadata,\n} from '@modelcontextprotocol/server'\nimport type { Endpoint, PayloadHandler, PayloadRequest } from 'payload'\n\nimport { OAuthErrorCode } from '@modelcontextprotocol/server'\nimport { createHash } from 'crypto'\nimport { APIError, createAuthToken } from 'payload'\nimport { formatAdminURL } from 'payload/shared'\n\nimport type { MCPBuiltinOAuthConfig } from '../types.js'\n\nimport { getPluginConfig } from '../utils/getPluginConfig.js'\nimport { getMCPURL } from './oauth.js'\n\ntype Client = { name: string; redirectURIs: string[] }\n\ntype Consent = {\n clientID: string\n codeChallenge: string\n collection: string\n redirectURI: string\n state: null | string\n userID: number | string\n}\n\ntype Code = {\n accessToken: string\n clientID: string\n codeChallenge: string\n expiresIn?: number\n redirectURI: string\n}\n\n/**\n * Turns Payload into the OAuth authorization server for MCP clients when `oauth.type` is\n * `'builtin'`. Clients register, the user allows them after logging in to the admin panel, and\n * the client receives the access token `issueToken` creates.\n *\n * Clients, consent forms, and authorization codes are encrypted and handed to the client instead\n * of being stored, so nothing is written to the database.\n */\nexport const oauthServerEndpoints: Endpoint[] = [\n {\n disableAuth: true,\n handler: (req) => withIssuer(req, (issuer) => metadataResponse(issuer)),\n method: 'get',\n // OpenID discovery is the only metadata location MCP clients check below the issuer's path,\n // which lets this work under /api without a route at the site root\n path: '/mcp/oauth/.well-known/openid-configuration',\n },\n {\n disableAuth: true,\n handler: (req) => withIssuer(req, () => Response.json({ keys: [] })),\n method: 'get',\n path: '/mcp/oauth/jwks',\n },\n {\n disableAuth: true,\n handler: (req) => withIssuer(req, () => register(req)),\n method: 'post',\n path: '/mcp/oauth/register',\n },\n {\n handler: (req) => withIssuer(req, () => authorize(req)),\n method: 'get',\n path: '/mcp/oauth/authorize',\n },\n {\n handler: (req) => withIssuer(req, () => approve(req)),\n method: 'post',\n path: '/mcp/oauth/authorize',\n },\n {\n disableAuth: true,\n handler: (req) => withIssuer(req, () => token(req)),\n method: 'post',\n path: '/mcp/oauth/token',\n },\n]\n\n/** The authorization server URL, or `undefined` when Payload isn't the authorization server. */\nconst getIssuer = (req: PayloadRequest): string | undefined =>\n getPluginConfig({ config: req.payload.config }).oauth?.type === 'builtin'\n ? `${getMCPURL({ config: req.payload.config, url: req.url! })}/oauth`\n : undefined\n\nconst withIssuer = (\n req: PayloadRequest,\n handler: (issuer: string) => Promise<Response> | Response,\n): Promise<Response> | Response => {\n const issuer = getIssuer(req)\n return issuer ? handler(issuer) : new Response(null, { status: 404 })\n}\n\nconst metadataResponse = (issuer: string): Response =>\n Response.json(\n {\n authorization_endpoint: `${issuer}/authorize`,\n code_challenge_methods_supported: ['S256'],\n grant_types_supported: ['authorization_code'],\n // Required by OpenID discovery. No ID tokens are issued.\n id_token_signing_alg_values_supported: ['RS256'],\n issuer,\n jwks_uri: `${issuer}/jwks`,\n registration_endpoint: `${issuer}/register`,\n response_types_supported: ['code'],\n subject_types_supported: ['public'],\n token_endpoint: `${issuer}/token`,\n token_endpoint_auth_methods_supported: ['none'],\n } satisfies OpenIdProviderDiscoveryMetadata,\n { headers: { 'Access-Control-Allow-Origin': '*' } },\n )\n\nconst register: PayloadHandler = async (req) => {\n const { client_name, redirect_uris } = await req.json!().catch(() => ({}))\n\n if (\n !Array.isArray(redirect_uris) ||\n !redirect_uris.length ||\n !redirect_uris.every(\n (uri) => typeof uri === 'string' && URL.canParse(uri) && !uri.includes('#'),\n )\n ) {\n return Response.json({ error: OAuthErrorCode.InvalidRedirectUri }, { status: 400 })\n }\n\n const client: Client = {\n name: typeof client_name === 'string' ? client_name : 'An MCP client',\n redirectURIs: redirect_uris,\n }\n\n return Response.json(\n {\n client_id: seal({ type: 'client', data: client, req }),\n client_name: client.name,\n grant_types: ['authorization_code'],\n redirect_uris: client.redirectURIs,\n response_types: ['code'],\n token_endpoint_auth_method: 'none',\n } satisfies OAuthClientInformationFull,\n { status: 201 },\n )\n}\n\nconst authorize: PayloadHandler = (req) => {\n const url = new URL(req.url!)\n const params = url.searchParams\n const clientID = params.get('client_id')\n const client = unseal<Client>({ type: 'client', req, value: clientID })\n const redirectURI = params.get('redirect_uri')\n\n // Never redirect to an address the client didn't register\n if (!client || !redirectURI || !client.redirectURIs.includes(redirectURI)) {\n return page({\n content: '<p>Start the sign-in again from your MCP client.</p>',\n status: 400,\n title: 'This sign-in link is invalid',\n })\n }\n\n const state = params.get('state')\n const codeChallenge = params.get('code_challenge')\n\n if (\n params.get('response_type') !== 'code' ||\n params.get('code_challenge_method') !== 'S256' ||\n !codeChallenge\n ) {\n return redirect({ params: { error: OAuthErrorCode.InvalidRequest, state }, redirectURI })\n }\n\n if (!req.user) {\n const loginURL = formatAdminURL({ adminRoute: req.payload.config.routes.admin, path: '/login' })\n\n return new Response(null, {\n headers: {\n Location: `${loginURL}?redirect=${encodeURIComponent(url.pathname + url.search)}`,\n },\n status: 302,\n })\n }\n\n const consent: Consent = {\n clientID: clientID!,\n codeChallenge,\n collection: req.user.collection,\n redirectURI,\n state,\n userID: req.user.id,\n }\n\n return page({\n content: `<p>It will be able to use this app's MCP tools as ${escapeHTML(String(req.user.email ?? req.user.id))}, and you will return to ${escapeHTML(new URL(redirectURI).host || redirectURI)}.</p>\n<form method=\"post\">\n<input name=\"consent\" type=\"hidden\" value=\"${seal({ type: 'consent', data: consent, expiresIn: 20 * 60 * 60, req })}\">\n<button name=\"decision\" type=\"submit\" value=\"allow\">Allow</button>\n<button name=\"decision\" type=\"submit\" value=\"deny\">Deny</button>\n</form>`,\n status: 200,\n title: `Allow ${client.name} to use your account?`,\n })\n}\n\nconst approve: PayloadHandler = async (req) => {\n const form = new URLSearchParams(await req.text!())\n const consent = unseal<Consent>({ type: 'consent', req, value: form.get('consent') })\n\n // The form is only valid for the user it was shown to\n if (!consent || req.user?.id !== consent.userID || req.user.collection !== consent.collection) {\n return page({\n content: '<p>Start the sign-in again from your MCP client.</p>',\n status: 400,\n title: 'This sign-in request expired',\n })\n }\n\n const { clientID, codeChallenge, redirectURI, state } = consent\n\n if (form.get('decision') !== 'allow') {\n return redirect({ params: { error: OAuthErrorCode.AccessDenied, state }, redirectURI })\n }\n\n const { issueToken = issueLoginToken } = getPluginConfig({ config: req.payload.config })\n .oauth as MCPBuiltinOAuthConfig\n const { accessToken, expiresIn } = await issueToken({ req })\n\n const code: Code = { accessToken, clientID, codeChallenge, expiresIn, redirectURI }\n\n return redirect({\n params: { code: seal({ type: 'code', data: code, expiresIn: 60, req }), state },\n redirectURI,\n })\n}\n\nconst token: PayloadHandler = async (req) => {\n const form = new URLSearchParams(await req.text!())\n const code = unseal<Code>({ type: 'code', req, value: form.get('code') })\n\n if (\n form.get('grant_type') !== 'authorization_code' ||\n !code ||\n code.clientID !== form.get('client_id') ||\n code.redirectURI !== form.get('redirect_uri') ||\n // PKCE: only the client that started the sign-in knows the verifier\n createHash('sha256')\n .update(form.get('code_verifier') ?? '')\n .digest('base64url') !== code.codeChallenge\n ) {\n return Response.json({ error: OAuthErrorCode.InvalidGrant }, { status: 400 })\n }\n\n return Response.json(\n {\n access_token: code.accessToken,\n expires_in: code.expiresIn,\n token_type: 'Bearer',\n } satisfies OAuthTokens,\n { headers: { 'Cache-Control': 'no-store' } },\n )\n}\n\n/** The default `issueToken`: a Payload login token, which Payload's JWT strategy accepts. */\nconst issueLoginToken: NonNullable<MCPBuiltinOAuthConfig['issueToken']> = async ({ req }) => {\n const collection = req.payload.collections[req.user!.collection]!\n\n if (collection.config.auth.disableLocalStrategy) {\n throw new APIError(\n `The \"${collection.config.slug}\" collection has its local strategy disabled, so MCP clients can't get Payload login tokens for it. Pass oauth.issueToken to the MCP plugin.`,\n )\n }\n\n // The full document, like the one a password login starts the session on\n const user = (await req.payload.db.findOne<NonNullable<PayloadRequest['user']>>({\n collection: collection.config.slug,\n req,\n where: { id: { equals: req.user!.id } },\n }))!\n\n const { token } = await createAuthToken({\n collectionConfig: collection.config,\n email: user.email!,\n req,\n user,\n })\n\n return { accessToken: token, expiresIn: collection.config.auth.tokenExpiration }\n}\n\n/**\n * Encrypts data for the client to hold, so it doesn't need to be stored. `type` keeps one kind\n * from being used as another, e.g. a client ID as an authorization code.\n */\nconst seal = ({\n type,\n data,\n expiresIn,\n req,\n}: {\n data: object\n /** Seconds */\n expiresIn?: number\n req: PayloadRequest\n type: string\n}): string =>\n req.payload.encrypt(\n JSON.stringify({ ...data, type, exp: expiresIn && Date.now() + expiresIn * 1000 }),\n )\n\nconst unseal = <T>({\n type,\n req,\n value,\n}: {\n req: PayloadRequest\n type: string\n value: null | string\n}): T | undefined => {\n // Only the v1 format is authenticated. Older formats decrypt anything without an error.\n if (!value?.startsWith('v1:')) {\n return undefined\n }\n\n try {\n const data = JSON.parse(req.payload.decrypt(value))\n return data.type === type && (!data.exp || data.exp > Date.now()) ? data : undefined\n } catch {\n return undefined\n }\n}\n\nconst redirect = ({\n params,\n redirectURI,\n}: {\n params: Record<string, null | string>\n redirectURI: string\n}): Response => {\n const location = new URL(redirectURI)\n\n for (const [key, value] of Object.entries(params)) {\n if (value) {\n location.searchParams.set(key, value)\n }\n }\n\n return new Response(null, { headers: { Location: location.href }, status: 302 })\n}\n\nconst page = ({\n content,\n status,\n title,\n}: {\n content: string\n status: number\n title: string\n}): Response =>\n new Response(\n `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${escapeHTML(title)}</title>\n<style>\nbody { font-family: system-ui, sans-serif; margin: 4rem auto; max-width: 32rem; overflow-wrap: anywhere; padding: 0 1rem }\nbutton { font: inherit; margin-inline-end: 0.5rem; padding: 0.5rem 1rem }\n</style>\n</head>\n<body><h1>${escapeHTML(title)}</h1>${content}</body>\n</html>`,\n {\n headers: {\n // Stop other sites from framing the page to trick users into clicking Allow\n 'Content-Security-Policy': \"frame-ancestors 'none'\",\n 'Content-Type': 'text/html; charset=utf-8',\n },\n status,\n },\n )\n\nconst escapeHTML = (value: string): string =>\n value.replace(/[&<>\"']/g, (character) => `&#${character.charCodeAt(0)};`)\n"],"names":["OAuthErrorCode","createHash","APIError","createAuthToken","formatAdminURL","getPluginConfig","getMCPURL","oauthServerEndpoints","disableAuth","handler","req","withIssuer","issuer","metadataResponse","method","path","Response","json","keys","register","authorize","approve","token","getIssuer","config","payload","oauth","type","url","undefined","status","authorization_endpoint","code_challenge_methods_supported","grant_types_supported","id_token_signing_alg_values_supported","jwks_uri","registration_endpoint","response_types_supported","subject_types_supported","token_endpoint","token_endpoint_auth_methods_supported","headers","client_name","redirect_uris","catch","Array","isArray","length","every","uri","URL","canParse","includes","error","InvalidRedirectUri","client","name","redirectURIs","client_id","seal","data","grant_types","response_types","token_endpoint_auth_method","params","searchParams","clientID","get","unseal","value","redirectURI","page","content","title","state","codeChallenge","redirect","InvalidRequest","user","loginURL","adminRoute","routes","admin","Location","encodeURIComponent","pathname","search","consent","collection","userID","id","escapeHTML","String","email","host","expiresIn","form","URLSearchParams","text","AccessDenied","issueToken","issueLoginToken","accessToken","code","update","digest","InvalidGrant","access_token","expires_in","token_type","collections","auth","disableLocalStrategy","slug","db","findOne","where","equals","collectionConfig","tokenExpiration","encrypt","JSON","stringify","exp","Date","now","startsWith","parse","decrypt","location","key","Object","entries","set","href","replace","character","charCodeAt"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig } from './types.js';
|
|
1
|
+
import type { AuthorizedMCP, MCPBuiltinOAuthConfig, MCPExternalOAuthConfig, MCPOAuthConfig, MCPPluginConfig, SanitizedMCPPluginConfig } from './types.js';
|
|
2
2
|
declare module 'payload' {
|
|
3
3
|
interface PayloadRequestAPI {
|
|
4
4
|
MCP: true;
|
|
@@ -7,8 +7,16 @@ declare module 'payload' {
|
|
|
7
7
|
/** After the plugin's `plugin` callback runs, `options` holds the sanitized config. */
|
|
8
8
|
'@payloadcms/plugin-mcp': MCPPluginConfig;
|
|
9
9
|
}
|
|
10
|
+
interface RequestContext {
|
|
11
|
+
/** Set on MCP requests before they're authenticated. */
|
|
12
|
+
mcp?: {
|
|
13
|
+
/** The MCP endpoint's address, which OAuth access tokens for MCP are issued for. */
|
|
14
|
+
resource: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
10
17
|
}
|
|
11
|
-
export type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig };
|
|
18
|
+
export type { AuthorizedMCP, MCPBuiltinOAuthConfig, MCPExternalOAuthConfig, MCPOAuthConfig, MCPPluginConfig, SanitizedMCPPluginConfig, };
|
|
12
19
|
export { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js';
|
|
20
|
+
export { oauthProtectedResourceHandler } from './endpoint/oauth.js';
|
|
13
21
|
export declare const mcpPlugin: (options: MCPPluginConfig) => import("payload").Plugin;
|
|
14
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,wBAAwB,EACzB,MAAM,YAAY,CAAA;AAOnB,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAiB,iBAAiB;QAChC,GAAG,EAAE,IAAI,CAAA;KACV;IACD,UAAU,iBAAiB;QACzB,uFAAuF;QACvF,wBAAwB,EAAE,eAAe,CAAA;KAC1C;IACD,UAAiB,cAAc;QAC7B,wDAAwD;QACxD,GAAG,CAAC,EAAE;YACJ,oFAAoF;YACpF,QAAQ,EAAE,MAAM,CAAA;SACjB,CAAA;KACF;CACF;AAED,YAAY,EACV,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,wBAAwB,GACzB,CAAA;AAED,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAClG,OAAO,EAAE,6BAA6B,EAAE,MAAM,qBAAqB,CAAA;AAEnE,eAAO,MAAM,SAAS,wDA0CpB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { definePlugin } from 'payload';
|
|
2
2
|
import { mcpEndpoint } from './endpoint/index.js';
|
|
3
|
+
import { oauthProtectedResourceEndpoint } from './endpoint/oauth.js';
|
|
4
|
+
import { oauthServerEndpoints } from './endpoint/oauthServer.js';
|
|
3
5
|
import { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js';
|
|
4
6
|
export { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js';
|
|
7
|
+
export { oauthProtectedResourceHandler } from './endpoint/oauth.js';
|
|
5
8
|
export const mcpPlugin = definePlugin({
|
|
6
9
|
slug: '@payloadcms/plugin-mcp',
|
|
7
10
|
order: 10,
|
|
@@ -23,8 +26,10 @@ export const mcpPlugin = definePlugin({
|
|
|
23
26
|
...config,
|
|
24
27
|
endpoints: [
|
|
25
28
|
...config.endpoints ?? [],
|
|
26
|
-
// Payload prefixes /api, so the full path is /api/mcp.
|
|
29
|
+
// Payload prefixes /api, so the full path is /api/mcp. getAuthorizedMCP authenticates the
|
|
30
|
+
// request with dev auto-login turned off, so Payload doesn't authenticate it first.
|
|
27
31
|
{
|
|
32
|
+
disableAuth: true,
|
|
28
33
|
handler: mcpEndpoint,
|
|
29
34
|
method: 'post',
|
|
30
35
|
path: '/mcp'
|
|
@@ -32,6 +37,7 @@ export const mcpPlugin = definePlugin({
|
|
|
32
37
|
// Streamable HTTP's optional server=>client GET stream. We don't offer
|
|
33
38
|
// one, so answer 405 per spec-— clients then skip it cleanly
|
|
34
39
|
{
|
|
40
|
+
disableAuth: true,
|
|
35
41
|
handler: ()=>new Response(null, {
|
|
36
42
|
headers: {
|
|
37
43
|
Allow: 'POST'
|
|
@@ -40,7 +46,14 @@ export const mcpPlugin = definePlugin({
|
|
|
40
46
|
}),
|
|
41
47
|
method: 'get',
|
|
42
48
|
path: '/mcp'
|
|
43
|
-
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
disableAuth: true,
|
|
52
|
+
handler: oauthProtectedResourceEndpoint,
|
|
53
|
+
method: 'get',
|
|
54
|
+
path: '/mcp/oauth-protected-resource'
|
|
55
|
+
},
|
|
56
|
+
...oauthServerEndpoints
|
|
44
57
|
]
|
|
45
58
|
};
|
|
46
59
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { definePlugin } from 'payload'\n\nimport type {
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { definePlugin } from 'payload'\n\nimport type {\n AuthorizedMCP,\n MCPBuiltinOAuthConfig,\n MCPExternalOAuthConfig,\n MCPOAuthConfig,\n MCPPluginConfig,\n SanitizedMCPPluginConfig,\n} from './types.js'\n\nimport { mcpEndpoint } from './endpoint/index.js'\nimport { oauthProtectedResourceEndpoint } from './endpoint/oauth.js'\nimport { oauthServerEndpoints } from './endpoint/oauthServer.js'\nimport { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js'\n\ndeclare module 'payload' {\n export interface PayloadRequestAPI {\n MCP: true\n }\n interface RegisteredPlugins {\n /** After the plugin's `plugin` callback runs, `options` holds the sanitized config. */\n '@payloadcms/plugin-mcp': MCPPluginConfig\n }\n export interface RequestContext {\n /** Set on MCP requests before they're authenticated. */\n mcp?: {\n /** The MCP endpoint's address, which OAuth access tokens for MCP are issued for. */\n resource: string\n }\n }\n}\n\nexport type {\n AuthorizedMCP,\n MCPBuiltinOAuthConfig,\n MCPExternalOAuthConfig,\n MCPOAuthConfig,\n MCPPluginConfig,\n SanitizedMCPPluginConfig,\n}\n\nexport { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js'\nexport { oauthProtectedResourceHandler } from './endpoint/oauth.js'\n\nexport const mcpPlugin = definePlugin<MCPPluginConfig>({\n slug: '@payloadcms/plugin-mcp',\n order: 10,\n plugin: ({ config, options, plugins }) => {\n const pluginConfig = sanitizeMCPConfig({ config, pluginConfig: options })\n\n // Stash the sanitized config on plugin options so `getPluginConfig()` reads it.\n const registered = plugins['@payloadcms/plugin-mcp']\n if (registered) {\n // @ts-expect-error\n registered.sanitizedOptions = pluginConfig\n }\n\n if (pluginConfig.disabled) {\n return config\n }\n\n return {\n ...config,\n endpoints: [\n ...(config.endpoints ?? []),\n // Payload prefixes /api, so the full path is /api/mcp. getAuthorizedMCP authenticates the\n // request with dev auto-login turned off, so Payload doesn't authenticate it first.\n { disableAuth: true, handler: mcpEndpoint, method: 'post', path: '/mcp' },\n // Streamable HTTP's optional server=>client GET stream. We don't offer\n // one, so answer 405 per spec-— clients then skip it cleanly\n {\n disableAuth: true,\n handler: () => new Response(null, { headers: { Allow: 'POST' }, status: 405 }),\n method: 'get',\n path: '/mcp',\n },\n {\n disableAuth: true,\n handler: oauthProtectedResourceEndpoint,\n method: 'get',\n path: '/mcp/oauth-protected-resource',\n },\n ...oauthServerEndpoints,\n ],\n }\n },\n})\n"],"names":["definePlugin","mcpEndpoint","oauthProtectedResourceEndpoint","oauthServerEndpoints","sanitizeMCPConfig","defineCollectionTool","defineGlobalTool","definePrompt","defineTool","oauthProtectedResourceHandler","mcpPlugin","slug","order","plugin","config","options","plugins","pluginConfig","registered","sanitizedOptions","disabled","endpoints","disableAuth","handler","method","path","Response","headers","Allow","status"],"mappings":"AAAA,SAASA,YAAY,QAAQ,UAAS;AAWtC,SAASC,WAAW,QAAQ,sBAAqB;AACjD,SAASC,8BAA8B,QAAQ,sBAAqB;AACpE,SAASC,oBAAoB,QAAQ,4BAA2B;AAChE,SAASC,iBAAiB,QAAQ,6BAA4B;AA4B9D,SAASC,oBAAoB,EAAEC,gBAAgB,EAAEC,YAAY,EAAEC,UAAU,QAAQ,kBAAiB;AAClG,SAASC,6BAA6B,QAAQ,sBAAqB;AAEnE,OAAO,MAAMC,YAAYV,aAA8B;IACrDW,MAAM;IACNC,OAAO;IACPC,QAAQ,CAAC,EAAEC,MAAM,EAAEC,OAAO,EAAEC,OAAO,EAAE;QACnC,MAAMC,eAAeb,kBAAkB;YAAEU;YAAQG,cAAcF;QAAQ;QAEvE,gFAAgF;QAChF,MAAMG,aAAaF,OAAO,CAAC,yBAAyB;QACpD,IAAIE,YAAY;YACd,mBAAmB;YACnBA,WAAWC,gBAAgB,GAAGF;QAChC;QAEA,IAAIA,aAAaG,QAAQ,EAAE;YACzB,OAAON;QACT;QAEA,OAAO;YACL,GAAGA,MAAM;YACTO,WAAW;mBACLP,OAAOO,SAAS,IAAI,EAAE;gBAC1B,0FAA0F;gBAC1F,oFAAoF;gBACpF;oBAAEC,aAAa;oBAAMC,SAAStB;oBAAauB,QAAQ;oBAAQC,MAAM;gBAAO;gBACxE,uEAAuE;gBACvE,6DAA6D;gBAC7D;oBACEH,aAAa;oBACbC,SAAS,IAAM,IAAIG,SAAS,MAAM;4BAAEC,SAAS;gCAAEC,OAAO;4BAAO;4BAAGC,QAAQ;wBAAI;oBAC5EL,QAAQ;oBACRC,MAAM;gBACR;gBACA;oBACEH,aAAa;oBACbC,SAASrB;oBACTsB,QAAQ;oBACRC,MAAM;gBACR;mBACGtB;aACJ;QACH;IACF;AACF,GAAE"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
1
2
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
-
import { type PayloadRequest } from 'payload';
|
|
3
3
|
import type { AuthorizedMCP, SanitizedMCPPluginConfig } from '../types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Serving-entry-agnostic core: registers every authorized MCP item onto a fresh
|
|
@@ -9,7 +9,7 @@ import type { AuthorizedMCP, SanitizedMCPPluginConfig } from '../types.js';
|
|
|
9
9
|
*
|
|
10
10
|
* `req` is the request context handlers see. For HTTP it's the live
|
|
11
11
|
* `PayloadRequest` derived from the incoming HTTP request; for stdio it's a
|
|
12
|
-
* synthesized one built via `
|
|
12
|
+
* synthesized one built via `createPayloadRequest`.
|
|
13
13
|
*/
|
|
14
14
|
export declare const buildMcpServer: ({ authorizedMCP, pluginConfig, req, }: {
|
|
15
15
|
authorizedMCP: AuthorizedMCP;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildMcpServer.d.ts","sourceRoot":"","sources":["../../src/mcp/buildMcpServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"buildMcpServer.d.ts","sourceRoot":"","sources":["../../src/mcp/buildMcpServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,OAAO,EAAE,SAAS,EAAsB,MAAM,8BAA8B,CAAA;AAG5E,OAAO,KAAK,EACV,aAAa,EAMb,wBAAwB,EAEzB,MAAM,aAAa,CAAA;AAKpB;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,0CAIxB;IACD,aAAa,EAAE,aAAa,CAAA;IAC5B,YAAY,EAAE,wBAAwB,CAAA;IACtC,GAAG,EAAE,cAAc,CAAA;CACpB,KAAG,SAgNH,CAAA"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
-
import { APIError } from 'payload';
|
|
3
|
-
import { z } from 'zod';
|
|
2
|
+
import { APIError, strictObject, z } from 'payload';
|
|
4
3
|
import { getLogger } from '../utils/getLogger.js';
|
|
5
4
|
import { toStandardSchema } from '../utils/toStandardSchema.js';
|
|
6
5
|
/**
|
|
@@ -11,8 +10,9 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
|
|
|
11
10
|
*
|
|
12
11
|
* `req` is the request context handlers see. For HTTP it's the live
|
|
13
12
|
* `PayloadRequest` derived from the incoming HTTP request; for stdio it's a
|
|
14
|
-
* synthesized one built via `
|
|
13
|
+
* synthesized one built via `createPayloadRequest`.
|
|
15
14
|
*/ export const buildMcpServer = ({ authorizedMCP, pluginConfig, req })=>{
|
|
15
|
+
z.config(z.locales.en());
|
|
16
16
|
const serverOptions = pluginConfig.mcp?.serverOptions || {};
|
|
17
17
|
const server = new McpServer({
|
|
18
18
|
name: 'Payload MCP Server',
|
|
@@ -40,20 +40,19 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
|
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
42
|
* Runs a collection/global tool call:
|
|
43
|
-
* - reads `
|
|
43
|
+
* - reads `slug` from the input
|
|
44
44
|
* - runs access control: errors if `authorizedMCP.items` has no entry for this tool + slug
|
|
45
45
|
* - runs the tool handler and finalizes its response
|
|
46
46
|
*/ const callEntityTool = async ({ input, item, serverContext })=>{
|
|
47
47
|
const entity = item.type === 'collectionTool' ? 'collection' : 'global';
|
|
48
|
-
const slugKey = item.type === 'collectionTool' ? 'collectionSlug' : 'globalSlug';
|
|
49
48
|
const toolInput = input ?? {};
|
|
50
|
-
const slug = toolInput
|
|
49
|
+
const slug = toolInput.slug;
|
|
51
50
|
if (!slug) {
|
|
52
51
|
return {
|
|
53
52
|
content: [
|
|
54
53
|
{
|
|
55
54
|
type: 'text',
|
|
56
|
-
text: `Error: "${item.mcpName}" requires
|
|
55
|
+
text: `Error: "${item.mcpName}" requires slug. Use getConfigInfo to inspect ${entity} slugs.`
|
|
57
56
|
}
|
|
58
57
|
],
|
|
59
58
|
isError: true
|
|
@@ -79,10 +78,10 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
|
|
|
79
78
|
};
|
|
80
79
|
const response = await (match.type === 'collectionTool' ? match.tool.handler({
|
|
81
80
|
...handlerArgs,
|
|
82
|
-
|
|
81
|
+
slug
|
|
83
82
|
}) : match.tool.handler({
|
|
84
83
|
...handlerArgs,
|
|
85
|
-
|
|
84
|
+
slug
|
|
86
85
|
}));
|
|
87
86
|
return finalizeToolResponse({
|
|
88
87
|
input: toolInput,
|
|
@@ -103,7 +102,6 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
|
|
|
103
102
|
}
|
|
104
103
|
registeredEntityTools.add(item.mcpName);
|
|
105
104
|
const inputSchema = withSlugInput({
|
|
106
|
-
name: item.type === 'collectionTool' ? 'collectionSlug' : 'globalSlug',
|
|
107
105
|
input: item.tool.input
|
|
108
106
|
});
|
|
109
107
|
server.registerTool(item.mcpName, {
|
|
@@ -189,33 +187,47 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
|
|
|
189
187
|
}
|
|
190
188
|
return server;
|
|
191
189
|
};
|
|
192
|
-
const withSlugInput = ({
|
|
193
|
-
const description =
|
|
194
|
-
const slugSchema = z.string().describe(description);
|
|
190
|
+
const withSlugInput = ({ input })=>{
|
|
191
|
+
const description = 'The target slug.';
|
|
192
|
+
const slugSchema = z.string().check(z.describe(description));
|
|
195
193
|
if (!input) {
|
|
196
|
-
return
|
|
197
|
-
|
|
194
|
+
return strictObject({
|
|
195
|
+
slug: slugSchema
|
|
198
196
|
});
|
|
199
197
|
}
|
|
200
|
-
if (input
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
if (typeof input === 'object' && input !== null && '~standard' in input) {
|
|
199
|
+
const schema = input['~standard'].jsonSchema.input({
|
|
200
|
+
target: 'draft-2020-12'
|
|
201
|
+
});
|
|
202
|
+
const properties = schema.properties;
|
|
203
|
+
if (properties?.slug) {
|
|
204
|
+
return input;
|
|
205
|
+
}
|
|
206
|
+
return addSlugToJSONSchema({
|
|
207
|
+
description,
|
|
208
|
+
schema
|
|
203
209
|
});
|
|
204
210
|
}
|
|
205
|
-
|
|
211
|
+
return addSlugToJSONSchema({
|
|
212
|
+
description,
|
|
213
|
+
schema: input
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
const addSlugToJSONSchema = ({ description, schema })=>{
|
|
217
|
+
const objectSchema = schema;
|
|
206
218
|
return {
|
|
207
|
-
...
|
|
219
|
+
...objectSchema,
|
|
208
220
|
type: 'object',
|
|
209
221
|
properties: {
|
|
210
|
-
...
|
|
211
|
-
|
|
222
|
+
...objectSchema.properties,
|
|
223
|
+
slug: {
|
|
212
224
|
type: 'string',
|
|
213
225
|
description
|
|
214
226
|
}
|
|
215
227
|
},
|
|
216
228
|
required: Array.from(new Set([
|
|
217
|
-
|
|
218
|
-
...
|
|
229
|
+
'slug',
|
|
230
|
+
...objectSchema.required ?? []
|
|
219
231
|
]))
|
|
220
232
|
};
|
|
221
233
|
};
|