Skip to content

Commit b555e41

Browse files
committed
Address the schema architecture crisis
1 parent 2eec036 commit b555e41

File tree

4 files changed

+114
-40
lines changed

4 files changed

+114
-40
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ const DynamicJsonForm = ({
113113
currentValue: JsonValue,
114114
path: string[] = [],
115115
depth: number = 0,
116+
parentSchema?: JsonSchemaType,
117+
propertyName?: string,
116118
) => {
117119
if (
118120
depth >= maxDepth &&
@@ -122,7 +124,7 @@ const DynamicJsonForm = ({
122124
return (
123125
<JsonEditor
124126
value={JSON.stringify(
125-
currentValue ?? generateDefaultValue(propSchema),
127+
currentValue ?? generateDefaultValue(propSchema, propertyName, parentSchema),
126128
null,
127129
2,
128130
)}
@@ -140,6 +142,9 @@ const DynamicJsonForm = ({
140142
);
141143
}
142144

145+
// Check if this property is required in the parent schema
146+
const isRequired = parentSchema?.required?.includes(propertyName || "") ?? false;
147+
143148
switch (propSchema.type) {
144149
case "string":
145150
return (
@@ -148,16 +153,11 @@ const DynamicJsonForm = ({
148153
value={(currentValue as string) ?? ""}
149154
onChange={(e) => {
150155
const val = e.target.value;
151-
// Allow clearing non-required fields by setting undefined
152-
// This preserves the distinction between empty string and unset
153-
if (!val && !propSchema.required) {
154-
handleFieldChange(path, undefined);
155-
} else {
156-
handleFieldChange(path, val);
157-
}
156+
// Always allow setting string values, including empty strings
157+
handleFieldChange(path, val);
158158
}}
159159
placeholder={propSchema.description}
160-
required={propSchema.required}
160+
required={isRequired}
161161
/>
162162
);
163163
case "number":
@@ -167,9 +167,7 @@ const DynamicJsonForm = ({
167167
value={(currentValue as number)?.toString() ?? ""}
168168
onChange={(e) => {
169169
const val = e.target.value;
170-
// Allow clearing non-required number fields
171-
// This preserves the distinction between 0 and unset
172-
if (!val && !propSchema.required) {
170+
if (!val && !isRequired) {
173171
handleFieldChange(path, undefined);
174172
} else {
175173
const num = Number(val);
@@ -179,7 +177,7 @@ const DynamicJsonForm = ({
179177
}
180178
}}
181179
placeholder={propSchema.description}
182-
required={propSchema.required}
180+
required={isRequired}
183181
/>
184182
);
185183
case "integer":
@@ -190,9 +188,7 @@ const DynamicJsonForm = ({
190188
value={(currentValue as number)?.toString() ?? ""}
191189
onChange={(e) => {
192190
const val = e.target.value;
193-
// Allow clearing non-required integer fields
194-
// This preserves the distinction between 0 and unset
195-
if (!val && !propSchema.required) {
191+
if (!val && !isRequired) {
196192
handleFieldChange(path, undefined);
197193
} else {
198194
const num = Number(val);
@@ -203,7 +199,7 @@ const DynamicJsonForm = ({
203199
}
204200
}}
205201
placeholder={propSchema.description}
206-
required={propSchema.required}
202+
required={isRequired}
207203
/>
208204
);
209205
case "boolean":
@@ -213,7 +209,64 @@ const DynamicJsonForm = ({
213209
checked={(currentValue as boolean) ?? false}
214210
onChange={(e) => handleFieldChange(path, e.target.checked)}
215211
className="w-4 h-4"
216-
required={propSchema.required}
212+
required={isRequired}
213+
/>
214+
);
215+
case "object":
216+
if (!propSchema.properties) {
217+
return (
218+
<JsonEditor
219+
value={JSON.stringify(currentValue ?? {}, null, 2)}
220+
onChange={(newValue) => {
221+
try {
222+
const parsed = JSON.parse(newValue);
223+
handleFieldChange(path, parsed);
224+
setJsonError(undefined);
225+
} catch (err) {
226+
setJsonError(err instanceof Error ? err.message : "Invalid JSON");
227+
}
228+
}}
229+
error={jsonError}
230+
/>
231+
);
232+
}
233+
234+
return (
235+
<div className="space-y-2 border rounded p-3">
236+
{Object.entries(propSchema.properties).map(([key, subSchema]) => (
237+
<div key={key}>
238+
<label className="block text-sm font-medium mb-1">
239+
{key}
240+
{propSchema.required?.includes(key) && (
241+
<span className="text-red-500 ml-1">*</span>
242+
)}
243+
</label>
244+
{renderFormFields(
245+
subSchema as JsonSchemaType,
246+
(currentValue as Record<string, JsonValue>)?.[key],
247+
[...path, key],
248+
depth + 1,
249+
propSchema,
250+
key,
251+
)}
252+
</div>
253+
))}
254+
</div>
255+
);
256+
case "array":
257+
return (
258+
<JsonEditor
259+
value={JSON.stringify(currentValue ?? [], null, 2)}
260+
onChange={(newValue) => {
261+
try {
262+
const parsed = JSON.parse(newValue);
263+
handleFieldChange(path, parsed);
264+
setJsonError(undefined);
265+
} catch (err) {
266+
setJsonError(err instanceof Error ? err.message : "Invalid JSON");
267+
}
268+
}}
269+
error={jsonError}
217270
/>
218271
);
219272
default:

client/src/components/ToolsTab.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { TabsContent } from "@/components/ui/tabs";
77
import { Textarea } from "@/components/ui/textarea";
88
import DynamicJsonForm from "./DynamicJsonForm";
99
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
10-
import { generateDefaultValue } from "@/utils/schemaUtils";
10+
import { generateDefaultValue, isPropertyRequired } from "@/utils/schemaUtils";
1111
import {
1212
CompatibilityCallToolResult,
1313
ListToolsResult,
@@ -48,7 +48,7 @@ const ToolsTab = ({
4848
selectedTool?.inputSchema.properties ?? [],
4949
).map(([key, value]) => [
5050
key,
51-
generateDefaultValue(value as JsonSchemaType),
51+
generateDefaultValue(value as JsonSchemaType, key, selectedTool?.inputSchema as JsonSchemaType),
5252
]);
5353
setParams(Object.fromEntries(params));
5454
}, [selectedTool]);
@@ -92,13 +92,15 @@ const ToolsTab = ({
9292
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
9393
([key, value]) => {
9494
const prop = value as JsonSchemaType;
95+
const inputSchema = selectedTool.inputSchema as JsonSchemaType;
96+
const required = isPropertyRequired(key, inputSchema);
9597
return (
9698
<div key={key}>
9799
<Label
98100
htmlFor={key}
99101
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
100102
>
101-
{key}
103+
{key}{required && <span className="text-red-500 ml-1">*</span>}
102104
</Label>
103105
{prop.type === "boolean" ? (
104106
<div className="flex items-center space-x-2 mt-2">

client/src/utils/jsonUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type JsonSchemaType = {
1717
| "object"
1818
| "null";
1919
description?: string;
20-
required?: boolean;
20+
required?: string[];
2121
default?: JsonValue;
2222
properties?: Record<string, JsonSchemaType>;
2323
items?: JsonSchemaType;

client/src/utils/schemaUtils.ts

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,46 +81,65 @@ export function hasOutputSchema(toolName: string): boolean {
8181
/**
8282
* Generates a default value based on a JSON schema type
8383
* @param schema The JSON schema definition
84-
* @returns A default value matching the schema type, or null for non-required fields
84+
* @param propertyName Optional property name for checking if it's required in parent schema
85+
* @param parentSchema Optional parent schema to check required array
86+
* @returns A default value matching the schema type
8587
*/
86-
export function generateDefaultValue(schema: JsonSchemaType): JsonValue {
87-
if ("default" in schema) {
88+
export function generateDefaultValue(
89+
schema: JsonSchemaType,
90+
propertyName?: string,
91+
parentSchema?: JsonSchemaType
92+
): JsonValue {
93+
if ("default" in schema && schema.default !== undefined) {
8894
return schema.default;
8995
}
9096

91-
if (!schema.required) {
92-
if (schema.type === "array") return [];
93-
if (schema.type === "object") return {};
94-
return undefined;
95-
}
97+
// Check if this property is required in the parent schema
98+
const isRequired = parentSchema?.required?.includes(propertyName || "") ?? false;
9699

97100
switch (schema.type) {
98101
case "string":
99-
return "";
102+
return isRequired ? "" : undefined;
100103
case "number":
101104
case "integer":
102-
return 0;
105+
return isRequired ? 0 : undefined;
103106
case "boolean":
104-
return false;
107+
return isRequired ? false : undefined;
105108
case "array":
106109
return [];
107110
case "object": {
108111
if (!schema.properties) return {};
109112

110113
const obj: JsonObject = {};
111-
Object.entries(schema.properties)
112-
.filter(([, prop]) => prop.required)
113-
.forEach(([key, prop]) => {
114-
const value = generateDefaultValue(prop);
115-
obj[key] = value;
116-
});
114+
// Only include properties that are required according to the schema's required array
115+
Object.entries(schema.properties).forEach(([key, prop]) => {
116+
const isPropertyRequired = schema.required?.includes(key) ?? false;
117+
if (isPropertyRequired) {
118+
const value = generateDefaultValue(prop, key, schema);
119+
if (value !== undefined) {
120+
obj[key] = value;
121+
}
122+
}
123+
});
117124
return obj;
118125
}
119-
default:
126+
case "null":
120127
return null;
128+
default:
129+
return undefined;
121130
}
122131
}
123132

133+
/**
134+
* Helper function to check if a property is required in a schema
135+
* @param propertyName The name of the property to check
136+
* @param schema The parent schema containing the required array
137+
* @returns true if the property is required, false otherwise
138+
*/
139+
export function isPropertyRequired(propertyName: string, schema: JsonSchemaType): boolean {
140+
return schema.required?.includes(propertyName) ?? false;
141+
}
142+
124143
/**
125144
* Formats a field key into a human-readable label
126145
* @param key The field key to format

0 commit comments

Comments
 (0)