diff --git a/client/src/components/SidePanel/Agents/AgentConfig.tsx b/client/src/components/SidePanel/Agents/AgentConfig.tsx
index 0d8d5d008f..927fc1a5bd 100644
--- a/client/src/components/SidePanel/Agents/AgentConfig.tsx
+++ b/client/src/components/SidePanel/Agents/AgentConfig.tsx
@@ -116,7 +116,10 @@ export default function AgentConfig() {
type="button"
onClick={() => setActivePanel(Panel.model)}
title={model || undefined}
- className="relative flex h-9 w-full min-w-0 items-center overflow-hidden rounded-lg border border-border-light bg-surface-secondary px-1 text-sm font-medium text-text-primary transition-colors hover:bg-surface-tertiary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
+ className={cn(
+ 'relative flex h-9 w-full min-w-0 items-center overflow-hidden rounded-lg border border-border-light bg-surface-secondary text-sm font-medium text-text-primary transition-colors hover:bg-surface-tertiary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary',
+ model != null && model ? 'px-1' : 'px-3',
+ )}
>
{Icon && (
diff --git a/client/src/components/SidePanel/Agents/Tools/ToolsMarketplaceDialog.tsx b/client/src/components/SidePanel/Agents/Tools/ToolsMarketplaceDialog.tsx
index efaaa90967..a1427b10f0 100644
--- a/client/src/components/SidePanel/Agents/Tools/ToolsMarketplaceDialog.tsx
+++ b/client/src/components/SidePanel/Agents/Tools/ToolsMarketplaceDialog.tsx
@@ -7,12 +7,13 @@ import {
OGDialogTitle,
OGDialogContent,
OGDialogDescription,
+ useToastContext,
} from '@librechat/client';
import type { AgentItem, AgentItemKind, ItemFilter } from './items/types';
import type { AgentForm } from '~/common';
+import { itemKey, mcpServerToken, matchesMcpServer } from './items/selectors';
import { useAgentItems, useUninstallToolCredentials } from './hooks';
import AddMcpServerDialog from './ItemDialog/AddMcpServerDialog';
-import { itemKey, mcpServerToken } from './items/selectors';
import { computeToggleAction } from './items/mutations';
import { useLocalize, useToolFavorites } from '~/hooks';
import MarketplaceSidebar from './MarketplaceSidebar';
@@ -20,6 +21,7 @@ import MarketplaceCatalog from './MarketplaceCatalog';
import ItemDialog from './ItemDialog/ItemDialog';
import { applyFilter } from './items/filtering';
import { NEW_ACTION_ID } from './items/types';
+import { isEphemeralAgent } from '~/common';
interface ToolsMarketplaceDialogProps {
open: boolean;
@@ -36,6 +38,7 @@ export default function ToolsMarketplaceDialog({
agentId,
}: ToolsMarketplaceDialogProps) {
const localize = useLocalize();
+ const { showToast } = useToastContext();
const { getValues, setValue } = useFormContext
();
const uninstallToolCredentials = useUninstallToolCredentials();
@@ -56,6 +59,13 @@ export default function ToolsMarketplaceDialog({
setAddMcpOpen(true);
return;
}
+ /** Actions are persisted against the agent id, so an unsaved agent has
+ * nothing to attach them to — surface the save-first error instead of
+ * letting the editor fail on submit. */
+ if (isEphemeralAgent(agentId)) {
+ showToast({ message: localize('com_agents_no_agent_id_error'), status: 'error' });
+ return;
+ }
setDetailItem({
kind: 'action',
id: NEW_ACTION_ID,
@@ -65,7 +75,7 @@ export default function ToolsMarketplaceDialog({
endpointCount: 0,
});
},
- [localize],
+ [agentId, localize, showToast],
);
const selectedIds = useMemo(() => new Set(selectedItems.map(itemKey)), [selectedItems]);
@@ -123,11 +133,10 @@ export default function ToolsMarketplaceDialog({
case 'mcp-remove': {
if (item.kind !== 'mcp') break;
const toolIds = new Set((item.server.tools ?? []).map((t) => t.tool_id));
- const serverToken = mcpServerToken(item.id);
const current = (getValues('tools') ?? []) as string[];
setValue(
'tools',
- current.filter((t) => t !== serverToken && !toolIds.has(t)),
+ current.filter((t) => !matchesMcpServer(t, item.id) && !toolIds.has(t)),
{ shouldDirty: true },
);
break;
diff --git a/client/src/components/SidePanel/Agents/Tools/items/__tests__/selectors.spec.ts b/client/src/components/SidePanel/Agents/Tools/items/__tests__/selectors.spec.ts
index d52e23925c..44383f81a8 100644
--- a/client/src/components/SidePanel/Agents/Tools/items/__tests__/selectors.spec.ts
+++ b/client/src/components/SidePanel/Agents/Tools/items/__tests__/selectors.spec.ts
@@ -1,7 +1,7 @@
import { AgentCapabilities, Constants } from 'librechat-data-provider';
import type { AgentItem } from '../types';
import { makePlugin, makeSkill, makeMcpServer, makeAction } from 'test/itemFactories';
-import { deriveSelectedItems } from '../selectors';
+import { deriveSelectedItems, matchesMcpServer } from '../selectors';
const mcpServerToken = (serverName: string) =>
`${Constants.mcp_server}${Constants.mcp_delimiter}${serverName}`;
@@ -197,3 +197,23 @@ describe('deriveSelectedItems', () => {
expect(kindOrder.indexOf('tool')).toBeLessThan(kindOrder.indexOf('skill'));
});
});
+
+describe('matchesMcpServer', () => {
+ test('matches every persisted token format for the server', () => {
+ expect(matchesMcpServer(`${Constants.mcp_server}${Constants.mcp_delimiter}srv`, 'srv')).toBe(
+ true,
+ );
+ expect(matchesMcpServer('srv', 'srv')).toBe(true);
+ expect(matchesMcpServer('mcp_srv', 'srv')).toBe(true);
+ expect(matchesMcpServer('mcp_srv_tool', 'srv')).toBe(true);
+ expect(matchesMcpServer('search_mcp_srv', 'srv')).toBe(true);
+ expect(matchesMcpServer(`sys__all__sys${Constants.mcp_delimiter}srv`, 'srv')).toBe(true);
+ });
+
+ test('does not match tokens for other servers or plain tool ids', () => {
+ expect(matchesMcpServer('mcp_other', 'srv')).toBe(false);
+ expect(matchesMcpServer('search_mcp_other', 'srv')).toBe(false);
+ expect(matchesMcpServer('dalle', 'srv')).toBe(false);
+ expect(matchesMcpServer('srv2', 'srv')).toBe(false);
+ });
+});
diff --git a/client/src/components/SidePanel/Agents/Tools/items/selectors.ts b/client/src/components/SidePanel/Agents/Tools/items/selectors.ts
index 40a56324dc..916153a814 100644
--- a/client/src/components/SidePanel/Agents/Tools/items/selectors.ts
+++ b/client/src/components/SidePanel/Agents/Tools/items/selectors.ts
@@ -62,17 +62,28 @@ export function mcpServerToken(serverName: string): string {
return `${Constants.mcp_server}${Constants.mcp_delimiter}${serverName}`;
}
+/**
+ * Whether a form `tools` token references the given MCP server, across every
+ * format ever persisted: the server placeholder token, the raw server name,
+ * the legacy `mcp_` token, and per-tool ids in both prefix and suffix
+ * shapes. Selection and removal must share this predicate — anything the
+ * selection logic counts as attached, an explicit remove must also strip, or
+ * a legacy token leaves the server permanently selected.
+ */
+export function matchesMcpServer(token: string, serverName: string): boolean {
+ const prefixed = `${MCP_PREFIX}${serverName}`;
+ return (
+ token === mcpServerToken(serverName) ||
+ token === serverName ||
+ token === prefixed ||
+ token.startsWith(`${prefixed}_`) ||
+ token.endsWith(`_${prefixed}`)
+ );
+}
+
function isMcpSelected(item: AgentItem, form: FormSelection): boolean {
if (item.kind !== 'mcp') return false;
- const prefixed = `${MCP_PREFIX}${item.id}`;
- return form.tools.some(
- (t) =>
- t === mcpServerToken(item.id) ||
- t === item.id ||
- t === prefixed ||
- t.startsWith(`${prefixed}_`) ||
- t.endsWith(`_${prefixed}`),
- );
+ return form.tools.some((t) => matchesMcpServer(t, item.id));
}
function isSkillSelected(item: AgentItem, form: FormSelection): boolean {