Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"devDependencies": {},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.5",
"@modelcontextprotocol/sdk": "^1.13.0",
"commander": "^13.1.0",
"spawn-rx": "^5.1.2"
}
Expand Down
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"test:watch": "jest --config jest.config.cjs --watch"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.5",
"@modelcontextprotocol/sdk": "^1.13.0",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-icons": "^1.3.0",
Expand Down
24 changes: 24 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ import { loadOAuthTokens, handleOAuthDebugConnect } from "./services/oauth";
import { getMCPProxyAddress } from "./utils/configUtils";
import { handleRootsChange, MCPHelperDependencies } from "./utils/mcpHelpers";

import ElicitationModal, {
ElicitationRequest,
} from "./components/ElicitationModal";

// Types
import {
MCPJamServerConfig,
Expand Down Expand Up @@ -136,6 +140,18 @@ const App = () => {
[mcpOperations],
);

const onElicitationRequest = useCallback(
(request: ElicitationRequest, resolve: (result: any) => void) => {
mcpOperations.setPendingElicitationRequest({
id: nextRequestId.current++,
message: request.params.message,
requestedSchema: request.params.requestedSchema,
resolve,
});
},
[mcpOperations],
);

const getRootsCallback = useCallback(() => rootsRef.current, []);

// Connection info
Expand Down Expand Up @@ -196,6 +212,7 @@ const App = () => {
onStdErrNotification,
onPendingRequest,
getRootsCallback,
onElicitationRequest,
);
if (options.autoConnect) {
console.log("🔌 Auto-connecting to all servers...");
Expand Down Expand Up @@ -236,6 +253,7 @@ const App = () => {
configState,
onStdErrNotification,
onPendingRequest,
onElicitationRequest,
getRootsCallback,
addClientLog,
],
Expand Down Expand Up @@ -518,6 +536,7 @@ const App = () => {
onStdErrNotification,
onPendingRequest,
getRootsCallback,
onElicitationRequest
);
} catch (error) {
addClientLog(
Expand All @@ -538,6 +557,7 @@ const App = () => {
configState.claudeApiKey,
onStdErrNotification,
onPendingRequest,
onElicitationRequest,
getRootsCallback,
addClientLog,
]);
Expand Down Expand Up @@ -1071,6 +1091,10 @@ const App = () => {
onClearLogs={mcpOperations.clearClientLogs}
/>
</div>
<ElicitationModal
request={mcpOperations.pendingElicitationRequest}
onClose={mcpOperations.handleCloseElicitationModal}
/>
</div>

{/* GitHub Star Modal */}
Expand Down
163 changes: 140 additions & 23 deletions client/src/components/DynamicJsonForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,36 +138,144 @@ const DynamicJsonForm = ({
);
}

const isFieldRequired = (fieldPath: string[]): boolean => {
if (typeof schema.required === "boolean") {
return schema.required;
}
if (Array.isArray(schema.required) && fieldPath.length > 0) {
return schema.required.includes(fieldPath[fieldPath.length - 1]);
}
return false;
};

if (propSchema.type === "object" && propSchema.properties) {
const objectValue = (currentValue as Record<string, JsonValue>) || {};

return (
<div className="space-y-4">
{Object.entries(propSchema.properties).map(
([fieldName, fieldSchema]) => {
const fieldPath = [...path, fieldName];
const fieldValue = objectValue[fieldName];
const fieldRequired = isFieldRequired([fieldName]);

return (
<div key={fieldName} className="space-y-2">
<label
htmlFor={fieldName}
className="block text-sm font-medium"
>
{fieldSchema.title || fieldName}
{fieldRequired && (
<span className="text-red-500 ml-1">*</span>
)}
</label>
{fieldSchema.description && (
<p className="text-xs text-gray-500">
{fieldSchema.description}
</p>
)}
<div>
{renderFieldInput(
fieldSchema,
fieldValue,
fieldPath,
fieldRequired,
)}
</div>
</div>
);
},
)}
</div>
);
}

const fieldRequired = isFieldRequired(path);
return renderFieldInput(propSchema, currentValue, path, fieldRequired);
};

const renderFieldInput = (
propSchema: JsonSchemaType,
currentValue: JsonValue,
path: string[],
fieldRequired: boolean,
) => {
switch (propSchema.type) {
case "string":
case "string": {
if (propSchema.enum) {
return (
<select
value={(currentValue as string) ?? ""}
onChange={(e) => {
const val = e.target.value;
if (!val && !fieldRequired) {
handleFieldChange(path, undefined);
} else {
handleFieldChange(path, val);
}
}}
required={fieldRequired}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800"
>
<option value="">Select an option...</option>
{propSchema.enum.map((option, index) => (
<option key={option} value={option}>
{propSchema.enumNames?.[index] || option}
</option>
))}
</select>
);
}

let inputType = "text";
switch (propSchema.format) {
case "email":
inputType = "email";
break;
case "uri":
inputType = "url";
break;
case "date":
inputType = "date";
break;
case "date-time":
inputType = "datetime-local";
break;
default:
inputType = "text";
break;
}

return (
<Input
type="text"
type={inputType}
value={(currentValue as string) ?? ""}
onChange={(e) => {
const val = e.target.value;
// Allow clearing non-required fields by setting undefined
// This preserves the distinction between empty string and unset
if (!val && !propSchema.required) {
if (!val && !fieldRequired) {
handleFieldChange(path, undefined);
} else {
handleFieldChange(path, val);
}
}}
placeholder={propSchema.description}
required={propSchema.required}
required={fieldRequired}
minLength={propSchema.minLength}
maxLength={propSchema.maxLength}
pattern={propSchema.pattern}
/>
);
}

case "number":
return (
<Input
type="number"
value={(currentValue as number)?.toString() ?? ""}
onChange={(e) => {
const val = e.target.value;
// Allow clearing non-required number fields
// This preserves the distinction between 0 and unset
if (!val && !propSchema.required) {
if (!val && !fieldRequired) {
handleFieldChange(path, undefined);
} else {
const num = Number(val);
Expand All @@ -177,9 +285,12 @@ const DynamicJsonForm = ({
}
}}
placeholder={propSchema.description}
required={propSchema.required}
required={fieldRequired}
min={propSchema.minimum}
max={propSchema.maximum}
/>
);

case "integer":
return (
<Input
Expand All @@ -188,32 +299,38 @@ const DynamicJsonForm = ({
value={(currentValue as number)?.toString() ?? ""}
onChange={(e) => {
const val = e.target.value;
// Allow clearing non-required integer fields
// This preserves the distinction between 0 and unset
if (!val && !propSchema.required) {
if (!val && !fieldRequired) {
handleFieldChange(path, undefined);
} else {
const num = Number(val);
// Only update if it's a valid integer
if (!isNaN(num) && Number.isInteger(num)) {
handleFieldChange(path, num);
}
}
}}
placeholder={propSchema.description}
required={propSchema.required}
required={fieldRequired}
min={propSchema.minimum}
max={propSchema.maximum}
/>
);

case "boolean":
return (
<Input
type="checkbox"
checked={(currentValue as boolean) ?? false}
onChange={(e) => handleFieldChange(path, e.target.checked)}
className="w-4 h-4"
required={propSchema.required}
/>
<div className="flex items-center space-x-2">
<Input
type="checkbox"
checked={(currentValue as boolean) ?? false}
onChange={(e) => handleFieldChange(path, e.target.checked)}
className="w-4 h-4"
required={fieldRequired}
/>
<span className="text-sm">
{propSchema.description || "Enable this option"}
</span>
</div>
);

default:
return null;
}
Expand Down Expand Up @@ -303,4 +420,4 @@ const DynamicJsonForm = ({
);
};

export default DynamicJsonForm;
export default DynamicJsonForm;
Loading
Loading