Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
437dbf2
fix: use WindowsSelectorEventLoopPolicy for asyncio on Windows to res…
whatevertogo Feb 2, 2026
a1f4db1
test: add Windows event loop policy verification
whatevertogo Feb 2, 2026
cbad6f7
fix: use getattr() for cross-platform asyncio policy references
whatevertogo Feb 2, 2026
a25aff4
refactor: use get_running_loop() in async context (code review)
whatevertogo Feb 2, 2026
146a352
Merge branch 'CoplayDev:beta' into beta
whatevertogo Feb 6, 2026
7caf958
fix: improve Windows asyncio compatibility for WebSocket connections …
whatevertogo Feb 7, 2026
60d0d60
fix: ensure non-Windows platforms retain their default event loop policy
whatevertogo Feb 7, 2026
1d505ad
fix: comment out unused import and lambda for asyncio event loop in W…
whatevertogo Feb 7, 2026
caf6906
fix: update comment for Python 3.16+ SelectorEventLoop handling in Wi…
whatevertogo Feb 7, 2026
c688c37
fix: update comment for Python 3.16+ SelectorEventLoop handling in Wi…
whatevertogo Feb 7, 2026
993043f
refactor: use anyio loop_factory for Windows asyncio fix
whatevertogo Feb 10, 2026
221e343
Merge branch 'CoplayDev:beta' into beta
whatevertogo Feb 11, 2026
624a85f
chore: update Unity package to beta version 9.4.5-beta.2
actions-user Feb 11, 2026
fb3c0ac
Merge pull request #8 from whatevertogo/beta-version-9.4.5-beta.2-219…
github-actions[bot] Feb 11, 2026
616789d
chore: add auto-sync workflow for upstream beta branch
whatevertogo Feb 11, 2026
a16beac
Revert "chore: add auto-sync workflow for upstream beta branch"
whatevertogo Feb 11, 2026
c217f4c
Merge pull request #9 from whatevertogo/revert-sync-workflow
whatevertogo Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MCPForUnity/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "com.coplaydev.unity-mcp",
"version": "9.4.5-beta.1",
"version": "9.4.5-beta.2",
"displayName": "MCP for Unity",
"description": "A bridge that connects AI assistants to Unity via the MCP (Model Context Protocol). Allows AI clients like Claude Code, Cursor, and VSCode to directly control your Unity Editor for enhanced development workflows.\n\nFeatures automated setup wizard, cross-platform support, and seamless integration with popular AI development tools.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4",
"unity": "2021.3",
Expand Down
51 changes: 46 additions & 5 deletions Server/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,52 @@
import asyncio
import logging
from contextlib import asynccontextmanager
from functools import partial
import os
import sys
import threading
import time
from typing import AsyncIterator, Any
from typing import AsyncIterator, Any, Callable, Literal
from urllib.parse import urlparse

import anyio

# Keep this local to avoid importing FastMCP internal modules just for typing.
Transport = Literal["stdio", "http", "sse", "streamable-http"]

# Windows asyncio fix: Use SelectorEventLoop instead of ProactorEventLoop
# This fixes "WinError 64: The specified network name is no longer available"
# which occurs with WebSocket connections under heavy client reconnect scenarios.
#
# Set UNITY_MCP_ASYNCIO_POLICY=proactor to override and use ProactorEventLoop.
def _get_asyncio_loop_factory() -> Callable[[], asyncio.AbstractEventLoop] | None:
if sys.platform != "win32":
return None

policy = os.getenv("UNITY_MCP_ASYNCIO_POLICY", "selector").lower()
if policy == "selector":
return asyncio.SelectorEventLoop

return None


def _run_mcp(mcp: "FastMCP", transport: Transport, **transport_kwargs: Any) -> None:
"""Run FastMCP with an optional Windows-specific loop factory."""
loop_factory = _get_asyncio_loop_factory()
if loop_factory is None:
mcp.run(transport=transport, **transport_kwargs)
return

anyio.run(
partial(
mcp.run_async,
transport,
show_banner=True,
**transport_kwargs,
),
backend_options={"loop_factory": loop_factory},
)

Comment on lines +52 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

anyio.run backend_options loop_factory asyncio

💡 Result:

anyio.run() accepts backend-specific kwargs via backend_options. For the asyncio backend, you can pass an event loop factory with loop_factory (a callable that returns a new event loop). (deepwiki.com)

Example: use uvloop (AnyIO 4+)

import anyio
import uvloop

anyio.run(main, backend="asyncio", backend_options={"loop_factory": uvloop.new_event_loop})

AnyIO’s migration guide explicitly says to replace the old “event loop policy” approach with loop_factory. (anyio.readthedocs.io)

Notes

  • Asyncio backend options commonly include: debug, loop_factory, and use_uvloop (which internally selects uvloop.new_event_loop when available). (deepwiki.com)

Citations:


🏁 Script executed:

cat -n Server/src/main.py | sed -n '50,75p'

Repository: CoplayDev/unity-mcp

Length of output: 1177


Remove trailing whitespace on lines 68–69.

The anyio.run approach with backend_options={"loop_factory": loop_factory} is correct and follows AnyIO's recommended migration away from deprecated event loop policies.

The show_banner=True parameter is only passed in the anyio.run path (line 63), not in the mcp.run() path (line 56). This asymmetry is not problematic since mcp.run() has its own defaults, but note the difference.

Lines 68–69 contain trailing whitespace that should be removed.

🤖 Prompt for AI Agents
In `@Server/src/main.py` around lines 52 - 68, The trailing whitespace after the
anyio.run call in _run_mcp should be removed; open the _run_mcp function (which
uses _get_asyncio_loop_factory to decide between calling mcp.run and anyio.run
with mcp.run_async) and delete any extraneous spaces at the end of the lines
following the anyio.run invocation so there are no trailing whitespace
characters remaining.


# Workaround for environments where tool signature evaluation runs with a globals
# dict that does not include common `typing` names (e.g. when annotations are strings
# and evaluated via `eval()` during schema generation).
Expand Down Expand Up @@ -825,19 +865,20 @@ def main():
# Determine transport mode
if config.transport_mode == 'http':
# Use HTTP transport for FastMCP
transport = 'http'
transport: Transport = "http"
# Use the parsed host and port from URL/args
http_url = os.environ.get("UNITY_MCP_HTTP_URL", args.http_url)
parsed_url = urlparse(http_url)
host = args.http_host or os.environ.get(
"UNITY_MCP_HTTP_HOST") or parsed_url.hostname or "localhost"
port = args.http_port or _env_port or parsed_url.port or 8080
logger.info(f"Starting FastMCP with HTTP transport on {host}:{port}")
mcp.run(transport=transport, host=host, port=port)
_run_mcp(mcp, transport=transport, host=host, port=port)
else:
# Use stdio transport for traditional MCP
logger.info("Starting FastMCP with stdio transport")
mcp.run(transport='stdio')
transport: Transport = "stdio"
_run_mcp(mcp, transport=transport)


# Run the server
Expand Down
161 changes: 161 additions & 0 deletions Server/tests/test_event_loop_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""
Test event loop configuration for Windows.

This module verifies that the correct asyncio loop factory is selected
on different platforms to prevent WinError 64 on Windows.

WinError 64 occurs when using ProactorEventLoop with concurrent
WebSocket and HTTP connections. The fix is to use SelectorEventLoop
on Windows.

Related fix: Server/src/main.py
"""

import sys
import asyncio
from functools import partial
import pytest


@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific")
def test_windows_uses_selector_event_loop_factory():
"""
Verify that Windows uses SelectorEventLoop via loop_factory.

This prevents WinError 64 when handling concurrent WebSocket and HTTP connections.

Regression test for Windows asyncio bug where ProactorEventLoop's IOCP
has race conditions with rapid connection changes.

The fix is applied in Server/src/main.py
"""
import importlib
import main # type: ignore[import] - conftest.py adds src to sys.path
importlib.reload(main)

loop_factory = main._get_asyncio_loop_factory()
assert loop_factory is asyncio.SelectorEventLoop, (
"Expected SelectorEventLoop for Windows loop_factory"
)


@pytest.mark.skipif(sys.platform == "win32", reason="Non-Windows only")
def test_non_windows_uses_default_loop_factory():
"""
Verify that non-Windows platforms keep their default event loop behavior.

SelectorEventLoop should only be used on Windows to avoid the IOCP bug.
Other platforms should use their optimal default event loop.
"""
import importlib
import main # type: ignore[import] - conftest.py adds src to sys.path
importlib.reload(main)

loop_factory = main._get_asyncio_loop_factory()
assert loop_factory is None, "Non-Windows platforms should not set a loop_factory"


@pytest.mark.asyncio
async def test_async_operations_use_correct_event_loop():
"""
Smoke test to verify async operations work with the configured event loop.

This test creates a simple async operation to ensure the event loop
is functional. It doesn't test WinError 64 directly (which is a
timing-dependent race condition), but confirms the basic async
infrastructure works with the policy configured in main.py.
"""
# Import main to ensure the event loop policy is configured
import importlib
import main # type: ignore[import] - conftest.py adds src to sys.path
importlib.reload(main)

# Simple async operation
async def simple_task():
await asyncio.sleep(0.01)
return "success"

# Should complete without errors
result = await simple_task()
assert result == "success"

# Verify we're using the expected event loop
# Use get_running_loop() as we're in an async context
loop = asyncio.get_running_loop()
assert loop is not None, "Event loop should be running"
assert loop.is_running(), "Event loop should be in running state"


def test_run_mcp_uses_fastmcp_run_when_no_loop_factory(monkeypatch: pytest.MonkeyPatch):
"""When no loop factory is needed, _run_mcp should delegate to FastMCP.run."""
import importlib
import main # type: ignore[import] - conftest.py adds src to sys.path
importlib.reload(main)

class DummyMCP:
def __init__(self) -> None:
self.run_called = False
self.run_kwargs = {}

def run(self, **kwargs):
self.run_called = True
self.run_kwargs = kwargs

monkeypatch.setattr(main, "_get_asyncio_loop_factory", lambda: None)

mcp = DummyMCP()
main._run_mcp(mcp, transport="stdio")

assert mcp.run_called
assert mcp.run_kwargs["transport"] == "stdio"


def test_run_mcp_uses_anyio_with_loop_factory(monkeypatch: pytest.MonkeyPatch):
"""When loop factory exists, _run_mcp should use anyio.run with backend options."""
import importlib
import main # type: ignore[import] - conftest.py adds src to sys.path
importlib.reload(main)

class DummyMCP:
async def run_async(self, transport, show_banner=True, **kwargs):
return None

captured = {}

def fake_anyio_run(func, *args, **kwargs):
captured["func"] = func
captured["args"] = args
captured["kwargs"] = kwargs
return None

monkeypatch.setattr(main, "anyio", type("AnyIOStub", (), {"run": staticmethod(fake_anyio_run)}))
monkeypatch.setattr(main, "_get_asyncio_loop_factory", lambda: asyncio.SelectorEventLoop)

mcp = DummyMCP()
main._run_mcp(mcp, transport="http", host="localhost", port=8080)

assert isinstance(captured["func"], partial)
assert captured["func"].args[0] == "http"
assert captured["func"].keywords["show_banner"] is True
assert captured["func"].keywords["host"] == "localhost"
assert captured["func"].keywords["port"] == 8080
assert captured["kwargs"]["backend_options"]["loop_factory"] is asyncio.SelectorEventLoop


@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific")
def test_windows_loop_factory_prevents_proactor():
"""
Verify that Windows loop_factory explicitly avoids ProactorEventLoop.

ProactorEventLoop uses IOCP which has known issues with WinError 64
when handling rapid connection changes. This test confirms we're
not using ProactorEventLoop.
"""
import importlib
import main # type: ignore[import] - conftest.py adds src to sys.path
importlib.reload(main)

loop_factory = main._get_asyncio_loop_factory()
assert loop_factory is asyncio.SelectorEventLoop, (
"SelectorEventLoop should be used on Windows (prevents WinError 64)"
)