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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2e3e700975a36670beecb72ac35ea09692525da7af72e60b0aada577a06c15f
|
|
4
|
+
data.tar.gz: 4a7bc04fd73423db8a1704705c3d9855aeaf997ff677244029e935cd04d50f01
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
172
|
-
|
|
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
|
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.
|
|
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
|