Skip to content

Commit 1e26f3f

Browse files
author
Nic Barrett
committed
BSC-109575 Fix stale resource content in resources panel
1 parent adfcccc commit 1e26f3f

3 files changed

Lines changed: 197 additions & 9 deletions

File tree

client/src/App.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ const App = () => {
147147
const [resourceTemplates, setResourceTemplates] = useState<
148148
ResourceTemplate[]
149149
>([]);
150-
const [resourceContent, setResourceContent] = useState<string>("");
151150
const [resourceContentMap, setResourceContentMap] = useState<
152151
Record<string, string>
153152
>({});
@@ -902,8 +901,8 @@ const App = () => {
902901
setPromptContent(JSON.stringify(response, null, 2));
903902
};
904903

905-
const readResource = async (uri: string) => {
906-
if (fetchingResources.has(uri) || resourceContentMap[uri]) {
904+
const readResource = async (uri: string, force = false) => {
905+
if (fetchingResources.has(uri) || (!force && resourceContentMap[uri])) {
907906
return;
908907
}
909908

@@ -926,7 +925,6 @@ const App = () => {
926925
hasContents: !!(response as { contents?: unknown[] }).contents,
927926
});
928927
const content = JSON.stringify(response, null, 2);
929-
setResourceContent(content);
930928
setResourceContentMap((prev) => ({
931929
...prev,
932930
[uri]: content,
@@ -947,6 +945,10 @@ const App = () => {
947945
}
948946
};
949947

948+
const selectedResourceContent = selectedResource
949+
? (resourceContentMap[selectedResource.uri] ?? "")
950+
: "";
951+
950952
const subscribeToResource = async (uri: string) => {
951953
if (!resourceSubscriptions.has(uri)) {
952954
await sendMCPRequest(
@@ -1482,9 +1484,9 @@ const App = () => {
14821484
setResourceTemplates([]);
14831485
setNextResourceTemplateCursor(undefined);
14841486
}}
1485-
readResource={(uri) => {
1487+
readResource={(uri, force) => {
14861488
clearError("resources");
1487-
readResource(uri);
1489+
readResource(uri, force);
14881490
}}
14891491
selectedResource={selectedResource}
14901492
setSelectedResource={(resource) => {
@@ -1505,7 +1507,7 @@ const App = () => {
15051507
}}
15061508
handleCompletion={handleCompletion}
15071509
completionsSupported={completionsSupported}
1508-
resourceContent={resourceContent}
1510+
resourceContent={selectedResourceContent}
15091511
nextCursor={nextResourceCursor}
15101512
nextTemplateCursor={nextResourceTemplateCursor}
15111513
error={errors.resources}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import "@testing-library/jest-dom";
2+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3+
import App from "../App";
4+
import { useConnection } from "../lib/hooks/useConnection";
5+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6+
7+
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
8+
auth: jest.fn(),
9+
}));
10+
11+
jest.mock("../lib/oauth-state-machine", () => ({
12+
OAuthStateMachine: jest.fn(),
13+
}));
14+
15+
jest.mock("../lib/auth", () => ({
16+
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
17+
tokens: jest.fn().mockResolvedValue(null),
18+
clear: jest.fn(),
19+
})),
20+
DebugInspectorOAuthClientProvider: jest.fn(),
21+
}));
22+
23+
jest.mock("../utils/configUtils", () => ({
24+
...jest.requireActual("../utils/configUtils"),
25+
getMCPProxyAddress: jest.fn(() => "http://localhost:6277"),
26+
getMCPProxyAuthToken: jest.fn(() => ({
27+
token: "",
28+
header: "X-MCP-Proxy-Auth",
29+
})),
30+
getInitialTransportType: jest.fn(() => "stdio"),
31+
getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"),
32+
getInitialCommand: jest.fn(() => "mcp-server-everything"),
33+
getInitialArgs: jest.fn(() => ""),
34+
initializeInspectorConfig: jest.fn(() => ({})),
35+
saveInspectorConfig: jest.fn(),
36+
getMCPTaskTtl: jest.fn(() => 3600),
37+
}));
38+
39+
jest.mock("../lib/hooks/useDraggablePane", () => ({
40+
useDraggablePane: () => ({
41+
height: 300,
42+
handleDragStart: jest.fn(),
43+
}),
44+
useDraggableSidebar: () => ({
45+
width: 320,
46+
isDragging: false,
47+
handleDragStart: jest.fn(),
48+
}),
49+
}));
50+
51+
jest.mock("../components/Sidebar", () => ({
52+
__esModule: true,
53+
default: () => <div>Sidebar</div>,
54+
}));
55+
56+
global.fetch = jest.fn().mockResolvedValue({ json: () => Promise.resolve({}) });
57+
58+
jest.mock("../lib/hooks/useConnection", () => ({
59+
useConnection: jest.fn(),
60+
}));
61+
62+
describe("App - Resources panel", () => {
63+
const mockUseConnection = jest.mocked(useConnection);
64+
65+
beforeEach(() => {
66+
jest.restoreAllMocks();
67+
window.location.hash = "#resources";
68+
});
69+
70+
test("switching back to a cached resource shows the selected resource content", async () => {
71+
const makeRequest = jest.fn().mockImplementation(async (request) => {
72+
if (request.method === "resources/list") {
73+
return {
74+
resources: [
75+
{
76+
uri: "mcp://benefitsolver/report/tools-md",
77+
name: "Build a Report approved tools",
78+
description: "Tools payload",
79+
mimeType: "text/markdown",
80+
},
81+
{
82+
uri: "mcp://benefitsolver/report/context-md",
83+
name: "Build a Report current context",
84+
description: "Context payload",
85+
mimeType: "text/markdown",
86+
},
87+
],
88+
};
89+
}
90+
91+
if (
92+
request.method === "resources/read" &&
93+
request.params.uri === "mcp://benefitsolver/report/tools-md"
94+
) {
95+
return {
96+
contents: [
97+
{
98+
uri: "mcp://benefitsolver/report/tools-md",
99+
mimeType: "text/markdown",
100+
text: "# Approved Report MCP Tools",
101+
},
102+
],
103+
};
104+
}
105+
106+
if (
107+
request.method === "resources/read" &&
108+
request.params.uri === "mcp://benefitsolver/report/context-md"
109+
) {
110+
return {
111+
contents: [
112+
{
113+
uri: "mcp://benefitsolver/report/context-md",
114+
mimeType: "text/markdown",
115+
text: "# Current Report Builder Context",
116+
},
117+
],
118+
};
119+
}
120+
121+
return {};
122+
});
123+
124+
mockUseConnection.mockReturnValue({
125+
connectionStatus: "connected" as const,
126+
serverCapabilities: {
127+
resources: {},
128+
},
129+
serverImplementation: {
130+
name: "benefitsolver-report-mcp",
131+
version: "1.0.0",
132+
},
133+
mcpClient: {
134+
request: jest.fn(),
135+
notification: jest.fn(),
136+
close: jest.fn(),
137+
} as unknown as Client,
138+
requestHistory: [],
139+
clearRequestHistory: jest.fn(),
140+
makeRequest,
141+
cancelTask: jest.fn(),
142+
listTasks: jest.fn(),
143+
sendNotification: jest.fn(),
144+
handleCompletion: jest.fn(),
145+
completionsSupported: false,
146+
connect: jest.fn(),
147+
disconnect: jest.fn(),
148+
} as ReturnType<typeof useConnection>);
149+
150+
render(<App />);
151+
152+
fireEvent.click(screen.getByText("List Resources"));
153+
154+
await waitFor(() => {
155+
expect(
156+
screen.getByText("Build a Report approved tools"),
157+
).toBeInTheDocument();
158+
expect(
159+
screen.getByText("Build a Report current context"),
160+
).toBeInTheDocument();
161+
});
162+
163+
fireEvent.click(screen.getByText("Build a Report approved tools"));
164+
165+
await waitFor(() => {
166+
expect(screen.getByText(/Approved Report MCP Tools/)).toBeInTheDocument();
167+
});
168+
169+
fireEvent.click(screen.getByText("Build a Report current context"));
170+
171+
await waitFor(() => {
172+
expect(
173+
screen.getByText(/Current Report Builder Context/),
174+
).toBeInTheDocument();
175+
});
176+
177+
fireEvent.click(screen.getByText("Build a Report approved tools"));
178+
179+
await waitFor(() => {
180+
expect(screen.getByText(/Approved Report MCP Tools/)).toBeInTheDocument();
181+
expect(
182+
screen.queryByText(/Current Report Builder Context/),
183+
).not.toBeInTheDocument();
184+
});
185+
});
186+
});

client/src/components/ResourcesTab.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const ResourcesTab = ({
4646
clearResources: () => void;
4747
listResourceTemplates: () => void;
4848
clearResourceTemplates: () => void;
49-
readResource: (uri: string) => void;
49+
readResource: (uri: string, force?: boolean) => void;
5050
selectedResource: Resource | null;
5151
setSelectedResource: (resource: Resource | null) => void;
5252
handleCompletion: (
@@ -229,7 +229,7 @@ const ResourcesTab = ({
229229
<Button
230230
variant="outline"
231231
size="sm"
232-
onClick={() => readResource(selectedResource.uri)}
232+
onClick={() => readResource(selectedResource.uri, true)}
233233
>
234234
<RefreshCw className="w-4 h-4 mr-2" />
235235
Refresh

0 commit comments

Comments
 (0)