@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.
Files changed (60) hide show
  1. package/README.md +2 -2
  2. package/dist/adminPanelConfirmation.d.ts +47 -0
  3. package/dist/adminPanelConfirmation.js +180 -0
  4. package/dist/apiTools.d.ts +10 -12
  5. package/dist/apiTools.js +58 -49
  6. package/dist/authSecretStore.d.ts +10 -12
  7. package/dist/authSecretStore.js +42 -115
  8. package/dist/clientInfo.js +1 -1
  9. package/dist/custom/McpConfirm.vue +97 -0
  10. package/dist/custom/McpSettings.vue +3 -13
  11. package/dist/custom/instructions.md +1 -1
  12. package/dist/custom/skills/mutate_data/SKILL.md +1 -1
  13. package/dist/endpoints/confirmation.d.ts +3 -0
  14. package/dist/endpoints/confirmation.js +38 -0
  15. package/dist/endpoints/mcp.d.ts +2 -0
  16. package/dist/endpoints/mcp.js +24 -0
  17. package/dist/endpoints/oAuth.d.ts +3 -0
  18. package/dist/{oauthEndpoints.js → endpoints/oAuth.js} +16 -35
  19. package/dist/endpoints/settings.d.ts +4 -0
  20. package/dist/endpoints/settings.js +44 -0
  21. package/dist/endpoints/tools.d.ts +2 -0
  22. package/dist/endpoints/tools.js +19 -0
  23. package/dist/index.d.ts +3 -4
  24. package/dist/index.js +54 -175
  25. package/dist/mcpProtocol.d.ts +9 -22
  26. package/dist/mcpProtocol.js +23 -118
  27. package/dist/{oauth.d.ts → oauth/authorizationServer.d.ts} +8 -16
  28. package/dist/{oauth.js → oauth/authorizationServer.js} +10 -22
  29. package/dist/{oauthClientMetadata.d.ts → oauth/clientMetadata.d.ts} +1 -5
  30. package/dist/{oauthClientMetadata.js → oauth/clientMetadata.js} +8 -28
  31. package/dist/oauth/redirectUris.d.ts +4 -0
  32. package/dist/oauth/redirectUris.js +18 -0
  33. package/dist/oauth/setup.d.ts +7 -0
  34. package/dist/oauth/setup.js +25 -0
  35. package/dist/repositories/authSecret.d.ts +31 -0
  36. package/dist/repositories/authSecret.js +86 -0
  37. package/dist/requestExtra.d.ts +2 -0
  38. package/dist/requestExtra.js +10 -0
  39. package/dist/schemas/confirmation.d.ts +18 -0
  40. package/dist/schemas/confirmation.js +9 -0
  41. package/dist/schemas/oAuth.d.ts +13 -0
  42. package/dist/schemas/oAuth.js +12 -0
  43. package/dist/schemas/settings.d.ts +10 -0
  44. package/dist/schemas/settings.js +8 -0
  45. package/dist/schemas/tools.d.ts +7 -0
  46. package/dist/schemas/tools.js +7 -0
  47. package/dist/serverReadOnly.d.ts +4 -0
  48. package/dist/serverReadOnly.js +3 -0
  49. package/dist/skills.d.ts +2 -2
  50. package/dist/twoFactorContext.d.ts +6 -0
  51. package/dist/twoFactorContext.js +5 -0
  52. package/dist/types.d.ts +9 -13
  53. package/dist/urls.d.ts +10 -5
  54. package/dist/urls.js +10 -1
  55. package/package.json +9 -4
  56. package/dist/oauthEndpoints.d.ts +0 -3
  57. /package/dist/{inputSchema.d.ts → compactInputSchema.d.ts} +0 -0
  58. /package/dist/{inputSchema.js → compactInputSchema.js} +0 -0
  59. /package/dist/{oauthError.d.ts → oauth/errors.d.ts} +0 -0
  60. /package/dist/{oauthError.js → oauth/errors.js} +0 -0
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # AdminForth MCP
2
2
 
3
- `@adminforth/mcp` exposes the AdminForth API tool surface as an authenticated remote MCP server. Each MCP auth secret belongs to one AdminForth user, so resource permissions and hooks are preserved.
3
+ `@adminforth/mcp` exposes the AdminForth API tool surface as an authenticated remote MCP server. Agents connect through OAuth sign-in or with an auth secret, and act as the AdminForth user who connected them, so resource permissions and hooks are preserved.
4
4
 
5
- The server identifies itself to agents using the configured AdminForth `brandName`. An optional `adminPanelOrigin` can distinguish deployments further; the plugin combines it with the AdminForth `baseUrl`.
5
+ The required `adminPanelOrigin` is the public origin agents connect to; the plugin combines it with the AdminForth `baseUrl` to build the MCP URL and the OAuth issuer.
6
6
 
7
7
  > **Security:** an MCP auth secret carries the full permissions of the user who created it. An agent
8
8
  > holding it can do everything that user can do in the admin panel, including destructive actions and
@@ -0,0 +1,47 @@
1
+ import type { Tool } from '@modelcontextprotocol/server';
2
+ import type { AdminUser, HttpExtra, IAdminForth, IAdminForthEndpointHandlerInput } from 'adminforth';
3
+ import type { ContextualTwoFactorsAuth } from './twoFactorContext.js';
4
+ import { type McpUrls } from './urls.js';
5
+ export declare const WAIT_FOR_CONFIRMATION_TOOL_NAME = "wait_for_confirmation";
6
+ type ToolCallResult = {
7
+ output: unknown;
8
+ isError: boolean;
9
+ };
10
+ type AdminUserWithExecutor = AdminUser & {
11
+ executedBy?: string;
12
+ };
13
+ export type McpConnection = {
14
+ id: string;
15
+ name: string;
16
+ };
17
+ export declare class McpAdminPanelConfirmation {
18
+ private readonly adminforth;
19
+ private readonly twoFactorsAuth;
20
+ private readonly executionTimeoutMs;
21
+ private readonly pageUrl;
22
+ private readonly apiPrefix;
23
+ private readonly pending;
24
+ constructor(adminforth: IAdminForth, urls: McpUrls, twoFactorsAuth: ContextualTwoFactorsAuth, executionTimeoutMs: number);
25
+ toolDefinition(): Tool;
26
+ runUnverified<T>(extra: HttpExtra, callback: () => Promise<T>): Promise<unknown>;
27
+ offer(output: unknown, connection: McpConnection, adminUser: AdminUserWithExecutor, path: string, args: Record<string, unknown>): ToolCallResult | undefined;
28
+ describe(confirmationId: string, adminUser: AdminUser): {
29
+ error: string;
30
+ } | {
31
+ actionDescription: string | undefined;
32
+ connectionName: string;
33
+ path: string;
34
+ arguments: Record<string, unknown>;
35
+ };
36
+ run(confirmationId: string, verificationResult: Record<string, unknown>, input: IAdminForthEndpointHandlerInput & {
37
+ adminUser: AdminUser;
38
+ }): Promise<unknown>;
39
+ decline(confirmationId: string, adminUser: AdminUser): {
40
+ ok: true;
41
+ } | {
42
+ error: string;
43
+ };
44
+ wait(confirmationId: string, connectionId: string): Promise<ToolCallResult>;
45
+ private open;
46
+ }
47
+ export {};
@@ -0,0 +1,180 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { adminApiPrefix, CONFIRMATION_PAGE_PATH } from './urls.js';
3
+ export const WAIT_FOR_CONFIRMATION_TOOL_NAME = 'wait_for_confirmation';
4
+ const CONFIRMATION_TTL_MS = 10 * 60 * 1000;
5
+ const WAIT_MS = 50_000;
6
+ const TWO_FACTOR_REQUIRED_CODE = 'TWO_FACTOR_REQUIRED';
7
+ export class McpAdminPanelConfirmation {
8
+ adminforth;
9
+ twoFactorsAuth;
10
+ executionTimeoutMs;
11
+ pageUrl;
12
+ apiPrefix;
13
+ pending = new Map();
14
+ constructor(adminforth, urls, twoFactorsAuth, executionTimeoutMs) {
15
+ this.adminforth = adminforth;
16
+ this.twoFactorsAuth = twoFactorsAuth;
17
+ this.executionTimeoutMs = executionTimeoutMs;
18
+ this.pageUrl = new URL(`${adminforth.config.baseUrl}${CONFIRMATION_PAGE_PATH}`, urls.apiUrl).href;
19
+ this.apiPrefix = adminApiPrefix(adminforth.config.baseUrl);
20
+ }
21
+ toolDefinition() {
22
+ return {
23
+ name: WAIT_FOR_CONFIRMATION_TOOL_NAME,
24
+ description: 'Waits until the user confirms or declines in the admin panel an action that returned a confirmationUrl, and returns the result of the action. Returns a waiting status after about a minute; call it again to keep waiting.',
25
+ inputSchema: {
26
+ type: 'object',
27
+ required: ['confirmationId'],
28
+ properties: { confirmationId: { type: 'string' } },
29
+ },
30
+ annotations: { readOnlyHint: true },
31
+ };
32
+ }
33
+ runUnverified(extra, callback) {
34
+ return this.twoFactorsAuth.withVerificationContext(undefined, extra, callback);
35
+ }
36
+ offer(output, connection, adminUser, path, args) {
37
+ const { code, actionDescription } = (output ?? {});
38
+ if (code !== TWO_FACTOR_REQUIRED_CODE)
39
+ return undefined;
40
+ const confirmationId = randomUUID();
41
+ let resolve;
42
+ const pending = {
43
+ userPk: adminUser.pk,
44
+ connection,
45
+ executedBy: adminUser.executedBy,
46
+ actionDescription,
47
+ path,
48
+ arguments: args,
49
+ settled: false,
50
+ running: false,
51
+ settle: (result) => {
52
+ pending.settled = true;
53
+ resolve(result);
54
+ },
55
+ result: new Promise((settle) => { resolve = settle; }),
56
+ };
57
+ this.pending.set(confirmationId, pending);
58
+ setTimeout(() => this.pending.delete(confirmationId), CONFIRMATION_TTL_MS).unref();
59
+ return {
60
+ output: {
61
+ error: `This action needs a two-factor verification, which only the admin panel can ask for. Show the user the confirmationUrl, then call ${WAIT_FOR_CONFIRMATION_TOOL_NAME} with the confirmationId: it returns the result once the user confirms or declines the action there.`,
62
+ confirmationUrl: `${this.pageUrl}?id=${confirmationId}`,
63
+ confirmationId,
64
+ },
65
+ isError: true,
66
+ };
67
+ }
68
+ describe(confirmationId, adminUser) {
69
+ const pending = this.open(confirmationId, adminUser);
70
+ if ('error' in pending)
71
+ return pending;
72
+ return {
73
+ actionDescription: pending.actionDescription,
74
+ connectionName: pending.connection.name,
75
+ path: pending.path,
76
+ arguments: pending.arguments,
77
+ };
78
+ }
79
+ async run(confirmationId, verificationResult, input) {
80
+ const pending = this.open(confirmationId, input.adminUser);
81
+ if ('error' in pending) {
82
+ input.response.setStatus(400);
83
+ return pending;
84
+ }
85
+ pending.running = true;
86
+ const controller = new AbortController();
87
+ const abort = () => controller.abort();
88
+ input.abortSignal?.addEventListener('abort', abort);
89
+ let timer;
90
+ let output;
91
+ let responseStatus = 200;
92
+ const response = {
93
+ setHeader: (key, value) => input.response.setHeader(key, value),
94
+ setStatus: (status, message) => {
95
+ responseStatus = status;
96
+ input.response.setStatus(status, message);
97
+ },
98
+ blobStream: () => input.response.blobStream(),
99
+ };
100
+ try {
101
+ const schema = this.adminforth.openApi.registeredSchemas.find((candidate) => (candidate.path === `${this.apiPrefix}${pending.path}`));
102
+ const hasBody = !['GET', 'HEAD'].includes(schema.method.toUpperCase());
103
+ const extra = pending.arguments.extra;
104
+ const args = { ...pending.arguments, extra: { ...extra, verificationResult } };
105
+ const request = {
106
+ body: hasBody ? args : {},
107
+ query: hasBody ? {} : args,
108
+ headers: input.headers,
109
+ cookies: input.cookies,
110
+ requestUrl: schema.path,
111
+ response,
112
+ };
113
+ const execution = this.twoFactorsAuth.withVerificationContext(verificationResult, request, async () => schema.handler({
114
+ ...request,
115
+ adminUser: { ...input.adminUser, executedBy: pending.executedBy },
116
+ abortSignal: controller.signal,
117
+ tr: input.tr,
118
+ _raw_express_req: undefined,
119
+ _raw_express_res: undefined,
120
+ }));
121
+ const timeout = new Promise((resolve) => {
122
+ timer = setTimeout(() => {
123
+ controller.abort();
124
+ resolve({ ok: false, code: 'MCP_CONFIRMATION_TIMEOUT', error: `Action timed out after ${this.executionTimeoutMs / 1000} seconds. It may still complete; check the result before trying again.` });
125
+ }, this.executionTimeoutMs);
126
+ });
127
+ output = await Promise.race([execution, timeout]);
128
+ if (output === undefined)
129
+ output = { ok: false, error: 'Action handler completed without returning a response.' };
130
+ }
131
+ catch (error) {
132
+ output = { ok: false, code: 'MCP_CONFIRMATION_FAILED', error: error instanceof Error ? error.message : String(error) };
133
+ }
134
+ finally {
135
+ clearTimeout(timer);
136
+ input.abortSignal?.removeEventListener('abort', abort);
137
+ pending.running = false;
138
+ }
139
+ pending.settle({ output, isError: responseStatus >= 400 || Boolean(output && typeof output === 'object' && ('error' in output || output.ok === false)) });
140
+ return output;
141
+ }
142
+ decline(confirmationId, adminUser) {
143
+ const pending = this.open(confirmationId, adminUser);
144
+ if ('error' in pending)
145
+ return pending;
146
+ pending.settle({ output: { error: 'The user declined the action in the admin panel.' }, isError: true });
147
+ return { ok: true };
148
+ }
149
+ async wait(confirmationId, connectionId) {
150
+ const pending = this.pending.get(confirmationId);
151
+ if (pending?.connection.id !== connectionId) {
152
+ return { output: { error: 'Unknown or expired confirmationId.' }, isError: true };
153
+ }
154
+ let timer;
155
+ const timeout = new Promise((resolve) => {
156
+ timer = setTimeout(() => resolve({
157
+ output: { status: 'waiting', note: `The user has not confirmed or declined the action yet. Call ${WAIT_FOR_CONFIRMATION_TOOL_NAME} again to keep waiting.` },
158
+ isError: false,
159
+ }), WAIT_MS);
160
+ });
161
+ try {
162
+ return await Promise.race([pending.result, timeout]);
163
+ }
164
+ finally {
165
+ clearTimeout(timer);
166
+ }
167
+ }
168
+ open(confirmationId, adminUser) {
169
+ const pending = this.pending.get(confirmationId);
170
+ if (!pending)
171
+ return { error: 'This confirmation link is invalid or expired. Ask your agent for a new one.' };
172
+ if (pending.userPk !== adminUser.pk)
173
+ return { error: 'This confirmation link was created for another user.' };
174
+ if (pending.running)
175
+ return { error: 'This action is already running.' };
176
+ if (pending.settled)
177
+ return { error: 'This action was already confirmed or declined.' };
178
+ return pending;
179
+ }
180
+ }
@@ -1,24 +1,21 @@
1
+ import type { Tool } from '@modelcontextprotocol/server';
2
+ import type { McpAdminPanelConfirmation, McpConnection } from './adminPanelConfirmation.js';
1
3
  import type { McpPageSize } from './types.js';
2
4
  import type { AdminUser, IAdminForth } from 'adminforth';
3
- export interface McpToolDefinition {
4
- name: string;
5
- description?: string;
6
- inputSchema: Record<string, unknown>;
7
- annotations?: {
8
- readOnlyHint?: boolean;
9
- destructiveHint?: boolean;
10
- };
11
- }
12
5
  export declare class AdminForthApiTools {
13
6
  private readonly adminforth;
14
7
  private readonly hiddenResourceIds;
15
8
  private readonly toolTimeoutMs;
16
- constructor(adminforth: IAdminForth, hiddenResourceIds: ReadonlySet<string>, pageSize: McpPageSize, toolTimeoutMs: number);
9
+ private readonly adminPanelConfirmation;
10
+ constructor(adminforth: IAdminForth, hiddenResourceIds: ReadonlySet<string>, pageSize: McpPageSize, toolTimeoutMs: number, adminPanelConfirmation: McpAdminPanelConfirmation | undefined);
17
11
  private readonly toolOverrides;
18
- private schemas;
12
+ private fullCatalog?;
13
+ private readOnlyCatalog?;
14
+ private catalog;
15
+ private buildCatalog;
19
16
  private description;
20
17
  private withTimeout;
21
- list(readOnly: boolean): McpToolDefinition[];
18
+ list(readOnly: boolean): Tool[];
22
19
  call(params: {
23
20
  name: string;
24
21
  arguments?: Record<string, unknown>;
@@ -27,6 +24,7 @@ export declare class AdminForthApiTools {
27
24
  requestUrl: string;
28
25
  abortSignal: AbortSignal;
29
26
  readOnly: boolean;
27
+ connection: McpConnection;
30
28
  }): Promise<{
31
29
  output: unknown;
32
30
  isError: boolean;
package/dist/apiTools.js CHANGED
@@ -1,7 +1,9 @@
1
- import { compactInputSchema } from './inputSchema.js';
1
+ import { compactInputSchema } from './compactInputSchema.js';
2
2
  import { adminApiPrefix } from './urls.js';
3
3
  import { AdminForthDataTypes } from 'adminforth';
4
4
  const METHODS_WITHOUT_REQUEST_BODY = new Set(['GET', 'HEAD']);
5
+ const NON_TOOL_NAME_CHARACTER_RE = /[^a-zA-Z0-9_]+/g;
6
+ const EDGE_UNDERSCORE_RE = /^_+|_+$/g;
5
7
  // Milliseconds cost tokens on every datetime value and are never needed to answer the user.
6
8
  const ISO_MILLISECONDS_RE = /\.\d+(?=Z$)/;
7
9
  const TOOL_TIMEOUT = Symbol('TOOL_TIMEOUT');
@@ -44,15 +46,7 @@ function isRegisteredToolSchema(schema) {
44
46
  return typeof schema.handler === 'function' && !schema.agent?.hiddenFromAgents;
45
47
  }
46
48
  function endpointPathToToolName(path) {
47
- return path
48
- .replace(/^\/+/, '')
49
- .replace(/[^a-zA-Z0-9_]+/g, '_')
50
- .replace(/^_+|_+$/g, '');
51
- }
52
- function stripAdminApiPrefix(path, adminforth) {
53
- const apiPrefix = adminApiPrefix(adminforth.config.baseUrl);
54
- const strippedPath = path.startsWith(apiPrefix) ? path.slice(apiPrefix.length) : path;
55
- return strippedPath.startsWith('/') ? strippedPath : `/${strippedPath}`;
49
+ return path.replace(NON_TOOL_NAME_CHARACTER_RE, '_').replace(EDGE_UNDERSCORE_RE, '');
56
50
  }
57
51
  // Drops frontend-only parts of the get_resource response that are useless for MCP clients and only waste their context.
58
52
  // Copies instead of deleting: the response shares nested objects with the AdminForth config.
@@ -167,23 +161,46 @@ export class AdminForthApiTools {
167
161
  adminforth;
168
162
  hiddenResourceIds;
169
163
  toolTimeoutMs;
170
- constructor(adminforth, hiddenResourceIds, pageSize, toolTimeoutMs) {
164
+ adminPanelConfirmation;
165
+ constructor(adminforth, hiddenResourceIds, pageSize, toolTimeoutMs, adminPanelConfirmation) {
171
166
  this.adminforth = adminforth;
172
167
  this.hiddenResourceIds = hiddenResourceIds;
173
168
  this.toolTimeoutMs = toolTimeoutMs;
169
+ this.adminPanelConfirmation = adminPanelConfirmation;
174
170
  this.toolOverrides = createToolOverrides(pageSize, adminforth);
175
171
  }
176
172
  toolOverrides;
177
- schemas(readOnly) {
178
- const schemas = new Map();
179
- for (const schema of this.adminforth.openApi.registeredSchemas) {
180
- if (!isRegisteredToolSchema(schema))
181
- continue;
182
- if (readOnly && !schema.agent?.onlyReadsData)
183
- continue;
184
- schemas.set(endpointPathToToolName(stripAdminApiPrefix(schema.path, this.adminforth)), schema);
185
- }
186
- return schemas;
173
+ fullCatalog;
174
+ readOnlyCatalog;
175
+ catalog(readOnly) {
176
+ return readOnly
177
+ ? (this.readOnlyCatalog ??= this.buildCatalog(true))
178
+ : (this.fullCatalog ??= this.buildCatalog(false));
179
+ }
180
+ buildCatalog(readOnly) {
181
+ const apiPrefix = adminApiPrefix(this.adminforth.config.baseUrl);
182
+ const schemas = new Map(this.adminforth.openApi.registeredSchemas
183
+ .filter(isRegisteredToolSchema)
184
+ .filter((schema) => !readOnly || schema.agent?.onlyReadsData)
185
+ .map((schema) => [endpointPathToToolName(schema.path.slice(apiPrefix.length)), schema]));
186
+ const tools = Array.from(schemas.entries())
187
+ .sort(([left], [right]) => left.localeCompare(right))
188
+ .map(([name, schema]) => ({
189
+ name,
190
+ ...this.description(name, schema),
191
+ inputSchema: compactInputSchema(overrideInputSchema(schema.request_schema ?? {
192
+ type: 'object',
193
+ properties: {},
194
+ additionalProperties: true,
195
+ }, this.toolOverrides[name])),
196
+ ...(schema.agent?.onlyReadsData && {
197
+ annotations: { readOnlyHint: true },
198
+ }),
199
+ ...(schema.agent?.requiresHumanApproval && {
200
+ annotations: { destructiveHint: true },
201
+ }),
202
+ }));
203
+ return { schemas, tools };
187
204
  }
188
205
  description(name, schema) {
189
206
  const description = [
@@ -215,26 +232,10 @@ export class AdminForthApiTools {
215
232
  }
216
233
  }
217
234
  list(readOnly) {
218
- return Array.from(this.schemas(readOnly).entries())
219
- .sort(([left], [right]) => left.localeCompare(right))
220
- .map(([name, schema]) => ({
221
- name,
222
- ...this.description(name, schema),
223
- inputSchema: compactInputSchema(overrideInputSchema(schema.request_schema ?? {
224
- type: 'object',
225
- properties: {},
226
- additionalProperties: true,
227
- }, this.toolOverrides[name])),
228
- ...(schema.agent?.onlyReadsData && {
229
- annotations: { readOnlyHint: true },
230
- }),
231
- ...(schema.agent?.requiresHumanApproval && {
232
- annotations: { destructiveHint: true },
233
- }),
234
- }));
235
+ return this.catalog(readOnly).tools;
235
236
  }
236
237
  async call(params) {
237
- const schema = this.schemas(params.readOnly).get(params.name);
238
+ const schema = this.catalog(params.readOnly).schemas.get(params.name);
238
239
  if (!schema) {
239
240
  return { output: { error: `Unknown tool: ${params.name}` }, isError: true };
240
241
  }
@@ -261,20 +262,25 @@ export class AdminForthApiTools {
261
262
  };
262
263
  }
263
264
  const response = createDirectResponse();
264
- const language = String(params.headers['accept-language'] ?? 'en');
265
- const output = await this.withTimeout(params.abortSignal, (abortSignal) => schema.handler({
265
+ const request = {
266
266
  body,
267
- query,
267
+ query: query,
268
268
  headers: params.headers,
269
269
  cookies: [],
270
- adminUser: params.adminUser,
271
- response,
272
270
  requestUrl: params.requestUrl,
273
- abortSignal,
274
- _raw_express_req: undefined,
275
- _raw_express_res: undefined,
276
- tr: (message, category, translationParams, pluralizationNumber) => this.adminforth.tr(message, category, language, translationParams, pluralizationNumber),
277
- }));
271
+ response,
272
+ };
273
+ const output = await this.withTimeout(params.abortSignal, (abortSignal) => {
274
+ const execute = async () => schema.handler({
275
+ ...request,
276
+ adminUser: params.adminUser,
277
+ abortSignal,
278
+ _raw_express_req: undefined,
279
+ _raw_express_res: undefined,
280
+ tr: (message, category, translationParams, pluralizationNumber) => this.adminforth.tr(message, category, params.headers['accept-language'], translationParams, pluralizationNumber),
281
+ });
282
+ return this.adminPanelConfirmation ? this.adminPanelConfirmation.runUnverified(request, execute) : execute();
283
+ });
278
284
  if (output === TOOL_TIMEOUT) {
279
285
  return {
280
286
  output: {
@@ -295,6 +301,9 @@ export class AdminForthApiTools {
295
301
  isError: true,
296
302
  };
297
303
  }
304
+ const confirmation = this.adminPanelConfirmation?.offer(output, params.connection, params.adminUser, schema.path.slice(adminApiPrefix(this.adminforth.config.baseUrl).length), handlerArguments);
305
+ if (confirmation)
306
+ return confirmation;
298
307
  const responseValidation = this.adminforth.openApi.validateResponseSchema(schema, output);
299
308
  if (!responseValidation.valid) {
300
309
  return {
@@ -1,5 +1,6 @@
1
1
  import { type AdminUser, type HttpExtra, type IAdminForth } from 'adminforth';
2
- import type { McpAuthSecretResourceOptions, McpClientInfo } from './types.js';
2
+ import type { AuthSecretRepository } from './repositories/authSecret.js';
3
+ import type { McpClientInfo } from './types.js';
3
4
  export declare const SECRET_PREFIX = "afmcp_";
4
5
  type AdminUserWithExecutor = AdminUser & {
5
6
  executedBy?: string;
@@ -20,18 +21,17 @@ export interface AuthenticatedMcpSecret {
20
21
  }
21
22
  export declare class McpAuthSecretStore {
22
23
  private readonly adminforth;
23
- private readonly options;
24
- private readonly resource;
24
+ private readonly repository;
25
25
  private readonly refreshLock;
26
- constructor(adminforth: IAdminForth, options: McpAuthSecretResourceOptions);
26
+ constructor(adminforth: IAdminForth, repository: AuthSecretRepository);
27
27
  list(adminUser: AdminUser): Promise<{
28
- id: any;
29
- name: any;
30
- createdAt: any;
31
- lastUsedAt: any;
28
+ id: string;
29
+ name: string;
30
+ createdAt: string;
31
+ lastUsedAt: string | null;
32
32
  lastUsedByAgent: McpClientInfo | null;
33
- readOnly: any;
34
- oauthClientId: any;
33
+ readOnly: boolean;
34
+ oauthClientId: string | null;
35
35
  }[]>;
36
36
  create(name: string, readOnly: boolean, adminUser: AdminUser, extra: HttpExtra): Promise<{
37
37
  error: string;
@@ -59,8 +59,6 @@ export declare class McpAuthSecretStore {
59
59
  rotateRefreshToken(grantId: string, refreshToken: string, clientId: string, newRefreshToken: string): Promise<string | null>;
60
60
  /** Access tokens are checked against their grant on every request, so revoking the grant applies at once. */
61
61
  authenticateOAuthGrant(grantId: string, userId: string): Promise<AuthenticatedMcpSecret | null>;
62
- /** The OAuth grant methods are called only when OAuth is enabled, which requires this field. */
63
- private get oauthClientIdField();
64
62
  private loadAdminUser;
65
63
  private toAuthenticated;
66
64
  touch(recordId: string, client: McpClientInfo): void;