Skip to content

Commit db5ea00

Browse files
committed
feat: add hiddenFromAgents/onlyReadsData agent flags, rename isDangerous
1 parent 9e708fd commit db5ea00

8 files changed

Lines changed: 74 additions & 28 deletions

File tree

‎adminforth/documentation/docs/tutorial/09-Plugins/01-agent.md‎

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -668,28 +668,19 @@ To define a custom tool, register an API endpoint with `admin.express.endpoint`.
668668

669669
By default, `admin.express.endpoint` applies AdminForth authorization. The endpoint handler receives `adminUser` from the user who is controlling the agent. In other words, all permissions and access rights of the agent are defined by that admin user. At the same time, actions done by the agent are automatically attributed in the audit log to the admin user who is controlling the agent.
670670

671-
If a tool is risky, you can attach AdminForth agent metadata directly to the endpoint with the `agent` field.
671+
You can attach AdminForth agent metadata directly to the endpoint with the `agent` field.
672672

673673
```ts
674-
type AgentRiskLevel = 'safe' | 'danger';
675-
676674
type AgentToolMeta = {
677-
riskLevel?: AgentRiskLevel;
678-
confirmation?: {
679-
title?: string;
680-
message?: string;
681-
confirmLabel?: string;
682-
};
675+
requiresHumanApproval?: boolean;
676+
hiddenFromAgents?: boolean;
677+
onlyReadsData?: boolean;
683678
};
684679
```
685680

686-
Use it in `.endpoint(...)` like this:
687-
688-
- `riskLevel: 'safe'` marks the tool as low-risk.
689-
- `riskLevel: 'danger'` marks the tool as dangerous.
690-
- `confirmation` customizes the confirmation dialog shown before the tool is executed.
691-
692-
This metadata is rendered into the generated OpenAPI document, so the agent can understand that a tool is dangerous and requires an explicit confirmation UI before execution.
681+
- `requiresHumanApproval: true` makes the agent ask the user for approval before it runs the tool. MCP clients receive the `destructiveHint` annotation.
682+
- `hiddenFromAgents: true` keeps the endpoint out of agent tools, both in the agent and in the [MCP server](/docs/tutorial/Plugins/mcp/). Use it for endpoints that serve your admin panel UI and must not be called by agents.
683+
- `onlyReadsData: true` declares that the endpoint does not change any data. MCP clients receive the `readOnlyHint` annotation, and a [read-only MCP server](/docs/tutorial/Plugins/mcp/#read-only-mode) exposes only such endpoints.
693684

694685
This example uses the same email adapter pattern shown in the Email Invite and Email Password Reset plugins. The transport below uses Mailgun only to keep the snippet short; you can replace it with SES or any other adapter from [List of adapters](/docs/tutorial/ListOfAdapters/).
695686

@@ -728,12 +719,7 @@ export function initApi(app: Express, admin: IAdminForth) {
728719
path: '/send_email_to_user',
729720
description: 'Send an email to one AdminForth user by id. Use this after the user row is resolved.',
730721
agent: {
731-
riskLevel: 'danger',
732-
confirmation: {
733-
title: 'Send email to user',
734-
message: 'This action will send a real email to the selected user.',
735-
confirmLabel: 'Send email',
736-
},
722+
requiresHumanApproval: true,
737723
},
738724
request_schema: {
739725
type: 'object',

‎adminforth/documentation/docs/tutorial/09-Plugins/30-mcp.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ slug: /tutorial/Plugins/mcp
88

99
The MCP plugin exposes AdminForth API methods as remote MCP tools. Every request runs as the AdminForth user who created the auth secret, so the same resource permissions, validation, and hooks apply.
1010

11+
Endpoints marked with `agent: { hiddenFromAgents: true }` are not exposed. AdminForth and its plugins mark the endpoints the admin panel uses internally, such as login, two-factor authentication, passkeys, and agent chat. Mark your own endpoints the same way to keep them away from agents.
12+
1113
## Installation
1214

1315
```bash
@@ -27,6 +29,7 @@ model mcp_auth_secrets {
2729
created_at DateTime
2830
last_used_at DateTime?
2931
last_used_by_agent String?
32+
read_only Boolean @default(false)
3033
3134
@@index([user_id])
3235
}
@@ -58,6 +61,7 @@ export default {
5861
{ name: 'created_at', type: AdminForthDataTypes.DATETIME },
5962
{ name: 'last_used_at', type: AdminForthDataTypes.DATETIME, required: false },
6063
{ name: 'last_used_by_agent', type: AdminForthDataTypes.STRING, required: false },
64+
{ name: 'read_only', type: AdminForthDataTypes.BOOLEAN },
6165
],
6266
options: {
6367
allowedActions: {
@@ -94,6 +98,7 @@ new AdminForth({
9498
createdAtField: 'created_at',
9599
lastUsedAtField: 'last_used_at',
96100
lastUsedByAgentField: 'last_used_by_agent',
101+
readOnlyField: 'read_only',
97102
},
98103
}),
99104
],
@@ -118,6 +123,21 @@ Authorization: Bearer afmcp_...
118123

119124
`last_used_at` and `last_used_by_agent` are updated in the background without running resource hooks. Client identity comes from per-request `io.modelcontextprotocol/clientInfo` metadata in MCP `2026-07-28`, with legacy `initialize` and `User-Agent` fallbacks.
120125

126+
## Read-only mode
127+
128+
A read-only auth secret lets an agent only read data. Check **Read only** when you create the auth secret in **MCP Settings**. The mode is stored in `read_only` and cannot be changed later: create a new auth secret instead.
129+
130+
To make every auth secret read-only, set `readOnly: true` in the plugin options. The **Read only** checkbox is then hidden, and new auth secrets are stored as read-only, so they stay read-only if you remove the option later:
131+
132+
```ts
133+
new AdminForthMcpPlugin({
134+
readOnly: true,
135+
// ...
136+
}),
137+
```
138+
139+
In read-only mode the server exposes only endpoints marked with `agent: { onlyReadsData: true }`, such as `get_resource`, `get_resource_data`, and `aggregate`. Creating, updating, and deleting records and running actions are not available. To make your own endpoint available in read-only mode, mark it with `onlyReadsData: true`.
140+
121141
## Audit attribution
122142

123143
To show which agent acted on behalf of a user, add a nullable field to the audit log table:

‎adminforth/modules/restApi.ts‎

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
803803
noAuth: true,
804804
method: 'POST',
805805
path: '/login',
806+
agent: {
807+
hiddenFromAgents: true,
808+
},
806809
handler: async ({ body, response, headers, query, cookies, requestUrl, tr }) => {
807810

808811
const INVALID_MESSAGE = await tr('Invalid username or password', 'errors');
@@ -904,6 +907,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
904907
server.endpoint({
905908
method: 'POST',
906909
path: '/check_auth',
910+
agent: {
911+
hiddenFromAgents: true,
912+
},
907913
handler: async ({ adminUser }) => {
908914
return { ok: true };
909915
},
@@ -913,6 +919,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
913919
noAuth: true,
914920
method: 'POST',
915921
path: '/logout',
922+
agent: {
923+
hiddenFromAgents: true,
924+
},
916925
handler: async ({ body, headers, query, cookies, requestUrl, response, tr }) => {
917926
// endpoint is noAuth (expired session should be able to log out as well), so user is resolved here
918927
const jwt = this.adminforth.auth.getAuthCookie(cookies);
@@ -931,6 +940,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
931940
noAuth: true,
932941
method: 'GET',
933942
path: '/get_login_form_config',
943+
agent: {
944+
hiddenFromAgents: true,
945+
},
934946
handler: async ({ tr }) => {
935947
const loginPromptHTML = await getLoginPromptHTML(this.adminforth.config.auth.loginPromptHTML);
936948
return {
@@ -943,6 +955,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
943955
noAuth: true,
944956
method: 'GET',
945957
path: '/get_config',
958+
agent: {
959+
hiddenFromAgents: true,
960+
},
946961
handler: async ({ body, query, headers, cookies, requestUrl, tr, response }): Promise<GetConfigResponse>=> {
947962
let username = ''
948963
let userFullName = ''
@@ -1145,6 +1160,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
11451160
method: 'GET',
11461161
path: '/get_menu_badges',
11471162
description: 'Computes the current menu badge values for the authenticated admin user. Static badges are returned directly, and dynamic badge callbacks are resolved for all configured menu items, including nested items.',
1163+
agent: {
1164+
onlyReadsData: true,
1165+
},
11481166
response_schema: getMenuBadgesResponseSchema,
11491167
handler: async ({ adminUser }) => {
11501168
const badges = {};
@@ -1189,6 +1207,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
11891207
method: 'POST',
11901208
path: '/get_resource',
11911209
description: 'Returns the definition of a single resource. The response includes translated labels, column metadata, allowed actions, visible bulk actions, frontend action metadata, and resource options after permission checks and removal of backend-only internals.',
1210+
agent: {
1211+
onlyReadsData: true,
1212+
},
11921213
request_schema: getResourceRequestSchema,
11931214
response_schema: getResourceResponseSchema,
11941215
handler: async ({ body, adminUser, tr }): Promise<{ resource?: AdminForthResourceFrontend, error?: string }> => {
@@ -1400,6 +1421,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
14001421
method: 'POST',
14011422
path: '/get_resource_data',
14021423
description: 'Loads resource rows for list, show, or edit views. The endpoint validates access, applies request hooks, filters, sorting, pagination, record labels, and row click URLs, then returns the final dataset with resource options.',
1424+
agent: {
1425+
onlyReadsData: true,
1426+
},
14031427
request_schema: getResourceDataRequestSchema,
14041428
response_schema: getResourceDataResponseSchema,
14051429
handler: async ({ body, adminUser, headers, query, cookies, requestUrl, abortSignal }) => {
@@ -1855,6 +1879,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
18551879
method: 'POST',
18561880
path: '/aggregate',
18571881
description: 'Performs aggregation queries (sum, count, avg, min, max, median) on a resource, with optional grouping by field value or date truncation. Requires list and show access to the resource, and only accepts columns the user can read on the show view: backend-only columns and columns hidden from the show view are rejected.',
1882+
agent: {
1883+
onlyReadsData: true,
1884+
},
18581885
request_schema: aggregateRequestSchema,
18591886
response_schema: aggregateResponseSchema,
18601887
handler: async ({ body, adminUser, headers }) => {
@@ -1996,6 +2023,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
19962023
method: 'POST',
19972024
path: '/get_resource_foreign_data',
19982025
description: 'Loads dropdown options for a foreign-key column. It resolves the referenced resource or polymorphic resources, applies optional search text, hook-injected filters, pagination, and per-record labels, then returns sanitized option items.',
2026+
agent: {
2027+
onlyReadsData: true,
2028+
},
19992029
request_schema: getResourceForeignDataRequestSchema,
20002030
response_schema: getResourceForeignDataResponseSchema,
20012031
handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
@@ -2193,6 +2223,9 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
21932223
method: 'POST',
21942224
path: '/get_min_max_for_columns',
21952225
description: 'Returns min and max values for resource columns that explicitly opt in to min/max queries. This is used to build range-based filter controls without exposing columns that do not allow the query.',
2226+
agent: {
2227+
onlyReadsData: true,
2228+
},
21962229
request_schema: getMinMaxForColumnsRequestSchema,
21972230
response_schema: getMinMaxForColumnsResponseSchema,
21982231
handler: async ({ body }) => {
@@ -2226,7 +2259,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
22262259
path: '/create_record',
22272260
description: 'Creates a new record in the specified resource. The endpoint validates create permissions, required fields, hidden or backend-only field rules, polymorphic foreign keys, and resource hooks before persisting and returning the created primary key.',
22282261
agent: {
2229-
isDangerous: true,
2262+
requiresHumanApproval: true,
22302263
},
22312264
request_schema: createRecordRequestSchema,
22322265
response_schema: createRecordResponseSchema,
@@ -2403,7 +2436,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
24032436
path: '/update_record',
24042437
description: 'Updates an existing record by primary key. The endpoint validates edit permissions, current record existence, hidden, backend-only, and read-only field rules, polymorphic foreign keys, and resource hooks before saving changes.',
24052438
agent: {
2406-
isDangerous: true,
2439+
requiresHumanApproval: true,
24072440
},
24082441
request_schema: updateRecordRequestSchema,
24092442
response_schema: updateRecordResponseSchema,
@@ -2575,7 +2608,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
25752608
path: '/delete_record',
25762609
description: 'Deletes an existing record by primary key. The endpoint validates delete permissions, loads the current record, executes configured cascade child deletion, and then removes the record.',
25772610
agent: {
2578-
isDangerous: true,
2611+
requiresHumanApproval: true,
25792612
},
25802613
request_schema: deleteRecordRequestSchema,
25812614
response_schema: deleteRecordResponseSchema,
@@ -2664,7 +2697,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
26642697
path: '/start_custom_action',
26652698
description: 'Executes a custom resource action for a single record. The endpoint validates the resource, action existence, and action permissions, then either returns a redirect URL or executes the action handler and returns its result together with action context.',
26662699
agent: {
2667-
isDangerous: true,
2700+
requiresHumanApproval: true,
26682701
},
26692702
request_schema: startCustomActionRequestSchema,
26702703
response_schema: startCustomActionResponseSchema,
@@ -2723,7 +2756,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
27232756
path: '/start_custom_bulk_action',
27242757
description: 'Executes a custom resource action in bulk mode for multiple records. The endpoint validates the resource, action existence, bulk handler availability, and permissions, then runs the bulk handler and returns its result together with action context.',
27252758
agent: {
2726-
isDangerous: true,
2759+
requiresHumanApproval: true,
27272760
},
27282761
request_schema: startCustomBulkActionRequestSchema,
27292762
response_schema: startCustomBulkActionResponseSchema,

‎adminforth/types/Back.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export interface IAdminForthAuthenticatedEndpointHandlerInput extends IAdminFort
6464
}
6565

6666
export type AgentToolMeta = {
67-
isDangerous?: boolean;
67+
requiresHumanApproval?: boolean;
68+
hiddenFromAgents?: boolean;
69+
onlyReadsData?: boolean;
6870
};
6971

7072
export interface IAdminForthEndpointOptionsBase {

‎dev-demo/globalPlugins.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const globalPlugins = [
5555
createdAtField: 'created_at',
5656
lastUsedAtField: 'last_used_at',
5757
lastUsedByAgentField: 'last_used_by_agent',
58+
readOnlyField: 'read_only',
5859
},
5960
}),
6061
new AdminForthAgent({
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "mcp_auth_secrets" ADD COLUMN "read_only" BOOLEAN NOT NULL DEFAULT false;

‎dev-demo/migrations/prisma/sqlite/schema.prisma‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ model mcp_auth_secrets {
6565
created_at DateTime
6666
last_used_at DateTime?
6767
last_used_by_agent String?
68+
read_only Boolean @default(false)
6869
6970
@@index([user_id])
7071
}

‎dev-demo/resources/mcpAuthSecrets.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default {
1414
{ name: 'created_at', type: AdminForthDataTypes.DATETIME },
1515
{ name: 'last_used_at', type: AdminForthDataTypes.DATETIME, required: false },
1616
{ name: 'last_used_by_agent', type: AdminForthDataTypes.STRING, required: false },
17+
{ name: 'read_only', type: AdminForthDataTypes.BOOLEAN },
1718
],
1819
options: {
1920
allowedActions: {

0 commit comments

Comments
 (0)