Visitar URL original
[Bug]: Custom tools silently fail to register on Python 3.14 (`Failed to register custom tool '<name>' globally: '<param>'`) · Issue #1430 · CoplayDev/unity-mcp · GitHub
Skip to content

[Bug]: Custom tools silently fail to register on Python 3.14 (Failed to register custom tool '<name>' globally: '<param>') #1430

Open

Description

@maxless

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.lock pins) and fastmcp 3.4.8 / pydantic 2.13.5 (what uvx --from mcpforunityserver==… resolves today) — both fail.
  • Both stdio and HTTP: both go through CustomToolService.register_global_tools.

Root cause

  • CustomToolService._build_global_tool_handler builds _handler and assigns __signature__ and __annotations__ by hand (Server/src/services/custom_tool_service.py:404-405 @ v10.2.0).
  • _register_global_tool wraps it with log_execution and telemetry_tool (:357-358). Both use functools.wraps.
  • Python 3.14 (PEP 649/749) changed functools.WRAPPER_ASSIGNMENTS to copy __annotate__ instead of __annotations__ (Lib/functools.py:32-33 @ v3.14.0). Assigning __annotations__ sets the function's __annotate__ to None. So the wrapper gets __annotate__ = None and empty annotations, while __signature__ (it lives in __dict__) still lists the parameters.
  • fastmcp hands the wrapper to pydantic. inspect.signature still shows action: str, get_type_hints returns {}, and pydantic's type_hints[name] raises KeyError('action') (pydantic/_internal/_generate_schema.py:2005 in 2.13.5).
  • _register_global_tool catches 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), while pyproject.toml declares a 3.14 classifier.

Reproduction steps

Steps to reproduce (no Unity needed)

  1. 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())))
  1. 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)

  1. 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.
  2. 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" };
}
  1. 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.
  2. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions