stack-service-base 0.0.98 → 0.0.100

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stack-service-base/command_init.rb +8 -6
  3. data/lib/stack-service-base/examples/mcp_config.ru +16 -16
  4. data/lib/stack-service-base/frontend_template/github/.github/workflows/main.yml +19 -0
  5. data/lib/stack-service-base/frontend_template/github/docker/docker-compose.yml +15 -0
  6. data/lib/stack-service-base/frontend_template/gitlab/.gitlab-ci.yml +48 -0
  7. data/lib/stack-service-base/frontend_template/gitlab/docker/Dockerfile.build +36 -0
  8. data/lib/stack-service-base/frontend_template/gitlab/docker/docker-compose.yml +15 -0
  9. data/lib/stack-service-base/frontend_template/gitlab/docker/local_build.sh +10 -0
  10. data/lib/stack-service-base/frontend_template/gitlab-c/.gitlab-ci.yml +24 -0
  11. data/lib/stack-service-base/frontend_template/gitlab-c/docker/Dockerfile.build +17 -0
  12. data/lib/stack-service-base/frontend_template/gitlab-c/docker/docker-compose.yml +15 -0
  13. data/lib/stack-service-base/frontend_template/gitlab-c/docker/local_build.sh +11 -0
  14. data/lib/stack-service-base/frontend_template/home/.editorconfig +12 -0
  15. data/lib/stack-service-base/frontend_template/home/.gitattributes +4 -0
  16. data/lib/stack-service-base/frontend_template/home/.gitignore +17 -0
  17. data/lib/stack-service-base/frontend_template/home/AGENTS.md +3 -0
  18. data/lib/stack-service-base/frontend_template/home/README.md +68 -0
  19. data/lib/stack-service-base/frontend_template/home/docker/.env +3 -0
  20. data/lib/stack-service-base/frontend_template/home/docker/frontend/15-runtime-settings.envsh +90 -0
  21. data/lib/stack-service-base/frontend_template/home/docker/frontend/40-path-prefix-settings.sh +30 -0
  22. data/lib/stack-service-base/frontend_template/home/docker/frontend/45-runtime-config-settings.sh +24 -0
  23. data/lib/stack-service-base/frontend_template/home/docker/frontend/Dockerfile +46 -0
  24. data/lib/stack-service-base/frontend_template/home/docker/frontend/nginx.conf.template +75 -0
  25. data/lib/stack-service-base/frontend_template/home/src/.dockerignore +9 -0
  26. data/lib/stack-service-base/frontend_template/home/src/.env.example +7 -0
  27. data/lib/stack-service-base/frontend_template/home/src/.version +1 -0
  28. data/lib/stack-service-base/frontend_template/home/src/app/main.ts +4 -0
  29. data/lib/stack-service-base/frontend_template/home/src/app/runtime-config.ts +22 -0
  30. data/lib/stack-service-base/frontend_template/home/src/app/runtime-config.types.ts +5 -0
  31. data/lib/stack-service-base/frontend_template/home/src/app/style.css +82 -0
  32. data/lib/stack-service-base/frontend_template/home/src/app/vite-env.d.ts +1 -0
  33. data/lib/stack-service-base/frontend_template/home/src/eslint.config.js +34 -0
  34. data/lib/stack-service-base/frontend_template/home/src/index.html +24 -0
  35. data/lib/stack-service-base/frontend_template/home/src/package-lock.json +2812 -0
  36. data/lib/stack-service-base/frontend_template/home/src/package.json +30 -0
  37. data/lib/stack-service-base/frontend_template/home/src/proxy.config.json +8 -0
  38. data/lib/stack-service-base/frontend_template/home/src/public/runtime-config.js +1 -0
  39. data/lib/stack-service-base/frontend_template/home/src/tests/build-proxy.test.ts +28 -0
  40. data/lib/stack-service-base/frontend_template/home/src/tests/javascript-support.test.js +23 -0
  41. data/lib/stack-service-base/frontend_template/home/src/tests/path-prefix.test.ts +17 -0
  42. data/lib/stack-service-base/frontend_template/home/src/tests/runtime-config.test.ts +22 -0
  43. data/lib/stack-service-base/frontend_template/home/src/tests/vite-config.test.ts +16 -0
  44. data/lib/stack-service-base/frontend_template/home/src/tools/vite/build-proxy.ts +62 -0
  45. data/lib/stack-service-base/frontend_template/home/src/tools/vite/path-prefix.ts +27 -0
  46. data/lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config-dev-plugin.ts +27 -0
  47. data/lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config.ts +10 -0
  48. data/lib/stack-service-base/frontend_template/home/src/tsconfig.json +27 -0
  49. data/lib/stack-service-base/frontend_template/home/src/vite.config.ts +40 -0
  50. data/lib/stack-service-base/mcp/mcp_helper.rb +1 -1
  51. data/lib/stack-service-base/mcp/mcp_processor.rb +22 -22
  52. data/lib/stack-service-base/mcp/mcp_tool_registry.rb +1 -1
  53. data/lib/stack-service-base/project_template/gitlab/.gitlab-ci.yml +42 -11
  54. data/lib/stack-service-base/project_template/gitlab/docker/Dockerfile.build +36 -0
  55. data/lib/stack-service-base/project_template/gitlab/docker/docker-compose.yml +1 -1
  56. data/lib/stack-service-base/project_template/gitlab/docker/local_build.sh +10 -0
  57. data/lib/stack-service-base/version.rb +1 -1
  58. metadata +49 -1
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "${service_name}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": "^24.18.0"
8
+ },
9
+ "scripts": {
10
+ "start": "vite --host 0.0.0.0",
11
+ "build": "vite build",
12
+ "build:preview": "vite build --mode preview",
13
+ "preview": "npm run build:preview && vite preview --host 0.0.0.0 --mode preview",
14
+ "typecheck": "tsc --noEmit",
15
+ "lint": "eslint .",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "check": "npm run typecheck && npm run lint && npm run test && npm run build"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/js": "9.39.4",
22
+ "@types/node": "24.13.3",
23
+ "eslint": "9.39.4",
24
+ "globals": "15.15.0",
25
+ "typescript": "5.9.3",
26
+ "typescript-eslint": "8.59.3",
27
+ "vite": "8.1.5",
28
+ "vitest": "4.1.10"
29
+ }
30
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "/api": {
3
+ "targetEnv": "BACKEND_URL",
4
+ "secure": true,
5
+ "changeOrigin": true,
6
+ "ws": true
7
+ }
8
+ }
@@ -0,0 +1 @@
1
+ window.__APP_CONFIG__ = {};
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildProxy } from '../tools/vite/build-proxy';
3
+
4
+ const rules = {
5
+ '/api': {
6
+ targetEnv: 'BACKEND_URL',
7
+ secure: true,
8
+ changeOrigin: true,
9
+ ws: true
10
+ }
11
+ };
12
+
13
+ describe('development proxy', () => {
14
+ it('builds root and prefixed routes for the configured backend', () => {
15
+ const proxy = buildProxy(
16
+ { PATH_PREFIX: '/portal', BACKEND_URL: 'https://api.example.com' },
17
+ rules
18
+ );
19
+
20
+ expect(proxy['/api']?.target).toBe('https://api.example.com');
21
+ expect(proxy['/portal/api']?.target).toBe('https://api.example.com');
22
+ expect(proxy['/portal/api']?.rewrite?.('/portal/api/status')).toBe('/api/status');
23
+ });
24
+
25
+ it('does not create routes when the backend is not configured', () => {
26
+ expect(buildProxy({}, rules)).toEqual({});
27
+ });
28
+ });
@@ -0,0 +1,23 @@
1
+ import { ESLint } from 'eslint';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { normalizePathPrefix } from '../tools/vite/path-prefix';
6
+
7
+ const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
+
9
+ describe('JavaScript support', () => {
10
+ it('allows JavaScript modules to import TypeScript modules', () => {
11
+ expect(normalizePathPrefix('/team/ui/')).toBe('/team/ui');
12
+ });
13
+
14
+ it('lints application JavaScript with browser globals', async () => {
15
+ const eslint = new ESLint({ cwd: projectRoot });
16
+ const [result] = await eslint.lintText(
17
+ 'document.body.dataset.path = window.location.pathname;',
18
+ { filePath: resolve(projectRoot, 'app/javascript-support.js') }
19
+ );
20
+
21
+ expect(result.messages).toEqual([]);
22
+ });
23
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizePathPrefix, pathPrefixBase } from '../tools/vite/path-prefix';
3
+
4
+ describe('path prefix', () => {
5
+ it('normalizes empty and nested prefixes', () => {
6
+ expect(normalizePathPrefix(undefined)).toBe('');
7
+ expect(normalizePathPrefix(' /team/ui/ ')).toBe('/team/ui');
8
+ expect(pathPrefixBase('/team/ui')).toBe('/team/ui/');
9
+ });
10
+
11
+ it.each(['/api', '/assets', '/healthcheck', '/team//ui', '/../admin', '/admin?debug=true'])(
12
+ 'rejects an unsafe or runtime-reserved prefix: %s',
13
+ (pathPrefix) => {
14
+ expect(() => normalizePathPrefix(pathPrefix)).toThrow();
15
+ }
16
+ );
17
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildRuntimeConfigFromEnv } from '../tools/vite/runtime-config';
3
+
4
+ describe('runtime config', () => {
5
+ it('exposes only the documented browser-visible values', () => {
6
+ const runtimeConfig = buildRuntimeConfigFromEnv({
7
+ PATH_PREFIX: ' /team/ui/ ',
8
+ API_BASE_URL: ' /team/ui/api ',
9
+ LOG_LEVEL: ' debug ',
10
+ BACKEND_URL: 'https://internal-api.example.com',
11
+ AUTH_PASS: 'must-not-be-public'
12
+ });
13
+
14
+ expect(runtimeConfig).toEqual({
15
+ PATH_PREFIX: '/team/ui',
16
+ API_BASE_URL: '/team/ui/api',
17
+ LOG_LEVEL: 'debug'
18
+ });
19
+ expect(JSON.stringify(runtimeConfig)).not.toContain('internal-api');
20
+ expect(JSON.stringify(runtimeConfig)).not.toContain('must-not-be-public');
21
+ });
22
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildBase } from '../vite.config';
3
+
4
+ describe('buildBase', () => {
5
+ it('keeps the runtime placeholder in a container build', () => {
6
+ expect(buildBase('build', 'production', '/team/ui')).toBe('/__PATH_PREFIX__/');
7
+ });
8
+
9
+ it('uses the configured prefix in a local production preview', () => {
10
+ expect(buildBase('build', 'preview', '/team/ui')).toBe('/team/ui/');
11
+ });
12
+
13
+ it('uses the configured prefix in development', () => {
14
+ expect(buildBase('serve', 'development', '/team/ui')).toBe('/team/ui/');
15
+ });
16
+ });
@@ -0,0 +1,62 @@
1
+ import type { ProxyOptions } from 'vite';
2
+ import { normalizePathPrefix } from './path-prefix';
3
+
4
+ const DEFAULT_PROXY_OPTIONS: ProxyOptions = {
5
+ cookieDomainRewrite: '',
6
+ secure: true,
7
+ changeOrigin: true
8
+ };
9
+
10
+ export type ProxyRuleConfig = {
11
+ targetEnv: string;
12
+ secure?: boolean;
13
+ changeOrigin?: boolean;
14
+ ws?: boolean;
15
+ pathRewrite?: Record<string, string>;
16
+ };
17
+
18
+ export function buildProxy(
19
+ env: Record<string, string>,
20
+ rules: Record<string, ProxyRuleConfig>
21
+ ): Record<string, ProxyOptions> {
22
+ const proxy: Record<string, ProxyOptions> = {};
23
+ const pathPrefix = normalizePathPrefix(env.PATH_PREFIX);
24
+
25
+ for (const [context, rule] of Object.entries(rules)) {
26
+ const target = env[rule.targetEnv];
27
+ if (!target) continue;
28
+
29
+ proxy[context] = createProxyOptions(target, rule);
30
+ if (pathPrefix) proxy[`${pathPrefix}${context}`] = createProxyOptions(target, rule, pathPrefix);
31
+ }
32
+
33
+ return proxy;
34
+ }
35
+
36
+ function createProxyOptions(target: string, rule: ProxyRuleConfig, pathPrefix = ''): ProxyOptions {
37
+ const { pathRewrite, targetEnv: _targetEnv, ...overrides } = rule;
38
+ const shouldRewrite = Boolean(pathPrefix || pathRewrite);
39
+
40
+ return {
41
+ target,
42
+ ...DEFAULT_PROXY_OPTIONS,
43
+ ...overrides,
44
+ ...(shouldRewrite ? { rewrite: (path: string) => rewritePath(stripPathPrefix(path, pathPrefix), pathRewrite) } : {})
45
+ };
46
+ }
47
+
48
+ function stripPathPrefix(path: string, pathPrefix: string): string {
49
+ if (!pathPrefix || !path.startsWith(`${pathPrefix}/`)) return path;
50
+ return path.slice(pathPrefix.length) || '/';
51
+ }
52
+
53
+ function rewritePath(path: string, pathRewrite?: Record<string, string>): string {
54
+ if (!pathRewrite) return path;
55
+
56
+ for (const [from, to] of Object.entries(pathRewrite)) {
57
+ const rewrittenPath = path.replace(new RegExp(from), to);
58
+ if (rewrittenPath !== path) return rewrittenPath;
59
+ }
60
+
61
+ return path;
62
+ }
@@ -0,0 +1,27 @@
1
+ const PATH_PREFIX_PATTERN = /^[A-Za-z0-9_/-]+$/u;
2
+ const RESERVED_PATH_PREFIXES = new Set(['/api', '/assets', '/healthcheck']);
3
+
4
+ export function normalizePathPrefix(value: string | undefined): string {
5
+ const normalized = value?.trim().replace(/^\/+|\/+$/gu, '') ?? '';
6
+
7
+ if (!normalized) {
8
+ return '';
9
+ }
10
+
11
+ if (!PATH_PREFIX_PATTERN.test(normalized) || normalized.includes('//') || normalized.split('/').includes('..')) {
12
+ throw new Error(`Invalid PATH_PREFIX: ${value}`);
13
+ }
14
+
15
+ const pathPrefix = `/${normalized}`;
16
+
17
+ if (RESERVED_PATH_PREFIXES.has(pathPrefix)) {
18
+ throw new Error(`PATH_PREFIX is reserved by the service runtime: ${pathPrefix}`);
19
+ }
20
+
21
+ return pathPrefix;
22
+ }
23
+
24
+ export function pathPrefixBase(value: string | undefined): string {
25
+ const pathPrefix = normalizePathPrefix(value);
26
+ return pathPrefix ? `${pathPrefix}/` : '/';
27
+ }
@@ -0,0 +1,27 @@
1
+ import type { Plugin } from 'vite';
2
+ import { buildRuntimeConfigFromEnv } from './runtime-config';
3
+ import { normalizePathPrefix } from './path-prefix';
4
+
5
+ export function runtimeConfigDevPlugin(env: Record<string, string>): Plugin {
6
+ const pathPrefix = normalizePathPrefix(env.PATH_PREFIX);
7
+ const runtimeConfigPath = `${pathPrefix}/runtime-config.js`;
8
+
9
+ return {
10
+ name: 'runtime-config-dev',
11
+ configureServer(server) {
12
+ server.middlewares.use((request, response, next) => {
13
+ const requestPath = request.url?.split('?', 1)[0];
14
+
15
+ if (requestPath !== runtimeConfigPath) {
16
+ next();
17
+ return;
18
+ }
19
+
20
+ const runtimeConfig = buildRuntimeConfigFromEnv(env);
21
+ response.setHeader('Content-Type', 'application/javascript; charset=utf-8');
22
+ response.setHeader('Cache-Control', 'no-store');
23
+ response.end(`window.__APP_CONFIG__ = ${JSON.stringify(runtimeConfig)};`);
24
+ });
25
+ }
26
+ };
27
+ }
@@ -0,0 +1,10 @@
1
+ import type { RuntimeConfig } from '../../app/runtime-config.types';
2
+ import { normalizePathPrefix } from './path-prefix';
3
+
4
+ export function buildRuntimeConfigFromEnv(env: Record<string, string>): RuntimeConfig {
5
+ return {
6
+ PATH_PREFIX: normalizePathPrefix(env.PATH_PREFIX),
7
+ API_BASE_URL: env.API_BASE_URL?.trim() ?? '',
8
+ LOG_LEVEL: env.LOG_LEVEL?.trim() || 'info'
9
+ };
10
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "noUnusedLocals": true,
9
+ "noUnusedParameters": true,
10
+ "noImplicitAny": true,
11
+ "useDefineForClassFields": false,
12
+ "skipLibCheck": true,
13
+ "resolveJsonModule": true,
14
+ "allowJs": true,
15
+ "checkJs": false,
16
+ "noEmit": true
17
+ },
18
+ "include": [
19
+ "app/**/*.js",
20
+ "app/**/*.ts",
21
+ "tests/**/*.js",
22
+ "tests/**/*.ts",
23
+ "tools/**/*.js",
24
+ "tools/**/*.ts"
25
+ ],
26
+ "exclude": ["node_modules", "dist"]
27
+ }
@@ -0,0 +1,40 @@
1
+ import { defineConfig, loadEnv } from 'vite';
2
+ import proxyRules from './proxy.config.json';
3
+ import { buildProxy } from './tools/vite/build-proxy';
4
+ import { runtimeConfigDevPlugin } from './tools/vite/runtime-config-dev-plugin';
5
+ import { pathPrefixBase } from './tools/vite/path-prefix';
6
+
7
+ const DEPLOY_PATH_PLACEHOLDER = '/__PATH_PREFIX__/';
8
+
9
+ export function buildBase(
10
+ command: 'build' | 'serve',
11
+ mode: string,
12
+ pathPrefix: string | undefined
13
+ ): string {
14
+ const isContainerBuild = command === 'build' && mode !== 'preview';
15
+
16
+ return isContainerBuild ? DEPLOY_PATH_PLACEHOLDER : pathPrefixBase(pathPrefix);
17
+ }
18
+
19
+ export default defineConfig(({ command, mode }) => {
20
+ const env = loadEnv(mode, '.', '');
21
+
22
+ return {
23
+ base: buildBase(command, mode, env.PATH_PREFIX),
24
+ plugins: [runtimeConfigDevPlugin(env)],
25
+ build: {
26
+ outDir: 'dist',
27
+ emptyOutDir: true
28
+ },
29
+ server: {
30
+ port: 3000,
31
+ strictPort: true,
32
+ open: false,
33
+ proxy: buildProxy(env, proxyRules)
34
+ },
35
+ preview: {
36
+ port: 3000,
37
+ strictPort: true
38
+ }
39
+ };
40
+ });
@@ -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.100'
3
3
  end