🎯 feat: Per-Tool Intent Label Toggles in the Agent Builder (#14550)

* 🎯 feat: Per-Tool Intent Label Toggles in the Agent Builder

Saved agents have had per-tool intent control on the backend since the
capability landed (tool_options[name].describe_intent, consumed by
applyIntentLabels), but the builder offered no way to set it - the
capability was invisible to saved agents on MCP tools, which default
off. This is the deferred UI slice.

The MCP tools panel gains a fourth per-tool option toggle (Captions
icon, teal) next to defer / programmatic / background, plus the
matching section-header bulk toggle, gated on the tool_intents
capability. The toggle writes describe_intent: true through the same
withBooleanOption path the sibling flags use, so an opt-in composes
with existing entries and clearing the last flag drops the tool's
entry entirely.

No backend changes: the agent CRUD schema already validates
describe_intent and initialization already consumes it.

* 🧯 fix: Keep the Intent Toggle Truthful for Programmatic-Only Tools

A tool marked Programmatic in the builder gets allowed_callers:
['code_execution'], and the backend's canInjectIntentParam deliberately
skips non-direct tools (no card renders for calls made from code), so
an intent opt-in on such a tool is guaranteed inert. The UI could
nevertheless show both settings active.

The intent toggle now mirrors the runtime gate: isToolProgrammaticOnly
(allowed_callers set and missing 'direct', the exact backend predicate)
renders the per-row toggle inert with a tooltip explaining why, shows
it unpressed regardless of any stored flag, and the bulk toggle and its
all-state consider only tools the label can actually reach. The stored
describe_intent value is preserved, so unmarking Programmatic restores
the user's earlier choice instead of destroying it.

OptionToggle gains a disabled state (dimmed, non-interactive, tooltip
kept) shared by the row and bulk variants.
This commit is contained in:
Danny Avila 2026-07-31 12:11:10 -04:00 committed by GitHub
parent 52b2ebf948
commit a67b0c1da8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 278 additions and 15 deletions

View file

@ -1,5 +1,5 @@
import { useState } from 'react';
import { Check, Clock, Code2, Info, Zap } from 'lucide-react';
import { Check, Clock, Code2, Captions, Info, Zap } from 'lucide-react';
import type { AgentToolType } from 'librechat-data-provider';
import OptionToggle from './OptionToggle';
import { useLocalize } from '~/hooks';
@ -11,13 +11,19 @@ interface MCPToolItemProps {
isDeferred: boolean;
isProgrammatic: boolean;
isBackground: boolean;
isIntent: boolean;
/** Intent labels never reach a programmatic-only tool (no card renders for
* calls made from code), so the toggle is shown inert with an explanation. */
intentDisabled: boolean;
deferredToolsEnabled: boolean;
programmaticToolsEnabled: boolean;
backgroundToolsEnabled: boolean;
toolIntentsEnabled: boolean;
onToggleSelect: () => void;
onToggleDefer: () => void;
onToggleProgrammatic: () => void;
onToggleBackground: () => void;
onToggleIntent: () => void;
}
const iconButton =
@ -33,9 +39,13 @@ export default function MCPToolItem({
onToggleProgrammatic,
isBackground,
onToggleBackground,
isIntent,
intentDisabled,
onToggleIntent,
deferredToolsEnabled,
programmaticToolsEnabled,
backgroundToolsEnabled,
toolIntentsEnabled,
}: MCPToolItemProps) {
const localize = useLocalize();
const [expanded, setExpanded] = useState(false);
@ -100,6 +110,19 @@ export default function MCPToolItem({
onToggle={onToggleBackground}
/>
)}
{toolIntentsEnabled && (
<OptionToggle
icon={Captions}
pressed={isIntent}
disabled={intentDisabled}
label={localize('com_ui_mcp_intent')}
tooltip={localize(
intentDisabled ? 'com_ui_mcp_intent_programmatic' : 'com_ui_mcp_click_to_intent',
)}
activeClass="text-teal-500"
onToggle={onToggleIntent}
/>
)}
<button
type="button"
onClick={() => setExpanded((value) => !value)}

View file

@ -12,11 +12,18 @@ interface OptionToggleProps {
activeClass: string;
onToggle: () => void;
size?: 'sm' | 'md';
/**
* Renders the toggle inert (dimmed, non-interactive) while keeping it
* visible with its tooltip, so the user can learn WHY the option is
* unavailable instead of it silently disappearing.
*/
disabled?: boolean;
}
/**
* Icon toggle for a per-tool option (defer / programmatic / background), shared
* between the per-tool row (`sm`) and the section-header bulk action (`md`).
* Icon toggle for a per-tool option (defer / programmatic / background /
* intent), shared between the per-tool row (`sm`) and the section-header bulk
* action (`md`).
*/
export default function OptionToggle({
icon: Icon,
@ -26,6 +33,7 @@ export default function OptionToggle({
activeClass,
onToggle,
size = 'sm',
disabled = false,
}: OptionToggleProps) {
return (
<TooltipAnchor
@ -34,13 +42,19 @@ export default function OptionToggle({
render={
<button
type="button"
onClick={onToggle}
onClick={disabled ? undefined : onToggle}
aria-pressed={pressed}
aria-label={label}
aria-disabled={disabled || undefined}
className={cn(
'flex items-center justify-center rounded-md transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary',
'flex items-center justify-center rounded-md transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary',
size === 'sm' ? 'size-6' : 'size-7',
pressed ? activeClass : 'text-text-secondary hover:text-text-primary',
disabled
? 'cursor-not-allowed text-text-tertiary opacity-60'
: cn(
'hover:bg-surface-hover',
pressed ? activeClass : 'text-text-secondary hover:text-text-primary',
),
)}
>
<Icon className="size-4" aria-hidden="true" />

View file

@ -8,6 +8,14 @@ const mockSetValue = jest.fn();
const mockGetValues = jest.fn((): string[] => []);
const mockInitializeServer = jest.fn();
const mockIsConnectionDeferred = jest.fn((): boolean => false);
const mockToggleIntentAll = jest.fn();
const mockIsToolProgrammaticOnly = jest.fn((_toolId: string): boolean => false);
const mockCapabilities = {
deferredToolsEnabled: false,
programmaticToolsEnabled: false,
backgroundToolsEnabled: false,
toolIntentsEnabled: false,
};
jest.mock('react-hook-form', () => ({
useFormContext: () => ({ control: {}, setValue: mockSetValue, getValues: mockGetValues }),
@ -26,10 +34,7 @@ jest.mock('~/components/ui', () => ({
jest.mock('~/hooks', () => ({
useLocalize: () => (key: string) => key,
useCopyToClipboard: () => jest.fn(),
useAgentCapabilities: () => ({
deferredToolsEnabled: false,
programmaticToolsEnabled: false,
}),
useAgentCapabilities: () => mockCapabilities,
useGetAgentsConfig: () => ({ agentsConfig: { capabilities: [] } }),
useMCPServerManager: () => ({
getServerStatusIconProps: () => null,
@ -45,15 +50,20 @@ jest.mock('~/hooks', () => ({
isToolDeferred: () => false,
isToolProgrammatic: () => false,
isToolBackground: () => false,
isToolIntent: () => false,
isToolProgrammaticOnly: mockIsToolProgrammaticOnly,
toggleToolDefer: jest.fn(),
toggleToolProgrammatic: jest.fn(),
toggleToolBackground: jest.fn(),
toggleToolIntent: jest.fn(),
areAllToolsDeferred: () => false,
areAllToolsProgrammatic: () => false,
areAllToolsBackground: () => false,
areAllToolsIntent: () => false,
toggleDeferAll: jest.fn(),
toggleProgrammaticAll: jest.fn(),
toggleBackgroundAll: jest.fn(),
toggleIntentAll: mockToggleIntentAll,
}),
}));
@ -96,6 +106,7 @@ jest.mock('~/components/MCP/McpOAuthDialog', () => ({
jest.mock('@librechat/client', () => {
const React = jest.requireActual('react');
return {
TooltipAnchor: ({ render }: { render: React.ReactElement }) => render,
Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) =>
React.createElement('button', { type: 'button', onClick }, children),
Checkbox: ({
@ -141,6 +152,10 @@ describe('McpSection', () => {
mockInitializeServer.mockReset();
mockIsConnectionDeferred.mockReset();
mockIsConnectionDeferred.mockReturnValue(false);
mockToggleIntentAll.mockClear();
mockIsToolProgrammaticOnly.mockReset();
mockIsToolProgrammaticOnly.mockReturnValue(false);
mockCapabilities.toolIntentsEnabled = false;
});
test('renders one row per tool', () => {
@ -283,6 +298,27 @@ describe('McpSection', () => {
);
});
test('bulk intent toggle renders only when the tool_intents capability is enabled', () => {
const { unmount } = render(<McpSection item={item} />);
expect(screen.queryByRole('button', { name: 'com_ui_mcp_intent_all' })).not.toBeInTheDocument();
unmount();
mockCapabilities.toolIntentsEnabled = true;
render(<McpSection item={item} />);
fireEvent.click(screen.getByRole('button', { name: 'com_ui_mcp_intent_all' }));
expect(mockToggleIntentAll).toHaveBeenCalledWith(item.server.tools);
});
test('bulk intent skips programmatic-only tools (label can never reach them)', () => {
mockCapabilities.toolIntentsEnabled = true;
mockIsToolProgrammaticOnly.mockImplementation((toolId: string) => toolId === 'mcp:srv:a');
render(<McpSection item={item} />);
fireEvent.click(screen.getByRole('button', { name: 'com_ui_mcp_intent_all' }));
expect(mockToggleIntentAll).toHaveBeenCalledWith([
expect.objectContaining({ tool_id: 'mcp:srv:b' }),
]);
});
test('shows the runtime-tools hint when attached via the wildcard', () => {
mockGetValues.mockReturnValue(['sys__all__sys_mcp_srv']);
const empty: McpItem = {

View file

@ -1,5 +1,5 @@
import { useState, useMemo, useEffect, useCallback } from 'react';
import { Clock, Code2, Zap } from 'lucide-react';
import { Clock, Code2, Captions, Zap } from 'lucide-react';
import { useFormContext, useWatch } from 'react-hook-form';
import { Button, Spinner, Checkbox, Skeleton } from '@librechat/client';
import type { MouseEvent } from 'react';
@ -76,21 +76,30 @@ export default function McpSection({ item }: Props) {
const [autoSelectPending, setAutoSelectPending] = useState(false);
const { mcpServersMap, mcpToolsLoading } = useAgentPanelContext();
const { agentsConfig } = useGetAgentsConfig();
const { deferredToolsEnabled, programmaticToolsEnabled, backgroundToolsEnabled } =
useAgentCapabilities(agentsConfig?.capabilities);
const {
deferredToolsEnabled,
programmaticToolsEnabled,
backgroundToolsEnabled,
toolIntentsEnabled,
} = useAgentCapabilities(agentsConfig?.capabilities);
const {
isToolDeferred,
isToolProgrammatic,
isToolBackground,
isToolIntent,
isToolProgrammaticOnly,
toggleToolDefer,
toggleToolProgrammatic,
toggleToolBackground,
toggleToolIntent,
areAllToolsDeferred,
areAllToolsProgrammatic,
areAllToolsBackground,
areAllToolsIntent,
toggleDeferAll,
toggleProgrammaticAll,
toggleBackgroundAll,
toggleIntentAll,
} = useMCPToolOptions();
const serverName = item.server.serverName;
@ -154,6 +163,11 @@ export default function McpSection({ item }: Props) {
const allDeferred = areAllToolsDeferred(tools);
const allProgrammatic = areAllToolsProgrammatic(tools);
const allBackground = areAllToolsBackground(tools);
/** Programmatic-only tools can never carry an intent label (the backend's
* `canInjectIntentParam` skips non-direct tools), so both the bulk toggle
* and its all-state only consider tools the label can actually reach. */
const intentEligibleTools = tools.filter((tool) => !isToolProgrammaticOnly(tool.tool_id));
const allIntent = areAllToolsIntent(intentEligibleTools);
const statusIconProps = getServerStatusIconProps(serverName);
const configDialogProps = getConfigDialogProps();
const connectionState = statusIconProps?.serverStatus?.connectionState;
@ -339,7 +353,21 @@ export default function McpSection({ item }: Props) {
onToggle={() => toggleBackgroundAll(tools)}
/>
)}
{(deferredToolsEnabled || programmaticToolsEnabled || backgroundToolsEnabled) && (
{toolIntentsEnabled && (
<OptionToggle
icon={Captions}
size="md"
pressed={allIntent}
disabled={intentEligibleTools.length === 0}
label={localize(allIntent ? 'com_ui_mcp_unintent_all' : 'com_ui_mcp_intent_all')}
activeClass="text-teal-600 dark:text-teal-500"
onToggle={() => toggleIntentAll(intentEligibleTools)}
/>
)}
{(deferredToolsEnabled ||
programmaticToolsEnabled ||
backgroundToolsEnabled ||
toolIntentsEnabled) && (
<span className="mx-1 h-4 w-px bg-border-light" aria-hidden="true" />
)}
<label className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 text-xs text-text-secondary">
@ -376,13 +404,21 @@ export default function McpSection({ item }: Props) {
isDeferred={deferredToolsEnabled && isToolDeferred(tool.tool_id)}
isProgrammatic={programmaticToolsEnabled && isToolProgrammatic(tool.tool_id)}
isBackground={backgroundToolsEnabled && isToolBackground(tool.tool_id)}
isIntent={
toolIntentsEnabled &&
isToolIntent(tool.tool_id) &&
!isToolProgrammaticOnly(tool.tool_id)
}
intentDisabled={isToolProgrammaticOnly(tool.tool_id)}
deferredToolsEnabled={deferredToolsEnabled}
programmaticToolsEnabled={programmaticToolsEnabled}
backgroundToolsEnabled={backgroundToolsEnabled}
toolIntentsEnabled={toolIntentsEnabled}
onToggleSelect={() => toggleToolSelect(tool.tool_id)}
onToggleDefer={() => toggleToolDefer(tool.tool_id)}
onToggleProgrammatic={() => toggleToolProgrammatic(tool.tool_id)}
onToggleBackground={() => toggleToolBackground(tool.tool_id)}
onToggleIntent={() => toggleToolIntent(tool.tool_id)}
/>
))}
</div>

View file

@ -23,13 +23,17 @@ function setup(overrides: Partial<React.ComponentProps<typeof MCPToolItem>> = {}
isDeferred: false,
isProgrammatic: false,
isBackground: false,
isIntent: false,
intentDisabled: false,
deferredToolsEnabled: false,
programmaticToolsEnabled: false,
backgroundToolsEnabled: false,
toolIntentsEnabled: false,
onToggleSelect: jest.fn(),
onToggleDefer: jest.fn(),
onToggleProgrammatic: jest.fn(),
onToggleBackground: jest.fn(),
onToggleIntent: jest.fn(),
...overrides,
};
render(<MCPToolItem {...props} />);
@ -106,4 +110,32 @@ describe('MCPToolItem', () => {
setup();
expect(screen.queryByRole('button', { name: 'com_ui_mcp_background' })).not.toBeInTheDocument();
});
test('intent label is an inline button rendered only when enabled', () => {
const props = setup({ toolIntentsEnabled: true });
const intentButton = screen.getByRole('button', { name: 'com_ui_mcp_intent' });
fireEvent.click(intentButton);
expect(props.onToggleIntent).toHaveBeenCalledTimes(1);
});
test('intent button is absent when tool intents are disabled', () => {
setup();
expect(screen.queryByRole('button', { name: 'com_ui_mcp_intent' })).not.toBeInTheDocument();
});
test('intent button reflects the opted-in state via aria-pressed', () => {
setup({ toolIntentsEnabled: true, isIntent: true });
expect(screen.getByRole('button', { name: 'com_ui_mcp_intent' })).toHaveAttribute(
'aria-pressed',
'true',
);
});
test('intent button is inert for programmatic-only tools (label can never render)', () => {
const props = setup({ toolIntentsEnabled: true, intentDisabled: true });
const intentButton = screen.getByRole('button', { name: 'com_ui_mcp_intent' });
expect(intentButton).toHaveAttribute('aria-disabled', 'true');
fireEvent.click(intentButton);
expect(props.onToggleIntent).not.toHaveBeenCalled();
});
});

View file

@ -109,4 +109,17 @@ describe('useAgentCapabilities', () => {
expect(result.current.deferredToolsEnabled).toBe(true);
expect(result.current.programmaticToolsEnabled).toBe(true);
});
it('should return toolIntentsEnabled as true when tool_intents is in capabilities', () => {
const { result } = renderHook(() => useAgentCapabilities([AgentCapabilities.tool_intents]));
expect(result.current.toolIntentsEnabled).toBe(true);
expect(result.current.backgroundToolsEnabled).toBe(false);
});
it('should return toolIntentsEnabled as false when absent', () => {
const { result } = renderHook(() => useAgentCapabilities([]));
expect(result.current.toolIntentsEnabled).toBe(false);
});
});

View file

@ -632,6 +632,78 @@ describe('useMCPToolOptions', () => {
});
});
describe('intent (describe_intent)', () => {
it('reads the flag only when explicitly true', () => {
(useWatch as jest.Mock).mockReturnValue({
tool1: { describe_intent: true },
tool2: { run_in_background: true },
});
const { result } = renderHook(() => useMCPToolOptions());
expect(result.current.isToolIntent('tool1')).toBe(true);
expect(result.current.isToolIntent('tool2')).toBe(false);
});
it('toggling on writes describe_intent without clobbering sibling flags', () => {
mockGetValues.mockReturnValue({ tool1: { run_in_background: true } });
const { result } = renderHook(() => useMCPToolOptions());
act(() => result.current.toggleToolIntent('tool1'));
expect(mockSetValue).toHaveBeenCalledWith(
'tool_options',
{ tool1: { run_in_background: true, describe_intent: true } },
{ shouldDirty: true },
);
});
it('toggling off removes the flag and drops an empty entry', () => {
mockGetValues.mockReturnValue({ tool1: { describe_intent: true } });
(useWatch as jest.Mock).mockReturnValue({ tool1: { describe_intent: true } });
const { result } = renderHook(() => useMCPToolOptions());
act(() => result.current.toggleToolIntent('tool1'));
expect(mockSetValue).toHaveBeenCalledWith('tool_options', {}, { shouldDirty: true });
});
it('bulk toggle marks every tool and unmarks when all are set', () => {
const tools = [createMockTool('tool1'), createMockTool('tool2')];
mockGetValues.mockReturnValue({});
const { result } = renderHook(() => useMCPToolOptions());
act(() => result.current.toggleIntentAll(tools));
expect(mockSetValue).toHaveBeenCalledWith(
'tool_options',
{ tool1: { describe_intent: true }, tool2: { describe_intent: true } },
{ shouldDirty: true },
);
});
});
describe('isToolProgrammaticOnly', () => {
it('mirrors the backend canInjectIntentParam gate exactly', () => {
(useWatch as jest.Mock).mockReturnValue({
codeOnly: { allowed_callers: ['code_execution'] },
both: { allowed_callers: ['direct', 'code_execution'] },
directOnly: { allowed_callers: ['direct'] },
empty: { allowed_callers: [] },
unset: { defer_loading: true },
});
const { result } = renderHook(() => useMCPToolOptions());
expect(result.current.isToolProgrammaticOnly('codeOnly')).toBe(true);
expect(result.current.isToolProgrammaticOnly('both')).toBe(false);
expect(result.current.isToolProgrammaticOnly('directOnly')).toBe(false);
expect(result.current.isToolProgrammaticOnly('empty')).toBe(false);
expect(result.current.isToolProgrammaticOnly('unset')).toBe(false);
expect(result.current.isToolProgrammaticOnly('missing')).toBe(false);
});
});
describe('formToolOptions', () => {
it('should return undefined when useWatch returns undefined', () => {
(useWatch as jest.Mock).mockReturnValue(undefined);

View file

@ -15,6 +15,7 @@ interface AgentCapabilitiesResult {
deferredToolsEnabled: boolean;
programmaticToolsEnabled: boolean;
backgroundToolsEnabled: boolean;
toolIntentsEnabled: boolean;
}
export default function useAgentCapabilities(
@ -85,6 +86,11 @@ export default function useAgentCapabilities(
[capabilities],
);
const toolIntentsEnabled = useMemo(
() => capabilities?.includes(AgentCapabilities.tool_intents) ?? false,
[capabilities],
);
return {
ocrEnabled,
codeEnabled,
@ -99,5 +105,6 @@ export default function useAgentCapabilities(
deferredToolsEnabled,
programmaticToolsEnabled,
backgroundToolsEnabled,
toolIntentsEnabled,
};
}

View file

@ -4,7 +4,7 @@ import type { AgentToolOptions, AllowedCaller, AgentToolType } from 'librechat-d
import type { UseFormGetValues, UseFormSetValue } from 'react-hook-form';
import type { AgentForm } from '~/common';
type BooleanToolOptionKey = 'defer_loading' | 'run_in_background';
type BooleanToolOptionKey = 'defer_loading' | 'run_in_background' | 'describe_intent';
interface BooleanOptionHandlers {
isSet: (toolId: string) => boolean;
@ -24,15 +24,20 @@ interface UseMCPToolOptionsReturn {
isToolDeferred: (toolId: string) => boolean;
isToolProgrammatic: (toolId: string) => boolean;
isToolBackground: (toolId: string) => boolean;
isToolIntent: (toolId: string) => boolean;
isToolProgrammaticOnly: (toolId: string) => boolean;
toggleToolDefer: (toolId: string) => void;
toggleToolProgrammatic: (toolId: string) => void;
toggleToolBackground: (toolId: string) => void;
toggleToolIntent: (toolId: string) => void;
areAllToolsDeferred: (tools: AgentToolType[]) => boolean;
areAllToolsProgrammatic: (tools: AgentToolType[]) => boolean;
areAllToolsBackground: (tools: AgentToolType[]) => boolean;
areAllToolsIntent: (tools: AgentToolType[]) => boolean;
toggleDeferAll: (tools: AgentToolType[]) => void;
toggleProgrammaticAll: (tools: AgentToolType[]) => void;
toggleBackgroundAll: (tools: AgentToolType[]) => void;
toggleIntentAll: (tools: AgentToolType[]) => void;
}
/**
@ -146,6 +151,7 @@ export default function useMCPToolOptions(): UseMCPToolOptionsReturn {
const defer = useBooleanToolOption('defer_loading', formContext);
const background = useBooleanToolOption('run_in_background', formContext);
const intent = useBooleanToolOption('describe_intent', formContext);
/** `allowed_callers` is array-valued, so the programmatic family stays bespoke. */
const isToolProgrammatic = useCallback(
@ -154,6 +160,20 @@ export default function useMCPToolOptions(): UseMCPToolOptionsReturn {
[formToolOptions],
);
/**
* Whether the tool can NEVER be called directly (`allowed_callers` set and
* missing `direct`) mirrors the backend's `canInjectIntentParam` gate: no
* card renders for such calls, so intent labels are guaranteed inert and
* the intent toggle must not present a setting runtime will ignore.
*/
const isToolProgrammaticOnly = useCallback(
(toolId: string): boolean => {
const callers = formToolOptions?.[toolId]?.allowed_callers;
return callers != null && callers.length > 0 && !callers.includes('direct');
},
[formToolOptions],
);
const toggleToolProgrammatic = useCallback(
(toolId: string) => {
const currentOptions = getValues('tool_options') || {};
@ -240,14 +260,19 @@ export default function useMCPToolOptions(): UseMCPToolOptionsReturn {
isToolDeferred: defer.isSet,
isToolProgrammatic,
isToolBackground: background.isSet,
isToolIntent: intent.isSet,
isToolProgrammaticOnly,
toggleToolDefer: defer.toggle,
toggleToolProgrammatic,
toggleToolBackground: background.toggle,
toggleToolIntent: intent.toggle,
areAllToolsDeferred: defer.areAllSet,
areAllToolsProgrammatic,
areAllToolsBackground: background.areAllSet,
areAllToolsIntent: intent.areAllSet,
toggleDeferAll: defer.toggleAll,
toggleProgrammaticAll,
toggleBackgroundAll: background.toggleAll,
toggleIntentAll: intent.toggleAll,
};
}

View file

@ -1343,6 +1343,7 @@
"com_ui_mcp_background_all": "Mark all as background",
"com_ui_mcp_click_to_background": "Runs this tool in the background: it returns immediately and the agent polls for the result, so it can keep working while the tool runs.",
"com_ui_mcp_click_to_defer": "Loads this tool only when the agent needs it instead of keeping it active the whole time. Useful when a server has many tools.",
"com_ui_mcp_click_to_intent": "The agent states in one sentence what each call is about to do, shown live as the call's status. Adds a small token cost per request.",
"com_ui_mcp_click_to_programmatic": "The agent calls this tool by writing code, not as a direct tool call. Best for tools meant to run in code.",
"com_ui_mcp_defer": "Defer",
"com_ui_mcp_defer_all": "Defer all tools",
@ -1353,6 +1354,9 @@
"com_ui_mcp_init_failed": "Failed to initialize MCP server",
"com_ui_mcp_initialize": "Initialize",
"com_ui_mcp_initialized_success": "MCP server '{{0}}' initialized successfully",
"com_ui_mcp_intent": "Intent label",
"com_ui_mcp_intent_all": "Enable intent labels for all",
"com_ui_mcp_intent_programmatic": "Intent labels don't apply to programmatic-only tools: calls made from code render no tool card, so there is nowhere to show the label.",
"com_ui_mcp_invalid_url": "Please enter a valid URL",
"com_ui_mcp_missing_custom_user_vars": "MCP server '{{0}}' requires variables [{{1}}] which are not set",
"com_ui_mcp_no_description": "No description available",
@ -1391,6 +1395,7 @@
"com_ui_mcp_unbackground_all": "Unmark all as background",
"com_ui_mcp_undefer": "Undefer",
"com_ui_mcp_undefer_all": "Undefer all tools",
"com_ui_mcp_unintent_all": "Disable intent labels for all",
"com_ui_mcp_unprogrammatic_all": "Unmark all as programmatic",
"com_ui_mcp_update_var": "Update {{0}}",
"com_ui_mcp_url": "MCP Server URL",