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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e336c562299eb5de7192243106f42dc18602589811fff5eb0282f12b605ea9b8
4
- data.tar.gz: 006f6aac09bceb97605517a9b1b7346a4aef6fc2ddc4c59ab48b36b658825078
3
+ metadata.gz: 5eb6b95afa3bf6d96a2ae33ced52621014f535f11542dcbee38b88638580e6a9
4
+ data.tar.gz: c87121826ee5e29805cfa9f3fd8faf1f5042698ec94d46c5fbd7bbc90f08b3f3
5
5
  SHA512:
6
- metadata.gz: 602940dbd9eb2a0337220672258f343aff84d0851eaec4b0199b38cb99f843adca48ee085ab201ed896ec29f260fe5e8ae6a8844b567990611e4c866d3abd05f
7
- data.tar.gz: 54e2309b725add92b02af4ebbaefdafd5cfa8c349890211c22f0bb391f00863764e89cf50f7e5f6ace5f293962f473ad75f5197f7813702b939cfc2dc95d5b9d
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 (CanCanCan, Pundit, etc.) as the authenticated MCP
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 every ActiveAdmin resource along with its attributes. |
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
- adapter_class = config.namespace.authorization_adapter
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 all ActiveAdmin resources with their attributes",
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
- { resources: ResourceRegistry.all }
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
- records = resource[:model].ransack(q).result.limit(limit)
107
- { resource: resource[:name], count: records.size, records: records.as_json }
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
- discover.map { |r| resource_info(r) }
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
- resource = discover.find { |r| r.resource_class.name == name }
12
- return unless resource
15
+ resources.find { |entry| entry[:name] == name }
16
+ end
13
17
 
14
- { name: resource.resource_class.name, model: resource.resource_class, config: resource }
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 resource_info(resource)
30
- klass = resource.resource_class
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveadminMcp
4
- VERSION = "0.0.2"
4
+ VERSION = "0.0.4"
5
5
  end
@@ -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.2
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