# 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:
--------------------------------------------------------------------------------
```
1 | .git
2 | .gitignore
3 | .DS_Store
4 | node_modules
5 | *.log
6 | .vscode
7 | .idea
8 | .env
9 | *.swp
10 | *.swo
```
--------------------------------------------------------------------------------
/.swcrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "$schema": "https://json.schemastore.org/swcrc",
3 | "jsc": {
4 | "parser": {
5 | "syntax": "typescript",
6 | "dynamicImport": true,
7 | "decorators": true
8 | },
9 | "transform": {
10 | "legacyDecorator": true,
11 | "decoratorMetadata": true
12 | },
13 | "target": "es2018",
14 | "loose": false
15 | },
16 | "module": {
17 | "type": "es6"
18 | },
19 | "sourceMaps": false,
20 | "minify": true
21 | }
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Logs
2 |
3 | logs
4 | _.log
5 | npm-debug.log_
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 |
13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14 |
15 | # Runtime data
16 |
17 | pids
18 | _.pid
19 | _.seed
20 | \*.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 |
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 |
28 | coverage
29 | \*.lcov
30 |
31 | # nyc test coverage
32 |
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 |
37 | .grunt
38 |
39 | # Bower dependency directory (https://bower.io/)
40 |
41 | bower_components
42 |
43 | # node-waf configuration
44 |
45 | .lock-wscript
46 |
47 | # Compiled binary addons (https://nodejs.org/api/addons.html)
48 |
49 | build/Release
50 |
51 | # Dependency directories
52 |
53 | node_modules/
54 | jspm_packages/
55 |
56 | # Snowpack dependency directory (https://snowpack.dev/)
57 |
58 | web_modules/
59 |
60 | # TypeScript cache
61 |
62 | \*.tsbuildinfo
63 |
64 | # Optional npm cache directory
65 |
66 | .npm
67 |
68 | # Optional eslint cache
69 |
70 | .eslintcache
71 |
72 | # Optional stylelint cache
73 |
74 | .stylelintcache
75 |
76 | # Microbundle cache
77 |
78 | .rpt2_cache/
79 | .rts2_cache_cjs/
80 | .rts2_cache_es/
81 | .rts2_cache_umd/
82 |
83 | # Optional REPL history
84 |
85 | .node_repl_history
86 |
87 | # Output of 'npm pack'
88 |
89 | \*.tgz
90 |
91 | # Yarn Integrity file
92 |
93 | .yarn-integrity
94 |
95 | # dotenv environment variable files
96 |
97 | .env
98 | .env.development.local
99 | .env.test.local
100 | .env.production.local
101 | .env.local
102 |
103 | # parcel-bundler cache (https://parceljs.org/)
104 |
105 | .cache
106 | .parcel-cache
107 |
108 | # Next.js build output
109 |
110 | .next
111 | out
112 |
113 | # Nuxt.js build / generate output
114 |
115 | .nuxt
116 |
117 | # Gatsby files
118 |
119 | .cache/
120 |
121 | # Comment in the public line in if your project uses Gatsby and not Next.js
122 |
123 | # https://nextjs.org/blog/next-9-1#public-directory-support
124 |
125 | # public
126 |
127 | # vuepress build output
128 |
129 | .vuepress/dist
130 |
131 | # vuepress v2.x temp and cache directory
132 |
133 | .temp
134 | .cache
135 |
136 | # Docusaurus cache and generated files
137 |
138 | .docusaurus
139 |
140 | # Serverless directories
141 |
142 | .serverless/
143 |
144 | # FuseBox cache
145 |
146 | .fusebox/
147 |
148 | # DynamoDB Local files
149 |
150 | .dynamodb/
151 |
152 | # TernJS port file
153 |
154 | .tern-port
155 |
156 | # Stores VSCode versions used for testing VSCode extensions
157 |
158 | .vscode-test
159 |
160 | # yarn v2
161 |
162 | .yarn/cache
163 | .yarn/unplugged
164 | .yarn/build-state.yml
165 | .yarn/install-state.gz
166 | .pnp.\*
167 |
168 | # wrangler project
169 |
170 | .dev.vars
171 | .wrangler/
172 | .specstory
173 | images/
174 | .cursor
175 | test-output
176 | cursor-mcp-bridge.log
177 | cursor-mcp-bridge.log.old
178 | draw-things-mcp.log
179 | draw-things-mcp.log.old
180 | *.log
181 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Draw Things MCP
2 |
3 | Draw Things API integration for Cursor using Model Context Protocol (MCP).
4 |
5 | ## Prerequisites
6 |
7 | - Node.js >= 14.0.0
8 | - Draw Things API running on http://127.0.0.1:7888
9 |
10 | ## Installation
11 |
12 | ```bash
13 | # Install globally
14 | npm install -g draw-things-mcp-cursor
15 |
16 | # Or run directly
17 | npx draw-things-mcp-cursor
18 | ```
19 |
20 | ## Cursor Integration
21 |
22 | To set up this tool in Cursor, see the detailed guide in [cursor-setup.md](./cursor-setup.md).
23 |
24 | Quick setup:
25 |
26 | 1. Create or edit `~/.cursor/claude_desktop_config.json`:
27 | ```json
28 | {
29 | "mcpServers": {
30 | "draw-things": {
31 | "command": "draw-things-mcp-cursor",
32 | "args": []
33 | }
34 | }
35 | }
36 | ```
37 |
38 | 2. Restart Cursor
39 | 3. Use in Cursor: `generateImage({"prompt": "a cute cat"})`
40 |
41 | ## CLI Usage
42 |
43 | ### Generate Image
44 |
45 | ```bash
46 | echo '{"prompt": "your prompt here"}' | npx draw-things-mcp-cursor
47 | ```
48 |
49 | ### Parameters
50 |
51 | - `prompt`: The text prompt for image generation (required)
52 | - `negative_prompt`: The negative prompt for image generation
53 | - `width`: Image width (default: 360)
54 | - `height`: Image height (default: 360)
55 | - `steps`: Number of steps for generation (default: 8)
56 | - `model`: Model to use for generation (default: "flux_1_schnell_q5p.ckpt")
57 | - `sampler`: Sampling method (default: "DPM++ 2M AYS")
58 |
59 | Example:
60 |
61 | ```bash
62 | echo '{
63 | "prompt": "a happy smiling dog, professional photography",
64 | "negative_prompt": "ugly, deformed, blurry",
65 | "width": 360,
66 | "height": 360,
67 | "steps": 4
68 | }' | npx draw-things-mcp-cursor
69 | ```
70 |
71 | ### MCP Tool Integration
72 |
73 | When used as an MCP tool in Cursor, the tool will be registered as `generateImage` with the following parameters:
74 |
75 | ```typescript
76 | {
77 | prompt: string; // Required - The prompt to generate the image from
78 | negative_prompt?: string; // Optional - The negative prompt
79 | width?: number; // Optional - Image width (default: 360)
80 | height?: number; // Optional - Image height (default: 360)
81 | model?: string; // Optional - Model name
82 | steps?: number; // Optional - Number of steps (default: 8)
83 | }
84 | ```
85 |
86 | The generated images will be saved in the `images` directory with a filename format of:
87 | `<sanitized_prompt>_<timestamp>.png`
88 |
89 | ## Response Format
90 |
91 | Success:
92 | ```json
93 | {
94 | "type": "success",
95 | "content": [{
96 | "type": "image",
97 | "data": "base64 encoded image data",
98 | "mimeType": "image/png"
99 | }],
100 | "metadata": {
101 | "parameters": { ... }
102 | }
103 | }
104 | ```
105 |
106 | Error:
107 | ```json
108 | {
109 | "type": "error",
110 | "error": "error message",
111 | "code": 500
112 | }
113 | ```
114 |
115 | ## Troubleshooting
116 |
117 | If you encounter issues:
118 |
119 | - Ensure Draw Things API is running at http://127.0.0.1:7888
120 | - Check log files in `~/.cursor/logs` if using with Cursor
121 | - Make sure src/index.js has execution permissions: `chmod +x src/index.js`
122 |
123 | ## License
124 |
125 | MIT
```
--------------------------------------------------------------------------------
/src/interfaces/jsonRpc.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * JSON-RPC 2.0 相關的介面定義
3 | */
4 |
5 | /**
6 | * JSON-RPC 2.0 請求格式
7 | */
8 | export interface JsonRpcRequest {
9 | jsonrpc: string;
10 | id: string;
11 | method: string;
12 | params?: {
13 | tool: string;
14 | parameters: any;
15 | };
16 | prompt?: string;
17 | parameters?: any;
18 | }
19 |
20 | /**
21 | * JSON-RPC 2.0 回應格式
22 | */
23 | export interface JsonRpcResponse {
24 | jsonrpc: string;
25 | id: string;
26 | result?: any;
27 | error?: {
28 | code: number;
29 | message: string;
30 | data?: any;
31 | };
32 | }
```
--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
```
1 | import { nodeResolve } from '@rollup/plugin-node-resolve';
2 |
3 | export default {
4 | input: 'dist-temp/index.js', // SWC translated entry file
5 | output: {
6 | file: 'dist/index.js',
7 | format: 'es', // keep ES Module format
8 | sourcemap: false,
9 | },
10 | external: [
11 | // external dependencies, not packaged into the final file
12 | /@modelcontextprotocol\/.*/,
13 | 'axios',
14 | 'zod',
15 | 'path',
16 | 'fs',
17 | 'os',
18 | 'url',
19 | 'util',
20 | 'node:fs',
21 | 'node:path',
22 | 'node:os',
23 | 'node:url',
24 | 'node:util'
25 | ],
26 | plugins: [
27 | nodeResolve({
28 | exportConditions: ['node'],
29 | preferBuiltins: true,
30 | }),
31 | ]
32 | };
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "nodenext",
5 | "moduleResolution": "nodenext",
6 | "esModuleInterop": true,
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "skipLibCheck": true,
11 | "outDir": "dist",
12 | "sourceMap": false,
13 | "rootDir": "src",
14 | "declaration": false,
15 | "resolveJsonModule": true,
16 | "lib": ["ES2018"],
17 | "types": ["node", "jsdom"],
18 | "moduleSuffixes": ["js", "ts"],
19 | "baseUrl": ".",
20 | "paths": {
21 | "@/*": ["src/*"]
22 | },
23 | "preserveSymlinks": true,
24 | "allowJs": true,
25 | "removeComments": true
26 |
27 | },
28 | "include": ["src/**/*"],
29 | "exclude": ["node_modules", "dist"]
30 | }
```
--------------------------------------------------------------------------------
/build.mjs:
--------------------------------------------------------------------------------
```
1 | #!/usr/bin/env node
2 |
3 | import { execSync } from 'child_process';
4 | import fs from 'fs';
5 | import path from 'path';
6 |
7 | // create temporary directory
8 | const tempDir = 'dist-temp';
9 | if (!fs.existsSync(tempDir)) {
10 | fs.mkdirSync(tempDir);
11 | }
12 |
13 | try {
14 | // first step: use SWC to translate TypeScript to JavaScript (keep the original build command logic)
15 | console.log('step 1: use SWC to translate TypeScript to JavaScript...');
16 | execSync(`rimraf ${tempDir} && swc src -d ${tempDir} --strip-leading-paths`, {
17 | stdio: 'inherit'
18 | });
19 |
20 | // second step: use Rollup to package as a single file
21 | console.log('step 2: use Rollup to package as a single file...');
22 | execSync('rimraf dist && rollup -c', {
23 | stdio: 'inherit'
24 | });
25 |
26 | // third step: ensure dist/index.js has execution permission (because it is a bin file)
27 | console.log('step 3: set execution permission...');
28 | fs.chmodSync('dist/index.js', '755');
29 |
30 | // fourth step: clean temporary directory
31 | console.log('step 4: clean temporary directory...');
32 | execSync(`rimraf ${tempDir}`, {
33 | stdio: 'inherit'
34 | });
35 |
36 | console.log('build completed! output: dist/index.js');
37 | } catch (error) {
38 | console.error('error occurred during the build process:', error);
39 | process.exit(1);
40 | }
```
--------------------------------------------------------------------------------
/src/interfaces/imageGeneration.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * images generation interfaces
3 | */
4 |
5 | /**
6 | * image response format
7 | */
8 | export interface ImageResponse {
9 | content: Array<{
10 | base64: string;
11 | path: string;
12 | prompt: string;
13 | negative_prompt?: string;
14 | seed: number;
15 | width: number;
16 | height: number;
17 | meta: Record<string, any>;
18 | }>;
19 | imageSavedPath?: string; // optional property, for storing image file path
20 | }
21 |
22 | /**
23 | * image generation parameters
24 | */
25 | export interface ImageGenerationParameters {
26 | prompt?: string;
27 | negative_prompt?: string;
28 | seed?: number;
29 | width?: number;
30 | height?: number;
31 | num_inference_steps?: number;
32 | guidance_scale?: number;
33 | model?: string;
34 | random_string?: string;
35 | [key: string]: any;
36 | }
37 |
38 | /**
39 | * image generation result
40 | */
41 | export interface ImageGenerationResult {
42 | status?: number; // changed to optional
43 | error?: string;
44 | images?: string[];
45 | imageData?: string;
46 | isError?: boolean;
47 | errorMessage?: string;
48 | }
49 |
50 | /**
51 | * Draw Things service generation result
52 | */
53 | export interface DrawThingsGenerationResult {
54 | isError: boolean;
55 | imageData?: string;
56 | errorMessage?: string;
57 | parameters?: Record<string, any>;
58 | status?: number; // added property to compatible with ImageGenerationResult
59 | images?: string[]; // added property to compatible with ImageGenerationResult
60 | error?: string; // added property to compatible with ImageGenerationResult
61 | imagePath?: string; // added property to store the path of the generated image
62 | metadata?: {
63 | alt: string;
64 | inference_time_ms: number;
65 | }; // added metadata
66 | }
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "draw-things-mcp-cursor",
3 | "version": "1.4.3",
4 | "description": "Draw Things API integration for Cursor using Model Context Protocol (MCP)",
5 | "private": false,
6 | "type": "module",
7 | "main": "dist/index.js",
8 | "bin": {
9 | "draw-things-mcp-cursor": "./dist/index.js"
10 | },
11 | "scripts": {
12 | "start": "node --experimental-vm-modules --no-warnings dist/index.js",
13 | "dev": "NODE_OPTIONS='--loader ts-node/esm' ts-node src/index.ts",
14 | "build": "node build.mjs",
15 | "test": "node --experimental-vm-modules --no-warnings test-mcp.js",
16 | "prepare": "npm run build",
17 | "prepublishOnly": "npm run build",
18 | "typecheck": "tsc --noEmit"
19 | },
20 | "dependencies": {
21 | "@modelcontextprotocol/sdk": "^1.7.0",
22 | "axios": "^1.8.0",
23 | "zod": "^3.24.2"
24 | },
25 | "devDependencies": {
26 | "@rollup/plugin-node-resolve": "^16.0.0",
27 | "@swc/cli": "^0.3.8",
28 | "@swc/core": "^1.4.8",
29 | "@swc/helpers": "^0.5.6",
30 | "@types/node": "^20.17.19",
31 | "rimraf": "^5.0.10",
32 | "rollup": "^4.34.9",
33 | "ts-node": "^10.9.2",
34 | "typescript": "^5.3.3",
35 | "zod-to-json-schema": "3.20.3"
36 | },
37 | "overrides": {
38 | "zod-to-json-schema": "3.20.3"
39 | },
40 | "resolutions": {
41 | "zod-to-json-schema": "3.20.3"
42 | },
43 | "files": [
44 | "dist",
45 | "README.md",
46 | "cursor-setup.md"
47 | ],
48 | "keywords": [
49 | "cursor",
50 | "mcp",
51 | "draw-things",
52 | "ai",
53 | "image-generation",
54 | "stable-diffusion"
55 | ],
56 | "author": "jaokuohsuan",
57 | "license": "MIT",
58 | "repository": {
59 | "type": "git",
60 | "url": "https://github.com/jaokuohsuan/draw-things-mcp"
61 | },
62 | "bugs": {
63 | "url": "https://github.com/jaokuohsuan/draw-things-mcp/issues"
64 | },
65 | "homepage": "https://github.com/jaokuohsuan/draw-things-mcp#readme",
66 | "engines": {
67 | "node": ">=16.0.0"
68 | },
69 | "packageManager": "[email protected]+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
70 | }
71 |
```
--------------------------------------------------------------------------------
/src/services/defaultParams.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ImageGenerationParams } from './schemas.js';
2 |
3 | // Default parameters for image generation
4 | export const defaultParams: ImageGenerationParams = {
5 | speed_up_with_guidance_embed: true,
6 | motion_scale: 127,
7 | image_guidance: 1.5,
8 | tiled_decoding: false,
9 | decoding_tile_height: 640,
10 | negative_prompt_for_image_prior: true,
11 | batch_size: 1,
12 | decoding_tile_overlap: 128,
13 | separate_open_clip_g: false,
14 | hires_fix_height: 960,
15 | decoding_tile_width: 640,
16 | diffusion_tile_height: 1024,
17 | num_frames: 14,
18 | stage_2_guidance: 1,
19 | t5_text_encoder_decoding: true,
20 | mask_blur_outset: 0,
21 | resolution_dependent_shift: true,
22 | model: "flux_1_schnell_q5p.ckpt",
23 | hires_fix: false,
24 | strength: 1,
25 | loras: [],
26 | diffusion_tile_width: 1024,
27 | diffusion_tile_overlap: 128,
28 | original_width: 512,
29 | seed: -1,
30 | zero_negative_prompt: false,
31 | upscaler_scale: 0,
32 | steps: 8,
33 | upscaler: null,
34 | mask_blur: 1.5,
35 | sampler: "DPM++ 2M AYS",
36 | width: 320,
37 | negative_original_width: 512,
38 | batch_count: 1,
39 | refiner_model: null,
40 | shift: 1,
41 | stage_2_shift: 1,
42 | open_clip_g_text: null,
43 | crop_left: 0,
44 | controls: [],
45 | start_frame_guidance: 1,
46 | original_height: 512,
47 | image_prior_steps: 5,
48 | guiding_frame_noise: 0.019999999552965164,
49 | clip_weight: 1,
50 | clip_skip: 1,
51 | crop_top: 0,
52 | negative_original_height: 512,
53 | preserve_original_after_inpaint: true,
54 | separate_clip_l: false,
55 | guidance_embed: 3.5,
56 | negative_aesthetic_score: 2.5,
57 | aesthetic_score: 6,
58 | clip_l_text: null,
59 | hires_fix_strength: 0.699999988079071,
60 | guidance_scale: 7.5,
61 | stochastic_sampling_gamma: 0.3,
62 | seed_mode: "Scale Alike",
63 | target_width: 512,
64 | hires_fix_width: 960,
65 | tiled_diffusion: false,
66 | fps: 5,
67 | refiner_start: 0.8500000238418579,
68 | height: 512,
69 | prompt: "A cute koala sitting on a eucalyptus tree, watercolor style, beautiful lighting, detailed",
70 | negative_prompt: "deformed, distorted, unnatural pose, extra limbs, blurry, low quality, ugly, bad anatomy, poor details, mutated, text, watermark"
71 | };
```
--------------------------------------------------------------------------------
/cursor-setup.md:
--------------------------------------------------------------------------------
```markdown
1 | # Cursor MCP Tool Setup Guide
2 |
3 | ## Setting Up Draw Things MCP Tool in Cursor
4 |
5 | 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.
6 |
7 | ### Prerequisites
8 |
9 | - Ensure Draw Things API is running (http://127.0.0.1:7888)
10 | - Node.js v14.0.0 or higher
11 |
12 | ### 1. Install the Package
13 |
14 | #### Local Development Mode
15 |
16 | If you're developing or modifying this tool, you can use local linking:
17 |
18 | ```bash
19 | # In the project directory
20 | npm link
21 | ```
22 |
23 | #### Publishing to NPM (if needed)
24 |
25 | If you want to publish this tool for others to use:
26 |
27 | ```bash
28 | npm publish
29 | ```
30 |
31 | Then install globally via npm:
32 |
33 | ```bash
34 | npm install -g draw-things-mcp-cursor
35 | ```
36 |
37 | ### 2. Create Cursor MCP Configuration File
38 |
39 | You need to create or edit the `~/.cursor/claude_desktop_config.json` file to register the MCP tool with Cursor:
40 |
41 | ```json
42 | {
43 | "mcpServers": {
44 | "draw-things": {
45 | "command": "draw-things-mcp-cursor",
46 | "args": []
47 | }
48 | }
49 | }
50 | ```
51 |
52 | #### Local Development Configuration
53 |
54 | If you're developing locally, it's recommended to use an absolute path to your JS file:
55 |
56 | ```json
57 | {
58 | "mcpServers": {
59 | "draw-things": {
60 | "command": "node",
61 | "args": [
62 | "/Users/james_jao/m800/my-mcp/src/index.js"
63 | ]
64 | }
65 | }
66 | }
67 | ```
68 |
69 | ### 3. Restart Cursor
70 |
71 | After configuration, completely close and restart the Cursor editor to ensure the new MCP configuration is properly loaded.
72 |
73 | ### 4. Using the MCP Tool
74 |
75 | In Cursor, you can call the image generation tool when chatting with the AI assistant using the following format:
76 |
77 | #### Basic Usage
78 |
79 | ```
80 | generateImage({"prompt": "a cute cat"})
81 | ```
82 |
83 | #### Advanced Usage
84 |
85 | You can specify additional parameters to fine-tune the generated image:
86 |
87 | ```
88 | generateImage({
89 | "prompt": "a cute cat",
90 | "negative_prompt": "ugly, deformed",
91 | "width": 512,
92 | "height": 512,
93 | "steps": 4,
94 | "model": "flux_1_schnell_q5p.ckpt"
95 | })
96 | ```
97 |
98 | ### Available Parameters
99 |
100 | | Parameter Name | Description | Default Value |
101 | |----------------|-------------|---------------|
102 | | prompt | The image generation prompt | (Required) |
103 | | negative_prompt | Elements to avoid in the image | (Empty) |
104 | | width | Image width (pixels) | 360 |
105 | | height | Image height (pixels) | 360 |
106 | | steps | Number of generation steps (higher is more detailed but slower) | 8 |
107 | | model | Model name to use | "flux_1_schnell_q5p.ckpt" |
108 |
109 | ### Troubleshooting
110 |
111 | If you encounter issues when setting up or using the MCP tool, check:
112 |
113 | - Log files in the `~/.cursor/logs` directory for detailed error information
114 | - Ensure Draw Things API is started and running at http://127.0.0.1:7888
115 | - Make sure the src/index.js file has execution permissions: `chmod +x src/index.js`
116 | - Check for error messages in the terminal: `draw-things-mcp-cursor`
117 |
118 | ### Getting Help
119 |
120 | If you have any questions, please open an issue on the project's GitHub page:
121 | https://github.com/james-jao/draw-things-mcp/issues
```
--------------------------------------------------------------------------------
/src/services/schemas.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * image generation params interface
3 | */
4 |
5 | // params interface
6 | export interface ImageGenerationParams {
7 | // basic params
8 | prompt?: string;
9 | negative_prompt?: string;
10 |
11 | // size params
12 | width?: number;
13 | height?: number;
14 |
15 | // generate control params
16 | steps?: number;
17 | seed?: number;
18 | guidance_scale?: number;
19 |
20 | // model params
21 | model?: string;
22 | sampler?: string;
23 |
24 | // MCP special params
25 | random_string?: string;
26 |
27 | // allow other params
28 | [key: string]: any;
29 | }
30 |
31 | // function for validating ImageGenerationParams
32 | export function validateImageGenerationParams(params: any): {
33 | valid: boolean;
34 | errors: string[];
35 | } {
36 | const errors: string[] = [];
37 |
38 | // check params type
39 | if (params.prompt !== undefined && typeof params.prompt !== "string") {
40 | errors.push("prompt must be a string");
41 | }
42 |
43 | if (
44 | params.negative_prompt !== undefined &&
45 | typeof params.negative_prompt !== "string"
46 | ) {
47 | errors.push("negative_prompt must be a string");
48 | }
49 |
50 | if (
51 | params.width !== undefined &&
52 | (typeof params.width !== "number" ||
53 | params.width <= 0 ||
54 | !Number.isInteger(params.width))
55 | ) {
56 | errors.push("width must be a positive integer");
57 | }
58 |
59 | if (
60 | params.height !== undefined &&
61 | (typeof params.height !== "number" ||
62 | params.height <= 0 ||
63 | !Number.isInteger(params.height))
64 | ) {
65 | errors.push("height must be a positive integer");
66 | }
67 |
68 | if (
69 | params.steps !== undefined &&
70 | (typeof params.steps !== "number" ||
71 | params.steps <= 0 ||
72 | !Number.isInteger(params.steps))
73 | ) {
74 | errors.push("steps must be a positive integer");
75 | }
76 |
77 | if (
78 | params.seed !== undefined &&
79 | (typeof params.seed !== "number" || !Number.isInteger(params.seed))
80 | ) {
81 | errors.push("seed must be an integer");
82 | }
83 |
84 | if (
85 | params.guidance_scale !== undefined &&
86 | (typeof params.guidance_scale !== "number" || params.guidance_scale <= 0)
87 | ) {
88 | errors.push("guidance_scale must be a positive number");
89 | }
90 |
91 | if (params.model !== undefined && typeof params.model !== "string") {
92 | errors.push("model must be a string");
93 | }
94 |
95 | if (params.sampler !== undefined && typeof params.sampler !== "string") {
96 | errors.push("sampler must be a string");
97 | }
98 |
99 | if (
100 | params.random_string !== undefined &&
101 | typeof params.random_string !== "string"
102 | ) {
103 | errors.push("random_string must be a string");
104 | }
105 |
106 | return { valid: errors.length === 0, errors };
107 | }
108 |
109 | // response interface
110 | export interface ImageGenerationResult {
111 | status?: number;
112 | images?: string[];
113 | parameters?: Record<string, any>;
114 | error?: string;
115 | }
116 |
117 | // success response interface
118 | export interface SuccessResponse {
119 | content: Array<{
120 | type: "image";
121 | data: string;
122 | mimeType: string;
123 | }>;
124 | }
125 |
126 | // error response interface
127 | export interface ErrorResponse {
128 | content: Array<{
129 | type: "text";
130 | text: string;
131 | }>;
132 | isError: true;
133 | }
134 |
135 | // MCP response interface
136 | export type McpResponse = SuccessResponse | ErrorResponse;
137 |
```
--------------------------------------------------------------------------------
/CURSOR_INTEGRATION.md:
--------------------------------------------------------------------------------
```markdown
1 | # Complete Guide for Using Draw Things MCP in Cursor
2 |
3 | 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.
4 |
5 | ## Background
6 |
7 | When directly using the `mcp_generateImage` tool in Cursor, the following issues occur:
8 |
9 | 1. Cursor sends plain text prompts instead of the correct JSON-RPC 2.0 format
10 | 2. MCP service cannot parse this input format, resulting in the error `Unexpected token A in JSON at position 0`
11 | 3. According to the [MCP documentation](https://docs.cursor.com/context/model-context-protocol), communication must use a specific JSON-RPC 2.0 format
12 |
13 | ## Solution: Bridge Service
14 |
15 | We provide a bridge service that automatically converts Cursor's plain text prompts to the correct JSON-RPC 2.0 format.
16 |
17 | ### Step 1: Environment Setup
18 |
19 | First, ensure you have installed these prerequisites:
20 |
21 | 1. Node.js and npm
22 | 2. Draw Things application
23 | 3. API enabled in Draw Things (port 7888)
24 |
25 | ### Step 2: Start the Bridge Service
26 |
27 | We provide a startup script that easily launches the bridge service:
28 |
29 | ```bash
30 | # Grant execution permission
31 | chmod +x start-cursor-bridge.sh
32 |
33 | # Basic usage
34 | ./start-cursor-bridge.sh
35 |
36 | # Use debug mode
37 | ./start-cursor-bridge.sh --debug
38 |
39 | # View help
40 | ./start-cursor-bridge.sh --help
41 | ```
42 |
43 | This script will:
44 | 1. Check if the Draw Things API is available
45 | 2. Start the bridge service
46 | 3. Start the MCP service
47 | 4. Connect the two services so they can communicate with each other
48 |
49 | ### Step 3: Using in Cursor
50 |
51 | When the bridge service is running, the following two input methods are supported when using the `mcp_generateImage` tool in Cursor:
52 |
53 | 1. **Directly send English prompts** (the bridge service will automatically convert to JSON-RPC format):
54 | ```
55 | A group of adorable kittens playing together, cute, fluffy, detailed fur, warm lighting, playful mood
56 | ```
57 |
58 | 2. **Use JSON objects** (suitable when more custom parameters are needed):
59 | ```json
60 | {
61 | "prompt": "A group of adorable kittens playing together, cute, fluffy, detailed fur, warm lighting, playful mood",
62 | "negative_prompt": "blurry, distorted, low quality",
63 | "seed": 12345
64 | }
65 | ```
66 |
67 | ### Step 4: View Results
68 |
69 | 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.
70 |
71 | ## JSON-RPC 2.0 Format Explanation
72 |
73 | According to the MCP specification, the correct request format should be:
74 |
75 | ```json
76 | {
77 | "jsonrpc": "2.0",
78 | "id": "request-123",
79 | "method": "mcp.invoke",
80 | "params": {
81 | "tool": "generateImage",
82 | "parameters": {
83 | "prompt": "A group of adorable kittens playing together",
84 | "negative_prompt": "blurry, low quality",
85 | "seed": 12345
86 | }
87 | }
88 | }
89 | ```
90 |
91 | Our bridge service automatically converts simple inputs to this format.
92 |
93 | ## Custom Options
94 |
95 | You can modify default parameters by editing the `src/services/drawThings/defaultParams.js` file, such as:
96 |
97 | - Model selection
98 | - Image dimensions
99 | - Sampler type
100 | - Other generation parameters
101 |
102 | ## Troubleshooting
103 |
104 | ### Check Logs
105 |
106 | If you encounter problems, first check these logs:
107 |
108 | 1. `cursor-mcp-bridge.log` - Bridge service logs
109 | 2. `cursor-mcp-debug.log` - Detailed logs when debug mode is enabled
110 | 3. `error.log` - MCP service error logs
111 |
112 | ### Common Issues
113 |
114 | 1. **Connection Error**: Ensure the Draw Things application is running and API is enabled (127.0.0.1:7888).
115 |
116 | 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.
117 |
118 | 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.
119 |
120 | ## Technical Details
121 |
122 | How the bridge service works:
123 |
124 | 1. Receives plain text or JSON input from Cursor
125 | 2. Converts it to JSON-RPC 2.0 format compliant with MCP specifications
126 | 3. Passes the converted request to the MCP service
127 | 4. MCP service communicates with the Draw Things API
128 | 5. Receives the response and saves the generated image to the file system
129 |
130 | ### Transport Layer
131 |
132 | According to the MCP specification, our bridge service implements the following functions:
133 |
134 | - Uses stdin/stdout as the transport layer
135 | - Correctly handles JSON-RPC 2.0 request/response formats
136 | - Supports error handling and logging
137 | - Automatically saves generated images
138 |
139 | If you need more customization, you can edit the `cursor-mcp-bridge.js` file.
```
--------------------------------------------------------------------------------
/start-cursor-bridge.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # Cursor MCP and Draw Things Bridge Service Startup Script
4 |
5 | # Ensure the program aborts on error
6 | set -e
7 |
8 | echo "========================================================"
9 | echo " Cursor MCP and Draw Things Bridge Service Tool "
10 | echo " Image Generation Service Compliant with Model Context Protocol "
11 | echo "========================================================"
12 | echo
13 |
14 | # Check dependencies
15 | command -v node >/dev/null 2>&1 || { echo "Error: Node.js is required but not installed"; exit 1; }
16 | command -v npm >/dev/null 2>&1 || { echo "Error: npm is required but not installed"; exit 1; }
17 |
18 | # Ensure script has execution permissions
19 | chmod +x cursor-mcp-bridge.js
20 |
21 | # Check if help information is needed
22 | if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
23 | echo "Usage: ./start-cursor-bridge.sh [options]"
24 | echo
25 | echo "Options:"
26 | echo " --help, -h Display this help information"
27 | echo " --debug Enable additional debug output"
28 | echo " --no-cleanup Keep old log files"
29 | echo " --port PORT Specify custom port for Draw Things API (default: 7888)"
30 | echo
31 | echo "This script is used to start the Cursor MCP and Draw Things bridge service."
32 | echo "It will start a service that allows Cursor to generate images using plain text prompts."
33 | echo
34 | echo "Dependencies:"
35 | echo " - Node.js and npm"
36 | echo " - Draw Things application (must be running with API enabled)"
37 | echo
38 | exit 0
39 | fi
40 |
41 | # Parse parameters
42 | DEBUG_MODE=false
43 | CLEANUP=true
44 | API_PORT=7888
45 |
46 | for arg in "$@"; do
47 | case $arg in
48 | --debug)
49 | DEBUG_MODE=true
50 | shift
51 | ;;
52 | --no-cleanup)
53 | CLEANUP=false
54 | shift
55 | ;;
56 | --port=*)
57 | API_PORT="${arg#*=}"
58 | shift
59 | ;;
60 | esac
61 | done
62 |
63 | # Install dependencies
64 | echo "Checking and installing necessary dependencies..."
65 | npm install --quiet
66 |
67 | # Clean up old logs
68 | if [ "$CLEANUP" = true ]; then
69 | echo "Cleaning up old log files..."
70 | if [ -f cursor-mcp-bridge.log ]; then
71 | mv cursor-mcp-bridge.log cursor-mcp-bridge.log.old
72 | fi
73 | if [ -f draw-things-mcp.log ]; then
74 | mv draw-things-mcp.log draw-things-mcp.log.old
75 | fi
76 | fi
77 |
78 | # Ensure images directory exists
79 | mkdir -p images
80 | # Ensure logs directory exists
81 | mkdir -p logs
82 |
83 | echo
84 | echo "Step 1: Checking if Draw Things API is available..."
85 |
86 | # Create a simple test script to check API connection
87 | cat > test-api.js << EOL
88 | import http from 'http';
89 |
90 | const options = {
91 | host: '127.0.0.1',
92 | port: ${API_PORT},
93 | path: '/sdapi/v1/options',
94 | method: 'GET',
95 | timeout: 5000,
96 | headers: {
97 | 'User-Agent': 'DrawThingsMCP/1.0',
98 | 'Accept': 'application/json'
99 | }
100 | };
101 |
102 | const req = http.request(options, (res) => {
103 | console.log('Draw Things API connection successful! Status code:', res.statusCode);
104 | process.exit(0);
105 | });
106 |
107 | req.on('error', (e) => {
108 | if (e.code === 'ECONNREFUSED') {
109 | console.error('Error: Unable to connect to Draw Things API. Make sure Draw Things application is running and API is enabled.');
110 | } else if (e.code === 'ETIMEDOUT') {
111 | console.error('Error: Connection to Draw Things API timed out. Make sure Draw Things application is running normally.');
112 | } else {
113 | console.error('Error:', e.message);
114 | }
115 | process.exit(1);
116 | });
117 |
118 | req.on('timeout', () => {
119 | console.error('Error: Connection to Draw Things API timed out. Make sure Draw Things application is running normally.');
120 | req.destroy();
121 | process.exit(1);
122 | });
123 |
124 | req.end();
125 | EOL
126 |
127 | # Run API test
128 | if node test-api.js; then
129 | echo "Draw Things API is available, continuing to start bridge service..."
130 | else
131 | echo
132 | echo "Warning: Draw Things API appears to be unavailable on port ${API_PORT}."
133 | echo "Please ensure:"
134 | echo "1. Draw Things application is running"
135 | echo "2. API is enabled in Draw Things settings"
136 | echo "3. API is listening on 127.0.0.1:${API_PORT}"
137 | echo
138 | read -p "Continue starting the bridge service anyway? (y/n) " -n 1 -r
139 | echo
140 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then
141 | echo "Canceling bridge service startup."
142 | exit 1
143 | fi
144 | fi
145 |
146 | # Clean up temporary files
147 | rm -f test-api.js
148 |
149 | echo
150 | echo "Step 2: Starting Services..."
151 | echo
152 |
153 | # Set up environment variables
154 | export DRAW_THINGS_FORCE_STAY_ALIVE=true
155 | export MCP_BRIDGE_DEDUP=true
156 | export DEBUG_MODE=$DEBUG_MODE
157 | export DRAW_THINGS_API_PORT=$API_PORT
158 | export DRAW_THINGS_API_URL="http://127.0.0.1:${API_PORT}"
159 |
160 | # Set up debug mode
161 | if [ "$DEBUG_MODE" = true ]; then
162 | echo "Debug mode enabled, all log output will be displayed"
163 | echo "Starting MCP bridge service in debug mode..."
164 |
165 | # Start both services in debug mode
166 | node cursor-mcp-bridge.js 2>&1 | tee -a cursor-mcp-debug.log | node src/index.js
167 | else
168 | # Start bridge service
169 | echo "Starting bridge service in normal mode..."
170 | echo "All logs will be saved to cursor-mcp-bridge.log and draw-things-mcp.log"
171 |
172 | # Start MCP bridge service and pipe output to MCP service
173 | node cursor-mcp-bridge.js | node src/index.js
174 | fi
175 |
176 | echo
177 | echo "Service has ended."
178 | echo "Log files:"
179 | echo " - cursor-mcp-bridge.log"
180 | echo " - draw-things-mcp.log"
181 | echo " - logs/error.log (if errors occurred)"
182 | echo "If generation was successful, images will be saved in the images directory."
183 |
184 | # Display service status
185 | if [ -f "images/image_$(date +%Y%m%d)*.png" ] || [ -f "images/generated-image_*.png" ]; then
186 | echo "Images were successfully generated today!"
187 | ls -la images/ | grep "$(date +%Y-%m-%d)"
188 | else
189 | echo "No images generated today were found. Please check the logs for more information."
190 | fi
```
--------------------------------------------------------------------------------
/src/services/drawThingsService.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defaultParams } from "./defaultParams.js";
2 | import {
3 | ImageGenerationParams,
4 | validateImageGenerationParams,
5 | } from "./schemas.js";
6 | import axios, { AxiosInstance } from "axios";
7 | import fs from "fs";
8 | import path from "path";
9 | import { fileURLToPath } from "url";
10 | import { DrawThingsGenerationResult } from "../../interfaces/index.js";
11 |
12 | /**
13 | * simplified DrawThingsService
14 | * focus on core functionality: connect to Draw Things API and generate image
15 | */
16 | export class DrawThingsService {
17 | // make baseUrl public for compatibility with index.ts
18 | public baseUrl: string;
19 | // change to public axios for compatibility
20 | public axios: AxiosInstance;
21 |
22 | constructor(apiUrl = "http://127.0.0.1:7888") {
23 | this.baseUrl = apiUrl;
24 |
25 | // initialize axios
26 | this.axios = axios.create({
27 | baseURL: this.baseUrl,
28 | timeout: 300000, // 5 minutes timeout (image generation may take time)
29 | headers: {
30 | "Content-Type": "application/json",
31 | Accept: "application/json",
32 | },
33 | });
34 |
35 | // log initialization
36 | console.error(
37 | `DrawThingsService initialized, API location: ${this.baseUrl}`
38 | );
39 | }
40 |
41 | /**
42 | * Set new base URL and update axios instance
43 | * @param url new base URL
44 | */
45 | setBaseUrl(url: string): void {
46 | this.baseUrl = url;
47 | this.axios.defaults.baseURL = url;
48 | console.error(`Updated API base URL to: ${url}`);
49 | }
50 |
51 | /**
52 | * check API connection
53 | * simplified version that just checks if API is available
54 | */
55 | async checkApiConnection(): Promise<boolean> {
56 | try {
57 | console.error(`Checking API connection to: ${this.baseUrl}`);
58 |
59 | // Try simple endpoint with short timeout
60 | const response = await this.axios.get("/sdapi/v1/options", {
61 | timeout: 5000,
62 | validateStatus: (status) => status >= 200,
63 | });
64 |
65 | const isConnected = response.status >= 200;
66 | console.error(
67 | `API connection check: ${isConnected ? "Success" : "Failed"}`
68 | );
69 | return isConnected;
70 | } catch (error) {
71 | console.error(`API connection check failed: ${(error as Error).message}`);
72 | return false;
73 | }
74 | }
75 |
76 | // Helper function to save images to the file system
77 | async saveImage({
78 | base64Data,
79 | outputPath,
80 | fileName
81 | }: {
82 | base64Data: string;
83 | outputPath?: string;
84 | fileName?: string;
85 | }): Promise<string> {
86 | const __filename = fileURLToPath(import.meta.url);
87 | // Get directory name
88 | const __dirname = path.dirname(__filename);
89 | const projectRoot: string = path.resolve(__dirname, "..");
90 |
91 | try {
92 | // if no output path provided, use default path
93 | const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
94 | const defaultFileName = fileName || `generated-image-${timestamp}.png`;
95 | const defaultImagesDir = path.resolve(projectRoot, "..", "images");
96 | const finalOutputPath = outputPath || path.join(defaultImagesDir, defaultFileName);
97 |
98 | // ensure the images directory exists
99 | const imagesDir = path.dirname(finalOutputPath);
100 | if (!fs.existsSync(imagesDir)) {
101 | await fs.promises.mkdir(imagesDir, { recursive: true });
102 | }
103 |
104 | const cleanBase64 = base64Data.replace(/^data:image\/\w+;base64,/, "");
105 | const buffer = Buffer.from(cleanBase64, "base64");
106 |
107 | const absolutePath = path.resolve(finalOutputPath);
108 | await fs.promises.writeFile(absolutePath, buffer);
109 | return absolutePath;
110 | } catch (error) {
111 | console.error(
112 | `Failed to save image: ${
113 | error instanceof Error ? error.message : String(error)
114 | }`
115 | );
116 | if (error instanceof Error) {
117 | console.error(error.stack || "No stack trace available");
118 | }
119 | throw error;
120 | }
121 | }
122 |
123 | /**
124 | * get default params
125 | */
126 | getDefaultParams(): ImageGenerationParams {
127 | return defaultParams;
128 | }
129 |
130 | /**
131 | * generate image
132 | * @param inputParams user provided params
133 | */
134 | async generateImage(
135 | inputParams: Partial<ImageGenerationParams> = {}
136 | ): Promise<DrawThingsGenerationResult> {
137 | try {
138 | // handle input params
139 | let params: Partial<ImageGenerationParams> = {};
140 |
141 | // validate params
142 | try {
143 | const validationResult = validateImageGenerationParams(inputParams);
144 | if (validationResult.valid) {
145 | params = inputParams;
146 | } else {
147 | console.error("parameter validation failed, use default params");
148 | }
149 | } catch (error) {
150 | console.error("parameter validation error:", error);
151 | }
152 |
153 | // handle random_string special case
154 | if (
155 | params.random_string &&
156 | (!params.prompt || Object.keys(params).length === 1)
157 | ) {
158 | params.prompt = params.random_string;
159 | delete params.random_string;
160 | }
161 |
162 | // ensure prompt
163 | if (!params.prompt) {
164 | params.prompt = inputParams.prompt || defaultParams.prompt;
165 | }
166 |
167 | // merge params
168 | const requestParams = {
169 | ...defaultParams,
170 | ...params,
171 | seed: params.seed ?? Math.floor(Math.random() * 2147483647),
172 | };
173 |
174 | console.error(`use prompt: "${requestParams.prompt}"`);
175 |
176 | // send request to Draw Things API
177 | console.error("send request to Draw Things API...");
178 | const response = await this.axios.post(
179 | "/sdapi/v1/txt2img",
180 | requestParams
181 | );
182 |
183 | // handle response
184 | if (
185 | !response.data ||
186 | !response.data.images ||
187 | response.data.images.length === 0
188 | ) {
189 | throw new Error("API did not return image data");
190 | }
191 |
192 | // handle image data
193 | const imageData = response.data.images[0];
194 |
195 | // format image data
196 | const formattedImageData = imageData.startsWith("data:image/")
197 | ? imageData
198 | : `data:image/png;base64,${imageData}`;
199 |
200 | console.error("image generation success");
201 |
202 | // record the start time of image generation
203 | const startTime = Date.now() - 2000; // assume the image generation took 2 seconds
204 | const endTime = Date.now();
205 |
206 | // automatically save the generated image
207 | const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
208 | const defaultFileName = `generated-image-${timestamp}.png`;
209 |
210 | // save the generated image
211 | const imagePath = await this.saveImage({
212 | base64Data: formattedImageData,
213 | fileName: defaultFileName
214 | });
215 |
216 | return {
217 | isError: false,
218 | imageData: formattedImageData,
219 | imagePath: imagePath,
220 | metadata: {
221 | alt: `Image generated from prompt: ${requestParams.prompt}`,
222 | inference_time_ms: endTime - startTime,
223 | }
224 | };
225 | } catch (error) {
226 | console.error("image generation error:", error);
227 |
228 | // error message
229 | let errorMessage = "unknown error";
230 |
231 | if (error instanceof Error) {
232 | errorMessage = error.message;
233 | }
234 |
235 | // handle axios error
236 | const axiosError = error as any;
237 | if (axiosError.response) {
238 | errorMessage = `API error: ${axiosError.response.status} - ${
239 | axiosError.response.data?.error || axiosError.message
240 | }`;
241 | } else if (axiosError.code === "ECONNREFUSED") {
242 | errorMessage =
243 | "cannot connect to Draw Things API. please ensure Draw Things is running and API is enabled.";
244 | } else if (axiosError.code === "ETIMEDOUT") {
245 | errorMessage =
246 | "connection to Draw Things API timeout. image generation may take longer, or API not responding.";
247 | }
248 |
249 | return {
250 | isError: true,
251 | errorMessage,
252 | };
253 | }
254 | }
255 | }
256 |
```
--------------------------------------------------------------------------------
/test-api-connection.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Draw Things API Connection Test
5 | * 測試不同方式連接到 Draw Things API
6 | */
7 |
8 | import http from 'http';
9 | import https from 'https';
10 | import axios from 'axios';
11 | import fs from 'fs';
12 |
13 | // 記錄檔
14 | const logFile = 'api-connection-test.log';
15 | function log(message) {
16 | const timestamp = new Date().toISOString();
17 | const logMessage = `${timestamp} - ${message}\n`;
18 | fs.appendFileSync(logFile, logMessage);
19 | console.log(message);
20 | }
21 |
22 | // 錯誤記錄
23 | function logError(error, message = 'Error') {
24 | const timestamp = new Date().toISOString();
25 | const errorDetails = error instanceof Error ?
26 | `${error.message}\n${error.stack}` :
27 | String(error);
28 | const logMessage = `${timestamp} - [ERROR] ${message}: ${errorDetails}\n`;
29 | fs.appendFileSync(logFile, logMessage);
30 | console.error(`${message}: ${error.message}`);
31 | }
32 |
33 | // 讀取參數
34 | const apiPort = process.env.DRAW_THINGS_API_PORT || 7888;
35 | const apiProxyPort = process.env.PROXY_PORT || 7889;
36 |
37 | log('Draw Things API Connection Test');
38 | log('===========================');
39 | log(`Testing API on port ${apiPort}`);
40 | log(`Testing proxy on port ${apiProxyPort}`);
41 | log('');
42 |
43 | // 測試 1: 直接使用 HTTP 模組連接
44 | async function testHttpModule() {
45 | log('Test 1: 使用 Node.js HTTP 模組連接');
46 |
47 | return new Promise((resolve) => {
48 | try {
49 | const urls = [
50 | { name: '直接 API 連接 (127.0.0.1)', host: '127.0.0.1', port: apiPort },
51 | { name: '直接 API 連接 (localhost)', host: 'localhost', port: apiPort },
52 | { name: '代理伺服器連接 (127.0.0.1)', host: '127.0.0.1', port: apiProxyPort },
53 | { name: '代理伺服器連接 (localhost)', host: 'localhost', port: apiProxyPort }
54 | ];
55 |
56 | let completedTests = 0;
57 | const results = [];
58 |
59 | for (const url of urls) {
60 | log(`測試連接: ${url.name}`);
61 |
62 | const options = {
63 | hostname: url.host,
64 | port: url.port,
65 | path: '/sdapi/v1/options',
66 | method: 'GET',
67 | timeout: 5000,
68 | headers: {
69 | 'User-Agent': 'DrawThingsMCP/1.0',
70 | 'Accept': 'application/json'
71 | }
72 | };
73 |
74 | const req = http.request(options, (res) => {
75 | log(`${url.name} 回應狀態碼: ${res.statusCode}`);
76 |
77 | let data = '';
78 | res.on('data', chunk => {
79 | data += chunk;
80 | });
81 |
82 | res.on('end', () => {
83 | const success = res.statusCode >= 200 && res.statusCode < 300;
84 | results.push({
85 | name: url.name,
86 | success,
87 | statusCode: res.statusCode,
88 | hasData: !!data
89 | });
90 |
91 | completedTests++;
92 | if (completedTests === urls.length) {
93 | resolve(results);
94 | }
95 | });
96 | });
97 |
98 | req.on('error', (e) => {
99 | log(`${url.name} 錯誤: ${e.message}`);
100 | results.push({
101 | name: url.name,
102 | success: false,
103 | error: e.message
104 | });
105 |
106 | completedTests++;
107 | if (completedTests === urls.length) {
108 | resolve(results);
109 | }
110 | });
111 |
112 | req.on('timeout', () => {
113 | log(`${url.name} 連接逾時`);
114 | req.destroy();
115 |
116 | results.push({
117 | name: url.name,
118 | success: false,
119 | error: 'Timeout'
120 | });
121 |
122 | completedTests++;
123 | if (completedTests === urls.length) {
124 | resolve(results);
125 | }
126 | });
127 |
128 | req.end();
129 | }
130 | } catch (error) {
131 | logError(error, 'HTTP 模組測試發生錯誤');
132 | resolve([]);
133 | }
134 | });
135 | }
136 |
137 | // 測試 2: 使用 Axios 連接
138 | async function testAxios() {
139 | log('Test 2: 使用 Axios 連接');
140 |
141 | try {
142 | const urls = [
143 | { name: '直接 API 連接 (127.0.0.1)', url: `http://127.0.0.1:${apiPort}/sdapi/v1/options` },
144 | { name: '直接 API 連接 (localhost)', url: `http://localhost:${apiPort}/sdapi/v1/options` },
145 | { name: '代理伺服器連接 (127.0.0.1)', url: `http://127.0.0.1:${apiProxyPort}/sdapi/v1/options` },
146 | { name: '代理伺服器連接 (localhost)', url: `http://localhost:${apiProxyPort}/sdapi/v1/options` },
147 | ];
148 |
149 | const results = [];
150 |
151 | for (const url of urls) {
152 | log(`測試連接: ${url.name}`);
153 |
154 | try {
155 | const response = await axios.get(url.url, {
156 | timeout: 5000,
157 | headers: {
158 | 'User-Agent': 'DrawThingsMCP/1.0',
159 | 'Accept': 'application/json'
160 | }
161 | });
162 |
163 | log(`${url.name} 回應狀態碼: ${response.status}`);
164 |
165 | results.push({
166 | name: url.name,
167 | success: response.status >= 200 && response.status < 300,
168 | statusCode: response.status,
169 | hasData: !!response.data
170 | });
171 | } catch (error) {
172 | log(`${url.name} 錯誤: ${error.message}`);
173 |
174 | results.push({
175 | name: url.name,
176 | success: false,
177 | error: error.message
178 | });
179 | }
180 | }
181 |
182 | return results;
183 | } catch (error) {
184 | logError(error, 'Axios 測試發生錯誤');
185 | return [];
186 | }
187 | }
188 |
189 | // 測試 3: 嘗試不同的端點
190 | async function testDifferentEndpoints() {
191 | log('Test 3: 測試不同的 API 端點');
192 |
193 | try {
194 | // 使用工作正常的連接方式 (localhost 或 127.0.0.1)
195 | const baseUrl = `http://127.0.0.1:${apiPort}`;
196 |
197 | const endpoints = [
198 | '/sdapi/v1/options',
199 | '/sdapi/v1/samplers',
200 | '/sdapi/v1/sd-models',
201 | '/sdapi/v1/prompt-styles',
202 | '/'
203 | ];
204 |
205 | const results = [];
206 |
207 | for (const endpoint of endpoints) {
208 | log(`測試端點: ${endpoint}`);
209 |
210 | try {
211 | const response = await axios.get(`${baseUrl}${endpoint}`, {
212 | timeout: 5000,
213 | headers: {
214 | 'User-Agent': 'DrawThingsMCP/1.0',
215 | 'Accept': 'application/json'
216 | }
217 | });
218 |
219 | log(`端點 ${endpoint} 回應狀態碼: ${response.status}`);
220 |
221 | results.push({
222 | endpoint,
223 | success: response.status >= 200 && response.status < 300,
224 | statusCode: response.status,
225 | hasData: !!response.data
226 | });
227 | } catch (error) {
228 | log(`端點 ${endpoint} 錯誤: ${error.message}`);
229 |
230 | results.push({
231 | endpoint,
232 | success: false,
233 | error: error.message
234 | });
235 | }
236 | }
237 |
238 | return results;
239 | } catch (error) {
240 | logError(error, '端點測試發生錯誤');
241 | return [];
242 | }
243 | }
244 |
245 | // 執行測試
246 | async function runTests() {
247 | try {
248 | // 測試 1: HTTP 模組
249 | log('\n執行 HTTP 模組測試...');
250 | const httpResults = await testHttpModule();
251 |
252 | log('\nHTTP 模組測試結果:');
253 | httpResults.forEach(result => {
254 | log(`${result.name}: ${result.success ? '成功' : '失敗'} ${result.statusCode ? `(狀態碼: ${result.statusCode})` : ''} ${result.error ? `(錯誤: ${result.error})` : ''}`);
255 | });
256 |
257 | // 測試 2: Axios
258 | log('\n執行 Axios 測試...');
259 | const axiosResults = await testAxios();
260 |
261 | log('\nAxios 測試結果:');
262 | axiosResults.forEach(result => {
263 | log(`${result.name}: ${result.success ? '成功' : '失敗'} ${result.statusCode ? `(狀態碼: ${result.statusCode})` : ''} ${result.error ? `(錯誤: ${result.error})` : ''}`);
264 | });
265 |
266 | // 測試 3: 不同端點
267 | log('\n執行不同端點測試...');
268 | const endpointResults = await testDifferentEndpoints();
269 |
270 | log('\n不同端點測試結果:');
271 | endpointResults.forEach(result => {
272 | log(`端點 ${result.endpoint}: ${result.success ? '成功' : '失敗'} ${result.statusCode ? `(狀態碼: ${result.statusCode})` : ''} ${result.error ? `(錯誤: ${result.error})` : ''}`);
273 | });
274 |
275 | // 總結
276 | const httpSuccess = httpResults.some(r => r.success);
277 | const axiosSuccess = axiosResults.some(r => r.success);
278 | const endpointSuccess = endpointResults.some(r => r.success);
279 |
280 | log('\n=== 測試總結 ===');
281 | log(`HTTP 模組連接測試: ${httpSuccess ? '至少有一個成功' : '全部失敗'}`);
282 | log(`Axios 連接測試: ${axiosSuccess ? '至少有一個成功' : '全部失敗'}`);
283 | log(`端點測試: ${endpointSuccess ? '至少有一個成功' : '全部失敗'}`);
284 |
285 | if (httpSuccess || axiosSuccess) {
286 | log('\nAPI 連接測試成功! 您的 Draw Things API 似乎可以正常工作。');
287 |
288 | // 建議最佳連接方式
289 | const bestConnection = [...httpResults, ...axiosResults].find(r => r.success);
290 | if (bestConnection) {
291 | log(`建議使用連接方式: ${bestConnection.name}`);
292 | }
293 | } else {
294 | log('\nAPI 連接測試失敗! 請確認:');
295 | log('1. Draw Things 應用程式正在運行');
296 | log('2. Draw Things 已啟用 API 功能');
297 | log('3. API 在設定的端口上運行 (默認 7888)');
298 | log('4. 沒有防火牆阻擋連接');
299 | }
300 |
301 | } catch (error) {
302 | logError(error, '測試執行過程中發生錯誤');
303 | }
304 | }
305 |
306 | // 執行測試
307 | runTests().catch(error => {
308 | logError(error, '測試主程序發生錯誤');
309 | });
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Draw Things MCP - A Model Context Protocol implementation for Draw Things API
5 | * Integrated with Cursor MCP Bridge functionality for multiple input formats
6 | *
7 | * NOTE: Requires Node.js version 14+ for optional chaining support in dependencies
8 | */
9 |
10 | import path from "path";
11 | import fs from "fs";
12 | import { fileURLToPath } from "url";
13 | import { z } from "zod";
14 |
15 | // MCP SDK imports
16 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18 | import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
19 |
20 | // Local service imports
21 | import { DrawThingsService } from "./services/drawThingsService.js";
22 | import {
23 | ImageGenerationParameters,
24 | ImageGenerationResult,
25 | } from "./services/schemas.js";
26 |
27 | // Constants and environment variables
28 | const DEBUG_MODE: boolean = process.env.DEBUG_MODE === "true";
29 | // Get current file path in ESM
30 | const __filename = fileURLToPath(import.meta.url);
31 | // Get directory name
32 | const __dirname = path.dirname(__filename);
33 | const projectRoot: string = path.resolve(__dirname, "..");
34 | const logsDir: string = path.join(projectRoot, "logs");
35 |
36 | // Create logs directory if it doesn't exist
37 | try {
38 | if (!fs.existsSync(logsDir)) {
39 | fs.mkdirSync(logsDir, { recursive: true });
40 | console.error(`Created logs directory: ${logsDir}`);
41 | }
42 | } catch (error) {
43 | console.error(
44 | `Failed to create logs directory: ${
45 | error instanceof Error ? error.message : String(error)
46 | }`
47 | );
48 | }
49 |
50 | const logFile: string = path.join(logsDir, "draw-things-mcp.log");
51 |
52 | // Basic logging function
53 | function log(message: string): void {
54 | const timestamp = new Date().toISOString();
55 | const logMessage = `${timestamp} - ${message}\n`;
56 | try {
57 | fs.appendFileSync(logFile, logMessage);
58 | // Only output to stderr to avoid polluting JSON-RPC communication
59 | console.error(logMessage);
60 | } catch (error) {
61 | console.error(
62 | `Failed to write to log file: ${
63 | error instanceof Error ? error.message : String(error)
64 | }`
65 | );
66 | }
67 | }
68 |
69 | // Enhanced error logging to dedicated error log file
70 | async function logError(error: Error | unknown): Promise<void> {
71 | try {
72 | const errorLogFile = path.join(logsDir, "error.log");
73 | const timestamp = new Date().toISOString();
74 | const errorDetails =
75 | error instanceof Error
76 | ? `${error.message}\n${error.stack}`
77 | : String(error);
78 |
79 | const errorLog = `${timestamp} - ERROR: ${errorDetails}\n\n`;
80 |
81 | try {
82 | await fs.promises.appendFile(errorLogFile, errorLog);
83 |
84 | log(`Error logged to ${errorLogFile}`);
85 |
86 | if (DEBUG_MODE) {
87 | console.error(`\n[DEBUG] FULL ERROR DETAILS:\n${errorDetails}\n`);
88 | }
89 | } catch (writeError) {
90 | // Fallback to sync writing
91 | try {
92 | fs.appendFileSync(errorLogFile, errorLog);
93 | } catch (syncWriteError) {
94 | console.error(
95 | `Failed to write to error log: ${
96 | syncWriteError instanceof Error
97 | ? syncWriteError.message
98 | : String(syncWriteError)
99 | }`
100 | );
101 | console.error(`Original error: ${errorDetails}`);
102 | }
103 | }
104 | } catch (logError) {
105 | console.error("Critical error in error logging system:");
106 | console.error(logError);
107 | console.error("Original error:");
108 | console.error(error);
109 | }
110 | }
111 |
112 | // Print connection information and help message on startup
113 | function printConnectionInfo(): void {
114 | // Only print to stderr to avoid polluting JSON-RPC communication
115 | const infoText = `
116 | ---------------------------------------------
117 | | Draw Things MCP - Image Generation Service |
118 | ---------------------------------------------
119 |
120 | Attempting to connect to Draw Things API at:
121 | http://127.0.0.1:7888
122 |
123 | TROUBLESHOOTING TIPS:
124 | 1. Ensure Draw Things is running on your computer
125 | 2. Make sure the API is enabled in Draw Things settings
126 | 3. If you changed the default port in Draw Things, set the environment variable:
127 | DRAW_THINGS_API_URL=http://127.0.0.1:YOUR_PORT
128 |
129 | Starting service...
130 | `;
131 |
132 | // Log to file and stderr
133 | log(infoText);
134 | }
135 |
136 | const drawThingsService = new DrawThingsService();
137 |
138 | const server = new McpServer({
139 | name: "draw-things-mcp",
140 | version: "1.0.0",
141 | });
142 |
143 | // Define the image generation tool schema
144 | const paramsSchema = {
145 | prompt: z.string().optional(),
146 | negative_prompt: z.string().optional(),
147 | width: z.number().optional(),
148 | height: z.number().optional(),
149 | steps: z.number().optional(),
150 | seed: z.number().optional(),
151 | guidance_scale: z.number().optional(),
152 | random_string: z.string().optional(),
153 | };
154 |
155 | server.tool(
156 | "generateImage",
157 | "Generate an image based on a prompt",
158 | paramsSchema,
159 | async (mcpParams: any) => {
160 | try {
161 | log("Received image generation request");
162 | log(`mcpParams====== ${JSON.stringify(mcpParams)}`);
163 | // handle ai prompts
164 | const parameters =
165 | mcpParams?.params?.arguments || mcpParams?.arguments || mcpParams || {};
166 |
167 | if (parameters.prompt) {
168 | log(`Using provided prompt: ${parameters.prompt}`);
169 | } else {
170 | log("No prompt provided, using default");
171 | parameters.prompt = "A cute dog";
172 | }
173 |
174 | // Generate image
175 | const result: ImageGenerationResult =
176 | await drawThingsService.generateImage(parameters);
177 |
178 | // Handle generation result
179 | if (result.isError) {
180 | log(`Error generating image: ${result.errorMessage}`);
181 | throw new Error(result.errorMessage || "Unknown error");
182 | }
183 |
184 | if (!result.imageData && (!result.images || result.images.length === 0)) {
185 | log("No image data returned from generation");
186 | throw new Error("No image data returned from generation");
187 | }
188 |
189 | const imageData =
190 | result.imageData ||
191 | (result.images && result.images.length > 0
192 | ? result.images[0]
193 | : undefined);
194 | if (!imageData) {
195 | log("No valid image data available");
196 | throw new Error("No valid image data available");
197 | }
198 |
199 | log("Successfully generated image, returning directly via MCP");
200 |
201 | // calculate the difference between the start and end time (example value)
202 | const startTime = Date.now() - 2000; // assume the image generation took 2 seconds
203 | const endTime = Date.now();
204 |
205 | // build the response format
206 | const responseData = {
207 | image_paths: result.imagePath ? [result.imagePath] : [],
208 | metadata: {
209 | alt: `Image generated from prompt: ${parameters.prompt}`,
210 | inference_time_ms:
211 | result.metadata?.inference_time_ms || endTime - startTime,
212 | },
213 | };
214 |
215 | return {
216 | content: [
217 | {
218 | type: "text",
219 | text: JSON.stringify(responseData, null, 2),
220 | },
221 | ],
222 | };
223 | } catch (error) {
224 | log(
225 | `Error handling image generation: ${
226 | error instanceof Error ? error.message : String(error)
227 | }`
228 | );
229 | await logError(error);
230 | throw error;
231 | }
232 | }
233 | );
234 |
235 | // Main program
236 | async function main(): Promise<void> {
237 | try {
238 | log("Starting Draw Things MCP service...");
239 |
240 | // Print connection info to the console
241 | printConnectionInfo();
242 |
243 | log("Initializing Draw Things MCP service");
244 |
245 | // Enhanced API connection verification with direct method
246 | log("Checking Draw Things API connection before starting service...");
247 | const apiPort = process.env.DRAW_THINGS_API_PORT || 7888;
248 |
249 | // Final drawThingsService connection check
250 | const isApiConnected = await drawThingsService.checkApiConnection();
251 | if (!isApiConnected) {
252 | log("\nFAILED TO CONNECT TO DRAW THINGS API");
253 | log("Please make sure Draw Things is running and the API is enabled.");
254 | log(
255 | "The service will continue running, but image generation will not work until the API is available.\n"
256 | );
257 | } else {
258 | log("\nSUCCESSFULLY CONNECTED TO DRAW THINGS API");
259 | log("The service is ready to generate images.\n");
260 | drawThingsService.setBaseUrl(`http://127.0.0.1:${apiPort}`);
261 | }
262 |
263 | // Create transport and connect server
264 | log("Creating transport and connecting server...");
265 | const transport = new StdioServerTransport();
266 |
267 | // Connect server to transport
268 | log("Connecting server to transport...");
269 | await server.connect(transport);
270 | log("MCP Server started successfully!");
271 | } catch (error) {
272 | log(
273 | `Error in main program: ${
274 | error instanceof Error ? error.message : String(error)
275 | }`
276 | );
277 | await logError(error);
278 | }
279 | }
280 |
281 | main().catch(async (error) => {
282 | log("server.log", `${new Date().toISOString()} - ${error.stack || error}\n`);
283 | console.error(error);
284 | process.exit(1);
285 | });
286 |
```
--------------------------------------------------------------------------------
/test-mcp.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Draw Things MCP Test Script
5 | *
6 | * This script is used to test whether the Draw Things MCP service can start normally and process image generation requests.
7 | * It simulates MCP client behavior, sending requests to the MCP service and handling responses.
8 | */
9 |
10 | import { spawn } from 'child_process';
11 | import { writeFile, mkdir } from 'fs/promises';
12 | import path from 'path';
13 | import { fileURLToPath } from 'url';
14 |
15 | // Get the directory path of the current file
16 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
17 |
18 | // Ensure test output directory exists
19 | const testOutputDir = path.join(__dirname, 'test-output');
20 | try {
21 | await mkdir(testOutputDir, { recursive: true });
22 | console.log(`Test output directory created: ${testOutputDir}`);
23 | } catch (error) {
24 | if (error.code !== 'EEXIST') {
25 | console.error('Error creating test output directory:', error);
26 | process.exit(1);
27 | }
28 | }
29 |
30 | // MCP request example - format corrected to comply with MCP protocol
31 | const mcpRequest = {
32 | jsonrpc: "2.0",
33 | id: "test-request-" + Date.now(),
34 | method: "mcp.invoke",
35 | params: {
36 | tool: "generateImage",
37 | parameters: {
38 | prompt: "Beautiful Taiwan landscape, mountain and water painting style"
39 | }
40 | }
41 | };
42 |
43 | // Save request to file
44 | const requestFilePath = path.join(testOutputDir, 'mcp-request.json');
45 | try {
46 | await writeFile(requestFilePath, JSON.stringify(mcpRequest, null, 2));
47 | console.log(`MCP request saved to: ${requestFilePath}`);
48 | } catch (error) {
49 | console.error('Error saving MCP request:', error);
50 | process.exit(1);
51 | }
52 |
53 | console.log('Starting Draw Things MCP service for testing...');
54 |
55 | // Start MCP service process
56 | const mcpProcess = spawn('node', ['src/index.js'], {
57 | stdio: ['pipe', 'pipe', 'pipe']
58 | });
59 |
60 | // Add progress display timer
61 | let waitTime = 0;
62 | const progressInterval = setInterval(() => {
63 | waitTime += 30;
64 | console.log(`Waited ${waitTime} seconds... Image generation may take some time, please be patient`);
65 | }, 30000);
66 |
67 | // Cleanup function - called when terminating the service in any situation
68 | function cleanup() {
69 | clearInterval(progressInterval);
70 | if (mcpProcess && !mcpProcess.killed) {
71 | mcpProcess.kill();
72 | }
73 | }
74 |
75 | // Record standard output
76 | let stdoutData = '';
77 | mcpProcess.stdout.on('data', (data) => {
78 | const dataStr = data.toString();
79 | console.log(`MCP standard output: ${dataStr}`);
80 | stdoutData += dataStr;
81 |
82 | try {
83 | // Try to parse output as JSON
84 | const lines = dataStr.trim().split('\n');
85 | for (const line of lines) {
86 | if (!line.trim()) continue;
87 |
88 | try {
89 | const jsonData = JSON.parse(line);
90 | console.log('Parsed JSON response:', JSON.stringify(jsonData).substring(0, 100) + '...');
91 |
92 | // If it's an MCP response, save and analyze
93 | if (jsonData.id && (jsonData.result || jsonData.error)) {
94 | console.log('Received MCP response ID:', jsonData.id);
95 |
96 | if (jsonData.error) {
97 | console.error('MCP error response:', jsonData.error);
98 | const errorFile = path.join(testOutputDir, 'mcp-error.json');
99 | writeFile(errorFile, JSON.stringify(jsonData, null, 2))
100 | .catch(e => console.error('Failed to write error file:', e));
101 | } else if (jsonData.result) {
102 | console.log('MCP successful response type:', jsonData.result.content?.[0]?.type || 'unknown');
103 |
104 | // Determine if the response contains an error
105 | if (jsonData.result.isError) {
106 | console.error('Error response:', jsonData.result.content[0].text);
107 | const errorResultFile = path.join(testOutputDir, 'mcp-error-result.json');
108 | writeFile(errorResultFile, JSON.stringify(jsonData, null, 2))
109 | .catch(e => console.error('Failed to write error result file:', e));
110 | } else {
111 | // Successful response, should contain image data
112 | console.log('Successfully generated image!');
113 | if (jsonData.result.content && jsonData.result.content[0].type === 'image') {
114 | const imageData = jsonData.result.content[0].data;
115 | console.log(`Image data size: ${imageData.length} characters`);
116 |
117 | // Use immediately executed async function
118 | (async function() {
119 | try {
120 | const savedImagePath = await saveImage(imageData);
121 | console.log(`Image successfully saved to: ${savedImagePath}`);
122 |
123 | const successFile = path.join(testOutputDir, 'mcp-success.json');
124 | await writeFile(successFile, JSON.stringify(jsonData.result, null, 2));
125 | console.log('Successfully saved result information to JSON file');
126 |
127 | // Extend wait time to ensure all operations complete
128 | setTimeout(() => {
129 | console.log('Test completed, image processing successful, terminating MCP service...');
130 | cleanup();
131 | process.exit(0);
132 | }, 3000); // Increased to 3 seconds
133 | } catch (saveError) {
134 | console.error('Error saving image or results:', saveError);
135 | const errorFile = path.join(testOutputDir, 'mcp-save-error.json');
136 | writeFile(errorFile, JSON.stringify({ error: saveError.message }, null, 2))
137 | .catch(e => console.error('Failed to write error information:', e));
138 |
139 | // End test normally even if there's an error
140 | setTimeout(() => {
141 | console.log('Test completed, but errors occurred during image processing, terminating MCP service...');
142 | cleanup();
143 | process.exit(1);
144 | }, 3000);
145 | }
146 | })();
147 | }
148 | }
149 | }
150 | }
151 | } catch (parseError) {
152 | // Not valid JSON, might be regular log output
153 | // console.log('Non-JSON data:', line);
154 | }
155 | }
156 | } catch (error) {
157 | console.error('Error processing MCP output:', error);
158 | }
159 | });
160 |
161 | // Record standard error
162 | mcpProcess.stderr.on('data', (data) => {
163 | const logMsg = data.toString().trim();
164 | console.log(`MCP service log: ${logMsg}`);
165 |
166 | // Monitor specific log messages to confirm service status
167 | if (logMsg.includes('MCP service is ready')) {
168 | console.log('Detected MCP service is ready, preparing to send request...');
169 | // Delay sending request
170 | setTimeout(() => {
171 | sendRequest();
172 | }, 1000);
173 | }
174 | });
175 |
176 | // Handle process exit
177 | mcpProcess.on('close', (code) => {
178 | if (code !== 0 && code !== null) {
179 | console.error(`MCP service exited with code ${code}`);
180 |
181 | // Save output for diagnosis
182 | try {
183 | writeFile(path.join(testOutputDir, 'mcp-stdout.log'), stdoutData);
184 | console.log('MCP service standard output log saved');
185 | } catch (error) {
186 | console.error('Error saving output log:', error);
187 | }
188 |
189 | cleanup();
190 | process.exit(1);
191 | }
192 | });
193 |
194 | // Handle errors
195 | mcpProcess.on('error', (error) => {
196 | console.error('Error starting MCP service:', error);
197 | cleanup();
198 | process.exit(1);
199 | });
200 |
201 | // Function to send MCP request
202 | function sendRequest() {
203 | console.log('Sending image generation request...');
204 | console.log('Request content:', JSON.stringify(mcpRequest));
205 |
206 | // Ensure request string ends with newline
207 | const requestString = JSON.stringify(mcpRequest) + '\n';
208 | mcpProcess.stdin.write(requestString);
209 | console.log(`Sent ${requestString.length} bytes of request data`);
210 | console.log('\n========================================');
211 | console.log('Image generation has started, this may take a few minutes...');
212 | console.log('Wait progress will be displayed every 30 seconds');
213 | console.log('Please be patient, do not interrupt the test');
214 | console.log('========================================\n');
215 |
216 | // Save the raw request sent
217 | writeFile(path.join(testOutputDir, 'mcp-raw-request.txt'), requestString)
218 | .catch(e => console.error('Failed to save raw request:', e));
219 | }
220 |
221 | // Helper function: Save image
222 | async function saveImage(base64Data) {
223 | try {
224 | // Create buffer from base64 string
225 | const imageBuffer = Buffer.from(base64Data, 'base64');
226 |
227 | // Save image to file
228 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
229 | const imagePath = path.join(testOutputDir, `generated-image-${timestamp}.png`);
230 |
231 | await writeFile(imagePath, imageBuffer);
232 | console.log(`Generated image saved to: ${imagePath}`);
233 | return imagePath; // Make sure to return the saved path
234 | } catch (error) {
235 | console.error('Error saving image:', error);
236 | throw error; // Throw error so the upper function can catch and handle it
237 | }
238 | }
239 |
240 | // Don't send request immediately, wait for service log to indicate readiness
241 |
242 | // Timeout handling
243 | setTimeout(() => {
244 | console.error('Test timeout, terminating MCP service...');
245 | writeFile(path.join(testOutputDir, 'mcp-timeout.log'), 'Test timed out after 300 seconds')
246 | .catch(e => console.error('Failed to save timeout log:', e));
247 | cleanup();
248 | process.exit(1);
249 | }, 300000); // 5 minute timeout, providing more time to complete image generation
```
--------------------------------------------------------------------------------
/cursor-mcp-bridge.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Cursor MCP Bridge - Connect Cursor MCP and Draw Things API
5 | * Converts simple text prompts to proper JSON-RPC requests
6 | */
7 |
8 | import fs from 'fs';
9 | import path from 'path';
10 | import { fileURLToPath } from 'url';
11 | import readline from 'readline';
12 | import http from 'http';
13 |
14 | // Set up log file
15 | const logFile = 'cursor-mcp-bridge.log';
16 | function log(message) {
17 | const timestamp = new Date().toISOString();
18 | const logMessage = `${timestamp} - ${message}\n`;
19 | fs.appendFileSync(logFile, logMessage);
20 | console.error(logMessage); // Also output to stderr for debugging
21 | }
22 |
23 | // Enhanced error logging
24 | function logError(error, message = 'Error') {
25 | const timestamp = new Date().toISOString();
26 | const errorDetails = error instanceof Error ?
27 | `${error.message}\n${error.stack}` :
28 | String(error);
29 | const logMessage = `${timestamp} - [ERROR] ${message}: ${errorDetails}\n`;
30 | fs.appendFileSync(logFile, logMessage);
31 | console.error(logMessage);
32 | }
33 |
34 | // Initialize log
35 | log('Cursor MCP Bridge started');
36 | log('Waiting for input...');
37 |
38 | // Get current directory path
39 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
40 |
41 | // Ensure images directory exists
42 | const imagesDir = path.join(__dirname, 'images');
43 | if (!fs.existsSync(imagesDir)) {
44 | fs.mkdirSync(imagesDir, { recursive: true });
45 | log(`Created image storage directory: ${imagesDir}`);
46 | }
47 |
48 | // Verify API connection - direct connection check
49 | async function verifyApiConnection() {
50 | // Read API port from environment or use default
51 | const apiPort = process.env.DRAW_THINGS_API_PORT || 7888;
52 | const apiUrl = process.env.DRAW_THINGS_API_URL || `http://127.0.0.1:${apiPort}`;
53 |
54 | log(`Verifying API connection to ${apiUrl}`);
55 |
56 | return new Promise((resolve) => {
57 | // Try multiple endpoints
58 | const endpoints = ['/sdapi/v1/options', '/sdapi/v1/samplers', '/'];
59 | let endpointIndex = 0;
60 | let retryCount = 0;
61 | const maxRetries = 2;
62 |
63 | const tryEndpoint = () => {
64 | if (endpointIndex >= endpoints.length) {
65 | log('All endpoints failed, API connection verification failed');
66 | resolve(false);
67 | return;
68 | }
69 |
70 | const endpoint = endpoints[endpointIndex];
71 | const url = new URL(endpoint, apiUrl);
72 |
73 | log(`Trying endpoint: ${url.toString()}`);
74 |
75 | const options = {
76 | hostname: url.hostname,
77 | port: url.port,
78 | path: url.pathname,
79 | method: 'GET',
80 | timeout: 5000,
81 | headers: {
82 | 'User-Agent': 'DrawThingsMCP/1.0',
83 | 'Accept': 'application/json'
84 | }
85 | };
86 |
87 | const req = http.request(options, (res) => {
88 | log(`API connection check response: ${res.statusCode}`);
89 |
90 | // Any 2xx response is good
91 | if (res.statusCode >= 200 && res.statusCode < 300) {
92 | log('API connection verified successfully');
93 | resolve(true);
94 | return;
95 | }
96 |
97 | // Try next endpoint
98 | endpointIndex++;
99 | retryCount = 0;
100 | tryEndpoint();
101 | });
102 |
103 | req.on('error', (e) => {
104 | log(`API connection error (${endpoint}): ${e.message}`);
105 |
106 | // Retry same endpoint a few times
107 | if (retryCount < maxRetries) {
108 | retryCount++;
109 | log(`Retrying ${endpoint} (attempt ${retryCount}/${maxRetries})...`);
110 | setTimeout(tryEndpoint, 1000);
111 | return;
112 | }
113 |
114 | // Move to next endpoint
115 | endpointIndex++;
116 | retryCount = 0;
117 | tryEndpoint();
118 | });
119 |
120 | req.on('timeout', () => {
121 | log(`API connection timeout (${endpoint})`);
122 | req.destroy();
123 |
124 | // Retry same endpoint a few times
125 | if (retryCount < maxRetries) {
126 | retryCount++;
127 | log(`Retrying ${endpoint} after timeout (attempt ${retryCount}/${maxRetries})...`);
128 | setTimeout(tryEndpoint, 1000);
129 | return;
130 | }
131 |
132 | // Move to next endpoint
133 | endpointIndex++;
134 | retryCount = 0;
135 | tryEndpoint();
136 | });
137 |
138 | req.end();
139 | };
140 |
141 | tryEndpoint();
142 | });
143 | }
144 |
145 | // Initial API verification
146 | let apiVerified = false;
147 | verifyApiConnection().then(result => {
148 | apiVerified = result;
149 | if (result) {
150 | log('API connection verified on startup');
151 | } else {
152 | log('API connection verification failed on startup - will retry on requests');
153 | }
154 | });
155 |
156 | // Set up readline interface
157 | const rl = readline.createInterface({
158 | input: process.stdin,
159 | output: process.stdout,
160 | terminal: false
161 | });
162 |
163 | // Listen for line input
164 | rl.on('line', async (line) => {
165 | log(`Received input: ${line.substring(0, 100)}${line.length > 100 ? '...' : ''}`);
166 |
167 | // If API connection hasn't been verified yet, try again
168 | if (!apiVerified) {
169 | apiVerified = await verifyApiConnection();
170 | if (!apiVerified) {
171 | log('API connection still not available');
172 | // Return error response but continue processing the request
173 | // This allows the MCP service to handle the error properly
174 | }
175 | }
176 |
177 | // Check if input is already in JSON format
178 | try {
179 | const jsonInput = JSON.parse(line);
180 | log('Input is valid JSON, checking if it conforms to JSON-RPC 2.0 standard');
181 |
182 | // Check if it conforms to JSON-RPC 2.0 standard
183 | if (jsonInput.jsonrpc === "2.0" && jsonInput.method && jsonInput.id) {
184 | log('Input already conforms to JSON-RPC 2.0 standard, forwarding directly');
185 | process.stdout.write(line + '\n');
186 | return;
187 | } else {
188 | log('JSON format is valid but does not conform to JSON-RPC 2.0 standard, converting');
189 | processRequest(jsonInput);
190 | }
191 | return;
192 | } catch (e) {
193 | log(`Input is not valid JSON: ${e.message}`);
194 | }
195 |
196 | // Check if it's a plain text prompt from Cursor
197 | if (line && typeof line === 'string' && !line.startsWith('{')) {
198 | log('Detected plain text prompt, converting to JSON-RPC request');
199 |
200 | // Create request conforming to JSON-RPC 2.0 standard
201 | const request = {
202 | jsonrpc: "2.0",
203 | id: Date.now().toString(),
204 | method: "mcp.invoke",
205 | params: {
206 | tool: "generateImage",
207 | parameters: {
208 | prompt: line.trim()
209 | }
210 | }
211 | };
212 |
213 | processRequest(request);
214 | } else {
215 | log('Unrecognized input format, cannot process');
216 | sendErrorResponse('Unrecognized input format', "parse_error", -32700);
217 | }
218 | });
219 |
220 | // Process request
221 | function processRequest(request) {
222 | log(`Processing request: ${JSON.stringify(request).substring(0, 100)}...`);
223 |
224 | try {
225 | // Ensure request has the correct structure
226 | if (!request.jsonrpc) request.jsonrpc = "2.0";
227 | if (!request.id) request.id = Date.now().toString();
228 |
229 | // If no method, set to mcp.invoke
230 | if (!request.method) {
231 | request.method = "mcp.invoke";
232 | }
233 |
234 | // Process params
235 | if (!request.params) {
236 | // Try to build params from different sources
237 | if (request.prompt || request.parameters) {
238 | request.params = {
239 | tool: "generateImage",
240 | parameters: request.prompt
241 | ? { prompt: request.prompt }
242 | : (request.parameters || {})
243 | };
244 | } else {
245 | // No usable parameters found
246 | log('No usable parameters found, using empty object');
247 | request.params = {
248 | tool: "generateImage",
249 | parameters: {}
250 | };
251 | }
252 | } else if (!request.params.tool) {
253 | // Ensure there's a tool parameter
254 | request.params.tool = "generateImage";
255 | }
256 |
257 | // Ensure there are parameters
258 | if (!request.params.parameters) {
259 | request.params.parameters = {};
260 | }
261 |
262 | // Add API verification status to the request for debugging
263 | if (!apiVerified) {
264 | log('Warning: Adding API status information to request');
265 | request.params.parameters._apiVerified = apiVerified;
266 | }
267 |
268 | log(`Final request: ${JSON.stringify(request).substring(0, 150)}...`);
269 | process.stdout.write(JSON.stringify(request) + '\n');
270 | } catch (error) {
271 | logError(error, 'Error processing request');
272 | sendErrorResponse(`Error processing request: ${error.message}`, "internal_error", -32603);
273 | }
274 | }
275 |
276 | // Send error response conforming to JSON-RPC 2.0
277 | function sendErrorResponse(message, errorType = "invalid_request", code = -32600) {
278 | const errorResponse = {
279 | jsonrpc: "2.0",
280 | id: "error-" + Date.now(),
281 | error: {
282 | code: code,
283 | message: errorType,
284 | data: message
285 | }
286 | };
287 |
288 | log(`Sending error response: ${JSON.stringify(errorResponse)}`);
289 | process.stdout.write(JSON.stringify(errorResponse) + '\n');
290 | }
291 |
292 | // Buffer for assembling complete JSON responses
293 | let responseBuffer = '';
294 |
295 | // Handle responses from the MCP service
296 | process.stdin.on('data', (data) => {
297 | try {
298 | const dataStr = data.toString();
299 | log(`Received data chunk from MCP service: ${dataStr.substring(0, 100)}${dataStr.length > 100 ? '...' : ''}`);
300 |
301 | // Append to buffer to handle chunked responses
302 | responseBuffer += dataStr;
303 |
304 | // Check if we have a complete JSON object by trying to find matching braces
305 | if (isCompleteJson(responseBuffer)) {
306 | log('Detected complete JSON response, processing');
307 | processCompleteResponse(responseBuffer);
308 | responseBuffer = ''; // Clear buffer after processing
309 | } else {
310 | log('Incomplete JSON detected, buffering for more data');
311 | }
312 | } catch (error) {
313 | logError(error, 'Error handling MCP service data');
314 |
315 | // If there's an error, try to forward the original data as a fallback
316 | try {
317 | process.stdout.write(data);
318 | } catch (writeError) {
319 | logError(writeError, 'Error forwarding original data');
320 | }
321 | }
322 | });
323 |
324 | // Check if a string contains a complete JSON object
325 | function isCompleteJson(str) {
326 | try {
327 | JSON.parse(str);
328 | return true;
329 | } catch (e) {
330 | // Not complete or not valid JSON
331 | // Try basic brace matching as a fallback
332 | let openBraces = 0;
333 | let insideString = false;
334 | let escapeNext = false;
335 |
336 | for (let i = 0; i < str.length; i++) {
337 | const char = str[i];
338 |
339 | if (escapeNext) {
340 | escapeNext = false;
341 | continue;
342 | }
343 |
344 | if (char === '\\' && insideString) {
345 | escapeNext = true;
346 | continue;
347 | }
348 |
349 | if (char === '"') {
350 | insideString = !insideString;
351 | continue;
352 | }
353 |
354 | if (!insideString) {
355 | if (char === '{') openBraces++;
356 | if (char === '}') openBraces--;
357 | }
358 | }
359 |
360 | // Complete JSON object should have matching braces
361 | return openBraces === 0 && str.trim().startsWith('{') && str.trim().endsWith('}');
362 | }
363 | }
364 |
365 | // Process a complete response
366 | function processCompleteResponse(responseStr) {
367 | try {
368 | log(`Processing complete response: ${responseStr.substring(0, 100)}${responseStr.length > 100 ? '...' : ''}`);
369 |
370 | // Check for API error messages and update connection status
371 | if (responseStr.includes("Draw Things API is not running or cannot be connected")) {
372 | log('Detected API connection error in response');
373 | // Trigger a new API verification
374 | verifyApiConnection().then(result => {
375 | apiVerified = result;
376 | log(`API verification after error message: ${apiVerified ? 'successful' : 'failed'}`);
377 | });
378 | }
379 |
380 | // Try to parse as JSON
381 | const response = JSON.parse(responseStr);
382 | log('Successfully parsed MCP service response as JSON');
383 |
384 | // Check if it's an image generation result
385 | if (response.result && response.result.content) {
386 | // Find image content
387 | const imageContent = response.result.content.find(item => item.type === 'image');
388 | if (imageContent && imageContent.data) {
389 | // Save the image
390 | const timestamp = Date.now();
391 | const imagePath = path.join(imagesDir, `image_${timestamp}.png`);
392 |
393 | // Remove data:image/png;base64, prefix
394 | const base64Data = imageContent.data.replace(/^data:image\/\w+;base64,/, '');
395 |
396 | fs.writeFileSync(imagePath, Buffer.from(base64Data, 'base64'));
397 | log(`Image saved to: ${imagePath}`);
398 |
399 | // Add saved path info to the response
400 | response.result.imageSavedPath = imagePath;
401 |
402 | // Successful image generation indicates API is working
403 | apiVerified = true;
404 | }
405 | }
406 |
407 | // Forward the processed response
408 | process.stdout.write(JSON.stringify(response) + '\n');
409 | } catch (error) {
410 | logError(error, 'Error processing complete response');
411 |
412 | // Try to convert non-JSON response to proper JSON-RPC
413 | if (responseStr.trim() && !responseStr.trim().startsWith('{')) {
414 | log('Converting non-JSON response to proper JSON-RPC response');
415 |
416 | // Create a JSON-RPC response with the text as content
417 | const jsonResponse = {
418 | jsonrpc: "2.0",
419 | id: "response-" + Date.now(),
420 | result: {
421 | content: [{
422 | type: 'text',
423 | text: responseStr.trim()
424 | }]
425 | }
426 | };
427 |
428 | process.stdout.write(JSON.stringify(jsonResponse) + '\n');
429 | } else {
430 | // Forward original response as fallback
431 | log('Forwarding original response as fallback');
432 | process.stdout.write(responseStr + '\n');
433 | }
434 | }
435 | }
436 |
437 | // Handle end of input
438 | rl.on('close', () => {
439 | log('Input stream closed, program ending');
440 | process.exit(0);
441 | });
442 |
443 | // Handle errors
444 | process.on('uncaughtException', (error) => {
445 | logError(error, 'Uncaught exception');
446 | sendErrorResponse(`Error processing request: ${error.message}`, "internal_error", -32603);
447 | });
448 |
449 | log('Bridge service ready, waiting for Cursor input...');
450 |
451 | // Periodically check API connection
452 | setInterval(async () => {
453 | const prevStatus = apiVerified;
454 | apiVerified = await verifyApiConnection();
455 |
456 | if (prevStatus !== apiVerified) {
457 | log(`API connection status changed: ${prevStatus} -> ${apiVerified}`);
458 | }
459 | }, 60000); // Check every minute
```