Merge pull request #170 from cliffhall/add-subscribe-to-resource

Add subscribe to resource functionality
This commit is contained in:
Ola Hungerford
2025-03-10 21:08:46 -07:00
committed by GitHub
6 changed files with 100 additions and 13 deletions

View File

@@ -128,6 +128,10 @@ const App = () => {
const [selectedResource, setSelectedResource] = useState<Resource | null>( const [selectedResource, setSelectedResource] = useState<Resource | null>(
null, null,
); );
const [resourceSubscriptions, setResourceSubscriptions] = useState<
Set<string>
>(new Set<string>());
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null); const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [selectedTool, setSelectedTool] = useState<Tool | null>(null); const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
const [nextResourceCursor, setNextResourceCursor] = useState< const [nextResourceCursor, setNextResourceCursor] = useState<
@@ -308,6 +312,38 @@ const App = () => {
setResourceContent(JSON.stringify(response, null, 2)); setResourceContent(JSON.stringify(response, null, 2));
}; };
const subscribeToResource = async (uri: string) => {
if (!resourceSubscriptions.has(uri)) {
await makeRequest(
{
method: "resources/subscribe" as const,
params: { uri },
},
z.object({}),
"resources",
);
const clone = new Set(resourceSubscriptions);
clone.add(uri);
setResourceSubscriptions(clone);
}
};
const unsubscribeFromResource = async (uri: string) => {
if (resourceSubscriptions.has(uri)) {
await makeRequest(
{
method: "resources/unsubscribe" as const,
params: { uri },
},
z.object({}),
"resources",
);
const clone = new Set(resourceSubscriptions);
clone.delete(uri);
setResourceSubscriptions(clone);
}
};
const listPrompts = async () => { const listPrompts = async () => {
const response = await makeRequest( const response = await makeRequest(
{ {
@@ -485,6 +521,18 @@ const App = () => {
clearError("resources"); clearError("resources");
setSelectedResource(resource); setSelectedResource(resource);
}} }}
resourceSubscriptionsSupported={
serverCapabilities?.resources?.subscribe || false
}
resourceSubscriptions={resourceSubscriptions}
subscribeToResource={(uri) => {
clearError("resources");
subscribeToResource(uri);
}}
unsubscribeFromResource={(uri) => {
clearError("resources");
unsubscribeFromResource(uri);
}}
handleCompletion={handleCompletion} handleCompletion={handleCompletion}
completionsSupported={completionsSupported} completionsSupported={completionsSupported}
resourceContent={resourceContent} resourceContent={resourceContent}

View File

@@ -26,6 +26,10 @@ const ResourcesTab = ({
readResource, readResource,
selectedResource, selectedResource,
setSelectedResource, setSelectedResource,
resourceSubscriptionsSupported,
resourceSubscriptions,
subscribeToResource,
unsubscribeFromResource,
handleCompletion, handleCompletion,
completionsSupported, completionsSupported,
resourceContent, resourceContent,
@@ -52,6 +56,10 @@ const ResourcesTab = ({
nextCursor: ListResourcesResult["nextCursor"]; nextCursor: ListResourcesResult["nextCursor"];
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"]; nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
error: string | null; error: string | null;
resourceSubscriptionsSupported: boolean;
resourceSubscriptions: Set<string>;
subscribeToResource: (uri: string) => void;
unsubscribeFromResource: (uri: string) => void;
}) => { }) => {
const [selectedTemplate, setSelectedTemplate] = const [selectedTemplate, setSelectedTemplate] =
useState<ResourceTemplate | null>(null); useState<ResourceTemplate | null>(null);
@@ -164,14 +172,38 @@ const ResourcesTab = ({
: "Select a resource or template"} : "Select a resource or template"}
</h3> </h3>
{selectedResource && ( {selectedResource && (
<Button <div className="flex row-auto gap-1 justify-end w-2/5">
variant="outline" {resourceSubscriptionsSupported &&
size="sm" !resourceSubscriptions.has(selectedResource.uri) && (
onClick={() => readResource(selectedResource.uri)} <Button
> variant="outline"
<RefreshCw className="w-4 h-4 mr-2" /> size="sm"
Refresh onClick={() => subscribeToResource(selectedResource.uri)}
</Button> >
Subscribe
</Button>
)}
{resourceSubscriptionsSupported &&
resourceSubscriptions.has(selectedResource.uri) && (
<Button
variant="outline"
size="sm"
onClick={() =>
unsubscribeFromResource(selectedResource.uri)
}
>
Unsubscribe
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={() => readResource(selectedResource.uri)}
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
</div>
)} )}
</div> </div>
<div className="p-4"> <div className="p-4">

View File

@@ -9,6 +9,7 @@ import {
CreateMessageRequestSchema, CreateMessageRequestSchema,
ListRootsRequestSchema, ListRootsRequestSchema,
ProgressNotificationSchema, ProgressNotificationSchema,
ResourceUpdatedNotificationSchema,
Request, Request,
Result, Result,
ServerCapabilities, ServerCapabilities,
@@ -247,6 +248,11 @@ export function useConnection({
ProgressNotificationSchema, ProgressNotificationSchema,
onNotification, onNotification,
); );
client.setNotificationHandler(
ResourceUpdatedNotificationSchema,
onNotification,
);
} }
if (onStdErrNotification) { if (onStdErrNotification) {

View File

@@ -1,6 +1,7 @@
import { import {
NotificationSchema as BaseNotificationSchema, NotificationSchema as BaseNotificationSchema,
ClientNotificationSchema, ClientNotificationSchema,
ServerNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod"; import { z } from "zod";
@@ -13,7 +14,7 @@ export const StdErrNotificationSchema = BaseNotificationSchema.extend({
export const NotificationSchema = ClientNotificationSchema.or( export const NotificationSchema = ClientNotificationSchema.or(
StdErrNotificationSchema, StdErrNotificationSchema,
); ).or(ServerNotificationSchema);
export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>; export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
export type Notification = z.infer<typeof NotificationSchema>; export type Notification = z.infer<typeof NotificationSchema>;

4
package-lock.json generated
View File

@@ -13,8 +13,8 @@
"server" "server"
], ],
"dependencies": { "dependencies": {
"@modelcontextprotocol/inspector-client": "0.4.1", "@modelcontextprotocol/inspector-client": "^0.5.1",
"@modelcontextprotocol/inspector-server": "0.4.1", "@modelcontextprotocol/inspector-server": "^0.5.1",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"shell-quote": "^1.8.2", "shell-quote": "^1.8.2",
"spawn-rx": "^5.1.2", "spawn-rx": "^5.1.2",

View File

@@ -34,8 +34,8 @@
"publish-all": "npm publish --workspaces --access public && npm publish --access public" "publish-all": "npm publish --workspaces --access public && npm publish --access public"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/inspector-client": "0.4.1", "@modelcontextprotocol/inspector-client": "^0.5.1",
"@modelcontextprotocol/inspector-server": "0.4.1", "@modelcontextprotocol/inspector-server": "^0.5.1",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"shell-quote": "^1.8.2", "shell-quote": "^1.8.2",
"spawn-rx": "^5.1.2", "spawn-rx": "^5.1.2",