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 +4 -4
- data/lib/stack-service-base/command_init.rb +3 -3
- data/lib/stack-service-base/examples/mcp_config.ru +16 -16
- data/lib/stack-service-base/mcp/mcp_helper.rb +1 -1
- data/lib/stack-service-base/mcp/mcp_processor.rb +22 -22
- data/lib/stack-service-base/mcp/mcp_tool_registry.rb +1 -1
- data/lib/stack-service-base/project_template/gitlab/.gitlab-ci.yml +42 -11
- data/lib/stack-service-base/project_template/gitlab/docker/Dockerfile.build +36 -0
- data/lib/stack-service-base/project_template/gitlab/docker/docker-compose.yml +1 -1
- data/lib/stack-service-base/project_template/gitlab/docker/local_build.sh +10 -0
- data/lib/stack-service-base/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3db078c7ea6acaca31ec7b32b7991fd213dcaacd03fcab42733f8f3d299b6456
|
|
4
|
+
data.tar.gz: 621b4b6c3b116f0f72555740ca83f9f62029e0a58b3d19655fdf77db45f27a5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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('
|
|
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
|
-
|
|
8
|
-
status:
|
|
9
|
-
uptime: 72 * 3600,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
37
|
+
input_schema type: 'object',
|
|
38
38
|
properties: {
|
|
39
|
-
value: { type:
|
|
39
|
+
value: { type: 'string', description: 'Value to echo' }
|
|
40
40
|
},
|
|
41
|
-
required: [
|
|
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:
|
|
50
|
+
input value: { type: 'string', description: 'Value to echo', required: true }
|
|
51
51
|
call do |inputs|
|
|
52
52
|
{
|
|
53
53
|
content: [
|
|
54
|
-
{ type:
|
|
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:
|
|
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:
|
|
83
|
-
force: { type:
|
|
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] =
|
|
89
|
+
service[:status] = 'running'
|
|
90
90
|
service[:last_restart] = Time.now
|
|
91
91
|
service[:uptime] = 0
|
|
92
92
|
{
|
|
@@ -30,7 +30,7 @@ class McpProcessor
|
|
|
30
30
|
def initialize(body:, status:)
|
|
31
31
|
@body = body
|
|
32
32
|
@status = status
|
|
33
|
-
super(
|
|
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[
|
|
50
|
-
params = req[
|
|
49
|
+
method = req['method']
|
|
50
|
+
params = req['params']
|
|
51
51
|
|
|
52
|
-
if req.key?(
|
|
53
|
-
rpc_response(id: req[
|
|
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: -
|
|
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
|
|
90
|
-
# when
|
|
91
|
-
# when
|
|
92
|
-
when
|
|
93
|
-
when
|
|
94
|
-
when
|
|
95
|
-
when
|
|
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!(-
|
|
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
|
|
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:
|
|
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: -
|
|
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[
|
|
148
|
-
arguments = params[
|
|
149
|
-
tool = registry.fetch(name) || rpc_error!(-
|
|
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,
|
|
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
|
-
{
|
|
171
|
+
{ 'type' => 'text', 'text' => response.is_a?(String) ? response : JSON.dump(response) }
|
|
172
172
|
],
|
|
173
173
|
isError: false
|
|
174
174
|
}
|
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
stages:
|
|
2
|
+
- test
|
|
2
3
|
- build
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
image: docker:
|
|
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
|
-
|
|
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
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
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
|
+
"]
|
|
@@ -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
|
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.
|
|
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
|