# How MCP Custom Tool Service Handles Project-Scoped Tools in Unity

> Discover how MCP custom tool service manages project-scoped tools in Unity. Learn about per-project definitions and visibility scope in the CoplayDev/unity-mcp repository.

- Repository: [Coplay/unity-mcp](https://github.com/CoplayDev/unity-mcp)
- Tags: how-to-guide
- Published: 2026-07-06

---

**The MCP server in CoplayDev/unity-mcp supports both global and project-scoped custom tools, storing per-project definitions in `_project_tools` and checking the `project_scoped_tools` flag during registration to determine visibility scope.**

The **MCP custom tool service** provides a flexible registration system that isolates tools per Unity project or shares them globally across all connected instances. This architecture lives primarily in [`Server/src/services/custom_tool_service.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/services/custom_tool_service.py) and allows developers to expose project-specific utilities without polluting the global tool namespace.

## Service Initialization and Scope Configuration

When the server boots in [`Server/src/main.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/main.py), it instantiates `CustomToolService` with a boolean flag that controls scoping behavior.

The constructor signature defines the default behavior:

```python
def __init__(self, mcp: FastMCP, project_scoped_tools: bool = True):
    CustomToolService._instance = self
    self._mcp = mcp
    self._project_scoped_tools = project_scoped_tools    # ← scope flag

```

**`project_scoped_tools`** defaults to `True`, meaning tools submitted by a Unity project are stored privately unless the server explicitly disables isolation. This flag is checked later during the registration flow at lines L108‑L124.

## Registering Tools via the HTTP Endpoint

Unity projects communicate with the service through the **`/register-tools`** POST route. This endpoint accepts a JSON payload containing the `project_id`, optional `project_hash`, and a list of tool definitions.

The route handler in [`Server/src/services/custom_tool_service.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/services/custom_tool_service.py#L80)‑L89) validates the payload and delegates to the internal registration method:

```python
@self._mcp.custom_route("/register-tools", methods=["POST"])
async def register_tools(request: Request) -> JSONResponse:
    payload = RegisterToolsPayload.model_validate(await request.json())
    registered, replaced = self._register_project_tools(
        payload.project_id, payload.tools, project_hash=payload.project_hash)

```

## Project-Scoped Registration Logic

The method `_register_project_tools` implements the core isolation logic. It stores every tool definition under `self._project_tools[project_id]`, creating a nested dictionary that maps project identifiers to their private tool sets.

During iteration, the service respects the `project_scoped_tools` flag to decide whether to also promote the tool globally:

```python
for tool in tools:
    # ... validation logic ...

    self._register_tool(project_id, tool)          # always saved per-project

    if not self._project_scoped_tools:            # global only when disabled

        self._register_global_tool(tool)

```

When **`project_scoped_tools` is `True`**, the tool remains private to the specified `project_id`. When `False`, the definition is duplicated into `self._global_tools`, making it visible to every Unity project connected to the server.

## Global Tool Handling and Overrides

Even when project-scoped mode is enabled, administrators can inject global tools programmatically using `register_global_tools` (lines L290‑L334). This public method skips any names that collide with built-in tools to prevent accidental overrides:

```python

# From Server/src/services/custom_tool_service.py

def register_global_tools(self, tools: List[ToolDefinitionModel]):
    for tool in tools:
        if tool.name in self.BUILT_IN_TOOL_NAMES:
            logger.warning(f"Skipping global registration of built-in tool: {tool.name}")
            continue
        self._register_global_tool(tool)

```

## Tool Execution and Lookup Order

When an MCP client invokes a tool, `execute_tool` resolves the correct definition using a prioritized fallback chain (lines L20‑L26):

```python

# 1. Check project-scoped registry first

tool = self._project_tools.get(project_id, {}).get(tool_name)
if tool:
    return tool

# 2. Fall back to global registry

tool = self._global_tools.get(tool_name)
if tool:
    return tool

# 3. Final fallback to Hub-provided definitions

return await PluginHub.get_tool_definition(project_id, tool_name, user_id=user_id)

```

This lookup order ensures that **project-scoped definitions shadow global tools** of the same name, allowing per-project overrides while maintaining a shared fallback layer.

## Resolving Project Identity via Hash

Unity instances often identify themselves using a `Name@hash` format. The service resolves these to canonical `project_id` values through `resolve_project_id_for_unity_instance` (lines L99‑L146):

- **Direct resolution:** If the instance was discovered via the stdio connection pool, the hash provided by Unity is returned immediately.
- **Hash-to-project mapping:** During registration, the service populates `self._hash_to_project` with mappings derived from the `project_hash` field, enabling lookup even for instances not currently in the connection pool.

## Practical Examples

### Registering a Project-Scoped Tool via HTTP

Projects send tool definitions to the registration endpoint. Because the server defaults to `project_scoped_tools=True`, these remain private:

```bash
curl -X POST http://localhost:8000/register-tools \
     -H "Content-Type: application/json" \
     -d '{
           "project_id": "my-awesome-game",
           "tools": [
               {
                 "name": "generate-terrain",
                 "description": "Creates procedural terrain",
                 "parameters": [
                   {"name": "width", "type": "int", "required": true},
                   {"name": "height", "type": "int", "required": true}
                 ],
                 "requires_polling": false
               }
           ]
         }'

```

The `generate-terrain` tool is now only callable when the MCP client specifies `"my-awesome-game"` as the active project.

### Disabling Project Scope for Global Registration

To force all incoming tools into the global registry, modify the service instantiation in [`Server/src/main.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/main.py):

```python
from services.custom_tool_service import CustomToolService

# Disable per-project isolation

custom_tool_service = CustomToolService(mcp, project_scoped_tools=False)

```

Now `_register_project_tools` will invoke `self._register_global_tool(tool)` for every submitted definition, making them available to any Unity instance regardless of `project_id`.

### Programmatic Global Registration

Server-side plugins can inject shared utilities without using the HTTP route:

```python
from services.custom_tool_service import CustomToolService
from models.models import ToolDefinitionModel, ToolParameterModel

ci_tool = ToolDefinitionModel(
    name="upload-build",
    description="Uploads a build to the CI server",
    parameters=[ToolParameterModel(name="branch", type="string", required=True)],
    requires_polling=False,
)

CustomToolService.get_instance().register_global_tools([ci_tool])

```

## Summary

- **Isolation by default:** The `CustomToolService` constructor sets `project_scoped_tools=True`, ensuring tools are stored per-project in `self._project_tools` unless explicitly configured otherwise.
- **Dual storage paths:** The `_register_project_tools` method always saves tools to the project dictionary and conditionally adds them to `self._global_tools` based on the scope flag.
- **Priority resolution:** During execution, the service checks project-scoped definitions first, then global tools, then falls back to the Plugin Hub.
- **Hash resolution:** The service maintains `self._hash_to_project` mappings to resolve Unity instance hashes to canonical project IDs for proper tool routing.
- **Collision safety:** Global registration methods skip built-in tool names to prevent accidental overrides of core MCP functionality.

## Frequently Asked Questions

### How do I make a custom tool available to all Unity projects?

Set `project_scoped_tools=False` when initializing `CustomToolService` in [`Server/src/main.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/main.py). Alternatively, call `register_global_tools()` programmatically with your tool definitions, which bypasses the project-scoped storage entirely.

### What happens if two projects register tools with the same name?

Each project's tools are stored under separate keys in `self._project_tools[project_id]`, so name collisions only occur within the same project. During execution, the lookup uses the requesting project's ID, ensuring the correct definition is retrieved. Global tools share a single namespace, so the last registered global definition wins if names collide.

### Can a project override a global tool with its own version?

Yes. The lookup order in `execute_tool` checks `self._project_tools` before `self._global_tools`. If a project registers a tool with the same name as a global tool, the project-scoped version takes precedence for that specific project.

### How does the service handle Unity projects that haven't registered tools yet?

If no project-scoped or global definition exists, the service falls back to `PluginHub.get_tool_definition()`. This delegates to the Hub system, which may provide default tools or return a "not found" response depending on the Hub configuration.