encom 0.1.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.
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'open3'
5
+ require 'timeout'
6
+ require 'json'
7
+
8
+ module Encom
9
+ module Transport
10
+ class Stdio
11
+ attr_reader :process_pid
12
+
13
+ def initialize(
14
+ command:,
15
+ args: []
16
+ )
17
+ super()
18
+ @command = command
19
+ @args = args
20
+ @process = nil
21
+ @read_buffer = +'' # Use mutable string
22
+ @callbacks = {
23
+ error: [],
24
+ close: [],
25
+ data: []
26
+ }
27
+ @mutex = Mutex.new
28
+ @json_buffer = '' # Buffer for accumulating JSON messages
29
+ end
30
+
31
+ # Register event handlers
32
+ def on_error(&block)
33
+ @callbacks[:error] << block
34
+ self
35
+ end
36
+
37
+ def on_close(&block)
38
+ @callbacks[:close] << block
39
+ self
40
+ end
41
+
42
+ def on_data(&block)
43
+ @callbacks[:data] << block
44
+ self
45
+ end
46
+
47
+ def start
48
+ raise 'StdioClientTransport already started!' if @process
49
+
50
+ env = ENV.to_h
51
+
52
+ command = @command
53
+ args = @args
54
+
55
+ @stdin, @stdout, @stderr, @process = Open3.popen3(
56
+ env,
57
+ command,
58
+ *args
59
+ # chdir: @server_params[:cwd]
60
+ )
61
+
62
+ @process_pid = @process.pid
63
+
64
+ start_stdout_thread
65
+ start_stderr_thread
66
+ start_process_monitor_thread
67
+
68
+ self
69
+ end
70
+
71
+ def send(data)
72
+ return false unless @process && @stdin
73
+
74
+ data = "#{data}\n" unless data.end_with?("\n")
75
+
76
+ begin
77
+ @stdin.write(data)
78
+ @stdin.flush
79
+ true
80
+ rescue IOError, Errno::EPIPE => e
81
+ trigger_error(e)
82
+ false
83
+ end
84
+ end
85
+
86
+ def send_line(data)
87
+ send("#{data}\n")
88
+ end
89
+
90
+ def close
91
+ return unless @process
92
+
93
+ begin
94
+ @stdin.close
95
+ rescue StandardError
96
+ nil
97
+ end
98
+
99
+ begin
100
+ Timeout.timeout(2) do
101
+ Process.wait(@process.pid)
102
+ rescue StandardError
103
+ nil
104
+ end
105
+ rescue Timeout::Error
106
+ begin
107
+ Process.kill('TERM', @process.pid)
108
+ rescue StandardError
109
+ nil
110
+ end
111
+
112
+ begin
113
+ Timeout.timeout(1) do
114
+ Process.wait(@process.pid)
115
+ rescue StandardError
116
+ nil
117
+ end
118
+ rescue Timeout::Error
119
+ begin
120
+ Process.kill('KILL', @process.pid)
121
+ rescue StandardError
122
+ nil
123
+ end
124
+ end
125
+ end
126
+
127
+ # Clean up resources
128
+ begin
129
+ @stdout.close
130
+ rescue StandardError
131
+ nil
132
+ end
133
+ begin
134
+ @stderr.close
135
+ rescue StandardError
136
+ nil
137
+ end
138
+
139
+ trigger_close
140
+
141
+ @process = nil
142
+ end
143
+
144
+ private
145
+
146
+ def start_stdout_thread
147
+ Thread.new do
148
+ loop do
149
+ chunk = @stdout.read_nonblock(1024)
150
+ if chunk && !chunk.empty?
151
+ @mutex.synchronize do
152
+ @read_buffer << chunk
153
+ end
154
+ process_read_buffer
155
+ end
156
+ rescue IO::WaitReadable
157
+ IO.select([@stdout], nil, nil, 0.1)
158
+ retry
159
+ rescue EOFError
160
+ break
161
+ end
162
+ rescue IOError, Errno::EPIPE => e
163
+ trigger_error(e) unless @process.nil?
164
+ ensure
165
+ trigger_close if @process && !@stdout.closed?
166
+ end
167
+ end
168
+
169
+ def start_stderr_thread
170
+ Thread.new do
171
+ while (line = @stderr.gets)
172
+ trigger_error(RuntimeError.new("Process stderr: #{line.strip}"))
173
+ end
174
+ rescue IOError, Errno::EPIPE => e
175
+ trigger_error(e) unless @process.nil?
176
+ end
177
+ end
178
+
179
+ def start_process_monitor_thread
180
+ Thread.new do
181
+ Process.wait(@process.pid)
182
+ exit_status = $CHILD_STATUS.exitstatus
183
+ trigger_close(exit_status)
184
+ rescue Errno::ECHILD, Errno::ESRCH
185
+ trigger_close
186
+ end
187
+ end
188
+
189
+ def process_read_buffer
190
+ data = nil
191
+
192
+ @mutex.synchronize do
193
+ data = @read_buffer.dup
194
+ @read_buffer.clear
195
+ end
196
+
197
+ return unless data && !data.empty?
198
+
199
+ @json_buffer += data
200
+
201
+ process_json_messages
202
+ end
203
+
204
+ def process_json_messages
205
+ while @json_buffer.include?("\n")
206
+ message, remainder = @json_buffer.split("\n", 2)
207
+
208
+ if message && !message.empty?
209
+ begin
210
+ JSON.parse(message)
211
+
212
+ trigger_data(message)
213
+ rescue JSON::ParserError
214
+ @json_buffer = "#{message}\n#{remainder || ''}"
215
+ return
216
+ end
217
+ end
218
+
219
+ @json_buffer = remainder || ''
220
+ end
221
+ end
222
+
223
+ def trigger_error(error)
224
+ @callbacks[:error].each { |callback| callback.call(error) }
225
+ end
226
+
227
+ def trigger_close(exit_status = nil)
228
+ @callbacks[:close].each { |callback| callback.call(exit_status) }
229
+ end
230
+
231
+ def trigger_data(data)
232
+ @callbacks[:data].each { |callback| callback.call(data) }
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Encom
4
+ VERSION = '0.1.0'
5
+ end
data/lib/encom.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'encom/version'
4
+
5
+ module Encom
6
+ class Error < StandardError; end
7
+ # Your code goes here...
8
+ end
data/sig/encom.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Encom
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: encom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kyle Byrne
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-03-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Library for implementing MCP (Model context protocol) servers and clients
14
+ in Ruby
15
+ email:
16
+ - kyletbyrne96@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".rspec"
22
+ - ".standard.yml"
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - examples/filesystem_demo.rb
27
+ - lib/encom.rb
28
+ - lib/encom/client.rb
29
+ - lib/encom/error_codes.rb
30
+ - lib/encom/server.rb
31
+ - lib/encom/server/tool.rb
32
+ - lib/encom/server_transport/base.rb
33
+ - lib/encom/server_transport/stdio.rb
34
+ - lib/encom/transport/stdio.rb
35
+ - lib/encom/version.rb
36
+ - sig/encom.rbs
37
+ homepage: https://github.com/kylebyrne/encom
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ homepage_uri: https://github.com/kylebyrne/encom
42
+ source_code_uri: https://github.com/kylebyrne/encom
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 3.0.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.3.7
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Ruby implementation of MCP
62
+ test_files: []