Repository navigation
[Bug]: Custom tools silently fail to register on Python 3.14 (Failed to register custom tool '<name>' globally: '<param>') #1430
Description
What happened?
Summary
On Python 3.14 the MCP for Unity server registers none of the project's custom tools ([McpForUnityTool] with a Parameters class). Built-in tools register fine. The only trace is one WARNING per tool in the server log:
WARNING mcp-for-unity-server: Failed to register custom tool 'my_tool' globally: 'action'
Unity reports the tools as registered; the MCP client never sees them. On stdio there is no fallback at all; on HTTP with project-scoped tools they stay reachable only through execute_custom_tool, never under their own names.
uvx runs the server on the newest uv-managed Python, and neither the client-config wizard (CodexConfigHelper.cs) nor ServerCommandBuilder.cs passes --python. So any machine with a uv-managed 3.14 — or a uv that downloads one because it finds no Python — hits this.
Affected
- MCP for Unity 10.1.2, 10.2.0 and 10.3.0 — the handler code is identical from 10.0.0 to current
beta. - CPython 3.14.0 and 3.14.8. 3.13.9 works.
- fastmcp 3.0.2 / pydantic 2.12.5 (the
uv.lockpins) and fastmcp 3.4.8 / pydantic 2.13.5 (whatuvx --from mcpforunityserver==…resolves today) — both fail. - Both stdio and HTTP: both go through
CustomToolService.register_global_tools.
Root cause
CustomToolService._build_global_tool_handlerbuilds_handlerand assigns__signature__and__annotations__by hand (Server/src/services/custom_tool_service.py:404-405@ v10.2.0)._register_global_toolwraps it withlog_executionandtelemetry_tool(:357-358). Both usefunctools.wraps.- Python 3.14 (PEP 649/749) changed
functools.WRAPPER_ASSIGNMENTSto copy__annotate__instead of__annotations__(Lib/functools.py:32-33@ v3.14.0). Assigning__annotations__sets the function's__annotate__toNone. So the wrapper gets__annotate__ = Noneand empty annotations, while__signature__(it lives in__dict__) still lists the parameters. - fastmcp hands the wrapper to pydantic.
inspect.signaturestill showsaction: str,get_type_hintsreturns{}, and pydantic'stype_hints[name]raisesKeyError('action')(pydantic/_internal/_generate_schema.py:2005in 2.13.5). _register_global_toolcatches the exception, logs the warning (:365-371), and the tool is dropped.- Built-in tools are unaffected: their annotations are compiled, so they have a real
__annotate__.
Minimal CPython demonstration:
import functools, sys
def handler(**kwargs):
pass
handler.__annotations__ = {"action": str}
@functools.wraps(handler)
def wrapper(*args, **kwargs):
pass
print(sys.version.split()[0], wrapper.__annotations__)3.13.9 prints {'action': <class 'str'>}; 3.14.8 prints {}.
Suggested fix
Give the handler a real __annotate__ next to the assignment in _build_global_tool_handler:
annotations = self._build_annotations(definition)
_handler.__annotations__ = annotations
_handler.__annotate__ = lambda format: dict(annotations)Patched into the repro above, this registers my_tool with its action parameter on both 3.14.8 and 3.13.9. Alternatives we did not test: assign __annotations__ on the wrapped function after the decorators, or pass assigned=functools.WRAPPER_ASSIGNMENTS + ("__annotations__",) in the two decorators.
Two more things would have made this easier to find:
- The failure is silent: one WARNING in the server log, while the Unity side shows the tools as registered. An error surfaced in the MCP for Unity window would have saved us days.
- CI tests the server only on 3.10 (
.github/workflows/python-tests.yml:45), whilepyproject.tomldeclares a 3.14 classifier.
Reproduction steps
Steps to reproduce (no Unity needed)
- Save as
repro.py:
import asyncio
import logging
import os
import sys
os.environ["DISABLE_TELEMETRY"] = "1"
logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(name)s: %(message)s")
from fastmcp import FastMCP
from models.models import ToolDefinitionModel, ToolParameterModel
from services.custom_tool_service import CustomToolService
mcp = FastMCP("repro")
svc = CustomToolService(mcp, project_scoped_tools=True)
svc.register_global_tools([
ToolDefinitionModel(
name="my_tool",
description="repro",
parameters=[ToolParameterModel(name="action", type="string", required=False)],
)
])
print(sys.version.split()[0], sorted(t.name for t in asyncio.run(mcp.list_tools())))- Run it on both Pythons:
uv run --no-project --python 3.14 --with mcpforunityserver==10.3.0 python repro.py
uv run --no-project --python 3.13 --with mcpforunityserver==10.3.0 python repro.py
Expected: both print ['my_tool'].
Actual:
WARNING mcp-for-unity-server: Failed to register custom tool 'my_tool' globally: 'action'
3.14.8 []
and 3.13.9 ['my_tool'].
Steps to reproduce (in Unity)
- Make 3.14 the newest uv-managed Python:
uv python install 3.14. Check what the server will run on:uvx --from mcpforunityserver==10.3.0 python -c "import sys; print(sys.version)"→3.14.x. - In a project with MCP for Unity 10.3.0, add any custom tool with a parameter, e.g.:
using MCPForUnity.Editor.Tools;
using Newtonsoft.Json.Linq;
[McpForUnityTool("my_tool", Description = "repro")]
public static class MyTool
{
public class Parameters
{
[ToolParameter("any action", Required = false)]
public string action { get; set; }
}
public static object HandleCommand(JObject @params) => new { success = true, message = "ok" };
}- Configure a stdio client with the wizard (
uvx --from mcpforunityserver==10.3.0 mcp-for-unity --transport stdio) and connect — or start the HTTP server from Window → MCP for Unity. - List the tools from the client.
Expected: my_tool is listed. Actual: it is not, and the server log has the warning above. With the server on Python 3.13, the tool is listed.
(The Unity-free repro above is what we ran exactly. In Unity we saw the same thing on a stdio setup: our two custom tools were missing for days, and they appeared as soon as the server was moved to Python 3.13.)
Unity version
6000.3.22f1
MCP for Unity package version
10.2.0
Python server version
No response
MCP client
Codex
Transport
stdio
OS
Windows
Relevant logs / console output
Checks
- I searched existing issues and did not find a duplicate
- I included logs / steps to reproduce
Activity