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.
- coplay_mcp_server/__init__.py +3 -0
- coplay_mcp_server/code_generator.py +370 -0
- coplay_mcp_server/generated_tools/.gitignore +4 -0
- coplay_mcp_server/generated_tools/__init__.py +4 -0
- coplay_mcp_server/generated_tools/agent_tool_tools.py +347 -0
- coplay_mcp_server/generated_tools/coplay_tool_tools.py +58 -0
- coplay_mcp_server/generated_tools/image_tool_tools.py +146 -0
- coplay_mcp_server/generated_tools/input_action_tool_tools.py +718 -0
- coplay_mcp_server/generated_tools/package_tool_tools.py +240 -0
- coplay_mcp_server/generated_tools/profiler_functions_tools.py +63 -0
- coplay_mcp_server/generated_tools/scene_view_functions_tools.py +58 -0
- coplay_mcp_server/generated_tools/screenshot_tool_tools.py +87 -0
- coplay_mcp_server/generated_tools/snapping_functions_tools.py +409 -0
- coplay_mcp_server/generated_tools/ui_functions_tools.py +419 -0
- coplay_mcp_server/generated_tools/unity_functions_tools.py +1643 -0
- coplay_mcp_server/image_utils.py +96 -0
- coplay_mcp_server/process_discovery.py +168 -0
- coplay_mcp_server/server.py +236 -0
- coplay_mcp_server/unity_client.py +342 -0
- coplay_mcp_server-1.4.1.dist-info/METADATA +70 -0
- coplay_mcp_server-1.4.1.dist-info/RECORD +24 -0
- coplay_mcp_server-1.4.1.dist-info/WHEEL +4 -0
- coplay_mcp_server-1.4.1.dist-info/entry_points.txt +3 -0
- coplay_mcp_server-1.4.1.dist-info/licenses/LICENSE +21 -0
|
@@ -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()
|