mcp_authorization 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 +7 -0
- data/LICENSE +21 -0
- data/README.md +552 -0
- data/app/controllers/mcp_authorization/mcp_controller.rb +37 -0
- data/lib/mcp_authorization/configuration.rb +81 -0
- data/lib/mcp_authorization/dsl.rb +64 -0
- data/lib/mcp_authorization/engine.rb +68 -0
- data/lib/mcp_authorization/rbs_schema_compiler.rb +1015 -0
- data/lib/mcp_authorization/tool.rb +245 -0
- data/lib/mcp_authorization/tool_registry.rb +89 -0
- data/lib/mcp_authorization/version.rb +3 -0
- data/lib/mcp_authorization.rb +57 -0
- data/lib/tasks/mcp_authorization.rake +99 -0
- metadata +112 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
module McpAuthorization
|
|
2
|
+
# Base class for MCP tools with schema-shaping authorization.
|
|
3
|
+
#
|
|
4
|
+
# Subclass this instead of MCP::Tool directly. Each subclass is a thin
|
|
5
|
+
# declarative wrapper — the actual business logic lives in a *handler
|
|
6
|
+
# class* (a plain Ruby class that includes DSL) pointed to by
|
|
7
|
+
# +dynamic_contract+.
|
|
8
|
+
#
|
|
9
|
+
# == Defining a tool
|
|
10
|
+
#
|
|
11
|
+
# class Tools::ListOrders < McpAuthorization::Tool
|
|
12
|
+
# tool_name "list_orders"
|
|
13
|
+
# authorization :view_orders
|
|
14
|
+
# tags "operator", "fulfillment"
|
|
15
|
+
# read_only!
|
|
16
|
+
#
|
|
17
|
+
# dynamic_contract Handlers::ListOrders
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
class Tool < MCP::Tool
|
|
21
|
+
class NotAuthorizedError < StandardError; end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
#: Symbol?
|
|
25
|
+
attr_reader :_permission
|
|
26
|
+
|
|
27
|
+
#: Array[String]?
|
|
28
|
+
attr_reader :_tags
|
|
29
|
+
|
|
30
|
+
#: untyped
|
|
31
|
+
attr_reader :_contract_handler
|
|
32
|
+
|
|
33
|
+
#: (Class) -> void
|
|
34
|
+
def inherited(subclass)
|
|
35
|
+
super
|
|
36
|
+
McpAuthorization::ToolRegistry.register(subclass)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Declare the permission flag required to see this tool.
|
|
40
|
+
#: (Symbol) -> void
|
|
41
|
+
def authorization(permission)
|
|
42
|
+
@_permission = permission
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Declare which MCP domains this tool belongs to.
|
|
46
|
+
#: (*String | Array[String]) -> void
|
|
47
|
+
def tags(*list)
|
|
48
|
+
@_tags = list.flatten
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# MCP annotation hint shorthands
|
|
52
|
+
#: () -> void
|
|
53
|
+
def read_only!; merge_annotations(read_only_hint: true) end
|
|
54
|
+
#: () -> void
|
|
55
|
+
def destructive!; merge_annotations(destructive_hint: true) end
|
|
56
|
+
#: () -> void
|
|
57
|
+
def not_destructive!; merge_annotations(destructive_hint: false) end
|
|
58
|
+
#: () -> void
|
|
59
|
+
def idempotent!; merge_annotations(idempotent_hint: true) end
|
|
60
|
+
#: () -> void
|
|
61
|
+
def open_world!; merge_annotations(open_world_hint: true) end
|
|
62
|
+
#: () -> void
|
|
63
|
+
def closed_world!; merge_annotations(open_world_hint: false) end
|
|
64
|
+
|
|
65
|
+
# Point this tool at its handler class.
|
|
66
|
+
#: (untyped) -> void
|
|
67
|
+
def dynamic_contract(handler_class)
|
|
68
|
+
@_contract_handler = handler_class
|
|
69
|
+
@_contract_validated = false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Build the tool description for this user.
|
|
73
|
+
#: (server_context: untyped) -> String
|
|
74
|
+
def dynamic_description(server_context:)
|
|
75
|
+
handler_instance(server_context).description
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Compile the input JSON Schema for this user.
|
|
79
|
+
#: (server_context: untyped) -> Hash[Symbol, untyped]
|
|
80
|
+
def dynamic_input_schema(server_context:)
|
|
81
|
+
McpAuthorization::RbsSchemaCompiler.compile_input(
|
|
82
|
+
_contract_handler,
|
|
83
|
+
server_context: server_context
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Compile the output JSON Schema for this user.
|
|
88
|
+
#: (server_context: untyped) -> Hash[Symbol, untyped]?
|
|
89
|
+
def dynamic_output_schema(server_context:)
|
|
90
|
+
McpAuthorization::RbsSchemaCompiler.compile_output(
|
|
91
|
+
_contract_handler,
|
|
92
|
+
server_context: server_context
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check whether the current user is allowed to see this tool.
|
|
97
|
+
#: (untyped) -> bool
|
|
98
|
+
def permitted?(server_context)
|
|
99
|
+
return true if _permission.nil?
|
|
100
|
+
server_context.current_user.can?(_permission)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Build the full MCP tool definition hash for +tools/list+.
|
|
104
|
+
# Returns nil if the user is not permitted.
|
|
105
|
+
#: (server_context: untyped) -> Hash[Symbol, untyped]?
|
|
106
|
+
def to_mcp_definition(server_context:)
|
|
107
|
+
return nil unless permitted?(server_context)
|
|
108
|
+
validate_contract!(_contract_handler) unless @_contract_validated
|
|
109
|
+
@_contract_validated = true
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
name: tool_name,
|
|
113
|
+
description: dynamic_description(server_context: server_context),
|
|
114
|
+
inputSchema: dynamic_input_schema(server_context: server_context),
|
|
115
|
+
outputSchema: dynamic_output_schema(server_context: server_context),
|
|
116
|
+
annotations: @_annotations_hash || {}
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Execute the tool by delegating to the handler.
|
|
121
|
+
#: (?server_context: untyped?, **untyped) -> untyped
|
|
122
|
+
def call(server_context: nil, **params)
|
|
123
|
+
raise NotAuthorizedError unless server_context && permitted?(server_context)
|
|
124
|
+
handler_instance(server_context).call(**params)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Create an anonymous MCP::Tool subclass with this user's schemas baked in.
|
|
128
|
+
#: (untyped) -> Class?
|
|
129
|
+
def materialize_for(server_context)
|
|
130
|
+
defn = to_mcp_definition(server_context: server_context)
|
|
131
|
+
return nil unless defn
|
|
132
|
+
|
|
133
|
+
handler = _contract_handler
|
|
134
|
+
ctx = server_context
|
|
135
|
+
|
|
136
|
+
Class.new(MCP::Tool) do
|
|
137
|
+
tool_name defn[:name]
|
|
138
|
+
description defn[:description]
|
|
139
|
+
input_schema defn[:inputSchema]
|
|
140
|
+
output_schema defn[:outputSchema] if defn[:outputSchema]
|
|
141
|
+
annotations(**defn[:annotations]) if defn[:annotations]&.any?
|
|
142
|
+
|
|
143
|
+
define_singleton_method(:call) do |server_context: nil, **params|
|
|
144
|
+
effective_ctx = server_context || ctx
|
|
145
|
+
result = handler.new(server_context: effective_ctx).call(**params)
|
|
146
|
+
MCP::Tool::Response.new([ { type: "text", text: result.to_json } ])
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
#: (**untyped) -> void
|
|
154
|
+
def merge_annotations(**new_hints)
|
|
155
|
+
hints = (@_annotation_hints || {}).merge(new_hints)
|
|
156
|
+
@_annotation_hints = hints
|
|
157
|
+
annotations(**hints)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
#: (untyped) -> untyped
|
|
161
|
+
def handler_instance(server_context)
|
|
162
|
+
_contract_handler.new(server_context: server_context)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
#: (untyped) -> void
|
|
166
|
+
def validate_contract!(handler_class)
|
|
167
|
+
errors = []
|
|
168
|
+
|
|
169
|
+
unless handler_class.method_defined?(:call)
|
|
170
|
+
errors << "missing instance method #call"
|
|
171
|
+
end
|
|
172
|
+
unless handler_class.method_defined?(:description)
|
|
173
|
+
errors << "missing instance method #description"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
init = handler_class.instance_method(:initialize) rescue nil
|
|
177
|
+
unless init&.parameters&.any? { |type, name| name == :server_context && type == :keyreq }
|
|
178
|
+
errors << "missing initialize(server_context:)"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
source_file = McpAuthorization::RbsSchemaCompiler.send(:find_source_file, handler_class)
|
|
182
|
+
if source_file && File.exist?(source_file)
|
|
183
|
+
content = File.read(source_file)
|
|
184
|
+
has_input = content.include?("# @rbs type input =")
|
|
185
|
+
has_call_annotation = content.match?(/^\s*#:.*->/m)
|
|
186
|
+
has_output = content.include?("# @rbs type output =")
|
|
187
|
+
|
|
188
|
+
unless has_input || has_call_annotation
|
|
189
|
+
errors << "missing input schema (define #: annotation above def call, or # @rbs type input = { ... })"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
unless has_output
|
|
193
|
+
errors << "missing output schema (define # @rbs type output = variant1 | variant2 | ...)"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if has_output
|
|
197
|
+
begin
|
|
198
|
+
cached = McpAuthorization::RbsSchemaCompiler.send(:cache_for, handler_class)
|
|
199
|
+
if cached[:raw_output]&.dig(:kind) == :union
|
|
200
|
+
primitives = %w[String Integer Float bool true false]
|
|
201
|
+
parts = cached[:raw_output][:body].split("|").map(&:strip).reject(&:empty?)
|
|
202
|
+
parts.each do |part|
|
|
203
|
+
name = part.gsub(/\s*@\w+\([^)]*\)/, "").strip
|
|
204
|
+
next if primitives.include?(name)
|
|
205
|
+
next if cached[:type_map].key?(name)
|
|
206
|
+
errors << "output variant '#{name}' does not resolve to a defined type (check @rbs type definitions and @rbs import statements)"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
rescue => e
|
|
210
|
+
# Don't fail validation if cache isn't ready
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
elsif source_file.nil?
|
|
214
|
+
errors << "could not locate source file (is #call defined?)"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
return if errors.empty?
|
|
218
|
+
|
|
219
|
+
raise ArgumentError, <<~MSG
|
|
220
|
+
#{handler_class} does not satisfy the McpAuthorization handler contract.
|
|
221
|
+
|
|
222
|
+
Problems:
|
|
223
|
+
#{errors.map { |e| "- #{e}" }.join("\n ")}
|
|
224
|
+
|
|
225
|
+
A handler class should look like:
|
|
226
|
+
|
|
227
|
+
class MyHandler
|
|
228
|
+
include McpAuthorization::DSL
|
|
229
|
+
|
|
230
|
+
# @rbs type output = success | error
|
|
231
|
+
|
|
232
|
+
def description
|
|
233
|
+
"What this tool does"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
#: (name: String, ?force: bool @requires(:admin)) -> Hash[Symbol, untyped]
|
|
237
|
+
def call(name:, force: false)
|
|
238
|
+
# ...
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
MSG
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module McpAuthorization
|
|
2
|
+
# Global registry of all McpAuthorization::Tool subclasses.
|
|
3
|
+
#
|
|
4
|
+
# Tools self-register via the +inherited+ hook in Tool, so there is no
|
|
5
|
+
# manual registration step — defining a class that inherits from Tool is
|
|
6
|
+
# enough.
|
|
7
|
+
#
|
|
8
|
+
# The registry is the entry point for two main operations:
|
|
9
|
+
#
|
|
10
|
+
# * *Listing* — +list_tools+ returns JSON-serialisable tool definitions
|
|
11
|
+
# filtered by domain and the current user's permissions.
|
|
12
|
+
#
|
|
13
|
+
# * *Materializing* — +tool_classes_for+ returns concrete MCP::Tool
|
|
14
|
+
# subclasses with per-user schemas baked in, ready to be handed to an
|
|
15
|
+
# MCP::Server for request handling.
|
|
16
|
+
#
|
|
17
|
+
class ToolRegistry
|
|
18
|
+
class << self
|
|
19
|
+
# Register a tool class. Called automatically by Tool.inherited.
|
|
20
|
+
#: (Class) -> void
|
|
21
|
+
def register(tool_class)
|
|
22
|
+
tools = (@registered_tools ||= [])
|
|
23
|
+
tools << tool_class unless tools.include?(tool_class)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# All registered tool classes. Triggers eager loading on first access.
|
|
27
|
+
#: () -> Array[singleton(McpAuthorization::Tool)]
|
|
28
|
+
def registered_tools
|
|
29
|
+
tools = (@registered_tools ||= [])
|
|
30
|
+
ensure_tools_loaded! if tools.empty?
|
|
31
|
+
tools
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Force-loads tool directories so tool classes self-register.
|
|
35
|
+
#: () -> void
|
|
36
|
+
def ensure_tools_loaded!
|
|
37
|
+
return if @registered_tools&.any?
|
|
38
|
+
return unless defined?(Rails)
|
|
39
|
+
|
|
40
|
+
McpAuthorization.config.tool_paths.each do |path|
|
|
41
|
+
full_path = Rails.root.join(path)
|
|
42
|
+
Rails.autoloaders.main.eager_load_dir(full_path) if File.directory?(full_path)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Groups registered tools by their domain tags.
|
|
47
|
+
#: () -> Hash[String, Array[singleton(McpAuthorization::Tool)]]
|
|
48
|
+
def tools_by_domain
|
|
49
|
+
initial = Hash.new { |h, k| h[k] = [] } #: Hash[String, Array[singleton(McpAuthorization::Tool)]]
|
|
50
|
+
registered_tools.each_with_object(initial) do |tool_class, map|
|
|
51
|
+
(tool_class._tags || ["default"]).each do |tag|
|
|
52
|
+
map[tag] << tool_class
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Tool definitions for +tools/list+, filtered by domain and permissions.
|
|
58
|
+
#: (domain: String, server_context: untyped) -> Array[Hash[Symbol, untyped]]
|
|
59
|
+
def list_tools(domain:, server_context:)
|
|
60
|
+
candidates = tools_by_domain[domain] || []
|
|
61
|
+
candidates.filter_map do |tool_class|
|
|
62
|
+
tool_class.to_mcp_definition(server_context: server_context)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Concrete MCP::Tool subclasses with per-user schemas baked in.
|
|
67
|
+
#: (domain: String, server_context: untyped) -> Array[singleton(MCP::Tool)]
|
|
68
|
+
def tool_classes_for(domain:, server_context:)
|
|
69
|
+
candidates = tools_by_domain[domain] || []
|
|
70
|
+
candidates.filter_map do |tool_class|
|
|
71
|
+
next unless tool_class.permitted?(server_context)
|
|
72
|
+
tool_class.materialize_for(server_context)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Look up a tool by its MCP tool name across all domains.
|
|
77
|
+
#: (String) -> singleton(McpAuthorization::Tool)?
|
|
78
|
+
def find_tool(name)
|
|
79
|
+
registered_tools.find { |t| t.tool_name == name }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Clear the registry. Called by the Engine's reloader on code change.
|
|
83
|
+
#: () -> void
|
|
84
|
+
def reset!
|
|
85
|
+
@registered_tools = []
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require "mcp"
|
|
2
|
+
require_relative "mcp_authorization/version"
|
|
3
|
+
require_relative "mcp_authorization/configuration"
|
|
4
|
+
require_relative "mcp_authorization/dsl"
|
|
5
|
+
require_relative "mcp_authorization/rbs_schema_compiler"
|
|
6
|
+
require_relative "mcp_authorization/tool_registry"
|
|
7
|
+
require_relative "mcp_authorization/tool"
|
|
8
|
+
require_relative "mcp_authorization/engine" if defined?(Rails)
|
|
9
|
+
|
|
10
|
+
# MCP Authorization — schema-shaping authorization for MCP tool servers.
|
|
11
|
+
#
|
|
12
|
+
# Instead of rejecting unauthorized requests after the fact, this gem shapes
|
|
13
|
+
# the JSON Schema that each user sees so that fields and output variants they
|
|
14
|
+
# are not permitted to use never appear in the schema at all. The LLM (or
|
|
15
|
+
# any MCP client) therefore never knows those options exist.
|
|
16
|
+
#
|
|
17
|
+
# == Quick start (Rails)
|
|
18
|
+
#
|
|
19
|
+
# # config/initializers/mcp_authorization.rb
|
|
20
|
+
# McpAuthorization.configure do |c|
|
|
21
|
+
# c.server_name = "my-app"
|
|
22
|
+
# c.server_version = "1.0.0"
|
|
23
|
+
# c.context_builder = ->(request) {
|
|
24
|
+
# ServerContext.new(current_user: current_user_from(request))
|
|
25
|
+
# }
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# == How it works
|
|
29
|
+
#
|
|
30
|
+
# 1. Tool classes inherit from McpAuthorization::Tool and declare a handler
|
|
31
|
+
# class via +dynamic_contract+.
|
|
32
|
+
# 2. Handler classes use +@rbs type+ comments and +#:+ annotations to define
|
|
33
|
+
# input/output schemas. Fields can be tagged with +@requires(:flag)+ to
|
|
34
|
+
# gate them on user permissions.
|
|
35
|
+
# 3. On each request the RbsSchemaCompiler compiles a per-user JSON Schema
|
|
36
|
+
# by filtering out fields whose +@requires+ flag the user lacks.
|
|
37
|
+
# 4. A fresh set of MCP::Tool subclasses is materialized with the filtered
|
|
38
|
+
# schemas baked in, and handed to a stateless MCP::Server.
|
|
39
|
+
#
|
|
40
|
+
# See CLAUDE.md for the full architecture walkthrough.
|
|
41
|
+
module McpAuthorization
|
|
42
|
+
class << self
|
|
43
|
+
# Yields the global Configuration instance for block-style setup.
|
|
44
|
+
#: () { (Configuration) -> void } -> void
|
|
45
|
+
def configure
|
|
46
|
+
yield configuration
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns the global Configuration instance, creating it with defaults
|
|
50
|
+
# on first access.
|
|
51
|
+
#: () -> Configuration
|
|
52
|
+
def configuration
|
|
53
|
+
@configuration ||= Configuration.new
|
|
54
|
+
end
|
|
55
|
+
alias_method :config, :configuration
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
namespace :mcp do
|
|
2
|
+
desc "Start Rails server and MCP Inspector for a given domain and role"
|
|
3
|
+
task :inspect, [ :domain, :role ] => :environment do |_t, args|
|
|
4
|
+
domain = args[:domain] || McpAuthorization.config.default_domain
|
|
5
|
+
role = args[:role] || "operator"
|
|
6
|
+
port = ENV.fetch("PORT", "3000")
|
|
7
|
+
mount = McpAuthorization.config.mount_path || "/mcp"
|
|
8
|
+
url = "http://localhost:#{port}#{mount}/#{domain}"
|
|
9
|
+
|
|
10
|
+
puts "Starting MCP Inspector"
|
|
11
|
+
puts " Domain: #{domain}"
|
|
12
|
+
puts " Role: #{role}"
|
|
13
|
+
puts " URL: #{url}"
|
|
14
|
+
puts ""
|
|
15
|
+
|
|
16
|
+
config = {
|
|
17
|
+
mcpServers: {
|
|
18
|
+
McpAuthorization.config.server_name => {
|
|
19
|
+
url: url,
|
|
20
|
+
headers: { "Authorization" => role }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
config_path = Rails.root.join("tmp/mcp_inspector.json")
|
|
25
|
+
File.write(config_path, JSON.pretty_generate(config))
|
|
26
|
+
|
|
27
|
+
rails_pid = spawn(
|
|
28
|
+
"bundle exec rails server -p #{port}",
|
|
29
|
+
out: File::NULL, err: File::NULL
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
sleep 3
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
exec(
|
|
36
|
+
"npx", "@modelcontextprotocol/inspector",
|
|
37
|
+
"--config", config_path.to_s,
|
|
38
|
+
"--server", McpAuthorization.config.server_name
|
|
39
|
+
)
|
|
40
|
+
ensure
|
|
41
|
+
Process.kill("TERM", rails_pid) rescue nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "Print Claude Code MCP config for a given domain and role"
|
|
46
|
+
task :claude, [ :domain, :role ] => :environment do |_t, args|
|
|
47
|
+
domain = args[:domain] || McpAuthorization.config.default_domain
|
|
48
|
+
role = args[:role] || "operator"
|
|
49
|
+
port = ENV.fetch("PORT", "3000")
|
|
50
|
+
mount = McpAuthorization.config.mount_path || "/mcp"
|
|
51
|
+
url = "http://localhost:#{port}#{mount}/#{domain}"
|
|
52
|
+
|
|
53
|
+
config = {
|
|
54
|
+
"mcpServers" => {
|
|
55
|
+
"#{McpAuthorization.config.server_name}-#{domain}" => {
|
|
56
|
+
"url" => url,
|
|
57
|
+
"headers" => { "Authorization" => role }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
puts "Add this to your Claude Code settings (~/.claude/settings.json):"
|
|
63
|
+
puts ""
|
|
64
|
+
puts JSON.pretty_generate(config)
|
|
65
|
+
puts ""
|
|
66
|
+
puts "Make sure Rails is running: bundle exec rails server -p #{port}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
desc "List available tools for a domain and role"
|
|
70
|
+
task :tools, [ :domain, :role ] => :environment do |_t, args|
|
|
71
|
+
require "json"
|
|
72
|
+
domain = args[:domain] || McpAuthorization.config.default_domain
|
|
73
|
+
role = args[:role] || "operator"
|
|
74
|
+
|
|
75
|
+
builder = McpAuthorization.config.cli_context_builder
|
|
76
|
+
unless builder
|
|
77
|
+
puts "McpAuthorization.config.cli_context_builder is not configured."
|
|
78
|
+
puts "Set it in config/initializers/mcp_authorization.rb"
|
|
79
|
+
exit 1
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
ctx = builder.call(domain: domain, role: role)
|
|
83
|
+
|
|
84
|
+
tools = McpAuthorization::ToolRegistry.list_tools(domain: domain, server_context: ctx)
|
|
85
|
+
|
|
86
|
+
if tools.empty?
|
|
87
|
+
puts "No tools available for domain '#{domain}' with role '#{role}'"
|
|
88
|
+
else
|
|
89
|
+
tools.each do |tool|
|
|
90
|
+
puts "#{tool[:name]}"
|
|
91
|
+
puts " #{tool[:description]}"
|
|
92
|
+
puts " input: #{tool[:inputSchema][:properties].keys.join(', ')}"
|
|
93
|
+
output_shapes = tool.dig(:outputSchema, :oneOf)&.map { |s| s[:properties]&.keys&.join(', ') }
|
|
94
|
+
puts " output: #{output_shapes&.join(' | ')}" if output_shapes
|
|
95
|
+
puts ""
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mcp_authorization
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andy
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: mcp
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.10'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.10'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: minitest
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '5.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '5.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '13.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '13.0'
|
|
69
|
+
description: Add MCP tool serving to any Rails app. Write @rbs type annotations with
|
|
70
|
+
@requires(:flag) tags and the gem compiles per-user JSON Schema automatically. Feature
|
|
71
|
+
flags, permissions, and plan tiers all work through a single can?(:symbol) predicate.
|
|
72
|
+
email:
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- LICENSE
|
|
78
|
+
- README.md
|
|
79
|
+
- app/controllers/mcp_authorization/mcp_controller.rb
|
|
80
|
+
- lib/mcp_authorization.rb
|
|
81
|
+
- lib/mcp_authorization/configuration.rb
|
|
82
|
+
- lib/mcp_authorization/dsl.rb
|
|
83
|
+
- lib/mcp_authorization/engine.rb
|
|
84
|
+
- lib/mcp_authorization/rbs_schema_compiler.rb
|
|
85
|
+
- lib/mcp_authorization/tool.rb
|
|
86
|
+
- lib/mcp_authorization/tool_registry.rb
|
|
87
|
+
- lib/mcp_authorization/version.rb
|
|
88
|
+
- lib/tasks/mcp_authorization.rake
|
|
89
|
+
homepage:
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata: {}
|
|
93
|
+
post_install_message:
|
|
94
|
+
rdoc_options: []
|
|
95
|
+
require_paths:
|
|
96
|
+
- lib
|
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '3.1'
|
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0'
|
|
107
|
+
requirements: []
|
|
108
|
+
rubygems_version: 3.4.10
|
|
109
|
+
signing_key:
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: Rails engine for MCP tools with per-request schema discrimination
|
|
112
|
+
test_files: []
|