Add support for listing and filling resource templates
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user