Skip to content

Commit dc49d46

Browse files
committed
refactor: extract draggable pane and connection logic into hooks
- Create useDraggablePane hook for history pane drag behavior - Create useConnection hook for MCP client connection and requests - Update App.tsx to use both hooks
1 parent ef32a8f commit dc49d46

File tree

3 files changed

+292
-220
lines changed

3 files changed

+292
-220
lines changed

client/src/App.tsx

Lines changed: 60 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,26 @@
1-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2-
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
1+
import { useDraggablePane } from "./lib/hooks/useDraggablePane";
2+
import { useConnection } from "./lib/hooks/useConnection";
33
import {
4-
ClientNotification,
54
ClientRequest,
65
CompatibilityCallToolResult,
76
CompatibilityCallToolResultSchema,
8-
CreateMessageRequestSchema,
97
CreateMessageResult,
108
EmptyResultSchema,
119
GetPromptResultSchema,
1210
ListPromptsResultSchema,
1311
ListResourcesResultSchema,
1412
ListResourceTemplatesResultSchema,
15-
ListRootsRequestSchema,
16-
ListToolsResultSchema,
17-
ProgressNotificationSchema,
1813
ReadResourceResultSchema,
19-
Request,
14+
ListToolsResultSchema,
2015
Resource,
2116
ResourceTemplate,
2217
Root,
2318
ServerNotification,
24-
Tool,
25-
ServerCapabilitiesSchema,
26-
Result,
19+
Tool
2720
} from "@modelcontextprotocol/sdk/types.js";
28-
import { useCallback, useEffect, useRef, useState } from "react";
21+
import { useEffect, useRef, useState } from "react";
2922

30-
import {
31-
Notification,
32-
StdErrNotification,
33-
StdErrNotificationSchema,
34-
} from "./lib/notificationTypes";
23+
import { StdErrNotification } from "./lib/notificationTypes";
3524

3625
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
3726
import {
@@ -43,8 +32,7 @@ import {
4332
MessageSquare,
4433
} from "lucide-react";
4534

46-
import { toast } from "react-toastify";
47-
import { z, type ZodType } from "zod";
35+
import { z } from "zod";
4836
import "./App.css";
4937
import ConsoleTab from "./components/ConsoleTab";
5038
import HistoryAndNotifications from "./components/History";
@@ -56,21 +44,11 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab";
5644
import Sidebar from "./components/Sidebar";
5745
import ToolsTab from "./components/ToolsTab";
5846

59-
type ServerCapabilities = z.infer<typeof ServerCapabilitiesSchema>;
60-
61-
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
62-
6347
const params = new URLSearchParams(window.location.search);
6448
const PROXY_PORT = params.get("proxyPort") ?? "3000";
65-
const REQUEST_TIMEOUT =
66-
parseInt(params.get("timeout") ?? "") || DEFAULT_REQUEST_TIMEOUT_MSEC;
6749
const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`;
6850

6951
const App = () => {
70-
const [connectionStatus, setConnectionStatus] = useState<
71-
"disconnected" | "connected" | "error"
72-
>("disconnected");
73-
const [serverCapabilities, setServerCapabilities] = useState<ServerCapabilities | null>(null);
7452
const [resources, setResources] = useState<Resource[]>([]);
7553
const [resourceTemplates, setResourceTemplates] = useState<
7654
ResourceTemplate[]
@@ -95,10 +73,6 @@ const App = () => {
9573

9674
const [sseUrl, setSseUrl] = useState<string>("http://localhost:3001/sse");
9775
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
98-
const [requestHistory, setRequestHistory] = useState<
99-
{ request: string; response?: string }[]
100-
>([]);
101-
const [mcpClient, setMcpClient] = useState<Client | null>(null);
10276
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
10377
const [stdErrNotifications, setStdErrNotifications] = useState<
10478
StdErrNotification[]
@@ -149,49 +123,64 @@ const App = () => {
149123
>();
150124
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
151125
const progressTokenRef = useRef(0);
152-
const [historyPaneHeight, setHistoryPaneHeight] = useState<number>(300);
153-
const [isDragging, setIsDragging] = useState(false);
154-
const dragStartY = useRef<number>(0);
155-
const dragStartHeight = useRef<number>(0);
156126

157-
const handleDragStart = useCallback(
158-
(e: React.MouseEvent) => {
159-
setIsDragging(true);
160-
dragStartY.current = e.clientY;
161-
dragStartHeight.current = historyPaneHeight;
162-
document.body.style.userSelect = "none";
127+
const {
128+
height: historyPaneHeight,
129+
handleDragStart
130+
} = useDraggablePane(300);
131+
132+
const {
133+
connectionStatus,
134+
serverCapabilities,
135+
mcpClient,
136+
requestHistory,
137+
makeRequest: makeConnectionRequest,
138+
sendNotification,
139+
connect: connectMcpServer
140+
} = useConnection({
141+
transportType,
142+
command,
143+
args,
144+
sseUrl,
145+
env,
146+
proxyServerUrl: PROXY_SERVER_URL,
147+
onNotification: (notification) => {
148+
setNotifications(prev => [...prev, notification as ServerNotification]);
163149
},
164-
[historyPaneHeight],
165-
);
166-
167-
const handleDragMove = useCallback(
168-
(e: MouseEvent) => {
169-
if (!isDragging) return;
170-
const deltaY = dragStartY.current - e.clientY;
171-
const newHeight = Math.max(
172-
100,
173-
Math.min(800, dragStartHeight.current + deltaY),
174-
);
175-
setHistoryPaneHeight(newHeight);
150+
onStdErrNotification: (notification) => {
151+
setStdErrNotifications(prev => [...prev, notification as StdErrNotification]);
176152
},
177-
[isDragging],
178-
);
179-
180-
const handleDragEnd = useCallback(() => {
181-
setIsDragging(false);
182-
document.body.style.userSelect = "";
183-
}, []);
153+
onPendingRequest: (request, resolve, reject) => {
154+
setPendingSampleRequests(prev => [
155+
...prev,
156+
{ id: nextRequestId.current++, request, resolve, reject }
157+
]);
158+
},
159+
getRoots: () => rootsRef.current
160+
});
184161

185-
useEffect(() => {
186-
if (isDragging) {
187-
window.addEventListener("mousemove", handleDragMove);
188-
window.addEventListener("mouseup", handleDragEnd);
189-
return () => {
190-
window.removeEventListener("mousemove", handleDragMove);
191-
window.removeEventListener("mouseup", handleDragEnd);
192-
};
162+
const makeRequest = async <T extends z.ZodType>(
163+
request: ClientRequest,
164+
schema: T,
165+
tabKey?: keyof typeof errors,
166+
) => {
167+
try {
168+
const response = await makeConnectionRequest(request, schema);
169+
if (tabKey !== undefined) {
170+
clearError(tabKey);
171+
}
172+
return response;
173+
} catch (e) {
174+
const errorString = (e as Error).message ?? String(e);
175+
if (tabKey !== undefined) {
176+
setErrors((prev) => ({
177+
...prev,
178+
[tabKey]: errorString,
179+
}));
180+
}
181+
throw e;
193182
}
194-
}, [isDragging, handleDragMove, handleDragEnd]);
183+
};
195184

196185
useEffect(() => {
197186
localStorage.setItem("lastCommand", command);
@@ -228,83 +217,10 @@ const App = () => {
228217
}
229218
}, []);
230219

231-
const pushHistory = (request: object, response?: object) => {
232-
setRequestHistory((prev) => [
233-
...prev,
234-
{
235-
request: JSON.stringify(request),
236-
response: response !== undefined ? JSON.stringify(response) : undefined,
237-
},
238-
]);
239-
};
240-
241220
const clearError = (tabKey: keyof typeof errors) => {
242221
setErrors((prev) => ({ ...prev, [tabKey]: null }));
243222
};
244223

245-
const makeRequest = async <T extends ZodType<object>>(
246-
request: ClientRequest,
247-
schema: T,
248-
tabKey?: keyof typeof errors,
249-
) => {
250-
if (!mcpClient) {
251-
throw new Error("MCP client not connected");
252-
}
253-
254-
try {
255-
const abortController = new AbortController();
256-
const timeoutId = setTimeout(() => {
257-
abortController.abort("Request timed out");
258-
}, REQUEST_TIMEOUT);
259-
260-
let response;
261-
try {
262-
response = await mcpClient.request(request, schema, {
263-
signal: abortController.signal,
264-
});
265-
pushHistory(request, response);
266-
} catch (error) {
267-
const errorMessage = error instanceof Error ? error.message : String(error);
268-
pushHistory(request, { error: errorMessage });
269-
throw error;
270-
} finally {
271-
clearTimeout(timeoutId);
272-
}
273-
274-
if (tabKey !== undefined) {
275-
clearError(tabKey);
276-
}
277-
278-
return response;
279-
} catch (e: unknown) {
280-
const errorString = (e as Error).message ?? String(e);
281-
if (tabKey === undefined) {
282-
toast.error(errorString);
283-
} else {
284-
setErrors((prev) => ({
285-
...prev,
286-
[tabKey]: errorString,
287-
}));
288-
}
289-
290-
throw e;
291-
}
292-
};
293-
294-
const sendNotification = async (notification: ClientNotification) => {
295-
if (!mcpClient) {
296-
throw new Error("MCP client not connected");
297-
}
298-
299-
try {
300-
await mcpClient.notification(notification);
301-
pushHistory(notification);
302-
} catch (e: unknown) {
303-
toast.error((e as Error).message ?? String(e));
304-
throw e;
305-
}
306-
};
307-
308224
const listResources = async () => {
309225
const response = await makeRequest(
310226
{
@@ -407,82 +323,6 @@ const App = () => {
407323
await sendNotification({ method: "notifications/roots/list_changed" });
408324
};
409325

410-
const connectMcpServer = async () => {
411-
try {
412-
const client = new Client<Request, Notification, Result>(
413-
{
414-
name: "mcp-inspector",
415-
version: "0.0.1",
416-
},
417-
{
418-
capabilities: {
419-
// Support all client capabilities since we're an inspector tool
420-
sampling: {},
421-
roots: {
422-
listChanged: true,
423-
},
424-
},
425-
},
426-
);
427-
428-
const backendUrl = new URL(`${PROXY_SERVER_URL}/sse`);
429-
430-
backendUrl.searchParams.append("transportType", transportType);
431-
if (transportType === "stdio") {
432-
backendUrl.searchParams.append("command", command);
433-
backendUrl.searchParams.append("args", args);
434-
backendUrl.searchParams.append("env", JSON.stringify(env));
435-
} else {
436-
backendUrl.searchParams.append("url", sseUrl);
437-
}
438-
439-
const clientTransport = new SSEClientTransport(backendUrl);
440-
client.setNotificationHandler(
441-
ProgressNotificationSchema,
442-
(notification) => {
443-
setNotifications((prevNotifications) => [
444-
...prevNotifications,
445-
notification,
446-
]);
447-
},
448-
);
449-
450-
client.setNotificationHandler(
451-
StdErrNotificationSchema,
452-
(notification) => {
453-
setStdErrNotifications((prevErrorNotifications) => [
454-
...prevErrorNotifications,
455-
notification,
456-
]);
457-
},
458-
);
459-
460-
await client.connect(clientTransport);
461-
462-
const capabilities = client.getServerCapabilities();
463-
setServerCapabilities(capabilities ?? null);
464-
465-
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
466-
return new Promise<CreateMessageResult>((resolve, reject) => {
467-
setPendingSampleRequests((prev) => [
468-
...prev,
469-
{ id: nextRequestId.current++, request, resolve, reject },
470-
]);
471-
});
472-
});
473-
474-
client.setRequestHandler(ListRootsRequestSchema, async () => {
475-
return { roots: rootsRef.current };
476-
});
477-
478-
setMcpClient(client);
479-
setConnectionStatus("connected");
480-
} catch (e) {
481-
console.error(e);
482-
setConnectionStatus("error");
483-
}
484-
};
485-
486326
return (
487327
<div className="flex h-screen bg-background">
488328
<Sidebar

0 commit comments

Comments
 (0)