Skip to content

Commit a45247a

Browse files
committed
add validations against spec, add reserved metadata key validations
1 parent 3044d8f commit a45247a

File tree

6 files changed

+489
-108
lines changed

6 files changed

+489
-108
lines changed

client/src/App.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import {
1919
} from "@modelcontextprotocol/sdk/types.js";
2020
import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
2121
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
22+
import {
23+
hasValidMetaName,
24+
hasValidMetaPrefix,
25+
isReservedMetaKey,
26+
} from "@/utils/metaUtils";
2227
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
2328
import { OAuthStateMachine } from "./lib/oauth-state-machine";
2429
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
@@ -85,6 +90,24 @@ import MetadataTab from "./components/MetadataTab";
8590

8691
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
8792

93+
const filterReservedMetadata = (
94+
metadata: Record<string, string>,
95+
): Record<string, string> => {
96+
return Object.entries(metadata).reduce<Record<string, string>>(
97+
(acc, [key, value]) => {
98+
if (
99+
!isReservedMetaKey(key) &&
100+
hasValidMetaPrefix(key) &&
101+
hasValidMetaName(key)
102+
) {
103+
acc[key] = value;
104+
}
105+
return acc;
106+
},
107+
{},
108+
);
109+
};
110+
88111
const App = () => {
89112
const [resources, setResources] = useState<Resource[]>([]);
90113
const [resourceTemplates, setResourceTemplates] = useState<
@@ -206,7 +229,10 @@ const App = () => {
206229
const savedMetadata = localStorage.getItem("lastMetadata");
207230
if (savedMetadata) {
208231
try {
209-
return JSON.parse(savedMetadata);
232+
const parsed = JSON.parse(savedMetadata);
233+
if (parsed && typeof parsed === "object") {
234+
return filterReservedMetadata(parsed);
235+
}
210236
} catch (error) {
211237
console.warn("Failed to parse saved metadata:", error);
212238
}
@@ -219,8 +245,9 @@ const App = () => {
219245
};
220246

221247
const handleMetadataChange = (newMetadata: Record<string, string>) => {
222-
setMetadata(newMetadata);
223-
localStorage.setItem("lastMetadata", JSON.stringify(newMetadata));
248+
const sanitizedMetadata = filterReservedMetadata(newMetadata);
249+
setMetadata(sanitizedMetadata);
250+
localStorage.setItem("lastMetadata", JSON.stringify(sanitizedMetadata));
224251
};
225252
const nextRequestId = useRef(0);
226253
const rootsRef = useRef<Root[]>([]);
@@ -824,7 +851,7 @@ const App = () => {
824851
const mergedMetadata = {
825852
...metadata, // General metadata
826853
progressToken: progressTokenRef.current++,
827-
...(toolMetadata ?? {}), // Tool-specific metadata
854+
...toolMetadata, // Tool-specific metadata
828855
};
829856

830857
const response = await sendMCPRequest(

client/src/components/MetadataTab.tsx

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import { Button } from "@/components/ui/button";
44
import { Input } from "@/components/ui/input";
55
import { Label } from "@/components/ui/label";
66
import { Trash2, Plus } from "lucide-react";
7+
import { cn } from "@/lib/utils";
8+
import {
9+
META_NAME_RULES_MESSAGE,
10+
META_PREFIX_RULES_MESSAGE,
11+
RESERVED_NAMESPACE_MESSAGE,
12+
hasValidMetaName,
13+
hasValidMetaPrefix,
14+
isReservedMetaKey,
15+
} from "@/utils/metaUtils";
716

817
interface MetadataEntry {
918
key: string;
@@ -47,8 +56,15 @@ const MetadataTab: React.FC<MetadataTabProps> = ({
4756
const updateMetadata = (newEntries: MetadataEntry[]) => {
4857
const metadataObject: Record<string, string> = {};
4958
newEntries.forEach(({ key, value }) => {
50-
if (key.trim() && value.trim()) {
51-
metadataObject[key.trim()] = value.trim();
59+
const trimmedKey = key.trim();
60+
if (
61+
trimmedKey &&
62+
value.trim() &&
63+
hasValidMetaPrefix(trimmedKey) &&
64+
!isReservedMetaKey(trimmedKey) &&
65+
hasValidMetaName(trimmedKey)
66+
) {
67+
metadataObject[trimmedKey] = value.trim();
5268
}
5369
});
5470
onMetadataChange(metadataObject);
@@ -71,39 +87,72 @@ const MetadataTab: React.FC<MetadataTabProps> = ({
7187
</div>
7288

7389
<div className="space-y-3">
74-
{entries.map((entry, index) => (
75-
<div key={index} className="flex items-center space-x-2">
76-
<div className="flex-1">
77-
<Label htmlFor={`key-${index}`} className="sr-only">
78-
Key
79-
</Label>
80-
<Input
81-
id={`key-${index}`}
82-
placeholder="Key"
83-
value={entry.key}
84-
onChange={(e) => updateEntry(index, "key", e.target.value)}
85-
/>
90+
{entries.map((entry, index) => {
91+
const trimmedKey = entry.key.trim();
92+
const hasInvalidPrefix =
93+
trimmedKey !== "" && !hasValidMetaPrefix(trimmedKey);
94+
const isReservedKey =
95+
trimmedKey !== "" && isReservedMetaKey(trimmedKey);
96+
const hasInvalidName =
97+
trimmedKey !== "" && !hasValidMetaName(trimmedKey);
98+
const validationMessage = hasInvalidPrefix
99+
? META_PREFIX_RULES_MESSAGE
100+
: isReservedKey
101+
? RESERVED_NAMESPACE_MESSAGE
102+
: hasInvalidName
103+
? META_NAME_RULES_MESSAGE
104+
: null;
105+
return (
106+
<div key={index} className="space-y-1">
107+
<div className="flex items-center space-x-2">
108+
<div className="flex-1">
109+
<Label htmlFor={`key-${index}`} className="sr-only">
110+
Key
111+
</Label>
112+
<Input
113+
id={`key-${index}`}
114+
placeholder="Key"
115+
value={entry.key}
116+
onChange={(e) =>
117+
updateEntry(index, "key", e.target.value)
118+
}
119+
aria-invalid={Boolean(validationMessage)}
120+
className={cn(
121+
validationMessage &&
122+
"border-red-500 focus-visible:ring-red-500 focus-visible:ring-1",
123+
)}
124+
/>
125+
</div>
126+
<div className="flex-1">
127+
<Label htmlFor={`value-${index}`} className="sr-only">
128+
Value
129+
</Label>
130+
<Input
131+
id={`value-${index}`}
132+
placeholder="Value"
133+
value={entry.value}
134+
onChange={(e) =>
135+
updateEntry(index, "value", e.target.value)
136+
}
137+
disabled={Boolean(validationMessage)}
138+
/>
139+
</div>
140+
<Button
141+
variant="outline"
142+
size="sm"
143+
onClick={() => removeEntry(index)}
144+
>
145+
<Trash2 className="w-4 h-4" />
146+
</Button>
147+
</div>
148+
{validationMessage && (
149+
<p className="text-xs text-red-600 dark:text-red-400">
150+
{validationMessage}
151+
</p>
152+
)}
86153
</div>
87-
<div className="flex-1">
88-
<Label htmlFor={`value-${index}`} className="sr-only">
89-
Value
90-
</Label>
91-
<Input
92-
id={`value-${index}`}
93-
placeholder="Value"
94-
value={entry.value}
95-
onChange={(e) => updateEntry(index, "value", e.target.value)}
96-
/>
97-
</div>
98-
<Button
99-
variant="outline"
100-
size="sm"
101-
onClick={() => removeEntry(index)}
102-
>
103-
<Trash2 className="w-4 h-4" />
104-
</Button>
105-
</div>
106-
))}
154+
);
155+
})}
107156
</div>
108157

109158
{entries.length === 0 && (

0 commit comments

Comments
 (0)