Skip to content

Commit 645f2e9

Browse files
committed
Add support for listing and filling resource templates
1 parent d80214d commit 645f2e9

File tree

2 files changed

+193
-58
lines changed

2 files changed

+193
-58
lines changed

client/src/App.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
GetPromptResultSchema,
1010
ListPromptsResultSchema,
1111
ListResourcesResultSchema,
12+
ListResourceTemplatesResultSchema,
1213
ListToolsResultSchema,
1314
ProgressNotificationSchema,
1415
ReadResourceResultSchema,
1516
Resource,
17+
ResourceTemplate,
1618
ServerNotification,
1719
Tool,
1820
} from "@modelcontextprotocol/sdk/types.js";
@@ -56,6 +58,9 @@ const App = () => {
5658
"disconnected" | "connected" | "error"
5759
>("disconnected");
5860
const [resources, setResources] = useState<Resource[]>([]);
61+
const [resourceTemplates, setResourceTemplates] = useState<
62+
ResourceTemplate[]
63+
>([]);
5964
const [resourceContent, setResourceContent] = useState<string>("");
6065
const [prompts, setPrompts] = useState<Prompt[]>([]);
6166
const [promptContent, setPromptContent] = useState<string>("");
@@ -116,6 +121,9 @@ const App = () => {
116121
const [nextResourceCursor, setNextResourceCursor] = useState<
117122
string | undefined
118123
>();
124+
const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState<
125+
string | undefined
126+
>();
119127
const [nextPromptCursor, setNextPromptCursor] = useState<
120128
string | undefined
121129
>();
@@ -167,6 +175,22 @@ const App = () => {
167175
setNextResourceCursor(response.nextCursor);
168176
};
169177

178+
const listResourceTemplates = async () => {
179+
const response = await makeRequest(
180+
{
181+
method: "resources/templates/list" as const,
182+
params: nextResourceTemplateCursor
183+
? { cursor: nextResourceTemplateCursor }
184+
: {},
185+
},
186+
ListResourceTemplatesResultSchema,
187+
);
188+
setResourceTemplates(
189+
resourceTemplates.concat(response.resourceTemplates ?? []),
190+
);
191+
setNextResourceTemplateCursor(response.nextCursor);
192+
};
193+
170194
const readResource = async (uri: string) => {
171195
const response = await makeRequest(
172196
{
@@ -368,12 +392,15 @@ const App = () => {
368392
<div className="w-full">
369393
<ResourcesTab
370394
resources={resources}
395+
resourceTemplates={resourceTemplates}
371396
listResources={listResources}
397+
listResourceTemplates={listResourceTemplates}
372398
readResource={readResource}
373399
selectedResource={selectedResource}
374400
setSelectedResource={setSelectedResource}
375401
resourceContent={resourceContent}
376402
nextCursor={nextResourceCursor}
403+
nextTemplateCursor={nextResourceTemplateCursor}
377404
error={error}
378405
/>
379406
<PromptsTab
Lines changed: 166 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,199 @@
11
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
22
import { Button } from "@/components/ui/button";
3+
import { Input } from "@/components/ui/input";
34
import { TabsContent } from "@/components/ui/tabs";
45
import {
56
ListResourcesResult,
67
Resource,
8+
ResourceTemplate,
9+
ListResourceTemplatesResult,
710
} from "@modelcontextprotocol/sdk/types.js";
811
import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react";
912
import ListPane from "./ListPane";
13+
import { useState } from "react";
1014

1115
const ResourcesTab = ({
1216
resources,
17+
resourceTemplates,
1318
listResources,
19+
listResourceTemplates,
1420
readResource,
1521
selectedResource,
1622
setSelectedResource,
1723
resourceContent,
1824
nextCursor,
25+
nextTemplateCursor,
1926
error,
2027
}: {
2128
resources: Resource[];
29+
resourceTemplates: ResourceTemplate[];
2230
listResources: () => void;
31+
listResourceTemplates: () => void;
2332
readResource: (uri: string) => void;
2433
selectedResource: Resource | null;
25-
setSelectedResource: (resource: Resource) => void;
34+
setSelectedResource: (resource: Resource | null) => void;
2635
resourceContent: string;
2736
nextCursor: ListResourcesResult["nextCursor"];
37+
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
2838
error: string | null;
29-
}) => (
30-
<TabsContent value="resources" className="grid grid-cols-2 gap-4">
31-
<ListPane
32-
items={resources}
33-
listItems={listResources}
34-
setSelectedItem={(resource) => {
35-
setSelectedResource(resource);
36-
readResource(resource.uri);
37-
}}
38-
renderItem={(resource) => (
39-
<div className="flex items-center w-full">
40-
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
41-
<span className="flex-1 truncate" title={resource.uri.toString()}>
42-
{resource.name}
43-
</span>
44-
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
45-
</div>
46-
)}
47-
title="Resources"
48-
buttonText={nextCursor ? "List More Resources" : "List Resources"}
49-
isButtonDisabled={!nextCursor && resources.length > 0}
50-
/>
39+
}) => {
40+
const [selectedTemplate, setSelectedTemplate] =
41+
useState<ResourceTemplate | null>(null);
42+
const [templateValues, setTemplateValues] = useState<Record<string, string>>(
43+
{},
44+
);
5145

52-
<div className="bg-white rounded-lg shadow">
53-
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
54-
<h3 className="font-semibold truncate" title={selectedResource?.name}>
55-
{selectedResource ? selectedResource.name : "Select a resource"}
56-
</h3>
57-
{selectedResource && (
58-
<Button
59-
variant="outline"
60-
size="sm"
61-
onClick={() => readResource(selectedResource.uri)}
62-
>
63-
<RefreshCw className="w-4 h-4 mr-2" />
64-
Refresh
65-
</Button>
46+
const fillTemplate = (
47+
template: string,
48+
values: Record<string, string>,
49+
): string => {
50+
return template.replace(
51+
/{([^}]+)}/g,
52+
(_, key) => values[key] || `{${key}}`,
53+
);
54+
};
55+
56+
const handleReadTemplateResource = () => {
57+
if (selectedTemplate) {
58+
const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);
59+
readResource(uri);
60+
setSelectedTemplate(null);
61+
// We don't have the full Resource object here, so we create a partial one
62+
setSelectedResource({ uri, name: uri } as Resource);
63+
}
64+
};
65+
66+
return (
67+
<TabsContent value="resources" className="grid grid-cols-3 gap-4">
68+
<ListPane
69+
items={resources}
70+
listItems={listResources}
71+
setSelectedItem={(resource) => {
72+
setSelectedResource(resource);
73+
readResource(resource.uri);
74+
setSelectedTemplate(null);
75+
}}
76+
renderItem={(resource) => (
77+
<div className="flex items-center w-full">
78+
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
79+
<span className="flex-1 truncate" title={resource.uri.toString()}>
80+
{resource.name}
81+
</span>
82+
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
83+
</div>
6684
)}
67-
</div>
68-
<div className="p-4">
69-
{error ? (
70-
<Alert variant="destructive">
71-
<AlertCircle className="h-4 w-4" />
72-
<AlertTitle>Error</AlertTitle>
73-
<AlertDescription>{error}</AlertDescription>
74-
</Alert>
75-
) : selectedResource ? (
76-
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words">
77-
{resourceContent}
78-
</pre>
79-
) : (
80-
<Alert>
81-
<AlertDescription>
82-
Select a resource from the list to view its contents
83-
</AlertDescription>
84-
</Alert>
85+
title="Resources"
86+
buttonText={nextCursor ? "List More Resources" : "List Resources"}
87+
isButtonDisabled={!nextCursor && resources.length > 0}
88+
/>
89+
90+
<ListPane
91+
items={resourceTemplates}
92+
listItems={listResourceTemplates}
93+
setSelectedItem={(template) => {
94+
setSelectedTemplate(template);
95+
setSelectedResource(null);
96+
setTemplateValues({});
97+
}}
98+
renderItem={(template) => (
99+
<div className="flex items-center w-full">
100+
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
101+
<span className="flex-1 truncate" title={template.uriTemplate}>
102+
{template.name}
103+
</span>
104+
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
105+
</div>
85106
)}
107+
title="Resource Templates"
108+
buttonText={
109+
nextTemplateCursor ? "List More Templates" : "List Templates"
110+
}
111+
isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
112+
/>
113+
114+
<div className="bg-white rounded-lg shadow">
115+
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
116+
<h3
117+
className="font-semibold truncate"
118+
title={selectedResource?.name || selectedTemplate?.name}
119+
>
120+
{selectedResource
121+
? selectedResource.name
122+
: selectedTemplate
123+
? selectedTemplate.name
124+
: "Select a resource or template"}
125+
</h3>
126+
{selectedResource && (
127+
<Button
128+
variant="outline"
129+
size="sm"
130+
onClick={() => readResource(selectedResource.uri)}
131+
>
132+
<RefreshCw className="w-4 h-4 mr-2" />
133+
Refresh
134+
</Button>
135+
)}
136+
</div>
137+
<div className="p-4">
138+
{error ? (
139+
<Alert variant="destructive">
140+
<AlertCircle className="h-4 w-4" />
141+
<AlertTitle>Error</AlertTitle>
142+
<AlertDescription>{error}</AlertDescription>
143+
</Alert>
144+
) : selectedResource ? (
145+
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words">
146+
{resourceContent}
147+
</pre>
148+
) : selectedTemplate ? (
149+
<div className="space-y-4">
150+
<p className="text-sm text-gray-600">
151+
{selectedTemplate.description}
152+
</p>
153+
{selectedTemplate.uriTemplate
154+
.match(/{([^}]+)}/g)
155+
?.map((param) => {
156+
const key = param.slice(1, -1);
157+
return (
158+
<div key={key}>
159+
<label
160+
htmlFor={key}
161+
className="block text-sm font-medium text-gray-700"
162+
>
163+
{key}
164+
</label>
165+
<Input
166+
id={key}
167+
value={templateValues[key] || ""}
168+
onChange={(e) =>
169+
setTemplateValues({
170+
...templateValues,
171+
[key]: e.target.value,
172+
})
173+
}
174+
className="mt-1"
175+
/>
176+
</div>
177+
);
178+
})}
179+
<Button
180+
onClick={handleReadTemplateResource}
181+
disabled={Object.keys(templateValues).length === 0}
182+
>
183+
Read Resource
184+
</Button>
185+
</div>
186+
) : (
187+
<Alert>
188+
<AlertDescription>
189+
Select a resource or template from the list to view its contents
190+
</AlertDescription>
191+
</Alert>
192+
)}
193+
</div>
86194
</div>
87-
</div>
88-
</TabsContent>
89-
);
195+
</TabsContent>
196+
);
197+
};
90198

91199
export default ResourcesTab;

0 commit comments

Comments
 (0)