portage-ucp-client 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 96ae3a5da5ef868663d2b296539ef3812249479031f6237f0c4dbb5679dda268
4
+ data.tar.gz: 7b132dc0d5c2152a2bba93db9629e5c897331a5360d95894cc84028fb9052ee9
5
+ SHA512:
6
+ metadata.gz: 88439c696a4d6c6b4ead359c4b463b00fc07f02e811774096e745d7e95880b45b67ec3d948c5beffea0f0bd56456834305c774cc4d2b0a7281846840fd526bc3
7
+ data.tar.gz: d0965725d9e6914ae0bd19a0f2e8acabd182c943d01d0cb810458e3edc9d9d3001cf67bea1931eca3258b0315f32328e47b156a752c5f935bc07772d1ac9d0b3
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format loosely follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.0.0/); this project is
5
+ pre-1.0, so APIs may still shift between minor versions.
6
+
7
+ ## [0.1.0] - Unreleased
8
+
9
+ - Initial pre-release. Client-side SDK — loopback, stdio, and Streamable HTTP
10
+ transports behind one `Session` interface.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tom Whitbread
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # portage-ucp-client
2
+
3
+ Client-side SDK for [`portage-ucp`](https://github.com/tomtom87/Portage/tree/main/portage-ucp)
4
+ — the other direction from every adapter gem. Every adapter gem lets a Ruby
5
+ program *expose* a commerce backend to agents (server side). This gem lets a
6
+ Ruby program *act as* the shopper's agent: connect to somebody else's
7
+ `/.well-known/ucp` manifest, or drive your own `Adapter` directly, and place an
8
+ order as the client.
9
+
10
+ Three transports behind one `Session` interface — callers never know which they
11
+ got:
12
+
13
+ | Transport | Use case |
14
+ |---|---|
15
+ | `Portage::Ucp::Client.for_adapter(adapter)` | Loopback over an in-process `Adapter` — no subprocess, no socket. Still runs the real authenticator/rate-limiter/`Dispatcher` stack, just without the wire hop. For driving your own store's `Adapter` directly. |
16
+ | `Portage::Ucp::Client.connect(command: ...)` | stdio, spawns a subprocess (an MCP server exe). |
17
+ | `Portage::Ucp::Client.connect(url: ...)` | Streamable HTTP, connects to a remote MCP endpoint. |
18
+ | `Portage::Ucp::Client.discover(url)` | Fetches `<url>/.well-known/ucp`, parses the manifest, and connects to whatever transport it advertises — the entry point for buying from a store you've never talked to before. |
19
+
20
+ Depends only on `portage-ucp` and the `mcp` gem's client half — no adapter gem is
21
+ a dependency.
22
+
23
+ ## Installation
24
+
25
+ ```ruby
26
+ # Gemfile
27
+ gem "portage-ucp-client"
28
+ ```
29
+
30
+ ```bash
31
+ bundle install
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```ruby
37
+ require "portage/ucp"
38
+ require "portage/ucp/client"
39
+
40
+ # Discover and connect to a live store you've never talked to before:
41
+ session = Portage::Ucp::Client.discover("https://your-shop.example")
42
+
43
+ products = session.search_catalog(query: "snowboard", limit: 5)
44
+ checkout = session.create_checkout(line_items: [{ product_id: products.first.id, quantity: 1 }])
45
+ completed = session.complete_checkout(checkout_id: checkout["id"], payment_token: "spt_1a2b3c...")
46
+ order = session.get_order(order_id: "gid://shopify/Order/9001")
47
+ ```
48
+
49
+ `Session` generates an `idempotency_key` for you on every mutating call unless you
50
+ pass your own — retrying with the same key replays the cached result instead of
51
+ double-charging.
52
+
53
+ See the
54
+ [detailed walkthrough](https://github.com/tomtom87/Portage/blob/main/docs/walkthrough.md)
55
+ for the full runnable example (via the loopback transport) including
56
+ `requires_escalation` handling and `RawPanRejectedError`, and
57
+ [`portage-cli`](https://github.com/tomtom87/Portage/tree/main/portage-cli) for a
58
+ ready-made `portage buy <url>` command built on top of this gem.
59
+
60
+ ## Development
61
+
62
+ ```bash
63
+ bundle exec rspec
64
+ bundle exec rubocop
65
+ ```
66
+
67
+ ## License
68
+
69
+ [MIT](LICENSE) — Copyright (c) 2026 Tom Whitbread.
@@ -0,0 +1,19 @@
1
+ module Portage
2
+ module Ucp
3
+ module Client
4
+ # Base class for every error this gem raises.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when a tool call's JSON-RPC response comes back with
8
+ # `isError: true` — e.g. an AuthenticationError/RateLimitExceededError/
9
+ # RawPanRejectedError the server side surfaced. `#message` is the text
10
+ # content the server returned, not a generic string.
11
+ class ServerError < Error; end
12
+
13
+ # Raised by .discover when the manifest can't be fetched/parsed, or has
14
+ # no `services` entry for the mcp transport to connect to (see
15
+ # Portage::Ucp::Manifest#services — the core-gem fix this depends on).
16
+ class DiscoveryError < Error; end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,90 @@
1
+ require "securerandom"
2
+
3
+ module Portage
4
+ module Ucp
5
+ module Client
6
+ # Caller-facing object returned by Client.for_adapter/.connect/.discover
7
+ # — same convenience method names as the merchant-side
8
+ # Portage::Ucp::Adapter, regardless of which transport is underneath
9
+ # (loopback/stdio/HTTP; callers never know which they got).
10
+ #
11
+ # Generates an `idempotency_key` per mutating call unless the caller
12
+ # supplies one, and runs PaymentTokenGuard client-side before a
13
+ # payment_token goes out over complete_checkout — belt-and-suspenders
14
+ # with the merchant's own guard (§9), not a replacement for it.
15
+ #
16
+ # A Checkout/Order response with `status == "requires_escalation"` is
17
+ # returned normally, with its `links`, not raised as an error — callers
18
+ # must branch on it themselves.
19
+ class Session
20
+ MUTATING_ACTIONS = %w[create_cart update_cart cancel_cart create_checkout update_checkout
21
+ complete_checkout cancel_checkout].freeze
22
+
23
+ # @param capabilities [Array<String>, nil] reverse-domain capability
24
+ # names advertised by the server, when known upfront (Client.discover
25
+ # populates this from the manifest; Client.for_adapter/.connect leave
26
+ # it nil since nothing was fetched to populate it from).
27
+ def initialize(transport:, capabilities: nil)
28
+ @transport = transport
29
+ @capabilities = capabilities
30
+ end
31
+
32
+ attr_reader :capabilities
33
+
34
+ # @return [Boolean, nil] nil when capabilities weren't known upfront
35
+ # (see #capabilities) — callers with a nil result can't tell either
36
+ # way and should just attempt the call.
37
+ def advertises?(capability_name)
38
+ capabilities&.include?(capability_name)
39
+ end
40
+
41
+ def search_catalog(query:, limit: 20) = call("search_catalog", query: query, limit: limit)
42
+ def get_product(product_id:) = call("get_product", product_id: product_id)
43
+
44
+ def get_cart(cart_id:) = call("get_cart", cart_id: cart_id)
45
+
46
+ def create_cart(line_items:, idempotency_key: nil)
47
+ call("create_cart", line_items: line_items, idempotency_key: idempotency_key)
48
+ end
49
+
50
+ def update_cart(cart_id:, line_items:, idempotency_key: nil)
51
+ call("update_cart", cart_id: cart_id, line_items: line_items, idempotency_key: idempotency_key)
52
+ end
53
+
54
+ def cancel_cart(cart_id:, idempotency_key: nil)
55
+ call("cancel_cart", cart_id: cart_id, idempotency_key: idempotency_key)
56
+ end
57
+
58
+ def create_checkout(line_items:, idempotency_key: nil)
59
+ call("create_checkout", line_items: line_items, idempotency_key: idempotency_key)
60
+ end
61
+
62
+ def get_checkout(checkout_id:) = call("get_checkout", checkout_id: checkout_id)
63
+
64
+ def update_checkout(checkout_id:, line_items:, idempotency_key: nil)
65
+ call("update_checkout", checkout_id: checkout_id, line_items: line_items, idempotency_key: idempotency_key)
66
+ end
67
+
68
+ def complete_checkout(checkout_id:, payment_token:, idempotency_key: nil)
69
+ Portage::Ucp::PaymentTokenGuard.validate!(payment_token)
70
+ call("complete_checkout", checkout_id: checkout_id, payment_token: payment_token,
71
+ idempotency_key: idempotency_key)
72
+ end
73
+
74
+ def cancel_checkout(checkout_id:, idempotency_key: nil)
75
+ call("cancel_checkout", checkout_id: checkout_id, idempotency_key: idempotency_key)
76
+ end
77
+
78
+ def get_order(order_id:) = call("get_order", order_id: order_id)
79
+ def link_identity(oauth_token:) = call("link_identity", oauth_token: oauth_token)
80
+
81
+ private
82
+
83
+ def call(action, **arguments)
84
+ arguments[:idempotency_key] ||= SecureRandom.uuid if MUTATING_ACTIONS.include?(action)
85
+ @transport.call_tool(name: action, arguments: arguments)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,29 @@
1
+ module Portage
2
+ module Ucp
3
+ module Client
4
+ # Normalizes a tools/call JSON-RPC response into either the tool's
5
+ # structuredContent, or a raised ServerError — shared across transports
6
+ # since the loopback transport gets a symbol-keyed response (no JSON
7
+ # round-trip, see Transports::Loopback) while stdio/HTTP get a
8
+ # string-keyed one (real wire JSON, parsed by the `mcp` gem's client).
9
+ module ToolResult
10
+ def self.extract(response, symbol_keys:)
11
+ result = fetch(response, "result", symbol_keys) || {}
12
+ content = fetch(result, "content", symbol_keys)
13
+ raise ServerError, text(content, symbol_keys: symbol_keys) if fetch(result, "isError", symbol_keys)
14
+
15
+ fetch(result, "structuredContent", symbol_keys)
16
+ end
17
+
18
+ def self.text(content, symbol_keys:)
19
+ Array(content).filter_map { |block| fetch(block, "text", symbol_keys) }.join(" ")
20
+ end
21
+
22
+ def self.fetch(hash, key, symbol_keys)
23
+ hash[symbol_keys ? key.to_sym : key]
24
+ end
25
+ private_class_method :fetch
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ module Portage
2
+ module Ucp
3
+ module Client
4
+ module Transports
5
+ # Connects to a UCP/MCP server over Streamable HTTP via the official
6
+ # `mcp` gem's client half (MCP::Client + MCP::Client::HTTP). Performs
7
+ # the `initialize` handshake eagerly so a caller's first real call
8
+ # doesn't pay for it.
9
+ class Http
10
+ def initialize(url:, headers: {})
11
+ @client = ::MCP::Client.new(transport: ::MCP::Client::HTTP.new(url: url, headers: headers))
12
+ @client.connect
13
+ end
14
+
15
+ def call_tool(name:, arguments:)
16
+ response = @client.call_tool(name: name, arguments: arguments)
17
+ ToolResult.extract(response, symbol_keys: false)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module Portage
2
+ module Ucp
3
+ module Client
4
+ module Transports
5
+ # Wraps a Portage::Ucp::Mcp::Server built directly from an Adapter —
6
+ # no subprocess, no socket. This still runs the exact request a real
7
+ # server would handle over stdio/HTTP (authenticator, rate limiter,
8
+ # Dispatcher, WireEnvelope — see Portage::Ucp::Mcp::Server.build), it
9
+ # just skips the wire hop. This is what makes the
10
+ # merchant-buys-from-their-own-store case, and specs/examples that
11
+ # want a full buy cycle, possible without two processes.
12
+ class Loopback
13
+ def initialize(adapter:, **server_opts)
14
+ @server = Portage::Ucp::Mcp::Server.build(adapter: adapter, **server_opts)
15
+ @next_id = 0
16
+ end
17
+
18
+ def call_tool(name:, arguments:)
19
+ @next_id += 1
20
+ response = @server.handle(
21
+ { jsonrpc: "2.0", id: @next_id, method: "tools/call", params: { name: name, arguments: arguments } }
22
+ )
23
+ ToolResult.extract(response, symbol_keys: true)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ module Portage
2
+ module Ucp
3
+ module Client
4
+ module Transports
5
+ # Connects to a UCP/MCP server over stdio — a subprocess speaking
6
+ # JSON-RPC on stdin/stdout — via the official `mcp` gem's client half
7
+ # (MCP::Client + MCP::Client::Stdio). Performs the `initialize`
8
+ # handshake eagerly so a caller's first real call doesn't pay for it.
9
+ class Stdio
10
+ def initialize(command:, args: [], env: nil)
11
+ @client = ::MCP::Client.new(transport: ::MCP::Client::Stdio.new(command: command, args: args, env: env))
12
+ @client.connect
13
+ end
14
+
15
+ def call_tool(name:, arguments:)
16
+ response = @client.call_tool(name: name, arguments: arguments)
17
+ ToolResult.extract(response, symbol_keys: false)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ module Portage
2
+ module Ucp
3
+ module Client
4
+ VERSION = "0.1.0".freeze
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,88 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "json"
4
+ require "mcp"
5
+ require "portage/ucp"
6
+
7
+ require_relative "client/version"
8
+ require_relative "client/errors"
9
+ require_relative "client/tool_result"
10
+ require_relative "client/session"
11
+ require_relative "client/transports/loopback"
12
+ require_relative "client/transports/stdio"
13
+ require_relative "client/transports/http"
14
+
15
+ module Portage
16
+ module Ucp
17
+ # Client-side counterpart to the rest of portage-ucp: everything else in
18
+ # this repo lets a Ruby program *expose* a commerce backend to agents
19
+ # (server side). This gem lets a Ruby program *act as* the shopper's
20
+ # agent — discover somebody else's manifest, or drive an owner's own
21
+ # Adapter directly, and place an order.
22
+ module Client
23
+ MANIFEST_PATH = "/.well-known/ucp".freeze
24
+
25
+ # Wraps an already-built Adapter directly — no subprocess/socket. Still
26
+ # runs the real merchant-side contract (authenticator, rate limiter,
27
+ # Dispatcher, WireEnvelope, via Portage::Ucp::Mcp::Server), just
28
+ # in-process. This is the transport for the "your own store" case: you
29
+ # already have credentials for this Adapter, so there's no manifest to
30
+ # discover and no wire hop to make.
31
+ def self.for_adapter(adapter, **server_opts)
32
+ Session.new(transport: Transports::Loopback.new(adapter: adapter, **server_opts))
33
+ end
34
+
35
+ # Connects over stdio (a subprocess) or Streamable HTTP (a URL) — pass
36
+ # exactly one of `command:` or `url:`.
37
+ def self.connect(command: nil, args: [], env: nil, url: nil, headers: {}, capabilities: nil)
38
+ transport = if command
39
+ Transports::Stdio.new(command: command, args: args, env: env)
40
+ elsif url
41
+ Transports::Http.new(url: url, headers: headers)
42
+ else
43
+ raise ArgumentError, "connect requires either command: or url:"
44
+ end
45
+ Session.new(transport: transport, capabilities: capabilities)
46
+ end
47
+
48
+ # GETs `<url>/.well-known/ucp`, parses the manifest, and connects to the
49
+ # `mcp`-transport endpoint it advertises in `services` (see
50
+ # Portage::Ucp::Manifest#services — the core-gem fix this client
51
+ # depends on to know where to connect).
52
+ # @return [Session] scoped to the manifest's advertised capabilities.
53
+ def self.discover(url)
54
+ manifest = fetch_manifest(url)
55
+ connect(url: mcp_endpoint(manifest), capabilities: capability_names(manifest))
56
+ end
57
+
58
+ def self.fetch_manifest(url)
59
+ uri = URI.parse("#{url.to_s.sub(%r{/\z}, '')}#{MANIFEST_PATH}")
60
+ response = Net::HTTP.get_response(uri)
61
+ raise DiscoveryError, "GET #{uri} returned #{response.code}" unless response.is_a?(Net::HTTPSuccess)
62
+
63
+ JSON.parse(response.body)
64
+ rescue JSON::ParserError => e
65
+ raise DiscoveryError, "manifest at #{url} is not valid JSON: #{e.message}"
66
+ rescue DiscoveryError
67
+ raise
68
+ rescue StandardError => e
69
+ raise DiscoveryError, "couldn't reach #{url}: #{e.class}: #{e.message}"
70
+ end
71
+ private_class_method :fetch_manifest
72
+
73
+ def self.mcp_endpoint(manifest)
74
+ service = Array(manifest["services"]).find { |s| s["transport"] == "mcp" }
75
+ endpoint = service && service["endpoint"]
76
+ raise DiscoveryError, "manifest has no mcp service entry to connect to" unless endpoint
77
+
78
+ endpoint
79
+ end
80
+ private_class_method :mcp_endpoint
81
+
82
+ def self.capability_names(manifest)
83
+ Array(manifest["capabilities"]).map { |c| c["name"] }
84
+ end
85
+ private_class_method :capability_names
86
+ end
87
+ end
88
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: portage-ucp-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tom Whitbread
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mcp
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.24'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.24'
27
+ - !ruby/object:Gem::Dependency
28
+ name: portage-ucp
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.88'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.88'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.24'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.24'
83
+ - !ruby/object:Gem::Dependency
84
+ name: yard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.9'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.9'
97
+ description: 'Every other portage-ucp gem lets a Ruby program expose a commerce backend
98
+ to agents (server side). This gem is the other direction: connect to somebody else''s
99
+ /.well-known/ucp manifest, or drive your own Adapter directly, and place an order
100
+ as the client. Three transports behind one interface (loopback over an in-process
101
+ Adapter, stdio, Streamable HTTP) — callers never know which they got. Depends only
102
+ on portage-ucp + the mcp gem''s client half; no adapter gem is a dependency.'
103
+ email:
104
+ executables: []
105
+ extensions: []
106
+ extra_rdoc_files: []
107
+ files:
108
+ - CHANGELOG.md
109
+ - LICENSE
110
+ - README.md
111
+ - lib/portage/ucp/client.rb
112
+ - lib/portage/ucp/client/errors.rb
113
+ - lib/portage/ucp/client/session.rb
114
+ - lib/portage/ucp/client/tool_result.rb
115
+ - lib/portage/ucp/client/transports/http.rb
116
+ - lib/portage/ucp/client/transports/loopback.rb
117
+ - lib/portage/ucp/client/transports/stdio.rb
118
+ - lib/portage/ucp/client/version.rb
119
+ homepage: https://github.com/tomtom87/Portage/tree/main/portage-ucp-client
120
+ licenses:
121
+ - MIT
122
+ metadata:
123
+ source_code_uri: https://github.com/tomtom87/Portage/tree/main/portage-ucp-client
124
+ changelog_uri: https://github.com/tomtom87/Portage/blob/main/portage-ucp-client/CHANGELOG.md
125
+ rubygems_mfa_required: 'true'
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '3.2'
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubygems_version: 3.5.22
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Client-side SDK for portage-ucp — act as the shopper's agent against any
145
+ UCP/MCP server
146
+ test_files: []