diff --git a/api/server/controllers/agents/openai.js b/api/server/controllers/agents/openai.js index 9045e0055c8..6c9eb0a0ffa 100644 --- a/api/server/controllers/agents/openai.js +++ b/api/server/controllers/agents/openai.js @@ -335,6 +335,7 @@ const OpenAIChatCompletionController = async (req, res) => { ephemeralSkillsToggle, }), codeEnvAvailable: enabledCapabilities.has(AgentCapabilities.execute_code), + backgroundToolsAvailable: enabledCapabilities.has(AgentCapabilities.run_in_background), statefulSessionsAvailable: enabledCapabilities.has( AgentCapabilities.stateful_code_sessions, ), @@ -415,6 +416,7 @@ const OpenAIChatCompletionController = async (req, res) => { defaultActiveOnShare, /** @see DiscoverConnectedAgentsParams.codeEnvAvailable */ codeEnvAvailable: enabledCapabilities.has(AgentCapabilities.execute_code), + backgroundToolsAvailable: enabledCapabilities.has(AgentCapabilities.run_in_background), statefulSessionsAvailable: enabledCapabilities.has( AgentCapabilities.stateful_code_sessions, ), @@ -500,6 +502,7 @@ const OpenAIChatCompletionController = async (req, res) => { agent: ctx.agent ?? agent, signal: abortController.signal, toolRegistry: ctx.toolRegistry, + backgroundToolNames: ctx.backgroundToolNames, mcpAvailableTools: ctx.mcpAvailableTools, requestScopedConnections: ctx.requestScopedConnections, userMCPAuthMap: ctx.userMCPAuthMap, diff --git a/api/server/controllers/agents/responses.js b/api/server/controllers/agents/responses.js index a0d40195c5c..a9551ff66d0 100644 --- a/api/server/controllers/agents/responses.js +++ b/api/server/controllers/agents/responses.js @@ -459,6 +459,7 @@ const createResponse = async (req, res) => { ephemeralSkillsToggle, }), codeEnvAvailable: enabledCapabilities.has(AgentCapabilities.execute_code), + backgroundToolsAvailable: enabledCapabilities.has(AgentCapabilities.run_in_background), statefulSessionsAvailable: enabledCapabilities.has( AgentCapabilities.stateful_code_sessions, ), @@ -539,6 +540,7 @@ const createResponse = async (req, res) => { defaultActiveOnShare, /** @see DiscoverConnectedAgentsParams.codeEnvAvailable */ codeEnvAvailable: enabledCapabilities.has(AgentCapabilities.execute_code), + backgroundToolsAvailable: enabledCapabilities.has(AgentCapabilities.run_in_background), statefulSessionsAvailable: enabledCapabilities.has( AgentCapabilities.stateful_code_sessions, ), @@ -720,6 +722,7 @@ const createResponse = async (req, res) => { agent: ctx.agent ?? agent, signal: abortController.signal, toolRegistry: ctx.toolRegistry, + backgroundToolNames: ctx.backgroundToolNames, mcpAvailableTools: ctx.mcpAvailableTools, requestScopedConnections: ctx.requestScopedConnections, userMCPAuthMap: ctx.userMCPAuthMap, @@ -900,6 +903,7 @@ const createResponse = async (req, res) => { agent: ctx.agent ?? agent, signal: abortController.signal, toolRegistry: ctx.toolRegistry, + backgroundToolNames: ctx.backgroundToolNames, mcpAvailableTools: ctx.mcpAvailableTools, requestScopedConnections: ctx.requestScopedConnections, userMCPAuthMap: ctx.userMCPAuthMap, diff --git a/api/server/services/Endpoints/agents/addedConvo.js b/api/server/services/Endpoints/agents/addedConvo.js index 91ea170d346..09fad7a67fc 100644 --- a/api/server/services/Endpoints/agents/addedConvo.js +++ b/api/server/services/Endpoints/agents/addedConvo.js @@ -81,6 +81,7 @@ const processAddedConvo = async ({ skillStates, defaultActiveOnShare, codeEnvAvailable, + backgroundToolsAvailable, statefulSessionsAvailable, memoryAvailable, }) => { @@ -177,6 +178,7 @@ const processAddedConvo = async ({ ephemeralSkillsToggle, }), codeEnvAvailable, + backgroundToolsAvailable, statefulSessionsAvailable, memoryAvailable, skillStates, diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 2a7e0264862..d94f48d6f6a 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -149,6 +149,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { const enabledCapabilities = new Set(appConfig?.endpoints?.[EModelEndpoint.agents]?.capabilities); const skillsCapabilityEnabled = enabledCapabilities.has(AgentCapabilities.skills); const codeEnvAvailable = enabledCapabilities.has(AgentCapabilities.execute_code); + const backgroundToolsAvailable = enabledCapabilities.has(AgentCapabilities.run_in_background); const statefulSessionsAvailable = enabledCapabilities.has( AgentCapabilities.stateful_code_sessions, ); @@ -230,6 +231,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { toolNames, agent: ctx.agent, toolRegistry: ctx.toolRegistry, + backgroundToolNames: ctx.backgroundToolNames, mcpAvailableTools: ctx.mcpAvailableTools, requestScopedConnections: ctx.requestScopedConnections, userMCPAuthMap: ctx.userMCPAuthMap, @@ -408,6 +410,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { accessibleSkillIds: primaryScopedSkillIds, skillAuthoringAvailable: primarySkillAuthoringAvailable, codeEnvAvailable, + backgroundToolsAvailable, statefulSessionsAvailable, memoryAvailable, skillStates, @@ -485,6 +488,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { skillStates, defaultActiveOnShare, codeEnvAvailable, + backgroundToolsAvailable, statefulSessionsAvailable, memoryAvailable, }, @@ -558,6 +562,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { skillStates, defaultActiveOnShare, codeEnvAvailable, + backgroundToolsAvailable, statefulSessionsAvailable, memoryAvailable, }); @@ -698,6 +703,11 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { * false`, so `bash_tool` / `read_file` sandbox fallback are * silently gated off even though the seed walk found it. */ codeEnvAvailable, + /* Background tool calls are intentionally NOT enabled for pure + * subagents (spawn-tool child graphs): their tools don't reach the + * host ON_TOOL_EXECUTE background interceptor, so injecting the + * schema would advertise a poll tool the host can't honor. Backgrounding + * subagent work is the durable follow-up. */ statefulSessionsAvailable, memoryAvailable, skillStates, diff --git a/api/server/services/Endpoints/agents/skillDeps.js b/api/server/services/Endpoints/agents/skillDeps.js index 7154c1d52b3..8a9098551cd 100644 --- a/api/server/services/Endpoints/agents/skillDeps.js +++ b/api/server/services/Endpoints/agents/skillDeps.js @@ -278,6 +278,7 @@ function buildAgentToolContext({ agent, config }) { * the producing agent's config in multi-endpoint graphs. */ endpointTokenConfig: config.endpointTokenConfig, toolRegistry: config.toolRegistry, + backgroundToolNames: config.backgroundToolNames, mcpAvailableTools: config.mcpAvailableTools, requestScopedConnections: config.requestScopedConnections, userMCPAuthMap: config.userMCPAuthMap, diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 46971a416e2..f6fc35f1ed5 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -887,6 +887,14 @@ function createToolInstance({ }); toolInstance.mcp = true; toolInstance.mcpRawServerName = serverName; + // Ephemeral request-scoped servers (runtime body placeholders) tear their + // connection down at request end, so they must never be backgrounded. A + // missing/stale config means the server's lifetime is unknowable, so fail + // closed (foreground) rather than risk a detached call against a torn-down + // connection. + toolInstance.mcpRequiresEphemeralConnection = capturedServerConfig + ? requiresEphemeralUserConnection(capturedServerConfig) + : true; // On Google/Vertex, propagate the union-flattened schema so definitions extracted // from this instance don't reach the Gemini converter with unsupported unions. toolInstance.mcpJsonSchema = isGoogle ? schema : parameters; diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index ffc0993d1e6..be67b60fa78 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -1450,6 +1450,7 @@ async function loadToolsForExecution({ agent, toolNames, toolRegistry, + backgroundToolNames, mcpAvailableTools, requestScopedConnections, userMCPAuthMap, @@ -1461,6 +1462,12 @@ async function loadToolsForExecution({ const allLoadedTools = []; const mcpRequestScopedConnections = requestScopedConnections ?? getMCPRequestContext(req, res); const configurable = { userMCPAuthMap, requestScopedConnections: mcpRequestScopedConnections }; + /** Per-agent set of tools that received the injected `run_in_background` + * param; the event-driven executor gates background dispatch and the + * `check_background_task` poll tool on this reliable per-agent channel. */ + if (backgroundToolNames?.length) { + configurable.backgroundToolNames = backgroundToolNames; + } const isToolSearch = toolNames.includes(AgentConstants.TOOL_SEARCH); const ptcToolNames = [ diff --git a/client/src/components/SidePanel/Agents/MCPToolItem.tsx b/client/src/components/SidePanel/Agents/MCPToolItem.tsx index d3a49308392..b82fc11797b 100644 --- a/client/src/components/SidePanel/Agents/MCPToolItem.tsx +++ b/client/src/components/SidePanel/Agents/MCPToolItem.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; -import { TooltipAnchor } from '@librechat/client'; -import { Check, Clock, Code2, Info } from 'lucide-react'; +import { Check, Clock, Code2, Info, Zap } from 'lucide-react'; import type { AgentToolType } from 'librechat-data-provider'; +import OptionToggle from './OptionToggle'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; @@ -10,11 +10,14 @@ interface MCPToolItemProps { isSelected: boolean; isDeferred: boolean; isProgrammatic: boolean; + isBackground: boolean; deferredToolsEnabled: boolean; programmaticToolsEnabled: boolean; + backgroundToolsEnabled: boolean; onToggleSelect: () => void; onToggleDefer: () => void; onToggleProgrammatic: () => void; + onToggleBackground: () => void; } const iconButton = @@ -28,8 +31,11 @@ export default function MCPToolItem({ onToggleSelect, isProgrammatic, onToggleProgrammatic, + isBackground, + onToggleBackground, deferredToolsEnabled, programmaticToolsEnabled, + backgroundToolsEnabled, }: MCPToolItemProps) { const localize = useLocalize(); const [expanded, setExpanded] = useState(false); @@ -65,45 +71,33 @@ export default function MCPToolItem({
{item.description}
++ {item.description} +
)}{item.description}
++ {item.description} +
) : ({localize('com_ui_tools_no_description')} diff --git a/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/ToolSection.tsx b/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/ToolSection.tsx index c4e974ffa77..31616ec4721 100644 --- a/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/ToolSection.tsx +++ b/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/ToolSection.tsx @@ -62,7 +62,9 @@ export default function ToolSection({ item }: Props) { return (
{item.description}
++ {item.description} +
) : (
{localize('com_ui_tools_no_description')}
diff --git a/client/src/components/SidePanel/Agents/__tests__/MCPToolItem.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/MCPToolItem.spec.tsx
index 6b6c131a4ee..b18a3621ec4 100644
--- a/client/src/components/SidePanel/Agents/__tests__/MCPToolItem.spec.tsx
+++ b/client/src/components/SidePanel/Agents/__tests__/MCPToolItem.spec.tsx
@@ -22,11 +22,14 @@ function setup(overrides: Partial