# Directory Structure ``` ├── .gitignore ├── build │ └── main.js ├── CODE_OF_CONDUCT.md ├── LICENSE ├── nodemon.json ├── package-lock.json ├── package.json ├── README.md ├── src │ └── main.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # three-js-mcp 2 | 3 | MCP server for controlling ThreeJs source code, only basic function 4 | ``` -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- ```markdown 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official email address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [email protected]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | 135 | ``` -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "watch": ["src/**/*.ts"], 3 | "ignore": ["src/**/*.spec.ts"], 4 | "exec": "npx ts-node ./main.ts" 5 | } 6 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "three-js-mcp", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc && chmod 755 build/main.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "devDependencies": { 15 | "@types/express": "^4.17.1", 16 | "@types/ws": "^8.18.0", 17 | "typescript": "^5.8.2" 18 | }, 19 | "dependencies": { 20 | "@modelcontextprotocol/sdk": "^1.7.0", 21 | "express": "^4.17.1", 22 | "ws": "^8.18.1" 23 | }, 24 | "files": [ 25 | "build" 26 | ] 27 | } 28 | ``` -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- ```typescript 1 | import WebSocket, { WebSocketServer } from 'ws'; 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; 5 | 6 | // Initialize WebSocket server 7 | const wss = new WebSocketServer({ port: 8082 }); 8 | let clientConnection: WebSocket | null = null; 9 | let sceneState: any = null; 10 | 11 | wss.on('connection', (ws: WebSocket) => { 12 | console.error('Client connected'); 13 | clientConnection = ws; 14 | 15 | ws.on('message', (message: string) => { 16 | try { 17 | sceneState = JSON.parse(message); 18 | console.error('Updated scene state:', sceneState); 19 | } catch (e) { 20 | console.error('Invalid scene state message:', message); 21 | } 22 | }); 23 | 24 | ws.on('close', () => { 25 | console.error('Client disconnected'); 26 | clientConnection = null; 27 | sceneState = null; 28 | }); 29 | }); 30 | 31 | // Initialize MCP server 32 | const server = new Server( 33 | { name: "threejs_mcp_server", version: "1.0.0" }, 34 | { capabilities: { prompts: {}, tools: {} } } 35 | ); 36 | 37 | server.onerror = (error) => { 38 | console.error("[MCP Error]", error); 39 | }; 40 | 41 | process.on("SIGINT", async () => { 42 | wss.close() 43 | await server.close(); 44 | process.exit(0); 45 | }); 46 | 47 | // Define MCP tools 48 | const tools = [ 49 | { 50 | name: "addObject", 51 | description: "Add an object to the scene", 52 | inputSchema: { 53 | type: "object", 54 | properties: { 55 | type: { type: "string" }, 56 | position: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 }, 57 | color: { type: "string" } 58 | }, 59 | required: ["type", "position", "color"] 60 | } 61 | }, 62 | { 63 | name: "moveObject", 64 | description: "Move an object to a new position", 65 | inputSchema: { 66 | type: "object", 67 | properties: { 68 | id: { type: "string" }, 69 | position: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 } 70 | }, 71 | required: ["id", "position"] 72 | } 73 | }, 74 | { 75 | name: "removeObject", 76 | description: "Remove an object", 77 | inputSchema: { 78 | type: "object", 79 | properties: { 80 | id: { type: "string" } 81 | }, 82 | required: ["id"] 83 | } 84 | }, 85 | { 86 | name: "startRotation", 87 | description: "Start rotating an object around the y-axis", 88 | inputSchema: { 89 | type: "object", 90 | properties: { 91 | id: { type: "string" }, // The ID of the object (e.g., "cube1") 92 | speed: { type: "number" } // Rotation speed in radians per frame 93 | }, 94 | required: ["id", "speed"] 95 | } 96 | }, 97 | { 98 | name: "stopRotation", 99 | description: "Stop rotating an object", 100 | inputSchema: { 101 | type: "object", 102 | properties: { 103 | id: { type: "string" } // The ID of the object 104 | }, 105 | required: ["id"] 106 | } 107 | }, 108 | { 109 | name: "getSceneState", 110 | description: "Get the current scene state", 111 | inputSchema: { type: "object", properties: {} } 112 | } 113 | ]; 114 | 115 | const prompts = [ 116 | { 117 | name: "asset-creation-strategy", 118 | description: "Defines the preferred strategy for creating assets in ThreeJS", 119 | arguments: [] 120 | } 121 | ] 122 | 123 | // Handle tool listing 124 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools })); 125 | server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts })); 126 | 127 | // server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] })); 128 | 129 | // Handle tool calls 130 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 131 | const { name, arguments: input } = request.params; 132 | 133 | console.error("request: ===================", request) 134 | console.error("state: ===================", sceneState) 135 | 136 | if (name === "addObject") { 137 | if (!clientConnection) { 138 | return { 139 | content: [ 140 | { 141 | type: "text", 142 | text: "No client connection available" 143 | } 144 | ] 145 | }; 146 | } 147 | const command = { action: "addObject", ...(input as any) }; 148 | clientConnection.send(JSON.stringify(command)); 149 | return { 150 | content: [ 151 | { 152 | type: "text", 153 | text: "sent" 154 | } 155 | ] 156 | }; 157 | } else if (name === "moveObject") { 158 | if (!clientConnection) { 159 | return { 160 | content: [ 161 | { 162 | type: "text", 163 | text: "No client connection available" 164 | } 165 | ] 166 | }; 167 | } 168 | const command = { action: "moveObject", ...(input as any) }; 169 | clientConnection.send(JSON.stringify(command)); 170 | return { 171 | content: [ 172 | { 173 | type: "text", 174 | text: "sent" 175 | } 176 | ] 177 | }; 178 | } else if (name === 'removeObject') { 179 | if (!clientConnection) { 180 | return { 181 | content: [ 182 | { 183 | type: "text", 184 | text: "No client connection available" 185 | } 186 | ] 187 | }; 188 | } 189 | const command = { action: "removeObject", ...(input as any) }; 190 | clientConnection.send(JSON.stringify(command)); 191 | return { 192 | content: [ 193 | { 194 | type: "text", 195 | text: "sent" 196 | } 197 | ] 198 | }; 199 | } else if (name === "getSceneState") { 200 | if (sceneState) { 201 | return { 202 | content: [ 203 | { 204 | type: "text", 205 | text: JSON.stringify(sceneState?.data, null, 2) 206 | } 207 | ] 208 | }; 209 | } else { 210 | return { 211 | content: [ 212 | { 213 | type: "text", 214 | text: "No scene state available" 215 | } 216 | ] 217 | }; 218 | } 219 | } else if (name === "startRotation") { 220 | if (!clientConnection) { 221 | return { 222 | content: [ 223 | { 224 | type: "text", 225 | text: "No client connection available" 226 | } 227 | ] 228 | }; 229 | } 230 | const command = { 231 | action: "startRotation", 232 | id: input?.id, 233 | speed: input?.speed 234 | }; 235 | clientConnection.send(JSON.stringify(command)); 236 | return { 237 | content: [ 238 | { 239 | type: "text", 240 | text: "sent" 241 | } 242 | ] 243 | }; 244 | } else if (name === "stopRotation") { 245 | if (!clientConnection) { 246 | return { 247 | content: [ 248 | { 249 | type: "text", 250 | text: "No client connection available" 251 | } 252 | ] 253 | }; 254 | } 255 | const command = { 256 | action: "stopRotation", 257 | id: input?.id 258 | }; 259 | clientConnection.send(JSON.stringify(command)); 260 | return { 261 | content: [ 262 | { 263 | type: "text", 264 | text: "sent" 265 | } 266 | ] 267 | }; 268 | } 269 | return { 270 | content: [ 271 | { 272 | type: "text", 273 | text: "Tool not found" 274 | } 275 | ] 276 | };; 277 | }); 278 | 279 | server.setRequestHandler(GetPromptRequestSchema, async (request) => { 280 | if (request.params.name !== "asset-creation-strategy") { 281 | throw new Error("Unknown prompt"); 282 | } 283 | return { 284 | description: "Defines the preferred strategy for creating assets in ThreeJS", 285 | messages: [{ 286 | role: "assistant", 287 | content: { 288 | type: "text", 289 | text: ` 290 | When creating 3D content in ThreeJS, always start by checking if integrations are available: 291 | 0. Before anything, always check the scene from getSceneState() tool 292 | 1. Response of getSceneState() tool always give you with the format delimited by ### format ### 293 | ### 294 | { 295 | [ 296 | { 297 | id: "cube1", 298 | type: "cube", 299 | position: [0, 0, 0], 300 | color: "red", 301 | ... 302 | } 303 | ] 304 | } 305 | ### 306 | 2. Always find the id of the object in response of getSceneState() tool 307 | 3. Always use the id of the object to manipulate it with other tools 308 | ` 309 | } 310 | }] 311 | }; 312 | }); 313 | 314 | // Start MCP server with stdio transport 315 | async function main() { 316 | const transport = new StdioServerTransport(); 317 | await server.connect(transport); 318 | } 319 | 320 | main().catch((error) => { 321 | console.error("Server error:", error); 322 | process.exit(1); 323 | }); 324 | ```