@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/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.
|
|
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
|
|
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
|
+
}
|
package/dist/apiTools.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
12
|
+
private fullCatalog?;
|
|
13
|
+
private readOnlyCatalog?;
|
|
14
|
+
private catalog;
|
|
15
|
+
private buildCatalog;
|
|
19
16
|
private description;
|
|
20
17
|
private withTimeout;
|
|
21
|
-
list(readOnly: boolean):
|
|
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 './
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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 {
|
|
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
|
|
24
|
-
private readonly resource;
|
|
24
|
+
private readonly repository;
|
|
25
25
|
private readonly refreshLock;
|
|
26
|
-
constructor(adminforth: IAdminForth,
|
|
26
|
+
constructor(adminforth: IAdminForth, repository: AuthSecretRepository);
|
|
27
27
|
list(adminUser: AdminUser): Promise<{
|
|
28
|
-
id:
|
|
29
|
-
name:
|
|
30
|
-
createdAt:
|
|
31
|
-
lastUsedAt:
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
lastUsedAt: string | null;
|
|
32
32
|
lastUsedByAgent: McpClientInfo | null;
|
|
33
|
-
readOnly:
|
|
34
|
-
oauthClientId:
|
|
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;
|