Skip to main content

Three-Layer Python Design

The Python server (Server/src/) exposes three distinct surfaces. They look similar but serve different consumers and are not auto-generated from each other.

LayerWhereFrameworkConsumerTransport to Unity
MCP ToolsServer/src/services/tools/FastMCP (@mcp_for_unity_tool)AI assistants via MCPWebSocket (send_with_unity_instance)
CLI CommandsServer/src/cli/commands/Click (@click.command)Developers in a terminalHTTP (run_command)
ResourcesServer/src/services/resources/FastMCP (@mcp_for_unity_resource)AI assistants, read-onlyWebSocket

Both MCP tools and CLI commands eventually call the same C# HandleCommand methods inside MCPForUnity/Editor/Tools/. Resources are read-only — they observe state without mutating it.

Why three layers, not one

Each surface has a different shape of consumer:

  • MCP tools need rich type annotations (Annotated[Type, "description"]) because they're handed to an LLM. The descriptions are the prompt the LLM reads.
  • CLI commands need composable flags, shell-friendly defaults, and graceful error messages. Click gives those for free.
  • Resources need to be cheap to call repeatedly because the LLM polls them. They use a lighter decorator and skip the routing middleware.

Trying to autogenerate one layer from another erodes the ergonomics of all three. The cost is keeping the three in sync — which is mostly a discipline problem solved by domain symmetry.

Domain symmetry

When you add a new domain (say, manage_navigation), you write three files:

Server/src/services/tools/manage_navigation.py # @mcp_for_unity_tool
Server/src/cli/commands/navigation.py # @click.command
MCPForUnity/Editor/Tools/ManageNavigation.cs # [McpForUnityTool]

The Python tool and CLI command both invoke the C# handler — they just take different paths to it.

Tool registration

Tools are auto-discovered by walking Server/src/services/tools/. Each .py file with @mcp_for_unity_tool-decorated functions is imported at server startup; the decorator side-effects populate a global registry (services.registry). The registry is also what tools/generate_docs_reference.py reads to emit the tool reference.

A tool's group parameter controls per-session visibility — see Tool Groups. group=None means the tool is always visible (server meta-tools like set_active_instance and manage_tools).

Where the layers diverge from "just call the C# handler"

  • MCP tools add parameter normalization (camelCase → snake_case via ParamNormalizerMiddleware), telemetry, and per-session routing.
  • CLI commands add @handle_unity_errors for terminal-friendly stack traces, and synchronous wrappers around the async core.
  • Resources skip middleware entirely — they're meant to be hot-path.

Server entry point

Server/src/main.py (~935 lines) is the orchestrator:

  1. Builds the FastMCP server
  2. Calls register_all_tools(mcp) — auto-discovery
  3. Calls register_all_resources(mcp) — same pattern, different decorator
  4. Sets up the WebSocket hub for HTTP transport
  5. Configures middleware (telemetry, normalization, instance routing)
  6. Starts the transport (http/stdio from core.config)

Where to read more

  • Tool/CLI handler shape: Server/src/services/tools/manage_material.py is a canonical example
  • Registry: Server/src/services/registry/tool_registry.py (~130 LOC, the single source the docs reference generator reads)
  • Transport: Server/src/transport/ — plugin hub (plugin_hub.py), websocket client, legacy stdio bridge
  • C# side: MCPForUnity/Editor/Tools/ManageMaterial.cs is the C# half of manage_material