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.
- checksums.yaml +4 -4
- data/lib/stack-service-base/command_init.rb +8 -6
- data/lib/stack-service-base/examples/mcp_config.ru +16 -16
- data/lib/stack-service-base/frontend_template/github/.github/workflows/main.yml +19 -0
- data/lib/stack-service-base/frontend_template/github/docker/docker-compose.yml +15 -0
- data/lib/stack-service-base/frontend_template/gitlab/.gitlab-ci.yml +48 -0
- data/lib/stack-service-base/frontend_template/gitlab/docker/Dockerfile.build +36 -0
- data/lib/stack-service-base/frontend_template/gitlab/docker/docker-compose.yml +15 -0
- data/lib/stack-service-base/frontend_template/gitlab/docker/local_build.sh +10 -0
- data/lib/stack-service-base/frontend_template/gitlab-c/.gitlab-ci.yml +24 -0
- data/lib/stack-service-base/frontend_template/gitlab-c/docker/Dockerfile.build +17 -0
- data/lib/stack-service-base/frontend_template/gitlab-c/docker/docker-compose.yml +15 -0
- data/lib/stack-service-base/frontend_template/gitlab-c/docker/local_build.sh +11 -0
- data/lib/stack-service-base/frontend_template/home/.editorconfig +12 -0
- data/lib/stack-service-base/frontend_template/home/.gitattributes +4 -0
- data/lib/stack-service-base/frontend_template/home/.gitignore +17 -0
- data/lib/stack-service-base/frontend_template/home/AGENTS.md +3 -0
- data/lib/stack-service-base/frontend_template/home/README.md +68 -0
- data/lib/stack-service-base/frontend_template/home/docker/.env +3 -0
- data/lib/stack-service-base/frontend_template/home/docker/frontend/15-runtime-settings.envsh +90 -0
- data/lib/stack-service-base/frontend_template/home/docker/frontend/40-path-prefix-settings.sh +30 -0
- data/lib/stack-service-base/frontend_template/home/docker/frontend/45-runtime-config-settings.sh +24 -0
- data/lib/stack-service-base/frontend_template/home/docker/frontend/Dockerfile +46 -0
- data/lib/stack-service-base/frontend_template/home/docker/frontend/nginx.conf.template +75 -0
- data/lib/stack-service-base/frontend_template/home/src/.dockerignore +9 -0
- data/lib/stack-service-base/frontend_template/home/src/.env.example +7 -0
- data/lib/stack-service-base/frontend_template/home/src/.version +1 -0
- data/lib/stack-service-base/frontend_template/home/src/app/main.ts +4 -0
- data/lib/stack-service-base/frontend_template/home/src/app/runtime-config.ts +22 -0
- data/lib/stack-service-base/frontend_template/home/src/app/runtime-config.types.ts +5 -0
- data/lib/stack-service-base/frontend_template/home/src/app/style.css +82 -0
- data/lib/stack-service-base/frontend_template/home/src/app/vite-env.d.ts +1 -0
- data/lib/stack-service-base/frontend_template/home/src/eslint.config.js +34 -0
- data/lib/stack-service-base/frontend_template/home/src/index.html +24 -0
- data/lib/stack-service-base/frontend_template/home/src/package-lock.json +2812 -0
- data/lib/stack-service-base/frontend_template/home/src/package.json +30 -0
- data/lib/stack-service-base/frontend_template/home/src/proxy.config.json +8 -0
- data/lib/stack-service-base/frontend_template/home/src/public/runtime-config.js +1 -0
- data/lib/stack-service-base/frontend_template/home/src/tests/build-proxy.test.ts +28 -0
- data/lib/stack-service-base/frontend_template/home/src/tests/javascript-support.test.js +23 -0
- data/lib/stack-service-base/frontend_template/home/src/tests/path-prefix.test.ts +17 -0
- data/lib/stack-service-base/frontend_template/home/src/tests/runtime-config.test.ts +22 -0
- data/lib/stack-service-base/frontend_template/home/src/tests/vite-config.test.ts +16 -0
- data/lib/stack-service-base/frontend_template/home/src/tools/vite/build-proxy.ts +62 -0
- data/lib/stack-service-base/frontend_template/home/src/tools/vite/path-prefix.ts +27 -0
- data/lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config-dev-plugin.ts +27 -0
- data/lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config.ts +10 -0
- data/lib/stack-service-base/frontend_template/home/src/tsconfig.json +27 -0
- data/lib/stack-service-base/frontend_template/home/src/vite.config.ts +40 -0
- 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 +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 @@
|
|
|
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
|
+
}
|
data/lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config-dev-plugin.ts
ADDED
|
@@ -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
|
+
});
|
|
@@ -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
|