Skip to content

Commit 7f713fe

Browse files
author
Gavin Aboulhosn
committed
refactor(completions): improve completion handling and error states
- Move completion logic from App.tsx to useConnection hook - Replace useCompletion with simpler useCompletionState hook - Add graceful fallback for servers without completion support - Improve error handling and state management - Update PromptsTab and ResourcesTab to use new completion API - Add type safety improvements across completion interfaces
1 parent c66feff commit 7f713fe

File tree

6 files changed

+166
-180
lines changed

6 files changed

+166
-180
lines changed

client/src/App.tsx

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ import {
1515
Root,
1616
ServerNotification,
1717
Tool,
18-
PromptReference,
19-
ResourceReference,
20-
CompleteResultSchema,
2118
} from "@modelcontextprotocol/sdk/types.js";
2219
import React, { Suspense, useEffect, useRef, useState } from "react";
2320
import { useConnection } from "./lib/hooks/useConnection";
@@ -154,6 +151,8 @@ const App = () => {
154151
requestHistory,
155152
makeRequest: makeConnectionRequest,
156153
sendNotification,
154+
handleCompletion,
155+
completionsSupported,
157156
connect: connectMcpServer,
158157
} = useConnection({
159158
transportType,
@@ -267,38 +266,6 @@ const App = () => {
267266
}
268267
};
269268

270-
const handleCompletion = async (
271-
ref: ResourceReference | PromptReference,
272-
argName: string,
273-
value: string,
274-
signal?: AbortSignal,
275-
) => {
276-
if (!mcpClient) {
277-
throw new Error("MCP client not connected");
278-
}
279-
280-
const request: ClientRequest = {
281-
method: "completion/complete",
282-
params: {
283-
argument: {
284-
name: argName,
285-
value,
286-
},
287-
ref,
288-
},
289-
};
290-
291-
try {
292-
const result = await makeRequest(request, CompleteResultSchema);
293-
return result.completion.values;
294-
} catch (e: unknown) {
295-
const errorMessage = e instanceof Error ? e.message : String(e);
296-
297-
toast.error(errorMessage);
298-
throw e;
299-
}
300-
};
301-
302269
const listResources = async () => {
303270
const response = await makeRequest(
304271
{
@@ -518,7 +485,8 @@ const App = () => {
518485
clearError("resources");
519486
setSelectedResource(resource);
520487
}}
521-
onComplete={handleCompletion}
488+
handleCompletion={handleCompletion}
489+
completionsSupported={completionsSupported}
522490
resourceContent={resourceContent}
523491
nextCursor={nextResourceCursor}
524492
nextTemplateCursor={nextResourceTemplateCursor}
@@ -543,7 +511,8 @@ const App = () => {
543511
clearError("prompts");
544512
setSelectedPrompt(prompt);
545513
}}
546-
onComplete={handleCompletion}
514+
handleCompletion={handleCompletion}
515+
completionsSupported={completionsSupported}
547516
promptContent={promptContent}
548517
nextCursor={nextPromptCursor}
549518
error={errors.prompts}

client/src/components/PromptsTab.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { Textarea } from "@/components/ui/textarea";
77
import {
88
ListPromptsResult,
99
PromptReference,
10+
ResourceReference,
1011
} from "@modelcontextprotocol/sdk/types.js";
1112
import { AlertCircle } from "lucide-react";
1213
import { useEffect, useState } from "react";
1314
import ListPane from "./ListPane";
14-
import { useCompletions } from "@/lib/useCompletion";
15+
import { useCompletionState } from "@/lib/hooks/useCompletionState";
1516

1617
export type Prompt = {
1718
name: string;
@@ -30,7 +31,8 @@ const PromptsTab = ({
3031
getPrompt,
3132
selectedPrompt,
3233
setSelectedPrompt,
33-
onComplete,
34+
handleCompletion,
35+
completionsSupported,
3436
promptContent,
3537
nextCursor,
3638
error,
@@ -41,19 +43,19 @@ const PromptsTab = ({
4143
getPrompt: (name: string, args: Record<string, string>) => void;
4244
selectedPrompt: Prompt | null;
4345
setSelectedPrompt: (prompt: Prompt) => void;
44-
onComplete: (
45-
ref: PromptReference,
46+
handleCompletion: (
47+
ref: PromptReference | ResourceReference,
4648
argName: string,
4749
value: string,
4850
) => Promise<string[]>;
51+
completionsSupported: boolean;
4952
promptContent: string;
5053
nextCursor: ListPromptsResult["nextCursor"];
5154
error: string | null;
5255
}) => {
5356
const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
54-
const { completions, clearCompletions, requestCompletions } = useCompletions({
55-
onComplete,
56-
});
57+
const { completions, clearCompletions, requestCompletions } =
58+
useCompletionState(handleCompletion, completionsSupported);
5759

5860
useEffect(() => {
5961
clearCompletions();

client/src/components/ResourcesTab.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import {
99
ResourceTemplate,
1010
ListResourceTemplatesResult,
1111
ResourceReference,
12+
PromptReference,
1213
} from "@modelcontextprotocol/sdk/types.js";
1314
import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react";
1415
import ListPane from "./ListPane";
1516
import { useEffect, useState } from "react";
16-
import { useCompletions } from "@/lib/useCompletion";
17+
import { useCompletionState } from "@/lib/hooks/useCompletionState";
1718

1819
const ResourcesTab = ({
1920
resources,
@@ -25,7 +26,8 @@ const ResourcesTab = ({
2526
readResource,
2627
selectedResource,
2728
setSelectedResource,
28-
onComplete,
29+
handleCompletion,
30+
completionsSupported,
2931
resourceContent,
3032
nextCursor,
3133
nextTemplateCursor,
@@ -40,11 +42,12 @@ const ResourcesTab = ({
4042
readResource: (uri: string) => void;
4143
selectedResource: Resource | null;
4244
setSelectedResource: (resource: Resource | null) => void;
43-
onComplete: (
44-
ref: ResourceReference,
45+
handleCompletion: (
46+
ref: ResourceReference | PromptReference,
4547
argName: string,
4648
value: string,
4749
) => Promise<string[]>;
50+
completionsSupported: boolean;
4851
resourceContent: string;
4952
nextCursor: ListResourcesResult["nextCursor"];
5053
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
@@ -56,9 +59,8 @@ const ResourcesTab = ({
5659
{},
5760
);
5861

59-
const { clearCompletions, completions, requestCompletions } = useCompletions({
60-
onComplete,
61-
});
62+
const { completions, clearCompletions, requestCompletions } =
63+
useCompletionState(handleCompletion, completionsSupported);
6264

6365
useEffect(() => {
6466
clearCompletions();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useState, useCallback, useEffect } from "react";
2+
import { ResourceReference, PromptReference } from "@modelcontextprotocol/sdk/types.js";
3+
4+
interface CompletionState {
5+
completions: Record<string, string[]>;
6+
loading: Record<string, boolean>;
7+
error: Record<string, string | null>;
8+
}
9+
10+
export function useCompletionState(
11+
handleCompletion: (
12+
ref: ResourceReference | PromptReference,
13+
argName: string,
14+
value: string,
15+
) => Promise<string[]>,
16+
completionsSupported: boolean = true,
17+
) {
18+
const [state, setState] = useState<CompletionState>({
19+
completions: {},
20+
loading: {},
21+
error: {},
22+
});
23+
24+
const clearCompletions = useCallback(() => {
25+
setState({
26+
completions: {},
27+
loading: {},
28+
error: {},
29+
});
30+
}, []);
31+
32+
const requestCompletions = useCallback(
33+
async (ref: ResourceReference | PromptReference, argName: string, value: string) => {
34+
if (!completionsSupported) {
35+
return;
36+
}
37+
38+
setState((prev) => ({
39+
...prev,
40+
loading: { ...prev.loading, [argName]: true },
41+
error: { ...prev.error, [argName]: null },
42+
}));
43+
44+
try {
45+
const values = await handleCompletion(ref, argName, value);
46+
setState((prev) => ({
47+
...prev,
48+
completions: { ...prev.completions, [argName]: values },
49+
loading: { ...prev.loading, [argName]: false },
50+
}));
51+
} catch (err) {
52+
const error = err instanceof Error ? err.message : String(err);
53+
setState((prev) => ({
54+
...prev,
55+
loading: { ...prev.loading, [argName]: false },
56+
error: { ...prev.error, [argName]: error },
57+
}));
58+
}
59+
},
60+
[handleCompletion, completionsSupported],
61+
);
62+
63+
// Clear completions when support status changes
64+
useEffect(() => {
65+
if (!completionsSupported) {
66+
clearCompletions();
67+
}
68+
}, [completionsSupported, clearCompletions]);
69+
70+
return {
71+
...state,
72+
clearCompletions,
73+
requestCompletions,
74+
completionsSupported,
75+
};
76+
}

client/src/lib/hooks/useConnection.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {
1212
Request,
1313
Result,
1414
ServerCapabilities,
15+
PromptReference,
16+
ResourceReference,
17+
McpError,
18+
CompleteResultSchema,
1519
} from "@modelcontextprotocol/sdk/types.js";
1620
import { useState } from "react";
1721
import { toast } from "react-toastify";
@@ -36,6 +40,11 @@ interface UseConnectionOptions {
3640
getRoots?: () => any[];
3741
}
3842

43+
interface RequestOptions {
44+
signal?: AbortSignal;
45+
timeout?: number;
46+
}
47+
3948
export function useConnection({
4049
transportType,
4150
command,
@@ -58,6 +67,7 @@ export function useConnection({
5867
const [requestHistory, setRequestHistory] = useState<
5968
{ request: string; response?: string }[]
6069
>([]);
70+
const [completionsSupported, setCompletionsSupported] = useState(true);
6171

6272
const pushHistory = (request: object, response?: object) => {
6373
setRequestHistory((prev) => [
@@ -72,7 +82,8 @@ export function useConnection({
7282
const makeRequest = async <T extends z.ZodType>(
7383
request: ClientRequest,
7484
schema: T,
75-
) => {
85+
options?: RequestOptions,
86+
): Promise<z.output<T>> => {
7687
if (!mcpClient) {
7788
throw new Error("MCP client not connected");
7889
}
@@ -81,12 +92,12 @@ export function useConnection({
8192
const abortController = new AbortController();
8293
const timeoutId = setTimeout(() => {
8394
abortController.abort("Request timed out");
84-
}, requestTimeout);
95+
}, options?.timeout ?? requestTimeout);
8596

8697
let response;
8798
try {
8899
response = await mcpClient.request(request, schema, {
89-
signal: abortController.signal,
100+
signal: options?.signal ?? abortController.signal,
90101
});
91102
pushHistory(request, response);
92103
} catch (error) {
@@ -100,9 +111,58 @@ export function useConnection({
100111

101112
return response;
102113
} catch (e: unknown) {
114+
// Check for Method not found error specifically for completions
115+
if (
116+
request.method === "completion/complete" &&
117+
e instanceof McpError &&
118+
e.code === -32601
119+
) {
120+
setCompletionsSupported(false);
121+
return { completion: { values: [] } } as z.output<T>;
122+
}
123+
103124
const errorString = (e as Error).message ?? String(e);
104125
toast.error(errorString);
126+
throw e;
127+
}
128+
};
129+
130+
const handleCompletion = async (
131+
ref: ResourceReference | PromptReference,
132+
argName: string,
133+
value: string,
134+
signal?: AbortSignal,
135+
): Promise<string[]> => {
136+
if (!mcpClient || !completionsSupported) {
137+
return [];
138+
}
139+
140+
const request: ClientRequest = {
141+
method: "completion/complete",
142+
params: {
143+
argument: {
144+
name: argName,
145+
value,
146+
},
147+
ref,
148+
},
149+
};
150+
151+
try {
152+
const response = await makeRequest(request, CompleteResultSchema, {
153+
signal,
154+
});
155+
return response?.completion.values || [];
156+
} catch (e: unknown) {
157+
const errorMessage = e instanceof Error ? e.message : String(e);
158+
pushHistory(request, { error: errorMessage });
159+
160+
if (e instanceof McpError && e.code === -32601) {
161+
setCompletionsSupported(false);
162+
return [];
163+
}
105164

165+
toast.error(errorMessage);
106166
throw e;
107167
}
108168
};
@@ -238,6 +298,7 @@ export function useConnection({
238298

239299
const capabilities = client.getServerCapabilities();
240300
setServerCapabilities(capabilities ?? null);
301+
setCompletionsSupported(true); // Reset completions support on new connection
241302

242303
if (onPendingRequest) {
243304
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
@@ -268,6 +329,8 @@ export function useConnection({
268329
requestHistory,
269330
makeRequest,
270331
sendNotification,
332+
handleCompletion,
333+
completionsSupported,
271334
connect,
272335
};
273336
}

0 commit comments

Comments
 (0)