Skip to content
Open
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
10 changes: 5 additions & 5 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ const cloneToolParams = (
};

const filterReservedMetadata = (
metadata: Record<string, string>,
): Record<string, string> => {
return Object.entries(metadata).reduce<Record<string, string>>(
metadata: Record<string, unknown>,
): Record<string, unknown> => {
return Object.entries(metadata).reduce<Record<string, unknown>>(
(acc, [key, value]) => {
if (
!isReservedMetaKey(key) &&
Expand Down Expand Up @@ -265,7 +265,7 @@ const App = () => {
useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);

// Metadata state - persisted in localStorage
const [metadata, setMetadata] = useState<Record<string, string>>(() => {
const [metadata, setMetadata] = useState<Record<string, unknown>>(() => {
const savedMetadata = localStorage.getItem("lastMetadata");
if (savedMetadata) {
try {
Expand All @@ -284,7 +284,7 @@ const App = () => {
setAuthState((prev) => ({ ...prev, ...updates }));
};

const handleMetadataChange = (newMetadata: Record<string, string>) => {
const handleMetadataChange = (newMetadata: Record<string, unknown>) => {
const sanitizedMetadata = filterReservedMetadata(newMetadata);
setMetadata(sanitizedMetadata);
localStorage.setItem("lastMetadata", JSON.stringify(sanitizedMetadata));
Expand Down
254 changes: 122 additions & 132 deletions client/src/components/MetadataTab.tsx
Original file line number Diff line number Diff line change
@@ -1,167 +1,157 @@
import React, { useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { TabsContent } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Trash2, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import {
META_NAME_RULES_MESSAGE,
META_PREFIX_RULES_MESSAGE,
RESERVED_NAMESPACE_MESSAGE,
hasValidMetaName,
hasValidMetaPrefix,
isReservedMetaKey,
} from "@/utils/metaUtils";

interface MetadataEntry {
key: string;
value: string;
}
import JsonEditor from "@/components/JsonEditor";

interface MetadataTabProps {
metadata: Record<string, string>;
onMetadataChange: (metadata: Record<string, string>) => void;
metadata: Record<string, unknown>;
onMetadataChange: (metadata: Record<string, unknown>) => void;
}

const MetadataTab: React.FC<MetadataTabProps> = ({
metadata,
onMetadataChange,
}) => {
const [entries, setEntries] = useState<MetadataEntry[]>(() => {
return Object.entries(metadata).map(([key, value]) => ({ key, value }));
});
const stringifyCompact = (
value: Record<string, unknown> | null | undefined,
) => {
if (!value || Object.keys(value).length === 0) {
return "";
}

const addEntry = () => {
setEntries([...entries, { key: "", value: "" }]);
try {
return JSON.stringify(value);
} catch {
return "";
}
};

const removeEntry = (index: number) => {
const newEntries = entries.filter((_, i) => i !== index);
setEntries(newEntries);
updateMetadata(newEntries);
const stringifyPretty = (value: Record<string, unknown>) => {
try {
return JSON.stringify(value, null, 2);
} catch {
return "";
}
};

const updateEntry = (
index: number,
field: "key" | "value",
value: string,
) => {
const newEntries = [...entries];
newEntries[index][field] = value;
setEntries(newEntries);
updateMetadata(newEntries);
const initialCompact = stringifyCompact(metadata);
const [jsonValue, setJsonValue] = useState<string>(() => {
if (!initialCompact) {
return "";
}

return stringifyPretty(metadata);
});

const [jsonError, setJsonError] = useState<string | null>(null);
const lastMetadataStringRef = useRef<string>(initialCompact);

useEffect(() => {
const compact = stringifyCompact(metadata);

if (compact === lastMetadataStringRef.current) {
return;
}

lastMetadataStringRef.current = compact;

if (!compact) {
setJsonValue("");
setJsonError(null);
return;
}

if (metadata) {
const pretty = stringifyPretty(metadata);
setJsonValue(pretty);
setJsonError(null);
}
}, [metadata]);

const handleJsonChange = (value: string) => {
setJsonValue(value);

if (!value.trim()) {
onMetadataChange({});
lastMetadataStringRef.current = "";
setJsonError(null);
return;
}

try {
const parsed = JSON.parse(value);

if (
parsed === null ||
Array.isArray(parsed) ||
typeof parsed !== "object"
) {
setJsonError("Meta data must be a JSON object");
return;
}

onMetadataChange(parsed);
lastMetadataStringRef.current = JSON.stringify(parsed);
setJsonError(null);
} catch {
setJsonError("Invalid JSON format");
}
};

const updateMetadata = (newEntries: MetadataEntry[]) => {
const metadataObject: Record<string, string> = {};
newEntries.forEach(({ key, value }) => {
const trimmedKey = key.trim();
const handlePrettyClick = () => {
if (!jsonValue.trim()) {
return;
}

try {
const parsed = JSON.parse(jsonValue);

if (
trimmedKey &&
value.trim() &&
hasValidMetaPrefix(trimmedKey) &&
!isReservedMetaKey(trimmedKey) &&
hasValidMetaName(trimmedKey)
parsed === null ||
Array.isArray(parsed) ||
typeof parsed !== "object"
) {
metadataObject[trimmedKey] = value.trim();
setJsonError("Meta data must be a JSON object");
return;
}
});
onMetadataChange(metadataObject);

const pretty = stringifyPretty(parsed);
setJsonValue(pretty);
onMetadataChange(parsed);
lastMetadataStringRef.current = JSON.stringify(parsed);
setJsonError(null);
} catch {
setJsonError("Invalid JSON format");
}
};

return (
<TabsContent value="metadata">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between gap-2">
<div>
<h3 className="text-lg font-semibold">Metadata</h3>
<h3 className="text-lg font-semibold">Meta Data</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Key-value pairs that will be included in all MCP requests
Provide an object containing key-value pairs that will be included
in all MCP requests.
</p>
</div>
<Button onClick={addEntry} size="sm">
<Plus className="w-4 h-4 mr-2" />
Add Entry
<Button
type="button"
size="sm"
variant="outline"
onClick={handlePrettyClick}
className="flex-shrink-0"
>
Pretty
</Button>
</div>

<div className="space-y-3">
{entries.map((entry, index) => {
const trimmedKey = entry.key.trim();
const hasInvalidPrefix =
trimmedKey !== "" && !hasValidMetaPrefix(trimmedKey);
const isReservedKey =
trimmedKey !== "" && isReservedMetaKey(trimmedKey);
const hasInvalidName =
trimmedKey !== "" && !hasValidMetaName(trimmedKey);
const validationMessage = hasInvalidPrefix
? META_PREFIX_RULES_MESSAGE
: isReservedKey
? RESERVED_NAMESPACE_MESSAGE
: hasInvalidName
? META_NAME_RULES_MESSAGE
: null;
return (
<div key={index} className="space-y-1">
<div className="flex items-center space-x-2">
<div className="flex-1">
<Label htmlFor={`key-${index}`} className="sr-only">
Key
</Label>
<Input
id={`key-${index}`}
placeholder="Key"
value={entry.key}
onChange={(e) =>
updateEntry(index, "key", e.target.value)
}
aria-invalid={Boolean(validationMessage)}
className={cn(
validationMessage &&
"border-red-500 focus-visible:ring-red-500 focus-visible:ring-1",
)}
/>
</div>
<div className="flex-1">
<Label htmlFor={`value-${index}`} className="sr-only">
Value
</Label>
<Input
id={`value-${index}`}
placeholder="Value"
value={entry.value}
onChange={(e) =>
updateEntry(index, "value", e.target.value)
}
disabled={Boolean(validationMessage)}
/>
</div>
<Button
variant="outline"
size="sm"
onClick={() => removeEntry(index)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
{validationMessage && (
<p className="text-xs text-red-600 dark:text-red-400">
{validationMessage}
</p>
)}
</div>
);
})}
</div>

{entries.length === 0 && (
<div className="text-center py-8">
<p className="text-gray-500 dark:text-gray-400 mb-4">
No metadata entries. Click "Add Entry" to add key-value pairs.
</p>
</div>
)}
<JsonEditor
value={jsonValue}
onChange={handleJsonChange}
error={jsonError || undefined}
/>
</div>
</TabsContent>
);
Expand Down
Loading