Skip to content

Commit b97233f

Browse files
authored
Merge branch 'main' into update-prism
2 parents dcbd1da + 806cdb2 commit b97233f

18 files changed

+890
-34
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Thanks for your interest in contributing! This guide explains how to get involve
77
1. Fork the repository and clone it locally
88
2. Install dependencies with `npm install`
99
3. Run `npm run dev` to start both client and server in development mode
10-
4. Use the web UI at http://localhost:5173 to interact with the inspector
10+
4. Use the web UI at http://127.0.0.1:5173 to interact with the inspector
1111

1212
## Development Process & Pull Requests
1313

bin/cli.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@ async function main() {
2727
}
2828

2929
if (parsingFlags && arg === "-e" && i + 1 < args.length) {
30-
const [key, value] = args[++i].split("=");
31-
if (key && value) {
30+
const envVar = args[++i];
31+
const equalsIndex = envVar.indexOf("=");
32+
33+
if (equalsIndex !== -1) {
34+
const key = envVar.substring(0, equalsIndex);
35+
const value = envVar.substring(equalsIndex + 1);
3236
envVars[key] = value;
37+
} else {
38+
envVars[envVar] = "";
3339
}
3440
} else if (!command) {
3541
command = arg;
@@ -96,7 +102,7 @@ async function main() {
96102
await Promise.any([server, client, delay(2 * 1000)]);
97103
const portParam = SERVER_PORT === "3000" ? "" : `?proxyPort=${SERVER_PORT}`;
98104
console.log(
99-
`\n🔍 MCP Inspector is up and running at http://localhost:${CLIENT_PORT}${portParam} 🚀`,
105+
`\n🔍 MCP Inspector is up and running at http://127.0.0.1:${CLIENT_PORT}${portParam} 🚀`,
100106
);
101107

102108
try {

client/jest.config.cjs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@ module.exports = {
33
testEnvironment: "jsdom",
44
moduleNameMapper: {
55
"^@/(.*)$": "<rootDir>/src/$1",
6-
"^../components/DynamicJsonForm$":
7-
"<rootDir>/src/utils/__mocks__/DynamicJsonForm.ts",
8-
"^../../components/DynamicJsonForm$":
9-
"<rootDir>/src/utils/__mocks__/DynamicJsonForm.ts",
6+
"\\.css$": "<rootDir>/src/__mocks__/styleMock.js",
107
},
118
transform: {
129
"^.+\\.tsx?$": [
1310
"ts-jest",
1411
{
15-
useESM: true,
1612
jsx: "react-jsx",
1713
tsconfig: "tsconfig.jest.json",
1814
},

client/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/inspector-client",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"description": "Client-side application for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -24,8 +24,8 @@
2424
},
2525
"dependencies": {
2626
"@modelcontextprotocol/sdk": "^1.6.1",
27-
"@radix-ui/react-dialog": "^1.1.3",
2827
"@radix-ui/react-checkbox": "^1.1.4",
28+
"@radix-ui/react-dialog": "^1.1.3",
2929
"@radix-ui/react-icons": "^1.3.0",
3030
"@radix-ui/react-label": "^2.1.0",
3131
"@radix-ui/react-popover": "^1.1.3",
@@ -50,6 +50,8 @@
5050
},
5151
"devDependencies": {
5252
"@eslint/js": "^9.11.1",
53+
"@testing-library/jest-dom": "^6.6.3",
54+
"@testing-library/react": "^16.2.0",
5355
"@types/jest": "^29.5.14",
5456
"@types/node": "^22.7.5",
5557
"@types/react": "^18.3.10",

client/src/App.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Root,
1616
ServerNotification,
1717
Tool,
18+
LoggingLevel,
1819
} from "@modelcontextprotocol/sdk/types.js";
1920
import React, { Suspense, useEffect, useRef, useState } from "react";
2021
import { useConnection } from "./lib/hooks/useConnection";
@@ -47,7 +48,7 @@ import ToolsTab from "./components/ToolsTab";
4748

4849
const params = new URLSearchParams(window.location.search);
4950
const PROXY_PORT = params.get("proxyPort") ?? "3000";
50-
const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`;
51+
const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`;
5152

5253
const App = () => {
5354
// Handle OAuth callback route
@@ -91,6 +92,7 @@ const App = () => {
9192
(localStorage.getItem("lastTransportType") as "stdio" | "sse") || "stdio"
9293
);
9394
});
95+
const [logLevel, setLogLevel] = useState<LoggingLevel>("debug");
9496
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
9597
const [stdErrNotifications, setStdErrNotifications] = useState<
9698
StdErrNotification[]
@@ -412,6 +414,17 @@ const App = () => {
412414
await sendNotification({ method: "notifications/roots/list_changed" });
413415
};
414416

417+
const sendLogLevelRequest = async (level: LoggingLevel) => {
418+
await makeRequest(
419+
{
420+
method: "logging/setLevel" as const,
421+
params: { level },
422+
},
423+
z.object({}),
424+
);
425+
setLogLevel(level);
426+
};
427+
415428
return (
416429
<div className="flex h-screen bg-background">
417430
<Sidebar
@@ -430,6 +443,9 @@ const App = () => {
430443
setBearerToken={setBearerToken}
431444
onConnect={connectMcpServer}
432445
stdErrNotifications={stdErrNotifications}
446+
logLevel={logLevel}
447+
sendLogLevelRequest={sendLogLevelRequest}
448+
loggingSupported={!!serverCapabilities?.logging || false}
433449
/>
434450
<div className="flex-1 flex flex-col overflow-hidden">
435451
<div className="flex-1 overflow-auto">

client/src/__mocks__/styleMock.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {};

client/src/components/Sidebar.tsx

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import {
1919
SelectValue,
2020
} from "@/components/ui/select";
2121
import { StdErrNotification } from "@/lib/notificationTypes";
22+
import {
23+
LoggingLevel,
24+
LoggingLevelSchema,
25+
} from "@modelcontextprotocol/sdk/types.js";
2226

2327
import useTheme from "../lib/useTheme";
2428
import { version } from "../../../package.json";
@@ -39,6 +43,9 @@ interface SidebarProps {
3943
setBearerToken: (token: string) => void;
4044
onConnect: () => void;
4145
stdErrNotifications: StdErrNotification[];
46+
logLevel: LoggingLevel;
47+
sendLogLevelRequest: (level: LoggingLevel) => void;
48+
loggingSupported: boolean;
4249
}
4350

4451
const Sidebar = ({
@@ -57,6 +64,9 @@ const Sidebar = ({
5764
setBearerToken,
5865
onConnect,
5966
stdErrNotifications,
67+
logLevel,
68+
sendLogLevelRequest,
69+
loggingSupported,
6070
}: SidebarProps) => {
6171
const [theme, setTheme] = useTheme();
6272
const [showEnvVars, setShowEnvVars] = useState(false);
@@ -177,9 +187,17 @@ const Sidebar = ({
177187
value={key}
178188
onChange={(e) => {
179189
const newKey = e.target.value;
180-
const newEnv = { ...env };
181-
delete newEnv[key];
182-
newEnv[newKey] = value;
190+
const newEnv = Object.entries(env).reduce(
191+
(acc, [k, v]) => {
192+
if (k === key) {
193+
acc[newKey] = value;
194+
} else {
195+
acc[k] = v;
196+
}
197+
return acc;
198+
},
199+
{} as Record<string, string>,
200+
);
183201
setEnv(newEnv);
184202
setShownEnvVars((prev) => {
185203
const next = new Set(prev);
@@ -290,6 +308,28 @@ const Sidebar = ({
290308
: "Disconnected"}
291309
</span>
292310
</div>
311+
312+
{loggingSupported && connectionStatus === "connected" && (
313+
<div className="space-y-2">
314+
<label className="text-sm font-medium">Logging Level</label>
315+
<Select
316+
value={logLevel}
317+
onValueChange={(value: LoggingLevel) =>
318+
sendLogLevelRequest(value)
319+
}
320+
>
321+
<SelectTrigger>
322+
<SelectValue placeholder="Select logging level" />
323+
</SelectTrigger>
324+
<SelectContent>
325+
{Object.values(LoggingLevelSchema.enum).map((level) => (
326+
<SelectItem value={level}>{level}</SelectItem>
327+
))}
328+
</SelectContent>
329+
</Select>
330+
</div>
331+
)}
332+
293333
{stdErrNotifications.length > 0 && (
294334
<>
295335
<div className="mt-4 border-t border-gray-200 pt-4">

client/src/components/ToolsTab.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import { Textarea } from "@/components/ui/textarea";
88
import DynamicJsonForm, { JsonSchemaType, JsonValue } from "./DynamicJsonForm";
99
import { generateDefaultValue } from "@/utils/schemaUtils";
1010
import {
11+
CallToolResultSchema,
12+
CompatibilityCallToolResult,
1113
ListToolsResult,
1214
Tool,
13-
CallToolResultSchema,
1415
} from "@modelcontextprotocol/sdk/types.js";
1516
import { AlertCircle, Send } from "lucide-react";
1617
import { useEffect, useState } from "react";
1718
import ListPane from "./ListPane";
18-
19-
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
19+
import { escapeUnicode } from "@/utils/escapeUnicode";
2020

2121
const ToolsTab = ({
2222
tools,
@@ -54,15 +54,15 @@ const ToolsTab = ({
5454
<>
5555
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
5656
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
57-
{JSON.stringify(toolResult, null, 2)}
57+
{escapeUnicode(toolResult)}
5858
</pre>
5959
<h4 className="font-semibold mb-2">Errors:</h4>
6060
{parsedResult.error.errors.map((error, idx) => (
6161
<pre
6262
key={idx}
6363
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64"
6464
>
65-
{JSON.stringify(error, null, 2)}
65+
{escapeUnicode(error)}
6666
</pre>
6767
))}
6868
</>
@@ -101,7 +101,7 @@ const ToolsTab = ({
101101
</audio>
102102
) : (
103103
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 whitespace-pre-wrap break-words p-4 rounded text-sm overflow-auto max-h-64">
104-
{JSON.stringify(item.resource, null, 2)}
104+
{escapeUnicode(item.resource)}
105105
</pre>
106106
))}
107107
</div>
@@ -113,7 +113,7 @@ const ToolsTab = ({
113113
<>
114114
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
115115
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
116-
{JSON.stringify(toolResult.toolResult, null, 2)}
116+
{escapeUnicode(toolResult.toolResult)}
117117
</pre>
118118
</>
119119
);
@@ -233,6 +233,7 @@ const ToolsTab = ({
233233
id={key}
234234
name={key}
235235
placeholder={prop.description}
236+
value={(params[key] as string) ?? ""}
236237
onChange={(e) =>
237238
setParams({
238239
...params,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { render, screen, fireEvent } from "@testing-library/react";
2+
import { describe, it, expect, jest } from "@jest/globals";
3+
import DynamicJsonForm from "../DynamicJsonForm";
4+
import type { JsonSchemaType } from "../DynamicJsonForm";
5+
6+
describe("DynamicJsonForm String Fields", () => {
7+
const renderForm = (props = {}) => {
8+
const defaultProps = {
9+
schema: {
10+
type: "string" as const,
11+
description: "Test string field",
12+
} satisfies JsonSchemaType,
13+
value: undefined,
14+
onChange: jest.fn(),
15+
};
16+
return render(<DynamicJsonForm {...defaultProps} {...props} />);
17+
};
18+
19+
describe("Type Validation", () => {
20+
it("should handle numeric input as string type", () => {
21+
const onChange = jest.fn();
22+
renderForm({ onChange });
23+
24+
const input = screen.getByRole("textbox");
25+
fireEvent.change(input, { target: { value: "123321" } });
26+
27+
expect(onChange).toHaveBeenCalledWith("123321");
28+
// Verify the value is a string, not a number
29+
expect(typeof onChange.mock.calls[0][0]).toBe("string");
30+
});
31+
32+
it("should render as text input, not number input", () => {
33+
renderForm();
34+
const input = screen.getByRole("textbox");
35+
expect(input).toHaveProperty("type", "text");
36+
});
37+
});
38+
});
39+
40+
describe("DynamicJsonForm Integer Fields", () => {
41+
const renderForm = (props = {}) => {
42+
const defaultProps = {
43+
schema: {
44+
type: "integer" as const,
45+
description: "Test integer field",
46+
} satisfies JsonSchemaType,
47+
value: undefined,
48+
onChange: jest.fn(),
49+
};
50+
return render(<DynamicJsonForm {...defaultProps} {...props} />);
51+
};
52+
53+
describe("Basic Operations", () => {
54+
it("should render number input with step=1", () => {
55+
renderForm();
56+
const input = screen.getByRole("spinbutton");
57+
expect(input).toHaveProperty("type", "number");
58+
expect(input).toHaveProperty("step", "1");
59+
});
60+
61+
it("should pass integer values to onChange", () => {
62+
const onChange = jest.fn();
63+
renderForm({ onChange });
64+
65+
const input = screen.getByRole("spinbutton");
66+
fireEvent.change(input, { target: { value: "42" } });
67+
68+
expect(onChange).toHaveBeenCalledWith(42);
69+
// Verify the value is a number, not a string
70+
expect(typeof onChange.mock.calls[0][0]).toBe("number");
71+
});
72+
73+
it("should not pass string values to onChange", () => {
74+
const onChange = jest.fn();
75+
renderForm({ onChange });
76+
77+
const input = screen.getByRole("spinbutton");
78+
fireEvent.change(input, { target: { value: "abc" } });
79+
80+
expect(onChange).not.toHaveBeenCalled();
81+
});
82+
});
83+
84+
describe("Edge Cases", () => {
85+
it("should handle non-numeric input by not calling onChange", () => {
86+
const onChange = jest.fn();
87+
renderForm({ onChange });
88+
89+
const input = screen.getByRole("spinbutton");
90+
fireEvent.change(input, { target: { value: "abc" } });
91+
92+
expect(onChange).not.toHaveBeenCalled();
93+
});
94+
});
95+
});

0 commit comments

Comments
 (0)