From 4d055ad49a3c8fb9f28f3d322595a919596d02c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 06:52:54 +0000 Subject: [PATCH 1/6] Initial plan From 48293ab954b78ad124b0fa18f8fab5e7e0fe5cac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 06:58:56 +0000 Subject: [PATCH 2/6] Initial plan: Refactor setup-safe-inputs to use require() instead of embedded FILES Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agent-performance-analyzer.lock.yml | 4 +--- .github/workflows/campaign-manager.lock.yml | 4 +--- .github/workflows/daily-file-diet.lock.yml | 4 +--- .github/workflows/daily-firewall-report.lock.yml | 4 +--- .github/workflows/deep-report.lock.yml | 4 +--- .github/workflows/human-ai-collaboration.lock.yml | 4 +--- .github/workflows/incident-response.lock.yml | 4 +--- .github/workflows/intelligence.lock.yml | 4 +--- .github/workflows/org-wide-rollout.lock.yml | 4 +--- .github/workflows/poem-bot.lock.yml | 4 +--- .github/workflows/security-compliance.lock.yml | 4 +--- .github/workflows/spec-kit-execute.lock.yml | 4 +--- .github/workflows/spec-kit-executor.lock.yml | 4 +--- .github/workflows/workflow-health-manager.lock.yml | 4 +--- 14 files changed, 14 insertions(+), 42 deletions(-) diff --git a/.github/workflows/agent-performance-analyzer.lock.yml b/.github/workflows/agent-performance-analyzer.lock.yml index b9b278ff4f4..f5cbadfb003 100644 --- a/.github/workflows/agent-performance-analyzer.lock.yml +++ b/.github/workflows/agent-performance-analyzer.lock.yml @@ -5550,9 +5550,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/campaign-manager.lock.yml b/.github/workflows/campaign-manager.lock.yml index ae7af8ccbec..337a5ec38e5 100644 --- a/.github/workflows/campaign-manager.lock.yml +++ b/.github/workflows/campaign-manager.lock.yml @@ -5418,9 +5418,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml index 2e9ba791aa2..3bee3dc0603 100644 --- a/.github/workflows/daily-file-diet.lock.yml +++ b/.github/workflows/daily-file-diet.lock.yml @@ -5938,9 +5938,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/daily-firewall-report.lock.yml b/.github/workflows/daily-firewall-report.lock.yml index e5c02db0b03..0ec5595366a 100644 --- a/.github/workflows/daily-firewall-report.lock.yml +++ b/.github/workflows/daily-firewall-report.lock.yml @@ -5261,9 +5261,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/deep-report.lock.yml b/.github/workflows/deep-report.lock.yml index 313f3811ecf..3081ffaa8f6 100644 --- a/.github/workflows/deep-report.lock.yml +++ b/.github/workflows/deep-report.lock.yml @@ -5303,9 +5303,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/human-ai-collaboration.lock.yml b/.github/workflows/human-ai-collaboration.lock.yml index f1c0c537929..d9d3efd68a4 100644 --- a/.github/workflows/human-ai-collaboration.lock.yml +++ b/.github/workflows/human-ai-collaboration.lock.yml @@ -5270,9 +5270,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/incident-response.lock.yml b/.github/workflows/incident-response.lock.yml index 7842e3acd27..2bf86ef7126 100644 --- a/.github/workflows/incident-response.lock.yml +++ b/.github/workflows/incident-response.lock.yml @@ -5428,9 +5428,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/intelligence.lock.yml b/.github/workflows/intelligence.lock.yml index c045400fc41..aaa9792fe7b 100644 --- a/.github/workflows/intelligence.lock.yml +++ b/.github/workflows/intelligence.lock.yml @@ -5963,9 +5963,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/org-wide-rollout.lock.yml b/.github/workflows/org-wide-rollout.lock.yml index c3c39532f7a..686dbc3bb21 100644 --- a/.github/workflows/org-wide-rollout.lock.yml +++ b/.github/workflows/org-wide-rollout.lock.yml @@ -5456,9 +5456,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 3832223397b..c9b64a23cde 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -5883,9 +5883,7 @@ jobs: core.summary.addRaw(summaryContent); await core.summary.write(); } - main().catch(error => { - core.setFailed(error instanceof Error ? error.message : String(error)); - }); + module.exports = { main }; update_cache_memory: needs: diff --git a/.github/workflows/security-compliance.lock.yml b/.github/workflows/security-compliance.lock.yml index bb996d17c36..7dc90c358b3 100644 --- a/.github/workflows/security-compliance.lock.yml +++ b/.github/workflows/security-compliance.lock.yml @@ -5090,9 +5090,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/spec-kit-execute.lock.yml b/.github/workflows/spec-kit-execute.lock.yml index f260f4c4bbb..1f66b921068 100644 --- a/.github/workflows/spec-kit-execute.lock.yml +++ b/.github/workflows/spec-kit-execute.lock.yml @@ -5208,9 +5208,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/spec-kit-executor.lock.yml b/.github/workflows/spec-kit-executor.lock.yml index d2b1a31fc7a..42d58c28778 100644 --- a/.github/workflows/spec-kit-executor.lock.yml +++ b/.github/workflows/spec-kit-executor.lock.yml @@ -5083,9 +5083,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: diff --git a/.github/workflows/workflow-health-manager.lock.yml b/.github/workflows/workflow-health-manager.lock.yml index be1cb8ee76a..46caf57b620 100644 --- a/.github/workflows/workflow-health-manager.lock.yml +++ b/.github/workflows/workflow-health-manager.lock.yml @@ -5404,9 +5404,7 @@ jobs: return; } } - main().catch(error => { - core.setFailed(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`); - }); + module.exports = { main }; safe_outputs: needs: From af60eb5ad4790af11277f09b99a930bbc79899a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 07:04:59 +0000 Subject: [PATCH 3/6] Refactor setup-safe-inputs to use require() instead of embedded FILES Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup-safe-inputs/action.yml | 9 +- actions/setup-safe-inputs/copy-files.sh | 82 ++ actions/setup-safe-inputs/index.js | 49 -- .../js/mcp_handler_python.cjs | 100 +++ .../js/mcp_handler_shell.cjs | 146 ++++ actions/setup-safe-inputs/js/mcp_logger.cjs | 53 ++ .../setup-safe-inputs/js/mcp_server_core.cjs | 747 ++++++++++++++++++ actions/setup-safe-inputs/js/read_buffer.cjs | 67 ++ .../js/safe_inputs_bootstrap.cjs | 80 ++ .../js/safe_inputs_config_loader.cjs | 53 ++ .../js/safe_inputs_mcp_server.cjs | 113 +++ .../js/safe_inputs_tool_factory.cjs | 37 + .../js/safe_inputs_validation.cjs | 32 + actions/setup/js/check_command_position.cjs | 2 +- actions/setup/js/check_membership.cjs | 3 +- actions/setup/js/check_skip_if_match.cjs | 3 +- actions/setup/js/check_stop_time.cjs | 3 +- .../setup/js/close_expired_discussions.cjs | 2 +- actions/setup/js/close_expired_issues.cjs | 2 +- actions/setup/js/collect_ndjson_output.cjs | 3 +- actions/setup/js/interpolate_prompt.cjs | 3 +- actions/setup/js/redact_secrets.cjs | 3 +- actions/setup/js/sanitize_output.cjs | 2 +- actions/setup/js/upload_assets.cjs | 2 +- pkg/cli/actions_build_command.go | 21 +- 25 files changed, 1545 insertions(+), 72 deletions(-) create mode 100755 actions/setup-safe-inputs/copy-files.sh delete mode 100644 actions/setup-safe-inputs/index.js create mode 100644 actions/setup-safe-inputs/js/mcp_handler_python.cjs create mode 100644 actions/setup-safe-inputs/js/mcp_handler_shell.cjs create mode 100644 actions/setup-safe-inputs/js/mcp_logger.cjs create mode 100644 actions/setup-safe-inputs/js/mcp_server_core.cjs create mode 100644 actions/setup-safe-inputs/js/read_buffer.cjs create mode 100644 actions/setup-safe-inputs/js/safe_inputs_bootstrap.cjs create mode 100644 actions/setup-safe-inputs/js/safe_inputs_config_loader.cjs create mode 100644 actions/setup-safe-inputs/js/safe_inputs_mcp_server.cjs create mode 100644 actions/setup-safe-inputs/js/safe_inputs_tool_factory.cjs create mode 100644 actions/setup-safe-inputs/js/safe_inputs_validation.cjs diff --git a/actions/setup-safe-inputs/action.yml b/actions/setup-safe-inputs/action.yml index cdd8e9ad51c..96c76c08520 100644 --- a/actions/setup-safe-inputs/action.yml +++ b/actions/setup-safe-inputs/action.yml @@ -13,8 +13,13 @@ outputs: description: 'Number of files copied' runs: - using: 'node20' - main: 'index.js' + using: 'composite' + steps: + - name: Copy safe-inputs files + shell: bash + run: ${{ github.action_path }}/copy-files.sh + env: + INPUT_DESTINATION: ${{ inputs.destination }} branding: icon: 'copy' diff --git a/actions/setup-safe-inputs/copy-files.sh b/actions/setup-safe-inputs/copy-files.sh new file mode 100755 index 00000000000..25eb6d7030d --- /dev/null +++ b/actions/setup-safe-inputs/copy-files.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Setup Safe Inputs Action +# Copies safe-inputs MCP server files to the agent environment +# +# This script copies JavaScript (.cjs) and JSON files from the js/ directory +# to the destination directory. The js/ directory is created by running +# 'make actions-build' which copies files from pkg/workflow/js/*.cjs +# +# Note: The js/ directory is in .gitignore as it's a build artifact. +# Workflows must ensure 'make actions-build' is run before using this action, +# or the js/ directory must be populated by another mechanism. + +set -e + +# Get destination from input or use default +DESTINATION="${INPUT_DESTINATION:-/tmp/gh-aw/safe-inputs}" + +echo "::notice::Copying safe-inputs files to ${DESTINATION}" + +# Create destination directory if it doesn't exist +mkdir -p "${DESTINATION}" +echo "::notice::Created directory: ${DESTINATION}" + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +JS_SOURCE_DIR="${SCRIPT_DIR}/js" + +echo "::debug::Script directory: ${SCRIPT_DIR}" +echo "::debug::Looking for JavaScript sources in: ${JS_SOURCE_DIR}" + +# Debug: List the contents of the script directory to understand the file layout +echo "::debug::Contents of ${SCRIPT_DIR}:" +ls -la "${SCRIPT_DIR}" || echo "::warning::Failed to list ${SCRIPT_DIR}" + +# Check if js directory exists +if [ ! -d "${JS_SOURCE_DIR}" ]; then + echo "::error::JavaScript source directory not found: ${JS_SOURCE_DIR}" + echo "::error::This typically means 'make actions-build' was not run to populate the js/ directory" + echo "::error::The js/ directory is a build artifact (in .gitignore) and must be created before running this script" + + # Additional debugging: show what's in the parent directory + echo "::debug::Contents of parent directory $(dirname "${SCRIPT_DIR}"):" + ls -la "$(dirname "${SCRIPT_DIR}")" || echo "::warning::Failed to list parent directory" + + exit 1 +fi + +# List files in js directory for debugging +echo "::debug::Files in ${JS_SOURCE_DIR}:" +ls -1 "${JS_SOURCE_DIR}" | head -10 || echo "::warning::Failed to list files in ${JS_SOURCE_DIR}" +FILE_COUNT_IN_DIR=$(ls -1 "${JS_SOURCE_DIR}" 2>/dev/null | wc -l) +echo "::notice::Found ${FILE_COUNT_IN_DIR} files in ${JS_SOURCE_DIR}" + +# Copy all .cjs files from js/ to destination +FILE_COUNT=0 +for file in "${JS_SOURCE_DIR}"/*.cjs; do + if [ -f "$file" ]; then + filename=$(basename "$file") + cp "$file" "${DESTINATION}/${filename}" + echo "::notice::Copied: ${filename}" + FILE_COUNT=$((FILE_COUNT + 1)) + fi +done + +# Copy any .json files as well +for file in "${JS_SOURCE_DIR}"/*.json; do + if [ -f "$file" ]; then + filename=$(basename "$file") + cp "$file" "${DESTINATION}/${filename}" + echo "::notice::Copied: ${filename}" + FILE_COUNT=$((FILE_COUNT + 1)) + fi +done + +echo "::notice::Successfully copied ${FILE_COUNT} files to ${DESTINATION}" + +# Set output +if [ -n "${GITHUB_OUTPUT}" ]; then + echo "files-copied=${FILE_COUNT}" >> "${GITHUB_OUTPUT}" +else + echo "::debug::GITHUB_OUTPUT not set, skipping output" +fi diff --git a/actions/setup-safe-inputs/index.js b/actions/setup-safe-inputs/index.js deleted file mode 100644 index 692f72ac8c1..00000000000 --- a/actions/setup-safe-inputs/index.js +++ /dev/null @@ -1,49 +0,0 @@ -// Safe Inputs Copy Action -// Copies safe-inputs MCP server files to the agent environment - -const core = require('@actions/core'); -const fs = require('fs'); -const path = require('path'); - -// Embedded safe-inputs files will be inserted here during build -const FILES = { - "mcp_logger.cjs": "// @ts-check\n/// \u003creference types=\"@actions/github-script\" /\u003e\n\n/**\n * MCP Logger Utility\n *\n * This module provides logger creation utilities for MCP servers.\n * It creates logger objects with debug and debugError methods that write\n * timestamped messages to stderr.\n *\n * Usage:\n * const { createLogger } = require(\"./mcp_logger.cjs\");\n * const logger = createLogger(\"my-server\");\n * logger.debug(\"Server started\");\n * logger.debugError(\"Error: \", new Error(\"Something went wrong\"));\n */\n\n/**\n * Create a logger object with debug and debugError methods\n * @param {string} serverName - Name to include in log messages\n * @returns {Object} Logger object with debug and debugError methods\n */\nfunction createLogger(serverName) {\n const logger = {\n /**\n * Log a debug message to stderr with timestamp\n * @param {string} msg - Message to log\n */\n debug: msg =\u003e {\n const timestamp = new Date().toISOString();\n process.stderr.write(`[] [] \\n`);\n },\n\n /**\n * Log an error with optional stack trace\n * @param {string} prefix - Prefix for the error message\n * @param {Error|string|any} error - Error object or message\n */\n debugError: (prefix, error) =\u003e {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.debug(``);\n if (error instanceof Error \u0026\u0026 error.stack) {\n logger.debug(`Stack trace: ${error.stack}`);\n }\n },\n };\n\n return logger;\n}\n\nmodule.exports = {\n createLogger,\n};\n", - "mcp_server_core.cjs": "// @ts-check\n/// \u003creference types=\"@actions/github-script\" /\u003e\n\n/**\n * MCP Server Core Module\n *\n * This module provides a reusable API for creating MCP (Model Context Protocol) servers.\n * It handles JSON-RPC 2.0 message parsing, tool registration, and server lifecycle.\n *\n * Usage:\n * const { createServer, registerTool, start } = require(\"./mcp_server_core.cjs\");\n *\n * const server = createServer({ name: \"my-server\", version: \"1.0.0\" });\n * registerTool(server, {\n * name: \"my_tool\",\n * description: \"A tool\",\n * inputSchema: { type: \"object\", properties: {} },\n * handler: (args) =\u003e ({ content: [{ type: \"text\", text: \"result\" }] })\n * });\n * start(server);\n */\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst { ReadBuffer } = require(\"./read_buffer.cjs\");\nconst { validateRequiredFields } = require(\"./safe_inputs_validation.cjs\");\n\nconst encoder = new TextEncoder();\n\n/**\n * @typedef {Object} ServerInfo\n * @property {string} name - Server name\n * @property {string} version - Server version\n */\n\n/**\n * @typedef {Object} Tool\n * @property {string} name - Tool name\n * @property {string} description - Tool description\n * @property {Object} inputSchema - JSON Schema for tool inputs\n * @property {Function} [handler] - Tool handler function\n * @property {string} [handlerPath] - Optional file path to handler module (original path from config)\n * @property {number} [timeout] - Timeout in seconds for tool execution (default: 60)\n */\n\n/**\n * @typedef {Object} MCPServer\n * @property {ServerInfo} serverInfo - Server information\n * @property {Object\u003cstring, Tool\u003e} tools - Registered tools\n * @property {Function} debug - Debug logging function\n * @property {Function} debugError - Debug logging function for errors (extracts message from Error objects)\n * @property {Function} writeMessage - Write message to stdout\n * @property {Function} replyResult - Send a result response\n * @property {Function} replyError - Send an error response\n * @property {ReadBuffer} readBuffer - Message buffer\n * @property {string} [logDir] - Optional log directory\n * @property {string} [logFilePath] - Optional log file path\n * @property {boolean} logFileInitialized - Whether log file has been initialized\n */\n\n/**\n * Initialize log file for the server\n * @param {MCPServer} server - The MCP server instance\n */\nfunction initLogFile(server) {\n if (server.logFileInitialized || !server.logDir || !server.logFilePath) return;\n try {\n if (!fs.existsSync(server.logDir)) {\n fs.mkdirSync(server.logDir, { recursive: true });\n }\n // Initialize/truncate log file with header\n const timestamp = new Date().toISOString();\n fs.writeFileSync(server.logFilePath, `# ${server.serverInfo.name} MCP Server Log\\n# Started: \\n# Version: ${server.serverInfo.version}\\n\\n`);\n server.logFileInitialized = true;\n } catch {\n // Silently ignore errors - logging to stderr will still work\n }\n}\n\n/**\n * Create a debug function for the server\n * @param {MCPServer} server - The MCP server instance\n * @returns {Function} Debug function\n */\nfunction createDebugFunction(server) {\n return msg =\u003e {\n const timestamp = new Date().toISOString();\n const formattedMsg = `[] [${server.serverInfo.name}] \\n`;\n\n // Always write to stderr\n process.stderr.write(formattedMsg);\n\n // Also write to log file if log directory is set (initialize on first use)\n if (server.logDir \u0026\u0026 server.logFilePath) {\n if (!server.logFileInitialized) {\n initLogFile(server);\n }\n if (server.logFileInitialized) {\n try {\n fs.appendFileSync(server.logFilePath, formattedMsg);\n } catch {\n // Silently ignore file write errors - stderr logging still works\n }\n }\n }\n };\n}\n\n/**\n * Create a debugError function for the server that handles error casting\n * @param {MCPServer} server - The MCP server instance\n * @returns {Function} Debug error function that extracts message from Error objects\n */\nfunction createDebugErrorFunction(server) {\n return (prefix, error) =\u003e {\n const errorMessage = error instanceof Error ? error.message : String(error);\n server.debug(``);\n if (error instanceof Error \u0026\u0026 error.stack) {\n server.debug(`Stack trace: ${error.stack}`);\n }\n };\n}\n\n/**\n * Create a writeMessage function for the server\n * @param {MCPServer} server - The MCP server instance\n * @returns {Function} Write message function\n */\nfunction createWriteMessageFunction(server) {\n return obj =\u003e {\n const json = JSON.stringify(obj);\n server.debug(`send: `);\n const message = json + \"\\n\";\n const bytes = encoder.encode(message);\n fs.writeSync(1, bytes);\n };\n}\n\n/**\n * Create a replyResult function for the server\n * @param {MCPServer} server - The MCP server instance\n * @returns {Function} Reply result function\n */\nfunction createReplyResultFunction(server) {\n return (id, result) =\u003e {\n if (id === undefined || id === null) return; // notification\n const res = { jsonrpc: \"2.0\", id, result };\n server.writeMessage(res);\n };\n}\n\n/**\n * Create a replyError function for the server\n * @param {MCPServer} server - The MCP server instance\n * @returns {Function} Reply error function\n */\nfunction createReplyErrorFunction(server) {\n return (id, code, message) =\u003e {\n // Don't send error responses for notifications (id is null/undefined)\n if (id === undefined || id === null) {\n server.debug(`Error for notification: `);\n return;\n }\n\n const error = { code, message };\n const res = {\n jsonrpc: \"2.0\",\n id,\n error,\n };\n server.writeMessage(res);\n };\n}\n\n/**\n * Create a new MCP server instance\n * @param {ServerInfo} serverInfo - Server information (name and version)\n * @param {Object} [options] - Optional server configuration\n * @param {string} [options.logDir] - Directory for log file (optional)\n * @returns {MCPServer} The MCP server instance\n */\nfunction createServer(serverInfo, options = {}) {\n const logDir = options.logDir || undefined;\n const logFilePath = logDir ? path.join(logDir, \"server.log\") : undefined;\n\n /** @type {MCPServer} */\n const server = {\n serverInfo,\n tools: {},\n debug: () =\u003e {}, // placeholder\n debugError: () =\u003e {}, // placeholder\n writeMessage: () =\u003e {}, // placeholder\n replyResult: () =\u003e {}, // placeholder\n replyError: () =\u003e {}, // placeholder\n readBuffer: new ReadBuffer(),\n logDir,\n logFilePath,\n logFileInitialized: false,\n };\n\n // Initialize functions with references to server\n server.debug = createDebugFunction(server);\n server.debugError = createDebugErrorFunction(server);\n server.writeMessage = createWriteMessageFunction(server);\n server.replyResult = createReplyResultFunction(server);\n server.replyError = createReplyErrorFunction(server);\n\n return server;\n}\n\n/**\n * Create a wrapped handler function that normalizes results to MCP format.\n * Extracted to avoid creating closures with excessive scope in loadToolHandlers.\n *\n * @param {MCPServer} server - The MCP server instance for logging\n * @param {string} toolName - Name of the tool for logging purposes\n * @param {Function} handlerFn - The original handler function to wrap\n * @returns {Function} Wrapped async handler function\n */\nfunction createWrappedHandler(server, toolName, handlerFn) {\n return async args =\u003e {\n server.debug(` [] Invoking handler with args: ${JSON.stringify(args)}`);\n\n try {\n // Call the handler (may be sync or async)\n const result = await Promise.resolve(handlerFn(args));\n server.debug(` [] Handler returned result type: ${typeof result}`);\n\n // If the result is already in MCP format (has content array), return as-is\n if (result \u0026\u0026 typeof result === \"object\" \u0026\u0026 Array.isArray(result.content)) {\n server.debug(` [] Result is already in MCP format`);\n return result;\n }\n\n // Otherwise, serialize the result to text\n // Use try-catch for serialization to handle circular references and non-serializable values\n let serializedResult;\n try {\n serializedResult = JSON.stringify(result);\n } catch (serializationError) {\n server.debugError(` [] Serialization error: `, serializationError);\n // Fall back to String() for non-serializable values\n serializedResult = String(result);\n }\n server.debug(` [] Serialized result: ${serializedResult.substring(0, 200)}${serializedResult.length \u003e 200 ? \"...\" : \"\"}`);\n\n return {\n content: [\n {\n type: \"text\",\n text: serializedResult,\n },\n ],\n };\n } catch (error) {\n server.debugError(` [] Handler threw error: `, error);\n throw error;\n }\n };\n}\n\n/**\n * Load handler functions from file paths specified in tools configuration.\n * This function iterates through tools and loads handler modules based on file extension:\n *\n * For JavaScript handlers (.js, .cjs, .mjs):\n * - Uses require() to load the module\n * - Handler must export a function as default export\n * - Handler signature: async function handler(args: Record\u003cstring, unknown\u003e): Promise\u003cunknown\u003e\n *\n * For Shell script handlers (.sh):\n * - Uses GitHub Actions convention for passing inputs/outputs\n * - Inputs are passed as environment variables prefixed with INPUT_ (uppercased)\n * - Outputs are read from GITHUB_OUTPUT file (key=value format per line)\n * - Returns: { stdout, stderr, outputs }\n *\n * For Python script handlers (.py):\n * - Uses GitHub Actions convention for passing inputs/outputs\n * - Inputs are passed as environment variables prefixed with INPUT_ (uppercased)\n * - Outputs are read from GITHUB_OUTPUT file (key=value format per line)\n * - Executed using python3 command\n * - Returns: { stdout, stderr, outputs }\n *\n * SECURITY NOTE: Handler paths are loaded from tools.json configuration file,\n * which should be controlled by the server administrator. When basePath is provided,\n * relative paths are resolved within it, preventing directory traversal outside\n * the intended directory. Absolute paths bypass this validation but are still\n * logged for auditing purposes.\n *\n * @param {MCPServer} server - The MCP server instance for logging\n * @param {Array\u003cObject\u003e} tools - Array of tool configurations from tools.json\n * @param {string} [basePath] - Optional base path for resolving relative handler paths.\n * When provided, relative paths are validated to be within this directory.\n * @returns {Array\u003cObject\u003e} The tools array with loaded handlers attached\n */\nfunction loadToolHandlers(server, tools, basePath) {\n server.debug(`Loading tool handlers...`);\n server.debug(` Total tools to process: ${tools.length}`);\n server.debug(` Base path: ${basePath || \"(not specified)\"}`);\n\n let loadedCount = 0;\n let skippedCount = 0;\n let errorCount = 0;\n\n for (const tool of tools) {\n const toolName = tool.name || \"(unnamed)\";\n\n // Check if tool has a handler path specified\n if (!tool.handler) {\n server.debug(` [] No handler path specified, skipping handler load`);\n skippedCount++;\n continue;\n }\n\n const handlerPath = tool.handler;\n server.debug(` [] Handler path specified: `);\n\n // Resolve the handler path\n let resolvedPath = handlerPath;\n if (basePath \u0026\u0026 !path.isAbsolute(handlerPath)) {\n resolvedPath = path.resolve(basePath, handlerPath);\n server.debug(` [] Resolved relative path to: `);\n\n // Security validation: Ensure resolved path is within basePath to prevent directory traversal\n const normalizedBase = path.resolve(basePath);\n const normalizedResolved = path.resolve(resolvedPath);\n if (!normalizedResolved.startsWith(normalizedBase + path.sep) \u0026\u0026 normalizedResolved !== normalizedBase) {\n server.debug(` [] ERROR: Handler path escapes base directory: is not within `);\n errorCount++;\n continue;\n }\n } else if (path.isAbsolute(handlerPath)) {\n server.debug(` [] Using absolute path (bypasses basePath validation): `);\n }\n\n // Store the original handler path for reference\n tool.handlerPath = handlerPath;\n\n try {\n server.debug(` [] Loading handler from: `);\n\n // Check if file exists before loading\n if (!fs.existsSync(resolvedPath)) {\n server.debug(` [] ERROR: Handler file does not exist: `);\n errorCount++;\n continue;\n }\n\n // Detect handler type by file extension\n const ext = path.extname(resolvedPath).toLowerCase();\n server.debug(` [] Handler file extension: `);\n\n if (ext === \".sh\") {\n // Shell script handler - use GitHub Actions convention\n server.debug(` [] Detected shell script handler`);\n\n // Make sure the script is executable (on Unix-like systems)\n try {\n fs.accessSync(resolvedPath, fs.constants.X_OK);\n server.debug(` [] Shell script is executable`);\n } catch {\n // Try to make it executable\n try {\n fs.chmodSync(resolvedPath, 0o755);\n server.debug(` [] Made shell script executable`);\n } catch (chmodError) {\n server.debugError(` [] Warning: Could not make shell script executable: `, chmodError);\n // Continue anyway - it might work depending on the shell\n }\n }\n\n // Lazy-load shell handler module\n const { createShellHandler } = require(\"./mcp_handler_shell.cjs\");\n const timeout = tool.timeout || 60; // Default to 60 seconds if not specified\n tool.handler = createShellHandler(server, toolName, resolvedPath, timeout);\n\n loadedCount++;\n server.debug(` [] Shell handler created successfully with timeout: s`);\n } else if (ext === \".py\") {\n // Python script handler - use GitHub Actions convention\n server.debug(` [] Detected Python script handler`);\n\n // Make sure the script is executable (on Unix-like systems)\n try {\n fs.accessSync(resolvedPath, fs.constants.X_OK);\n server.debug(` [] Python script is executable`);\n } catch {\n // Try to make it executable\n try {\n fs.chmodSync(resolvedPath, 0o755);\n server.debug(` [] Made Python script executable`);\n } catch (chmodError) {\n server.debugError(` [] Warning: Could not make Python script executable: `, chmodError);\n // Continue anyway - python3 will be called explicitly\n }\n }\n\n // Lazy-load Python handler module\n const { createPythonHandler } = require(\"./mcp_handler_python.cjs\");\n const timeout = tool.timeout || 60; // Default to 60 seconds if not specified\n tool.handler = createPythonHandler(server, toolName, resolvedPath, timeout);\n\n loadedCount++;\n server.debug(` [] Python handler created successfully with timeout: s`);\n } else {\n // JavaScript/CommonJS handler - use require()\n server.debug(` [] Loading JavaScript handler module`);\n\n // Load the handler module\n const handlerModule = require(resolvedPath);\n server.debug(` [] Handler module loaded successfully`);\n server.debug(` [] Module type: ${typeof handlerModule}`);\n\n // Get the handler function (support default export patterns)\n let handlerFn = handlerModule;\n\n // Handle ES module default export pattern (module.default)\n if (handlerModule \u0026\u0026 typeof handlerModule === \"object\" \u0026\u0026 typeof handlerModule.default === \"function\") {\n handlerFn = handlerModule.default;\n server.debug(` [] Using module.default export`);\n }\n\n // Validate that the handler is a function\n if (typeof handlerFn !== \"function\") {\n server.debug(` [] ERROR: Handler is not a function, got: ${typeof handlerFn}`);\n server.debug(` [] Module keys: ${Object.keys(handlerModule || {}).join(\", \") || \"(none)\"}`);\n errorCount++;\n continue;\n }\n\n server.debug(` [] Handler function validated successfully`);\n server.debug(` [] Handler function name: ${handlerFn.name || \"(anonymous)\"}`);\n\n // Wrap the handler using the separate function to avoid bloating the closure\n tool.handler = createWrappedHandler(server, toolName, handlerFn);\n\n loadedCount++;\n server.debug(` [] JavaScript handler loaded and wrapped successfully`);\n }\n } catch (error) {\n server.debugError(` [] ERROR loading handler: `, error);\n errorCount++;\n }\n }\n\n server.debug(`Handler loading complete:`);\n server.debug(` Loaded: `);\n server.debug(` Skipped (no handler path): `);\n server.debug(` Errors: `);\n\n return tools;\n}\n\n/**\n * Register a tool with the server\n * @param {MCPServer} server - The MCP server instance\n * @param {Tool} tool - The tool to register\n */\nfunction registerTool(server, tool) {\n const normalizedName = normalizeTool(tool.name);\n server.tools[normalizedName] = {\n ...tool,\n name: normalizedName,\n };\n server.debug(`Registered tool: `);\n}\n\n/**\n * Normalize a tool name (convert dashes to underscores, lowercase)\n * @param {string} name - The tool name to normalize\n * @returns {string} Normalized tool name\n */\nfunction normalizeTool(name) {\n return name.replace(/-/g, \"_\").toLowerCase();\n}\n\n/**\n * Handle an incoming JSON-RPC request and return a response (for HTTP transport)\n * This function is compatible with the MCPServer class's handleRequest method.\n * @param {MCPServer} server - The MCP server instance\n * @param {Object} request - The incoming JSON-RPC request\n * @param {Function} [defaultHandler] - Default handler for tools without a handler\n * @returns {Promise\u003cObject|null\u003e} JSON-RPC response object, or null for notifications\n */\nasync function handleRequest(server, request, defaultHandler) {\n const { id, method, params } = request;\n\n try {\n // Handle notifications per JSON-RPC 2.0 spec:\n // Requests without id field are notifications (no response)\n // Note: id can be null for valid requests, so we check for field presence with \"in\" operator\n if (!(\"id\" in request)) {\n // No id field - this is a notification (no response)\n return null;\n }\n\n let result;\n\n if (method === \"initialize\") {\n const protocolVersion = params?.protocolVersion || \"2024-11-05\";\n result = {\n protocolVersion,\n serverInfo: server.serverInfo,\n capabilities: {\n tools: {},\n },\n };\n } else if (method === \"ping\") {\n result = {};\n } else if (method === \"tools/list\") {\n const list = [];\n Object.values(server.tools).forEach(tool =\u003e {\n const toolDef = {\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n };\n list.push(toolDef);\n });\n result = { tools: list };\n } else if (method === \"tools/call\") {\n const name = params?.name;\n const args = params?.arguments ?? {};\n if (!name || typeof name !== \"string\") {\n throw {\n code: -32602,\n message: \"Invalid params: 'name' must be a string\",\n };\n }\n const tool = server.tools[normalizeTool(name)];\n if (!tool) {\n throw {\n code: -32602,\n message: `Tool '' not found`,\n };\n }\n\n // Use tool handler, or default handler, or error\n let handler = tool.handler;\n if (!handler \u0026\u0026 defaultHandler) {\n handler = defaultHandler(tool.name);\n }\n if (!handler) {\n throw {\n code: -32603,\n message: `No handler for tool: `,\n };\n }\n\n const missing = validateRequiredFields(args, tool.inputSchema);\n if (missing.length) {\n throw {\n code: -32602,\n message: `Invalid arguments: missing or empty ${missing.map(m =\u003e `''`).join(\", \")}`,\n };\n }\n\n // Call handler and await the result (supports both sync and async handlers)\n const handlerResult = await Promise.resolve(handler(args));\n const content = handlerResult \u0026\u0026 handlerResult.content ? handlerResult.content : [];\n result = { content, isError: false };\n } else if (/^notifications\\//.test(method)) {\n // Notifications don't need a response\n return null;\n } else {\n throw {\n code: -32601,\n message: `Method not found: `,\n };\n }\n\n return {\n jsonrpc: \"2.0\",\n id,\n result,\n };\n } catch (error) {\n /** @type {any} */\n const err = error;\n return {\n jsonrpc: \"2.0\",\n id,\n error: {\n code: err.code || -32603,\n message: err.message || \"Internal error\",\n },\n };\n }\n}\n\n/**\n * Handle an incoming JSON-RPC message (for stdio transport)\n * @param {MCPServer} server - The MCP server instance\n * @param {Object} req - The incoming request\n * @param {Function} [defaultHandler] - Default handler for tools without a handler\n * @returns {Promise\u003cvoid\u003e}\n */\nasync function handleMessage(server, req, defaultHandler) {\n // Validate basic JSON-RPC structure\n if (!req || typeof req !== \"object\") {\n server.debug(`Invalid message: not an object`);\n return;\n }\n\n if (req.jsonrpc !== \"2.0\") {\n server.debug(`Invalid message: missing or invalid jsonrpc field`);\n return;\n }\n\n const { id, method, params } = req;\n\n // Validate method field\n if (!method || typeof method !== \"string\") {\n server.replyError(id, -32600, \"Invalid Request: method must be a string\");\n return;\n }\n\n try {\n if (method === \"initialize\") {\n const clientInfo = params?.clientInfo ?? {};\n server.debug(`client info: ${JSON.stringify(clientInfo)}`);\n const protocolVersion = params?.protocolVersion ?? undefined;\n const result = {\n serverInfo: server.serverInfo,\n ...(protocolVersion ? { protocolVersion } : {}),\n capabilities: {\n tools: {},\n },\n };\n server.replyResult(id, result);\n } else if (method === \"tools/list\") {\n const list = [];\n Object.values(server.tools).forEach(tool =\u003e {\n const toolDef = {\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n };\n list.push(toolDef);\n });\n server.replyResult(id, { tools: list });\n } else if (method === \"tools/call\") {\n const name = params?.name;\n const args = params?.arguments ?? {};\n if (!name || typeof name !== \"string\") {\n server.replyError(id, -32602, \"Invalid params: 'name' must be a string\");\n return;\n }\n const tool = server.tools[normalizeTool(name)];\n if (!tool) {\n server.replyError(id, -32601, `Tool not found: (${normalizeTool(name)})`);\n return;\n }\n\n // Use tool handler, or default handler, or error\n let handler = tool.handler;\n if (!handler \u0026\u0026 defaultHandler) {\n handler = defaultHandler(tool.name);\n }\n if (!handler) {\n server.replyError(id, -32603, `No handler for tool: `);\n return;\n }\n\n const missing = validateRequiredFields(args, tool.inputSchema);\n if (missing.length) {\n server.replyError(id, -32602, `Invalid arguments: missing or empty ${missing.map(m =\u003e `''`).join(\", \")}`);\n return;\n }\n\n // Call handler and await the result (supports both sync and async handlers)\n server.debug(`Calling handler for tool: `);\n const result = await Promise.resolve(handler(args));\n server.debug(`Handler returned for tool: `);\n const content = result \u0026\u0026 result.content ? result.content : [];\n server.replyResult(id, { content, isError: false });\n } else if (/^notifications\\//.test(method)) {\n server.debug(`ignore `);\n } else {\n server.replyError(id, -32601, `Method not found: `);\n }\n } catch (e) {\n server.replyError(id, -32603, e instanceof Error ? e.message : String(e));\n }\n}\n\n/**\n * Process the read buffer and handle messages\n * @param {MCPServer} server - The MCP server instance\n * @param {Function} [defaultHandler] - Default handler for tools without a handler\n * @returns {Promise\u003cvoid\u003e}\n */\nasync function processReadBuffer(server, defaultHandler) {\n while (true) {\n try {\n const message = server.readBuffer.readMessage();\n if (!message) {\n break;\n }\n server.debug(`recv: ${JSON.stringify(message)}`);\n await handleMessage(server, message, defaultHandler);\n } catch (error) {\n // For parse errors, we can't know the request id, so we shouldn't send a response\n // according to JSON-RPC spec. Just log the error.\n server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n}\n\n/**\n * Start the MCP server on stdio\n * @param {MCPServer} server - The MCP server instance\n * @param {Object} [options] - Start options\n * @param {Function} [options.defaultHandler] - Default handler for tools without a handler\n */\nfunction start(server, options = {}) {\n const { defaultHandler } = options;\n\n server.debug(`v${server.serverInfo.version} ready on stdio`);\n server.debug(` tools: ${Object.keys(server.tools).join(\", \")}`);\n\n if (!Object.keys(server.tools).length) {\n throw new Error(\"No tools registered\");\n }\n\n const onData = async chunk =\u003e {\n server.readBuffer.append(chunk);\n await processReadBuffer(server, defaultHandler);\n };\n\n process.stdin.on(\"data\", onData);\n process.stdin.on(\"error\", err =\u003e server.debug(`stdin error: `));\n process.stdin.resume();\n server.debug(`listening...`);\n}\n\nmodule.exports = {\n createServer,\n registerTool,\n normalizeTool,\n handleRequest,\n handleMessage,\n processReadBuffer,\n start,\n loadToolHandlers,\n};\n", - "safe_inputs_bootstrap.cjs": "// @ts-check\n\n/**\n * Safe Inputs Bootstrap Module\n *\n * This module provides shared bootstrap logic for safe-inputs MCP servers.\n * It handles configuration loading, tool handler loading, and cleanup that is\n * common between stdio and HTTP transport implementations.\n *\n * Usage:\n * const { bootstrapSafeInputsServer } = require(\"./safe_inputs_bootstrap.cjs\");\n * const { config, basePath, tools } = bootstrapSafeInputsServer(configPath, logger);\n */\n\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { loadConfig } = require(\"./safe_inputs_config_loader.cjs\");\nconst { loadToolHandlers } = require(\"./mcp_server_core.cjs\");\n\n/**\n * @typedef {Object} Logger\n * @property {Function} debug - Debug logging function\n * @property {Function} debugError - Error logging function\n */\n\n/**\n * @typedef {Object} BootstrapResult\n * @property {Object} config - Loaded configuration\n * @property {string} basePath - Base path for resolving handler files\n * @property {Array} tools - Loaded tool handlers\n */\n\n/**\n * Bootstrap a safe-inputs server by loading configuration and tool handlers.\n * This function performs the common initialization steps shared by both stdio\n * and HTTP transport implementations.\n *\n * @param {string} configPath - Path to the configuration JSON file\n * @param {Logger} logger - Logger instance for debug messages\n * @returns {BootstrapResult} Configuration, base path, and loaded tools\n */\nfunction bootstrapSafeInputsServer(configPath, logger) {\n // Load configuration\n logger.debug(`Loading safe-inputs configuration from: `);\n const config = loadConfig(configPath);\n\n // Determine base path for resolving relative handler paths\n const basePath = path.dirname(configPath);\n logger.debug(`Base path for handlers: `);\n logger.debug(`Tools to load: ${config.tools.length}`);\n\n // Load tool handlers from file paths\n const tools = loadToolHandlers(logger, config.tools, basePath);\n\n return { config, basePath, tools };\n}\n\n/**\n * Delete the configuration file to ensure no secrets remain on disk.\n * This should be called after the server has been configured and started.\n *\n * @param {string} configPath - Path to the configuration file to delete\n * @param {Logger} logger - Logger instance for debug messages\n */\nfunction cleanupConfigFile(configPath, logger) {\n try {\n if (fs.existsSync(configPath)) {\n fs.unlinkSync(configPath);\n logger.debug(`Deleted configuration file: `);\n }\n } catch (error) {\n logger.debugError(`Warning: Could not delete configuration file: `, error);\n // Continue anyway - the server is already running\n }\n}\n\nmodule.exports = {\n bootstrapSafeInputsServer,\n cleanupConfigFile,\n};\n", - "safe_inputs_config_loader.cjs": "// @ts-check\n\n/**\n * Safe Inputs Configuration Loader\n *\n * This module provides utilities for loading and validating safe-inputs\n * configuration from JSON files.\n */\n\nconst fs = require(\"fs\");\n\n/**\n * @typedef {Object} SafeInputsToolConfig\n * @property {string} name - Tool name\n * @property {string} description - Tool description\n * @property {Object} inputSchema - JSON Schema for tool inputs\n * @property {string} [handler] - Path to handler file (.cjs, .sh, or .py)\n * @property {number} [timeout] - Timeout in seconds for tool execution (default: 60)\n */\n\n/**\n * @typedef {Object} SafeInputsConfig\n * @property {string} [serverName] - Server name (defaults to \"safeinputs\")\n * @property {string} [version] - Server version (defaults to \"1.0.0\")\n * @property {string} [logDir] - Log directory path\n * @property {SafeInputsToolConfig[]} tools - Array of tool configurations\n */\n\n/**\n * Load safe-inputs configuration from a JSON file\n * @param {string} configPath - Path to the configuration JSON file\n * @returns {SafeInputsConfig} The loaded configuration\n * @throws {Error} If the file doesn't exist or configuration is invalid\n */\nfunction loadConfig(configPath) {\n if (!fs.existsSync(configPath)) {\n throw new Error(`Configuration file not found: `);\n }\n\n const configContent = fs.readFileSync(configPath, \"utf-8\");\n const config = JSON.parse(configContent);\n\n // Validate required fields\n if (!config.tools || !Array.isArray(config.tools)) {\n throw new Error(\"Configuration must contain a 'tools' array\");\n }\n\n return config;\n}\n\nmodule.exports = {\n loadConfig,\n};\n", - "safe_inputs_mcp_server.cjs": "// @ts-check\n/// \u003creference types=\"@actions/github-script\" /\u003e\n\n/**\n * Safe Inputs MCP Server Module\n *\n * This module provides a reusable MCP server for safe-inputs configuration.\n * It uses the mcp_server_core module for JSON-RPC handling and tool registration.\n *\n * The server reads tool configuration from a JSON file and loads handlers from\n * JavaScript (.cjs), shell script (.sh), or Python script (.py) files.\n *\n * Usage:\n * node safe_inputs_mcp_server.cjs /path/to/tools.json\n *\n * Or as a module:\n * const { startSafeInputsServer } = require(\"./safe_inputs_mcp_server.cjs\");\n * startSafeInputsServer(\"/path/to/tools.json\");\n */\n\nconst { createServer, registerTool, start } = require(\"./mcp_server_core.cjs\");\nconst { loadConfig } = require(\"./safe_inputs_config_loader.cjs\");\nconst { createToolConfig } = require(\"./safe_inputs_tool_factory.cjs\");\nconst { bootstrapSafeInputsServer, cleanupConfigFile } = require(\"./safe_inputs_bootstrap.cjs\");\n\n/**\n * @typedef {Object} SafeInputsToolConfig\n * @property {string} name - Tool name\n * @property {string} description - Tool description\n * @property {Object} inputSchema - JSON Schema for tool inputs\n * @property {string} [handler] - Path to handler file (.cjs, .sh, or .py)\n */\n\n/**\n * @typedef {Object} SafeInputsConfig\n * @property {string} [serverName] - Server name (defaults to \"safeinputs\")\n * @property {string} [version] - Server version (defaults to \"1.0.0\")\n * @property {string} [logDir] - Log directory path\n * @property {SafeInputsToolConfig[]} tools - Array of tool configurations\n */\n\n/**\n * Start the safe-inputs MCP server with the given configuration\n * @param {string} configPath - Path to the configuration JSON file\n * @param {Object} [options] - Additional options\n * @param {string} [options.logDir] - Override log directory from config\n * @param {boolean} [options.skipCleanup] - Skip deletion of config file (useful for stdio mode with agent restarts)\n */\nfunction startSafeInputsServer(configPath, options = {}) {\n // Create server first to have logger available\n const logDir = options.logDir || undefined;\n const server = createServer({ name: \"safeinputs\", version: \"1.0.0\" }, { logDir });\n\n // Bootstrap: load configuration and tools using shared logic\n const { config, tools } = bootstrapSafeInputsServer(configPath, server);\n\n // Update server info with actual config values\n server.serverInfo.name = config.serverName || \"safeinputs\";\n server.serverInfo.version = config.version || \"1.0.0\";\n\n // Use logDir from config if not overridden by options\n if (!options.logDir \u0026\u0026 config.logDir) {\n server.logDir = config.logDir;\n }\n\n // Register all tools with the server\n for (const tool of tools) {\n registerTool(server, tool);\n }\n\n // Cleanup: delete the configuration file after loading (unless skipCleanup is true)\n if (!options.skipCleanup) {\n cleanupConfigFile(configPath, server);\n }\n\n // Start the server\n start(server);\n}\n\n// If run directly, start the server with command-line arguments\nif (require.main === module) {\n const args = process.argv.slice(2);\n\n if (args.length \u003c 1) {\n console.error(\"Usage: node safe_inputs_mcp_server.cjs \u003cconfig.json\u003e [--log-dir \u003cpath\u003e]\");\n process.exit(1);\n }\n\n const configPath = args[0];\n const options = {};\n\n // Parse optional arguments\n for (let i = 1; i \u003c args.length; i++) {\n if (args[i] === \"--log-dir\" \u0026\u0026 args[i + 1]) {\n options.logDir = args[i + 1];\n i++;\n }\n }\n\n try {\n startSafeInputsServer(configPath, options);\n } catch (error) {\n console.error(`Error starting safe-inputs server: ${error instanceof Error ? error.message : String(error)}`);\n process.exit(1);\n }\n}\n\nmodule.exports = {\n startSafeInputsServer,\n // Re-export helpers for convenience\n loadConfig,\n createToolConfig,\n};\n", - "safe_inputs_tool_factory.cjs": "// @ts-check\n\n/**\n * Safe Inputs Tool Factory\n *\n * This module provides a factory function for creating tool configuration objects\n * for different handler types (JavaScript, Shell, Python).\n */\n\n/**\n * @typedef {Object} SafeInputsToolConfig\n * @property {string} name - Tool name\n * @property {string} description - Tool description\n * @property {Object} inputSchema - JSON Schema for tool inputs\n * @property {string} handler - Path to handler file (.cjs, .sh, or .py)\n */\n\n/**\n * Create a tool configuration object\n * @param {string} name - Tool name\n * @param {string} description - Tool description\n * @param {Object} inputSchema - JSON Schema for tool inputs\n * @param {string} handlerPath - Path to the handler file (.cjs, .sh, or .py)\n * @returns {SafeInputsToolConfig} Tool configuration object\n */\nfunction createToolConfig(name, description, inputSchema, handlerPath) {\n return {\n name,\n description,\n inputSchema,\n handler: handlerPath,\n };\n}\n\nmodule.exports = {\n createToolConfig,\n};\n", - "safe_inputs_validation.cjs": "// @ts-check\n\n/**\n * Safe Inputs Validation Helpers\n *\n * This module provides validation utilities for safe-inputs MCP server.\n */\n\n/**\n * Validate required fields in tool arguments\n * @param {Object} args - The arguments object to validate\n * @param {Object} inputSchema - The input schema containing required fields\n * @returns {string[]} Array of missing field names (empty if all required fields are present)\n */\nfunction validateRequiredFields(args, inputSchema) {\n const requiredFields = inputSchema \u0026\u0026 Array.isArray(inputSchema.required) ? inputSchema.required : [];\n\n if (!requiredFields.length) {\n return [];\n }\n\n const missing = requiredFields.filter(f =\u003e {\n const value = args[f];\n return value === undefined || value === null || (typeof value === \"string\" \u0026\u0026 value.trim() === \"\");\n });\n\n return missing;\n}\n\nmodule.exports = {\n validateRequiredFields,\n};\n" - }; - -async function run() { - try { - const destination = core.getInput('destination') || '/tmp/gh-aw/safe-inputs'; - - core.info(`Copying safe-inputs files to ${destination}`); - - // Create destination directory if it doesn't exist - if (!fs.existsSync(destination)) { - fs.mkdirSync(destination, { recursive: true }); - core.info(`Created directory: ${destination}`); - } - - let fileCount = 0; - - // Copy each embedded file - for (const [filename, content] of Object.entries(FILES)) { - const filePath = path.join(destination, filename); - fs.writeFileSync(filePath, content, 'utf8'); - core.info(`Copied: ${filename}`); - fileCount++; - } - - core.setOutput('files-copied', fileCount.toString()); - core.info(`✓ Successfully copied ${fileCount} files`); - - } catch (error) { - core.setFailed(`Action failed: ${error.message}`); - } -} - -run(); diff --git a/actions/setup-safe-inputs/js/mcp_handler_python.cjs b/actions/setup-safe-inputs/js/mcp_handler_python.cjs new file mode 100644 index 00000000000..1807e86c56d --- /dev/null +++ b/actions/setup-safe-inputs/js/mcp_handler_python.cjs @@ -0,0 +1,100 @@ +// @ts-check + +/** + * Python Script Handler for Safe-Inputs + * + * This module provides a handler for executing Python scripts in safe-inputs tools. + * It uses a Pythonic approach for passing inputs via JSON on stdin. + */ + +const { execFile } = require("child_process"); + +/** + * Create a Python script handler function that executes a .py file. + * Inputs are passed as JSON via stdin for a more Pythonic approach: + * - Inputs are passed as JSON object via stdin (similar to JavaScript tools) + * - Python script reads and parses JSON from stdin into 'inputs' dictionary + * - Outputs are read from stdout (JSON format expected) + * + * @param {Object} server - The MCP server instance for logging + * @param {string} toolName - Name of the tool for logging purposes + * @param {string} scriptPath - Path to the Python script to execute + * @param {number} [timeoutSeconds=60] - Timeout in seconds for script execution + * @returns {Function} Async handler function that executes the Python script + */ +function createPythonHandler(server, toolName, scriptPath, timeoutSeconds = 60) { + return async args => { + server.debug(` [${toolName}] Invoking Python handler: ${scriptPath}`); + server.debug(` [${toolName}] Python handler args: ${JSON.stringify(args)}`); + server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); + + // Pass inputs as JSON via stdin (more Pythonic approach) + const inputJson = JSON.stringify(args || {}); + server.debug(` [${toolName}] Input JSON (${inputJson.length} bytes): ${inputJson.substring(0, 200)}${inputJson.length > 200 ? "..." : ""}`); + + return new Promise((resolve, reject) => { + server.debug(` [${toolName}] Executing Python script...`); + + const child = execFile( + "python3", + [scriptPath], + { + env: process.env, + timeout: timeoutSeconds * 1000, // Convert to milliseconds + 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}] Python script error: `, error); + reject(error); + return; + } + + // Parse output from stdout + let result; + try { + // Try to parse stdout as JSON + if (stdout && stdout.trim()) { + result = JSON.parse(stdout.trim()); + } else { + result = { stdout: stdout || "", stderr: stderr || "" }; + } + } catch (parseError) { + server.debug(` [${toolName}] Output is not JSON, returning as text`); + result = { stdout: stdout || "", stderr: stderr || "" }; + } + + server.debug(` [${toolName}] Python handler completed successfully`); + + // Return MCP format + resolve({ + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }); + } + ); + + // Write input JSON to stdin + if (child.stdin) { + child.stdin.write(inputJson); + child.stdin.end(); + } + }); + }; +} + +module.exports = { + createPythonHandler, +}; diff --git a/actions/setup-safe-inputs/js/mcp_handler_shell.cjs b/actions/setup-safe-inputs/js/mcp_handler_shell.cjs new file mode 100644 index 00000000000..cda3f282766 --- /dev/null +++ b/actions/setup-safe-inputs/js/mcp_handler_shell.cjs @@ -0,0 +1,146 @@ +// @ts-check + +/** + * Shell Script Handler for Safe-Inputs + * + * This module provides a handler for executing shell scripts in safe-inputs tools. + * It follows GitHub Actions conventions for passing inputs and reading outputs. + */ + +const fs = require("fs"); +const path = require("path"); +const { execFile } = require("child_process"); +const os = require("os"); + +/** + * Create a shell script handler function that executes a .sh file. + * Uses GitHub Actions convention for passing inputs/outputs: + * - Inputs are passed as environment variables prefixed with INPUT_ (uppercased, dashes replaced with underscores) + * - Outputs are read from GITHUB_OUTPUT file (key=value format, one per line) + * - Returns: { stdout, stderr, outputs } + * + * @param {Object} 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 + * @param {number} [timeoutSeconds=60] - Timeout in seconds for script execution + * @returns {Function} Async handler function that executes the shell script + */ +function createShellHandler(server, toolName, scriptPath, timeoutSeconds = 60) { + return async args => { + server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); + server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); + server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); + + // 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: timeoutSeconds * 1000, // Convert to milliseconds + 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), + }, + ], + }); + } + ); + }); + }; +} + +module.exports = { + createShellHandler, +}; diff --git a/actions/setup-safe-inputs/js/mcp_logger.cjs b/actions/setup-safe-inputs/js/mcp_logger.cjs new file mode 100644 index 00000000000..c4e764160ac --- /dev/null +++ b/actions/setup-safe-inputs/js/mcp_logger.cjs @@ -0,0 +1,53 @@ +// @ts-check +/// + +/** + * MCP Logger Utility + * + * This module provides logger creation utilities for MCP servers. + * It creates logger objects with debug and debugError methods that write + * timestamped messages to stderr. + * + * Usage: + * const { createLogger } = require("./mcp_logger.cjs"); + * const logger = createLogger("my-server"); + * logger.debug("Server started"); + * logger.debugError("Error: ", new Error("Something went wrong")); + */ + +/** + * Create a logger object with debug and debugError methods + * @param {string} serverName - Name to include in log messages + * @returns {Object} Logger object with debug and debugError methods + */ +function createLogger(serverName) { + const logger = { + /** + * Log a debug message to stderr with timestamp + * @param {string} msg - Message to log + */ + debug: msg => { + const timestamp = new Date().toISOString(); + process.stderr.write(`[${timestamp}] [${serverName}] ${msg}\n`); + }, + + /** + * Log an error with optional stack trace + * @param {string} prefix - Prefix for the error message + * @param {Error|string|any} error - Error object or message + */ + debugError: (prefix, error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.debug(`${prefix}${errorMessage}`); + if (error instanceof Error && error.stack) { + logger.debug(`${prefix}Stack trace: ${error.stack}`); + } + }, + }; + + return logger; +} + +module.exports = { + createLogger, +}; diff --git a/actions/setup-safe-inputs/js/mcp_server_core.cjs b/actions/setup-safe-inputs/js/mcp_server_core.cjs new file mode 100644 index 00000000000..ab1a60832cf --- /dev/null +++ b/actions/setup-safe-inputs/js/mcp_server_core.cjs @@ -0,0 +1,747 @@ +// @ts-check +/// + +/** + * MCP Server Core Module + * + * This module provides a reusable API for creating MCP (Model Context Protocol) servers. + * It handles JSON-RPC 2.0 message parsing, tool registration, and server lifecycle. + * + * Usage: + * const { createServer, registerTool, start } = require("./mcp_server_core.cjs"); + * + * const server = createServer({ name: "my-server", version: "1.0.0" }); + * registerTool(server, { + * name: "my_tool", + * description: "A tool", + * inputSchema: { type: "object", properties: {} }, + * handler: (args) => ({ content: [{ type: "text", text: "result" }] }) + * }); + * start(server); + */ + +const fs = require("fs"); +const path = require("path"); + +const { ReadBuffer } = require("./read_buffer.cjs"); +const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); + +const encoder = new TextEncoder(); + +/** + * @typedef {Object} ServerInfo + * @property {string} name - Server name + * @property {string} version - Server version + */ + +/** + * @typedef {Object} Tool + * @property {string} name - Tool name + * @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) + * @property {number} [timeout] - Timeout in seconds for tool execution (default: 60) + */ + +/** + * @typedef {Object} MCPServer + * @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 + * @property {ReadBuffer} readBuffer - Message buffer + * @property {string} [logDir] - Optional log directory + * @property {string} [logFilePath] - Optional log file path + * @property {boolean} logFileInitialized - Whether log file has been initialized + */ + +/** + * Initialize log file for the server + * @param {MCPServer} server - The MCP server instance + */ +function initLogFile(server) { + if (server.logFileInitialized || !server.logDir || !server.logFilePath) return; + try { + if (!fs.existsSync(server.logDir)) { + fs.mkdirSync(server.logDir, { recursive: true }); + } + // Initialize/truncate log file with header + const timestamp = new Date().toISOString(); + fs.writeFileSync(server.logFilePath, `# ${server.serverInfo.name} MCP Server Log\n# Started: ${timestamp}\n# Version: ${server.serverInfo.version}\n\n`); + server.logFileInitialized = true; + } catch { + // Silently ignore errors - logging to stderr will still work + } +} + +/** + * Create a debug function for the server + * @param {MCPServer} server - The MCP server instance + * @returns {Function} Debug function + */ +function createDebugFunction(server) { + return msg => { + const timestamp = new Date().toISOString(); + const formattedMsg = `[${timestamp}] [${server.serverInfo.name}] ${msg}\n`; + + // Always write to stderr + process.stderr.write(formattedMsg); + + // Also write to log file if log directory is set (initialize on first use) + if (server.logDir && server.logFilePath) { + if (!server.logFileInitialized) { + initLogFile(server); + } + if (server.logFileInitialized) { + try { + fs.appendFileSync(server.logFilePath, formattedMsg); + } catch { + // Silently ignore file write errors - stderr logging still works + } + } + } + }; +} + +/** + * 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 + * @returns {Function} Write message function + */ +function createWriteMessageFunction(server) { + return obj => { + const json = JSON.stringify(obj); + server.debug(`send: ${json}`); + const message = json + "\n"; + const bytes = encoder.encode(message); + fs.writeSync(1, bytes); + }; +} + +/** + * Create a replyResult function for the server + * @param {MCPServer} server - The MCP server instance + * @returns {Function} Reply result function + */ +function createReplyResultFunction(server) { + return (id, result) => { + if (id === undefined || id === null) return; // notification + const res = { jsonrpc: "2.0", id, result }; + server.writeMessage(res); + }; +} + +/** + * Create a replyError function for the server + * @param {MCPServer} server - The MCP server instance + * @returns {Function} Reply error function + */ +function createReplyErrorFunction(server) { + return (id, code, message) => { + // Don't send error responses for notifications (id is null/undefined) + if (id === undefined || id === null) { + server.debug(`Error for notification: ${message}`); + return; + } + + const error = { code, message }; + const res = { + jsonrpc: "2.0", + id, + error, + }; + server.writeMessage(res); + }; +} + +/** + * Create a new MCP server instance + * @param {ServerInfo} serverInfo - Server information (name and version) + * @param {Object} [options] - Optional server configuration + * @param {string} [options.logDir] - Directory for log file (optional) + * @returns {MCPServer} The MCP server instance + */ +function createServer(serverInfo, options = {}) { + const logDir = options.logDir || undefined; + const logFilePath = logDir ? path.join(logDir, "server.log") : undefined; + + /** @type {MCPServer} */ + const server = { + serverInfo, + tools: {}, + debug: () => {}, // placeholder + debugError: () => {}, // placeholder + writeMessage: () => {}, // placeholder + replyResult: () => {}, // placeholder + replyError: () => {}, // placeholder + readBuffer: new ReadBuffer(), + logDir, + logFilePath, + logFileInitialized: false, + }; + + // 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); + + 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; + } + }; +} + +/** + * 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 } + * + * For Python script handlers (.py): + * - 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) + * - Executed using python3 command + * - 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 + } + } + + // Lazy-load shell handler module + const { createShellHandler } = require("./mcp_handler_shell.cjs"); + const timeout = tool.timeout || 60; // Default to 60 seconds if not specified + tool.handler = createShellHandler(server, toolName, resolvedPath, timeout); + + loadedCount++; + server.debug(` [${toolName}] Shell handler created successfully with timeout: ${timeout}s`); + } else if (ext === ".py") { + // Python script handler - use GitHub Actions convention + server.debug(` [${toolName}] Detected Python script handler`); + + // Make sure the script is executable (on Unix-like systems) + try { + fs.accessSync(resolvedPath, fs.constants.X_OK); + server.debug(` [${toolName}] Python script is executable`); + } catch { + // Try to make it executable + try { + fs.chmodSync(resolvedPath, 0o755); + server.debug(` [${toolName}] Made Python script executable`); + } catch (chmodError) { + server.debugError(` [${toolName}] Warning: Could not make Python script executable: `, chmodError); + // Continue anyway - python3 will be called explicitly + } + } + + // Lazy-load Python handler module + const { createPythonHandler } = require("./mcp_handler_python.cjs"); + const timeout = tool.timeout || 60; // Default to 60 seconds if not specified + tool.handler = createPythonHandler(server, toolName, resolvedPath, timeout); + + loadedCount++; + server.debug(` [${toolName}] Python handler created successfully with timeout: ${timeout}s`); + } 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 + * @param {Tool} tool - The tool to register + */ +function registerTool(server, tool) { + const normalizedName = normalizeTool(tool.name); + server.tools[normalizedName] = { + ...tool, + name: normalizedName, + }; + server.debug(`Registered tool: ${normalizedName}`); +} + +/** + * Normalize a tool name (convert dashes to underscores, lowercase) + * @param {string} name - The tool name to normalize + * @returns {string} Normalized tool name + */ +function normalizeTool(name) { + return name.replace(/-/g, "_").toLowerCase(); +} + +/** + * Handle an incoming JSON-RPC request and return a response (for HTTP transport) + * This function is compatible with the MCPServer class's handleRequest method. + * @param {MCPServer} server - The MCP server instance + * @param {Object} request - The incoming JSON-RPC request + * @param {Function} [defaultHandler] - Default handler for tools without a handler + * @returns {Promise} JSON-RPC response object, or null for notifications + */ +async function handleRequest(server, request, defaultHandler) { + const { id, method, params } = request; + + try { + // Handle notifications per JSON-RPC 2.0 spec: + // Requests without id field are notifications (no response) + // Note: id can be null for valid requests, so we check for field presence with "in" operator + if (!("id" in request)) { + // No id field - this is a notification (no response) + return null; + } + + let result; + + if (method === "initialize") { + const protocolVersion = params?.protocolVersion || "2024-11-05"; + result = { + protocolVersion, + serverInfo: server.serverInfo, + capabilities: { + tools: {}, + }, + }; + } else if (method === "ping") { + result = {}; + } else if (method === "tools/list") { + const list = []; + Object.values(server.tools).forEach(tool => { + const toolDef = { + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + }; + list.push(toolDef); + }); + result = { tools: list }; + } else if (method === "tools/call") { + const name = params?.name; + const args = params?.arguments ?? {}; + if (!name || typeof name !== "string") { + throw { + code: -32602, + message: "Invalid params: 'name' must be a string", + }; + } + const tool = server.tools[normalizeTool(name)]; + if (!tool) { + throw { + code: -32602, + message: `Tool '${name}' not found`, + }; + } + + // Use tool handler, or default handler, or error + let handler = tool.handler; + if (!handler && defaultHandler) { + handler = defaultHandler(tool.name); + } + if (!handler) { + throw { + code: -32603, + message: `No handler for tool: ${name}`, + }; + } + + const missing = validateRequiredFields(args, tool.inputSchema); + if (missing.length) { + throw { + code: -32602, + message: `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`, + }; + } + + // Call handler and await the result (supports both sync and async handlers) + const handlerResult = await Promise.resolve(handler(args)); + const content = handlerResult && handlerResult.content ? handlerResult.content : []; + result = { content, isError: false }; + } else if (/^notifications\//.test(method)) { + // Notifications don't need a response + return null; + } else { + throw { + code: -32601, + message: `Method not found: ${method}`, + }; + } + + return { + jsonrpc: "2.0", + id, + result, + }; + } catch (error) { + /** @type {any} */ + const err = error; + return { + jsonrpc: "2.0", + id, + error: { + code: err.code || -32603, + message: err.message || "Internal error", + }, + }; + } +} + +/** + * Handle an incoming JSON-RPC message (for stdio transport) + * @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} + */ +async function handleMessage(server, req, defaultHandler) { + // Validate basic JSON-RPC structure + if (!req || typeof req !== "object") { + server.debug(`Invalid message: not an object`); + return; + } + + if (req.jsonrpc !== "2.0") { + server.debug(`Invalid message: missing or invalid jsonrpc field`); + return; + } + + const { id, method, params } = req; + + // Validate method field + if (!method || typeof method !== "string") { + server.replyError(id, -32600, "Invalid Request: method must be a string"); + return; + } + + try { + if (method === "initialize") { + const clientInfo = params?.clientInfo ?? {}; + server.debug(`client info: ${JSON.stringify(clientInfo)}`); + const protocolVersion = params?.protocolVersion ?? undefined; + const result = { + serverInfo: server.serverInfo, + ...(protocolVersion ? { protocolVersion } : {}), + capabilities: { + tools: {}, + }, + }; + server.replyResult(id, result); + } else if (method === "tools/list") { + const list = []; + Object.values(server.tools).forEach(tool => { + const toolDef = { + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + }; + list.push(toolDef); + }); + server.replyResult(id, { tools: list }); + } else if (method === "tools/call") { + const name = params?.name; + const args = params?.arguments ?? {}; + if (!name || typeof name !== "string") { + server.replyError(id, -32602, "Invalid params: 'name' must be a string"); + return; + } + const tool = server.tools[normalizeTool(name)]; + if (!tool) { + server.replyError(id, -32601, `Tool not found: ${name} (${normalizeTool(name)})`); + return; + } + + // Use tool handler, or default handler, or error + let handler = tool.handler; + if (!handler && defaultHandler) { + handler = defaultHandler(tool.name); + } + if (!handler) { + server.replyError(id, -32603, `No handler for tool: ${name}`); + return; + } + + const missing = validateRequiredFields(args, tool.inputSchema); + if (missing.length) { + server.replyError(id, -32602, `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); + return; + } + + // 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)) { + server.debug(`ignore ${method}`); + } else { + server.replyError(id, -32601, `Method not found: ${method}`); + } + } catch (e) { + server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); + } +} + +/** + * 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} + */ +async function processReadBuffer(server, defaultHandler) { + while (true) { + try { + const message = server.readBuffer.readMessage(); + if (!message) { + break; + } + server.debug(`recv: ${JSON.stringify(message)}`); + 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. + server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); + } + } +} + +/** + * Start the MCP server on stdio + * @param {MCPServer} server - The MCP server instance + * @param {Object} [options] - Start options + * @param {Function} [options.defaultHandler] - Default handler for tools without a handler + */ +function start(server, options = {}) { + const { defaultHandler } = options; + + server.debug(`v${server.serverInfo.version} ready on stdio`); + server.debug(` tools: ${Object.keys(server.tools).join(", ")}`); + + if (!Object.keys(server.tools).length) { + throw new Error("No tools registered"); + } + + const onData = async chunk => { + server.readBuffer.append(chunk); + await processReadBuffer(server, defaultHandler); + }; + + process.stdin.on("data", onData); + process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); + process.stdin.resume(); + server.debug(`listening...`); +} + +module.exports = { + createServer, + registerTool, + normalizeTool, + handleRequest, + handleMessage, + processReadBuffer, + start, + loadToolHandlers, +}; diff --git a/actions/setup-safe-inputs/js/read_buffer.cjs b/actions/setup-safe-inputs/js/read_buffer.cjs new file mode 100644 index 00000000000..4221c67735a --- /dev/null +++ b/actions/setup-safe-inputs/js/read_buffer.cjs @@ -0,0 +1,67 @@ +// @ts-check +/// + +/** + * ReadBuffer Module + * + * This module provides a buffer class for parsing JSON-RPC messages from stdin. + * It handles line-by-line reading and JSON parsing with support for both + * Unix (\n) and Windows (\r\n) line endings. + * + * Usage: + * const { ReadBuffer } = require("./read_buffer.cjs"); + * + * const buffer = new ReadBuffer(); + * buffer.append(chunk); + * const message = buffer.readMessage(); + */ + +/** + * ReadBuffer class for parsing JSON-RPC messages from stdin + */ +class ReadBuffer { + constructor() { + /** @type {Buffer|null} */ + this._buffer = null; + } + + /** + * Append data to the buffer + * @param {Buffer} chunk - Data chunk to append + */ + append(chunk) { + this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; + } + + /** + * Read a complete message from the buffer + * @returns {Object|null} Parsed JSON message or null if no complete message + */ + readMessage() { + if (!this._buffer) { + return null; + } + + const index = this._buffer.indexOf("\n"); + if (index === -1) { + return null; + } + + const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, ""); + this._buffer = this._buffer.subarray(index + 1); + + if (line.trim() === "") { + return this.readMessage(); // Skip empty lines recursively + } + + try { + return JSON.parse(line); + } catch (error) { + throw new Error(`Parse error: ${error instanceof Error ? error.message : String(error)}`); + } + } +} + +module.exports = { + ReadBuffer, +}; diff --git a/actions/setup-safe-inputs/js/safe_inputs_bootstrap.cjs b/actions/setup-safe-inputs/js/safe_inputs_bootstrap.cjs new file mode 100644 index 00000000000..1b498309e65 --- /dev/null +++ b/actions/setup-safe-inputs/js/safe_inputs_bootstrap.cjs @@ -0,0 +1,80 @@ +// @ts-check + +/** + * Safe Inputs Bootstrap Module + * + * This module provides shared bootstrap logic for safe-inputs MCP servers. + * It handles configuration loading, tool handler loading, and cleanup that is + * common between stdio and HTTP transport implementations. + * + * Usage: + * const { bootstrapSafeInputsServer } = require("./safe_inputs_bootstrap.cjs"); + * const { config, basePath, tools } = bootstrapSafeInputsServer(configPath, logger); + */ + +const path = require("path"); +const fs = require("fs"); +const { loadConfig } = require("./safe_inputs_config_loader.cjs"); +const { loadToolHandlers } = require("./mcp_server_core.cjs"); + +/** + * @typedef {Object} Logger + * @property {Function} debug - Debug logging function + * @property {Function} debugError - Error logging function + */ + +/** + * @typedef {Object} BootstrapResult + * @property {Object} config - Loaded configuration + * @property {string} basePath - Base path for resolving handler files + * @property {Array} tools - Loaded tool handlers + */ + +/** + * Bootstrap a safe-inputs server by loading configuration and tool handlers. + * This function performs the common initialization steps shared by both stdio + * and HTTP transport implementations. + * + * @param {string} configPath - Path to the configuration JSON file + * @param {Logger} logger - Logger instance for debug messages + * @returns {BootstrapResult} Configuration, base path, and loaded tools + */ +function bootstrapSafeInputsServer(configPath, logger) { + // Load configuration + logger.debug(`Loading safe-inputs configuration from: ${configPath}`); + const config = loadConfig(configPath); + + // Determine base path for resolving relative handler paths + const basePath = path.dirname(configPath); + logger.debug(`Base path for handlers: ${basePath}`); + logger.debug(`Tools to load: ${config.tools.length}`); + + // Load tool handlers from file paths + const tools = loadToolHandlers(logger, config.tools, basePath); + + return { config, basePath, tools }; +} + +/** + * Delete the configuration file to ensure no secrets remain on disk. + * This should be called after the server has been configured and started. + * + * @param {string} configPath - Path to the configuration file to delete + * @param {Logger} logger - Logger instance for debug messages + */ +function cleanupConfigFile(configPath, logger) { + try { + if (fs.existsSync(configPath)) { + fs.unlinkSync(configPath); + logger.debug(`Deleted configuration file: ${configPath}`); + } + } catch (error) { + logger.debugError(`Warning: Could not delete configuration file: `, error); + // Continue anyway - the server is already running + } +} + +module.exports = { + bootstrapSafeInputsServer, + cleanupConfigFile, +}; diff --git a/actions/setup-safe-inputs/js/safe_inputs_config_loader.cjs b/actions/setup-safe-inputs/js/safe_inputs_config_loader.cjs new file mode 100644 index 00000000000..33836c2e0ec --- /dev/null +++ b/actions/setup-safe-inputs/js/safe_inputs_config_loader.cjs @@ -0,0 +1,53 @@ +// @ts-check + +/** + * Safe Inputs Configuration Loader + * + * This module provides utilities for loading and validating safe-inputs + * configuration from JSON files. + */ + +const fs = require("fs"); + +/** + * @typedef {Object} SafeInputsToolConfig + * @property {string} name - Tool name + * @property {string} description - Tool description + * @property {Object} inputSchema - JSON Schema for tool inputs + * @property {string} [handler] - Path to handler file (.cjs, .sh, or .py) + * @property {number} [timeout] - Timeout in seconds for tool execution (default: 60) + */ + +/** + * @typedef {Object} SafeInputsConfig + * @property {string} [serverName] - Server name (defaults to "safeinputs") + * @property {string} [version] - Server version (defaults to "1.0.0") + * @property {string} [logDir] - Log directory path + * @property {SafeInputsToolConfig[]} tools - Array of tool configurations + */ + +/** + * Load safe-inputs configuration from a JSON file + * @param {string} configPath - Path to the configuration JSON file + * @returns {SafeInputsConfig} The loaded configuration + * @throws {Error} If the file doesn't exist or configuration is invalid + */ +function loadConfig(configPath) { + if (!fs.existsSync(configPath)) { + throw new Error(`Configuration file not found: ${configPath}`); + } + + const configContent = fs.readFileSync(configPath, "utf-8"); + const config = JSON.parse(configContent); + + // Validate required fields + if (!config.tools || !Array.isArray(config.tools)) { + throw new Error("Configuration must contain a 'tools' array"); + } + + return config; +} + +module.exports = { + loadConfig, +}; diff --git a/actions/setup-safe-inputs/js/safe_inputs_mcp_server.cjs b/actions/setup-safe-inputs/js/safe_inputs_mcp_server.cjs new file mode 100644 index 00000000000..208fda0847f --- /dev/null +++ b/actions/setup-safe-inputs/js/safe_inputs_mcp_server.cjs @@ -0,0 +1,113 @@ +// @ts-check +/// + +/** + * Safe Inputs MCP Server Module + * + * This module provides a reusable MCP server for safe-inputs configuration. + * It uses the mcp_server_core module for JSON-RPC handling and tool registration. + * + * The server reads tool configuration from a JSON file and loads handlers from + * JavaScript (.cjs), shell script (.sh), or Python script (.py) files. + * + * Usage: + * node safe_inputs_mcp_server.cjs /path/to/tools.json + * + * Or as a module: + * const { startSafeInputsServer } = require("./safe_inputs_mcp_server.cjs"); + * startSafeInputsServer("/path/to/tools.json"); + */ + +const { createServer, registerTool, start } = require("./mcp_server_core.cjs"); +const { loadConfig } = require("./safe_inputs_config_loader.cjs"); +const { createToolConfig } = require("./safe_inputs_tool_factory.cjs"); +const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); + +/** + * @typedef {Object} SafeInputsToolConfig + * @property {string} name - Tool name + * @property {string} description - Tool description + * @property {Object} inputSchema - JSON Schema for tool inputs + * @property {string} [handler] - Path to handler file (.cjs, .sh, or .py) + */ + +/** + * @typedef {Object} SafeInputsConfig + * @property {string} [serverName] - Server name (defaults to "safeinputs") + * @property {string} [version] - Server version (defaults to "1.0.0") + * @property {string} [logDir] - Log directory path + * @property {SafeInputsToolConfig[]} tools - Array of tool configurations + */ + +/** + * Start the safe-inputs MCP server with the given configuration + * @param {string} configPath - Path to the configuration JSON file + * @param {Object} [options] - Additional options + * @param {string} [options.logDir] - Override log directory from config + * @param {boolean} [options.skipCleanup] - Skip deletion of config file (useful for stdio mode with agent restarts) + */ +function startSafeInputsServer(configPath, options = {}) { + // Create server first to have logger available + const logDir = options.logDir || undefined; + const server = createServer({ name: "safeinputs", version: "1.0.0" }, { logDir }); + + // Bootstrap: load configuration and tools using shared logic + const { config, tools } = bootstrapSafeInputsServer(configPath, server); + + // Update server info with actual config values + server.serverInfo.name = config.serverName || "safeinputs"; + server.serverInfo.version = config.version || "1.0.0"; + + // Use logDir from config if not overridden by options + if (!options.logDir && config.logDir) { + server.logDir = config.logDir; + } + + // Register all tools with the server + for (const tool of tools) { + registerTool(server, tool); + } + + // Cleanup: delete the configuration file after loading (unless skipCleanup is true) + if (!options.skipCleanup) { + cleanupConfigFile(configPath, server); + } + + // Start the server + start(server); +} + +// If run directly, start the server with command-line arguments +if (require.main === module) { + const args = process.argv.slice(2); + + if (args.length < 1) { + console.error("Usage: node safe_inputs_mcp_server.cjs [--log-dir ]"); + process.exit(1); + } + + const configPath = args[0]; + const options = {}; + + // Parse optional arguments + for (let i = 1; i < args.length; i++) { + if (args[i] === "--log-dir" && args[i + 1]) { + options.logDir = args[i + 1]; + i++; + } + } + + try { + startSafeInputsServer(configPath, options); + } catch (error) { + console.error(`Error starting safe-inputs server: ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } +} + +module.exports = { + startSafeInputsServer, + // Re-export helpers for convenience + loadConfig, + createToolConfig, +}; diff --git a/actions/setup-safe-inputs/js/safe_inputs_tool_factory.cjs b/actions/setup-safe-inputs/js/safe_inputs_tool_factory.cjs new file mode 100644 index 00000000000..19cfb119265 --- /dev/null +++ b/actions/setup-safe-inputs/js/safe_inputs_tool_factory.cjs @@ -0,0 +1,37 @@ +// @ts-check + +/** + * Safe Inputs Tool Factory + * + * This module provides a factory function for creating tool configuration objects + * for different handler types (JavaScript, Shell, Python). + */ + +/** + * @typedef {Object} SafeInputsToolConfig + * @property {string} name - Tool name + * @property {string} description - Tool description + * @property {Object} inputSchema - JSON Schema for tool inputs + * @property {string} handler - Path to handler file (.cjs, .sh, or .py) + */ + +/** + * Create a tool configuration object + * @param {string} name - Tool name + * @param {string} description - Tool description + * @param {Object} inputSchema - JSON Schema for tool inputs + * @param {string} handlerPath - Path to the handler file (.cjs, .sh, or .py) + * @returns {SafeInputsToolConfig} Tool configuration object + */ +function createToolConfig(name, description, inputSchema, handlerPath) { + return { + name, + description, + inputSchema, + handler: handlerPath, + }; +} + +module.exports = { + createToolConfig, +}; diff --git a/actions/setup-safe-inputs/js/safe_inputs_validation.cjs b/actions/setup-safe-inputs/js/safe_inputs_validation.cjs new file mode 100644 index 00000000000..4c64f6a4b12 --- /dev/null +++ b/actions/setup-safe-inputs/js/safe_inputs_validation.cjs @@ -0,0 +1,32 @@ +// @ts-check + +/** + * Safe Inputs Validation Helpers + * + * This module provides validation utilities for safe-inputs MCP server. + */ + +/** + * Validate required fields in tool arguments + * @param {Object} args - The arguments object to validate + * @param {Object} inputSchema - The input schema containing required fields + * @returns {string[]} Array of missing field names (empty if all required fields are present) + */ +function validateRequiredFields(args, inputSchema) { + const requiredFields = inputSchema && Array.isArray(inputSchema.required) ? inputSchema.required : []; + + if (!requiredFields.length) { + return []; + } + + const missing = requiredFields.filter(f => { + const value = args[f]; + return value === undefined || value === null || (typeof value === "string" && value.trim() === ""); + }); + + return missing; +} + +module.exports = { + validateRequiredFields, +}; diff --git a/actions/setup/js/check_command_position.cjs b/actions/setup/js/check_command_position.cjs index 812e774e557..d0df669e004 100644 --- a/actions/setup/js/check_command_position.cjs +++ b/actions/setup/js/check_command_position.cjs @@ -66,4 +66,4 @@ async function main() { } } -await main(); +module.exports = { main }; diff --git a/actions/setup/js/check_membership.cjs b/actions/setup/js/check_membership.cjs index a9192d554b8..d5f6c8a5c9e 100644 --- a/actions/setup/js/check_membership.cjs +++ b/actions/setup/js/check_membership.cjs @@ -96,4 +96,5 @@ async function main() { core.setOutput("error_message", `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}`); } } -await main(); + +module.exports = { main }; diff --git a/actions/setup/js/check_skip_if_match.cjs b/actions/setup/js/check_skip_if_match.cjs index 7290bc7d72b..fa8525b9283 100644 --- a/actions/setup/js/check_skip_if_match.cjs +++ b/actions/setup/js/check_skip_if_match.cjs @@ -57,4 +57,5 @@ async function main() { return; } } -await main(); + +module.exports = { main }; diff --git a/actions/setup/js/check_stop_time.cjs b/actions/setup/js/check_stop_time.cjs index 165a42fe305..4e86da0f8a4 100644 --- a/actions/setup/js/check_stop_time.cjs +++ b/actions/setup/js/check_stop_time.cjs @@ -37,4 +37,5 @@ async function main() { core.setOutput("stop_time_ok", "true"); } -await main(); + +module.exports = { main }; diff --git a/actions/setup/js/close_expired_discussions.cjs b/actions/setup/js/close_expired_discussions.cjs index 8cf285b39d1..0cd3db2dec8 100644 --- a/actions/setup/js/close_expired_discussions.cjs +++ b/actions/setup/js/close_expired_discussions.cjs @@ -279,4 +279,4 @@ async function main() { core.info(`Successfully closed ${closedCount} expired discussion(s)`); } -await main(); +module.exports = { main }; diff --git a/actions/setup/js/close_expired_issues.cjs b/actions/setup/js/close_expired_issues.cjs index fc5473d893b..63436f36681 100644 --- a/actions/setup/js/close_expired_issues.cjs +++ b/actions/setup/js/close_expired_issues.cjs @@ -272,4 +272,4 @@ async function main() { core.info(`Successfully closed ${closedCount} expired issue(s)`); } -await main(); +module.exports = { main }; diff --git a/actions/setup/js/collect_ndjson_output.cjs b/actions/setup/js/collect_ndjson_output.cjs index bfd0d865580..d53305392f0 100644 --- a/actions/setup/js/collect_ndjson_output.cjs +++ b/actions/setup/js/collect_ndjson_output.cjs @@ -355,4 +355,5 @@ async function main() { core.setOutput("has_patch", hasPatch ? "true" : "false"); } } -await main(); + +module.exports = { main }; diff --git a/actions/setup/js/interpolate_prompt.cjs b/actions/setup/js/interpolate_prompt.cjs index 23def554622..3b7d798c325 100644 --- a/actions/setup/js/interpolate_prompt.cjs +++ b/actions/setup/js/interpolate_prompt.cjs @@ -122,5 +122,4 @@ async function main() { } } -// Execute main function -main(); +module.exports = { main }; diff --git a/actions/setup/js/redact_secrets.cjs b/actions/setup/js/redact_secrets.cjs index f1521940564..103c3fb1341 100644 --- a/actions/setup/js/redact_secrets.cjs +++ b/actions/setup/js/redact_secrets.cjs @@ -148,4 +148,5 @@ async function main() { core.setFailed(`Secret redaction failed: ${error instanceof Error ? error.message : String(error)}`); } } -await main(); + +module.exports = { main }; diff --git a/actions/setup/js/sanitize_output.cjs b/actions/setup/js/sanitize_output.cjs index f9bdf1b9471..4b07e46f65a 100644 --- a/actions/setup/js/sanitize_output.cjs +++ b/actions/setup/js/sanitize_output.cjs @@ -40,4 +40,4 @@ async function main() { } } -await main(); +module.exports = { main }; diff --git a/actions/setup/js/upload_assets.cjs b/actions/setup/js/upload_assets.cjs index 325b12164c5..9eb258b9350 100644 --- a/actions/setup/js/upload_assets.cjs +++ b/actions/setup/js/upload_assets.cjs @@ -192,4 +192,4 @@ async function main() { core.setOutput("branch_name", normalizedBranchName); } -await main(); +module.exports = { main }; diff --git a/pkg/cli/actions_build_command.go b/pkg/cli/actions_build_command.go index d46139beca5..37790c154a7 100644 --- a/pkg/cli/actions_build_command.go +++ b/pkg/cli/actions_build_command.go @@ -105,8 +105,8 @@ func ActionsCleanCommand() error { cleanedCount := 0 for _, actionName := range actionDirs { - // Clean index.js for actions that use it (except setup-safe-outputs and setup) - if actionName != "setup-safe-outputs" && actionName != "setup" { + // Clean index.js for actions that use it (except setup-safe-outputs, setup-safe-inputs, and setup) + if actionName != "setup-safe-outputs" && actionName != "setup-safe-inputs" && actionName != "setup" { indexPath := filepath.Join(actionsDir, actionName, "index.js") if _, err := os.Stat(indexPath); err == nil { if err := os.Remove(indexPath); err != nil { @@ -117,8 +117,8 @@ func ActionsCleanCommand() error { } } - // Clean js/ directory for setup-safe-outputs - if actionName == "setup-safe-outputs" { + // Clean js/ directory for setup-safe-outputs and setup-safe-inputs + if actionName == "setup-safe-outputs" || actionName == "setup-safe-inputs" { jsDir := filepath.Join(actionsDir, actionName, "js") if _, err := os.Stat(jsDir); err == nil { if err := os.RemoveAll(jsDir); err != nil { @@ -237,9 +237,9 @@ func buildAction(actionsDir, actionName string) error { return err } - // Special handling for setup-safe-outputs: copy files instead of embedding - if actionName == "setup-safe-outputs" { - return buildSetupSafeOutputsAction(actionsDir, actionName) + // Special handling for setup-safe-outputs and setup-safe-inputs: copy files instead of embedding + if actionName == "setup-safe-outputs" || actionName == "setup-safe-inputs" { + return buildCopyFilesAction(actionsDir, actionName) } // Special handling for setup: build shell script with embedded files @@ -305,8 +305,8 @@ func buildAction(actionsDir, actionName string) error { return nil } -// buildSetupSafeOutputsAction builds the setup-safe-outputs action by copying JavaScript files -func buildSetupSafeOutputsAction(actionsDir, actionName string) error { +// buildCopyFilesAction builds actions that copy JavaScript files (setup-safe-outputs and setup-safe-inputs) +func buildCopyFilesAction(actionsDir, actionName string) error { actionPath := filepath.Join(actionsDir, actionName) jsDir := filepath.Join(actionPath, "js") @@ -435,6 +435,9 @@ func getActionDependencies(actionName string) []string { "safe_inputs_validation.cjs", "mcp_server_core.cjs", "mcp_logger.cjs", + "mcp_handler_python.cjs", + "mcp_handler_shell.cjs", + "read_buffer.cjs", }, "noop": { "load_agent_output.cjs", From 3bc06cb707982279fc2452344d176067a7ef8456 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 07:12:53 +0000 Subject: [PATCH 4/6] Use setup-safe-inputs action to copy JavaScript files instead of embedding them Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../smoke-copilot-safe-inputs.lock.yml | 1238 +---------------- pkg/workflow/mcp_servers.go | 158 ++- 2 files changed, 104 insertions(+), 1292 deletions(-) diff --git a/.github/workflows/smoke-copilot-safe-inputs.lock.yml b/.github/workflows/smoke-copilot-safe-inputs.lock.yml index b5e133fe48e..cb48901d48d 100644 --- a/.github/workflows/smoke-copilot-safe-inputs.lock.yml +++ b/.github/workflows/smoke-copilot-safe-inputs.lock.yml @@ -397,1234 +397,20 @@ jobs: EOF chmod +x /tmp/gh-aw/safeoutputs/mcp-server.cjs - - name: Setup Safe Inputs JavaScript and Config + - name: Setup Safe Inputs Directory run: | mkdir -p /tmp/gh-aw/safe-inputs/logs - cat > /tmp/gh-aw/safe-inputs/read_buffer.cjs << 'EOF_READ_BUFFER' - class ReadBuffer { - constructor() { - this._buffer = null; - } - append(chunk) { - this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; - } - readMessage() { - if (!this._buffer) { - return null; - } - const index = this._buffer.indexOf("\n"); - if (index === -1) { - return null; - } - const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, ""); - this._buffer = this._buffer.subarray(index + 1); - if (line.trim() === "") { - return this.readMessage(); - } - try { - return JSON.parse(line); - } catch (error) { - throw new Error(`Parse error: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - module.exports = { - ReadBuffer, - }; - EOF_READ_BUFFER - cat > /tmp/gh-aw/safe-inputs/mcp_server_core.cjs << 'EOF_MCP_CORE' - const fs = require("fs"); - const path = require("path"); - const { ReadBuffer } = require("./read_buffer.cjs"); - const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); - const encoder = new TextEncoder(); - function initLogFile(server) { - if (server.logFileInitialized || !server.logDir || !server.logFilePath) return; - try { - if (!fs.existsSync(server.logDir)) { - fs.mkdirSync(server.logDir, { recursive: true }); - } - const timestamp = new Date().toISOString(); - fs.writeFileSync(server.logFilePath, `# ${server.serverInfo.name} MCP Server Log\n# Started: ${timestamp}\n# Version: ${server.serverInfo.version}\n\n`); - server.logFileInitialized = true; - } catch { - } - } - function createDebugFunction(server) { - return msg => { - const timestamp = new Date().toISOString(); - const formattedMsg = `[${timestamp}] [${server.serverInfo.name}] ${msg}\n`; - process.stderr.write(formattedMsg); - if (server.logDir && server.logFilePath) { - if (!server.logFileInitialized) { - initLogFile(server); - } - if (server.logFileInitialized) { - try { - fs.appendFileSync(server.logFilePath, formattedMsg); - } catch { - } - } - } - }; - } - 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); - server.debug(`send: ${json}`); - const message = json + "\n"; - const bytes = encoder.encode(message); - fs.writeSync(1, bytes); - }; - } - function createReplyResultFunction(server) { - return (id, result) => { - if (id === undefined || id === null) return; - const res = { jsonrpc: "2.0", id, result }; - server.writeMessage(res); - }; - } - function createReplyErrorFunction(server) { - return (id, code, message) => { - if (id === undefined || id === null) { - server.debug(`Error for notification: ${message}`); - return; - } - const error = { code, message }; - const res = { - jsonrpc: "2.0", - id, - error, - }; - server.writeMessage(res); - }; - } - function createServer(serverInfo, options = {}) { - const logDir = options.logDir || undefined; - const logFilePath = logDir ? path.join(logDir, "server.log") : undefined; - const server = { - serverInfo, - tools: {}, - debug: () => {}, - debugError: () => {}, - writeMessage: () => {}, - replyResult: () => {}, - replyError: () => {}, - readBuffer: new ReadBuffer(), - logDir, - logFilePath, - 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 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); - } - } - const { createShellHandler } = require("./mcp_handler_shell.cjs"); - const timeout = tool.timeout || 60; - tool.handler = createShellHandler(server, toolName, resolvedPath, timeout); - loadedCount++; - server.debug(` [${toolName}] Shell handler created successfully with timeout: ${timeout}s`); - } else if (ext === ".py") { - server.debug(` [${toolName}] Detected Python script handler`); - try { - fs.accessSync(resolvedPath, fs.constants.X_OK); - server.debug(` [${toolName}] Python script is executable`); - } catch { - try { - fs.chmodSync(resolvedPath, 0o755); - server.debug(` [${toolName}] Made Python script executable`); - } catch (chmodError) { - server.debugError(` [${toolName}] Warning: Could not make Python script executable: `, chmodError); - } - } - const { createPythonHandler } = require("./mcp_handler_python.cjs"); - const timeout = tool.timeout || 60; - tool.handler = createPythonHandler(server, toolName, resolvedPath, timeout); - loadedCount++; - server.debug(` [${toolName}] Python handler created successfully with timeout: ${timeout}s`); - } 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] = { - ...tool, - name: normalizedName, - }; - server.debug(`Registered tool: ${normalizedName}`); - } - function normalizeTool(name) { - return name.replace(/-/g, "_").toLowerCase(); - } - async function handleRequest(server, request, defaultHandler) { - const { id, method, params } = request; - try { - if (!("id" in request)) { - return null; - } - let result; - if (method === "initialize") { - const protocolVersion = params?.protocolVersion || "2024-11-05"; - result = { - protocolVersion, - serverInfo: server.serverInfo, - capabilities: { - tools: {}, - }, - }; - } else if (method === "ping") { - result = {}; - } else if (method === "tools/list") { - const list = []; - Object.values(server.tools).forEach(tool => { - const toolDef = { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - list.push(toolDef); - }); - result = { tools: list }; - } else if (method === "tools/call") { - const name = params?.name; - const args = params?.arguments ?? {}; - if (!name || typeof name !== "string") { - throw { - code: -32602, - message: "Invalid params: 'name' must be a string", - }; - } - const tool = server.tools[normalizeTool(name)]; - if (!tool) { - throw { - code: -32602, - message: `Tool '${name}' not found`, - }; - } - let handler = tool.handler; - if (!handler && defaultHandler) { - handler = defaultHandler(tool.name); - } - if (!handler) { - throw { - code: -32603, - message: `No handler for tool: ${name}`, - }; - } - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - throw { - code: -32602, - message: `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`, - }; - } - const handlerResult = await Promise.resolve(handler(args)); - const content = handlerResult && handlerResult.content ? handlerResult.content : []; - result = { content, isError: false }; - } else if (/^notifications\//.test(method)) { - return null; - } else { - throw { - code: -32601, - message: `Method not found: ${method}`, - }; - } - return { - jsonrpc: "2.0", - id, - result, - }; - } catch (error) { - const err = error; - return { - jsonrpc: "2.0", - id, - error: { - code: err.code || -32603, - message: err.message || "Internal error", - }, - }; - } - } - async function handleMessage(server, req, defaultHandler) { - if (!req || typeof req !== "object") { - server.debug(`Invalid message: not an object`); - return; - } - if (req.jsonrpc !== "2.0") { - server.debug(`Invalid message: missing or invalid jsonrpc field`); - return; - } - const { id, method, params } = req; - if (!method || typeof method !== "string") { - server.replyError(id, -32600, "Invalid Request: method must be a string"); - return; - } - try { - if (method === "initialize") { - const clientInfo = params?.clientInfo ?? {}; - server.debug(`client info: ${JSON.stringify(clientInfo)}`); - const protocolVersion = params?.protocolVersion ?? undefined; - const result = { - serverInfo: server.serverInfo, - ...(protocolVersion ? { protocolVersion } : {}), - capabilities: { - tools: {}, - }, - }; - server.replyResult(id, result); - } else if (method === "tools/list") { - const list = []; - Object.values(server.tools).forEach(tool => { - const toolDef = { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - list.push(toolDef); - }); - server.replyResult(id, { tools: list }); - } else if (method === "tools/call") { - const name = params?.name; - const args = params?.arguments ?? {}; - if (!name || typeof name !== "string") { - server.replyError(id, -32602, "Invalid params: 'name' must be a string"); - return; - } - const tool = server.tools[normalizeTool(name)]; - if (!tool) { - server.replyError(id, -32601, `Tool not found: ${name} (${normalizeTool(name)})`); - return; - } - let handler = tool.handler; - if (!handler && defaultHandler) { - handler = defaultHandler(tool.name); - } - if (!handler) { - server.replyError(id, -32603, `No handler for tool: ${name}`); - return; - } - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - server.replyError(id, -32602, `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); - return; - } - 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)) { - server.debug(`ignore ${method}`); - } else { - server.replyError(id, -32601, `Method not found: ${method}`); - } - } catch (e) { - server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); - } - } - async function processReadBuffer(server, defaultHandler) { - while (true) { - try { - const message = server.readBuffer.readMessage(); - if (!message) { - break; - } - server.debug(`recv: ${JSON.stringify(message)}`); - await handleMessage(server, message, defaultHandler); - } catch (error) { - server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - function start(server, options = {}) { - const { defaultHandler } = options; - server.debug(`v${server.serverInfo.version} ready on stdio`); - server.debug(` tools: ${Object.keys(server.tools).join(", ")}`); - if (!Object.keys(server.tools).length) { - throw new Error("No tools registered"); - } - const onData = async chunk => { - server.readBuffer.append(chunk); - await processReadBuffer(server, defaultHandler); - }; - process.stdin.on("data", onData); - process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); - process.stdin.resume(); - server.debug(`listening...`); - } - module.exports = { - createServer, - registerTool, - normalizeTool, - handleRequest, - handleMessage, - processReadBuffer, - start, - loadToolHandlers, - }; - EOF_MCP_CORE - cat > /tmp/gh-aw/safe-inputs/mcp_http_transport.cjs << 'EOF_MCP_HTTP_TRANSPORT' - const http = require("http"); - const { randomUUID } = require("crypto"); - const { createServer, registerTool, handleRequest } = require("./mcp_server_core.cjs"); - class MCPServer { - constructor(serverInfo, options = {}) { - this._coreServer = createServer(serverInfo, options); - this.serverInfo = serverInfo; - this.capabilities = options.capabilities || { tools: {} }; - this.tools = new Map(); - this.transport = null; - this.initialized = false; - } - tool(name, description, inputSchema, handler) { - this.tools.set(name, { - name, - description, - inputSchema, - handler, - }); - registerTool(this._coreServer, { - name, - description, - inputSchema, - handler, - }); - } - async connect(transport) { - this.transport = transport; - transport.setServer(this); - await transport.start(); - } - async handleRequest(request) { - if (request.method === "initialize") { - this.initialized = true; - } - return handleRequest(this._coreServer, request); - } - } - class MCPHTTPTransport { - constructor(options = {}) { - this.sessionIdGenerator = options.sessionIdGenerator; - this.enableJsonResponse = options.enableJsonResponse !== false; - this.enableDnsRebindingProtection = options.enableDnsRebindingProtection || false; - this.server = null; - this.sessionId = null; - this.started = false; - } - setServer(server) { - this.server = server; - } - async start() { - if (this.started) { - throw new Error("Transport already started"); - } - this.started = true; - } - async handleRequest(req, res, parsedBody) { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id"); - if (req.method === "OPTIONS") { - res.writeHead(200); - res.end(); - return; - } - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Method not allowed" })); - return; - } - try { - let body = parsedBody; - if (!body) { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const bodyStr = Buffer.concat(chunks).toString(); - try { - body = bodyStr ? JSON.parse(bodyStr) : null; - } catch (parseError) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32700, - message: "Parse error: Invalid JSON in request body", - }, - id: null, - }) - ); - return; - } - } - if (!body) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: Empty request body", - }, - id: null, - }) - ); - return; - } - if (!body.jsonrpc || body.jsonrpc !== "2.0") { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: jsonrpc must be '2.0'", - }, - id: body.id || null, - }) - ); - return; - } - if (this.sessionIdGenerator) { - if (body.method === "initialize") { - this.sessionId = this.sessionIdGenerator(); - } else { - const requestSessionId = req.headers["mcp-session-id"]; - if (!requestSessionId) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: Missing Mcp-Session-Id header", - }, - id: body.id || null, - }) - ); - return; - } - if (requestSessionId !== this.sessionId) { - res.writeHead(404, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32001, - message: "Session not found", - }, - id: body.id || null, - }) - ); - return; - } - } - } - const response = await this.server.handleRequest(body); - if (response === null) { - res.writeHead(204); - res.end(); - return; - } - const headers = { "Content-Type": "application/json" }; - if (this.sessionId) { - headers["mcp-session-id"] = this.sessionId; - } - res.writeHead(200, headers); - res.end(JSON.stringify(response)); - } catch (error) { - if (!res.headersSent) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), - }, - id: null, - }) - ); - } - } - } - } - module.exports = { - MCPServer, - MCPHTTPTransport, - }; - EOF_MCP_HTTP_TRANSPORT - cat > /tmp/gh-aw/safe-inputs/mcp_logger.cjs << 'EOF_MCP_LOGGER' - function createLogger(serverName) { - const logger = { - debug: msg => { - const timestamp = new Date().toISOString(); - process.stderr.write(`[${timestamp}] [${serverName}] ${msg}\n`); - }, - debugError: (prefix, error) => { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.debug(`${prefix}${errorMessage}`); - if (error instanceof Error && error.stack) { - logger.debug(`${prefix}Stack trace: ${error.stack}`); - } - }, - }; - return logger; - } - module.exports = { - createLogger, - }; - EOF_MCP_LOGGER - cat > /tmp/gh-aw/safe-inputs/mcp_handler_shell.cjs << 'EOF_HANDLER_SHELL' - const fs = require("fs"); - const path = require("path"); - const { execFile } = require("child_process"); - const os = require("os"); - function createShellHandler(server, toolName, scriptPath, timeoutSeconds = 60) { - return async args => { - server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); - server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); - server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); - 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: timeoutSeconds * 1000, - 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), - }, - ], - }); - } - ); - }); - }; - } - module.exports = { - createShellHandler, - }; - EOF_HANDLER_SHELL - cat > /tmp/gh-aw/safe-inputs/mcp_handler_python.cjs << 'EOF_HANDLER_PYTHON' - const { execFile } = require("child_process"); - function createPythonHandler(server, toolName, scriptPath, timeoutSeconds = 60) { - return async args => { - server.debug(` [${toolName}] Invoking Python handler: ${scriptPath}`); - server.debug(` [${toolName}] Python handler args: ${JSON.stringify(args)}`); - server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); - const inputJson = JSON.stringify(args || {}); - server.debug(` [${toolName}] Input JSON (${inputJson.length} bytes): ${inputJson.substring(0, 200)}${inputJson.length > 200 ? "..." : ""}`); - return new Promise((resolve, reject) => { - server.debug(` [${toolName}] Executing Python script...`); - const child = execFile( - "python3", - [scriptPath], - { - env: process.env, - timeout: timeoutSeconds * 1000, - 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}] Python script error: `, error); - reject(error); - return; - } - let result; - try { - if (stdout && stdout.trim()) { - result = JSON.parse(stdout.trim()); - } else { - result = { stdout: stdout || "", stderr: stderr || "" }; - } - } catch (parseError) { - server.debug(` [${toolName}] Output is not JSON, returning as text`); - result = { stdout: stdout || "", stderr: stderr || "" }; - } - server.debug(` [${toolName}] Python handler completed successfully`); - resolve({ - content: [ - { - type: "text", - text: JSON.stringify(result), - }, - ], - }); - } - ); - if (child.stdin) { - child.stdin.write(inputJson); - child.stdin.end(); - } - }); - }; - } - module.exports = { - createPythonHandler, - }; - EOF_HANDLER_PYTHON - cat > /tmp/gh-aw/safe-inputs/safe_inputs_config_loader.cjs << 'EOF_CONFIG_LOADER' - const fs = require("fs"); - function loadConfig(configPath) { - if (!fs.existsSync(configPath)) { - throw new Error(`Configuration file not found: ${configPath}`); - } - const configContent = fs.readFileSync(configPath, "utf-8"); - const config = JSON.parse(configContent); - if (!config.tools || !Array.isArray(config.tools)) { - throw new Error("Configuration must contain a 'tools' array"); - } - return config; - } - module.exports = { - loadConfig, - }; - EOF_CONFIG_LOADER - cat > /tmp/gh-aw/safe-inputs/safe_inputs_tool_factory.cjs << 'EOF_TOOL_FACTORY' - function createToolConfig(name, description, inputSchema, handlerPath) { - return { - name, - description, - inputSchema, - handler: handlerPath, - }; - } - module.exports = { - createToolConfig, - }; - EOF_TOOL_FACTORY - cat > /tmp/gh-aw/safe-inputs/safe_inputs_validation.cjs << 'EOF_VALIDATION' - function validateRequiredFields(args, inputSchema) { - const requiredFields = inputSchema && Array.isArray(inputSchema.required) ? inputSchema.required : []; - if (!requiredFields.length) { - return []; - } - const missing = requiredFields.filter(f => { - const value = args[f]; - return value === undefined || value === null || (typeof value === "string" && value.trim() === ""); - }); - return missing; - } - module.exports = { - validateRequiredFields, - }; - EOF_VALIDATION - cat > /tmp/gh-aw/safe-inputs/safe_inputs_bootstrap.cjs << 'EOF_BOOTSTRAP' - const path = require("path"); - const fs = require("fs"); - const { loadConfig } = require("./safe_inputs_config_loader.cjs"); - const { loadToolHandlers } = require("./mcp_server_core.cjs"); - function bootstrapSafeInputsServer(configPath, logger) { - logger.debug(`Loading safe-inputs configuration from: ${configPath}`); - const config = loadConfig(configPath); - const basePath = path.dirname(configPath); - logger.debug(`Base path for handlers: ${basePath}`); - logger.debug(`Tools to load: ${config.tools.length}`); - const tools = loadToolHandlers(logger, config.tools, basePath); - return { config, basePath, tools }; - } - function cleanupConfigFile(configPath, logger) { - try { - if (fs.existsSync(configPath)) { - fs.unlinkSync(configPath); - logger.debug(`Deleted configuration file: ${configPath}`); - } - } catch (error) { - logger.debugError(`Warning: Could not delete configuration file: `, error); - } - } - module.exports = { - bootstrapSafeInputsServer, - cleanupConfigFile, - }; - EOF_BOOTSTRAP - cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server.cjs << 'EOF_SAFE_INPUTS_SERVER' - const { createServer, registerTool, start } = require("./mcp_server_core.cjs"); - const { loadConfig } = require("./safe_inputs_config_loader.cjs"); - const { createToolConfig } = require("./safe_inputs_tool_factory.cjs"); - const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); - function startSafeInputsServer(configPath, options = {}) { - const logDir = options.logDir || undefined; - const server = createServer({ name: "safeinputs", version: "1.0.0" }, { logDir }); - const { config, tools } = bootstrapSafeInputsServer(configPath, server); - server.serverInfo.name = config.serverName || "safeinputs"; - server.serverInfo.version = config.version || "1.0.0"; - if (!options.logDir && config.logDir) { - server.logDir = config.logDir; - } - for (const tool of tools) { - registerTool(server, tool); - } - if (!options.skipCleanup) { - cleanupConfigFile(configPath, server); - } - start(server); - } - if (require.main === module) { - const args = process.argv.slice(2); - if (args.length < 1) { - console.error("Usage: node safe_inputs_mcp_server.cjs [--log-dir ]"); - process.exit(1); - } - const configPath = args[0]; - const options = {}; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--log-dir" && args[i + 1]) { - options.logDir = args[i + 1]; - i++; - } - } - try { - startSafeInputsServer(configPath, options); - } catch (error) { - console.error(`Error starting safe-inputs server: ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - } - } - module.exports = { - startSafeInputsServer, - loadConfig, - createToolConfig, - }; - EOF_SAFE_INPUTS_SERVER - cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server_http.cjs << 'EOF_SAFE_INPUTS_SERVER_HTTP' - const http = require("http"); - const { randomUUID } = require("crypto"); - const { MCPServer, MCPHTTPTransport } = require("./mcp_http_transport.cjs"); - const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); - const { createLogger } = require("./mcp_logger.cjs"); - const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); - function createMCPServer(configPath, options = {}) { - const logger = createLogger("safeinputs"); - logger.debug(`=== Creating MCP Server ===`); - logger.debug(`Configuration file: ${configPath}`); - const { config, tools } = bootstrapSafeInputsServer(configPath, logger); - const serverName = config.serverName || "safeinputs"; - const version = config.version || "1.0.0"; - logger.debug(`Server name: ${serverName}`); - logger.debug(`Server version: ${version}`); - const server = new MCPServer( - { - name: serverName, - version: version, - }, - { - capabilities: { - tools: {}, - }, - } - ); - logger.debug(`Registering tools with MCP server...`); - let registeredCount = 0; - let skippedCount = 0; - for (const tool of tools) { - if (!tool.handler) { - logger.debug(`Skipping tool ${tool.name} - no handler loaded`); - skippedCount++; - continue; - } - logger.debug(`Registering tool: ${tool.name}`); - server.tool(tool.name, tool.description || "", tool.inputSchema || { type: "object", properties: {} }, async args => { - logger.debug(`Calling handler for tool: ${tool.name}`); - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - throw new Error(`Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); - } - const result = await Promise.resolve(tool.handler(args)); - logger.debug(`Handler returned for tool: ${tool.name}`); - const content = result && result.content ? result.content : []; - return { content, isError: false }; - }); - registeredCount++; - } - logger.debug(`Tool registration complete: ${registeredCount} registered, ${skippedCount} skipped`); - logger.debug(`=== MCP Server Creation Complete ===`); - cleanupConfigFile(configPath, logger); - return { server, config, logger }; - } - async function startHttpServer(configPath, options = {}) { - const port = options.port || 3000; - const stateless = options.stateless || false; - const logger = createLogger("safe-inputs-startup"); - logger.debug(`=== Starting Safe Inputs MCP HTTP Server ===`); - logger.debug(`Configuration file: ${configPath}`); - logger.debug(`Port: ${port}`); - logger.debug(`Mode: ${stateless ? "stateless" : "stateful"}`); - logger.debug(`Environment: NODE_VERSION=${process.version}, PLATFORM=${process.platform}`); - try { - const { server, config, logger: mcpLogger } = createMCPServer(configPath, { logDir: options.logDir }); - Object.assign(logger, mcpLogger); - logger.debug(`MCP server created successfully`); - logger.debug(`Server name: ${config.serverName || "safeinputs"}`); - logger.debug(`Server version: ${config.version || "1.0.0"}`); - logger.debug(`Tools configured: ${config.tools.length}`); - logger.debug(`Creating HTTP transport...`); - const transport = new MCPHTTPTransport({ - sessionIdGenerator: stateless ? undefined : () => randomUUID(), - enableJsonResponse: true, - enableDnsRebindingProtection: false, - }); - logger.debug(`HTTP transport created`); - logger.debug(`Connecting server to transport...`); - await server.connect(transport); - logger.debug(`Server connected to transport successfully`); - logger.debug(`Creating HTTP server...`); - const httpServer = http.createServer(async (req, res) => { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept"); - if (req.method === "OPTIONS") { - res.writeHead(200); - res.end(); - return; - } - if (req.method === "GET" && req.url === "/health") { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - status: "ok", - server: config.serverName || "safeinputs", - version: config.version || "1.0.0", - tools: config.tools.length, - }) - ); - return; - } - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Method not allowed" })); - return; - } - try { - let body = null; - if (req.method === "POST") { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const bodyStr = Buffer.concat(chunks).toString(); - try { - body = bodyStr ? JSON.parse(bodyStr) : null; - } catch (parseError) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32700, - message: "Parse error: Invalid JSON in request body", - }, - id: null, - }) - ); - return; - } - } - await transport.handleRequest(req, res, body); - } catch (error) { - logger.debugError("Error handling request: ", error); - if (!res.headersSent) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), - }, - id: null, - }) - ); - } - } - }); - logger.debug(`Attempting to bind to port ${port}...`); - httpServer.listen(port, () => { - logger.debug(`=== Safe Inputs MCP HTTP Server Started Successfully ===`); - logger.debug(`HTTP server listening on http://localhost:${port}`); - logger.debug(`MCP endpoint: POST http://localhost:${port}/`); - logger.debug(`Server name: ${config.serverName || "safeinputs"}`); - logger.debug(`Server version: ${config.version || "1.0.0"}`); - logger.debug(`Tools available: ${config.tools.length}`); - logger.debug(`Server is ready to accept requests`); - }); - httpServer.on("error", error => { - if (error.code === "EADDRINUSE") { - logger.debugError(`ERROR: Port ${port} is already in use. `, error); - } else if (error.code === "EACCES") { - logger.debugError(`ERROR: Permission denied to bind to port ${port}. `, error); - } else { - logger.debugError(`ERROR: Failed to start HTTP server: `, error); - } - process.exit(1); - }); - process.on("SIGINT", () => { - logger.debug("Received SIGINT, shutting down..."); - httpServer.close(() => { - logger.debug("HTTP server closed"); - process.exit(0); - }); - }); - process.on("SIGTERM", () => { - logger.debug("Received SIGTERM, shutting down..."); - httpServer.close(() => { - logger.debug("HTTP server closed"); - process.exit(0); - }); - }); - return httpServer; - } catch (error) { - const errorLogger = createLogger("safe-inputs-startup-error"); - errorLogger.debug(`=== FATAL ERROR: Failed to start Safe Inputs MCP HTTP Server ===`); - errorLogger.debug(`Error type: ${error.constructor.name}`); - errorLogger.debug(`Error message: ${error.message}`); - if (error.stack) { - errorLogger.debug(`Stack trace:\n${error.stack}`); - } - if (error.code) { - errorLogger.debug(`Error code: ${error.code}`); - } - errorLogger.debug(`Configuration file: ${configPath}`); - errorLogger.debug(`Port: ${port}`); - throw error; - } - } - if (require.main === module) { - const args = process.argv.slice(2); - if (args.length < 1) { - console.error("Usage: node safe_inputs_mcp_server_http.cjs [--port ] [--stateless] [--log-dir ]"); - process.exit(1); - } - const configPath = args[0]; - const options = { - port: 3000, - stateless: false, - logDir: undefined, - }; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--port" && args[i + 1]) { - options.port = parseInt(args[i + 1], 10); - i++; - } else if (args[i] === "--stateless") { - options.stateless = true; - } else if (args[i] === "--log-dir" && args[i + 1]) { - options.logDir = args[i + 1]; - i++; - } - } - startHttpServer(configPath, options).catch(error => { - console.error(`Error starting HTTP server: ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - }); - } - module.exports = { - startHttpServer, - createMCPServer, - }; - EOF_SAFE_INPUTS_SERVER_HTTP + - name: Checkout actions folder for safe-inputs + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + sparse-checkout: | + actions + - name: Setup Safe Inputs JavaScript Files + uses: ./actions/setup-safe-inputs + with: + destination: /tmp/gh-aw/safe-inputs + - name: Setup Safe Inputs Config Files + run: | cat > /tmp/gh-aw/safe-inputs/tools.json << 'EOF_TOOLS_JSON' { "serverName": "safeinputs", diff --git a/pkg/workflow/mcp_servers.go b/pkg/workflow/mcp_servers.go index 254b9dee605..ffb78a8c3fe 100644 --- a/pkg/workflow/mcp_servers.go +++ b/pkg/workflow/mcp_servers.go @@ -380,87 +380,113 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any, // Write safe-inputs MCP server if configured and feature flag is enabled // For stdio mode, we only write the files but don't start the HTTP server if IsSafeInputsEnabled(workflowData.SafeInputs, workflowData) { - // Step 1: Write JavaScript and config files - yaml.WriteString(" - name: Setup Safe Inputs JavaScript and Config\n") + // Step 1: Create logs directory and copy JavaScript files using the setup-safe-inputs action + yaml.WriteString(" - name: Setup Safe Inputs Directory\n") yaml.WriteString(" run: |\n") yaml.WriteString(" mkdir -p /tmp/gh-aw/safe-inputs/logs\n") - // Write the reusable MCP server core modules - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/read_buffer.cjs << 'EOF_READ_BUFFER'\n") - for _, line := range FormatJavaScriptForYAML(GetReadBufferScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_READ_BUFFER\n") + // Step 2: Copy JavaScript files using the setup-safe-inputs action + setupSafeInputsActionRef := c.resolveActionReference("./actions/setup-safe-inputs", workflowData) + if setupSafeInputsActionRef != "" { + // For dev mode (local action path), checkout the actions folder first + if c.actionMode.IsDev() { + yaml.WriteString(" - name: Checkout actions folder for safe-inputs\n") + fmt.Fprintf(yaml, " uses: %s\n", GetActionPin("actions/checkout")) + yaml.WriteString(" with:\n") + yaml.WriteString(" sparse-checkout: |\n") + yaml.WriteString(" actions\n") + } - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/mcp_server_core.cjs << 'EOF_MCP_CORE'\n") - for _, line := range FormatJavaScriptForYAML(GetMCPServerCoreScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_MCP_CORE\n") + yaml.WriteString(" - name: Setup Safe Inputs JavaScript Files\n") + fmt.Fprintf(yaml, " uses: %s\n", setupSafeInputsActionRef) + yaml.WriteString(" with:\n") + yaml.WriteString(" destination: /tmp/gh-aw/safe-inputs\n") + } else { + // Fallback: Write JavaScript files directly if action reference cannot be resolved + yaml.WriteString(" - name: Write Safe Inputs JavaScript Files\n") + yaml.WriteString(" run: |\n") - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/mcp_http_transport.cjs << 'EOF_MCP_HTTP_TRANSPORT'\n") - for _, line := range FormatJavaScriptForYAML(GetMCPHTTPTransportScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_MCP_HTTP_TRANSPORT\n") + // Write the reusable MCP server core modules + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/read_buffer.cjs << 'EOF_READ_BUFFER'\n") + for _, line := range FormatJavaScriptForYAML(GetReadBufferScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_READ_BUFFER\n") - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/mcp_logger.cjs << 'EOF_MCP_LOGGER'\n") - for _, line := range FormatJavaScriptForYAML(GetMCPLoggerScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_MCP_LOGGER\n") + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/mcp_server_core.cjs << 'EOF_MCP_CORE'\n") + for _, line := range FormatJavaScriptForYAML(GetMCPServerCoreScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_MCP_CORE\n") - // Write handler modules (only loaded when needed) - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/mcp_handler_shell.cjs << 'EOF_HANDLER_SHELL'\n") - for _, line := range FormatJavaScriptForYAML(GetMCPHandlerShellScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_HANDLER_SHELL\n") + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/mcp_http_transport.cjs << 'EOF_MCP_HTTP_TRANSPORT'\n") + for _, line := range FormatJavaScriptForYAML(GetMCPHTTPTransportScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_MCP_HTTP_TRANSPORT\n") - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/mcp_handler_python.cjs << 'EOF_HANDLER_PYTHON'\n") - for _, line := range FormatJavaScriptForYAML(GetMCPHandlerPythonScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_HANDLER_PYTHON\n") + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/mcp_logger.cjs << 'EOF_MCP_LOGGER'\n") + for _, line := range FormatJavaScriptForYAML(GetMCPLoggerScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_MCP_LOGGER\n") - // Write safe-inputs helper modules - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_config_loader.cjs << 'EOF_CONFIG_LOADER'\n") - for _, line := range FormatJavaScriptForYAML(GetSafeInputsConfigLoaderScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_CONFIG_LOADER\n") + // Write handler modules (only loaded when needed) + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/mcp_handler_shell.cjs << 'EOF_HANDLER_SHELL'\n") + for _, line := range FormatJavaScriptForYAML(GetMCPHandlerShellScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_HANDLER_SHELL\n") - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_tool_factory.cjs << 'EOF_TOOL_FACTORY'\n") - for _, line := range FormatJavaScriptForYAML(GetSafeInputsToolFactoryScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_TOOL_FACTORY\n") + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/mcp_handler_python.cjs << 'EOF_HANDLER_PYTHON'\n") + for _, line := range FormatJavaScriptForYAML(GetMCPHandlerPythonScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_HANDLER_PYTHON\n") - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_validation.cjs << 'EOF_VALIDATION'\n") - for _, line := range FormatJavaScriptForYAML(GetSafeInputsValidationScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_VALIDATION\n") + // Write safe-inputs helper modules + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_config_loader.cjs << 'EOF_CONFIG_LOADER'\n") + for _, line := range FormatJavaScriptForYAML(GetSafeInputsConfigLoaderScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_CONFIG_LOADER\n") - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_bootstrap.cjs << 'EOF_BOOTSTRAP'\n") - for _, line := range FormatJavaScriptForYAML(GetSafeInputsBootstrapScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_BOOTSTRAP\n") + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_tool_factory.cjs << 'EOF_TOOL_FACTORY'\n") + for _, line := range FormatJavaScriptForYAML(GetSafeInputsToolFactoryScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_TOOL_FACTORY\n") - // Write safe-inputs MCP server main module - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server.cjs << 'EOF_SAFE_INPUTS_SERVER'\n") - for _, line := range FormatJavaScriptForYAML(GetSafeInputsMCPServerScript()) { - yaml.WriteString(line) - } - yaml.WriteString(" EOF_SAFE_INPUTS_SERVER\n") + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_validation.cjs << 'EOF_VALIDATION'\n") + for _, line := range FormatJavaScriptForYAML(GetSafeInputsValidationScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_VALIDATION\n") - // Write safe-inputs MCP server HTTP transport module - yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server_http.cjs << 'EOF_SAFE_INPUTS_SERVER_HTTP'\n") - for _, line := range FormatJavaScriptForYAML(GetSafeInputsMCPServerHTTPScript()) { - yaml.WriteString(line) + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_bootstrap.cjs << 'EOF_BOOTSTRAP'\n") + for _, line := range FormatJavaScriptForYAML(GetSafeInputsBootstrapScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_BOOTSTRAP\n") + + // Write safe-inputs MCP server main module + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server.cjs << 'EOF_SAFE_INPUTS_SERVER'\n") + for _, line := range FormatJavaScriptForYAML(GetSafeInputsMCPServerScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_SAFE_INPUTS_SERVER\n") + + // Write safe-inputs MCP server HTTP transport module + yaml.WriteString(" cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server_http.cjs << 'EOF_SAFE_INPUTS_SERVER_HTTP'\n") + for _, line := range FormatJavaScriptForYAML(GetSafeInputsMCPServerHTTPScript()) { + yaml.WriteString(line) + } + yaml.WriteString(" EOF_SAFE_INPUTS_SERVER_HTTP\n") } - yaml.WriteString(" EOF_SAFE_INPUTS_SERVER_HTTP\n") + + // Step 3: Write configuration files (tools.json and mcp-server.cjs entry point) + yaml.WriteString(" - name: Setup Safe Inputs Config Files\n") + yaml.WriteString(" run: |\n") // Generate the tools.json configuration file toolsJSON := generateSafeInputsToolsConfig(workflowData.SafeInputs) From fd7c39e6759b4c89cad99645ef4a90f4c60c607d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 07:13:41 +0000 Subject: [PATCH 5/6] Recompile all workflows with setup-safe-inputs action Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../copilot-pr-merged-report.lock.yml | 1238 +---------------- .../daily-performance-summary.lock.yml | 1238 +---------------- .../smoke-copilot-no-firewall.lock.yml | 1238 +---------------- .../smoke-copilot-playwright.lock.yml | 1238 +---------------- 4 files changed, 48 insertions(+), 4904 deletions(-) diff --git a/.github/workflows/copilot-pr-merged-report.lock.yml b/.github/workflows/copilot-pr-merged-report.lock.yml index 504c357497f..40fe7ef5f30 100644 --- a/.github/workflows/copilot-pr-merged-report.lock.yml +++ b/.github/workflows/copilot-pr-merged-report.lock.yml @@ -347,1234 +347,20 @@ jobs: EOF chmod +x /tmp/gh-aw/safeoutputs/mcp-server.cjs - - name: Setup Safe Inputs JavaScript and Config + - name: Setup Safe Inputs Directory run: | mkdir -p /tmp/gh-aw/safe-inputs/logs - cat > /tmp/gh-aw/safe-inputs/read_buffer.cjs << 'EOF_READ_BUFFER' - class ReadBuffer { - constructor() { - this._buffer = null; - } - append(chunk) { - this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; - } - readMessage() { - if (!this._buffer) { - return null; - } - const index = this._buffer.indexOf("\n"); - if (index === -1) { - return null; - } - const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, ""); - this._buffer = this._buffer.subarray(index + 1); - if (line.trim() === "") { - return this.readMessage(); - } - try { - return JSON.parse(line); - } catch (error) { - throw new Error(`Parse error: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - module.exports = { - ReadBuffer, - }; - EOF_READ_BUFFER - cat > /tmp/gh-aw/safe-inputs/mcp_server_core.cjs << 'EOF_MCP_CORE' - const fs = require("fs"); - const path = require("path"); - const { ReadBuffer } = require("./read_buffer.cjs"); - const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); - const encoder = new TextEncoder(); - function initLogFile(server) { - if (server.logFileInitialized || !server.logDir || !server.logFilePath) return; - try { - if (!fs.existsSync(server.logDir)) { - fs.mkdirSync(server.logDir, { recursive: true }); - } - const timestamp = new Date().toISOString(); - fs.writeFileSync(server.logFilePath, `# ${server.serverInfo.name} MCP Server Log\n# Started: ${timestamp}\n# Version: ${server.serverInfo.version}\n\n`); - server.logFileInitialized = true; - } catch { - } - } - function createDebugFunction(server) { - return msg => { - const timestamp = new Date().toISOString(); - const formattedMsg = `[${timestamp}] [${server.serverInfo.name}] ${msg}\n`; - process.stderr.write(formattedMsg); - if (server.logDir && server.logFilePath) { - if (!server.logFileInitialized) { - initLogFile(server); - } - if (server.logFileInitialized) { - try { - fs.appendFileSync(server.logFilePath, formattedMsg); - } catch { - } - } - } - }; - } - 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); - server.debug(`send: ${json}`); - const message = json + "\n"; - const bytes = encoder.encode(message); - fs.writeSync(1, bytes); - }; - } - function createReplyResultFunction(server) { - return (id, result) => { - if (id === undefined || id === null) return; - const res = { jsonrpc: "2.0", id, result }; - server.writeMessage(res); - }; - } - function createReplyErrorFunction(server) { - return (id, code, message) => { - if (id === undefined || id === null) { - server.debug(`Error for notification: ${message}`); - return; - } - const error = { code, message }; - const res = { - jsonrpc: "2.0", - id, - error, - }; - server.writeMessage(res); - }; - } - function createServer(serverInfo, options = {}) { - const logDir = options.logDir || undefined; - const logFilePath = logDir ? path.join(logDir, "server.log") : undefined; - const server = { - serverInfo, - tools: {}, - debug: () => {}, - debugError: () => {}, - writeMessage: () => {}, - replyResult: () => {}, - replyError: () => {}, - readBuffer: new ReadBuffer(), - logDir, - logFilePath, - 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 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); - } - } - const { createShellHandler } = require("./mcp_handler_shell.cjs"); - const timeout = tool.timeout || 60; - tool.handler = createShellHandler(server, toolName, resolvedPath, timeout); - loadedCount++; - server.debug(` [${toolName}] Shell handler created successfully with timeout: ${timeout}s`); - } else if (ext === ".py") { - server.debug(` [${toolName}] Detected Python script handler`); - try { - fs.accessSync(resolvedPath, fs.constants.X_OK); - server.debug(` [${toolName}] Python script is executable`); - } catch { - try { - fs.chmodSync(resolvedPath, 0o755); - server.debug(` [${toolName}] Made Python script executable`); - } catch (chmodError) { - server.debugError(` [${toolName}] Warning: Could not make Python script executable: `, chmodError); - } - } - const { createPythonHandler } = require("./mcp_handler_python.cjs"); - const timeout = tool.timeout || 60; - tool.handler = createPythonHandler(server, toolName, resolvedPath, timeout); - loadedCount++; - server.debug(` [${toolName}] Python handler created successfully with timeout: ${timeout}s`); - } 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] = { - ...tool, - name: normalizedName, - }; - server.debug(`Registered tool: ${normalizedName}`); - } - function normalizeTool(name) { - return name.replace(/-/g, "_").toLowerCase(); - } - async function handleRequest(server, request, defaultHandler) { - const { id, method, params } = request; - try { - if (!("id" in request)) { - return null; - } - let result; - if (method === "initialize") { - const protocolVersion = params?.protocolVersion || "2024-11-05"; - result = { - protocolVersion, - serverInfo: server.serverInfo, - capabilities: { - tools: {}, - }, - }; - } else if (method === "ping") { - result = {}; - } else if (method === "tools/list") { - const list = []; - Object.values(server.tools).forEach(tool => { - const toolDef = { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - list.push(toolDef); - }); - result = { tools: list }; - } else if (method === "tools/call") { - const name = params?.name; - const args = params?.arguments ?? {}; - if (!name || typeof name !== "string") { - throw { - code: -32602, - message: "Invalid params: 'name' must be a string", - }; - } - const tool = server.tools[normalizeTool(name)]; - if (!tool) { - throw { - code: -32602, - message: `Tool '${name}' not found`, - }; - } - let handler = tool.handler; - if (!handler && defaultHandler) { - handler = defaultHandler(tool.name); - } - if (!handler) { - throw { - code: -32603, - message: `No handler for tool: ${name}`, - }; - } - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - throw { - code: -32602, - message: `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`, - }; - } - const handlerResult = await Promise.resolve(handler(args)); - const content = handlerResult && handlerResult.content ? handlerResult.content : []; - result = { content, isError: false }; - } else if (/^notifications\//.test(method)) { - return null; - } else { - throw { - code: -32601, - message: `Method not found: ${method}`, - }; - } - return { - jsonrpc: "2.0", - id, - result, - }; - } catch (error) { - const err = error; - return { - jsonrpc: "2.0", - id, - error: { - code: err.code || -32603, - message: err.message || "Internal error", - }, - }; - } - } - async function handleMessage(server, req, defaultHandler) { - if (!req || typeof req !== "object") { - server.debug(`Invalid message: not an object`); - return; - } - if (req.jsonrpc !== "2.0") { - server.debug(`Invalid message: missing or invalid jsonrpc field`); - return; - } - const { id, method, params } = req; - if (!method || typeof method !== "string") { - server.replyError(id, -32600, "Invalid Request: method must be a string"); - return; - } - try { - if (method === "initialize") { - const clientInfo = params?.clientInfo ?? {}; - server.debug(`client info: ${JSON.stringify(clientInfo)}`); - const protocolVersion = params?.protocolVersion ?? undefined; - const result = { - serverInfo: server.serverInfo, - ...(protocolVersion ? { protocolVersion } : {}), - capabilities: { - tools: {}, - }, - }; - server.replyResult(id, result); - } else if (method === "tools/list") { - const list = []; - Object.values(server.tools).forEach(tool => { - const toolDef = { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - list.push(toolDef); - }); - server.replyResult(id, { tools: list }); - } else if (method === "tools/call") { - const name = params?.name; - const args = params?.arguments ?? {}; - if (!name || typeof name !== "string") { - server.replyError(id, -32602, "Invalid params: 'name' must be a string"); - return; - } - const tool = server.tools[normalizeTool(name)]; - if (!tool) { - server.replyError(id, -32601, `Tool not found: ${name} (${normalizeTool(name)})`); - return; - } - let handler = tool.handler; - if (!handler && defaultHandler) { - handler = defaultHandler(tool.name); - } - if (!handler) { - server.replyError(id, -32603, `No handler for tool: ${name}`); - return; - } - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - server.replyError(id, -32602, `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); - return; - } - 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)) { - server.debug(`ignore ${method}`); - } else { - server.replyError(id, -32601, `Method not found: ${method}`); - } - } catch (e) { - server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); - } - } - async function processReadBuffer(server, defaultHandler) { - while (true) { - try { - const message = server.readBuffer.readMessage(); - if (!message) { - break; - } - server.debug(`recv: ${JSON.stringify(message)}`); - await handleMessage(server, message, defaultHandler); - } catch (error) { - server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - function start(server, options = {}) { - const { defaultHandler } = options; - server.debug(`v${server.serverInfo.version} ready on stdio`); - server.debug(` tools: ${Object.keys(server.tools).join(", ")}`); - if (!Object.keys(server.tools).length) { - throw new Error("No tools registered"); - } - const onData = async chunk => { - server.readBuffer.append(chunk); - await processReadBuffer(server, defaultHandler); - }; - process.stdin.on("data", onData); - process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); - process.stdin.resume(); - server.debug(`listening...`); - } - module.exports = { - createServer, - registerTool, - normalizeTool, - handleRequest, - handleMessage, - processReadBuffer, - start, - loadToolHandlers, - }; - EOF_MCP_CORE - cat > /tmp/gh-aw/safe-inputs/mcp_http_transport.cjs << 'EOF_MCP_HTTP_TRANSPORT' - const http = require("http"); - const { randomUUID } = require("crypto"); - const { createServer, registerTool, handleRequest } = require("./mcp_server_core.cjs"); - class MCPServer { - constructor(serverInfo, options = {}) { - this._coreServer = createServer(serverInfo, options); - this.serverInfo = serverInfo; - this.capabilities = options.capabilities || { tools: {} }; - this.tools = new Map(); - this.transport = null; - this.initialized = false; - } - tool(name, description, inputSchema, handler) { - this.tools.set(name, { - name, - description, - inputSchema, - handler, - }); - registerTool(this._coreServer, { - name, - description, - inputSchema, - handler, - }); - } - async connect(transport) { - this.transport = transport; - transport.setServer(this); - await transport.start(); - } - async handleRequest(request) { - if (request.method === "initialize") { - this.initialized = true; - } - return handleRequest(this._coreServer, request); - } - } - class MCPHTTPTransport { - constructor(options = {}) { - this.sessionIdGenerator = options.sessionIdGenerator; - this.enableJsonResponse = options.enableJsonResponse !== false; - this.enableDnsRebindingProtection = options.enableDnsRebindingProtection || false; - this.server = null; - this.sessionId = null; - this.started = false; - } - setServer(server) { - this.server = server; - } - async start() { - if (this.started) { - throw new Error("Transport already started"); - } - this.started = true; - } - async handleRequest(req, res, parsedBody) { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id"); - if (req.method === "OPTIONS") { - res.writeHead(200); - res.end(); - return; - } - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Method not allowed" })); - return; - } - try { - let body = parsedBody; - if (!body) { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const bodyStr = Buffer.concat(chunks).toString(); - try { - body = bodyStr ? JSON.parse(bodyStr) : null; - } catch (parseError) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32700, - message: "Parse error: Invalid JSON in request body", - }, - id: null, - }) - ); - return; - } - } - if (!body) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: Empty request body", - }, - id: null, - }) - ); - return; - } - if (!body.jsonrpc || body.jsonrpc !== "2.0") { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: jsonrpc must be '2.0'", - }, - id: body.id || null, - }) - ); - return; - } - if (this.sessionIdGenerator) { - if (body.method === "initialize") { - this.sessionId = this.sessionIdGenerator(); - } else { - const requestSessionId = req.headers["mcp-session-id"]; - if (!requestSessionId) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: Missing Mcp-Session-Id header", - }, - id: body.id || null, - }) - ); - return; - } - if (requestSessionId !== this.sessionId) { - res.writeHead(404, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32001, - message: "Session not found", - }, - id: body.id || null, - }) - ); - return; - } - } - } - const response = await this.server.handleRequest(body); - if (response === null) { - res.writeHead(204); - res.end(); - return; - } - const headers = { "Content-Type": "application/json" }; - if (this.sessionId) { - headers["mcp-session-id"] = this.sessionId; - } - res.writeHead(200, headers); - res.end(JSON.stringify(response)); - } catch (error) { - if (!res.headersSent) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), - }, - id: null, - }) - ); - } - } - } - } - module.exports = { - MCPServer, - MCPHTTPTransport, - }; - EOF_MCP_HTTP_TRANSPORT - cat > /tmp/gh-aw/safe-inputs/mcp_logger.cjs << 'EOF_MCP_LOGGER' - function createLogger(serverName) { - const logger = { - debug: msg => { - const timestamp = new Date().toISOString(); - process.stderr.write(`[${timestamp}] [${serverName}] ${msg}\n`); - }, - debugError: (prefix, error) => { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.debug(`${prefix}${errorMessage}`); - if (error instanceof Error && error.stack) { - logger.debug(`${prefix}Stack trace: ${error.stack}`); - } - }, - }; - return logger; - } - module.exports = { - createLogger, - }; - EOF_MCP_LOGGER - cat > /tmp/gh-aw/safe-inputs/mcp_handler_shell.cjs << 'EOF_HANDLER_SHELL' - const fs = require("fs"); - const path = require("path"); - const { execFile } = require("child_process"); - const os = require("os"); - function createShellHandler(server, toolName, scriptPath, timeoutSeconds = 60) { - return async args => { - server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); - server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); - server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); - 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: timeoutSeconds * 1000, - 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), - }, - ], - }); - } - ); - }); - }; - } - module.exports = { - createShellHandler, - }; - EOF_HANDLER_SHELL - cat > /tmp/gh-aw/safe-inputs/mcp_handler_python.cjs << 'EOF_HANDLER_PYTHON' - const { execFile } = require("child_process"); - function createPythonHandler(server, toolName, scriptPath, timeoutSeconds = 60) { - return async args => { - server.debug(` [${toolName}] Invoking Python handler: ${scriptPath}`); - server.debug(` [${toolName}] Python handler args: ${JSON.stringify(args)}`); - server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); - const inputJson = JSON.stringify(args || {}); - server.debug(` [${toolName}] Input JSON (${inputJson.length} bytes): ${inputJson.substring(0, 200)}${inputJson.length > 200 ? "..." : ""}`); - return new Promise((resolve, reject) => { - server.debug(` [${toolName}] Executing Python script...`); - const child = execFile( - "python3", - [scriptPath], - { - env: process.env, - timeout: timeoutSeconds * 1000, - 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}] Python script error: `, error); - reject(error); - return; - } - let result; - try { - if (stdout && stdout.trim()) { - result = JSON.parse(stdout.trim()); - } else { - result = { stdout: stdout || "", stderr: stderr || "" }; - } - } catch (parseError) { - server.debug(` [${toolName}] Output is not JSON, returning as text`); - result = { stdout: stdout || "", stderr: stderr || "" }; - } - server.debug(` [${toolName}] Python handler completed successfully`); - resolve({ - content: [ - { - type: "text", - text: JSON.stringify(result), - }, - ], - }); - } - ); - if (child.stdin) { - child.stdin.write(inputJson); - child.stdin.end(); - } - }); - }; - } - module.exports = { - createPythonHandler, - }; - EOF_HANDLER_PYTHON - cat > /tmp/gh-aw/safe-inputs/safe_inputs_config_loader.cjs << 'EOF_CONFIG_LOADER' - const fs = require("fs"); - function loadConfig(configPath) { - if (!fs.existsSync(configPath)) { - throw new Error(`Configuration file not found: ${configPath}`); - } - const configContent = fs.readFileSync(configPath, "utf-8"); - const config = JSON.parse(configContent); - if (!config.tools || !Array.isArray(config.tools)) { - throw new Error("Configuration must contain a 'tools' array"); - } - return config; - } - module.exports = { - loadConfig, - }; - EOF_CONFIG_LOADER - cat > /tmp/gh-aw/safe-inputs/safe_inputs_tool_factory.cjs << 'EOF_TOOL_FACTORY' - function createToolConfig(name, description, inputSchema, handlerPath) { - return { - name, - description, - inputSchema, - handler: handlerPath, - }; - } - module.exports = { - createToolConfig, - }; - EOF_TOOL_FACTORY - cat > /tmp/gh-aw/safe-inputs/safe_inputs_validation.cjs << 'EOF_VALIDATION' - function validateRequiredFields(args, inputSchema) { - const requiredFields = inputSchema && Array.isArray(inputSchema.required) ? inputSchema.required : []; - if (!requiredFields.length) { - return []; - } - const missing = requiredFields.filter(f => { - const value = args[f]; - return value === undefined || value === null || (typeof value === "string" && value.trim() === ""); - }); - return missing; - } - module.exports = { - validateRequiredFields, - }; - EOF_VALIDATION - cat > /tmp/gh-aw/safe-inputs/safe_inputs_bootstrap.cjs << 'EOF_BOOTSTRAP' - const path = require("path"); - const fs = require("fs"); - const { loadConfig } = require("./safe_inputs_config_loader.cjs"); - const { loadToolHandlers } = require("./mcp_server_core.cjs"); - function bootstrapSafeInputsServer(configPath, logger) { - logger.debug(`Loading safe-inputs configuration from: ${configPath}`); - const config = loadConfig(configPath); - const basePath = path.dirname(configPath); - logger.debug(`Base path for handlers: ${basePath}`); - logger.debug(`Tools to load: ${config.tools.length}`); - const tools = loadToolHandlers(logger, config.tools, basePath); - return { config, basePath, tools }; - } - function cleanupConfigFile(configPath, logger) { - try { - if (fs.existsSync(configPath)) { - fs.unlinkSync(configPath); - logger.debug(`Deleted configuration file: ${configPath}`); - } - } catch (error) { - logger.debugError(`Warning: Could not delete configuration file: `, error); - } - } - module.exports = { - bootstrapSafeInputsServer, - cleanupConfigFile, - }; - EOF_BOOTSTRAP - cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server.cjs << 'EOF_SAFE_INPUTS_SERVER' - const { createServer, registerTool, start } = require("./mcp_server_core.cjs"); - const { loadConfig } = require("./safe_inputs_config_loader.cjs"); - const { createToolConfig } = require("./safe_inputs_tool_factory.cjs"); - const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); - function startSafeInputsServer(configPath, options = {}) { - const logDir = options.logDir || undefined; - const server = createServer({ name: "safeinputs", version: "1.0.0" }, { logDir }); - const { config, tools } = bootstrapSafeInputsServer(configPath, server); - server.serverInfo.name = config.serverName || "safeinputs"; - server.serverInfo.version = config.version || "1.0.0"; - if (!options.logDir && config.logDir) { - server.logDir = config.logDir; - } - for (const tool of tools) { - registerTool(server, tool); - } - if (!options.skipCleanup) { - cleanupConfigFile(configPath, server); - } - start(server); - } - if (require.main === module) { - const args = process.argv.slice(2); - if (args.length < 1) { - console.error("Usage: node safe_inputs_mcp_server.cjs [--log-dir ]"); - process.exit(1); - } - const configPath = args[0]; - const options = {}; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--log-dir" && args[i + 1]) { - options.logDir = args[i + 1]; - i++; - } - } - try { - startSafeInputsServer(configPath, options); - } catch (error) { - console.error(`Error starting safe-inputs server: ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - } - } - module.exports = { - startSafeInputsServer, - loadConfig, - createToolConfig, - }; - EOF_SAFE_INPUTS_SERVER - cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server_http.cjs << 'EOF_SAFE_INPUTS_SERVER_HTTP' - const http = require("http"); - const { randomUUID } = require("crypto"); - const { MCPServer, MCPHTTPTransport } = require("./mcp_http_transport.cjs"); - const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); - const { createLogger } = require("./mcp_logger.cjs"); - const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); - function createMCPServer(configPath, options = {}) { - const logger = createLogger("safeinputs"); - logger.debug(`=== Creating MCP Server ===`); - logger.debug(`Configuration file: ${configPath}`); - const { config, tools } = bootstrapSafeInputsServer(configPath, logger); - const serverName = config.serverName || "safeinputs"; - const version = config.version || "1.0.0"; - logger.debug(`Server name: ${serverName}`); - logger.debug(`Server version: ${version}`); - const server = new MCPServer( - { - name: serverName, - version: version, - }, - { - capabilities: { - tools: {}, - }, - } - ); - logger.debug(`Registering tools with MCP server...`); - let registeredCount = 0; - let skippedCount = 0; - for (const tool of tools) { - if (!tool.handler) { - logger.debug(`Skipping tool ${tool.name} - no handler loaded`); - skippedCount++; - continue; - } - logger.debug(`Registering tool: ${tool.name}`); - server.tool(tool.name, tool.description || "", tool.inputSchema || { type: "object", properties: {} }, async args => { - logger.debug(`Calling handler for tool: ${tool.name}`); - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - throw new Error(`Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); - } - const result = await Promise.resolve(tool.handler(args)); - logger.debug(`Handler returned for tool: ${tool.name}`); - const content = result && result.content ? result.content : []; - return { content, isError: false }; - }); - registeredCount++; - } - logger.debug(`Tool registration complete: ${registeredCount} registered, ${skippedCount} skipped`); - logger.debug(`=== MCP Server Creation Complete ===`); - cleanupConfigFile(configPath, logger); - return { server, config, logger }; - } - async function startHttpServer(configPath, options = {}) { - const port = options.port || 3000; - const stateless = options.stateless || false; - const logger = createLogger("safe-inputs-startup"); - logger.debug(`=== Starting Safe Inputs MCP HTTP Server ===`); - logger.debug(`Configuration file: ${configPath}`); - logger.debug(`Port: ${port}`); - logger.debug(`Mode: ${stateless ? "stateless" : "stateful"}`); - logger.debug(`Environment: NODE_VERSION=${process.version}, PLATFORM=${process.platform}`); - try { - const { server, config, logger: mcpLogger } = createMCPServer(configPath, { logDir: options.logDir }); - Object.assign(logger, mcpLogger); - logger.debug(`MCP server created successfully`); - logger.debug(`Server name: ${config.serverName || "safeinputs"}`); - logger.debug(`Server version: ${config.version || "1.0.0"}`); - logger.debug(`Tools configured: ${config.tools.length}`); - logger.debug(`Creating HTTP transport...`); - const transport = new MCPHTTPTransport({ - sessionIdGenerator: stateless ? undefined : () => randomUUID(), - enableJsonResponse: true, - enableDnsRebindingProtection: false, - }); - logger.debug(`HTTP transport created`); - logger.debug(`Connecting server to transport...`); - await server.connect(transport); - logger.debug(`Server connected to transport successfully`); - logger.debug(`Creating HTTP server...`); - const httpServer = http.createServer(async (req, res) => { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept"); - if (req.method === "OPTIONS") { - res.writeHead(200); - res.end(); - return; - } - if (req.method === "GET" && req.url === "/health") { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - status: "ok", - server: config.serverName || "safeinputs", - version: config.version || "1.0.0", - tools: config.tools.length, - }) - ); - return; - } - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Method not allowed" })); - return; - } - try { - let body = null; - if (req.method === "POST") { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const bodyStr = Buffer.concat(chunks).toString(); - try { - body = bodyStr ? JSON.parse(bodyStr) : null; - } catch (parseError) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32700, - message: "Parse error: Invalid JSON in request body", - }, - id: null, - }) - ); - return; - } - } - await transport.handleRequest(req, res, body); - } catch (error) { - logger.debugError("Error handling request: ", error); - if (!res.headersSent) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), - }, - id: null, - }) - ); - } - } - }); - logger.debug(`Attempting to bind to port ${port}...`); - httpServer.listen(port, () => { - logger.debug(`=== Safe Inputs MCP HTTP Server Started Successfully ===`); - logger.debug(`HTTP server listening on http://localhost:${port}`); - logger.debug(`MCP endpoint: POST http://localhost:${port}/`); - logger.debug(`Server name: ${config.serverName || "safeinputs"}`); - logger.debug(`Server version: ${config.version || "1.0.0"}`); - logger.debug(`Tools available: ${config.tools.length}`); - logger.debug(`Server is ready to accept requests`); - }); - httpServer.on("error", error => { - if (error.code === "EADDRINUSE") { - logger.debugError(`ERROR: Port ${port} is already in use. `, error); - } else if (error.code === "EACCES") { - logger.debugError(`ERROR: Permission denied to bind to port ${port}. `, error); - } else { - logger.debugError(`ERROR: Failed to start HTTP server: `, error); - } - process.exit(1); - }); - process.on("SIGINT", () => { - logger.debug("Received SIGINT, shutting down..."); - httpServer.close(() => { - logger.debug("HTTP server closed"); - process.exit(0); - }); - }); - process.on("SIGTERM", () => { - logger.debug("Received SIGTERM, shutting down..."); - httpServer.close(() => { - logger.debug("HTTP server closed"); - process.exit(0); - }); - }); - return httpServer; - } catch (error) { - const errorLogger = createLogger("safe-inputs-startup-error"); - errorLogger.debug(`=== FATAL ERROR: Failed to start Safe Inputs MCP HTTP Server ===`); - errorLogger.debug(`Error type: ${error.constructor.name}`); - errorLogger.debug(`Error message: ${error.message}`); - if (error.stack) { - errorLogger.debug(`Stack trace:\n${error.stack}`); - } - if (error.code) { - errorLogger.debug(`Error code: ${error.code}`); - } - errorLogger.debug(`Configuration file: ${configPath}`); - errorLogger.debug(`Port: ${port}`); - throw error; - } - } - if (require.main === module) { - const args = process.argv.slice(2); - if (args.length < 1) { - console.error("Usage: node safe_inputs_mcp_server_http.cjs [--port ] [--stateless] [--log-dir ]"); - process.exit(1); - } - const configPath = args[0]; - const options = { - port: 3000, - stateless: false, - logDir: undefined, - }; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--port" && args[i + 1]) { - options.port = parseInt(args[i + 1], 10); - i++; - } else if (args[i] === "--stateless") { - options.stateless = true; - } else if (args[i] === "--log-dir" && args[i + 1]) { - options.logDir = args[i + 1]; - i++; - } - } - startHttpServer(configPath, options).catch(error => { - console.error(`Error starting HTTP server: ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - }); - } - module.exports = { - startHttpServer, - createMCPServer, - }; - EOF_SAFE_INPUTS_SERVER_HTTP + - name: Checkout actions folder for safe-inputs + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + sparse-checkout: | + actions + - name: Setup Safe Inputs JavaScript Files + uses: ./actions/setup-safe-inputs + with: + destination: /tmp/gh-aw/safe-inputs + - name: Setup Safe Inputs Config Files + run: | cat > /tmp/gh-aw/safe-inputs/tools.json << 'EOF_TOOLS_JSON' { "serverName": "safeinputs", diff --git a/.github/workflows/daily-performance-summary.lock.yml b/.github/workflows/daily-performance-summary.lock.yml index 30c60777385..798a3ba33e5 100644 --- a/.github/workflows/daily-performance-summary.lock.yml +++ b/.github/workflows/daily-performance-summary.lock.yml @@ -498,1234 +498,20 @@ jobs: EOF chmod +x /tmp/gh-aw/safeoutputs/mcp-server.cjs - - name: Setup Safe Inputs JavaScript and Config + - name: Setup Safe Inputs Directory run: | mkdir -p /tmp/gh-aw/safe-inputs/logs - cat > /tmp/gh-aw/safe-inputs/read_buffer.cjs << 'EOF_READ_BUFFER' - class ReadBuffer { - constructor() { - this._buffer = null; - } - append(chunk) { - this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; - } - readMessage() { - if (!this._buffer) { - return null; - } - const index = this._buffer.indexOf("\n"); - if (index === -1) { - return null; - } - const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, ""); - this._buffer = this._buffer.subarray(index + 1); - if (line.trim() === "") { - return this.readMessage(); - } - try { - return JSON.parse(line); - } catch (error) { - throw new Error(`Parse error: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - module.exports = { - ReadBuffer, - }; - EOF_READ_BUFFER - cat > /tmp/gh-aw/safe-inputs/mcp_server_core.cjs << 'EOF_MCP_CORE' - const fs = require("fs"); - const path = require("path"); - const { ReadBuffer } = require("./read_buffer.cjs"); - const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); - const encoder = new TextEncoder(); - function initLogFile(server) { - if (server.logFileInitialized || !server.logDir || !server.logFilePath) return; - try { - if (!fs.existsSync(server.logDir)) { - fs.mkdirSync(server.logDir, { recursive: true }); - } - const timestamp = new Date().toISOString(); - fs.writeFileSync(server.logFilePath, `# ${server.serverInfo.name} MCP Server Log\n# Started: ${timestamp}\n# Version: ${server.serverInfo.version}\n\n`); - server.logFileInitialized = true; - } catch { - } - } - function createDebugFunction(server) { - return msg => { - const timestamp = new Date().toISOString(); - const formattedMsg = `[${timestamp}] [${server.serverInfo.name}] ${msg}\n`; - process.stderr.write(formattedMsg); - if (server.logDir && server.logFilePath) { - if (!server.logFileInitialized) { - initLogFile(server); - } - if (server.logFileInitialized) { - try { - fs.appendFileSync(server.logFilePath, formattedMsg); - } catch { - } - } - } - }; - } - 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); - server.debug(`send: ${json}`); - const message = json + "\n"; - const bytes = encoder.encode(message); - fs.writeSync(1, bytes); - }; - } - function createReplyResultFunction(server) { - return (id, result) => { - if (id === undefined || id === null) return; - const res = { jsonrpc: "2.0", id, result }; - server.writeMessage(res); - }; - } - function createReplyErrorFunction(server) { - return (id, code, message) => { - if (id === undefined || id === null) { - server.debug(`Error for notification: ${message}`); - return; - } - const error = { code, message }; - const res = { - jsonrpc: "2.0", - id, - error, - }; - server.writeMessage(res); - }; - } - function createServer(serverInfo, options = {}) { - const logDir = options.logDir || undefined; - const logFilePath = logDir ? path.join(logDir, "server.log") : undefined; - const server = { - serverInfo, - tools: {}, - debug: () => {}, - debugError: () => {}, - writeMessage: () => {}, - replyResult: () => {}, - replyError: () => {}, - readBuffer: new ReadBuffer(), - logDir, - logFilePath, - 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 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); - } - } - const { createShellHandler } = require("./mcp_handler_shell.cjs"); - const timeout = tool.timeout || 60; - tool.handler = createShellHandler(server, toolName, resolvedPath, timeout); - loadedCount++; - server.debug(` [${toolName}] Shell handler created successfully with timeout: ${timeout}s`); - } else if (ext === ".py") { - server.debug(` [${toolName}] Detected Python script handler`); - try { - fs.accessSync(resolvedPath, fs.constants.X_OK); - server.debug(` [${toolName}] Python script is executable`); - } catch { - try { - fs.chmodSync(resolvedPath, 0o755); - server.debug(` [${toolName}] Made Python script executable`); - } catch (chmodError) { - server.debugError(` [${toolName}] Warning: Could not make Python script executable: `, chmodError); - } - } - const { createPythonHandler } = require("./mcp_handler_python.cjs"); - const timeout = tool.timeout || 60; - tool.handler = createPythonHandler(server, toolName, resolvedPath, timeout); - loadedCount++; - server.debug(` [${toolName}] Python handler created successfully with timeout: ${timeout}s`); - } 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] = { - ...tool, - name: normalizedName, - }; - server.debug(`Registered tool: ${normalizedName}`); - } - function normalizeTool(name) { - return name.replace(/-/g, "_").toLowerCase(); - } - async function handleRequest(server, request, defaultHandler) { - const { id, method, params } = request; - try { - if (!("id" in request)) { - return null; - } - let result; - if (method === "initialize") { - const protocolVersion = params?.protocolVersion || "2024-11-05"; - result = { - protocolVersion, - serverInfo: server.serverInfo, - capabilities: { - tools: {}, - }, - }; - } else if (method === "ping") { - result = {}; - } else if (method === "tools/list") { - const list = []; - Object.values(server.tools).forEach(tool => { - const toolDef = { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - list.push(toolDef); - }); - result = { tools: list }; - } else if (method === "tools/call") { - const name = params?.name; - const args = params?.arguments ?? {}; - if (!name || typeof name !== "string") { - throw { - code: -32602, - message: "Invalid params: 'name' must be a string", - }; - } - const tool = server.tools[normalizeTool(name)]; - if (!tool) { - throw { - code: -32602, - message: `Tool '${name}' not found`, - }; - } - let handler = tool.handler; - if (!handler && defaultHandler) { - handler = defaultHandler(tool.name); - } - if (!handler) { - throw { - code: -32603, - message: `No handler for tool: ${name}`, - }; - } - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - throw { - code: -32602, - message: `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`, - }; - } - const handlerResult = await Promise.resolve(handler(args)); - const content = handlerResult && handlerResult.content ? handlerResult.content : []; - result = { content, isError: false }; - } else if (/^notifications\//.test(method)) { - return null; - } else { - throw { - code: -32601, - message: `Method not found: ${method}`, - }; - } - return { - jsonrpc: "2.0", - id, - result, - }; - } catch (error) { - const err = error; - return { - jsonrpc: "2.0", - id, - error: { - code: err.code || -32603, - message: err.message || "Internal error", - }, - }; - } - } - async function handleMessage(server, req, defaultHandler) { - if (!req || typeof req !== "object") { - server.debug(`Invalid message: not an object`); - return; - } - if (req.jsonrpc !== "2.0") { - server.debug(`Invalid message: missing or invalid jsonrpc field`); - return; - } - const { id, method, params } = req; - if (!method || typeof method !== "string") { - server.replyError(id, -32600, "Invalid Request: method must be a string"); - return; - } - try { - if (method === "initialize") { - const clientInfo = params?.clientInfo ?? {}; - server.debug(`client info: ${JSON.stringify(clientInfo)}`); - const protocolVersion = params?.protocolVersion ?? undefined; - const result = { - serverInfo: server.serverInfo, - ...(protocolVersion ? { protocolVersion } : {}), - capabilities: { - tools: {}, - }, - }; - server.replyResult(id, result); - } else if (method === "tools/list") { - const list = []; - Object.values(server.tools).forEach(tool => { - const toolDef = { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - list.push(toolDef); - }); - server.replyResult(id, { tools: list }); - } else if (method === "tools/call") { - const name = params?.name; - const args = params?.arguments ?? {}; - if (!name || typeof name !== "string") { - server.replyError(id, -32602, "Invalid params: 'name' must be a string"); - return; - } - const tool = server.tools[normalizeTool(name)]; - if (!tool) { - server.replyError(id, -32601, `Tool not found: ${name} (${normalizeTool(name)})`); - return; - } - let handler = tool.handler; - if (!handler && defaultHandler) { - handler = defaultHandler(tool.name); - } - if (!handler) { - server.replyError(id, -32603, `No handler for tool: ${name}`); - return; - } - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - server.replyError(id, -32602, `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); - return; - } - 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)) { - server.debug(`ignore ${method}`); - } else { - server.replyError(id, -32601, `Method not found: ${method}`); - } - } catch (e) { - server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); - } - } - async function processReadBuffer(server, defaultHandler) { - while (true) { - try { - const message = server.readBuffer.readMessage(); - if (!message) { - break; - } - server.debug(`recv: ${JSON.stringify(message)}`); - await handleMessage(server, message, defaultHandler); - } catch (error) { - server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - function start(server, options = {}) { - const { defaultHandler } = options; - server.debug(`v${server.serverInfo.version} ready on stdio`); - server.debug(` tools: ${Object.keys(server.tools).join(", ")}`); - if (!Object.keys(server.tools).length) { - throw new Error("No tools registered"); - } - const onData = async chunk => { - server.readBuffer.append(chunk); - await processReadBuffer(server, defaultHandler); - }; - process.stdin.on("data", onData); - process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); - process.stdin.resume(); - server.debug(`listening...`); - } - module.exports = { - createServer, - registerTool, - normalizeTool, - handleRequest, - handleMessage, - processReadBuffer, - start, - loadToolHandlers, - }; - EOF_MCP_CORE - cat > /tmp/gh-aw/safe-inputs/mcp_http_transport.cjs << 'EOF_MCP_HTTP_TRANSPORT' - const http = require("http"); - const { randomUUID } = require("crypto"); - const { createServer, registerTool, handleRequest } = require("./mcp_server_core.cjs"); - class MCPServer { - constructor(serverInfo, options = {}) { - this._coreServer = createServer(serverInfo, options); - this.serverInfo = serverInfo; - this.capabilities = options.capabilities || { tools: {} }; - this.tools = new Map(); - this.transport = null; - this.initialized = false; - } - tool(name, description, inputSchema, handler) { - this.tools.set(name, { - name, - description, - inputSchema, - handler, - }); - registerTool(this._coreServer, { - name, - description, - inputSchema, - handler, - }); - } - async connect(transport) { - this.transport = transport; - transport.setServer(this); - await transport.start(); - } - async handleRequest(request) { - if (request.method === "initialize") { - this.initialized = true; - } - return handleRequest(this._coreServer, request); - } - } - class MCPHTTPTransport { - constructor(options = {}) { - this.sessionIdGenerator = options.sessionIdGenerator; - this.enableJsonResponse = options.enableJsonResponse !== false; - this.enableDnsRebindingProtection = options.enableDnsRebindingProtection || false; - this.server = null; - this.sessionId = null; - this.started = false; - } - setServer(server) { - this.server = server; - } - async start() { - if (this.started) { - throw new Error("Transport already started"); - } - this.started = true; - } - async handleRequest(req, res, parsedBody) { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id"); - if (req.method === "OPTIONS") { - res.writeHead(200); - res.end(); - return; - } - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Method not allowed" })); - return; - } - try { - let body = parsedBody; - if (!body) { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const bodyStr = Buffer.concat(chunks).toString(); - try { - body = bodyStr ? JSON.parse(bodyStr) : null; - } catch (parseError) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32700, - message: "Parse error: Invalid JSON in request body", - }, - id: null, - }) - ); - return; - } - } - if (!body) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: Empty request body", - }, - id: null, - }) - ); - return; - } - if (!body.jsonrpc || body.jsonrpc !== "2.0") { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: jsonrpc must be '2.0'", - }, - id: body.id || null, - }) - ); - return; - } - if (this.sessionIdGenerator) { - if (body.method === "initialize") { - this.sessionId = this.sessionIdGenerator(); - } else { - const requestSessionId = req.headers["mcp-session-id"]; - if (!requestSessionId) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: Missing Mcp-Session-Id header", - }, - id: body.id || null, - }) - ); - return; - } - if (requestSessionId !== this.sessionId) { - res.writeHead(404, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32001, - message: "Session not found", - }, - id: body.id || null, - }) - ); - return; - } - } - } - const response = await this.server.handleRequest(body); - if (response === null) { - res.writeHead(204); - res.end(); - return; - } - const headers = { "Content-Type": "application/json" }; - if (this.sessionId) { - headers["mcp-session-id"] = this.sessionId; - } - res.writeHead(200, headers); - res.end(JSON.stringify(response)); - } catch (error) { - if (!res.headersSent) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), - }, - id: null, - }) - ); - } - } - } - } - module.exports = { - MCPServer, - MCPHTTPTransport, - }; - EOF_MCP_HTTP_TRANSPORT - cat > /tmp/gh-aw/safe-inputs/mcp_logger.cjs << 'EOF_MCP_LOGGER' - function createLogger(serverName) { - const logger = { - debug: msg => { - const timestamp = new Date().toISOString(); - process.stderr.write(`[${timestamp}] [${serverName}] ${msg}\n`); - }, - debugError: (prefix, error) => { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.debug(`${prefix}${errorMessage}`); - if (error instanceof Error && error.stack) { - logger.debug(`${prefix}Stack trace: ${error.stack}`); - } - }, - }; - return logger; - } - module.exports = { - createLogger, - }; - EOF_MCP_LOGGER - cat > /tmp/gh-aw/safe-inputs/mcp_handler_shell.cjs << 'EOF_HANDLER_SHELL' - const fs = require("fs"); - const path = require("path"); - const { execFile } = require("child_process"); - const os = require("os"); - function createShellHandler(server, toolName, scriptPath, timeoutSeconds = 60) { - return async args => { - server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); - server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); - server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); - 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: timeoutSeconds * 1000, - 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), - }, - ], - }); - } - ); - }); - }; - } - module.exports = { - createShellHandler, - }; - EOF_HANDLER_SHELL - cat > /tmp/gh-aw/safe-inputs/mcp_handler_python.cjs << 'EOF_HANDLER_PYTHON' - const { execFile } = require("child_process"); - function createPythonHandler(server, toolName, scriptPath, timeoutSeconds = 60) { - return async args => { - server.debug(` [${toolName}] Invoking Python handler: ${scriptPath}`); - server.debug(` [${toolName}] Python handler args: ${JSON.stringify(args)}`); - server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); - const inputJson = JSON.stringify(args || {}); - server.debug(` [${toolName}] Input JSON (${inputJson.length} bytes): ${inputJson.substring(0, 200)}${inputJson.length > 200 ? "..." : ""}`); - return new Promise((resolve, reject) => { - server.debug(` [${toolName}] Executing Python script...`); - const child = execFile( - "python3", - [scriptPath], - { - env: process.env, - timeout: timeoutSeconds * 1000, - 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}] Python script error: `, error); - reject(error); - return; - } - let result; - try { - if (stdout && stdout.trim()) { - result = JSON.parse(stdout.trim()); - } else { - result = { stdout: stdout || "", stderr: stderr || "" }; - } - } catch (parseError) { - server.debug(` [${toolName}] Output is not JSON, returning as text`); - result = { stdout: stdout || "", stderr: stderr || "" }; - } - server.debug(` [${toolName}] Python handler completed successfully`); - resolve({ - content: [ - { - type: "text", - text: JSON.stringify(result), - }, - ], - }); - } - ); - if (child.stdin) { - child.stdin.write(inputJson); - child.stdin.end(); - } - }); - }; - } - module.exports = { - createPythonHandler, - }; - EOF_HANDLER_PYTHON - cat > /tmp/gh-aw/safe-inputs/safe_inputs_config_loader.cjs << 'EOF_CONFIG_LOADER' - const fs = require("fs"); - function loadConfig(configPath) { - if (!fs.existsSync(configPath)) { - throw new Error(`Configuration file not found: ${configPath}`); - } - const configContent = fs.readFileSync(configPath, "utf-8"); - const config = JSON.parse(configContent); - if (!config.tools || !Array.isArray(config.tools)) { - throw new Error("Configuration must contain a 'tools' array"); - } - return config; - } - module.exports = { - loadConfig, - }; - EOF_CONFIG_LOADER - cat > /tmp/gh-aw/safe-inputs/safe_inputs_tool_factory.cjs << 'EOF_TOOL_FACTORY' - function createToolConfig(name, description, inputSchema, handlerPath) { - return { - name, - description, - inputSchema, - handler: handlerPath, - }; - } - module.exports = { - createToolConfig, - }; - EOF_TOOL_FACTORY - cat > /tmp/gh-aw/safe-inputs/safe_inputs_validation.cjs << 'EOF_VALIDATION' - function validateRequiredFields(args, inputSchema) { - const requiredFields = inputSchema && Array.isArray(inputSchema.required) ? inputSchema.required : []; - if (!requiredFields.length) { - return []; - } - const missing = requiredFields.filter(f => { - const value = args[f]; - return value === undefined || value === null || (typeof value === "string" && value.trim() === ""); - }); - return missing; - } - module.exports = { - validateRequiredFields, - }; - EOF_VALIDATION - cat > /tmp/gh-aw/safe-inputs/safe_inputs_bootstrap.cjs << 'EOF_BOOTSTRAP' - const path = require("path"); - const fs = require("fs"); - const { loadConfig } = require("./safe_inputs_config_loader.cjs"); - const { loadToolHandlers } = require("./mcp_server_core.cjs"); - function bootstrapSafeInputsServer(configPath, logger) { - logger.debug(`Loading safe-inputs configuration from: ${configPath}`); - const config = loadConfig(configPath); - const basePath = path.dirname(configPath); - logger.debug(`Base path for handlers: ${basePath}`); - logger.debug(`Tools to load: ${config.tools.length}`); - const tools = loadToolHandlers(logger, config.tools, basePath); - return { config, basePath, tools }; - } - function cleanupConfigFile(configPath, logger) { - try { - if (fs.existsSync(configPath)) { - fs.unlinkSync(configPath); - logger.debug(`Deleted configuration file: ${configPath}`); - } - } catch (error) { - logger.debugError(`Warning: Could not delete configuration file: `, error); - } - } - module.exports = { - bootstrapSafeInputsServer, - cleanupConfigFile, - }; - EOF_BOOTSTRAP - cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server.cjs << 'EOF_SAFE_INPUTS_SERVER' - const { createServer, registerTool, start } = require("./mcp_server_core.cjs"); - const { loadConfig } = require("./safe_inputs_config_loader.cjs"); - const { createToolConfig } = require("./safe_inputs_tool_factory.cjs"); - const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); - function startSafeInputsServer(configPath, options = {}) { - const logDir = options.logDir || undefined; - const server = createServer({ name: "safeinputs", version: "1.0.0" }, { logDir }); - const { config, tools } = bootstrapSafeInputsServer(configPath, server); - server.serverInfo.name = config.serverName || "safeinputs"; - server.serverInfo.version = config.version || "1.0.0"; - if (!options.logDir && config.logDir) { - server.logDir = config.logDir; - } - for (const tool of tools) { - registerTool(server, tool); - } - if (!options.skipCleanup) { - cleanupConfigFile(configPath, server); - } - start(server); - } - if (require.main === module) { - const args = process.argv.slice(2); - if (args.length < 1) { - console.error("Usage: node safe_inputs_mcp_server.cjs [--log-dir ]"); - process.exit(1); - } - const configPath = args[0]; - const options = {}; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--log-dir" && args[i + 1]) { - options.logDir = args[i + 1]; - i++; - } - } - try { - startSafeInputsServer(configPath, options); - } catch (error) { - console.error(`Error starting safe-inputs server: ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - } - } - module.exports = { - startSafeInputsServer, - loadConfig, - createToolConfig, - }; - EOF_SAFE_INPUTS_SERVER - cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server_http.cjs << 'EOF_SAFE_INPUTS_SERVER_HTTP' - const http = require("http"); - const { randomUUID } = require("crypto"); - const { MCPServer, MCPHTTPTransport } = require("./mcp_http_transport.cjs"); - const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); - const { createLogger } = require("./mcp_logger.cjs"); - const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); - function createMCPServer(configPath, options = {}) { - const logger = createLogger("safeinputs"); - logger.debug(`=== Creating MCP Server ===`); - logger.debug(`Configuration file: ${configPath}`); - const { config, tools } = bootstrapSafeInputsServer(configPath, logger); - const serverName = config.serverName || "safeinputs"; - const version = config.version || "1.0.0"; - logger.debug(`Server name: ${serverName}`); - logger.debug(`Server version: ${version}`); - const server = new MCPServer( - { - name: serverName, - version: version, - }, - { - capabilities: { - tools: {}, - }, - } - ); - logger.debug(`Registering tools with MCP server...`); - let registeredCount = 0; - let skippedCount = 0; - for (const tool of tools) { - if (!tool.handler) { - logger.debug(`Skipping tool ${tool.name} - no handler loaded`); - skippedCount++; - continue; - } - logger.debug(`Registering tool: ${tool.name}`); - server.tool(tool.name, tool.description || "", tool.inputSchema || { type: "object", properties: {} }, async args => { - logger.debug(`Calling handler for tool: ${tool.name}`); - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - throw new Error(`Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); - } - const result = await Promise.resolve(tool.handler(args)); - logger.debug(`Handler returned for tool: ${tool.name}`); - const content = result && result.content ? result.content : []; - return { content, isError: false }; - }); - registeredCount++; - } - logger.debug(`Tool registration complete: ${registeredCount} registered, ${skippedCount} skipped`); - logger.debug(`=== MCP Server Creation Complete ===`); - cleanupConfigFile(configPath, logger); - return { server, config, logger }; - } - async function startHttpServer(configPath, options = {}) { - const port = options.port || 3000; - const stateless = options.stateless || false; - const logger = createLogger("safe-inputs-startup"); - logger.debug(`=== Starting Safe Inputs MCP HTTP Server ===`); - logger.debug(`Configuration file: ${configPath}`); - logger.debug(`Port: ${port}`); - logger.debug(`Mode: ${stateless ? "stateless" : "stateful"}`); - logger.debug(`Environment: NODE_VERSION=${process.version}, PLATFORM=${process.platform}`); - try { - const { server, config, logger: mcpLogger } = createMCPServer(configPath, { logDir: options.logDir }); - Object.assign(logger, mcpLogger); - logger.debug(`MCP server created successfully`); - logger.debug(`Server name: ${config.serverName || "safeinputs"}`); - logger.debug(`Server version: ${config.version || "1.0.0"}`); - logger.debug(`Tools configured: ${config.tools.length}`); - logger.debug(`Creating HTTP transport...`); - const transport = new MCPHTTPTransport({ - sessionIdGenerator: stateless ? undefined : () => randomUUID(), - enableJsonResponse: true, - enableDnsRebindingProtection: false, - }); - logger.debug(`HTTP transport created`); - logger.debug(`Connecting server to transport...`); - await server.connect(transport); - logger.debug(`Server connected to transport successfully`); - logger.debug(`Creating HTTP server...`); - const httpServer = http.createServer(async (req, res) => { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept"); - if (req.method === "OPTIONS") { - res.writeHead(200); - res.end(); - return; - } - if (req.method === "GET" && req.url === "/health") { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - status: "ok", - server: config.serverName || "safeinputs", - version: config.version || "1.0.0", - tools: config.tools.length, - }) - ); - return; - } - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Method not allowed" })); - return; - } - try { - let body = null; - if (req.method === "POST") { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const bodyStr = Buffer.concat(chunks).toString(); - try { - body = bodyStr ? JSON.parse(bodyStr) : null; - } catch (parseError) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32700, - message: "Parse error: Invalid JSON in request body", - }, - id: null, - }) - ); - return; - } - } - await transport.handleRequest(req, res, body); - } catch (error) { - logger.debugError("Error handling request: ", error); - if (!res.headersSent) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), - }, - id: null, - }) - ); - } - } - }); - logger.debug(`Attempting to bind to port ${port}...`); - httpServer.listen(port, () => { - logger.debug(`=== Safe Inputs MCP HTTP Server Started Successfully ===`); - logger.debug(`HTTP server listening on http://localhost:${port}`); - logger.debug(`MCP endpoint: POST http://localhost:${port}/`); - logger.debug(`Server name: ${config.serverName || "safeinputs"}`); - logger.debug(`Server version: ${config.version || "1.0.0"}`); - logger.debug(`Tools available: ${config.tools.length}`); - logger.debug(`Server is ready to accept requests`); - }); - httpServer.on("error", error => { - if (error.code === "EADDRINUSE") { - logger.debugError(`ERROR: Port ${port} is already in use. `, error); - } else if (error.code === "EACCES") { - logger.debugError(`ERROR: Permission denied to bind to port ${port}. `, error); - } else { - logger.debugError(`ERROR: Failed to start HTTP server: `, error); - } - process.exit(1); - }); - process.on("SIGINT", () => { - logger.debug("Received SIGINT, shutting down..."); - httpServer.close(() => { - logger.debug("HTTP server closed"); - process.exit(0); - }); - }); - process.on("SIGTERM", () => { - logger.debug("Received SIGTERM, shutting down..."); - httpServer.close(() => { - logger.debug("HTTP server closed"); - process.exit(0); - }); - }); - return httpServer; - } catch (error) { - const errorLogger = createLogger("safe-inputs-startup-error"); - errorLogger.debug(`=== FATAL ERROR: Failed to start Safe Inputs MCP HTTP Server ===`); - errorLogger.debug(`Error type: ${error.constructor.name}`); - errorLogger.debug(`Error message: ${error.message}`); - if (error.stack) { - errorLogger.debug(`Stack trace:\n${error.stack}`); - } - if (error.code) { - errorLogger.debug(`Error code: ${error.code}`); - } - errorLogger.debug(`Configuration file: ${configPath}`); - errorLogger.debug(`Port: ${port}`); - throw error; - } - } - if (require.main === module) { - const args = process.argv.slice(2); - if (args.length < 1) { - console.error("Usage: node safe_inputs_mcp_server_http.cjs [--port ] [--stateless] [--log-dir ]"); - process.exit(1); - } - const configPath = args[0]; - const options = { - port: 3000, - stateless: false, - logDir: undefined, - }; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--port" && args[i + 1]) { - options.port = parseInt(args[i + 1], 10); - i++; - } else if (args[i] === "--stateless") { - options.stateless = true; - } else if (args[i] === "--log-dir" && args[i + 1]) { - options.logDir = args[i + 1]; - i++; - } - } - startHttpServer(configPath, options).catch(error => { - console.error(`Error starting HTTP server: ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - }); - } - module.exports = { - startHttpServer, - createMCPServer, - }; - EOF_SAFE_INPUTS_SERVER_HTTP + - name: Checkout actions folder for safe-inputs + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + sparse-checkout: | + actions + - name: Setup Safe Inputs JavaScript Files + uses: ./actions/setup-safe-inputs + with: + destination: /tmp/gh-aw/safe-inputs + - name: Setup Safe Inputs Config Files + run: | cat > /tmp/gh-aw/safe-inputs/tools.json << 'EOF_TOOLS_JSON' { "serverName": "safeinputs", diff --git a/.github/workflows/smoke-copilot-no-firewall.lock.yml b/.github/workflows/smoke-copilot-no-firewall.lock.yml index 70ecaee504a..2c6b16059d4 100644 --- a/.github/workflows/smoke-copilot-no-firewall.lock.yml +++ b/.github/workflows/smoke-copilot-no-firewall.lock.yml @@ -435,1234 +435,20 @@ jobs: EOF chmod +x /tmp/gh-aw/safeoutputs/mcp-server.cjs - - name: Setup Safe Inputs JavaScript and Config + - name: Setup Safe Inputs Directory run: | mkdir -p /tmp/gh-aw/safe-inputs/logs - cat > /tmp/gh-aw/safe-inputs/read_buffer.cjs << 'EOF_READ_BUFFER' - class ReadBuffer { - constructor() { - this._buffer = null; - } - append(chunk) { - this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; - } - readMessage() { - if (!this._buffer) { - return null; - } - const index = this._buffer.indexOf("\n"); - if (index === -1) { - return null; - } - const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, ""); - this._buffer = this._buffer.subarray(index + 1); - if (line.trim() === "") { - return this.readMessage(); - } - try { - return JSON.parse(line); - } catch (error) { - throw new Error(`Parse error: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - module.exports = { - ReadBuffer, - }; - EOF_READ_BUFFER - cat > /tmp/gh-aw/safe-inputs/mcp_server_core.cjs << 'EOF_MCP_CORE' - const fs = require("fs"); - const path = require("path"); - const { ReadBuffer } = require("./read_buffer.cjs"); - const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); - const encoder = new TextEncoder(); - function initLogFile(server) { - if (server.logFileInitialized || !server.logDir || !server.logFilePath) return; - try { - if (!fs.existsSync(server.logDir)) { - fs.mkdirSync(server.logDir, { recursive: true }); - } - const timestamp = new Date().toISOString(); - fs.writeFileSync(server.logFilePath, `# ${server.serverInfo.name} MCP Server Log\n# Started: ${timestamp}\n# Version: ${server.serverInfo.version}\n\n`); - server.logFileInitialized = true; - } catch { - } - } - function createDebugFunction(server) { - return msg => { - const timestamp = new Date().toISOString(); - const formattedMsg = `[${timestamp}] [${server.serverInfo.name}] ${msg}\n`; - process.stderr.write(formattedMsg); - if (server.logDir && server.logFilePath) { - if (!server.logFileInitialized) { - initLogFile(server); - } - if (server.logFileInitialized) { - try { - fs.appendFileSync(server.logFilePath, formattedMsg); - } catch { - } - } - } - }; - } - 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); - server.debug(`send: ${json}`); - const message = json + "\n"; - const bytes = encoder.encode(message); - fs.writeSync(1, bytes); - }; - } - function createReplyResultFunction(server) { - return (id, result) => { - if (id === undefined || id === null) return; - const res = { jsonrpc: "2.0", id, result }; - server.writeMessage(res); - }; - } - function createReplyErrorFunction(server) { - return (id, code, message) => { - if (id === undefined || id === null) { - server.debug(`Error for notification: ${message}`); - return; - } - const error = { code, message }; - const res = { - jsonrpc: "2.0", - id, - error, - }; - server.writeMessage(res); - }; - } - function createServer(serverInfo, options = {}) { - const logDir = options.logDir || undefined; - const logFilePath = logDir ? path.join(logDir, "server.log") : undefined; - const server = { - serverInfo, - tools: {}, - debug: () => {}, - debugError: () => {}, - writeMessage: () => {}, - replyResult: () => {}, - replyError: () => {}, - readBuffer: new ReadBuffer(), - logDir, - logFilePath, - 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 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); - } - } - const { createShellHandler } = require("./mcp_handler_shell.cjs"); - const timeout = tool.timeout || 60; - tool.handler = createShellHandler(server, toolName, resolvedPath, timeout); - loadedCount++; - server.debug(` [${toolName}] Shell handler created successfully with timeout: ${timeout}s`); - } else if (ext === ".py") { - server.debug(` [${toolName}] Detected Python script handler`); - try { - fs.accessSync(resolvedPath, fs.constants.X_OK); - server.debug(` [${toolName}] Python script is executable`); - } catch { - try { - fs.chmodSync(resolvedPath, 0o755); - server.debug(` [${toolName}] Made Python script executable`); - } catch (chmodError) { - server.debugError(` [${toolName}] Warning: Could not make Python script executable: `, chmodError); - } - } - const { createPythonHandler } = require("./mcp_handler_python.cjs"); - const timeout = tool.timeout || 60; - tool.handler = createPythonHandler(server, toolName, resolvedPath, timeout); - loadedCount++; - server.debug(` [${toolName}] Python handler created successfully with timeout: ${timeout}s`); - } 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] = { - ...tool, - name: normalizedName, - }; - server.debug(`Registered tool: ${normalizedName}`); - } - function normalizeTool(name) { - return name.replace(/-/g, "_").toLowerCase(); - } - async function handleRequest(server, request, defaultHandler) { - const { id, method, params } = request; - try { - if (!("id" in request)) { - return null; - } - let result; - if (method === "initialize") { - const protocolVersion = params?.protocolVersion || "2024-11-05"; - result = { - protocolVersion, - serverInfo: server.serverInfo, - capabilities: { - tools: {}, - }, - }; - } else if (method === "ping") { - result = {}; - } else if (method === "tools/list") { - const list = []; - Object.values(server.tools).forEach(tool => { - const toolDef = { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - list.push(toolDef); - }); - result = { tools: list }; - } else if (method === "tools/call") { - const name = params?.name; - const args = params?.arguments ?? {}; - if (!name || typeof name !== "string") { - throw { - code: -32602, - message: "Invalid params: 'name' must be a string", - }; - } - const tool = server.tools[normalizeTool(name)]; - if (!tool) { - throw { - code: -32602, - message: `Tool '${name}' not found`, - }; - } - let handler = tool.handler; - if (!handler && defaultHandler) { - handler = defaultHandler(tool.name); - } - if (!handler) { - throw { - code: -32603, - message: `No handler for tool: ${name}`, - }; - } - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - throw { - code: -32602, - message: `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`, - }; - } - const handlerResult = await Promise.resolve(handler(args)); - const content = handlerResult && handlerResult.content ? handlerResult.content : []; - result = { content, isError: false }; - } else if (/^notifications\//.test(method)) { - return null; - } else { - throw { - code: -32601, - message: `Method not found: ${method}`, - }; - } - return { - jsonrpc: "2.0", - id, - result, - }; - } catch (error) { - const err = error; - return { - jsonrpc: "2.0", - id, - error: { - code: err.code || -32603, - message: err.message || "Internal error", - }, - }; - } - } - async function handleMessage(server, req, defaultHandler) { - if (!req || typeof req !== "object") { - server.debug(`Invalid message: not an object`); - return; - } - if (req.jsonrpc !== "2.0") { - server.debug(`Invalid message: missing or invalid jsonrpc field`); - return; - } - const { id, method, params } = req; - if (!method || typeof method !== "string") { - server.replyError(id, -32600, "Invalid Request: method must be a string"); - return; - } - try { - if (method === "initialize") { - const clientInfo = params?.clientInfo ?? {}; - server.debug(`client info: ${JSON.stringify(clientInfo)}`); - const protocolVersion = params?.protocolVersion ?? undefined; - const result = { - serverInfo: server.serverInfo, - ...(protocolVersion ? { protocolVersion } : {}), - capabilities: { - tools: {}, - }, - }; - server.replyResult(id, result); - } else if (method === "tools/list") { - const list = []; - Object.values(server.tools).forEach(tool => { - const toolDef = { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - list.push(toolDef); - }); - server.replyResult(id, { tools: list }); - } else if (method === "tools/call") { - const name = params?.name; - const args = params?.arguments ?? {}; - if (!name || typeof name !== "string") { - server.replyError(id, -32602, "Invalid params: 'name' must be a string"); - return; - } - const tool = server.tools[normalizeTool(name)]; - if (!tool) { - server.replyError(id, -32601, `Tool not found: ${name} (${normalizeTool(name)})`); - return; - } - let handler = tool.handler; - if (!handler && defaultHandler) { - handler = defaultHandler(tool.name); - } - if (!handler) { - server.replyError(id, -32603, `No handler for tool: ${name}`); - return; - } - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - server.replyError(id, -32602, `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); - return; - } - 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)) { - server.debug(`ignore ${method}`); - } else { - server.replyError(id, -32601, `Method not found: ${method}`); - } - } catch (e) { - server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); - } - } - async function processReadBuffer(server, defaultHandler) { - while (true) { - try { - const message = server.readBuffer.readMessage(); - if (!message) { - break; - } - server.debug(`recv: ${JSON.stringify(message)}`); - await handleMessage(server, message, defaultHandler); - } catch (error) { - server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - function start(server, options = {}) { - const { defaultHandler } = options; - server.debug(`v${server.serverInfo.version} ready on stdio`); - server.debug(` tools: ${Object.keys(server.tools).join(", ")}`); - if (!Object.keys(server.tools).length) { - throw new Error("No tools registered"); - } - const onData = async chunk => { - server.readBuffer.append(chunk); - await processReadBuffer(server, defaultHandler); - }; - process.stdin.on("data", onData); - process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); - process.stdin.resume(); - server.debug(`listening...`); - } - module.exports = { - createServer, - registerTool, - normalizeTool, - handleRequest, - handleMessage, - processReadBuffer, - start, - loadToolHandlers, - }; - EOF_MCP_CORE - cat > /tmp/gh-aw/safe-inputs/mcp_http_transport.cjs << 'EOF_MCP_HTTP_TRANSPORT' - const http = require("http"); - const { randomUUID } = require("crypto"); - const { createServer, registerTool, handleRequest } = require("./mcp_server_core.cjs"); - class MCPServer { - constructor(serverInfo, options = {}) { - this._coreServer = createServer(serverInfo, options); - this.serverInfo = serverInfo; - this.capabilities = options.capabilities || { tools: {} }; - this.tools = new Map(); - this.transport = null; - this.initialized = false; - } - tool(name, description, inputSchema, handler) { - this.tools.set(name, { - name, - description, - inputSchema, - handler, - }); - registerTool(this._coreServer, { - name, - description, - inputSchema, - handler, - }); - } - async connect(transport) { - this.transport = transport; - transport.setServer(this); - await transport.start(); - } - async handleRequest(request) { - if (request.method === "initialize") { - this.initialized = true; - } - return handleRequest(this._coreServer, request); - } - } - class MCPHTTPTransport { - constructor(options = {}) { - this.sessionIdGenerator = options.sessionIdGenerator; - this.enableJsonResponse = options.enableJsonResponse !== false; - this.enableDnsRebindingProtection = options.enableDnsRebindingProtection || false; - this.server = null; - this.sessionId = null; - this.started = false; - } - setServer(server) { - this.server = server; - } - async start() { - if (this.started) { - throw new Error("Transport already started"); - } - this.started = true; - } - async handleRequest(req, res, parsedBody) { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id"); - if (req.method === "OPTIONS") { - res.writeHead(200); - res.end(); - return; - } - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Method not allowed" })); - return; - } - try { - let body = parsedBody; - if (!body) { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const bodyStr = Buffer.concat(chunks).toString(); - try { - body = bodyStr ? JSON.parse(bodyStr) : null; - } catch (parseError) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32700, - message: "Parse error: Invalid JSON in request body", - }, - id: null, - }) - ); - return; - } - } - if (!body) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: Empty request body", - }, - id: null, - }) - ); - return; - } - if (!body.jsonrpc || body.jsonrpc !== "2.0") { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: jsonrpc must be '2.0'", - }, - id: body.id || null, - }) - ); - return; - } - if (this.sessionIdGenerator) { - if (body.method === "initialize") { - this.sessionId = this.sessionIdGenerator(); - } else { - const requestSessionId = req.headers["mcp-session-id"]; - if (!requestSessionId) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: Missing Mcp-Session-Id header", - }, - id: body.id || null, - }) - ); - return; - } - if (requestSessionId !== this.sessionId) { - res.writeHead(404, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32001, - message: "Session not found", - }, - id: body.id || null, - }) - ); - return; - } - } - } - const response = await this.server.handleRequest(body); - if (response === null) { - res.writeHead(204); - res.end(); - return; - } - const headers = { "Content-Type": "application/json" }; - if (this.sessionId) { - headers["mcp-session-id"] = this.sessionId; - } - res.writeHead(200, headers); - res.end(JSON.stringify(response)); - } catch (error) { - if (!res.headersSent) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), - }, - id: null, - }) - ); - } - } - } - } - module.exports = { - MCPServer, - MCPHTTPTransport, - }; - EOF_MCP_HTTP_TRANSPORT - cat > /tmp/gh-aw/safe-inputs/mcp_logger.cjs << 'EOF_MCP_LOGGER' - function createLogger(serverName) { - const logger = { - debug: msg => { - const timestamp = new Date().toISOString(); - process.stderr.write(`[${timestamp}] [${serverName}] ${msg}\n`); - }, - debugError: (prefix, error) => { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.debug(`${prefix}${errorMessage}`); - if (error instanceof Error && error.stack) { - logger.debug(`${prefix}Stack trace: ${error.stack}`); - } - }, - }; - return logger; - } - module.exports = { - createLogger, - }; - EOF_MCP_LOGGER - cat > /tmp/gh-aw/safe-inputs/mcp_handler_shell.cjs << 'EOF_HANDLER_SHELL' - const fs = require("fs"); - const path = require("path"); - const { execFile } = require("child_process"); - const os = require("os"); - function createShellHandler(server, toolName, scriptPath, timeoutSeconds = 60) { - return async args => { - server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); - server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); - server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); - 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: timeoutSeconds * 1000, - 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), - }, - ], - }); - } - ); - }); - }; - } - module.exports = { - createShellHandler, - }; - EOF_HANDLER_SHELL - cat > /tmp/gh-aw/safe-inputs/mcp_handler_python.cjs << 'EOF_HANDLER_PYTHON' - const { execFile } = require("child_process"); - function createPythonHandler(server, toolName, scriptPath, timeoutSeconds = 60) { - return async args => { - server.debug(` [${toolName}] Invoking Python handler: ${scriptPath}`); - server.debug(` [${toolName}] Python handler args: ${JSON.stringify(args)}`); - server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); - const inputJson = JSON.stringify(args || {}); - server.debug(` [${toolName}] Input JSON (${inputJson.length} bytes): ${inputJson.substring(0, 200)}${inputJson.length > 200 ? "..." : ""}`); - return new Promise((resolve, reject) => { - server.debug(` [${toolName}] Executing Python script...`); - const child = execFile( - "python3", - [scriptPath], - { - env: process.env, - timeout: timeoutSeconds * 1000, - 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}] Python script error: `, error); - reject(error); - return; - } - let result; - try { - if (stdout && stdout.trim()) { - result = JSON.parse(stdout.trim()); - } else { - result = { stdout: stdout || "", stderr: stderr || "" }; - } - } catch (parseError) { - server.debug(` [${toolName}] Output is not JSON, returning as text`); - result = { stdout: stdout || "", stderr: stderr || "" }; - } - server.debug(` [${toolName}] Python handler completed successfully`); - resolve({ - content: [ - { - type: "text", - text: JSON.stringify(result), - }, - ], - }); - } - ); - if (child.stdin) { - child.stdin.write(inputJson); - child.stdin.end(); - } - }); - }; - } - module.exports = { - createPythonHandler, - }; - EOF_HANDLER_PYTHON - cat > /tmp/gh-aw/safe-inputs/safe_inputs_config_loader.cjs << 'EOF_CONFIG_LOADER' - const fs = require("fs"); - function loadConfig(configPath) { - if (!fs.existsSync(configPath)) { - throw new Error(`Configuration file not found: ${configPath}`); - } - const configContent = fs.readFileSync(configPath, "utf-8"); - const config = JSON.parse(configContent); - if (!config.tools || !Array.isArray(config.tools)) { - throw new Error("Configuration must contain a 'tools' array"); - } - return config; - } - module.exports = { - loadConfig, - }; - EOF_CONFIG_LOADER - cat > /tmp/gh-aw/safe-inputs/safe_inputs_tool_factory.cjs << 'EOF_TOOL_FACTORY' - function createToolConfig(name, description, inputSchema, handlerPath) { - return { - name, - description, - inputSchema, - handler: handlerPath, - }; - } - module.exports = { - createToolConfig, - }; - EOF_TOOL_FACTORY - cat > /tmp/gh-aw/safe-inputs/safe_inputs_validation.cjs << 'EOF_VALIDATION' - function validateRequiredFields(args, inputSchema) { - const requiredFields = inputSchema && Array.isArray(inputSchema.required) ? inputSchema.required : []; - if (!requiredFields.length) { - return []; - } - const missing = requiredFields.filter(f => { - const value = args[f]; - return value === undefined || value === null || (typeof value === "string" && value.trim() === ""); - }); - return missing; - } - module.exports = { - validateRequiredFields, - }; - EOF_VALIDATION - cat > /tmp/gh-aw/safe-inputs/safe_inputs_bootstrap.cjs << 'EOF_BOOTSTRAP' - const path = require("path"); - const fs = require("fs"); - const { loadConfig } = require("./safe_inputs_config_loader.cjs"); - const { loadToolHandlers } = require("./mcp_server_core.cjs"); - function bootstrapSafeInputsServer(configPath, logger) { - logger.debug(`Loading safe-inputs configuration from: ${configPath}`); - const config = loadConfig(configPath); - const basePath = path.dirname(configPath); - logger.debug(`Base path for handlers: ${basePath}`); - logger.debug(`Tools to load: ${config.tools.length}`); - const tools = loadToolHandlers(logger, config.tools, basePath); - return { config, basePath, tools }; - } - function cleanupConfigFile(configPath, logger) { - try { - if (fs.existsSync(configPath)) { - fs.unlinkSync(configPath); - logger.debug(`Deleted configuration file: ${configPath}`); - } - } catch (error) { - logger.debugError(`Warning: Could not delete configuration file: `, error); - } - } - module.exports = { - bootstrapSafeInputsServer, - cleanupConfigFile, - }; - EOF_BOOTSTRAP - cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server.cjs << 'EOF_SAFE_INPUTS_SERVER' - const { createServer, registerTool, start } = require("./mcp_server_core.cjs"); - const { loadConfig } = require("./safe_inputs_config_loader.cjs"); - const { createToolConfig } = require("./safe_inputs_tool_factory.cjs"); - const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); - function startSafeInputsServer(configPath, options = {}) { - const logDir = options.logDir || undefined; - const server = createServer({ name: "safeinputs", version: "1.0.0" }, { logDir }); - const { config, tools } = bootstrapSafeInputsServer(configPath, server); - server.serverInfo.name = config.serverName || "safeinputs"; - server.serverInfo.version = config.version || "1.0.0"; - if (!options.logDir && config.logDir) { - server.logDir = config.logDir; - } - for (const tool of tools) { - registerTool(server, tool); - } - if (!options.skipCleanup) { - cleanupConfigFile(configPath, server); - } - start(server); - } - if (require.main === module) { - const args = process.argv.slice(2); - if (args.length < 1) { - console.error("Usage: node safe_inputs_mcp_server.cjs [--log-dir ]"); - process.exit(1); - } - const configPath = args[0]; - const options = {}; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--log-dir" && args[i + 1]) { - options.logDir = args[i + 1]; - i++; - } - } - try { - startSafeInputsServer(configPath, options); - } catch (error) { - console.error(`Error starting safe-inputs server: ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - } - } - module.exports = { - startSafeInputsServer, - loadConfig, - createToolConfig, - }; - EOF_SAFE_INPUTS_SERVER - cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server_http.cjs << 'EOF_SAFE_INPUTS_SERVER_HTTP' - const http = require("http"); - const { randomUUID } = require("crypto"); - const { MCPServer, MCPHTTPTransport } = require("./mcp_http_transport.cjs"); - const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); - const { createLogger } = require("./mcp_logger.cjs"); - const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); - function createMCPServer(configPath, options = {}) { - const logger = createLogger("safeinputs"); - logger.debug(`=== Creating MCP Server ===`); - logger.debug(`Configuration file: ${configPath}`); - const { config, tools } = bootstrapSafeInputsServer(configPath, logger); - const serverName = config.serverName || "safeinputs"; - const version = config.version || "1.0.0"; - logger.debug(`Server name: ${serverName}`); - logger.debug(`Server version: ${version}`); - const server = new MCPServer( - { - name: serverName, - version: version, - }, - { - capabilities: { - tools: {}, - }, - } - ); - logger.debug(`Registering tools with MCP server...`); - let registeredCount = 0; - let skippedCount = 0; - for (const tool of tools) { - if (!tool.handler) { - logger.debug(`Skipping tool ${tool.name} - no handler loaded`); - skippedCount++; - continue; - } - logger.debug(`Registering tool: ${tool.name}`); - server.tool(tool.name, tool.description || "", tool.inputSchema || { type: "object", properties: {} }, async args => { - logger.debug(`Calling handler for tool: ${tool.name}`); - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - throw new Error(`Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); - } - const result = await Promise.resolve(tool.handler(args)); - logger.debug(`Handler returned for tool: ${tool.name}`); - const content = result && result.content ? result.content : []; - return { content, isError: false }; - }); - registeredCount++; - } - logger.debug(`Tool registration complete: ${registeredCount} registered, ${skippedCount} skipped`); - logger.debug(`=== MCP Server Creation Complete ===`); - cleanupConfigFile(configPath, logger); - return { server, config, logger }; - } - async function startHttpServer(configPath, options = {}) { - const port = options.port || 3000; - const stateless = options.stateless || false; - const logger = createLogger("safe-inputs-startup"); - logger.debug(`=== Starting Safe Inputs MCP HTTP Server ===`); - logger.debug(`Configuration file: ${configPath}`); - logger.debug(`Port: ${port}`); - logger.debug(`Mode: ${stateless ? "stateless" : "stateful"}`); - logger.debug(`Environment: NODE_VERSION=${process.version}, PLATFORM=${process.platform}`); - try { - const { server, config, logger: mcpLogger } = createMCPServer(configPath, { logDir: options.logDir }); - Object.assign(logger, mcpLogger); - logger.debug(`MCP server created successfully`); - logger.debug(`Server name: ${config.serverName || "safeinputs"}`); - logger.debug(`Server version: ${config.version || "1.0.0"}`); - logger.debug(`Tools configured: ${config.tools.length}`); - logger.debug(`Creating HTTP transport...`); - const transport = new MCPHTTPTransport({ - sessionIdGenerator: stateless ? undefined : () => randomUUID(), - enableJsonResponse: true, - enableDnsRebindingProtection: false, - }); - logger.debug(`HTTP transport created`); - logger.debug(`Connecting server to transport...`); - await server.connect(transport); - logger.debug(`Server connected to transport successfully`); - logger.debug(`Creating HTTP server...`); - const httpServer = http.createServer(async (req, res) => { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept"); - if (req.method === "OPTIONS") { - res.writeHead(200); - res.end(); - return; - } - if (req.method === "GET" && req.url === "/health") { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - status: "ok", - server: config.serverName || "safeinputs", - version: config.version || "1.0.0", - tools: config.tools.length, - }) - ); - return; - } - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Method not allowed" })); - return; - } - try { - let body = null; - if (req.method === "POST") { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const bodyStr = Buffer.concat(chunks).toString(); - try { - body = bodyStr ? JSON.parse(bodyStr) : null; - } catch (parseError) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32700, - message: "Parse error: Invalid JSON in request body", - }, - id: null, - }) - ); - return; - } - } - await transport.handleRequest(req, res, body); - } catch (error) { - logger.debugError("Error handling request: ", error); - if (!res.headersSent) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), - }, - id: null, - }) - ); - } - } - }); - logger.debug(`Attempting to bind to port ${port}...`); - httpServer.listen(port, () => { - logger.debug(`=== Safe Inputs MCP HTTP Server Started Successfully ===`); - logger.debug(`HTTP server listening on http://localhost:${port}`); - logger.debug(`MCP endpoint: POST http://localhost:${port}/`); - logger.debug(`Server name: ${config.serverName || "safeinputs"}`); - logger.debug(`Server version: ${config.version || "1.0.0"}`); - logger.debug(`Tools available: ${config.tools.length}`); - logger.debug(`Server is ready to accept requests`); - }); - httpServer.on("error", error => { - if (error.code === "EADDRINUSE") { - logger.debugError(`ERROR: Port ${port} is already in use. `, error); - } else if (error.code === "EACCES") { - logger.debugError(`ERROR: Permission denied to bind to port ${port}. `, error); - } else { - logger.debugError(`ERROR: Failed to start HTTP server: `, error); - } - process.exit(1); - }); - process.on("SIGINT", () => { - logger.debug("Received SIGINT, shutting down..."); - httpServer.close(() => { - logger.debug("HTTP server closed"); - process.exit(0); - }); - }); - process.on("SIGTERM", () => { - logger.debug("Received SIGTERM, shutting down..."); - httpServer.close(() => { - logger.debug("HTTP server closed"); - process.exit(0); - }); - }); - return httpServer; - } catch (error) { - const errorLogger = createLogger("safe-inputs-startup-error"); - errorLogger.debug(`=== FATAL ERROR: Failed to start Safe Inputs MCP HTTP Server ===`); - errorLogger.debug(`Error type: ${error.constructor.name}`); - errorLogger.debug(`Error message: ${error.message}`); - if (error.stack) { - errorLogger.debug(`Stack trace:\n${error.stack}`); - } - if (error.code) { - errorLogger.debug(`Error code: ${error.code}`); - } - errorLogger.debug(`Configuration file: ${configPath}`); - errorLogger.debug(`Port: ${port}`); - throw error; - } - } - if (require.main === module) { - const args = process.argv.slice(2); - if (args.length < 1) { - console.error("Usage: node safe_inputs_mcp_server_http.cjs [--port ] [--stateless] [--log-dir ]"); - process.exit(1); - } - const configPath = args[0]; - const options = { - port: 3000, - stateless: false, - logDir: undefined, - }; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--port" && args[i + 1]) { - options.port = parseInt(args[i + 1], 10); - i++; - } else if (args[i] === "--stateless") { - options.stateless = true; - } else if (args[i] === "--log-dir" && args[i + 1]) { - options.logDir = args[i + 1]; - i++; - } - } - startHttpServer(configPath, options).catch(error => { - console.error(`Error starting HTTP server: ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - }); - } - module.exports = { - startHttpServer, - createMCPServer, - }; - EOF_SAFE_INPUTS_SERVER_HTTP + - name: Checkout actions folder for safe-inputs + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + sparse-checkout: | + actions + - name: Setup Safe Inputs JavaScript Files + uses: ./actions/setup-safe-inputs + with: + destination: /tmp/gh-aw/safe-inputs + - name: Setup Safe Inputs Config Files + run: | cat > /tmp/gh-aw/safe-inputs/tools.json << 'EOF_TOOLS_JSON' { "serverName": "safeinputs", diff --git a/.github/workflows/smoke-copilot-playwright.lock.yml b/.github/workflows/smoke-copilot-playwright.lock.yml index 8c51298b1ad..b07ef9aac90 100644 --- a/.github/workflows/smoke-copilot-playwright.lock.yml +++ b/.github/workflows/smoke-copilot-playwright.lock.yml @@ -528,1234 +528,20 @@ jobs: EOF chmod +x /tmp/gh-aw/safeoutputs/mcp-server.cjs - - name: Setup Safe Inputs JavaScript and Config + - name: Setup Safe Inputs Directory run: | mkdir -p /tmp/gh-aw/safe-inputs/logs - cat > /tmp/gh-aw/safe-inputs/read_buffer.cjs << 'EOF_READ_BUFFER' - class ReadBuffer { - constructor() { - this._buffer = null; - } - append(chunk) { - this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; - } - readMessage() { - if (!this._buffer) { - return null; - } - const index = this._buffer.indexOf("\n"); - if (index === -1) { - return null; - } - const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, ""); - this._buffer = this._buffer.subarray(index + 1); - if (line.trim() === "") { - return this.readMessage(); - } - try { - return JSON.parse(line); - } catch (error) { - throw new Error(`Parse error: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - module.exports = { - ReadBuffer, - }; - EOF_READ_BUFFER - cat > /tmp/gh-aw/safe-inputs/mcp_server_core.cjs << 'EOF_MCP_CORE' - const fs = require("fs"); - const path = require("path"); - const { ReadBuffer } = require("./read_buffer.cjs"); - const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); - const encoder = new TextEncoder(); - function initLogFile(server) { - if (server.logFileInitialized || !server.logDir || !server.logFilePath) return; - try { - if (!fs.existsSync(server.logDir)) { - fs.mkdirSync(server.logDir, { recursive: true }); - } - const timestamp = new Date().toISOString(); - fs.writeFileSync(server.logFilePath, `# ${server.serverInfo.name} MCP Server Log\n# Started: ${timestamp}\n# Version: ${server.serverInfo.version}\n\n`); - server.logFileInitialized = true; - } catch { - } - } - function createDebugFunction(server) { - return msg => { - const timestamp = new Date().toISOString(); - const formattedMsg = `[${timestamp}] [${server.serverInfo.name}] ${msg}\n`; - process.stderr.write(formattedMsg); - if (server.logDir && server.logFilePath) { - if (!server.logFileInitialized) { - initLogFile(server); - } - if (server.logFileInitialized) { - try { - fs.appendFileSync(server.logFilePath, formattedMsg); - } catch { - } - } - } - }; - } - 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); - server.debug(`send: ${json}`); - const message = json + "\n"; - const bytes = encoder.encode(message); - fs.writeSync(1, bytes); - }; - } - function createReplyResultFunction(server) { - return (id, result) => { - if (id === undefined || id === null) return; - const res = { jsonrpc: "2.0", id, result }; - server.writeMessage(res); - }; - } - function createReplyErrorFunction(server) { - return (id, code, message) => { - if (id === undefined || id === null) { - server.debug(`Error for notification: ${message}`); - return; - } - const error = { code, message }; - const res = { - jsonrpc: "2.0", - id, - error, - }; - server.writeMessage(res); - }; - } - function createServer(serverInfo, options = {}) { - const logDir = options.logDir || undefined; - const logFilePath = logDir ? path.join(logDir, "server.log") : undefined; - const server = { - serverInfo, - tools: {}, - debug: () => {}, - debugError: () => {}, - writeMessage: () => {}, - replyResult: () => {}, - replyError: () => {}, - readBuffer: new ReadBuffer(), - logDir, - logFilePath, - 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 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); - } - } - const { createShellHandler } = require("./mcp_handler_shell.cjs"); - const timeout = tool.timeout || 60; - tool.handler = createShellHandler(server, toolName, resolvedPath, timeout); - loadedCount++; - server.debug(` [${toolName}] Shell handler created successfully with timeout: ${timeout}s`); - } else if (ext === ".py") { - server.debug(` [${toolName}] Detected Python script handler`); - try { - fs.accessSync(resolvedPath, fs.constants.X_OK); - server.debug(` [${toolName}] Python script is executable`); - } catch { - try { - fs.chmodSync(resolvedPath, 0o755); - server.debug(` [${toolName}] Made Python script executable`); - } catch (chmodError) { - server.debugError(` [${toolName}] Warning: Could not make Python script executable: `, chmodError); - } - } - const { createPythonHandler } = require("./mcp_handler_python.cjs"); - const timeout = tool.timeout || 60; - tool.handler = createPythonHandler(server, toolName, resolvedPath, timeout); - loadedCount++; - server.debug(` [${toolName}] Python handler created successfully with timeout: ${timeout}s`); - } 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] = { - ...tool, - name: normalizedName, - }; - server.debug(`Registered tool: ${normalizedName}`); - } - function normalizeTool(name) { - return name.replace(/-/g, "_").toLowerCase(); - } - async function handleRequest(server, request, defaultHandler) { - const { id, method, params } = request; - try { - if (!("id" in request)) { - return null; - } - let result; - if (method === "initialize") { - const protocolVersion = params?.protocolVersion || "2024-11-05"; - result = { - protocolVersion, - serverInfo: server.serverInfo, - capabilities: { - tools: {}, - }, - }; - } else if (method === "ping") { - result = {}; - } else if (method === "tools/list") { - const list = []; - Object.values(server.tools).forEach(tool => { - const toolDef = { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - list.push(toolDef); - }); - result = { tools: list }; - } else if (method === "tools/call") { - const name = params?.name; - const args = params?.arguments ?? {}; - if (!name || typeof name !== "string") { - throw { - code: -32602, - message: "Invalid params: 'name' must be a string", - }; - } - const tool = server.tools[normalizeTool(name)]; - if (!tool) { - throw { - code: -32602, - message: `Tool '${name}' not found`, - }; - } - let handler = tool.handler; - if (!handler && defaultHandler) { - handler = defaultHandler(tool.name); - } - if (!handler) { - throw { - code: -32603, - message: `No handler for tool: ${name}`, - }; - } - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - throw { - code: -32602, - message: `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`, - }; - } - const handlerResult = await Promise.resolve(handler(args)); - const content = handlerResult && handlerResult.content ? handlerResult.content : []; - result = { content, isError: false }; - } else if (/^notifications\//.test(method)) { - return null; - } else { - throw { - code: -32601, - message: `Method not found: ${method}`, - }; - } - return { - jsonrpc: "2.0", - id, - result, - }; - } catch (error) { - const err = error; - return { - jsonrpc: "2.0", - id, - error: { - code: err.code || -32603, - message: err.message || "Internal error", - }, - }; - } - } - async function handleMessage(server, req, defaultHandler) { - if (!req || typeof req !== "object") { - server.debug(`Invalid message: not an object`); - return; - } - if (req.jsonrpc !== "2.0") { - server.debug(`Invalid message: missing or invalid jsonrpc field`); - return; - } - const { id, method, params } = req; - if (!method || typeof method !== "string") { - server.replyError(id, -32600, "Invalid Request: method must be a string"); - return; - } - try { - if (method === "initialize") { - const clientInfo = params?.clientInfo ?? {}; - server.debug(`client info: ${JSON.stringify(clientInfo)}`); - const protocolVersion = params?.protocolVersion ?? undefined; - const result = { - serverInfo: server.serverInfo, - ...(protocolVersion ? { protocolVersion } : {}), - capabilities: { - tools: {}, - }, - }; - server.replyResult(id, result); - } else if (method === "tools/list") { - const list = []; - Object.values(server.tools).forEach(tool => { - const toolDef = { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - list.push(toolDef); - }); - server.replyResult(id, { tools: list }); - } else if (method === "tools/call") { - const name = params?.name; - const args = params?.arguments ?? {}; - if (!name || typeof name !== "string") { - server.replyError(id, -32602, "Invalid params: 'name' must be a string"); - return; - } - const tool = server.tools[normalizeTool(name)]; - if (!tool) { - server.replyError(id, -32601, `Tool not found: ${name} (${normalizeTool(name)})`); - return; - } - let handler = tool.handler; - if (!handler && defaultHandler) { - handler = defaultHandler(tool.name); - } - if (!handler) { - server.replyError(id, -32603, `No handler for tool: ${name}`); - return; - } - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - server.replyError(id, -32602, `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); - return; - } - 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)) { - server.debug(`ignore ${method}`); - } else { - server.replyError(id, -32601, `Method not found: ${method}`); - } - } catch (e) { - server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); - } - } - async function processReadBuffer(server, defaultHandler) { - while (true) { - try { - const message = server.readBuffer.readMessage(); - if (!message) { - break; - } - server.debug(`recv: ${JSON.stringify(message)}`); - await handleMessage(server, message, defaultHandler); - } catch (error) { - server.debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - function start(server, options = {}) { - const { defaultHandler } = options; - server.debug(`v${server.serverInfo.version} ready on stdio`); - server.debug(` tools: ${Object.keys(server.tools).join(", ")}`); - if (!Object.keys(server.tools).length) { - throw new Error("No tools registered"); - } - const onData = async chunk => { - server.readBuffer.append(chunk); - await processReadBuffer(server, defaultHandler); - }; - process.stdin.on("data", onData); - process.stdin.on("error", err => server.debug(`stdin error: ${err}`)); - process.stdin.resume(); - server.debug(`listening...`); - } - module.exports = { - createServer, - registerTool, - normalizeTool, - handleRequest, - handleMessage, - processReadBuffer, - start, - loadToolHandlers, - }; - EOF_MCP_CORE - cat > /tmp/gh-aw/safe-inputs/mcp_http_transport.cjs << 'EOF_MCP_HTTP_TRANSPORT' - const http = require("http"); - const { randomUUID } = require("crypto"); - const { createServer, registerTool, handleRequest } = require("./mcp_server_core.cjs"); - class MCPServer { - constructor(serverInfo, options = {}) { - this._coreServer = createServer(serverInfo, options); - this.serverInfo = serverInfo; - this.capabilities = options.capabilities || { tools: {} }; - this.tools = new Map(); - this.transport = null; - this.initialized = false; - } - tool(name, description, inputSchema, handler) { - this.tools.set(name, { - name, - description, - inputSchema, - handler, - }); - registerTool(this._coreServer, { - name, - description, - inputSchema, - handler, - }); - } - async connect(transport) { - this.transport = transport; - transport.setServer(this); - await transport.start(); - } - async handleRequest(request) { - if (request.method === "initialize") { - this.initialized = true; - } - return handleRequest(this._coreServer, request); - } - } - class MCPHTTPTransport { - constructor(options = {}) { - this.sessionIdGenerator = options.sessionIdGenerator; - this.enableJsonResponse = options.enableJsonResponse !== false; - this.enableDnsRebindingProtection = options.enableDnsRebindingProtection || false; - this.server = null; - this.sessionId = null; - this.started = false; - } - setServer(server) { - this.server = server; - } - async start() { - if (this.started) { - throw new Error("Transport already started"); - } - this.started = true; - } - async handleRequest(req, res, parsedBody) { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id"); - if (req.method === "OPTIONS") { - res.writeHead(200); - res.end(); - return; - } - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Method not allowed" })); - return; - } - try { - let body = parsedBody; - if (!body) { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const bodyStr = Buffer.concat(chunks).toString(); - try { - body = bodyStr ? JSON.parse(bodyStr) : null; - } catch (parseError) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32700, - message: "Parse error: Invalid JSON in request body", - }, - id: null, - }) - ); - return; - } - } - if (!body) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: Empty request body", - }, - id: null, - }) - ); - return; - } - if (!body.jsonrpc || body.jsonrpc !== "2.0") { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: jsonrpc must be '2.0'", - }, - id: body.id || null, - }) - ); - return; - } - if (this.sessionIdGenerator) { - if (body.method === "initialize") { - this.sessionId = this.sessionIdGenerator(); - } else { - const requestSessionId = req.headers["mcp-session-id"]; - if (!requestSessionId) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32600, - message: "Invalid Request: Missing Mcp-Session-Id header", - }, - id: body.id || null, - }) - ); - return; - } - if (requestSessionId !== this.sessionId) { - res.writeHead(404, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32001, - message: "Session not found", - }, - id: body.id || null, - }) - ); - return; - } - } - } - const response = await this.server.handleRequest(body); - if (response === null) { - res.writeHead(204); - res.end(); - return; - } - const headers = { "Content-Type": "application/json" }; - if (this.sessionId) { - headers["mcp-session-id"] = this.sessionId; - } - res.writeHead(200, headers); - res.end(JSON.stringify(response)); - } catch (error) { - if (!res.headersSent) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), - }, - id: null, - }) - ); - } - } - } - } - module.exports = { - MCPServer, - MCPHTTPTransport, - }; - EOF_MCP_HTTP_TRANSPORT - cat > /tmp/gh-aw/safe-inputs/mcp_logger.cjs << 'EOF_MCP_LOGGER' - function createLogger(serverName) { - const logger = { - debug: msg => { - const timestamp = new Date().toISOString(); - process.stderr.write(`[${timestamp}] [${serverName}] ${msg}\n`); - }, - debugError: (prefix, error) => { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.debug(`${prefix}${errorMessage}`); - if (error instanceof Error && error.stack) { - logger.debug(`${prefix}Stack trace: ${error.stack}`); - } - }, - }; - return logger; - } - module.exports = { - createLogger, - }; - EOF_MCP_LOGGER - cat > /tmp/gh-aw/safe-inputs/mcp_handler_shell.cjs << 'EOF_HANDLER_SHELL' - const fs = require("fs"); - const path = require("path"); - const { execFile } = require("child_process"); - const os = require("os"); - function createShellHandler(server, toolName, scriptPath, timeoutSeconds = 60) { - return async args => { - server.debug(` [${toolName}] Invoking shell handler: ${scriptPath}`); - server.debug(` [${toolName}] Shell handler args: ${JSON.stringify(args)}`); - server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); - 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: timeoutSeconds * 1000, - 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), - }, - ], - }); - } - ); - }); - }; - } - module.exports = { - createShellHandler, - }; - EOF_HANDLER_SHELL - cat > /tmp/gh-aw/safe-inputs/mcp_handler_python.cjs << 'EOF_HANDLER_PYTHON' - const { execFile } = require("child_process"); - function createPythonHandler(server, toolName, scriptPath, timeoutSeconds = 60) { - return async args => { - server.debug(` [${toolName}] Invoking Python handler: ${scriptPath}`); - server.debug(` [${toolName}] Python handler args: ${JSON.stringify(args)}`); - server.debug(` [${toolName}] Timeout: ${timeoutSeconds}s`); - const inputJson = JSON.stringify(args || {}); - server.debug(` [${toolName}] Input JSON (${inputJson.length} bytes): ${inputJson.substring(0, 200)}${inputJson.length > 200 ? "..." : ""}`); - return new Promise((resolve, reject) => { - server.debug(` [${toolName}] Executing Python script...`); - const child = execFile( - "python3", - [scriptPath], - { - env: process.env, - timeout: timeoutSeconds * 1000, - 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}] Python script error: `, error); - reject(error); - return; - } - let result; - try { - if (stdout && stdout.trim()) { - result = JSON.parse(stdout.trim()); - } else { - result = { stdout: stdout || "", stderr: stderr || "" }; - } - } catch (parseError) { - server.debug(` [${toolName}] Output is not JSON, returning as text`); - result = { stdout: stdout || "", stderr: stderr || "" }; - } - server.debug(` [${toolName}] Python handler completed successfully`); - resolve({ - content: [ - { - type: "text", - text: JSON.stringify(result), - }, - ], - }); - } - ); - if (child.stdin) { - child.stdin.write(inputJson); - child.stdin.end(); - } - }); - }; - } - module.exports = { - createPythonHandler, - }; - EOF_HANDLER_PYTHON - cat > /tmp/gh-aw/safe-inputs/safe_inputs_config_loader.cjs << 'EOF_CONFIG_LOADER' - const fs = require("fs"); - function loadConfig(configPath) { - if (!fs.existsSync(configPath)) { - throw new Error(`Configuration file not found: ${configPath}`); - } - const configContent = fs.readFileSync(configPath, "utf-8"); - const config = JSON.parse(configContent); - if (!config.tools || !Array.isArray(config.tools)) { - throw new Error("Configuration must contain a 'tools' array"); - } - return config; - } - module.exports = { - loadConfig, - }; - EOF_CONFIG_LOADER - cat > /tmp/gh-aw/safe-inputs/safe_inputs_tool_factory.cjs << 'EOF_TOOL_FACTORY' - function createToolConfig(name, description, inputSchema, handlerPath) { - return { - name, - description, - inputSchema, - handler: handlerPath, - }; - } - module.exports = { - createToolConfig, - }; - EOF_TOOL_FACTORY - cat > /tmp/gh-aw/safe-inputs/safe_inputs_validation.cjs << 'EOF_VALIDATION' - function validateRequiredFields(args, inputSchema) { - const requiredFields = inputSchema && Array.isArray(inputSchema.required) ? inputSchema.required : []; - if (!requiredFields.length) { - return []; - } - const missing = requiredFields.filter(f => { - const value = args[f]; - return value === undefined || value === null || (typeof value === "string" && value.trim() === ""); - }); - return missing; - } - module.exports = { - validateRequiredFields, - }; - EOF_VALIDATION - cat > /tmp/gh-aw/safe-inputs/safe_inputs_bootstrap.cjs << 'EOF_BOOTSTRAP' - const path = require("path"); - const fs = require("fs"); - const { loadConfig } = require("./safe_inputs_config_loader.cjs"); - const { loadToolHandlers } = require("./mcp_server_core.cjs"); - function bootstrapSafeInputsServer(configPath, logger) { - logger.debug(`Loading safe-inputs configuration from: ${configPath}`); - const config = loadConfig(configPath); - const basePath = path.dirname(configPath); - logger.debug(`Base path for handlers: ${basePath}`); - logger.debug(`Tools to load: ${config.tools.length}`); - const tools = loadToolHandlers(logger, config.tools, basePath); - return { config, basePath, tools }; - } - function cleanupConfigFile(configPath, logger) { - try { - if (fs.existsSync(configPath)) { - fs.unlinkSync(configPath); - logger.debug(`Deleted configuration file: ${configPath}`); - } - } catch (error) { - logger.debugError(`Warning: Could not delete configuration file: `, error); - } - } - module.exports = { - bootstrapSafeInputsServer, - cleanupConfigFile, - }; - EOF_BOOTSTRAP - cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server.cjs << 'EOF_SAFE_INPUTS_SERVER' - const { createServer, registerTool, start } = require("./mcp_server_core.cjs"); - const { loadConfig } = require("./safe_inputs_config_loader.cjs"); - const { createToolConfig } = require("./safe_inputs_tool_factory.cjs"); - const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); - function startSafeInputsServer(configPath, options = {}) { - const logDir = options.logDir || undefined; - const server = createServer({ name: "safeinputs", version: "1.0.0" }, { logDir }); - const { config, tools } = bootstrapSafeInputsServer(configPath, server); - server.serverInfo.name = config.serverName || "safeinputs"; - server.serverInfo.version = config.version || "1.0.0"; - if (!options.logDir && config.logDir) { - server.logDir = config.logDir; - } - for (const tool of tools) { - registerTool(server, tool); - } - if (!options.skipCleanup) { - cleanupConfigFile(configPath, server); - } - start(server); - } - if (require.main === module) { - const args = process.argv.slice(2); - if (args.length < 1) { - console.error("Usage: node safe_inputs_mcp_server.cjs [--log-dir ]"); - process.exit(1); - } - const configPath = args[0]; - const options = {}; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--log-dir" && args[i + 1]) { - options.logDir = args[i + 1]; - i++; - } - } - try { - startSafeInputsServer(configPath, options); - } catch (error) { - console.error(`Error starting safe-inputs server: ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - } - } - module.exports = { - startSafeInputsServer, - loadConfig, - createToolConfig, - }; - EOF_SAFE_INPUTS_SERVER - cat > /tmp/gh-aw/safe-inputs/safe_inputs_mcp_server_http.cjs << 'EOF_SAFE_INPUTS_SERVER_HTTP' - const http = require("http"); - const { randomUUID } = require("crypto"); - const { MCPServer, MCPHTTPTransport } = require("./mcp_http_transport.cjs"); - const { validateRequiredFields } = require("./safe_inputs_validation.cjs"); - const { createLogger } = require("./mcp_logger.cjs"); - const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); - function createMCPServer(configPath, options = {}) { - const logger = createLogger("safeinputs"); - logger.debug(`=== Creating MCP Server ===`); - logger.debug(`Configuration file: ${configPath}`); - const { config, tools } = bootstrapSafeInputsServer(configPath, logger); - const serverName = config.serverName || "safeinputs"; - const version = config.version || "1.0.0"; - logger.debug(`Server name: ${serverName}`); - logger.debug(`Server version: ${version}`); - const server = new MCPServer( - { - name: serverName, - version: version, - }, - { - capabilities: { - tools: {}, - }, - } - ); - logger.debug(`Registering tools with MCP server...`); - let registeredCount = 0; - let skippedCount = 0; - for (const tool of tools) { - if (!tool.handler) { - logger.debug(`Skipping tool ${tool.name} - no handler loaded`); - skippedCount++; - continue; - } - logger.debug(`Registering tool: ${tool.name}`); - server.tool(tool.name, tool.description || "", tool.inputSchema || { type: "object", properties: {} }, async args => { - logger.debug(`Calling handler for tool: ${tool.name}`); - const missing = validateRequiredFields(args, tool.inputSchema); - if (missing.length) { - throw new Error(`Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`); - } - const result = await Promise.resolve(tool.handler(args)); - logger.debug(`Handler returned for tool: ${tool.name}`); - const content = result && result.content ? result.content : []; - return { content, isError: false }; - }); - registeredCount++; - } - logger.debug(`Tool registration complete: ${registeredCount} registered, ${skippedCount} skipped`); - logger.debug(`=== MCP Server Creation Complete ===`); - cleanupConfigFile(configPath, logger); - return { server, config, logger }; - } - async function startHttpServer(configPath, options = {}) { - const port = options.port || 3000; - const stateless = options.stateless || false; - const logger = createLogger("safe-inputs-startup"); - logger.debug(`=== Starting Safe Inputs MCP HTTP Server ===`); - logger.debug(`Configuration file: ${configPath}`); - logger.debug(`Port: ${port}`); - logger.debug(`Mode: ${stateless ? "stateless" : "stateful"}`); - logger.debug(`Environment: NODE_VERSION=${process.version}, PLATFORM=${process.platform}`); - try { - const { server, config, logger: mcpLogger } = createMCPServer(configPath, { logDir: options.logDir }); - Object.assign(logger, mcpLogger); - logger.debug(`MCP server created successfully`); - logger.debug(`Server name: ${config.serverName || "safeinputs"}`); - logger.debug(`Server version: ${config.version || "1.0.0"}`); - logger.debug(`Tools configured: ${config.tools.length}`); - logger.debug(`Creating HTTP transport...`); - const transport = new MCPHTTPTransport({ - sessionIdGenerator: stateless ? undefined : () => randomUUID(), - enableJsonResponse: true, - enableDnsRebindingProtection: false, - }); - logger.debug(`HTTP transport created`); - logger.debug(`Connecting server to transport...`); - await server.connect(transport); - logger.debug(`Server connected to transport successfully`); - logger.debug(`Creating HTTP server...`); - const httpServer = http.createServer(async (req, res) => { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept"); - if (req.method === "OPTIONS") { - res.writeHead(200); - res.end(); - return; - } - if (req.method === "GET" && req.url === "/health") { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - status: "ok", - server: config.serverName || "safeinputs", - version: config.version || "1.0.0", - tools: config.tools.length, - }) - ); - return; - } - if (req.method !== "POST") { - res.writeHead(405, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Method not allowed" })); - return; - } - try { - let body = null; - if (req.method === "POST") { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); - } - const bodyStr = Buffer.concat(chunks).toString(); - try { - body = bodyStr ? JSON.parse(bodyStr) : null; - } catch (parseError) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32700, - message: "Parse error: Invalid JSON in request body", - }, - id: null, - }) - ); - return; - } - } - await transport.handleRequest(req, res, body); - } catch (error) { - logger.debugError("Error handling request: ", error); - if (!res.headersSent) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32603, - message: error instanceof Error ? error.message : String(error), - }, - id: null, - }) - ); - } - } - }); - logger.debug(`Attempting to bind to port ${port}...`); - httpServer.listen(port, () => { - logger.debug(`=== Safe Inputs MCP HTTP Server Started Successfully ===`); - logger.debug(`HTTP server listening on http://localhost:${port}`); - logger.debug(`MCP endpoint: POST http://localhost:${port}/`); - logger.debug(`Server name: ${config.serverName || "safeinputs"}`); - logger.debug(`Server version: ${config.version || "1.0.0"}`); - logger.debug(`Tools available: ${config.tools.length}`); - logger.debug(`Server is ready to accept requests`); - }); - httpServer.on("error", error => { - if (error.code === "EADDRINUSE") { - logger.debugError(`ERROR: Port ${port} is already in use. `, error); - } else if (error.code === "EACCES") { - logger.debugError(`ERROR: Permission denied to bind to port ${port}. `, error); - } else { - logger.debugError(`ERROR: Failed to start HTTP server: `, error); - } - process.exit(1); - }); - process.on("SIGINT", () => { - logger.debug("Received SIGINT, shutting down..."); - httpServer.close(() => { - logger.debug("HTTP server closed"); - process.exit(0); - }); - }); - process.on("SIGTERM", () => { - logger.debug("Received SIGTERM, shutting down..."); - httpServer.close(() => { - logger.debug("HTTP server closed"); - process.exit(0); - }); - }); - return httpServer; - } catch (error) { - const errorLogger = createLogger("safe-inputs-startup-error"); - errorLogger.debug(`=== FATAL ERROR: Failed to start Safe Inputs MCP HTTP Server ===`); - errorLogger.debug(`Error type: ${error.constructor.name}`); - errorLogger.debug(`Error message: ${error.message}`); - if (error.stack) { - errorLogger.debug(`Stack trace:\n${error.stack}`); - } - if (error.code) { - errorLogger.debug(`Error code: ${error.code}`); - } - errorLogger.debug(`Configuration file: ${configPath}`); - errorLogger.debug(`Port: ${port}`); - throw error; - } - } - if (require.main === module) { - const args = process.argv.slice(2); - if (args.length < 1) { - console.error("Usage: node safe_inputs_mcp_server_http.cjs [--port ] [--stateless] [--log-dir ]"); - process.exit(1); - } - const configPath = args[0]; - const options = { - port: 3000, - stateless: false, - logDir: undefined, - }; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--port" && args[i + 1]) { - options.port = parseInt(args[i + 1], 10); - i++; - } else if (args[i] === "--stateless") { - options.stateless = true; - } else if (args[i] === "--log-dir" && args[i + 1]) { - options.logDir = args[i + 1]; - i++; - } - } - startHttpServer(configPath, options).catch(error => { - console.error(`Error starting HTTP server: ${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - }); - } - module.exports = { - startHttpServer, - createMCPServer, - }; - EOF_SAFE_INPUTS_SERVER_HTTP + - name: Checkout actions folder for safe-inputs + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + sparse-checkout: | + actions + - name: Setup Safe Inputs JavaScript Files + uses: ./actions/setup-safe-inputs + with: + destination: /tmp/gh-aw/safe-inputs + - name: Setup Safe Inputs Config Files + run: | cat > /tmp/gh-aw/safe-inputs/tools.json << 'EOF_TOOLS_JSON' { "serverName": "safeinputs", From 52dfb62e2359b2a9eaac417f980a8cb47cf4f9f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 07:16:32 +0000 Subject: [PATCH 6/6] Fix linter warning for shell script permissions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/actions_build_command.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cli/actions_build_command.go b/pkg/cli/actions_build_command.go index 37790c154a7..307e9975d38 100644 --- a/pkg/cli/actions_build_command.go +++ b/pkg/cli/actions_build_command.go @@ -392,6 +392,7 @@ func buildSetupAction(actionsDir, actionName string) error { for filename, content := range shellScripts { destPath := filepath.Join(shDir, filename) // Shell scripts should be executable (0755) + //nolint:gosec // G306: Shell scripts require executable permissions if err := os.WriteFile(destPath, []byte(content), 0755); err != nil { return fmt.Errorf("failed to write %s: %w", filename, err) }