Skip to content

Commit 04442b5

Browse files
committed
Merge branch 'main' into cleanup-old-transports
2 parents 0215557 + 539f32b commit 04442b5

19 files changed

+567
-118
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ Thanks for your interest in contributing! This guide explains how to get involve
1212
## Development Process & Pull Requests
1313

1414
1. Create a new branch for your changes
15-
2. Make your changes following existing code style and conventions
16-
3. Test changes locally
15+
2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable.
16+
3. Test changes locally by running `npm test`
1717
4. Update documentation as needed
1818
5. Use clear commit messages explaining your changes
1919
6. Verify all changes work as expected

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ The inspector supports bearer token authentication for SSE connections. Enter yo
4646

4747
The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server.
4848

49+
### Configuration
50+
51+
The MCP Inspector supports the following configuration settings. To change them click on the `Configuration` button in the MCP Inspector UI :
52+
53+
| Name | Purpose | Default Value |
54+
| -------------------------- | ----------------------------------------------------------------------------------------- | ------------- |
55+
| MCP_SERVER_REQUEST_TIMEOUT | Maximum time in milliseconds to wait for a response from the MCP server before timing out | 10000 |
56+
4957
### From this repository
5058

5159
If you're working on the inspector itself:

client/src/App.tsx

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,16 @@ import RootsTab from "./components/RootsTab";
4545
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
4646
import Sidebar from "./components/Sidebar";
4747
import ToolsTab from "./components/ToolsTab";
48+
import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants";
49+
import { InspectorConfig } from "./lib/configurationTypes";
4850

4951
const params = new URLSearchParams(window.location.search);
5052
const PROXY_PORT = params.get("proxyPort") ?? "3000";
5153
const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`;
54+
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
5255

5356
const App = () => {
5457
// Handle OAuth callback route
55-
if (window.location.pathname === "/oauth/callback") {
56-
const OAuthCallback = React.lazy(
57-
() => import("./components/OAuthCallback"),
58-
);
59-
return (
60-
<Suspense fallback={<div>Loading...</div>}>
61-
<OAuthCallback />
62-
</Suspense>
63-
);
64-
}
6558
const [resources, setResources] = useState<Resource[]>([]);
6659
const [resourceTemplates, setResourceTemplates] = useState<
6760
ResourceTemplate[]
@@ -99,6 +92,11 @@ const App = () => {
9992
>([]);
10093
const [roots, setRoots] = useState<Root[]>([]);
10194
const [env, setEnv] = useState<Record<string, string>>({});
95+
96+
const [config, setConfig] = useState<InspectorConfig>(() => {
97+
const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY);
98+
return savedConfig ? JSON.parse(savedConfig) : DEFAULT_INSPECTOR_CONFIG;
99+
});
102100
const [bearerToken, setBearerToken] = useState<string>(() => {
103101
return localStorage.getItem("lastBearerToken") || "";
104102
});
@@ -114,22 +112,6 @@ const App = () => {
114112
const nextRequestId = useRef(0);
115113
const rootsRef = useRef<Root[]>([]);
116114

117-
const handleApproveSampling = (id: number, result: CreateMessageResult) => {
118-
setPendingSampleRequests((prev) => {
119-
const request = prev.find((r) => r.id === id);
120-
request?.resolve(result);
121-
return prev.filter((r) => r.id !== id);
122-
});
123-
};
124-
125-
const handleRejectSampling = (id: number) => {
126-
setPendingSampleRequests((prev) => {
127-
const request = prev.find((r) => r.id === id);
128-
request?.reject(new Error("Sampling request rejected"));
129-
return prev.filter((r) => r.id !== id);
130-
});
131-
};
132-
133115
const [selectedResource, setSelectedResource] = useState<Resource | null>(
134116
null,
135117
);
@@ -171,6 +153,7 @@ const App = () => {
171153
env,
172154
bearerToken,
173155
proxyServerUrl: PROXY_SERVER_URL,
156+
requestTimeout: config.MCP_SERVER_REQUEST_TIMEOUT.value as number,
174157
onNotification: (notification) => {
175158
setNotifications((prev) => [...prev, notification as ServerNotification]);
176159
},
@@ -209,6 +192,10 @@ const App = () => {
209192
localStorage.setItem("lastBearerToken", bearerToken);
210193
}, [bearerToken]);
211194

195+
useEffect(() => {
196+
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
197+
}, [config]);
198+
212199
// Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback)
213200
useEffect(() => {
214201
const serverUrl = params.get("serverUrl");
@@ -224,7 +211,7 @@ const App = () => {
224211
// Connect to the server
225212
connectMcpServer();
226213
}
227-
}, []);
214+
}, [connectMcpServer]);
228215

229216
useEffect(() => {
230217
fetch(`${PROXY_SERVER_URL}/config`)
@@ -253,6 +240,22 @@ const App = () => {
253240
}
254241
}, []);
255242

243+
const handleApproveSampling = (id: number, result: CreateMessageResult) => {
244+
setPendingSampleRequests((prev) => {
245+
const request = prev.find((r) => r.id === id);
246+
request?.resolve(result);
247+
return prev.filter((r) => r.id !== id);
248+
});
249+
};
250+
251+
const handleRejectSampling = (id: number) => {
252+
setPendingSampleRequests((prev) => {
253+
const request = prev.find((r) => r.id === id);
254+
request?.reject(new Error("Sampling request rejected"));
255+
return prev.filter((r) => r.id !== id);
256+
});
257+
};
258+
256259
const clearError = (tabKey: keyof typeof errors) => {
257260
setErrors((prev) => ({ ...prev, [tabKey]: null }));
258261
};
@@ -425,6 +428,17 @@ const App = () => {
425428
setLogLevel(level);
426429
};
427430

431+
if (window.location.pathname === "/oauth/callback") {
432+
const OAuthCallback = React.lazy(
433+
() => import("./components/OAuthCallback"),
434+
);
435+
return (
436+
<Suspense fallback={<div>Loading...</div>}>
437+
<OAuthCallback />
438+
</Suspense>
439+
);
440+
}
441+
428442
return (
429443
<div className="flex h-screen bg-background">
430444
<Sidebar
@@ -439,6 +453,8 @@ const App = () => {
439453
setSseUrl={setSseUrl}
440454
env={env}
441455
setEnv={setEnv}
456+
config={config}
457+
setConfig={setConfig}
442458
bearerToken={bearerToken}
443459
setBearerToken={setBearerToken}
444460
onConnect={connectMcpServer}

client/src/components/DynamicJsonForm.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ const DynamicJsonForm = ({
108108
}
109109
};
110110

111+
const formatJson = () => {
112+
try {
113+
const jsonStr = rawJsonValue.trim();
114+
if (!jsonStr) {
115+
return;
116+
}
117+
const formatted = JSON.stringify(JSON.parse(jsonStr), null, 2);
118+
setRawJsonValue(formatted);
119+
debouncedUpdateParent(formatted);
120+
setJsonError(undefined);
121+
} catch (err) {
122+
setJsonError(err instanceof Error ? err.message : "Invalid JSON");
123+
}
124+
};
125+
111126
const renderFormFields = (
112127
propSchema: JsonSchemaType,
113128
currentValue: JsonValue,
@@ -353,7 +368,12 @@ const DynamicJsonForm = ({
353368

354369
return (
355370
<div className="space-y-4">
356-
<div className="flex justify-end">
371+
<div className="flex justify-end space-x-2">
372+
{isJsonMode && (
373+
<Button variant="outline" size="sm" onClick={formatJson}>
374+
Format JSON
375+
</Button>
376+
)}
357377
<Button variant="outline" size="sm" onClick={handleSwitchToFormMode}>
358378
{isJsonMode ? "Switch to Form" : "Switch to JSON"}
359379
</Button>

client/src/components/History.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
22
import { Copy } from "lucide-react";
33
import { useState } from "react";
4+
import JsonView from "./JsonView";
45

56
const HistoryAndNotifications = ({
67
requestHistory,
@@ -74,9 +75,9 @@ const HistoryAndNotifications = ({
7475
<Copy size={16} />
7576
</button>
7677
</div>
77-
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
78-
{JSON.stringify(JSON.parse(request.request), null, 2)}
79-
</pre>
78+
<div className="bg-background p-2 rounded">
79+
<JsonView data={request.request} />
80+
</div>
8081
</div>
8182
{request.response && (
8283
<div className="mt-2">
@@ -91,13 +92,9 @@ const HistoryAndNotifications = ({
9192
<Copy size={16} />
9293
</button>
9394
</div>
94-
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
95-
{JSON.stringify(
96-
JSON.parse(request.response),
97-
null,
98-
2,
99-
)}
100-
</pre>
95+
<div className="bg-background p-2 rounded">
96+
<JsonView data={request.response} />
97+
</div>
10198
</div>
10299
)}
103100
</>
@@ -146,9 +143,11 @@ const HistoryAndNotifications = ({
146143
<Copy size={16} />
147144
</button>
148145
</div>
149-
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
150-
{JSON.stringify(notification, null, 2)}
151-
</pre>
146+
<div className="bg-background p-2 rounded">
147+
<JsonView
148+
data={JSON.stringify(notification, null, 2)}
149+
/>
150+
</div>
152151
</div>
153152
)}
154153
</li>

client/src/components/JsonEditor.tsx

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Editor from "react-simple-code-editor";
33
import Prism from "prismjs";
44
import "prismjs/components/prism-json";
55
import "prismjs/themes/prism.css";
6-
import { Button } from "@/components/ui/button";
76

87
interface JsonEditorProps {
98
value: string;
@@ -16,49 +15,25 @@ const JsonEditor = ({
1615
onChange,
1716
error: externalError,
1817
}: JsonEditorProps) => {
19-
const [editorContent, setEditorContent] = useState(value);
18+
const [editorContent, setEditorContent] = useState(value || "");
2019
const [internalError, setInternalError] = useState<string | undefined>(
2120
undefined,
2221
);
2322

2423
useEffect(() => {
25-
setEditorContent(value);
24+
setEditorContent(value || "");
2625
}, [value]);
2726

28-
const formatJson = (json: string): string => {
29-
try {
30-
return JSON.stringify(JSON.parse(json), null, 2);
31-
} catch {
32-
return json;
33-
}
34-
};
35-
3627
const handleEditorChange = (newContent: string) => {
3728
setEditorContent(newContent);
3829
setInternalError(undefined);
3930
onChange(newContent);
4031
};
4132

42-
const handleFormatJson = () => {
43-
try {
44-
const formatted = formatJson(editorContent);
45-
setEditorContent(formatted);
46-
onChange(formatted);
47-
setInternalError(undefined);
48-
} catch (err) {
49-
setInternalError(err instanceof Error ? err.message : "Invalid JSON");
50-
}
51-
};
52-
5333
const displayError = internalError || externalError;
5434

5535
return (
56-
<div className="relative space-y-2">
57-
<div className="flex justify-end">
58-
<Button variant="outline" size="sm" onClick={handleFormatJson}>
59-
Format JSON
60-
</Button>
61-
</div>
36+
<div className="relative">
6237
<div
6338
className={`border rounded-md ${
6439
displayError

0 commit comments

Comments
 (0)