stack-service-base 0.0.57 → 0.0.59

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: 81646afebee3b19d18a2f13f58aeae9559b79033315ec6200c86085611ac088d
4
- data.tar.gz: df126a48c152469880314fc4bc440555a93528c6d3f71e53d58485763d4175bc
3
+ metadata.gz: 55f435587160a4c4210903f8cf0af139ac161402c49cd82e34da0ec1f0d2115d
4
+ data.tar.gz: 44c4c02fbd588a70835cdd6cb10348e78df5cef0a5017b667445046763084411
5
5
  SHA512:
6
- metadata.gz: 653ebe71e422922b1d688b9bb9d00a4d81450b2da6c1f956a56c64c60659ceb52d83048a9bcef5cd6175bf2a48e357055f0826f391ac3bc70aca7bbc83f98368
7
- data.tar.gz: 38a1901294e10dc628547f6ea83f0a837f2989e57849d06435105328936db516e1cecf9eac5b596b728f25bf26f397a6f3910b67a2440d8138c39ad149039bcb
6
+ metadata.gz: 95d9aff0cff97f4f857247eefe3ed61a03ea05a7d202eb3e118d485d418b7578e76a60190aa4965f6e4fbf4e424d67617e1e394519352817bb0b5fd6c538eeac
7
+ data.tar.gz: 17adacbd6686f78010139866873cb22a6d3c328ae93ace314b8821861af3341bc0740642d262650804a1129aeabb50af6232c6711963d4715ccf877a8077cad7
@@ -0,0 +1,73 @@
1
+ require 'sinatra'
2
+ require 'stack-service-base'
3
+
4
+ StackServiceBase.rack_setup self
5
+
6
+ SERVICES = {
7
+ "database-backend" => {
8
+ status: "running",
9
+ uptime: 72 * 3600, # seconds
10
+ last_restart: Time.now - 72 * 3600
11
+ }
12
+ }
13
+
14
+ require 'stack-service-base/mcp_processor'
15
+ require 'stack-service-base/mcp_tool_registry'
16
+
17
+ MCP_PROCESSOR = McpProcessor.new
18
+
19
+ Tool :service_status do
20
+ description 'Check current status of a service'
21
+ input service_name: { type: "string", description: "Service name to inspect", required: true }
22
+ execute do |inputs|
23
+ service_name = inputs[:service_name]
24
+ service = SERVICES[service_name]
25
+ rpc_error!(404, "Unknown service #{service_name}") unless service
26
+ {
27
+ toolResult: {
28
+ service_name: service_name,
29
+ status: service[:status],
30
+ uptime_sec: service[:uptime],
31
+ last_restart: service[:last_restart].utc.iso8601,
32
+ }
33
+ }
34
+ end
35
+ end
36
+
37
+ Tool :restart_service do
38
+ description 'Restart a service'
39
+ input service_name: { type: "string", description: "Service name to restart", required: true },
40
+ force: { type: "boolean", default: false, description: "Force restart if graceful fails" }
41
+ execute do |inputs|
42
+ service_name = inputs[:service_name]
43
+ service = SERVICES[service_name]
44
+ rpc_error!(404, "Unknown service #{service_name}") unless service
45
+
46
+ service[:status] = "running"
47
+ service[:last_restart] = Time.now
48
+ service[:uptime] = 0
49
+
50
+ {
51
+ toolResult: {
52
+ service_name: service_name,
53
+ status: service[:status],
54
+ restarted_at: service[:last_restart].utc.iso8601,
55
+ force: inputs.fetch(:force, false)
56
+ }
57
+ }
58
+ end
59
+ end
60
+
61
+ before { content_type :json }
62
+ error McpProcessor::ParseError do |err|
63
+ status err.status
64
+ err.body
65
+ end
66
+
67
+ get '/', &MCP_PROCESSOR.method(:root_endpoint)
68
+ post '/' do
69
+ request.body.rewind
70
+ MCP_PROCESSOR.rpc_endpoint(request.body.read)
71
+ end
72
+
73
+ run Sinatra::Application
@@ -0,0 +1,118 @@
1
+ class JsonRpcError < StandardError
2
+ attr_reader :code
3
+
4
+ def initialize(code:, message:)
5
+ super(message)
6
+ @code = code
7
+ end
8
+ end
9
+
10
+ module RpcErrorHelpers
11
+ def rpc_error!(code, message)
12
+ raise ::JsonRpcError.new(code: code, message: message)
13
+ end
14
+ end
15
+
16
+ class McpProcessor
17
+ PROTOCOL_VERSION = '2025-06-18'
18
+
19
+ include RpcErrorHelpers
20
+ ParseError = Class.new(StandardError) do
21
+ attr_reader :body, :status
22
+
23
+ def initialize(body:, status:)
24
+ @body = body
25
+ @status = status
26
+ super("MCP parse error")
27
+ end
28
+ end
29
+
30
+ def initialize(logger: LOGGER)
31
+ @logger = logger
32
+ end
33
+
34
+ def root_endpoint
35
+ root_response
36
+ end
37
+
38
+ def rpc_endpoint(raw_body)
39
+ req = JSON.parse(raw_body.to_s)
40
+ rpc_response(id: req["id"], method: req["method"], params: req["params"])
41
+ rescue JSON::ParserError
42
+ body = error_response(id: nil, code: -32700, message: "Parse error")
43
+ raise ParseError.new(body: body, status: 400)
44
+ end
45
+
46
+ def list_tools
47
+ { tools: ToolRegistry.list, nextCursor: nil }
48
+ end
49
+
50
+ def root_response
51
+ json_rpc_response(id: nil) { list_tools }
52
+ end
53
+
54
+ def error_response(id:, code:, message:)
55
+ json_rpc_response(id: id) { rpc_error!(code, message) }
56
+ end
57
+
58
+ def rpc_response(id:, method:, params:)
59
+ json_rpc_response(id: id) { |body| handle(method: method, params: params, body: body) }
60
+ end
61
+
62
+ def handle(method:, params:, body: )
63
+ case method
64
+ when "tools/list" then list_tools
65
+ when "tools/call" then call_tool(params || {})
66
+ when "initialize" then initialize_(body)
67
+ when "initialized" then {}
68
+ else
69
+ rpc_error!(-32601, "Unknown method #{method}")
70
+ end
71
+ end
72
+
73
+ # https://gist.github.com/ruvnet/7b6843c457822cbcf42fc4aa635eadbb
74
+
75
+ def initialize_(body)
76
+ body[:serverInfo] = {
77
+ name: 'mcp-server',
78
+ title: 'MCP Server',
79
+ version: '1.0.0'
80
+ }
81
+ # result
82
+ {
83
+ protocolVersion: PROTOCOL_VERSION,
84
+ capabilities: {
85
+ logging: {},
86
+ prompts: { listChanged: false },
87
+ resources: { listChanged: false },
88
+ tools: { listChanged: false }
89
+ }
90
+ }
91
+ end
92
+
93
+ private
94
+
95
+ def json_rpc_response(id:)
96
+ body = { jsonrpc: "2.0", id: id }
97
+
98
+ begin
99
+ result = yield(body)
100
+ body[:result] = result unless body[:error] || result.nil?
101
+ rescue JsonRpcError => e
102
+ body[:error] = { code: e.code, message: e.message }
103
+ rescue => e
104
+ @logger&.error("Unhandled RPC error: #{e.class}: #{e.message}\n#{e.backtrace&.first}")
105
+ body[:error] = { code: -32603, message: "Internal error" }
106
+ end
107
+
108
+ body.delete(:result) if body[:error]
109
+ JSON.dump(body)
110
+ end
111
+
112
+ def call_tool(params)
113
+ name = params["name"]
114
+ arguments = params["arguments"] || {}
115
+ tool = ToolRegistry.fetch(name) || rpc_error!(-32601, "Unknown tool #{name}")
116
+ tool.call(arguments)
117
+ end
118
+ end
@@ -0,0 +1,84 @@
1
+ module ToolRegistry
2
+ class ExecutionContext
3
+ include RpcErrorHelpers
4
+ end
5
+
6
+ class Definition
7
+ attr_reader :name
8
+
9
+ def initialize(name)
10
+ @name = name.to_s
11
+ @inputs = {}
12
+ end
13
+
14
+ def description(text = nil)
15
+ return @description if text.nil?
16
+ @description = text
17
+ end
18
+
19
+ def input(fields = {})
20
+ @inputs.merge!(fields.transform_keys(&:to_s))
21
+ end
22
+
23
+ def input_schema
24
+ properties = {}
25
+ required = []
26
+
27
+ @inputs.each do |field, config|
28
+ cfg = config.transform_keys { |key| key.is_a?(Symbol) ? key : key.to_sym }
29
+ required << field if cfg.delete(:required)
30
+ properties[field] = cfg.transform_keys(&:to_s)
31
+ end
32
+
33
+ { type: "object", properties: properties, required: required }
34
+ end
35
+
36
+ def execute(&block) = @executor = block
37
+
38
+ def to_h
39
+ {
40
+ name: name,
41
+ description: description,
42
+ inputSchema: input_schema
43
+ }
44
+ end
45
+
46
+ def call(arguments)
47
+ raise JsonRpcError.new(code: 500, message: "Tool #{name} missing executor") unless @executor
48
+
49
+ ExecutionContext.new.instance_exec(symbolize_keys(arguments || {}), &@executor)
50
+ end
51
+
52
+ private
53
+
54
+ def symbolize_keys(hash)
55
+ hash.each_with_object({}) do |(key, value), memo|
56
+ memo[key.to_sym] = value
57
+ end
58
+ end
59
+ end
60
+
61
+ module_function
62
+
63
+ def define(name, &block)
64
+ definition = Definition.new(name)
65
+ definition.instance_eval(&block)
66
+ registry[definition.name] = definition
67
+ end
68
+
69
+ def registry
70
+ @registry ||= {}
71
+ end
72
+
73
+ def list
74
+ registry.values.map(&:to_h)
75
+ end
76
+
77
+ def fetch(name)
78
+ registry[name.to_s]
79
+ end
80
+ end
81
+
82
+ def Tool(name, &block)
83
+ ToolRegistry.define(name, &block)
84
+ end
@@ -2,6 +2,7 @@ require 'async'
2
2
 
3
3
  ENV['OTEL_LOG_LEVEL'] ||= 'debug'
4
4
  ENV['OTEL_TRACES_EXPORTER'] ||= 'console,otlp'
5
+ ENV['OTEL_LOGS_EXPORTER'] ||= 'otlp,console'
5
6
 
6
7
  unless defined? OTEL_ENABLED
7
8
  OTEL_ENABLED = !ENV['OTEL_EXPORTER_OTLP_ENDPOINT'].to_s.empty?
@@ -22,6 +23,9 @@ if OTEL_ENABLED
22
23
  require 'opentelemetry/exporter/otlp'
23
24
  require 'opentelemetry/instrumentation/all'
24
25
  require 'opentelemetry-api'
26
+ require 'opentelemetry-sdk'
27
+ require 'opentelemetry-logs-sdk'
28
+ require 'opentelemetry-exporter-otlp-logs'
25
29
  end
26
30
 
27
31
  if defined? Async and OTEL_ENABLED
@@ -76,9 +80,21 @@ def otel_initialize
76
80
  # c.service_name = SERVICE_NAME
77
81
  end
78
82
 
83
+ # Logs API
84
+ # ENV['OTEL_LOGS_EXPORTER'] = 'otlp,console' # Export logs to the console ,console
85
+ if ENV['OTEL_LOGS_EXPORTER'] =~ /console/
86
+ processor = OpenTelemetry::SDK::Logs::Export::SimpleLogRecordProcessor.new(OpenTelemetry::SDK::Logs::Export::ConsoleLogRecordExporter.new)
87
+ OpenTelemetry.logger_provider.add_log_record_processor(processor)
88
+ end
89
+
90
+ # Access a Logger for your library from the LoggerProvider configured by the OpenTelemetry API
91
+ logger = OpenTelemetry.logger_provider.logger(name: SERVICE_NAME, version: '0.1.0')
92
+ logger.on_emit( timestamp: Time.now, severity_text: 'INFO', body: 'Log provider initialized ', attributes: { 'ready' => true } )
93
+
79
94
  at_exit do
80
95
  OpenTelemetry.tracer_provider.force_flush
81
96
  OpenTelemetry.tracer_provider.shutdown
97
+ OpenTelemetry.logger_provider.shutdown
82
98
  end
83
99
 
84
100
  $tracer_ = OpenTelemetry.tracer_provider.tracer(SERVICE_NAME)
@@ -8,7 +8,7 @@ COPY . .
8
8
 
9
9
  CMD ["bash", "-c", "-x", "\
10
10
  env && cd /build/docker && \
11
- build-labels -n -c docker-compose.yml gitlab set_version to_dockerfiles to_compose | tee bake.yml && \
11
+ build-labels -n -c docker-compose.yml changed gitlab set_version to_dockerfiles to_compose | tee bake.yml && \
12
12
  export OTEL_RESOURCE_ATTRIBUTES=service.name=docker-builder,pipeline.id=${CI_PIPELINE_ID},project.name=${CI_PROJECT_NAME} && \
13
13
  export REGISTRY_HOST=$CI_REGISTRY_HOST && \
14
14
  grep \"services: {}\" bake.yml || docker buildx bake -f bake.yml $([ -z \"$CI_SKIP_PUSH\" ] && echo \"--push\") && \
@@ -1,5 +1,5 @@
1
1
  Options name: 'stack-name'
2
2
 
3
- Service :service_name, image: 'service_name/master', ports: 8000, ingress: { host: 'service_name.*' }
3
+ Service :service_name, image: 'service_name/master', ports: 7000, ingress: { host: 'service_name.*' }
4
4
 
5
5
  raise 'Configure the stack (https://github.com/artyomb/dry-stack)'
@@ -1,3 +1,3 @@
1
1
  module StackServiceBase
2
- VERSION = '0.0.57'
2
+ VERSION = '0.0.59'
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stack-service-base
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.57
4
+ version: 0.0.59
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artyom B
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-11-14 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rack
@@ -94,6 +93,48 @@ dependencies:
94
93
  - - ">="
95
94
  - !ruby/object:Gem::Version
96
95
  version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: opentelemetry-logs-api
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: opentelemetry-logs-sdk
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: opentelemetry-exporter-otlp-logs
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
97
138
  - !ruby/object:Gem::Dependency
98
139
  name: nats-pure
99
140
  requirement: !ruby/object:Gem::Requirement
@@ -234,8 +275,118 @@ dependencies:
234
275
  - - "~>"
235
276
  - !ruby/object:Gem::Version
236
277
  version: 0.5.1
237
- description:
238
- email:
278
+ - !ruby/object:Gem::Dependency
279
+ name: rspec-benchmark
280
+ requirement: !ruby/object:Gem::Requirement
281
+ requirements:
282
+ - - ">="
283
+ - !ruby/object:Gem::Version
284
+ version: '0'
285
+ type: :development
286
+ prerelease: false
287
+ version_requirements: !ruby/object:Gem::Requirement
288
+ requirements:
289
+ - - ">="
290
+ - !ruby/object:Gem::Version
291
+ version: '0'
292
+ - !ruby/object:Gem::Dependency
293
+ name: rack-test
294
+ requirement: !ruby/object:Gem::Requirement
295
+ requirements:
296
+ - - ">="
297
+ - !ruby/object:Gem::Version
298
+ version: '0'
299
+ type: :development
300
+ prerelease: false
301
+ version_requirements: !ruby/object:Gem::Requirement
302
+ requirements:
303
+ - - ">="
304
+ - !ruby/object:Gem::Version
305
+ version: '0'
306
+ - !ruby/object:Gem::Dependency
307
+ name: async-rspec
308
+ requirement: !ruby/object:Gem::Requirement
309
+ requirements:
310
+ - - ">="
311
+ - !ruby/object:Gem::Version
312
+ version: '0'
313
+ type: :development
314
+ prerelease: false
315
+ version_requirements: !ruby/object:Gem::Requirement
316
+ requirements:
317
+ - - ">="
318
+ - !ruby/object:Gem::Version
319
+ version: '0'
320
+ - !ruby/object:Gem::Dependency
321
+ name: rspec-snapshot
322
+ requirement: !ruby/object:Gem::Requirement
323
+ requirements:
324
+ - - ">="
325
+ - !ruby/object:Gem::Version
326
+ version: '0'
327
+ type: :development
328
+ prerelease: false
329
+ version_requirements: !ruby/object:Gem::Requirement
330
+ requirements:
331
+ - - ">="
332
+ - !ruby/object:Gem::Version
333
+ version: '0'
334
+ - !ruby/object:Gem::Dependency
335
+ name: testcontainers
336
+ requirement: !ruby/object:Gem::Requirement
337
+ requirements:
338
+ - - ">="
339
+ - !ruby/object:Gem::Version
340
+ version: '0'
341
+ type: :development
342
+ prerelease: false
343
+ version_requirements: !ruby/object:Gem::Requirement
344
+ requirements:
345
+ - - ">="
346
+ - !ruby/object:Gem::Version
347
+ version: '0'
348
+ - !ruby/object:Gem::Dependency
349
+ name: simplecov
350
+ requirement: !ruby/object:Gem::Requirement
351
+ requirements:
352
+ - - ">="
353
+ - !ruby/object:Gem::Version
354
+ version: '0'
355
+ type: :development
356
+ prerelease: false
357
+ version_requirements: !ruby/object:Gem::Requirement
358
+ requirements:
359
+ - - ">="
360
+ - !ruby/object:Gem::Version
361
+ version: '0'
362
+ - !ruby/object:Gem::Dependency
363
+ name: sinatra
364
+ requirement: !ruby/object:Gem::Requirement
365
+ requirements:
366
+ - - ">="
367
+ - !ruby/object:Gem::Version
368
+ version: '0'
369
+ type: :development
370
+ prerelease: false
371
+ version_requirements: !ruby/object:Gem::Requirement
372
+ requirements:
373
+ - - ">="
374
+ - !ruby/object:Gem::Version
375
+ version: '0'
376
+ - !ruby/object:Gem::Dependency
377
+ name: slim
378
+ requirement: !ruby/object:Gem::Requirement
379
+ requirements:
380
+ - - ">="
381
+ - !ruby/object:Gem::Version
382
+ version: '0'
383
+ type: :development
384
+ prerelease: false
385
+ version_requirements: !ruby/object:Gem::Requirement
386
+ requirements:
387
+ - - ">="
388
+ - !ruby/object:Gem::Version
389
+ version: '0'
239
390
  executables:
240
391
  - ssbase
241
392
  extensions: []
@@ -247,8 +398,11 @@ files:
247
398
  - lib/stack-service-base/command_line.rb
248
399
  - lib/stack-service-base/database.rb
249
400
  - lib/stack-service-base/debugger.rb
401
+ - lib/stack-service-base/examples/mcp_config.ru
250
402
  - lib/stack-service-base/fiber_pool.rb
251
403
  - lib/stack-service-base/logging.rb
404
+ - lib/stack-service-base/mcp_processor.rb
405
+ - lib/stack-service-base/mcp_tool_registry.rb
252
406
  - lib/stack-service-base/nats_patch_1.rb
253
407
  - lib/stack-service-base/nats_service.rb
254
408
  - lib/stack-service-base/open_telemetry.rb
@@ -290,10 +444,8 @@ files:
290
444
  - lib/stack-service-base/stack_template/home/stack/stack.drs
291
445
  - lib/stack-service-base/version.rb
292
446
  - lib/stack-service-base/views/ssbase_info.slim
293
- homepage:
294
447
  licenses: []
295
448
  metadata: {}
296
- post_install_message:
297
449
  rdoc_options: []
298
450
  require_paths:
299
451
  - lib
@@ -301,15 +453,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
301
453
  requirements:
302
454
  - - ">="
303
455
  - !ruby/object:Gem::Version
304
- version: 3.3.1
456
+ version: 3.4.4
305
457
  required_rubygems_version: !ruby/object:Gem::Requirement
306
458
  requirements:
307
459
  - - ">="
308
460
  - !ruby/object:Gem::Version
309
461
  version: '0'
310
462
  requirements: []
311
- rubygems_version: 3.5.9
312
- signing_key:
463
+ rubygems_version: 3.6.7
313
464
  specification_version: 4
314
465
  summary: Common files
315
466
  test_files: []