stack-service-base 0.0.98 → 0.0.99

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: 4637e888970a11131f20239dd6b2e64c661d40daafe1942cdd2193d6b05f495e
4
- data.tar.gz: a9d68103d79f1ab23a0c9cac1c2366b42238a1e5006b8c716b126e791966cf08
3
+ metadata.gz: 3db078c7ea6acaca31ec7b32b7991fd213dcaacd03fcab42733f8f3d299b6456
4
+ data.tar.gz: 621b4b6c3b116f0f72555740ca83f9f62029e0a58b3d19655fdf77db45f27a5b
5
5
  SHA512:
6
- metadata.gz: 747b9ae740e563c28057aaee05321183280a44152ac748fe949e2d04e9077b5bc8c66add36340daa79759cbce31a8f75dfb3da0811745aed259c35e3370a7059
7
- data.tar.gz: a70bdbb1c4915a65db80745a470c905114add9e35df94c7ea4b9a3b057abfee7af29488c64b612cf9ed0fc609b0f98a3a402509583dda244d7d08ad5157dba9f
6
+ metadata.gz: 0ecbb4d62eb789d83cbbc72d422ed7316a5ac486d1cf3dbcf538cd253aefb156d3dcb71b5937e5bd9f99e9360c10310fea242d42abda559532d2ba73594b9213
7
+ data.tar.gz: 4d3d7862b719bcb04d26881dada64385c2a61f3bcad4e1ce13d080b26249fb6accaa31bf650b820d5988de99c132570e09c03b19bbd74121f7f076af8f7b2f28
@@ -66,7 +66,9 @@ SSBase::CommandLine::COMMANDS[:init] = Class.new do
66
66
 
67
67
  def update_service_name(s_name)
68
68
  $stdout.puts "Update service name: #{s_name}"
69
- Dir.glob('**/*').each do |file|
69
+ Dir.glob('**/{*,.*}', File::FNM_DOTMATCH).each do |file|
70
+ path_parts = file.split('/')
71
+ next if path_parts[0...-1].any? { |part| part.start_with?('.') }
70
72
  next unless File.file? file
71
73
  content = File.read(file)
72
74
  include = content.include?('${service_name}') ? '(found)' : nil
@@ -80,5 +82,3 @@ SSBase::CommandLine::COMMANDS[:init] = Class.new do
80
82
  def help = ['Create basic service file structure',
81
83
  '[... to_compose <deploy name>] ']
82
84
  end.new
83
-
84
-
@@ -4,9 +4,9 @@ require 'stack-service-base'
4
4
  StackServiceBase.rack_setup self
5
5
 
6
6
  SERVICES = {
7
- "database-backend" => {
8
- status: "running",
9
- uptime: 72 * 3600, # seconds
7
+ 'database-backend' => {
8
+ status: 'running',
9
+ uptime: 72 * 3600, # seconds
10
10
  last_restart: Time.now - 72 * 3600
11
11
  }
12
12
  }
@@ -16,29 +16,29 @@ helpers McpHelper
16
16
 
17
17
  Tool :search do
18
18
  description 'Search for a term in the database'
19
- input query: { type: "string", description: "Term to search for", required: true }
19
+ input query: { type: 'string', description: 'Term to search for', required: true }
20
20
  call do |inputs|
21
21
  query = inputs[:query]
22
- { results: [{id:"doc-1",title:"...",url:"..."}] }
22
+ { results: [{id: 'doc-1', title: '...', url: '...'}] }
23
23
  end
24
24
  end
25
25
 
26
26
  Tool :fetch do
27
27
  description 'Fetch a resource from the database'
28
- input resource_id: { type: "string", description: "Resource ID to fetch", required: true }
28
+ input resource_id: { type: 'string', description: 'Resource ID to fetch', required: true }
29
29
  call do |inputs|
30
30
  id = inputs[:id]
31
- { id: "doc-1", title: "...", text: "full text...", url: "https://example.com/doc", metadata: { source: "vector_store" } }
31
+ { id: 'doc-1', title: '...', text: 'full text...', url: 'https://example.com/doc', metadata: { source: 'vector_store' } }
32
32
  end
33
33
  end
34
34
 
35
35
  Tool :schema_echo do
36
36
  description 'Echo a value using a direct JSON schema'
37
- input_schema type: "object",
37
+ input_schema type: 'object',
38
38
  properties: {
39
- value: { type: "string", description: "Value to echo" }
39
+ value: { type: 'string', description: 'Value to echo' }
40
40
  },
41
- required: ["value"]
41
+ required: ['value']
42
42
  annotations readOnlyHint: true
43
43
  call do |inputs|
44
44
  { value: inputs[:value] }
@@ -47,11 +47,11 @@ end
47
47
 
48
48
  Tool :full_response_echo do
49
49
  description 'Echo a value using a complete MCP tool response'
50
- input value: { type: "string", description: "Value to echo", required: true }
50
+ input value: { type: 'string', description: 'Value to echo', required: true }
51
51
  call do |inputs|
52
52
  {
53
53
  content: [
54
- { type: "text", text: inputs[:value] }
54
+ { type: 'text', text: inputs[:value] }
55
55
  ],
56
56
  structuredContent: {
57
57
  value: inputs[:value]
@@ -63,7 +63,7 @@ end
63
63
 
64
64
  Tool :service_status do
65
65
  description 'Check current status of a service'
66
- input service_name: { type: "string", description: "Service name to inspect", required: true }
66
+ input service_name: { type: 'string', description: 'Service name to inspect', required: true }
67
67
  call do |inputs|
68
68
  service_name = inputs[:service_name]
69
69
  service = SERVICES[service_name]
@@ -79,14 +79,14 @@ end
79
79
 
80
80
  Tool :restart_service do
81
81
  description 'Restart a service'
82
- input service_name: { type: "string", description: "Service name to restart", required: true },
83
- force: { type: "boolean", default: false, description: "Force restart if graceful fails" }
82
+ input service_name: { type: 'string', description: 'Service name to restart', required: true },
83
+ force: { type: 'boolean', default: false, description: 'Force restart if graceful fails' }
84
84
  call do |inputs|
85
85
  service_name = inputs[:service_name]
86
86
  service = SERVICES[service_name]
87
87
  rpc_error!(-32000, "Unknown service #{service_name}") unless service
88
88
 
89
- service[:status] = "running"
89
+ service[:status] = 'running'
90
90
  service[:last_restart] = Time.now
91
91
  service[:uptime] = 0
92
92
  {
@@ -58,7 +58,7 @@ module McpHelper
58
58
 
59
59
  stream true do |s|
60
60
  s.callback { LOGGER.debug "stream closed: #{s}" }
61
- s << "event: message\ndata: #{response_body}\n\n"
61
+ s << ['event: message', "data: #{response_body}", '', ''].join($/)
62
62
  s.close
63
63
  end
64
64
  end
@@ -30,7 +30,7 @@ class McpProcessor
30
30
  def initialize(body:, status:)
31
31
  @body = body
32
32
  @status = status
33
- super("MCP parse error")
33
+ super('MCP parse error')
34
34
  end
35
35
  end
36
36
 
@@ -46,17 +46,17 @@ class McpProcessor
46
46
 
47
47
  def rpc_endpoint(raw_body)
48
48
  req = JSON.parse(raw_body.to_s)
49
- method = req["method"]
50
- params = req["params"]
49
+ method = req['method']
50
+ params = req['params']
51
51
 
52
- if req.key?("id")
53
- rpc_response(id: req["id"], method: method, params: params)
52
+ if req.key?('id')
53
+ rpc_response(id: req['id'], method: method, params: params)
54
54
  else
55
55
  notification_response(method: method, params: params)
56
56
  end
57
57
  rescue JSON::ParserError => e
58
58
  @logger&.warn("MCP JSON parse failed: #{e.message}")
59
- body = error_response(id: nil, code: -32700, message: "Parse error")
59
+ body = error_response(id: nil, code: -32_700, message: 'Parse error')
60
60
  raise ParseError.new(body: body, status: 400)
61
61
  end
62
62
 
@@ -86,21 +86,21 @@ class McpProcessor
86
86
 
87
87
  def handle(method:, params:)
88
88
  case method
89
- when "tools/list" then list_tools
90
- # when "resources/list" then {}
91
- # when "prompts/list" then {}
92
- when "tools/call" then call_tool(params || {})
93
- when "initialize" then initialize_response
94
- when "notifications/initialized" then @logger&.debug(params); {}
95
- when "logging/setLevel" then @logger&.debug(params); {}
89
+ when 'tools/list' then list_tools
90
+ # when 'resources/list' then {}
91
+ # when 'prompts/list' then {}
92
+ when 'tools/call' then call_tool(params || {})
93
+ when 'initialize' then initialize_response
94
+ when 'notifications/initialized' then @logger&.debug(params); {}
95
+ when 'logging/setLevel' then @logger&.debug(params); {}
96
96
  else
97
- rpc_error!(-32601, "Unknown method #{method}")
97
+ rpc_error!(-32_601, "Unknown method #{method}")
98
98
  end
99
99
  end
100
100
 
101
101
  def handle_notification(method:, params:)
102
102
  case method
103
- when "notifications/initialized", "notifications/cancelled"
103
+ when 'notifications/initialized', 'notifications/cancelled'
104
104
  @logger&.debug("MCP notification accepted: #{method}")
105
105
  else
106
106
  @logger&.debug("MCP notification ignored: #{method}")
@@ -127,7 +127,7 @@ class McpProcessor
127
127
  private
128
128
 
129
129
  def json_rpc_response(id:)
130
- body = { jsonrpc: "2.0", id: id }
130
+ body = { jsonrpc: '2.0', id: id }
131
131
 
132
132
  begin
133
133
  result = yield
@@ -136,7 +136,7 @@ class McpProcessor
136
136
  body[:error] = { code: e.code, message: e.message }
137
137
  rescue => e
138
138
  @logger&.error("Unhandled RPC error: #{e.class}: #{e.message}\n#{e.backtrace&.first}")
139
- body[:error] = { code: -32603, message: "Internal error" }
139
+ body[:error] = { code: -32_603, message: 'Internal error' }
140
140
  end
141
141
 
142
142
  body.delete(:result) if body[:error]
@@ -144,9 +144,9 @@ class McpProcessor
144
144
  end
145
145
 
146
146
  def call_tool(params)
147
- name = params["name"]
148
- arguments = params["arguments"] || {}
149
- tool = registry.fetch(name) || rpc_error!(-32601, "Unknown tool #{name}")
147
+ name = params['name']
148
+ arguments = params['arguments'] || {}
149
+ tool = registry.fetch(name) || rpc_error!(-32_601, "Unknown tool #{name}")
150
150
  response = tool.call_tool(arguments)
151
151
  return response if mcp_tool_response?(response)
152
152
 
@@ -160,7 +160,7 @@ class McpProcessor
160
160
  def mcp_tool_response?(response)
161
161
  return false unless response.is_a?(Hash)
162
162
 
163
- [:content, :structuredContent, :isError, "content", "structuredContent", "isError"].any? do |key|
163
+ [:content, :structuredContent, :isError, 'content', 'structuredContent', 'isError'].any? do |key|
164
164
  response.key?(key)
165
165
  end
166
166
  end
@@ -168,7 +168,7 @@ class McpProcessor
168
168
  def wrap_tool_response(response)
169
169
  {
170
170
  content: [
171
- { "type": "text", "text": response.is_a?(String) ? response : JSON.dump(response) }
171
+ { 'type' => 'text', 'text' => response.is_a?(String) ? response : JSON.dump(response) }
172
172
  ],
173
173
  isError: false
174
174
  }
@@ -81,7 +81,7 @@ module ToolRegistry
81
81
 
82
82
  @inputs.each do |field, config|
83
83
  cfg = stringify_keys(config)
84
- required << field if cfg.delete("required")
84
+ required << field if cfg.delete('required')
85
85
  properties[field] = cfg
86
86
  end
87
87
 
@@ -1,17 +1,48 @@
1
1
  stages:
2
+ - test
2
3
  - build
3
4
 
4
- build-job:
5
- stage: build
6
- image: docker:latest
5
+ .build_block: &build_block
6
+ # tags: [ build ]
7
+ image: docker:27.5.1-cli
7
8
  services:
8
- - docker:dind
9
+ - name: docker:27.5.1-dind
10
+ alias: docker
9
11
  variables:
10
- DOCKER_TLS_CERTDIR: "/certs"
11
-
12
- before_script:
13
- - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
12
+ DOCKER_HOST: tcp://docker:2375
13
+ DOCKER_TLS_CERTDIR: ""
14
14
  script:
15
- - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -t $CI_REGISTRY_IMAGE:latest -f docker/Dockerfile ./src
16
- - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
17
- - docker push $CI_REGISTRY_IMAGE:latest
15
+ - export CI_COMMIT_TAG="${CI_COMMIT_TAG:-0.0.0}"
16
+ - |
17
+ if [ -n "${CI_REGISTRY:-}" ] && [ -n "${CI_REGISTRY_USER:-}" ] && [ -n "${CI_REGISTRY_PASSWORD:-}" ]; then
18
+ echo "${CI_REGISTRY_PASSWORD}" | docker login "${CI_REGISTRY}" -u "${CI_REGISTRY_USER}" --password-stdin
19
+ fi
20
+ - docker buildx create --name "${CI_PROJECT_NAME}-wrapper-builder" --driver docker-container --use || docker buildx use "${CI_PROJECT_NAME}-wrapper-builder"
21
+ - docker buildx inspect --bootstrap
22
+ - |
23
+ docker buildx build --load \
24
+ -t build/${CI_PROJECT_NAME} \
25
+ -f docker/Dockerfile.build \
26
+ --cache-from type=registry,ref=${CI_REGISTRY_IMAGE}/ci-wrapper:${CI_COMMIT_REF_SLUG}-cache \
27
+ --cache-to type=registry,ref=${CI_REGISTRY_IMAGE}/ci-wrapper:${CI_COMMIT_REF_SLUG}-cache,mode=max \
28
+ .
29
+ - docker run --rm `env | grep -o '^CI_[^=]*' | sed 's/^/-e /'`
30
+ -e REGISTRY_HOST=$CI_REGISTRY/$CI_PROJECT_NAMESPACE
31
+ -v /var/run/docker.sock:/var/run/docker.sock
32
+ -v /root/.docker:/root/.docker
33
+ build/${CI_PROJECT_NAME} 2>&1
34
+
35
+ build push:
36
+ <<: *build_block
37
+ stage: build
38
+ needs: [ tests ]
39
+
40
+ tests:
41
+ <<: *build_block
42
+ stage: test
43
+ variables:
44
+ DOCKER_HOST: tcp://docker:2375
45
+ DOCKER_TLS_CERTDIR: ""
46
+ CI_SKIP_PUSH: true
47
+ # build-labels sets target for each service in docker-compose.yml
48
+ CI_BUILD_TARGET: tests
@@ -0,0 +1,36 @@
1
+ FROM ruby:3.4.4-slim-bookworm AS base
2
+ RUN apt-get update && apt-get install -y --no-install-recommends bash ca-certificates curl git tar \
3
+ && rm -rf /var/lib/apt/lists/*
4
+ RUN gem install build-labels:0.0.79
5
+ RUN BUILDX_VERSION=v0.20.1 COMPOSE_VERSION=v2.33.0 install-docker-static 27.5.1
6
+
7
+ WORKDIR /build
8
+
9
+ COPY .. .
10
+
11
+ CMD ["bash", "-euo", "pipefail", "-c", "\
12
+ cd /build/docker; \
13
+ if [ -n \"${CI_REGISTRY:-}\" ] && [ -n \"${CI_REGISTRY_USER:-}\" ] && [ -n \"${CI_REGISTRY_PASSWORD:-}\" ]; then \
14
+ echo \"$CI_REGISTRY_PASSWORD\" | docker login \"$CI_REGISTRY\" -u \"$CI_REGISTRY_USER\" --password-stdin; \
15
+ fi; \
16
+ build-labels -n -c docker-compose.yml changed gitlab set_version cache to_dockerfiles to_compose | tee bake.yml; \
17
+ export OTEL_RESOURCE_ATTRIBUTES=\"service.name=docker-builder,pipeline.id=${CI_PIPELINE_ID:-local},project.name=${service_name}\"; \
18
+ export REGISTRY_HOST=\"${REGISTRY_HOST:-${CI_REGISTRY_HOST:-${CI_REGISTRY_IMAGE:-}}}\"; \
19
+ : \"${REGISTRY_HOST:?REGISTRY_HOST, CI_REGISTRY_IMAGE, or CI_REGISTRY_HOST is required}\"; \
20
+ export BUILDX_BAKE_ENTITLEMENTS_FS=0; \
21
+ if grep -q \"services: {}\" bake.yml; then \
22
+ echo \"No changed services to build.\"; \
23
+ exit 0; \
24
+ fi; \
25
+ docker buildx create --name \"${service_name}-builder\" --driver docker-container --use || docker buildx use \"${service_name}-builder\"; \
26
+ docker buildx inspect --bootstrap; \
27
+ if [ -n \"${CI_SKIP_PUSH:-}\" ]; then \
28
+ docker buildx bake --load --allow=fs.read=../src -f bake.yml; \
29
+ else \
30
+ docker buildx bake --push --allow=fs.read=../src -f bake.yml; \
31
+ fi; \
32
+ if [ \"${CI_BUILD_TARGET:-}\" = \"tests\" ]; then \
33
+ docker compose -f bake.yml down --remove-orphans; \
34
+ docker compose -f bake.yml up --no-build --force-recreate --abort-on-container-failure; \
35
+ fi \
36
+ "]
@@ -1,6 +1,6 @@
1
1
  services:
2
2
  ${service_name}:
3
- image: ${REGISTRY_HOST}/${service_name}
3
+ image: ${REGISTRY_HOST}/${service_name}/${CI_COMMIT_REF_SLUG:-local}
4
4
  build:
5
5
  context: ../src
6
6
  additional_contexts:
@@ -0,0 +1,10 @@
1
+ export CI_PROJECT_NAME=${service_name}
2
+ export CI_PIPELINE_ID=local
3
+ export CI_PIPELINE_IID=0
4
+ export CI_REGISTRY_HOST=localhost
5
+ export CI_SKIP_PUSH=true
6
+
7
+ # -v /root/.docker:/root/.docker \
8
+ docker run --rm --env-file <(env | grep ^CI_) \
9
+ -v /var/run/docker.sock:/var/run/docker.sock \
10
+ $(docker build -q -t build/${CI_PROJECT_NAME} -f Dockerfile.build ..) 2>&1
@@ -1,3 +1,3 @@
1
1
  module StackServiceBase
2
- VERSION = '0.0.98'
2
+ VERSION = '0.0.99'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stack-service-base
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.98
4
+ version: 0.0.99
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artyom B
@@ -433,7 +433,9 @@ files:
433
433
  - lib/stack-service-base/project_template/gitlab-c/docker/docker-compose.yml
434
434
  - lib/stack-service-base/project_template/gitlab-c/docker/local_build.sh
435
435
  - lib/stack-service-base/project_template/gitlab/.gitlab-ci.yml
436
+ - lib/stack-service-base/project_template/gitlab/docker/Dockerfile.build
436
437
  - lib/stack-service-base/project_template/gitlab/docker/docker-compose.yml
438
+ - lib/stack-service-base/project_template/gitlab/docker/local_build.sh
437
439
  - lib/stack-service-base/project_template/home/.gitignore
438
440
  - lib/stack-service-base/project_template/home/AGENTS.md
439
441
  - lib/stack-service-base/project_template/home/docker/.env