diff --git a/client/package.json b/client/package.json
index e3445da..c32dfa1 100644
--- a/client/package.json
+++ b/client/package.json
@@ -27,12 +27,15 @@
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1",
+ "@types/prismjs": "^1.26.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.447.0",
+ "prismjs": "^1.29.0",
"pkce-challenge": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-simple-code-editor": "^0.14.1",
"react-toastify": "^10.0.6",
"serve-handler": "^6.1.6",
"tailwind-merge": "^2.5.3",
diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx
new file mode 100644
index 0000000..ff26118
--- /dev/null
+++ b/client/src/components/DynamicJsonForm.tsx
@@ -0,0 +1,269 @@
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import JsonEditor from "./JsonEditor";
+
+export type JsonValue =
+ | string
+ | number
+ | boolean
+ | null
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+export type JsonSchemaType = {
+ type: "string" | "number" | "integer" | "boolean" | "array" | "object";
+ description?: string;
+ properties?: Record;
+ items?: JsonSchemaType;
+};
+
+type JsonObject = { [key: string]: JsonValue };
+
+interface DynamicJsonFormProps {
+ schema: JsonSchemaType;
+ value: JsonValue;
+ onChange: (value: JsonValue) => void;
+ maxDepth?: number;
+}
+
+const formatFieldLabel = (key: string): string => {
+ return key
+ .replace(/([A-Z])/g, " $1") // Insert space before capital letters
+ .replace(/_/g, " ") // Replace underscores with spaces
+ .replace(/^\w/, (c) => c.toUpperCase()); // Capitalize first letter
+};
+
+const DynamicJsonForm = ({
+ schema,
+ value,
+ onChange,
+ maxDepth = 3,
+}: DynamicJsonFormProps) => {
+ const [isJsonMode, setIsJsonMode] = useState(false);
+ const [jsonError, setJsonError] = useState();
+
+ const generateDefaultValue = (propSchema: JsonSchemaType): JsonValue => {
+ switch (propSchema.type) {
+ case "string":
+ return "";
+ case "number":
+ case "integer":
+ return 0;
+ case "boolean":
+ return false;
+ case "array":
+ return [];
+ case "object": {
+ const obj: JsonObject = {};
+ if (propSchema.properties) {
+ Object.entries(propSchema.properties).forEach(([key, prop]) => {
+ obj[key] = generateDefaultValue(prop);
+ });
+ }
+ return obj;
+ }
+ default:
+ return null;
+ }
+ };
+
+ const renderFormFields = (
+ propSchema: JsonSchemaType,
+ currentValue: JsonValue,
+ path: string[] = [],
+ depth: number = 0,
+ ) => {
+ if (
+ depth >= maxDepth &&
+ (propSchema.type === "object" || propSchema.type === "array")
+ ) {
+ // Render as JSON editor when max depth is reached
+ return (
+ {
+ try {
+ const parsed = JSON.parse(newValue);
+ handleFieldChange(path, parsed);
+ setJsonError(undefined);
+ } catch (err) {
+ setJsonError(err instanceof Error ? err.message : "Invalid JSON");
+ }
+ }}
+ error={jsonError}
+ />
+ );
+ }
+
+ switch (propSchema.type) {
+ case "string":
+ case "number":
+ case "integer":
+ return (
+
+ handleFieldChange(
+ path,
+ propSchema.type === "string"
+ ? e.target.value
+ : Number(e.target.value),
+ )
+ }
+ placeholder={propSchema.description}
+ />
+ );
+ case "boolean":
+ return (
+ handleFieldChange(path, e.target.checked)}
+ className="w-4 h-4"
+ />
+ );
+ case "object":
+ if (!propSchema.properties) return null;
+ return (
+
+ {Object.entries(propSchema.properties).map(([key, prop]) => (
+
+
+ {renderFormFields(
+ prop,
+ (currentValue as JsonObject)?.[key],
+ [...path, key],
+ depth + 1,
+ )}
+
+ ))}
+
+ );
+ case "array": {
+ const arrayValue = Array.isArray(currentValue) ? currentValue : [];
+ if (!propSchema.items) return null;
+ return (
+
+ {propSchema.description && (
+
{propSchema.description}
+ )}
+
+ {propSchema.items?.description && (
+
+ Items: {propSchema.items.description}
+
+ )}
+
+
+ {arrayValue.map((item, index) => (
+
+ {renderFormFields(
+ propSchema.items as JsonSchemaType,
+ item,
+ [...path, index.toString()],
+ depth + 1,
+ )}
+
+
+ ))}
+
+
+
+ );
+ }
+ default:
+ return null;
+ }
+ };
+
+ const handleFieldChange = (path: string[], fieldValue: JsonValue) => {
+ if (path.length === 0) {
+ onChange(fieldValue);
+ return;
+ }
+
+ const newValue = {
+ ...(typeof value === "object" && value !== null && !Array.isArray(value)
+ ? value
+ : {}),
+ } as JsonObject;
+ let current: JsonObject = newValue;
+
+ for (let i = 0; i < path.length - 1; i++) {
+ const key = path[i];
+ if (!(key in current)) {
+ current[key] = {};
+ }
+ current = current[key] as JsonObject;
+ }
+
+ current[path[path.length - 1]] = fieldValue;
+ onChange(newValue);
+ };
+
+ return (
+
+
+
+
+
+ {isJsonMode ? (
+
{
+ try {
+ onChange(JSON.parse(newValue));
+ setJsonError(undefined);
+ } catch (err) {
+ setJsonError(err instanceof Error ? err.message : "Invalid JSON");
+ }
+ }}
+ error={jsonError}
+ />
+ ) : (
+ renderFormFields(schema, value)
+ )}
+
+ );
+};
+
+export default DynamicJsonForm;
diff --git a/client/src/components/JsonEditor.tsx b/client/src/components/JsonEditor.tsx
new file mode 100644
index 0000000..2fb7e26
--- /dev/null
+++ b/client/src/components/JsonEditor.tsx
@@ -0,0 +1,59 @@
+import Editor from "react-simple-code-editor";
+import Prism from "prismjs";
+import "prismjs/components/prism-json";
+import "prismjs/themes/prism.css";
+import { Button } from "@/components/ui/button";
+
+interface JsonEditorProps {
+ value: string;
+ onChange: (value: string) => void;
+ error?: string;
+}
+
+const JsonEditor = ({ value, onChange, error }: JsonEditorProps) => {
+ const formatJson = (json: string): string => {
+ try {
+ return JSON.stringify(JSON.parse(json), null, 2);
+ } catch {
+ return json;
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Prism.highlight(code, Prism.languages.json, "json")
+ }
+ padding={10}
+ style={{
+ fontFamily: '"Fira code", "Fira Mono", monospace',
+ fontSize: 14,
+ backgroundColor: "transparent",
+ minHeight: "100px",
+ }}
+ className="w-full"
+ />
+
+ {error &&
{error}
}
+
+ );
+};
+
+export default JsonEditor;
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx
index a3a7ff2..41b2ef7 100644
--- a/client/src/components/ToolsTab.tsx
+++ b/client/src/components/ToolsTab.tsx
@@ -4,6 +4,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { TabsContent } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
+import DynamicJsonForm, { JsonSchemaType, JsonValue } from "./DynamicJsonForm";
import {
ListToolsResult,
Tool,
@@ -15,6 +16,12 @@ import ListPane from "./ListPane";
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
+type SchemaProperty = {
+ type: string;
+ description?: string;
+ properties?: Record;
+};
+
const ToolsTab = ({
tools,
listTools,
@@ -159,22 +166,21 @@ const ToolsTab = ({
{selectedTool.description}
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
- ([key, value]) => (
-
-
- {
- /* @ts-expect-error value type is currently unknown */
- value.type === "string" ? (
+ ([key, value]) => {
+ const prop = value as SchemaProperty;
+ return (
+
+
+ {prop.type === "string" ? (
- ),
+ )}
+
+ );
+ },
)}