administrate-mcp 0.4.0 → 0.5.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/CHANGELOG.md +28 -1
- data/app/lib/administrate/mcp/action_result.rb +36 -0
- data/app/lib/administrate/mcp/actions/result_data.rb +96 -0
- data/app/lib/administrate/mcp/actions.rb +15 -6
- data/app/lib/administrate/mcp/base_tool.rb +9 -4
- data/docs/configuration.md +3 -0
- data/docs/dashboards.md +56 -1
- data/lib/administrate/mcp/configuration.rb +2 -0
- data/lib/administrate/mcp/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e0b5788d3dd59f527d55db5f4e6e4d748c23f1c6d4f247c2afaa660e4dce89e
|
|
4
|
+
data.tar.gz: 932950d82e3472408f440cdb2a736b8107978ca8016c55e625e1221e21e99ddc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41f1ca74dbe3ed51314f8ec39ea8f38d99ba1831dd725e4e19ca17e8ef940b31d359f7651526a8fe1b8dfb53bcfb74ed2cba615d45e48a8cba230cb3b463711d
|
|
7
|
+
data.tar.gz: e1575180b4778ba2e68addc80c8c90441045436588281fc413d66a6abe43554be3eccc39d6a3965f7a927949f4ae911019890bb0e931bb31cb5a7ee0c6c2f767
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,32 @@ minor release may change behaviour a host depends on; the entry says so when it
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.5.0] - 2026-10-22
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- An `mcp_action` block can send data back to the client. It returns
|
|
16
|
+
`Administrate::MCP::ActionResult.success(record:, data:)`, or any result object answering
|
|
17
|
+
`mcp_data`, and the answer gains a `data` key next to `status`, `model`, `id` and `action`, which
|
|
18
|
+
are unchanged. The record, optional, goes through the same checks as a record returned directly,
|
|
19
|
+
so an unsaved or invalid one is still an error and a collection action still answers its `id`.
|
|
20
|
+
`ActionResult.failure(message, data:)` is an error whose first content block is the message, as
|
|
21
|
+
before; when there is data, a second block carries `{ "error": message, "data": … }` as JSON and
|
|
22
|
+
the result's `structuredContent` carries the same object. The data must be a Hash of plain JSON
|
|
23
|
+
values nested at most 32 levels deep, with valid UTF-8 strings; the engine encodes a copy
|
|
24
|
+
built from those values, never the host's objects, and its JSON may be at most
|
|
25
|
+
`config.action_data_max_bytes` (new, 32 KB by default, `nil` for no limit). Anything else is
|
|
26
|
+
answered as an error naming the offending path and saying the action ran. A plain Hash returned
|
|
27
|
+
by a block is still not sent back. See
|
|
28
|
+
[Returning data from an action](docs/dashboards.md#returning-data-from-an-action).
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- A successful `mcp_action` call now also returns its answer as the result's `structuredContent`,
|
|
33
|
+
the same object as the JSON in its text content, which is unchanged.
|
|
34
|
+
- A tool error may now carry a second content block. Clients that read only the first block still
|
|
35
|
+
get the message on its own; only failures that return data add the second block.
|
|
36
|
+
|
|
11
37
|
## [0.4.0] - 2026-10-21
|
|
12
38
|
|
|
13
39
|
### Added
|
|
@@ -177,7 +203,8 @@ minor release may change behaviour a host depends on; the entry says so when it
|
|
|
177
203
|
took the gem from git before 0.1.0 must reinstall its migrations rather than migrate
|
|
178
204
|
incrementally.
|
|
179
205
|
|
|
180
|
-
[Unreleased]: https://github.com/sorare/administrate-mcp/compare/v0.
|
|
206
|
+
[Unreleased]: https://github.com/sorare/administrate-mcp/compare/v0.5.0...HEAD
|
|
207
|
+
[0.5.0]: https://github.com/sorare/administrate-mcp/compare/v0.4.0...v0.5.0
|
|
181
208
|
[0.4.0]: https://github.com/sorare/administrate-mcp/compare/v0.3.1...v0.4.0
|
|
182
209
|
[0.3.1]: https://github.com/sorare/administrate-mcp/compare/v0.3.0...v0.3.1
|
|
183
210
|
[0.3.0]: https://github.com/sorare/administrate-mcp/compare/v0.2.0...v0.3.0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Administrate
|
|
4
|
+
module MCP
|
|
5
|
+
# What an `mcp_action` block may return to send data back to the client. `record` goes through
|
|
6
|
+
# the same checks as a record returned directly, and gives a collection action its `id`; `data`
|
|
7
|
+
# is answered as `mcp_data`, which the engine adds to the response under `data`, or to the error
|
|
8
|
+
# of a failure. Any result object that answers `mcp_data` works the same way.
|
|
9
|
+
class ActionResult
|
|
10
|
+
attr_reader :record, :data, :error
|
|
11
|
+
|
|
12
|
+
alias mcp_data data
|
|
13
|
+
|
|
14
|
+
private_class_method :new
|
|
15
|
+
|
|
16
|
+
def self.success(record: nil, data: nil)
|
|
17
|
+
new(record:, data:, error: nil, success: true)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.failure(error = nil, data: nil)
|
|
21
|
+
new(record: nil, data:, error:, success: false)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(record:, data:, error:, success:)
|
|
25
|
+
@record = record
|
|
26
|
+
@data = data
|
|
27
|
+
@error = error
|
|
28
|
+
@success = success
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def success?
|
|
32
|
+
@success
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Administrate
|
|
4
|
+
module MCP
|
|
5
|
+
module Actions
|
|
6
|
+
# Reads the `mcp_data` of an action's result and turns it into what goes back to the client: a
|
|
7
|
+
# copy built only from plain Hashes with String keys, Arrays, Strings and JSON scalars, no deeper
|
|
8
|
+
# than MAX_DEPTH and no larger than `config.action_data_max_bytes` once encoded. The copy, not
|
|
9
|
+
# the host's objects, is what gets encoded, so no `as_json` of theirs runs. The action has
|
|
10
|
+
# already run when this is checked, so the error says so.
|
|
11
|
+
module ResultData
|
|
12
|
+
MAX_DEPTH = 32
|
|
13
|
+
|
|
14
|
+
SCALARS = [Integer, TrueClass, FalseClass, NilClass].freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def extract!(result, tool_name:)
|
|
18
|
+
return nil unless result.respond_to?(:mcp_data)
|
|
19
|
+
|
|
20
|
+
data = result.mcp_data
|
|
21
|
+
return nil if data.nil?
|
|
22
|
+
|
|
23
|
+
normalize_root!(data, tool_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def normalize_root!(data, tool_name)
|
|
29
|
+
raise TypeError, "data (#{data.class}) is not a Hash" unless data.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
normalize!(data, 'data').tap { |copy| check_size!(copy) }
|
|
32
|
+
rescue TypeError, RangeError => e
|
|
33
|
+
raise Error, "#{tool_name} ran, but its data cannot be returned: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def normalize!(value, path, depth = 0)
|
|
37
|
+
raise RangeError, "#{path} is nested deeper than #{MAX_DEPTH} levels" if depth > MAX_DEPTH
|
|
38
|
+
|
|
39
|
+
case value
|
|
40
|
+
when Hash then normalize_hash!(value, path, depth)
|
|
41
|
+
when Array then value.each_with_index.map { |item, index| normalize!(item, "#{path}[#{index}]", depth + 1) }
|
|
42
|
+
when String, Symbol then normalize_string!(value, path)
|
|
43
|
+
else normalize_scalar!(value, path)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def normalize_string!(value, path)
|
|
48
|
+
copy = String.new(value.to_s).encode(Encoding::UTF_8)
|
|
49
|
+
raise TypeError, "#{path} is not valid UTF-8" unless copy.valid_encoding?
|
|
50
|
+
|
|
51
|
+
copy
|
|
52
|
+
rescue EncodingError
|
|
53
|
+
raise TypeError, "#{path} cannot be converted to UTF-8"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def normalize_hash!(hash, path, depth)
|
|
57
|
+
hash.each_with_object({}) do |(key, item), copy|
|
|
58
|
+
name = key_name!(key, path)
|
|
59
|
+
raise TypeError, "#{path} has the key #{name} twice" if copy.key?(name)
|
|
60
|
+
|
|
61
|
+
copy[name] = normalize!(item, "#{path}.#{name}", depth + 1)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def key_name!(key, path)
|
|
66
|
+
return normalize_string!(key, "a key of #{path}") if key.is_a?(String) || key.is_a?(Symbol)
|
|
67
|
+
|
|
68
|
+
raise TypeError, "#{path} has a key (#{key.class}) that is not a String or Symbol"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalize_scalar!(value, path)
|
|
72
|
+
if value.instance_of?(Float)
|
|
73
|
+
raise TypeError, "#{path} is not a finite number" unless value.finite?
|
|
74
|
+
|
|
75
|
+
return value
|
|
76
|
+
end
|
|
77
|
+
return value if SCALARS.any? { |type| value.instance_of?(type) }
|
|
78
|
+
|
|
79
|
+
raise TypeError, "#{path} (#{value.class}) is not a JSON value"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def check_size!(copy)
|
|
83
|
+
limit = Administrate::MCP.config.action_data_max_bytes
|
|
84
|
+
return if limit.nil?
|
|
85
|
+
|
|
86
|
+
bytes = copy.to_json.bytesize
|
|
87
|
+
return if bytes <= limit
|
|
88
|
+
|
|
89
|
+
raise RangeError,
|
|
90
|
+
"data is #{bytes} bytes of JSON, over the #{limit}-byte limit (config.action_data_max_bytes)"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -177,7 +177,11 @@ module Administrate
|
|
|
177
177
|
end
|
|
178
178
|
klass.define_singleton_method(:execute) do |admin:, id: nil, **params|
|
|
179
179
|
outcome = ToolFactory.send(:invoke, defn, admin, id, params)
|
|
180
|
-
outcome.key?(:error)
|
|
180
|
+
if outcome.key?(:error)
|
|
181
|
+
error_response(outcome[:error], data: outcome[:details])
|
|
182
|
+
else
|
|
183
|
+
json_response(outcome[:data], structured: true)
|
|
184
|
+
end
|
|
181
185
|
end
|
|
182
186
|
end
|
|
183
187
|
|
|
@@ -190,15 +194,20 @@ module Administrate
|
|
|
190
194
|
declared = params.slice(*defn.params.keys, *Uploads::Declarations.param_schemas(defn).keys)
|
|
191
195
|
arguments = { admin:, params: Uploads::Declarations.resolve_params(defn, admin, declared) }
|
|
192
196
|
arguments[:record] = Actions.find_record!(defn.model_class, id) if defn.on == :member
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
returned = defn.invoke.call(**arguments)
|
|
198
|
+
result = returned.is_a?(ActionResult) ? returned.record : returned
|
|
199
|
+
data = ResultData.extract!(returned, tool_name: Actions.tool_name(defn))
|
|
200
|
+
failures = [returned, result].select { |candidate| failed?(candidate) }
|
|
201
|
+
unless failures.empty?
|
|
202
|
+
return { error: failure_message(defn, failures.first, arguments[:record]), details: data }
|
|
203
|
+
end
|
|
195
204
|
|
|
196
|
-
{ data: success_data(defn, id, result) }
|
|
205
|
+
{ data: success_data(defn, id, result, data) }
|
|
197
206
|
end
|
|
198
207
|
|
|
199
|
-
def success_data(defn, id, result)
|
|
208
|
+
def success_data(defn, id, result, data)
|
|
200
209
|
id = result.id if defn.on == :collection && result.is_a?(ActiveRecord::Base)
|
|
201
|
-
{ status: 'ok', model: defn.model_class.name.underscore, id:, action: defn.name.to_s }.compact
|
|
210
|
+
{ status: 'ok', model: defn.model_class.name.underscore, id:, action: defn.name.to_s, data: }.compact
|
|
202
211
|
end
|
|
203
212
|
|
|
204
213
|
def failed?(result)
|
|
@@ -106,12 +106,17 @@ module Administrate
|
|
|
106
106
|
::MCP::Tool::Response.new([{ type: 'text', text: }])
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
def json_response(data)
|
|
110
|
-
::MCP::Tool::Response.new([{ type: 'text', text: data.to_json }])
|
|
109
|
+
def json_response(data, structured: false)
|
|
110
|
+
::MCP::Tool::Response.new([{ type: 'text', text: data.to_json }], structured_content: (data if structured))
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
-
def error_response(message)
|
|
114
|
-
|
|
113
|
+
def error_response(message, data: nil)
|
|
114
|
+
content = [{ type: 'text', text: message }]
|
|
115
|
+
return ::MCP::Tool::Response.new(content, error: true) if data.nil?
|
|
116
|
+
|
|
117
|
+
structured = { error: message, data: }
|
|
118
|
+
content << { type: 'text', text: structured.to_json }
|
|
119
|
+
::MCP::Tool::Response.new(content, error: true, structured_content: structured)
|
|
115
120
|
end
|
|
116
121
|
end
|
|
117
122
|
end
|
data/docs/configuration.md
CHANGED
|
@@ -111,6 +111,7 @@ class Configuration
|
|
|
111
111
|
:persist_feedback,
|
|
112
112
|
:feedback_tool,
|
|
113
113
|
:sidekiq_stats_provider,
|
|
114
|
+
:action_data_max_bytes,
|
|
114
115
|
:admin_route_namespace,
|
|
115
116
|
:admin_url_options,
|
|
116
117
|
:skipped_field_classes,
|
|
@@ -148,6 +149,7 @@ class Configuration
|
|
|
148
149
|
@persist_feedback = false
|
|
149
150
|
@feedback_tool = true
|
|
150
151
|
@sidekiq_stats_provider = nil
|
|
152
|
+
@action_data_max_bytes = 32_768
|
|
151
153
|
end
|
|
152
154
|
|
|
153
155
|
def assign_fields
|
|
@@ -197,6 +199,7 @@ end
|
|
|
197
199
|
| `persist_feedback` | `false` | Whether `report_mcp_improvement` creates an `Administrate::MCP::Feedback` row before calling `on_feedback`. False needs no `administrate_mcp_feedbacks` table at all |
|
|
198
200
|
| `feedback_tool` | `true` | Whether `report_mcp_improvement` is published |
|
|
199
201
|
| `sidekiq_stats_provider` | `nil` | Object answering `counts`, `total_counts`, `queues`, `stats_cleared_at`; a class name String or a Proc is resolved lazily so autoloaded providers can be named in an initializer |
|
|
202
|
+
| `action_data_max_bytes` | `32_768` | Largest `data` an `mcp_action` may return, in bytes of JSON; larger data is an error. `nil` removes the limit. See [Returning data from an action](dashboards.md#returning-data-from-an-action) |
|
|
200
203
|
| `admin_route_namespace` | `:admin` | Namespace record URLs are built from |
|
|
201
204
|
| `admin_url_options` | `{}` | Options passed to `polymorphic_url`; when no `:host` is given, host, protocol and port are taken from `admin_origin` |
|
|
202
205
|
| `skipped_field_classes` | Password | Field class names never serialized |
|
data/docs/dashboards.md
CHANGED
|
@@ -53,7 +53,8 @@ base class, is not applied to every resource. A subclass that wants one declares
|
|
|
53
53
|
saved or has errors is an error too, with its validation messages. So is `false`, the result of
|
|
54
54
|
a failed `update` or of a `destroy` a callback aborted; for a member action the message is the
|
|
55
55
|
loaded record's validation messages when it has any. Anything else is a success, including a
|
|
56
|
-
record the block destroyed.
|
|
56
|
+
record the block destroyed. To send data back with the answer, return an
|
|
57
|
+
`Administrate::MCP::ActionResult`; see [Returning data from an action](#returning-data-from-an-action).
|
|
57
58
|
- The tool is announced as destructive unless the declaration says `destructive: false`. Clients
|
|
58
59
|
may ask for confirmation before running a destructive tool, so set it on actions that only add
|
|
59
60
|
or prepare something.
|
|
@@ -72,6 +73,60 @@ base class, is not applied to every resource. A subclass that wants one declares
|
|
|
72
73
|
`max_size:`) adds an optional `<attribute>_upload_id` param for each of them, and the block
|
|
73
74
|
receives the uploaded file as `params[:<attribute>]`. See [Presigned uploads](uploads.md).
|
|
74
75
|
|
|
76
|
+
## Returning data from an action
|
|
77
|
+
|
|
78
|
+
An action answers `{ status, model, id, action }` by default, as JSON text and, identically, as the
|
|
79
|
+
result's `structuredContent`. When the client needs more than that, such as what an import created
|
|
80
|
+
and what it skipped, the block returns an `Administrate::MCP::ActionResult` and the answer gains a
|
|
81
|
+
`data` key:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
mcp_action :import_batch,
|
|
85
|
+
on: :collection,
|
|
86
|
+
description: 'Import orders from a marketplace export.',
|
|
87
|
+
params: { references: { type: 'array', items: { type: 'string' } } } do |admin:, params:|
|
|
88
|
+
report = Orders::ImportBatch.call!(references: params[:references], admin:)
|
|
89
|
+
next Administrate::MCP::ActionResult.failure('No order matched', data: { unknown: report.unknown }) if report.empty?
|
|
90
|
+
|
|
91
|
+
Administrate::MCP::ActionResult.success(record: report.batch,
|
|
92
|
+
data: { created: report.created.size, unknown: report.unknown })
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "status": "ok", "model": "order", "id": 42, "action": "import_batch", "data": { "created": 3, "unknown": ["X-9"] } }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- `ActionResult.success(record:, data:)`: `record` is optional and is checked as if the block had
|
|
101
|
+
returned it, so an unsaved or invalid record is still an error, and a collection action still
|
|
102
|
+
answers its `id`.
|
|
103
|
+
- `ActionResult.failure(message, data:)` is an error with `message`, or `<action> failed` without
|
|
104
|
+
one. The error's first content block stays the message alone. When there is data, a second block
|
|
105
|
+
carries `{ "error": message, "data": … }` as JSON, and the result's `structuredContent` carries the
|
|
106
|
+
same object, so clients that only read the content still see the data.
|
|
107
|
+
- These two are the only constructors. Any result object that answers `mcp_data` with a Hash works
|
|
108
|
+
the same way, so a host's own service result can carry its data without being wrapped; `success?`
|
|
109
|
+
and `error` are read from it as for any other result.
|
|
110
|
+
- A plain Hash returned by the block is not sent back, as before: data reaches the client only when
|
|
111
|
+
the block asks for it, never because a service happened to return a Hash.
|
|
112
|
+
- `data` must be a Hash of JSON values: Hashes with String or Symbol keys, Arrays, Strings, Symbols,
|
|
113
|
+
Integers, finite Floats, `true`, `false` and `nil`, nested at most 32 levels deep. Strings and
|
|
114
|
+
keys must be valid UTF-8 or convertible to it, so check bytes read from a file. Convert
|
|
115
|
+
anything else, such as a time (`at.iso8601`) or a record (`record.id`), yourself. The engine
|
|
116
|
+
encodes a plain copy it builds from those values, so an object's own `as_json` never runs, and a
|
|
117
|
+
key given both as a String and as a Symbol is refused rather than sent twice. The copy's JSON may
|
|
118
|
+
be at most `config.action_data_max_bytes` (32 KB by default; `nil` removes the limit), so that an
|
|
119
|
+
action cannot fill the client's context. Return counts and the few items the client must act on,
|
|
120
|
+
not every row.
|
|
121
|
+
- Data that breaks a rule is answered as an error naming the offending path, for example
|
|
122
|
+
`data.report.rows[1] (Object) is not a JSON value`. The block has already run by then, so the
|
|
123
|
+
message says the action ran: fix the action rather than retrying the call.
|
|
124
|
+
- `data` reaches the client as it is, without the protections fields get from
|
|
125
|
+
`skipped_field_classes` and the field serializers. Don't return secrets, tokens or credentials. A
|
|
126
|
+
download URL is a bearer credential that ends up in transcripts and client logs, so make it
|
|
127
|
+
short-lived. Strings that came from end users (names, notes, references) are untrusted text in the
|
|
128
|
+
model's context, as they are in any field.
|
|
129
|
+
|
|
75
130
|
## Search
|
|
76
131
|
|
|
77
132
|
The `admin_resource_list` tool's `query` argument matches exactly by default, so searching for an id
|
|
@@ -59,6 +59,7 @@ module Administrate
|
|
|
59
59
|
:persist_feedback,
|
|
60
60
|
:feedback_tool,
|
|
61
61
|
:sidekiq_stats_provider,
|
|
62
|
+
:action_data_max_bytes,
|
|
62
63
|
:admin_route_namespace,
|
|
63
64
|
:admin_url_options,
|
|
64
65
|
:skipped_field_classes,
|
|
@@ -102,6 +103,7 @@ module Administrate
|
|
|
102
103
|
@persist_feedback = false
|
|
103
104
|
@feedback_tool = true
|
|
104
105
|
@sidekiq_stats_provider = nil
|
|
106
|
+
@action_data_max_bytes = 32_768
|
|
105
107
|
@uploads = UploadsConfiguration.new
|
|
106
108
|
end
|
|
107
109
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: administrate-mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sorare
|
|
@@ -89,7 +89,9 @@ files:
|
|
|
89
89
|
- app/controllers/concerns/administrate/mcp/resource_controller.rb
|
|
90
90
|
- app/dashboards/administrate/mcp/api_key_dashboard.rb
|
|
91
91
|
- app/dashboards/administrate/mcp/feedback_dashboard.rb
|
|
92
|
+
- app/lib/administrate/mcp/action_result.rb
|
|
92
93
|
- app/lib/administrate/mcp/actions.rb
|
|
94
|
+
- app/lib/administrate/mcp/actions/result_data.rb
|
|
93
95
|
- app/lib/administrate/mcp/admin_dashboard_tool.rb
|
|
94
96
|
- app/lib/administrate/mcp/authentication.rb
|
|
95
97
|
- app/lib/administrate/mcp/base_tool.rb
|