mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🃏 fix: Attach Request-Scoped MCP Servers From the Builder via the mcp_all Wildcard (#14177)
* fix: Attach Request-Scoped MCP Servers from the Agent Builder via mcp_all Follow-up to #14148 / #14074: request-scoped MCP servers (runtime {{LIBRECHAT_BODY_*}} placeholder headers) defer their connection on reinitialize, so their tools are never enumerable in the agent builder and the attach flow (which waits for isConnected && hasTools) silently attaches nothing. The runtime already resolves an mcp_all (sys__all__sys_mcp_<server>) tool entry into the server's full tool set at chat-turn time - the builder just never writes that token. - reinitMCPServer returns connectionDeferred: true on the deferred branch so clients can distinguish it from a plain empty success (server configs are sanitized client-side, so the response is the only reliable signal) - /mcp/:serverName/reinitialize forwards the flag; data-provider mutation type includes it - McpSection attaches [mcp_server, mcp_all] tokens on a deferred connect (idempotent) and shows a "tools are resolved at runtime" hint instead of "no tools yet" when wildcard-attached - selectors: mcpAllToken() helper beside mcpServerToken() Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: address review — deferred attach via init state; strip stale wildcard Two review findings: 1. Servers with customUserVars route Connect through the config dialog, whose save path calls initializeServer inside the manager — the McpSection never awaits that response, so the deferred attach was unreachable. Record connectionDeferred in the shared per-server init state (MCPServerInitState) on every initialize attempt and key the attach off that state in the auto-select effect: one attach site now covers both the direct Connect and the config-dialog path. 2. updateFormTools kept an existing mcp_all wildcard when rewriting a per-tool selection, so a server that later exposes a normal tool list would still grant every tool at runtime while the UI showed a subset. The wildcard is now stripped unless explicitly re-passed, making per-tool selection always supersede it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: address review — stale deferred state; fold wildcard into display Second review round: 1. connectionDeferred persisted across attempts, so a later Connect click could attach the wildcard from a stale flag before the new attempt reported. Reset it at the start of every initializeServer call, and clear it before routing into the customUserVars config dialog (resetConnectionDeferred) so only the current attempt's outcome can trigger the auto-attach effect. 2. With a wildcard attached and the server's tools later enumerable, the dialog showed every tool unchecked while runtime granted all of them. getSelectedTools now folds the wildcard into the display (all tools selected); any selection interaction rewrites the form with concrete ids and drops the wildcard, converting the attachment on first touch. Also sorts imports in McpSection.tsx (CI sort-imports gate). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8cfe4d8d07
commit
4182f9094f
10 changed files with 204 additions and 15 deletions
|
|
@ -718,7 +718,7 @@ router.post(
|
|||
return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' });
|
||||
}
|
||||
|
||||
const { success, message, oauthRequired, oauthUrl } = result;
|
||||
const { success, message, oauthRequired, oauthUrl, connectionDeferred } = result;
|
||||
|
||||
if (oauthRequired) {
|
||||
const flowId = getOAuthFlowId(user.id, serverName);
|
||||
|
|
@ -731,6 +731,7 @@ router.post(
|
|||
oauthUrl,
|
||||
serverName,
|
||||
oauthRequired,
|
||||
connectionDeferred,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MCP Reinitialize] Unexpected error', error);
|
||||
|
|
|
|||
|
|
@ -139,6 +139,10 @@ async function reinitMCPServer({
|
|||
return {
|
||||
availableTools: null,
|
||||
success: true,
|
||||
/** Lets clients distinguish "connection deferred to a chat turn" from a
|
||||
* plain success with no tools, e.g. to attach the server at the server
|
||||
* level instead of waiting for a tool list that never arrives. */
|
||||
connectionDeferred: true,
|
||||
message: `MCP server '${serverName}' uses request-scoped placeholders; connection will be established on first use in a chat turn`,
|
||||
oauthRequired: false,
|
||||
serverName,
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ describe('reinitMCPServer — runtime BODY placeholder pre-check (issue #14074)'
|
|||
expect(result).toMatchObject({
|
||||
availableTools: null,
|
||||
success: true,
|
||||
connectionDeferred: true,
|
||||
tools: null,
|
||||
oauthRequired: false,
|
||||
serverName,
|
||||
|
|
@ -240,7 +241,7 @@ describe('reinitMCPServer — runtime BODY placeholder pre-check (issue #14074)'
|
|||
fetchTools: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
|
||||
await reinitMCPServer({
|
||||
const result = await reinitMCPServer({
|
||||
user,
|
||||
serverName,
|
||||
serverConfig,
|
||||
|
|
@ -249,6 +250,7 @@ describe('reinitMCPServer — runtime BODY placeholder pre-check (issue #14074)'
|
|||
});
|
||||
|
||||
expect(mockGetConnection).toHaveBeenCalledTimes(1);
|
||||
expect(result.connectionDeferred).toBeUndefined();
|
||||
});
|
||||
|
||||
it('reports missing customUserVars before deferring on body placeholders', async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { McpItem } from '../../items/types';
|
||||
import McpSection from '../sections/McpSection';
|
||||
|
|
@ -7,6 +7,7 @@ import McpSection from '../sections/McpSection';
|
|||
const mockSetValue = jest.fn();
|
||||
const mockGetValues = jest.fn((): string[] => []);
|
||||
const mockInitializeServer = jest.fn();
|
||||
const mockIsConnectionDeferred = jest.fn((): boolean => false);
|
||||
|
||||
jest.mock('react-hook-form', () => ({
|
||||
useFormContext: () => ({ control: {}, setValue: mockSetValue, getValues: mockGetValues }),
|
||||
|
|
@ -34,6 +35,8 @@ jest.mock('~/hooks', () => ({
|
|||
getServerStatusIconProps: () => null,
|
||||
getConfigDialogProps: () => null,
|
||||
initializeServer: mockInitializeServer,
|
||||
isConnectionDeferred: mockIsConnectionDeferred,
|
||||
resetConnectionDeferred: jest.fn(),
|
||||
getOAuthUrl: () => undefined,
|
||||
isCancellable: () => false,
|
||||
cancelOAuthFlow: jest.fn(),
|
||||
|
|
@ -132,6 +135,8 @@ describe('McpSection', () => {
|
|||
mockSetValue.mockClear();
|
||||
mockGetValues.mockReturnValue([]);
|
||||
mockInitializeServer.mockReset();
|
||||
mockIsConnectionDeferred.mockReset();
|
||||
mockIsConnectionDeferred.mockReturnValue(false);
|
||||
});
|
||||
|
||||
test('renders one row per tool', () => {
|
||||
|
|
@ -212,4 +217,76 @@ describe('McpSection', () => {
|
|||
render(<McpSection item={empty} />);
|
||||
expect(screen.getByText('com_ui_tools_mcp_no_tools')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('deferred connect attaches the whole server via the mcp_all wildcard', async () => {
|
||||
// Request-scoped servers (runtime {{LIBRECHAT_BODY_*}} placeholders) defer
|
||||
// their connection to the next chat turn, so no tool list arrives here —
|
||||
// Connect should attach the server-wide wildcard instead of waiting.
|
||||
mockInitializeServer.mockResolvedValue({ success: true, connectionDeferred: true });
|
||||
mockIsConnectionDeferred.mockReturnValue(true);
|
||||
const empty: McpItem = {
|
||||
...item,
|
||||
server: { ...item.server, tools: [] } as never,
|
||||
toolCount: 0,
|
||||
};
|
||||
render(<McpSection item={empty} />);
|
||||
fireEvent.click(screen.getByText('com_nav_mcp_connect_server'));
|
||||
await waitFor(() =>
|
||||
expect(mockSetValue).toHaveBeenCalledWith(
|
||||
'tools',
|
||||
['sys__server__sys_mcp_srv', 'sys__all__sys_mcp_srv'],
|
||||
expect.objectContaining({ shouldDirty: true }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('deferred connect does not duplicate an already-attached wildcard', async () => {
|
||||
mockInitializeServer.mockResolvedValue({ success: true, connectionDeferred: true });
|
||||
mockIsConnectionDeferred.mockReturnValue(true);
|
||||
mockGetValues.mockReturnValue(['sys__server__sys_mcp_srv', 'sys__all__sys_mcp_srv']);
|
||||
const empty: McpItem = {
|
||||
...item,
|
||||
server: { ...item.server, tools: [] } as never,
|
||||
toolCount: 0,
|
||||
};
|
||||
render(<McpSection item={empty} />);
|
||||
fireEvent.click(screen.getByText('com_nav_mcp_connect_server'));
|
||||
await waitFor(() => expect(mockInitializeServer).toHaveBeenCalled());
|
||||
expect(mockSetValue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('wildcard attachment shows every enumerable tool as selected', () => {
|
||||
// The mcp_all wildcard grants every tool at runtime; if the server's tools
|
||||
// become enumerable, the display must reflect that instead of showing
|
||||
// unchecked boxes while runtime grants everything.
|
||||
mockGetValues.mockReturnValue(['sys__all__sys_mcp_srv']);
|
||||
render(<McpSection item={item} />);
|
||||
expect(screen.getByTestId('tool-mcp:srv:a')).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(screen.getByTestId('tool-mcp:srv:b')).toHaveAttribute('aria-pressed', 'true');
|
||||
});
|
||||
|
||||
test('touching a selection converts the wildcard to concrete tool ids', () => {
|
||||
// With a wildcard attached and tools enumerable, deselecting one tool must
|
||||
// rewrite the form with the remaining concrete ids and drop the wildcard —
|
||||
// otherwise runtime would still grant every tool while the UI shows a subset.
|
||||
mockGetValues.mockReturnValue(['sys__all__sys_mcp_srv']);
|
||||
render(<McpSection item={item} />);
|
||||
fireEvent.click(screen.getByTestId('tool-mcp:srv:a'));
|
||||
expect(mockSetValue).toHaveBeenCalledWith(
|
||||
'tools',
|
||||
['sys__server__sys_mcp_srv', 'mcp:srv:b'],
|
||||
expect.objectContaining({ shouldDirty: true }),
|
||||
);
|
||||
});
|
||||
|
||||
test('shows the runtime-tools hint when attached via the wildcard', () => {
|
||||
mockGetValues.mockReturnValue(['sys__all__sys_mcp_srv']);
|
||||
const empty: McpItem = {
|
||||
...item,
|
||||
server: { ...item.server, tools: [] } as never,
|
||||
toolCount: 0,
|
||||
};
|
||||
render(<McpSection item={empty} />);
|
||||
expect(screen.getByText('com_ui_tools_mcp_runtime_tools')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import {
|
|||
useMCPToolOptions,
|
||||
} from '~/hooks';
|
||||
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
|
||||
import { mcpAllToken, mcpServerToken } from '../../items/selectors';
|
||||
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
||||
import McpOAuthDialog from '~/components/MCP/McpOAuthDialog';
|
||||
import { mcpServerToken } from '../../items/selectors';
|
||||
import { useAgentPanelContext } from '~/Providers';
|
||||
import { getIconForItem } from '../../items/icons';
|
||||
import MCPToolItem from '../../../MCPToolItem';
|
||||
|
|
@ -61,8 +61,14 @@ interface Props {
|
|||
export default function McpSection({ item }: Props) {
|
||||
const localize = useLocalize();
|
||||
const { control, getValues, setValue } = useFormContext<AgentForm>();
|
||||
const { getServerStatusIconProps, getConfigDialogProps, initializeServer, getOAuthUrl } =
|
||||
useMCPServerManager();
|
||||
const {
|
||||
getServerStatusIconProps,
|
||||
getConfigDialogProps,
|
||||
initializeServer,
|
||||
isConnectionDeferred,
|
||||
resetConnectionDeferred,
|
||||
getOAuthUrl,
|
||||
} = useMCPServerManager();
|
||||
const [oauthOpen, setOauthOpen] = useState(false);
|
||||
const [oauthUrl, setOauthUrl] = useState<string | null>(null);
|
||||
const [prevConnected, setPrevConnected] = useState(false);
|
||||
|
|
@ -85,6 +91,7 @@ export default function McpSection({ item }: Props) {
|
|||
|
||||
const serverName = item.server.serverName;
|
||||
const serverToken = mcpServerToken(serverName);
|
||||
const serverAllToken = mcpAllToken(serverName);
|
||||
/** Live server data — `item.server` is a snapshot from card click and goes stale once
|
||||
* the MCP query refetches (e.g., after a server connects), so read from the live map. */
|
||||
const liveServer = mcpServersMap.get(serverName) ?? item.server;
|
||||
|
|
@ -94,22 +101,36 @@ export default function McpSection({ item }: Props) {
|
|||
/** Subscribe to the tools field so selection toggles re-render this section.
|
||||
* `getValues` is a non-reactive read and left the checkboxes visually stale. */
|
||||
const formTools = (useWatch({ control, name: 'tools' }) ?? []) as string[];
|
||||
/** Attached via the server-wide `mcp_all` wildcard — used by request-scoped
|
||||
* servers whose tools resolve at chat-turn time and can't be listed here. */
|
||||
const isWildcardAttached = formTools.includes(serverAllToken);
|
||||
|
||||
/** The `mcp_all` wildcard grants every server tool at runtime, so when the
|
||||
* server's tools ARE enumerable (e.g. it stopped being request-scoped), fold
|
||||
* the wildcard into the display as "all selected" — otherwise the dialog
|
||||
* would show unchecked boxes while runtime grants everything. Any selection
|
||||
* interaction then rewrites the form with concrete tool ids (the wildcard is
|
||||
* stripped by `updateFormTools`), converting the attachment on first touch. */
|
||||
const getSelectedTools = (): string[] =>
|
||||
tools.filter((t) => formTools.includes(t.tool_id)).map((t) => t.tool_id);
|
||||
isWildcardAttached
|
||||
? tools.map((t) => t.tool_id)
|
||||
: tools.filter((t) => formTools.includes(t.tool_id)).map((t) => t.tool_id);
|
||||
|
||||
/** Replace this server's tool selection while keeping the server attached: the
|
||||
* placeholder token is always rewritten, so deselect-all leaves the server
|
||||
* pinned with zero tools; only an explicit remove detaches it. */
|
||||
* pinned with zero tools; only an explicit remove detaches it. The `mcp_all`
|
||||
* wildcard is also stripped unless explicitly re-passed in `next`, so a
|
||||
* per-tool selection always supersedes a stale wildcard (e.g. after a server
|
||||
* stops being request-scoped and its tools become enumerable). */
|
||||
const updateFormTools = useCallback(
|
||||
(next: string[]) => {
|
||||
const current = (getValues('tools') ?? []) as string[];
|
||||
const otherTools = current.filter(
|
||||
(t) => t !== serverToken && !tools.some((st) => st.tool_id === t),
|
||||
(t) => t !== serverToken && t !== serverAllToken && !tools.some((st) => st.tool_id === t),
|
||||
);
|
||||
setValue('tools', [...otherTools, serverToken, ...next], { shouldDirty: true });
|
||||
},
|
||||
[getValues, setValue, serverToken, tools],
|
||||
[getValues, setValue, serverToken, serverAllToken, tools],
|
||||
);
|
||||
|
||||
const toggleToolSelect = (toolId: string) => {
|
||||
|
|
@ -155,14 +176,42 @@ export default function McpSection({ item }: Props) {
|
|||
/** Connecting from this dialog implies the user wants the server's tools:
|
||||
* once the connection settles and the tools arrive (query refetch for direct
|
||||
* connects, polling for OAuth), select them all — an effect because both
|
||||
* signals come from external systems, not from anything rendered here. */
|
||||
* signals come from external systems, not from anything rendered here.
|
||||
*
|
||||
* Request-scoped servers (runtime `{{LIBRECHAT_BODY_*}}` placeholders) defer
|
||||
* their connection to the next chat turn, so no tool list will ever arrive —
|
||||
* attach the whole server via the `mcp_all` wildcard instead; the backend
|
||||
* resolves it into the server's full tool set at turn time. Keying on the
|
||||
* manager's init state (not the awaited response) also covers connects that
|
||||
* happen behind the customUserVars config dialog, which this component does
|
||||
* not await. */
|
||||
const serverDeferred = isConnectionDeferred(serverName);
|
||||
useEffect(() => {
|
||||
if (!autoSelectPending || !isConnected || !hasTools) {
|
||||
if (!autoSelectPending) {
|
||||
return;
|
||||
}
|
||||
if (serverDeferred && !hasTools) {
|
||||
setAutoSelectPending(false);
|
||||
if (!isWildcardAttached) {
|
||||
updateFormTools([serverAllToken]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!isConnected || !hasTools) {
|
||||
return;
|
||||
}
|
||||
setAutoSelectPending(false);
|
||||
updateFormTools(tools.map((t) => t.tool_id));
|
||||
}, [autoSelectPending, isConnected, hasTools, tools, updateFormTools]);
|
||||
}, [
|
||||
autoSelectPending,
|
||||
serverDeferred,
|
||||
isConnected,
|
||||
hasTools,
|
||||
tools,
|
||||
updateFormTools,
|
||||
isWildcardAttached,
|
||||
serverAllToken,
|
||||
]);
|
||||
|
||||
/** Connect inline from this first dialog. Servers with custom user variables are
|
||||
* routed to the config dialog (which sets the vars and initializes); others
|
||||
|
|
@ -171,6 +220,11 @@ export default function McpSection({ item }: Props) {
|
|||
const handleConnect = async (e: MouseEvent) => {
|
||||
setAutoSelectPending(true);
|
||||
if (statusIconProps != null && statusIconProps.hasCustomUserVars) {
|
||||
/** A stale deferred flag from an earlier attempt must not fire the
|
||||
* auto-attach effect while the config dialog is open — only this
|
||||
* attempt's outcome (recorded on save → initialize) counts. The direct
|
||||
* path below needs no reset: initializeServer clears it up front. */
|
||||
resetConnectionDeferred(serverName);
|
||||
statusIconProps.onConfigClick(e);
|
||||
return;
|
||||
}
|
||||
|
|
@ -361,7 +415,9 @@ export default function McpSection({ item }: Props) {
|
|||
</Collapse>
|
||||
<Collapse open={!hasTools && !toolsLoading}>
|
||||
<p className="rounded-xl border border-dashed border-border-light p-3 text-center text-xs text-text-tertiary">
|
||||
{localize('com_ui_tools_mcp_no_tools')}
|
||||
{localize(
|
||||
isWildcardAttached ? 'com_ui_tools_mcp_runtime_tools' : 'com_ui_tools_mcp_no_tools',
|
||||
)}
|
||||
</p>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -65,6 +65,18 @@ export function mcpServerToken(serverName: string): string {
|
|||
return `${Constants.mcp_server}${Constants.mcp_delimiter}${serverName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-wide wildcard token (`sys__all__sys_mcp_<serverName>`). Unlike the
|
||||
* UI-only `mcp_server` placeholder (skipped at runtime), `mcp_all` is resolved
|
||||
* by the backend into ALL of the server's tools at chat-turn time. Used for
|
||||
* request-scoped servers (runtime `{{LIBRECHAT_BODY_*}}` placeholders) whose
|
||||
* tools cannot be enumerated outside a chat turn, so per-tool selection is
|
||||
* impossible and the server must be attached as a whole.
|
||||
*/
|
||||
export function mcpAllToken(serverName: string): string {
|
||||
return `${Constants.mcp_all}${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,
|
||||
|
|
|
|||
|
|
@ -345,9 +345,17 @@ export function useMCPServerManager({
|
|||
|
||||
const initializeServer = useCallback(
|
||||
async (serverName: string, autoOpenOAuth: boolean = true) => {
|
||||
updateServerInitState(serverName, { isInitializing: true });
|
||||
/** connectionDeferred is reset up front so a stale value from a previous
|
||||
* attempt can never be mistaken for this attempt's outcome. */
|
||||
updateServerInitState(serverName, { isInitializing: true, connectionDeferred: false });
|
||||
try {
|
||||
const response = await reinitializeMutation.mutateAsync(serverName);
|
||||
/** Record whether this attempt deferred to a chat turn (request-scoped
|
||||
* server) so consumers that didn't await this call — e.g. the agent
|
||||
* builder behind the customUserVars config dialog — can react to it. */
|
||||
updateServerInitState(serverName, {
|
||||
connectionDeferred: Boolean(response.connectionDeferred),
|
||||
});
|
||||
if (!response.success) {
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
||||
|
|
@ -456,6 +464,23 @@ export function useMCPServerManager({
|
|||
[serverInitStates],
|
||||
);
|
||||
|
||||
const isConnectionDeferred = useCallback(
|
||||
(serverName: string) => {
|
||||
return getServerInitState(serverInitStates, serverName).connectionDeferred;
|
||||
},
|
||||
[serverInitStates],
|
||||
);
|
||||
|
||||
/** Clear a recorded deferred outcome without starting a new attempt — used
|
||||
* before routing into the customUserVars config dialog so a stale flag from
|
||||
* an earlier attempt can't trigger consumers while the dialog is open. */
|
||||
const resetConnectionDeferred = useCallback(
|
||||
(serverName: string) => {
|
||||
updateServerInitState(serverName, { connectionDeferred: false });
|
||||
},
|
||||
[updateServerInitState],
|
||||
);
|
||||
|
||||
const getOAuthUrl = useCallback(
|
||||
(serverName: string) => {
|
||||
return getServerInitState(serverInitStates, serverName).oauthUrl;
|
||||
|
|
@ -678,6 +703,8 @@ export function useMCPServerManager({
|
|||
cancelOAuthFlow,
|
||||
isInitializing,
|
||||
isCancellable,
|
||||
isConnectionDeferred,
|
||||
resetConnectionDeferred,
|
||||
getOAuthUrl,
|
||||
mcpValues,
|
||||
setMCPValues,
|
||||
|
|
|
|||
|
|
@ -1914,6 +1914,7 @@
|
|||
"com_ui_tools_marketplace_search": "Search tools…",
|
||||
"com_ui_tools_mcp_deselect_all": "Deselect all",
|
||||
"com_ui_tools_mcp_no_tools": "This server has not exposed any tools yet.",
|
||||
"com_ui_tools_mcp_runtime_tools": "All of this server's tools are attached; they are resolved at runtime, during chat.",
|
||||
"com_ui_tools_mcp_select_all": "Select all",
|
||||
"com_ui_tools_mcp_status_unconfigured": "Needs configuration",
|
||||
"com_ui_tools_mcp_tools_section": "Tools in this server",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ export interface MCPServerInitState {
|
|||
isCancellable: boolean;
|
||||
oauthUrl: string | null;
|
||||
oauthStartTime: number | null;
|
||||
/** Last initialize attempt reported a request-scoped server whose connection
|
||||
* is deferred to the next chat turn (runtime body placeholders) — its tools
|
||||
* cannot be enumerated up front. Consumers attach such servers wholesale via
|
||||
* the `mcp_all` wildcard instead of waiting for a tool list. */
|
||||
connectionDeferred: boolean;
|
||||
}
|
||||
|
||||
const defaultServerInitState: MCPServerInitState = {
|
||||
|
|
@ -46,6 +51,7 @@ const defaultServerInitState: MCPServerInitState = {
|
|||
isCancellable: false,
|
||||
oauthUrl: null,
|
||||
oauthStartTime: null,
|
||||
connectionDeferred: false,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -340,6 +340,9 @@ export const useReinitializeMCPServerMutation = (): UseMutationResult<
|
|||
serverName: string;
|
||||
oauthRequired?: boolean;
|
||||
oauthUrl?: string;
|
||||
/** True when the server uses request-scoped placeholders and the connection
|
||||
* was deferred to the next chat turn (tools are not enumerable up front). */
|
||||
connectionDeferred?: boolean;
|
||||
},
|
||||
unknown,
|
||||
string,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue