coplay-mcp-server 1.4.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.
@@ -0,0 +1,3 @@
1
+ """Coplay MCP Server - Unity Editor integration via MCP protocol."""
2
+
3
+ __version__ = "1.4.1"
@@ -0,0 +1,370 @@
1
+ """Code generator for MCP tools from JSON schema files."""
2
+
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Dict, Any, List, Optional
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class MCPToolCodeGenerator:
12
+ """Generates Python code for MCP tools from JSON schema files."""
13
+
14
+ def __init__(self, backend_path: Optional[Path] = None):
15
+ """Initialize the code generator.
16
+
17
+ Args:
18
+ backend_path: Path to the Backend directory. If None, will try to find it automatically.
19
+ """
20
+ self.backend_path = backend_path or self._find_backend_path()
21
+ self.generated_tools_path = Path(__file__).parent / "generated_tools"
22
+
23
+ # Schema files to process (from Backend/coplay/tool_schemas/)
24
+ self.SCHEMA_FILES = [
25
+ "unity_functions_schema.json",
26
+ "image_tool_schema.json",
27
+ "coplay_tool_schema.json",
28
+ "agent_tool_schema.json",
29
+ "package_tool_schema.json",
30
+ "input_action_tool_schema.json",
31
+ "ui_functions_schema.json",
32
+ "snapping_functions_schema.json",
33
+ "scene_view_functions_schema.json",
34
+ "profiler_functions_schema.json",
35
+ "screenshot_tool_schema.json",
36
+ ]
37
+
38
+ def _find_backend_path(self) -> Path:
39
+ """Try to find the Backend directory automatically."""
40
+ current_path = Path(__file__).parent
41
+
42
+ # Look for Backend directory in parent directories
43
+ for parent in [current_path] + list(current_path.parents):
44
+ backend_path = parent / "Backend"
45
+ if (
46
+ backend_path.exists()
47
+ and (backend_path / "coplay" / "tool_schemas").exists()
48
+ ):
49
+ return backend_path
50
+
51
+ # Fallback to relative path
52
+ return Path("../Backend")
53
+
54
+ def generate_all_tools(self) -> None:
55
+ """Generate Python files for all schema files."""
56
+ logger.info("Starting MCP tool code generation")
57
+
58
+ tool_schemas_path = self.backend_path / "coplay" / "tool_schemas"
59
+ if not tool_schemas_path.exists():
60
+ logger.error(f"Tool schemas directory not found: {tool_schemas_path}")
61
+ return
62
+
63
+ total_tools = 0
64
+ for schema_file in self.SCHEMA_FILES:
65
+ schema_path = tool_schemas_path / schema_file
66
+ if schema_path.exists():
67
+ tools_count = self.generate_tools_from_schema(schema_path)
68
+ total_tools += tools_count
69
+ logger.info(f"Generated {tools_count} tools from {schema_file}")
70
+ else:
71
+ logger.warning(f"Schema file not found: {schema_path}")
72
+
73
+ logger.info(f"Total tools generated: {total_tools}")
74
+
75
+ def generate_tools_from_schema(self, schema_path: Path) -> int:
76
+ """Generate Python file for a single schema file.
77
+
78
+ Args:
79
+ schema_path: Path to the JSON schema file
80
+
81
+ Returns:
82
+ Number of tools generated
83
+ """
84
+ try:
85
+ schema_data = json.loads(schema_path.read_text(encoding="utf-8"))
86
+
87
+ if not isinstance(schema_data, list):
88
+ logger.error(f"Schema {schema_path.name} is not a list format")
89
+ return 0
90
+
91
+ # Generate Python file name from schema file name
92
+ python_file_name = schema_path.stem.replace("_schema", "_tools") + ".py"
93
+ output_path = self.generated_tools_path / python_file_name
94
+
95
+ # Generate Python code
96
+ python_code = self._generate_python_file(schema_data, schema_path.stem)
97
+
98
+ # Write to file
99
+ output_path.write_text(python_code, encoding="utf-8")
100
+
101
+ # Count tools
102
+ tools_count = len(
103
+ [tool for tool in schema_data if tool.get("type") == "function"]
104
+ )
105
+ return tools_count
106
+
107
+ except Exception as e:
108
+ logger.error(f"Error generating tools from {schema_path}: {e}")
109
+ return 0
110
+
111
+ def _generate_python_file(
112
+ self, schema_data: List[Dict[str, Any]], schema_name: str
113
+ ) -> str:
114
+ """Generate Python code for a schema file.
115
+
116
+ Args:
117
+ schema_data: List of tool definitions from schema
118
+ schema_name: Name of the schema file (without extension)
119
+
120
+ Returns:
121
+ Generated Python code as string
122
+ """
123
+ lines = []
124
+
125
+ # File header
126
+ lines.append(f'"""Generated MCP tools from {schema_name}.json"""')
127
+ lines.append("")
128
+ lines.append("import logging")
129
+ lines.append("from typing import Annotated, Optional, Any, Dict, Literal")
130
+ lines.append("from pydantic import Field")
131
+ lines.append("from fastmcp import FastMCP")
132
+ lines.append("from ..unity_client import UnityRpcClient")
133
+ lines.append("")
134
+ lines.append("logger = logging.getLogger(__name__)")
135
+ lines.append("")
136
+ lines.append("# Global references to be set by register_tools")
137
+ lines.append("_mcp: Optional[FastMCP] = None")
138
+ lines.append("_unity_client: Optional[UnityRpcClient] = None")
139
+ lines.append("")
140
+ lines.append("")
141
+
142
+ # Generate function for each tool
143
+ for tool_def in schema_data:
144
+ if tool_def.get("type") != "function":
145
+ continue
146
+
147
+ function_def = tool_def.get("function")
148
+ if not function_def:
149
+ continue
150
+
151
+ tool_code = self._generate_tool_function(function_def)
152
+ lines.extend(tool_code)
153
+ lines.append("")
154
+
155
+ # Registration function
156
+ lines.append(
157
+ "def register_tools(mcp: FastMCP, unity_client: UnityRpcClient) -> None:"
158
+ )
159
+ lines.append(
160
+ f' """Register all tools from {schema_name} with the MCP server."""'
161
+ )
162
+ lines.append(" global _mcp, _unity_client")
163
+ lines.append(" _mcp = mcp")
164
+ lines.append(" _unity_client = unity_client")
165
+ lines.append("")
166
+
167
+ # Register each tool function
168
+ for tool_def in schema_data:
169
+ if tool_def.get("type") != "function":
170
+ continue
171
+
172
+ function_def = tool_def.get("function")
173
+ if not function_def:
174
+ continue
175
+
176
+ tool_name = function_def.get("name", "")
177
+ lines.append(f" # Register {tool_name}")
178
+ lines.append(f" mcp.tool()({tool_name})")
179
+
180
+ lines.append("")
181
+
182
+ return "\n".join(lines)
183
+
184
+ def _generate_tool_function(self, func_def: Dict[str, Any]) -> List[str]:
185
+ """Generate Python function code for a single tool.
186
+
187
+ Args:
188
+ func_def: Function definition from schema
189
+
190
+ Returns:
191
+ List of code lines
192
+ """
193
+ lines = []
194
+
195
+ tool_name = func_def.get("name", "")
196
+ description = func_def.get("description", "")
197
+ parameters = func_def.get("parameters", {})
198
+
199
+ properties = parameters.get("properties", {})
200
+ required = parameters.get("required", [])
201
+
202
+ # Function signature (no decorator at module level)
203
+ lines.append(f"async def {tool_name}(")
204
+
205
+ # Parameters - separate required and optional to avoid syntax errors
206
+ required_params = []
207
+ optional_params = []
208
+
209
+ for param_name, param_def in properties.items():
210
+ param_description = param_def.get("description", "")
211
+ is_required = param_name in required
212
+
213
+ # Convert JSON schema type to Python type with Annotated description
214
+ python_type = self._get_python_type_annotation(
215
+ param_def, param_description, is_required
216
+ )
217
+
218
+ if is_required:
219
+ required_params.append(f" {param_name}: {python_type},")
220
+ else:
221
+ optional_params.append(f" {param_name}: {python_type} = None,")
222
+
223
+ # Add required parameters first, then optional parameters
224
+ lines.extend(required_params)
225
+ lines.extend(optional_params)
226
+ lines.append(") -> Any:")
227
+
228
+ # Docstring
229
+ lines.append(f' """{description}"""')
230
+ lines.append(" try:")
231
+ lines.append(
232
+ f' logger.debug(f"Executing {tool_name} with parameters: {{locals()}}")'
233
+ )
234
+ lines.append("")
235
+ lines.append(" # Prepare parameters for Unity RPC call")
236
+ lines.append(" params = {}")
237
+
238
+ # Parameter preparation
239
+ for param_name in properties.keys():
240
+ lines.append(f" if {param_name} is not None:")
241
+ lines.append(f" params['{param_name}'] = str({param_name})")
242
+
243
+ lines.append("")
244
+ lines.append(" # Execute Unity RPC call")
245
+ lines.append(
246
+ f" result = await _unity_client.execute_request('{tool_name}', params)"
247
+ )
248
+ lines.append(f' logger.debug(f"{tool_name} completed successfully")')
249
+ lines.append(" return result")
250
+ lines.append("")
251
+ lines.append(" except Exception as e:")
252
+ lines.append(f' logger.error(f"Failed to execute {tool_name}: {{e}}")')
253
+ lines.append(
254
+ f' raise RuntimeError(f"Tool execution failed for {tool_name}: {{e}}")'
255
+ )
256
+ lines.append("")
257
+
258
+ return lines
259
+
260
+ def _get_single_type(self, param_type: str) -> str:
261
+ """Convert a single JSON schema type to Python type.
262
+
263
+ Args:
264
+ param_type: JSON schema type string
265
+
266
+ Returns:
267
+ Python type string
268
+ """
269
+ type_mapping = {
270
+ "string": "str",
271
+ "integer": "int",
272
+ "number": "float",
273
+ "boolean": "bool",
274
+ "array": "list",
275
+ "object": "dict",
276
+ }
277
+ return type_mapping.get(param_type, "Any")
278
+
279
+ def _get_python_union_type(self, param_types: list) -> str:
280
+ """Convert JSON schema union types to Python union syntax.
281
+
282
+ Args:
283
+ param_types: List of JSON schema types
284
+
285
+ Returns:
286
+ Python union type string using modern | syntax
287
+ """
288
+ python_types = []
289
+ has_null = False
290
+
291
+ for param_type in param_types:
292
+ if param_type == "null":
293
+ has_null = True
294
+ else:
295
+ python_types.append(self._get_single_type(param_type))
296
+
297
+ # Remove duplicates while preserving order
298
+ seen = set()
299
+ unique_types = []
300
+ for t in python_types:
301
+ if t not in seen:
302
+ seen.add(t)
303
+ unique_types.append(t)
304
+
305
+ # Build the union type
306
+ if len(unique_types) == 0:
307
+ return "None" if has_null else "Any"
308
+ elif len(unique_types) == 1:
309
+ base_type = unique_types[0]
310
+ return f"{base_type} | None" if has_null else base_type
311
+ else:
312
+ union_type = " | ".join(unique_types)
313
+ return f"{union_type} | None" if has_null else union_type
314
+
315
+ def _get_python_type_annotation(
316
+ self, param_def: Dict[str, Any], description: str, is_required: bool
317
+ ) -> str:
318
+ """Get Python type annotation with description for a parameter.
319
+
320
+ Args:
321
+ param_def: Full parameter definition from JSON schema
322
+ description: Parameter description
323
+ is_required: Whether parameter is required
324
+
325
+ Returns:
326
+ Python type annotation string
327
+ """
328
+ param_type = param_def.get("type", "string")
329
+ enum_values = param_def.get("enum")
330
+
331
+ # Handle enum values - create Literal type
332
+ if enum_values:
333
+ # Create literal values, properly quoted for strings
334
+ literal_values = []
335
+ for value in enum_values:
336
+ if isinstance(value, str):
337
+ literal_values.append(f"'{value}'")
338
+ else:
339
+ literal_values.append(str(value))
340
+
341
+ base_type = f"Literal[{', '.join(literal_values)}]"
342
+
343
+ # Handle union types with enum (e.g., ["string", "null"] with enum)
344
+ if isinstance(param_type, list) and "null" in param_type:
345
+ base_type = f"{base_type} | None"
346
+ else:
347
+ # Handle regular types without enum
348
+ if isinstance(param_type, list):
349
+ base_type = self._get_python_union_type(param_type)
350
+ else:
351
+ base_type = self._get_single_type(param_type)
352
+
353
+ # Add | None for optional parameters (not in required list)
354
+ if not is_required and not base_type.endswith("| None"):
355
+ base_type = f"{base_type} | None"
356
+
357
+ # Create annotated type with Field(description=...) for proper MCP parameter descriptions
358
+ return f'Annotated[\n {base_type},\n Field(\n description="""{description}"""\n ),\n ]'
359
+
360
+
361
+ def main():
362
+ """Main function to generate all MCP tools."""
363
+ logging.basicConfig(level=logging.INFO)
364
+
365
+ generator = MCPToolCodeGenerator()
366
+ generator.generate_all_tools()
367
+
368
+
369
+ if __name__ == "__main__":
370
+ main()
@@ -0,0 +1,4 @@
1
+ # Generated tool files - these are auto-generated from schema JSON files
2
+ *.py
3
+ !__init__.py
4
+ !.gitignore
@@ -0,0 +1,4 @@
1
+ """Generated MCP tools from schema JSON files."""
2
+
3
+ # This package contains auto-generated tool functions
4
+ # Generated files are ignored by git - see .gitignore