Develop

Registering custom tools for Meta XR Operator

Updated: Jul 23, 2026
Experimental feature
Meta XR Operator is an experimental component of the Meta XR Core SDK. Tool names, APIs, and behavior may change in future releases. Avoid depending on it in production apps.
Beyond the built-in tools, your app can register its own custom MCP tools so an AI agent can call app-specific functionality, such as spawning an object, changing a setting, or reading game state. There are two ways to register a tool:
  • The Unity C# API, for Unity apps. This is the recommended path for most developers.
  • The native OpenXR API layer extension, for native OpenXR apps or other engines.
Both register a named tool, a description, a set of typed parameters, and a callback. The agent discovers the tool over MCP, calls it with arguments as a JSON string, and receives your result string back.
Register each tool once, from Start() rather than Awake(), so registration runs after the XR session has initialized. Tools can’t be unregistered during a session, and registering a name that’s already taken fails. Callbacks run on the application main thread by default, so keep them lightweight.

Register a tool from Unity (C#)

Call MetaXROperatorExternalTool.RegisterAgenticTool (namespace Meta.XR) from a MonoBehaviour:
public static unsafe XrResult RegisterAgenticTool(
    string toolName,
    string toolDescription,
    AgenticToolParameter[] parameters,   // null for a tool with no parameters
    AgenticToolCallback callback,
    XrAgenticExternalToolCallbackInfoFlagsMETAX1 callbackFlags =
        XrAgenticExternalToolCallbackInfoFlagsMETAX1.ApplicationMainThreadBit);
The callback delegate receives the agent’s arguments as a JSON string and returns a result string (return JSON for structured data):
public delegate string AgenticToolCallback(string parameters);

Example tool with no parameters

using Meta.XR;
using UnityEngine;

public class MetaXROperatorTools : MonoBehaviour
{
    void Start()
    {
        MetaXROperatorExternalTool.RegisterAgenticTool(
            "MyApp_HelloWorld",
            "An example tool registered from Unity.",
            null,
            HelloWorldCallback);
    }

    private static string HelloWorldCallback(string parameters)
    {
        return "Hello from a Unity external tool!";
    }
}

Example tool with one parameter

using System;
using Meta.XR;
using UnityEngine;

public class EchoTool : MonoBehaviour
{
    [Serializable]
    private struct EchoParams
    {
        public string input;
    }

    void Start()
    {
        MetaXROperatorExternalTool.RegisterAgenticTool(
            "MyApp_Echo",
            "Echoes the input string back as the result.",
            new[]
            {
                new AgenticToolParameter
                {
                    Name = "input",
                    Description = "The string to echo back.",
                    ParamType = XrAgenticExternalToolParameterTypeMETAX1.String,
                    IsRequired = true,
                },
            },
            EchoCallback);
    }

    private static string EchoCallback(string parameters)
    {
        try
        {
            return JsonUtility.FromJson<EchoParams>(parameters).input;
        }
        catch
        {
            return "Invalid input.";
        }
    }
}

Tool parameter fields

FieldTypeDescription
Name
string
The parameter name (up to 63 bytes).
Description
string
What the parameter does. Agents rely on this to call the tool correctly.
ParamType
XrAgenticExternalToolParameterTypeMETAX1
One of String, Number, Boolean, Array, or Object.
IsRequired
bool
Whether the agent must supply the parameter.
ArrayItemType
XrAgenticExternalToolParameterTypeMETAX1
The item type, when ParamType is Array.
ObjectProperties
string
A JSON schema for the object’s properties, when ParamType is Object (up to 4095 bytes).
RegisterAgenticTool returns an XrResult. Registering a tool whose name is already in use returns XrResult.ErrorValidationFailure. There is no unregister call, so register each tool once per session.

Register a tool from the OpenXR API layer (native)

Native OpenXR apps, or apps on other engines, register tools through the XR_METAX1_agentic_external_tool OpenXR instance extension.
  1. Enable the extension when you create the OpenXR instance by adding XR_METAX1_AGENTIC_EXTERNAL_TOOL_EXTENSION_NAME to XrInstanceCreateInfo.enabledExtensionNames, and include the header:
    #include <meta_openxr_preview/metax1_agentic_external_tool.h>
    
  2. Resolve the registration function with xrGetInstanceProcAddr:
    PFN_xrAgenticRegisterExternalToolMETAX1 xrAgenticRegisterExternalToolMETAX1 = nullptr;
    if (XR_FAILED(xrGetInstanceProcAddr(
            instance,
            "xrAgenticRegisterExternalToolMETAX1",
            reinterpret_cast<PFN_xrVoidFunction*>(&xrAgenticRegisterExternalToolMETAX1))) ||
        xrAgenticRegisterExternalToolMETAX1 == nullptr) {
        // The extension isn't available on this runtime; skip tool registration.
        return;
    }
    
  3. Fill XrAgenticExternalToolRegisterInfoMETAX1 and register the tool. The parameter and register-info structs use fixed-size character fields, so copy your strings into them:
Note: You may need to change the suffix META to METAX1 in the sample code below.
// Define tool parameters
XrAgenticExternalToolParameterMETA params[4] = {};

// name (optional string)
params[0].type = XR_TYPE_AGENTIC_EXTERNAL_TOOL_PARAMETER_META;
params[0].next = nullptr;
strcpy(params[0].paramName, "name");
strcpy(params[0].paramDescription,
       "Unique identifier for the cube (auto-generated if not provided)");
params[0].paramType = XR_AGENTIC_EXTERNAL_TOOL_PARAMETER_TYPE_STRING_META;
params[0].isRequired = XR_FALSE;

// position (required array of 3 numbers)
params[1].type = XR_TYPE_AGENTIC_EXTERNAL_TOOL_PARAMETER_META;
params[1].next = nullptr;
strcpy(params[1].paramName, "position");
strcpy(params[1].paramDescription, "Position in 3D space as [x, y, z]");
params[1].paramType = XR_AGENTIC_EXTERNAL_TOOL_PARAMETER_TYPE_ARRAY_META;
params[1].isRequired = XR_TRUE;
params[1].arrayItemType =
    XR_AGENTIC_EXTERNAL_TOOL_PARAMETER_TYPE_NUMBER_META;

// orientation (optional array of 4 numbers)
params[2].type = XR_TYPE_AGENTIC_EXTERNAL_TOOL_PARAMETER_META;
params[2].next = nullptr;
strcpy(params[2].paramName, "orientation");
strcpy(params[2].paramDescription,
       "Orientation quaternion as [x, y, z, w] (default: [0, 0, 0, 1])");
params[2].paramType = XR_AGENTIC_EXTERNAL_TOOL_PARAMETER_TYPE_ARRAY_META;
params[2].isRequired = XR_FALSE;
params[2].arrayItemType =
    XR_AGENTIC_EXTERNAL_TOOL_PARAMETER_TYPE_NUMBER_META;

// scale (optional array of 3 numbers)
params[3].type = XR_TYPE_AGENTIC_EXTERNAL_TOOL_PARAMETER_META;
params[3].next = nullptr;
strcpy(params[3].paramName, "scale");
strcpy(params[3].paramDescription,
       "Scale of the cube as [x, y, z] (default: [0.05, 0.05, 0.05])");
params[3].paramType = XR_AGENTIC_EXTERNAL_TOOL_PARAMETER_TYPE_ARRAY_META;
params[3].isRequired = XR_FALSE;
params[3].arrayItemType =
    XR_AGENTIC_EXTERNAL_TOOL_PARAMETER_TYPE_NUMBER_META;

// Register the tool
XrAgenticExternalToolRegisterInfoMETA registerInfo = {};
registerInfo.type = XR_TYPE_AGENTIC_EXTERNAL_TOOL_REGISTER_INFO_META;
registerInfo.next = nullptr;
strcpy(registerInfo.toolName, "create_cube");
strcpy(registerInfo.toolDescription,
       "Create a new animated cube in the scene with specified position, "
       "orientation, and scale");
registerInfo.toolParameterCount = 4;
registerInfo.toolParameters = params;
registerInfo.toolCallback = CreateCubeCallback;
registerInfo.toolCallbackInfoFlags =
    XR_AGENTIC_EXTERNAL_TOOL_CALLBACK_INFO_APPLICATION_MAIN_THREAD_BIT_META;
registerInfo.toolUserData = appState;

CHK_XR(xrAgenticRegisterExternalToolMETA(instance, &registerInfo));
The register-info and parameter structs use fixed-size character fields, so the usable string length is one byte less than each field’s size, leaving room for the terminating NUL: tool and parameter names up to 63 bytes, descriptions up to 1023 bytes, and objectProperties up to 4095 bytes. snprintf truncates safely if a string is longer.
The callback uses the standard OpenXR two-call buffer pattern. The runtime may first call it with resultBuffer set to nullptr to query the size it needs, then call it again with a buffer to fill. On every call, set *resultBufferCountOutput to the number of bytes your result needs, including the terminating NUL. Write into resultBuffer only when it is non-null and resultBufferCapacityInput is large enough; otherwise return XR_ERROR_SIZE_INSUFFICIENT without writing.
Option 1 (if a reasonable max size can be defined):
XrResult XRAPI_PTR CreateCubeCallback_Option1(
    char* resultBuffer,
    uint32_t resultBufferCapacityInput,
    uint32_t* resultBufferCountOutput,
    const char* parameters,
    void* userData) {

  // For size query, return estimated max size without performing action
  if (resultBufferCapacityInput == 0 || resultBuffer == nullptr) {
    *resultBufferCountOutput = MAX_CREATE_CUBE_RESULT_SIZE;
    return XR_SUCCESS;
  }

  // Ensure buffer is large enough before performing any action
  if (resultBufferCapacityInput < MAX_CREATE_CUBE_RESULT_SIZE) {
    *resultBufferCountOutput = MAX_CREATE_CUBE_RESULT_SIZE;
    return XR_ERROR_SIZE_INSUFFICIENT;
  }

  // Buffer is guaranteed to be large enough, safe to create cube
  std::string result = CreateCube(parameters, userData);
  uint32_t requiredSize = static_cast<uint32_t>(result.size()) + 1;
  assert(requiredSize <= MAX_CREATE_CUBE_RESULT_SIZE);

  *resultBufferCountOutput = requiredSize;
  memcpy(resultBuffer, result.c_str(), requiredSize);
  return XR_SUCCESS;
}
Option 2 (robust for complex operations):
XrResult XRAPI_PTR CreateCubeCallback_Option2(
    char* resultBuffer,
    uint32_t resultBufferCapacityInput,
    uint32_t* resultBufferCountOutput,
    const char* parameters,
    void* userData) {

  // Dry run: validate params and compute result size without side effects
  std::string result = CreateCubeDryRun(parameters, userData);
  uint32_t requiredSize = static_cast<uint32_t>(result.size()) + 1;
  *resultBufferCountOutput = requiredSize;

  if (resultBufferCapacityInput == 0 || resultBuffer == nullptr) {
    return XR_SUCCESS;
  }
  if (resultBufferCapacityInput < requiredSize) {
    return XR_ERROR_SIZE_INSUFFICIENT;
  }

  // Actually create the cube only when we can write the result
  result = CreateCubeExecute(parameters, userData);
  assert(static_cast<uint32_t>(result.size()) + 1 == requiredSize);
  memcpy(resultBuffer, result.c_str(), requiredSize);
  return XR_SUCCESS;
}

Best practices

  • Prefix tool names with your app name, such as MyApp_SpawnEnemy, to avoid collisions with built-in tools.
  • Write clear, specific descriptions for the tool and each parameter. Agents choose and call tools based on these descriptions, so vague text leads to wrong calls.
  • Return structured JSON for anything the agent needs to parse.
  • Keep callbacks lightweight. They run on the application main thread by default, so heavy work can stall rendering.
  • Parse incoming parameters defensively, since they come from an agent and may be malformed.