inspector/clients/tui/src/utils/schemaToForm.ts
Bob Dickinson 2488ca5e38
feat: port CLI, TUI, and launcher from v1.5 into v2 (#1452)
Ports the CLI, TUI, and launcher from v1.5 into v2 behind a single
`mcp-inspector` entrypoint (launcher → web/cli/tui). Includes the
`mcp-inspector --web --dev` launcher fix (Vite alias resolution + prod-server
double-start), the root `postinstall` install cascade with simplified CI, and
spec/AGENTS docs (no-workspaces rationale, Development setup).

Closes #1246.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:27:49 -04:00

137 lines
3.5 KiB
TypeScript

/**
* Converts JSON Schema to ink-form format
*/
import type { FormStructure, FormSection, FormField } from "ink-form";
/** Minimal JSON Schema property shape used when building tool parameter forms */
interface JsonSchemaProperty {
type?: string;
title?: string;
enum?: unknown[];
items?: { enum?: unknown[] };
minimum?: number;
maximum?: number;
default?: unknown;
}
/** Minimal JSON Schema object shape (properties + required) */
interface JsonSchemaObject {
properties?: Record<string, JsonSchemaProperty>;
required?: string[];
}
/**
* Converts a JSON Schema to ink-form structure
*/
export function schemaToForm(
schema: JsonSchemaObject | null | undefined,
toolName: string,
): FormStructure {
const fields: FormField[] = [];
if (!schema || !schema.properties) {
return {
title: `Test Tool: ${toolName}`,
sections: [{ title: "Parameters", fields: [] }],
};
}
const properties = schema.properties || {};
const required = schema.required || [];
for (const [key, prop] of Object.entries(properties)) {
const property = prop as JsonSchemaProperty;
const baseField = {
name: key,
label: property.title || key,
required: required.includes(key),
};
let field: FormField;
// Handle enum -> select
if (property.enum) {
if (property.type === "array" && property.items?.enum) {
// For array of enums, we'll use select but handle it differently
// Note: ink-form doesn't have multiselect, so we'll use select
field = {
type: "select",
...baseField,
options: property.items.enum.map((val: unknown) => ({
label: String(val),
value: String(val),
})),
} as FormField;
} else {
// Single select
field = {
type: "select",
...baseField,
options: property.enum.map((val: unknown) => ({
label: String(val),
value: String(val),
})),
} as FormField;
}
} else {
// Map JSON Schema types to ink-form types
switch (property.type) {
case "string":
field = {
type: "string",
...baseField,
} as FormField;
break;
case "integer":
field = {
type: "integer",
...baseField,
...(property.minimum !== undefined && { min: property.minimum }),
...(property.maximum !== undefined && { max: property.maximum }),
} as FormField;
break;
case "number":
field = {
type: "float",
...baseField,
...(property.minimum !== undefined && { min: property.minimum }),
...(property.maximum !== undefined && { max: property.maximum }),
} as FormField;
break;
case "boolean":
field = {
type: "boolean",
...baseField,
} as FormField;
break;
default:
// Default to string for unknown types
field = {
type: "string",
...baseField,
} as FormField;
}
}
// Set initial value from default (ink-form FormField allows initialValue for some types)
if (property.default !== undefined) {
(field as FormField & { initialValue?: unknown }).initialValue =
property.default;
}
fields.push(field);
}
const sections: FormSection[] = [
{
title: "Parameters",
fields,
},
];
return {
title: `Test Tool: ${toolName}`,
sections,
};
}