omni_agent 0.1.8 → 0.1.10

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,128 @@
1
+ module OmniAgent
2
+ module MCP
3
+ class Handler
4
+ class ProtocolError < StandardError
5
+ attr_reader :code
6
+
7
+ def initialize(code, message)
8
+ super(message)
9
+ @code = code
10
+ end
11
+ end
12
+
13
+ def initialize(server)
14
+ @server = server
15
+ end
16
+
17
+ # Takes a parsed JSON-RPC message and returns the response Hash, or nil
18
+ # when no response is due (notifications and client responses).
19
+ def call(message)
20
+ unless message.is_a?(Hash)
21
+ reason = message.is_a?(Array) ? "Batch requests are not supported" : "Invalid Request"
22
+ return error_response(nil, INVALID_REQUEST, reason)
23
+ end
24
+
25
+ message = message.transform_keys(&:to_s)
26
+ return nil if client_response?(message)
27
+
28
+ id = message["id"]
29
+ notification = !message.key?("id")
30
+
31
+ unless message["jsonrpc"] == JSONRPC_VERSION && message["method"].is_a?(String)
32
+ return notification ? nil : error_response(id, INVALID_REQUEST, "Invalid Request")
33
+ end
34
+
35
+ return nil if notification
36
+
37
+ result = dispatch(message["method"], message["params"])
38
+ success_response(id, result)
39
+ rescue ProtocolError => e
40
+ error_response(id, e.code, e.message)
41
+ rescue StandardError => e
42
+ log_error("MCP internal error", e)
43
+ error_response(id, INTERNAL_ERROR, "Internal error")
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :server
49
+
50
+ def dispatch(method, params)
51
+ params = normalize_params(params)
52
+
53
+ case method
54
+ when "initialize" then initialize_result(params)
55
+ when "ping" then {}
56
+ when "tools/list" then tools_list_result
57
+ when "tools/call" then tools_call_result(params)
58
+ else
59
+ raise ProtocolError.new(METHOD_NOT_FOUND, "Method not found: #{method}")
60
+ end
61
+ end
62
+
63
+ def normalize_params(params)
64
+ return {} if params.nil?
65
+ raise ProtocolError.new(INVALID_PARAMS, "params must be an object") unless params.is_a?(Hash)
66
+
67
+ params.transform_keys(&:to_s)
68
+ end
69
+
70
+ def initialize_result(params)
71
+ requested = params["protocolVersion"]
72
+ protocol_version = SUPPORTED_PROTOCOL_VERSIONS.include?(requested) ? requested : LATEST_PROTOCOL_VERSION
73
+ server_class = server.class
74
+
75
+ result = {
76
+ protocolVersion: protocol_version,
77
+ capabilities: { tools: { listChanged: false } },
78
+ serverInfo: { name: server_class.server_name, version: server_class.server_version }
79
+ }
80
+ result[:instructions] = server_class.instructions if server_class.instructions
81
+ result
82
+ end
83
+
84
+ def tools_list_result
85
+ tools = server.visible_tools.map { |name, tool_class| ToolAdapter.definition(name, tool_class) }
86
+ { tools: tools }
87
+ end
88
+
89
+ def tools_call_result(params)
90
+ name = params["name"]
91
+ raise ProtocolError.new(INVALID_PARAMS, "Missing tool name") unless name.is_a?(String)
92
+
93
+ tool_class = server.find_tool(name)
94
+ raise ProtocolError.new(INVALID_PARAMS, "Unknown tool: #{name}") unless tool_class
95
+
96
+ arguments = params["arguments"] || {}
97
+ raise ProtocolError.new(INVALID_PARAMS, "arguments must be an object") unless arguments.is_a?(Hash)
98
+
99
+ execute_tool(name, tool_class, arguments)
100
+ end
101
+
102
+ def execute_tool(name, tool_class, arguments)
103
+ tool = tool_class.new
104
+ tool.context = server.tool_context
105
+ ToolAdapter.result(tool.invoke(arguments))
106
+ rescue StandardError => e
107
+ log_error("MCP tool #{name} failed", e)
108
+ ToolAdapter.error_result("Error executing tool: #{e.message}")
109
+ end
110
+
111
+ def client_response?(message)
112
+ !message.key?("method") && (message.key?("result") || message.key?("error"))
113
+ end
114
+
115
+ def success_response(id, result)
116
+ { jsonrpc: JSONRPC_VERSION, id: id, result: result }
117
+ end
118
+
119
+ def error_response(id, code, message)
120
+ { jsonrpc: JSONRPC_VERSION, id: id, error: { code: code, message: message } }
121
+ end
122
+
123
+ def log_error(prefix, error)
124
+ MCP.logger&.error("[OmniAgent::MCP] #{prefix}: #{error.class}: #{error.message}")
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,107 @@
1
+ require "json"
2
+
3
+ module OmniAgent
4
+ module MCP
5
+ # Streamable HTTP transport, stateless: every POST carries one JSON-RPC
6
+ # message and gets a plain JSON response. No SSE stream, no sessions.
7
+ class RackApp
8
+ JSON_HEADERS = { "content-type" => "application/json" }.freeze
9
+ PARSE_FAILED = Object.new.freeze
10
+
11
+ # Accepts the server class or its name. Passing a String defers the
12
+ # constant lookup to request time, which keeps routes reload-safe.
13
+ attr_reader :server
14
+
15
+ def initialize(server)
16
+ @server = server
17
+ end
18
+
19
+ def inspect
20
+ "#<#{self.class.name} #{server}>"
21
+ end
22
+
23
+ def call(env)
24
+ server_class = MCP.resolve_server_class(@server)
25
+ request = Request.new(env)
26
+
27
+ return forbidden unless origin_allowed?(request, server_class)
28
+
29
+ auth = Authenticator.new(server_class).authenticate(request)
30
+ return unauthorized(server_class) unless auth.authenticated?
31
+
32
+ return method_not_allowed unless request.post?
33
+
34
+ if request.protocol_version && !SUPPORTED_PROTOCOL_VERSIONS.include?(request.protocol_version)
35
+ return http_error(400, "Unsupported MCP-Protocol-Version: #{request.protocol_version}")
36
+ end
37
+
38
+ body = read_body(request)
39
+ return http_error(413, "Request body too large") if body.nil?
40
+
41
+ message = parse_json(body)
42
+ return json_response(400, parse_error_response) if message.equal?(PARSE_FAILED)
43
+
44
+ server = server_class.new(principal: auth.principal, request: request, transport: :http)
45
+ response = Handler.new(server).call(message)
46
+ return [ 202, {}, [] ] if response.nil?
47
+
48
+ status = response[:error] && response[:id].nil? ? 400 : 200
49
+ json_response(status, response)
50
+ end
51
+
52
+ private
53
+
54
+ def origin_allowed?(request, server_class)
55
+ origin = request.origin
56
+ return true if origin.nil? || origin.empty?
57
+
58
+ allowed = server_class.configured_allowed_origins + Array(OmniAgent.configuration.mcp_allowed_origins)
59
+ allowed.any? do |pattern|
60
+ pattern.is_a?(Regexp) ? pattern.match?(origin) : pattern.to_s == origin
61
+ end
62
+ end
63
+
64
+ def read_body(request)
65
+ max_bytes = OmniAgent.configuration.mcp_max_request_bytes
66
+ return nil if request.content_length.to_i > max_bytes
67
+
68
+ io = request.body
69
+ return "" if io.nil?
70
+
71
+ body = io.read(max_bytes + 1).to_s
72
+ body.bytesize > max_bytes ? nil : body
73
+ end
74
+
75
+ def parse_json(body)
76
+ JSON.parse(body)
77
+ rescue JSON::ParserError
78
+ PARSE_FAILED
79
+ end
80
+
81
+ def parse_error_response
82
+ { jsonrpc: JSONRPC_VERSION, id: nil, error: { code: PARSE_ERROR, message: "Parse error" } }
83
+ end
84
+
85
+ def json_response(status, payload)
86
+ [ status, JSON_HEADERS.dup, [ JSON.generate(payload) ] ]
87
+ end
88
+
89
+ def http_error(status, message)
90
+ json_response(status, { error: message })
91
+ end
92
+
93
+ def forbidden
94
+ http_error(403, "Origin not allowed")
95
+ end
96
+
97
+ def method_not_allowed
98
+ [ 405, JSON_HEADERS.merge("allow" => "POST"), [ JSON.generate({ error: "Method not allowed" }) ] ]
99
+ end
100
+
101
+ def unauthorized(server_class)
102
+ headers = JSON_HEADERS.merge("www-authenticate" => %(Bearer realm="#{server_class.server_name}"))
103
+ [ 401, headers, [ JSON.generate({ error: "Unauthorized" }) ] ]
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,24 @@
1
+ require "rack"
2
+
3
+ module OmniAgent
4
+ module MCP
5
+ class Request < Rack::Request
6
+ def bearer_token
7
+ header = get_header("HTTP_AUTHORIZATION").to_s
8
+ match = header.match(/\ABearer\s+(.+)\z/i)
9
+ return if match.nil?
10
+
11
+ token = match[1].strip
12
+ token.empty? ? nil : token
13
+ end
14
+
15
+ def protocol_version
16
+ get_header("HTTP_MCP_PROTOCOL_VERSION")
17
+ end
18
+
19
+ def origin
20
+ get_header("HTTP_ORIGIN")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ require "active_support/core_ext/string/inflections"
2
+
3
+ module OmniAgent
4
+ module MCP
5
+ # Router helpers, included into ActionDispatch::Routing::Mapper by the engine.
6
+ #
7
+ # mcp_server :support # SupportServer at /mcp/support
8
+ # mcp_server :support, path: "/internal/mcp" # custom path
9
+ # mcp_servers :support, :billing # several at once
10
+ # mcp_servers # every app/mcp_servers/**/*_server.rb
11
+ module Routing
12
+ DEFAULT_PATH_PREFIX = "/mcp".freeze
13
+
14
+ def mcp_server(name, path: nil, server: nil, as: nil)
15
+ slug = OmniAgent::MCP::Routing.slug_for(name)
16
+ server_name = server.is_a?(Class) ? server.name : (server || "#{slug}_server".camelize).to_s
17
+
18
+ mount OmniAgent::MCP::RackApp.new(server_name) => (path || "#{DEFAULT_PATH_PREFIX}/#{slug.dasherize}"),
19
+ as: (as || "#{slug.tr('/', '_')}_mcp_server")
20
+ end
21
+
22
+ def mcp_servers(*names, except: [], directory: nil)
23
+ names = names.flatten
24
+ names = OmniAgent::MCP::Routing.discover(directory || Rails.root.join("app", "mcp_servers")) if names.empty?
25
+ excluded = Array(except).map { |name| OmniAgent::MCP::Routing.slug_for(name) }
26
+
27
+ names.each do |name|
28
+ mcp_server(name) unless excluded.include?(OmniAgent::MCP::Routing.slug_for(name))
29
+ end
30
+ end
31
+
32
+ # :support, "support", "support_server", "SupportServer" -> "support"
33
+ # "Admin::SupportServer", "admin/support" -> "admin/support"
34
+ def self.slug_for(name)
35
+ name.to_s.underscore.delete_suffix("_server")
36
+ end
37
+
38
+ # Server slugs for every *_server.rb file, without loading any code.
39
+ # Skips files inside a server's own directory (e.g. support_server/tools/)
40
+ # and abstract base servers named application_*.
41
+ def self.discover(directory)
42
+ root = Pathname.new(directory.to_s)
43
+ return [] unless root.directory?
44
+
45
+ Dir.glob(root.join("**", "*_server.rb").to_s).sort.filter_map do |file|
46
+ relative = Pathname.new(file).relative_path_from(root).to_s.delete_suffix(".rb")
47
+ *dirs, base = relative.split("/")
48
+
49
+ next if dirs.any? { |dir| dir.end_with?("_server") }
50
+ next if base.start_with?("application_")
51
+
52
+ slug_for(relative)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,238 @@
1
+ require "active_support/core_ext/string/inflections"
2
+
3
+ module OmniAgent
4
+ module MCP
5
+ class Server
6
+ TOOL_NAME_PATTERN = /\A[A-Za-z0-9_.-]{1,128}\z/
7
+
8
+ class << self
9
+ def server_name(value = nil)
10
+ @server_name = value.to_s if value
11
+ setting(:@server_name) || default_server_name
12
+ end
13
+
14
+ def server_version(value = nil)
15
+ @server_version = value.to_s if value
16
+ setting(:@server_version) || "0.1.0"
17
+ end
18
+
19
+ def instructions(text = nil)
20
+ @instructions = text if text
21
+ setting(:@instructions)
22
+ end
23
+
24
+ def tools(*tool_classes)
25
+ tool_classes.flatten.each { |tool_class| tool(tool_class) }
26
+ end
27
+
28
+ def tool(tool_class, as: nil)
29
+ add_tool_declaration(type: :tool, tool_class: tool_class, name: as&.to_s)
30
+ end
31
+
32
+ def tools_from(*agent_classes)
33
+ agent_classes.flatten.each do |agent_class|
34
+ add_tool_declaration(type: :agent_tools, agent_class: agent_class)
35
+ end
36
+ end
37
+
38
+ def expose_agent(agent_class, as:, description: nil, run_alias: nil, forward: [])
39
+ unless agent_class.is_a?(Class) && agent_class <= OmniAgent::Agent
40
+ raise ArgumentError, "expose_agent requires an OmniAgent::Agent subclass"
41
+ end
42
+
43
+ tool_class = OmniAgent::Agent.build_delegated_tool_class(
44
+ agent_class,
45
+ description: description,
46
+ run_alias: run_alias,
47
+ forward: forward
48
+ )
49
+
50
+ add_tool_declaration(type: :tool, tool_class: tool_class, name: as.to_s)
51
+ end
52
+
53
+ def authenticate(strategy = nil, tokens: nil, &block)
54
+ @authentication = if block
55
+ { strategy: :custom, block: block }
56
+ elsif strategy == :none
57
+ { strategy: :none }
58
+ elsif strategy == :bearer
59
+ raise ArgumentError, "authenticate :bearer requires tokens:" if tokens.nil?
60
+
61
+ { strategy: :bearer, tokens: tokens }
62
+ else
63
+ raise ArgumentError, "authenticate expects :bearer (with tokens:), :none, or a block"
64
+ end
65
+ end
66
+
67
+ def authorize_tool(&block)
68
+ raise ArgumentError, "authorize_tool requires a block" unless block
69
+
70
+ @authorize_tool_block = block
71
+ end
72
+
73
+ def context(&block)
74
+ raise ArgumentError, "context requires a block" unless block
75
+
76
+ @context_block = block
77
+ end
78
+
79
+ def stdio_principal(&block)
80
+ raise ArgumentError, "stdio_principal requires a block" unless block
81
+
82
+ @stdio_principal_block = block
83
+ end
84
+
85
+ def allowed_origins(*origins)
86
+ @allowed_origins = (@allowed_origins || []) + origins.flatten
87
+ end
88
+
89
+ def configured_tool_declarations; list_setting(:@tool_declarations); end
90
+ def configured_authentication; setting(:@authentication); end
91
+ def configured_authorize_tool_block; setting(:@authorize_tool_block); end
92
+ def configured_context_block; setting(:@context_block); end
93
+ def configured_stdio_principal_block; setting(:@stdio_principal_block); end
94
+ def configured_allowed_origins; list_setting(:@allowed_origins); end
95
+
96
+ # Ordered { "ToolName" => tool_class }: for each server in the
97
+ # inheritance chain, its <Server>::Tools namespace first, then its
98
+ # declared tools. Resolved on every call so lazily referenced tools
99
+ # (strings, tools_from, the Tools namespace) pick up code reloads.
100
+ def tool_registry
101
+ server_ancestors.reverse.each_with_object({}) do |server_class, registry|
102
+ entries = server_class.namespace_tool_classes.map { |tool_class| [ default_tool_name(tool_class), tool_class ] }
103
+ entries += (server_class.instance_variable_get(:@tool_declarations) || []).flat_map do |declaration|
104
+ resolve_declaration(declaration)
105
+ end
106
+
107
+ entries.each { |name, tool_class| register_tool(registry, name, tool_class) }
108
+ end
109
+ end
110
+
111
+ # Tools defined under this server's own namespace, e.g.
112
+ # app/mcp_servers/support_server/tools/*.rb -> SupportServer::Tools::*.
113
+ def namespace_tool_classes
114
+ return [] unless const_defined?(:Tools, false)
115
+
116
+ namespace = const_get(:Tools, false)
117
+ return [] unless namespace.is_a?(Module)
118
+
119
+ namespace.constants.sort.filter_map do |const_name|
120
+ const = namespace.const_get(const_name, false)
121
+ const if const.is_a?(Class) && const < OmniAgent::Tool
122
+ end
123
+ end
124
+
125
+ def resolve_stdio_principal
126
+ configured_stdio_principal_block&.call
127
+ end
128
+
129
+ private
130
+
131
+ # Servers inherit settings from parent servers (e.g. an ApplicationMCPServer
132
+ # that declares `authenticate` once). Scalars: nearest wins. Lists: concatenated.
133
+ def server_ancestors
134
+ ancestors.select { |klass| klass.is_a?(Class) && klass < OmniAgent::MCP::Server }
135
+ end
136
+
137
+ def setting(ivar)
138
+ server_ancestors.each do |klass|
139
+ value = klass.instance_variable_get(ivar)
140
+ return value unless value.nil?
141
+ end
142
+
143
+ nil
144
+ end
145
+
146
+ def list_setting(ivar)
147
+ server_ancestors.reverse.flat_map { |klass| klass.instance_variable_get(ivar) || [] }
148
+ end
149
+
150
+ def default_server_name
151
+ return "omni_agent" if name.nil?
152
+
153
+ base = name.demodulize.underscore
154
+ base = base.delete_suffix("_server") unless base == "server"
155
+ base
156
+ end
157
+
158
+ def add_tool_declaration(declaration)
159
+ @tool_declarations = (@tool_declarations || []) + [ declaration ]
160
+ end
161
+
162
+ def resolve_declaration(declaration)
163
+ case declaration[:type]
164
+ when :tool
165
+ tool_class = resolve_constant(declaration[:tool_class])
166
+ [ [ declaration[:name] || default_tool_name(tool_class), tool_class ] ]
167
+ when :agent_tools
168
+ agent_class = resolve_constant(declaration[:agent_class])
169
+ agent_class.tool_classes.map { |tool_class| [ default_tool_name(tool_class), tool_class ] }
170
+ end
171
+ end
172
+
173
+ def resolve_constant(value)
174
+ return value if value.is_a?(Class)
175
+
176
+ Object.const_get(value.to_s)
177
+ end
178
+
179
+ def default_tool_name(tool_class)
180
+ if tool_class.name.nil?
181
+ raise OmniAgent::MCPError, "Anonymous tool classes need an explicit name: `tool klass, as: \"name\"`"
182
+ end
183
+
184
+ tool_class.name.demodulize
185
+ end
186
+
187
+ def register_tool(registry, name, tool_class)
188
+ unless tool_class.is_a?(Class) && tool_class < OmniAgent::Tool
189
+ raise OmniAgent::MCPError, "#{tool_class.inspect} is not an OmniAgent::Tool subclass"
190
+ end
191
+
192
+ unless name.match?(TOOL_NAME_PATTERN)
193
+ raise OmniAgent::MCPError,
194
+ "Invalid MCP tool name #{name.inspect} in #{self.name}: use 1-128 characters from A-Z, a-z, 0-9, _, -, ."
195
+ end
196
+
197
+ if registry.key?(name)
198
+ raise OmniAgent::MCPError,
199
+ "Duplicate MCP tool name #{name.inspect} in #{self.name}. Use `tool klass, as: \"other_name\"` to rename one."
200
+ end
201
+
202
+ registry[name] = tool_class
203
+ end
204
+ end
205
+
206
+ attr_reader :principal, :request, :transport
207
+
208
+ def initialize(principal: nil, request: nil, transport: :http)
209
+ @principal = principal
210
+ @request = request
211
+ @transport = transport
212
+ end
213
+
214
+ def visible_tools
215
+ authorizer = self.class.configured_authorize_tool_block
216
+ registry = self.class.tool_registry
217
+ return registry unless authorizer
218
+
219
+ registry.select do |name, tool_class|
220
+ instance_exec(tool_class, principal, name, &authorizer)
221
+ end
222
+ end
223
+
224
+ def find_tool(name)
225
+ visible_tools[name.to_s]
226
+ end
227
+
228
+ def tool_context
229
+ base = { mcp_principal: principal, mcp_request: request }
230
+ context_block = self.class.configured_context_block
231
+ return base unless context_block
232
+
233
+ extra = instance_exec(principal, request, &context_block)
234
+ extra.is_a?(Hash) ? base.merge(extra) : base
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,56 @@
1
+ require "json"
2
+
3
+ module OmniAgent
4
+ module MCP
5
+ # stdio transport: newline-delimited JSON-RPC over stdin/stdout. Runs as a
6
+ # local subprocess of the client, so it skips HTTP authentication and takes
7
+ # its principal from the server's `stdio_principal` block instead.
8
+ class Stdio
9
+ # Entry point for `omni_agent mcp` and `rake omni_agent:mcp`. Keeps a
10
+ # private handle on the real stdout for the protocol, then points fd 1
11
+ # at stderr so stray `puts` or logger output cannot corrupt the stream.
12
+ def self.start(server, input: $stdin)
13
+ server_class = MCP.resolve_server_class(server)
14
+
15
+ protocol_output = $stdout.dup
16
+ $stdout.flush
17
+ $stdout.reopen($stderr)
18
+
19
+ new(server_class, input: input, output: protocol_output).run
20
+ end
21
+
22
+ def initialize(server, input:, output:)
23
+ @server_class = MCP.resolve_server_class(server)
24
+ @input = input
25
+ @output = output
26
+ end
27
+
28
+ def run
29
+ server = @server_class.new(principal: @server_class.resolve_stdio_principal, transport: :stdio)
30
+ handler = Handler.new(server)
31
+
32
+ @input.each_line do |line|
33
+ line = line.strip
34
+ next if line.empty?
35
+
36
+ message = begin
37
+ JSON.parse(line)
38
+ rescue JSON::ParserError
39
+ write(jsonrpc: JSONRPC_VERSION, id: nil, error: { code: PARSE_ERROR, message: "Parse error" })
40
+ next
41
+ end
42
+
43
+ response = handler.call(message)
44
+ write(response) if response
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def write(payload)
51
+ @output.write("#{JSON.generate(payload)}\n")
52
+ @output.flush
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,45 @@
1
+ require "json"
2
+
3
+ module OmniAgent
4
+ module MCP
5
+ module ToolAdapter
6
+ module_function
7
+
8
+ def definition(name, tool_class)
9
+ definition = { name: name }
10
+ definition[:title] = tool_class.title if tool_class.title
11
+ definition[:description] = tool_class.description
12
+ definition[:inputSchema] = tool_class.json_schema
13
+
14
+ annotations = tool_class.mcp_annotations
15
+ definition[:annotations] = annotations unless annotations.empty?
16
+
17
+ definition
18
+ end
19
+
20
+ def result(value)
21
+ case value
22
+ when nil
23
+ { content: [ text_content("") ] }
24
+ when String
25
+ { content: [ text_content(value) ] }
26
+ when Hash
27
+ json = JSON.generate(value)
28
+ { content: [ text_content(json) ], structuredContent: JSON.parse(json) }
29
+ when Array
30
+ { content: [ text_content(JSON.generate(value)) ] }
31
+ else
32
+ { content: [ text_content(value.to_s) ] }
33
+ end
34
+ end
35
+
36
+ def error_result(message)
37
+ { content: [ text_content(message) ], isError: true }
38
+ end
39
+
40
+ def text_content(text)
41
+ { type: "text", text: text }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ module OmniAgent
2
+ module MCP
3
+ SUPPORTED_PROTOCOL_VERSIONS = [ "2025-11-25", "2025-06-18", "2025-03-26" ].freeze
4
+ LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS.first
5
+
6
+ JSONRPC_VERSION = "2.0".freeze
7
+
8
+ PARSE_ERROR = -32_700
9
+ INVALID_REQUEST = -32_600
10
+ METHOD_NOT_FOUND = -32_601
11
+ INVALID_PARAMS = -32_602
12
+ INTERNAL_ERROR = -32_603
13
+
14
+ def self.logger
15
+ return unless defined?(Rails) && Rails.respond_to?(:logger)
16
+
17
+ Rails.logger
18
+ end
19
+
20
+ def self.resolve_server_class(server)
21
+ return server if server.is_a?(Class)
22
+
23
+ server_class = Object.const_get(server.to_s)
24
+ unless server_class.is_a?(Class) && server_class < OmniAgent::MCP::Server
25
+ raise OmniAgent::MCPError, "#{server} is not an OmniAgent::MCP::Server subclass"
26
+ end
27
+
28
+ server_class
29
+ end
30
+ end
31
+ end
@@ -9,7 +9,7 @@ module OmniAgent
9
9
  @model = model || default_model
10
10
  end
11
11
 
12
- def chat(messages:, tools: [], **_options)
12
+ def chat(messages:, tools: [], stream: nil, **_options)
13
13
  raise NotImplementedError, "Providers must implement #chat"
14
14
  end
15
15