From 747c0154c51dfc42f001c3f14cdd9484dcdd8d29 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 8 Mar 2025 11:05:13 -0500 Subject: [PATCH 1/3] WIP: Subscribe to resources * In App.tsx - added subscribeToResource() - takes a uri - sends a `resources/subscribe` message with the uri - added unsubscribeFromResource() - takes a uri - sends a `resources/unsubscribe` message with the uri - in ResourcesTab element, - pass subscribeToResource and subscribeToResource invokers to component * In notificationTypes.ts - add ServerNotificationSchema to NotificationSchema to permit server update messages. * In ResourcesTab.tsx - deconstruct subscribeToResource and unsubscribeFromResource and add prop types - Add Subscribe and Unsubscribe buttons to selected resource panel, left of the refresh button. They call the sub and unsub functions that came in on props, passing the selected resource URI. - [WIP]: Will show the appropriate button in a follow up commit. * In useConnection.ts - import ResourceUpdatedNotificationSchema - in the connect function, - set onNotification as the handler for ResourceUpdatedNotificationSchema --- client/src/App.tsx | 33 +++++++++++++++++++++++ client/src/components/ResourcesTab.tsx | 36 ++++++++++++++++++++------ client/src/lib/hooks/useConnection.ts | 6 +++++ client/src/lib/notificationTypes.ts | 7 ++--- 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index a9adea5..f5b1f79 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -308,6 +308,31 @@ const App = () => { setResourceContent(JSON.stringify(response, null, 2)); }; + const subscribeToResource = async (uri: string) => { + + await makeRequest( + { + method: "resources/subscribe" as const, + params: { uri }, + }, + z.object({}), + "resources", + ); + }; + + const unsubscribeFromResource = async (uri: string) => { + + await makeRequest( + { + method: "resources/unsubscribe" as const, + params: { uri }, + }, + z.object({}), + "resources", + ); + }; + + const listPrompts = async () => { const response = await makeRequest( { @@ -485,6 +510,14 @@ const App = () => { clearError("resources"); setSelectedResource(resource); }} + subscribeToResource={(uri) => { + clearError("resources"); + subscribeToResource(uri); + }} + unsubscribeFromResource={(uri) => { + clearError("resources"); + unsubscribeFromResource(uri); + }} handleCompletion={handleCompletion} completionsSupported={completionsSupported} resourceContent={resourceContent} diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 93127a9..9d94296 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -26,6 +26,8 @@ const ResourcesTab = ({ readResource, selectedResource, setSelectedResource, + subscribeToResource, + unsubscribeFromResource, handleCompletion, completionsSupported, resourceContent, @@ -52,6 +54,8 @@ const ResourcesTab = ({ nextCursor: ListResourcesResult["nextCursor"]; nextTemplateCursor: ListResourceTemplatesResult["nextCursor"]; error: string | null; + subscribeToResource: (uri: string) => void; + unsubscribeFromResource: (uri: string) => void; }) => { const [selectedTemplate, setSelectedTemplate] = useState(null); @@ -164,14 +168,30 @@ const ResourcesTab = ({ : "Select a resource or template"} {selectedResource && ( - + <> + + + + )}
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 75b5467..9e7bb11 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -9,6 +9,7 @@ import { CreateMessageRequestSchema, ListRootsRequestSchema, ProgressNotificationSchema, + ResourceUpdatedNotificationSchema, Request, Result, ServerCapabilities, @@ -247,6 +248,11 @@ export function useConnection({ ProgressNotificationSchema, onNotification, ); + + client.setNotificationHandler( + ResourceUpdatedNotificationSchema, + onNotification, + ); } if (onStdErrNotification) { diff --git a/client/src/lib/notificationTypes.ts b/client/src/lib/notificationTypes.ts index 7aa6518..abd714b 100644 --- a/client/src/lib/notificationTypes.ts +++ b/client/src/lib/notificationTypes.ts @@ -1,6 +1,7 @@ import { NotificationSchema as BaseNotificationSchema, ClientNotificationSchema, + ServerNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; @@ -11,9 +12,9 @@ export const StdErrNotificationSchema = BaseNotificationSchema.extend({ }), }); -export const NotificationSchema = ClientNotificationSchema.or( - StdErrNotificationSchema, -); +export const NotificationSchema = ClientNotificationSchema + .or(StdErrNotificationSchema) + .or(ServerNotificationSchema); export type StdErrNotification = z.infer; export type Notification = z.infer; From a669272fda9f5b2af99bd9a0f2241e779fc947e1 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 8 Mar 2025 13:40:37 -0500 Subject: [PATCH 2/3] Track subscribed resources and show the appropriate subscribe or unsubscribe button on selected resource panel. If the server does not support resource subscriptions, do not show any subscription buttons. * In App.tsx - useState for resourceSubscriptions, setResourceSubscriptions a Set of type string. - in subscribeToResource() - only make the request to subscribe if the uri is not in the resourceSubscriptions set - in unsubscribeFromResource() - only make the request to unsubscribe if the uri is in the resourceSubscriptions set - in ResourceTab element, - pass a boolean resourceSubscriptionsSupported as serverCapabilities.resources.subscribe - pass resourceSubscriptions as a prop * In ResourcesTab.tsx - deconstruct resourceSubscriptions and resourceSubscriptionsSupported from props and add prop type - in selected resource panel - don't show subscribe or unsubscribe buttons unless resourceSubscriptionsSupported is true - only show subscribe button if selected resource uri is not in resourceSubscriptions set - only show unsubscribe button if selected resource uri is in resourceSubscriptions set - wrap buttons in a flex div that is - justified right - has a minimal gap between - 2/5 wide (just big enough to contain two buttons and leave the h3 text 3/5 of the row to render and not overflow. --- client/src/App.tsx | 47 +++++++++++++++++--------- client/src/components/ResourcesTab.tsx | 13 +++++-- package-lock.json | 4 +-- package.json | 4 +-- 4 files changed, 45 insertions(+), 23 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index f5b1f79..382ae03 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -128,6 +128,8 @@ const App = () => { const [selectedResource, setSelectedResource] = useState( null, ); + const [resourceSubscriptions, setResourceSubscriptions] = useState>(new Set()); + const [selectedPrompt, setSelectedPrompt] = useState(null); const [selectedTool, setSelectedTool] = useState(null); const [nextResourceCursor, setNextResourceCursor] = useState< @@ -310,26 +312,37 @@ const App = () => { const subscribeToResource = async (uri: string) => { - await makeRequest( - { - method: "resources/subscribe" as const, - params: { uri }, - }, - z.object({}), - "resources", - ); + 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) => { - await makeRequest( - { - method: "resources/unsubscribe" as const, - params: { uri }, - }, - z.object({}), - "resources", - ); + 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); + } }; @@ -510,6 +523,8 @@ const App = () => { clearError("resources"); setSelectedResource(resource); }} + resourceSubscriptionsSupported={serverCapabilities?.resources?.subscribe || false} + resourceSubscriptions={resourceSubscriptions} subscribeToResource={(uri) => { clearError("resources"); subscribeToResource(uri); diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 9d94296..317ec85 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -26,6 +26,8 @@ const ResourcesTab = ({ readResource, selectedResource, setSelectedResource, + resourceSubscriptionsSupported, + resourceSubscriptions, subscribeToResource, unsubscribeFromResource, handleCompletion, @@ -54,6 +56,8 @@ const ResourcesTab = ({ nextCursor: ListResourcesResult["nextCursor"]; nextTemplateCursor: ListResourceTemplatesResult["nextCursor"]; error: string | null; + resourceSubscriptionsSupported: boolean; + resourceSubscriptions: Set; subscribeToResource: (uri: string) => void; unsubscribeFromResource: (uri: string) => void; }) => { @@ -168,14 +172,16 @@ const ResourcesTab = ({ : "Select a resource or template"} {selectedResource && ( - <> - + } + { resourceSubscriptionsSupported && resourceSubscriptions.has(selectedResource.uri) && + } - +
)}
diff --git a/package-lock.json b/package-lock.json index 550bb75..ed9bc96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,8 @@ "server" ], "dependencies": { - "@modelcontextprotocol/inspector-client": "0.4.1", - "@modelcontextprotocol/inspector-server": "0.4.1", + "@modelcontextprotocol/inspector-client": "^0.5.1", + "@modelcontextprotocol/inspector-server": "^0.5.1", "concurrently": "^9.0.1", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", diff --git a/package.json b/package.json index 3de7ce4..b84fbd6 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "publish-all": "npm publish --workspaces --access public && npm publish --access public" }, "dependencies": { - "@modelcontextprotocol/inspector-client": "0.4.1", - "@modelcontextprotocol/inspector-server": "0.4.1", + "@modelcontextprotocol/inspector-client": "^0.5.1", + "@modelcontextprotocol/inspector-server": "^0.5.1", "concurrently": "^9.0.1", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", From 952bee2605e4acd25f2d5fc5c2263f81edfd2c20 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 8 Mar 2025 15:08:12 -0500 Subject: [PATCH 3/3] Fix prettier complaints --- client/src/App.tsx | 12 ++++---- client/src/components/ResourcesTab.tsx | 39 +++++++++++++++----------- client/src/lib/notificationTypes.ts | 6 ++-- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 382ae03..e902da9 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -128,7 +128,9 @@ const App = () => { const [selectedResource, setSelectedResource] = useState( null, ); - const [resourceSubscriptions, setResourceSubscriptions] = useState>(new Set()); + const [resourceSubscriptions, setResourceSubscriptions] = useState< + Set + >(new Set()); const [selectedPrompt, setSelectedPrompt] = useState(null); const [selectedTool, setSelectedTool] = useState(null); @@ -311,7 +313,6 @@ const App = () => { }; const subscribeToResource = async (uri: string) => { - if (!resourceSubscriptions.has(uri)) { await makeRequest( { @@ -325,11 +326,9 @@ const App = () => { clone.add(uri); setResourceSubscriptions(clone); } - }; const unsubscribeFromResource = async (uri: string) => { - if (resourceSubscriptions.has(uri)) { await makeRequest( { @@ -345,7 +344,6 @@ const App = () => { } }; - const listPrompts = async () => { const response = await makeRequest( { @@ -523,7 +521,9 @@ const App = () => { clearError("resources"); setSelectedResource(resource); }} - resourceSubscriptionsSupported={serverCapabilities?.resources?.subscribe || false} + resourceSubscriptionsSupported={ + serverCapabilities?.resources?.subscribe || false + } resourceSubscriptions={resourceSubscriptions} subscribeToResource={(uri) => { clearError("resources"); diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 317ec85..f000840 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -173,23 +173,28 @@ const ResourcesTab = ({ {selectedResource && (
- { resourceSubscriptionsSupported && !resourceSubscriptions.has(selectedResource.uri) && - } - { resourceSubscriptionsSupported && resourceSubscriptions.has(selectedResource.uri) && - - } + {resourceSubscriptionsSupported && + !resourceSubscriptions.has(selectedResource.uri) && ( + + )} + {resourceSubscriptionsSupported && + resourceSubscriptions.has(selectedResource.uri) && ( + + )}