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.
@@ -0,0 +1,64 @@
1
+ module McpAuthorization
2
+ # Mixin for handler classes — the plain Ruby objects that implement the
3
+ # business logic behind each MCP tool.
4
+ #
5
+ # Include this module instead of hand-rolling +initialize+, permission
6
+ # checks, and MCP notification plumbing:
7
+ #
8
+ # class Handlers::ListOrders
9
+ # include McpAuthorization::DSL
10
+ #
11
+ # def description
12
+ # "List recent orders"
13
+ # end
14
+ #
15
+ # #: (status: String, ?limit: Integer @requires(:admin)) -> Hash[Symbol, untyped]
16
+ # def call(status:, limit: 25)
17
+ # orders = Order.where(status: status).limit(limit)
18
+ # { orders: orders.map(&:as_json) }
19
+ # end
20
+ # end
21
+ #
22
+ # == What it provides
23
+ #
24
+ # * +server_context+ — the per-request context object built by the host
25
+ # app's +context_builder+.
26
+ # * +can?(flag)+ — convenience delegation to +current_user.can?+.
27
+ # * +report_progress+ / +notify_log_message+ — thin wrappers around MCP
28
+ # session notifications, safe to call even when the context doesn't
29
+ # support them (e.g. during +tools/list+).
30
+ #
31
+ module DSL
32
+ # The per-request context built by the host app's +context_builder+.
33
+ #: untyped
34
+ attr_reader :server_context
35
+
36
+ #: (server_context: untyped) -> void
37
+ def initialize(server_context:)
38
+ @server_context = server_context
39
+ end
40
+
41
+ # Convenience check — delegates to +server_context.current_user.can?+.
42
+ # Use inside +#call+ for runtime branching beyond +@requires+ filtering.
43
+ #: (Symbol) -> bool
44
+ def can?(flag)
45
+ server_context.current_user.can?(flag)
46
+ end
47
+
48
+ # Send a progress notification to the MCP client (MCP 0.10+).
49
+ # Safe to call unconditionally — no-ops when context lacks support.
50
+ #: (Numeric, ?total: Numeric?, ?message: String?) -> void
51
+ def report_progress(progress, total: nil, message: nil)
52
+ return unless server_context.respond_to?(:report_progress)
53
+ server_context.report_progress(progress, total: total, message: message)
54
+ end
55
+
56
+ # Send a log message notification to the MCP client (MCP 0.10+).
57
+ # Safe to call unconditionally — no-ops when context lacks support.
58
+ #: (data: untyped, level: String | Symbol, ?logger: String?) -> void
59
+ def notify_log_message(data:, level:, logger: nil)
60
+ return unless server_context.respond_to?(:notify_log_message)
61
+ server_context.notify_log_message(data: data, level: level, logger: logger)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,68 @@
1
+ module McpAuthorization
2
+ # Rails Engine that wires the gem into a host application.
3
+ #
4
+ # The engine handles three things automatically so the host app doesn't
5
+ # have to:
6
+ #
7
+ # 1. *Autoload paths* — tool directories (default +app/mcp+) are added to
8
+ # the Rails autoloader so tool and handler classes are discovered
9
+ # without explicit requires.
10
+ #
11
+ # 2. *Cache invalidation* — on code reload (development mode) the
12
+ # ToolRegistry and RbsSchemaCompiler caches are cleared. Tools
13
+ # re-register themselves via the +inherited+ hook when their classes
14
+ # are reloaded.
15
+ #
16
+ # 3. *Route mounting* — MCP endpoints are prepended to the host app's
17
+ # router at +config.mount_path+ (default +/mcp+). The routes support
18
+ # multi-domain routing via an optional +:domain+ segment:
19
+ #
20
+ # POST /mcp → default domain
21
+ # POST /mcp/operator → "operator" domain
22
+ # POST /mcp/recruiting → "recruiting" domain
23
+ #
24
+ # All three HTTP methods required by the MCP StreamableHTTP transport
25
+ # (GET, POST, DELETE) are accepted.
26
+ #
27
+ class Engine < ::Rails::Engine
28
+ isolate_namespace McpAuthorization
29
+
30
+ # Add configured tool_paths to the Rails autoloader so tool classes
31
+ # (and their handler classes) are discovered without manual requires.
32
+ initializer "mcp_authorization.autoload_paths", before: :set_autoload_paths do |app|
33
+ McpAuthorization.config.tool_paths.each do |path|
34
+ full_path = Rails.root.join(path).to_s
35
+ if File.directory?(full_path)
36
+ app.config.autoload_paths << full_path
37
+ app.config.eager_load_paths << full_path
38
+ end
39
+ end
40
+ end
41
+
42
+ # Clear caches on code reload so stale class references and parsed
43
+ # schemas are dropped. Tools re-register via the inherited hook when
44
+ # their classes are reloaded by Zeitwerk.
45
+ initializer "mcp_authorization.reloader" do |app|
46
+ app.reloader.to_prepare do
47
+ McpAuthorization::ToolRegistry.reset!
48
+ McpAuthorization::RbsSchemaCompiler.reset_cache!
49
+ end
50
+ end
51
+
52
+ # Prepend MCP routes into the host app's router. Uses +prepend+ so the
53
+ # MCP endpoint is available before any catch-all routes the host may
54
+ # define. Supports both domain-scoped and bare paths.
55
+ initializer "mcp_authorization.routes", before: :finisher_hook do |app|
56
+ default_domain = McpAuthorization.config.default_domain
57
+ mount_path = McpAuthorization.config.mount_path
58
+
59
+ app.routes.prepend do
60
+ scope mount_path, module: :mcp_authorization do
61
+ match ":domain", to: "mcp#handle", via: [ :get, :post, :delete ]
62
+ match "/", to: "mcp#handle", via: [ :get, :post, :delete ],
63
+ defaults: { domain: default_domain }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end