hitch-rails 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 +7 -0
- data/CHANGELOG.md +103 -0
- data/MIT-LICENSE +20 -0
- data/README.md +460 -0
- data/SECURITY.md +118 -0
- data/app/controllers/concerns/hitch/cors_support.rb +97 -0
- data/app/controllers/concerns/hitch/host_validation.rb +51 -0
- data/app/controllers/concerns/hitch/issuer_url.rb +26 -0
- data/app/controllers/concerns/hitch/mcp/endpoint.rb +355 -0
- data/app/controllers/concerns/hitch/oauth_form_admission.rb +83 -0
- data/app/controllers/concerns/hitch/oauth_parameter_validation.rb +26 -0
- data/app/controllers/concerns/hitch/registration_admission.rb +115 -0
- data/app/controllers/concerns/hitch/request_admission.rb +46 -0
- data/app/controllers/concerns/hitch/uri_validation.rb +116 -0
- data/app/controllers/hitch/application_controller.rb +59 -0
- data/app/controllers/hitch/authorizations_controller.rb +152 -0
- data/app/controllers/hitch/metadata_controller.rb +114 -0
- data/app/controllers/hitch/preflights_controller.rb +14 -0
- data/app/controllers/hitch/public_endpoint_controller.rb +36 -0
- data/app/controllers/hitch/registrations_controller.rb +135 -0
- data/app/controllers/hitch/revocations_controller.rb +31 -0
- data/app/controllers/hitch/tokens_controller.rb +89 -0
- data/app/models/hitch/access_token.rb +267 -0
- data/app/models/hitch/application_record.rb +7 -0
- data/app/models/hitch/authorization_request.rb +252 -0
- data/app/models/hitch/client/credentials.rb +30 -0
- data/app/models/hitch/client.rb +237 -0
- data/app/models/hitch/client_authentication.rb +80 -0
- data/app/models/hitch/client_id_metadata/cache.rb +69 -0
- data/app/models/hitch/client_id_metadata/fetcher.rb +277 -0
- data/app/models/hitch/client_id_metadata/throttle.rb +119 -0
- data/app/models/hitch/client_id_metadata.rb +316 -0
- data/app/models/hitch/client_redirect_uri.rb +14 -0
- data/app/models/hitch/mcp/context.rb +91 -0
- data/app/models/hitch/mcp/forbidden.rb +10 -0
- data/app/models/hitch/mcp/internal/bearer_challenge.rb +51 -0
- data/app/models/hitch/mcp/internal/cors_policy.rb +53 -0
- data/app/models/hitch/mcp/internal/endpoint_error_reporter.rb +40 -0
- data/app/models/hitch/mcp/internal/error_normalizer.rb +74 -0
- data/app/models/hitch/mcp/internal/header_field.rb +31 -0
- data/app/models/hitch/mcp/internal/hmac_identity.rb +37 -0
- data/app/models/hitch/mcp/internal/host_authority.rb +51 -0
- data/app/models/hitch/mcp/internal/json_values.rb +182 -0
- data/app/models/hitch/mcp/internal/local_diagnosis.rb +31 -0
- data/app/models/hitch/mcp/internal/media_type.rb +61 -0
- data/app/models/hitch/mcp/internal/observation.rb +333 -0
- data/app/models/hitch/mcp/internal/registry_runtime.rb +312 -0
- data/app/models/hitch/mcp/internal/result_normalizer.rb +167 -0
- data/app/models/hitch/mcp/internal/sanitized_report.rb +36 -0
- data/app/models/hitch/mcp/internal/schema_contract.rb +173 -0
- data/app/models/hitch/mcp/internal/sdk_adapter/response_normalizer.rb +173 -0
- data/app/models/hitch/mcp/internal/sdk_adapter.rb +222 -0
- data/app/models/hitch/mcp/internal/server_info.rb +49 -0
- data/app/models/hitch/mcp/internal/verified_request.rb +229 -0
- data/app/models/hitch/mcp/internal.rb +11 -0
- data/app/models/hitch/mcp/rate_limit_key.rb +29 -0
- data/app/models/hitch/mcp/registry.rb +70 -0
- data/app/models/hitch/mcp/result.rb +63 -0
- data/app/models/hitch/mcp/tool.rb +148 -0
- data/app/models/hitch/oauth_request_parameters.rb +74 -0
- data/app/views/hitch/authorizations/new.html.erb +57 -0
- data/config/routes.rb +37 -0
- data/db/migrate/20260817000000_create_hitch_tables.rb +77 -0
- data/docs/operator/doctor.md +82 -0
- data/docs/operator/rate_limiting.md +98 -0
- data/docs/public_api/0.2.0.md +322 -0
- data/docs/removing.md +43 -0
- data/lib/generators/hitch/generator_guards.rb +36 -0
- data/lib/generators/hitch/install/install_generator.rb +168 -0
- data/lib/generators/hitch/install/templates/controller.rb.tt +11 -0
- data/lib/generators/hitch/install/templates/initializer.rb +40 -0
- data/lib/generators/hitch/install/templates/registry.rb +6 -0
- data/lib/generators/hitch/tool/templates/tool.rb.tt +54 -0
- data/lib/generators/hitch/tool/templates/tool_test.rb.tt +58 -0
- data/lib/generators/hitch/tool_generator.rb +153 -0
- data/lib/hitch/configuration.rb +386 -0
- data/lib/hitch/doctor.rb +647 -0
- data/lib/hitch/dynamic_registration_rate_limit.rb +75 -0
- data/lib/hitch/engine.rb +154 -0
- data/lib/hitch/mcp/configuration.rb +190 -0
- data/lib/hitch/mcp/protocol.rb +36 -0
- data/lib/hitch/mcp/test_helper.rb +203 -0
- data/lib/hitch/pkce.rb +18 -0
- data/lib/hitch/rack_form_guard.rb +109 -0
- data/lib/hitch/rate_limit_store.rb +47 -0
- data/lib/hitch/resource_uri.rb +71 -0
- data/lib/hitch/version.rb +5 -0
- data/lib/hitch-rails.rb +6 -0
- data/lib/hitch.rb +51 -0
- data/lib/tasks/hitch.rake +197 -0
- metadata +230 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% @controller_modules.each_with_index do |namespace, index| -%>
|
|
4
|
+
<%= " " * index %>module <%= namespace %>
|
|
5
|
+
<% end -%>
|
|
6
|
+
<%= " " * @controller_modules.length %>class <%= @controller_leaf %> < ActionController::API
|
|
7
|
+
<%= " " * (@controller_modules.length + 1) %>include Hitch::MCP::Endpoint
|
|
8
|
+
<%= " " * @controller_modules.length %>end
|
|
9
|
+
<% @controller_modules.length.times.reverse_each do |index| -%>
|
|
10
|
+
<%= " " * index %>end
|
|
11
|
+
<% end -%>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# hitch-rails configuration. These are the knobs every host must set; the
|
|
4
|
+
# full reference (proxy hosts, scopes, token lifetimes, byte caps, rate
|
|
5
|
+
# limits) lives at github.com/tylerklose/hitch-rails.
|
|
6
|
+
Hitch.configure do |config|
|
|
7
|
+
# This MCP server's canonical resource URI for RFC 8707 audience binding.
|
|
8
|
+
# MUST match what MCP clients send in the `resource` parameter.
|
|
9
|
+
config.resource_uri = "https://your-app.example.com/mcp"
|
|
10
|
+
|
|
11
|
+
# Display name shown on the OAuth consent screen.
|
|
12
|
+
config.brand_name = "Your App"
|
|
13
|
+
|
|
14
|
+
# Where to send a signed-out visitor who lands on the consent screen.
|
|
15
|
+
# Without it the browser OAuth flow answers a bare 401 and no MCP client
|
|
16
|
+
# can complete sign-in. "/session/new" is what Rails 8's own
|
|
17
|
+
# `bin/rails generate authentication` creates; Devise uses
|
|
18
|
+
# "/users/sign_in".
|
|
19
|
+
config.login_path = "/session/new"
|
|
20
|
+
|
|
21
|
+
# Exact browser origins allowed to call the endpoint, including scheme and
|
|
22
|
+
# non-default port. Denied by default; development and test also accept
|
|
23
|
+
# loopback origins.
|
|
24
|
+
config.allowed_origins = []
|
|
25
|
+
|
|
26
|
+
# Client ID Metadata Documents — how MCP 2026-07-28 clients register.
|
|
27
|
+
# Requires DIRECT outbound https on port 443 (Hitch deliberately ignores
|
|
28
|
+
# http_proxy); set false if this tier has no direct egress. Verify with:
|
|
29
|
+
# bin/rails 'hitch:cimd:check[https://some-client.example/client.json]'
|
|
30
|
+
config.client_id_metadata_enabled = true
|
|
31
|
+
|
|
32
|
+
# Dynamic Client Registration is unauthenticated and deprecated by the MCP
|
|
33
|
+
# specification, so new installations do not expose it.
|
|
34
|
+
config.dynamic_client_registration_enabled = false
|
|
35
|
+
|
|
36
|
+
# The authenticated /mcp endpoint. Tools stay deny-default until they are
|
|
37
|
+
# reviewed and registered in app/tools/mcp_tool_registry.rb.
|
|
38
|
+
config.mcp.enabled = true
|
|
39
|
+
config.mcp.registry = "McpToolRegistry"
|
|
40
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% @class_modules.each_with_index do |namespace, index| -%>
|
|
4
|
+
<%= " " * index %>module <%= namespace %>
|
|
5
|
+
<% end -%>
|
|
6
|
+
<%= " " * @class_modules.length %>class <%= @class_leaf %> < Hitch::MCP::Tool
|
|
7
|
+
<%= " " * (@class_modules.length + 1) %>tool_name "<%= @tool_name %>"
|
|
8
|
+
<%= " " * (@class_modules.length + 1) %>description "TODO: describe <%= @tool_name %>"
|
|
9
|
+
<%= " " * (@class_modules.length + 1) %>input_schema(
|
|
10
|
+
<%= " " * (@class_modules.length + 2) %>type: "object",
|
|
11
|
+
<%= " " * (@class_modules.length + 2) %>properties: {},
|
|
12
|
+
<%= " " * (@class_modules.length + 2) %>additionalProperties: false
|
|
13
|
+
<%= " " * (@class_modules.length + 1) %>)
|
|
14
|
+
<% if deny_default? -%>
|
|
15
|
+
<%= " " * (@class_modules.length + 1) %>annotations read_only_hint: false,
|
|
16
|
+
<%= " " * (@class_modules.length + 2) %>destructive_hint: true,
|
|
17
|
+
<%= " " * (@class_modules.length + 2) %>idempotent_hint: false,
|
|
18
|
+
<%= " " * (@class_modules.length + 2) %>open_world_hint: true
|
|
19
|
+
|
|
20
|
+
<%= " " * (@class_modules.length + 1) %>def self.available_to?(_context)
|
|
21
|
+
<%= " " * (@class_modules.length + 2) %>false
|
|
22
|
+
<%= " " * (@class_modules.length + 1) %>end
|
|
23
|
+
|
|
24
|
+
<%= " " * (@class_modules.length + 1) %>def self.authorize!(_context, arguments:)
|
|
25
|
+
<%= " " * (@class_modules.length + 2) %>raise Hitch::MCP::Forbidden
|
|
26
|
+
<%= " " * (@class_modules.length + 1) %>end
|
|
27
|
+
|
|
28
|
+
<%= " " * (@class_modules.length + 1) %>def self.perform(_context, arguments:)
|
|
29
|
+
<%= " " * (@class_modules.length + 2) %>raise "Implement <%= @class_name %>.perform before making the tool available"
|
|
30
|
+
<%= " " * (@class_modules.length + 1) %>end
|
|
31
|
+
<% else -%>
|
|
32
|
+
<%= " " * (@class_modules.length + 1) %>annotations read_only_hint: true,
|
|
33
|
+
<%= " " * (@class_modules.length + 2) %>destructive_hint: false,
|
|
34
|
+
<%= " " * (@class_modules.length + 2) %>idempotent_hint: true,
|
|
35
|
+
<%= " " * (@class_modules.length + 2) %>open_world_hint: false
|
|
36
|
+
|
|
37
|
+
<%= " " * (@class_modules.length + 1) %>def self.available_to?(_context)
|
|
38
|
+
<%= " " * (@class_modules.length + 2) %>true
|
|
39
|
+
<%= " " * (@class_modules.length + 1) %>end
|
|
40
|
+
|
|
41
|
+
<%= " " * (@class_modules.length + 1) %>def self.authorize!(_context, arguments:)
|
|
42
|
+
<%= " " * (@class_modules.length + 2) %># Returning without raising ALLOWS the call. Put your policy here,
|
|
43
|
+
<%= " " * (@class_modules.length + 2) %># naming the context argument to use it:
|
|
44
|
+
<%= " " * (@class_modules.length + 2) %># raise Hitch::MCP::Forbidden unless context.principal.admin?
|
|
45
|
+
<%= " " * (@class_modules.length + 1) %>end
|
|
46
|
+
|
|
47
|
+
<%= " " * (@class_modules.length + 1) %>def self.perform(_context, arguments:)
|
|
48
|
+
<%= " " * (@class_modules.length + 2) %>Hitch::MCP::Result.text("<%= @tool_name %> is wired up; implement perform to do real work")
|
|
49
|
+
<%= " " * (@class_modules.length + 1) %>end
|
|
50
|
+
<% end -%>
|
|
51
|
+
<%= " " * @class_modules.length %>end
|
|
52
|
+
<% @class_modules.length.times.reverse_each do |index| -%>
|
|
53
|
+
<%= " " * index %>end
|
|
54
|
+
<% end -%>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "hitch/mcp/test_helper"
|
|
5
|
+
|
|
6
|
+
class <%= @test_class_name %> < ActionDispatch::IntegrationTest
|
|
7
|
+
# post_mcp drives your mounted /mcp endpoint over real HTTP — see
|
|
8
|
+
# Hitch::MCP::TestHelper.
|
|
9
|
+
include Hitch::MCP::TestHelper
|
|
10
|
+
|
|
11
|
+
setup do
|
|
12
|
+
# post_mcp needs a real bearer token. Point this at however your tests
|
|
13
|
+
# get a signed-in user — a fixture, a factory, or a record created here.
|
|
14
|
+
@token = mint_mcp_token(principal: User.first!)
|
|
15
|
+
end
|
|
16
|
+
<% if deny_default? -%>
|
|
17
|
+
|
|
18
|
+
test "stays hidden until the host implements and opens it" do
|
|
19
|
+
post_mcp(method: "tools/list", token: @token)
|
|
20
|
+
|
|
21
|
+
assert_response :success
|
|
22
|
+
tools = JSON.parse(response.body).dig("result", "tools")
|
|
23
|
+
refute_includes tools.map { |tool| tool.fetch("name") }, "<%= @tool_name %>"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
test "calls are refused while unavailable" do
|
|
27
|
+
post_mcp(
|
|
28
|
+
method: "tools/call",
|
|
29
|
+
token: @token,
|
|
30
|
+
params: { name: "<%= @tool_name %>", arguments: {} }
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert_response :success
|
|
34
|
+
assert_equal(-32602, JSON.parse(response.body).dig("error", "code"))
|
|
35
|
+
end
|
|
36
|
+
<% else -%>
|
|
37
|
+
|
|
38
|
+
test "is listed for an authorized principal" do
|
|
39
|
+
post_mcp(method: "tools/list", token: @token)
|
|
40
|
+
|
|
41
|
+
assert_response :success
|
|
42
|
+
tools = JSON.parse(response.body).dig("result", "tools")
|
|
43
|
+
assert_includes tools.map { |tool| tool.fetch("name") }, "<%= @tool_name %>"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
test "responds when called" do
|
|
47
|
+
post_mcp(
|
|
48
|
+
method: "tools/call",
|
|
49
|
+
token: @token,
|
|
50
|
+
params: { name: "<%= @tool_name %>", arguments: {} }
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert_response :success
|
|
54
|
+
result = JSON.parse(response.body).fetch("result")
|
|
55
|
+
assert_includes result.dig("content", 0, "text"), "<%= @tool_name %>"
|
|
56
|
+
end
|
|
57
|
+
<% end -%>
|
|
58
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/inflections"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
require_relative "generator_guards"
|
|
6
|
+
|
|
7
|
+
module Hitch
|
|
8
|
+
module Generators
|
|
9
|
+
# Generates a working, registered MCP tool plus an integration test that
|
|
10
|
+
# proves it responds over real HTTP. --deny-default emits a hardened
|
|
11
|
+
# variant instead: unavailable, denying, and unimplemented until the
|
|
12
|
+
# host fills it in.
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# bin/rails generate hitch:tool NAME
|
|
16
|
+
# bin/rails destroy hitch:tool NAME
|
|
17
|
+
class ToolGenerator < ::Rails::Generators::Base
|
|
18
|
+
include Hitch::Generators::GeneratorGuards
|
|
19
|
+
|
|
20
|
+
namespace "hitch:tool"
|
|
21
|
+
|
|
22
|
+
source_root File.expand_path("tool/templates", __dir__)
|
|
23
|
+
|
|
24
|
+
argument :name, type: :string, required: true,
|
|
25
|
+
desc: "Tool name, optionally nested with / or ::"
|
|
26
|
+
class_option :namespace,
|
|
27
|
+
type: :string,
|
|
28
|
+
default: "McpTools",
|
|
29
|
+
desc: "Root Ruby namespace for the generated tool"
|
|
30
|
+
class_option :deny_default,
|
|
31
|
+
type: :boolean,
|
|
32
|
+
default: false,
|
|
33
|
+
desc: "Generate the hardened variant: unavailable and denying until implemented"
|
|
34
|
+
|
|
35
|
+
NAME_SEGMENT_PATTERN =
|
|
36
|
+
/\A(?:[a-z][a-z0-9]*(?:[_-][a-z0-9]+)*|[A-Z][A-Za-z0-9]*)\z/
|
|
37
|
+
CANONICAL_SEGMENT_PATTERN = /\A[a-z][a-z0-9]*(?:_[a-z0-9]+)*\z/
|
|
38
|
+
NAMESPACE_PATTERN = /\A[A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)*\z/
|
|
39
|
+
|
|
40
|
+
def self.exit_on_failure?
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def generate_or_revoke
|
|
45
|
+
prepare_identity!
|
|
46
|
+
behavior == :revoke ? preflight_revoke! : preflight!
|
|
47
|
+
|
|
48
|
+
# Behavior-aware Thor actions: create/inject in invoke, remove in
|
|
49
|
+
# revoke.
|
|
50
|
+
template "tool.rb.tt", @tool_path
|
|
51
|
+
template "tool_test.rb.tt", @test_path
|
|
52
|
+
inject_into_class REGISTRY_PATH, "McpToolRegistry", " #{@registration_line}\n"
|
|
53
|
+
|
|
54
|
+
print_next_steps unless behavior == :revoke
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# inject_into_class revokes by exact bytes; a reformatted registration
|
|
60
|
+
# line would survive while the tool file disappears, stranding a
|
|
61
|
+
# NameError at the next boot. Refuse the whole rollback instead.
|
|
62
|
+
def preflight_revoke!
|
|
63
|
+
registry = destination_file?(REGISTRY_PATH) ? File.binread(destination_path(REGISTRY_PATH)) : ""
|
|
64
|
+
return unless registry.match?(/^\s*register\s+#{Regexp.escape(@class_name)}\b/)
|
|
65
|
+
return if registry.include?(@registration_line)
|
|
66
|
+
|
|
67
|
+
refuse!("rollback", [
|
|
68
|
+
"#{REGISTRY_PATH} contains an edited registration for #{@class_name}; " \
|
|
69
|
+
"remove that line by hand, then rerun bin/rails destroy hitch:tool"
|
|
70
|
+
])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def preflight!
|
|
74
|
+
errors = []
|
|
75
|
+
[ @tool_path, @test_path ].each do |path|
|
|
76
|
+
errors << "file collision: #{path}" if destination_file?(path)
|
|
77
|
+
end
|
|
78
|
+
errors << "constant collision: #{@class_name}" if constant_collision?(@class_name)
|
|
79
|
+
errors << "constant collision: #{@test_class_name}" if constant_collision?(@test_class_name)
|
|
80
|
+
errors << "#{REGISTRY_PATH} is missing; run `bin/rails generate hitch:install` first" unless
|
|
81
|
+
destination_file?(REGISTRY_PATH)
|
|
82
|
+
refuse!("generation", errors) if errors.any?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def prepare_identity!
|
|
86
|
+
namespace = options.fetch("namespace")
|
|
87
|
+
canonical_segments = normalize_name(name)
|
|
88
|
+
errors = []
|
|
89
|
+
errors << "name must contain valid snake-case, kebab-case, or constant segments" unless canonical_segments
|
|
90
|
+
errors << "namespace must be a bounded constant path" unless valid_namespace?(namespace)
|
|
91
|
+
refuse!(operation, errors) if errors.any?
|
|
92
|
+
|
|
93
|
+
@tool_name = canonical_segments.join(".")
|
|
94
|
+
refuse!(operation, [ "normalized MCP tool name must be 1 to 64 characters" ]) unless
|
|
95
|
+
@tool_name.length.between?(1, 64)
|
|
96
|
+
|
|
97
|
+
nested_constants = canonical_segments.map(&:camelize)
|
|
98
|
+
@class_name = ([ namespace ] + nested_constants).join("::")
|
|
99
|
+
@test_class_name = "#{@class_name}Test"
|
|
100
|
+
@class_modules = @class_name.split("::")[0...-1]
|
|
101
|
+
@class_leaf = @class_name.split("::").last
|
|
102
|
+
@tool_path = "app/tools/#{@class_name.underscore}.rb"
|
|
103
|
+
@test_path = "test/integration/#{@class_name.underscore}_test.rb"
|
|
104
|
+
@registration_line = %(register #{@class_name}, scopes: [ "mcp" ])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def normalize_name(value)
|
|
108
|
+
return unless value.is_a?(String) && value.bytesize.between?(1, 256)
|
|
109
|
+
|
|
110
|
+
source = value.gsub("::", "/")
|
|
111
|
+
return if source.start_with?("/") || source.end_with?("/") || source.include?("//")
|
|
112
|
+
|
|
113
|
+
segments = source.split("/")
|
|
114
|
+
normalized = segments.filter_map do |segment|
|
|
115
|
+
next unless NAME_SEGMENT_PATTERN.match?(segment)
|
|
116
|
+
|
|
117
|
+
candidate = segment.match?(/\A[A-Z]/) ? segment.underscore : segment.tr("-", "_")
|
|
118
|
+
candidate if CANONICAL_SEGMENT_PATTERN.match?(candidate)
|
|
119
|
+
end
|
|
120
|
+
normalized if normalized.length == segments.length
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def valid_namespace?(value)
|
|
124
|
+
return false unless value.is_a?(String) && value.bytesize.between?(1, 255)
|
|
125
|
+
return false unless NAMESPACE_PATTERN.match?(value)
|
|
126
|
+
|
|
127
|
+
segments = value.split("::")
|
|
128
|
+
segments.length <= 8 && segments.all? { |segment| segment.bytesize <= 64 }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def deny_default?
|
|
132
|
+
options.fetch("deny_default")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def operation
|
|
136
|
+
behavior == :revoke ? "rollback" : "generation"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def refusal_subject = "Hitch tool"
|
|
140
|
+
|
|
141
|
+
def print_next_steps
|
|
142
|
+
say ""
|
|
143
|
+
if deny_default?
|
|
144
|
+
say "Generated and registered deny-default Hitch MCP tool #{@class_name}.", :green
|
|
145
|
+
say "It stays unavailable until you implement it."
|
|
146
|
+
else
|
|
147
|
+
say "Generated and registered Hitch MCP tool #{@class_name}.", :green
|
|
148
|
+
say "Verify it responds: bin/rails test #{@test_path}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|