Skip to content

Commit 30353cd

Browse files
committed
Merge origin/main: combine read-only mode with MCP OAuth AdminForth/1970/please-introduce-same-oauth-au
Keeps both new MCP auth secret columns, read_only from main and oauth_client_id from this branch, in the dev-demo resource, Prisma schema and plugin config. The MCP docs describe the read-only checkbox for OAuth connections and keep the page size, tool timeout and agent guidance sections.
2 parents e857e0f + 91be51b commit 30353cd

61 files changed

Lines changed: 1041 additions & 1695 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ connectors/*
1010
!connectors/AGENTS.md
1111
background-jobs-dbs
1212
tests/jest_tests/db/
13+
ads

‎adminforth/commands/createApp/templates/package.json.hbs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
"tsx": "4.11.2",
3737
"@types/express": "^5.0.6",
3838
"@types/node": "latest"{{#if includePrismaMigrations}},
39-
"@prisma/client": "latest",
4039
"prisma": "^7.0.0"
4140
{{/if}}
4241
}

‎adminforth/commands/createApp/templates/schema.prisma.hbs‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
generator client {
2-
provider = "prisma-client-js"
3-
}
4-
51
datasource db {
62
provider = "{{provider}}"
73
}

‎adminforth/commands/createApp/utils.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ function providerToConnectorName(provider) {
117117
}
118118

119119
// Returns the default-exported connector class for the given connector name.
120-
// Connectors are optional peer dependencies, so they are not present during a
121-
// fresh `npx adminforth create-app`. We first try to import an already-installed
120+
// Connectors are installed by the user's app, not by adminforth, so they are not
121+
// present during a fresh `npx adminforth create-app`. We first try to import an already-installed
122122
// one (local development / monorepo), then fall back to installing it on demand.
123123
async function loadConnectorClass(connectorName) {
124124
const pkg = `@adminforth/connector-${connectorName}`;

‎adminforth/documentation/blog/2024-10-01-ai-blog/index.md‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,6 @@ Open `./schema.prisma` and put next content there:
131131

132132

133133
```yaml title="./schema.prisma"
134-
generator client {
135-
provider = "prisma-client-js"
136-
}
137-
138134
datasource db {
139135
provider = "sqlite"
140136
url = env("PRISMA_DATABASE_URL")

‎adminforth/documentation/docs/tutorial/01-helloWorld.md‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ nvm use 24
3333
mkdir af-hello
3434
cd af-hello
3535
pnpm init
36-
pnpm add adminforth express@^4 @dotenvx/dotenvx @types/express typescript tsx @types/node prisma @prisma/client -D
36+
pnpm add adminforth express@^4 @dotenvx/dotenvx @types/express typescript tsx @types/node prisma -D
3737
npx --yes tsc --init --module NodeNext --target ESNext
3838
```
3939

@@ -419,10 +419,6 @@ Create new directory `./custom/tsconfig.json` with following content:
419419
Create `./schema.prisma` and put next content there:
420420

421421
```text title="./schema.prisma"
422-
generator client {
423-
provider = "prisma-client-js"
424-
}
425-
426422
datasource db {
427423
provider = "sqlite"
428424
}

‎adminforth/documentation/docs/tutorial/03-Customization/12-security.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ So to completely hide the email field from all users apart superadmins, you shou
296296
297297
So if you will configure the email column in user resource like this, only superadmin will be able to see emails, and only in the list view.
298298
299+
> ☝️ `backendOnly` cannot be set on a primary key column (neither `true` nor a function): frontend needs primary key values to build record links, so AdminForth refuses such config on startup.
300+
299301
## Blocking login attempts before credentials check
300302
301303
`auth.beforeLoginAttempt` hooks are called on every request to the login endpoint, before AdminForth looks the user up in the database.

‎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/23-background-jobs.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ type JobTask = {
387387
task_number: number;
388388
task_counter: number;
389389
};
390-
status: 'SCHEDULED' | 'IN_PROGRESS' | 'DONE' | 'FAILED';
390+
status: 'SCHEDULED' | 'IN_PROGRESS' | 'DONE' | 'FAILED' | 'DELETED';
391391
};
392392

393393
const tasks = ref<JobTask[]>([]);
@@ -406,6 +406,7 @@ const taskStatusLabels: Record<JobTask['status'], string> = {
406406
IN_PROGRESS: 'running',
407407
DONE: 'done',
408408
FAILED: 'failed',
409+
DELETED: 'deleted',
409410
};
410411

411412
const props = defineProps<{
@@ -699,7 +700,7 @@ getJobStateField(jobId: string, key: string)
699700
getJobState(jobId: string)
700701
//add task to the running job
701702
addNewTasksToExistingJob(jobId: string, tasks: taskType[])
702-
//delete task from the running job (if not started yet)
703+
//delete task from the running job (if not started yet). The task is not removed, it gets the DELETED status and keeps its index
703704
deleteTasksFromExistingJob(jobId: string, taskIndex: number)
704705
/**
705706
*

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

Lines changed: 21 additions & 1 deletion
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. An agent connects either through OAuth sign-in, where the user approves it in the browser, or with an auth secret the user creates. Every request runs as that AdminForth user, 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
oauth_client_id String?
3134
3235
@@index([user_id])
@@ -59,6 +62,7 @@ export default {
5962
{ name: 'created_at', type: AdminForthDataTypes.DATETIME },
6063
{ name: 'last_used_at', type: AdminForthDataTypes.DATETIME, required: false },
6164
{ name: 'last_used_by_agent', type: AdminForthDataTypes.STRING, required: false },
65+
{ name: 'read_only', type: AdminForthDataTypes.BOOLEAN },
6266
{ name: 'oauth_client_id', type: AdminForthDataTypes.STRING, required: false },
6367
],
6468
options: {
@@ -98,6 +102,7 @@ new AdminForth({
98102
createdAtField: 'created_at',
99103
lastUsedAtField: 'last_used_at',
100104
lastUsedByAgentField: 'last_used_by_agent',
105+
readOnlyField: 'read_only',
101106
oauthClientIdField: 'oauth_client_id',
102107
},
103108
}),
@@ -131,7 +136,7 @@ The plugin adds **MCP Settings** under the user profile. With OAuth sign-in enab
131136

132137
OAuth sign-in is enabled by `oauthClientIdField` together with `adminPanelOrigin`, see [Plugin setup](#plugin-setup).
133138

134-
When an agent calls the MCP endpoint without a token, the server answers `401` with a `WWW-Authenticate` header that leads the agent to the OAuth metadata. The agent then opens the browser on the consent page `/mcp-authorize`. If the user is not logged in, AdminForth shows its login page first, including two-factor authentication if it is enabled, and returns to the consent page. After the user clicks **Allow**, the agent gets an access token and a refresh token and the connection appears in **MCP Settings** under the client's name.
139+
When an agent calls the MCP endpoint without a token, the server answers `401` with a `WWW-Authenticate` header that leads the agent to the OAuth metadata. The agent then opens the browser on the consent page `/mcp-authorize`. If the user is not logged in, AdminForth shows its login page first, including two-factor authentication if it is enabled, and returns to the consent page. The consent page has a **Read only** checkbox, see [Read-only mode](#read-only-mode). After the user clicks **Allow**, the agent gets an access token and a refresh token and the connection appears in **MCP Settings** under the client's name.
135140

136141
The plugin is an OAuth 2.1 authorization server for public clients:
137142

@@ -265,6 +270,21 @@ Tool responses are trimmed to save the agent's context:
265270
- `get_resource_data` rows leave out columns with `null`, empty array and empty object values, and datetimes come without milliseconds.
266271
- Tool outputs are serialized as YAML instead of JSON.
267272

273+
## Read-only mode
274+
275+
A read-only connection lets an agent only read data. Check **Read only** when you create an auth secret in **MCP Settings**, or on the consent page when you connect an agent with OAuth sign-in. The mode is stored in `read_only` and cannot be changed later: create a new auth secret or connect the agent again instead.
276+
277+
To make every connection read-only, set `readOnly: true` in the plugin options. The **Read only** checkbox is then hidden, the consent page says that the agent can only read data, and new auth secrets and OAuth connections are stored as read-only, so they stay read-only if you remove the option later:
278+
279+
```ts
280+
new AdminForthMcpPlugin({
281+
readOnly: true,
282+
// ...
283+
}),
284+
```
285+
286+
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`.
287+
268288
## Audit attribution
269289

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

0 commit comments

Comments
 (0)