activeadmin_mcp 0.0.3 → 0.1.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.
- checksums.yaml +4 -4
- data/README.md +350 -24
- data/app/controllers/activeadmin_mcp/mcp_controller.rb +0 -2
- data/app/models/activeadmin_mcp/api_token.rb +0 -2
- data/config/routes.rb +0 -2
- data/lib/activeadmin_mcp/action_catalog.rb +64 -0
- data/lib/activeadmin_mcp/action_definition.rb +183 -0
- data/lib/activeadmin_mcp/action_params.rb +78 -0
- data/lib/activeadmin_mcp/action_runner.rb +163 -0
- data/lib/activeadmin_mcp/action_schema.rb +66 -0
- data/lib/activeadmin_mcp/active_admin_ext.rb +39 -0
- data/lib/activeadmin_mcp/authorization.rb +0 -2
- data/lib/activeadmin_mcp/configuration.rb +0 -2
- data/lib/activeadmin_mcp/controller_dispatcher.rb +138 -0
- data/lib/activeadmin_mcp/engine.rb +6 -2
- data/lib/activeadmin_mcp/form_description.rb +149 -0
- data/lib/activeadmin_mcp/form_field_collector.rb +68 -15
- data/lib/activeadmin_mcp/record_writer.rb +161 -0
- data/lib/activeadmin_mcp/request_handler.rb +149 -38
- data/lib/activeadmin_mcp/resource_registry.rb +0 -2
- data/lib/activeadmin_mcp/version.rb +1 -3
- data/lib/activeadmin_mcp.rb +9 -3
- data/lib/generators/activeadmin_mcp/install/install_generator.rb +0 -2
- data/lib/generators/activeadmin_mcp/install/templates/initializer.rb +5 -2
- data/lib/generators/activeadmin_mcp/install/templates/mcp_api_tokens.rb +0 -2
- data/lib/generators/activeadmin_mcp/install/templates/migration.rb.erb +0 -2
- metadata +18 -10
- data/lib/activeadmin_mcp/record_updater.rb +0 -108
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aac4aa9e2af5d377f711090b2e8adad79915844e26842733f5c8af6b02f37d7b
|
|
4
|
+
data.tar.gz: c418625d7b5bff3c9e555d0d248e098c08a8ac1a224644e900fa14fa3a95dfa7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aea999468c061e6d532b0edb1e3241393a78dc34c1f94c0b07c2108431ab4370b4c89d45940a6691dce0ce5bfbc6903f92bc087457fc4d557fd70a97dacc9278
|
|
7
|
+
data.tar.gz: 588af72e3801e3c2c728d0407e217c7ebf54277d07a8dbdbd518fdbfe7e9cb335601cc3afe90cbcbbe76af40ef55b6bddde982ce11632107aca957875e34e464
|
data/README.md
CHANGED
|
@@ -20,7 +20,11 @@ The server is a Rails engine mounted inside your application (by default at
|
|
|
20
20
|
are the ones you have already configured.
|
|
21
21
|
- **Queries use Ransack.** The `query` tool passes its arguments straight to
|
|
22
22
|
[Ransack](https://activerecord-hackery.github.io/ransack/), the same search
|
|
23
|
-
library ActiveAdmin uses for filtering.
|
|
23
|
+
library ActiveAdmin uses for filtering. The tool calls `ransack` on the
|
|
24
|
+
model directly rather than going through an ActiveAdmin filter form, so on
|
|
25
|
+
Ransack 4 the model must allowlist the attributes it wants queryable via
|
|
26
|
+
`ransackable_attributes`; without that allowlist, `query` against that
|
|
27
|
+
resource will raise instead of returning an empty result.
|
|
24
28
|
- **Reads go through ActiveAdmin too.** `list_resources` and `query` run through
|
|
25
29
|
the same authorization adapter (CanCanCan, Pundit, etc.) as the authenticated
|
|
26
30
|
MCP user: resources the user cannot read are hidden from the listing and
|
|
@@ -28,18 +32,21 @@ The server is a Rails engine mounted inside your application (by default at
|
|
|
28
32
|
`scope_collection`, so the MCP user only ever sees the records they could see
|
|
29
33
|
in the admin UI. With ActiveAdmin's default adapter every check passes, so
|
|
30
34
|
applications without an authorization adapter are unaffected.
|
|
31
|
-
- **Writes go through ActiveAdmin.** The `
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
authorization adapter as
|
|
35
|
+
- **Writes go through ActiveAdmin.** The `create` and `update` tools dispatch
|
|
36
|
+
the resource's real ActiveAdmin controller action, so `permit_params`, your
|
|
37
|
+
`before_save`/`after_update` callbacks, the controller's `before_action`
|
|
38
|
+
chain and your authorization adapter all apply exactly as they do when
|
|
39
|
+
someone clicks Save in the admin UI. Resources that don't register the
|
|
40
|
+
action are refused, and `describe_form` will tell a client what a given
|
|
41
|
+
resource's form accepts before it tries.
|
|
35
42
|
- **Authentication is optional but built in.** Enable Bearer-token auth and the
|
|
36
43
|
installer adds an "MCP Tokens" management page to your ActiveAdmin panel.
|
|
37
44
|
|
|
38
45
|
## Requirements
|
|
39
46
|
|
|
40
|
-
- Ruby >=
|
|
41
|
-
- Rails >=
|
|
42
|
-
- ActiveAdmin
|
|
47
|
+
- Ruby >= 4.0
|
|
48
|
+
- Rails >= 7.2
|
|
49
|
+
- ActiveAdmin ~> 3.5
|
|
43
50
|
|
|
44
51
|
## Installation
|
|
45
52
|
|
|
@@ -65,34 +72,230 @@ read/query setup without authentication.
|
|
|
65
72
|
|------|-------------|
|
|
66
73
|
| `list_resources` | List the ActiveAdmin resources the current user may read, along with their attributes. |
|
|
67
74
|
| `query` | Query a resource the current user may read, using Ransack syntax, scoped to the records they may access (`limit` defaults to 25, capped at 100). |
|
|
68
|
-
| `
|
|
75
|
+
| `create` | Create a new record through the resource's ActiveAdmin create action, honouring its permitted params, callbacks and authorization. |
|
|
76
|
+
| `update` | Update an existing record through the resource's ActiveAdmin update action, honouring its permitted params, callbacks and authorization. |
|
|
77
|
+
| `describe_form` | Describe the fields of a resource's form — input types, labels, hints, allowed values, column types and which are required — so a `create` or `update` call need not guess them. |
|
|
78
|
+
| *(per action)* | Any ActiveAdmin member, collection or batch action the application has opted in with an `mcp:` option, exposed as its own tool. |
|
|
79
|
+
|
|
80
|
+
The examples below are drawn from the application the end-to-end suite builds
|
|
81
|
+
and drives over HTTP, which is checked in under
|
|
82
|
+
[`spec/e2e/fixture_app/`](spec/e2e/fixture_app/): `Post` permits `title` and
|
|
83
|
+
`body` but not `slug`, `Author` is registered `actions :index, :show`, `Review`
|
|
84
|
+
declares its own form block, and `Tag` declares no `permit_params` at all. Most
|
|
85
|
+
of what this README claims is asserted against that running application in
|
|
86
|
+
[`spec/e2e/`](spec/e2e/), so a claim here can be read next to the example that
|
|
87
|
+
exercises it.
|
|
69
88
|
|
|
70
89
|
### Query examples
|
|
71
90
|
|
|
72
91
|
```
|
|
73
|
-
|
|
74
|
-
→ query(resource: "
|
|
92
|
+
Find the post whose title mentions Earthsea
|
|
93
|
+
→ query(resource: "Post", q: { title_cont: "Earthsea" })
|
|
75
94
|
|
|
76
|
-
Find
|
|
77
|
-
→ query(resource: "Post", q: { status_eq: "
|
|
95
|
+
Find draft posts created since the start of the month
|
|
96
|
+
→ query(resource: "Post", q: { status_eq: "draft", created_at_gt: "2026-08-01" })
|
|
97
|
+
|
|
98
|
+
Look an author up by email
|
|
99
|
+
→ query(resource: "Author", q: { email_eq: "ursula@example.com" })
|
|
78
100
|
```
|
|
79
101
|
|
|
80
|
-
###
|
|
102
|
+
### Creating and updating records
|
|
81
103
|
|
|
82
104
|
```
|
|
83
|
-
|
|
84
|
-
→
|
|
105
|
+
Create a post
|
|
106
|
+
→ create(resource: "Post", attributes: { title: "Mort", body: "The fourth." })
|
|
107
|
+
|
|
108
|
+
Retitle a post
|
|
109
|
+
→ update(resource: "Post", id: 42, attributes: { title: "Pyramids" })
|
|
85
110
|
```
|
|
86
111
|
|
|
87
|
-
|
|
112
|
+
Both tools dispatch the resource's own ActiveAdmin `create` or `update`
|
|
113
|
+
action, so a write from MCP is the same write the admin UI makes:
|
|
88
114
|
|
|
89
|
-
- **
|
|
90
|
-
|
|
91
|
-
- **Authorization** — the
|
|
92
|
-
authorization adapter for the authenticated MCP user,
|
|
93
|
-
|
|
115
|
+
- **Registered actions only** — resources registered without the action
|
|
116
|
+
(e.g. `Author`, registered `actions :index, :show`) are refused.
|
|
117
|
+
- **Authorization** — the write runs through the resource namespace's
|
|
118
|
+
authorization adapter for the authenticated MCP user, both before dispatch
|
|
119
|
+
and again inside the controller, so it can only write what that user is
|
|
120
|
+
allowed to write in admin.
|
|
94
121
|
- **Permitted fields only** — attributes are filtered through the resource's
|
|
95
|
-
`permit_params`; fields the admin form doesn't accept are silently dropped
|
|
122
|
+
`permit_params`; fields the admin form doesn't accept are silently dropped,
|
|
123
|
+
so a `slug` sent to a `Post` permitting only `title` and `body` never
|
|
124
|
+
reaches the record. A resource that declares no `permit_params` at all (like
|
|
125
|
+
`Tag`) is refused outright, with a message saying so — ActiveAdmin cannot
|
|
126
|
+
write such a resource through its own forms either.
|
|
127
|
+
- **Your callbacks run** — ActiveAdmin's `before_build`, `before_create`,
|
|
128
|
+
`before_save`, `after_update` and friends all fire, because the controller
|
|
129
|
+
action is what fires them. A `before_create` that fills in a `slug` the form
|
|
130
|
+
never accepted still fills it in.
|
|
131
|
+
|
|
132
|
+
A write rejected by the model comes back as a `Validation failed` error with
|
|
133
|
+
the model's own messages in `details`, and nothing is written.
|
|
134
|
+
|
|
135
|
+
### Describing a form
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
What can I set when writing a review?
|
|
139
|
+
→ describe_form(resource: "Review") # source: "form"
|
|
140
|
+
|
|
141
|
+
What can I set when creating a post?
|
|
142
|
+
→ describe_form(resource: "Post") # source: "permit_params"
|
|
143
|
+
→ describe_form(resource: "Post", action: "edit")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`describe_form` reads the resource's own `form do ... end` block when it
|
|
147
|
+
declares one, reporting each input's `as:`, `label:`, `hint:` and — when the
|
|
148
|
+
`collection:` is a literal array — its allowed values. `Review` declares one,
|
|
149
|
+
so its `status` comes back as a `select` labelled "Moderation status" with
|
|
150
|
+
`["pending", "approved"]` as its allowed values. Resources that declare
|
|
151
|
+
no form block get a description derived from their `permit_params` instead:
|
|
152
|
+
ActiveAdmin renders a bare `f.inputs` for those, which Formtastic only expands
|
|
153
|
+
at render time, so there is nothing to read. The response's `source` says which
|
|
154
|
+
of the two you are looking at.
|
|
155
|
+
|
|
156
|
+
Either way every field is annotated from the model with the column type it is
|
|
157
|
+
stored in and whether the model validates its presence. `has_many` blocks are
|
|
158
|
+
reported under `nested` rather than flattened in with the record's own fields.
|
|
159
|
+
|
|
160
|
+
`action:` selects the gate, not the shape — ActiveAdmin uses one form block for
|
|
161
|
+
both. `"new"` (the default) requires the resource to register `create` and pass
|
|
162
|
+
`create` authorization; `"edit"` requires `update`. Describing a form you could
|
|
163
|
+
never submit tells you nothing you can act on, so it is refused with the same
|
|
164
|
+
messages `create` and `update` use: `Author` is not creatable, and `Tag`
|
|
165
|
+
declares no permitted params.
|
|
166
|
+
|
|
167
|
+
Two limits worth knowing:
|
|
168
|
+
|
|
169
|
+
- A `collection:` that is an `ActiveRecord::Relation` or a proc is omitted
|
|
170
|
+
rather than evaluated. Describing a form should not fire a query, and a
|
|
171
|
+
relation can be arbitrarily large. Use an action's
|
|
172
|
+
[`suggestions:`](#running-member-collection-and-batch-actions) when you want
|
|
173
|
+
dynamic values.
|
|
174
|
+
- A field a form block declares but `permit_params` omits is described and then
|
|
175
|
+
silently dropped on write. This cannot arise on the `permit_params` fallback
|
|
176
|
+
path.
|
|
177
|
+
|
|
178
|
+
### Running member, collection and batch actions
|
|
179
|
+
|
|
180
|
+
ActiveAdmin actions are **not** exposed by default. An action becomes an MCP
|
|
181
|
+
tool only when you add an `mcp:` option to it:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
ActiveAdmin.register Post do
|
|
185
|
+
member_action :publish, method: :post, mcp: {
|
|
186
|
+
description: "Publish a post with the given visibility",
|
|
187
|
+
params: {
|
|
188
|
+
visibility: { type: :string, required: true,
|
|
189
|
+
enum: %w[public unlisted],
|
|
190
|
+
hint: "Who can see the post once published" }
|
|
191
|
+
}
|
|
192
|
+
} do
|
|
193
|
+
# your existing action body, unchanged by opting in
|
|
194
|
+
resource.update!(status: params[:visibility])
|
|
195
|
+
redirect_to resource_path(resource), notice: "Published"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# No mcp: key, so this one is not a tool — not listed, and refused by name.
|
|
199
|
+
member_action :archive, method: :post do
|
|
200
|
+
resource.update!(status: "archived")
|
|
201
|
+
redirect_to resource_path(resource), notice: "Archived"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
That registers a `post_publish` tool, and no `post_archive` tool. Batch actions
|
|
207
|
+
opt in the same way, and inherit their param types from the `form:` hash you
|
|
208
|
+
already declare:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
batch_action :set_status, form: { status: :text },
|
|
212
|
+
mcp: { description: "Set the status on the selected posts" } do |ids, inputs|
|
|
213
|
+
Post.where(id: ids).update_all(status: inputs["status"])
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
That registers a `post_set_status` tool taking an `ids` array alongside the
|
|
218
|
+
`status` string it took from `form:`.
|
|
219
|
+
|
|
220
|
+
**Declaring params**
|
|
221
|
+
|
|
222
|
+
- `type:` — one of `:string`, `:integer`, `:number`, `:boolean`, `:array`, `:object`.
|
|
223
|
+
- `required:` — refuses the call when the value is missing.
|
|
224
|
+
- `hint:` — static guidance shown to the agent. Always a plain string.
|
|
225
|
+
- `enum:` — **binding**. A value outside the list is refused before dispatch.
|
|
226
|
+
- `suggestions:` — a proc evaluated when tools are listed, e.g.
|
|
227
|
+
`suggestions: -> { Post.distinct.pluck(:status) }`. **Advisory only**, never
|
|
228
|
+
enforced, so use it for live values from the database. If it raises, the tool
|
|
229
|
+
is still listed without suggestions.
|
|
230
|
+
|
|
231
|
+
On a batch action, every declared param must also appear in the action's `form:`
|
|
232
|
+
hash. ActiveAdmin slices submitted inputs down to the declared `form:` keys
|
|
233
|
+
before calling the block, so a param declared only under `mcp:` would be
|
|
234
|
+
advertised to the client and then dropped; the declaration is refused instead.
|
|
235
|
+
|
|
236
|
+
**Authorization**
|
|
237
|
+
|
|
238
|
+
`permission:` is an *additional* gate, never a replacement. Every call first
|
|
239
|
+
passes your ActiveAdmin authorization adapter exactly as `query` and `update`
|
|
240
|
+
do; the proc can only narrow access further, never widen it. It is evaluated in
|
|
241
|
+
controller context, so `current_admin_user`, `can?` and the usual admin helpers
|
|
242
|
+
are available. Return `false` to refuse, or a `String` to refuse with a reason
|
|
243
|
+
the agent can act on.
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
# Takes the record, so it can only be resolved when the call arrives.
|
|
247
|
+
member_action :feature, method: :post, mcp: {
|
|
248
|
+
description: "Feature a published post on the front page",
|
|
249
|
+
permission: ->(post) { post.status == "draft" ? "Only a published post can be featured" : true }
|
|
250
|
+
} do
|
|
251
|
+
resource.update!(status: "featured")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Takes no arguments, so it is resolved when tools are listed.
|
|
255
|
+
collection_action :purge_drafts, method: :post, mcp: {
|
|
256
|
+
description: "Purge every draft post",
|
|
257
|
+
permission: -> { current_admin_user&.editor? }
|
|
258
|
+
} do
|
|
259
|
+
redirect_to collection_path, notice: "Purged"
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Tools are also listed per user: an action whose resource the adapter refuses is
|
|
264
|
+
left out of `tools/list` entirely, and a `permission:` proc that takes no
|
|
265
|
+
arguments is evaluated at listing time (in the same controller context) so the
|
|
266
|
+
tool is hidden rather than offered and then refused. A proc that takes the
|
|
267
|
+
record cannot be resolved without one, so `post_feature` stays advertised and
|
|
268
|
+
refuses at call time with the string the proc returned.
|
|
269
|
+
|
|
270
|
+
For **batch actions** the adapter check is necessarily resource-level — there is
|
|
271
|
+
no single record to authorize — so it is `authorized?(:<action>, YourModel)`
|
|
272
|
+
rather than a per-record policy evaluation. To stop that being a hole, the ids
|
|
273
|
+
the client submits are run back through the adapter's `scope_collection`, and
|
|
274
|
+
the whole call is refused if any of them falls outside the scope. Nothing is
|
|
275
|
+
narrowed silently: the call either acts on every id you asked for or on none.
|
|
276
|
+
|
|
277
|
+
One caveat on authentication. Dispatch neutralises the namespace's
|
|
278
|
+
`authentication_method` callback, because the MCP request has already
|
|
279
|
+
authenticated by bearer token and that callback would otherwise redirect to a
|
|
280
|
+
login page. If your `authentication_method` is a *combined* authentication-and-
|
|
281
|
+
authorization method — one that also, say, rejects non-superusers — then
|
|
282
|
+
neutralising it disables that authorization half too. Resource-level
|
|
283
|
+
authorization still runs through the adapter, but the "we only skip
|
|
284
|
+
authentication" framing is not universal; keep authorization in the adapter,
|
|
285
|
+
not in the authentication callback.
|
|
286
|
+
|
|
287
|
+
**What you get back**
|
|
288
|
+
|
|
289
|
+
Actions are executed through your real ActiveAdmin controller, so the action's
|
|
290
|
+
`before_action` chain, authorization and callbacks all run. The tool returns the
|
|
291
|
+
response status, the redirect target and any flash messages — not the rendered
|
|
292
|
+
HTML. An action whose body raises comes back as a generic `Post#publish failed`
|
|
293
|
+
naming the resource and action, with the exception's own message — which can
|
|
294
|
+
carry SQL, table names and file paths — kept away from the client.
|
|
295
|
+
|
|
296
|
+
Redirect-style (submit-side) actions are the supported case; a `GET` action that
|
|
297
|
+
renders a full admin view is best-effort and may fail for want of a view
|
|
298
|
+
context.
|
|
96
299
|
|
|
97
300
|
## Connecting a client
|
|
98
301
|
|
|
@@ -203,10 +406,95 @@ end
|
|
|
203
406
|
}
|
|
204
407
|
```
|
|
205
408
|
|
|
409
|
+
## Claude Desktop (MCPB bundle)
|
|
410
|
+
|
|
411
|
+
Claude Code talks to the server over HTTP directly, but **Claude Desktop** cannot:
|
|
412
|
+
its remote connector UI has no way to send an API token header. The `mcpb/`
|
|
413
|
+
directory solves this with an
|
|
414
|
+
[MCP bundle](https://claude.com/docs/connectors/building/mcpb) — a `.mcpb` file
|
|
415
|
+
your colleagues install with a double-click.
|
|
416
|
+
|
|
417
|
+
Inside the bundle is a small Node script that speaks stdio to Claude Desktop and
|
|
418
|
+
forwards every message, unchanged, to your server over HTTPS with the token
|
|
419
|
+
attached. It has no dependencies and adds no capabilities of its own, so the
|
|
420
|
+
tools it exposes are exactly the ones your server exposes.
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
Claude Desktop ──stdio──▶ mcpb proxy ──HTTPS + token──▶ your Rails app
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Building the bundle
|
|
427
|
+
|
|
428
|
+
Requires Node 18 or newer. From the repository root:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
cd mcpb
|
|
432
|
+
npm test # no dependencies to install
|
|
433
|
+
npx @anthropic-ai/mcpb pack . ../activeadmin-mcp.mcpb
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
That writes `activeadmin-mcp.mcpb` (a zip of `manifest.json`, `package.json` and
|
|
437
|
+
`server/index.js`) to the repository root. You do not bump the bundle's
|
|
438
|
+
`version` by hand: publishing a GitHub Release writes the release tag into
|
|
439
|
+
`mcpb/manifest.json` and `mcpb/package.json`, packs the bundle from that, and
|
|
440
|
+
attaches `activeadmin-mcp-<version>.mcpb` to the release — so the bundle
|
|
441
|
+
version Claude Desktop uses to detect upgrades always matches the gem version.
|
|
442
|
+
See [RELEASING.md](RELEASING.md).
|
|
443
|
+
|
|
444
|
+
So the bundle for any released version is on that release's page, and CI packs
|
|
445
|
+
the bundle on every push and attaches it as a build artifact if you want an
|
|
446
|
+
unreleased build from the Actions tab.
|
|
447
|
+
|
|
448
|
+
Distribute the file however suits you — pointing colleagues at the release
|
|
449
|
+
asset, an internal file share, or an S3 bucket. Anyone with the file can
|
|
450
|
+
install it, but it is inert without a token.
|
|
451
|
+
|
|
452
|
+
### Installing
|
|
453
|
+
|
|
454
|
+
1. **Generate a token.** Sign in to your admin panel, go to **MCP Tokens**, name
|
|
455
|
+
the token after the machine you are installing on (e.g. "Claude Desktop —
|
|
456
|
+
work laptop") and copy it. It is shown only once.
|
|
457
|
+
2. **Install the bundle.** Double-click the `.mcpb` file, or drag it onto the
|
|
458
|
+
Claude Desktop window, or use **Settings → Extensions → Advanced settings →
|
|
459
|
+
Install Extension…**.
|
|
460
|
+
3. **Fill in the three settings** Claude Desktop prompts for:
|
|
461
|
+
|
|
462
|
+
| Setting | Value |
|
|
463
|
+
|---------|-------|
|
|
464
|
+
| Server URL | The full MCP endpoint, e.g. `https://admin.example.com/admin/mcp` |
|
|
465
|
+
| API token | The token from step 1 (stored in the OS keychain, never shown again) |
|
|
466
|
+
| Authentication header | `Authorization`, unless your app sets a custom [`auth_header_name`](#custom-auth-header) |
|
|
467
|
+
|
|
468
|
+
4. **Check it works.** Start a new chat and ask Claude to list the admin
|
|
469
|
+
resources it can see. You should get back the resources your account can read.
|
|
470
|
+
|
|
471
|
+
Installation is per-person: each colleague installs the bundle and generates
|
|
472
|
+
their own token, so every MCP call is attributed to them and constrained by
|
|
473
|
+
their own admin permissions.
|
|
474
|
+
|
|
475
|
+
### Revoking access
|
|
476
|
+
|
|
477
|
+
A token is a long-lived credential granting everything that user can do in
|
|
478
|
+
admin. Revoke one from the same **MCP Tokens** page — the `Last used` column
|
|
479
|
+
shows which tokens are still live. Revoking takes effect immediately; the
|
|
480
|
+
installed bundle simply starts reporting an authentication failure.
|
|
481
|
+
|
|
482
|
+
### Troubleshooting
|
|
483
|
+
|
|
484
|
+
| Symptom | Cause |
|
|
485
|
+
|---------|-------|
|
|
486
|
+
| "The server rejected the API token (HTTP 401)" | The token is wrong, revoked, or being sent in the wrong header. Check the **Authentication header** setting matches your `auth_header_name`. |
|
|
487
|
+
| "Could not reach …" | The URL is wrong or unreachable from this machine — check VPN, and that the URL includes the full mount path. |
|
|
488
|
+
| The extension shows no tools | Claude Desktop only refreshes tools on connect. Toggle the extension off and on in Settings → Extensions. |
|
|
489
|
+
| Anything else | Claude Desktop's extension logs carry the proxy's stderr output, each line prefixed `[activeadmin_mcp]`. |
|
|
490
|
+
|
|
206
491
|
## Configuration
|
|
207
492
|
|
|
208
493
|
The generator writes an initializer to
|
|
209
|
-
`config/initializers/activeadmin_mcp.rb
|
|
494
|
+
`config/initializers/activeadmin_mcp.rb` carrying every option below,
|
|
495
|
+
commented out. Running it with `--auth devise_token` uncomments the
|
|
496
|
+
authentication method for you, so the first edit is usually just naming your
|
|
497
|
+
Devise model:
|
|
210
498
|
|
|
211
499
|
```ruby
|
|
212
500
|
ActiveadminMcp.configure do |config|
|
|
@@ -266,6 +554,44 @@ bundle install
|
|
|
266
554
|
bundle exec rspec
|
|
267
555
|
```
|
|
268
556
|
|
|
557
|
+
### Running the tests
|
|
558
|
+
|
|
559
|
+
`bundle exec rspec` (or `rake spec`, the default rake task) runs the unit
|
|
560
|
+
suite only — it's fast, and it's what CI's `rspec` job runs. It never touches
|
|
561
|
+
`spec/e2e`; that directory is excluded via `.rspec`.
|
|
562
|
+
|
|
563
|
+
The end-to-end suite lives in `spec/e2e` and runs separately:
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
bundle exec rake e2e
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
This generates a real Rails 7.2 + ActiveAdmin + Devise application under
|
|
570
|
+
`tmp/e2e_app`, installs the gem into it with `path:`, boots it under Puma,
|
|
571
|
+
and drives it over real HTTP with a minimal JSON-RPC client — the same way
|
|
572
|
+
Claude Code or any other MCP client would. It's how the gem is tested
|
|
573
|
+
against ActiveAdmin's and Ransack's actual behaviour rather than mocks.
|
|
574
|
+
|
|
575
|
+
The registrations, models, migrations and seeds that application uses are
|
|
576
|
+
checked in under [`spec/e2e/fixture_app/`](spec/e2e/fixture_app/) rather than
|
|
577
|
+
written from heredocs, and they are the `Post`, `Author`, `Review` and `Tag`
|
|
578
|
+
this README's examples are written against — so a claim above can be read next
|
|
579
|
+
to both the registration it describes and the example that exercises it.
|
|
580
|
+
[`spec/e2e/fixture_app/README.md`](spec/e2e/fixture_app/README.md) says which
|
|
581
|
+
claim each fixture exists to support.
|
|
582
|
+
|
|
583
|
+
The generated application is expensive to build (a full `bundle install`
|
|
584
|
+
against rubygems.org, and a `gem install rails` if Rails 7.2.2.2 isn't
|
|
585
|
+
already on your system) so it's cached in `tmp/e2e_app` between runs, keyed
|
|
586
|
+
on the contents of `spec/e2e/support/app_builder.rb`. The first run costs a
|
|
587
|
+
few minutes and needs network access; edit that file and the next run
|
|
588
|
+
rebuilds from scratch, otherwise reruns are quick. Force a rebuild without
|
|
589
|
+
editing anything by setting `E2E_REBUILD=1`:
|
|
590
|
+
|
|
591
|
+
```bash
|
|
592
|
+
E2E_REBUILD=1 bundle exec rake e2e
|
|
593
|
+
```
|
|
594
|
+
|
|
269
595
|
## Contributing
|
|
270
596
|
|
|
271
597
|
Bug reports and pull requests are welcome on GitHub.
|
data/config/routes.rb
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module ActiveadminMcp
|
|
2
|
+
# Finds every ActiveAdmin action an application has opted in to MCP.
|
|
3
|
+
#
|
|
4
|
+
# Nothing is cached: ActiveAdmin reloads resources in development, and a
|
|
5
|
+
# stale catalog would advertise tools that no longer exist.
|
|
6
|
+
module ActionCatalog
|
|
7
|
+
# Tool names the engine reserves for itself, including names Phase 2 and 3
|
|
8
|
+
# will take, so an application cannot silently shadow one later.
|
|
9
|
+
RESERVED = %w[list_resources query update create describe_form].freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def all
|
|
13
|
+
ResourceRegistry.resources.flat_map { |entry| definitions_for(entry[:config]) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def find(tool_name)
|
|
17
|
+
all.find { |definition| definition.tool_name == tool_name }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def definitions_for(config)
|
|
23
|
+
candidates(config).filter_map do |action, kind|
|
|
24
|
+
definition = ActionDefinition.build(config: config, action: action, kind: kind)
|
|
25
|
+
next unless definition
|
|
26
|
+
|
|
27
|
+
next warn_and_skip(definition.errors.join("; ")) unless definition.valid?
|
|
28
|
+
next warn_and_skip("#{definition.tool_name} collides with a built-in tool") if reserved?(definition)
|
|
29
|
+
|
|
30
|
+
definition
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def candidates(config)
|
|
35
|
+
pairs = []
|
|
36
|
+
pairs.concat(safe_actions(config, :member_actions).map { |a| [a, :member] })
|
|
37
|
+
pairs.concat(safe_actions(config, :collection_actions).map { |a| [a, :collection] })
|
|
38
|
+
pairs.concat(safe_actions(config, :batch_actions).map { |a| [a, :batch] }) if batch_enabled?(config)
|
|
39
|
+
pairs.select { |action, _kind| action.respond_to?(:mcp_options) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def safe_actions(config, reader)
|
|
43
|
+
config.respond_to?(reader) ? Array(config.public_send(reader)) : []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# ActiveAdmin keeps registered batch actions even when the namespace has
|
|
47
|
+
# batch actions switched off, and hides them in the UI. Match that.
|
|
48
|
+
def batch_enabled?(config)
|
|
49
|
+
return false unless config.respond_to?(:batch_actions_enabled?)
|
|
50
|
+
|
|
51
|
+
config.batch_actions_enabled?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def reserved?(definition)
|
|
55
|
+
RESERVED.include?(definition.tool_name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def warn_and_skip(message)
|
|
59
|
+
warn("[activeadmin_mcp] ignoring action: #{message}")
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|