@adminforth/mcp 1.1.2 → 1.3.0
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/README.md +2 -2
- package/dist/adminPanelConfirmation.d.ts +47 -0
- package/dist/adminPanelConfirmation.js +180 -0
- package/dist/apiTools.d.ts +10 -12
- package/dist/apiTools.js +58 -49
- package/dist/authSecretStore.d.ts +10 -12
- package/dist/authSecretStore.js +42 -115
- package/dist/clientInfo.js +1 -1
- package/dist/custom/McpConfirm.vue +97 -0
- package/dist/custom/McpSettings.vue +3 -13
- package/dist/custom/instructions.md +1 -1
- package/dist/custom/skills/mutate_data/SKILL.md +1 -1
- package/dist/endpoints/confirmation.d.ts +3 -0
- package/dist/endpoints/confirmation.js +38 -0
- package/dist/endpoints/mcp.d.ts +2 -0
- package/dist/endpoints/mcp.js +24 -0
- package/dist/endpoints/oAuth.d.ts +3 -0
- package/dist/{oauthEndpoints.js → endpoints/oAuth.js} +16 -35
- package/dist/endpoints/settings.d.ts +4 -0
- package/dist/endpoints/settings.js +44 -0
- package/dist/endpoints/tools.d.ts +2 -0
- package/dist/endpoints/tools.js +19 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +54 -175
- package/dist/mcpProtocol.d.ts +9 -22
- package/dist/mcpProtocol.js +23 -118
- package/dist/{oauth.d.ts → oauth/authorizationServer.d.ts} +8 -16
- package/dist/{oauth.js → oauth/authorizationServer.js} +10 -22
- package/dist/{oauthClientMetadata.d.ts → oauth/clientMetadata.d.ts} +1 -5
- package/dist/{oauthClientMetadata.js → oauth/clientMetadata.js} +8 -28
- package/dist/oauth/redirectUris.d.ts +4 -0
- package/dist/oauth/redirectUris.js +18 -0
- package/dist/oauth/setup.d.ts +7 -0
- package/dist/oauth/setup.js +25 -0
- package/dist/repositories/authSecret.d.ts +31 -0
- package/dist/repositories/authSecret.js +86 -0
- package/dist/requestExtra.d.ts +2 -0
- package/dist/requestExtra.js +10 -0
- package/dist/schemas/confirmation.d.ts +18 -0
- package/dist/schemas/confirmation.js +9 -0
- package/dist/schemas/oAuth.d.ts +13 -0
- package/dist/schemas/oAuth.js +12 -0
- package/dist/schemas/settings.d.ts +10 -0
- package/dist/schemas/settings.js +8 -0
- package/dist/schemas/tools.d.ts +7 -0
- package/dist/schemas/tools.js +7 -0
- package/dist/serverReadOnly.d.ts +4 -0
- package/dist/serverReadOnly.js +3 -0
- package/dist/skills.d.ts +2 -2
- package/dist/twoFactorContext.d.ts +6 -0
- package/dist/twoFactorContext.js +5 -0
- package/dist/types.d.ts +9 -13
- package/dist/urls.d.ts +10 -5
- package/dist/urls.js +10 -1
- package/package.json +9 -4
- package/dist/oauthEndpoints.d.ts +0 -3
- /package/dist/{inputSchema.d.ts → compactInputSchema.d.ts} +0 -0
- /package/dist/{inputSchema.js → compactInputSchema.js} +0 -0
- /package/dist/{oauthError.d.ts → oauth/errors.d.ts} +0 -0
- /package/dist/{oauthError.js → oauth/errors.js} +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -4,16 +4,16 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
4
4
|
options: PluginOptions;
|
|
5
5
|
pluginsScope: 'global';
|
|
6
6
|
private authSecretStore;
|
|
7
|
-
/** Null when OAuth sign-in is not configured; then only auth secrets authenticate. */
|
|
8
7
|
private oauth;
|
|
9
8
|
private apiTools;
|
|
9
|
+
private adminPanelConfirmation?;
|
|
10
10
|
private skills;
|
|
11
|
-
/** Null without adminPanelOrigin; then the settings page shows the MCP URL of the address it was opened at. */
|
|
12
11
|
private urls;
|
|
12
|
+
private readonly protocol;
|
|
13
|
+
private readonly serverReadOnly;
|
|
13
14
|
constructor(options: PluginOptions);
|
|
14
15
|
instanceUniqueRepresentation(): string;
|
|
15
16
|
modifyGlobalConfig(adminforth: IAdminForth): void;
|
|
16
|
-
private createOAuth;
|
|
17
17
|
private validateAuthSecretResource;
|
|
18
18
|
setupEndpoints(server: IHttpServer): void;
|
|
19
19
|
/** Accepts personal auth secrets and OAuth access tokens; access tokens are JWTs, secrets have their prefix. */
|
|
@@ -22,4 +22,3 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
22
22
|
}
|
|
23
23
|
export type { McpAuthSecretResourceOptions, McpOAuthClient, PluginOptions } from './types.js';
|
|
24
24
|
export { canonicalAgentName, formatMcpExecutedBy, readMcpClient } from './clientInfo.js';
|
|
25
|
-
export { handleMcpProtocol } from './mcpProtocol.js';
|
package/dist/index.js
CHANGED
|
@@ -1,53 +1,27 @@
|
|
|
1
|
-
import { AdminForthPlugin,
|
|
1
|
+
import { AdminForthPlugin, } from 'adminforth';
|
|
2
|
+
import { McpAdminPanelConfirmation, WAIT_FOR_CONFIRMATION_TOOL_NAME } from './adminPanelConfirmation.js';
|
|
3
|
+
import { registerConfirmationEndpoints } from './endpoints/confirmation.js';
|
|
2
4
|
import { AdminForthApiTools } from './apiTools.js';
|
|
3
5
|
import { formatMcpExecutedBy, readMcpClient, UNKNOWN_CLIENT } from './clientInfo.js';
|
|
4
|
-
import { createMcpServerPresentation,
|
|
6
|
+
import { createMcpServerPresentation, McpProtocol } from './mcpProtocol.js';
|
|
5
7
|
import { McpAuthSecretStore, SECRET_PREFIX } from './authSecretStore.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { registerMcpEndpoints } from './endpoints/mcp.js';
|
|
9
|
+
import { registerOAuthEndpoints } from './endpoints/oAuth.js';
|
|
10
|
+
import { registerSettingsEndpoints } from './endpoints/settings.js';
|
|
11
|
+
import { registerToolEndpoints } from './endpoints/tools.js';
|
|
12
|
+
import { AuthSecretRepository } from './repositories/authSecret.js';
|
|
13
|
+
import { setupOAuth } from './oauth/setup.js';
|
|
14
|
+
import { requestExtra } from './requestExtra.js';
|
|
15
|
+
import { createServerReadOnly } from './serverReadOnly.js';
|
|
9
16
|
import { FETCH_SKILL_TOOL_NAME, McpSkills } from './skills.js';
|
|
10
|
-
import {
|
|
17
|
+
import { findContextualTwoFactorsAuth } from './twoFactorContext.js';
|
|
18
|
+
import { adminApiPrefix, CONFIRMATION_PAGE_PATH, createMcpUrls } from './urls.js';
|
|
11
19
|
const BEARER_TOKEN_RE = /^Bearer (\S+)$/i;
|
|
12
20
|
const CREDENTIAL_HEADERS = new Set(['authorization', 'proxy-authorization', 'cookie']);
|
|
13
21
|
const DEFAULT_PAGE_SIZE = 10;
|
|
14
22
|
const MAX_PAGE_SIZE = 100;
|
|
15
23
|
const DEFAULT_TOOL_TIMEOUT_MS = 15_000;
|
|
16
24
|
const DEFAULT_TOOL_CALLS_PER_REQUEST = 10;
|
|
17
|
-
const RESOURCES_LIST_RESPONSE_SCHEMA = {
|
|
18
|
-
type: 'object',
|
|
19
|
-
required: ['resources'],
|
|
20
|
-
properties: {
|
|
21
|
-
resources: {
|
|
22
|
-
type: 'array',
|
|
23
|
-
description: 'Every resource of the admin panel. Access is checked when a resource is used, so the user may still be forbidden to list, show, create, edit or delete records of a listed resource.',
|
|
24
|
-
items: {
|
|
25
|
-
type: 'object',
|
|
26
|
-
required: ['resourceId', 'label'],
|
|
27
|
-
properties: {
|
|
28
|
-
resourceId: {
|
|
29
|
-
type: 'string',
|
|
30
|
-
description: 'Resource identifier. Pass it as resourceId to get_resource, get_resource_data, aggregate, create_record, update_record, delete_record and other resource tools. Call get_resource with it to get the columns and allowed actions of the resource.',
|
|
31
|
-
},
|
|
32
|
-
label: {
|
|
33
|
-
type: 'string',
|
|
34
|
-
description: 'Human readable resource name, translated for the current user.',
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
function requestExtra(input) {
|
|
42
|
-
return {
|
|
43
|
-
body: input.body,
|
|
44
|
-
query: input.query,
|
|
45
|
-
headers: input.headers,
|
|
46
|
-
cookies: input.cookies,
|
|
47
|
-
requestUrl: input.requestUrl,
|
|
48
|
-
response: input.response,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
25
|
/**
|
|
52
26
|
* The MCP auth secret is a long-lived credential, so it is dropped as soon as it is verified and
|
|
53
27
|
* never reaches tool handlers, which may log, persist or forward the headers they are given.
|
|
@@ -66,15 +40,17 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
66
40
|
options;
|
|
67
41
|
pluginsScope = 'global';
|
|
68
42
|
authSecretStore;
|
|
69
|
-
/** Null when OAuth sign-in is not configured; then only auth secrets authenticate. */
|
|
70
43
|
oauth;
|
|
71
44
|
apiTools;
|
|
45
|
+
adminPanelConfirmation;
|
|
72
46
|
skills;
|
|
73
|
-
/** Null without adminPanelOrigin; then the settings page shows the MCP URL of the address it was opened at. */
|
|
74
47
|
urls;
|
|
48
|
+
protocol = new McpProtocol();
|
|
49
|
+
serverReadOnly;
|
|
75
50
|
constructor(options) {
|
|
76
51
|
super(options, import.meta.url);
|
|
77
52
|
this.options = options;
|
|
53
|
+
this.serverReadOnly = createServerReadOnly(options.readOnly);
|
|
78
54
|
this.shouldHaveSingleInstancePerWholeApp = () => true;
|
|
79
55
|
}
|
|
80
56
|
instanceUniqueRepresentation() {
|
|
@@ -107,47 +83,29 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
107
83
|
isVisible: () => true,
|
|
108
84
|
});
|
|
109
85
|
}
|
|
110
|
-
this.urls = this.options.adminPanelOrigin
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
86
|
+
this.urls = createMcpUrls(this.options.adminPanelOrigin, adminforth.config.baseUrl);
|
|
87
|
+
this.authSecretStore = new McpAuthSecretStore(adminforth, new AuthSecretRepository(adminforth, this.options.authSecretResource));
|
|
88
|
+
this.oauth = setupOAuth(adminforth, this.authSecretStore, this.urls, this.options, this.componentPath('McpAuthorize.vue'), this.serverReadOnly);
|
|
89
|
+
const toolTimeoutMs = this.options.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
90
|
+
const twoFactorsAuth = findContextualTwoFactorsAuth(adminforth);
|
|
91
|
+
if (twoFactorsAuth) {
|
|
92
|
+
adminforth.config.customization.customPages.push({
|
|
93
|
+
path: CONFIRMATION_PAGE_PATH,
|
|
94
|
+
component: { file: this.componentPath('McpConfirm.vue'), meta: { sidebarAndHeader: 'none' } },
|
|
95
|
+
});
|
|
96
|
+
this.adminPanelConfirmation = new McpAdminPanelConfirmation(adminforth, this.urls, twoFactorsAuth, toolTimeoutMs);
|
|
97
|
+
}
|
|
115
98
|
const pageSize = {
|
|
116
99
|
default: this.options.pageSize?.default ?? DEFAULT_PAGE_SIZE,
|
|
117
100
|
max: this.options.pageSize?.max ?? MAX_PAGE_SIZE,
|
|
118
101
|
};
|
|
119
|
-
this.apiTools = new AdminForthApiTools(adminforth, new Set([this.options.authSecretResource.resourceId]), pageSize, this.
|
|
102
|
+
this.apiTools = new AdminForthApiTools(adminforth, new Set([this.options.authSecretResource.resourceId]), pageSize, toolTimeoutMs, this.adminPanelConfirmation);
|
|
120
103
|
this.skills = new McpSkills(this.customFolderPath, {
|
|
121
104
|
'pageSize.default': pageSize.default,
|
|
122
105
|
'pageSize.max': pageSize.max,
|
|
123
106
|
toolCallsPerRequest: this.options.toolCallsPerRequest ?? DEFAULT_TOOL_CALLS_PER_REQUEST,
|
|
124
107
|
});
|
|
125
108
|
}
|
|
126
|
-
createOAuth(adminforth) {
|
|
127
|
-
if (!this.urls) {
|
|
128
|
-
throw new Error('AdminForthMcpPlugin: adminPanelOrigin is required with authSecretResource.oauthClientIdField, '
|
|
129
|
-
+ 'OAuth issuer and MCP resource URLs are built from it');
|
|
130
|
-
}
|
|
131
|
-
// OAuth sends codes and tokens through these endpoints, so they must be served over TLS (RFC 6749 §3.1);
|
|
132
|
-
// plain http is left for a local admin panel.
|
|
133
|
-
const origin = new URL(this.urls.adminPanelUrl);
|
|
134
|
-
if (origin.protocol !== 'https:' && !isLoopbackUrl(origin)) {
|
|
135
|
-
throw new Error(`AdminForthMcpPlugin: adminPanelOrigin must use https with OAuth sign-in, got "${origin.origin}"; `
|
|
136
|
-
+ 'http is allowed only for localhost and 127.0.0.1');
|
|
137
|
-
}
|
|
138
|
-
adminforth.config.customization.customPages.push({
|
|
139
|
-
path: CONSENT_PAGE_PATH,
|
|
140
|
-
component: {
|
|
141
|
-
file: this.componentPath('McpAuthorize.vue'),
|
|
142
|
-
meta: { sidebarAndHeader: 'none' },
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
const isProduction = process.env.NODE_ENV === 'production';
|
|
146
|
-
if (isProduction && this.options.devOAuthClients?.length) {
|
|
147
|
-
logger.warn('AdminForthMcpPlugin: devOAuthClients are ignored because NODE_ENV is "production"');
|
|
148
|
-
}
|
|
149
|
-
return new McpOAuth(adminforth, this.authSecretStore, this.urls, isProduction ? [] : this.options.devOAuthClients, this.options.readOnly);
|
|
150
|
-
}
|
|
151
109
|
validateAuthSecretResource(resource) {
|
|
152
110
|
const fields = this.options.authSecretResource;
|
|
153
111
|
for (const fieldName of [
|
|
@@ -159,7 +117,7 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
159
117
|
fields.lastUsedAtField,
|
|
160
118
|
fields.lastUsedByAgentField,
|
|
161
119
|
fields.readOnlyField,
|
|
162
|
-
|
|
120
|
+
fields.oauthClientIdField,
|
|
163
121
|
]) {
|
|
164
122
|
if (!resource.columns.some((column) => column.name === fieldName)) {
|
|
165
123
|
throw new Error(`AdminForthMcpPlugin: column "${fieldName}" not found in auth secret resource "${resource.resourceId}"`);
|
|
@@ -170,95 +128,17 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
170
128
|
}
|
|
171
129
|
}
|
|
172
130
|
setupEndpoints(server) {
|
|
173
|
-
server.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
},
|
|
180
|
-
response_schema: RESOURCES_LIST_RESPONSE_SCHEMA,
|
|
181
|
-
handler: async ({ tr }) => {
|
|
182
|
-
const resources = await Promise.all(this.adminforth.config.resources.map(async (resource) => ({
|
|
183
|
-
resourceId: resource.resourceId,
|
|
184
|
-
label: await tr(resource.label, `resource.${resource.resourceId}`),
|
|
185
|
-
})));
|
|
186
|
-
return { resources };
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
server.endpoint({
|
|
190
|
-
method: 'GET',
|
|
191
|
-
path: `${MCP_PATH}/auth-secrets`,
|
|
192
|
-
agent: {
|
|
193
|
-
hiddenFromAgents: true,
|
|
194
|
-
},
|
|
195
|
-
handler: async ({ adminUser }) => ({
|
|
196
|
-
authSecrets: await this.authSecretStore.list(adminUser),
|
|
197
|
-
oauthEnabled: Boolean(this.oauth),
|
|
198
|
-
mcpUrl: this.urls?.mcpUrl ?? null,
|
|
199
|
-
serverReadOnly: this.options.readOnly ?? false,
|
|
200
|
-
}),
|
|
201
|
-
});
|
|
202
|
-
server.endpoint({
|
|
203
|
-
method: 'POST',
|
|
204
|
-
path: `${MCP_PATH}/auth-secrets`,
|
|
205
|
-
agent: {
|
|
206
|
-
hiddenFromAgents: true,
|
|
207
|
-
},
|
|
208
|
-
handler: async (input) => {
|
|
209
|
-
const name = String(input.body.name ?? '').trim();
|
|
210
|
-
if (!name) {
|
|
211
|
-
input.response.setStatus(400);
|
|
212
|
-
return { error: 'Auth secret name is required' };
|
|
213
|
-
}
|
|
214
|
-
return this.authSecretStore.create(name, this.options.readOnly || input.body.readOnly === true, input.adminUser, requestExtra(input));
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
server.endpoint({
|
|
218
|
-
method: 'DELETE',
|
|
219
|
-
path: `${MCP_PATH}/auth-secrets`,
|
|
220
|
-
agent: {
|
|
221
|
-
hiddenFromAgents: true,
|
|
222
|
-
},
|
|
223
|
-
handler: async (input) => {
|
|
224
|
-
const result = await this.authSecretStore.revoke(String(input.body.id), input.adminUser, requestExtra(input));
|
|
225
|
-
if (result.error)
|
|
226
|
-
input.response.setStatus(404);
|
|
227
|
-
return result;
|
|
228
|
-
},
|
|
229
|
-
});
|
|
230
|
-
if (this.oauth) {
|
|
231
|
-
setupOAuthEndpoints(server, this.oauth, adminApiPrefix(this.adminforth.config.baseUrl));
|
|
232
|
-
}
|
|
233
|
-
server.endpoint({
|
|
234
|
-
method: 'POST',
|
|
235
|
-
path: MCP_PATH,
|
|
236
|
-
agent: {
|
|
237
|
-
hiddenFromAgents: true,
|
|
238
|
-
},
|
|
239
|
-
noAuth: true,
|
|
240
|
-
handler: async (input) => this.handleMcpRequest(input),
|
|
241
|
-
});
|
|
242
|
-
// Streamable HTTP clients open a GET stream for server-initiated messages; 405 tells them this server has none.
|
|
243
|
-
server.endpoint({
|
|
244
|
-
method: 'GET',
|
|
245
|
-
path: MCP_PATH,
|
|
246
|
-
agent: {
|
|
247
|
-
hiddenFromAgents: true,
|
|
248
|
-
},
|
|
249
|
-
noAuth: true,
|
|
250
|
-
handler: async ({ response }) => {
|
|
251
|
-
response.setHeader('Allow', 'POST');
|
|
252
|
-
response.setStatus(405, 'Method Not Allowed');
|
|
253
|
-
},
|
|
254
|
-
});
|
|
131
|
+
registerToolEndpoints(server, this.adminforth);
|
|
132
|
+
registerSettingsEndpoints(server, this.authSecretStore, this.urls.mcpUrl, this.serverReadOnly);
|
|
133
|
+
registerOAuthEndpoints(server, this.oauth, adminApiPrefix(this.adminforth.config.baseUrl));
|
|
134
|
+
if (this.adminPanelConfirmation)
|
|
135
|
+
registerConfirmationEndpoints(server, this.adminPanelConfirmation);
|
|
136
|
+
registerMcpEndpoints(server, (input) => this.handleMcpRequest(input));
|
|
255
137
|
}
|
|
256
138
|
/** Accepts personal auth secrets and OAuth access tokens; access tokens are JWTs, secrets have their prefix. */
|
|
257
139
|
async authenticate(token) {
|
|
258
140
|
if (token.startsWith(SECRET_PREFIX))
|
|
259
141
|
return this.authSecretStore.authenticate(token);
|
|
260
|
-
if (!this.oauth)
|
|
261
|
-
return null;
|
|
262
142
|
const accessToken = await this.oauth.verifyAccessToken(token);
|
|
263
143
|
return accessToken && this.authSecretStore.authenticateOAuthGrant(accessToken.grantId, accessToken.pk);
|
|
264
144
|
}
|
|
@@ -271,13 +151,11 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
271
151
|
error: { code: -32003, message: 'Browser-originated MCP requests are not allowed.' },
|
|
272
152
|
};
|
|
273
153
|
}
|
|
274
|
-
const token =
|
|
154
|
+
const token = input.headers.authorization?.match(BEARER_TOKEN_RE)?.[1];
|
|
275
155
|
const authenticated = token ? await this.authenticate(token) : null;
|
|
276
156
|
if (!authenticated) {
|
|
277
157
|
// resource_metadata leads OAuth clients to the authorization server; invalid_token makes them refresh.
|
|
278
|
-
input.response.setHeader('WWW-Authenticate', this.oauth
|
|
279
|
-
? `Bearer ${token ? 'error="invalid_token", ' : ''}resource_metadata="${this.oauth.resourceMetadataUrl}"`
|
|
280
|
-
: 'Bearer');
|
|
158
|
+
input.response.setHeader('WWW-Authenticate', `Bearer ${token ? 'error="invalid_token", ' : ''}resource_metadata="${this.oauth.resourceMetadataUrl}"`);
|
|
281
159
|
input.response.setStatus(401);
|
|
282
160
|
return jsonRpcAuthError(input.body.id ?? null);
|
|
283
161
|
}
|
|
@@ -296,15 +174,20 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
296
174
|
...authenticated.adminUser,
|
|
297
175
|
executedBy: formatMcpExecutedBy(client, authenticated.name),
|
|
298
176
|
};
|
|
299
|
-
const readOnly =
|
|
300
|
-
const protocolResponse = await
|
|
301
|
-
...createMcpServerPresentation(this.adminforth.config.customization.brandName, this.urls
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
177
|
+
const readOnly = authenticated.readOnly || await this.serverReadOnly(authenticated.adminUser);
|
|
178
|
+
const protocolResponse = await this.protocol.handle(new Request(this.urls.mcpUrl, { method: 'POST', headers: toolHeaders }), input.body, {
|
|
179
|
+
...createMcpServerPresentation(this.adminforth.config.customization.brandName, this.urls.adminPanelUrl, this.skills.serverInstructions(), readOnly),
|
|
180
|
+
listTools: () => [
|
|
181
|
+
...this.apiTools.list(readOnly),
|
|
182
|
+
this.skills.toolDefinition(),
|
|
183
|
+
...(this.adminPanelConfirmation ? [this.adminPanelConfirmation.toolDefinition()] : []),
|
|
184
|
+
],
|
|
305
185
|
callTool: async (name, arguments_) => {
|
|
306
186
|
if (name === FETCH_SKILL_TOOL_NAME)
|
|
307
187
|
return this.skills.call(arguments_);
|
|
188
|
+
if (name === WAIT_FOR_CONFIRMATION_TOOL_NAME && this.adminPanelConfirmation) {
|
|
189
|
+
return this.adminPanelConfirmation.wait(String(arguments_?.confirmationId), authenticated.recordId);
|
|
190
|
+
}
|
|
308
191
|
return this.apiTools.call({
|
|
309
192
|
name,
|
|
310
193
|
arguments: arguments_,
|
|
@@ -313,16 +196,12 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
313
196
|
requestUrl: input.requestUrl,
|
|
314
197
|
abortSignal: input.abortSignal,
|
|
315
198
|
readOnly,
|
|
199
|
+
connection: { id: authenticated.recordId, name: authenticated.name },
|
|
316
200
|
});
|
|
317
201
|
},
|
|
318
202
|
});
|
|
319
|
-
input.response.
|
|
320
|
-
|
|
321
|
-
input._raw_express_res.status(protocolResponse.status).end();
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
return protocolResponse.body;
|
|
203
|
+
protocolResponse.headers.forEach((value, name) => input.response.setHeader(name, value));
|
|
204
|
+
input.response.setStatus(protocolResponse.status, await protocolResponse.text());
|
|
325
205
|
}
|
|
326
206
|
}
|
|
327
207
|
export { canonicalAgentName, formatMcpExecutedBy, readMcpClient } from './clientInfo.js';
|
|
328
|
-
export { handleMcpProtocol } from './mcpProtocol.js';
|
package/dist/mcpProtocol.d.ts
CHANGED
|
@@ -1,31 +1,18 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export declare const MODERN_PROTOCOL_VERSION = "2026-07-28";
|
|
3
|
-
export declare const LEGACY_PROTOCOL_VERSION = "2025-11-25";
|
|
4
|
-
export interface McpServerInfo {
|
|
5
|
-
name: string;
|
|
6
|
-
title: string;
|
|
7
|
-
version: string;
|
|
8
|
-
description: string;
|
|
9
|
-
websiteUrl?: string;
|
|
10
|
-
}
|
|
1
|
+
import { type Implementation, type Tool } from '@modelcontextprotocol/server';
|
|
11
2
|
export interface McpServerPresentation {
|
|
12
|
-
serverInfo:
|
|
3
|
+
serverInfo: Implementation;
|
|
13
4
|
instructions: string;
|
|
14
5
|
}
|
|
15
|
-
export declare function createMcpServerPresentation(brandName: string, adminPanelUrl
|
|
16
|
-
export interface McpProtocolContext {
|
|
17
|
-
|
|
18
|
-
headers: Record<string, any>;
|
|
19
|
-
serverInfo: McpServerInfo;
|
|
20
|
-
instructions: string;
|
|
21
|
-
listTools: () => McpToolDefinition[];
|
|
6
|
+
export declare function createMcpServerPresentation(brandName: string, adminPanelUrl: string, guidance?: string, readOnly?: boolean): McpServerPresentation;
|
|
7
|
+
export interface McpProtocolContext extends McpServerPresentation {
|
|
8
|
+
listTools: () => Tool[];
|
|
22
9
|
callTool: (name: string, arguments_: Record<string, unknown> | undefined) => Promise<{
|
|
23
10
|
output: unknown;
|
|
24
11
|
isError: boolean;
|
|
25
12
|
}>;
|
|
26
13
|
}
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
export declare class McpProtocol {
|
|
15
|
+
private readonly contexts;
|
|
16
|
+
private readonly handler;
|
|
17
|
+
handle(request: Request, parsedBody: unknown, context: McpProtocolContext): Promise<Response>;
|
|
30
18
|
}
|
|
31
|
-
export declare function handleMcpProtocol(context: McpProtocolContext): Promise<McpProtocolResponse>;
|
package/dist/mcpProtocol.js
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
|
+
import { createMcpHandler, Server } from '@modelcontextprotocol/server';
|
|
1
2
|
import YAML from 'yaml';
|
|
2
|
-
|
|
3
|
-
export const LEGACY_PROTOCOL_VERSION = '2025-11-25';
|
|
4
|
-
const LEGACY_PROTOCOL_VERSIONS = new Set([
|
|
5
|
-
LEGACY_PROTOCOL_VERSION,
|
|
6
|
-
'2025-06-18',
|
|
7
|
-
'2025-03-26',
|
|
8
|
-
]);
|
|
9
|
-
const PROTOCOL_VERSION_META_KEY = 'io.modelcontextprotocol/protocolVersion';
|
|
10
|
-
const CLIENT_CAPABILITIES_META_KEY = 'io.modelcontextprotocol/clientCapabilities';
|
|
11
|
-
const SERVER_INFO_META_KEY = 'io.modelcontextprotocol/serverInfo';
|
|
3
|
+
const UNCACHED = { ttlMs: 0, cacheScope: 'private' };
|
|
12
4
|
export function createMcpServerPresentation(brandName, adminPanelUrl, guidance = '', readOnly = false) {
|
|
13
|
-
const adminPanel = `AdminForth admin panel for "${brandName}"
|
|
5
|
+
const adminPanel = `AdminForth admin panel for "${brandName}" at ${adminPanelUrl}`;
|
|
14
6
|
return {
|
|
15
7
|
serverInfo: {
|
|
16
8
|
name: 'adminforth-mcp',
|
|
17
9
|
title: `${brandName} Admin Panel`,
|
|
18
10
|
version: '1.0.0',
|
|
19
11
|
description: `${adminPanel}.`,
|
|
20
|
-
|
|
12
|
+
websiteUrl: adminPanelUrl,
|
|
21
13
|
},
|
|
22
14
|
instructions: [
|
|
23
15
|
`This is the ${adminPanel}. Use its tools to ${readOnly ? 'read' : 'read and update'} data allowed for the authenticated user.`,
|
|
@@ -25,115 +17,28 @@ export function createMcpServerPresentation(brandName, adminPanelUrl, guidance =
|
|
|
25
17
|
].filter(Boolean).join('\n\n'),
|
|
26
18
|
};
|
|
27
19
|
}
|
|
28
|
-
function errorResponse(id, code, message, status = 400) {
|
|
29
|
-
return {
|
|
30
|
-
status,
|
|
31
|
-
body: { jsonrpc: '2.0', id, error: { code, message } },
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
function isModernRequest(body) {
|
|
35
|
-
return body.params?._meta?.[PROTOCOL_VERSION_META_KEY] === MODERN_PROTOCOL_VERSION;
|
|
36
|
-
}
|
|
37
|
-
function completeResult(result, modern, serverInfo) {
|
|
38
|
-
if (!modern)
|
|
39
|
-
return result;
|
|
40
|
-
return {
|
|
41
|
-
resultType: 'complete',
|
|
42
|
-
...result,
|
|
43
|
-
_meta: {
|
|
44
|
-
...result._meta,
|
|
45
|
-
[SERVER_INFO_META_KEY]: serverInfo,
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function successResponse(id, result, modern, serverInfo) {
|
|
50
|
-
return {
|
|
51
|
-
status: 200,
|
|
52
|
-
body: {
|
|
53
|
-
jsonrpc: '2.0',
|
|
54
|
-
id,
|
|
55
|
-
result: completeResult(result, modern, serverInfo),
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function validateModernHeaders(body, headers) {
|
|
60
|
-
const meta = body.params?._meta;
|
|
61
|
-
if (!meta || !(CLIENT_CAPABILITIES_META_KEY in meta)) {
|
|
62
|
-
return errorResponse(body.id ?? null, -32602, 'Missing required MCP request metadata.');
|
|
63
|
-
}
|
|
64
|
-
if (headers['mcp-protocol-version'] !== MODERN_PROTOCOL_VERSION
|
|
65
|
-
|| headers['mcp-method'] !== body.method
|
|
66
|
-
|| (body.method === 'tools/call' && headers['mcp-name'] !== body.params?.name)) {
|
|
67
|
-
return errorResponse(body.id ?? null, -32020, 'MCP request headers do not match the JSON-RPC body.');
|
|
68
|
-
}
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
20
|
// The JSON round trip drops functions and undefined values that handler responses may carry, which YAML cannot serialize.
|
|
72
21
|
function serializeToolOutput(output) {
|
|
73
22
|
return typeof output === 'string' ? output : YAML.stringify(JSON.parse(JSON.stringify(output ?? null)));
|
|
74
23
|
}
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return errorResponse(null, -32600, 'JSON-RPC id must be a string or number.');
|
|
95
|
-
}
|
|
96
|
-
switch (body.method) {
|
|
97
|
-
case 'server/discover':
|
|
98
|
-
if (!modern) {
|
|
99
|
-
return errorResponse(body.id, -32602, 'server/discover requires modern MCP request metadata.');
|
|
100
|
-
}
|
|
101
|
-
return successResponse(body.id, {
|
|
102
|
-
supportedVersions: [MODERN_PROTOCOL_VERSION],
|
|
103
|
-
capabilities: { tools: {} },
|
|
104
|
-
instructions,
|
|
105
|
-
ttlMs: 0,
|
|
106
|
-
cacheScope: 'private',
|
|
107
|
-
}, true, serverInfo);
|
|
108
|
-
case 'initialize': {
|
|
109
|
-
const requestedVersion = body.params?.protocolVersion;
|
|
110
|
-
return successResponse(body.id, {
|
|
111
|
-
protocolVersion: typeof requestedVersion === 'string' && LEGACY_PROTOCOL_VERSIONS.has(requestedVersion)
|
|
112
|
-
? requestedVersion
|
|
113
|
-
: LEGACY_PROTOCOL_VERSION,
|
|
114
|
-
capabilities: { tools: { listChanged: false } },
|
|
115
|
-
serverInfo,
|
|
116
|
-
instructions,
|
|
117
|
-
}, false, serverInfo);
|
|
118
|
-
}
|
|
119
|
-
case 'ping':
|
|
120
|
-
return successResponse(body.id, {}, modern, serverInfo);
|
|
121
|
-
case 'tools/list':
|
|
122
|
-
return successResponse(body.id, {
|
|
123
|
-
tools: context.listTools(),
|
|
124
|
-
...(modern && { ttlMs: 0, cacheScope: 'private' }),
|
|
125
|
-
}, modern, serverInfo);
|
|
126
|
-
case 'tools/call': {
|
|
127
|
-
if (typeof body.params?.name !== 'string') {
|
|
128
|
-
return errorResponse(body.id, -32602, 'Tool name is required.');
|
|
129
|
-
}
|
|
130
|
-
const result = await context.callTool(body.params.name, body.params.arguments);
|
|
131
|
-
return successResponse(body.id, {
|
|
132
|
-
content: [{ type: 'text', text: serializeToolOutput(result.output) }],
|
|
133
|
-
isError: result.isError,
|
|
134
|
-
}, modern, serverInfo);
|
|
135
|
-
}
|
|
136
|
-
default:
|
|
137
|
-
return errorResponse(body.id, -32601, `Method not found: ${body.method}`);
|
|
24
|
+
function createServer({ serverInfo, instructions, listTools, callTool }) {
|
|
25
|
+
const server = new Server(serverInfo, {
|
|
26
|
+
capabilities: { tools: { listChanged: false } },
|
|
27
|
+
instructions,
|
|
28
|
+
cacheHints: { 'tools/list': UNCACHED, 'server/discover': UNCACHED },
|
|
29
|
+
});
|
|
30
|
+
server.setRequestHandler('tools/list', () => ({ tools: listTools() }));
|
|
31
|
+
server.setRequestHandler('tools/call', async ({ params }) => {
|
|
32
|
+
const { output, isError } = await callTool(params.name, params.arguments);
|
|
33
|
+
return { content: [{ type: 'text', text: serializeToolOutput(output) }], isError };
|
|
34
|
+
});
|
|
35
|
+
return server;
|
|
36
|
+
}
|
|
37
|
+
export class McpProtocol {
|
|
38
|
+
contexts = new WeakMap();
|
|
39
|
+
handler = createMcpHandler(({ requestInfo }) => createServer(this.contexts.get(requestInfo)));
|
|
40
|
+
handle(request, parsedBody, context) {
|
|
41
|
+
this.contexts.set(request, context);
|
|
42
|
+
return this.handler.fetch(request, { parsedBody });
|
|
138
43
|
}
|
|
139
44
|
}
|
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import type { AdminUser, IAdminForth } from 'adminforth';
|
|
2
|
-
import type { McpAuthSecretStore } from '
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
export declare const OAUTH_PATHS: {
|
|
7
|
-
protectedResourceMetadata: string;
|
|
8
|
-
authorizationServerMetadata: string;
|
|
9
|
-
jwks: string;
|
|
10
|
-
authorize: string;
|
|
11
|
-
token: string;
|
|
12
|
-
authorization: string;
|
|
13
|
-
};
|
|
14
|
-
export declare const CONSENT_PAGE_PATH = "/mcp-authorize";
|
|
2
|
+
import type { McpAuthSecretStore } from '../authSecretStore.js';
|
|
3
|
+
import type { ServerReadOnly } from '../serverReadOnly.js';
|
|
4
|
+
import type { McpOAuthClient } from '../types.js';
|
|
5
|
+
import { type McpUrls } from '../urls.js';
|
|
15
6
|
interface AccessToken {
|
|
16
7
|
pk: string;
|
|
17
8
|
grantId: string;
|
|
@@ -35,9 +26,10 @@ export declare class McpOAuth {
|
|
|
35
26
|
private readonly fetchedClients;
|
|
36
27
|
/**
|
|
37
28
|
* `configuredClients` are trusted as is, without fetching their client metadata documents. `serverReadOnly` is
|
|
38
|
-
* the plugin readOnly option: it makes
|
|
29
|
+
* the plugin readOnly option for a user: it makes the connections of that user read-only, whatever they choose on
|
|
30
|
+
* the consent page.
|
|
39
31
|
*/
|
|
40
|
-
constructor(adminforth: IAdminForth, store: McpAuthSecretStore, urls: McpUrls, configuredClients
|
|
32
|
+
constructor(adminforth: IAdminForth, store: McpAuthSecretStore, urls: McpUrls, configuredClients: McpOAuthClient[] | undefined, serverReadOnly: ServerReadOnly);
|
|
41
33
|
protectedResourceMetadata(): {
|
|
42
34
|
resource: string;
|
|
43
35
|
authorization_servers: string[];
|
|
@@ -64,7 +56,7 @@ export declare class McpOAuth {
|
|
|
64
56
|
*/
|
|
65
57
|
authorize(query: Record<string, unknown>): Promise<string>;
|
|
66
58
|
/** What the consent page shows about a pending authorization request. */
|
|
67
|
-
describeAuthorization(signedRequest: string): Promise<{
|
|
59
|
+
describeAuthorization(signedRequest: string, adminUser: AdminUser): Promise<{
|
|
68
60
|
clientName: string;
|
|
69
61
|
clientHost: string | null;
|
|
70
62
|
redirectHost: string;
|