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 +4 -4
- data/README.md +41 -0
- data/lib/omniai/chat/content.rb +1 -1
- data/lib/omniai/chat.rb +6 -1
- data/lib/omniai/mcp/jrpc/error.rb +36 -0
- data/lib/omniai/mcp/jrpc/request.rb +70 -0
- data/lib/omniai/mcp/jrpc/response.rb +60 -0
- data/lib/omniai/mcp/jrpc.rb +11 -0
- data/lib/omniai/mcp/server.rb +118 -0
- data/lib/omniai/mcp/transport/base.rb +23 -0
- data/lib/omniai/mcp/transport/stdio.rb +33 -0
- data/lib/omniai/tool.rb +1 -1
- data/lib/omniai/version.rb +1 -1
- data/lib/omniai.rb +3 -1
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0d7be9d7a666bdabc7c18e8ce6b090e5c69622d469d3444278a5054a82fab92
|
4
|
+
data.tar.gz: c6a0aaf46493811c8210ac1a8bec5dcb1999094dc38edd4f5fee3fdc489f554d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
```
|
data/lib/omniai/chat/content.rb
CHANGED
@@ -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 { |
|
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
|
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,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
data/lib/omniai/version.rb
CHANGED
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.
|
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-
|
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
|