# Directory Structure ``` ├── .gitignore ├── .npmignore ├── .swcrc ├── build.mjs ├── CURSOR_INTEGRATION.md ├── cursor-mcp-bridge.js ├── cursor-setup.md ├── dist │ └── index.js ├── package-lock.json ├── package.json ├── README.md ├── rollup.config.mjs ├── src │ ├── index.ts │ ├── interfaces │ │ ├── http.ts │ │ ├── imageGeneration.ts │ │ ├── index.ts │ │ └── jsonRpc.ts │ └── services │ ├── defaultParams.ts │ ├── drawThingsService.ts │ └── schemas.ts ├── start-cursor-bridge.sh ├── test-api-connection.js ├── test-mcp.js └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` .git .gitignore .DS_Store node_modules *.log .vscode .idea .env *.swp *.swo ``` -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- ``` { "$schema": "https://json.schemastore.org/swcrc", "jsc": { "parser": { "syntax": "typescript", "dynamicImport": true, "decorators": true }, "transform": { "legacyDecorator": true, "decoratorMetadata": true }, "target": "es2018", "loose": false }, "module": { "type": "es6" }, "sourceMaps": false, "minify": true } ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Logs logs _.log npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data pids _.pid _.seed \*.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage \*.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache \*.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' \*.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.\* # wrangler project .dev.vars .wrangler/ .specstory images/ .cursor test-output cursor-mcp-bridge.log cursor-mcp-bridge.log.old draw-things-mcp.log draw-things-mcp.log.old *.log ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Draw Things MCP Draw Things API integration for Cursor using Model Context Protocol (MCP). ## Prerequisites - Node.js >= 14.0.0 - Draw Things API running on http://127.0.0.1:7888 ## Installation ```bash # Install globally npm install -g draw-things-mcp-cursor # Or run directly npx draw-things-mcp-cursor ``` ## Cursor Integration To set up this tool in Cursor, see the detailed guide in [cursor-setup.md](./cursor-setup.md). Quick setup: 1. Create or edit `~/.cursor/claude_desktop_config.json`: ```json { "mcpServers": { "draw-things": { "command": "draw-things-mcp-cursor", "args": [] } } } ``` 2. Restart Cursor 3. Use in Cursor: `generateImage({"prompt": "a cute cat"})` ## CLI Usage ### Generate Image ```bash echo '{"prompt": "your prompt here"}' | npx draw-things-mcp-cursor ``` ### Parameters - `prompt`: The text prompt for image generation (required) - `negative_prompt`: The negative prompt for image generation - `width`: Image width (default: 360) - `height`: Image height (default: 360) - `steps`: Number of steps for generation (default: 8) - `model`: Model to use for generation (default: "flux_1_schnell_q5p.ckpt") - `sampler`: Sampling method (default: "DPM++ 2M AYS") Example: ```bash echo '{ "prompt": "a happy smiling dog, professional photography", "negative_prompt": "ugly, deformed, blurry", "width": 360, "height": 360, "steps": 4 }' | npx draw-things-mcp-cursor ``` ### MCP Tool Integration When used as an MCP tool in Cursor, the tool will be registered as `generateImage` with the following parameters: ```typescript { prompt: string; // Required - The prompt to generate the image from negative_prompt?: string; // Optional - The negative prompt width?: number; // Optional - Image width (default: 360) height?: number; // Optional - Image height (default: 360) model?: string; // Optional - Model name steps?: number; // Optional - Number of steps (default: 8) } ``` The generated images will be saved in the `images` directory with a filename format of: `<sanitized_prompt>_<timestamp>.png` ## Response Format Success: ```json { "type": "success", "content": [{ "type": "image", "data": "base64 encoded image data", "mimeType": "image/png" }], "metadata": { "parameters": { ... } } } ``` Error: ```json { "type": "error", "error": "error message", "code": 500 } ``` ## Troubleshooting If you encounter issues: - Ensure Draw Things API is running at http://127.0.0.1:7888 - Check log files in `~/.cursor/logs` if using with Cursor - Make sure src/index.js has execution permissions: `chmod +x src/index.js` ## License MIT ``` -------------------------------------------------------------------------------- /src/interfaces/jsonRpc.ts: -------------------------------------------------------------------------------- ```typescript /** * JSON-RPC 2.0 相關的介面定義 */ /** * JSON-RPC 2.0 請求格式 */ export interface JsonRpcRequest { jsonrpc: string; id: string; method: string; params?: { tool: string; parameters: any; }; prompt?: string; parameters?: any; } /** * JSON-RPC 2.0 回應格式 */ export interface JsonRpcResponse { jsonrpc: string; id: string; result?: any; error?: { code: number; message: string; data?: any; }; } ``` -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- ``` import { nodeResolve } from '@rollup/plugin-node-resolve'; export default { input: 'dist-temp/index.js', // SWC translated entry file output: { file: 'dist/index.js', format: 'es', // keep ES Module format sourcemap: false, }, external: [ // external dependencies, not packaged into the final file /@modelcontextprotocol\/.*/, 'axios', 'zod', 'path', 'fs', 'os', 'url', 'util', 'node:fs', 'node:path', 'node:os', 'node:url', 'node:util' ], plugins: [ nodeResolve({ exportConditions: ['node'], preferBuiltins: true, }), ] }; ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2018", "module": "nodenext", "moduleResolution": "nodenext", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "outDir": "dist", "sourceMap": false, "rootDir": "src", "declaration": false, "resolveJsonModule": true, "lib": ["ES2018"], "types": ["node", "jsdom"], "moduleSuffixes": ["js", "ts"], "baseUrl": ".", "paths": { "@/*": ["src/*"] }, "preserveSymlinks": true, "allowJs": true, "removeComments": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /build.mjs: -------------------------------------------------------------------------------- ``` #!/usr/bin/env node import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; // create temporary directory const tempDir = 'dist-temp'; if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir); } try { // first step: use SWC to translate TypeScript to JavaScript (keep the original build command logic) console.log('step 1: use SWC to translate TypeScript to JavaScript...'); execSync(`rimraf ${tempDir} && swc src -d ${tempDir} --strip-leading-paths`, { stdio: 'inherit' }); // second step: use Rollup to package as a single file console.log('step 2: use Rollup to package as a single file...'); execSync('rimraf dist && rollup -c', { stdio: 'inherit' }); // third step: ensure dist/index.js has execution permission (because it is a bin file) console.log('step 3: set execution permission...'); fs.chmodSync('dist/index.js', '755'); // fourth step: clean temporary directory console.log('step 4: clean temporary directory...'); execSync(`rimraf ${tempDir}`, { stdio: 'inherit' }); console.log('build completed! output: dist/index.js'); } catch (error) { console.error('error occurred during the build process:', error); process.exit(1); } ``` -------------------------------------------------------------------------------- /src/interfaces/imageGeneration.ts: -------------------------------------------------------------------------------- ```typescript /** * images generation interfaces */ /** * image response format */ export interface ImageResponse { content: Array<{ base64: string; path: string; prompt: string; negative_prompt?: string; seed: number; width: number; height: number; meta: Record<string, any>; }>; imageSavedPath?: string; // optional property, for storing image file path } /** * image generation parameters */ export interface ImageGenerationParameters { prompt?: string; negative_prompt?: string; seed?: number; width?: number; height?: number; num_inference_steps?: number; guidance_scale?: number; model?: string; random_string?: string; [key: string]: any; } /** * image generation result */ export interface ImageGenerationResult { status?: number; // changed to optional error?: string; images?: string[]; imageData?: string; isError?: boolean; errorMessage?: string; } /** * Draw Things service generation result */ export interface DrawThingsGenerationResult { isError: boolean; imageData?: string; errorMessage?: string; parameters?: Record<string, any>; status?: number; // added property to compatible with ImageGenerationResult images?: string[]; // added property to compatible with ImageGenerationResult error?: string; // added property to compatible with ImageGenerationResult imagePath?: string; // added property to store the path of the generated image metadata?: { alt: string; inference_time_ms: number; }; // added metadata } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "draw-things-mcp-cursor", "version": "1.4.3", "description": "Draw Things API integration for Cursor using Model Context Protocol (MCP)", "private": false, "type": "module", "main": "dist/index.js", "bin": { "draw-things-mcp-cursor": "./dist/index.js" }, "scripts": { "start": "node --experimental-vm-modules --no-warnings dist/index.js", "dev": "NODE_OPTIONS='--loader ts-node/esm' ts-node src/index.ts", "build": "node build.mjs", "test": "node --experimental-vm-modules --no-warnings test-mcp.js", "prepare": "npm run build", "prepublishOnly": "npm run build", "typecheck": "tsc --noEmit" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.7.0", "axios": "^1.8.0", "zod": "^3.24.2" }, "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.0", "@swc/cli": "^0.3.8", "@swc/core": "^1.4.8", "@swc/helpers": "^0.5.6", "@types/node": "^20.17.19", "rimraf": "^5.0.10", "rollup": "^4.34.9", "ts-node": "^10.9.2", "typescript": "^5.3.3", "zod-to-json-schema": "3.20.3" }, "overrides": { "zod-to-json-schema": "3.20.3" }, "resolutions": { "zod-to-json-schema": "3.20.3" }, "files": [ "dist", "README.md", "cursor-setup.md" ], "keywords": [ "cursor", "mcp", "draw-things", "ai", "image-generation", "stable-diffusion" ], "author": "jaokuohsuan", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/jaokuohsuan/draw-things-mcp" }, "bugs": { "url": "https://github.com/jaokuohsuan/draw-things-mcp/issues" }, "homepage": "https://github.com/jaokuohsuan/draw-things-mcp#readme", "engines": { "node": ">=16.0.0" }, "packageManager": "[email protected]+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab" } ``` -------------------------------------------------------------------------------- /src/services/defaultParams.ts: -------------------------------------------------------------------------------- ```typescript import { ImageGenerationParams } from './schemas.js'; // Default parameters for image generation export const defaultParams: ImageGenerationParams = { speed_up_with_guidance_embed: true, motion_scale: 127, image_guidance: 1.5, tiled_decoding: false, decoding_tile_height: 640, negative_prompt_for_image_prior: true, batch_size: 1, decoding_tile_overlap: 128, separate_open_clip_g: false, hires_fix_height: 960, decoding_tile_width: 640, diffusion_tile_height: 1024, num_frames: 14, stage_2_guidance: 1, t5_text_encoder_decoding: true, mask_blur_outset: 0, resolution_dependent_shift: true, model: "flux_1_schnell_q5p.ckpt", hires_fix: false, strength: 1, loras: [], diffusion_tile_width: 1024, diffusion_tile_overlap: 128, original_width: 512, seed: -1, zero_negative_prompt: false, upscaler_scale: 0, steps: 8, upscaler: null, mask_blur: 1.5, sampler: "DPM++ 2M AYS", width: 320, negative_original_width: 512, batch_count: 1, refiner_model: null, shift: 1, stage_2_shift: 1, open_clip_g_text: null, crop_left: 0, controls: [], start_frame_guidance: 1, original_height: 512, image_prior_steps: 5, guiding_frame_noise: 0.019999999552965164, clip_weight: 1, clip_skip: 1, crop_top: 0, negative_original_height: 512, preserve_original_after_inpaint: true, separate_clip_l: false, guidance_embed: 3.5, negative_aesthetic_score: 2.5, aesthetic_score: 6, clip_l_text: null, hires_fix_strength: 0.699999988079071, guidance_scale: 7.5, stochastic_sampling_gamma: 0.3, seed_mode: "Scale Alike", target_width: 512, hires_fix_width: 960, tiled_diffusion: false, fps: 5, refiner_start: 0.8500000238418579, height: 512, prompt: "A cute koala sitting on a eucalyptus tree, watercolor style, beautiful lighting, detailed", negative_prompt: "deformed, distorted, unnatural pose, extra limbs, blurry, low quality, ugly, bad anatomy, poor details, mutated, text, watermark" }; ``` -------------------------------------------------------------------------------- /cursor-setup.md: -------------------------------------------------------------------------------- ```markdown # Cursor MCP Tool Setup Guide ## Setting Up Draw Things MCP Tool in Cursor This guide will help you set up and use the Draw Things MCP tool in Cursor editor, allowing you to generate AI images directly within Cursor. ### Prerequisites - Ensure Draw Things API is running (http://127.0.0.1:7888) - Node.js v14.0.0 or higher ### 1. Install the Package #### Local Development Mode If you're developing or modifying this tool, you can use local linking: ```bash # In the project directory npm link ``` #### Publishing to NPM (if needed) If you want to publish this tool for others to use: ```bash npm publish ``` Then install globally via npm: ```bash npm install -g draw-things-mcp-cursor ``` ### 2. Create Cursor MCP Configuration File You need to create or edit the `~/.cursor/claude_desktop_config.json` file to register the MCP tool with Cursor: ```json { "mcpServers": { "draw-things": { "command": "draw-things-mcp-cursor", "args": [] } } } ``` #### Local Development Configuration If you're developing locally, it's recommended to use an absolute path to your JS file: ```json { "mcpServers": { "draw-things": { "command": "node", "args": [ "/Users/james_jao/m800/my-mcp/src/index.js" ] } } } ``` ### 3. Restart Cursor After configuration, completely close and restart the Cursor editor to ensure the new MCP configuration is properly loaded. ### 4. Using the MCP Tool In Cursor, you can call the image generation tool when chatting with the AI assistant using the following format: #### Basic Usage ``` generateImage({"prompt": "a cute cat"}) ``` #### Advanced Usage You can specify additional parameters to fine-tune the generated image: ``` generateImage({ "prompt": "a cute cat", "negative_prompt": "ugly, deformed", "width": 512, "height": 512, "steps": 4, "model": "flux_1_schnell_q5p.ckpt" }) ``` ### Available Parameters | Parameter Name | Description | Default Value | |----------------|-------------|---------------| | prompt | The image generation prompt | (Required) | | negative_prompt | Elements to avoid in the image | (Empty) | | width | Image width (pixels) | 360 | | height | Image height (pixels) | 360 | | steps | Number of generation steps (higher is more detailed but slower) | 8 | | model | Model name to use | "flux_1_schnell_q5p.ckpt" | ### Troubleshooting If you encounter issues when setting up or using the MCP tool, check: - Log files in the `~/.cursor/logs` directory for detailed error information - Ensure Draw Things API is started and running at http://127.0.0.1:7888 - Make sure the src/index.js file has execution permissions: `chmod +x src/index.js` - Check for error messages in the terminal: `draw-things-mcp-cursor` ### Getting Help If you have any questions, please open an issue on the project's GitHub page: https://github.com/james-jao/draw-things-mcp/issues ``` -------------------------------------------------------------------------------- /src/services/schemas.ts: -------------------------------------------------------------------------------- ```typescript /** * image generation params interface */ // params interface export interface ImageGenerationParams { // basic params prompt?: string; negative_prompt?: string; // size params width?: number; height?: number; // generate control params steps?: number; seed?: number; guidance_scale?: number; // model params model?: string; sampler?: string; // MCP special params random_string?: string; // allow other params [key: string]: any; } // function for validating ImageGenerationParams export function validateImageGenerationParams(params: any): { valid: boolean; errors: string[]; } { const errors: string[] = []; // check params type if (params.prompt !== undefined && typeof params.prompt !== "string") { errors.push("prompt must be a string"); } if ( params.negative_prompt !== undefined && typeof params.negative_prompt !== "string" ) { errors.push("negative_prompt must be a string"); } if ( params.width !== undefined && (typeof params.width !== "number" || params.width <= 0 || !Number.isInteger(params.width)) ) { errors.push("width must be a positive integer"); } if ( params.height !== undefined && (typeof params.height !== "number" || params.height <= 0 || !Number.isInteger(params.height)) ) { errors.push("height must be a positive integer"); } if ( params.steps !== undefined && (typeof params.steps !== "number" || params.steps <= 0 || !Number.isInteger(params.steps)) ) { errors.push("steps must be a positive integer"); } if ( params.seed !== undefined && (typeof params.seed !== "number" || !Number.isInteger(params.seed)) ) { errors.push("seed must be an integer"); } if ( params.guidance_scale !== undefined && (typeof params.guidance_scale !== "number" || params.guidance_scale <= 0) ) { errors.push("guidance_scale must be a positive number"); } if (params.model !== undefined && typeof params.model !== "string") { errors.push("model must be a string"); } if (params.sampler !== undefined && typeof params.sampler !== "string") { errors.push("sampler must be a string"); } if ( params.random_string !== undefined && typeof params.random_string !== "string" ) { errors.push("random_string must be a string"); } return { valid: errors.length === 0, errors }; } // response interface export interface ImageGenerationResult { status?: number; images?: string[]; parameters?: Record<string, any>; error?: string; } // success response interface export interface SuccessResponse { content: Array<{ type: "image"; data: string; mimeType: string; }>; } // error response interface export interface ErrorResponse { content: Array<{ type: "text"; text: string; }>; isError: true; } // MCP response interface export type McpResponse = SuccessResponse | ErrorResponse; ``` -------------------------------------------------------------------------------- /CURSOR_INTEGRATION.md: -------------------------------------------------------------------------------- ```markdown # Complete Guide for Using Draw Things MCP in Cursor This documentation provides steps for correctly using Draw Things MCP service to generate images in Cursor, following the [Model Context Protocol](https://modelcontextprotocol.io/docs/concepts/transports) specification. ## Background When directly using the `mcp_generateImage` tool in Cursor, the following issues occur: 1. Cursor sends plain text prompts instead of the correct JSON-RPC 2.0 format 2. MCP service cannot parse this input format, resulting in the error `Unexpected token A in JSON at position 0` 3. According to the [MCP documentation](https://docs.cursor.com/context/model-context-protocol), communication must use a specific JSON-RPC 2.0 format ## Solution: Bridge Service We provide a bridge service that automatically converts Cursor's plain text prompts to the correct JSON-RPC 2.0 format. ### Step 1: Environment Setup First, ensure you have installed these prerequisites: 1. Node.js and npm 2. Draw Things application 3. API enabled in Draw Things (port 7888) ### Step 2: Start the Bridge Service We provide a startup script that easily launches the bridge service: ```bash # Grant execution permission chmod +x start-cursor-bridge.sh # Basic usage ./start-cursor-bridge.sh # Use debug mode ./start-cursor-bridge.sh --debug # View help ./start-cursor-bridge.sh --help ``` This script will: 1. Check if the Draw Things API is available 2. Start the bridge service 3. Start the MCP service 4. Connect the two services so they can communicate with each other ### Step 3: Using in Cursor When the bridge service is running, the following two input methods are supported when using the `mcp_generateImage` tool in Cursor: 1. **Directly send English prompts** (the bridge service will automatically convert to JSON-RPC format): ``` A group of adorable kittens playing together, cute, fluffy, detailed fur, warm lighting, playful mood ``` 2. **Use JSON objects** (suitable when more custom parameters are needed): ```json { "prompt": "A group of adorable kittens playing together, cute, fluffy, detailed fur, warm lighting, playful mood", "negative_prompt": "blurry, distorted, low quality", "seed": 12345 } ``` ### Step 4: View Results Generated images will be saved in the `images` directory. You can also check the `cursor-mcp-bridge.log` file to understand the bridge service's operation. ## JSON-RPC 2.0 Format Explanation According to the MCP specification, the correct request format should be: ```json { "jsonrpc": "2.0", "id": "request-123", "method": "mcp.invoke", "params": { "tool": "generateImage", "parameters": { "prompt": "A group of adorable kittens playing together", "negative_prompt": "blurry, low quality", "seed": 12345 } } } ``` Our bridge service automatically converts simple inputs to this format. ## Custom Options You can modify default parameters by editing the `src/services/drawThings/defaultParams.js` file, such as: - Model selection - Image dimensions - Sampler type - Other generation parameters ## Troubleshooting ### Check Logs If you encounter problems, first check these logs: 1. `cursor-mcp-bridge.log` - Bridge service logs 2. `cursor-mcp-debug.log` - Detailed logs when debug mode is enabled 3. `error.log` - MCP service error logs ### Common Issues 1. **Connection Error**: Ensure the Draw Things application is running and API is enabled (127.0.0.1:7888). 2. **Parsing Error**: Check the format of prompts sent from Cursor. The bridge service should handle most cases, but complex JSON structures may cause issues. 3. **Service Not Started**: Make sure both the bridge service and MCP service are running. Please use the provided startup script, which will automatically handle both services. ## Technical Details How the bridge service works: 1. Receives plain text or JSON input from Cursor 2. Converts it to JSON-RPC 2.0 format compliant with MCP specifications 3. Passes the converted request to the MCP service 4. MCP service communicates with the Draw Things API 5. Receives the response and saves the generated image to the file system ### Transport Layer According to the MCP specification, our bridge service implements the following functions: - Uses stdin/stdout as the transport layer - Correctly handles JSON-RPC 2.0 request/response formats - Supports error handling and logging - Automatically saves generated images If you need more customization, you can edit the `cursor-mcp-bridge.js` file. ``` -------------------------------------------------------------------------------- /start-cursor-bridge.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Cursor MCP and Draw Things Bridge Service Startup Script # Ensure the program aborts on error set -e echo "========================================================" echo " Cursor MCP and Draw Things Bridge Service Tool " echo " Image Generation Service Compliant with Model Context Protocol " echo "========================================================" echo # Check dependencies command -v node >/dev/null 2>&1 || { echo "Error: Node.js is required but not installed"; exit 1; } command -v npm >/dev/null 2>&1 || { echo "Error: npm is required but not installed"; exit 1; } # Ensure script has execution permissions chmod +x cursor-mcp-bridge.js # Check if help information is needed if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then echo "Usage: ./start-cursor-bridge.sh [options]" echo echo "Options:" echo " --help, -h Display this help information" echo " --debug Enable additional debug output" echo " --no-cleanup Keep old log files" echo " --port PORT Specify custom port for Draw Things API (default: 7888)" echo echo "This script is used to start the Cursor MCP and Draw Things bridge service." echo "It will start a service that allows Cursor to generate images using plain text prompts." echo echo "Dependencies:" echo " - Node.js and npm" echo " - Draw Things application (must be running with API enabled)" echo exit 0 fi # Parse parameters DEBUG_MODE=false CLEANUP=true API_PORT=7888 for arg in "$@"; do case $arg in --debug) DEBUG_MODE=true shift ;; --no-cleanup) CLEANUP=false shift ;; --port=*) API_PORT="${arg#*=}" shift ;; esac done # Install dependencies echo "Checking and installing necessary dependencies..." npm install --quiet # Clean up old logs if [ "$CLEANUP" = true ]; then echo "Cleaning up old log files..." if [ -f cursor-mcp-bridge.log ]; then mv cursor-mcp-bridge.log cursor-mcp-bridge.log.old fi if [ -f draw-things-mcp.log ]; then mv draw-things-mcp.log draw-things-mcp.log.old fi fi # Ensure images directory exists mkdir -p images # Ensure logs directory exists mkdir -p logs echo echo "Step 1: Checking if Draw Things API is available..." # Create a simple test script to check API connection cat > test-api.js << EOL import http from 'http'; const options = { host: '127.0.0.1', port: ${API_PORT}, path: '/sdapi/v1/options', method: 'GET', timeout: 5000, headers: { 'User-Agent': 'DrawThingsMCP/1.0', 'Accept': 'application/json' } }; const req = http.request(options, (res) => { console.log('Draw Things API connection successful! Status code:', res.statusCode); process.exit(0); }); req.on('error', (e) => { if (e.code === 'ECONNREFUSED') { console.error('Error: Unable to connect to Draw Things API. Make sure Draw Things application is running and API is enabled.'); } else if (e.code === 'ETIMEDOUT') { console.error('Error: Connection to Draw Things API timed out. Make sure Draw Things application is running normally.'); } else { console.error('Error:', e.message); } process.exit(1); }); req.on('timeout', () => { console.error('Error: Connection to Draw Things API timed out. Make sure Draw Things application is running normally.'); req.destroy(); process.exit(1); }); req.end(); EOL # Run API test if node test-api.js; then echo "Draw Things API is available, continuing to start bridge service..." else echo echo "Warning: Draw Things API appears to be unavailable on port ${API_PORT}." echo "Please ensure:" echo "1. Draw Things application is running" echo "2. API is enabled in Draw Things settings" echo "3. API is listening on 127.0.0.1:${API_PORT}" echo read -p "Continue starting the bridge service anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo "Canceling bridge service startup." exit 1 fi fi # Clean up temporary files rm -f test-api.js echo echo "Step 2: Starting Services..." echo # Set up environment variables export DRAW_THINGS_FORCE_STAY_ALIVE=true export MCP_BRIDGE_DEDUP=true export DEBUG_MODE=$DEBUG_MODE export DRAW_THINGS_API_PORT=$API_PORT export DRAW_THINGS_API_URL="http://127.0.0.1:${API_PORT}" # Set up debug mode if [ "$DEBUG_MODE" = true ]; then echo "Debug mode enabled, all log output will be displayed" echo "Starting MCP bridge service in debug mode..." # Start both services in debug mode node cursor-mcp-bridge.js 2>&1 | tee -a cursor-mcp-debug.log | node src/index.js else # Start bridge service echo "Starting bridge service in normal mode..." echo "All logs will be saved to cursor-mcp-bridge.log and draw-things-mcp.log" # Start MCP bridge service and pipe output to MCP service node cursor-mcp-bridge.js | node src/index.js fi echo echo "Service has ended." echo "Log files:" echo " - cursor-mcp-bridge.log" echo " - draw-things-mcp.log" echo " - logs/error.log (if errors occurred)" echo "If generation was successful, images will be saved in the images directory." # Display service status if [ -f "images/image_$(date +%Y%m%d)*.png" ] || [ -f "images/generated-image_*.png" ]; then echo "Images were successfully generated today!" ls -la images/ | grep "$(date +%Y-%m-%d)" else echo "No images generated today were found. Please check the logs for more information." fi ``` -------------------------------------------------------------------------------- /src/services/drawThingsService.ts: -------------------------------------------------------------------------------- ```typescript import { defaultParams } from "./defaultParams.js"; import { ImageGenerationParams, validateImageGenerationParams, } from "./schemas.js"; import axios, { AxiosInstance } from "axios"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { DrawThingsGenerationResult } from "../../interfaces/index.js"; /** * simplified DrawThingsService * focus on core functionality: connect to Draw Things API and generate image */ export class DrawThingsService { // make baseUrl public for compatibility with index.ts public baseUrl: string; // change to public axios for compatibility public axios: AxiosInstance; constructor(apiUrl = "http://127.0.0.1:7888") { this.baseUrl = apiUrl; // initialize axios this.axios = axios.create({ baseURL: this.baseUrl, timeout: 300000, // 5 minutes timeout (image generation may take time) headers: { "Content-Type": "application/json", Accept: "application/json", }, }); // log initialization console.error( `DrawThingsService initialized, API location: ${this.baseUrl}` ); } /** * Set new base URL and update axios instance * @param url new base URL */ setBaseUrl(url: string): void { this.baseUrl = url; this.axios.defaults.baseURL = url; console.error(`Updated API base URL to: ${url}`); } /** * check API connection * simplified version that just checks if API is available */ async checkApiConnection(): Promise<boolean> { try { console.error(`Checking API connection to: ${this.baseUrl}`); // Try simple endpoint with short timeout const response = await this.axios.get("/sdapi/v1/options", { timeout: 5000, validateStatus: (status) => status >= 200, }); const isConnected = response.status >= 200; console.error( `API connection check: ${isConnected ? "Success" : "Failed"}` ); return isConnected; } catch (error) { console.error(`API connection check failed: ${(error as Error).message}`); return false; } } // Helper function to save images to the file system async saveImage({ base64Data, outputPath, fileName }: { base64Data: string; outputPath?: string; fileName?: string; }): Promise<string> { const __filename = fileURLToPath(import.meta.url); // Get directory name const __dirname = path.dirname(__filename); const projectRoot: string = path.resolve(__dirname, ".."); try { // if no output path provided, use default path const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const defaultFileName = fileName || `generated-image-${timestamp}.png`; const defaultImagesDir = path.resolve(projectRoot, "..", "images"); const finalOutputPath = outputPath || path.join(defaultImagesDir, defaultFileName); // ensure the images directory exists const imagesDir = path.dirname(finalOutputPath); if (!fs.existsSync(imagesDir)) { await fs.promises.mkdir(imagesDir, { recursive: true }); } const cleanBase64 = base64Data.replace(/^data:image\/\w+;base64,/, ""); const buffer = Buffer.from(cleanBase64, "base64"); const absolutePath = path.resolve(finalOutputPath); await fs.promises.writeFile(absolutePath, buffer); return absolutePath; } catch (error) { console.error( `Failed to save image: ${ error instanceof Error ? error.message : String(error) }` ); if (error instanceof Error) { console.error(error.stack || "No stack trace available"); } throw error; } } /** * get default params */ getDefaultParams(): ImageGenerationParams { return defaultParams; } /** * generate image * @param inputParams user provided params */ async generateImage( inputParams: Partial<ImageGenerationParams> = {} ): Promise<DrawThingsGenerationResult> { try { // handle input params let params: Partial<ImageGenerationParams> = {}; // validate params try { const validationResult = validateImageGenerationParams(inputParams); if (validationResult.valid) { params = inputParams; } else { console.error("parameter validation failed, use default params"); } } catch (error) { console.error("parameter validation error:", error); } // handle random_string special case if ( params.random_string && (!params.prompt || Object.keys(params).length === 1) ) { params.prompt = params.random_string; delete params.random_string; } // ensure prompt if (!params.prompt) { params.prompt = inputParams.prompt || defaultParams.prompt; } // merge params const requestParams = { ...defaultParams, ...params, seed: params.seed ?? Math.floor(Math.random() * 2147483647), }; console.error(`use prompt: "${requestParams.prompt}"`); // send request to Draw Things API console.error("send request to Draw Things API..."); const response = await this.axios.post( "/sdapi/v1/txt2img", requestParams ); // handle response if ( !response.data || !response.data.images || response.data.images.length === 0 ) { throw new Error("API did not return image data"); } // handle image data const imageData = response.data.images[0]; // format image data const formattedImageData = imageData.startsWith("data:image/") ? imageData : `data:image/png;base64,${imageData}`; console.error("image generation success"); // record the start time of image generation const startTime = Date.now() - 2000; // assume the image generation took 2 seconds const endTime = Date.now(); // automatically save the generated image const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const defaultFileName = `generated-image-${timestamp}.png`; // save the generated image const imagePath = await this.saveImage({ base64Data: formattedImageData, fileName: defaultFileName }); return { isError: false, imageData: formattedImageData, imagePath: imagePath, metadata: { alt: `Image generated from prompt: ${requestParams.prompt}`, inference_time_ms: endTime - startTime, } }; } catch (error) { console.error("image generation error:", error); // error message let errorMessage = "unknown error"; if (error instanceof Error) { errorMessage = error.message; } // handle axios error const axiosError = error as any; if (axiosError.response) { errorMessage = `API error: ${axiosError.response.status} - ${ axiosError.response.data?.error || axiosError.message }`; } else if (axiosError.code === "ECONNREFUSED") { errorMessage = "cannot connect to Draw Things API. please ensure Draw Things is running and API is enabled."; } else if (axiosError.code === "ETIMEDOUT") { errorMessage = "connection to Draw Things API timeout. image generation may take longer, or API not responding."; } return { isError: true, errorMessage, }; } } } ``` -------------------------------------------------------------------------------- /test-api-connection.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Draw Things API Connection Test * 測試不同方式連接到 Draw Things API */ import http from 'http'; import https from 'https'; import axios from 'axios'; import fs from 'fs'; // 記錄檔 const logFile = 'api-connection-test.log'; function log(message) { const timestamp = new Date().toISOString(); const logMessage = `${timestamp} - ${message}\n`; fs.appendFileSync(logFile, logMessage); console.log(message); } // 錯誤記錄 function logError(error, message = 'Error') { const timestamp = new Date().toISOString(); const errorDetails = error instanceof Error ? `${error.message}\n${error.stack}` : String(error); const logMessage = `${timestamp} - [ERROR] ${message}: ${errorDetails}\n`; fs.appendFileSync(logFile, logMessage); console.error(`${message}: ${error.message}`); } // 讀取參數 const apiPort = process.env.DRAW_THINGS_API_PORT || 7888; const apiProxyPort = process.env.PROXY_PORT || 7889; log('Draw Things API Connection Test'); log('==========================='); log(`Testing API on port ${apiPort}`); log(`Testing proxy on port ${apiProxyPort}`); log(''); // 測試 1: 直接使用 HTTP 模組連接 async function testHttpModule() { log('Test 1: 使用 Node.js HTTP 模組連接'); return new Promise((resolve) => { try { const urls = [ { name: '直接 API 連接 (127.0.0.1)', host: '127.0.0.1', port: apiPort }, { name: '直接 API 連接 (localhost)', host: 'localhost', port: apiPort }, { name: '代理伺服器連接 (127.0.0.1)', host: '127.0.0.1', port: apiProxyPort }, { name: '代理伺服器連接 (localhost)', host: 'localhost', port: apiProxyPort } ]; let completedTests = 0; const results = []; for (const url of urls) { log(`測試連接: ${url.name}`); const options = { hostname: url.host, port: url.port, path: '/sdapi/v1/options', method: 'GET', timeout: 5000, headers: { 'User-Agent': 'DrawThingsMCP/1.0', 'Accept': 'application/json' } }; const req = http.request(options, (res) => { log(`${url.name} 回應狀態碼: ${res.statusCode}`); let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { const success = res.statusCode >= 200 && res.statusCode < 300; results.push({ name: url.name, success, statusCode: res.statusCode, hasData: !!data }); completedTests++; if (completedTests === urls.length) { resolve(results); } }); }); req.on('error', (e) => { log(`${url.name} 錯誤: ${e.message}`); results.push({ name: url.name, success: false, error: e.message }); completedTests++; if (completedTests === urls.length) { resolve(results); } }); req.on('timeout', () => { log(`${url.name} 連接逾時`); req.destroy(); results.push({ name: url.name, success: false, error: 'Timeout' }); completedTests++; if (completedTests === urls.length) { resolve(results); } }); req.end(); } } catch (error) { logError(error, 'HTTP 模組測試發生錯誤'); resolve([]); } }); } // 測試 2: 使用 Axios 連接 async function testAxios() { log('Test 2: 使用 Axios 連接'); try { const urls = [ { name: '直接 API 連接 (127.0.0.1)', url: `http://127.0.0.1:${apiPort}/sdapi/v1/options` }, { name: '直接 API 連接 (localhost)', url: `http://localhost:${apiPort}/sdapi/v1/options` }, { name: '代理伺服器連接 (127.0.0.1)', url: `http://127.0.0.1:${apiProxyPort}/sdapi/v1/options` }, { name: '代理伺服器連接 (localhost)', url: `http://localhost:${apiProxyPort}/sdapi/v1/options` }, ]; const results = []; for (const url of urls) { log(`測試連接: ${url.name}`); try { const response = await axios.get(url.url, { timeout: 5000, headers: { 'User-Agent': 'DrawThingsMCP/1.0', 'Accept': 'application/json' } }); log(`${url.name} 回應狀態碼: ${response.status}`); results.push({ name: url.name, success: response.status >= 200 && response.status < 300, statusCode: response.status, hasData: !!response.data }); } catch (error) { log(`${url.name} 錯誤: ${error.message}`); results.push({ name: url.name, success: false, error: error.message }); } } return results; } catch (error) { logError(error, 'Axios 測試發生錯誤'); return []; } } // 測試 3: 嘗試不同的端點 async function testDifferentEndpoints() { log('Test 3: 測試不同的 API 端點'); try { // 使用工作正常的連接方式 (localhost 或 127.0.0.1) const baseUrl = `http://127.0.0.1:${apiPort}`; const endpoints = [ '/sdapi/v1/options', '/sdapi/v1/samplers', '/sdapi/v1/sd-models', '/sdapi/v1/prompt-styles', '/' ]; const results = []; for (const endpoint of endpoints) { log(`測試端點: ${endpoint}`); try { const response = await axios.get(`${baseUrl}${endpoint}`, { timeout: 5000, headers: { 'User-Agent': 'DrawThingsMCP/1.0', 'Accept': 'application/json' } }); log(`端點 ${endpoint} 回應狀態碼: ${response.status}`); results.push({ endpoint, success: response.status >= 200 && response.status < 300, statusCode: response.status, hasData: !!response.data }); } catch (error) { log(`端點 ${endpoint} 錯誤: ${error.message}`); results.push({ endpoint, success: false, error: error.message }); } } return results; } catch (error) { logError(error, '端點測試發生錯誤'); return []; } } // 執行測試 async function runTests() { try { // 測試 1: HTTP 模組 log('\n執行 HTTP 模組測試...'); const httpResults = await testHttpModule(); log('\nHTTP 模組測試結果:'); httpResults.forEach(result => { log(`${result.name}: ${result.success ? '成功' : '失敗'} ${result.statusCode ? `(狀態碼: ${result.statusCode})` : ''} ${result.error ? `(錯誤: ${result.error})` : ''}`); }); // 測試 2: Axios log('\n執行 Axios 測試...'); const axiosResults = await testAxios(); log('\nAxios 測試結果:'); axiosResults.forEach(result => { log(`${result.name}: ${result.success ? '成功' : '失敗'} ${result.statusCode ? `(狀態碼: ${result.statusCode})` : ''} ${result.error ? `(錯誤: ${result.error})` : ''}`); }); // 測試 3: 不同端點 log('\n執行不同端點測試...'); const endpointResults = await testDifferentEndpoints(); log('\n不同端點測試結果:'); endpointResults.forEach(result => { log(`端點 ${result.endpoint}: ${result.success ? '成功' : '失敗'} ${result.statusCode ? `(狀態碼: ${result.statusCode})` : ''} ${result.error ? `(錯誤: ${result.error})` : ''}`); }); // 總結 const httpSuccess = httpResults.some(r => r.success); const axiosSuccess = axiosResults.some(r => r.success); const endpointSuccess = endpointResults.some(r => r.success); log('\n=== 測試總結 ==='); log(`HTTP 模組連接測試: ${httpSuccess ? '至少有一個成功' : '全部失敗'}`); log(`Axios 連接測試: ${axiosSuccess ? '至少有一個成功' : '全部失敗'}`); log(`端點測試: ${endpointSuccess ? '至少有一個成功' : '全部失敗'}`); if (httpSuccess || axiosSuccess) { log('\nAPI 連接測試成功! 您的 Draw Things API 似乎可以正常工作。'); // 建議最佳連接方式 const bestConnection = [...httpResults, ...axiosResults].find(r => r.success); if (bestConnection) { log(`建議使用連接方式: ${bestConnection.name}`); } } else { log('\nAPI 連接測試失敗! 請確認:'); log('1. Draw Things 應用程式正在運行'); log('2. Draw Things 已啟用 API 功能'); log('3. API 在設定的端口上運行 (默認 7888)'); log('4. 沒有防火牆阻擋連接'); } } catch (error) { logError(error, '測試執行過程中發生錯誤'); } } // 執行測試 runTests().catch(error => { logError(error, '測試主程序發生錯誤'); }); ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node /** * Draw Things MCP - A Model Context Protocol implementation for Draw Things API * Integrated with Cursor MCP Bridge functionality for multiple input formats * * NOTE: Requires Node.js version 14+ for optional chaining support in dependencies */ import path from "path"; import fs from "fs"; import { fileURLToPath } from "url"; import { z } from "zod"; // MCP SDK imports import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; // Local service imports import { DrawThingsService } from "./services/drawThingsService.js"; import { ImageGenerationParameters, ImageGenerationResult, } from "./services/schemas.js"; // Constants and environment variables const DEBUG_MODE: boolean = process.env.DEBUG_MODE === "true"; // Get current file path in ESM const __filename = fileURLToPath(import.meta.url); // Get directory name const __dirname = path.dirname(__filename); const projectRoot: string = path.resolve(__dirname, ".."); const logsDir: string = path.join(projectRoot, "logs"); // Create logs directory if it doesn't exist try { if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true }); console.error(`Created logs directory: ${logsDir}`); } } catch (error) { console.error( `Failed to create logs directory: ${ error instanceof Error ? error.message : String(error) }` ); } const logFile: string = path.join(logsDir, "draw-things-mcp.log"); // Basic logging function function log(message: string): void { const timestamp = new Date().toISOString(); const logMessage = `${timestamp} - ${message}\n`; try { fs.appendFileSync(logFile, logMessage); // Only output to stderr to avoid polluting JSON-RPC communication console.error(logMessage); } catch (error) { console.error( `Failed to write to log file: ${ error instanceof Error ? error.message : String(error) }` ); } } // Enhanced error logging to dedicated error log file async function logError(error: Error | unknown): Promise<void> { try { const errorLogFile = path.join(logsDir, "error.log"); const timestamp = new Date().toISOString(); const errorDetails = error instanceof Error ? `${error.message}\n${error.stack}` : String(error); const errorLog = `${timestamp} - ERROR: ${errorDetails}\n\n`; try { await fs.promises.appendFile(errorLogFile, errorLog); log(`Error logged to ${errorLogFile}`); if (DEBUG_MODE) { console.error(`\n[DEBUG] FULL ERROR DETAILS:\n${errorDetails}\n`); } } catch (writeError) { // Fallback to sync writing try { fs.appendFileSync(errorLogFile, errorLog); } catch (syncWriteError) { console.error( `Failed to write to error log: ${ syncWriteError instanceof Error ? syncWriteError.message : String(syncWriteError) }` ); console.error(`Original error: ${errorDetails}`); } } } catch (logError) { console.error("Critical error in error logging system:"); console.error(logError); console.error("Original error:"); console.error(error); } } // Print connection information and help message on startup function printConnectionInfo(): void { // Only print to stderr to avoid polluting JSON-RPC communication const infoText = ` --------------------------------------------- | Draw Things MCP - Image Generation Service | --------------------------------------------- Attempting to connect to Draw Things API at: http://127.0.0.1:7888 TROUBLESHOOTING TIPS: 1. Ensure Draw Things is running on your computer 2. Make sure the API is enabled in Draw Things settings 3. If you changed the default port in Draw Things, set the environment variable: DRAW_THINGS_API_URL=http://127.0.0.1:YOUR_PORT Starting service... `; // Log to file and stderr log(infoText); } const drawThingsService = new DrawThingsService(); const server = new McpServer({ name: "draw-things-mcp", version: "1.0.0", }); // Define the image generation tool schema const paramsSchema = { prompt: z.string().optional(), negative_prompt: z.string().optional(), width: z.number().optional(), height: z.number().optional(), steps: z.number().optional(), seed: z.number().optional(), guidance_scale: z.number().optional(), random_string: z.string().optional(), }; server.tool( "generateImage", "Generate an image based on a prompt", paramsSchema, async (mcpParams: any) => { try { log("Received image generation request"); log(`mcpParams====== ${JSON.stringify(mcpParams)}`); // handle ai prompts const parameters = mcpParams?.params?.arguments || mcpParams?.arguments || mcpParams || {}; if (parameters.prompt) { log(`Using provided prompt: ${parameters.prompt}`); } else { log("No prompt provided, using default"); parameters.prompt = "A cute dog"; } // Generate image const result: ImageGenerationResult = await drawThingsService.generateImage(parameters); // Handle generation result if (result.isError) { log(`Error generating image: ${result.errorMessage}`); throw new Error(result.errorMessage || "Unknown error"); } if (!result.imageData && (!result.images || result.images.length === 0)) { log("No image data returned from generation"); throw new Error("No image data returned from generation"); } const imageData = result.imageData || (result.images && result.images.length > 0 ? result.images[0] : undefined); if (!imageData) { log("No valid image data available"); throw new Error("No valid image data available"); } log("Successfully generated image, returning directly via MCP"); // calculate the difference between the start and end time (example value) const startTime = Date.now() - 2000; // assume the image generation took 2 seconds const endTime = Date.now(); // build the response format const responseData = { image_paths: result.imagePath ? [result.imagePath] : [], metadata: { alt: `Image generated from prompt: ${parameters.prompt}`, inference_time_ms: result.metadata?.inference_time_ms || endTime - startTime, }, }; return { content: [ { type: "text", text: JSON.stringify(responseData, null, 2), }, ], }; } catch (error) { log( `Error handling image generation: ${ error instanceof Error ? error.message : String(error) }` ); await logError(error); throw error; } } ); // Main program async function main(): Promise<void> { try { log("Starting Draw Things MCP service..."); // Print connection info to the console printConnectionInfo(); log("Initializing Draw Things MCP service"); // Enhanced API connection verification with direct method log("Checking Draw Things API connection before starting service..."); const apiPort = process.env.DRAW_THINGS_API_PORT || 7888; // Final drawThingsService connection check const isApiConnected = await drawThingsService.checkApiConnection(); if (!isApiConnected) { log("\nFAILED TO CONNECT TO DRAW THINGS API"); log("Please make sure Draw Things is running and the API is enabled."); log( "The service will continue running, but image generation will not work until the API is available.\n" ); } else { log("\nSUCCESSFULLY CONNECTED TO DRAW THINGS API"); log("The service is ready to generate images.\n"); drawThingsService.setBaseUrl(`http://127.0.0.1:${apiPort}`); } // Create transport and connect server log("Creating transport and connecting server..."); const transport = new StdioServerTransport(); // Connect server to transport log("Connecting server to transport..."); await server.connect(transport); log("MCP Server started successfully!"); } catch (error) { log( `Error in main program: ${ error instanceof Error ? error.message : String(error) }` ); await logError(error); } } main().catch(async (error) => { log("server.log", `${new Date().toISOString()} - ${error.stack || error}\n`); console.error(error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /test-mcp.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Draw Things MCP Test Script * * This script is used to test whether the Draw Things MCP service can start normally and process image generation requests. * It simulates MCP client behavior, sending requests to the MCP service and handling responses. */ import { spawn } from 'child_process'; import { writeFile, mkdir } from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; // Get the directory path of the current file const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Ensure test output directory exists const testOutputDir = path.join(__dirname, 'test-output'); try { await mkdir(testOutputDir, { recursive: true }); console.log(`Test output directory created: ${testOutputDir}`); } catch (error) { if (error.code !== 'EEXIST') { console.error('Error creating test output directory:', error); process.exit(1); } } // MCP request example - format corrected to comply with MCP protocol const mcpRequest = { jsonrpc: "2.0", id: "test-request-" + Date.now(), method: "mcp.invoke", params: { tool: "generateImage", parameters: { prompt: "Beautiful Taiwan landscape, mountain and water painting style" } } }; // Save request to file const requestFilePath = path.join(testOutputDir, 'mcp-request.json'); try { await writeFile(requestFilePath, JSON.stringify(mcpRequest, null, 2)); console.log(`MCP request saved to: ${requestFilePath}`); } catch (error) { console.error('Error saving MCP request:', error); process.exit(1); } console.log('Starting Draw Things MCP service for testing...'); // Start MCP service process const mcpProcess = spawn('node', ['src/index.js'], { stdio: ['pipe', 'pipe', 'pipe'] }); // Add progress display timer let waitTime = 0; const progressInterval = setInterval(() => { waitTime += 30; console.log(`Waited ${waitTime} seconds... Image generation may take some time, please be patient`); }, 30000); // Cleanup function - called when terminating the service in any situation function cleanup() { clearInterval(progressInterval); if (mcpProcess && !mcpProcess.killed) { mcpProcess.kill(); } } // Record standard output let stdoutData = ''; mcpProcess.stdout.on('data', (data) => { const dataStr = data.toString(); console.log(`MCP standard output: ${dataStr}`); stdoutData += dataStr; try { // Try to parse output as JSON const lines = dataStr.trim().split('\n'); for (const line of lines) { if (!line.trim()) continue; try { const jsonData = JSON.parse(line); console.log('Parsed JSON response:', JSON.stringify(jsonData).substring(0, 100) + '...'); // If it's an MCP response, save and analyze if (jsonData.id && (jsonData.result || jsonData.error)) { console.log('Received MCP response ID:', jsonData.id); if (jsonData.error) { console.error('MCP error response:', jsonData.error); const errorFile = path.join(testOutputDir, 'mcp-error.json'); writeFile(errorFile, JSON.stringify(jsonData, null, 2)) .catch(e => console.error('Failed to write error file:', e)); } else if (jsonData.result) { console.log('MCP successful response type:', jsonData.result.content?.[0]?.type || 'unknown'); // Determine if the response contains an error if (jsonData.result.isError) { console.error('Error response:', jsonData.result.content[0].text); const errorResultFile = path.join(testOutputDir, 'mcp-error-result.json'); writeFile(errorResultFile, JSON.stringify(jsonData, null, 2)) .catch(e => console.error('Failed to write error result file:', e)); } else { // Successful response, should contain image data console.log('Successfully generated image!'); if (jsonData.result.content && jsonData.result.content[0].type === 'image') { const imageData = jsonData.result.content[0].data; console.log(`Image data size: ${imageData.length} characters`); // Use immediately executed async function (async function() { try { const savedImagePath = await saveImage(imageData); console.log(`Image successfully saved to: ${savedImagePath}`); const successFile = path.join(testOutputDir, 'mcp-success.json'); await writeFile(successFile, JSON.stringify(jsonData.result, null, 2)); console.log('Successfully saved result information to JSON file'); // Extend wait time to ensure all operations complete setTimeout(() => { console.log('Test completed, image processing successful, terminating MCP service...'); cleanup(); process.exit(0); }, 3000); // Increased to 3 seconds } catch (saveError) { console.error('Error saving image or results:', saveError); const errorFile = path.join(testOutputDir, 'mcp-save-error.json'); writeFile(errorFile, JSON.stringify({ error: saveError.message }, null, 2)) .catch(e => console.error('Failed to write error information:', e)); // End test normally even if there's an error setTimeout(() => { console.log('Test completed, but errors occurred during image processing, terminating MCP service...'); cleanup(); process.exit(1); }, 3000); } })(); } } } } } catch (parseError) { // Not valid JSON, might be regular log output // console.log('Non-JSON data:', line); } } } catch (error) { console.error('Error processing MCP output:', error); } }); // Record standard error mcpProcess.stderr.on('data', (data) => { const logMsg = data.toString().trim(); console.log(`MCP service log: ${logMsg}`); // Monitor specific log messages to confirm service status if (logMsg.includes('MCP service is ready')) { console.log('Detected MCP service is ready, preparing to send request...'); // Delay sending request setTimeout(() => { sendRequest(); }, 1000); } }); // Handle process exit mcpProcess.on('close', (code) => { if (code !== 0 && code !== null) { console.error(`MCP service exited with code ${code}`); // Save output for diagnosis try { writeFile(path.join(testOutputDir, 'mcp-stdout.log'), stdoutData); console.log('MCP service standard output log saved'); } catch (error) { console.error('Error saving output log:', error); } cleanup(); process.exit(1); } }); // Handle errors mcpProcess.on('error', (error) => { console.error('Error starting MCP service:', error); cleanup(); process.exit(1); }); // Function to send MCP request function sendRequest() { console.log('Sending image generation request...'); console.log('Request content:', JSON.stringify(mcpRequest)); // Ensure request string ends with newline const requestString = JSON.stringify(mcpRequest) + '\n'; mcpProcess.stdin.write(requestString); console.log(`Sent ${requestString.length} bytes of request data`); console.log('\n========================================'); console.log('Image generation has started, this may take a few minutes...'); console.log('Wait progress will be displayed every 30 seconds'); console.log('Please be patient, do not interrupt the test'); console.log('========================================\n'); // Save the raw request sent writeFile(path.join(testOutputDir, 'mcp-raw-request.txt'), requestString) .catch(e => console.error('Failed to save raw request:', e)); } // Helper function: Save image async function saveImage(base64Data) { try { // Create buffer from base64 string const imageBuffer = Buffer.from(base64Data, 'base64'); // Save image to file const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const imagePath = path.join(testOutputDir, `generated-image-${timestamp}.png`); await writeFile(imagePath, imageBuffer); console.log(`Generated image saved to: ${imagePath}`); return imagePath; // Make sure to return the saved path } catch (error) { console.error('Error saving image:', error); throw error; // Throw error so the upper function can catch and handle it } } // Don't send request immediately, wait for service log to indicate readiness // Timeout handling setTimeout(() => { console.error('Test timeout, terminating MCP service...'); writeFile(path.join(testOutputDir, 'mcp-timeout.log'), 'Test timed out after 300 seconds') .catch(e => console.error('Failed to save timeout log:', e)); cleanup(); process.exit(1); }, 300000); // 5 minute timeout, providing more time to complete image generation ``` -------------------------------------------------------------------------------- /cursor-mcp-bridge.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Cursor MCP Bridge - Connect Cursor MCP and Draw Things API * Converts simple text prompts to proper JSON-RPC requests */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import readline from 'readline'; import http from 'http'; // Set up log file const logFile = 'cursor-mcp-bridge.log'; function log(message) { const timestamp = new Date().toISOString(); const logMessage = `${timestamp} - ${message}\n`; fs.appendFileSync(logFile, logMessage); console.error(logMessage); // Also output to stderr for debugging } // Enhanced error logging function logError(error, message = 'Error') { const timestamp = new Date().toISOString(); const errorDetails = error instanceof Error ? `${error.message}\n${error.stack}` : String(error); const logMessage = `${timestamp} - [ERROR] ${message}: ${errorDetails}\n`; fs.appendFileSync(logFile, logMessage); console.error(logMessage); } // Initialize log log('Cursor MCP Bridge started'); log('Waiting for input...'); // Get current directory path const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Ensure images directory exists const imagesDir = path.join(__dirname, 'images'); if (!fs.existsSync(imagesDir)) { fs.mkdirSync(imagesDir, { recursive: true }); log(`Created image storage directory: ${imagesDir}`); } // Verify API connection - direct connection check async function verifyApiConnection() { // Read API port from environment or use default const apiPort = process.env.DRAW_THINGS_API_PORT || 7888; const apiUrl = process.env.DRAW_THINGS_API_URL || `http://127.0.0.1:${apiPort}`; log(`Verifying API connection to ${apiUrl}`); return new Promise((resolve) => { // Try multiple endpoints const endpoints = ['/sdapi/v1/options', '/sdapi/v1/samplers', '/']; let endpointIndex = 0; let retryCount = 0; const maxRetries = 2; const tryEndpoint = () => { if (endpointIndex >= endpoints.length) { log('All endpoints failed, API connection verification failed'); resolve(false); return; } const endpoint = endpoints[endpointIndex]; const url = new URL(endpoint, apiUrl); log(`Trying endpoint: ${url.toString()}`); const options = { hostname: url.hostname, port: url.port, path: url.pathname, method: 'GET', timeout: 5000, headers: { 'User-Agent': 'DrawThingsMCP/1.0', 'Accept': 'application/json' } }; const req = http.request(options, (res) => { log(`API connection check response: ${res.statusCode}`); // Any 2xx response is good if (res.statusCode >= 200 && res.statusCode < 300) { log('API connection verified successfully'); resolve(true); return; } // Try next endpoint endpointIndex++; retryCount = 0; tryEndpoint(); }); req.on('error', (e) => { log(`API connection error (${endpoint}): ${e.message}`); // Retry same endpoint a few times if (retryCount < maxRetries) { retryCount++; log(`Retrying ${endpoint} (attempt ${retryCount}/${maxRetries})...`); setTimeout(tryEndpoint, 1000); return; } // Move to next endpoint endpointIndex++; retryCount = 0; tryEndpoint(); }); req.on('timeout', () => { log(`API connection timeout (${endpoint})`); req.destroy(); // Retry same endpoint a few times if (retryCount < maxRetries) { retryCount++; log(`Retrying ${endpoint} after timeout (attempt ${retryCount}/${maxRetries})...`); setTimeout(tryEndpoint, 1000); return; } // Move to next endpoint endpointIndex++; retryCount = 0; tryEndpoint(); }); req.end(); }; tryEndpoint(); }); } // Initial API verification let apiVerified = false; verifyApiConnection().then(result => { apiVerified = result; if (result) { log('API connection verified on startup'); } else { log('API connection verification failed on startup - will retry on requests'); } }); // Set up readline interface const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); // Listen for line input rl.on('line', async (line) => { log(`Received input: ${line.substring(0, 100)}${line.length > 100 ? '...' : ''}`); // If API connection hasn't been verified yet, try again if (!apiVerified) { apiVerified = await verifyApiConnection(); if (!apiVerified) { log('API connection still not available'); // Return error response but continue processing the request // This allows the MCP service to handle the error properly } } // Check if input is already in JSON format try { const jsonInput = JSON.parse(line); log('Input is valid JSON, checking if it conforms to JSON-RPC 2.0 standard'); // Check if it conforms to JSON-RPC 2.0 standard if (jsonInput.jsonrpc === "2.0" && jsonInput.method && jsonInput.id) { log('Input already conforms to JSON-RPC 2.0 standard, forwarding directly'); process.stdout.write(line + '\n'); return; } else { log('JSON format is valid but does not conform to JSON-RPC 2.0 standard, converting'); processRequest(jsonInput); } return; } catch (e) { log(`Input is not valid JSON: ${e.message}`); } // Check if it's a plain text prompt from Cursor if (line && typeof line === 'string' && !line.startsWith('{')) { log('Detected plain text prompt, converting to JSON-RPC request'); // Create request conforming to JSON-RPC 2.0 standard const request = { jsonrpc: "2.0", id: Date.now().toString(), method: "mcp.invoke", params: { tool: "generateImage", parameters: { prompt: line.trim() } } }; processRequest(request); } else { log('Unrecognized input format, cannot process'); sendErrorResponse('Unrecognized input format', "parse_error", -32700); } }); // Process request function processRequest(request) { log(`Processing request: ${JSON.stringify(request).substring(0, 100)}...`); try { // Ensure request has the correct structure if (!request.jsonrpc) request.jsonrpc = "2.0"; if (!request.id) request.id = Date.now().toString(); // If no method, set to mcp.invoke if (!request.method) { request.method = "mcp.invoke"; } // Process params if (!request.params) { // Try to build params from different sources if (request.prompt || request.parameters) { request.params = { tool: "generateImage", parameters: request.prompt ? { prompt: request.prompt } : (request.parameters || {}) }; } else { // No usable parameters found log('No usable parameters found, using empty object'); request.params = { tool: "generateImage", parameters: {} }; } } else if (!request.params.tool) { // Ensure there's a tool parameter request.params.tool = "generateImage"; } // Ensure there are parameters if (!request.params.parameters) { request.params.parameters = {}; } // Add API verification status to the request for debugging if (!apiVerified) { log('Warning: Adding API status information to request'); request.params.parameters._apiVerified = apiVerified; } log(`Final request: ${JSON.stringify(request).substring(0, 150)}...`); process.stdout.write(JSON.stringify(request) + '\n'); } catch (error) { logError(error, 'Error processing request'); sendErrorResponse(`Error processing request: ${error.message}`, "internal_error", -32603); } } // Send error response conforming to JSON-RPC 2.0 function sendErrorResponse(message, errorType = "invalid_request", code = -32600) { const errorResponse = { jsonrpc: "2.0", id: "error-" + Date.now(), error: { code: code, message: errorType, data: message } }; log(`Sending error response: ${JSON.stringify(errorResponse)}`); process.stdout.write(JSON.stringify(errorResponse) + '\n'); } // Buffer for assembling complete JSON responses let responseBuffer = ''; // Handle responses from the MCP service process.stdin.on('data', (data) => { try { const dataStr = data.toString(); log(`Received data chunk from MCP service: ${dataStr.substring(0, 100)}${dataStr.length > 100 ? '...' : ''}`); // Append to buffer to handle chunked responses responseBuffer += dataStr; // Check if we have a complete JSON object by trying to find matching braces if (isCompleteJson(responseBuffer)) { log('Detected complete JSON response, processing'); processCompleteResponse(responseBuffer); responseBuffer = ''; // Clear buffer after processing } else { log('Incomplete JSON detected, buffering for more data'); } } catch (error) { logError(error, 'Error handling MCP service data'); // If there's an error, try to forward the original data as a fallback try { process.stdout.write(data); } catch (writeError) { logError(writeError, 'Error forwarding original data'); } } }); // Check if a string contains a complete JSON object function isCompleteJson(str) { try { JSON.parse(str); return true; } catch (e) { // Not complete or not valid JSON // Try basic brace matching as a fallback let openBraces = 0; let insideString = false; let escapeNext = false; for (let i = 0; i < str.length; i++) { const char = str[i]; if (escapeNext) { escapeNext = false; continue; } if (char === '\\' && insideString) { escapeNext = true; continue; } if (char === '"') { insideString = !insideString; continue; } if (!insideString) { if (char === '{') openBraces++; if (char === '}') openBraces--; } } // Complete JSON object should have matching braces return openBraces === 0 && str.trim().startsWith('{') && str.trim().endsWith('}'); } } // Process a complete response function processCompleteResponse(responseStr) { try { log(`Processing complete response: ${responseStr.substring(0, 100)}${responseStr.length > 100 ? '...' : ''}`); // Check for API error messages and update connection status if (responseStr.includes("Draw Things API is not running or cannot be connected")) { log('Detected API connection error in response'); // Trigger a new API verification verifyApiConnection().then(result => { apiVerified = result; log(`API verification after error message: ${apiVerified ? 'successful' : 'failed'}`); }); } // Try to parse as JSON const response = JSON.parse(responseStr); log('Successfully parsed MCP service response as JSON'); // Check if it's an image generation result if (response.result && response.result.content) { // Find image content const imageContent = response.result.content.find(item => item.type === 'image'); if (imageContent && imageContent.data) { // Save the image const timestamp = Date.now(); const imagePath = path.join(imagesDir, `image_${timestamp}.png`); // Remove data:image/png;base64, prefix const base64Data = imageContent.data.replace(/^data:image\/\w+;base64,/, ''); fs.writeFileSync(imagePath, Buffer.from(base64Data, 'base64')); log(`Image saved to: ${imagePath}`); // Add saved path info to the response response.result.imageSavedPath = imagePath; // Successful image generation indicates API is working apiVerified = true; } } // Forward the processed response process.stdout.write(JSON.stringify(response) + '\n'); } catch (error) { logError(error, 'Error processing complete response'); // Try to convert non-JSON response to proper JSON-RPC if (responseStr.trim() && !responseStr.trim().startsWith('{')) { log('Converting non-JSON response to proper JSON-RPC response'); // Create a JSON-RPC response with the text as content const jsonResponse = { jsonrpc: "2.0", id: "response-" + Date.now(), result: { content: [{ type: 'text', text: responseStr.trim() }] } }; process.stdout.write(JSON.stringify(jsonResponse) + '\n'); } else { // Forward original response as fallback log('Forwarding original response as fallback'); process.stdout.write(responseStr + '\n'); } } } // Handle end of input rl.on('close', () => { log('Input stream closed, program ending'); process.exit(0); }); // Handle errors process.on('uncaughtException', (error) => { logError(error, 'Uncaught exception'); sendErrorResponse(`Error processing request: ${error.message}`, "internal_error", -32603); }); log('Bridge service ready, waiting for Cursor input...'); // Periodically check API connection setInterval(async () => { const prevStatus = apiVerified; apiVerified = await verifyApiConnection(); if (prevStatus !== apiVerified) { log(`API connection status changed: ${prevStatus} -> ${apiVerified}`); } }, 60000); // Check every minute ```