From 48170b6e41483dd2bd89caa0873a44814e887341 Mon Sep 17 00:00:00 2001 From: momo5502 Date: Sun, 8 Jun 2025 13:51:45 +0200 Subject: [PATCH] Add experimental MCP server --- mcp/.gitignore | 41 ++++++ mcp/README.md | 118 +++++++++++++++++ mcp/package-lock.json | 151 +++++++++++++++++++++ mcp/package.json | 17 +++ mcp/server.js | 296 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 623 insertions(+) create mode 100644 mcp/.gitignore create mode 100644 mcp/README.md create mode 100644 mcp/package-lock.json create mode 100644 mcp/package.json create mode 100644 mcp/server.js diff --git a/mcp/.gitignore b/mcp/.gitignore new file mode 100644 index 00000000..1a24826e --- /dev/null +++ b/mcp/.gitignore @@ -0,0 +1,41 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Editor directories and files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 00000000..da194167 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,118 @@ +# Sogen MCP Server + +A Model Context Protocol (MCP) server that provides AI access to the sogen userspace emulator. + +## Setup + +1. Install dependencies: + ```bash + npm install + ``` + +2. Run the server: + ```bash + npm start + ``` + + Or for development with auto-restart: + ```bash + npm run dev + ``` + +## Structure + +- `server.js` - Main server implementation +- `package.json` - Node.js project configuration + +## Available Tools + +### `list_applications` +Lists all available applications in the sogen userspace emulator. + +**Parameters:** None + +**Example usage:** +```json +{ + "name": "list_applications", + "arguments": {} +} +``` + +### `run_application` +Executes a specific application from the allowed list in the sogen userspace emulator. + +**Parameters:** +- `application` (string, required): The name of the application to run (use `list_applications` to see available apps) +- `args` (array of strings, optional): Arguments to pass to the application +- `timeout` (number, optional): Timeout in milliseconds (default: 5000) + +**Example usage:** +```json +{ + "name": "run_application", + "arguments": { + "application": "echo", + "args": ["Hello from sogen!"], + "timeout": 3000 + } +} +``` + +## Adding More Tools + +To add additional tools: +1. Add the tool definition to the `ListToolsRequestSchema` handler +2. Add the implementation to the `CallToolRequestSchema` handler +3. Create the corresponding method in the `MyMCPServer` class + +## Execution Model + +The server uses an **analyzer-based execution model**: + +- Applications are not executed directly +- Instead, the server runs: `C:/analyer.exe -e C:/somedir [args...]` +- The analyzer handles the actual execution within the sogen environment +- All output comes from the analyzer process + +### Command Structure +``` +C:/analyer.exe -e C:/somedir [arguments...] +``` + +Where: +- `C:/analyer.exe` - The sogen analyzer executable +- `-e C:/somedir` - Analyzer flags and environment directory +- `` - The application from `get_applications()` +- `[arguments...]` - Optional arguments passed to the application + +## Implementation Required + +You need to provide the implementation for the `get_applications()` method in `server.js`. This method should: + +```javascript +async get_applications() { + // Return a Promise that resolves to a string array + // Example: return ['echo', 'ls', 'cat', 'grep']; +} +``` + +## Usage + +This server allows AI assistants to: +1. **List available applications** using `list_applications` +2. **Execute specific applications** using `run_application` with validation + +The server communicates over stdio and is designed for MCP-compatible clients. + +### Example Workflow +1. Call `list_applications` to see what's available +2. Call `run_application` with a valid application name and arguments + +## Next Steps + +1. **Implement `get_applications()` method** - Provide the actual implementation +2. Add application-specific argument validation +3. Implement resource handling for file access +4. Add comprehensive logging and monitoring +5. Consider per-application sandboxing for enhanced security diff --git a/mcp/package-lock.json b/mcp/package-lock.json new file mode 100644 index 00000000..e35dcb57 --- /dev/null +++ b/mcp/package-lock.json @@ -0,0 +1,151 @@ +{ + "name": "my-mcp-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "my-mcp-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^0.5.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.5.0.tgz", + "integrity": "sha512-RXgulUX6ewvxjAG0kOpLMEdXXWkzWgaoCGaA2CwNW7cQCIphjpJhjpHSiaPdVCnisjRF/0Cm9KWHUuIoeiAblQ==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/zod": { + "version": "3.25.56", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.56.tgz", + "integrity": "sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/mcp/package.json b/mcp/package.json new file mode 100644 index 00000000..afbdd4e9 --- /dev/null +++ b/mcp/package.json @@ -0,0 +1,17 @@ +{ + "name": "sogen-mcp-server", + "version": "1.0.0", + "description": "MCP server for sogen userspace emulator", + "main": "server.js", + "type": "module", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^0.5.0" + }, + "keywords": ["mcp", "server"], + "author": "", + "license": "MIT" +} diff --git a/mcp/server.js b/mcp/server.js new file mode 100644 index 00000000..2e473985 --- /dev/null +++ b/mcp/server.js @@ -0,0 +1,296 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { spawn } from "child_process"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +class SogenMCPServer { + constructor() { + this.server = new Server( + { + name: "sogen-mcp-server", + version: "1.0.0", + }, + { + capabilities: { + tools: {}, + }, + } + ); + + this.setupToolHandlers(); + this.setupErrorHandling(); + } + + setupToolHandlers() { + // Handle tool listing + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "list_applications", + description: + "List all available applications in the sogen userspace emulator", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, + }, + { + name: "run_application", + description: + "Execute a specific application in the sogen userspace emulator", + inputSchema: { + type: "object", + properties: { + application: { + type: "string", + description: + "The name of the application to run (use list_applications to see available apps)", + }, + args: { + type: "array", + items: { + type: "string", + }, + description: "Arguments to pass to the application", + default: [], + }, + timeout: { + type: "number", + description: "Timeout in milliseconds (default: 50000)", + default: 50000, + }, + }, + required: ["application"], + }, + }, + ], + }; + }); + + // Handle tool execution + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + switch (name) { + case "list_applications": + return await this.listApplications(); + + case "run_application": + return await this.runApplication( + args.application, + args.args || [], + args.timeout || 50000 + ); + + default: + throw new Error(`Unknown tool: ${name}`); + } + }); + } + + async listApplications() { + try { + const applications = await this.get_applications(); + + return { + content: [ + { + type: "text", + text: `Available applications in sogen:\n\n${applications + .map((app) => `• ${app}`) + .join("\n")}\n\nTotal: ${applications.length} application(s)`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error listing applications: ${error.message}`, + }, + ], + }; + } + } + + async runApplication(applicationName, args = [], timeout = 5000) { + try { + // First, get the list of available applications to validate + const availableApps = await this.get_applications(); + + if (!availableApps.includes(applicationName)) { + return { + content: [ + { + type: "text", + text: `Error: Application '${applicationName}' is not available.\n\nAvailable applications:\n${availableApps + .map((app) => `• ${app}`) + .join("\n")}`, + }, + ], + }; + } + + return await this.executeApplication(applicationName, args, timeout); + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error running application '${applicationName}': ${error.message}`, + }, + ], + }; + } + } + + async executeApplication(applicationName, args, timeout) { + return new Promise((resolve, reject) => { + // Get the parent directory (emulator root) + const emulatorRoot = path.dirname(__dirname); + + // Build the analyzer command: C:/analyer.exe -e C:/somedir + const analyzerPath = + "C:\\Users\\mauri\\Desktop\\emulator\\build\\vs2022\\artifacts-relwithdebinfo\\analyzer.exe"; + const analyzerArgs = [ + "-c", + "-e", + "C:\\Users\\mauri\\Desktop\\emulator\\src\\tools\\root", + applicationName, + ...args, + ]; + + const child = spawn(analyzerPath, analyzerArgs, { + cwd: "C:\\Users\\mauri\\Desktop\\emulator\\build\\vs2022\\artifacts-relwithdebinfo", + shell: true, + stdio: ["pipe", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + let timedOut = false; + + // Set up timeout + const timer = setTimeout(() => { + timedOut = true; + child.kill("SIGTERM"); + }, timeout); + + // Collect stdout + child.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + // Collect stderr + child.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + // Handle process completion + child.on("close", (code) => { + clearTimeout(timer); + + if (timedOut) { + resolve({ + content: [ + { + type: "text", + text: `Application '${applicationName}' timed out after ${timeout}ms\nAnalyzer command: ${analyzerPath} ${analyzerArgs.join( + " " + )}\nPartial stdout: ${stdout}\nPartial stderr: ${stderr}`, + }, + ], + }); + } else { + const output = []; + + if (stdout) { + output.push(`STDOUT:\n${stdout}`); + } + + if (stderr) { + output.push(`STDERR:\n${stderr}`); + } + + if (!stdout && !stderr) { + output.push("Application executed successfully with no output."); + } + + output.push(`Exit code: ${code}`); + + resolve({ + content: [ + { + type: "text", + text: `Application: ${applicationName}\nArguments: [${args.join( + ", " + )}]\nAnalyzer command: ${analyzerPath} ${analyzerArgs.join( + " " + )}\n\n${output.join("\n\n")}`, + }, + ], + }); + } + }); + + // Handle spawn errors + child.on("error", (error) => { + clearTimeout(timer); + resolve({ + content: [ + { + type: "text", + text: `Error executing application '${applicationName}' via analyzer: ${ + error.message + }\nAnalyzer command: ${analyzerPath} ${analyzerArgs.join(" ")}`, + }, + ], + }); + }); + }); + } + + async get_applications() { + return [ + "c:/test-sample.exe", + "c:/wukong/b1/Binaries/Win64/b1-Win64-Shipping.exe", + ]; + } + + setupErrorHandling() { + this.server.onerror = (error) => { + console.error("[MCP Error]", error); + }; + + process.on("SIGINT", async () => { + await this.server.close(); + process.exit(0); + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error("MCP Server running on stdio"); + } +} + +// Start the server +const server = new SogenMCPServer(); +server.run().catch((error) => { + console.error("Failed to start server:", error); + process.exit(1); +});