feat: add mcp oauth support via /mcp connect (#570)
This commit is contained in:
599
src/cli/components/McpConnectFlow.tsx
Normal file
599
src/cli/components/McpConnectFlow.tsx
Normal file
@@ -0,0 +1,599 @@
|
||||
/**
|
||||
* Interactive TUI for connecting to MCP servers with OAuth support.
|
||||
* Flow: Select transport → Enter URL → Connect (OAuth if needed) → Enter name → Create
|
||||
*/
|
||||
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import { memo, useCallback, useState } from "react";
|
||||
|
||||
import { getClient } from "../../agent/client";
|
||||
import {
|
||||
connectMcpServer,
|
||||
type McpConnectConfig,
|
||||
type McpTool,
|
||||
OauthStreamEvent,
|
||||
} from "../helpers/mcpOauth";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { colors } from "./colors";
|
||||
import { PasteAwareTextInput } from "./PasteAwareTextInput";
|
||||
|
||||
const SOLID_LINE = "─";
|
||||
|
||||
// Validate URL (outside component to avoid useCallback dependency)
|
||||
function validateUrl(url: string): string | null {
|
||||
if (!url.trim()) {
|
||||
return "URL is required";
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (!["http:", "https:"].includes(parsed.protocol)) {
|
||||
return "URL must use http or https protocol";
|
||||
}
|
||||
} catch {
|
||||
return "Invalid URL format";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate server name (outside component to avoid useCallback dependency)
|
||||
function validateName(name: string): string | null {
|
||||
if (!name.trim()) {
|
||||
return "Server name is required";
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(name.trim())) {
|
||||
return "Name can only contain letters, numbers, hyphens, and underscores";
|
||||
}
|
||||
if (name.trim().length > 64) {
|
||||
return "Name must be 64 characters or less";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface McpConnectFlowProps {
|
||||
onComplete: (serverName: string, serverId: string, toolCount: number) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
type Step =
|
||||
| "select-transport"
|
||||
| "enter-url"
|
||||
| "connecting"
|
||||
| "enter-name"
|
||||
| "creating";
|
||||
|
||||
type Transport = "http" | "sse";
|
||||
|
||||
const TRANSPORTS: { value: Transport; label: string; description: string }[] = [
|
||||
{
|
||||
value: "http",
|
||||
label: "Streamable HTTP",
|
||||
description: "Modern HTTP-based transport (recommended)",
|
||||
},
|
||||
{
|
||||
value: "sse",
|
||||
label: "Server-Sent Events",
|
||||
description: "SSE-based transport for legacy servers",
|
||||
},
|
||||
];
|
||||
|
||||
export const McpConnectFlow = memo(function McpConnectFlow({
|
||||
onComplete,
|
||||
onCancel,
|
||||
}: McpConnectFlowProps) {
|
||||
const terminalWidth = useTerminalWidth();
|
||||
const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10));
|
||||
|
||||
// Step state
|
||||
const [step, setStep] = useState<Step>("select-transport");
|
||||
|
||||
// Transport selection
|
||||
const [transportIndex, setTransportIndex] = useState(0);
|
||||
const [selectedTransport, setSelectedTransport] = useState<Transport | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// URL input
|
||||
const [urlInput, setUrlInput] = useState("");
|
||||
const [urlError, setUrlError] = useState("");
|
||||
|
||||
// Connection state
|
||||
const [connectionStatus, setConnectionStatus] = useState("");
|
||||
const [authUrl, setAuthUrl] = useState<string | null>(null);
|
||||
const [discoveredTools, setDiscoveredTools] = useState<McpTool[]>([]);
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||
|
||||
// Name input
|
||||
const [nameInput, setNameInput] = useState("");
|
||||
const [nameError, setNameError] = useState("");
|
||||
|
||||
// Creating state
|
||||
const [creatingStatus, setCreatingStatus] = useState("");
|
||||
|
||||
// Handle transport selection
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (key.ctrl && input === "c") {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.escape) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (step === "select-transport") {
|
||||
if (key.upArrow) {
|
||||
setTransportIndex((prev) => Math.max(0, prev - 1));
|
||||
} else if (key.downArrow) {
|
||||
setTransportIndex((prev) =>
|
||||
Math.min(TRANSPORTS.length - 1, prev + 1),
|
||||
);
|
||||
} else if (key.return) {
|
||||
const selected = TRANSPORTS[transportIndex];
|
||||
if (selected) {
|
||||
setSelectedTransport(selected.value);
|
||||
setStep("enter-url");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ isActive: step === "select-transport" },
|
||||
);
|
||||
|
||||
// Handle URL input escape
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (key.ctrl && input === "c") {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.escape) {
|
||||
// Go back to transport selection
|
||||
setStep("select-transport");
|
||||
setUrlInput("");
|
||||
setUrlError("");
|
||||
}
|
||||
},
|
||||
{ isActive: step === "enter-url" },
|
||||
);
|
||||
|
||||
// Handle connection step escape
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (key.ctrl && input === "c") {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.escape && connectionError) {
|
||||
// Go back to URL input on error
|
||||
setStep("enter-url");
|
||||
setConnectionError(null);
|
||||
setConnectionStatus("");
|
||||
setAuthUrl(null);
|
||||
}
|
||||
},
|
||||
{ isActive: step === "connecting" },
|
||||
);
|
||||
|
||||
// Handle name input escape
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (key.ctrl && input === "c") {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.escape) {
|
||||
// Go back to URL input
|
||||
setStep("enter-url");
|
||||
setNameInput("");
|
||||
setNameError("");
|
||||
}
|
||||
},
|
||||
{ isActive: step === "enter-name" },
|
||||
);
|
||||
|
||||
// Handle URL submission
|
||||
const handleUrlSubmit = useCallback(
|
||||
async (text: string) => {
|
||||
const trimmed = text.trim();
|
||||
const error = validateUrl(trimmed);
|
||||
if (error) {
|
||||
setUrlError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
setUrlError("");
|
||||
setStep("connecting");
|
||||
setConnectionStatus("Connecting...");
|
||||
setConnectionError(null);
|
||||
setAuthUrl(null);
|
||||
|
||||
const config: McpConnectConfig = {
|
||||
server_name: "temp-connection-test",
|
||||
type: selectedTransport === "http" ? "streamable_http" : "sse",
|
||||
server_url: trimmed,
|
||||
};
|
||||
|
||||
try {
|
||||
const tools = await connectMcpServer(config, {
|
||||
onEvent: (event) => {
|
||||
switch (event.event) {
|
||||
case OauthStreamEvent.CONNECTION_ATTEMPT:
|
||||
setConnectionStatus("Connecting to server...");
|
||||
break;
|
||||
case OauthStreamEvent.OAUTH_REQUIRED:
|
||||
setConnectionStatus("OAuth authentication required...");
|
||||
break;
|
||||
case OauthStreamEvent.AUTHORIZATION_URL:
|
||||
if (event.url) {
|
||||
const authorizationUrl = event.url;
|
||||
setAuthUrl(authorizationUrl);
|
||||
setConnectionStatus("Opening browser for authorization...");
|
||||
// Open browser
|
||||
import("open")
|
||||
.then(({ default: open }) => open(authorizationUrl))
|
||||
.catch(() => {});
|
||||
}
|
||||
break;
|
||||
case OauthStreamEvent.WAITING_FOR_AUTH:
|
||||
setConnectionStatus("Waiting for authorization in browser...");
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Success!
|
||||
setDiscoveredTools(tools);
|
||||
setConnectionStatus("");
|
||||
|
||||
// Generate default name from URL
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
const defaultName =
|
||||
parsed.hostname.replace(/^(www|mcp|api)\./, "").split(".")[0] ||
|
||||
"mcp-server";
|
||||
setNameInput(defaultName);
|
||||
} catch {
|
||||
setNameInput("mcp-server");
|
||||
}
|
||||
|
||||
setStep("enter-name");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setConnectionError(message);
|
||||
setConnectionStatus("");
|
||||
}
|
||||
},
|
||||
[selectedTransport],
|
||||
);
|
||||
|
||||
// Handle name submission and create server
|
||||
const handleNameSubmit = useCallback(
|
||||
async (text: string) => {
|
||||
const trimmed = text.trim();
|
||||
const error = validateName(trimmed);
|
||||
if (error) {
|
||||
setNameError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
setNameError("");
|
||||
setStep("creating");
|
||||
setCreatingStatus("Creating MCP server...");
|
||||
|
||||
try {
|
||||
const client = await getClient();
|
||||
|
||||
const serverConfig =
|
||||
selectedTransport === "http"
|
||||
? {
|
||||
mcp_server_type: "streamable_http" as const,
|
||||
server_url: urlInput.trim(),
|
||||
}
|
||||
: {
|
||||
mcp_server_type: "sse" as const,
|
||||
server_url: urlInput.trim(),
|
||||
};
|
||||
|
||||
const server = await client.mcpServers.create({
|
||||
server_name: trimmed,
|
||||
config: serverConfig,
|
||||
});
|
||||
|
||||
onComplete(trimmed, server.id || "", discoveredTools.length);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setNameError(`Failed to create server: ${message}`);
|
||||
setStep("enter-name");
|
||||
setCreatingStatus("");
|
||||
}
|
||||
},
|
||||
[selectedTransport, urlInput, discoveredTools.length, onComplete],
|
||||
);
|
||||
|
||||
// Render transport selection step
|
||||
if (step === "select-transport") {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>{"> /mcp connect"}</Text>
|
||||
<Text dimColor>{solidLine}</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Text bold color={colors.selector.title}>
|
||||
Connect to MCP Server
|
||||
</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2}>
|
||||
<Text>Select transport type:</Text>
|
||||
</Box>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box flexDirection="column">
|
||||
{TRANSPORTS.map((transport, index) => {
|
||||
const isSelected = index === transportIndex;
|
||||
return (
|
||||
<Box
|
||||
key={transport.value}
|
||||
flexDirection="column"
|
||||
marginBottom={1}
|
||||
>
|
||||
<Box>
|
||||
<Text
|
||||
color={
|
||||
isSelected ? colors.selector.itemHighlighted : undefined
|
||||
}
|
||||
bold={isSelected}
|
||||
>
|
||||
{isSelected ? "> " : " "}
|
||||
{transport.label}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box paddingLeft={4}>
|
||||
<Text dimColor>{transport.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>↑↓ navigate · Enter select · Esc cancel</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Render URL input step
|
||||
if (step === "enter-url") {
|
||||
const transportLabel =
|
||||
TRANSPORTS.find((t) => t.value === selectedTransport)?.label || "";
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>{"> /mcp connect"}</Text>
|
||||
<Text dimColor>{solidLine}</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Text bold color={colors.selector.title}>
|
||||
Connect to MCP Server
|
||||
</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2}>
|
||||
<Text>
|
||||
Transport: <Text bold>{transportLabel}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2}>
|
||||
<Text>Enter the server URL:</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={colors.selector.itemHighlighted}>{">"}</Text>
|
||||
<Text> </Text>
|
||||
<PasteAwareTextInput
|
||||
value={urlInput}
|
||||
onChange={(val) => {
|
||||
setUrlInput(val);
|
||||
setUrlError("");
|
||||
}}
|
||||
onSubmit={handleUrlSubmit}
|
||||
placeholder="https://mcp.example.com/mcp"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{urlError && (
|
||||
<Box paddingLeft={2} marginTop={1}>
|
||||
<Text color="red">{urlError}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>Enter submit · Esc back</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Render connecting step
|
||||
if (step === "connecting") {
|
||||
const transportLabel =
|
||||
TRANSPORTS.find((t) => t.value === selectedTransport)?.label || "";
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>{"> /mcp connect"}</Text>
|
||||
<Text dimColor>{solidLine}</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Text bold color={colors.selector.title}>
|
||||
Connect to MCP Server
|
||||
</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2} flexDirection="column">
|
||||
<Text>
|
||||
Transport: <Text bold>{transportLabel}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
URL: <Text bold>{urlInput}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
{connectionStatus && (
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="yellow">{connectionStatus}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{authUrl && (
|
||||
<Box paddingLeft={2} marginTop={1} flexDirection="column">
|
||||
<Text dimColor>Authorization URL:</Text>
|
||||
<Text dimColor>{authUrl}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{connectionError && (
|
||||
<Box paddingLeft={2} marginTop={1} flexDirection="column">
|
||||
<Text color="red">Connection failed:</Text>
|
||||
<Text color="red">{connectionError}</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Esc to go back and try again</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Render name input step
|
||||
if (step === "enter-name") {
|
||||
const transportLabel =
|
||||
TRANSPORTS.find((t) => t.value === selectedTransport)?.label || "";
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>{"> /mcp connect"}</Text>
|
||||
<Text dimColor>{solidLine}</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Text bold color={colors.selector.title}>
|
||||
Connect to MCP Server
|
||||
</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2} flexDirection="column">
|
||||
<Text>
|
||||
Transport: <Text bold>{transportLabel}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
URL: <Text bold>{urlInput}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="green">
|
||||
✓ Connection successful! Discovered {discoveredTools.length} tool
|
||||
{discoveredTools.length === 1 ? "" : "s"}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{discoveredTools.length > 0 && (
|
||||
<Box paddingLeft={4} marginTop={1} flexDirection="column">
|
||||
{discoveredTools.slice(0, 5).map((tool) => (
|
||||
<Text key={tool.name} dimColor>
|
||||
• {tool.name}
|
||||
</Text>
|
||||
))}
|
||||
{discoveredTools.length > 5 && (
|
||||
<Text dimColor>... and {discoveredTools.length - 5} more</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2}>
|
||||
<Text>Enter a name for this server:</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={colors.selector.itemHighlighted}>{">"}</Text>
|
||||
<Text> </Text>
|
||||
<PasteAwareTextInput
|
||||
value={nameInput}
|
||||
onChange={(val) => {
|
||||
setNameInput(val);
|
||||
setNameError("");
|
||||
}}
|
||||
onSubmit={handleNameSubmit}
|
||||
placeholder="my-mcp-server"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{nameError && (
|
||||
<Box paddingLeft={2} marginTop={1}>
|
||||
<Text color="red">{nameError}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>Enter create · Esc back</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Render creating step
|
||||
if (step === "creating") {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>{"> /mcp connect"}</Text>
|
||||
<Text dimColor>{solidLine}</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Text bold color={colors.selector.title}>
|
||||
Connect to MCP Server
|
||||
</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="yellow">{creatingStatus}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
McpConnectFlow.displayName = "McpConnectFlow";
|
||||
@@ -109,6 +109,16 @@ export const McpSelector = memo(function McpSelector({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchAttachedToolIds = useCallback(
|
||||
async (client: Awaited<ReturnType<typeof getClient>>) => {
|
||||
const agent = await client.agents.retrieve(agentId, {
|
||||
include: ["agent.tools"],
|
||||
});
|
||||
return new Set(agent.tools?.map((t) => t.id) || []);
|
||||
},
|
||||
[agentId],
|
||||
);
|
||||
|
||||
// Load tools for a specific server
|
||||
const loadTools = useCallback(
|
||||
async (server: McpServer) => {
|
||||
@@ -138,8 +148,7 @@ export const McpSelector = memo(function McpSelector({
|
||||
setTools(toolsList);
|
||||
|
||||
// Fetch agent's current tools to check which are attached
|
||||
const agent = await client.agents.retrieve(agentId);
|
||||
const agentToolIds = new Set(agent.tools?.map((t) => t.id) || []);
|
||||
const agentToolIds = await fetchAttachedToolIds(client);
|
||||
setAttachedToolIds(agentToolIds);
|
||||
|
||||
setToolsPage(0);
|
||||
@@ -153,7 +162,7 @@ export const McpSelector = memo(function McpSelector({
|
||||
setToolsLoading(false);
|
||||
}
|
||||
},
|
||||
[agentId],
|
||||
[fetchAttachedToolIds],
|
||||
);
|
||||
|
||||
// Refresh tools from MCP server
|
||||
@@ -174,8 +183,7 @@ export const McpSelector = memo(function McpSelector({
|
||||
setTools(toolsList);
|
||||
|
||||
// Refresh agent's current tools
|
||||
const agent = await client.agents.retrieve(agentId);
|
||||
const agentToolIds = new Set(agent.tools?.map((t) => t.id) || []);
|
||||
const agentToolIds = await fetchAttachedToolIds(client);
|
||||
setAttachedToolIds(agentToolIds);
|
||||
|
||||
setToolsPage(0);
|
||||
@@ -194,7 +202,7 @@ export const McpSelector = memo(function McpSelector({
|
||||
} finally {
|
||||
setToolsLoading(false);
|
||||
}
|
||||
}, [agentId, viewingServer]);
|
||||
}, [agentId, fetchAttachedToolIds, viewingServer]);
|
||||
|
||||
// Toggle tool attachment
|
||||
const toggleTool = useCallback(
|
||||
@@ -213,8 +221,7 @@ export const McpSelector = memo(function McpSelector({
|
||||
}
|
||||
|
||||
// Fetch agent's current tools to get accurate total count
|
||||
const agent = await client.agents.retrieve(agentId);
|
||||
const agentToolIds = new Set(agent.tools?.map((t) => t.id) || []);
|
||||
const agentToolIds = await fetchAttachedToolIds(client);
|
||||
setAttachedToolIds(agentToolIds);
|
||||
} catch (err) {
|
||||
setToolsError(
|
||||
@@ -226,7 +233,7 @@ export const McpSelector = memo(function McpSelector({
|
||||
setIsTogglingTool(false);
|
||||
}
|
||||
},
|
||||
[agentId, attachedToolIds],
|
||||
[agentId, attachedToolIds, fetchAttachedToolIds],
|
||||
);
|
||||
|
||||
// Attach all tools
|
||||
@@ -244,8 +251,7 @@ export const McpSelector = memo(function McpSelector({
|
||||
);
|
||||
|
||||
// Fetch agent's current tools to get accurate total count
|
||||
const agent = await client.agents.retrieve(agentId);
|
||||
const agentToolIds = new Set(agent.tools?.map((t) => t.id) || []);
|
||||
const agentToolIds = await fetchAttachedToolIds(client);
|
||||
setAttachedToolIds(agentToolIds);
|
||||
} catch (err) {
|
||||
setToolsError(
|
||||
@@ -254,7 +260,7 @@ export const McpSelector = memo(function McpSelector({
|
||||
} finally {
|
||||
setIsTogglingTool(false);
|
||||
}
|
||||
}, [agentId, tools, attachedToolIds]);
|
||||
}, [agentId, tools, attachedToolIds, fetchAttachedToolIds]);
|
||||
|
||||
// Detach all tools
|
||||
const detachAllTools = useCallback(async () => {
|
||||
@@ -271,8 +277,7 @@ export const McpSelector = memo(function McpSelector({
|
||||
);
|
||||
|
||||
// Fetch agent's current tools to get accurate total count
|
||||
const agent = await client.agents.retrieve(agentId);
|
||||
const agentToolIds = new Set(agent.tools?.map((t) => t.id) || []);
|
||||
const agentToolIds = await fetchAttachedToolIds(client);
|
||||
setAttachedToolIds(agentToolIds);
|
||||
} catch (err) {
|
||||
setToolsError(
|
||||
@@ -281,7 +286,7 @@ export const McpSelector = memo(function McpSelector({
|
||||
} finally {
|
||||
setIsTogglingTool(false);
|
||||
}
|
||||
}, [agentId, tools, attachedToolIds]);
|
||||
}, [agentId, tools, attachedToolIds, fetchAttachedToolIds]);
|
||||
|
||||
useEffect(() => {
|
||||
loadServers();
|
||||
|
||||
Reference in New Issue
Block a user