diff --git a/.changeset/patch-add-mcp-tool-handlers.md b/.changeset/patch-add-mcp-tool-handlers.md new file mode 100644 index 00000000000..8cb853a30a1 --- /dev/null +++ b/.changeset/patch-add-mcp-tool-handlers.md @@ -0,0 +1,12 @@ +--- +"gh-aw": patch +--- + +Add support for loading MCP tool handlers from external files. This change +updates the JavaScript MCP server core and its schema to allow a tool's +`handler` field to point to a file whose default export (or module export) +is used as the handler function. Supports sync/async handlers, ES module +default exports, shell-script handlers, path traversal protection, +serialization fallback for circular structures, and extensive logging. Tests +for handler loading were added and workflows were recompiled. + diff --git a/.github/workflows/ai-triage-campaign.lock.yml b/.github/workflows/ai-triage-campaign.lock.yml index 86bd9162885..3d32f128f24 100644 --- a/.github/workflows/ai-triage-campaign.lock.yml +++ b/.github/workflows/ai-triage-campaign.lock.yml @@ -686,6 +686,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -747,6 +748,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -785,6 +795,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -794,11 +805,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -810,7 +1032,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -879,7 +1101,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -891,7 +1115,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -899,7 +1123,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -912,9 +1136,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/archie.lock.yml b/.github/workflows/archie.lock.yml index 07fd49df6a3..f4e2e9fefb5 100644 --- a/.github/workflows/archie.lock.yml +++ b/.github/workflows/archie.lock.yml @@ -2159,6 +2159,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2220,6 +2221,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2258,6 +2268,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2267,11 +2278,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2283,7 +2505,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2352,7 +2574,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2364,7 +2588,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2372,7 +2596,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2385,9 +2609,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/artifacts-summary.lock.yml b/.github/workflows/artifacts-summary.lock.yml index 2827c6f25ea..cf432984ad5 100644 --- a/.github/workflows/artifacts-summary.lock.yml +++ b/.github/workflows/artifacts-summary.lock.yml @@ -760,6 +760,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -821,6 +822,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -859,6 +869,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -868,11 +879,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -884,7 +1106,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -953,7 +1175,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -965,7 +1189,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -973,7 +1197,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -986,9 +1210,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index 8cb3a2940cd..228d9553066 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -1571,6 +1571,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1632,6 +1633,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1670,6 +1680,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1679,11 +1690,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1695,7 +1917,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1764,7 +1986,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1776,7 +2000,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1784,7 +2008,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1797,9 +2021,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/blog-auditor.lock.yml b/.github/workflows/blog-auditor.lock.yml index 32374aa783e..30fd9ce72e7 100644 --- a/.github/workflows/blog-auditor.lock.yml +++ b/.github/workflows/blog-auditor.lock.yml @@ -1075,6 +1075,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1136,6 +1137,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1174,6 +1184,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1183,11 +1194,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1199,7 +1421,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1268,7 +1490,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1280,7 +1504,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1288,7 +1512,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1301,9 +1525,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/brave.lock.yml b/.github/workflows/brave.lock.yml index baaf241eeb3..49889d4a490 100644 --- a/.github/workflows/brave.lock.yml +++ b/.github/workflows/brave.lock.yml @@ -2043,6 +2043,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2104,6 +2105,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2142,6 +2152,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2151,11 +2162,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2167,7 +2389,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2236,7 +2458,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2248,7 +2472,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2256,7 +2480,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2269,9 +2493,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/breaking-change-checker.lock.yml b/.github/workflows/breaking-change-checker.lock.yml index 9e6315a48cf..da83a1d2190 100644 --- a/.github/workflows/breaking-change-checker.lock.yml +++ b/.github/workflows/breaking-change-checker.lock.yml @@ -800,6 +800,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -861,6 +862,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -899,6 +909,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -908,11 +919,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -924,7 +1146,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -993,7 +1215,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1005,7 +1229,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1013,7 +1237,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1026,9 +1250,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index 4fdd6ce3a92..95fa194d2d3 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -1687,6 +1687,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1748,6 +1749,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1786,6 +1796,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1795,11 +1806,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1811,7 +2033,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1880,7 +2102,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1892,7 +2116,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1900,7 +2124,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1913,9 +2137,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index e9112cd2901..dfcbe0464e8 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -1464,6 +1464,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1525,6 +1526,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1563,6 +1573,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1572,11 +1583,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1588,7 +1810,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1657,7 +1879,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1669,7 +1893,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1677,7 +1901,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1690,9 +1914,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/cli-consistency-checker.lock.yml b/.github/workflows/cli-consistency-checker.lock.yml index 2b0f74db1e8..d8b5743741b 100644 --- a/.github/workflows/cli-consistency-checker.lock.yml +++ b/.github/workflows/cli-consistency-checker.lock.yml @@ -809,6 +809,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -870,6 +871,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -908,6 +918,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -917,11 +928,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -933,7 +1155,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1002,7 +1224,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1014,7 +1238,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1022,7 +1246,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1035,9 +1259,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index 6a927df4e52..f2dd6315f06 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -975,6 +975,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1036,6 +1037,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1074,6 +1084,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1083,11 +1094,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1099,7 +1321,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1168,7 +1390,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1180,7 +1404,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1188,7 +1412,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1201,9 +1425,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index 29db7082c5d..e8df1d71388 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -2521,6 +2521,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2582,6 +2583,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2620,6 +2630,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2629,11 +2640,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2645,7 +2867,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2714,7 +2936,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2726,7 +2950,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2734,7 +2958,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2747,9 +2971,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/close-old-discussions.lock.yml b/.github/workflows/close-old-discussions.lock.yml index 2b0a5f53e10..43900d28551 100644 --- a/.github/workflows/close-old-discussions.lock.yml +++ b/.github/workflows/close-old-discussions.lock.yml @@ -973,6 +973,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1034,6 +1035,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1072,6 +1082,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1081,11 +1092,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1097,7 +1319,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1166,7 +1388,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1178,7 +1402,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1186,7 +1410,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1199,9 +1423,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/commit-changes-analyzer.lock.yml b/.github/workflows/commit-changes-analyzer.lock.yml index b1861722bd9..6ea7521558a 100644 --- a/.github/workflows/commit-changes-analyzer.lock.yml +++ b/.github/workflows/commit-changes-analyzer.lock.yml @@ -1035,6 +1035,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1096,6 +1097,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1134,6 +1144,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1143,11 +1154,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1159,7 +1381,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1228,7 +1450,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1240,7 +1464,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1248,7 +1472,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1261,9 +1485,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml index c111934dc5b..76635ca9e7c 100644 --- a/.github/workflows/copilot-agent-analysis.lock.yml +++ b/.github/workflows/copilot-agent-analysis.lock.yml @@ -1383,6 +1383,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1444,6 +1445,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1482,6 +1492,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1491,11 +1502,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1507,7 +1729,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1576,7 +1798,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1588,7 +1812,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1596,7 +1820,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1609,9 +1833,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml index da56ce7f908..0f3fd345613 100644 --- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml +++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml @@ -1598,6 +1598,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1659,6 +1660,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1697,6 +1707,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1706,11 +1717,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1722,7 +1944,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1791,7 +2013,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1803,7 +2027,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1811,7 +2035,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1824,9 +2048,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/copilot-pr-prompt-analysis.lock.yml b/.github/workflows/copilot-pr-prompt-analysis.lock.yml index 0462d0b330c..205ddb2e3bc 100644 --- a/.github/workflows/copilot-pr-prompt-analysis.lock.yml +++ b/.github/workflows/copilot-pr-prompt-analysis.lock.yml @@ -1098,6 +1098,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1159,6 +1160,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1197,6 +1207,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1206,11 +1217,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1222,7 +1444,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1291,7 +1513,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1303,7 +1527,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1311,7 +1535,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1324,9 +1548,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml index 2fa5a7b51c8..39cdf43afc1 100644 --- a/.github/workflows/copilot-session-insights.lock.yml +++ b/.github/workflows/copilot-session-insights.lock.yml @@ -2455,6 +2455,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2516,6 +2517,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2554,6 +2564,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2563,11 +2574,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2579,7 +2801,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2648,7 +2870,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2660,7 +2884,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2668,7 +2892,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2681,9 +2905,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/craft.lock.yml b/.github/workflows/craft.lock.yml index 39965fb9913..c454a58905c 100644 --- a/.github/workflows/craft.lock.yml +++ b/.github/workflows/craft.lock.yml @@ -2253,6 +2253,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2314,6 +2315,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2352,6 +2362,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2361,11 +2372,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2377,7 +2599,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2446,7 +2668,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2458,7 +2682,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2466,7 +2690,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2479,9 +2703,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index e4becfa2f9a..1c63226d25c 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -1614,6 +1614,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1675,6 +1676,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1713,6 +1723,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1722,11 +1733,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1738,7 +1960,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1807,7 +2029,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1819,7 +2043,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1827,7 +2051,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1840,9 +2064,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index 33dc3f1e253..f5e6c7e86ea 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -932,6 +932,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -993,6 +994,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1031,6 +1041,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1040,11 +1051,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1056,7 +1278,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1125,7 +1347,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1137,7 +1361,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1145,7 +1369,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1158,9 +1382,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-fact.lock.yml b/.github/workflows/daily-fact.lock.yml index 77306812182..c41decc0392 100644 --- a/.github/workflows/daily-fact.lock.yml +++ b/.github/workflows/daily-fact.lock.yml @@ -1249,6 +1249,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1310,6 +1311,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1348,6 +1358,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1357,11 +1368,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1373,7 +1595,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1442,7 +1664,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1454,7 +1678,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1462,7 +1686,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1475,9 +1699,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml index 6b05292f73a..3a0d3925be4 100644 --- a/.github/workflows/daily-file-diet.lock.yml +++ b/.github/workflows/daily-file-diet.lock.yml @@ -960,6 +960,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1021,6 +1022,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1059,6 +1069,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1068,11 +1079,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1084,7 +1306,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1153,7 +1375,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1165,7 +1389,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1173,7 +1397,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1186,9 +1410,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-firewall-report.lock.yml b/.github/workflows/daily-firewall-report.lock.yml index 36d8483ab99..a5fbcf0decd 100644 --- a/.github/workflows/daily-firewall-report.lock.yml +++ b/.github/workflows/daily-firewall-report.lock.yml @@ -1323,6 +1323,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1384,6 +1385,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1422,6 +1432,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1431,11 +1442,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1447,7 +1669,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1516,7 +1738,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1528,7 +1752,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1536,7 +1760,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1549,9 +1773,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-issues-report.lock.yml b/.github/workflows/daily-issues-report.lock.yml index 44b897d301b..dfeb1146983 100644 --- a/.github/workflows/daily-issues-report.lock.yml +++ b/.github/workflows/daily-issues-report.lock.yml @@ -1760,6 +1760,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1821,6 +1822,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1859,6 +1869,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1868,11 +1879,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1884,7 +2106,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1953,7 +2175,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1965,7 +2189,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1973,7 +2197,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1986,9 +2210,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index e1b060e8a34..fab7f53f3be 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -936,6 +936,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -997,6 +998,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1035,6 +1045,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1044,11 +1055,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1060,7 +1282,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1129,7 +1351,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1141,7 +1365,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1149,7 +1373,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1162,9 +1386,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index 0bf5f98ad83..d0fd0037703 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -891,6 +891,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -952,6 +953,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -990,6 +1000,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -999,11 +1010,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1015,7 +1237,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1084,7 +1306,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1096,7 +1320,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1104,7 +1328,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1117,9 +1341,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index ea3a3e2b7b3..a02838b730e 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -1684,6 +1684,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1745,6 +1746,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1783,6 +1793,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1792,11 +1803,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1808,7 +2030,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1877,7 +2099,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1889,7 +2113,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1897,7 +2121,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1910,9 +2134,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-performance-summary.lock.yml b/.github/workflows/daily-performance-summary.lock.yml index 3b3365b966d..d12061884a6 100644 --- a/.github/workflows/daily-performance-summary.lock.yml +++ b/.github/workflows/daily-performance-summary.lock.yml @@ -1532,6 +1532,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1593,6 +1594,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1631,6 +1641,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1640,11 +1651,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1656,7 +1878,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1725,7 +1947,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1737,7 +1961,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1745,7 +1969,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1758,9 +1982,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-repo-chronicle.lock.yml b/.github/workflows/daily-repo-chronicle.lock.yml index 69b286f1e81..3cb64a044c1 100644 --- a/.github/workflows/daily-repo-chronicle.lock.yml +++ b/.github/workflows/daily-repo-chronicle.lock.yml @@ -1370,6 +1370,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1431,6 +1432,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1469,6 +1479,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1478,11 +1489,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1494,7 +1716,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1563,7 +1785,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1575,7 +1799,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1583,7 +1807,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1596,9 +1820,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/daily-team-status.lock.yml b/.github/workflows/daily-team-status.lock.yml index 1b504bb2963..e5ce2d09f62 100644 --- a/.github/workflows/daily-team-status.lock.yml +++ b/.github/workflows/daily-team-status.lock.yml @@ -727,6 +727,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -788,6 +789,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -826,6 +836,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -835,11 +846,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -851,7 +1073,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -920,7 +1142,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -932,7 +1156,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -940,7 +1164,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -953,9 +1177,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/deep-report.lock.yml b/.github/workflows/deep-report.lock.yml index 30a224bb219..edfad5d9f77 100644 --- a/.github/workflows/deep-report.lock.yml +++ b/.github/workflows/deep-report.lock.yml @@ -1203,6 +1203,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1264,6 +1265,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1302,6 +1312,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1311,11 +1322,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1327,7 +1549,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1396,7 +1618,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1408,7 +1632,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1416,7 +1640,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1429,9 +1653,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/dependabot-go-checker.lock.yml b/.github/workflows/dependabot-go-checker.lock.yml index 0966667f08a..ec5e236a206 100644 --- a/.github/workflows/dependabot-go-checker.lock.yml +++ b/.github/workflows/dependabot-go-checker.lock.yml @@ -1104,6 +1104,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1165,6 +1166,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1203,6 +1213,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1212,11 +1223,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1228,7 +1450,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1297,7 +1519,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1309,7 +1533,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1317,7 +1541,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1330,9 +1554,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/dev-hawk.lock.yml b/.github/workflows/dev-hawk.lock.yml index 53f9b018462..7cafd130704 100644 --- a/.github/workflows/dev-hawk.lock.yml +++ b/.github/workflows/dev-hawk.lock.yml @@ -1305,6 +1305,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1366,6 +1367,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1404,6 +1414,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1413,11 +1424,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1429,7 +1651,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1498,7 +1720,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1510,7 +1734,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1518,7 +1742,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1531,9 +1755,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 5e1c74d6bf2..8c893a06157 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -704,6 +704,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -765,6 +766,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -803,6 +813,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -812,11 +823,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -828,7 +1050,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -897,7 +1119,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -909,7 +1133,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -917,7 +1141,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -930,9 +1154,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index 84a8edf439f..98a613586ee 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -1504,6 +1504,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1565,6 +1566,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1603,6 +1613,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1612,11 +1623,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1628,7 +1850,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1697,7 +1919,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1709,7 +1933,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1717,7 +1941,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1730,9 +1954,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml index bcdaf2174af..72a3d335768 100644 --- a/.github/workflows/dictation-prompt.lock.yml +++ b/.github/workflows/dictation-prompt.lock.yml @@ -782,6 +782,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -843,6 +844,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -881,6 +891,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -890,11 +901,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -906,7 +1128,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -975,7 +1197,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -987,7 +1211,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -995,7 +1219,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1008,9 +1232,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index 03c597da48f..e67368beea0 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -815,6 +815,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -876,6 +877,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -914,6 +924,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -923,11 +934,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -939,7 +1161,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1008,7 +1230,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1020,7 +1244,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1028,7 +1252,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1041,9 +1265,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index e5d83c71b83..07705494946 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -865,6 +865,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -926,6 +927,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -964,6 +974,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -973,11 +984,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -989,7 +1211,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1058,7 +1280,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1070,7 +1294,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1078,7 +1302,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1091,9 +1315,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/example-workflow-analyzer.lock.yml b/.github/workflows/example-workflow-analyzer.lock.yml index 361d496567b..89c647e58ef 100644 --- a/.github/workflows/example-workflow-analyzer.lock.yml +++ b/.github/workflows/example-workflow-analyzer.lock.yml @@ -832,6 +832,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -893,6 +894,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -931,6 +941,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -940,11 +951,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -956,7 +1178,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1025,7 +1247,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1037,7 +1261,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1045,7 +1269,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1058,9 +1282,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/github-mcp-structural-analysis.lock.yml b/.github/workflows/github-mcp-structural-analysis.lock.yml index 283ea2a50f4..24a427d08b2 100644 --- a/.github/workflows/github-mcp-structural-analysis.lock.yml +++ b/.github/workflows/github-mcp-structural-analysis.lock.yml @@ -1470,6 +1470,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1531,6 +1532,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1569,6 +1579,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1578,11 +1589,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1594,7 +1816,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1663,7 +1885,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1675,7 +1899,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1683,7 +1907,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1696,9 +1920,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml index 6a2ddfb0fc1..c14eac7a0d7 100644 --- a/.github/workflows/github-mcp-tools-report.lock.yml +++ b/.github/workflows/github-mcp-tools-report.lock.yml @@ -1354,6 +1354,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1415,6 +1416,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1453,6 +1463,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1462,11 +1473,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1478,7 +1700,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1547,7 +1769,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1559,7 +1783,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1567,7 +1791,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1580,9 +1804,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index c2dc43ee4d4..209b9d4c3a8 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -1303,6 +1303,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1364,6 +1365,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1402,6 +1412,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1411,11 +1422,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1427,7 +1649,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1496,7 +1718,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1508,7 +1732,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1516,7 +1740,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1529,9 +1753,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index d3fd22faec3..f6fc4c48e06 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -1059,6 +1059,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1120,6 +1121,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1158,6 +1168,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1167,11 +1178,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1183,7 +1405,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1252,7 +1474,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1264,7 +1488,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1272,7 +1496,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1285,9 +1509,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index 14592090c2b..0d849559c68 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -929,6 +929,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -990,6 +991,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1028,6 +1038,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1037,11 +1048,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1053,7 +1275,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1122,7 +1344,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1134,7 +1358,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1142,7 +1366,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1155,9 +1379,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/grumpy-reviewer.lock.yml b/.github/workflows/grumpy-reviewer.lock.yml index f8b330f44c6..9627825807f 100644 --- a/.github/workflows/grumpy-reviewer.lock.yml +++ b/.github/workflows/grumpy-reviewer.lock.yml @@ -2169,6 +2169,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2230,6 +2231,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2268,6 +2278,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2277,11 +2288,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2293,7 +2515,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2362,7 +2584,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2374,7 +2598,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2382,7 +2606,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2395,9 +2619,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml index 87fa5f5af5b..44b59ac6ac4 100644 --- a/.github/workflows/instructions-janitor.lock.yml +++ b/.github/workflows/instructions-janitor.lock.yml @@ -929,6 +929,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -990,6 +991,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1028,6 +1038,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1037,11 +1048,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1053,7 +1275,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1122,7 +1344,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1134,7 +1358,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1142,7 +1366,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1155,9 +1379,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/issue-arborist.lock.yml b/.github/workflows/issue-arborist.lock.yml index 5c5a5da702b..c180bd21207 100644 --- a/.github/workflows/issue-arborist.lock.yml +++ b/.github/workflows/issue-arborist.lock.yml @@ -910,6 +910,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -971,6 +972,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1009,6 +1019,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1018,11 +1029,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1034,7 +1256,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1103,7 +1325,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1115,7 +1339,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1123,7 +1347,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1136,9 +1360,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index 4719fe9d16b..9000ffad912 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -1934,6 +1934,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1995,6 +1996,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2033,6 +2043,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2042,11 +2053,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2058,7 +2280,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2127,7 +2349,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2139,7 +2363,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2147,7 +2371,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2160,9 +2384,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/issue-monster.lock.yml b/.github/workflows/issue-monster.lock.yml index 2b33bd43c13..4de777ba6dc 100644 --- a/.github/workflows/issue-monster.lock.yml +++ b/.github/workflows/issue-monster.lock.yml @@ -1338,6 +1338,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1399,6 +1400,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1437,6 +1447,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1446,11 +1457,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1462,7 +1684,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1531,7 +1753,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1543,7 +1767,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1551,7 +1775,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1564,9 +1788,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/issue-triage-agent.lock.yml b/.github/workflows/issue-triage-agent.lock.yml index 8d8fede8e24..f3aa2a18cc6 100644 --- a/.github/workflows/issue-triage-agent.lock.yml +++ b/.github/workflows/issue-triage-agent.lock.yml @@ -1103,6 +1103,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1164,6 +1165,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1202,6 +1212,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1211,11 +1222,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1227,7 +1449,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1296,7 +1518,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1308,7 +1532,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1316,7 +1540,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1329,9 +1553,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/lockfile-stats.lock.yml b/.github/workflows/lockfile-stats.lock.yml index 329bc934ff7..1cc05573964 100644 --- a/.github/workflows/lockfile-stats.lock.yml +++ b/.github/workflows/lockfile-stats.lock.yml @@ -1149,6 +1149,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1210,6 +1211,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1248,6 +1258,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1257,11 +1268,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1273,7 +1495,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1342,7 +1564,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1354,7 +1578,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1362,7 +1586,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1375,9 +1599,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index 3820813cc97..a16e8d15679 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -987,6 +987,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1048,6 +1049,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1086,6 +1096,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1095,11 +1106,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1111,7 +1333,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1180,7 +1402,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1192,7 +1416,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1200,7 +1424,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1213,9 +1437,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/mergefest.lock.yml b/.github/workflows/mergefest.lock.yml index f1590a0a510..241f780b168 100644 --- a/.github/workflows/mergefest.lock.yml +++ b/.github/workflows/mergefest.lock.yml @@ -1337,6 +1337,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1398,6 +1399,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1436,6 +1446,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1445,11 +1456,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1461,7 +1683,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1530,7 +1752,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1542,7 +1766,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1550,7 +1774,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1563,9 +1787,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/notion-issue-summary.lock.yml b/.github/workflows/notion-issue-summary.lock.yml index c584c473b32..4915c85f3ce 100644 --- a/.github/workflows/notion-issue-summary.lock.yml +++ b/.github/workflows/notion-issue-summary.lock.yml @@ -570,6 +570,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -631,6 +632,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -669,6 +679,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -678,11 +689,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -694,7 +916,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -763,7 +985,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -775,7 +999,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -783,7 +1007,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -796,9 +1020,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/pdf-summary.lock.yml b/.github/workflows/pdf-summary.lock.yml index d679b1635d8..dbf195a1170 100644 --- a/.github/workflows/pdf-summary.lock.yml +++ b/.github/workflows/pdf-summary.lock.yml @@ -2154,6 +2154,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2215,6 +2216,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2253,6 +2263,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2262,11 +2273,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2278,7 +2500,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2347,7 +2569,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2359,7 +2583,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2367,7 +2591,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2380,9 +2604,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index eaaab6f6b03..e34df77c81b 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -1575,6 +1575,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1636,6 +1637,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1674,6 +1684,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1683,11 +1694,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1699,7 +1921,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1768,7 +1990,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1780,7 +2004,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1788,7 +2012,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1801,9 +2025,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 7749f32cb42..30c7b2253b9 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -3082,6 +3082,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -3143,6 +3144,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -3181,6 +3191,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -3190,11 +3201,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -3206,7 +3428,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -3275,7 +3497,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -3287,7 +3511,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -3295,7 +3519,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -3308,9 +3532,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/pr-nitpick-reviewer.lock.yml b/.github/workflows/pr-nitpick-reviewer.lock.yml index a92d6f93718..e5115667d89 100644 --- a/.github/workflows/pr-nitpick-reviewer.lock.yml +++ b/.github/workflows/pr-nitpick-reviewer.lock.yml @@ -2201,6 +2201,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2262,6 +2263,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2300,6 +2310,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2309,11 +2320,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2325,7 +2547,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2394,7 +2616,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2406,7 +2630,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2414,7 +2638,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2427,9 +2651,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/prompt-clustering-analysis.lock.yml b/.github/workflows/prompt-clustering-analysis.lock.yml index 4699a10cdef..96bedc699f2 100644 --- a/.github/workflows/prompt-clustering-analysis.lock.yml +++ b/.github/workflows/prompt-clustering-analysis.lock.yml @@ -1833,6 +1833,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1894,6 +1895,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1932,6 +1942,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1941,11 +1952,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1957,7 +2179,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2026,7 +2248,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2038,7 +2262,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2046,7 +2270,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2059,9 +2283,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/python-data-charts.lock.yml b/.github/workflows/python-data-charts.lock.yml index 32bea977988..8d57d08d9f4 100644 --- a/.github/workflows/python-data-charts.lock.yml +++ b/.github/workflows/python-data-charts.lock.yml @@ -1672,6 +1672,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1733,6 +1734,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1771,6 +1781,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1780,11 +1791,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1796,7 +2018,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1865,7 +2087,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1877,7 +2101,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1885,7 +2109,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1898,9 +2122,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index 35245f01c2b..9697c6657ad 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -2473,6 +2473,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2534,6 +2535,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2572,6 +2582,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2581,11 +2592,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2597,7 +2819,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2666,7 +2888,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2678,7 +2902,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2686,7 +2910,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2699,9 +2923,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index c9b8f71397b..b07825f0c52 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -867,6 +867,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -928,6 +929,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -966,6 +976,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -975,11 +986,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -991,7 +1213,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1060,7 +1282,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1072,7 +1296,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1080,7 +1304,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1093,9 +1317,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/repo-tree-map.lock.yml b/.github/workflows/repo-tree-map.lock.yml index 4f1f18a0d43..cea2d11c8d4 100644 --- a/.github/workflows/repo-tree-map.lock.yml +++ b/.github/workflows/repo-tree-map.lock.yml @@ -805,6 +805,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -866,6 +867,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -904,6 +914,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -913,11 +924,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -929,7 +1151,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -998,7 +1220,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1010,7 +1234,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1018,7 +1242,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1031,9 +1255,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/repository-quality-improver.lock.yml b/.github/workflows/repository-quality-improver.lock.yml index b225161cbc7..887fa643d2a 100644 --- a/.github/workflows/repository-quality-improver.lock.yml +++ b/.github/workflows/repository-quality-improver.lock.yml @@ -1261,6 +1261,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1322,6 +1323,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1360,6 +1370,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1369,11 +1380,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1385,7 +1607,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1454,7 +1676,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1466,7 +1690,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1474,7 +1698,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1487,9 +1711,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/research.lock.yml b/.github/workflows/research.lock.yml index cbea50e7361..dd766f73133 100644 --- a/.github/workflows/research.lock.yml +++ b/.github/workflows/research.lock.yml @@ -736,6 +736,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -797,6 +798,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -835,6 +845,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -844,11 +855,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -860,7 +1082,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -929,7 +1151,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -941,7 +1165,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -949,7 +1173,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -962,9 +1186,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/safe-output-health.lock.yml b/.github/workflows/safe-output-health.lock.yml index f7e0e852c32..a99dbc3c231 100644 --- a/.github/workflows/safe-output-health.lock.yml +++ b/.github/workflows/safe-output-health.lock.yml @@ -1274,6 +1274,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1335,6 +1336,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1373,6 +1383,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1382,11 +1393,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1398,7 +1620,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1467,7 +1689,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1479,7 +1703,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1487,7 +1711,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1500,9 +1724,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/schema-consistency-checker.lock.yml b/.github/workflows/schema-consistency-checker.lock.yml index 42e816cdd68..f56e945d5bc 100644 --- a/.github/workflows/schema-consistency-checker.lock.yml +++ b/.github/workflows/schema-consistency-checker.lock.yml @@ -1159,6 +1159,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1220,6 +1221,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1258,6 +1268,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1267,11 +1278,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1283,7 +1505,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1352,7 +1574,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1364,7 +1588,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1372,7 +1596,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1385,9 +1609,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml index f4407b15d00..def5203bc59 100644 --- a/.github/workflows/scout.lock.yml +++ b/.github/workflows/scout.lock.yml @@ -2466,6 +2466,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2527,6 +2528,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2565,6 +2575,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2574,11 +2585,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2590,7 +2812,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2659,7 +2881,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2671,7 +2895,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2679,7 +2903,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2692,9 +2916,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml index 89567138cd3..dd0c45ef7ad 100644 --- a/.github/workflows/security-fix-pr.lock.yml +++ b/.github/workflows/security-fix-pr.lock.yml @@ -906,6 +906,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -967,6 +968,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1005,6 +1015,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1014,11 +1025,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1030,7 +1252,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1099,7 +1321,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1111,7 +1335,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1119,7 +1343,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1132,9 +1356,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml index 109dab0e6f0..a6d230731c4 100644 --- a/.github/workflows/semantic-function-refactor.lock.yml +++ b/.github/workflows/semantic-function-refactor.lock.yml @@ -1310,6 +1310,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1371,6 +1372,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1409,6 +1419,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1418,11 +1429,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1434,7 +1656,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1503,7 +1725,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1515,7 +1739,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1523,7 +1747,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1536,9 +1760,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 0ebb6093de1..f5690005819 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -223,7 +223,7 @@ # ## Test Requirements # # 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }} -# 2. **File Writing Testing**: Create a test file `/tmp/smoke-test-claude-${{ github.run_id }}.txt` with content "Smoke test passed for Claude at $(date)" +# 2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-claude-${{ github.run_id }}.txt` with content "Smoke test passed for Claude at $(date)" (create the directory if it doesn't exist) # 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) # 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" # @@ -2549,6 +2549,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2610,6 +2611,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2648,6 +2658,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2657,11 +2668,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2673,7 +2895,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2742,7 +2964,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2754,7 +2978,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2762,7 +2986,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2775,9 +2999,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); @@ -3388,7 +3612,7 @@ jobs: ## Test Requirements 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${GH_AW_GITHUB_REPOSITORY} - 2. **File Writing Testing**: Create a test file `/tmp/smoke-test-claude-${GH_AW_GITHUB_RUN_ID}.txt` with content "Smoke test passed for Claude at $(date)" + 2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-claude-${GH_AW_GITHUB_RUN_ID}.txt` with content "Smoke test passed for Claude at $(date)" (create the directory if it doesn't exist) 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" diff --git a/.github/workflows/smoke-claude.md b/.github/workflows/smoke-claude.md index d097474ac11..3772a8a022d 100644 --- a/.github/workflows/smoke-claude.md +++ b/.github/workflows/smoke-claude.md @@ -55,7 +55,7 @@ timeout-minutes: 10 ## Test Requirements 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }} -2. **File Writing Testing**: Create a test file `/tmp/smoke-test-claude-${{ github.run_id }}.txt` with content "Smoke test passed for Claude at $(date)" +2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-claude-${{ github.run_id }}.txt` with content "Smoke test passed for Claude at $(date)" (create the directory if it doesn't exist) 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 3eee4380c1d..f08b4418652 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -102,7 +102,7 @@ # ## Test Requirements # # 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }} -# 2. **File Writing Testing**: Create a test file `/tmp/smoke-test-codex-${{ github.run_id }}.txt` with content "Smoke test passed for Codex at $(date)" +# 2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-codex-${{ github.run_id }}.txt` with content "Smoke test passed for Codex at $(date)" (create the directory if it doesn't exist) # 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) # 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" # @@ -2319,6 +2319,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2380,6 +2381,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2418,6 +2428,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2427,11 +2438,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2443,7 +2665,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2512,7 +2734,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2524,7 +2748,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2532,7 +2756,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2545,9 +2769,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); @@ -3045,7 +3269,7 @@ jobs: ## Test Requirements 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${GH_AW_GITHUB_REPOSITORY} - 2. **File Writing Testing**: Create a test file `/tmp/smoke-test-codex-${GH_AW_GITHUB_RUN_ID}.txt` with content "Smoke test passed for Codex at $(date)" + 2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-codex-${GH_AW_GITHUB_RUN_ID}.txt` with content "Smoke test passed for Codex at $(date)" (create the directory if it doesn't exist) 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" diff --git a/.github/workflows/smoke-codex.md b/.github/workflows/smoke-codex.md index ff1cc763fe5..55b9bfb9174 100644 --- a/.github/workflows/smoke-codex.md +++ b/.github/workflows/smoke-codex.md @@ -49,7 +49,7 @@ timeout-minutes: 10 ## Test Requirements 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }} -2. **File Writing Testing**: Create a test file `/tmp/smoke-test-codex-${{ github.run_id }}.txt` with content "Smoke test passed for Codex at $(date)" +2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-codex-${{ github.run_id }}.txt` with content "Smoke test passed for Codex at $(date)" (create the directory if it doesn't exist) 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" diff --git a/.github/workflows/smoke-copilot-no-firewall.lock.yml b/.github/workflows/smoke-copilot-no-firewall.lock.yml index 943b016bf8a..bf6f1c7d0a1 100644 --- a/.github/workflows/smoke-copilot-no-firewall.lock.yml +++ b/.github/workflows/smoke-copilot-no-firewall.lock.yml @@ -109,7 +109,7 @@ # ## Test Requirements # # 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }} -# 2. **File Writing Testing**: Create a test file `/tmp/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" +# 2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist) # 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) # 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" # @@ -2392,6 +2392,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2453,6 +2454,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2491,6 +2501,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2500,11 +2511,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2516,7 +2738,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2585,7 +2807,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2597,7 +2821,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2605,7 +2829,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2618,9 +2842,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); @@ -3117,7 +3341,7 @@ jobs: ## Test Requirements 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${GH_AW_GITHUB_REPOSITORY} - 2. **File Writing Testing**: Create a test file `/tmp/smoke-test-copilot-${GH_AW_GITHUB_RUN_ID}.txt` with content "Smoke test passed for Copilot at $(date)" + 2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-${GH_AW_GITHUB_RUN_ID}.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist) 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" diff --git a/.github/workflows/smoke-copilot-no-firewall.md b/.github/workflows/smoke-copilot-no-firewall.md index 5999a92989f..86292faf411 100644 --- a/.github/workflows/smoke-copilot-no-firewall.md +++ b/.github/workflows/smoke-copilot-no-firewall.md @@ -52,7 +52,7 @@ strict: false ## Test Requirements 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }} -2. **File Writing Testing**: Create a test file `/tmp/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" +2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist) 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 782ef1c0574..c6ff91c026d 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -163,7 +163,7 @@ # ## Test Requirements # # 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }} -# 2. **File Writing Testing**: Create a test file `/tmp/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" +# 2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist) # 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) # 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" # @@ -2393,6 +2393,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2454,6 +2455,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2492,6 +2502,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2501,11 +2512,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2517,7 +2739,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2586,7 +2808,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2598,7 +2822,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2606,7 +2830,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2619,9 +2843,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); @@ -3118,7 +3342,7 @@ jobs: ## Test Requirements 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${GH_AW_GITHUB_REPOSITORY} - 2. **File Writing Testing**: Create a test file `/tmp/smoke-test-copilot-${GH_AW_GITHUB_RUN_ID}.txt` with content "Smoke test passed for Copilot at $(date)" + 2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-${GH_AW_GITHUB_RUN_ID}.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist) 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 88b304eee25..cc496b58bcf 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -110,7 +110,7 @@ post-steps: ## Test Requirements 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }} -2. **File Writing Testing**: Create a test file `/tmp/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" +2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist) 3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) 4. **Playwright MCP Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" diff --git a/.github/workflows/smoke-detector.lock.yml b/.github/workflows/smoke-detector.lock.yml index 89977447cf2..4522329513a 100644 --- a/.github/workflows/smoke-detector.lock.yml +++ b/.github/workflows/smoke-detector.lock.yml @@ -2182,6 +2182,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2243,6 +2244,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2281,6 +2291,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2290,11 +2301,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2306,7 +2528,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2375,7 +2597,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2387,7 +2611,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2395,7 +2619,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2408,9 +2632,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/smoke-srt.lock.yml b/.github/workflows/smoke-srt.lock.yml index 718231c9568..7a662ad7d4d 100644 --- a/.github/workflows/smoke-srt.lock.yml +++ b/.github/workflows/smoke-srt.lock.yml @@ -679,6 +679,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -740,6 +741,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -778,6 +788,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -787,11 +798,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -803,7 +1025,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -872,7 +1094,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -884,7 +1108,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -892,7 +1116,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -905,9 +1129,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/static-analysis-report.lock.yml b/.github/workflows/static-analysis-report.lock.yml index 137ca3032b1..967a3f7eed0 100644 --- a/.github/workflows/static-analysis-report.lock.yml +++ b/.github/workflows/static-analysis-report.lock.yml @@ -1183,6 +1183,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1244,6 +1245,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1282,6 +1292,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1291,11 +1302,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1307,7 +1529,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1376,7 +1598,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1388,7 +1612,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1396,7 +1620,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1409,9 +1633,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/super-linter.lock.yml b/.github/workflows/super-linter.lock.yml index 87ff90455f4..5f407c7c227 100644 --- a/.github/workflows/super-linter.lock.yml +++ b/.github/workflows/super-linter.lock.yml @@ -916,6 +916,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -977,6 +978,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1015,6 +1025,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1024,11 +1035,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1040,7 +1262,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1109,7 +1331,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1121,7 +1345,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1129,7 +1353,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1142,9 +1366,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 2b439957e39..11856607052 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -1772,6 +1772,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1833,6 +1834,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1871,6 +1881,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1880,11 +1891,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1896,7 +2118,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1965,7 +2187,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1977,7 +2201,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1985,7 +2209,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1998,9 +2222,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index 43fd99420a6..b7543dfd281 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -1216,6 +1216,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1277,6 +1278,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1315,6 +1325,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1324,11 +1335,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1340,7 +1562,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1409,7 +1631,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1421,7 +1645,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1429,7 +1653,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1442,9 +1666,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/typist.lock.yml b/.github/workflows/typist.lock.yml index ff299cfed58..14b61e5479b 100644 --- a/.github/workflows/typist.lock.yml +++ b/.github/workflows/typist.lock.yml @@ -1295,6 +1295,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1356,6 +1357,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1394,6 +1404,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1403,11 +1414,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1419,7 +1641,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1488,7 +1710,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1500,7 +1724,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1508,7 +1732,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1521,9 +1745,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 2f00ebf62f3..21e41ccb353 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -2227,6 +2227,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -2288,6 +2289,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -2326,6 +2336,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -2335,11 +2346,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -2351,7 +2573,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -2420,7 +2642,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -2432,7 +2656,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -2440,7 +2664,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -2453,9 +2677,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/video-analyzer.lock.yml b/.github/workflows/video-analyzer.lock.yml index 9cfcfba6fc2..a4e6509c68c 100644 --- a/.github/workflows/video-analyzer.lock.yml +++ b/.github/workflows/video-analyzer.lock.yml @@ -914,6 +914,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -975,6 +976,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1013,6 +1023,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1022,11 +1033,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1038,7 +1260,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1107,7 +1329,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1119,7 +1343,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1127,7 +1351,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1140,9 +1364,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/.github/workflows/weekly-issue-summary.lock.yml b/.github/workflows/weekly-issue-summary.lock.yml index 077a872e708..5d76073d8a6 100644 --- a/.github/workflows/weekly-issue-summary.lock.yml +++ b/.github/workflows/weekly-issue-summary.lock.yml @@ -1272,6 +1272,7 @@ jobs: patchPath: patchPath, }; } + const os = require("os"); class ReadBuffer { constructor() { this._buffer = null; @@ -1333,6 +1334,15 @@ jobs: } }; } + function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; + } function createWriteMessageFunction(server) { return obj => { const json = JSON.stringify(obj); @@ -1371,6 +1381,7 @@ jobs: serverInfo, tools: {}, debug: () => {}, + debugError: () => {}, writeMessage: () => {}, replyResult: () => {}, replyError: () => {}, @@ -1380,11 +1391,222 @@ jobs: logFileInitialized: false, }; server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); return server; } + function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + try { + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; + } + function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + fs.writeFileSync(outputFile, ""); + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + execFile( + scriptPath, + [], + { + env, + timeout: 300000, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + reject(error); + return; + } + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + } + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; + } + function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + tool.handlerPath = handlerPath; + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + if (ext === ".sh") { + server.debug(` [${toolName}] Detected shell script handler`); + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + } + } + tool.handler = createShellHandler(server, toolName, resolvedPath); + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + server.debug(` [${toolName}] Loading JavaScript handler module`); + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + let handlerFn = handlerModule; + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + tool.handler = createWrappedHandler(server, toolName, handlerFn); + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + return tools; + } function registerTool(server, tool) { const normalizedName = normalizeTool(tool.name); server.tools[normalizedName] = { @@ -1396,7 +1618,7 @@ jobs: function normalizeTool(name) { return name.replace(/-/g, "_").toLowerCase(); } - function handleMessage(server, req, defaultHandler) { + async function handleMessage(server, req, defaultHandler) { if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); return; @@ -1465,7 +1687,9 @@ jobs: return; } } - const result = handler(args); + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -1477,7 +1701,7 @@ jobs: server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); } } - function processReadBuffer(server, defaultHandler) { + async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -1485,7 +1709,7 @@ jobs: break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); } @@ -1498,9 +1722,9 @@ jobs: if (!Object.keys(server.tools).length) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); diff --git a/pkg/workflow/js/mcp_server_core.cjs b/pkg/workflow/js/mcp_server_core.cjs index 2136c2b8c17..61e9c5d9e49 100644 --- a/pkg/workflow/js/mcp_server_core.cjs +++ b/pkg/workflow/js/mcp_server_core.cjs @@ -22,6 +22,8 @@ const fs = require("fs"); const path = require("path"); +const { execFile } = require("child_process"); +const os = require("os"); const { ReadBuffer } = require("./read_buffer.cjs"); @@ -39,6 +41,7 @@ const encoder = new TextEncoder(); * @property {string} description - Tool description * @property {Object} inputSchema - JSON Schema for tool inputs * @property {Function} [handler] - Tool handler function + * @property {string} [handlerPath] - Optional file path to handler module (original path from config) */ /** @@ -46,6 +49,7 @@ const encoder = new TextEncoder(); * @property {ServerInfo} serverInfo - Server information * @property {Object} tools - Registered tools * @property {Function} debug - Debug logging function + * @property {Function} debugError - Debug logging function for errors (extracts message from Error objects) * @property {Function} writeMessage - Write message to stdout * @property {Function} replyResult - Send a result response * @property {Function} replyError - Send an error response @@ -106,6 +110,21 @@ function createDebugFunction(server) { }; } +/** + * Create a debugError function for the server that handles error casting + * @param {MCPServer} server - The MCP server instance + * @returns {Function} Debug error function that extracts message from Error objects + */ +function createDebugErrorFunction(server) { + return (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + server.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + server.debug(`${prefix}Stack trace: ${error.stack}`); + } + }; +} + /** * Create a writeMessage function for the server * @param {MCPServer} server - The MCP server instance @@ -173,6 +192,7 @@ function createServer(serverInfo, options = {}) { serverInfo, tools: {}, debug: () => {}, // placeholder + debugError: () => {}, // placeholder writeMessage: () => {}, // placeholder replyResult: () => {}, // placeholder replyError: () => {}, // placeholder @@ -184,6 +204,7 @@ function createServer(serverInfo, options = {}) { // Initialize functions with references to server server.debug = createDebugFunction(server); + server.debugError = createDebugErrorFunction(server); server.writeMessage = createWriteMessageFunction(server); server.replyResult = createReplyResultFunction(server); server.replyError = createReplyErrorFunction(server); @@ -191,6 +212,343 @@ function createServer(serverInfo, options = {}) { return server; } +/** + * Create a wrapped handler function that normalizes results to MCP format. + * Extracted to avoid creating closures with excessive scope in loadToolHandlers. + * + * @param {MCPServer} server - The MCP server instance for logging + * @param {string} toolName - Name of the tool for logging purposes + * @param {Function} handlerFn - The original handler function to wrap + * @returns {Function} Wrapped async handler function + */ +function createWrappedHandler(server, toolName, handlerFn) { + return async args => { + server.debug(` [${toolName}] Invoking handler with args: ${JSON.stringify(args)}`); + + try { + // Call the handler (may be sync or async) + const result = await Promise.resolve(handlerFn(args)); + server.debug(` [${toolName}] Handler returned result type: ${typeof result}`); + + // If the result is already in MCP format (has content array), return as-is + if (result && typeof result === "object" && Array.isArray(result.content)) { + server.debug(` [${toolName}] Result is already in MCP format`); + return result; + } + + // Otherwise, serialize the result to text + // Use try-catch for serialization to handle circular references and non-serializable values + let serializedResult; + try { + serializedResult = JSON.stringify(result); + } catch (serializationError) { + server.debugError(` [${toolName}] Serialization error: `, serializationError); + // Fall back to String() for non-serializable values + serializedResult = String(result); + } + server.debug(` [${toolName}] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length > 200 ? "..." : ""}`); + + return { + content: [ + { + type: "text", + text: serializedResult, + }, + ], + }; + } catch (error) { + server.debugError(` [${toolName}] Handler threw error: `, error); + throw error; + } + }; +} + +/** + * Create a shell script handler function that executes a .sh file. + * Uses GitHub Actions convention for passing inputs and reading outputs: + * - Inputs are passed as environment variables prefixed with INPUT_ (uppercased) + * - Outputs are read from a temporary file specified by GITHUB_OUTPUT env var + * - Output format is key=value per line + * + * @param {MCPServer} server - The MCP server instance for logging + * @param {string} toolName - Name of the tool for logging purposes + * @param {string} scriptPath - Path to the shell script to execute + * @returns {Function} Async handler function that executes the shell script + */ +function createShellHandler(server, toolName, scriptPath) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + + // Create environment variables from args (GitHub Actions convention: INPUT_NAME) + const env = { ...process.env }; + for (const [key, value] of Object.entries(args || {})) { + const envKey = `INPUT_${key.toUpperCase().replace(/-/g, "_")}`; + env[envKey] = String(value); + server.debug(` [${toolName}] Set env: ${envKey}=${String(value).substring(0, 100)}${String(value).length > 100 ? "..." : ""}`); + } + + // Create a temporary file for outputs (GitHub Actions convention: GITHUB_OUTPUT) + const outputFile = path.join(os.tmpdir(), `mcp-shell-output-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`); + env.GITHUB_OUTPUT = outputFile; + server.debug(` [${toolName}] Output file: ${outputFile}`); + + // Create the output file (empty) + fs.writeFileSync(outputFile, ""); + + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing shell script...`); + + execFile( + scriptPath, + [], + { + env, + timeout: 300000, // 5 minute timeout + maxBuffer: 10 * 1024 * 1024, // 10MB buffer + }, + (error, stdout, stderr) => { + // Log stdout and stderr + if (stdout) { + server.debug(` [${toolName}] stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? "..." : ""}`); + } + if (stderr) { + server.debug(` [${toolName}] stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? "..." : ""}`); + } + + if (error) { + server.debugError(` [${toolName}] Shell script error: `, error); + + // Clean up output file + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + // Ignore cleanup errors + } + + reject(error); + return; + } + + // Read outputs from the GITHUB_OUTPUT file + /** @type {Record} */ + const outputs = {}; + try { + if (fs.existsSync(outputFile)) { + const outputContent = fs.readFileSync(outputFile, "utf-8"); + server.debug( + ` [${toolName}] Output file content: ${outputContent.substring(0, 500)}${outputContent.length > 500 ? "..." : ""}` + ); + + // Parse outputs (key=value format, one per line) + const lines = outputContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && trimmed.includes("=")) { + const eqIndex = trimmed.indexOf("="); + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + outputs[key] = value; + server.debug(` [${toolName}] Parsed output: ${key}=${value.substring(0, 100)}${value.length > 100 ? "..." : ""}`); + } + } + } + } catch (readError) { + server.debugError(` [${toolName}] Error reading output file: `, readError); + } + + // Clean up output file + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + } catch { + // Ignore cleanup errors + } + + // Build the result + const result = { + stdout: stdout || "", + stderr: stderr || "", + outputs, + }; + + server.debug(` [${toolName}] Shell handler completed, outputs: ${Object.keys(outputs).join(", ") || "(none)"}`); + + // Return MCP format + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + }); + }; +} + +/** + * Load handler functions from file paths specified in tools configuration. + * This function iterates through tools and loads handler modules based on file extension: + * + * For JavaScript handlers (.js, .cjs, .mjs): + * - Uses require() to load the module + * - Handler must export a function as default export + * - Handler signature: async function handler(args: Record): Promise + * + * For Shell script handlers (.sh): + * - Uses GitHub Actions convention for passing inputs/outputs + * - Inputs are passed as environment variables prefixed with INPUT_ (uppercased) + * - Outputs are read from GITHUB_OUTPUT file (key=value format per line) + * - Returns: { stdout, stderr, outputs } + * + * SECURITY NOTE: Handler paths are loaded from tools.json configuration file, + * which should be controlled by the server administrator. When basePath is provided, + * relative paths are resolved within it, preventing directory traversal outside + * the intended directory. Absolute paths bypass this validation but are still + * logged for auditing purposes. + * + * @param {MCPServer} server - The MCP server instance for logging + * @param {Array} tools - Array of tool configurations from tools.json + * @param {string} [basePath] - Optional base path for resolving relative handler paths. + * When provided, relative paths are validated to be within this directory. + * @returns {Array} The tools array with loaded handlers attached + */ +function loadToolHandlers(server, tools, basePath) { + server.debug(`Loading tool handlers...`); + server.debug(` Total tools to process: ${tools.length}`); + server.debug(` Base path: ${basePath || "(not specified)"}`); + + let loadedCount = 0; + let skippedCount = 0; + let errorCount = 0; + + for (const tool of tools) { + const toolName = tool.name || "(unnamed)"; + + // Check if tool has a handler path specified + if (!tool.handler) { + server.debug(` [${toolName}] No handler path specified, skipping handler load`); + skippedCount++; + continue; + } + + const handlerPath = tool.handler; + server.debug(` [${toolName}] Handler path specified: ${handlerPath}`); + + // Resolve the handler path + let resolvedPath = handlerPath; + if (basePath && !path.isAbsolute(handlerPath)) { + resolvedPath = path.resolve(basePath, handlerPath); + server.debug(` [${toolName}] Resolved relative path to: ${resolvedPath}`); + + // Security validation: Ensure resolved path is within basePath to prevent directory traversal + const normalizedBase = path.resolve(basePath); + const normalizedResolved = path.resolve(resolvedPath); + if (!normalizedResolved.startsWith(normalizedBase + path.sep) && normalizedResolved !== normalizedBase) { + server.debug(` [${toolName}] ERROR: Handler path escapes base directory: ${resolvedPath} is not within ${basePath}`); + errorCount++; + continue; + } + } else if (path.isAbsolute(handlerPath)) { + server.debug(` [${toolName}] Using absolute path (bypasses basePath validation): ${handlerPath}`); + } + + // Store the original handler path for reference + tool.handlerPath = handlerPath; + + try { + server.debug(` [${toolName}] Loading handler from: ${resolvedPath}`); + + // Check if file exists before loading + if (!fs.existsSync(resolvedPath)) { + server.debug(` [${toolName}] ERROR: Handler file does not exist: ${resolvedPath}`); + errorCount++; + continue; + } + + // Detect handler type by file extension + const ext = path.extname(resolvedPath).toLowerCase(); + server.debug(` [${toolName}] Handler file extension: ${ext}`); + + if (ext === ".sh") { + // Shell script handler - use GitHub Actions convention + server.debug(` [${toolName}] Detected shell script handler`); + + // Make sure the script is executable (on Unix-like systems) + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Shell script is executable`); + } catch { + // Try to make it executable + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made shell script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make shell script executable: `, chmodError); + // Continue anyway - it might work depending on the shell + } + } + + // Create the shell handler + tool.handler = createShellHandler(server, toolName, resolvedPath); + + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully`); + } else { + // JavaScript/CommonJS handler - use require() + server.debug(` [${toolName}] Loading JavaScript handler module`); + + // Load the handler module + const handlerModule = require(resolvedPath); + server.debug(` [${toolName}] Handler module loaded successfully`); + server.debug(` [${toolName}] Module type: ${typeof handlerModule}`); + + // Get the handler function (support default export patterns) + let handlerFn = handlerModule; + + // Handle ES module default export pattern (module.default) + if (handlerModule && typeof handlerModule === "object" && typeof handlerModule.default === "function") { + handlerFn = handlerModule.default; + server.debug(` [${toolName}] Using module.default export`); + } + + // Validate that the handler is a function + if (typeof handlerFn !== "function") { + server.debug(` [${toolName}] ERROR: Handler is not a function, got: ${typeof handlerFn}`); + server.debug(` [${toolName}] Module keys: ${Object.keys(handlerModule || {}).join(", ") || "(none)"}`); + errorCount++; + continue; + } + + server.debug(` [${toolName}] Handler function validated successfully`); + server.debug(` [${toolName}] Handler function name: ${handlerFn.name || "(anonymous)"}`); + + // Wrap the handler using the separate function to avoid bloating the closure + tool.handler = createWrappedHandler(server, toolName, handlerFn); + + loadedCount++; + server.debug(` [${toolName}] JavaScript handler loaded and wrapped successfully`); + } + } catch (error) { + server.debugError(` [${toolName}] ERROR loading handler: `, error); + errorCount++; + } + } + + server.debug(`Handler loading complete:`); + server.debug(` Loaded: ${loadedCount}`); + server.debug(` Skipped (no handler path): ${skippedCount}`); + server.debug(` Errors: ${errorCount}`); + + return tools; +} + /** * Register a tool with the server * @param {MCPServer} server - The MCP server instance @@ -219,8 +577,9 @@ function normalizeTool(name) { * @param {MCPServer} server - The MCP server instance * @param {Object} req - The incoming request * @param {Function} [defaultHandler] - Default handler for tools without a handler + * @returns {Promise} */ -function handleMessage(server, req, defaultHandler) { +async function handleMessage(server, req, defaultHandler) { // Validate basic JSON-RPC structure if (!req || typeof req !== "object") { server.debug(`Invalid message: not an object`); @@ -298,7 +657,11 @@ function handleMessage(server, req, defaultHandler) { return; } } - const result = handler(args); + + // Call handler and await the result (supports both sync and async handlers) + server.debug(`Calling handler for tool: ${name}`); + const result = await Promise.resolve(handler(args)); + server.debug(`Handler returned for tool: ${name}`); const content = result && result.content ? result.content : []; server.replyResult(id, { content, isError: false }); } else if (/^notifications\//.test(method)) { @@ -315,8 +678,9 @@ function handleMessage(server, req, defaultHandler) { * Process the read buffer and handle messages * @param {MCPServer} server - The MCP server instance * @param {Function} [defaultHandler] - Default handler for tools without a handler + * @returns {Promise} */ -function processReadBuffer(server, defaultHandler) { +async function processReadBuffer(server, defaultHandler) { while (true) { try { const message = server.readBuffer.readMessage(); @@ -324,7 +688,7 @@ function processReadBuffer(server, defaultHandler) { break; } server.debug(`recv: ${JSON.stringify(message)}`); - handleMessage(server, message, defaultHandler); + await handleMessage(server, message, defaultHandler); } catch (error) { // For parse errors, we can't know the request id, so we shouldn't send a response // according to JSON-RPC spec. Just log the error. @@ -349,9 +713,9 @@ function start(server, options = {}) { throw new Error("No tools registered"); } - const onData = chunk => { + const onData = async chunk => { server.readBuffer.append(chunk); - processReadBuffer(server, defaultHandler); + await processReadBuffer(server, defaultHandler); }; process.stdin.on("data", onData); @@ -367,4 +731,5 @@ module.exports = { handleMessage, processReadBuffer, start, + loadToolHandlers, }; diff --git a/pkg/workflow/js/mcp_server_core.test.cjs b/pkg/workflow/js/mcp_server_core.test.cjs index 644c2f6812a..3c630954ec4 100644 --- a/pkg/workflow/js/mcp_server_core.test.cjs +++ b/pkg/workflow/js/mcp_server_core.test.cjs @@ -141,7 +141,7 @@ describe("mcp_server_core.cjs", () => { it("should handle initialize method", async () => { const { handleMessage } = await import("./mcp_server_core.cjs"); - handleMessage(server, { + await handleMessage(server, { jsonrpc: "2.0", id: 1, method: "initialize", @@ -157,7 +157,7 @@ describe("mcp_server_core.cjs", () => { it("should handle tools/list method", async () => { const { handleMessage } = await import("./mcp_server_core.cjs"); - handleMessage(server, { + await handleMessage(server, { jsonrpc: "2.0", id: 1, method: "tools/list", @@ -171,7 +171,7 @@ describe("mcp_server_core.cjs", () => { it("should handle tools/call method with handler", async () => { const { handleMessage } = await import("./mcp_server_core.cjs"); - handleMessage(server, { + await handleMessage(server, { jsonrpc: "2.0", id: 1, method: "tools/call", @@ -189,7 +189,7 @@ describe("mcp_server_core.cjs", () => { it("should return error for unknown tool", async () => { const { handleMessage } = await import("./mcp_server_core.cjs"); - handleMessage(server, { + await handleMessage(server, { jsonrpc: "2.0", id: 1, method: "tools/call", @@ -207,7 +207,7 @@ describe("mcp_server_core.cjs", () => { it("should return error for missing required fields", async () => { const { handleMessage } = await import("./mcp_server_core.cjs"); - handleMessage(server, { + await handleMessage(server, { jsonrpc: "2.0", id: 1, method: "tools/call", @@ -225,7 +225,7 @@ describe("mcp_server_core.cjs", () => { it("should return error for unknown method", async () => { const { handleMessage } = await import("./mcp_server_core.cjs"); - handleMessage(server, { + await handleMessage(server, { jsonrpc: "2.0", id: 1, method: "unknown/method", @@ -239,7 +239,7 @@ describe("mcp_server_core.cjs", () => { it("should ignore notifications (no response)", async () => { const { handleMessage } = await import("./mcp_server_core.cjs"); - handleMessage(server, { + await handleMessage(server, { jsonrpc: "2.0", method: "notifications/initialized", // no id - this is a notification @@ -251,7 +251,7 @@ describe("mcp_server_core.cjs", () => { it("should validate JSON-RPC version", async () => { const { handleMessage } = await import("./mcp_server_core.cjs"); - handleMessage(server, { + await handleMessage(server, { jsonrpc: "1.0", // wrong version id: 1, method: "test", @@ -275,7 +275,7 @@ describe("mcp_server_core.cjs", () => { content: [{ type: "text", text: `default handler for ${type}` }], }); - handleMessage( + await handleMessage( server, { jsonrpc: "2.0", @@ -293,4 +293,608 @@ describe("mcp_server_core.cjs", () => { expect(results[0].result.content[0].text).toBe("default handler for no_handler_tool"); }); }); + + describe("loadToolHandlers", () => { + let server; + const fs = require("fs"); + const path = require("path"); + const os = require("os"); + let tempDir; + + beforeEach(async () => { + vi.resetModules(); + + // Suppress stderr output during tests + vi.spyOn(process.stderr, "write").mockImplementation(() => true); + + const { createServer } = await import("./mcp_server_core.cjs"); + server = createServer({ name: "test-server", version: "1.0.0" }); + + // Create a temporary directory for test handler files + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-test-handlers-")); + }); + + afterEach(() => { + // Clean up temporary directory + if (tempDir && fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true }); + } + }); + + it("should load a sync handler from file path", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a test handler file + const handlerPath = path.join(tempDir, "sync_handler.cjs"); + fs.writeFileSync( + handlerPath, + `module.exports = function(args) { + return { result: "sync result: " + args.input }; + };` + ); + + // Create tool with handler path + const tools = [ + { + name: "test_sync_tool", + description: "A tool with sync handler", + inputSchema: { type: "object", properties: { input: { type: "string" } } }, + handler: handlerPath, + }, + ]; + + // Load handlers + loadToolHandlers(server, tools, tempDir); + + // Verify handler was loaded + expect(typeof tools[0].handler).toBe("function"); + + // Register and call tool + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_sync_tool", arguments: { input: "hello" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].result.content[0].text).toContain("sync result: hello"); + }); + + it("should load an async handler from file path", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a test async handler file + const handlerPath = path.join(tempDir, "async_handler.cjs"); + fs.writeFileSync( + handlerPath, + `module.exports = async function(args) { + await new Promise(resolve => setTimeout(resolve, 10)); + return { result: "async result: " + args.input }; + };` + ); + + // Create tool with handler path + const tools = [ + { + name: "test_async_tool", + description: "A tool with async handler", + inputSchema: { type: "object", properties: { input: { type: "string" } } }, + handler: handlerPath, + }, + ]; + + // Load handlers + loadToolHandlers(server, tools, tempDir); + + // Verify handler was loaded + expect(typeof tools[0].handler).toBe("function"); + + // Register and call tool + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_async_tool", arguments: { input: "world" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].result.content[0].text).toContain("async result: world"); + }); + + it("should handle handler that returns MCP format directly", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a handler that returns MCP format + const handlerPath = path.join(tempDir, "mcp_format_handler.cjs"); + fs.writeFileSync( + handlerPath, + `module.exports = function(args) { + return { + content: [{ type: "text", text: "MCP format: " + args.input }] + }; + };` + ); + + const tools = [ + { + name: "test_mcp_format", + description: "A tool returning MCP format", + inputSchema: { type: "object", properties: { input: { type: "string" } } }, + handler: handlerPath, + }, + ]; + + loadToolHandlers(server, tools, tempDir); + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_mcp_format", arguments: { input: "test" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].result.content[0].text).toBe("MCP format: test"); + }); + + it("should handle handler with module.default export pattern", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a handler with default export pattern + const handlerPath = path.join(tempDir, "default_export_handler.cjs"); + fs.writeFileSync( + handlerPath, + `module.exports = { + default: function(args) { + return { result: "default export: " + args.input }; + } + };` + ); + + const tools = [ + { + name: "test_default_export", + description: "A tool with default export", + inputSchema: { type: "object", properties: { input: { type: "string" } } }, + handler: handlerPath, + }, + ]; + + loadToolHandlers(server, tools, tempDir); + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_default_export", arguments: { input: "hi" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].result.content[0].text).toContain("default export: hi"); + }); + + it("should skip tools without handler path", async () => { + const { loadToolHandlers } = await import("./mcp_server_core.cjs"); + + const tools = [ + { + name: "tool_without_handler", + description: "A tool without handler path", + inputSchema: { type: "object", properties: {} }, + }, + ]; + + const result = loadToolHandlers(server, tools, tempDir); + + // Handler should remain undefined + expect(tools[0].handler).toBeUndefined(); + expect(result).toBe(tools); // Should return the same array + }); + + it("should handle non-existent handler file", async () => { + const { loadToolHandlers } = await import("./mcp_server_core.cjs"); + + const tools = [ + { + name: "tool_with_missing_handler", + description: "A tool with missing handler file", + inputSchema: { type: "object", properties: {} }, + handler: "/non/existent/handler.cjs", + }, + ]; + + // Should not throw, but handler should remain the string path (or be updated to undefined) + loadToolHandlers(server, tools, tempDir); + + // Handler should still be the original string (load failed) + expect(tools[0].handler).toBe("/non/existent/handler.cjs"); + }); + + it("should handle handler that is not a function", async () => { + const { loadToolHandlers } = await import("./mcp_server_core.cjs"); + + // Create a handler file that exports an object, not a function + const handlerPath = path.join(tempDir, "not_a_function.cjs"); + fs.writeFileSync(handlerPath, `module.exports = { notAFunction: true };`); + + const tools = [ + { + name: "tool_with_invalid_handler", + description: "A tool with invalid handler", + inputSchema: { type: "object", properties: {} }, + handler: handlerPath, + }, + ]; + + loadToolHandlers(server, tools, tempDir); + + // Handler should remain the original string (validation failed) + expect(tools[0].handler).toBe(handlerPath); + }); + + it("should handle handler that throws error", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a handler that throws an error + const handlerPath = path.join(tempDir, "error_handler.cjs"); + fs.writeFileSync( + handlerPath, + `module.exports = function(args) { + throw new Error("Handler error: " + args.input); + };` + ); + + const tools = [ + { + name: "test_error_handler", + description: "A tool that throws", + inputSchema: { type: "object", properties: { input: { type: "string" } } }, + handler: handlerPath, + }, + ]; + + loadToolHandlers(server, tools, tempDir); + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_error_handler", arguments: { input: "oops" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].error.code).toBe(-32603); + expect(results[0].error.message).toContain("Handler error: oops"); + }); + + it("should resolve relative paths from basePath", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a handler in a subdirectory + const subDir = path.join(tempDir, "handlers"); + fs.mkdirSync(subDir, { recursive: true }); + const handlerPath = path.join(subDir, "relative_handler.cjs"); + fs.writeFileSync( + handlerPath, + `module.exports = function(args) { + return { result: "relative: " + args.input }; + };` + ); + + // Use relative path in tool definition + const tools = [ + { + name: "test_relative_path", + description: "A tool with relative handler path", + inputSchema: { type: "object", properties: { input: { type: "string" } } }, + handler: "handlers/relative_handler.cjs", + }, + ]; + + loadToolHandlers(server, tools, tempDir); + + expect(typeof tools[0].handler).toBe("function"); + + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_relative_path", arguments: { input: "path" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].result.content[0].text).toContain("relative: path"); + }); + + it("should prevent directory traversal attacks with relative paths", async () => { + const { loadToolHandlers } = await import("./mcp_server_core.cjs"); + + // Create a handler outside of tempDir + const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-outside-")); + const outsideHandlerPath = path.join(outsideDir, "outside_handler.cjs"); + fs.writeFileSync(outsideHandlerPath, `module.exports = function() { return "should not be loaded"; };`); + + // Use relative path that tries to escape basePath + const tools = [ + { + name: "traversal_attack_tool", + description: "A tool trying to escape basePath", + inputSchema: { type: "object", properties: {} }, + handler: "../../../" + path.basename(outsideDir) + "/outside_handler.cjs", + }, + ]; + + loadToolHandlers(server, tools, tempDir); + + // Handler should remain the original string (validation blocked it) + expect(tools[0].handler).toBe("../../../" + path.basename(outsideDir) + "/outside_handler.cjs"); + + // Clean up + fs.rmSync(outsideDir, { recursive: true }); + }); + + it("should handle handler returning non-serializable value (circular reference)", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a handler that returns a circular reference + const handlerPath = path.join(tempDir, "circular_handler.cjs"); + fs.writeFileSync( + handlerPath, + `module.exports = function(args) { + const obj = { input: args.input }; + obj.self = obj; // Circular reference + return obj; + };` + ); + + const tools = [ + { + name: "test_circular", + description: "A tool returning circular reference", + inputSchema: { type: "object", properties: { input: { type: "string" } } }, + handler: handlerPath, + }, + ]; + + loadToolHandlers(server, tools, tempDir); + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_circular", arguments: { input: "test" } }, + }); + + // Should fall back to String() serialization for circular references + expect(results).toHaveLength(1); + expect(results[0].result.content[0].text).toBe("[object Object]"); + }); + + it("should load and execute shell script handler", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a shell script handler + const handlerPath = path.join(tempDir, "test_handler.sh"); + fs.writeFileSync( + handlerPath, + `#!/bin/bash +echo "Hello from shell script" +echo "Input was: $INPUT_NAME" +echo "result=success" >> $GITHUB_OUTPUT +`, + { mode: 0o755 } + ); + + const tools = [ + { + name: "test_shell", + description: "A shell script tool", + inputSchema: { type: "object", properties: { name: { type: "string" } } }, + handler: handlerPath, + }, + ]; + + loadToolHandlers(server, tools, tempDir); + + expect(typeof tools[0].handler).toBe("function"); + + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_shell", arguments: { name: "world" } }, + }); + + expect(results).toHaveLength(1); + const resultContent = JSON.parse(results[0].result.content[0].text); + expect(resultContent.stdout).toContain("Hello from shell script"); + expect(resultContent.stdout).toContain("Input was: world"); + expect(resultContent.outputs.result).toBe("success"); + }); + + it("should handle shell script with multiple outputs", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a shell script with multiple outputs + const handlerPath = path.join(tempDir, "multi_output.sh"); + fs.writeFileSync( + handlerPath, + `#!/bin/bash +echo "first=value1" >> $GITHUB_OUTPUT +echo "second=value2" >> $GITHUB_OUTPUT +echo "third=value with spaces" >> $GITHUB_OUTPUT +`, + { mode: 0o755 } + ); + + const tools = [ + { + name: "test_multi_output", + description: "Shell script with multiple outputs", + inputSchema: { type: "object", properties: {} }, + handler: handlerPath, + }, + ]; + + loadToolHandlers(server, tools, tempDir); + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_multi_output", arguments: {} }, + }); + + expect(results).toHaveLength(1); + const resultContent = JSON.parse(results[0].result.content[0].text); + expect(resultContent.outputs.first).toBe("value1"); + expect(resultContent.outputs.second).toBe("value2"); + expect(resultContent.outputs.third).toBe("value with spaces"); + }); + + it("should handle shell script errors", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a shell script that exits with error + const handlerPath = path.join(tempDir, "error_handler.sh"); + fs.writeFileSync( + handlerPath, + `#!/bin/bash +echo "About to fail" >&2 +exit 1 +`, + { mode: 0o755 } + ); + + const tools = [ + { + name: "test_shell_error", + description: "Shell script that errors", + inputSchema: { type: "object", properties: {} }, + handler: handlerPath, + }, + ]; + + loadToolHandlers(server, tools, tempDir); + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_shell_error", arguments: {} }, + }); + + // Should receive an error response + expect(results).toHaveLength(1); + expect(results[0].error).toBeDefined(); + }); + + it("should convert input names with dashes to underscores", async () => { + const { loadToolHandlers, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + // Create a shell script that echoes input env vars + const handlerPath = path.join(tempDir, "env_handler.sh"); + fs.writeFileSync( + handlerPath, + `#!/bin/bash +echo "my-input value: $INPUT_MY_INPUT" +echo "result=$INPUT_MY_INPUT" >> $GITHUB_OUTPUT +`, + { mode: 0o755 } + ); + + const tools = [ + { + name: "test_env_conversion", + description: "Tests env var conversion", + inputSchema: { type: "object", properties: { "my-input": { type: "string" } } }, + handler: handlerPath, + }, + ]; + + loadToolHandlers(server, tools, tempDir); + registerTool(server, tools[0]); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test_env_conversion", arguments: { "my-input": "test-value" } }, + }); + + expect(results).toHaveLength(1); + const resultContent = JSON.parse(results[0].result.content[0].text); + expect(resultContent.outputs.result).toBe("test-value"); + }); + }); }); diff --git a/pkg/workflow/schemas/mcp-tools.json b/pkg/workflow/schemas/mcp-tools.json index 972d82c715f..bfb0cdc4046 100644 --- a/pkg/workflow/schemas/mcp-tools.json +++ b/pkg/workflow/schemas/mcp-tools.json @@ -35,6 +35,10 @@ "annotations": { "$ref": "#/definitions/ToolAnnotations", "description": "Optional annotations providing hints about the tool's behavior" + }, + "handler": { + "type": "string", + "description": "Optional file path to a handler module that exports a function to handle tool calls. The path can be resolved by require() at runtime. The handler function signature is: async function handler(args: Record): Promise" } }, "additionalProperties": false