Skip to content
Open
Show file tree
Hide file tree
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
7 changes: 6 additions & 1 deletion mcpjam-inspector/client/src/components/ElicitationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,12 @@ export function ElicitationDialog({
};

return (
<Dialog open={!!elicitationRequest} onOpenChange={() => {}}>
<Dialog
open={!!elicitationRequest}
onOpenChange={(open) => {
if (!open) handleResponse("cancel");
}}
>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-sm font-medium">
Expand Down
7 changes: 7 additions & 0 deletions mcpjam-inspector/client/src/components/ToolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@ export function ToolsTab({ serverConfig, serverName }: ToolsTabProps) {
) => {
if (!activeElicitation) {
logger.warn("Cannot handle elicitation response: no active request");
// Elicitation may have already timed out server-side; ensure dialog closes.
setActiveElicitation(null);
return;
}

Expand All @@ -531,6 +533,11 @@ export function ToolsTab({ serverConfig, serverName }: ToolsTabProps) {
error: message,
});
setError(message);
// Always close the dialog on cancel/decline even if the API call failed
// so users are never stuck with an unclosable dialog.
if (action === "cancel" || action === "decline") {
setActiveElicitation(null);
}
} finally {
setElicitationLoading(false);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ElicitationDialog } from "../ElicitationDialog";
import type { DialogElicitation } from "../ToolsTab";

vi.mock("@mcpjam/design-system/dialog", () => ({
Dialog: ({
open,
onOpenChange,
children,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
children: React.ReactNode;
}) =>
open ? (
<div role="dialog">
<button
data-testid="dialog-x-button"
onClick={() => onOpenChange(false)}
/>
{children}
</div>
) : null,
DialogContent: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
DialogHeader: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
DialogTitle: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
DialogDescription: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
DialogFooter: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
}));

vi.mock("@mcpjam/design-system/button", () => ({
Button: ({
children,
onClick,
disabled,
}: {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}) => (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
),
}));

const makeRequest = (overrides?: Partial<DialogElicitation>): DialogElicitation => ({
requestId: "req-1",
message: "Please provide your name",
schema: {
type: "object",
properties: {
name: { type: "string", description: "Your name" },
},
required: ["name"],
},
timestamp: new Date().toISOString(),
...overrides,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

describe("ElicitationDialog", () => {
it("does not render when elicitationRequest is null", () => {
const onResponse = vi.fn();
render(<ElicitationDialog elicitationRequest={null} onResponse={onResponse} />);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});

it("renders when elicitationRequest is set", () => {
const onResponse = vi.fn();
render(
<ElicitationDialog
elicitationRequest={makeRequest()}
onResponse={onResponse}
/>,
);
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Please provide your name")).toBeInTheDocument();
});

it("calls onResponse with cancel when Cancel button is clicked", async () => {
const user = userEvent.setup();
const onResponse = vi.fn().mockResolvedValue(undefined);
render(
<ElicitationDialog
elicitationRequest={makeRequest()}
onResponse={onResponse}
/>,
);
await user.click(screen.getByText("Cancel"));
expect(onResponse).toHaveBeenCalledWith("cancel");
});

it("calls onResponse with cancel when the X button closes the dialog", async () => {
const user = userEvent.setup();
const onResponse = vi.fn().mockResolvedValue(undefined);
render(
<ElicitationDialog
elicitationRequest={makeRequest()}
onResponse={onResponse}
/>,
);
await user.click(screen.getByTestId("dialog-x-button"));
expect(onResponse).toHaveBeenCalledWith("cancel");
});
});