rails_mcp_engine 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46864199e9aea2b6d1e7b9b9c065109f3f70a012c35cc68a632a8fe39fd1ff06
4
- data.tar.gz: a3cbbe5460b4c2d36e84a1d25fa3481e82ab678d1cd12dfef083819db352feb4
3
+ metadata.gz: b2e3e700975a36670beecb72ac35ea09692525da7af72e60b0aada577a06c15f
4
+ data.tar.gz: 4a7bc04fd73423db8a1704705c3d9855aeaf997ff677244029e935cd04d50f01
5
5
  SHA512:
6
- metadata.gz: a7f7134090f0de5dfb89878baf43ae74e6d5c0d9b9020744cdf4673f49e3daed9e0128e6d80c33d3e6fda0cd12af6b4e43e99307d6e44b817a88cf28e1952487
7
- data.tar.gz: c7eb6c1be9166b14e06341b5ab10a097345fceb88790fceb432c20e09d0c3d707066f9d756e0e30aaa411e1f7d48d0d75f0e7b572ddc1d2cad897ec7987056c5
6
+ metadata.gz: d3be894c46aa48a2a14116d77d6d5427df142ed026f89da13a3042c2ff7e935daa91ddd78f3e1a36702025926bbc9ff6f74e0bf249d4ce74f9861d99c92c55fe
7
+ data.tar.gz: 99c99e5a1e62477b9b0f24d01598e4738a032d4ae9bc2317b56326e05a5e29cc51679a770a33f50e7d01968d3653496bf197d9e1bbd90fb5fbfb578317b8321b
@@ -12,14 +12,11 @@ module RailsMcpEngine
12
12
 
13
13
  def register
14
14
  source = params[:source].to_s
15
- class_name = extract_class_name(source)
16
15
 
17
16
  result = if source.strip.empty?
18
17
  { error: 'Tool source code is required' }
19
- elsif class_name.nil?
20
- { error: 'Could not infer class name from the provided source' }
21
18
  else
22
- register_source(source, class_name)
19
+ register_source(source)
23
20
  end
24
21
 
25
22
  flash[:register_result] = result
@@ -50,7 +47,7 @@ module RailsMcpEngine
50
47
  result = if schema.nil?
51
48
  { error: "Tool not found: #{tool_name}" }
52
49
  else
53
- delete_tool_from_registry(schema[:service_class])
50
+ delete_tool_from_registry(tool_name)
54
51
  end
55
52
 
56
53
  flash[:register_result] = result
@@ -63,70 +60,7 @@ module RailsMcpEngine
63
60
  ToolMeta.registry.map { |service_class| ToolSchema::Builder.build(service_class) }
64
61
  end
65
62
 
66
- def extract_class_name(source)
67
- require 'ripper'
68
- sexp = Ripper.sexp(source)
69
- return nil unless sexp
70
-
71
- # sexp is [:program, statements]
72
- statements = sexp[1]
73
- find_class(statements, [])
74
- end
75
-
76
- def find_class(statements, namespace)
77
- return nil unless statements.is_a?(Array)
78
-
79
- statements.each do |stmt|
80
- next unless stmt.is_a?(Array)
81
-
82
- case stmt.first
83
- when :module
84
- # [:module, const_ref, body]
85
- # body is [:bodystmt, statements, ...]
86
- const_node = stmt[1]
87
- const_name = get_const_name(const_node)
88
-
89
- body_stmt = stmt[2]
90
- inner_statements = body_stmt[1]
91
-
92
- result = find_class(inner_statements, namespace + [const_name])
93
- return result if result
94
- when :class
95
- # [:class, const_ref, superclass, body]
96
- const_node = stmt[1]
97
- const_name = get_const_name(const_node)
98
-
99
- return (namespace + [const_name]).join('::')
100
- end
101
- end
102
- nil
103
- end
104
-
105
- def get_const_name(node)
106
- return nil unless node.is_a?(Array)
107
-
108
- type = node.first
109
- if type == :const_ref
110
- # [:const_ref, [:@const, "Name", ...]]
111
- node[1][1]
112
- elsif type == :const_path_ref
113
- # [:const_path_ref, parent, child]
114
- parent = node[1]
115
- child = node[2] # [:@const, "Name", ...]
116
-
117
- parent_name = if parent.first == :var_ref
118
- parent[1][1]
119
- else
120
- get_const_name(parent)
121
- end
122
-
123
- "#{parent_name}::#{child[1]}"
124
- else
125
- nil
126
- end
127
- end
128
-
129
- def register_source(source, class_name)
63
+ def register_source(source)
130
64
  Object.class_eval(source)
131
65
  # Use the engine's namespace or ensure Tools is available.
132
66
  # Assuming Tools module is defined in the host app or globally.
@@ -139,7 +73,8 @@ module RailsMcpEngine
139
73
  # The engine.rb defines ApplicationTool.
140
74
 
141
75
  # Re-using the logic from ManualController but adapting for Engine.
142
- ::Tools::MetaToolService.new.register_tool(
76
+
77
+ ::Tools::MetaToolWriteService.new.register_tool(
143
78
  class_name,
144
79
  before_call: ->(args) { Rails.logger.info(" [MCP] Request #{class_name}: #{args.inspect}") },
145
80
  after_call: ->(result) { Rails.logger.info(" [MCP] Response #{class_name}: #{result.inspect}") }
@@ -168,14 +103,8 @@ module RailsMcpEngine
168
103
  { error: e.message }
169
104
  end
170
105
 
171
- def delete_tool_from_registry(service_class)
172
- ToolMeta.registry.delete(service_class)
173
-
174
- # Also remove the RubyLLM tool class constant
175
- tool_constant = ToolSchema::RubyLlmFactory.tool_class_name(service_class)
176
- ::Tools.send(:remove_const, tool_constant) if ::Tools.const_defined?(tool_constant, false)
177
-
178
- { success: 'Tool deleted successfully' }
106
+ def delete_tool_from_registry(tool_name)
107
+ ::Tools::MetaToolWriteService.new.delete_tool(tool_name)
179
108
  rescue StandardError => e
180
109
  { error: e.message }
181
110
  end
@@ -44,26 +44,7 @@ module Tools
44
44
  end
45
45
  end
46
46
 
47
- sig { params(class_name: T.nilable(String), before_call: T.nilable(Proc), after_call: T.nilable(Proc)).returns(T::Hash[Symbol, T.untyped]) }
48
- def register_tool(class_name, before_call: nil, after_call: nil)
49
- return { error: 'class_name is required for register' } if class_name.nil? || class_name.empty?
50
47
 
51
- service_class = constantize(class_name)
52
- return { error: "Could not find #{class_name}" } if service_class.nil?
53
- return { error: "#{class_name} must extend ToolMeta" } unless service_class.respond_to?(:tool_metadata)
54
-
55
- ToolMeta.registry << service_class unless ToolMeta.registry.include?(service_class)
56
-
57
- schema = ToolSchema::Builder.build(service_class)
58
- ToolSchema::RubyLlmFactory.build(service_class, schema, before_call: before_call, after_call: after_call)
59
- ToolSchema::FastMcpFactory.build(service_class, schema, before_call: before_call, after_call: after_call)
60
-
61
- { status: 'registered', tool: summary_payload(schema) }
62
- rescue ToolMeta::MissingSignatureError => e
63
- { error: e.message }
64
- rescue NameError => e
65
- { error: "Could not find #{class_name}: #{e.message}" }
66
- end
67
48
 
68
49
  sig { params(tool_names: T::Array[String]).returns(T::Array[T.class_of(Object)]) }
69
50
  def self.ruby_llm_tools(tool_names)
@@ -76,7 +57,6 @@ module Tools
76
57
  end
77
58
  end
78
59
 
79
- private
80
60
 
81
61
  sig { params(query: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
82
62
  def search_tools(query)
@@ -153,11 +133,6 @@ module Tools
153
133
  "#{schema[:name]}(#{param_list})"
154
134
  end
155
135
 
156
- sig { params(name: String).returns(T.class_of(Object)) }
157
- def constantize(name)
158
- Object.const_get(name)
159
- end
160
-
161
136
  sig { params(value: T.untyped).returns(T.untyped) }
162
137
  def deep_symbolize(value)
163
138
  case value
@@ -0,0 +1,143 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+ require 'tool_meta'
6
+ require 'tool_schema/builder'
7
+ require 'tool_schema/ruby_llm_factory'
8
+ require 'tool_schema/fast_mcp_factory'
9
+
10
+ module Tools
11
+ class MetaToolWriteService
12
+ extend T::Sig
13
+
14
+ sig { params(class_name: T.nilable(String), before_call: T.nilable(Proc), after_call: T.nilable(Proc)).returns(T::Hash[Symbol, T.untyped]) }
15
+ def register_tool(class_name, before_call: nil, after_call: nil)
16
+ return { error: 'class_name is required for register' } if class_name.nil? || class_name.empty?
17
+
18
+ service_class = constantize(class_name)
19
+ return { error: "Could not find #{class_name}" } if service_class.nil?
20
+ return { error: "#{class_name} must extend ToolMeta" } unless service_class.respond_to?(:tool_metadata)
21
+
22
+ ToolMeta.registry << service_class unless ToolMeta.registry.include?(service_class)
23
+
24
+ schema = ToolSchema::Builder.build(service_class)
25
+ ToolSchema::RubyLlmFactory.build(service_class, schema, before_call: before_call, after_call: after_call)
26
+ ToolSchema::FastMcpFactory.build(service_class, schema, before_call: before_call, after_call: after_call)
27
+
28
+ { status: 'registered', tool: meta_service.summary_payload(schema) }
29
+ rescue ToolMeta::MissingSignatureError => e
30
+ { error: e.message }
31
+ rescue NameError => e
32
+ { error: "Could not find #{class_name}: #{e.message}" }
33
+ end
34
+
35
+ sig { params(source: T.nilable(String), before_call: T.nilable(Proc), after_call: T.nilable(Proc)).returns(T::Hash[Symbol, T.untyped]) }
36
+ def register_tool_from_source(source: nil, before_call: nil, after_call: nil)
37
+ class_name = extract_class_name(source)
38
+ return { error: 'class_name is required for register' } if class_name.nil? || class_name.empty?
39
+
40
+ # If source is provided, evaluate it first
41
+ if source
42
+ begin
43
+ Object.class_eval(source)
44
+ rescue StandardError => e
45
+ return { error: "Failed to evaluate source: #{e.message}" }
46
+ end
47
+ end
48
+
49
+ register_tool(class_name, before_call: before_call, after_call: after_call)
50
+ end
51
+
52
+ sig { params(tool_name: String).returns(T::Hash[Symbol, T.untyped]) }
53
+ def delete_tool(tool_name)
54
+ schema = meta_service.find_schema(tool_name)
55
+ return { error: "Tool not found: #{tool_name}" } unless schema
56
+
57
+ service_class = schema[:service_class]
58
+ ToolMeta.registry.delete(service_class)
59
+
60
+ tool_constant = ToolSchema::RubyLlmFactory.tool_class_name(service_class)
61
+ Tools.send(:remove_const, tool_constant) if Tools.const_defined?(tool_constant, false)
62
+ fast_mcp_constant = ToolSchema::FastMcpFactory.tool_class_name(service_class)
63
+ Mcp.send(:remove_const, fast_mcp_constant) if Mcp.const_defined?(fast_mcp_constant, false)
64
+
65
+ { success: 'Tool deleted successfully' }
66
+ rescue StandardError => e
67
+ { error: e.message }
68
+ end
69
+
70
+ private
71
+
72
+ def meta_service
73
+ @meta_service ||= Tools::MetaToolService.new
74
+ end
75
+
76
+ def extract_class_name(source)
77
+ require 'ripper'
78
+ sexp = Ripper.sexp(source)
79
+ return nil unless sexp
80
+
81
+ # sexp is [:program, statements]
82
+ statements = sexp[1]
83
+ find_class(statements, [])
84
+ end
85
+
86
+ def find_class(statements, namespace)
87
+ return nil unless statements.is_a?(Array)
88
+
89
+ statements.each do |stmt|
90
+ next unless stmt.is_a?(Array)
91
+
92
+ case stmt.first
93
+ when :module
94
+ # [:module, const_ref, body]
95
+ # body is [:bodystmt, statements, ...]
96
+ const_node = stmt[1]
97
+ const_name = get_const_name(const_node)
98
+
99
+ body_stmt = stmt[2]
100
+ inner_statements = body_stmt[1]
101
+
102
+ result = find_class(inner_statements, namespace + [const_name])
103
+ return result if result
104
+ when :class
105
+ # [:class, const_ref, superclass, body]
106
+ const_node = stmt[1]
107
+ const_name = get_const_name(const_node)
108
+
109
+ return (namespace + [const_name]).join('::')
110
+ end
111
+ end
112
+ nil
113
+ end
114
+
115
+ def get_const_name(node)
116
+ return nil unless node.is_a?(Array)
117
+
118
+ type = node.first
119
+ if type == :const_ref
120
+ # [:const_ref, [:@const, "Name", ...]]
121
+ node[1][1]
122
+ elsif type == :const_path_ref
123
+ # [:const_path_ref, parent, child]
124
+ parent = node[1]
125
+ child = node[2] # [:@const, "Name", ...]
126
+
127
+ parent_name = if parent.first == :var_ref
128
+ parent[1][1]
129
+ else
130
+ get_const_name(parent)
131
+ end
132
+
133
+ "#{parent_name}::#{child[1]}"
134
+ else
135
+ nil
136
+ end
137
+ end
138
+
139
+ def constantize(name)
140
+ Object.const_get(name)
141
+ end
142
+ end
143
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsMcpEngine
2
- VERSION = '0.2.0'
2
+ VERSION = '0.4.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_mcp_engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Soonoh Jung
@@ -92,6 +92,7 @@ files:
92
92
  - app/lib/tool_schema/ruby_llm_factory.rb
93
93
  - app/lib/tool_schema/sorbet_type_mapper.rb
94
94
  - app/services/tools/meta_tool_service.rb
95
+ - app/services/tools/meta_tool_write_service.rb
95
96
  - app/views/rails_mcp_engine/chat/show.html.erb
96
97
  - app/views/rails_mcp_engine/playground/show.html.erb
97
98
  - config/routes.rb