solidus_mcp 0.2.0.rc1 → 0.2.0.rc2
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 +8 -0
- data/CONTRIBUTING.md +51 -4
- data/README.md +91 -0
- data/app/services/solidus_mcp/serializers.rb +28 -4
- data/lib/solidus_mcp/configuration.rb +19 -1
- data/lib/solidus_mcp/engine.rb +1 -0
- data/lib/solidus_mcp/policy.rb +5 -1
- data/lib/solidus_mcp/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5922154aa4d65341aecb0312e8a1a72d20ef1e81a5e423f000eaa6f6239e4fd
|
|
4
|
+
data.tar.gz: 2ff83f035e9b9268c9df907b4f40004b1b3528af8c30ff033b8b3fe4493847b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd23eee0805800c0de1d3a939c1ea99358aef8ad711b3437f4c002c17a67b5da3388dd0fa37dfc7fa0bb48ceb0cf10b588e7b8e3e8f92db836f13ada990141ba
|
|
7
|
+
data.tar.gz: a8aba2d7d878b8993289046e915eba690d8dd70a7fd1fc9fe241337174bfd12b741fbcc305658b809cceab09002179533b8fc43f43fbb4f2bbc8765cd1eae4a5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Add optional product and order serializer hooks for built-in read tools, with
|
|
6
|
+
a plain Ruby interface and an Alba adapter example. Preserve record authorization,
|
|
7
|
+
pagination, and tool-owned order line items; resolve class names across Rails reloads.
|
|
8
|
+
- Add `Policy#can?` so serializers can omit inaccessible optional related fields
|
|
9
|
+
while retaining strict authorization for required records.
|
|
10
|
+
|
|
3
11
|
## 0.2.0.rc1 (2026-11-08)
|
|
4
12
|
|
|
5
13
|
First public release candidate. The earlier registry implementation was unpublished.
|
data/CONTRIBUTING.md
CHANGED
|
@@ -25,7 +25,54 @@ The direct migration command is `bin/rails solidus_mcp:migrate` after host
|
|
|
25
25
|
migrations. Keep migrations immutable and safe alongside the previous release.
|
|
26
26
|
Do not run down migrations automatically during deployment rollback.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
## Releasing
|
|
29
|
+
|
|
30
|
+
Pushing a version tag triggers `.github/workflows/release.yml`. The workflow
|
|
31
|
+
requires the tag to match `SolidusMcp::VERSION` exactly (for example,
|
|
32
|
+
`v0.2.0.rc2`) and its commit to be on `main`. It runs the full Rails/admin CI
|
|
33
|
+
matrix before building and publishing the gem to RubyGems. Ordinary branch pushes
|
|
34
|
+
and pull requests do not publish. The built gem is retained as a workflow artifact.
|
|
35
|
+
Publishing does not update or deploy any host application.
|
|
36
|
+
|
|
37
|
+
### One-time setup
|
|
38
|
+
|
|
39
|
+
1. In this repository's GitHub **Settings > Environments**, create `rubygems`.
|
|
40
|
+
Restrict its deployment tags to `v*`; optionally require a release reviewer.
|
|
41
|
+
Allow version tags to be created only by release maintainers using a GitHub
|
|
42
|
+
tag ruleset. Tagged code executes in the publishing workflow, so access to
|
|
43
|
+
create release tags is release authority.
|
|
44
|
+
2. On RubyGems.org, open the `solidus_mcp` gem's **Settings > Trusted Publishers**
|
|
45
|
+
and add a GitHub Actions publisher with these exact values:
|
|
46
|
+
- Repository owner: `michaelmichael`
|
|
47
|
+
- Repository name: `solidus_mcp`
|
|
48
|
+
- Workflow filename: `release.yml`
|
|
49
|
+
- Environment: `rubygems`
|
|
50
|
+
|
|
51
|
+
The workflow uses OIDC to obtain short-lived publishing credentials. Do not add
|
|
52
|
+
a `RUBYGEMS_API_KEY` or other long-lived publishing secret to GitHub. The
|
|
53
|
+
`id-token: write` permission is limited to the publishing job, after tests pass.
|
|
54
|
+
|
|
55
|
+
### Each release
|
|
56
|
+
|
|
57
|
+
1. Bump `lib/solidus_mcp/version.rb` and move the applicable `CHANGELOG.md`
|
|
58
|
+
entries into a section for that version.
|
|
59
|
+
2. Run `bin/ci` (or `docker compose run --rm test`) and review the package and
|
|
60
|
+
changelog. Merge the version change into `main` and wait for CI to pass.
|
|
61
|
+
3. From a clean checkout of the merged `main` commit, create and push the tag:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git switch main
|
|
65
|
+
git pull --ff-only
|
|
66
|
+
test -z "$(git status --porcelain)"
|
|
67
|
+
version=$(ruby -r ./lib/solidus_mcp/version -e 'print SolidusMcp::VERSION')
|
|
68
|
+
git tag -a "v${version}" -m "Release ${version}"
|
|
69
|
+
git push origin "v${version}"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
4. Watch the **Release gem** workflow, approve the `rubygems` deployment if
|
|
73
|
+
required, and verify the new version on RubyGems.org before updating hosts.
|
|
74
|
+
|
|
75
|
+
Do not move a published tag or try to replace a published gem. Fixes need a new
|
|
76
|
+
version. A failed run can be re-run if publishing never succeeded; if the gem is
|
|
77
|
+
already on RubyGems, do not re-run the publish job (RubyGems rejects duplicates).
|
|
78
|
+
Verify the real host integration before production promotion.
|
data/README.md
CHANGED
|
@@ -230,6 +230,97 @@ The built-in price update tools use Solidus's standard pricing options. Stores w
|
|
|
230
230
|
|
|
231
231
|
Product and price updates take effect immediately. There are currently no built-in order or inventory mutation tools. Add your own, as desired.
|
|
232
232
|
|
|
233
|
+
## Custom serializers
|
|
234
|
+
|
|
235
|
+
You can change the product and order fields returned by the built-in tools without replacing those tools. Configure a serializer class in your initializer:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
config.product_serializer = 'Mcp::ProductSerializer'
|
|
239
|
+
# config.order_serializer = 'Mcp::OrderSerializer'
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Both settings are optional. Leave one unset to keep that record's default representation. Use class-name strings so Rails can reload your serializers in development.
|
|
243
|
+
|
|
244
|
+
Each class implements `.call(record, context:)` and returns a JSON-compatible Ruby hash. For example, in `app/serializers/mcp/product_serializer.rb`:
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
module Mcp
|
|
248
|
+
class ProductSerializer
|
|
249
|
+
def self.call(product, context:)
|
|
250
|
+
{
|
|
251
|
+
id: product.id,
|
|
252
|
+
name: product.name,
|
|
253
|
+
display_name: "#{product.name} (#{product.slug})"
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The returned hash replaces the default fields; it isn't merged with them. You choose which fields to include. The same interface works for orders.
|
|
261
|
+
|
|
262
|
+
| Setting | Used by |
|
|
263
|
+
| --- | --- |
|
|
264
|
+
| `product_serializer` | `search_products` and `get_product` |
|
|
265
|
+
| `order_serializer` | `search_orders` and `get_order` |
|
|
266
|
+
|
|
267
|
+
Pagination stays under the tool's control. `get_order` still adds its authorized, bounded line items and truncation indicator, so order serializers cannot return `line_items` or `line_items_truncated`. Variants, prices, inventory, order status, and write responses keep their existing representations.
|
|
268
|
+
|
|
269
|
+
### Using Alba or another library
|
|
270
|
+
|
|
271
|
+
The hook accepts plain Ruby classes or adapters for your preferred serialization library. Add that library to your app's Gemfile; it is not a runtime dependency of Solidus MCP.
|
|
272
|
+
|
|
273
|
+
For Alba, define `app/serializers/mcp/product_resource.rb`:
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
module Mcp
|
|
277
|
+
class ProductResource
|
|
278
|
+
include Alba::Resource
|
|
279
|
+
|
|
280
|
+
attributes :id, :name, :slug
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Then use this adapter in `app/serializers/mcp/product_serializer.rb`:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
module Mcp
|
|
289
|
+
class ProductSerializer
|
|
290
|
+
def self.call(product, context:)
|
|
291
|
+
ProductResource.new(product, params: {policy: context}).to_h
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Return a hash, not an encoded JSON string. For another library, adapt its output to the same interface.
|
|
298
|
+
|
|
299
|
+
### Authorization and field access
|
|
300
|
+
|
|
301
|
+
The gem checks that the connected user can read the product or order before calling your serializer. If you include information from related records, your serializer must check access to those records as well. For example, a store may allow someone to read an order's totals while restricting access to its shipping address.
|
|
302
|
+
|
|
303
|
+
For optional information, use `context.can?` to check access and omit the fields when the record is missing or access is denied:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
def self.call(order, context:)
|
|
307
|
+
result = {number: order.number}
|
|
308
|
+
address = order.ship_address
|
|
309
|
+
result[:shipping_city] = address.city if context.can?(:read, address)
|
|
310
|
+
result
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The order still comes back if the shipping address is inaccessible; it simply has no `shipping_city` field. Clients should not assume an omitted field means no related record exists. The check uses the store's existing Solidus abilities and returns `false` for a missing record.
|
|
315
|
+
|
|
316
|
+
This does not create a separate MCP permission for addresses. An account with broad Solidus access will pass these record checks. Your store controls which fields its serializers expose.
|
|
317
|
+
|
|
318
|
+
If a related record is essential to the response, use `context.authorize_record!(:read, record)` instead. It raises an access-denied error and fails the call when the record is missing or access is denied. Access to the primary product or order is always required.
|
|
319
|
+
|
|
320
|
+
For related collections, use `context.scope(Model)` to return only records the connected user can read. These helpers do not automatically restrict the queries or associations you access elsewhere in the serializer.
|
|
321
|
+
|
|
322
|
+
These hooks do not add field-level permissions. Serializer changes apply to existing connections with access to the affected tools. To expose sensitive information under a separate grant, use a custom tool with its own permission key. Serializers are trusted application code, just like custom tools.
|
|
323
|
+
|
|
233
324
|
## Custom tools
|
|
234
325
|
|
|
235
326
|
To create your own custom MCP tools, add a named subclass of `SolidusMcp::Tool` under `app/tools`. For example, `app/tools/count_products.rb`:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SolidusMcp
|
|
4
|
-
#
|
|
4
|
+
# Record authorization stays outside host serialization. Custom serializers are
|
|
5
|
+
# trusted application code and must authorize any related records they expose.
|
|
5
6
|
class Serializers
|
|
6
7
|
def initialize(policy)
|
|
7
8
|
@policy = policy
|
|
@@ -9,7 +10,9 @@ module SolidusMcp
|
|
|
9
10
|
|
|
10
11
|
def product(record)
|
|
11
12
|
@policy.authorize_record!(:read, record)
|
|
12
|
-
|
|
13
|
+
serialize(:product, record) do
|
|
14
|
+
record.attributes.slice("id", "name", "slug", "description", "meta_title", "meta_description", "meta_keywords")
|
|
15
|
+
end
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def variant(record)
|
|
@@ -36,8 +39,10 @@ module SolidusMcp
|
|
|
36
39
|
|
|
37
40
|
def order(record)
|
|
38
41
|
@policy.authorize_record!(:read, record)
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
serialize(:order, record) do
|
|
43
|
+
status(record).merge(currency: record.currency, total: record.total.to_s("F"),
|
|
44
|
+
item_total: record.item_total.to_s("F"), completed_at: record.completed_at&.iso8601)
|
|
45
|
+
end
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
def status(record)
|
|
@@ -45,5 +50,24 @@ module SolidusMcp
|
|
|
45
50
|
{id: record.id, number: record.number, state: record.state,
|
|
46
51
|
payment_state: record.payment_state, shipment_state: record.shipment_state}
|
|
47
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def serialize(kind, record)
|
|
57
|
+
serializer = SolidusMcp.configuration.serializer_class(kind)
|
|
58
|
+
return yield unless serializer
|
|
59
|
+
|
|
60
|
+
result = serializer.call(record, context: @policy)
|
|
61
|
+
valid = result.is_a?(Hash) && result.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
|
|
62
|
+
unless valid
|
|
63
|
+
raise ToolError, "#{serializer.name} must return a Hash with string or symbol keys, not encoded JSON"
|
|
64
|
+
end
|
|
65
|
+
keys = result.keys.map(&:to_s)
|
|
66
|
+
raise ToolError, "#{serializer.name} returned duplicate field names" unless keys.uniq == keys
|
|
67
|
+
if kind == :order && (keys & %w[line_items line_items_truncated]).any?
|
|
68
|
+
raise ToolError, "Order serializers cannot replace line_items or line_items_truncated"
|
|
69
|
+
end
|
|
70
|
+
result.dup
|
|
71
|
+
end
|
|
48
72
|
end
|
|
49
73
|
end
|
|
@@ -5,7 +5,8 @@ require "uri"
|
|
|
5
5
|
module SolidusMcp
|
|
6
6
|
class Configuration
|
|
7
7
|
attr_accessor :custom_tools, :endpoint_url, :issuer_url, :allowed_origins,
|
|
8
|
-
:server_name, :instructions, :resource_owner_authenticator, :default_shipping_category_id, :superadmin_emails
|
|
8
|
+
:server_name, :instructions, :resource_owner_authenticator, :default_shipping_category_id, :superadmin_emails,
|
|
9
|
+
:product_serializer, :order_serializer
|
|
9
10
|
|
|
10
11
|
def initialize
|
|
11
12
|
@superadmin_emails = ENV.fetch("SOLIDUS_MCP_SUPERADMIN_EMAILS", "").split(",").map { |email| email.strip.downcase }.uniq
|
|
@@ -23,6 +24,23 @@ module SolidusMcp
|
|
|
23
24
|
endpoint_url.present? && issuer_url.present?
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
def serializer_class(kind)
|
|
28
|
+
name = public_send(:"#{kind}_serializer")
|
|
29
|
+
return if name.nil?
|
|
30
|
+
unless name.is_a?(String) && name.present?
|
|
31
|
+
raise ArgumentError, "#{kind}_serializer must be a class-name string or nil"
|
|
32
|
+
end
|
|
33
|
+
serializer = name.constantize
|
|
34
|
+
unless serializer.is_a?(Class) && serializer.respond_to?(:call)
|
|
35
|
+
raise ArgumentError, "#{name} must implement .call(record, context:)"
|
|
36
|
+
end
|
|
37
|
+
serializer
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate_serializers!
|
|
41
|
+
%i[product order].each { |kind| serializer_class(kind) }
|
|
42
|
+
end
|
|
43
|
+
|
|
26
44
|
def validate!
|
|
27
45
|
[endpoint_url, issuer_url].each do |value|
|
|
28
46
|
uri = URI.parse(value.to_s)
|
data/lib/solidus_mcp/engine.rb
CHANGED
data/lib/solidus_mcp/policy.rb
CHANGED
|
@@ -31,10 +31,14 @@ module SolidusMcp
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def authorize_record!(action, record)
|
|
34
|
-
raise AccessDenied, "Record is unavailable" unless
|
|
34
|
+
raise AccessDenied, "Record is unavailable" unless can?(action, record)
|
|
35
35
|
record
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
def can?(action, record)
|
|
39
|
+
!record.nil? && ability.can?(action, record)
|
|
40
|
+
end
|
|
41
|
+
|
|
38
42
|
def scope(model, action = :read)
|
|
39
43
|
model.accessible_by(ability, action)
|
|
40
44
|
end
|
data/lib/solidus_mcp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solidus_mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.0.
|
|
4
|
+
version: 0.2.0.rc2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Caviness
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: solidus_core
|