Custom Tools
Implement and register app-specific Ansight tools with ITool, schemas, guard scopes, and security metadata.
Custom tools let a paired agent call app-specific operations that the built-in suites cannot know about. Use them for narrow diagnostic and development workflows such as exposing a feature-flag snapshot, clearing a local test queue, replaying a known fixture, or reading a domain-specific health report.
Custom tools use the same remote-tool pipeline as the packaged suites:
- implement
Ansight.Tools.ITool - register the tool with
OptionsBuilder.AddTool(...)orAddTools(...) - choose a guard with
WithReadOnlyToolAccess(),WithReadWriteToolAccess(),WithAllToolAccess(), orWithToolGuard(...) - keep the implementation and build-time remote-tool opt-in limited to local development builds
Recommended Shape
For more than one tool, keep the code organized like a first-party suite:
MyAppToolIds: stable tool ids such asmyapp.diagnostics_snapshotMyAppToolSchemas: sharedToolSchemaobjects for arguments and resultsMyAppToolSecurityProfiles: sharedToolSecuritydeclarations- one small
IToolimplementation per operation MyAppAnsightOptionsBuilderExtensions: aWithMyAppTools(...)registration method- optional runtime wrappers for user confirmation or product-specific access checks
This keeps metadata consistent and makes it harder to accidentally ship a broad, hidden command surface.
Build-Time Opt-In
The Ansight build target scans for concrete ITool implementations in your app output, including custom tools defined inside your app assembly.
Use a project-local flag to include custom tools only in local development builds:
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<MyAppAnsightRemoteToolsEnabled>true</MyAppAnsightRemoteToolsEnabled>
</PropertyGroup>
<PropertyGroup>
<MyAppAnsightRemoteToolsEnabled Condition="'$(MyAppAnsightRemoteToolsEnabled)' == ''">false</MyAppAnsightRemoteToolsEnabled>
<AnsightRemoteToolsPolicy Condition="'$(AnsightRemoteToolsPolicy)' == '' and '$(MyAppAnsightRemoteToolsEnabled)' == 'true'">AllowedWithWarnings</AnsightRemoteToolsPolicy>
<AnsightRemoteToolsPolicy Condition="'$(AnsightRemoteToolsPolicy)' == ''">Disallowed</AnsightRemoteToolsPolicy>
</PropertyGroup>
<PropertyGroup Condition="'$(MyAppAnsightRemoteToolsEnabled)' == 'true'">
<DefineConstants>$(DefineConstants);MYAPP_ANSIGHT_TOOLS</DefineConstants>
</PropertyGroup>
Then wrap the app-specific tool files in the same symbol:
#if MYAPP_ANSIGHT_TOOLS
// Custom Ansight tools live here.
#endif
Protected Release, CI, TestFlight, App Store, Play Store, and other distributable builds should omit custom tool implementations or use AnsightRemoteToolsPolicy=Disallowed so the build fails if any concrete ITool remains.
Shared Metadata
Centralize ids, schemas, and security profiles before writing individual tools.
#if MYAPP_ANSIGHT_TOOLS
using Ansight.Tools;
internal static class MyAppToolIds
{
public const string DiagnosticsSnapshot = "myapp.diagnostics_snapshot";
}
internal static class MyAppToolSchemas
{
public static ToolSchema DiagnosticsSnapshotArguments { get; } = ToolSchema.Object(
description: "Arguments for reading a small app diagnostic summary.",
properties: new Dictionary<string, ToolSchema>
{
["includeQueues"] = ToolSchema.Boolean("Include queue counters in the result.")
});
public static ToolSchema DiagnosticsSnapshotResult { get; } = ToolSchema.Object(
description: "App diagnostic summary payload.",
properties: new Dictionary<string, ToolSchema>
{
["connectionState"] = ToolSchema.String("Current backend connection state."),
["pendingQueueCount"] = ToolSchema.Integer("Pending queue count when includeQueues is true.")
},
required: new[] { "connectionState" });
}
internal static class MyAppToolSecurityProfiles
{
public static ToolSecurity DiagnosticsSnapshot { get; } = new(
ToolSecurityLevel.Low,
"Reads app-owned diagnostic state.",
ToolSecurityImplications.ReadsAppData,
ToolSecurityImplications.InspectsRuntimeState);
}
#endif
Implement a Tool
Each tool declares catalog metadata, a guard scope, argument and result schemas, security metadata, and an execution method. ITool.Definition is supplied by the interface from those properties, so most tools do not need to implement it manually.
#if MYAPP_ANSIGHT_TOOLS
using System.Text.Json.Nodes;
using Ansight.Tools;
internal sealed record DiagnosticsSnapshot(
string ConnectionState,
int PendingQueueCount);
internal sealed class DiagnosticsSnapshotTool(
Func<DiagnosticsSnapshot> snapshotProvider) : ITool
{
public string Category => "myapp";
public ToolScope Scope => ToolScope.Read;
public string Id => MyAppToolIds.DiagnosticsSnapshot;
public string Name => "Diagnostics Snapshot";
public string Description => "Returns a small app-specific diagnostic summary.";
public string Keywords => "diagnostics health queues";
public ToolSchema ArgumentsSchema => MyAppToolSchemas.DiagnosticsSnapshotArguments;
public ToolSchema ResultSchema => MyAppToolSchemas.DiagnosticsSnapshotResult;
public ToolSecurity Security => MyAppToolSecurityProfiles.DiagnosticsSnapshot;
public Task<ToolResult> Execute(IReadOnlyDictionary<string, string> arguments)
{
ArgumentNullException.ThrowIfNull(arguments);
var includeQueues = arguments.TryGetValue("includeQueues", out var includeQueuesText)
&& bool.TryParse(includeQueuesText, out var parsedIncludeQueues)
&& parsedIncludeQueues;
var snapshot = snapshotProvider();
var payload = new JsonObject
{
["connectionState"] = snapshot.ConnectionState
};
if (includeQueues)
{
payload["pendingQueueCount"] = snapshot.PendingQueueCount;
}
return Task.FromResult(ToolResult.Success(payload));
}
}
#endif
Arguments arrive as flattened string values. Parse and validate them inside Execute(...), and return ToolResult.Failure(...) with a stable errorCode when the request is invalid or the app cannot perform the operation.
For MAUI tools that touch UI state, marshal onto the main thread inside the tool or a shared helper before reading or mutating controls.
Register the Tools
Expose one extension method that registers your app tools as a group:
#if MYAPP_ANSIGHT_TOOLS
using Ansight;
using Ansight.Tools;
internal static class MyAppAnsightOptionsBuilderExtensions
{
public static Options.OptionsBuilder WithMyAppTools(
this Options.OptionsBuilder builder,
Func<DiagnosticsSnapshot> snapshotProvider)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(snapshotProvider);
return builder.AddTools(new ITool[]
{
new DiagnosticsSnapshotTool(snapshotProvider)
});
}
}
#endif
For a core-only setup:
var optionsBuilder = Options.CreateBuilder();
#if MYAPP_ANSIGHT_TOOLS
optionsBuilder = optionsBuilder.WithMyAppTools(diagnostics.CreateSnapshot);
#endif
var options = optionsBuilder
.WithReadOnlyToolAccess()
.Build();
For the all-in-one package:
var options = Options.CreateBuilder()
.WithAnsightSdk(ansight =>
{
#if MYAPP_ANSIGHT_TOOLS
ansight.WithMyAppTools(diagnostics.CreateSnapshot);
#endif
ansight.WithReadOnlyToolAccess();
})
.Build();
For MAUI:
builder.UseAnsight<App>(ansight =>
{
#if MYAPP_ANSIGHT_TOOLS
ansight.WithMyAppTools(diagnostics.CreateSnapshot);
#endif
ansight.WithReadOnlyToolAccess();
});
Registered tools stay hidden and unusable until the guard allows their ToolScope.
Runtime Confirmation
For sensitive tools, add a runtime consent or policy wrapper in addition to build-time and guard controls:
internal sealed class UserConfirmedTool(
ITool innerTool,
Func<string, Task<bool>> confirmAsync) : ITool
{
public string Category => innerTool.Category;
public ToolScope Scope => innerTool.Scope;
public string Id => innerTool.Id;
public string Name => innerTool.Name;
public string Description => innerTool.Description;
public string Keywords => innerTool.Keywords;
public ToolSchema ArgumentsSchema => innerTool.ArgumentsSchema;
public ToolSchema ResultSchema => innerTool.ResultSchema;
public ToolSecurity Security => innerTool.Security;
public async Task<ToolResult> Execute(IReadOnlyDictionary<string, string> arguments)
{
if (!await confirmAsync(Name))
{
return ToolResult.Failure(
"Remote tool access was not allowed by the user.",
errorCode: "remote_tools_not_allowed");
}
return await innerTool.Execute(arguments);
}
}
To wrap every registered tool, build once, replace the tool collection, then choose the guard:
var configured = optionsBuilder.Build();
var confirmedTools = configured.Tools.Select(tool =>
new UserConfirmedTool(tool, ConfirmRemoteToolAsync));
var options = Options.CreateBuilder(configured)
.WithTools(confirmedTools)
.WithReadOnlyToolAccess()
.Build();
Runtime confirmation is useful for internal dogfood or staff builds, but it is not a reason to ship remote tools in public distribution builds.
Scopes and Guards
Choose the narrowest scope that matches the operation:
| Scope | Use for | Guard that enables it |
|---|---|---|
ToolScope.Read | Inspecting app state without mutation. | WithReadOnlyToolAccess() |
ToolScope.Write | Creating, updating, replaying, resetting, or otherwise mutating app-owned state. | WithReadWriteToolAccess() |
ToolScope.Delete | Removing app-owned data or destructive cleanup. | WithAllToolAccess() |
A custom ToolGuard can be used when a workflow needs a narrower policy than the built-in presets.
Schemas
Use ToolSchema so Ansight Studio and MCP clients can show useful argument templates:
ToolSchema.Object(...)ToolSchema.Array(...)ToolSchema.String(...)ToolSchema.Integer(...)ToolSchema.Number(...)ToolSchema.Boolean(...)
Keep schemas small and explicit. Prefer stable argument names, mark required fields on object schemas, and avoid accepting arbitrary JSON unless the tool genuinely needs it.
Security Metadata
Declare Security for every custom tool. Choose the highest level that reflects the real risk, then list relevant ToolSecurityImplications.
Common implications include:
ToolSecurityImplications.ReadsAppDataToolSecurityImplications.WritesAppDataToolSecurityImplications.DeletesAppDataToolSecurityImplications.AccessesFileSystemToolSecurityImplications.AccessesDatabasesToolSecurityImplications.AccessesPreferencesToolSecurityImplications.AccessesSecureStorageToolSecurityImplications.HandlesSecretsToolSecurityImplications.InspectsRuntimeStateToolSecurityImplications.MutatesRuntimeStateToolSecurityImplications.InvokesAppCodeToolSecurityImplications.CapturesScreenshots
Do not expose secrets, account data, production endpoints, destructive app actions, or broad command dispatchers through custom tools. Build the smallest operation that satisfies the local debugging workflow.
Calling From Ansight Studio or MCP
After the app is paired and connected, Ansight Studio can query the live app tool catalog. MCP clients can use:
ansight_list_app_toolsto see visible custom and packaged app toolsansight_call_app_toolwith the customtoolIdand JSON arguments to invoke a visible tool
Example MCP call payload:
{
"toolId": "myapp.diagnostics_snapshot",
"arguments": {
"includeQueues": true
}
}
If the tool does not appear in the catalog, check that the app is connected, the tool is registered, the custom tool code is included only in a local development build, and the active guard allows the tool scope.