activeadmin_mcp 0.0.2 → 0.0.3
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 +10 -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: 1d97e042cb6e4af51f5939d4edd39a96f7193ece948a05a70dd8e18851f1bc85
|
|
4
|
+
data.tar.gz: 6b91b93d2d0511f3bea310f62044589eb35b90358547e942d379be214582d5e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6714a33bbef7072a22f067f422837e6ccf88ebd6a835157c8ffa8fff5487791ee2494fbcd1316ac4ca65c9767e53ac6e48a7c672e84846615fb77af29b507080
|
|
7
|
+
data.tar.gz: fb49f14f716cd9c5a5e0b9ca80c546adaaa7fcab15b8867965c7d823084f456580982270185c65861477c11840c219cbdf41fd676a597c2b39c4835ec7d0f5f4
|
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
|
|
@@ -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.3
|
|
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
|