Skip to content

Commit 9912093

Browse files
authored
Merge branch 'main' into fix/set-scope-dcr
2 parents 3b247ea + c7683b4 commit 9912093

File tree

13 files changed

+986
-588
lines changed

13 files changed

+986
-588
lines changed

README.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,15 @@ ALLOWED_ORIGINS=http://localhost:6274,http://localhost:8000 npm start
236236

237237
The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:
238238

239-
| Setting | Description | Default |
240-
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
241-
| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 |
242-
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
243-
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
244-
| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" |
245-
| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true |
239+
| Setting | Description | Default |
240+
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
241+
| `MCP_SERVER_REQUEST_TIMEOUT` | Client-side timeout (ms) - Inspector will cancel the request if no response is received within this time. Note: servers may have their own timeouts | 300000 |
242+
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
243+
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
244+
| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" |
245+
| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true |
246+
247+
**Note on Timeouts:** The timeout settings above control when the Inspector (as an MCP client) will cancel requests. These are independent of any server-side timeouts. For example, if a server tool has a 10-minute timeout but the Inspector's timeout is set to 30 seconds, the Inspector will cancel the request after 30 seconds. Conversely, if the Inspector's timeout is 10 minutes but the server times out after 30 seconds, you'll receive the server's timeout error. For tools that require user interaction (like elicitation) or long-running operations, ensure the Inspector's timeout is set appropriately.
246248

247249
These settings can be adjusted in real-time through the UI and will persist across sessions.
248250

@@ -361,7 +363,7 @@ http://localhost:6274/?transport=stdio&serverCommand=npx&serverArgs=arg1%20arg2
361363
You can also set initial config settings via query params, for example:
362364

363365
```
364-
http://localhost:6274/?MCP_SERVER_REQUEST_TIMEOUT=10000&MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS=false&MCP_PROXY_FULL_ADDRESS=http://10.1.1.22:5577
366+
http://localhost:6274/?MCP_SERVER_REQUEST_TIMEOUT=60000&MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS=false&MCP_PROXY_FULL_ADDRESS=http://10.1.1.22:5577
365367
```
366368

367369
Note that if both the query param and the corresponding localStorage item are set, the query param will take precedence.
@@ -456,6 +458,17 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --me
456458
| **Automation** | N/A | Ideal for CI/CD pipelines, batch processing, and integration with coding assistants |
457459
| **Learning MCP** | Rich visual interface helps new users understand server capabilities | Simplified commands for focused learning of specific endpoints |
458460

461+
## Tool Input Validation Guidelines
462+
463+
When implementing or modifying tool input parameter handling in the Inspector:
464+
465+
- **Omit optional fields with empty values** - When processing form inputs, omit empty strings or null values for optional parameters, UNLESS the field has an explicit default value in the schema that matches the current value
466+
- **Preserve explicit default values** - If a field schema contains an explicit default (e.g., `default: null`), and the current value matches that default, include it in the request. This is a meaningful value the tool expects
467+
- **Always include required fields** - Preserve required field values even when empty, allowing the MCP server to validate and return appropriate error messages
468+
- **Defer deep validation to the server** - Implement basic field presence checking in the Inspector client, but rely on the MCP server for parameter validation according to its schema
469+
470+
These guidelines maintain clean parameter passing and proper separation of concerns between the Inspector client and MCP servers.
471+
459472
## License
460473

461474
This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.

cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/inspector-cli",
3-
"version": "0.17.1",
3+
"version": "0.17.2",
44
"description": "CLI for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -24,7 +24,7 @@
2424
},
2525
"devDependencies": {},
2626
"dependencies": {
27-
"@modelcontextprotocol/sdk": "^1.18.0",
27+
"@modelcontextprotocol/sdk": "^1.20.1",
2828
"commander": "^13.1.0",
2929
"spawn-rx": "^5.1.2"
3030
}

client/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/inspector-client",
3-
"version": "0.17.1",
3+
"version": "0.17.2",
44
"description": "Client-side application for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -25,7 +25,7 @@
2525
"cleanup:e2e": "node e2e/global-teardown.js"
2626
},
2727
"dependencies": {
28-
"@modelcontextprotocol/sdk": "^1.18.0",
28+
"@modelcontextprotocol/sdk": "^1.20.1",
2929
"@radix-ui/react-checkbox": "^1.1.4",
3030
"@radix-ui/react-dialog": "^1.1.3",
3131
"@radix-ui/react-icons": "^1.3.0",
@@ -61,7 +61,7 @@
6161
"@types/react": "^18.3.23",
6262
"@types/react-dom": "^18.3.0",
6363
"@types/serve-handler": "^6.1.4",
64-
"@vitejs/plugin-react": "^4.7.0",
64+
"@vitejs/plugin-react": "^5.0.4",
6565
"autoprefixer": "^10.4.20",
6666
"co": "^4.6.0",
6767
"eslint": "^9.11.1",
@@ -77,6 +77,6 @@
7777
"ts-jest": "^29.4.0",
7878
"typescript": "^5.5.3",
7979
"typescript-eslint": "^8.38.0",
80-
"vite": "^6.3.5"
80+
"vite": "^7.1.11"
8181
}
8282
}

client/src/components/ToolsTab.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,18 @@ const ToolsTab = ({
179179
onCheckedChange={(checked: boolean) =>
180180
setParams({
181181
...params,
182-
[key]: checked ? null : prop.default,
182+
[key]: checked
183+
? null
184+
: prop.default !== null
185+
? prop.default
186+
: prop.type === "boolean"
187+
? false
188+
: prop.type === "string"
189+
? ""
190+
: prop.type === "number" ||
191+
prop.type === "integer"
192+
? undefined
193+
: undefined,
183194
})
184195
}
185196
/>

client/src/components/__tests__/Sidebar.test.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -913,7 +913,8 @@ describe("Sidebar", () => {
913913
expect.objectContaining({
914914
MCP_SERVER_REQUEST_TIMEOUT: {
915915
label: "Request Timeout",
916-
description: "Timeout for requests to the MCP server (ms)",
916+
description:
917+
"Client-side timeout (ms) - Inspector will cancel requests after this time",
917918
value: 5000,
918919
is_session_item: false,
919920
},
@@ -988,7 +989,8 @@ describe("Sidebar", () => {
988989
expect.objectContaining({
989990
MCP_SERVER_REQUEST_TIMEOUT: {
990991
label: "Request Timeout",
991-
description: "Timeout for requests to the MCP server (ms)",
992+
description:
993+
"Client-side timeout (ms) - Inspector will cancel requests after this time",
992994
value: 0,
993995
is_session_item: false,
994996
},
@@ -1035,7 +1037,8 @@ describe("Sidebar", () => {
10351037
expect.objectContaining({
10361038
MCP_SERVER_REQUEST_TIMEOUT: {
10371039
label: "Request Timeout",
1038-
description: "Timeout for requests to the MCP server (ms)",
1040+
description:
1041+
"Client-side timeout (ms) - Inspector will cancel requests after this time",
10391042
value: 3000,
10401043
is_session_item: false,
10411044
},

client/src/components/__tests__/ToolsTab.test.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,103 @@ describe("ToolsTab", () => {
177177
});
178178
});
179179

180+
it("should support tri-state nullable boolean (null -> false -> true -> null)", async () => {
181+
const mockCallTool = jest.fn();
182+
const toolWithNullableBoolean: Tool = {
183+
name: "testTool",
184+
description: "Tool with nullable boolean",
185+
inputSchema: {
186+
type: "object" as const,
187+
properties: {
188+
optionalBoolean: {
189+
type: ["boolean", "null"] as const,
190+
default: null,
191+
},
192+
},
193+
},
194+
};
195+
196+
renderToolsTab({
197+
tools: [toolWithNullableBoolean],
198+
selectedTool: toolWithNullableBoolean,
199+
callTool: mockCallTool,
200+
});
201+
202+
const nullCheckbox = screen.getByRole("checkbox", { name: /null/i });
203+
const runButton = screen.getByRole("button", { name: /run tool/i });
204+
205+
// State 1: Initial state should be null (input disabled)
206+
const wrapper = screen.getByRole("toolinputwrapper");
207+
expect(wrapper.classList).toContain("pointer-events-none");
208+
expect(wrapper.classList).toContain("opacity-50");
209+
210+
// Verify tool is called with null initially
211+
await act(async () => {
212+
fireEvent.click(runButton);
213+
});
214+
expect(mockCallTool).toHaveBeenCalledWith(toolWithNullableBoolean.name, {
215+
optionalBoolean: null,
216+
});
217+
218+
// State 2: Uncheck null checkbox -> should set value to false and enable input
219+
await act(async () => {
220+
fireEvent.click(nullCheckbox);
221+
});
222+
expect(wrapper.classList).not.toContain("pointer-events-none");
223+
224+
// Clear previous calls to make assertions clearer
225+
mockCallTool.mockClear();
226+
227+
// Verify tool can be called with false
228+
await act(async () => {
229+
fireEvent.click(runButton);
230+
});
231+
expect(mockCallTool).toHaveBeenLastCalledWith(
232+
toolWithNullableBoolean.name,
233+
{
234+
optionalBoolean: false,
235+
},
236+
);
237+
238+
// State 3: Check boolean checkbox -> should set value to true
239+
// Find the boolean checkbox within the input wrapper (to avoid ID conflict with null checkbox)
240+
const booleanCheckbox = within(wrapper).getByRole("checkbox");
241+
242+
mockCallTool.mockClear();
243+
244+
await act(async () => {
245+
fireEvent.click(booleanCheckbox);
246+
});
247+
248+
// Verify tool can be called with true
249+
await act(async () => {
250+
fireEvent.click(runButton);
251+
});
252+
expect(mockCallTool).toHaveBeenLastCalledWith(
253+
toolWithNullableBoolean.name,
254+
{
255+
optionalBoolean: true,
256+
},
257+
);
258+
259+
// State 4: Check null checkbox again -> should set value back to null and disable input
260+
await act(async () => {
261+
fireEvent.click(nullCheckbox);
262+
});
263+
expect(wrapper.classList).toContain("pointer-events-none");
264+
265+
// Verify tool can be called with null again
266+
await act(async () => {
267+
fireEvent.click(runButton);
268+
});
269+
expect(mockCallTool).toHaveBeenLastCalledWith(
270+
toolWithNullableBoolean.name,
271+
{
272+
optionalBoolean: null,
273+
},
274+
);
275+
});
276+
180277
it("should disable button and change text while tool is running", async () => {
181278
// Create a promise that we can resolve later
182279
let resolvePromise: ((value: unknown) => void) | undefined;

client/src/lib/configurationTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export type ConfigItem = {
1414
*/
1515
export type InspectorConfig = {
1616
/**
17-
* Maximum time in milliseconds to wait for a response from the MCP server before timing out.
17+
* Client-side timeout in milliseconds. The Inspector will cancel the request if no response
18+
* is received within this time. Note: This is independent of any server-side timeouts.
1819
*/
1920
MCP_SERVER_REQUEST_TIMEOUT: ConfigItem;
2021

client/src/lib/constants.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
4444
export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
4545
MCP_SERVER_REQUEST_TIMEOUT: {
4646
label: "Request Timeout",
47-
description: "Timeout for requests to the MCP server (ms)",
48-
value: 10000,
47+
description:
48+
"Client-side timeout (ms) - Inspector will cancel requests after this time",
49+
value: 300000, // 5 minutes - increased to support elicitation and other long-running tools
4950
is_session_item: false,
5051
},
5152
MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: {

client/src/utils/__tests__/paramUtils.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,112 @@ describe("cleanParams", () => {
205205
// optionalField omitted entirely
206206
});
207207
});
208+
209+
it("should preserve null values when field has default: null", () => {
210+
const schema: JsonSchemaType = {
211+
type: "object",
212+
required: [],
213+
properties: {
214+
optionalFieldWithNullDefault: { type: "string", default: null },
215+
optionalFieldWithoutDefault: { type: "string" },
216+
},
217+
};
218+
219+
const params = {
220+
optionalFieldWithNullDefault: null,
221+
optionalFieldWithoutDefault: null,
222+
};
223+
224+
const cleaned = cleanParams(params, schema);
225+
226+
expect(cleaned).toEqual({
227+
optionalFieldWithNullDefault: null, // preserved because default: null
228+
// optionalFieldWithoutDefault omitted
229+
});
230+
});
231+
232+
it("should preserve default values that match current value", () => {
233+
const schema: JsonSchemaType = {
234+
type: "object",
235+
required: [],
236+
properties: {
237+
fieldWithDefaultString: { type: "string", default: "defaultValue" },
238+
fieldWithDefaultNumber: { type: "number", default: 42 },
239+
fieldWithDefaultNull: { type: "string", default: null },
240+
fieldWithDefaultBoolean: { type: "boolean", default: false },
241+
},
242+
};
243+
244+
const params = {
245+
fieldWithDefaultString: "defaultValue",
246+
fieldWithDefaultNumber: 42,
247+
fieldWithDefaultNull: null,
248+
fieldWithDefaultBoolean: false,
249+
};
250+
251+
const cleaned = cleanParams(params, schema);
252+
253+
expect(cleaned).toEqual({
254+
fieldWithDefaultString: "defaultValue",
255+
fieldWithDefaultNumber: 42,
256+
fieldWithDefaultNull: null,
257+
fieldWithDefaultBoolean: false,
258+
});
259+
});
260+
261+
it("should omit values that do not match their default", () => {
262+
const schema: JsonSchemaType = {
263+
type: "object",
264+
required: [],
265+
properties: {
266+
fieldWithDefault: { type: "string", default: "defaultValue" },
267+
},
268+
};
269+
270+
const params = {
271+
fieldWithDefault: null, // doesn't match default
272+
};
273+
274+
const cleaned = cleanParams(params, schema);
275+
276+
expect(cleaned).toEqual({
277+
// fieldWithDefault omitted because value (null) doesn't match default ("defaultValue")
278+
});
279+
});
280+
281+
it("should fix regression from issue #846 - tools with multiple null defaults", () => {
282+
// Reproduces the exact scenario from https://github.com/modelcontextprotocol/inspector/issues/846
283+
// In v0.17.0, the cleanParams function would remove all null values,
284+
// breaking tools that have parameters with explicit default: null
285+
const schema: JsonSchemaType = {
286+
type: "object",
287+
required: ["requiredString"],
288+
properties: {
289+
optionalString: { type: ["string", "null"], default: null },
290+
optionalNumber: { type: ["number", "null"], default: null },
291+
optionalBoolean: { type: ["boolean", "null"], default: null },
292+
requiredString: { type: "string" },
293+
},
294+
};
295+
296+
// When a user opens the tool in Inspector, fields initialize with their defaults
297+
const params = {
298+
optionalString: null, // initialized to default
299+
optionalNumber: null, // initialized to default
300+
optionalBoolean: null, // initialized to default
301+
requiredString: "test",
302+
};
303+
304+
const cleaned = cleanParams(params, schema);
305+
306+
// In v0.16, null defaults were preserved (working behavior)
307+
// In v0.17.0, they were removed (regression)
308+
// This fix restores the v0.16 behavior
309+
expect(cleaned).toEqual({
310+
optionalString: null,
311+
optionalNumber: null,
312+
optionalBoolean: null,
313+
requiredString: "test",
314+
});
315+
});
208316
});

0 commit comments

Comments
 (0)