omniai 2.1.1 → 2.2.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: 90c0c98df3552c3e1351671ebf9d02847f96a4cf8c92163a25cd1e119d3b3e7d
4
- data.tar.gz: 509907dabedafa041381621aea668747547cd60da6edd78d9fdfd325c224c1a5
3
+ metadata.gz: e0d7be9d7a666bdabc7c18e8ce6b090e5c69622d469d3444278a5054a82fab92
4
+ data.tar.gz: c6a0aaf46493811c8210ac1a8bec5dcb1999094dc38edd4f5fee3fdc489f554d
5
5
  SHA512:
6
- metadata.gz: e96e2441296a6283ce5bfe9f3025c2a3838466d1f2c44ff4bc95f1f4b84a7dd7559c227f909382e7d01fb676b13eb344b3b164c0c96e0963b52bf377c2bb73d8
7
- data.tar.gz: bcc1d3f357c57c010b0064ba2e2becc74df572263d9378351a22d0a9f17b4fa3336b9645e7f221a78d620d72a2afe2c922f2f337b56f6c955a752e7188457f38
6
+ metadata.gz: 277b89d8176066963f4c6401eedd1397539789b8dc148ddbd6332d42a8e109ccf2f862f0f5257913075695911c7c7e4c2aa0d863cd5a471166a1e0835bd3f08d
7
+ data.tar.gz: 00b867d114d24be9ddf474e8ace64bbf6ae662737527cdd9fe27ea69941bbea082c505581dd781fb56b6eba03980b77580e0d4eeaf6caeca4174c0e376341236
data/README.md CHANGED
@@ -530,3 +530,44 @@ Type 'exit' or 'quit' to abort.
530
530
  0.0
531
531
  ...
532
532
  ```
533
+
534
+ ### MCP
535
+
536
+ [MCP](https://modelcontextprotocol.io/introduction) is an open protocol designed to standardize giving context to LLMs. The OmniAI implementation supports building an MCP server that operates via the [stdio](https://modelcontextprotocol.io/docs/concepts/transports) transport.
537
+
538
+ **main.rb**
539
+
540
+ ```ruby
541
+ class Weather < OmniAI::Tool
542
+ description "Lookup the weather for a location"
543
+
544
+ parameter :location, :string, description: "A location (e.g. 'London' or 'Madrid')."
545
+ required %i[location]
546
+
547
+ # @param location [String] required
548
+ # @return [String]
549
+ def execute(location:)
550
+ case location
551
+ when 'London' then 'Rainy'
552
+ when 'Madrid' then 'Sunny'
553
+ end
554
+ end
555
+ end
556
+
557
+ transport = OmniAI::MCP::Transport::Stdio.new
558
+ mcp = OmniAI::MCP::Server.new(tools: [Weather.new])
559
+ mcp.run(transport:)
560
+ ```
561
+
562
+ ```bash
563
+ ruby main.rb
564
+ ```
565
+
566
+ ```bash
567
+ {
568
+ "jsonrpc": "2.0",
569
+ "id": 1,
570
+ "method": "tools/call",
571
+ "params": { "name": "echo", "arguments": { "message": "Hello, world!" } }
572
+ }
573
+ ```
@@ -25,7 +25,7 @@ module OmniAI
25
25
  # @return [Content]
26
26
  def self.deserialize(data, context: nil)
27
27
  return data if data.nil?
28
- return data.map { |data| deserialize(data, context:) } if data.is_a?(Array)
28
+ return data.map { |entry| deserialize(entry, context:) } if data.is_a?(Array)
29
29
 
30
30
  deserialize = context&.deserializer(:content)
31
31
  return deserialize.call(data, context:) if deserialize
data/lib/omniai/chat.rb CHANGED
@@ -109,6 +109,11 @@ module OmniAI
109
109
 
110
110
  protected
111
111
 
112
+ # @return [Boolean]
113
+ def stream?
114
+ !@stream.nil?
115
+ end
116
+
112
117
  # Override to provide an context for serializers / deserializes for a provider.
113
118
  #
114
119
  # @return [Context, nil]
@@ -151,7 +156,7 @@ module OmniAI
151
156
  #
152
157
  # @return [OmniAI::Chat::Response]
153
158
  def parse!(response:)
154
- if @stream
159
+ if stream?
155
160
  stream!(response:)
156
161
  else
157
162
  complete!(response:)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module MCP
5
+ module JRPC
6
+ # @example
7
+ # raise OmniAI::MCP::JRPC::Error.new(code: OmniAI::MCP::JRPC::Error::PARSE_ERROR, message: "Invalid JSON")
8
+ class Error < StandardError
9
+ module Code
10
+ PARSE_ERROR = -32_700
11
+ INVALID_REQUEST = -32_600
12
+ METHOD_NOT_FOUND = -32_601
13
+ INVALID_PARAMS = -32_602
14
+ INTERNAL_ERROR = -32_603
15
+ end
16
+
17
+ # @!attribute [r] code
18
+ # @return [Integer]
19
+ attr_accessor :code
20
+
21
+ # @!attribute [r] message
22
+ # @return [String]
23
+ attr_accessor :message
24
+
25
+ # @param code [Integer]
26
+ # @param message [String]
27
+ def initialize(code:, message:)
28
+ super("code=#{code} message=#{message}")
29
+
30
+ @code = code
31
+ @message = message
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module MCP
5
+ module JRPC
6
+ # @example
7
+ # request = OmniAI::MCP::JRPC::Request.new(id: 0, method: "ping", params: {})
8
+ # request.id #=> 0
9
+ # request.method #=> "ping"
10
+ # request.params #=> {}
11
+ class Request
12
+ # @!attribute [rw] id
13
+ # @return [Integer]
14
+ attr_accessor :id
15
+
16
+ # @!attribute [rw] method
17
+ # @return [String]
18
+ attr_accessor :method
19
+
20
+ # @!attribute [rw] params
21
+ # @return [Hash]
22
+ attr_accessor :params
23
+
24
+ # @param id [Integer]
25
+ # @param method [String]
26
+ # @param params [Hash]
27
+ def initialize(id:, method:, params:)
28
+ @id = id
29
+ @method = method
30
+ @params = params
31
+ end
32
+
33
+ # @return [String]
34
+ def inspect
35
+ "#<#{self.class.name} id=#{@id} method=#{@method} params=#{@params}>"
36
+ end
37
+
38
+ # @return [String]
39
+ def generate
40
+ JSON.generate({
41
+ jsonrpc: VERSION,
42
+ id: @id,
43
+ method: @method,
44
+ params: @params,
45
+ })
46
+ end
47
+
48
+ # @param text [String]
49
+ #
50
+ # @raise [OmniAI::MCP::JRPC::Error]
51
+ #
52
+ # @return [OmniAI::MCP::JRPC::Request]
53
+ def self.parse(text)
54
+ data =
55
+ begin
56
+ JSON.parse(text)
57
+ rescue JSON::ParserError => e
58
+ raise Error.new(code: Error::Code::PARSE_ERROR, message: e.message)
59
+ end
60
+
61
+ id = data["id"] || raise(Error.new(code: Error::Code::PARSE_ERROR, message: "missing id"))
62
+ method = data["method"] || raise(Error.new(code: Error::Code::PARSE_ERROR, message: "missing method"))
63
+ params = data["params"] || raise(Error.new(code: Error::Code::PARSE_ERROR, message: "missing params"))
64
+
65
+ new(id:, method:, params:)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module MCP
5
+ module JRPC
6
+ # @example
7
+ # request = OmniAI::MCP::JRPC::Response.new(id: 0, result: "OK")
8
+ # request.generate #=> { "jsonrpc": "2.0", "id": 0, "result": "OK" }
9
+ class Response
10
+ # @!attribute [rw] id
11
+ # @return [Integer]
12
+ attr_accessor :id
13
+
14
+ # @!attribute [rw] result
15
+ # @return [String]
16
+ attr_accessor :result
17
+
18
+ # @param id [Integer]
19
+ # @param result [String]
20
+ def initialize(id:, result:)
21
+ @id = id
22
+ @result = result
23
+ end
24
+
25
+ # @return [String]
26
+ def inspect
27
+ "#<#{self.class.name} id=#{@id} result=#{@result}>"
28
+ end
29
+
30
+ # @return [String]
31
+ def generate
32
+ JSON.generate({
33
+ jsonrpc: VERSION,
34
+ id: @id,
35
+ result: @result,
36
+ })
37
+ end
38
+
39
+ # @param text [String]
40
+ #
41
+ # @raise [OmniAI::MCP::JRPC::Error]
42
+ #
43
+ # @return [OmniAI::MCP::JRPC::Response]
44
+ def self.parse(text)
45
+ data =
46
+ begin
47
+ JSON.parse(text)
48
+ rescue JSON::ParserError => e
49
+ raise Error.new(code: Error::Code::PARSE_ERROR, message: e.message)
50
+ end
51
+
52
+ id = data["id"] || raise(Error.new(code: Error::Code::PARSE_ERROR, message: "missing id"))
53
+ result = data["result"] || raise(Error.new(code: Error::Code::PARSE_ERROR, message: "missing result"))
54
+
55
+ new(id:, result:)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module MCP
5
+ # @example
6
+ # OmniAI::MCP::JRPC::VERSION
7
+ module JRPC
8
+ VERSION = "2.0"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module MCP
5
+ # @example
6
+ # server = OmniAI::MCP::Server.new(tools: [...])
7
+ # server.run
8
+ class Server
9
+ PROTOCOL_VERSION = "2025-03-26"
10
+
11
+ # @param tools [Array<OmniAI::Tool>]
12
+ # @param logger [Logger, nil]
13
+ def initialize(tools:, logger: nil, name: "OmniAI", version: OmniAI::VERSION)
14
+ @tools = tools
15
+ @logger = logger
16
+ @name = name
17
+ @version = version
18
+ end
19
+
20
+ # @param transport [OmniAI::MCP::Transport]
21
+ def run(transport: OmniAI::MCP::Transport::Stdio.new)
22
+ loop do
23
+ message = transport.gets
24
+ break if message.nil?
25
+
26
+ @logger&.info("#{self.class}#run: message=#{message.inspect}")
27
+ response = process(message)
28
+ @logger&.info("#{self.class}#run: response=#{response.inspect}")
29
+
30
+ transport.puts(response) if response
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # @param message [String]
37
+ #
38
+ # @return [String, nil]
39
+ def process(message)
40
+ request = JRPC::Request.parse(message)
41
+
42
+ response =
43
+ case request.method
44
+ when "initialize" then process_initialize(request:)
45
+ when "ping" then process_ping(request:)
46
+ when "tools/list" then process_tools_list(request:)
47
+ when "tools/call" then process_tools_call(request:)
48
+ end
49
+
50
+ response&.generate if response&.result
51
+ rescue JRPC::Error => e
52
+ JSON.generate({
53
+ jsonrpc: JRPC::VERSION,
54
+ id: request&.id,
55
+ error: {
56
+ code: e.code,
57
+ message: e.message,
58
+ },
59
+ })
60
+ end
61
+
62
+ # @param request [JRPC::Request]
63
+ # @return [JRPC::Response]
64
+ def process_initialize(request:)
65
+ JRPC::Response.new(id: request.id, result: {
66
+ protocolVersion: PROTOCOL_VERSION,
67
+ serverInfo: {
68
+ name: @name,
69
+ version: @version,
70
+ },
71
+ capabilities: {},
72
+ })
73
+ end
74
+
75
+ # @param request [JRPC::Request]
76
+ # @return [JRPC::Response]
77
+ def process_ping(request:)
78
+ JRPC::Response.new(id: request.id, result: {})
79
+ end
80
+
81
+ # @param request [JRPC::Request]
82
+ #
83
+ # @raise [JRPC::Error]
84
+ #
85
+ # @return [JRPC::Response]
86
+ def process_tools_list(request:)
87
+ result = @tools.map do |tool|
88
+ {
89
+ name: tool.name,
90
+ description: tool.description,
91
+ inputSchema: tool.parameters.serialize,
92
+ }
93
+ end
94
+
95
+ JRPC::Response.new(id: request.id, result:)
96
+ end
97
+
98
+ # @param request [JRPC::Request]
99
+ #
100
+ # @raise [JRPC::Error]
101
+ #
102
+ # @return [JRPC::Response]
103
+ def process_tools_call(request:)
104
+ name = request.params["name"]
105
+ tool = @tools.find { |tool| tool.name.eql?(name) }
106
+
107
+ result =
108
+ begin
109
+ tool.call(request.params["input"])
110
+ rescue StandardError => e
111
+ raise JRPC::Error.new(code: JRPC::Error::Code::INTERNAL_ERROR, message: e.message)
112
+ end
113
+
114
+ JRPC::Response.new(id: request.id, result:)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module MCP
5
+ module Transport
6
+ # @example
7
+ # transport = OmniAI::MCP::Transport::Base.new
8
+ # transport.puts("Hello World")
9
+ # transport.gets
10
+ class Base
11
+ # @param text [String]
12
+ def puts(text)
13
+ raise NotImplementedError, "#{self.class}#gets undefined"
14
+ end
15
+
16
+ # @return [String]
17
+ def gets
18
+ raise NotImplementedError, "#{self.class}#gets undefined"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module MCP
5
+ module Transport
6
+ # @example
7
+ # transport = OmniAI::MCP::Transport::Stdio.new
8
+ # transport.puts("Hello World")
9
+ # transport.gets
10
+ class Stdio < Base
11
+ # @param stdin [IO]
12
+ # @param stdout [IO]
13
+ # @param stderr [IO]
14
+ def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr)
15
+ super()
16
+ @stdin = stdin
17
+ @stdout = stdout
18
+ @stderr = stderr
19
+ end
20
+
21
+ # @param text [String]
22
+ def puts(text)
23
+ @stdout.puts(text)
24
+ end
25
+
26
+ # @return [String]
27
+ def gets
28
+ @stdin.gets
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/omniai/tool.rb CHANGED
@@ -72,7 +72,7 @@ module OmniAI
72
72
  # @param function [Proc]
73
73
  # @param name [String]
74
74
  # @param description [String]
75
- # @param parameters [Hash]
75
+ # @param parameters [OmniAI::Tool::Parameters]
76
76
  def initialize(
77
77
  function = method(:execute),
78
78
  name: self.class.namify,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniAI
4
- VERSION = "2.1.1"
4
+ VERSION = "2.2.0"
5
5
  end
data/lib/omniai.rb CHANGED
@@ -8,8 +8,10 @@ require "zeitwerk"
8
8
 
9
9
  loader = Zeitwerk::Loader.for_gem
10
10
  loader.inflector.inflect "omniai" => "OmniAI"
11
- loader.inflector.inflect "url" => "URL"
12
11
  loader.inflector.inflect "cli" => "CLI"
12
+ loader.inflector.inflect "jrpc" => "JRPC"
13
+ loader.inflector.inflect "mcp" => "MCP"
14
+ loader.inflector.inflect "url" => "URL"
13
15
  loader.setup
14
16
 
15
17
  module OmniAI
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniai
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-24 00:00:00.000000000 Z
10
+ date: 2025-03-27 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: event_stream_parser
@@ -111,6 +111,13 @@ files:
111
111
  - lib/omniai/embed/usage.rb
112
112
  - lib/omniai/files.rb
113
113
  - lib/omniai/instrumentation.rb
114
+ - lib/omniai/mcp/jrpc.rb
115
+ - lib/omniai/mcp/jrpc/error.rb
116
+ - lib/omniai/mcp/jrpc/request.rb
117
+ - lib/omniai/mcp/jrpc/response.rb
118
+ - lib/omniai/mcp/server.rb
119
+ - lib/omniai/mcp/transport/base.rb
120
+ - lib/omniai/mcp/transport/stdio.rb
114
121
  - lib/omniai/speak.rb
115
122
  - lib/omniai/tool.rb
116
123
  - lib/omniai/tool/array.rb