Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,14 @@ The project is organized as a monorepo with workspaces:
- `client/`: React frontend with Vite, TypeScript and Tailwind
- `server/`: Express backend with TypeScript
- `cli/`: Command-line interface for testing and invoking MCP server methods directly

## Tool Input Validation Guidelines

When handling tool input parameters and form fields:

- **Optional fields with empty values should be omitted entirely** - Do not send empty strings or null values for optional parameters, UNLESS the field has an explicit default value in the schema that matches the current value
- **Fields with explicit defaults should preserve their default values** - If a field has an explicit default in its schema (e.g., `default: null`), and the current value matches that default, include it in the request. This is a meaningful value the tool expects
- **Required fields should preserve their values even when empty** - This allows the server to properly validate and return appropriate error messages
- **Deeper validation should be handled by the server** - Inspector should focus on basic field presence, while the MCP server handles parameter validation according to its schema

These guidelines ensure clean parameter passing and proper separation of concerns between the Inspector client and MCP servers.
72 changes: 72 additions & 0 deletions client/src/utils/__tests__/paramUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,76 @@ 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")
});
});
});
12 changes: 11 additions & 1 deletion client/src/utils/paramUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
Loading