diff --git a/README.md b/README.md index a6da9f4..a6ab6d4 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging). +### Authentication + +The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. + ### From this repository If you're working on the inspector itself: diff --git a/client/package.json b/client/package.json index d45cdc7..0471765 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.5.1", + "version": "0.6.0", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/client/src/App.tsx b/client/src/App.tsx index a9adea5..5650954 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -97,6 +97,9 @@ const App = () => { >([]); const [roots, setRoots] = useState([]); const [env, setEnv] = useState>({}); + const [bearerToken, setBearerToken] = useState(() => { + return localStorage.getItem("lastBearerToken") || ""; + }); const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< @@ -128,6 +131,10 @@ const App = () => { const [selectedResource, setSelectedResource] = useState( null, ); + const [resourceSubscriptions, setResourceSubscriptions] = useState< + Set + >(new Set()); + const [selectedPrompt, setSelectedPrompt] = useState(null); const [selectedTool, setSelectedTool] = useState(null); const [nextResourceCursor, setNextResourceCursor] = useState< @@ -160,6 +167,7 @@ const App = () => { args, sseUrl, env, + bearerToken, proxyServerUrl: PROXY_SERVER_URL, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); @@ -195,6 +203,10 @@ const App = () => { localStorage.setItem("lastTransportType", transportType); }, [transportType]); + useEffect(() => { + localStorage.setItem("lastBearerToken", bearerToken); + }, [bearerToken]); + // Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback) useEffect(() => { const serverUrl = params.get("serverUrl"); @@ -308,6 +320,38 @@ const App = () => { 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 response = await makeRequest( { @@ -382,6 +426,8 @@ const App = () => { setSseUrl={setSseUrl} env={env} setEnv={setEnv} + bearerToken={bearerToken} + setBearerToken={setBearerToken} onConnect={connectMcpServer} stdErrNotifications={stdErrNotifications} /> @@ -485,6 +531,18 @@ const App = () => { clearError("resources"); setSelectedResource(resource); }} + resourceSubscriptionsSupported={ + serverCapabilities?.resources?.subscribe || false + } + resourceSubscriptions={resourceSubscriptions} + 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..f000840 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -26,6 +26,10 @@ const ResourcesTab = ({ readResource, selectedResource, setSelectedResource, + resourceSubscriptionsSupported, + resourceSubscriptions, + subscribeToResource, + unsubscribeFromResource, handleCompletion, completionsSupported, resourceContent, @@ -52,6 +56,10 @@ const ResourcesTab = ({ nextCursor: ListResourcesResult["nextCursor"]; nextTemplateCursor: ListResourceTemplatesResult["nextCursor"]; error: string | null; + resourceSubscriptionsSupported: boolean; + resourceSubscriptions: Set; + subscribeToResource: (uri: string) => void; + unsubscribeFromResource: (uri: string) => void; }) => { const [selectedTemplate, setSelectedTemplate] = useState(null); @@ -164,14 +172,38 @@ const ResourcesTab = ({ : "Select a resource or template"} {selectedResource && ( - +
+ {resourceSubscriptionsSupported && + !resourceSubscriptions.has(selectedResource.uri) && ( + + )} + {resourceSubscriptionsSupported && + resourceSubscriptions.has(selectedResource.uri) && ( + + )} + +
)}
diff --git a/client/src/components/SamplingTab.tsx b/client/src/components/SamplingTab.tsx index d851880..5c45400 100644 --- a/client/src/components/SamplingTab.tsx +++ b/client/src/components/SamplingTab.tsx @@ -43,7 +43,7 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {

Recent Requests

{pendingRequests.map((request) => (
-
+            
               {JSON.stringify(request.request, null, 2)}
             
diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index c95f621..48c6ff2 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -35,6 +35,8 @@ interface SidebarProps { setSseUrl: (url: string) => void; env: Record; setEnv: (env: Record) => void; + bearerToken: string; + setBearerToken: (token: string) => void; onConnect: () => void; stdErrNotifications: StdErrNotification[]; } @@ -51,11 +53,14 @@ const Sidebar = ({ setSseUrl, env, setEnv, + bearerToken, + setBearerToken, onConnect, stdErrNotifications, }: SidebarProps) => { const [theme, setTheme] = useTheme(); const [showEnvVars, setShowEnvVars] = useState(false); + const [showBearerToken, setShowBearerToken] = useState(false); const [shownEnvVars, setShownEnvVars] = useState>(new Set()); return ( @@ -110,15 +115,43 @@ const Sidebar = ({
) : ( -
- - setSseUrl(e.target.value)} - className="font-mono" - /> -
+ <> +
+ + setSseUrl(e.target.value)} + className="font-mono" + /> +
+
+ + {showBearerToken && ( +
+ + setBearerToken(e.target.value)} + className="font-mono" + type="password" + /> +
+ )} +
+ )} {transportType === "stdio" && (
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 75b5467..ea9e05a 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, @@ -25,6 +26,7 @@ import { SESSION_KEYS } from "../constants"; import { Notification, StdErrNotificationSchema } from "../notificationTypes"; import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; import { authProvider } from "../auth"; +import packageJson from "../../../package.json"; const params = new URLSearchParams(window.location.search); const DEFAULT_REQUEST_TIMEOUT_MSEC = @@ -37,6 +39,7 @@ interface UseConnectionOptions { sseUrl: string; env: Record; proxyServerUrl: string; + bearerToken?: string; requestTimeout?: number; onNotification?: (notification: Notification) => void; onStdErrNotification?: (notification: Notification) => void; @@ -57,6 +60,7 @@ export function useConnection({ sseUrl, env, proxyServerUrl, + bearerToken, requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC, onNotification, onStdErrNotification, @@ -202,7 +206,7 @@ export function useConnection({ const client = new Client( { name: "mcp-inspector", - version: "0.0.1", + version: packageJson.version, }, { capabilities: { @@ -228,9 +232,11 @@ export function useConnection({ // Inject auth manually instead of using SSEClientTransport, because we're // proxying through the inspector server first. const headers: HeadersInit = {}; - const tokens = await authProvider.tokens(); - if (tokens) { - headers["Authorization"] = `Bearer ${tokens.access_token}`; + + // Use manually provided bearer token if available, otherwise use OAuth tokens + const token = bearerToken || (await authProvider.tokens())?.access_token; + if (token) { + headers["Authorization"] = `Bearer ${token}`; } const clientTransport = new SSEClientTransport(backendUrl, { @@ -247,6 +253,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..82c1fd8 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"; @@ -13,7 +14,7 @@ export const StdErrNotificationSchema = BaseNotificationSchema.extend({ export const NotificationSchema = ClientNotificationSchema.or( StdErrNotificationSchema, -); +).or(ServerNotificationSchema); export type StdErrNotification = z.infer; export type Notification = z.infer; diff --git a/package-lock.json b/package-lock.json index fd03732..b9b2854 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..3f40d2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.5.1", + "version": "0.6.0", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -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.6.0", + "@modelcontextprotocol/inspector-server": "^0.6.0", "concurrently": "^9.0.1", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", diff --git a/server/package.json b/server/package.json index e64557f..5d8839f 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.5.1", + "version": "0.6.0", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)",