diff --git a/README.md b/README.md index b312f71c..d5461e37 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/client/src/utils/__tests__/schemaUtils.test.ts b/client/src/utils/__tests__/schemaUtils.test.ts index 94e428ac..0674c7f5 100644 --- a/client/src/utils/__tests__/schemaUtils.test.ts +++ b/client/src/utils/__tests__/schemaUtils.test.ts @@ -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", () => { @@ -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", diff --git a/client/src/utils/jsonUtils.ts b/client/src/utils/jsonUtils.ts index 28cbf303..af155070 100644 --- a/client/src/utils/jsonUtils.ts +++ b/client/src/utils/jsonUtils.ts @@ -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; diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index 520b7908..92b53859 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -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 @@ -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": diff --git a/docs/tools-testing.md b/docs/tools-testing.md new file mode 100644 index 00000000..dfab633a --- /dev/null +++ b/docs/tools-testing.md @@ -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 | +| 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": "" }`