activeadmin_mcp 0.0.1
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +276 -0
- data/app/controllers/activeadmin_mcp/mcp_controller.rb +53 -0
- data/app/models/activeadmin_mcp/api_token.rb +44 -0
- data/config/routes.rb +5 -0
- data/lib/activeadmin_mcp/configuration.rb +34 -0
- data/lib/activeadmin_mcp/engine.rb +20 -0
- data/lib/activeadmin_mcp/record_updater.rb +87 -0
- data/lib/activeadmin_mcp/request_handler.rb +130 -0
- data/lib/activeadmin_mcp/resource_registry.rb +43 -0
- data/lib/activeadmin_mcp/version.rb +5 -0
- data/lib/activeadmin_mcp.rb +22 -0
- data/lib/generators/activeadmin_mcp/install/install_generator.rb +89 -0
- data/lib/generators/activeadmin_mcp/install/templates/initializer.rb +26 -0
- data/lib/generators/activeadmin_mcp/install/templates/mcp_api_tokens.rb +59 -0
- data/lib/generators/activeadmin_mcp/install/templates/migration.rb.erb +18 -0
- metadata +133 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 99312ac0bef360e80a8d058b79adf5f0af182d3bedc19fdfbf4caa69964f7e44
|
|
4
|
+
data.tar.gz: bba982b59472e271c18214bc4674e60a3ae96729b78172163d54eed756b3997c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 861ea8e3d8eeff901f12f4fa99385db60fd0854a0eae22462cf314cce67820632033b8e55ebc13252bcaa3389f0b6aceda4cc712d5bceb4ba0c342ac10a5815d
|
|
7
|
+
data.tar.gz: 035b92456d5b3ca111c5e4715c8f495f52531ab053eb7974f1c1dda12de73d87f08ba34b40f226723d8a207bc98a89c929c4d6bb8ecdc9ee0873fdc6fbefb275
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 harunkumars
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# ActiveadminMcp
|
|
2
|
+
|
|
3
|
+
> **Status: Experimental / work in progress**
|
|
4
|
+
|
|
5
|
+
`activeadmin_mcp` turns the resources you have already registered with
|
|
6
|
+
[ActiveAdmin](https://activeadmin.info/) into a
|
|
7
|
+
[Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server, so AI
|
|
8
|
+
assistants such as Claude Code can list, query, and update your admin data —
|
|
9
|
+
while respecting the exact same forms, permitted parameters, and authorization
|
|
10
|
+
rules as your ActiveAdmin UI.
|
|
11
|
+
|
|
12
|
+
The server is a Rails engine mounted inside your application (by default at
|
|
13
|
+
`/mcp`) and speaks MCP over HTTP (JSON-RPC 2.0, protocol revision
|
|
14
|
+
`2025-06-18`).
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
- **Nothing new to describe.** The engine reads your existing ActiveAdmin
|
|
19
|
+
registrations, so the resources, attributes, and permitted fields it exposes
|
|
20
|
+
are the ones you have already configured.
|
|
21
|
+
- **Queries use Ransack.** The `query` tool passes its arguments straight to
|
|
22
|
+
[Ransack](https://activerecord-hackery.github.io/ransack/), the same search
|
|
23
|
+
library ActiveAdmin uses for filtering.
|
|
24
|
+
- **Writes go through ActiveAdmin.** The `update` tool only writes fields
|
|
25
|
+
allowed by the resource's `permit_params`, refuses resources that don't
|
|
26
|
+
register the `update` action, and runs every change through your
|
|
27
|
+
authorization adapter (CanCanCan, Pundit, etc.) as the authenticated MCP
|
|
28
|
+
user.
|
|
29
|
+
- **Authentication is optional but built in.** Enable Bearer-token auth and the
|
|
30
|
+
installer adds an "MCP Tokens" management page to your ActiveAdmin panel.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Ruby >= 3.0
|
|
35
|
+
- Rails >= 6.1
|
|
36
|
+
- ActiveAdmin >= 2.0
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Add the gem to your Gemfile:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
gem "activeadmin_mcp"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Install it and run the generator:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bundle install
|
|
50
|
+
rails generate activeadmin_mcp:install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The MCP server is mounted at `/mcp` automatically. That's all you need for a
|
|
54
|
+
read/query setup without authentication.
|
|
55
|
+
|
|
56
|
+
## Available tools
|
|
57
|
+
|
|
58
|
+
| Tool | Description |
|
|
59
|
+
|------|-------------|
|
|
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). |
|
|
62
|
+
| `update` | Update an existing record, honouring ActiveAdmin's permitted params and authorization. |
|
|
63
|
+
|
|
64
|
+
### Query examples
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Query users whose email contains "example.com"
|
|
68
|
+
→ query(resource: "User", q: { email_cont: "example.com" })
|
|
69
|
+
|
|
70
|
+
Find active posts created since the start of the month
|
|
71
|
+
→ query(resource: "Post", q: { status_eq: "active", created_at_gt: "2026-08-01" })
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Updating records
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Update a user's name
|
|
78
|
+
→ update(resource: "User", id: 42, attributes: { name: "New name" })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The `update` tool applies the same rules as the ActiveAdmin UI:
|
|
82
|
+
|
|
83
|
+
- **Editable resources only** — resources registered without the `update`
|
|
84
|
+
action (e.g. `actions :index, :show`) are refused.
|
|
85
|
+
- **Authorization** — the change runs through the resource namespace's
|
|
86
|
+
authorization adapter for the authenticated MCP user, so it can only update
|
|
87
|
+
what that user is allowed to update in admin.
|
|
88
|
+
- **Permitted fields only** — attributes are filtered through the resource's
|
|
89
|
+
`permit_params`; fields the admin form doesn't accept are silently dropped.
|
|
90
|
+
|
|
91
|
+
## Connecting a client
|
|
92
|
+
|
|
93
|
+
`activeadmin_mcp` has been tested with **Claude Code** (Anthropic) over the
|
|
94
|
+
HTTP transport.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
claude mcp add --transport http my-app http://localhost:3000/mcp/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or add it to your `.mcp.json`:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"my-app": {
|
|
106
|
+
"type": "http",
|
|
107
|
+
"url": "http://localhost:3000/mcp/"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Authentication
|
|
114
|
+
|
|
115
|
+
To protect the MCP endpoint with API-token authentication, run the installer
|
|
116
|
+
with the `devise_token` strategy and migrate:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
rails generate activeadmin_mcp:install --auth devise_token
|
|
120
|
+
rails db:migrate
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This will:
|
|
124
|
+
|
|
125
|
+
- Create the `mcp_api_tokens` table.
|
|
126
|
+
- Add an "MCP Tokens" page to your ActiveAdmin panel (`app/admin/` by default).
|
|
127
|
+
- Enable token authentication in the initializer.
|
|
128
|
+
|
|
129
|
+
### Generator options
|
|
130
|
+
|
|
131
|
+
| Option | Default | Description |
|
|
132
|
+
|--------|---------|-------------|
|
|
133
|
+
| `--auth` | none | Authentication method to use (e.g. `devise_token`). |
|
|
134
|
+
| `--admin-path` | `app/admin` | Directory for the ActiveAdmin page file. |
|
|
135
|
+
|
|
136
|
+
Example with a custom admin path:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
rails generate activeadmin_mcp:install --auth devise_token --admin-path app/admin/mcp
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Managing tokens
|
|
143
|
+
|
|
144
|
+
1. Log in to your ActiveAdmin panel (`/admin`).
|
|
145
|
+
2. Navigate to **MCP Tokens** (or **Settings > MCP Tokens** if you set a
|
|
146
|
+
`menu_parent`).
|
|
147
|
+
3. Create a token and copy it — it is only shown once.
|
|
148
|
+
|
|
149
|
+
### Connecting with a token
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
claude mcp add --transport http \
|
|
153
|
+
--header 'Authorization: Bearer YOUR_TOKEN' \
|
|
154
|
+
my-app http://localhost:3000/mcp/
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Or in `.mcp.json`:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"mcpServers": {
|
|
162
|
+
"my-app": {
|
|
163
|
+
"type": "http",
|
|
164
|
+
"url": "http://localhost:3000/mcp/",
|
|
165
|
+
"headers": {
|
|
166
|
+
"Authorization": "Bearer YOUR_TOKEN"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Custom auth header
|
|
174
|
+
|
|
175
|
+
If your application sits behind a reverse proxy that strips the standard
|
|
176
|
+
`Authorization` header (e.g. AWS Verified Access), configure a custom header
|
|
177
|
+
name and pass the token through it instead:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
ActiveadminMcp.configure do |config|
|
|
181
|
+
config.authentication_method = :devise_token
|
|
182
|
+
config.auth_header_name = "X-MCP-Authorization"
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"mcpServers": {
|
|
189
|
+
"my-app": {
|
|
190
|
+
"type": "http",
|
|
191
|
+
"url": "https://admin.example.com/admin/mcp/",
|
|
192
|
+
"headers": {
|
|
193
|
+
"X-MCP-Authorization": "Bearer YOUR_TOKEN"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Configuration
|
|
201
|
+
|
|
202
|
+
The generator writes an initializer to
|
|
203
|
+
`config/initializers/activeadmin_mcp.rb`:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
ActiveadminMcp.configure do |config|
|
|
207
|
+
config.authentication_method = :devise_token
|
|
208
|
+
config.user_class = "User" # your Devise model class
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
| Option | Default | Description |
|
|
213
|
+
|--------|---------|-------------|
|
|
214
|
+
| `authentication_method` | `nil` | Set to `:devise_token` to enable Bearer-token auth. |
|
|
215
|
+
| `user_class` | `"User"` | The Devise model class name. |
|
|
216
|
+
| `current_user_method` | `:current_admin_user` | Controller method returning the current user. |
|
|
217
|
+
| `menu_parent` | `nil` | Parent menu for the MCP Tokens page (e.g. `"Settings"`). |
|
|
218
|
+
| `mount_path` | `"/mcp"` | Path where the MCP server is mounted. |
|
|
219
|
+
| `mount_strategy` | `:prepend` | Route mounting strategy: `:prepend`, `:append`, or `:none`. |
|
|
220
|
+
| `auth_header_name` | `"Authorization"` | HTTP header to read the Bearer token from. |
|
|
221
|
+
|
|
222
|
+
### Route mounting
|
|
223
|
+
|
|
224
|
+
By default the engine prepends its route to the top of your application's route
|
|
225
|
+
table. This suits most setups, but can cause problems when your admin routes
|
|
226
|
+
use constraints (e.g. hostname-based routing), because a prepended mount sits
|
|
227
|
+
outside any constraint blocks.
|
|
228
|
+
|
|
229
|
+
| Strategy | Behaviour |
|
|
230
|
+
|----------|-----------|
|
|
231
|
+
| `:prepend` | **(default)** Mounts at the top of the route table via `routes.prepend`. |
|
|
232
|
+
| `:append` | Mounts at the bottom of the route table via `routes.append`. |
|
|
233
|
+
| `:none` | Skips automatic mounting — you mount the engine yourself. |
|
|
234
|
+
|
|
235
|
+
To mount inside a constraint block, set `mount_strategy` to `:none` and mount
|
|
236
|
+
the engine manually:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
# config/initializers/activeadmin_mcp.rb
|
|
240
|
+
ActiveadminMcp.configure do |config|
|
|
241
|
+
config.mount_path = "/admin/mcp"
|
|
242
|
+
config.mount_strategy = :none
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
# config/routes.rb (or a drawn route file)
|
|
248
|
+
constraints AdminConstraint.new do
|
|
249
|
+
ActiveAdmin.routes(self)
|
|
250
|
+
mount ActiveadminMcp::Engine => ActiveadminMcp.config.mount_path
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
After checking out the repo, install dependencies and run the test suite:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
bundle install
|
|
260
|
+
bundle exec rspec
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Contributing
|
|
264
|
+
|
|
265
|
+
Bug reports and pull requests are welcome on GitHub.
|
|
266
|
+
|
|
267
|
+
## Credits
|
|
268
|
+
|
|
269
|
+
This project was forked from
|
|
270
|
+
[betacraft/active_admin_mcp](https://github.com/betacraft/active_admin_mcp),
|
|
271
|
+
originally created by [harunkumars](https://github.com/harunkumars), and has
|
|
272
|
+
been extended from there.
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
Released under the [MIT License](LICENSE.txt).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveadminMcp
|
|
4
|
+
class McpController < ActionController::API
|
|
5
|
+
before_action :authenticate_mcp_token!
|
|
6
|
+
|
|
7
|
+
attr_reader :current_mcp_user
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
request_body = JSON.parse(request.body.read)
|
|
11
|
+
response = RequestHandler.new(current_user: current_mcp_user).handle(request_body)
|
|
12
|
+
|
|
13
|
+
response ? render(json: response) : head(:no_content)
|
|
14
|
+
rescue JSON::ParserError => e
|
|
15
|
+
render json: { jsonrpc: "2.0", error: { code: -32_700, message: e.message } }, status: :bad_request
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def authenticate_mcp_token!
|
|
21
|
+
return unless ActiveadminMcp.config.authentication_enabled?
|
|
22
|
+
|
|
23
|
+
token = extract_bearer_token
|
|
24
|
+
unless token
|
|
25
|
+
render json: jsonrpc_error(-32_000, "Unauthorized"), status: :unauthorized
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
api_token = ApiToken.find_by_raw_token(token)
|
|
30
|
+
unless api_token
|
|
31
|
+
render json: jsonrpc_error(-32_000, "Unauthorized"), status: :unauthorized
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@current_mcp_user = api_token.user
|
|
36
|
+
api_token.touch_last_used!
|
|
37
|
+
rescue ActiveRecord::StatementInvalid
|
|
38
|
+
render json: jsonrpc_error(-32_000, "Authentication not configured — run migrations"),
|
|
39
|
+
status: :internal_server_error
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def extract_bearer_token
|
|
43
|
+
header = request.headers[ActiveadminMcp.config.auth_header_name]
|
|
44
|
+
return nil unless header&.start_with?("Bearer ")
|
|
45
|
+
|
|
46
|
+
header.delete_prefix("Bearer ")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def jsonrpc_error(code, message)
|
|
50
|
+
{ jsonrpc: "2.0", id: nil, error: { code: code, message: message } }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module ActiveadminMcp
|
|
7
|
+
class ApiToken < ActiveRecord::Base
|
|
8
|
+
self.table_name = "mcp_api_tokens"
|
|
9
|
+
|
|
10
|
+
belongs_to :user, class_name: ActiveadminMcp.config.user_class
|
|
11
|
+
|
|
12
|
+
attr_accessor :raw_token
|
|
13
|
+
|
|
14
|
+
validates :token_digest, presence: true, uniqueness: true
|
|
15
|
+
validates :user_id, presence: true
|
|
16
|
+
|
|
17
|
+
before_validation :generate_token, on: :create
|
|
18
|
+
|
|
19
|
+
LAST_USED_THROTTLE = 5.minutes
|
|
20
|
+
|
|
21
|
+
def self.find_by_raw_token(raw_token)
|
|
22
|
+
return nil if raw_token.blank?
|
|
23
|
+
|
|
24
|
+
find_by(token_digest: digest(raw_token))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.digest(raw_token)
|
|
28
|
+
Digest::SHA256.hexdigest(raw_token)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def touch_last_used!
|
|
32
|
+
return if last_used_at.present? && last_used_at > LAST_USED_THROTTLE.ago
|
|
33
|
+
|
|
34
|
+
update_column(:last_used_at, Time.current)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def generate_token
|
|
40
|
+
self.raw_token = "aamcp_#{SecureRandom.hex(32)}"
|
|
41
|
+
self.token_digest = self.class.digest(raw_token)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveadminMcp
|
|
4
|
+
class Configuration
|
|
5
|
+
MOUNT_STRATEGIES = %i[prepend append none].freeze
|
|
6
|
+
|
|
7
|
+
attr_accessor :authentication_method, :user_class, :current_user_method, :menu_parent, :mount_path,
|
|
8
|
+
:auth_header_name
|
|
9
|
+
|
|
10
|
+
attr_reader :mount_strategy
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@authentication_method = nil
|
|
14
|
+
@user_class = "User"
|
|
15
|
+
@current_user_method = :current_admin_user
|
|
16
|
+
@menu_parent = nil
|
|
17
|
+
@mount_path = "/mcp"
|
|
18
|
+
@mount_strategy = :prepend
|
|
19
|
+
@auth_header_name = "Authorization"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def mount_strategy=(strategy)
|
|
23
|
+
unless MOUNT_STRATEGIES.include?(strategy)
|
|
24
|
+
raise ArgumentError, "Invalid mount strategy: #{strategy}. Must be one of: #{MOUNT_STRATEGIES.join(', ')}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@mount_strategy = strategy
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def authentication_enabled?
|
|
31
|
+
authentication_method == :devise_token
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveadminMcp
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace ActiveadminMcp
|
|
6
|
+
|
|
7
|
+
initializer "activeadmin_mcp.mount" do |app|
|
|
8
|
+
case ActiveadminMcp.config.mount_strategy
|
|
9
|
+
when :prepend
|
|
10
|
+
app.routes.prepend do
|
|
11
|
+
mount ActiveadminMcp::Engine => ActiveadminMcp.config.mount_path
|
|
12
|
+
end
|
|
13
|
+
when :append
|
|
14
|
+
app.routes.append do
|
|
15
|
+
mount ActiveadminMcp::Engine => ActiveadminMcp.config.mount_path
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveadminMcp
|
|
4
|
+
# Updates a single ActiveAdmin-managed record, enforcing the same three gates
|
|
5
|
+
# the admin UI would: the resource must expose the update action, the current
|
|
6
|
+
# user must be authorized, and only fields the admin form permits are written.
|
|
7
|
+
class RecordUpdater
|
|
8
|
+
UPDATE = :update
|
|
9
|
+
|
|
10
|
+
# Raised internally when the resource's permitted params cannot be resolved.
|
|
11
|
+
class PermitError < StandardError; end
|
|
12
|
+
|
|
13
|
+
def initialize(resource:, current_user:)
|
|
14
|
+
@resource = resource
|
|
15
|
+
@current_user = current_user
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(id:, attributes:)
|
|
19
|
+
config = @resource[:config]
|
|
20
|
+
|
|
21
|
+
return error("Resource is not editable: #{@resource[:name]}") unless editable?(config)
|
|
22
|
+
|
|
23
|
+
record = @resource[:model].find_by(id: id)
|
|
24
|
+
return error("Record not found: #{@resource[:name]}##{id}") unless record
|
|
25
|
+
|
|
26
|
+
unless authorized?(config, record)
|
|
27
|
+
return error("Not authorized to update #{@resource[:name]}##{id}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
permitted = permitted_attributes(config, attributes)
|
|
32
|
+
rescue PermitError => e
|
|
33
|
+
return error(e.message)
|
|
34
|
+
end
|
|
35
|
+
return error("No permitted attributes to update") if permitted.empty?
|
|
36
|
+
|
|
37
|
+
if record.update(permitted)
|
|
38
|
+
{
|
|
39
|
+
resource: @resource[:name],
|
|
40
|
+
id: record.id,
|
|
41
|
+
updated: permitted.keys,
|
|
42
|
+
record: record.as_json,
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
error("Validation failed", details: record.errors.full_messages)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def editable?(config)
|
|
52
|
+
config.defined_actions.include?(UPDATE)
|
|
53
|
+
end
|
|
54
|
+
|
|
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)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Runs the incoming attributes through the resource controller's own
|
|
62
|
+
# permitted_params (compiled from `permit_params`), so we accept exactly what
|
|
63
|
+
# the admin form accepts. Fails closed if that cannot be resolved.
|
|
64
|
+
def permitted_attributes(config, attributes)
|
|
65
|
+
param_key = config.param_key.to_sym
|
|
66
|
+
controller = config.controller.new
|
|
67
|
+
|
|
68
|
+
unless controller.respond_to?(:permitted_params, true)
|
|
69
|
+
raise PermitError, "Resource does not declare permit_params: #{@resource[:name]}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
controller.params = ActionController::Parameters.new(param_key => attributes)
|
|
73
|
+
permitted = controller.send(:permitted_params)[param_key]
|
|
74
|
+
permitted ? permitted.to_h.symbolize_keys : {}
|
|
75
|
+
rescue PermitError
|
|
76
|
+
raise
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
raise PermitError, "Could not determine permitted attributes: #{e.message}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def error(message, details: nil)
|
|
82
|
+
result = { error: message }
|
|
83
|
+
result[:details] = details if details
|
|
84
|
+
result
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveadminMcp
|
|
4
|
+
class RequestHandler
|
|
5
|
+
PROTOCOL_VERSION = "2025-06-18"
|
|
6
|
+
|
|
7
|
+
def initialize(current_user: nil)
|
|
8
|
+
@current_user = current_user
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def handle(request)
|
|
12
|
+
id = request["id"]
|
|
13
|
+
method = request["method"]
|
|
14
|
+
params = request["params"] || {}
|
|
15
|
+
|
|
16
|
+
case method
|
|
17
|
+
when "initialize"
|
|
18
|
+
success(id, initialize_result)
|
|
19
|
+
when "notifications/initialized"
|
|
20
|
+
nil
|
|
21
|
+
when "tools/list"
|
|
22
|
+
success(id, tools_list)
|
|
23
|
+
when "tools/call"
|
|
24
|
+
success(id, call_tool(params))
|
|
25
|
+
when "ping"
|
|
26
|
+
success(id, {})
|
|
27
|
+
else
|
|
28
|
+
error(id, -32_601, "Method not found: #{method}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def initialize_result
|
|
35
|
+
{
|
|
36
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
37
|
+
serverInfo: { name: "activeadmin-mcp", version: ActiveadminMcp::VERSION },
|
|
38
|
+
capabilities: { tools: {} },
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def tools_list
|
|
43
|
+
{
|
|
44
|
+
tools: [
|
|
45
|
+
{
|
|
46
|
+
name: "list_resources",
|
|
47
|
+
description: "List all ActiveAdmin resources with their attributes",
|
|
48
|
+
inputSchema: { type: "object", properties: {} },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "query",
|
|
52
|
+
description: "Query an ActiveAdmin resource using Ransack syntax",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
resource: { type: "string", description: "Resource name (e.g., 'User', 'Post')" },
|
|
57
|
+
q: { type: "object", description: "Ransack query (e.g., {name_cont: 'john'})" },
|
|
58
|
+
limit: { type: "integer", description: "Max records (default: 25)" },
|
|
59
|
+
},
|
|
60
|
+
required: ["resource"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "update",
|
|
65
|
+
description: "Update an existing record. Only fields the resource's ActiveAdmin " \
|
|
66
|
+
"form permits are written, and the update respects ActiveAdmin authorization.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
resource: { type: "string", description: "Resource name (e.g., 'User', 'Post')" },
|
|
71
|
+
id: { type: ["integer", "string"], description: "Primary key of the record to update" },
|
|
72
|
+
attributes: { type: "object", description: "Attributes to update (e.g., {name: 'New name'})" },
|
|
73
|
+
},
|
|
74
|
+
required: %w[resource id attributes],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def call_tool(params)
|
|
82
|
+
name = params["name"]
|
|
83
|
+
args = params["arguments"] || {}
|
|
84
|
+
|
|
85
|
+
result = case name
|
|
86
|
+
when "list_resources" then tool_list_resources
|
|
87
|
+
when "query" then tool_query(args)
|
|
88
|
+
when "update" then tool_update(args)
|
|
89
|
+
else { error: "Unknown tool: #{name}" }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
{ content: [{ type: "text", text: JSON.pretty_generate(result) }] }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def tool_list_resources
|
|
96
|
+
{ resources: ResourceRegistry.all }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def tool_query(args)
|
|
100
|
+
resource = ResourceRegistry.find(args["resource"])
|
|
101
|
+
return { error: "Resource not found: #{args['resource']}" } unless resource
|
|
102
|
+
|
|
103
|
+
limit = [args["limit"] || 25, 100].min
|
|
104
|
+
q = args["q"] || {}
|
|
105
|
+
|
|
106
|
+
records = resource[:model].ransack(q).result.limit(limit)
|
|
107
|
+
{ resource: resource[:name], count: records.size, records: records.as_json }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def tool_update(args)
|
|
111
|
+
resource = ResourceRegistry.find(args["resource"])
|
|
112
|
+
return { error: "Resource not found: #{args['resource']}" } unless resource
|
|
113
|
+
return { error: "id is required" } if args["id"].nil?
|
|
114
|
+
|
|
115
|
+
attributes = args["attributes"] || {}
|
|
116
|
+
return { error: "attributes are required" } if attributes.empty?
|
|
117
|
+
|
|
118
|
+
RecordUpdater.new(resource: resource, current_user: @current_user)
|
|
119
|
+
.call(id: args["id"], attributes: attributes)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def success(id, result)
|
|
123
|
+
{ jsonrpc: "2.0", id: id, result: result }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def error(id, code, message)
|
|
127
|
+
{ jsonrpc: "2.0", id: id, error: { code: code, message: message } }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveadminMcp
|
|
4
|
+
module ResourceRegistry
|
|
5
|
+
class << self
|
|
6
|
+
def all
|
|
7
|
+
discover.map { |r| resource_info(r) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def find(name)
|
|
11
|
+
resource = discover.find { |r| r.resource_class.name == name }
|
|
12
|
+
return unless resource
|
|
13
|
+
|
|
14
|
+
{ name: resource.resource_class.name, model: resource.resource_class, config: resource }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def discover
|
|
20
|
+
return [] unless defined?(ActiveAdmin)
|
|
21
|
+
|
|
22
|
+
ActiveAdmin.application.namespaces[:admin]&.resources&.select do |r|
|
|
23
|
+
r.respond_to?(:resource_class) &&
|
|
24
|
+
r.resource_class.respond_to?(:ransack) &&
|
|
25
|
+
r.resource_class.table_exists?
|
|
26
|
+
end || []
|
|
27
|
+
end
|
|
28
|
+
|
|
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]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "activeadmin_mcp/version"
|
|
4
|
+
require_relative "activeadmin_mcp/configuration"
|
|
5
|
+
require_relative "activeadmin_mcp/resource_registry"
|
|
6
|
+
require_relative "activeadmin_mcp/record_updater"
|
|
7
|
+
require_relative "activeadmin_mcp/request_handler"
|
|
8
|
+
require_relative "activeadmin_mcp/engine"
|
|
9
|
+
|
|
10
|
+
module ActiveadminMcp
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def config
|
|
15
|
+
@config ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield config
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module ActiveadminMcp
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
class_option :auth, type: :string, default: nil,
|
|
14
|
+
desc: "Authentication method (e.g., devise_token)"
|
|
15
|
+
class_option :admin_path, type: :string, default: "app/admin",
|
|
16
|
+
desc: "Path for ActiveAdmin page file"
|
|
17
|
+
|
|
18
|
+
def copy_initializer
|
|
19
|
+
template "initializer.rb", "config/initializers/activeadmin_mcp.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def copy_migration
|
|
23
|
+
return unless auth_method
|
|
24
|
+
|
|
25
|
+
migration_template "migration.rb.erb", "db/migrate/create_mcp_api_tokens.rb"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def copy_admin_page
|
|
29
|
+
return unless auth_method
|
|
30
|
+
|
|
31
|
+
copy_file "mcp_api_tokens.rb", File.join(options[:admin_path], "mcp_api_tokens.rb")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def set_auth_config
|
|
35
|
+
return unless auth_method
|
|
36
|
+
|
|
37
|
+
gsub_file "config/initializers/activeadmin_mcp.rb",
|
|
38
|
+
"# config.authentication_method = :devise_token",
|
|
39
|
+
"config.authentication_method = :#{auth_method}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def show_instructions
|
|
43
|
+
say ""
|
|
44
|
+
say "=" * 60, :green
|
|
45
|
+
say " ActiveadminMcp installed!", :green
|
|
46
|
+
say "=" * 60, :green
|
|
47
|
+
say ""
|
|
48
|
+
say "Your MCP server is available at: /mcp"
|
|
49
|
+
say ""
|
|
50
|
+
|
|
51
|
+
if auth_method
|
|
52
|
+
say "Authentication (#{auth_method}) enabled! Next steps:", :yellow
|
|
53
|
+
say ""
|
|
54
|
+
say " 1. Run migrations:"
|
|
55
|
+
say " rails db:migrate"
|
|
56
|
+
say ""
|
|
57
|
+
say " 2. Create tokens via ActiveAdmin:"
|
|
58
|
+
say " Log in to /admin and visit 'MCP Tokens' under Settings"
|
|
59
|
+
say ""
|
|
60
|
+
say " 3. Connect Claude Code with your token:"
|
|
61
|
+
say " claude mcp add --transport http \\", :cyan
|
|
62
|
+
say " --header 'Authorization: Bearer YOUR_TOKEN' \\", :cyan
|
|
63
|
+
say " #{app_name} http://localhost:3000/mcp/", :cyan
|
|
64
|
+
else
|
|
65
|
+
say "Connect Claude Code:"
|
|
66
|
+
say ""
|
|
67
|
+
say " claude mcp add --transport http #{app_name} http://localhost:3000/mcp/"
|
|
68
|
+
say ""
|
|
69
|
+
say "To add authentication later:"
|
|
70
|
+
say " rails generate activeadmin_mcp:install --auth devise_token"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
say ""
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def auth_method
|
|
79
|
+
options[:auth]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def app_name
|
|
83
|
+
Rails.application.class.module_parent_name.underscore.dasherize
|
|
84
|
+
rescue StandardError
|
|
85
|
+
"my-app"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ActiveadminMcp.configure do |config|
|
|
4
|
+
# Uncomment to enable API token authentication.
|
|
5
|
+
# Requires running the auth migration first:
|
|
6
|
+
# rails generate activeadmin_mcp:install --auth
|
|
7
|
+
#
|
|
8
|
+
# config.authentication_method = :devise_token
|
|
9
|
+
|
|
10
|
+
# The Devise model class used for authentication.
|
|
11
|
+
# config.user_class = "User"
|
|
12
|
+
|
|
13
|
+
# The controller method that returns the current user.
|
|
14
|
+
# config.current_user_method = :current_admin_user
|
|
15
|
+
|
|
16
|
+
# Parent menu for the MCP Tokens page in ActiveAdmin.
|
|
17
|
+
# config.menu_parent = "Settings"
|
|
18
|
+
|
|
19
|
+
# Path where the MCP server is mounted.
|
|
20
|
+
# config.mount_path = "/mcp"
|
|
21
|
+
|
|
22
|
+
# HTTP header used to read the Bearer token from.
|
|
23
|
+
# Useful when a reverse proxy (e.g. AWS Verified Access) strips the
|
|
24
|
+
# standard Authorization header.
|
|
25
|
+
# config.auth_header_name = "Authorization"
|
|
26
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ActiveAdmin.register_page "MCP API Tokens" do
|
|
4
|
+
menu label: "MCP Tokens", parent: ActiveadminMcp.config.menu_parent, priority: 100
|
|
5
|
+
|
|
6
|
+
content do
|
|
7
|
+
@tokens = ActiveadminMcp::ApiToken.where(user: send(ActiveadminMcp.config.current_user_method)).order(created_at: :desc)
|
|
8
|
+
|
|
9
|
+
if flash[:mcp_raw_token]
|
|
10
|
+
panel "New Token Created", class: "mcp-token-created" do
|
|
11
|
+
para "Copy this token now — it will not be shown again:"
|
|
12
|
+
pre flash[:mcp_raw_token], class: "mcp-raw-token"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
panel "Your MCP API Tokens" do
|
|
17
|
+
table_for @tokens do
|
|
18
|
+
column :name
|
|
19
|
+
column(:created_at) { |t| l(t.created_at, format: :long) }
|
|
20
|
+
column(:last_used_at) { |t| t.last_used_at ? l(t.last_used_at, format: :long) : "Never" }
|
|
21
|
+
column "Actions" do |token|
|
|
22
|
+
link_to "Revoke", admin_mcp_api_tokens_destroy_path(token_id: token.id),
|
|
23
|
+
method: :delete,
|
|
24
|
+
data: { confirm: "Revoke token '#{token.name}'?" },
|
|
25
|
+
class: "button small"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if @tokens.empty?
|
|
30
|
+
para "No tokens yet. Create one to authenticate MCP clients."
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
panel "Create New Token" do
|
|
35
|
+
form action: admin_mcp_api_tokens_create_path, method: :post do
|
|
36
|
+
input type: :hidden, name: :authenticity_token, value: form_authenticity_token
|
|
37
|
+
label "Token Name", for: :mcp_token_name
|
|
38
|
+
input type: :text, name: "mcp_token[name]", id: :mcp_token_name, placeholder: "e.g., Claude Code laptop"
|
|
39
|
+
input type: :submit, value: "Generate Token"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
page_action :create, method: :post do
|
|
45
|
+
token = ActiveadminMcp::ApiToken.create!(
|
|
46
|
+
user: send(ActiveadminMcp.config.current_user_method),
|
|
47
|
+
name: params[:mcp_token][:name].presence || "Unnamed token"
|
|
48
|
+
)
|
|
49
|
+
flash[:mcp_raw_token] = token.raw_token
|
|
50
|
+
redirect_to admin_mcp_api_tokens_path()
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
page_action :destroy, method: :delete do
|
|
54
|
+
token = ActiveadminMcp::ApiToken.where(user: send(ActiveadminMcp.config.current_user_method)).find(params[:token_id])
|
|
55
|
+
token.destroy!
|
|
56
|
+
flash[:notice] = "Token revoked."
|
|
57
|
+
redirect_to admin_mcp_api_tokens_path()
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateMcpApiTokens < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
|
+
def change
|
|
5
|
+
return if table_exists?(:mcp_api_tokens)
|
|
6
|
+
|
|
7
|
+
create_table :mcp_api_tokens do |t|
|
|
8
|
+
t.references :user, null: false
|
|
9
|
+
t.string :token_digest, null: false
|
|
10
|
+
t.string :name
|
|
11
|
+
t.datetime :last_used_at
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :mcp_api_tokens, :token_digest, unique: true
|
|
17
|
+
end
|
|
18
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activeadmin_mcp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- harunkumars
|
|
8
|
+
- OLIOEX
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activeadmin
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: sqlite3
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.4'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.4'
|
|
83
|
+
description: Expose your ActiveAdmin resources to AI assistants via the Model Context
|
|
84
|
+
Protocol (MCP).
|
|
85
|
+
email:
|
|
86
|
+
- harun@betacraft.io
|
|
87
|
+
- lloyd@olioex.com
|
|
88
|
+
executables: []
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- LICENSE.txt
|
|
93
|
+
- README.md
|
|
94
|
+
- app/controllers/activeadmin_mcp/mcp_controller.rb
|
|
95
|
+
- app/models/activeadmin_mcp/api_token.rb
|
|
96
|
+
- config/routes.rb
|
|
97
|
+
- lib/activeadmin_mcp.rb
|
|
98
|
+
- lib/activeadmin_mcp/configuration.rb
|
|
99
|
+
- lib/activeadmin_mcp/engine.rb
|
|
100
|
+
- lib/activeadmin_mcp/record_updater.rb
|
|
101
|
+
- lib/activeadmin_mcp/request_handler.rb
|
|
102
|
+
- lib/activeadmin_mcp/resource_registry.rb
|
|
103
|
+
- lib/activeadmin_mcp/version.rb
|
|
104
|
+
- lib/generators/activeadmin_mcp/install/install_generator.rb
|
|
105
|
+
- lib/generators/activeadmin_mcp/install/templates/initializer.rb
|
|
106
|
+
- lib/generators/activeadmin_mcp/install/templates/mcp_api_tokens.rb
|
|
107
|
+
- lib/generators/activeadmin_mcp/install/templates/migration.rb.erb
|
|
108
|
+
homepage: https://github.com/OLIOEX/activeadmin_mcp
|
|
109
|
+
licenses:
|
|
110
|
+
- MIT
|
|
111
|
+
metadata:
|
|
112
|
+
homepage_uri: https://github.com/OLIOEX/activeadmin_mcp
|
|
113
|
+
source_code_uri: https://github.com/OLIOEX/activeadmin_mcp
|
|
114
|
+
changelog_uri: https://github.com/OLIOEX/activeadmin_mcp/blob/main/CHANGELOG.md
|
|
115
|
+
rubygems_mfa_required: 'true'
|
|
116
|
+
rdoc_options: []
|
|
117
|
+
require_paths:
|
|
118
|
+
- lib
|
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: 3.0.0
|
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - ">="
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '0'
|
|
129
|
+
requirements: []
|
|
130
|
+
rubygems_version: 3.6.9
|
|
131
|
+
specification_version: 4
|
|
132
|
+
summary: MCP server for Rails apps with ActiveAdmin
|
|
133
|
+
test_files: []
|