activeadmin_mcp 0.0.2 → 0.0.4
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 +88 -4
- data/lib/activeadmin_mcp/authorization.rb +32 -0
- data/lib/activeadmin_mcp/record_updater.rb +1 -3
- data/lib/activeadmin_mcp/request_handler.rb +26 -5
- data/lib/activeadmin_mcp/resource_registry.rb +20 -15
- data/lib/activeadmin_mcp/version.rb +1 -1
- data/lib/activeadmin_mcp.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5eb6b95afa3bf6d96a2ae33ced52621014f535f11542dcbee38b88638580e6a9
|
|
4
|
+
data.tar.gz: c87121826ee5e29805cfa9f3fd8faf1f5042698ec94d46c5fbd7bbc90f08b3f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b7a903b975a980a23246e944b2e321fab7019ac218c06a51f6645a5e0d7577951cb4bdce9c4319e7184d9b5f031b96320ee874666b8d28c1814850cb59533b9
|
|
7
|
+
data.tar.gz: 3a069d2f4d7a69038f3f96153e67911472f43fabb64aa4fc7a3ccdb421684cd6cabd126d208fbe9671dc1fa7c7ecc7f2789d68638f4e84cfdcb78e45d90ddb7b
|
data/README.md
CHANGED
|
@@ -21,11 +21,17 @@ The server is a Rails engine mounted inside your application (by default at
|
|
|
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
23
|
library ActiveAdmin uses for filtering.
|
|
24
|
+
- **Reads go through ActiveAdmin too.** `list_resources` and `query` run through
|
|
25
|
+
the same authorization adapter (CanCanCan, Pundit, etc.) as the authenticated
|
|
26
|
+
MCP user: resources the user cannot read are hidden from the listing and
|
|
27
|
+
refused by `query`, and every query is scoped with the adapter's
|
|
28
|
+
`scope_collection`, so the MCP user only ever sees the records they could see
|
|
29
|
+
in the admin UI. With ActiveAdmin's default adapter every check passes, so
|
|
30
|
+
applications without an authorization adapter are unaffected.
|
|
24
31
|
- **Writes go through ActiveAdmin.** The `update` tool only writes fields
|
|
25
32
|
allowed by the resource's `permit_params`, refuses resources that don't
|
|
26
33
|
register the `update` action, and runs every change through your
|
|
27
|
-
authorization adapter
|
|
28
|
-
user.
|
|
34
|
+
authorization adapter as the authenticated MCP user.
|
|
29
35
|
- **Authentication is optional but built in.** Enable Bearer-token auth and the
|
|
30
36
|
installer adds an "MCP Tokens" management page to your ActiveAdmin panel.
|
|
31
37
|
|
|
@@ -57,8 +63,8 @@ read/query setup without authentication.
|
|
|
57
63
|
|
|
58
64
|
| Tool | Description |
|
|
59
65
|
|------|-------------|
|
|
60
|
-
| `list_resources` | List
|
|
61
|
-
| `query` | Query a resource using Ransack syntax (`limit` defaults to 25, capped at 100). |
|
|
66
|
+
| `list_resources` | List the ActiveAdmin resources the current user may read, along with their attributes. |
|
|
67
|
+
| `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). |
|
|
62
68
|
| `update` | Update an existing record, honouring ActiveAdmin's permitted params and authorization. |
|
|
63
69
|
|
|
64
70
|
### Query examples
|
|
@@ -197,6 +203,84 @@ end
|
|
|
197
203
|
}
|
|
198
204
|
```
|
|
199
205
|
|
|
206
|
+
## Claude Desktop (MCPB bundle)
|
|
207
|
+
|
|
208
|
+
Claude Code talks to the server over HTTP directly, but **Claude Desktop** cannot:
|
|
209
|
+
its remote connector UI has no way to send an API token header. The `mcpb/`
|
|
210
|
+
directory solves this with an
|
|
211
|
+
[MCP bundle](https://claude.com/docs/connectors/building/mcpb) — a `.mcpb` file
|
|
212
|
+
your colleagues install with a double-click.
|
|
213
|
+
|
|
214
|
+
Inside the bundle is a small Node script that speaks stdio to Claude Desktop and
|
|
215
|
+
forwards every message, unchanged, to your server over HTTPS with the token
|
|
216
|
+
attached. It has no dependencies and adds no capabilities of its own, so the
|
|
217
|
+
tools it exposes are exactly the ones your server exposes.
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
Claude Desktop ──stdio──▶ mcpb proxy ──HTTPS + token──▶ your Rails app
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Building the bundle
|
|
224
|
+
|
|
225
|
+
Requires Node 18 or newer. From the repository root:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
cd mcpb
|
|
229
|
+
npm test # no dependencies to install
|
|
230
|
+
npx @anthropic-ai/mcpb pack . ../activeadmin-mcp.mcpb
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
That writes `activeadmin-mcp.mcpb` (a zip of `manifest.json`, `package.json` and
|
|
234
|
+
`server/index.js`) to the repository root, ready to distribute. Bump `version`
|
|
235
|
+
in **both** `mcpb/manifest.json` and `mcpb/package.json` before packing a
|
|
236
|
+
release — Claude Desktop uses the manifest version to detect upgrades.
|
|
237
|
+
|
|
238
|
+
CI packs the bundle on every push and attaches it as a build artifact, so you
|
|
239
|
+
can also download a build from the Actions tab rather than packing it yourself.
|
|
240
|
+
|
|
241
|
+
Distribute the file however suits you: an internal file share, a GitHub release
|
|
242
|
+
asset, or an S3 bucket. Anyone with the file can install it, but it is inert
|
|
243
|
+
without a token.
|
|
244
|
+
|
|
245
|
+
### Installing
|
|
246
|
+
|
|
247
|
+
1. **Generate a token.** Sign in to your admin panel, go to **MCP Tokens**, name
|
|
248
|
+
the token after the machine you are installing on (e.g. "Claude Desktop —
|
|
249
|
+
work laptop") and copy it. It is shown only once.
|
|
250
|
+
2. **Install the bundle.** Double-click the `.mcpb` file, or drag it onto the
|
|
251
|
+
Claude Desktop window, or use **Settings → Extensions → Advanced settings →
|
|
252
|
+
Install Extension…**.
|
|
253
|
+
3. **Fill in the three settings** Claude Desktop prompts for:
|
|
254
|
+
|
|
255
|
+
| Setting | Value |
|
|
256
|
+
|---------|-------|
|
|
257
|
+
| Server URL | The full MCP endpoint, e.g. `https://admin.example.com/admin/mcp` |
|
|
258
|
+
| API token | The token from step 1 (stored in the OS keychain, never shown again) |
|
|
259
|
+
| Authentication header | `Authorization`, unless your app sets a custom [`auth_header_name`](#custom-auth-header) |
|
|
260
|
+
|
|
261
|
+
4. **Check it works.** Start a new chat and ask Claude to list the admin
|
|
262
|
+
resources it can see. You should get back the resources your account can read.
|
|
263
|
+
|
|
264
|
+
Installation is per-person: each colleague installs the bundle and generates
|
|
265
|
+
their own token, so every MCP call is attributed to them and constrained by
|
|
266
|
+
their own admin permissions.
|
|
267
|
+
|
|
268
|
+
### Revoking access
|
|
269
|
+
|
|
270
|
+
A token is a long-lived credential granting everything that user can do in
|
|
271
|
+
admin. Revoke one from the same **MCP Tokens** page — the `Last used` column
|
|
272
|
+
shows which tokens are still live. Revoking takes effect immediately; the
|
|
273
|
+
installed bundle simply starts reporting an authentication failure.
|
|
274
|
+
|
|
275
|
+
### Troubleshooting
|
|
276
|
+
|
|
277
|
+
| Symptom | Cause |
|
|
278
|
+
|---------|-------|
|
|
279
|
+
| "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`. |
|
|
280
|
+
| "Could not reach …" | The URL is wrong or unreachable from this machine — check VPN, and that the URL includes the full mount path. |
|
|
281
|
+
| The extension shows no tools | Claude Desktop only refreshes tools on connect. Toggle the extension off and on in Settings → Extensions. |
|
|
282
|
+
| Anything else | Claude Desktop's extension logs carry the proxy's stderr output, each line prefixed `[activeadmin_mcp]`. |
|
|
283
|
+
|
|
200
284
|
## Configuration
|
|
201
285
|
|
|
202
286
|
The generator writes an initializer to
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveadminMcp
|
|
4
|
+
# Applies a resource's ActiveAdmin authorization adapter to MCP tool calls, so
|
|
5
|
+
# reads, listings and writes obey the same rules as the admin UI.
|
|
6
|
+
#
|
|
7
|
+
# ActiveAdmin's default adapter authorizes every action and returns collections
|
|
8
|
+
# unchanged, so applications without an authorization adapter are unaffected.
|
|
9
|
+
# Applications that configure one (CanCanCan via `cancan_ability_class`, Pundit,
|
|
10
|
+
# a custom adapter, ...) get their policy enforced on every path.
|
|
11
|
+
class Authorization
|
|
12
|
+
READ = :read
|
|
13
|
+
|
|
14
|
+
def self.for(config, current_user)
|
|
15
|
+
adapter_class = config.namespace.authorization_adapter
|
|
16
|
+
adapter_class = adapter_class.constantize if adapter_class.is_a?(String)
|
|
17
|
+
new(adapter_class.new(config, current_user))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(adapter)
|
|
21
|
+
@adapter = adapter
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def authorized?(action, subject)
|
|
25
|
+
@adapter.authorized?(action, subject)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def scope_collection(collection, action = READ)
|
|
29
|
+
@adapter.scope_collection(collection, action)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -53,9 +53,7 @@ module ActiveadminMcp
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def authorized?(config, record)
|
|
56
|
-
|
|
57
|
-
adapter_class = adapter_class.constantize if adapter_class.is_a?(String)
|
|
58
|
-
adapter_class.new(config, @current_user).authorized?(UPDATE, record)
|
|
56
|
+
Authorization.for(config, @current_user).authorized?(UPDATE, record)
|
|
59
57
|
end
|
|
60
58
|
|
|
61
59
|
# Resolves the fields we may write, accepting exactly what the admin form
|
|
@@ -44,12 +44,15 @@ module ActiveadminMcp
|
|
|
44
44
|
tools: [
|
|
45
45
|
{
|
|
46
46
|
name: "list_resources",
|
|
47
|
-
description: "List
|
|
47
|
+
description: "List the ActiveAdmin resources the authenticated user is authorized " \
|
|
48
|
+
"to read, with their attributes",
|
|
48
49
|
inputSchema: { type: "object", properties: {} },
|
|
49
50
|
},
|
|
50
51
|
{
|
|
51
52
|
name: "query",
|
|
52
|
-
description: "Query an ActiveAdmin resource using Ransack syntax"
|
|
53
|
+
description: "Query an ActiveAdmin resource using Ransack syntax. Respects ActiveAdmin " \
|
|
54
|
+
"authorization: the resource must be readable by the authenticated user, " \
|
|
55
|
+
"and results are scoped to the records they may access.",
|
|
53
56
|
inputSchema: {
|
|
54
57
|
type: "object",
|
|
55
58
|
properties: {
|
|
@@ -93,18 +96,21 @@ module ActiveadminMcp
|
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
def tool_list_resources
|
|
96
|
-
|
|
99
|
+
entries = ResourceRegistry.resources.select { |entry| authorized_to_read?(entry) }
|
|
100
|
+
{ resources: entries.map { |entry| ResourceRegistry.resource_info(entry) } }
|
|
97
101
|
end
|
|
98
102
|
|
|
99
103
|
def tool_query(args)
|
|
100
104
|
resource = ResourceRegistry.find(args["resource"])
|
|
101
105
|
return { error: "Resource not found: #{args['resource']}" } unless resource
|
|
106
|
+
return { error: "Not authorized to query #{resource[:name]}" } unless authorized_to_read?(resource)
|
|
102
107
|
|
|
103
108
|
limit = [args["limit"] || 25, 100].min
|
|
104
109
|
q = args["q"] || {}
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
relation = resource[:model].ransack(q).result
|
|
112
|
+
records = authorization(resource).scope_collection(relation, Authorization::READ).limit(limit)
|
|
113
|
+
{ resource: resource[:name], count: records.size, records: filter_sensitive(records.as_json) }
|
|
108
114
|
end
|
|
109
115
|
|
|
110
116
|
def tool_update(args)
|
|
@@ -119,6 +125,21 @@ module ActiveadminMcp
|
|
|
119
125
|
.call(id: args["id"], attributes: attributes)
|
|
120
126
|
end
|
|
121
127
|
|
|
128
|
+
def authorized_to_read?(resource)
|
|
129
|
+
authorization(resource).authorized?(Authorization::READ, resource[:model])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def authorization(resource)
|
|
133
|
+
Authorization.for(resource[:config], @current_user)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def filter_sensitive(records)
|
|
137
|
+
sensitive = ResourceRegistry.sensitive_attributes
|
|
138
|
+
Array(records).map do |record|
|
|
139
|
+
record.is_a?(Hash) ? record.except(*sensitive) : record
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
122
143
|
def success(id, result)
|
|
123
144
|
{ jsonrpc: "2.0", id: id, result: result }
|
|
124
145
|
end
|
|
@@ -4,14 +4,28 @@ module ActiveadminMcp
|
|
|
4
4
|
module ResourceRegistry
|
|
5
5
|
class << self
|
|
6
6
|
def all
|
|
7
|
-
|
|
7
|
+
resources.map { |entry| resource_info(entry) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def resources
|
|
11
|
+
discover.map { |r| entry(r) }
|
|
8
12
|
end
|
|
9
13
|
|
|
10
14
|
def find(name)
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
resources.find { |entry| entry[:name] == name }
|
|
16
|
+
end
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
def resource_info(entry)
|
|
19
|
+
klass = entry[:model]
|
|
20
|
+
{
|
|
21
|
+
name: klass.name,
|
|
22
|
+
table: klass.table_name,
|
|
23
|
+
attributes: klass.column_names - sensitive_attributes,
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sensitive_attributes
|
|
28
|
+
%w[encrypted_password password_digest reset_password_token api_key secret]
|
|
15
29
|
end
|
|
16
30
|
|
|
17
31
|
private
|
|
@@ -26,17 +40,8 @@ module ActiveadminMcp
|
|
|
26
40
|
end || []
|
|
27
41
|
end
|
|
28
42
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
name: klass.name,
|
|
33
|
-
table: klass.table_name,
|
|
34
|
-
attributes: klass.column_names - sensitive_attributes,
|
|
35
|
-
}
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def sensitive_attributes
|
|
39
|
-
%w[encrypted_password password_digest reset_password_token api_key secret]
|
|
43
|
+
def entry(resource)
|
|
44
|
+
{ name: resource.resource_class.name, model: resource.resource_class, config: resource }
|
|
40
45
|
end
|
|
41
46
|
end
|
|
42
47
|
end
|
data/lib/activeadmin_mcp.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "activeadmin_mcp/version"
|
|
4
4
|
require_relative "activeadmin_mcp/configuration"
|
|
5
|
+
require_relative "activeadmin_mcp/authorization"
|
|
5
6
|
require_relative "activeadmin_mcp/resource_registry"
|
|
6
7
|
require_relative "activeadmin_mcp/form_field_collector"
|
|
7
8
|
require_relative "activeadmin_mcp/record_updater"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeadmin_mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- harunkumars
|
|
@@ -95,6 +95,7 @@ files:
|
|
|
95
95
|
- app/models/activeadmin_mcp/api_token.rb
|
|
96
96
|
- config/routes.rb
|
|
97
97
|
- lib/activeadmin_mcp.rb
|
|
98
|
+
- lib/activeadmin_mcp/authorization.rb
|
|
98
99
|
- lib/activeadmin_mcp/configuration.rb
|
|
99
100
|
- lib/activeadmin_mcp/engine.rb
|
|
100
101
|
- lib/activeadmin_mcp/form_field_collector.rb
|