Merge branch 'main' into fix/set-scope-dcr
This commit is contained in:
commit
99120937e2
13 changed files with 986 additions and 588 deletions
29
README.md
29
README.md
|
|
@ -236,13 +236,15 @@ ALLOWED_ORIGINS=http://localhost:6274,http://localhost:8000 npm start
|
|||
|
||||
The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:
|
||||
|
||||
| Setting | Description | Default |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 |
|
||||
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
|
||||
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
|
||||
| `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 | "" |
|
||||
| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true |
|
||||
| Setting | Description | Default |
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `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 |
|
||||
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
|
||||
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
|
||||
| `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 | "" |
|
||||
| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true |
|
||||
|
||||
**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.
|
||||
|
||||
These settings can be adjusted in real-time through the UI and will persist across sessions.
|
||||
|
||||
|
|
@ -361,7 +363,7 @@ http://localhost:6274/?transport=stdio&serverCommand=npx&serverArgs=arg1%20arg2
|
|||
You can also set initial config settings via query params, for example:
|
||||
|
||||
```
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
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
|
|||
| **Automation** | N/A | Ideal for CI/CD pipelines, batch processing, and integration with coding assistants |
|
||||
| **Learning MCP** | Rich visual interface helps new users understand server capabilities | Simplified commands for focused learning of specific endpoints |
|
||||
|
||||
## Tool Input Validation Guidelines
|
||||
|
||||
When implementing or modifying tool input parameter handling in the Inspector:
|
||||
|
||||
- **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
|
||||
- **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
|
||||
- **Always include required fields** - Preserve required field values even when empty, allowing the MCP server to validate and return appropriate error messages
|
||||
- **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
|
||||
|
||||
These guidelines maintain clean parameter passing and proper separation of concerns between the Inspector client and MCP servers.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-cli",
|
||||
"version": "0.17.1",
|
||||
"version": "0.17.2",
|
||||
"description": "CLI for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
},
|
||||
"devDependencies": {},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.18.0",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"commander": "^13.1.0",
|
||||
"spawn-rx": "^5.1.2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-client",
|
||||
"version": "0.17.1",
|
||||
"version": "0.17.2",
|
||||
"description": "Client-side application for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
"cleanup:e2e": "node e2e/global-teardown.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.18.0",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.3",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/serve-handler": "^6.1.4",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"co": "^4.6.0",
|
||||
"eslint": "^9.11.1",
|
||||
|
|
@ -77,6 +77,6 @@
|
|||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^6.3.5"
|
||||
"vite": "^7.1.11"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,18 @@ const ToolsTab = ({
|
|||
onCheckedChange={(checked: boolean) =>
|
||||
setParams({
|
||||
...params,
|
||||
[key]: checked ? null : prop.default,
|
||||
[key]: checked
|
||||
? null
|
||||
: prop.default !== null
|
||||
? prop.default
|
||||
: prop.type === "boolean"
|
||||
? false
|
||||
: prop.type === "string"
|
||||
? ""
|
||||
: prop.type === "number" ||
|
||||
prop.type === "integer"
|
||||
? undefined
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -913,7 +913,8 @@ describe("Sidebar", () => {
|
|||
expect.objectContaining({
|
||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||
label: "Request Timeout",
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
description:
|
||||
"Client-side timeout (ms) - Inspector will cancel requests after this time",
|
||||
value: 5000,
|
||||
is_session_item: false,
|
||||
},
|
||||
|
|
@ -988,7 +989,8 @@ describe("Sidebar", () => {
|
|||
expect.objectContaining({
|
||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||
label: "Request Timeout",
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
description:
|
||||
"Client-side timeout (ms) - Inspector will cancel requests after this time",
|
||||
value: 0,
|
||||
is_session_item: false,
|
||||
},
|
||||
|
|
@ -1035,7 +1037,8 @@ describe("Sidebar", () => {
|
|||
expect.objectContaining({
|
||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||
label: "Request Timeout",
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
description:
|
||||
"Client-side timeout (ms) - Inspector will cancel requests after this time",
|
||||
value: 3000,
|
||||
is_session_item: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -177,6 +177,103 @@ describe("ToolsTab", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should support tri-state nullable boolean (null -> false -> true -> null)", async () => {
|
||||
const mockCallTool = jest.fn();
|
||||
const toolWithNullableBoolean: Tool = {
|
||||
name: "testTool",
|
||||
description: "Tool with nullable boolean",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
optionalBoolean: {
|
||||
type: ["boolean", "null"] as const,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderToolsTab({
|
||||
tools: [toolWithNullableBoolean],
|
||||
selectedTool: toolWithNullableBoolean,
|
||||
callTool: mockCallTool,
|
||||
});
|
||||
|
||||
const nullCheckbox = screen.getByRole("checkbox", { name: /null/i });
|
||||
const runButton = screen.getByRole("button", { name: /run tool/i });
|
||||
|
||||
// State 1: Initial state should be null (input disabled)
|
||||
const wrapper = screen.getByRole("toolinputwrapper");
|
||||
expect(wrapper.classList).toContain("pointer-events-none");
|
||||
expect(wrapper.classList).toContain("opacity-50");
|
||||
|
||||
// Verify tool is called with null initially
|
||||
await act(async () => {
|
||||
fireEvent.click(runButton);
|
||||
});
|
||||
expect(mockCallTool).toHaveBeenCalledWith(toolWithNullableBoolean.name, {
|
||||
optionalBoolean: null,
|
||||
});
|
||||
|
||||
// State 2: Uncheck null checkbox -> should set value to false and enable input
|
||||
await act(async () => {
|
||||
fireEvent.click(nullCheckbox);
|
||||
});
|
||||
expect(wrapper.classList).not.toContain("pointer-events-none");
|
||||
|
||||
// Clear previous calls to make assertions clearer
|
||||
mockCallTool.mockClear();
|
||||
|
||||
// Verify tool can be called with false
|
||||
await act(async () => {
|
||||
fireEvent.click(runButton);
|
||||
});
|
||||
expect(mockCallTool).toHaveBeenLastCalledWith(
|
||||
toolWithNullableBoolean.name,
|
||||
{
|
||||
optionalBoolean: false,
|
||||
},
|
||||
);
|
||||
|
||||
// State 3: Check boolean checkbox -> should set value to true
|
||||
// Find the boolean checkbox within the input wrapper (to avoid ID conflict with null checkbox)
|
||||
const booleanCheckbox = within(wrapper).getByRole("checkbox");
|
||||
|
||||
mockCallTool.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(booleanCheckbox);
|
||||
});
|
||||
|
||||
// Verify tool can be called with true
|
||||
await act(async () => {
|
||||
fireEvent.click(runButton);
|
||||
});
|
||||
expect(mockCallTool).toHaveBeenLastCalledWith(
|
||||
toolWithNullableBoolean.name,
|
||||
{
|
||||
optionalBoolean: true,
|
||||
},
|
||||
);
|
||||
|
||||
// State 4: Check null checkbox again -> should set value back to null and disable input
|
||||
await act(async () => {
|
||||
fireEvent.click(nullCheckbox);
|
||||
});
|
||||
expect(wrapper.classList).toContain("pointer-events-none");
|
||||
|
||||
// Verify tool can be called with null again
|
||||
await act(async () => {
|
||||
fireEvent.click(runButton);
|
||||
});
|
||||
expect(mockCallTool).toHaveBeenLastCalledWith(
|
||||
toolWithNullableBoolean.name,
|
||||
{
|
||||
optionalBoolean: null,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should disable button and change text while tool is running", async () => {
|
||||
// Create a promise that we can resolve later
|
||||
let resolvePromise: ((value: unknown) => void) | undefined;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export type ConfigItem = {
|
|||
*/
|
||||
export type InspectorConfig = {
|
||||
/**
|
||||
* Maximum time in milliseconds to wait for a response from the MCP server before timing out.
|
||||
* Client-side timeout in milliseconds. The Inspector will cancel the request if no response
|
||||
* is received within this time. Note: This is independent of any server-side timeouts.
|
||||
*/
|
||||
MCP_SERVER_REQUEST_TIMEOUT: ConfigItem;
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
|
|||
export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
|
||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||
label: "Request Timeout",
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
value: 10000,
|
||||
description:
|
||||
"Client-side timeout (ms) - Inspector will cancel requests after this time",
|
||||
value: 300000, // 5 minutes - increased to support elicitation and other long-running tools
|
||||
is_session_item: false,
|
||||
},
|
||||
MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: {
|
||||
|
|
|
|||
|
|
@ -205,4 +205,112 @@ describe("cleanParams", () => {
|
|||
// optionalField omitted entirely
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve null values when field has default: null", () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
required: [],
|
||||
properties: {
|
||||
optionalFieldWithNullDefault: { type: "string", default: null },
|
||||
optionalFieldWithoutDefault: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
const params = {
|
||||
optionalFieldWithNullDefault: null,
|
||||
optionalFieldWithoutDefault: null,
|
||||
};
|
||||
|
||||
const cleaned = cleanParams(params, schema);
|
||||
|
||||
expect(cleaned).toEqual({
|
||||
optionalFieldWithNullDefault: null, // preserved because default: null
|
||||
// optionalFieldWithoutDefault omitted
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve default values that match current value", () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
required: [],
|
||||
properties: {
|
||||
fieldWithDefaultString: { type: "string", default: "defaultValue" },
|
||||
fieldWithDefaultNumber: { type: "number", default: 42 },
|
||||
fieldWithDefaultNull: { type: "string", default: null },
|
||||
fieldWithDefaultBoolean: { type: "boolean", default: false },
|
||||
},
|
||||
};
|
||||
|
||||
const params = {
|
||||
fieldWithDefaultString: "defaultValue",
|
||||
fieldWithDefaultNumber: 42,
|
||||
fieldWithDefaultNull: null,
|
||||
fieldWithDefaultBoolean: false,
|
||||
};
|
||||
|
||||
const cleaned = cleanParams(params, schema);
|
||||
|
||||
expect(cleaned).toEqual({
|
||||
fieldWithDefaultString: "defaultValue",
|
||||
fieldWithDefaultNumber: 42,
|
||||
fieldWithDefaultNull: null,
|
||||
fieldWithDefaultBoolean: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should omit values that do not match their default", () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
required: [],
|
||||
properties: {
|
||||
fieldWithDefault: { type: "string", default: "defaultValue" },
|
||||
},
|
||||
};
|
||||
|
||||
const params = {
|
||||
fieldWithDefault: null, // doesn't match default
|
||||
};
|
||||
|
||||
const cleaned = cleanParams(params, schema);
|
||||
|
||||
expect(cleaned).toEqual({
|
||||
// fieldWithDefault omitted because value (null) doesn't match default ("defaultValue")
|
||||
});
|
||||
});
|
||||
|
||||
it("should fix regression from issue #846 - tools with multiple null defaults", () => {
|
||||
// Reproduces the exact scenario from https://github.com/modelcontextprotocol/inspector/issues/846
|
||||
// In v0.17.0, the cleanParams function would remove all null values,
|
||||
// breaking tools that have parameters with explicit default: null
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
required: ["requiredString"],
|
||||
properties: {
|
||||
optionalString: { type: ["string", "null"], default: null },
|
||||
optionalNumber: { type: ["number", "null"], default: null },
|
||||
optionalBoolean: { type: ["boolean", "null"], default: null },
|
||||
requiredString: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
// When a user opens the tool in Inspector, fields initialize with their defaults
|
||||
const params = {
|
||||
optionalString: null, // initialized to default
|
||||
optionalNumber: null, // initialized to default
|
||||
optionalBoolean: null, // initialized to default
|
||||
requiredString: "test",
|
||||
};
|
||||
|
||||
const cleaned = cleanParams(params, schema);
|
||||
|
||||
// In v0.16, null defaults were preserved (working behavior)
|
||||
// In v0.17.0, they were removed (regression)
|
||||
// This fix restores the v0.16 behavior
|
||||
expect(cleaned).toEqual({
|
||||
optionalString: null,
|
||||
optionalNumber: null,
|
||||
optionalBoolean: null,
|
||||
requiredString: "test",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { JsonSchemaType } from "./jsonUtils";
|
|||
|
||||
/**
|
||||
* Cleans parameters by removing undefined, null, and empty string values for optional fields
|
||||
* while preserving all values for required fields.
|
||||
* while preserving all values for required fields and fields with explicit default values.
|
||||
*
|
||||
* @param params - The parameters object to clean
|
||||
* @param schema - The JSON schema defining which fields are required
|
||||
|
|
@ -14,13 +14,23 @@ export function cleanParams(
|
|||
): Record<string, unknown> {
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
const required = schema.required || [];
|
||||
const properties = schema.properties || {};
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
const isFieldRequired = required.includes(key);
|
||||
const fieldSchema = properties[key] as JsonSchemaType | undefined;
|
||||
|
||||
// Check if the field has an explicit default value
|
||||
const hasDefault = fieldSchema && "default" in fieldSchema;
|
||||
const defaultValue = hasDefault ? fieldSchema.default : undefined;
|
||||
|
||||
if (isFieldRequired) {
|
||||
// Required fields: always include, even if empty string or falsy
|
||||
cleaned[key] = value;
|
||||
} else if (hasDefault && value === defaultValue) {
|
||||
// Field has a default value and current value matches it - preserve it
|
||||
// This is important for cases like default: null
|
||||
cleaned[key] = value;
|
||||
} else {
|
||||
// Optional fields: only include if they have meaningful values
|
||||
if (value !== undefined && value !== "" && value !== null) {
|
||||
|
|
|
|||
1123
package-lock.json
generated
1123
package-lock.json
generated
File diff suppressed because it is too large
Load diff
11
package.json
11
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.17.1",
|
||||
"version": "0.17.2",
|
||||
"description": "Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
@ -47,10 +47,10 @@
|
|||
"check-version": "node scripts/check-version-consistency.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/inspector-cli": "^0.17.1",
|
||||
"@modelcontextprotocol/inspector-client": "^0.17.1",
|
||||
"@modelcontextprotocol/inspector-server": "^0.17.1",
|
||||
"@modelcontextprotocol/sdk": "^1.18.0",
|
||||
"@modelcontextprotocol/inspector-cli": "^0.17.2",
|
||||
"@modelcontextprotocol/inspector-client": "^0.17.2",
|
||||
"@modelcontextprotocol/inspector-server": "^0.17.2",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"concurrently": "^9.2.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"open": "^10.2.0",
|
||||
|
|
@ -67,6 +67,7 @@
|
|||
"husky": "^9.1.7",
|
||||
"jest-fixed-jsdom": "^0.0.9",
|
||||
"lint-staged": "^16.1.5",
|
||||
"playwright": "^1.56.1",
|
||||
"prettier": "^3.6.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.4.2"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-server",
|
||||
"version": "0.17.1",
|
||||
"version": "0.17.2",
|
||||
"description": "Server-side application for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"typescript": "^5.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.18.0",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"shell-quote": "^1.8.3",
|
||||
|
|
|
|||
Loading…
Reference in a new issue