Skip to content

Commit db1b5cb

Browse files
Merge pull request #113 from gavinaboulhosn/feature/completions
Implement MCP Completion Support in Prompts and Resources Tabs
2 parents 717c394 + b4870b3 commit db1b5cb

File tree

11 files changed

+1177
-55
lines changed

11 files changed

+1177
-55
lines changed

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/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}

client/src/components/ResourcesTab.tsx

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
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 { Label } from "@/components/ui/label";
4+
import { Combobox } from "@/components/ui/combobox";
45
import { TabsContent } from "@/components/ui/tabs";
56
import {
67
ListResourcesResult,
78
Resource,
89
ResourceTemplate,
910
ListResourceTemplatesResult,
11+
ResourceReference,
12+
PromptReference,
1013
} from "@modelcontextprotocol/sdk/types.js";
1114
import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react";
1215
import ListPane from "./ListPane";
13-
import { useState } from "react";
16+
import { useEffect, useState } from "react";
17+
import { useCompletionState } from "@/lib/hooks/useCompletionState";
1418

1519
const ResourcesTab = ({
1620
resources,
@@ -22,6 +26,8 @@ const ResourcesTab = ({
2226
readResource,
2327
selectedResource,
2428
setSelectedResource,
29+
handleCompletion,
30+
completionsSupported,
2531
resourceContent,
2632
nextCursor,
2733
nextTemplateCursor,
@@ -36,6 +42,12 @@ const ResourcesTab = ({
3642
readResource: (uri: string) => void;
3743
selectedResource: Resource | null;
3844
setSelectedResource: (resource: Resource | null) => void;
45+
handleCompletion: (
46+
ref: ResourceReference | PromptReference,
47+
argName: string,
48+
value: string,
49+
) => Promise<string[]>;
50+
completionsSupported: boolean;
3951
resourceContent: string;
4052
nextCursor: ListResourcesResult["nextCursor"];
4153
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
@@ -47,6 +59,13 @@ const ResourcesTab = ({
4759
{},
4860
);
4961

62+
const { completions, clearCompletions, requestCompletions } =
63+
useCompletionState(handleCompletion, completionsSupported);
64+
65+
useEffect(() => {
66+
clearCompletions();
67+
}, [clearCompletions]);
68+
5069
const fillTemplate = (
5170
template: string,
5271
values: Record<string, string>,
@@ -57,6 +76,21 @@ const ResourcesTab = ({
5776
);
5877
};
5978

79+
const handleTemplateValueChange = async (key: string, value: string) => {
80+
setTemplateValues((prev) => ({ ...prev, [key]: value }));
81+
82+
if (selectedTemplate?.uriTemplate) {
83+
requestCompletions(
84+
{
85+
type: "ref/resource",
86+
uri: selectedTemplate.uriTemplate,
87+
},
88+
key,
89+
value,
90+
);
91+
}
92+
};
93+
6094
const handleReadTemplateResource = () => {
6195
if (selectedTemplate) {
6296
const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);
@@ -162,22 +196,18 @@ const ResourcesTab = ({
162196
const key = param.slice(1, -1);
163197
return (
164198
<div key={key}>
165-
<label
166-
htmlFor={key}
167-
className="block text-sm font-medium text-gray-700"
168-
>
169-
{key}
170-
</label>
171-
<Input
199+
<Label htmlFor={key}>{key}</Label>
200+
<Combobox
172201
id={key}
202+
placeholder={`Enter ${key}`}
173203
value={templateValues[key] || ""}
174-
onChange={(e) =>
175-
setTemplateValues({
176-
...templateValues,
177-
[key]: e.target.value,
178-
})
204+
onChange={(value) =>
205+
handleTemplateValueChange(key, value)
206+
}
207+
onInputChange={(value) =>
208+
handleTemplateValueChange(key, value)
179209
}
180-
className="mt-1"
210+
options={completions[key] || []}
181211
/>
182212
</div>
183213
);

0 commit comments

Comments
 (0)