Add support for listing and filling resource templates

This commit is contained in:
Justin Spahr-Summers
2024-11-07 14:02:53 +00:00
parent d80214d0a2
commit 645f2e942e
2 changed files with 194 additions and 59 deletions

View File

@@ -9,10 +9,12 @@ import {
GetPromptResultSchema, GetPromptResultSchema,
ListPromptsResultSchema, ListPromptsResultSchema,
ListResourcesResultSchema, ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ListToolsResultSchema, ListToolsResultSchema,
ProgressNotificationSchema, ProgressNotificationSchema,
ReadResourceResultSchema, ReadResourceResultSchema,
Resource, Resource,
ResourceTemplate,
ServerNotification, ServerNotification,
Tool, Tool,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
@@ -56,6 +58,9 @@ const App = () => {
"disconnected" | "connected" | "error" "disconnected" | "connected" | "error"
>("disconnected"); >("disconnected");
const [resources, setResources] = useState<Resource[]>([]); const [resources, setResources] = useState<Resource[]>([]);
const [resourceTemplates, setResourceTemplates] = useState<
ResourceTemplate[]
>([]);
const [resourceContent, setResourceContent] = useState<string>(""); const [resourceContent, setResourceContent] = useState<string>("");
const [prompts, setPrompts] = useState<Prompt[]>([]); const [prompts, setPrompts] = useState<Prompt[]>([]);
const [promptContent, setPromptContent] = useState<string>(""); const [promptContent, setPromptContent] = useState<string>("");
@@ -116,6 +121,9 @@ const App = () => {
const [nextResourceCursor, setNextResourceCursor] = useState< const [nextResourceCursor, setNextResourceCursor] = useState<
string | undefined string | undefined
>(); >();
const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState<
string | undefined
>();
const [nextPromptCursor, setNextPromptCursor] = useState< const [nextPromptCursor, setNextPromptCursor] = useState<
string | undefined string | undefined
>(); >();
@@ -167,6 +175,22 @@ const App = () => {
setNextResourceCursor(response.nextCursor); setNextResourceCursor(response.nextCursor);
}; };
const listResourceTemplates = async () => {
const response = await makeRequest(
{
method: "resources/templates/list" as const,
params: nextResourceTemplateCursor
? { cursor: nextResourceTemplateCursor }
: {},
},
ListResourceTemplatesResultSchema,
);
setResourceTemplates(
resourceTemplates.concat(response.resourceTemplates ?? []),
);
setNextResourceTemplateCursor(response.nextCursor);
};
const readResource = async (uri: string) => { const readResource = async (uri: string) => {
const response = await makeRequest( const response = await makeRequest(
{ {
@@ -368,12 +392,15 @@ const App = () => {
<div className="w-full"> <div className="w-full">
<ResourcesTab <ResourcesTab
resources={resources} resources={resources}
resourceTemplates={resourceTemplates}
listResources={listResources} listResources={listResources}
listResourceTemplates={listResourceTemplates}
readResource={readResource} readResource={readResource}
selectedResource={selectedResource} selectedResource={selectedResource}
setSelectedResource={setSelectedResource} setSelectedResource={setSelectedResource}
resourceContent={resourceContent} resourceContent={resourceContent}
nextCursor={nextResourceCursor} nextCursor={nextResourceCursor}
nextTemplateCursor={nextResourceTemplateCursor}
error={error} error={error}
/> />
<PromptsTab <PromptsTab

View File

@@ -1,39 +1,77 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs"; import { TabsContent } from "@/components/ui/tabs";
import { import {
ListResourcesResult, ListResourcesResult,
Resource, Resource,
ResourceTemplate,
ListResourceTemplatesResult,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react"; import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react";
import ListPane from "./ListPane"; import ListPane from "./ListPane";
import { useState } from "react";
const ResourcesTab = ({ const ResourcesTab = ({
resources, resources,
resourceTemplates,
listResources, listResources,
listResourceTemplates,
readResource, readResource,
selectedResource, selectedResource,
setSelectedResource, setSelectedResource,
resourceContent, resourceContent,
nextCursor, nextCursor,
nextTemplateCursor,
error, error,
}: { }: {
resources: Resource[]; resources: Resource[];
resourceTemplates: ResourceTemplate[];
listResources: () => void; listResources: () => void;
listResourceTemplates: () => void;
readResource: (uri: string) => void; readResource: (uri: string) => void;
selectedResource: Resource | null; selectedResource: Resource | null;
setSelectedResource: (resource: Resource) => void; setSelectedResource: (resource: Resource | null) => void;
resourceContent: string; resourceContent: string;
nextCursor: ListResourcesResult["nextCursor"]; nextCursor: ListResourcesResult["nextCursor"];
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
error: string | null; error: string | null;
}) => ( }) => {
<TabsContent value="resources" className="grid grid-cols-2 gap-4"> const [selectedTemplate, setSelectedTemplate] =
useState<ResourceTemplate | null>(null);
const [templateValues, setTemplateValues] = useState<Record<string, string>>(
{},
);
const fillTemplate = (
template: string,
values: Record<string, string>,
): string => {
return template.replace(
/{([^}]+)}/g,
(_, key) => values[key] || `{${key}}`,
);
};
const handleReadTemplateResource = () => {
if (selectedTemplate) {
const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);
readResource(uri);
setSelectedTemplate(null);
// We don't have the full Resource object here, so we create a partial one
setSelectedResource({ uri, name: uri } as Resource);
}
};
return (
<TabsContent value="resources" className="grid grid-cols-3 gap-4">
<ListPane <ListPane
items={resources} items={resources}
listItems={listResources} listItems={listResources}
setSelectedItem={(resource) => { setSelectedItem={(resource) => {
setSelectedResource(resource); setSelectedResource(resource);
readResource(resource.uri); readResource(resource.uri);
setSelectedTemplate(null);
}} }}
renderItem={(resource) => ( renderItem={(resource) => (
<div className="flex items-center w-full"> <div className="flex items-center w-full">
@@ -49,10 +87,41 @@ const ResourcesTab = ({
isButtonDisabled={!nextCursor && resources.length > 0} isButtonDisabled={!nextCursor && resources.length > 0}
/> />
<ListPane
items={resourceTemplates}
listItems={listResourceTemplates}
setSelectedItem={(template) => {
setSelectedTemplate(template);
setSelectedResource(null);
setTemplateValues({});
}}
renderItem={(template) => (
<div className="flex items-center w-full">
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
<span className="flex-1 truncate" title={template.uriTemplate}>
{template.name}
</span>
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
</div>
)}
title="Resource Templates"
buttonText={
nextTemplateCursor ? "List More Templates" : "List Templates"
}
isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
/>
<div className="bg-white rounded-lg shadow"> <div className="bg-white rounded-lg shadow">
<div className="p-4 border-b border-gray-200 flex justify-between items-center"> <div className="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 className="font-semibold truncate" title={selectedResource?.name}> <h3
{selectedResource ? selectedResource.name : "Select a resource"} className="font-semibold truncate"
title={selectedResource?.name || selectedTemplate?.name}
>
{selectedResource
? selectedResource.name
: selectedTemplate
? selectedTemplate.name
: "Select a resource or template"}
</h3> </h3>
{selectedResource && ( {selectedResource && (
<Button <Button
@@ -76,16 +145,55 @@ const ResourcesTab = ({
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words"> <pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words">
{resourceContent} {resourceContent}
</pre> </pre>
) : selectedTemplate ? (
<div className="space-y-4">
<p className="text-sm text-gray-600">
{selectedTemplate.description}
</p>
{selectedTemplate.uriTemplate
.match(/{([^}]+)}/g)
?.map((param) => {
const key = param.slice(1, -1);
return (
<div key={key}>
<label
htmlFor={key}
className="block text-sm font-medium text-gray-700"
>
{key}
</label>
<Input
id={key}
value={templateValues[key] || ""}
onChange={(e) =>
setTemplateValues({
...templateValues,
[key]: e.target.value,
})
}
className="mt-1"
/>
</div>
);
})}
<Button
onClick={handleReadTemplateResource}
disabled={Object.keys(templateValues).length === 0}
>
Read Resource
</Button>
</div>
) : ( ) : (
<Alert> <Alert>
<AlertDescription> <AlertDescription>
Select a resource from the list to view its contents Select a resource or template from the list to view its contents
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
</div> </div>
</div> </div>
</TabsContent> </TabsContent>
); );
};
export default ResourcesTab; export default ResourcesTab;