Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 27 additions & 27 deletions packages/plugins/mcp/src/react/AddMcpIntegration.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useReducer, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useReducer, useCallback, useMemo, useState } from "react";
import { useAtomSet } from "@effect/atom-react";
import * as Exit from "effect/Exit";
import * as Match from "effect/Match";
Expand Down Expand Up @@ -262,7 +262,6 @@ export default function AddMcpIntegration(props: {
const remoteSlugExists = useSlugAlreadyExists(remoteSlug);
const stdioSlugExists = useSlugAlreadyExists(stdioSlug);

const canAdd = Boolean(probe) && !isAdding && !remoteSlugExists;
// Probe failures are shown inline on the URL field; other failures
// (add server) render in the bottom error block.
const probeError = state.step === "error" && state.probe === null ? state.error : null;
Expand All @@ -285,24 +284,6 @@ export default function AddMcpIntegration(props: {
dispatch({ type: "probe-ok", probe: exit.value });
}, [state.url, doProbe]);

// Keep the latest handleProbe in a ref so the debounced effect can call it
// without depending on its identity (which changes every render).
const handleProbeRef = useRef(handleProbe);
handleProbeRef.current = handleProbe;

// Auto-probe whenever the URL changes (debounced) while we're on the
// remote transport and not already probing/probed.
useEffect(() => {
if (transport !== "remote") return;
if (state.step !== "url") return;
const trimmed = state.url.trim();
if (!trimmed) return;
const handle = setTimeout(() => {
handleProbeRef.current();
}, 400);
return () => clearTimeout(handle);
}, [transport, state.step, state.url]);

// Register the integration with the declared auth methods, returning the
// assigned slug (or null on failure — an error is dispatched in that case).
const registerIntegration = useCallback(
Expand Down Expand Up @@ -335,7 +316,23 @@ export default function AddMcpIntegration(props: {
);

const handleAddRemote = useCallback(async () => {
if (!probe) return;
const url = state.url.trim();
if (!url) {
dispatch({ type: "probe-fail", error: "Enter an MCP server URL." });
return;
}
// URL parsing is a browser API boundary; invalid user input is expected.
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor validates user input
try {
new URL(url);
} catch {
dispatch({ type: "probe-fail", error: "Enter a valid MCP server URL." });
return;
}
if (!probe) {
await handleProbe();
return;
}
dispatch({ type: "add-start" });
// Every row registers as a declared method (a lone no-auth row registers
// the open-server method). Slugs are assigned server-side by kind.
Expand All @@ -347,7 +344,7 @@ export default function AddMcpIntegration(props: {
);
if (slug === null) return;
props.onComplete(slug);
}, [probe, authMethodList.rows, registerIntegration, props]);
}, [probe, authMethodList.rows, handleProbe, registerIntegration, props, state.url]);

// ---- Stdio actions ----

Expand Down Expand Up @@ -487,11 +484,14 @@ export default function AddMcpIntegration(props: {
>
Cancel
</Button>
{(probe || isProbing) && (
<Button type="button" onClick={handleAddRemote} disabled={!canAdd} loading={isAdding}>
Add integration
</Button>
)}
<Button
type="button"
onClick={handleAddRemote}
disabled={isAdding || isProbing || Boolean(remoteSlugExists)}
loading={isAdding || isProbing}
>
{probe ? "Add integration" : "Check server"}
</Button>
</FloatActions>
</>
) : (
Expand Down
Loading