You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0e21b0d
Browse filesBrowse the repository at this point in the historyBrowse files
description: "Expose AdminForth resources and actions to Codex, Claude Code, Gemini, and other MCP clients with per-user revocable auth secrets."
3
+
description: "Expose AdminForth resources and actions to Codex, Claude Code, Gemini, and other MCP clients through OAuth sign-in or per-user revocable auth secrets."
4
4
slug: /tutorial/Plugins/mcp
5
5
---
6
6
7
7
# MCP Server
8
8
9
-
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.
9
+
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.
10
10
11
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
12
@@ -16,9 +16,9 @@ Endpoints marked with `agent: { hiddenFromAgents: true }` are not exposed. Admin
16
16
pnpm add @adminforth/mcp --save
17
17
```
18
18
19
-
## MCP auth secret table
19
+
## MCP connections table
20
20
21
-
Add a table for independently revocable MCP auth secrets. Only the SHA-256 secret hash is stored; the secret itself is returned once when it is created.
21
+
Add a table for the user's MCP connections: auth secrets and OAuth connections, each revocable on its own. Only SHA-256 hashes are stored: for an auth secret the hash of the secret, which is returned once when it is created, and for an OAuth connection the hash of its refresh token.
Register `mcpAuthSecrets` in the application's `resources` array.
79
81
82
+
`oauth_client_id` holds the client of an OAuth connection and stays empty for auth secrets. It is needed only for [OAuth sign-in](#oauth-sign-in); without it the plugin works with auth secrets alone. If the table already exists from an earlier plugin version, add only this nullable column with a migration to enable OAuth.
The MCP server identifies itself to agents as the AdminForth admin panel for the configured [`customization.brandName`](/docs/tutorial/Customization/branding/). Set `adminPanelOrigin` when the same agent uses several AdminForth installations. Pass only the public origin, such as `https://admin.example.com`; the plugin appends the existing AdminForth `baseUrl` path. The origin is optional and is not inferred from request headers.
113
+
`oauthClientIdField` enables OAuth sign-in. Leave it out to keep only auth secrets, as in plugin versions before OAuth; then the dialog in **MCP Settings** shows only the auth secret form and no OAuth routes are added.
109
114
110
-
The plugin adds **MCP Settings** under the user profile. A user can create or revoke auth secrets there and copy the short setup prompt. Use one auth secret per agent.
115
+
`adminPanelOrigin` is the public origin agents connect to, such as `https://admin.example.com`, without a path; the plugin appends the AdminForth `baseUrl`. It is required with OAuth sign-in, and the app does not start without it: the OAuth issuer and the MCP resource URL are built from it, and MCP clients reject a resource URL whose origin differs from the one they connected to. The origin is not inferred from request headers. The MCP server also identifies itself to agents by it and by [`customization.brandName`](/docs/tutorial/Customization/branding/), which tells several AdminForth installations apart.
If `baseUrl` is configured, it appears before `/adminapi/v1/mcp`. Authenticate every request with:
123
+
If `baseUrl` is configured, it appears before `/adminapi/v1/mcp`.
124
+
125
+
## Connecting an agent
126
+
127
+
The plugin adds **MCP Settings** under the user profile. With OAuth sign-in enabled, **Connect agent** opens a dialog with a tab per client:
128
+
129
+
-**Claude Code** – `claude mcp add --transport http <name> <url>`, then `/mcp` in Claude Code to sign in;
130
+
-**Codex** – `codex mcp add <name> --url <url>`; Codex opens the browser to sign in, or run `codex mcp login <name>`;
131
+
-**Other** – creates an auth secret for clients without OAuth sign-in.
132
+
133
+
`<name>` is `brandName` in lowercase with hyphens. The commands use the address the admin panel is opened at, so open it at its public address. The table below the button lists all connections of the user, auth secrets and OAuth ones, and revokes them. A revoked connection stops working on the agent's next request. Logging out, ending sessions and changing the password do not revoke MCP connections: revoke them in this table.
134
+
135
+
## OAuth sign-in
136
+
137
+
OAuth sign-in is enabled by `oauthClientIdField` together with `adminPanelOrigin`, see [Plugin setup](#plugin-setup).
138
+
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.
140
+
141
+
The plugin is an OAuth 2.1 authorization server for public clients:
142
+
143
+
- authorization code flow with PKCE `S256`, with `authorization_code` and `refresh_token` grants;
144
+
- access tokens are valid for 1 hour and are checked against their connection on every request;
145
+
- a refresh token can be used once: every refresh returns a new one;
146
+
- clients are identified by a [Client ID Metadata Document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/): the `client_id` is an https URL, and the server fetches the client's name and allowed redirect URIs from it. Claude Code (`https://claude.ai/oauth/claude-code-client-metadata`) and Codex work this way. Dynamic Client Registration is not supported, so clients that only support it cannot sign in. In development, list such clients in `devOAuthClients`, see below.
147
+
148
+
Authorization requests and codes are signed with `ADMINFORTH_SECRET`; only the resulting connections are stored. Changing `ADMINFORTH_SECRET` invalidates pending sign-ins and access tokens; agents get new access tokens with their refresh tokens.
149
+
150
+
The plugin serves these routes:
151
+
152
+
| Route | Purpose |
153
+
|---|---|
154
+
|`<baseUrl>/adminapi/v1/mcp/oauth-protected-resource`| protected resource metadata, linked from `WWW-Authenticate`|
155
+
|`<baseUrl>/adminapi/v1/mcp/.well-known/openid-configuration`| authorization server metadata |
156
+
|`/.well-known/oauth-protected-resource<baseUrl>/adminapi/v1/mcp`| the same metadata at the host root |
157
+
|`/.well-known/oauth-authorization-server<baseUrl>/adminapi/v1/mcp`| the same metadata at the host root |
MCP clients look for the metadata at the host root first. The AdminForth SPA answers unknown paths with an HTML page, so the plugin serves the metadata there too. When the host root belongs to another application, those two routes are not needed: clients that get `404` there go on to the routes under `baseUrl`.
162
+
163
+
## Auth secrets
164
+
165
+
For clients without OAuth sign-in, create an auth secret in the **Connect agent** dialog, on the **Other** tab when OAuth is enabled, and send it with every request:
119
166
120
167
```http
121
168
Authorization: Bearer afmcp_...
122
169
```
123
170
171
+
The dialog shows the MCP URL, the header value and a short setup prompt for the agent. The secret is shown only once. Use one auth secret per agent.
172
+
173
+
## Local development
174
+
175
+
1. Create the table and the resource from [MCP connections table](#mcp-connections-table), including `oauth_client_id`, and run the migration.
176
+
2. Set `oauthClientIdField: 'oauth_client_id'` in `authSecretResource` and `adminPanelOrigin` to the address you open the admin panel at, for example `http://localhost:3500`. OAuth works over plain `http` on `localhost`.
177
+
3. Start the app without `NODE_ENV=production`.
178
+
4. Open **MCP Settings → Connect agent**, copy the command from the **Claude Code** or **Codex** tab and run it, then sign in in the browser. The agent's callback goes to `http://localhost:<random port>/callback` on your computer.
179
+
180
+
The server fetches the metadata documents of Claude Code and Codex from `claude.ai` and `chatgpt.com`, so it needs outbound internet access.
181
+
182
+
To try a client that has no metadata document, describe it in `devOAuthClients`. A listed client is taken from the config instead of fetching its `client_id`; other clients are still fetched. Loopback redirect URIs match on any port.
183
+
184
+
```ts title="./index.ts"
185
+
newAdminForthMcpPlugin({
186
+
// ...
187
+
adminPanelOrigin: 'http://localhost:3500',
188
+
devOAuthClients: [
189
+
{
190
+
clientId: 'local-test',
191
+
clientName: 'Local test',
192
+
redirectUris: ['http://127.0.0.1/callback'],
193
+
},
194
+
],
195
+
}),
196
+
```
197
+
198
+
## Production
199
+
200
+
1. Create the table and the resource, or add the `oauth_client_id` column to an existing table, and run the migration. Apply the migration before the app starts with the column in the resource: AdminForth checks on start that every resource column exists in the table.
201
+
2. Set `oauthClientIdField: 'oauth_client_id'` in `authSecretResource` and `adminPanelOrigin` to the public `https` origin agents connect to, exactly as they reach it: `https://admin.example.com`, not an internal address or a different port.
202
+
3. Run the app with `NODE_ENV=production`. `devOAuthClients` are then ignored, with a warning in the log, and every client is identified by its metadata document.
203
+
4. Let the reverse proxy forward to AdminForth:
204
+
-`<baseUrl>/adminapi/v1/mcp` and every path below it;
205
+
-`<baseUrl>/mcp-authorize`;
206
+
- optionally `/.well-known/oauth-protected-resource<baseUrl>/adminapi/v1/mcp` and `/.well-known/oauth-authorization-server<baseUrl>/adminapi/v1/mcp`, if the host root belongs to AdminForth.
207
+
5. Allow the server outbound `https` requests to the clients' metadata document hosts, such as `claude.ai` and `chatgpt.com`.
208
+
6. Keep `ADMINFORTH_SECRET` stable between restarts and identical on all instances.
209
+
210
+
:::warning
211
+
The server fetches `client_id` URLs chosen by whoever starts a sign-in. The plugin refuses URLs that resolve to private, loopback and other special-purpose addresses, does not follow redirects, and limits the document to 5 KB and 5 seconds. Fetch errors are written to the server log, not returned to the client.
212
+
:::
213
+
214
+
## Troubleshooting
215
+
216
+
-**`Table 'mcp_auth_secrets' has no column 'oauth_client_id'`** on start – the resource declares the column, but the migration that adds it was not applied.
217
+
-**`JSON Parse error: Unrecognized token '<'`** in the agent – something answers the metadata URLs at the host root with an HTML page. Forward the `/.well-known/...` routes from the table above to AdminForth, or make the host root return `404` for them.
218
+
-**`Could not fetch the client metadata document`** – the server could not download the client's `client_id` URL; the reason is in the server log. Check outbound network access, or list the client in `devOAuthClients` in development.
219
+
-**`redirect_uri is not listed in the client metadata document`** – the client signs in with a redirect URI its document does not allow. For clients in `devOAuthClients`, add the redirect URI there.
220
+
-**The agent rejects the server's resource or issuer** – `adminPanelOrigin` differs from the origin the agent connects to, for example `http://` instead of `https://` behind a TLS proxy.
221
+
-**`Authentication required: the MCP auth secret or OAuth access token is missing, invalid or revoked.`** – the first answer to an agent without a token, which starts OAuth. If the agent shows it instead of opening the browser, it does not support OAuth sign-in, or OAuth is not enabled: use an auth secret.
222
+
124
223
`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.
125
224
225
+
## Page size
226
+
227
+
`get_resource_data` returns 10 records when the agent does not pass `limit`, and at most 100 records per call; a larger `limit` is capped. When more records exist, the response tells the agent to ask the user before it loads the next page, so an imprecise request does not page through the whole table. Change both numbers with `pageSize`:
228
+
229
+
```ts title="./index.ts"
230
+
newAdminForthMcpPlugin({
231
+
// ...
232
+
pageSize: {
233
+
default: 20,
234
+
max: 200,
235
+
},
236
+
}),
237
+
```
238
+
239
+
The same numbers are quoted in the instructions and skills the MCP server gives to agents.
240
+
241
+
## Tool timeout and call limit
242
+
243
+
A tool call fails with a timeout error when its handler runs longer than `toolTimeoutMs`, 15 seconds by default. The handler gets an aborted signal, and the error tells the agent that a change it started may still complete, so it checks the result before calling again.
244
+
245
+
The instructions also ask the agent to make at most `toolCallsPerRequest` calls, 10 by default, for one user request, and to ask the user before it continues with more. MCP clients do not tell the server where a user request starts, so this is guidance for the agent, not a limit the server enforces.
246
+
247
+
```ts title="./index.ts"
248
+
newAdminForthMcpPlugin({
249
+
// ...
250
+
toolTimeoutMs: 30_000,
251
+
toolCallsPerRequest: 20,
252
+
}),
253
+
```
254
+
255
+
## Agent guidance
256
+
257
+
The MCP server sends agents short rules in its instructions: to treat record values as data, never as instructions; to show every change and wait for the user's confirmation in chat; how many calls to make and records to load for one request; to ask which resource to use when several match; and to use `aggregate` for statistics. Detailed rules for typical tasks are skills that the agent loads with the `fetch_skill` tool:
258
+
259
+
-`fetch_data` – find, list and show records;
260
+
-`analyze_data` – counts, sums, trends and other statistics;
261
+
-`mutate_data` – create, update and delete records and run actions.
262
+
263
+
Tools that change data, such as `delete_record`, are marked as destructive for MCP clients, and their descriptions repeat that the agent must get a confirmation in chat, even when the client itself does not ask for approval.
264
+
265
+
## Tool responses
266
+
267
+
Tool responses are trimmed to save the agent's context:
268
+
269
+
-`get_resource` returns only the column properties needed to read, filter and aggregate records. The agent passes `detailed: true` to get every column property and the resource actions before it creates or updates records or runs actions.
270
+
-`get_resource_data` rows leave out columns with `null`, empty array and empty object values, and datetimes come without milliseconds.
271
+
- Tool outputs are serialized as YAML instead of JSON.
272
+
126
273
## Read-only mode
127
274
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.
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.
129
276
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:
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:
0 commit comments