Skip to content

Add docs with more details on testing tools and update behavior for optional values #356

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ 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 |

For detailed information about tool testing, including input type handling, validation rules, and best practices, see the [Tool Testing Guide](docs/tools-testing.md).

## License

This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.
88 changes: 80 additions & 8 deletions client/src/utils/__tests__/schemaUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ describe("generateDefaultValue", () => {
);
});

test("generates empty array for non-required array", () => {
expect(generateDefaultValue({ type: "array", required: false })).toEqual(
[],
);
test("omits non-required array", () => {
expect(
generateDefaultValue({ type: "array", required: false }),
).toBeUndefined();
});

test("generates empty object for non-required object", () => {
expect(generateDefaultValue({ type: "object", required: false })).toEqual(
{},
);
test("omits non-required object", () => {
expect(
generateDefaultValue({ type: "object", required: false }),
).toBeUndefined();
});

test("generates null for non-required primitive types", () => {
Expand All @@ -61,6 +61,78 @@ describe("generateDefaultValue", () => {
);
});

test("generates empty array when required", () => {
expect(generateDefaultValue({ type: "array", required: true })).toEqual([]);
});

test("generates empty object when required", () => {
expect(generateDefaultValue({ type: "object", required: true })).toEqual(
{},
);
});

test("omits non-required primitive types", () => {
expect(
generateDefaultValue({ type: "string", required: false }),
).toBeUndefined();
expect(
generateDefaultValue({ type: "number", required: false }),
).toBeUndefined();
expect(
generateDefaultValue({ type: "boolean", required: false }),
).toBeUndefined();
});

test("handles explicitly nullable fields", () => {
expect(
generateDefaultValue({
type: ["string", "null"],
required: true,
}),
).toBe(null);

expect(
generateDefaultValue({
type: ["number", "null"],
required: true,
}),
).toBe(null);
});

test("handles nullable arrays and objects", () => {
expect(
generateDefaultValue({
type: ["array", "null"],
required: true,
}),
).toBe(null);

expect(
generateDefaultValue({
type: ["object", "null"],
required: true,
}),
).toBe(null);
});

test("distinguishes between nullable and optional fields", () => {
// Optional field (should be omitted)
expect(
generateDefaultValue({
type: "string",
required: false,
}),
).toBeUndefined();

// Nullable field (can be explicitly null)
expect(
generateDefaultValue({
type: ["string", "null"],
required: true,
}),
).toBe(null);
});

test("generates object with properties", () => {
const schema: JsonSchemaType = {
type: "object",
Expand Down
18 changes: 10 additions & 8 deletions client/src/utils/jsonUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ export type JsonValue =
| JsonValue[]
| { [key: string]: JsonValue };

export type SchemaType =
| "string"
| "number"
| "integer"
| "boolean"
| "array"
| "object"
| "null";

export type JsonSchemaType = {
type:
| "string"
| "number"
| "integer"
| "boolean"
| "array"
| "object"
| "null";
type: SchemaType | SchemaType[];
description?: string;
required?: boolean;
default?: JsonValue;
Expand Down
21 changes: 17 additions & 4 deletions client/src/utils/schemaUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { JsonValue, JsonSchemaType, JsonObject } from "./jsonUtils";
import type {
JsonValue,
JsonSchemaType,
JsonObject,
SchemaType,
} from "./jsonUtils";

/**
* Generates a default value based on a JSON schema type
Expand All @@ -10,13 +15,21 @@ export function generateDefaultValue(schema: JsonSchemaType): JsonValue {
return schema.default;
}

// Handle union types (e.g. ["string", "null"])
if (Array.isArray(schema.type) && schema.type.includes("null")) {
return null;
}

const type: SchemaType = Array.isArray(schema.type)
? schema.type[0]
: schema.type;

if (!schema.required) {
if (schema.type === "array") return [];
if (schema.type === "object") return {};
// All optional fields should be omitted (undefined)
return undefined;
}

switch (schema.type) {
switch (type) {
case "string":
return "";
case "number":
Expand Down
82 changes: 82 additions & 0 deletions docs/tools-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Tool Testing Guide

This guide explains how different types of tool inputs are expected be handled in the MCP Inspector. (Note that this guide currently only covers testing using the UI and not CLI mode.)

The Tools UI provides both structured form and raw JSON editor modes. For supported types, the UI allows switching between modes while preserving values, with some exceptions for [complex objects](#complex-objects). Input types are validated in both modes.

## Input Type Handling

### Basic Types

| Type | Example Schema | Expected Input | Form Behavior |
| --------- | ----------------------- | -------------- | ------------------------ |
| `integer` | `{ "type": "integer" }` | `42` | Number input with step=1 |
| `number` | `{ "type": "number" }` | `42.5` | Number input |
| `string` | `{ "type": "string" }` | `"hello"` | Text input |
| `boolean` | `{ "type": "boolean" }` | `true`/`false` | Checkbox |
| `null` | `{ "type": "null" }` | `null` | N/A |

### Type Conversion Rules

| Input Type | Target Type | Conversion Behavior | Example |
| ---------------- | ----------- | -------------------------- | ------------------------ |
| String → Integer | `integer` | Reject if not whole number | "42" → 42, "1.5" → error |
| Number → Integer | `integer` | Reject if not whole number | 42 → 42, 1.5 → error |
| String → Number | `number` | Convert if valid number | "42.5" → 42.5 |
| String → Boolean | `boolean` | Only accept "true"/"false" | "true" → true |

## Complex Objects

Complex objects include nested structures (objects within objects) and arrays. Their handling differs between form and JSON modes based on complexity.

### Array Handling

| Type | Example Schema | Expected Input | Form Behavior |
| -------------- | --------------------------------------------------- | ---------------------------------------- | ------------------------------------------ |
| Simple Array | `{ "type": "array", "items": { "type": "string" }}` | `["item1", "item2"]` | Dynamic list with add/remove buttons |
| Object Array | `{ "type": "array", "items": { "type": "object" }}` | `[{"name": "item1"}, {"name": "item2"}]` | Dynamic form rows with nested fields |
| Optional Array | `{ "type": ["array", "null"] }` | `[]` or omitted | Empty array or field omitted, never `null` |

### Nested Objects

| Structure | Example Schema | Form Mode | JSON Mode |
| ------------ | ---------------------------------------------------------------- | --------------- | --------- |
| Single Level | `{"type": "object", "properties": {"name": {"type": "string"}}}` | Structured form | Available |
| Multi Level | `{"type": "object", "properties": {"user": {"type": "object"}}}` | JSON only | Available |
| Mixed Types | `{"type": "object", "properties": {"data": {"oneOf": [...]}}}` | JSON only | Available |

### Optional Fields in Complex Types

| Scenario | Example | Expected Behavior |
| ------------------------- | ------------------------------ | --------------------------------- |
| Optional field (any type) | `{"required": false}` | Omit field entirely from request |
| Nullable field | `{"type": ["string", "null"]}` | May explicitly set to `null` |
| Empty array (when set) | `{"type": "array"}` | Send `[]` if explicitly set empty |
Copy link
Member Author

@olaservo olaservo Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the above changes - want some feedback on if this should be the expected behavior for optional objects and arrays.

| Empty object (when set) | `{"type": "object"}` | Send `{}` if explicitly set empty |

## Testing Scenarios and Expected Results for Complex Objects

| Test Case | Description | Expected Behavior |
| -------------------------- | ------------------------------------------- | -------------------------------------------------------- |
| Complex nested form fields | Tool with multiple levels of nested objects | Only JSON mode is available |
| Array field manipulation | Add/remove items in array | Form mode allows for items to be added and removed |
| Optional nested fields | Object with optional nested properties | Field omitted entirely when not set |
| Mixed type arrays | Array accepting multiple types | Only JSON mode is available |
| Nullable vs Optional | Field allowing null vs optional field | Null explicitly set vs field omitted |
| Empty collections | Empty arrays or objects | Send `[]` or `{}` only if explicitly set, otherwise omit |

## Optional Parameters

**When an optional parameter is not set by the user:**

- The parameter will be completely omitted from the request
- Example: If `name` is optional and unset: `{}` will be sent in the request.

**When an optional parameter explicitly accepts null:**

- Schema must declare null as valid: `{ "type": ["string", "null"] }`
- Only then will null be sent: `{ "name": null }`

**When an optional string is set to empty:**

- Empty string will be sent, eg: `{ "name": "" }`
Loading