72 lines
4.0 KiB
Markdown
72 lines
4.0 KiB
Markdown
---
|
|
id: python-layers
|
|
slug: /architecture/python-layers
|
|
title: Three-Layer Python Design
|
|
sidebar_label: Three-Layer Python Design
|
|
description: The Python server has three independent surfaces — MCP tools, CLI commands, and resources — that all funnel into the same C# Editor handlers.
|
|
---
|
|
|
|
# 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**.
|
|
|
|
| Layer | Where | Framework | Consumer | Transport to Unity |
|
|
|---|---|---|---|---|
|
|
| **MCP Tools** | `Server/src/services/tools/` | FastMCP (`@mcp_for_unity_tool`) | AI assistants via MCP | WebSocket (`send_with_unity_instance`) |
|
|
| **CLI Commands** | `Server/src/cli/commands/` | Click (`@click.command`) | Developers in a terminal | HTTP (`run_command`) |
|
|
| **Resources** | `Server/src/services/resources/` | FastMCP (`@mcp_for_unity_resource`) | AI assistants, read-only | WebSocket |
|
|
|
|
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](/reference/tools).
|
|
|
|
A tool's `group` parameter controls per-session visibility — see [Tool Groups](/guides/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`
|