init project
This commit is contained in:
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerBuildImageTool(server) {
|
||||
server.registerTool('docker_build_image', {
|
||||
title: 'Docker Build Image',
|
||||
description: 'Build a Docker image from a Dockerfile in the workspace. Streams the build output back and returns the resulting image tag.',
|
||||
inputSchema: {
|
||||
tag: z.string().min(1).describe('Image tag, e.g. "my-app:latest".'),
|
||||
dockerfile: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('Dockerfile')
|
||||
.describe('Path to the Dockerfile, relative to the workspace root.'),
|
||||
buildContext: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('.')
|
||||
.describe('Build context directory, relative to the workspace root.'),
|
||||
},
|
||||
}, async ({ tag, dockerfile, buildContext }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
const dockerfilePath = resolveWorkspacePath(config.workspaceRoot, dockerfile);
|
||||
const contextPath = resolveWorkspacePath(config.workspaceRoot, buildContext);
|
||||
if (!fs.existsSync(dockerfilePath)) {
|
||||
return errorResult(`Dockerfile not found: ${dockerfilePath}`);
|
||||
}
|
||||
const stream = await dockerManager.getDocker().buildImage({
|
||||
context: contextPath,
|
||||
src: [path.basename(dockerfilePath), ...listContextFiles(contextPath)],
|
||||
}, { t: tag, dockerfile: path.basename(dockerfilePath) });
|
||||
const output = [];
|
||||
await new Promise((resolve, reject) => {
|
||||
dockerManager
|
||||
.getDocker()
|
||||
.modem.followProgress(stream, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const last = res?.[res.length - 1];
|
||||
if (last && 'error' in last && last.error) {
|
||||
reject(new Error(String(last.error)));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}, (event) => {
|
||||
const line = typeof event === 'string' ? event : JSON.stringify(event);
|
||||
output.push(line);
|
||||
if (output.length > config.maxBufferedLogLines) {
|
||||
output.splice(0, output.length - config.maxBufferedLogLines);
|
||||
}
|
||||
});
|
||||
});
|
||||
return jsonResult({
|
||||
tag,
|
||||
dockerfile: dockerfilePath,
|
||||
context: contextPath,
|
||||
output: output.join('\n').slice(-config.maxToolOutputChars),
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
function listContextFiles(root) {
|
||||
const files = [];
|
||||
const walk = (dir) => {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
const rel = path.relative(root, full);
|
||||
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist')
|
||||
continue;
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
}
|
||||
else {
|
||||
files.push(rel);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(root);
|
||||
return files;
|
||||
}
|
||||
//# sourceMappingURL=buildImage.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"buildImage.js","sourceRoot":"","sources":["../../../src/tools/docker/buildImage.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
/**
|
||||
* Docker Compose is invoked via the local `docker compose` or `docker-compose`
|
||||
* CLI. The server does not parse compose files itself; it delegates to the
|
||||
* official tool and streams the output back, just like workflow tools.
|
||||
*/
|
||||
export function registerDockerComposeTools(server) {
|
||||
server.registerTool('docker_compose_up', {
|
||||
title: 'Docker Compose Up',
|
||||
description: 'Bring up a docker-compose.yml stack in the workspace root. Detached by default so the server does not block.',
|
||||
inputSchema: {
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('docker-compose.yml')
|
||||
.describe('Compose file path, relative to the workspace root.'),
|
||||
services: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Specific services to start. Omit to start all.'),
|
||||
build: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Build images before starting.'),
|
||||
},
|
||||
}, async ({ file, services, build }) => {
|
||||
try {
|
||||
const args = ['compose', '-f', file, 'up', '-d'];
|
||||
if (build)
|
||||
args.push('--build');
|
||||
if (services && services.length > 0)
|
||||
args.push(...services);
|
||||
const result = await runCommand('docker', args, {
|
||||
cwd: config.workspaceRoot,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({ file, ...result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
server.registerTool('docker_compose_down', {
|
||||
title: 'Docker Compose Down',
|
||||
description: 'Shut down a docker-compose.yml stack in the workspace root.',
|
||||
inputSchema: {
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('docker-compose.yml')
|
||||
.describe('Compose file path, relative to the workspace root.'),
|
||||
volumes: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Also remove named volumes declared in the volumes section.'),
|
||||
},
|
||||
}, async ({ file, volumes }) => {
|
||||
try {
|
||||
const args = ['compose', '-f', file, 'down'];
|
||||
if (volumes)
|
||||
args.push('-v');
|
||||
const result = await runCommand('docker', args, {
|
||||
cwd: config.workspaceRoot,
|
||||
timeoutMs: config.defaultCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({ file, ...result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=compose.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../../../src/tools/docker/compose.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerContainerLogsTool(server) {
|
||||
server.registerTool('docker_get_container_logs', {
|
||||
title: 'Docker Get Container Logs',
|
||||
description: 'Fetch logs from a running or stopped Docker container started by this server.',
|
||||
inputSchema: {
|
||||
containerId: z.string().min(1).describe('Container ID (short or long) returned by docker_run_container.'),
|
||||
tail: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(200)
|
||||
.describe('Only return the last N lines.'),
|
||||
follow: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Stream logs (not implemented; kept for API compatibility).'),
|
||||
},
|
||||
}, async ({ containerId, tail, follow }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
if (follow) {
|
||||
return jsonResult({
|
||||
containerId,
|
||||
note: 'Live log streaming is not supported via this tool; use docker_get_container_logs with follow:false to poll.',
|
||||
logs: '',
|
||||
});
|
||||
}
|
||||
const docker = dockerManager.getDocker();
|
||||
const buffer = await docker.getContainer(containerId).logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
tail,
|
||||
});
|
||||
const raw = Buffer.isBuffer(buffer) ? buffer.toString('utf8') : String(buffer);
|
||||
return jsonResult({
|
||||
containerId,
|
||||
logs: raw.slice(-config.maxToolOutputChars),
|
||||
bytes: raw.length,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=containerLogs.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"containerLogs.js","sourceRoot":"","sources":["../../../src/tools/docker/containerLogs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,UAAU,+BAA+B,CAAC,MAAiB;IAC/D,MAAM,CAAC,YAAY,CACjB,2BAA2B,EAC3B;QACE,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,+EAA+E;QACjF,WAAW,EAAE;YACX,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,gEAAgE,CAAC;YACzG,IAAI,EAAE,CAAC;iBACJ,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,QAAQ,EAAE;iBACV,QAAQ,EAAE;iBACV,OAAO,CAAC,GAAG,CAAC;iBACZ,QAAQ,CAAC,+BAA+B,CAAC;YAC5C,MAAM,EAAE,CAAC;iBACN,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,4DAA4D,CAAC;SAC1E;KACF,EACD,KAAK,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE;QACtC,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YACtC,IAAI,MAAM,EAAE,CAAC;gBACX,OAAO,UAAU,CAAC;oBAChB,WAAW;oBACX,IAAI,EAAE,6GAA6G;oBACnH,IAAI,EAAE,EAAE;iBACT,CAAC,CAAC;YACL,CAAC;YAED,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,EAAE,CAAC;YACzC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC;gBACzD,MAAM,EAAE,IAAI;gBACZ,MAAM,EAAE,IAAI;gBACZ,IAAI;aACL,CAAC,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC/E,OAAO,UAAU,CAAC;gBAChB,WAAW;gBACX,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,kBAAkB,CAAC;gBAC3C,KAAK,EAAE,GAAG,CAAC,MAAM;aAClB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
import Docker from 'dockerode';
|
||||
import { config } from '../../config.js';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { logger } from '../../lib/logger.js';
|
||||
class DockerManager {
|
||||
docker;
|
||||
containers = new Map();
|
||||
daemonReachable = null;
|
||||
constructor() {
|
||||
this.docker = new Docker();
|
||||
}
|
||||
async ensureReachable() {
|
||||
if (this.daemonReachable === true)
|
||||
return;
|
||||
try {
|
||||
await this.docker.ping();
|
||||
this.daemonReachable = true;
|
||||
}
|
||||
catch (error) {
|
||||
this.daemonReachable = false;
|
||||
throw new Error('Docker daemon is not reachable. Make sure Docker is running and the current user can access it. ' +
|
||||
`Original error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
getDocker() {
|
||||
return this.docker;
|
||||
}
|
||||
track(containerId, image, name, ports) {
|
||||
if (this.containers.size >= config.maxConcurrentContainers) {
|
||||
throw new Error(`Refusing to start another container: already at the max of ${config.maxConcurrentContainers} concurrent containers.`);
|
||||
}
|
||||
const shortId = containerId.substring(0, 12);
|
||||
const managed = {
|
||||
id: shortId,
|
||||
image,
|
||||
name,
|
||||
startedAt: new Date().toISOString(),
|
||||
ports,
|
||||
};
|
||||
this.containers.set(shortId, managed);
|
||||
return managed;
|
||||
}
|
||||
getTracked(id) {
|
||||
return this.containers.get(id) ?? this.containers.get(id.substring(0, 12));
|
||||
}
|
||||
listTracked() {
|
||||
return Array.from(this.containers.values());
|
||||
}
|
||||
untrack(id) {
|
||||
this.containers.delete(id.substring(0, 12));
|
||||
}
|
||||
async cleanupAll() {
|
||||
for (const [id, managed] of this.containers.entries()) {
|
||||
try {
|
||||
const container = this.docker.getContainer(id);
|
||||
await container.stop({ t: 5 }).catch(() => null);
|
||||
await container.remove({ force: true }).catch(() => null);
|
||||
}
|
||||
catch (error) {
|
||||
logger.error(`failed to cleanup container ${managed.name} (${id})`, {
|
||||
error: error instanceof Error ? error.message : error,
|
||||
});
|
||||
}
|
||||
finally {
|
||||
this.containers.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export const dockerManager = new DockerManager();
|
||||
/**
|
||||
* Validates that a volume mount specification keeps the source path inside the
|
||||
* workspace root. Rejects Docker socket mounts, host-relative escapes, etc.
|
||||
*/
|
||||
export function validateVolumeMount(spec) {
|
||||
const parts = spec.split(':');
|
||||
if (parts.length < 2) {
|
||||
throw new Error(`Invalid volume mount "${spec}": expected format "source:target[:mode]".`);
|
||||
}
|
||||
const source = parts[0] ?? '';
|
||||
const target = parts[1] ?? '';
|
||||
const mode = parts[2] ?? 'rw';
|
||||
if (!source || !target) {
|
||||
throw new Error(`Invalid volume mount "${spec}": source and target are required.`);
|
||||
}
|
||||
const absoluteSource = resolveWorkspacePath(config.workspaceRoot, source);
|
||||
if (absoluteSource.toLowerCase().startsWith('\\\\.\\pipe\\docker_engine') || source.includes('/var/run/docker.sock')) {
|
||||
throw new Error(`Refusing to mount the Docker socket: "${source}".`);
|
||||
}
|
||||
return { source: absoluteSource, target, mode };
|
||||
}
|
||||
export function isAllowListedImage(image) {
|
||||
const base = (image.split(':')[0] ?? '').split('/').pop()?.toLowerCase() ?? '';
|
||||
return config.dockerAllowedBaseImages.some((allowed) => base === allowed || base.startsWith(allowed));
|
||||
}
|
||||
export function isLocallyBuiltImage(image) {
|
||||
// Heuristic: an image built by this server via docker_build_image won't have
|
||||
// a registry prefix. We treat "my-app" or "web-dev-mcp" as local; anything
|
||||
// with a registry host (contains '.' or '/') is not.
|
||||
return !/[/.]/.test(image);
|
||||
}
|
||||
//# sourceMappingURL=dockerManager.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"dockerManager.js","sourceRoot":"","sources":["../../../src/tools/docker/dockerManager.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerExecTool(server) {
|
||||
server.registerTool('docker_exec', {
|
||||
title: 'Docker Exec',
|
||||
description: 'Run a one-off command inside a running Docker container started by this server and return stdout/stderr.',
|
||||
inputSchema: {
|
||||
containerId: z.string().min(1).describe('Container ID (short or long) returned by docker_run_container.'),
|
||||
command: z.array(z.string()).min(1).describe('Command and arguments to execute inside the container.'),
|
||||
workingDir: z.string().optional().describe('Working directory inside the container.'),
|
||||
env: z.record(z.string()).optional().describe('Additional environment variables for the exec.'),
|
||||
},
|
||||
}, async ({ containerId, command, workingDir, env }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
const docker = dockerManager.getDocker();
|
||||
const exec = await docker.getContainer(containerId).exec({
|
||||
Cmd: command,
|
||||
WorkingDir: workingDir,
|
||||
Env: env ? Object.entries(env).map(([k, v]) => `${k}=${v}`) : undefined,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
const stream = await exec.start({ Detach: false, Tty: false });
|
||||
const chunks = [];
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.on('data', (chunk) => chunks.push(chunk));
|
||||
stream.on('end', resolve);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
const raw = Buffer.concat(chunks).toString('utf8');
|
||||
const result = await exec.inspect();
|
||||
return jsonResult({
|
||||
containerId,
|
||||
command,
|
||||
exitCode: result.ExitCode ?? null,
|
||||
output: raw.slice(-config.maxToolOutputChars),
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=exec.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../../src/tools/docker/exec.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+232
@@ -0,0 +1,232 @@
|
||||
import fs from 'node:fs';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { detectProject, resolvePackage } from '../../lib/frameworks/detect.js';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { requireLaravel } from '../laravel/requireLaravel.js';
|
||||
import { errorResult, jsonResult, packageSchema, toErrorMessage } from '../shared.js';
|
||||
function nodeDockerfile() {
|
||||
return `# Generated by web-dev-mcp
|
||||
FROM node:24-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
COPY --from=builder /app/dist ./dist
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/index.js"]
|
||||
`;
|
||||
}
|
||||
function phpDockerfile(kind) {
|
||||
const publicDir = kind === 'laravel' ? 'public' : 'public';
|
||||
return `# Generated by web-dev-mcp for ${kind}
|
||||
FROM php:8.3-cli-alpine AS vendor
|
||||
WORKDIR /app
|
||||
COPY composer.json composer.lock* ./
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \\
|
||||
&& composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist || composer install --no-interaction --prefer-dist
|
||||
|
||||
FROM php:8.3-apache
|
||||
WORKDIR /var/www/html
|
||||
RUN docker-php-ext-install pdo pdo_mysql
|
||||
COPY --from=vendor /app/vendor ./vendor
|
||||
COPY . .
|
||||
ENV APACHE_DOCUMENT_ROOT=/var/www/html/${publicDir}
|
||||
RUN sed -ri -e 's!/var/www/html!\\\${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \\
|
||||
&& a2enmod rewrite
|
||||
EXPOSE 80
|
||||
`;
|
||||
}
|
||||
function nginxNodeCompose() {
|
||||
return `# Generated by web-dev-mcp
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
depends_on:
|
||||
- redis
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: app
|
||||
POSTGRES_PASSWORD: app
|
||||
POSTGRES_DB: app
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
db_data:
|
||||
`;
|
||||
}
|
||||
function phpCompose() {
|
||||
return `# Generated by web-dev-mcp
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
APP_ENV: local
|
||||
DB_HOST: db
|
||||
DB_DATABASE: app
|
||||
DB_USERNAME: app
|
||||
DB_PASSWORD: app
|
||||
REDIS_HOST: redis
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
db:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_DATABASE: app
|
||||
MYSQL_USER: app
|
||||
MYSQL_PASSWORD: app
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
db_data:
|
||||
`;
|
||||
}
|
||||
export function registerGenerateDockerfileTool(server) {
|
||||
server.registerTool('generate_dockerfile', {
|
||||
title: 'Generate Dockerfile',
|
||||
description: 'Write a stack-aware Dockerfile at the workspace (or package) root based on detected framework (Node multi-stage or PHP/Apache for Laravel/CodeIgniter).',
|
||||
inputSchema: {
|
||||
package: packageSchema,
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
},
|
||||
}, async ({ package: packageName, overwrite }) => {
|
||||
try {
|
||||
const root = packageName
|
||||
? resolvePackage(config.workspaceRoot, packageName).pkg.absolutePath
|
||||
: config.workspaceRoot;
|
||||
const detected = detectProject(root);
|
||||
const file = resolveWorkspacePath(root, 'Dockerfile');
|
||||
if (!overwrite && fs.existsSync(file)) {
|
||||
return errorResult(`"${file}" already exists. Pass overwrite: true to replace it.`);
|
||||
}
|
||||
let contents;
|
||||
let stack;
|
||||
if (detected.backend?.kind === 'laravel' ||
|
||||
detected.backend?.kind === 'codeigniter' ||
|
||||
detected.backend?.kind === 'symfony') {
|
||||
stack = detected.backend.kind;
|
||||
contents = phpDockerfile(detected.backend.kind === 'codeigniter' ? 'codeigniter' : 'laravel');
|
||||
}
|
||||
else {
|
||||
const front = detected.frontend?.kind;
|
||||
stack =
|
||||
front === 'react' || front === 'vue' || front === 'nuxt'
|
||||
? `${front}/node`
|
||||
: detected.backend?.kind === 'django' || detected.backend?.kind === 'rails'
|
||||
? detected.backend.kind
|
||||
: 'node';
|
||||
contents = nodeDockerfile();
|
||||
}
|
||||
fs.writeFileSync(file, contents);
|
||||
return jsonResult({ file, stack, package: packageName ?? null });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGenerateComposeTool(server) {
|
||||
server.registerTool('generate_compose', {
|
||||
title: 'Generate docker-compose.yml',
|
||||
description: 'Write a docker-compose.yml with app + db + redis presets inferred from the detected stack (Postgres for Node, MySQL for PHP).',
|
||||
inputSchema: {
|
||||
package: packageSchema,
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
},
|
||||
}, async ({ package: packageName, overwrite }) => {
|
||||
try {
|
||||
const root = packageName
|
||||
? resolvePackage(config.workspaceRoot, packageName).pkg.absolutePath
|
||||
: config.workspaceRoot;
|
||||
const detected = detectProject(root);
|
||||
const file = resolveWorkspacePath(root, 'docker-compose.yml');
|
||||
if (!overwrite && fs.existsSync(file)) {
|
||||
return errorResult(`"${file}" already exists. Pass overwrite: true to replace it.`);
|
||||
}
|
||||
const isPhp = detected.backend?.kind === 'laravel' ||
|
||||
detected.backend?.kind === 'codeigniter' ||
|
||||
detected.backend?.kind === 'symfony';
|
||||
const contents = isPhp ? phpCompose() : nginxNodeCompose();
|
||||
fs.writeFileSync(file, contents);
|
||||
return jsonResult({
|
||||
file,
|
||||
stack: isPhp
|
||||
? detected.backend?.kind
|
||||
: (detected.frontend?.kind ?? detected.backend?.kind ?? 'node'),
|
||||
services: ['app', 'db', 'redis'],
|
||||
package: packageName ?? null,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGenerateSailTool(server) {
|
||||
server.registerTool('generate_sail', {
|
||||
title: 'Install Laravel Sail',
|
||||
description: 'Run `php artisan sail:install` in a detected Laravel project. Requires confirm: true because it modifies docker-compose and related files.',
|
||||
inputSchema: {
|
||||
with: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Optional Sail services, e.g. ["mysql", "redis", "mailpit"].'),
|
||||
confirm: z.boolean().optional().describe('Required as true to run sail:install.'),
|
||||
},
|
||||
}, async ({ with: services, confirm }) => {
|
||||
try {
|
||||
if (confirm !== true) {
|
||||
return errorResult('generate_sail requires confirm: true.');
|
||||
}
|
||||
const laravel = requireLaravel();
|
||||
const args = ['artisan', 'sail:install', '--no-interaction'];
|
||||
if (services && services.length > 0) {
|
||||
args.push(`--with=${services.join(',')}`);
|
||||
}
|
||||
const result = await runCommand('php', args, {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({
|
||||
services: services ?? null,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
cwd: result.cwd,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=generate.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
import { registerDockerBuildImageTool } from './buildImage.js';
|
||||
import { registerDockerRunContainerTool } from './runContainer.js';
|
||||
import { registerDockerListContainersTool } from './listContainers.js';
|
||||
import { registerDockerContainerLogsTool } from './containerLogs.js';
|
||||
import { registerDockerExecTool } from './exec.js';
|
||||
import { registerDockerStopRemoveTools } from './stopRemove.js';
|
||||
import { registerDockerComposeTools } from './compose.js';
|
||||
import { registerGenerateComposeTool, registerGenerateDockerfileTool, registerGenerateSailTool, } from './generate.js';
|
||||
import { registerDockerPushImageTool } from './pushImage.js';
|
||||
export function registerDockerTools(server) {
|
||||
registerDockerBuildImageTool(server);
|
||||
registerDockerRunContainerTool(server);
|
||||
registerDockerListContainersTool(server);
|
||||
registerDockerContainerLogsTool(server);
|
||||
registerDockerExecTool(server);
|
||||
registerDockerStopRemoveTools(server);
|
||||
registerDockerComposeTools(server);
|
||||
registerGenerateDockerfileTool(server);
|
||||
registerGenerateComposeTool(server);
|
||||
registerGenerateSailTool(server);
|
||||
registerDockerPushImageTool(server);
|
||||
}
|
||||
export { dockerManager } from './dockerManager.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/docker/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,+BAA+B,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,EAC9B,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAE7D,MAAM,UAAU,mBAAmB,CAAC,MAAiB;IACnD,4BAA4B,CAAC,MAAM,CAAC,CAAC;IACrC,8BAA8B,CAAC,MAAM,CAAC,CAAC;IACvC,gCAAgC,CAAC,MAAM,CAAC,CAAC;IACzC,+BAA+B,CAAC,MAAM,CAAC,CAAC;IACxC,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACtC,0BAA0B,CAAC,MAAM,CAAC,CAAC;IACnC,8BAA8B,CAAC,MAAM,CAAC,CAAC;IACvC,2BAA2B,CAAC,MAAM,CAAC,CAAC;IACpC,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjC,2BAA2B,CAAC,MAAM,CAAC,CAAC;AACtC,CAAC;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerListContainersTool(server) {
|
||||
server.registerTool('docker_list_containers', {
|
||||
title: 'Docker List Containers',
|
||||
description: 'List Docker containers started by this server with their current status and port mappings.',
|
||||
inputSchema: {
|
||||
includeDockerStatus: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Also query the Docker daemon for live status of each tracked container.'),
|
||||
},
|
||||
}, async ({ includeDockerStatus }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
const tracked = dockerManager.listTracked();
|
||||
if (!includeDockerStatus) {
|
||||
return jsonResult({ containers: tracked });
|
||||
}
|
||||
const docker = dockerManager.getDocker();
|
||||
const enriched = await Promise.all(tracked.map(async (c) => {
|
||||
try {
|
||||
const info = await docker.getContainer(c.id).inspect();
|
||||
return {
|
||||
...c,
|
||||
state: info.State?.Status ?? 'unknown',
|
||||
running: info.State?.Running ?? false,
|
||||
exitCode: info.State?.ExitCode ?? null,
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return { ...c, state: 'unknown', running: false, exitCode: null };
|
||||
}
|
||||
}));
|
||||
return jsonResult({ containers: enriched });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=listContainers.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"listContainers.js","sourceRoot":"","sources":["../../../src/tools/docker/listContainers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,UAAU,gCAAgC,CAAC,MAAiB;IAChE,MAAM,CAAC,YAAY,CACjB,wBAAwB,EACxB;QACE,KAAK,EAAE,wBAAwB;QAC/B,WAAW,EAAE,4FAA4F;QACzG,WAAW,EAAE;YACX,mBAAmB,EAAE,CAAC;iBACnB,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,yEAAyE,CAAC;SACvF;KACF,EACD,KAAK,EAAE,EAAE,mBAAmB,EAAE,EAAE,EAAE;QAChC,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YACtC,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,EAAE,CAAC;YAC5C,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBACzB,OAAO,UAAU,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;YAC7C,CAAC;YAED,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,EAAE,CAAC;YACzC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAChC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;gBACtB,IAAI,CAAC;oBACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;oBACvD,OAAO;wBACL,GAAG,CAAC;wBACJ,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,IAAI,SAAS;wBACtC,OAAO,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,IAAI,KAAK;wBACrC,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,IAAI,IAAI;qBACvC,CAAC;gBACJ,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;gBACpE,CAAC;YACH,CAAC,CAAC,CACH,CAAC;YACF,OAAO,UAAU,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerPushImageTool(server) {
|
||||
server.registerTool('docker_push_image', {
|
||||
title: 'Docker Push Image',
|
||||
description: 'Push a locally built or allow-listed Docker image to a registry (`docker push`). Requires confirm: true. ' +
|
||||
'Credentials must already be available via the Docker credential store / environment — never pass secrets as tool args.',
|
||||
inputSchema: {
|
||||
image: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Image reference to push, e.g. "ghcr.io/org/app:latest" or "my-app:1.0".'),
|
||||
confirm: z.boolean().optional().describe('Required as true to push an image.'),
|
||||
},
|
||||
}, async ({ image, confirm }) => {
|
||||
try {
|
||||
if (confirm !== true) {
|
||||
return errorResult('docker_push_image requires confirm: true.');
|
||||
}
|
||||
if (image.includes('--force') || /\s/.test(image)) {
|
||||
return errorResult('Invalid image reference.');
|
||||
}
|
||||
await dockerManager.ensureReachable();
|
||||
const result = await runCommand('docker', ['push', image], {
|
||||
cwd: config.workspaceRoot,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({
|
||||
image,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
timedOut: result.timedOut,
|
||||
output: result.output,
|
||||
note: 'Auth must come from `docker login` / credential helpers / env — not from this tool.',
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=pushImage.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"pushImage.js","sourceRoot":"","sources":["../../../src/tools/docker/pushImage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,UAAU,2BAA2B,CAAC,MAAiB;IAC3D,MAAM,CAAC,YAAY,CACjB,mBAAmB,EACnB;QACE,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,2GAA2G;YAC3G,wHAAwH;QAC1H,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,yEAAyE,CAAC;YACtF,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;SAC/E;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,IAAI,CAAC;YACH,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,OAAO,WAAW,CAAC,2CAA2C,CAAC,CAAC;YAClE,CAAC;YACD,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAClD,OAAO,WAAW,CAAC,0BAA0B,CAAC,CAAC;YACjD,CAAC;YAED,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YAEtC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE;gBACzD,GAAG,EAAE,MAAM,CAAC,aAAa;gBACzB,SAAS,EAAE,MAAM,CAAC,wBAAwB;aAC3C,CAAC,CAAC;YAEH,OAAO,UAAU,CAAC;gBAChB,KAAK;gBACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,IAAI,EAAE,qFAAqF;aAC5F,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager, isAllowListedImage, isLocallyBuiltImage, validateVolumeMount } from './dockerManager.js';
|
||||
export function registerDockerRunContainerTool(server) {
|
||||
server.registerTool('docker_run_container', {
|
||||
title: 'Docker Run Container',
|
||||
description: 'Run a Docker container from an image with port mappings, env vars, and workspace-confined volume mounts. ' +
|
||||
'Locally built images are allowed; non-local images must be on the allow-list unless confirm: true is passed.',
|
||||
inputSchema: {
|
||||
image: z.string().min(1).describe('Docker image to run, e.g. "my-app:latest" or "nginx".'),
|
||||
name: z.string().optional().describe('Optional container name.'),
|
||||
ports: z
|
||||
.record(z.number().int().positive())
|
||||
.optional()
|
||||
.describe('Port mapping { hostPort: containerPort }.'),
|
||||
env: z.record(z.string()).optional().describe('Environment variables.'),
|
||||
volumes: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Volume mounts in "source:target[:mode]" format. Source paths are confined to WORKSPACE_ROOT.'),
|
||||
command: z.array(z.string()).optional().describe('Override the container command.'),
|
||||
memory: z.string().optional().describe('Memory limit, e.g. "512m". Defaults to 512m.'),
|
||||
cpus: z.string().optional().describe('CPU limit, e.g. "1.0". Defaults to 1.0.'),
|
||||
confirm: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Required as true to run an image not on the allow-list.'),
|
||||
},
|
||||
}, async ({ image, name, ports, env, volumes, command, memory, cpus, confirm }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
if (!isLocallyBuiltImage(image) && !isAllowListedImage(image) && confirm !== true) {
|
||||
return errorResult(`Image "${image}" is not locally built and not on the allow-list (${config.dockerAllowedBaseImages.join(', ')}). ` +
|
||||
'Pass confirm: true to run it anyway.');
|
||||
}
|
||||
const portBindings = {};
|
||||
const exposedPorts = {};
|
||||
const portMap = {};
|
||||
if (ports) {
|
||||
for (const [hostPort, containerPort] of Object.entries(ports)) {
|
||||
const key = `${containerPort}/tcp`;
|
||||
portBindings[key] = [{ HostPort: hostPort }];
|
||||
exposedPorts[key] = {};
|
||||
portMap[Number(hostPort)] = containerPort;
|
||||
}
|
||||
}
|
||||
const binds = [];
|
||||
if (volumes) {
|
||||
for (const spec of volumes) {
|
||||
const mount = validateVolumeMount(spec);
|
||||
binds.push(`${mount.source}:${mount.target}:${mount.mode}`);
|
||||
}
|
||||
}
|
||||
const createOptions = {
|
||||
Image: image,
|
||||
name,
|
||||
Cmd: command,
|
||||
Env: env ? Object.entries(env).map(([k, v]) => `${k}=${v}`) : undefined,
|
||||
ExposedPorts: Object.keys(exposedPorts).length > 0 ? exposedPorts : undefined,
|
||||
HostConfig: {
|
||||
PortBindings: Object.keys(portBindings).length > 0 ? portBindings : undefined,
|
||||
Binds: binds.length > 0 ? binds : undefined,
|
||||
Memory: parseMemory(memory ?? config.defaultContainerMemory),
|
||||
NanoCpus: parseCpus(cpus ?? config.defaultContainerCpus),
|
||||
// Security guardrails:
|
||||
Privileged: false,
|
||||
NetworkMode: 'bridge',
|
||||
PidMode: undefined,
|
||||
IpcMode: undefined,
|
||||
},
|
||||
};
|
||||
const docker = dockerManager.getDocker();
|
||||
const container = await docker.createContainer(createOptions);
|
||||
await container.start();
|
||||
const info = await container.inspect();
|
||||
const managed = dockerManager.track(container.id, image, info.Name?.replace(/^\//, '') ?? container.id, portMap);
|
||||
return jsonResult({ ...managed, shortId: container.id.substring(0, 12) });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
function parseMemory(value) {
|
||||
const match = value.match(/^([0-9.]+)([kmgt]?b?)$/i);
|
||||
if (!match || !match[1])
|
||||
return 512 * 1024 * 1024;
|
||||
const num = Number.parseFloat(match[1]);
|
||||
const unit = match[2]?.toLowerCase() ?? '';
|
||||
switch (unit) {
|
||||
case 'k':
|
||||
case 'kb':
|
||||
return num * 1024;
|
||||
case 'm':
|
||||
case 'mb':
|
||||
return num * 1024 * 1024;
|
||||
case 'g':
|
||||
case 'gb':
|
||||
return num * 1024 * 1024 * 1024;
|
||||
case 't':
|
||||
case 'tb':
|
||||
return num * 1024 * 1024 * 1024 * 1024;
|
||||
default:
|
||||
return num;
|
||||
}
|
||||
}
|
||||
function parseCpus(value) {
|
||||
const num = Number.parseFloat(value);
|
||||
return Number.isNaN(num) ? 1_000_000_000 : Math.round(num * 1_000_000_000);
|
||||
}
|
||||
//# sourceMappingURL=runContainer.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"runContainer.js","sourceRoot":"","sources":["../../../src/tools/docker/runContainer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEjH,MAAM,UAAU,8BAA8B,CAAC,MAAiB;IAC9D,MAAM,CAAC,YAAY,CACjB,sBAAsB,EACtB;QACE,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EACT,2GAA2G;YAC3G,8GAA8G;QAChH,WAAW,EAAE;YACX,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,uDAAuD,CAAC;YAC1F,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0BAA0B,CAAC;YAChE,KAAK,EAAE,CAAC;iBACL,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;iBACnC,QAAQ,EAAE;iBACV,QAAQ,CAAC,2CAA2C,CAAC;YACxD,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wBAAwB,CAAC;YACvE,OAAO,EAAE,CAAC;iBACP,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;iBACjB,QAAQ,EAAE;iBACV,QAAQ,CAAC,8FAA8F,CAAC;YAC3G,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iCAAiC,CAAC;YACnF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8CAA8C,CAAC;YACtF,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;YAC/E,OAAO,EAAE,CAAC;iBACP,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,QAAQ,CAAC,yDAAyD,CAAC;SACvE;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;QAC7E,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YAEtC,IAAI,CAAC,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBAClF,OAAO,WAAW,CAChB,UAAU,KAAK,qDAAqD,MAAM,CAAC,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;oBAChH,sCAAsC,CACzC,CAAC;YACJ,CAAC;YAED,MAAM,YAAY,GAAgD,EAAE,CAAC;YACrE,MAAM,YAAY,GAAuB,EAAE,CAAC;YAC5C,MAAM,OAAO,GAA2B,EAAE,CAAC;YAC3C,IAAI,KAAK,EAAE,CAAC;gBACV,KAAK,MAAM,CAAC,QAAQ,EAAE,aAAa,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC9D,MAAM,GAAG,GAAG,GAAG,aAAa,MAAM,CAAC;oBACnC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;oBAC7C,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;oBACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,aAAa,CAAC;gBAC5C,CAAC;YACH,CAAC;YAED,MAAM,KAAK,GAAa,EAAE,CAAC;YAC3B,IAAI,OAAO,EAAE,CAAC;gBACZ,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;oBAC3B,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;oBACxC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC9D,CAAC;YACH,CAAC;YAED,MAAM,aAAa,GAAG;gBACpB,KAAK,EAAE,KAAK;gBACZ,IAAI;gBACJ,GAAG,EAAE,OAAO;gBACZ,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;gBACvE,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS;gBAC7E,UAAU,EAAE;oBACV,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS;oBAC7E,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;oBAC3C,MAAM,EAAE,WAAW,CAAC,MAAM,IAAI,MAAM,CAAC,sBAAsB,CAAC;oBAC5D,QAAQ,EAAE,SAAS,CAAC,IAAI,IAAI,MAAM,CAAC,oBAAoB,CAAC;oBACxD,uBAAuB;oBACvB,UAAU,EAAE,KAAK;oBACjB,WAAW,EAAE,QAAQ;oBACrB,OAAO,EAAE,SAAS;oBAClB,OAAO,EAAE,SAAS;iBACnB;aACF,CAAC;YAEF,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,EAAE,CAAC;YACzC,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;YAC9D,MAAM,SAAS,CAAC,KAAK,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,CAAC;YACvC,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CACjC,SAAS,CAAC,EAAE,EACZ,KAAK,EACL,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,SAAS,CAAC,EAAE,EAC7C,OAAO,CACR,CAAC;YACF,OAAO,UAAU,CAAC,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IACrD,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAAE,OAAO,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;IAClD,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAC3C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,GAAG,CAAC;QACT,KAAK,IAAI;YACP,OAAO,GAAG,GAAG,IAAI,CAAC;QACpB,KAAK,GAAG,CAAC;QACT,KAAK,IAAI;YACP,OAAO,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;QAC3B,KAAK,GAAG,CAAC;QACT,KAAK,IAAI;YACP,OAAO,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;QAClC,KAAK,GAAG,CAAC;QACT,KAAK,IAAI;YACP,OAAO,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;QACzC;YACE,OAAO,GAAG,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,KAAa;IAC9B,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,aAAa,CAAC,CAAC;AAC7E,CAAC"}
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerStopRemoveTools(server) {
|
||||
server.registerTool('docker_stop_container', {
|
||||
title: 'Docker Stop Container',
|
||||
description: 'Stop a Docker container started by this server.',
|
||||
inputSchema: {
|
||||
containerId: z.string().min(1).describe('Container ID returned by docker_run_container.'),
|
||||
timeout: z.number().int().positive().optional().default(10).describe('Seconds to wait before killing.'),
|
||||
},
|
||||
}, async ({ containerId, timeout }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
await dockerManager.getDocker().getContainer(containerId).stop({ t: timeout });
|
||||
return jsonResult({ stopped: true, containerId });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
server.registerTool('docker_remove_container', {
|
||||
title: 'Docker Remove Container',
|
||||
description: 'Remove a Docker container started by this server.',
|
||||
inputSchema: {
|
||||
containerId: z.string().min(1).describe('Container ID returned by docker_run_container.'),
|
||||
force: z.boolean().optional().default(false).describe('Force removal even if running.'),
|
||||
},
|
||||
}, async ({ containerId, force }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
await dockerManager.getDocker().getContainer(containerId).remove({ force });
|
||||
dockerManager.untrack(containerId);
|
||||
return jsonResult({ removed: true, containerId });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=stopRemove.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"stopRemove.js","sourceRoot":"","sources":["../../../src/tools/docker/stopRemove.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,UAAU,6BAA6B,CAAC,MAAiB;IAC7D,MAAM,CAAC,YAAY,CACjB,uBAAuB,EACvB;QACE,KAAK,EAAE,uBAAuB;QAC9B,WAAW,EAAE,iDAAiD;QAC9D,WAAW,EAAE;YACX,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,gDAAgD,CAAC;YACzF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,iCAAiC,CAAC;SACxG;KACF,EACD,KAAK,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,EAAE;QACjC,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YACtC,MAAM,aAAa,CAAC,SAAS,EAAE,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;YAC/E,OAAO,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,yBAAyB,EACzB;QACE,KAAK,EAAE,yBAAyB;QAChC,WAAW,EAAE,mDAAmD;QAChE,WAAW,EAAE;YACX,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,gDAAgD,CAAC;YACzF,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,gCAAgC,CAAC;SACxF;KACF,EACD,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE;QAC/B,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YACtC,MAAM,aAAa,CAAC,SAAS,EAAE,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;YAC5E,aAAa,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YACnC,OAAO,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Reference in New Issue
Block a user