UNPKG

user-feedback-mcp

Version:

mcp server to request user feedback via an UI instead of normal chat

563 lines (546 loc) 17.1 kB
#!/usr/bin/env node "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // package.json var require_package = __commonJS({ "package.json"(exports2, module2) { module2.exports = { name: "user-feedback-mcp", version: "0.2.5", description: "mcp server to request user feedback via an UI instead of normal chat", bin: { "user-feedback-mcp": "./dist/cli.js" }, scripts: { build: "node build/build.js", dev: "tsc --watch", lint: "eslint src --ext .ts,.js", test: "jest", start: "node ./dist/cli.js start", package: "node build/package.js" }, dependencies: { "@modelcontextprotocol/sdk": "^1.11.2", commander: "^10.0.1", "electron-store": "^8.1.0", "fs-extra": "^11.1.1", winston: "^3.17.0", zod: "^3.24.4" }, devDependencies: { "@types/fs-extra": "^11.0.1", "@types/jest": "^29.5.0", "@types/node": "^18.16.0", electron: "25.9.8", esbuild: "^0.25.4", jest: "^29.5.0", "ts-jest": "^29.1.0", typescript: "^5.0.4" }, files: [ "dist/**/*" ] }; } }); // ../../packages/shared/src/types.ts var init_types = __esm({ "../../packages/shared/src/types.ts"() { "use strict"; } }); // ../../packages/shared/src/constants.ts var path, os, TEMP_DIR, FEEDBACK_FILE_NAME, ENV_PROMPT, ENV_FEEDBACK_FILE, ENV_ENABLE_KNOWLEDGE_DB, ENV_ENABLE_UI_TESTER; var init_constants = __esm({ "../../packages/shared/src/constants.ts"() { "use strict"; path = __toESM(require("path")); os = __toESM(require("os")); TEMP_DIR = path.join(os.tmpdir(), "user-feedback-mcp"); FEEDBACK_FILE_NAME = "feedback.json"; ENV_PROMPT = "USER_FEEDBACK_PROMPT"; ENV_FEEDBACK_FILE = "USER_FEEDBACK_FILE"; ENV_ENABLE_KNOWLEDGE_DB = "ENABLE_KNOWLEDGE_DB"; ENV_ENABLE_UI_TESTER = "ENABLE_UI_TESTER"; } }); // ../../packages/shared/src/file-utils.ts async function ensureTempDir() { await fs.ensureDir(TEMP_DIR); } function generateFeedbackFilePath() { const uniqueId = Date.now().toString(); return path2.join(TEMP_DIR, `${uniqueId}-${FEEDBACK_FILE_NAME}`); } async function readFeedbackFromFile(filePath) { try { const data = await fs.readJson(filePath); if (data.cancelled) { return { feedback: "", status: "cancelled" /* CANCELLED */, error: "User cancelled feedback by closing the window" }; } const response = { feedback: data.feedback, status: "success" /* SUCCESS */ }; if (data.images && data.images.length > 0) { response.images = data.images; } return response; } catch (error) { return { feedback: "", status: "error" /* ERROR */, error: `Failed to read feedback file: ${error.message || String(error)}` }; } } async function cleanupFeedbackFile(filePath) { try { await fs.remove(filePath); } catch (error) { console.error( `Failed to clean up feedback file: ${error.message || String(error)}` ); } } var fs, path2; var init_file_utils = __esm({ "../../packages/shared/src/file-utils.ts"() { "use strict"; fs = __toESM(require("fs-extra")); path2 = __toESM(require("path")); init_constants(); init_types(); } }); // ../../packages/shared/src/error-utils.ts function createErrorResponse(error, status = "error" /* ERROR */) { const message = typeof error === "string" ? error : error.message; return { feedback: "", status, error: message }; } var init_error_utils = __esm({ "../../packages/shared/src/error-utils.ts"() { "use strict"; init_types(); } }); // ../../packages/shared/src/index.ts var init_src = __esm({ "../../packages/shared/src/index.ts"() { "use strict"; init_types(); init_constants(); init_file_utils(); init_error_utils(); } }); // ../../packages/mcp-server/src/utils/logger.ts var winston, logLevels, logger, logger_default; var init_logger = __esm({ "../../packages/mcp-server/src/utils/logger.ts"() { "use strict"; winston = __toESM(require("winston")); logLevels = { error: 0, warn: 1, info: 2, debug: 3 }; logger = winston.createLogger({ levels: logLevels, silent: true, // Completely disable all logging transports: [] }); logger_default = logger; } }); // ../../packages/mcp-server/src/handlers/user-feedback.ts async function getUserFeedback(params) { logger_default.info("Received user feedback request", { prompt: params.prompt }); if (!params.prompt) { logger_default.error("Missing required parameter: prompt"); return createErrorResponse("Prompt is required"); } await ensureTempDir(); const feedbackFilePath = generateFeedbackFilePath(); try { const electronGuiPath = getElectronGuiPath(); const env = { ...process.env, [ENV_PROMPT]: params.prompt, [ENV_FEEDBACK_FILE]: feedbackFilePath }; logger_default.debug("Launching Electron GUI", { electronGuiPath, feedbackFilePath }); const result = await launchElectronGui(electronGuiPath, env); if (result.status === "error" /* ERROR */) { logger_default.error("Error in Electron GUI subprocess", { error: result.error }); return createErrorResponse( result.error || "Unknown error", result.status ); } logger_default.debug("Reading feedback from file", { feedbackFilePath }); const response = await readFeedbackFromFile(feedbackFilePath); logger_default.info("User feedback received", { status: response.status, feedbackLength: response.feedback.length }); return response; } catch (error) { logger_default.error("Error in getUserFeedback", { error: error.message || String(error) }); return createErrorResponse(error); } finally { await cleanupFeedbackFile(feedbackFilePath); } } function getElectronGuiPath() { try { const bundledPath = path3.join(__dirname, "electron", "main.js"); if ((0, import_fs.existsSync)(bundledPath)) { logger_default.debug(`Using bundled Electron GUI path: ${bundledPath}`); return bundledPath; } const resolvedPath = require.resolve("./electron/main.js"); if ((0, import_fs.existsSync)(resolvedPath)) { logger_default.debug(`Using resolved Electron GUI path: ${resolvedPath}`); return resolvedPath; } throw new Error("Could not find Electron GUI path"); } catch (error) { logger_default.error("Failed to resolve Electron GUI path", { error: error.message || String(error) }); throw new Error( `Failed to resolve Electron GUI path: ${error.message || String(error)}` ); } } async function launchElectronGui(electronGuiPath, env) { return new Promise((resolve) => { let electronPath = "electron"; try { electronPath = require.resolve("electron/cli.js"); logger_default.debug(`Using local Electron executable: ${electronPath}`); } catch (error) { logger_default.debug( `Could not find local Electron executable, using global: ${error}` ); } const electronProcess = (0, import_child_process.spawn)(electronPath, [electronGuiPath], { env }); electronProcess.on("exit", (code) => { if (code === 0) { resolve({ status: "success" /* SUCCESS */ }); } else { resolve({ status: "error" /* ERROR */, error: `Electron process exited with code ${code}` }); } }); electronProcess.on("error", (error) => { resolve({ status: "error" /* ERROR */, error: `Failed to start Electron process: ${error.message}` }); }); electronProcess.stdout.on("data", (data) => { logger_default.debug(`Electron stdout: ${data}`); }); electronProcess.stderr.on("data", (data) => { logger_default.error(`Electron stderr: ${data}`); }); }); } var import_child_process, path3, import_fs; var init_user_feedback = __esm({ "../../packages/mcp-server/src/handlers/user-feedback.ts"() { "use strict"; import_child_process = require("child_process"); path3 = __toESM(require("path")); init_src(); init_logger(); import_fs = require("fs"); } }); // ../../packages/mcp-server/src/tools/user-feedback.ts function registerUserFeedbackTool(server) { server.tool( "get_user_feedback", "Tool for getting feedback from the user. Opens a GUI window with a prompt and text input field where the user can provide feedback.", { prompt: import_zod.z.string().describe("The prompt to display to the user") }, async (params) => { const userFeedbackParams = { prompt: params.prompt }; const result = await getUserFeedback(userFeedbackParams); const content = [ { type: "text", text: JSON.stringify(result) } ]; if (result.images && result.images.length > 0) { content.push({ type: "text", text: ` The user included ${result.images.length} image(s) in their feedback.` }); } return { content }; } ); server.tool( "ask_user", "Tool for asking the user a question and getting their response. Opens a GUI window with a question prompt and text input field.", { prompt: import_zod.z.string().describe("The question to ask the user") }, async (params) => { const userFeedbackParams = { prompt: params.prompt }; const result = await getUserFeedback(userFeedbackParams); const content = [ { type: "text", text: JSON.stringify(result) } ]; if (result.images && result.images.length > 0) { content.push({ type: "text", text: ` The user included ${result.images.length} image(s) in their feedback.` }); } return { content }; } ); if (process.env[ENV_ENABLE_KNOWLEDGE_DB] === "true") { server.tool( "universal_knowledge_database", "Tool for querying the universal knowledge database. Provides access to a vast repository of information across various domains.", { prompt: import_zod.z.string().describe("The query to send to the knowledge database") }, async (params) => { const userFeedbackParams = { prompt: params.prompt }; const result = await getUserFeedback(userFeedbackParams); const content = [ { type: "text", text: JSON.stringify(result) } ]; if (result.images && result.images.length > 0) { content.push({ type: "text", text: ` The user included ${result.images.length} image(s) in their feedback.` }); } return { content }; } ); } if (process.env[ENV_ENABLE_UI_TESTER] === "true") { server.tool( "ui_tester", "Tool for testing UI components and interactions. Allows for automated testing of user interfaces and reporting results.", { prompt: import_zod.z.string().describe("The UI test to perform") }, async (params) => { const userFeedbackParams = { prompt: params.prompt }; const result = await getUserFeedback(userFeedbackParams); const content = [ { type: "text", text: JSON.stringify(result) } ]; if (result.images && result.images.length > 0) { content.push({ type: "text", text: ` The user included ${result.images.length} image(s) in their feedback.` }); } return { content }; } ); } } var import_zod; var init_user_feedback2 = __esm({ "../../packages/mcp-server/src/tools/user-feedback.ts"() { "use strict"; import_zod = require("zod"); init_user_feedback(); init_src(); } }); // ../../packages/mcp-server/src/tools/index.ts var init_tools = __esm({ "../../packages/mcp-server/src/tools/index.ts"() { "use strict"; init_user_feedback2(); } }); // ../../packages/mcp-server/src/index.ts var src_exports = {}; __export(src_exports, { registerUserFeedbackTool: () => registerUserFeedbackTool, startMCPServer: () => startMCPServer, stopMCPServer: () => stopMCPServer }); async function startMCPServer() { if (isServerRunning) { logger_default.warn( "MCP server is already running, ignoring duplicate start request" ); return; } try { logger_default.info("Starting MCP server"); isServerRunning = true; serverInstance = new import_mcp.McpServer({ name: "User Feedback MCP", version: "1.0.0" }); registerUserFeedbackTool(serverInstance); const transport = new import_stdio.StdioServerTransport(); logger_default.info("Connecting to stdio transport"); await serverInstance.connect(transport); transport.onclose = () => { logger_default.info("Transport closed, resetting server state"); isServerRunning = false; serverInstance = null; }; logger_default.info("MCP server started successfully"); } catch (error) { logger_default.error("Failed to start MCP server", { error: error.message || String(error) }); isServerRunning = false; serverInstance = null; process.exit(1); } } async function stopMCPServer() { if (isServerRunning && serverInstance) { logger_default.info("Stopping MCP server"); isServerRunning = false; serverInstance = null; } } var import_mcp, import_stdio, isServerRunning, serverInstance; var init_src2 = __esm({ "../../packages/mcp-server/src/index.ts"() { "use strict"; import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js"); import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js"); init_tools(); init_logger(); init_tools(); isServerRunning = false; serverInstance = null; if (require.main === module) { startMCPServer(); } } }); // src/index.ts var import_commander = require("commander"); var { version } = require_package(); var safeConsole = { log: (..._args) => { }, info: (..._args) => { }, warn: (..._args) => { }, error: (..._args) => { } }; var program = new import_commander.Command(); program.name("user-feedback-mcp").description("Local User Feedback Interface for MCP Tool").version(version).option("-d, --debug", "Enable debug logging").option( "--enable-knowledge-db", "Enable the universal knowledge database tool" ).option("--enable-ui-tester", "Enable the UI tester tool"); async function main() { try { const options = program.opts(); if (options.debug) { process.env.DEBUG = "true"; safeConsole.log("Debug mode enabled"); } if (options.enableKnowledgeDb) { process.env.ENABLE_KNOWLEDGE_DB = "true"; safeConsole.log("Universal knowledge database tool enabled"); } if (options.enableUiTester) { process.env.ENABLE_UI_TESTER = "true"; safeConsole.log("UI tester tool enabled"); } safeConsole.log("Starting MCP server..."); const { startMCPServer: startMCPServer2 } = await Promise.resolve().then(() => (init_src2(), src_exports)); await startMCPServer2(); } catch (error) { safeConsole.error("Failed to start MCP server:", error); process.exit(1); } } program.parse(); main();