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.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);
public delegate string AgenticToolCallback(string 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!";
}
}
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.";
}
}
}
| Field | Type | Description |
|---|---|---|
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.XR_METAX1_agentic_external_tool OpenXR instance extension.XR_METAX1_AGENTIC_EXTERNAL_TOOL_EXTENSION_NAME to XrInstanceCreateInfo.enabledExtensionNames, and include the header:#include <meta_openxr_preview/metax1_agentic_external_tool.h>
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;
}
XrAgenticExternalToolRegisterInfoMETAX1 and register the tool. The parameter and register-info structs use fixed-size character fields, so copy your strings into them: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, ®isterInfo));
objectProperties up to 4095 bytes. snprintf truncates safely if a string is longer.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.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;
}
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;
}
MyApp_SpawnEnemy, to avoid collisions with built-in tools.