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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +159 -0
- data/Rakefile +10 -0
- data/examples/filesystem_demo.rb +191 -0
- data/lib/encom/client.rb +308 -0
- data/lib/encom/error_codes.rb +17 -0
- data/lib/encom/server/tool.rb +150 -0
- data/lib/encom/server.rb +294 -0
- data/lib/encom/server_transport/base.rb +42 -0
- data/lib/encom/server_transport/stdio.rb +73 -0
- data/lib/encom/transport/stdio.rb +236 -0
- data/lib/encom/version.rb +5 -0
- data/lib/encom.rb +8 -0
- data/sig/encom.rbs +4 -0
- metadata +62 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Encom
|
|
4
|
+
# Defines standard JSON-RPC error codes and MCP-specific error codes
|
|
5
|
+
module ErrorCodes
|
|
6
|
+
# Standard JSON-RPC error codes
|
|
7
|
+
PARSE_ERROR = -32_700
|
|
8
|
+
INVALID_REQUEST = -32_600
|
|
9
|
+
METHOD_NOT_FOUND = -32_601
|
|
10
|
+
INVALID_PARAMS = -32_602
|
|
11
|
+
INTERNAL_ERROR = -32_603
|
|
12
|
+
|
|
13
|
+
# MCP specific error codes
|
|
14
|
+
TOOL_EXECUTION_ERROR = -32_000
|
|
15
|
+
PROTOCOL_ERROR = -32_001
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Encom
|
|
4
|
+
class Server
|
|
5
|
+
class Tool
|
|
6
|
+
attr_reader :name, :description, :schema
|
|
7
|
+
|
|
8
|
+
# Initialize a new tool
|
|
9
|
+
#
|
|
10
|
+
# @param name [Symbol, String] The name of the tool
|
|
11
|
+
# @param description [String] A description of what the tool does
|
|
12
|
+
# @param schema [Hash] A hash describing the input schema for the tool
|
|
13
|
+
# @param proc [Proc] A proc that implements the tool's functionality
|
|
14
|
+
def initialize(name:, description:, schema:, proc:)
|
|
15
|
+
@name = name
|
|
16
|
+
@description = description
|
|
17
|
+
@schema = schema
|
|
18
|
+
@proc = proc
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def definition
|
|
22
|
+
{
|
|
23
|
+
name: @name.to_s,
|
|
24
|
+
description: @description,
|
|
25
|
+
inputSchema: schema_definition
|
|
26
|
+
}.compact
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(arguments)
|
|
30
|
+
result = nil
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
if @proc.parameters.first && @proc.parameters.first[0] == :keyreq
|
|
34
|
+
# If the proc expects keyword arguments, pass the arguments hash
|
|
35
|
+
result = @proc.call(**arguments)
|
|
36
|
+
else
|
|
37
|
+
# Otherwise, pass arguments as an array
|
|
38
|
+
result = @proc.call(*arguments)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Ensure the result is in the standard format
|
|
42
|
+
standardize_tool_response(result)
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
# Return error in MCP-compliant format as per docs
|
|
45
|
+
{
|
|
46
|
+
isError: true,
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: "Error: #{e.message}"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Standardize tool response to ensure it conforms to the MCP format
|
|
60
|
+
#
|
|
61
|
+
# @param result [Hash, String, Array, Object] The raw result from the tool proc
|
|
62
|
+
# @return [Hash] A standardized response hash with content array
|
|
63
|
+
def standardize_tool_response(result)
|
|
64
|
+
# If the result is already in the expected format (has content array), return it
|
|
65
|
+
return result if result.is_a?(Hash) && result[:content].is_a?(Array)
|
|
66
|
+
|
|
67
|
+
# If it's a hash with isError, leave it as is
|
|
68
|
+
return result if result.is_a?(Hash) && result[:isError]
|
|
69
|
+
|
|
70
|
+
# If it's a string, convert to text content
|
|
71
|
+
if result.is_a?(String)
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: result
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# If it's an array, assume it's already a content array
|
|
83
|
+
if result.is_a?(Array)
|
|
84
|
+
return {
|
|
85
|
+
content: result
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# For any other type, convert to string and wrap as text
|
|
90
|
+
{
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: result.to_s
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def schema_definition
|
|
101
|
+
properties = {}
|
|
102
|
+
required = []
|
|
103
|
+
|
|
104
|
+
@schema.each do |key, value|
|
|
105
|
+
properties[key] = if value.is_a?(Hash)
|
|
106
|
+
{
|
|
107
|
+
type: ruby_type_to_json_type(value[:type]),
|
|
108
|
+
description: value[:description]
|
|
109
|
+
}.compact
|
|
110
|
+
else
|
|
111
|
+
{
|
|
112
|
+
type: ruby_type_to_json_type(value)
|
|
113
|
+
}.compact
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@proc.parameters.each do |param_type, param_name|
|
|
118
|
+
required << param_name.to_s if param_type == :keyreq
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
{
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: properties,
|
|
124
|
+
required: required
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def ruby_type_to_json_type(type)
|
|
129
|
+
return nil unless type
|
|
130
|
+
|
|
131
|
+
case type.to_s
|
|
132
|
+
when 'Integer'
|
|
133
|
+
'number'
|
|
134
|
+
when 'String'
|
|
135
|
+
'string'
|
|
136
|
+
when 'TrueClass', 'FalseClass', 'Boolean'
|
|
137
|
+
'boolean'
|
|
138
|
+
when 'Float'
|
|
139
|
+
'number'
|
|
140
|
+
when 'Array'
|
|
141
|
+
'array'
|
|
142
|
+
when 'Hash'
|
|
143
|
+
'object'
|
|
144
|
+
else
|
|
145
|
+
type.to_s.downcase
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
data/lib/encom/server.rb
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
require 'encom/server/tool'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
module Encom
|
|
5
|
+
class Server
|
|
6
|
+
LATEST_PROTOCOL_VERSION = '2024-11-05'
|
|
7
|
+
SUPPORTED_PROTOCOL_VERSIONS = [
|
|
8
|
+
LATEST_PROTOCOL_VERSION
|
|
9
|
+
# Add more supported versions as they're developed
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def name(server_name = nil)
|
|
14
|
+
@server_name ||= server_name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def version(version = nil)
|
|
18
|
+
@version ||= version
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def tool(tool_name, description, schema, proc)
|
|
22
|
+
@tools ||= []
|
|
23
|
+
@tools << Tool.new(name: tool_name, description:, schema:, proc:)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :tools
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :transport, :capabilities
|
|
30
|
+
|
|
31
|
+
def initialize(options = {})
|
|
32
|
+
@message_id = 0
|
|
33
|
+
@capabilities = options[:capabilities] || {
|
|
34
|
+
roots: {
|
|
35
|
+
listChanged: true
|
|
36
|
+
},
|
|
37
|
+
sampling: {},
|
|
38
|
+
tools: {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Validate protocol version immediately
|
|
42
|
+
protocol_version = options[:protocol_version] || LATEST_PROTOCOL_VERSION
|
|
43
|
+
unless SUPPORTED_PROTOCOL_VERSIONS.include?(protocol_version)
|
|
44
|
+
raise ArgumentError, "Unsupported protocol version: #{protocol_version}. Supported versions: #{SUPPORTED_PROTOCOL_VERSIONS.join(', ')}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@protocol_version = protocol_version
|
|
48
|
+
@transport = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def name
|
|
52
|
+
self.class.name
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def version
|
|
56
|
+
self.class.version
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def tools
|
|
60
|
+
self.class.tools
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def call_tool(name, arguments)
|
|
64
|
+
tool = tools.find { |t| t.name.to_s == name.to_s || t.name == name.to_sym }
|
|
65
|
+
raise "Unknown tool: #{name}" unless tool
|
|
66
|
+
tool.call(arguments)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Run the server with the specified transport
|
|
70
|
+
def run(transport_class, transport_options = {})
|
|
71
|
+
@transport = transport_class.new(self, transport_options)
|
|
72
|
+
@transport.start
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
$stderr.puts "Error running server: #{e.message}"
|
|
75
|
+
$stderr.puts e.backtrace.join("\n") if transport_options[:debug]
|
|
76
|
+
raise
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Process incoming JSON-RPC message
|
|
80
|
+
def process_message(message)
|
|
81
|
+
return unless message.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
if @transport && @transport.respond_to?(:debug)
|
|
84
|
+
@transport.debug "Processing message: #{message.inspect}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check for jsonrpc version
|
|
88
|
+
unless message[:jsonrpc] == '2.0'
|
|
89
|
+
if message[:id]
|
|
90
|
+
respond_error(message[:id], Encom::ErrorCodes::INVALID_REQUEST, 'Invalid JSON-RPC request')
|
|
91
|
+
end
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Process request by method
|
|
96
|
+
case message[:method]
|
|
97
|
+
when 'initialize'
|
|
98
|
+
handle_initialize(message)
|
|
99
|
+
when 'initialized'
|
|
100
|
+
handle_initialized(message)
|
|
101
|
+
when 'resources/list'
|
|
102
|
+
handle_resources_list(message)
|
|
103
|
+
when 'roots/list'
|
|
104
|
+
handle_roots_list(message)
|
|
105
|
+
when 'sampling/prepare'
|
|
106
|
+
handle_sampling_prepare(message)
|
|
107
|
+
when 'sampling/sample'
|
|
108
|
+
handle_sampling_sample(message)
|
|
109
|
+
when 'tools/list'
|
|
110
|
+
handle_tools_list(message)
|
|
111
|
+
when 'tools/call'
|
|
112
|
+
handle_tools_call(message)
|
|
113
|
+
when 'shutdown'
|
|
114
|
+
handle_shutdown(message)
|
|
115
|
+
else
|
|
116
|
+
if message[:id]
|
|
117
|
+
if @transport && @transport.respond_to?(:debug)
|
|
118
|
+
@transport.debug "Unknown method: #{message[:method]}"
|
|
119
|
+
end
|
|
120
|
+
respond_error(message[:id], Encom::ErrorCodes::METHOD_NOT_FOUND, "Method not found: #{message[:method]}")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
rescue StandardError => e
|
|
124
|
+
if @transport && @transport.respond_to?(:debug)
|
|
125
|
+
@transport.debug "Error processing message: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if message && message[:id]
|
|
129
|
+
respond_error(message[:id], Encom::ErrorCodes::INTERNAL_ERROR, "Internal error: #{e.message}")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Generate and send a JSON-RPC response
|
|
134
|
+
def respond(id, result)
|
|
135
|
+
return unless @transport
|
|
136
|
+
|
|
137
|
+
response = {
|
|
138
|
+
jsonrpc: "2.0",
|
|
139
|
+
id: id,
|
|
140
|
+
result: result
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@transport.send_message(response)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Generate and send a JSON-RPC error response
|
|
147
|
+
def respond_error(id, code, message, data = nil)
|
|
148
|
+
return unless @transport
|
|
149
|
+
|
|
150
|
+
response = {
|
|
151
|
+
jsonrpc: "2.0",
|
|
152
|
+
id: id,
|
|
153
|
+
error: {
|
|
154
|
+
code: code,
|
|
155
|
+
message: message
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
response[:error][:data] = data if data
|
|
159
|
+
|
|
160
|
+
@transport.send_message(response)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Handle initialize request
|
|
164
|
+
def handle_initialize(message)
|
|
165
|
+
client_protocol_version = message[:params][:protocolVersion]
|
|
166
|
+
client_capabilities = message[:params][:capabilities]
|
|
167
|
+
client_info = message[:params][:clientInfo]
|
|
168
|
+
|
|
169
|
+
server_info = {
|
|
170
|
+
name: name,
|
|
171
|
+
version: version
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Debug log the received protocol version
|
|
175
|
+
if @transport && @transport.respond_to?(:debug)
|
|
176
|
+
@transport.debug "Received initialize request with protocol version: #{client_protocol_version}"
|
|
177
|
+
@transport.debug "Supported protocol versions: #{SUPPORTED_PROTOCOL_VERSIONS.inspect}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Check if the requested protocol version is supported
|
|
181
|
+
if SUPPORTED_PROTOCOL_VERSIONS.include?(client_protocol_version)
|
|
182
|
+
# Use the requested version if supported
|
|
183
|
+
protocol_version = client_protocol_version
|
|
184
|
+
|
|
185
|
+
if @transport && @transport.respond_to?(:debug)
|
|
186
|
+
@transport.debug "Protocol version #{protocol_version} is supported, sending success response"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Send initialize response
|
|
190
|
+
respond(message[:id], {
|
|
191
|
+
protocolVersion: protocol_version,
|
|
192
|
+
capabilities: @capabilities,
|
|
193
|
+
serverInfo: server_info
|
|
194
|
+
})
|
|
195
|
+
else
|
|
196
|
+
# Return an error for unsupported protocol versions
|
|
197
|
+
if @transport && @transport.respond_to?(:debug)
|
|
198
|
+
@transport.debug "Protocol version error: Client requested unsupported version #{client_protocol_version}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
respond_error(
|
|
202
|
+
message[:id],
|
|
203
|
+
Encom::ErrorCodes::PROTOCOL_ERROR,
|
|
204
|
+
"Unsupported protocol version: #{client_protocol_version}",
|
|
205
|
+
{ supportedVersions: SUPPORTED_PROTOCOL_VERSIONS }
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Handle initialized notification
|
|
211
|
+
def handle_initialized(message)
|
|
212
|
+
# No response needed for notifications
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Handle resources/list request
|
|
216
|
+
def handle_resources_list(message)
|
|
217
|
+
# Default implementation returns an empty list
|
|
218
|
+
respond(message[:id], {
|
|
219
|
+
resources: []
|
|
220
|
+
})
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Handle roots/list request
|
|
224
|
+
def handle_roots_list(message)
|
|
225
|
+
# Default implementation returns an empty list
|
|
226
|
+
respond(message[:id], {
|
|
227
|
+
roots: []
|
|
228
|
+
})
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Handle sampling/prepare request
|
|
232
|
+
def handle_sampling_prepare(message)
|
|
233
|
+
# Default implementation returns a simple response
|
|
234
|
+
respond(message[:id], {
|
|
235
|
+
prepared: false,
|
|
236
|
+
samplingId: nil
|
|
237
|
+
})
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Handle sampling/sample request
|
|
241
|
+
def handle_sampling_sample(message)
|
|
242
|
+
# Default implementation returns a simple response
|
|
243
|
+
respond(message[:id], {
|
|
244
|
+
completion: "Sampling not implemented",
|
|
245
|
+
completionId: nil
|
|
246
|
+
})
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Handle tools/list request
|
|
250
|
+
def handle_tools_list(message)
|
|
251
|
+
tool_definitions = tools ? tools.map(&:definition) : []
|
|
252
|
+
|
|
253
|
+
respond(message[:id], {
|
|
254
|
+
tools: tool_definitions
|
|
255
|
+
})
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Handle tools/call request
|
|
259
|
+
def handle_tools_call(message)
|
|
260
|
+
tool_name = message[:params][:name]
|
|
261
|
+
arguments = message[:params][:arguments] || {}
|
|
262
|
+
|
|
263
|
+
begin
|
|
264
|
+
result = call_tool(tool_name, arguments)
|
|
265
|
+
respond(message[:id], result)
|
|
266
|
+
rescue StandardError => e
|
|
267
|
+
respond_error(message[:id], Encom::ErrorCodes::TOOL_EXECUTION_ERROR, "Tool execution error: #{e.message}")
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Handle JSON-RPC shutdown request
|
|
272
|
+
def handle_shutdown(message)
|
|
273
|
+
if message[:id]
|
|
274
|
+
# If it's a request with ID, respond with a success result
|
|
275
|
+
respond(message[:id], {
|
|
276
|
+
shutdown: true
|
|
277
|
+
})
|
|
278
|
+
end
|
|
279
|
+
# Initiate clean shutdown
|
|
280
|
+
shutdown
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Shutdown the server and clean up
|
|
284
|
+
def shutdown
|
|
285
|
+
return if @shutting_down
|
|
286
|
+
@shutting_down = true
|
|
287
|
+
|
|
288
|
+
if @transport
|
|
289
|
+
@transport.stop
|
|
290
|
+
@transport = nil
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Encom
|
|
4
|
+
module ServerTransport
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :server
|
|
7
|
+
|
|
8
|
+
def initialize(server, options = {})
|
|
9
|
+
@server = server
|
|
10
|
+
@options = options
|
|
11
|
+
@debug = options[:debug] || false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Start the transport - must be implemented by subclasses
|
|
15
|
+
def start
|
|
16
|
+
raise NotImplementedError, 'Subclasses must implement #start'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Stop the transport - must be implemented by subclasses
|
|
20
|
+
def stop
|
|
21
|
+
raise NotImplementedError, 'Subclasses must implement #stop'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Send a message through the transport - must be implemented by subclasses
|
|
25
|
+
def send_message(message)
|
|
26
|
+
raise NotImplementedError, 'Subclasses must implement #send_message'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Process an incoming message - default implementation delegates to server
|
|
30
|
+
def process_message(message)
|
|
31
|
+
@server.process_message(message)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Log debug information if debug is enabled
|
|
35
|
+
def debug(message)
|
|
36
|
+
return unless @debug
|
|
37
|
+
|
|
38
|
+
warn "[Encom::ServerTransport] #{message}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'encom/server_transport/base'
|
|
5
|
+
|
|
6
|
+
module Encom
|
|
7
|
+
module ServerTransport
|
|
8
|
+
class Stdio < Base
|
|
9
|
+
def start
|
|
10
|
+
debug 'Starting StdIO transport server'
|
|
11
|
+
debug 'Listening on stdin, writing to stdout...'
|
|
12
|
+
|
|
13
|
+
# Enable line buffering for stdout
|
|
14
|
+
$stdout.sync = true
|
|
15
|
+
|
|
16
|
+
# Set up signal handlers for graceful shutdown
|
|
17
|
+
setup_signal_handlers
|
|
18
|
+
|
|
19
|
+
@running = true
|
|
20
|
+
|
|
21
|
+
# Process messages until stdin is closed or shutdown is requested
|
|
22
|
+
while @running && (line = $stdin.gets)
|
|
23
|
+
begin
|
|
24
|
+
message = JSON.parse(line, symbolize_names: true)
|
|
25
|
+
debug "Received: #{message.inspect}"
|
|
26
|
+
process_message(message)
|
|
27
|
+
rescue JSON::ParserError => e
|
|
28
|
+
debug "Error parsing message: #{e.message}"
|
|
29
|
+
next
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
debug "Error processing message: #{e.message}"
|
|
32
|
+
next
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
debug 'StdIO transport server stopped'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def stop
|
|
40
|
+
debug 'Stopping StdIO transport server'
|
|
41
|
+
@running = false
|
|
42
|
+
# No specific cleanup needed for stdio beyond setting running to false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def send_message(message)
|
|
46
|
+
json = JSON.generate(message)
|
|
47
|
+
debug "Sending: #{message.inspect}"
|
|
48
|
+
puts json # Write to stdout
|
|
49
|
+
$stdout.flush
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def debug(message)
|
|
54
|
+
return unless @debug
|
|
55
|
+
|
|
56
|
+
warn "[Encom::ServerTransport::Stdio] #{message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def setup_signal_handlers
|
|
62
|
+
# Handle INT (CTRL+C) and TERM signals for graceful shutdown
|
|
63
|
+
trap('INT') { handle_signal('INT') }
|
|
64
|
+
trap('TERM') { handle_signal('TERM') }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def handle_signal(signal)
|
|
68
|
+
debug "Received #{signal} signal, shutting down..."
|
|
69
|
+
stop
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|