Project scoped tools (#596)
* feat: Add project-scoped tools flag to control custom tool registration behavior Add `--project-scoped-tools` CLI flag and `UNITY_MCP_PROJECT_SCOPED_TOOLS` environment variable to control whether custom tools are registered globally or scoped to specific Unity projects. Closes #416 * Add .meta file * feat: Add project-scoped tools toggle for local HTTP transport Add UI toggle in Connection section to control project-scoped tools flag when using HTTP Local transport. The toggle: - Defaults to enabled (true) - Persists state in EditorPrefs - Only displays when HTTP Local transport is selected - Automatically appends `--project-scoped-tools` flag to uvx server command - Updates manual config display when toggled * Update Server/src/services/custom_tool_service.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Pass project_scoped_tools flag directly without environment variable conversion Remove unnecessary environment variable conversion for project_scoped_tools flag. * fix: Improve error handling and logging in global custom tool registration Split exception handling to distinguish between expected RuntimeError (service not initialized) and unexpected errors. --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>main
parent
d96dad3e9a
commit
b69ee80da9
|
|
@ -28,6 +28,7 @@ namespace MCPForUnity.Editor.Constants
|
|||
internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl";
|
||||
internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride";
|
||||
internal const string DevModeForceServerRefresh = "MCPForUnity.DevModeForceServerRefresh";
|
||||
internal const string ProjectScopedToolsLocalHttp = "MCPForUnity.ProjectScopedTools.LocalHttp";
|
||||
|
||||
internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath";
|
||||
internal const string PackageDeployLastBackupPath = "MCPForUnity.PackageDeploy.LastBackupPath";
|
||||
|
|
|
|||
|
|
@ -1312,9 +1312,14 @@ namespace MCPForUnity.Editor.Services
|
|||
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
|
||||
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
|
||||
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
|
||||
bool projectScopedTools = EditorPrefs.GetBool(
|
||||
EditorPrefKeys.ProjectScopedToolsLocalHttp,
|
||||
true
|
||||
);
|
||||
string scopedFlag = projectScopedTools ? " --project-scoped-tools" : string.Empty;
|
||||
string args = string.IsNullOrEmpty(fromUrl)
|
||||
? $"{devFlags}{packageName} --transport http --http-url {httpUrl}"
|
||||
: $"{devFlags}--from {fromUrl} {packageName} --transport http --http-url {httpUrl}";
|
||||
? $"{devFlags}{packageName} --transport http --http-url {httpUrl}{scopedFlag}"
|
||||
: $"{devFlags}--from {fromUrl} {packageName} --transport http --http-url {httpUrl}{scopedFlag}";
|
||||
|
||||
fileName = uvxPath;
|
||||
arguments = args;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
|
|||
private TextField httpUrlField;
|
||||
private Button startHttpServerButton;
|
||||
private Button stopHttpServerButton;
|
||||
private VisualElement projectScopedToolsRow;
|
||||
private Toggle projectScopedToolsToggle;
|
||||
private VisualElement unitySocketPortRow;
|
||||
private TextField unityPortField;
|
||||
private VisualElement statusIndicator;
|
||||
|
|
@ -83,6 +85,8 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
|
|||
httpUrlField = Root.Q<TextField>("http-url");
|
||||
startHttpServerButton = Root.Q<Button>("start-http-server-button");
|
||||
stopHttpServerButton = Root.Q<Button>("stop-http-server-button");
|
||||
projectScopedToolsRow = Root.Q<VisualElement>("project-scoped-tools-row");
|
||||
projectScopedToolsToggle = Root.Q<Toggle>("project-scoped-tools-toggle");
|
||||
unitySocketPortRow = Root.Q<VisualElement>("unity-socket-port-row");
|
||||
unityPortField = Root.Q<TextField>("unity-port");
|
||||
statusIndicator = Root.Q<VisualElement>("status-indicator");
|
||||
|
|
@ -124,6 +128,14 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
|
|||
|
||||
httpUrlField.value = HttpEndpointUtility.GetBaseUrl();
|
||||
|
||||
if (projectScopedToolsToggle != null)
|
||||
{
|
||||
projectScopedToolsToggle.value = EditorPrefs.GetBool(
|
||||
EditorPrefKeys.ProjectScopedToolsLocalHttp,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
int unityPort = EditorPrefs.GetInt(EditorPrefKeys.UnitySocketPort, 0);
|
||||
if (unityPort == 0)
|
||||
{
|
||||
|
|
@ -231,6 +243,16 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
|
|||
};
|
||||
}
|
||||
|
||||
if (projectScopedToolsToggle != null)
|
||||
{
|
||||
projectScopedToolsToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.ProjectScopedToolsLocalHttp, evt.newValue);
|
||||
UpdateHttpServerCommandDisplay();
|
||||
OnManualConfigUpdateRequested?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
if (copyHttpServerCommandButton != null)
|
||||
{
|
||||
copyHttpServerCommandButton.clicked += () =>
|
||||
|
|
@ -456,9 +478,24 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
|
|||
bool useHttp = (TransportProtocol)transportDropdown.value != TransportProtocol.Stdio;
|
||||
|
||||
httpUrlRow.style.display = useHttp ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
UpdateProjectScopedToolsVisibility();
|
||||
unitySocketPortRow.style.display = useHttp ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
private void UpdateProjectScopedToolsVisibility()
|
||||
{
|
||||
if (projectScopedToolsRow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool useHttp = transportDropdown != null && (TransportProtocol)transportDropdown.value != TransportProtocol.Stdio;
|
||||
bool httpLocalSelected = IsHttpLocalSelected();
|
||||
projectScopedToolsRow.style.display = useHttp && httpLocalSelected
|
||||
? DisplayStyle.Flex
|
||||
: DisplayStyle.None;
|
||||
}
|
||||
|
||||
private bool IsHttpLocalSelected()
|
||||
{
|
||||
return transportDropdown != null && (TransportProtocol)transportDropdown.value == TransportProtocol.HTTPLocal;
|
||||
|
|
@ -514,6 +551,7 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
|
|||
{
|
||||
UpdateStartHttpButtonState();
|
||||
UpdateHttpServerCommandDisplay();
|
||||
UpdateProjectScopedToolsVisibility();
|
||||
}
|
||||
|
||||
private async void OnHttpServerToggleClicked()
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@
|
|||
<ui:Button name="start-http-server-button" text="Start Server" class="action-button start-server-button" style="width: auto; flex-grow: 1;" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row" name="project-scoped-tools-row">
|
||||
<ui:Label text="Project-Scoped Tools:" class="setting-label" />
|
||||
<ui:Toggle name="project-scoped-tools-toggle" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row" name="unity-socket-port-row">
|
||||
<ui:Label text="Unity Socket Port:" class="setting-label" />
|
||||
<ui:TextField name="unity-port" class="port-field" />
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ namespace MCPForUnity.Editor.Windows
|
|||
{ EditorPrefKeys.CustomToolRegistrationEnabled, EditorPrefType.Bool },
|
||||
{ EditorPrefKeys.TelemetryDisabled, EditorPrefType.Bool },
|
||||
{ EditorPrefKeys.DevModeForceServerRefresh, EditorPrefType.Bool },
|
||||
{ EditorPrefKeys.ProjectScopedToolsLocalHttp, EditorPrefType.Bool },
|
||||
|
||||
// Integer prefs
|
||||
{ EditorPrefKeys.UnitySocketPort, EditorPrefType.Int },
|
||||
|
|
|
|||
|
|
@ -241,14 +241,23 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:
|
|||
_unity_connection_pool.disconnect_all()
|
||||
logger.info("MCP for Unity Server shut down")
|
||||
|
||||
# Initialize MCP server
|
||||
mcp = FastMCP(
|
||||
name="mcp-for-unity-server",
|
||||
lifespan=server_lifespan,
|
||||
instructions="""
|
||||
|
||||
def _build_instructions(project_scoped_tools: bool) -> str:
|
||||
if project_scoped_tools:
|
||||
custom_tools_note = (
|
||||
"I have a dynamic tool system. Always check the mcpforunity://custom-tools resource first "
|
||||
"to see what special capabilities are available for the current project."
|
||||
)
|
||||
else:
|
||||
custom_tools_note = (
|
||||
"Custom tools are registered as standard tools when Unity connects. "
|
||||
"No project-scoped custom tools resource is available."
|
||||
)
|
||||
|
||||
return f"""
|
||||
This server provides tools to interact with the Unity Game Engine Editor.
|
||||
|
||||
I have a dynamic tool system. Always check the mcpforunity://custom-tools resource first to see what special capabilities are available for the current project.
|
||||
{custom_tools_note}
|
||||
|
||||
Targeting Unity instances:
|
||||
- Use the resource mcpforunity://instances to list active Unity sessions (Name@hash).
|
||||
|
|
@ -295,10 +304,18 @@ Payload sizing & paging (important):
|
|||
- Use paging (`page_size`, `page_number`) and keep `page_size` modest (e.g. **25-50**) to avoid token-heavy responses.
|
||||
- Keep `generate_preview=false` unless you explicitly need thumbnails (previews may include large base64 payloads).
|
||||
"""
|
||||
|
||||
|
||||
def create_mcp_server(project_scoped_tools: bool) -> FastMCP:
|
||||
mcp = FastMCP(
|
||||
name="mcp-for-unity-server",
|
||||
lifespan=server_lifespan,
|
||||
instructions=_build_instructions(project_scoped_tools),
|
||||
)
|
||||
|
||||
custom_tool_service = CustomToolService(mcp)
|
||||
|
||||
global custom_tool_service
|
||||
custom_tool_service = CustomToolService(
|
||||
mcp, project_scoped_tools=project_scoped_tools)
|
||||
|
||||
@mcp.custom_route("/health", methods=["GET"])
|
||||
async def health_http(_: Request) -> JSONResponse:
|
||||
|
|
@ -308,13 +325,11 @@ async def health_http(_: Request) -> JSONResponse:
|
|||
"message": "MCP for Unity server is running"
|
||||
})
|
||||
|
||||
|
||||
@mcp.custom_route("/plugin/sessions", methods=["GET"])
|
||||
async def plugin_sessions_route(_: Request) -> JSONResponse:
|
||||
data = await PluginHub.get_sessions()
|
||||
return JSONResponse(data.model_dump())
|
||||
|
||||
|
||||
# Initialize and register middleware for session-based Unity instance routing
|
||||
# Using the singleton getter ensures we use the same instance everywhere
|
||||
unity_middleware = get_unity_instance_middleware()
|
||||
|
|
@ -331,10 +346,12 @@ if not existing_routes:
|
|||
WebSocketRoute("/hub/plugin", PluginHub))
|
||||
|
||||
# Register all tools
|
||||
register_all_tools(mcp)
|
||||
register_all_tools(mcp, project_scoped_tools=project_scoped_tools)
|
||||
|
||||
# Register all resources
|
||||
register_all_resources(mcp)
|
||||
register_all_resources(mcp, project_scoped_tools=project_scoped_tools)
|
||||
|
||||
return mcp
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -421,6 +438,11 @@ Examples:
|
|||
help="Optional path where the server will write its PID on startup. "
|
||||
"Used by Unity to stop the exact process it launched when running in a terminal."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--project-scoped-tools",
|
||||
action="store_true",
|
||||
help="Keep custom tools scoped to the active Unity project and enable the custom tools resource."
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
@ -469,6 +491,8 @@ Examples:
|
|||
if args.http_port:
|
||||
logger.info(f"HTTP port override: {http_port}")
|
||||
|
||||
mcp = create_mcp_server(args.project_scoped_tools)
|
||||
|
||||
# Determine transport mode
|
||||
if transport_mode == 'http':
|
||||
# Use HTTP transport for FastMCP
|
||||
|
|
|
|||
|
|
@ -1,21 +1,25 @@
|
|||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
import time
|
||||
from hashlib import sha256
|
||||
from typing import Optional
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp import Context, FastMCP
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from models.models import MCPResponse, ToolDefinitionModel, ToolParameterModel
|
||||
from core.logging_decorator import log_execution
|
||||
from core.telemetry_decorator import telemetry_tool
|
||||
from transport.unity_transport import send_with_unity_instance
|
||||
from transport.legacy.unity_connection import (
|
||||
async_send_command_with_retry,
|
||||
get_unity_connection_pool,
|
||||
)
|
||||
from transport.plugin_hub import PluginHub
|
||||
from services.tools import get_unity_instance_from_context
|
||||
|
||||
logger = logging.getLogger("mcp-for-unity-server")
|
||||
|
||||
|
|
@ -39,11 +43,13 @@ class ToolRegistrationResponse(BaseModel):
|
|||
class CustomToolService:
|
||||
_instance: "CustomToolService | None" = None
|
||||
|
||||
def __init__(self, mcp: FastMCP):
|
||||
def __init__(self, mcp: FastMCP, project_scoped_tools: bool = True):
|
||||
CustomToolService._instance = self
|
||||
self._mcp = mcp
|
||||
self._project_scoped_tools = project_scoped_tools
|
||||
self._project_tools: dict[str, dict[str, ToolDefinitionModel]] = {}
|
||||
self._hash_to_project: dict[str, str] = {}
|
||||
self._global_tools: dict[str, ToolDefinitionModel] = {}
|
||||
self._register_http_routes()
|
||||
|
||||
@classmethod
|
||||
|
|
@ -61,17 +67,8 @@ class CustomToolService:
|
|||
except ValidationError as exc:
|
||||
return JSONResponse({"success": False, "error": exc.errors()}, status_code=400)
|
||||
|
||||
registered: list[str] = []
|
||||
replaced: list[str] = []
|
||||
for tool in payload.tools:
|
||||
if self._is_registered(payload.project_id, tool.name):
|
||||
replaced.append(tool.name)
|
||||
self._register_tool(payload.project_id, tool)
|
||||
registered.append(tool.name)
|
||||
|
||||
if payload.project_hash:
|
||||
self._hash_to_project[payload.project_hash.lower(
|
||||
)] = payload.project_id
|
||||
registered, replaced = self._register_project_tools(
|
||||
payload.project_id, payload.tools, project_hash=payload.project_hash)
|
||||
|
||||
message = f"Registered {len(registered)} tool(s)"
|
||||
if replaced:
|
||||
|
|
@ -266,6 +263,153 @@ class CustomToolService:
|
|||
return None
|
||||
return {"message": str(response)}
|
||||
|
||||
def _register_project_tools(
|
||||
self,
|
||||
project_id: str,
|
||||
tools: list[ToolDefinitionModel],
|
||||
project_hash: str | None = None,
|
||||
) -> tuple[list[str], list[str]]:
|
||||
registered: list[str] = []
|
||||
replaced: list[str] = []
|
||||
for tool in tools:
|
||||
if self._is_registered(project_id, tool.name):
|
||||
replaced.append(tool.name)
|
||||
self._register_tool(project_id, tool)
|
||||
registered.append(tool.name)
|
||||
if not self._project_scoped_tools:
|
||||
self._register_global_tool(tool)
|
||||
|
||||
if project_hash:
|
||||
self._hash_to_project[project_hash.lower()] = project_id
|
||||
|
||||
return registered, replaced
|
||||
|
||||
def register_global_tools(self, tools: list[ToolDefinitionModel]) -> None:
|
||||
if self._project_scoped_tools:
|
||||
return
|
||||
for tool in tools:
|
||||
self._register_global_tool(tool)
|
||||
|
||||
def _register_global_tool(self, definition: ToolDefinitionModel) -> None:
|
||||
existing = self._global_tools.get(definition.name)
|
||||
if existing:
|
||||
if existing.model_dump() != definition.model_dump():
|
||||
logger.warning(
|
||||
"Custom tool '%s' already registered with a different schema; keeping existing definition.",
|
||||
definition.name,
|
||||
)
|
||||
return
|
||||
|
||||
handler = self._build_global_tool_handler(definition)
|
||||
wrapped = log_execution(definition.name, "Tool")(handler)
|
||||
wrapped = telemetry_tool(definition.name)(wrapped)
|
||||
|
||||
try:
|
||||
wrapped = self._mcp.tool(
|
||||
name=definition.name,
|
||||
description=definition.description,
|
||||
)(wrapped)
|
||||
except Exception as exc: # pragma: no cover - defensive against tool conflicts
|
||||
logger.warning(
|
||||
"Failed to register custom tool '%s' globally: %s",
|
||||
definition.name,
|
||||
exc,
|
||||
)
|
||||
return
|
||||
|
||||
self._global_tools[definition.name] = definition
|
||||
|
||||
def _build_global_tool_handler(self, definition: ToolDefinitionModel):
|
||||
async def _handler(ctx: Context, **kwargs) -> MCPResponse:
|
||||
unity_instance = get_unity_instance_from_context(ctx)
|
||||
if not unity_instance:
|
||||
return MCPResponse(
|
||||
success=False,
|
||||
message="No active Unity instance. Call set_active_instance with Name@hash from mcpforunity://instances.",
|
||||
)
|
||||
|
||||
project_id = resolve_project_id_for_unity_instance(unity_instance)
|
||||
if project_id is None:
|
||||
return MCPResponse(
|
||||
success=False,
|
||||
message=f"Could not resolve project id for {unity_instance}. Ensure Unity is running and reachable.",
|
||||
)
|
||||
|
||||
params = {k: v for k, v in kwargs.items() if v is not None}
|
||||
service = CustomToolService.get_instance()
|
||||
return await service.execute_tool(project_id, definition.name, unity_instance, params)
|
||||
|
||||
_handler.__name__ = f"custom_tool_{definition.name}"
|
||||
_handler.__doc__ = definition.description or ""
|
||||
_handler.__signature__ = self._build_signature(definition)
|
||||
_handler.__annotations__ = self._build_annotations(definition)
|
||||
return _handler
|
||||
|
||||
def _build_signature(self, definition: ToolDefinitionModel) -> inspect.Signature:
|
||||
params: list[inspect.Parameter] = [
|
||||
inspect.Parameter(
|
||||
"ctx",
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
annotation=Context,
|
||||
)
|
||||
]
|
||||
for param in definition.parameters:
|
||||
if not param.name.isidentifier():
|
||||
logger.warning(
|
||||
"Custom tool '%s' has non-identifier parameter '%s'; exposing via kwargs only.",
|
||||
definition.name,
|
||||
param.name,
|
||||
)
|
||||
continue
|
||||
default = inspect._empty if param.required else self._coerce_default(
|
||||
param.default_value, param.type)
|
||||
params.append(
|
||||
inspect.Parameter(
|
||||
param.name,
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
default=default,
|
||||
annotation=self._map_param_type(param),
|
||||
)
|
||||
)
|
||||
return inspect.Signature(parameters=params)
|
||||
|
||||
def _build_annotations(self, definition: ToolDefinitionModel) -> dict[str, object]:
|
||||
annotations: dict[str, object] = {"ctx": Context}
|
||||
for param in definition.parameters:
|
||||
if not param.name.isidentifier():
|
||||
continue
|
||||
annotations[param.name] = self._map_param_type(param)
|
||||
return annotations
|
||||
|
||||
def _map_param_type(self, param: ToolParameterModel):
|
||||
ptype = (param.type or "string").lower()
|
||||
if ptype in ("integer", "int"):
|
||||
return int
|
||||
if ptype in ("number", "float", "double"):
|
||||
return float
|
||||
if ptype in ("bool", "boolean"):
|
||||
return bool
|
||||
if ptype in ("array", "list"):
|
||||
return list
|
||||
if ptype in ("object", "dict"):
|
||||
return dict
|
||||
return str
|
||||
|
||||
def _coerce_default(self, value: str | None, param_type: str | None):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
ptype = (param_type or "string").lower()
|
||||
if ptype in ("integer", "int"):
|
||||
return int(value)
|
||||
if ptype in ("number", "float", "double"):
|
||||
return float(value)
|
||||
if ptype in ("bool", "boolean"):
|
||||
return str(value).lower() in ("1", "true", "yes", "on")
|
||||
return value
|
||||
except Exception:
|
||||
return value
|
||||
|
||||
|
||||
def compute_project_id(project_name: str, project_path: str) -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ logger = logging.getLogger("mcp-for-unity-server")
|
|||
__all__ = ['register_all_resources']
|
||||
|
||||
|
||||
def register_all_resources(mcp: FastMCP):
|
||||
def register_all_resources(mcp: FastMCP, *, project_scoped_tools: bool = True):
|
||||
"""
|
||||
Auto-discover and register all resources in the resources/ directory.
|
||||
|
||||
|
|
@ -46,6 +46,11 @@ def register_all_resources(mcp: FastMCP):
|
|||
description = resource_info['description']
|
||||
kwargs = resource_info['kwargs']
|
||||
|
||||
if not project_scoped_tools and resource_name == "custom_tools":
|
||||
logger.info(
|
||||
"Skipping custom_tools resource registration (project-scoped tools disabled)")
|
||||
continue
|
||||
|
||||
# Check if URI contains query parameters (e.g., {?unity_instance})
|
||||
has_query_params = '{?' in uri
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
def register_all_tools(mcp: FastMCP):
|
||||
def register_all_tools(mcp: FastMCP, *, project_scoped_tools: bool = True):
|
||||
"""
|
||||
Auto-discover and register all tools in the tools/ directory.
|
||||
|
||||
|
|
@ -46,6 +46,11 @@ def register_all_tools(mcp: FastMCP):
|
|||
description = tool_info['description']
|
||||
kwargs = tool_info['kwargs']
|
||||
|
||||
if not project_scoped_tools and tool_name == "execute_custom_tool":
|
||||
logger.info(
|
||||
"Skipping execute_custom_tool registration (project-scoped tools disabled)")
|
||||
continue
|
||||
|
||||
# Apply the @mcp.tool decorator, telemetry, and logging
|
||||
wrapped = log_execution(tool_name, "Tool")(func)
|
||||
wrapped = telemetry_tool(tool_name)(wrapped)
|
||||
|
|
|
|||
|
|
@ -315,6 +315,23 @@ class PluginHub(WebSocketEndpoint):
|
|||
logger.info(
|
||||
f"Registered {len(payload.tools)} tools for session {session_id}")
|
||||
|
||||
try:
|
||||
from services.custom_tool_service import CustomToolService
|
||||
|
||||
service = CustomToolService.get_instance()
|
||||
service.register_global_tools(payload.tools)
|
||||
except RuntimeError as exc:
|
||||
logger.debug(
|
||||
"Skipping global custom tool registration: CustomToolService not initialized yet (%s)",
|
||||
exc,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Unexpected error during global custom tool registration; "
|
||||
"custom tools may not be available globally",
|
||||
exc_info=exc,
|
||||
)
|
||||
|
||||
async def _handle_command_result(self, payload: CommandResultMessage) -> None:
|
||||
cls = type(self)
|
||||
lock = cls._lock
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 569d5f32146c348ad96a95a3ba1b394a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue