Added chall and some stuff
This commit is contained in:
@@ -7,6 +7,7 @@ import fs from "fs/promises";
|
||||
import { createReadStream } from "fs";
|
||||
import http from "http";
|
||||
import path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import { URL } from "url";
|
||||
import { z } from "zod";
|
||||
import { minimatch } from "minimatch";
|
||||
@@ -95,6 +96,15 @@ const GetFileInfoArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
});
|
||||
|
||||
const RunCommandArgsSchema = z.object({
|
||||
command: z.string().min(1),
|
||||
cwd: z.string().optional().describe("Working directory to run the command in. Must be within allowed directories."),
|
||||
timeoutMs: z.number().int().positive().optional().default(600_000).describe("Kill the command if it runs longer than this (ms). Default: 10 minutes."),
|
||||
env: z.record(z.string(), z.string()).optional().default({}).describe("Extra environment variables (string values)."),
|
||||
shell: z.enum(["bash", "sh"]).optional().default("bash").describe("Shell to use for the command. Default: bash."),
|
||||
maxOutputChars: z.number().int().positive().optional().default(200_000).describe("Maximum characters captured for each of stdout/stderr."),
|
||||
});
|
||||
|
||||
// Server setup
|
||||
const server = new McpServer(
|
||||
{
|
||||
@@ -121,6 +131,89 @@ async function readFileAsBase64Stream(filePath: string): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
function appendWithLimit(current: string, chunk: string, maxChars: number): { next: string; truncated: boolean } {
|
||||
if (maxChars <= 0) return { next: "", truncated: true };
|
||||
if (current.length >= maxChars) return { next: current, truncated: true };
|
||||
const remaining = maxChars - current.length;
|
||||
if (chunk.length <= remaining) return { next: current + chunk, truncated: false };
|
||||
return { next: current + chunk.slice(0, remaining), truncated: true };
|
||||
}
|
||||
|
||||
async function runShellCommand(args: z.infer<typeof RunCommandArgsSchema>): Promise<{
|
||||
exitCode: number | null;
|
||||
signal: string | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
stdoutTruncated: boolean;
|
||||
stderrTruncated: boolean;
|
||||
timedOut: boolean;
|
||||
effectiveCwd: string;
|
||||
}> {
|
||||
const defaultCwd = process.env.MCP_DEFAULT_CWD ?? "/workspace";
|
||||
const requestedCwd = args.cwd ?? defaultCwd;
|
||||
const effectiveCwd = await validatePath(requestedCwd);
|
||||
|
||||
const cwdStats = await fs.stat(effectiveCwd);
|
||||
if (!cwdStats.isDirectory()) {
|
||||
throw new Error(`cwd is not a directory: ${requestedCwd}`);
|
||||
}
|
||||
|
||||
const shellPath = args.shell === "sh" ? "/bin/sh" : "/bin/bash";
|
||||
const env: NodeJS.ProcessEnv = { ...process.env, ...args.env };
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const child = spawn(shellPath, ["-lc", args.command], {
|
||||
cwd: effectiveCwd,
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let stdoutTruncated = false;
|
||||
let stderrTruncated = false;
|
||||
let timedOut = false;
|
||||
|
||||
const killTimer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
try {
|
||||
child.kill("SIGKILL");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, args.timeoutMs);
|
||||
|
||||
child.stdout?.setEncoding("utf-8");
|
||||
child.stderr?.setEncoding("utf-8");
|
||||
|
||||
child.stdout?.on("data", (data: string) => {
|
||||
const result = appendWithLimit(stdout, data, args.maxOutputChars);
|
||||
stdout = result.next;
|
||||
stdoutTruncated = stdoutTruncated || result.truncated;
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data: string) => {
|
||||
const result = appendWithLimit(stderr, data, args.maxOutputChars);
|
||||
stderr = result.next;
|
||||
stderrTruncated = stderrTruncated || result.truncated;
|
||||
});
|
||||
|
||||
child.on("close", (exitCode, signal) => {
|
||||
clearTimeout(killTimer);
|
||||
resolve({
|
||||
exitCode,
|
||||
signal: signal ? String(signal) : null,
|
||||
stdout,
|
||||
stderr,
|
||||
stdoutTruncated,
|
||||
stderrTruncated,
|
||||
timedOut,
|
||||
effectiveCwd,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Tool registrations
|
||||
|
||||
// read_file (deprecated) and read_text_file
|
||||
@@ -616,6 +709,63 @@ server.registerTool(
|
||||
}
|
||||
);
|
||||
|
||||
async function runCommandToolHandler(rawArgs: unknown) {
|
||||
const args = RunCommandArgsSchema.parse(rawArgs);
|
||||
const result = await runShellCommand(args);
|
||||
|
||||
const textLines = [
|
||||
`cwd: ${result.effectiveCwd}`,
|
||||
`exitCode: ${result.exitCode === null ? "null" : String(result.exitCode)}`,
|
||||
`signal: ${result.signal ?? "null"}`,
|
||||
`timedOut: ${result.timedOut ? "true" : "false"}`,
|
||||
`stdoutTruncated: ${result.stdoutTruncated ? "true" : "false"}`,
|
||||
`stderrTruncated: ${result.stderrTruncated ? "true" : "false"}`,
|
||||
"",
|
||||
"stdout:",
|
||||
result.stdout || "(empty)",
|
||||
"",
|
||||
"stderr:",
|
||||
result.stderr || "(empty)",
|
||||
];
|
||||
|
||||
const text = textLines.join("\n");
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
structuredContent: {
|
||||
content: text,
|
||||
cwd: result.effectiveCwd,
|
||||
exitCode: result.exitCode,
|
||||
signal: result.signal,
|
||||
timedOut: result.timedOut,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
stdoutTruncated: result.stdoutTruncated,
|
||||
stderrTruncated: result.stderrTruncated,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const runCommandToolDefinition = {
|
||||
title: "Run Command",
|
||||
description:
|
||||
"Execute a shell command inside this same container (same filesystem as the filesystem tools). " +
|
||||
"Uses a non-interactive shell (`bash -lc` by default). Returns stdout/stderr, exit code, and timeout info.",
|
||||
inputSchema: {
|
||||
command: z.string(),
|
||||
cwd: z.string().optional().describe("Working directory to run the command in. Must be within allowed directories."),
|
||||
timeoutMs: z.number().optional().describe("Kill the command if it runs longer than this (ms). Default: 10 minutes."),
|
||||
env: z.record(z.string(), z.string()).optional().describe("Extra environment variables (string values)."),
|
||||
shell: z.enum(["bash", "sh"]).optional().describe("Shell to use for the command. Default: bash."),
|
||||
maxOutputChars: z.number().optional().describe("Maximum characters captured for each of stdout/stderr."),
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true },
|
||||
};
|
||||
|
||||
// Keep snake_case for consistency with existing filesystem tools, and also provide a camelCase alias.
|
||||
server.registerTool("run_command", runCommandToolDefinition, runCommandToolHandler);
|
||||
server.registerTool("runCommand", runCommandToolDefinition, runCommandToolHandler);
|
||||
|
||||
// SSE transport session routing (sessionId -> transport)
|
||||
const sseTransportsBySessionId = new Map<string, SSEServerTransport>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user