coplay-mcp-server 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of coplay-mcp-server might be problematic. Click here for more details.

@@ -0,0 +1,3 @@
1
+ """Coplay MCP Server - Unity Editor integration via MCP protocol."""
2
+
3
+ __version__ = "1.2.1"
@@ -0,0 +1,168 @@
1
+ """Unity process discovery utilities."""
2
+
3
+ import logging
4
+ import platform
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ import psutil
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def find_unity_processes() -> List[psutil.Process]:
14
+ """Find all running Unity Editor processes."""
15
+ unity_processes = []
16
+
17
+ try:
18
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
19
+ try:
20
+ proc_info = proc.info
21
+ if not proc_info['name']:
22
+ continue
23
+
24
+ name = proc_info['name'].lower()
25
+
26
+ # Check for Unity process names across platforms
27
+ if any(unity_name in name for unity_name in [
28
+ 'unity', 'unity.exe', 'unity editor', 'unity hub'
29
+ ]):
30
+ # Skip Unity Hub, we only want Editor instances
31
+ if 'hub' not in name:
32
+ unity_processes.append(proc)
33
+
34
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
35
+ continue
36
+
37
+ except Exception as e:
38
+ logger.error(f"Error finding Unity processes: {e}")
39
+
40
+ return unity_processes
41
+
42
+
43
+ def extract_project_path_from_cmdline(cmdline: List[str]) -> Optional[str]:
44
+ """Extract Unity project path from command line arguments."""
45
+ if not cmdline:
46
+ return None
47
+
48
+ try:
49
+ # Look for -projectPath argument
50
+ for i, arg in enumerate(cmdline):
51
+ if arg.lower() == "-projectpath" and i + 1 < len(cmdline):
52
+ project_path = cmdline[i + 1]
53
+ # Remove quotes if present
54
+ project_path = project_path.strip('"\'')
55
+ return project_path
56
+
57
+ except Exception as e:
58
+ logger.debug(f"Error extracting project path from cmdline: {e}")
59
+
60
+ return None
61
+
62
+
63
+ def verify_unity_project(project_path: str) -> bool:
64
+ """Verify that a path is a valid Unity project."""
65
+ try:
66
+ project_dir = Path(project_path)
67
+ if not project_dir.exists() or not project_dir.is_dir():
68
+ return False
69
+
70
+ # Check for Assets folder (required for Unity projects)
71
+ assets_dir = project_dir / "Assets"
72
+ if not assets_dir.exists() or not assets_dir.is_dir():
73
+ return False
74
+
75
+ return True
76
+
77
+ except Exception as e:
78
+ logger.debug(f"Error verifying Unity project at {project_path}: {e}")
79
+ return False
80
+
81
+
82
+ def _discover_from_temp_dirs() -> List[str]:
83
+ """Discover Unity projects from MCP request directories in temp locations."""
84
+ project_roots = []
85
+
86
+ try:
87
+ # Common temp directory locations
88
+ temp_paths = []
89
+
90
+ if platform.system() == "Windows":
91
+ import tempfile
92
+ temp_paths.append(Path(tempfile.gettempdir()))
93
+ # Also check AppData/Local
94
+ try:
95
+ appdata_local = Path.home() / "AppData" / "Local"
96
+ if appdata_local.exists():
97
+ temp_paths.append(appdata_local)
98
+ except Exception:
99
+ pass
100
+ else:
101
+ # Unix-like systems
102
+ temp_paths.extend([
103
+ Path("/tmp"),
104
+ Path.home() / ".cache",
105
+ Path("/var/tmp")
106
+ ])
107
+
108
+ for temp_path in temp_paths:
109
+ if not temp_path.exists():
110
+ continue
111
+
112
+ try:
113
+ # Look for Coplay MCP request directories
114
+ for path in temp_path.rglob("**/Temp/Coplay/MCPRequests"):
115
+ if path.is_dir():
116
+ # Try to infer project root from temp path structure
117
+ temp_index = None
118
+ parts = path.parts
119
+
120
+ for i, part in enumerate(parts):
121
+ if part.lower() == "temp":
122
+ temp_index = i
123
+ break
124
+
125
+ if temp_index and temp_index > 0:
126
+ project_path = str(Path(*parts[:temp_index]))
127
+ if verify_unity_project(project_path):
128
+ project_roots.append(project_path)
129
+
130
+ except Exception as e:
131
+ logger.debug(f"Error searching temp path {temp_path}: {e}")
132
+
133
+ except Exception as e:
134
+ logger.debug(f"Error discovering from temp directories: {e}")
135
+
136
+ return project_roots
137
+
138
+
139
+ def discover_unity_project_roots() -> List[str]:
140
+ """Discover all Unity project roots from running processes."""
141
+ project_roots = []
142
+
143
+ try:
144
+ unity_processes = find_unity_processes()
145
+ logger.info(f"Found {len(unity_processes)} Unity processes")
146
+
147
+ for proc in unity_processes:
148
+ try:
149
+ cmdline = proc.cmdline()
150
+ project_path = extract_project_path_from_cmdline(cmdline)
151
+
152
+ if project_path and verify_unity_project(project_path):
153
+ if project_path not in project_roots:
154
+ project_roots.append(project_path)
155
+ logger.info(f"Found Unity project: {project_path}")
156
+
157
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
158
+ continue
159
+ except Exception as e:
160
+ logger.debug(f"Error processing Unity process {proc.pid}: {e}")
161
+
162
+ except Exception as e:
163
+ logger.error(f"Error discovering Unity project roots: {e}")
164
+
165
+ # Also check for MCP request directories in temp locations
166
+ project_roots.extend(_discover_from_temp_dirs())
167
+
168
+ return list(set(project_roots)) # Remove duplicates
@@ -0,0 +1,228 @@
1
+ """Main entry point for Coplay MCP Server using FastMCP."""
2
+
3
+ import logging
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional
7
+
8
+ from .process_discovery import discover_unity_project_roots
9
+ from mcp.server.fastmcp import Context, FastMCP
10
+ from mcp import ServerSession
11
+
12
+ from .unity_client import UnityRpcClient
13
+
14
+
15
+ def setup_logging() -> None:
16
+ """Set up logging configuration."""
17
+
18
+ # Configure logging
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
22
+ handlers=[
23
+ # Log errors to stderr (visible to MCP client)
24
+ logging.StreamHandler(sys.stderr),
25
+ ]
26
+ )
27
+
28
+ # Set specific log levels for noisy libraries
29
+ logging.getLogger("watchdog").setLevel(logging.WARNING)
30
+ logging.getLogger("asyncio").setLevel(logging.WARNING)
31
+
32
+
33
+ setup_logging()
34
+
35
+
36
+ # Initialize FastMCP server
37
+ mcp = FastMCP(name="coplay-mcp-server")
38
+
39
+ # Global Unity client instance
40
+ unity_client = UnityRpcClient()
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ @mcp.tool()
46
+ async def set_unity_project_root(
47
+ unity_project_root: str,
48
+ ctx: Context[ServerSession, None]
49
+ ) -> str:
50
+ """Set the Unity project root path for the MCP server instance. This tool should be called before using any other Unity tools."""
51
+ try:
52
+ logger.info(f"Setting Unity project root to: {unity_project_root}")
53
+
54
+ if not unity_project_root or not unity_project_root.strip():
55
+ raise ValueError("Unity project root cannot be empty")
56
+
57
+ # Set the Unity project root in the RPC client
58
+ unity_client.set_unity_project_root(unity_project_root)
59
+
60
+ result = f"Unity project root set to: {unity_project_root}"
61
+ logger.info("Unity project root set successfully")
62
+ return result
63
+
64
+ except Exception as e:
65
+ logger.error(f"Failed to set Unity project root: {e}")
66
+ raise
67
+
68
+
69
+ @mcp.tool()
70
+ async def list_unity_project_roots(ctx: Context[ServerSession, None]) -> Any:
71
+ """List all project roots of currently open Unity instances. This tool discovers all running Unity Editor instances and returns their project root directories."""
72
+ try:
73
+ logger.info("Discovering Unity project roots...")
74
+
75
+ project_roots = discover_unity_project_roots()
76
+ return {
77
+ "count": len(project_roots),
78
+ "projectRoots": [
79
+ {
80
+ "projectRoot": root,
81
+ "projectName": root.split("/")[-1] if "/" in root else root.split("\\")[-1]
82
+ }
83
+ for root in project_roots
84
+ ]
85
+ }
86
+ except Exception as e:
87
+ logger.error(f"Failed to list Unity project roots: {e}")
88
+ raise
89
+
90
+
91
+ @mcp.tool()
92
+ async def execute_script(
93
+ file_path: str,
94
+ method_name: str = "Execute",
95
+ arguments: Optional[Dict[str, Any]] = None,
96
+ ctx: Context[ServerSession, None] = None
97
+ ) -> Any:
98
+ """Execute arbitrary C# code inside of the Unity Editor. The supplied code file must contain a class with a public static method that can be specified via method_name parameter (defaults to 'Execute'). This static method can return an object which will be serialized to JSON."""
99
+ try:
100
+ logger.info(f"Executing script: {file_path}, method: {method_name}")
101
+
102
+ params = {
103
+ "filePath": file_path,
104
+ "methodName": method_name
105
+ }
106
+ if arguments:
107
+ params["arguments"] = arguments
108
+
109
+ return await unity_client.execute_request("executeScript", params)
110
+ except Exception as e:
111
+ logger.error(f"Failed to execute script: {e}")
112
+ raise
113
+
114
+
115
+ @mcp.tool()
116
+ async def get_unity_logs(
117
+ skip: int,
118
+ limit: int,
119
+ show_logs: bool = True,
120
+ show_warnings: bool = True,
121
+ show_errors: bool = True,
122
+ search_term: Optional[str] = None,
123
+ ctx: Context[ServerSession, None] = None
124
+ ) -> Any:
125
+ """Gets logs sorted from newest to oldest, applying filtering and pagination."""
126
+ try:
127
+ logger.debug(f"Getting Unity logs: skip={skip}, limit={limit}")
128
+
129
+ params = {
130
+ "skip": skip,
131
+ "limit": limit,
132
+ "showLogs": show_logs,
133
+ "showWarnings": show_warnings,
134
+ "showErrors": show_errors
135
+ }
136
+ if search_term:
137
+ params["searchTerm"] = search_term
138
+
139
+ return await unity_client.execute_request("getUnityLogs", params)
140
+ except Exception as e:
141
+ logger.error(f"Failed to get Unity logs: {e}")
142
+ raise
143
+
144
+
145
+ @mcp.tool()
146
+ async def get_unity_editor_state(ctx: Context[ServerSession, None]) -> Any:
147
+ """Retrieve the current state of the Unity Editor, excluding scene hierarchy."""
148
+ try:
149
+ logger.info("Getting Unity Editor state...")
150
+
151
+ result = await unity_client.execute_request("getUnityEditorState", {})
152
+ return result
153
+
154
+ except Exception as e:
155
+ logger.error(f"Failed to get Unity Editor state: {e}")
156
+ raise
157
+
158
+
159
+ @mcp.tool()
160
+ async def list_game_objects_in_hierarchy(
161
+ reference_object_path: Optional[str] = None,
162
+ name_filter: Optional[str] = None,
163
+ tag_filter: Optional[str] = None,
164
+ component_filter: Optional[str] = None,
165
+ include_inactive: Optional[bool] = None,
166
+ limit: Optional[int] = None,
167
+ skip: Optional[int] = None,
168
+ only_paths: Optional[bool] = None,
169
+ ctx: Context[ServerSession, None] = None
170
+ ) -> Any:
171
+ """List game objects in the hierarchy with optional filtering capabilities. Uses breadth-first traversal to prioritize objects closer to the root. Results are truncated if they exceed the limit, with a message indicating the truncation."""
172
+ try:
173
+ logger.debug("Listing game objects in hierarchy...")
174
+
175
+ params = {}
176
+ if reference_object_path is not None:
177
+ params["referenceObjectPath"] = reference_object_path
178
+ if name_filter is not None:
179
+ params["nameFilter"] = name_filter
180
+ if tag_filter is not None:
181
+ params["tagFilter"] = tag_filter
182
+ if component_filter is not None:
183
+ params["componentFilter"] = component_filter
184
+ if include_inactive is not None:
185
+ params["includeInactive"] = include_inactive
186
+ if limit is not None:
187
+ params["limit"] = limit
188
+ if skip is not None:
189
+ params["skip"] = skip
190
+ if only_paths is not None:
191
+ params["onlyPaths"] = only_paths
192
+
193
+ return await unity_client.execute_request("listGameObjectsInHierarchy", params)
194
+ except Exception as e:
195
+ logger.error(f"Failed to list game objects: {e}")
196
+ raise
197
+
198
+
199
+ @mcp.tool()
200
+ async def create_coplay_task(
201
+ prompt: str,
202
+ file_paths: Optional[str] = None,
203
+ model: Optional[str] = None,
204
+ ctx: Context[ServerSession, None] = None
205
+ ) -> Any:
206
+ """Creates a new task in the Unity Editor with the specified prompt and optional file attachments. This will start a new chat thread and submit the prompt for processing."""
207
+ try:
208
+ logger.info(f"Creating task with prompt: {prompt[:100]}...")
209
+
210
+ params = {"prompt": prompt}
211
+ if file_paths:
212
+ params["file_paths"] = file_paths
213
+ if model:
214
+ params["model"] = model
215
+
216
+ result = await unity_client.execute_request("createTask", params)
217
+ return result
218
+ except Exception as e:
219
+ logger.error(f"Failed to create task: {e}")
220
+ raise
221
+
222
+
223
+ def main():
224
+ mcp.run()
225
+
226
+
227
+ if __name__ == "__main__":
228
+ main()
@@ -0,0 +1,224 @@
1
+ """Unity RPC client for file-based communication with Unity Editor."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import uuid
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional
9
+ import concurrent.futures
10
+
11
+ import aiofiles
12
+ from watchdog.events import FileSystemEventHandler
13
+ from watchdog.observers import Observer
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class UnityRpcClient:
19
+ """Client for communicating with Unity Editor via file-based RPC."""
20
+
21
+ def __init__(self) -> None:
22
+ self._unity_project_root: Optional[str] = None
23
+ self._pending_requests: Dict[str, concurrent.futures.Future[Any]] = {}
24
+ self._observer: Optional[Observer] = None
25
+ self._response_handler: Optional[ResponseFileHandler] = None
26
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
27
+
28
+ def set_unity_project_root(self, project_root: str) -> None:
29
+ """Set the Unity project root and start watching for responses."""
30
+ if self._unity_project_root == project_root:
31
+ return
32
+
33
+ # Stop existing watcher if any
34
+ self._stop_file_watcher()
35
+
36
+ self._unity_project_root = project_root
37
+ self._start_file_watcher()
38
+ logger.info(f"Unity project root set to: {project_root}")
39
+
40
+ def _start_file_watcher(self) -> None:
41
+ """Start watching for response files."""
42
+ if not self._unity_project_root:
43
+ return
44
+
45
+ requests_dir = Path(self._unity_project_root) / "Temp" / "Coplay" / "MCPRequests"
46
+ if not requests_dir.exists():
47
+ logger.warning(f"Unity requests directory does not exist: {requests_dir}")
48
+ return
49
+
50
+ # Store the current event loop for thread-safe communication
51
+ try:
52
+ self._loop = asyncio.get_running_loop()
53
+ except RuntimeError:
54
+ logger.warning("No running event loop found, file watching may not work properly")
55
+ return
56
+
57
+ self._response_handler = ResponseFileHandler(self._handle_response_file_sync, self._loop)
58
+ self._observer = Observer()
59
+ self._observer.schedule(
60
+ self._response_handler,
61
+ str(requests_dir),
62
+ recursive=False
63
+ )
64
+ self._observer.start()
65
+ logger.info(f"Started watching for responses in: {requests_dir}")
66
+
67
+ def _stop_file_watcher(self) -> None:
68
+ """Stop watching for response files."""
69
+ if self._observer:
70
+ self._observer.stop()
71
+ self._observer.join()
72
+ self._observer = None
73
+ self._response_handler = None
74
+
75
+ def _handle_response_file_sync(self, file_path: Path) -> None:
76
+ """Handle a response file from Unity (synchronous version for thread safety)."""
77
+ try:
78
+ logger.info(f"Handling file change: {file_path}")
79
+
80
+ if not file_path.name.startswith("response_") or not file_path.name.endswith(".json"):
81
+ return
82
+
83
+ # Read response file synchronously
84
+ try:
85
+ with open(file_path, 'r', encoding='utf-8') as f:
86
+ response_json = f.read()
87
+ except Exception as e:
88
+ logger.error(f"Failed to read response file {file_path}: {e}")
89
+ return
90
+
91
+ response_data = json.loads(response_json)
92
+ request_id = response_data.get('id')
93
+ if request_id not in self._pending_requests:
94
+ logger.warning(f"No pending request found for ID: {request_id}")
95
+ return
96
+
97
+ future = self._pending_requests.pop(request_id)
98
+
99
+ if "error" in response_data and response_data["error"]:
100
+ error_msg = response_data["error"]
101
+ if isinstance(error_msg, dict):
102
+ error_msg = error_msg.get("message", str(error_msg))
103
+ future.set_exception(Exception(str(error_msg)))
104
+ else:
105
+ future.set_result(response_data.get("result"))
106
+
107
+ # Clean up response file
108
+ try:
109
+ file_path.unlink()
110
+ logger.debug(f"Deleted response file: {file_path}")
111
+ except Exception as e:
112
+ logger.warning(f"Failed to delete response file {file_path}: {e}")
113
+
114
+ except Exception as e:
115
+ logger.error(f"Error handling response file {file_path}: {e}")
116
+
117
+ async def execute_request(
118
+ self,
119
+ method: str,
120
+ params: Optional[Dict[str, Any]] = None,
121
+ timeout: float = 60.0
122
+ ) -> Any:
123
+ """Execute an RPC request to Unity Editor."""
124
+ if not self._unity_project_root:
125
+ raise RuntimeError("Unity project root is not set. Call set_unity_project_root first.")
126
+
127
+ requests_dir = Path(self._unity_project_root) / "Temp" / "Coplay" / "MCPRequests"
128
+ if not requests_dir.exists():
129
+ raise RuntimeError("Unity Editor is not running at the specified project root")
130
+
131
+ request_id = str(uuid.uuid4())
132
+ request_file = requests_dir / f"req_{request_id}.json"
133
+
134
+ # Create request data
135
+ request_data = {
136
+ "jsonrpc": "2.0",
137
+ "id": request_id,
138
+ "method": method,
139
+ "params": params or {}
140
+ }
141
+
142
+ # Create concurrent.futures.Future for thread-safe completion
143
+ future: concurrent.futures.Future[Any] = concurrent.futures.Future()
144
+ self._pending_requests[request_id] = future
145
+
146
+ try:
147
+ # Write request file
148
+ async with aiofiles.open(request_file, 'w', encoding='utf-8') as f:
149
+ await f.write(json.dumps(request_data, indent=2))
150
+
151
+ logger.debug(f"Created request file: {request_file}")
152
+
153
+ # Wait for response with timeout using asyncio.wrap_future
154
+ try:
155
+ wrapped_future = asyncio.wrap_future(future)
156
+ result = await asyncio.wait_for(wrapped_future, timeout=timeout)
157
+ return result
158
+ except asyncio.TimeoutError:
159
+ raise TimeoutError(f"Request {method} timed out after {timeout} seconds")
160
+
161
+ except Exception as e:
162
+ # Clean up on error
163
+ self._pending_requests.pop(request_id, None)
164
+ try:
165
+ if request_file.exists():
166
+ request_file.unlink()
167
+ except Exception:
168
+ pass
169
+ raise e
170
+
171
+ def close(self) -> None:
172
+ """Close the Unity RPC client and clean up resources."""
173
+ self._stop_file_watcher()
174
+
175
+ # Cancel any pending requests
176
+ for future in self._pending_requests.values():
177
+ if not future.done():
178
+ future.cancel()
179
+ self._pending_requests.clear()
180
+
181
+
182
+ class ResponseFileHandler(FileSystemEventHandler):
183
+ """File system event handler for Unity response files."""
184
+
185
+ def __init__(self, callback, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
186
+ super().__init__()
187
+ self._callback = callback
188
+ self._loop = loop
189
+ self._processed_files: set[str] = set()
190
+ self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=2)
191
+
192
+ def on_created(self, event) -> None:
193
+ """Handle file creation events."""
194
+ if event.is_directory:
195
+ return
196
+
197
+ file_path = Path(event.src_path)
198
+ if file_path.name.startswith("response_") and file_path.name.endswith(".json"):
199
+ self._process_file_threadsafe(file_path)
200
+
201
+ def on_modified(self, event) -> None:
202
+ """Handle file modification events."""
203
+ if event.is_directory:
204
+ return
205
+
206
+ file_path = Path(event.src_path)
207
+ if file_path.name.startswith("response_") and file_path.name.endswith(".json"):
208
+ # Avoid processing the same file multiple times
209
+ if str(file_path) not in self._processed_files:
210
+ self._processed_files.add(str(file_path))
211
+ self._process_file_threadsafe(file_path)
212
+
213
+ def _process_file_threadsafe(self, file_path: Path) -> None:
214
+ """Process a response file in a thread-safe manner."""
215
+ def process_with_delay():
216
+ # Small delay to ensure file is fully written
217
+ import time
218
+ time.sleep(0.1)
219
+
220
+ if file_path.exists():
221
+ self._callback(file_path)
222
+
223
+ # Submit to thread pool to avoid blocking the file watcher
224
+ self._executor.submit(process_with_delay)
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: coplay-mcp-server
3
+ Version: 1.2.1
4
+ Summary: A Model Context Protocol (MCP) server for Coplay, providing Unity integration capabilities
5
+ Project-URL: Homepage, https://coplay.dev/
6
+ Project-URL: Repository, https://github.com/CoplayDev/coplay-unity-plugin
7
+ Project-URL: Documentation, https://docs.coplay.dev/
8
+ Project-URL: Issues, https://github.com/CoplayDev/coplay-unity-plugin/issues
9
+ Author-email: Coplay Dev <dev@coplay.com>
10
+ Maintainer-email: Coplay Dev <dev@coplay.com>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: coplay,mcp,model-context-protocol,unity,unity-editor
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Topic :: Games/Entertainment
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: aiofiles>=24.1.0
24
+ Requires-Dist: anyio>=4.10.0
25
+ Requires-Dist: mcp[cli]>=1.12.4
26
+ Requires-Dist: psutil>=7.0.0
27
+ Requires-Dist: pydantic>=2.11.7
28
+ Requires-Dist: watchdog>=6.0.0
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Coplay MCP Server
32
+
33
+ A Model Context Protocol (MCP) server for Coplay, providing Unity Editor integration capabilities through MCP tools.
34
+
35
+ ## Features
36
+
37
+ - **Unity Project Discovery**: Automatically discover running Unity Editor instances and their project roots
38
+ - **Unity Editor State**: Retrieve current Unity Editor state and scene hierarchy information
39
+ - **Script Execution**: Execute arbitrary C# scripts within the Unity Editor
40
+ - **Log Management**: Access and filter Unity console logs
41
+ - **GameObject Hierarchy**: List and filter GameObjects in the scene hierarchy
42
+ - **Task Creation**: Create new Coplay tasks directly from MCP clients
43
+
44
+ ## Usage
45
+
46
+ ### As an MCP server
47
+
48
+ Add to your MCP client configuration:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "coplay-testpypi": {
54
+ "autoApprove": [],
55
+ "disabled": false,
56
+ "timeout": 60,
57
+ "type": "stdio",
58
+ "command": "uvx",
59
+ "args": [
60
+ "coplay-mcp-server@latest"
61
+ ]
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ ## Available Tools
68
+
69
+ - `set_unity_project_root` - Set the Unity project root path
70
+ - `list_unity_project_roots` - Discover all running Unity Editor instances
71
+ - `execute_script` - Execute C# scripts in Unity Editor
72
+ - `get_unity_logs` - Retrieve Unity console logs with filtering
73
+ - `get_unity_editor_state` - Get current Unity Editor state
74
+ - `list_game_objects_in_hierarchy` - List GameObjects with filtering options
75
+ - `create_coplay_task` - Create new Coplay tasks
76
+
77
+ ## Development
78
+
79
+ To launch the server in development mode:
80
+
81
+ ```bash
82
+ uv run mcp dev coplay_mcp_server/server.py
83
+ ```
84
+
85
+ ### Prerequisites
86
+
87
+ - Python 3.11+
88
+ - [uv](https://docs.astral.sh/uv/) package manager
89
+ - Unity Editor with Coplay plugin installed
90
+
91
+ ### Project Structure
92
+
93
+ - `coplay_mcp_server/server.py` - Main MCP server implementation
94
+ - `coplay_mcp_server/unity_client.py` - Unity RPC client for communication
95
+ - `coplay_mcp_server/process_discovery.py` - Process discovery utilities
96
+ - `pyproject.toml` - Project configuration and dependencies
97
+
98
+ ## License
99
+
100
+ MIT License - see LICENSE file for details.
@@ -0,0 +1,9 @@
1
+ coplay_mcp_server/__init__.py,sha256=h8oQ4XIoeJ86DSsfvAq_1luDAHU2nHr4L6er3wCPdN8,95
2
+ coplay_mcp_server/process_discovery.py,sha256=kcY7dlyNJUG3EjdDJKAzf_8RqqM2zgqpjK6ZJTqc4aU,6151
3
+ coplay_mcp_server/server.py,sha256=g4EcyUvO6C-ZPT1cblixbJ1sBHndt34oihRbQxuIFuk,7792
4
+ coplay_mcp_server/unity_client.py,sha256=bPzDv9-dsS0tXha-Q9BBedoW63L5GadFHJXQOrOXEZA,8622
5
+ coplay_mcp_server-1.2.1.dist-info/METADATA,sha256=_KJ98ZbPtNQ55naSVXxyx-VR-d13VlQwEMeMwORU1xw,3263
6
+ coplay_mcp_server-1.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ coplay_mcp_server-1.2.1.dist-info/entry_points.txt,sha256=FYOh7Ksh98Q-cp6pgvof0dLlgR8vyMl8VFeKhGVxgjI,68
8
+ coplay_mcp_server-1.2.1.dist-info/licenses/LICENSE,sha256=K3xG9ipglyxcG95EQnM3rAnOAbRQ7wsB3GsQAaIo6Ho,1088
9
+ coplay_mcp_server-1.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ coplay-mcp-server = coplay_mcp_server.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Coplay Dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.