Skip to content

Commit 83b96ac

Browse files
committed
add instance params for resources
1 parent b75202a commit 83b96ac

File tree

10 files changed

+726
-448
lines changed

10 files changed

+726
-448
lines changed

MCPForUnity/UnityMcpServer~/src/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
88
"httpx>=0.27.2",
9-
"fastmcp>=2.12.5",
9+
"fastmcp>=2.13.0",
1010
"mcp>=1.16.0",
1111
"pydantic>=2.12.0",
1212
"tomli>=2.3.0",

MCPForUnity/UnityMcpServer~/src/resources/__init__.py

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
MCP Resources package - Auto-discovers and registers all resources in this directory.
33
"""
44
import logging
5+
import inspect
56
from pathlib import Path
7+
from typing import get_type_hints
68

79
from fastmcp import FastMCP
810
from telemetry_decorator import telemetry_resource
@@ -16,6 +18,23 @@
1618
__all__ = ['register_all_resources']
1719

1820

21+
def _create_fixed_wrapper(original_func, has_other_params):
22+
"""
23+
Factory function to create a wrapper that calls original_func with unity_instance=None.
24+
This avoids closure issues in loops.
25+
"""
26+
if has_other_params:
27+
# Has other parameters (like mode)
28+
async def fixed_wrapper(*args, **kwargs):
29+
return await original_func(*args, **kwargs, unity_instance=None)
30+
else:
31+
# No other parameters, just unity_instance
32+
async def fixed_wrapper():
33+
return await original_func(unity_instance=None)
34+
35+
return fixed_wrapper
36+
37+
1938
def register_all_resources(mcp: FastMCP):
2039
"""
2140
Auto-discover and register all resources in the resources/ directory.
@@ -36,18 +55,76 @@ def register_all_resources(mcp: FastMCP):
3655
logger.warning("No MCP resources registered!")
3756
return
3857

58+
registered_count = 0
3959
for resource_info in resources:
4060
func = resource_info['func']
4161
uri = resource_info['uri']
4262
resource_name = resource_info['name']
4363
description = resource_info['description']
4464
kwargs = resource_info['kwargs']
4565

46-
# Apply the @mcp.resource decorator and telemetry
47-
wrapped = telemetry_resource(resource_name)(func)
48-
wrapped = mcp.resource(uri=uri, name=resource_name,
49-
description=description, **kwargs)(wrapped)
50-
resource_info['func'] = wrapped
51-
logger.debug(f"Registered resource: {resource_name} - {description}")
66+
# Check if URI contains query parameters (e.g., {?unity_instance})
67+
has_query_params = '{?' in uri
68+
69+
if has_query_params:
70+
# Register two versions for backward compatibility:
71+
# 1. Template version with query parameters (for multi-instance)
72+
wrapped_template = telemetry_resource(resource_name)(func)
73+
wrapped_template = mcp.resource(uri=uri, name=resource_name,
74+
description=description, **kwargs)(wrapped_template)
75+
logger.debug(f"Registered resource template: {resource_name} - {uri}")
76+
registered_count += 1
77+
78+
# 2. Fixed version without query parameters (for single-instance/default)
79+
# Remove query parameters from URI
80+
fixed_uri = uri.split('{?')[0]
81+
fixed_name = f"{resource_name}_default"
82+
fixed_description = f"{description} (default instance)"
83+
84+
# Create a wrapper function that doesn't accept unity_instance parameter
85+
# This wrapper will call the original function with unity_instance=None
86+
sig = inspect.signature(func)
87+
params = list(sig.parameters.values())
88+
89+
# Filter out unity_instance parameter
90+
fixed_params = [p for p in params if p.name != 'unity_instance']
91+
92+
# Create wrapper using factory function to avoid closure issues
93+
has_other_params = len(fixed_params) > 0
94+
fixed_wrapper = _create_fixed_wrapper(func, has_other_params)
95+
96+
# Update signature to match filtered parameters
97+
if has_other_params:
98+
fixed_wrapper.__signature__ = sig.replace(parameters=fixed_params)
99+
fixed_wrapper.__annotations__ = {
100+
k: v for k, v in func.__annotations__.items()
101+
if k != 'unity_instance'
102+
}
103+
else:
104+
fixed_wrapper.__signature__ = inspect.Signature(parameters=[])
105+
fixed_wrapper.__annotations__ = {
106+
k: v for k, v in func.__annotations__.items()
107+
if k == 'return'
108+
}
109+
110+
# Preserve function metadata
111+
fixed_wrapper.__name__ = fixed_name
112+
fixed_wrapper.__doc__ = func.__doc__
113+
114+
wrapped_fixed = telemetry_resource(fixed_name)(fixed_wrapper)
115+
wrapped_fixed = mcp.resource(uri=fixed_uri, name=fixed_name,
116+
description=fixed_description, **kwargs)(wrapped_fixed)
117+
logger.debug(f"Registered resource (fixed): {fixed_name} - {fixed_uri}")
118+
registered_count += 1
119+
120+
resource_info['func'] = wrapped_template
121+
else:
122+
# No query parameters, register as-is
123+
wrapped = telemetry_resource(resource_name)(func)
124+
wrapped = mcp.resource(uri=uri, name=resource_name,
125+
description=description, **kwargs)(wrapped)
126+
resource_info['func'] = wrapped
127+
logger.debug(f"Registered resource: {resource_name} - {description}")
128+
registered_count += 1
52129

53-
logger.info(f"Registered {len(resources)} MCP resources")
130+
logger.info(f"Registered {registered_count} MCP resources ({len(resources)} unique)")

MCPForUnity/UnityMcpServer~/src/resources/menu_items.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@ class GetMenuItemsResponse(MCPResponse):
88

99

1010
@mcp_for_unity_resource(
11-
uri="mcpforunity://menu-items",
11+
uri="mcpforunity://menu-items{?unity_instance}",
1212
name="get_menu_items",
1313
description="Provides a list of all menu items."
1414
)
15-
async def get_menu_items() -> GetMenuItemsResponse:
16-
"""Provides a list of all menu items."""
17-
# Later versions of FastMCP support these as query parameters
18-
# See: https://gofastmcp.com/servers/resources#query-parameters
15+
async def get_menu_items(unity_instance: str | None = None) -> GetMenuItemsResponse:
16+
"""Provides a list of all menu items.
17+
18+
Args:
19+
unity_instance: Target Unity instance (project name, hash, or 'Name@hash').
20+
If not specified, uses default instance.
21+
"""
1922
params = {
2023
"refresh": True,
2124
"search": "",
2225
}
2326

24-
response = await async_send_command_with_retry("get_menu_items", params)
27+
response = await async_send_command_with_retry("get_menu_items", params, instance_id=unity_instance)
2528
return GetMenuItemsResponse(**response) if isinstance(response, dict) else response

MCPForUnity/UnityMcpServer~/src/resources/tests.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,29 @@ class GetTestsResponse(MCPResponse):
1717
data: list[TestItem] = []
1818

1919

20-
@mcp_for_unity_resource(uri="mcpforunity://tests", name="get_tests", description="Provides a list of all tests.")
21-
async def get_tests() -> GetTestsResponse:
22-
"""Provides a list of all tests."""
23-
response = await async_send_command_with_retry("get_tests", {})
20+
@mcp_for_unity_resource(uri="mcpforunity://tests{?unity_instance}", name="get_tests", description="Provides a list of all tests.")
21+
async def get_tests(unity_instance: str | None = None) -> GetTestsResponse:
22+
"""Provides a list of all tests.
23+
24+
Args:
25+
unity_instance: Target Unity instance (project name, hash, or 'Name@hash').
26+
If not specified, uses default instance.
27+
"""
28+
response = await async_send_command_with_retry("get_tests", {}, instance_id=unity_instance)
2429
return GetTestsResponse(**response) if isinstance(response, dict) else response
2530

2631

27-
@mcp_for_unity_resource(uri="mcpforunity://tests/{mode}", name="get_tests_for_mode", description="Provides a list of tests for a specific mode.")
28-
async def get_tests_for_mode(mode: Annotated[Literal["EditMode", "PlayMode"], Field(description="The mode to filter tests by.")]) -> GetTestsResponse:
29-
"""Provides a list of tests for a specific mode."""
30-
response = await async_send_command_with_retry("get_tests_for_mode", {"mode": mode})
32+
@mcp_for_unity_resource(uri="mcpforunity://tests/{mode}{?unity_instance}", name="get_tests_for_mode", description="Provides a list of tests for a specific mode.")
33+
async def get_tests_for_mode(
34+
mode: Annotated[Literal["EditMode", "PlayMode"], Field(description="The mode to filter tests by.")],
35+
unity_instance: str | None = None
36+
) -> GetTestsResponse:
37+
"""Provides a list of tests for a specific mode.
38+
39+
Args:
40+
mode: The test mode to filter by (EditMode or PlayMode).
41+
unity_instance: Target Unity instance (project name, hash, or 'Name@hash').
42+
If not specified, uses default instance.
43+
"""
44+
response = await async_send_command_with_retry("get_tests_for_mode", {"mode": mode}, instance_id=unity_instance)
3145
return GetTestsResponse(**response) if isinstance(response, dict) else response

0 commit comments

Comments
 (0)