Skip to content

Commit 1eba99c

Browse files
Merge branch 'main' into justin/sdk-auth
2 parents 88984c7 + 13ae2b5 commit 1eba99c

14 files changed

+1321
-84
lines changed

CLAUDE.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# MCP Inspector Development Guide
2+
3+
## Build Commands
4+
5+
- Build all: `npm run build`
6+
- Build client: `npm run build-client`
7+
- Build server: `npm run build-server`
8+
- Development mode: `npm run dev` (use `npm run dev:windows` on Windows)
9+
- Format code: `npm run prettier-fix`
10+
- Client lint: `cd client && npm run lint`
11+
12+
## Code Style Guidelines
13+
14+
- Use TypeScript with proper type annotations
15+
- Follow React functional component patterns with hooks
16+
- Use ES modules (import/export) not CommonJS
17+
- Use Prettier for formatting (auto-formatted on commit)
18+
- Follow existing naming conventions:
19+
- camelCase for variables and functions
20+
- PascalCase for component names and types
21+
- kebab-case for file names
22+
- Use async/await for asynchronous operations
23+
- Implement proper error handling with try/catch blocks
24+
- Use Tailwind CSS for styling in the client
25+
- Keep components small and focused on a single responsibility
26+
27+
## Project Organization
28+
29+
The project is organized as a monorepo with workspaces:
30+
31+
- `client/`: React frontend with Vite, TypeScript and Tailwind
32+
- `server/`: Express backend with TypeScript
33+
- `bin/`: CLI scripts

client/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222
},
2323
"dependencies": {
2424
"@modelcontextprotocol/sdk": "^1.4.1",
25+
"@radix-ui/react-dialog": "^1.1.3",
2526
"@radix-ui/react-checkbox": "^1.1.4",
2627
"@radix-ui/react-icons": "^1.3.0",
2728
"@radix-ui/react-label": "^2.1.0",
29+
"@radix-ui/react-popover": "^1.1.3",
2830
"@radix-ui/react-select": "^2.1.2",
2931
"@radix-ui/react-slot": "^1.1.0",
3032
"@radix-ui/react-tabs": "^1.1.1",
3133
"@types/prismjs": "^1.26.5",
3234
"class-variance-authority": "^0.7.0",
3335
"clsx": "^2.1.1",
36+
"cmdk": "^1.0.4",
3437
"lucide-react": "^0.447.0",
3538
"prismjs": "^1.29.0",
3639
"pkce-challenge": "^4.1.0",

client/src/App.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ const App = () => {
151151
requestHistory,
152152
makeRequest: makeConnectionRequest,
153153
sendNotification,
154+
handleCompletion,
155+
completionsSupported,
154156
connect: connectMcpServer,
155157
} = useConnection({
156158
transportType,
@@ -177,29 +179,6 @@ const App = () => {
177179
getRoots: () => rootsRef.current,
178180
});
179181

180-
const makeRequest = async <T extends z.ZodType>(
181-
request: ClientRequest,
182-
schema: T,
183-
tabKey?: keyof typeof errors,
184-
) => {
185-
try {
186-
const response = await makeConnectionRequest(request, schema);
187-
if (tabKey !== undefined) {
188-
clearError(tabKey);
189-
}
190-
return response;
191-
} catch (e) {
192-
const errorString = (e as Error).message ?? String(e);
193-
if (tabKey !== undefined) {
194-
setErrors((prev) => ({
195-
...prev,
196-
[tabKey]: errorString,
197-
}));
198-
}
199-
throw e;
200-
}
201-
};
202-
203182
useEffect(() => {
204183
localStorage.setItem("lastCommand", command);
205184
}, [command]);
@@ -264,6 +243,29 @@ const App = () => {
264243
setErrors((prev) => ({ ...prev, [tabKey]: null }));
265244
};
266245

246+
const makeRequest = async <T extends z.ZodType>(
247+
request: ClientRequest,
248+
schema: T,
249+
tabKey?: keyof typeof errors,
250+
) => {
251+
try {
252+
const response = await makeConnectionRequest(request, schema);
253+
if (tabKey !== undefined) {
254+
clearError(tabKey);
255+
}
256+
return response;
257+
} catch (e) {
258+
const errorString = (e as Error).message ?? String(e);
259+
if (tabKey !== undefined) {
260+
setErrors((prev) => ({
261+
...prev,
262+
[tabKey]: errorString,
263+
}));
264+
}
265+
throw e;
266+
}
267+
};
268+
267269
const listResources = async () => {
268270
const response = await makeRequest(
269271
{
@@ -483,6 +485,8 @@ const App = () => {
483485
clearError("resources");
484486
setSelectedResource(resource);
485487
}}
488+
handleCompletion={handleCompletion}
489+
completionsSupported={completionsSupported}
486490
resourceContent={resourceContent}
487491
nextCursor={nextResourceCursor}
488492
nextTemplateCursor={nextResourceTemplateCursor}
@@ -507,6 +511,8 @@ const App = () => {
507511
clearError("prompts");
508512
setSelectedPrompt(prompt);
509513
}}
514+
handleCompletion={handleCompletion}
515+
completionsSupported={completionsSupported}
510516
promptContent={promptContent}
511517
nextCursor={nextPromptCursor}
512518
error={errors.prompts}

client/src/components/DynamicJsonForm.tsx

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -215,23 +215,112 @@ const DynamicJsonForm = ({
215215
return;
216216
}
217217

218-
const newValue = {
219-
...(typeof value === "object" && value !== null && !Array.isArray(value)
220-
? value
221-
: {}),
222-
} as JsonObject;
223-
let current: JsonObject = newValue;
218+
const updateArray = (
219+
array: JsonValue[],
220+
path: string[],
221+
value: JsonValue,
222+
): JsonValue[] => {
223+
const [index, ...restPath] = path;
224+
const arrayIndex = Number(index);
224225

225-
for (let i = 0; i < path.length - 1; i++) {
226-
const key = path[i];
227-
if (!(key in current)) {
228-
current[key] = {};
226+
// Validate array index
227+
if (isNaN(arrayIndex)) {
228+
console.error(`Invalid array index: ${index}`);
229+
return array;
230+
}
231+
232+
// Check array bounds
233+
if (arrayIndex < 0) {
234+
console.error(`Array index out of bounds: ${arrayIndex} < 0`);
235+
return array;
236+
}
237+
238+
const newArray = [...array];
239+
240+
if (restPath.length === 0) {
241+
newArray[arrayIndex] = value;
242+
} else {
243+
// Ensure index position exists
244+
if (arrayIndex >= array.length) {
245+
console.warn(`Extending array to index ${arrayIndex}`);
246+
newArray.length = arrayIndex + 1;
247+
newArray.fill(null, array.length, arrayIndex);
248+
}
249+
newArray[arrayIndex] = updateValue(
250+
newArray[arrayIndex],
251+
restPath,
252+
value,
253+
);
254+
}
255+
return newArray;
256+
};
257+
258+
const updateObject = (
259+
obj: JsonObject,
260+
path: string[],
261+
value: JsonValue,
262+
): JsonObject => {
263+
const [key, ...restPath] = path;
264+
265+
// Validate object key
266+
if (typeof key !== "string") {
267+
console.error(`Invalid object key: ${key}`);
268+
return obj;
229269
}
230-
current = current[key] as JsonObject;
231-
}
232270

233-
current[path[path.length - 1]] = fieldValue;
234-
onChange(newValue);
271+
const newObj = { ...obj };
272+
273+
if (restPath.length === 0) {
274+
newObj[key] = value;
275+
} else {
276+
// Ensure key exists
277+
if (!(key in newObj)) {
278+
console.warn(`Creating new key in object: ${key}`);
279+
newObj[key] = {};
280+
}
281+
newObj[key] = updateValue(newObj[key], restPath, value);
282+
}
283+
return newObj;
284+
};
285+
286+
const updateValue = (
287+
current: JsonValue,
288+
path: string[],
289+
value: JsonValue,
290+
): JsonValue => {
291+
if (path.length === 0) return value;
292+
293+
try {
294+
if (!current) {
295+
current = !isNaN(Number(path[0])) ? [] : {};
296+
}
297+
298+
// Type checking
299+
if (Array.isArray(current)) {
300+
return updateArray(current, path, value);
301+
} else if (typeof current === "object" && current !== null) {
302+
return updateObject(current, path, value);
303+
} else {
304+
console.error(
305+
`Cannot update path ${path.join(".")} in non-object/array value:`,
306+
current,
307+
);
308+
return current;
309+
}
310+
} catch (error) {
311+
console.error(`Error updating value at path ${path.join(".")}:`, error);
312+
return current;
313+
}
314+
};
315+
316+
try {
317+
const newValue = updateValue(value, path, fieldValue);
318+
onChange(newValue);
319+
} catch (error) {
320+
console.error("Failed to update form value:", error);
321+
// Keep the original value unchanged
322+
onChange(value);
323+
}
235324
};
236325

237326
return (

client/src/components/PromptsTab.tsx

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
22
import { Button } from "@/components/ui/button";
3-
import { Input } from "@/components/ui/input";
3+
import { Combobox } from "@/components/ui/combobox";
44
import { Label } from "@/components/ui/label";
55
import { TabsContent } from "@/components/ui/tabs";
66
import { Textarea } from "@/components/ui/textarea";
7-
import { ListPromptsResult } from "@modelcontextprotocol/sdk/types.js";
7+
import {
8+
ListPromptsResult,
9+
PromptReference,
10+
ResourceReference,
11+
} from "@modelcontextprotocol/sdk/types.js";
812
import { AlertCircle } from "lucide-react";
9-
import { useState } from "react";
13+
import { useEffect, useState } from "react";
1014
import ListPane from "./ListPane";
15+
import { useCompletionState } from "@/lib/hooks/useCompletionState";
1116

1217
export type Prompt = {
1318
name: string;
@@ -26,6 +31,8 @@ const PromptsTab = ({
2631
getPrompt,
2732
selectedPrompt,
2833
setSelectedPrompt,
34+
handleCompletion,
35+
completionsSupported,
2936
promptContent,
3037
nextCursor,
3138
error,
@@ -36,14 +43,37 @@ const PromptsTab = ({
3643
getPrompt: (name: string, args: Record<string, string>) => void;
3744
selectedPrompt: Prompt | null;
3845
setSelectedPrompt: (prompt: Prompt) => void;
46+
handleCompletion: (
47+
ref: PromptReference | ResourceReference,
48+
argName: string,
49+
value: string,
50+
) => Promise<string[]>;
51+
completionsSupported: boolean;
3952
promptContent: string;
4053
nextCursor: ListPromptsResult["nextCursor"];
4154
error: string | null;
4255
}) => {
4356
const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
57+
const { completions, clearCompletions, requestCompletions } =
58+
useCompletionState(handleCompletion, completionsSupported);
4459

45-
const handleInputChange = (argName: string, value: string) => {
60+
useEffect(() => {
61+
clearCompletions();
62+
}, [clearCompletions, selectedPrompt]);
63+
64+
const handleInputChange = async (argName: string, value: string) => {
4665
setPromptArgs((prev) => ({ ...prev, [argName]: value }));
66+
67+
if (selectedPrompt) {
68+
requestCompletions(
69+
{
70+
type: "ref/prompt",
71+
name: selectedPrompt.name,
72+
},
73+
argName,
74+
value,
75+
);
76+
}
4777
};
4878

4979
const handleGetPrompt = () => {
@@ -96,14 +126,17 @@ const PromptsTab = ({
96126
{selectedPrompt.arguments?.map((arg) => (
97127
<div key={arg.name}>
98128
<Label htmlFor={arg.name}>{arg.name}</Label>
99-
<Input
129+
<Combobox
100130
id={arg.name}
101131
placeholder={`Enter ${arg.name}`}
102132
value={promptArgs[arg.name] || ""}
103-
onChange={(e) =>
104-
handleInputChange(arg.name, e.target.value)
133+
onChange={(value) => handleInputChange(arg.name, value)}
134+
onInputChange={(value) =>
135+
handleInputChange(arg.name, value)
105136
}
137+
options={completions[arg.name] || []}
106138
/>
139+
107140
{arg.description && (
108141
<p className="text-xs text-gray-500 mt-1">
109142
{arg.description}

0 commit comments

Comments
 (0)