administrate-mcp 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4da1e224450ced4f0f0e209279bf8f07e3e863b8cc54d53deb20071e09c162f5
4
- data.tar.gz: 7299ecaabb67efa26432087c689d6291d7b839692a97855997f0a5aa94ea8e5c
3
+ metadata.gz: 68fb06b806eb2dea5dbb0e0a01a865b4a9984b515a358bfd86fab8d0b949b65c
4
+ data.tar.gz: ac0a3ef739bf776dff2325bc3ae641e22b694047ffd03d078c944810b2071768
5
5
  SHA512:
6
- metadata.gz: 52016172bf5d81384da02d845c2a7e491ad88859b414ce20b1732804deeff46f930a4df8e4d73b254721c31c39fe8bf2907cda7e56c32dd7b1728641993025ae
7
- data.tar.gz: 78e0e4ac8f02ce53a3c4603d5bcb8d95ace6976192fed36519a0df092fa2a0b7bec2ac1fc071e3fcc2862e60f81195377943bd2c7693060c488d5ea4b4548a5a
6
+ metadata.gz: 1d6204806b8c490362a1e4a56939a3ae588897f069f7c1e21c4ff653eaed486d06cd238e3243d3c3a29edcc2399a7aef25deed80022e598f8b52fc36a7fc7895
7
+ data.tar.gz: 2064c7b7ad08e21f32eba4db06ebda81d594e1abf0c731ddf369a901282ce419c923eea77715faa1c25ae44601809e4f3195d55fc539c91e90ae5b2d4107eca1
data/CHANGELOG.md CHANGED
@@ -8,6 +8,37 @@ minor release may change behaviour a host depends on; the entry says so when it
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.2.0] - 2026-09-22
12
+
13
+ ### Added
14
+
15
+ - The admin console for the engine's own tables: the `Administrate::MCP::ApiKeyDashboard` and
16
+ `Administrate::MCP::FeedbackDashboard` base dashboards, and the `Administrate::MCP::ApiKeysAdmin`
17
+ and `Administrate::MCP::FeedbacksAdmin` controller concerns. A host subclasses each dashboard
18
+ under the name its own inflections give `administrate_mcp_api_key_dashboard.rb`, which differs
19
+ between an application that registers MCP as an acronym and one that does not. The concern goes in
20
+ a controller of the host's own, which keeps its base controller, authentication and policies; the
21
+ engine supplies the resource wiring, key generation, the one-time plaintext in the flash, and
22
+ revocation. Write-enabled keys are refused until the host overrides `mcp_write_access_allowed?`.
23
+ - `config.admin_foreign_key`, defaulting to `:admin_id`, so a host whose admin table already has a
24
+ differently named foreign key can keep that column name instead of renaming it. The association
25
+ stays called `admin` everywhere in the engine's API; only the column it reads and writes changes.
26
+ - `config.persist_feedback`, defaulting to `false`, so a host can use `report_mcp_improvement` and
27
+ `on_feedback` as a plain signal without carrying the `administrate_mcp_feedbacks` table, its model
28
+ or its migration. Set it to `true` for the previous behaviour: the report is persisted before
29
+ `on_feedback` runs, and the dashboard and `CleanOldFeedbacks` housekeeping become meaningful.
30
+ - `config.feedback_tool`, defaulting to `true`, to stop publishing `report_mcp_improvement`
31
+ altogether.
32
+
33
+ ### Changed
34
+
35
+ - `on_feedback` is now called with a `FeedbackReport`, a plain value object, instead of a persisted
36
+ `Feedback` record. A host reading attributes off the argument (`category`, `suggestion`,
37
+ `resource_name`, `admin`, `api_key`) sees no difference; a host that relied on it being an
38
+ `ActiveRecord` instance, for example calling `.save`, `.update` or `.reload` on it, or expecting
39
+ `.persisted?` to be true, must update its `on_feedback` hook. When `config.persist_feedback` is
40
+ `true` the persisted record is still reachable, as `report.record`.
41
+
11
42
  ## [0.1.0] - 2026-09-22
12
43
 
13
44
  ### Added
@@ -57,5 +88,6 @@ minor release may change behaviour a host depends on; the entry says so when it
57
88
  took the gem from git before 0.1.0 must reinstall its migrations rather than migrate
58
89
  incrementally.
59
90
 
60
- [Unreleased]: https://github.com/sorare/administrate-mcp/compare/v0.1.0...HEAD
91
+ [Unreleased]: https://github.com/sorare/administrate-mcp/compare/v0.2.0...HEAD
92
+ [0.2.0]: https://github.com/sorare/administrate-mcp/compare/v0.1.0...v0.2.0
61
93
  [0.1.0]: https://github.com/sorare/administrate-mcp/releases/tag/v0.1.0
data/README.md CHANGED
@@ -23,6 +23,7 @@ permissions the admin UI enforces.
23
23
  - [Dashboard declarations](#dashboard-declarations)
24
24
  - [Authentication](#authentication)
25
25
  - [OAuth](#oauth)
26
+ - [Improving the server from its own use](#improving-the-server-from-its-own-use)
26
27
  - [Rate limiting](#rate-limiting)
27
28
  - [Admin integration](#admin-integration)
28
29
  - [Development](#development)
@@ -53,8 +54,14 @@ about your application; everything host-specific goes through `Administrate::MCP
53
54
  as the admin UI, so a resource an admin cannot see in the browser is not exposed over MCP either.
54
55
  - Search, filters and field selection built from the dashboard's own declarations, plus foreign key
55
56
  filters that need no declaration at all.
56
- - A feedback tool, `report_mcp_improvement`, so users can report what the server got wrong, and a
57
- housekeeping method to clean up old feedback records.
57
+ - A feedback tool, `report_mcp_improvement`, so the client can tell you which of your descriptions,
58
+ filters and fields let it down, and you can fix them. See
59
+ [Improving the server from its own use](#improving-the-server-from-its-own-use). The signal is
60
+ always available through `config.on_feedback`; storing it, the dashboard and the clean-up service
61
+ are a batteries-included option a host turns on with `config.persist_feedback`.
62
+ - The admin console for its own tables, as two dashboards and two controller concerns you include
63
+ in controllers of your own: issuing an API key, revoking one, and reading the feedback, without
64
+ giving up your base controller, your authentication or your policies.
58
65
  - Optional Sidekiq introspection tools, `sidekiq_stats` and `sidekiq_retries`, wired to a stats
59
66
  provider object you supply.
60
67
  - No reference to a constant it does not own: field serializers are keyed on class names, dashboards
@@ -151,13 +158,20 @@ bin/rails db:migrate
151
158
 
152
159
  The tables are `administrate_mcp_api_keys`, `administrate_mcp_feedbacks`,
153
160
  `administrate_mcp_oauth_applications`, `administrate_mcp_oauth_access_grants` and
154
- `administrate_mcp_oauth_access_tokens`. They use uuid primary keys and a uuid `admin_id` column
155
- that is indexed but carries no foreign key constraint, so the engine works with any admin table.
161
+ `administrate_mcp_oauth_access_tokens`. They use uuid primary keys and a uuid column, `admin_id` by
162
+ default, that is indexed but carries no foreign key constraint, so the engine works with any admin
163
+ table. Set `config.admin_foreign_key` before running the migrations if the host's own admin table
164
+ already uses a different column name and renaming it is not an option, for example a live
165
+ credentials table.
166
+
167
+ `administrate_mcp_feedbacks` is only needed when `config.persist_feedback` is `true`; leave it
168
+ `false`, the default, and the migration ships but the table is never read from or written to.
156
169
 
157
170
  ## Quick start
158
171
 
159
172
  The smallest configuration that works. Save it as `config/initializers/administrate_mcp.rb`; it
160
- must run before the engine's models load, because the `admin` association reads `admin_class_name`:
173
+ must run before the engine's models load, because the `admin` association reads `admin_class_name`
174
+ and `admin_foreign_key`:
161
175
 
162
176
  ```ruby
163
177
  Administrate::MCP.configure do |c|
@@ -218,6 +232,35 @@ with the gem for hosts that run edge-managed OAuth in front of the application:
218
232
  The built-in OAuth 2.1 server, what turning it off with `c.oauth = false` changes, and when a host
219
233
  should: [docs/oauth.md](docs/oauth.md).
220
234
 
235
+ ## Improving the server from its own use
236
+
237
+ Every tool here is built from your dashboards: the resource names, the field lists, the filters and
238
+ the `MCP_DESCRIPTION` you wrote. The client calling those tools is the one that gets misled when any
239
+ of it is wrong, and it is the only party that knows which call it was trying to make. The engine has
240
+ no way to detect this on its own: a vague description is not an error, it is a successful call that
241
+ returned the wrong thing or a query the caller gave up on.
242
+
243
+ `report_mcp_improvement` is how the client tells you. Its categories are deliberately not free text.
244
+ Each one names a change you make in a dashboard:
245
+
246
+ | Category | What it points at |
247
+ | --- | --- |
248
+ | `description` | `MCP_DESCRIPTION` is missing, vague or actively misleading |
249
+ | `missing_filter` | the query needed a filter the dashboard does not declare |
250
+ | `missing_field` | a field the caller needed is not on the show page |
251
+ | `missing_resource` | a dashboard is not exposed, or does not exist |
252
+ | `serialization` | a field came back unreadable and needs a serializer registered |
253
+ | `other` | anything the categories above do not cover |
254
+
255
+ A report arrives with the category, the resource it concerns and the client's own account of what it
256
+ wanted, which is most of a change request already. Wire `config.on_feedback` to somewhere your team
257
+ will actually read, work through what arrives, and the next caller gets a server that describes
258
+ itself better. Turning on `config.persist_feedback` keeps the reports in a table so you can triage a
259
+ batch at a time rather than react to each one.
260
+
261
+ This is the loop the tool exists for. It is worth running deliberately rather than waiting for
262
+ complaints: point a client at the server, give it real tasks, and collect what it could not do.
263
+
221
264
  ## Rate limiting
222
265
 
223
266
  The rack-attack throttles recommended for the OAuth endpoints, and the helper that registers them
@@ -225,8 +268,9 @@ for you: [docs/oauth.md#rate-limiting](docs/oauth.md#rate-limiting).
225
268
 
226
269
  ## Admin integration
227
270
 
228
- Exposing the engine's own tables (API keys, feedback) in your host admin, customising the consent
229
- screen, and cleaning up old feedback: [docs/admin-integration.md](docs/admin-integration.md).
271
+ Including the API key and feedback consoles the engine ships, what a host overrides in them,
272
+ listing the engine's own tables over the protocol, customising the consent screen, and cleaning up
273
+ old feedback: [docs/admin-integration.md](docs/admin-integration.md).
230
274
 
231
275
  ## Development
232
276
 
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administrate
4
+ module MCP
5
+ # Administrate console for the API keys admins authenticate to the MCP server with. The host
6
+ # keeps its own base controller, authentication and policies; this supplies the resource wiring
7
+ # and the two actions a write-once credential needs, because the plaintext key exists only in
8
+ # the response that creates it.
9
+ #
10
+ # A host that lets some admins mint write-enabled keys overrides `mcp_write_access_allowed?`.
11
+ module ApiKeysAdmin
12
+ extend ActiveSupport::Concern
13
+ include ResourceController
14
+
15
+ TOKEN_PREFIX_LENGTH = 13
16
+ WRITE_ACCESS_REFUSAL = 'Only administrators allowed to grant write access can create write-enabled API keys.'
17
+
18
+ included do
19
+ administrate_mcp_resource Administrate::MCP::ApiKey
20
+ end
21
+
22
+ def create
23
+ token = Administrate::MCP::ApiKey.generate_token
24
+ api_key = build_mcp_api_key(token)
25
+ authorize_mcp_resource(api_key)
26
+
27
+ if api_key.write_access? && !mcp_write_access_allowed?
28
+ return render_mcp_form_error(api_key, WRITE_ACCESS_REFUSAL)
29
+ end
30
+
31
+ if api_key.save
32
+ redirect_to_mcp_index("MCP API key created. Copy it now, it is not shown again: #{token}")
33
+ else
34
+ render_mcp_form_error(api_key, api_key.errors.full_messages.join(', '))
35
+ end
36
+ end
37
+
38
+ def destroy
39
+ api_key = requested_resource
40
+ authorize_mcp_resource(api_key)
41
+ api_key.revoke!
42
+
43
+ redirect_to_mcp_index("API key '#{api_key.name}' has been revoked.")
44
+ end
45
+
46
+ private
47
+
48
+ def build_mcp_api_key(token)
49
+ Administrate::MCP::ApiKey.new(
50
+ admin: mcp_api_key_owner,
51
+ token_digest: Administrate::MCP::ApiKey.digest_token(token),
52
+ token_prefix: token[0, TOKEN_PREFIX_LENGTH],
53
+ name: mcp_api_key_name,
54
+ write_access: mcp_write_access_requested?
55
+ )
56
+ end
57
+
58
+ def mcp_api_key_owner
59
+ Administrate::MCP.config.current_admin.call(self)
60
+ end
61
+
62
+ def mcp_api_key_name
63
+ params.dig(resource_name, :name).presence || 'Unnamed key'
64
+ end
65
+
66
+ def mcp_write_access_requested?
67
+ ActiveModel::Type::Boolean.new.cast(params.dig(resource_name, :write_access)) || false
68
+ end
69
+
70
+ # A write-enabled key carries every write tool, so granting one is a decision the host makes.
71
+ def mcp_write_access_allowed?
72
+ false
73
+ end
74
+
75
+ def authorize_mcp_resource(api_key)
76
+ authorize(api_key) if respond_to?(:authorize, true)
77
+ end
78
+
79
+ def redirect_to_mcp_index(notice)
80
+ redirect_to({ action: :index }, notice:, status: :see_other)
81
+ end
82
+
83
+ def render_mcp_form_error(api_key, message)
84
+ flash.now[:error] = message
85
+ render :new, locals: { page: Administrate::Page::Form.new(dashboard, api_key) }, status: :unprocessable_content
86
+ end
87
+
88
+ def scoped_resource
89
+ super.order(created_at: :desc)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administrate
4
+ module MCP
5
+ # Administrate console for the improvement reports `report_mcp_improvement` stores. Reading them
6
+ # is the whole point, so no action is overridden beyond the resource wiring and the ordering:
7
+ # the newest report is the one a maintainer has not acted on yet.
8
+ #
9
+ # Only meaningful with `config.persist_feedback` on; without it nothing writes to the table.
10
+ module FeedbacksAdmin
11
+ extend ActiveSupport::Concern
12
+ include ResourceController
13
+
14
+ included do
15
+ administrate_mcp_resource Administrate::MCP::Feedback
16
+ end
17
+
18
+ private
19
+
20
+ def scoped_resource
21
+ super.order(created_at: :desc)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administrate
4
+ module MCP
5
+ # Points an Administrate controller at a model the engine owns, rather than at the one
6
+ # Administrate would infer from the controller's own name.
7
+ #
8
+ # The dashboard is left alone on purpose. Administrate finds it by camelizing the controller's
9
+ # name, and a host that registers MCP as an inflection acronym spells that constant differently
10
+ # from one that does not. Naming it here would be right in one host and an autoloading error in
11
+ # the other, so each host declares its own dashboard, usually a subclass of the engine's.
12
+ module ResourceController
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ class_attribute :mcp_resource_class, :mcp_dashboard_class
17
+ end
18
+
19
+ class_methods do
20
+ # `dashboard` is only needed for a dashboard Administrate cannot find from the controller's
21
+ # own name.
22
+ def administrate_mcp_resource(resource_class, dashboard: nil)
23
+ self.mcp_resource_class = resource_class
24
+ self.mcp_dashboard_class = dashboard
25
+ end
26
+ end
27
+
28
+ def resource_class
29
+ mcp_resource_class
30
+ end
31
+
32
+ private
33
+
34
+ def resource_resolver
35
+ @resource_resolver ||=
36
+ Administrate::MCP::ResourceResolver.new(
37
+ controller_path,
38
+ resource_class: mcp_resource_class,
39
+ dashboard_class: mcp_dashboard_class
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'administrate/base_dashboard'
4
+
5
+ module Administrate
6
+ module MCP
7
+ # Default Administrate dashboard for the engine's API keys. A host that wants other columns
8
+ # subclasses it and redefines the constants it cares about.
9
+ #
10
+ # Nothing here is registered as an MCP resource: the registry only reads the host's own
11
+ # dashboard directory, so a host that wants the keys listed over the protocol declares a
12
+ # subclass there with an `MCP_DESCRIPTION`.
13
+ class ApiKeyDashboard < Administrate::BaseDashboard
14
+ ATTRIBUTE_TYPES = {
15
+ id: Field::String.with_options(searchable: false),
16
+ admin: Field::BelongsTo.with_options(class_name: Administrate::MCP.config.admin_class_name),
17
+ name: Field::String,
18
+ token_prefix: Field::String,
19
+ write_access: Field::Boolean,
20
+ last_used_at: Field::DateTime,
21
+ revoked_at: Field::DateTime,
22
+ created_at: Field::DateTime,
23
+ updated_at: Field::DateTime
24
+ }.freeze
25
+
26
+ COLLECTION_ATTRIBUTES = %i[name admin token_prefix write_access last_used_at revoked_at].freeze
27
+
28
+ SHOW_PAGE_ATTRIBUTES = %i[
29
+ id
30
+ admin
31
+ name
32
+ token_prefix
33
+ write_access
34
+ last_used_at
35
+ revoked_at
36
+ created_at
37
+ updated_at
38
+ ].freeze
39
+
40
+ FORM_ATTRIBUTES = %i[name write_access].freeze
41
+
42
+ COLLECTION_FILTERS = {}.freeze
43
+
44
+ def self.model
45
+ Administrate::MCP::ApiKey
46
+ end
47
+
48
+ def display_resource(api_key)
49
+ "#{api_key.name} (#{api_key.token_prefix}...)"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'administrate/base_dashboard'
4
+
5
+ module Administrate
6
+ module MCP
7
+ # Default Administrate dashboard for the improvement reports the feedback tool stores. A host
8
+ # that wants other columns subclasses it and redefines the constants it cares about.
9
+ class FeedbackDashboard < Administrate::BaseDashboard
10
+ ATTRIBUTE_TYPES = {
11
+ id: Field::String.with_options(searchable: false),
12
+ admin: Field::BelongsTo.with_options(class_name: Administrate::MCP.config.admin_class_name),
13
+ api_key: Field::BelongsTo.with_options(class_name: 'Administrate::MCP::ApiKey'),
14
+ category: Field::Select.with_options(collection: -> { Administrate::MCP::Feedback.categories.keys }),
15
+ resource_name: Field::String,
16
+ suggestion: Field::Text,
17
+ status: Field::Select.with_options(collection: -> { Administrate::MCP::Feedback.statuses.keys }),
18
+ created_at: Field::DateTime,
19
+ updated_at: Field::DateTime
20
+ }.freeze
21
+
22
+ COLLECTION_ATTRIBUTES = %i[category resource_name status admin created_at].freeze
23
+
24
+ SHOW_PAGE_ATTRIBUTES = %i[id admin api_key category resource_name suggestion status created_at updated_at].freeze
25
+
26
+ # Triaging a report is the one edit worth making from the console.
27
+ FORM_ATTRIBUTES = %i[status].freeze
28
+
29
+ COLLECTION_FILTERS = {
30
+ status: ->(resources, status) { resources.where(status:) },
31
+ category: ->(resources, category) { resources.where(category:) }
32
+ }.freeze
33
+
34
+ def self.model
35
+ Administrate::MCP::Feedback
36
+ end
37
+
38
+ def display_resource(feedback)
39
+ "#{feedback.category} on #{feedback.resource_name.presence || 'the server'}"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administrate
4
+ module MCP
5
+ # The category list behind `Feedback#category` and the `report_mcp_improvement` schema, kept
6
+ # independent of the model so validation and the tool's input schema work whether or not the
7
+ # `administrate_mcp_feedbacks` table exists.
8
+ module FeedbackCategories
9
+ CATEGORIES = {
10
+ 'description' => 0,
11
+ 'missing_filter' => 1,
12
+ 'missing_field' => 2,
13
+ 'missing_resource' => 3,
14
+ 'serialization' => 4,
15
+ 'other' => 5
16
+ }.freeze
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administrate
4
+ module MCP
5
+ # Plain report of an MCP improvement suggestion, handed to `config.on_feedback` whether or not
6
+ # `config.persist_feedback` is on. `record` and `api_key` are only set when it is, so a host that
7
+ # wants the persisted row can reach it; the report itself never needs the `Feedback` table to
8
+ # exist.
9
+ FeedbackReport = Struct.new(:admin, :category, :suggestion, :resource_name, :api_key, :record, keyword_init: true)
10
+ end
11
+ end
@@ -2,9 +2,12 @@
2
2
 
3
3
  module Administrate
4
4
  module MCP
5
- # Persists an MCP improvement suggestion and hands it to the host's `on_feedback` hook.
5
+ # Validates an MCP improvement suggestion and hands a plain report to the host's `on_feedback`
6
+ # hook. Persisting the report to the `Feedback` table is opt-in through `config.persist_feedback`;
7
+ # when that is off, nothing here touches `Feedback` or `ApiKey`, so a host that never turns it on
8
+ # need not carry the table at all.
6
9
  class ReportImprovement
7
- Result = Struct.new(:success?, :feedback, :errors, keyword_init: true)
10
+ Result = Struct.new(:success?, :report, :errors, keyword_init: true)
8
11
 
9
12
  def self.call(...)
10
13
  new(...).call
@@ -19,11 +22,11 @@ module Administrate
19
22
 
20
23
  def call
21
24
  errors = validation_errors
22
- return Result.new(success?: false, feedback: nil, errors:) if errors.any?
25
+ return Result.new(success?: false, report: nil, errors:) if errors.any?
23
26
 
24
- feedback = create_feedback
25
- notify(feedback)
26
- Result.new(success?: true, feedback:, errors: [])
27
+ report = build_report
28
+ notify(report)
29
+ Result.new(success?: true, report:, errors: [])
27
30
  end
28
31
 
29
32
  private
@@ -32,13 +35,22 @@ module Administrate
32
35
 
33
36
  def validation_errors
34
37
  errors = []
35
- unless Feedback.categories.key?(category.to_s)
36
- errors << "category must be one of: #{Feedback.categories.keys.join(', ')}"
38
+ unless FeedbackCategories::CATEGORIES.key?(category.to_s)
39
+ errors << "category must be one of: #{FeedbackCategories::CATEGORIES.keys.join(', ')}"
37
40
  end
38
41
  errors << "suggestion can't be blank" if suggestion.blank?
39
42
  errors
40
43
  end
41
44
 
45
+ def build_report
46
+ record = create_feedback if persist_feedback?
47
+ FeedbackReport.new(admin:, category:, suggestion:, resource_name:, record:, api_key: record&.api_key)
48
+ end
49
+
50
+ def persist_feedback?
51
+ Administrate::MCP.config.persist_feedback
52
+ end
53
+
42
54
  def create_feedback
43
55
  Feedback.create!(admin:, api_key: latest_api_key, category:, resource_name:, suggestion:)
44
56
  end
@@ -47,9 +59,8 @@ module Administrate
47
59
  ApiKey.active.where(admin:).order(last_used_at: :desc).first
48
60
  end
49
61
 
50
- # The feedback is already persisted; a broken notifier must not take the report down with it.
51
- def notify(feedback)
52
- Administrate::MCP.config.on_feedback.call(feedback)
62
+ def notify(report)
63
+ Administrate::MCP.config.on_feedback.call(report)
53
64
  rescue StandardError => e
54
65
  Administrate::MCP.config.on_error.call(e)
55
66
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administrate
4
+ module MCP
5
+ # Administrate derives the model from the controller path, which cannot name a class living
6
+ # inside the engine's namespace. This resolver takes the model from the controller instead, and
7
+ # leaves the dashboard to Administrate: a dashboard's constant name follows the host's own
8
+ # inflections, which only the host can spell.
9
+ class ResourceResolver < Administrate::ResourceResolver
10
+ attr_reader :resource_class
11
+
12
+ def initialize(controller_path, resource_class:, dashboard_class: nil)
13
+ super(controller_path)
14
+ @resource_class = resource_class
15
+ @dashboard_class = dashboard_class
16
+ end
17
+
18
+ def dashboard_class
19
+ @dashboard_class || super
20
+ end
21
+
22
+ # The form builder names its parameters after the record, so the controller has to read them
23
+ # under the same key whatever path the host mounted it at.
24
+ def resource_name
25
+ resource_class.model_name.param_key.to_sym
26
+ end
27
+ end
28
+ end
29
+ end
@@ -8,9 +8,10 @@ module Administrate
8
8
  Administrate::MCP::Tools::AdminResourceList
9
9
  Administrate::MCP::Tools::AdminResourceShow
10
10
  Administrate::MCP::Tools::AdminResourceListResources
11
- Administrate::MCP::Tools::ReportImprovement
12
11
  ].freeze
13
12
 
13
+ REPORT_IMPROVEMENT_TOOL = 'Administrate::MCP::Tools::ReportImprovement'
14
+
14
15
  SIDEKIQ_RETRIES_TOOL = 'Administrate::MCP::Tools::SidekiqRetries'
15
16
 
16
17
  SIDEKIQ_STATS_TOOL = 'Administrate::MCP::Tools::SidekiqStats'
@@ -46,6 +47,7 @@ module Administrate
46
47
 
47
48
  def built_in_tools
48
49
  names = BUILT_IN_TOOLS.dup
50
+ names << REPORT_IMPROVEMENT_TOOL if Administrate::MCP.config.feedback_tool
49
51
  names << SIDEKIQ_RETRIES_TOOL if defined?(::Sidekiq)
50
52
  names << SIDEKIQ_STATS_TOOL if defined?(::Sidekiq) && Administrate::MCP.config.sidekiq_stats_provider
51
53
  names.filter_map(&:safe_constantize)
@@ -15,7 +15,7 @@ module Administrate
15
15
  properties: {
16
16
  category: {
17
17
  type: 'string',
18
- enum: Feedback.categories.keys,
18
+ enum: FeedbackCategories::CATEGORIES.keys,
19
19
  description: 'The type of improvement being reported.'
20
20
  },
21
21
  resource_name: {
@@ -35,7 +35,10 @@ module Administrate
35
35
 
36
36
  return error_response(result.errors.join(', ')) unless result.success?
37
37
 
38
- json_response({ status: 'created', feedback_id: result.feedback.id })
38
+ payload = { status: 'created' }
39
+ payload[:feedback_id] = result.report.record.id if result.report.record
40
+
41
+ json_response(payload)
39
42
  end
40
43
  end
41
44
  end
@@ -15,7 +15,10 @@ module Administrate
15
15
  end
16
16
 
17
17
  def belongs_to_admin
18
- belongs_to :admin, class_name: Administrate::MCP.config.admin_class_name, optional: false
18
+ belongs_to :admin, class_name: Administrate::MCP.config.admin_class_name,
19
+ foreign_key: Administrate::MCP.config.admin_foreign_key,
20
+ inverse_of: false,
21
+ optional: false
19
22
  end
20
23
  end
21
24
  end
@@ -9,8 +9,7 @@ module Administrate
9
9
  belongs_to_admin
10
10
  belongs_to :api_key, class_name: 'Administrate::MCP::ApiKey', optional: true, inverse_of: :feedbacks
11
11
 
12
- enum :category,
13
- { description: 0, missing_filter: 1, missing_field: 2, missing_resource: 3, serialization: 4, other: 5 }
12
+ enum :category, FeedbackCategories::CATEGORIES
14
13
  enum :status, { pending: 0, accepted: 1, rejected: 2, shipped: 3 }, prefix: true
15
14
 
16
15
  validates :suggestion, presence: true
@@ -2,8 +2,10 @@
2
2
 
3
3
  class CreateAdministrateModelContextProtocolApiKeys < ActiveRecord::Migration[8.1]
4
4
  def change
5
+ admin_foreign_key = Administrate::MCP.config.admin_foreign_key
6
+
5
7
  create_table :administrate_mcp_api_keys, id: :uuid, if_not_exists: true do |t|
6
- t.uuid :admin_id, null: false
8
+ t.uuid admin_foreign_key, null: false
7
9
  t.string :token_digest, null: false
8
10
  t.string :token_prefix, null: false
9
11
  t.string :name, null: false
@@ -14,7 +16,7 @@ class CreateAdministrateModelContextProtocolApiKeys < ActiveRecord::Migration[8.
14
16
  t.timestamps
15
17
  end
16
18
 
17
- add_index :administrate_mcp_api_keys, :admin_id, if_not_exists: true
19
+ add_index :administrate_mcp_api_keys, admin_foreign_key, if_not_exists: true
18
20
  add_index :administrate_mcp_api_keys, :token_digest, unique: true, if_not_exists: true
19
21
  end
20
22
  end
@@ -2,8 +2,10 @@
2
2
 
3
3
  class CreateAdministrateModelContextProtocolFeedbacks < ActiveRecord::Migration[8.1]
4
4
  def change
5
+ admin_foreign_key = Administrate::MCP.config.admin_foreign_key
6
+
5
7
  create_table :administrate_mcp_feedbacks, id: :uuid, if_not_exists: true do |t|
6
- t.uuid :admin_id
8
+ t.uuid admin_foreign_key
7
9
  t.uuid :api_key_id
8
10
  t.integer :category, null: false
9
11
  t.string :resource_name
@@ -13,7 +15,7 @@ class CreateAdministrateModelContextProtocolFeedbacks < ActiveRecord::Migration[
13
15
  t.timestamps
14
16
  end
15
17
 
16
- add_index :administrate_mcp_feedbacks, :admin_id, if_not_exists: true
18
+ add_index :administrate_mcp_feedbacks, admin_foreign_key, if_not_exists: true
17
19
  add_index :administrate_mcp_feedbacks, :api_key_id, if_not_exists: true
18
20
  end
19
21
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  class CreateAdministrateModelContextProtocolAuthorizationTables < ActiveRecord::Migration[8.1]
4
4
  def change
5
+ admin_foreign_key = Administrate::MCP.config.admin_foreign_key
6
+
5
7
  create_table :administrate_mcp_oauth_applications, id: :uuid, if_not_exists: true do |t|
6
8
  t.string :client_id, null: false
7
9
  t.text :client_secret_digest
@@ -15,7 +17,7 @@ class CreateAdministrateModelContextProtocolAuthorizationTables < ActiveRecord::
15
17
  add_index :administrate_mcp_oauth_applications, :client_id, unique: true, if_not_exists: true
16
18
 
17
19
  create_table :administrate_mcp_oauth_access_grants, id: :uuid, if_not_exists: true do |t|
18
- t.uuid :admin_id, null: false
20
+ t.uuid admin_foreign_key, null: false
19
21
  t.uuid :application_id, null: false
20
22
  t.string :token_digest, null: false
21
23
  t.integer :expires_in, null: false
@@ -28,12 +30,12 @@ class CreateAdministrateModelContextProtocolAuthorizationTables < ActiveRecord::
28
30
  t.datetime :created_at, null: false
29
31
  end
30
32
 
31
- add_index :administrate_mcp_oauth_access_grants, :admin_id, if_not_exists: true
33
+ add_index :administrate_mcp_oauth_access_grants, admin_foreign_key, if_not_exists: true
32
34
  add_index :administrate_mcp_oauth_access_grants, :application_id, if_not_exists: true
33
35
  add_index :administrate_mcp_oauth_access_grants, :token_digest, unique: true, if_not_exists: true
34
36
 
35
37
  create_table :administrate_mcp_oauth_access_tokens, id: :uuid, if_not_exists: true do |t|
36
- t.uuid :admin_id, null: false
38
+ t.uuid admin_foreign_key, null: false
37
39
  t.uuid :application_id, null: false
38
40
  t.string :token_digest, null: false
39
41
  t.string :refresh_token_digest, null: false
@@ -44,7 +46,7 @@ class CreateAdministrateModelContextProtocolAuthorizationTables < ActiveRecord::
44
46
  t.datetime :created_at, null: false
45
47
  end
46
48
 
47
- add_index :administrate_mcp_oauth_access_tokens, :admin_id, if_not_exists: true
49
+ add_index :administrate_mcp_oauth_access_tokens, admin_foreign_key, if_not_exists: true
48
50
  add_index :administrate_mcp_oauth_access_tokens, :application_id, if_not_exists: true
49
51
  add_index :administrate_mcp_oauth_access_tokens, :token_digest, unique: true, if_not_exists: true
50
52
  add_index :administrate_mcp_oauth_access_tokens, :refresh_token_digest, unique: true, if_not_exists: true
@@ -2,22 +2,116 @@
2
2
 
3
3
  [Back to README](../README.md)
4
4
 
5
- ## Exposing the engine's own tables in your admin
5
+ ## The API key and feedback consoles
6
6
 
7
- The engine's records keep their namespace in `model_name`, so `Administrate::MCP::ApiKey` routes as
8
- `administrate_mcp_api_keys` and never shadows a resource of your own called `api_keys`. Administrate
9
- needs three things to manage a namespaced model:
7
+ The engine ships the console for its own tables: two Administrate dashboards and two controller
8
+ concerns. Include them in controllers of your own, so that authentication, the base controller and
9
+ the policies stay yours:
10
10
 
11
11
  ```ruby
12
12
  # config/routes.rb, inside your admin namespace
13
13
  namespace :admin do
14
- resources :administrate_mcp_api_keys
15
- resources :administrate_mcp_feedbacks, only: %i[index show destroy]
14
+ resources :administrate_mcp_api_keys, only: %i[index show new create destroy]
15
+ # Only when config.persist_feedback is true; the table exists only then.
16
+ resources :administrate_mcp_feedbacks, only: %i[index show edit update]
16
17
  end
17
18
 
19
+ # app/dashboards/administrate_mcp_api_key_dashboard.rb
20
+ class AdministrateMcpApiKeyDashboard < Administrate::MCP::ApiKeyDashboard
21
+ end
22
+
23
+ # app/dashboards/administrate_mcp_feedback_dashboard.rb
24
+ class AdministrateMcpFeedbackDashboard < Administrate::MCP::FeedbackDashboard
25
+ end
26
+
27
+ # app/controllers/admin/administrate_mcp_api_keys_controller.rb
28
+ module Admin
29
+ class AdministrateMcpApiKeysController < Admin::ApplicationController
30
+ include Administrate::MCP::ApiKeysAdmin
31
+ end
32
+ end
33
+
34
+ # app/controllers/admin/administrate_mcp_feedbacks_controller.rb
35
+ module Admin
36
+ class AdministrateMcpFeedbacksController < Admin::ApplicationController
37
+ include Administrate::MCP::FeedbacksAdmin
38
+ end
39
+ end
40
+ ```
41
+
42
+ `ApiKeysAdmin` generates the key, stores its digest, and puts the plaintext in the flash of the
43
+ redirect that created it, because that response is the only place it can ever be read. `destroy`
44
+ revokes rather than deletes, so the row still says what the token was. Both concerns point
45
+ Administrate at the engine's model, which the controller's own name cannot spell.
46
+
47
+ The two empty subclasses are the part the engine cannot write for you. Administrate finds a
48
+ dashboard by camelizing the controller's name, and an application that registers MCP as an
49
+ inflection acronym expects `AdministrateMCPApiKeyDashboard` where one that does not expects
50
+ `AdministrateMcpApiKeyDashboard`. Name the file `administrate_mcp_api_key_dashboard.rb` and the
51
+ class whatever your own inflections produce for it; the columns come from the engine's class.
52
+
53
+ The key owner comes from `config.current_admin`, so no host code decides it twice. If your base
54
+ controller offers Pundit's `authorize`, both actions call it.
55
+
56
+ ### What a host overrides
57
+
58
+ | Method | Default | Override to |
59
+ | --- | --- | --- |
60
+ | `mcp_write_access_allowed?` | `false` | let some admins mint write-enabled keys |
61
+ | `mcp_api_key_owner` | `config.current_admin` | attribute the key to someone else |
62
+ | `scoped_resource` | every key, newest first | show an admin only their own keys |
63
+
64
+ ```ruby
65
+ module Admin
66
+ class AdministrateMcpApiKeysController < Admin::ApplicationController
67
+ include Administrate::MCP::ApiKeysAdmin
68
+
69
+ private
70
+
71
+ def mcp_write_access_allowed?
72
+ policy(Administrate::MCP::ApiKey).grant_write_access?
73
+ end
74
+
75
+ def scoped_resource
76
+ current_administrator.full_access? ? super : super.where(admin: current_administrator)
77
+ end
78
+ end
79
+ end
80
+ ```
81
+
82
+ ### Different columns
83
+
84
+ Redefine the constants you want to change in your subclass:
85
+
86
+ ```ruby
87
+ class AdministrateMcpApiKeyDashboard < Administrate::MCP::ApiKeyDashboard
88
+ COLLECTION_ATTRIBUTES = %i[name last_used_at].freeze
89
+ end
90
+ ```
91
+
92
+ For a console mounted at a path Administrate cannot derive the dashboard from, name it explicitly:
93
+
94
+ ```ruby
95
+ module Admin
96
+ class AdministrateMcpApiKeysController < Admin::ApplicationController
97
+ include Administrate::MCP::ApiKeysAdmin
98
+ administrate_mcp_resource Administrate::MCP::ApiKey, dashboard: ApiKeyConsoleDashboard
99
+ end
100
+ end
101
+ ```
102
+
103
+ ## Listing the engine's tables over the protocol
104
+
105
+ The consoles above are not MCP resources. The registry only reads your own dashboard directory, so
106
+ the engine's dashboards are invisible to it whatever they are called. To expose a key list over the
107
+ protocol, declare a dashboard in `app/dashboards` with an `MCP_DESCRIPTION` and a `self.model`:
108
+
109
+ ```ruby
18
110
  # app/dashboards/administrate_mcp/api_key_dashboard.rb
19
111
  module AdministrateMcp
20
112
  class ApiKeyDashboard < Administrate::BaseDashboard
113
+ MCP_DESCRIPTION = 'API keys admins authenticate to the MCP server with.'
114
+
21
115
  ATTRIBUTE_TYPES = { id: Field::String, name: Field::String, admin: Field::BelongsTo }.freeze
22
116
  COLLECTION_ATTRIBUTES = %i[id name].freeze
23
117
  SHOW_PAGE_ATTRIBUTES = %i[id name admin].freeze
@@ -28,18 +122,11 @@ module AdministrateMcp
28
122
  end
29
123
  end
30
124
  end
31
-
32
- # app/controllers/admin/administrate_mcp_api_keys_controller.rb
33
- module Admin
34
- class AdministrateMcpApiKeysController < Admin::ApplicationController
35
- def resource_class = Administrate::MCP::ApiKey
36
- def dashboard_class = AdministrateMcp::ApiKeyDashboard
37
- end
38
- end
39
125
  ```
40
126
 
41
- The MCP registry finds the dashboard through `self.model`, registers it as
42
- `administrate/mcp/api_key`, and builds record URLs from the namespaced route key.
127
+ The registry finds it through `self.model`, registers it as `administrate/mcp/api_key`, and builds
128
+ record URLs from the namespaced route key, so it never shadows a resource of your own called
129
+ `api_keys`.
43
130
 
44
131
  ## Customising the consent screen
45
132
 
@@ -48,6 +135,11 @@ has `@application`, `@redirect_uri`, `@redirect_host` and `@scopes`.
48
135
 
49
136
  ## Feedback housekeeping
50
137
 
138
+ The `administrate_mcp_feedbacks` table, its dashboard and this housekeeping service are only needed
139
+ when `config.persist_feedback` is `true`. With it off, `report_mcp_improvement` still works and
140
+ `on_feedback` still fires, just without a row behind it, and the table can be left out entirely; see
141
+ [docs/configuration.md](configuration.md) for the setting.
142
+
51
143
  `Administrate::MCP::CleanOldFeedbacks.call(till: 2.months.ago)` deletes one batch of 10,000 and
52
144
  reports `more?`; schedule it however your app schedules work.
53
145
 
@@ -3,7 +3,8 @@
3
3
  [Back to README](../README.md)
4
4
 
5
5
  Configure the engine in an initializer, `config/initializers/administrate_mcp.rb`. It must run
6
- before the engine's models load, because the `admin` association reads `admin_class_name`.
6
+ before the engine's models load, because the `admin` association reads `admin_class_name` and
7
+ `admin_foreign_key`.
7
8
 
8
9
  ```ruby
9
10
  Administrate::MCP.configure do |c|
@@ -11,6 +12,7 @@ Administrate::MCP.configure do |c|
11
12
  c.server_version = '1.0.0'
12
13
 
13
14
  c.admin_class_name = 'Administrator'
15
+ c.admin_foreign_key = :administrator_id
14
16
  c.current_admin = ->(controller) { controller.send(:warden)&.authenticate(scope: :administrator) }
15
17
  c.admin_active = ->(admin) { admin.admin? && admin.anonymized_at.nil? }
16
18
  c.sign_in = lambda do |controller|
@@ -36,10 +38,28 @@ Administrate::MCP.configure do |c|
36
38
  Admin::AuditOperation.call!(administrator_id: admin.id, request_method: 'POST',
37
39
  request_url: "mcp://tools/#{tool_name}", params: arguments)
38
40
  end
39
- c.on_feedback = ->(feedback) { SlackNotifier.notify(notification_type: :mcp_feedback, blocks: blocks_for(feedback)) }
41
+ c.persist_feedback = true
42
+ c.on_feedback = ->(report) { SlackNotifier.notify(notification_type: :mcp_feedback, blocks: blocks_for(report)) }
40
43
  end
41
44
  ```
42
45
 
46
+ Set `admin_foreign_key` when the host's own admin table already has a foreign key column under a
47
+ different name, for example a live credentials table called `administrator_id`, and renaming that
48
+ column is not something you want to do. The association is still called `admin` everywhere the
49
+ engine's API uses it; only the column it reads and writes changes.
50
+
51
+ `report_mcp_improvement` gives users of the MCP server a way to flag what the server got wrong; the
52
+ gem's job stops at reporting that signal to `on_feedback`. Storing what it reports, showing it in a
53
+ dashboard and cleaning up old rows is an opinion about how a host wants to handle the signal, not
54
+ something every host needs, so it is batteries-included rather than mandatory: set
55
+ `persist_feedback` to `true` to have the gem create an `Administrate::MCP::Feedback` row before
56
+ calling `on_feedback`, expose it in your admin as described in
57
+ [docs/admin-integration.md](admin-integration.md), and clean old rows up with
58
+ `Administrate::MCP::CleanOldFeedbacks`. Leave it `false`, the default, and `on_feedback` still fires
59
+ with the same report, just without a persisted record behind it, and the `administrate_mcp_feedbacks`
60
+ table is never touched. Set `feedback_tool` to `false` to stop publishing `report_mcp_improvement`
61
+ at all.
62
+
43
63
  ## The full configuration object
44
64
 
45
65
  ```ruby
@@ -77,6 +97,7 @@ class Configuration
77
97
  :oauth,
78
98
  :sign_in,
79
99
  :admin_class_name,
100
+ :admin_foreign_key,
80
101
  :authorization,
81
102
  :default_required_roles,
82
103
  :tool_paths,
@@ -87,6 +108,8 @@ class Configuration
87
108
  :on_error,
88
109
  :allow_localhost_redirects,
89
110
  :default_client_name,
111
+ :persist_feedback,
112
+ :feedback_tool,
90
113
  :sidekiq_stats_provider,
91
114
  :admin_route_namespace,
92
115
  :admin_url_options,
@@ -107,6 +130,7 @@ class Configuration
107
130
  @oauth = true
108
131
  @sign_in = nil
109
132
  @admin_class_name = 'Administrator'
133
+ @admin_foreign_key = :admin_id
110
134
  @authorization = default_authorization
111
135
  @default_required_roles = []
112
136
  @tool_paths = []
@@ -116,11 +140,13 @@ class Configuration
116
140
  def assign_hooks
117
141
  @instrument = ->(tool_name:, admin:, &block) { block.call }
118
142
  @on_tool_call = ->(tool_name:, admin:, arguments:, scopes:) {}
119
- @on_feedback = ->(feedback) {}
143
+ @on_feedback = ->(report) {}
120
144
  @on_error = ->(exception) {}
121
145
  @allow_localhost_redirects = true
122
146
  @api_key_token_prefix = 'amcp_'
123
147
  @default_client_name = 'MCP Client'
148
+ @persist_feedback = false
149
+ @feedback_tool = true
124
150
  @sidekiq_stats_provider = nil
125
151
  end
126
152
 
@@ -155,18 +181,21 @@ end
155
181
  | `identity_fallback` | returns `nil` | Proc taking the request, returning an `Authentication::Identity` or nil, consulted when no credential the engine issued matched |
156
182
  | `oauth` | `true` | Whether the engine serves its own OAuth 2.1 server. False draws no OAuth routes and never looks an access token up |
157
183
  | `sign_in` | `nil` (renders 401) | Proc taking the OAuth controller, sending an anonymous visitor to sign in |
158
- | `admin_class_name` | `'Administrator'` | Class the `admin_id` column points at |
184
+ | `admin_class_name` | `'Administrator'` | Class the admin foreign key column points at |
185
+ | `admin_foreign_key` | `:admin_id` | Column on the engine's own tables that stores the owning admin's id. The association is still called `admin`; only the column name changes |
159
186
  | `authorization` | Pundit if defined, else Permissive | Adapter: `authorize!`, `authorized?`, `authorize_roles!` |
160
187
  | `default_required_roles` | `[]` | Roles a tool requires unless it declares its own |
161
188
  | `tool_paths` | `[]` | Directories scanned for extra `BaseTool` subclasses |
162
189
  | `dashboard_paths` | `app/dashboards` | Where dashboards are found |
163
190
  | `instrument` | yields | Around hook `(tool_name:, admin:, &block)` |
164
191
  | `on_tool_call` | no-op | Audit hook `(tool_name:, admin:, arguments:, scopes:)`, after the permission checks |
165
- | `on_feedback` | no-op | Called with each new `Feedback` record |
192
+ | `on_feedback` | no-op | Called with each `FeedbackReport`, whether or not it was persisted |
166
193
  | `on_error` | no-op | Called with an exception the engine swallowed, so you can report it |
167
194
  | `allow_localhost_redirects` | `true` | Whether loopback OAuth redirect URIs are accepted. Inert when `oauth` is false |
168
195
  | `api_key_token_prefix` | `'amcp_'` | Prefix that marks a bearer token as an API key. Cannot be blank |
169
196
  | `default_client_name` | `'MCP Client'` | Name given to a dynamically registered client that sends no `client_name`. Inert when `oauth` is false |
197
+ | `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
+ | `feedback_tool` | `true` | Whether `report_mcp_improvement` is published |
170
199
  | `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 |
171
200
  | `admin_route_namespace` | `:admin` | Namespace record URLs are built from |
172
201
  | `admin_url_options` | `{}` | Options passed to `polymorphic_url`; when no `:host` is given, host, protocol and port are taken from `admin_origin` |
@@ -44,6 +44,7 @@ module Administrate
44
44
  :oauth,
45
45
  :sign_in,
46
46
  :admin_class_name,
47
+ :admin_foreign_key,
47
48
  :authorization,
48
49
  :default_required_roles,
49
50
  :tool_paths,
@@ -54,6 +55,8 @@ module Administrate
54
55
  :on_error,
55
56
  :allow_localhost_redirects,
56
57
  :default_client_name,
58
+ :persist_feedback,
59
+ :feedback_tool,
57
60
  :sidekiq_stats_provider,
58
61
  :admin_route_namespace,
59
62
  :admin_url_options,
@@ -80,6 +83,7 @@ module Administrate
80
83
  @oauth = true
81
84
  @sign_in = nil
82
85
  @admin_class_name = 'Administrator'
86
+ @admin_foreign_key = :admin_id
83
87
  @authorization = default_authorization
84
88
  @default_required_roles = []
85
89
  @tool_paths = []
@@ -89,11 +93,13 @@ module Administrate
89
93
  def assign_hooks
90
94
  @instrument = ->(tool_name:, admin:, &block) { block.call } # rubocop:disable Lint/UnusedBlockArgument
91
95
  @on_tool_call = ->(tool_name:, admin:, arguments:, scopes:) {}
92
- @on_feedback = ->(feedback) {}
96
+ @on_feedback = ->(report) {}
93
97
  @on_error = ->(exception) {}
94
98
  @allow_localhost_redirects = true
95
99
  @api_key_token_prefix = 'amcp_'
96
100
  @default_client_name = 'MCP Client'
101
+ @persist_feedback = false
102
+ @feedback_tool = true
97
103
  @sidekiq_stats_provider = nil
98
104
  end
99
105
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Administrate
4
4
  module MCP
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sorare
@@ -84,6 +84,11 @@ files:
84
84
  - README.md
85
85
  - app/controllers/administrate/mcp/json_rpc_controller.rb
86
86
  - app/controllers/administrate/mcp/o_auth_controller.rb
87
+ - app/controllers/concerns/administrate/mcp/api_keys_admin.rb
88
+ - app/controllers/concerns/administrate/mcp/feedbacks_admin.rb
89
+ - app/controllers/concerns/administrate/mcp/resource_controller.rb
90
+ - app/dashboards/administrate/mcp/api_key_dashboard.rb
91
+ - app/dashboards/administrate/mcp/feedback_dashboard.rb
87
92
  - app/lib/administrate/mcp/actions.rb
88
93
  - app/lib/administrate/mcp/admin_dashboard_tool.rb
89
94
  - app/lib/administrate/mcp/authentication.rb
@@ -91,9 +96,12 @@ files:
91
96
  - app/lib/administrate/mcp/clean_old_feedbacks.rb
92
97
  - app/lib/administrate/mcp/dashboard_registry.rb
93
98
  - app/lib/administrate/mcp/fast_search.rb
99
+ - app/lib/administrate/mcp/feedback_categories.rb
100
+ - app/lib/administrate/mcp/feedback_report.rb
94
101
  - app/lib/administrate/mcp/field_serializer.rb
95
102
  - app/lib/administrate/mcp/o_auth_service.rb
96
103
  - app/lib/administrate/mcp/report_improvement.rb
104
+ - app/lib/administrate/mcp/resource_resolver.rb
97
105
  - app/lib/administrate/mcp/server_builder.rb
98
106
  - app/lib/administrate/mcp/tools/admin_resource_list.rb
99
107
  - app/lib/administrate/mcp/tools/admin_resource_list_resources.rb