Merge branch 'main' into handle-empty-json-fields

This commit is contained in:
Ola Hungerford
2025-03-13 06:17:18 -07:00
committed by GitHub
11 changed files with 169 additions and 30 deletions

View File

@@ -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). 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 ### From this repository
If you're working on the inspector itself: If you're working on the inspector itself:

View File

@@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-client", "name": "@modelcontextprotocol/inspector-client",
"version": "0.5.1", "version": "0.6.0",
"description": "Client-side application for the Model Context Protocol inspector", "description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",

View File

@@ -97,6 +97,9 @@ const App = () => {
>([]); >([]);
const [roots, setRoots] = useState<Root[]>([]); const [roots, setRoots] = useState<Root[]>([]);
const [env, setEnv] = useState<Record<string, string>>({}); const [env, setEnv] = useState<Record<string, string>>({});
const [bearerToken, setBearerToken] = useState<string>(() => {
return localStorage.getItem("lastBearerToken") || "";
});
const [pendingSampleRequests, setPendingSampleRequests] = useState< const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array< Array<
@@ -128,6 +131,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<
@@ -160,6 +167,7 @@ const App = () => {
args, args,
sseUrl, sseUrl,
env, env,
bearerToken,
proxyServerUrl: PROXY_SERVER_URL, proxyServerUrl: PROXY_SERVER_URL,
onNotification: (notification) => { onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]); setNotifications((prev) => [...prev, notification as ServerNotification]);
@@ -195,6 +203,10 @@ const App = () => {
localStorage.setItem("lastTransportType", transportType); localStorage.setItem("lastTransportType", transportType);
}, [transportType]); }, [transportType]);
useEffect(() => {
localStorage.setItem("lastBearerToken", bearerToken);
}, [bearerToken]);
// Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback) // Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback)
useEffect(() => { useEffect(() => {
const serverUrl = params.get("serverUrl"); const serverUrl = params.get("serverUrl");
@@ -308,6 +320,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(
{ {
@@ -382,6 +426,8 @@ const App = () => {
setSseUrl={setSseUrl} setSseUrl={setSseUrl}
env={env} env={env}
setEnv={setEnv} setEnv={setEnv}
bearerToken={bearerToken}
setBearerToken={setBearerToken}
onConnect={connectMcpServer} onConnect={connectMcpServer}
stdErrNotifications={stdErrNotifications} stdErrNotifications={stdErrNotifications}
/> />
@@ -485,6 +531,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,6 +172,29 @@ const ResourcesTab = ({
: "Select a resource or template"} : "Select a resource or template"}
</h3> </h3>
{selectedResource && ( {selectedResource && (
<div className="flex row-auto gap-1 justify-end w-2/5">
{resourceSubscriptionsSupported &&
!resourceSubscriptions.has(selectedResource.uri) && (
<Button
variant="outline"
size="sm"
onClick={() => subscribeToResource(selectedResource.uri)}
>
Subscribe
</Button>
)}
{resourceSubscriptionsSupported &&
resourceSubscriptions.has(selectedResource.uri) && (
<Button
variant="outline"
size="sm"
onClick={() =>
unsubscribeFromResource(selectedResource.uri)
}
>
Unsubscribe
</Button>
)}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -172,6 +203,7 @@ const ResourcesTab = ({
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 mr-2" />
Refresh Refresh
</Button> </Button>
</div>
)} )}
</div> </div>
<div className="p-4"> <div className="p-4">

View File

@@ -43,7 +43,7 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
<h3 className="text-lg font-semibold">Recent Requests</h3> <h3 className="text-lg font-semibold">Recent Requests</h3>
{pendingRequests.map((request) => ( {pendingRequests.map((request) => (
<div key={request.id} className="p-4 border rounded-lg space-y-4"> <div key={request.id} className="p-4 border rounded-lg space-y-4">
<pre className="bg-gray-50 p-2 rounded"> <pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded">
{JSON.stringify(request.request, null, 2)} {JSON.stringify(request.request, null, 2)}
</pre> </pre>
<div className="flex space-x-2"> <div className="flex space-x-2">

View File

@@ -35,6 +35,8 @@ interface SidebarProps {
setSseUrl: (url: string) => void; setSseUrl: (url: string) => void;
env: Record<string, string>; env: Record<string, string>;
setEnv: (env: Record<string, string>) => void; setEnv: (env: Record<string, string>) => void;
bearerToken: string;
setBearerToken: (token: string) => void;
onConnect: () => void; onConnect: () => void;
stdErrNotifications: StdErrNotification[]; stdErrNotifications: StdErrNotification[];
} }
@@ -51,11 +53,14 @@ const Sidebar = ({
setSseUrl, setSseUrl,
env, env,
setEnv, setEnv,
bearerToken,
setBearerToken,
onConnect, onConnect,
stdErrNotifications, stdErrNotifications,
}: SidebarProps) => { }: SidebarProps) => {
const [theme, setTheme] = useTheme(); const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false); const [showEnvVars, setShowEnvVars] = useState(false);
const [showBearerToken, setShowBearerToken] = useState(false);
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set()); const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
return ( return (
@@ -110,6 +115,7 @@ const Sidebar = ({
</div> </div>
</> </>
) : ( ) : (
<>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">URL</label> <label className="text-sm font-medium">URL</label>
<Input <Input
@@ -119,6 +125,33 @@ const Sidebar = ({
className="font-mono" className="font-mono"
/> />
</div> </div>
<div className="space-y-2">
<Button
variant="outline"
onClick={() => setShowBearerToken(!showBearerToken)}
className="flex items-center w-full"
>
{showBearerToken ? (
<ChevronDown className="w-4 h-4 mr-2" />
) : (
<ChevronRight className="w-4 h-4 mr-2" />
)}
Authentication
</Button>
{showBearerToken && (
<div className="space-y-2">
<label className="text-sm font-medium">Bearer Token</label>
<Input
placeholder="Bearer Token"
value={bearerToken}
onChange={(e) => setBearerToken(e.target.value)}
className="font-mono"
type="password"
/>
</div>
)}
</div>
</>
)} )}
{transportType === "stdio" && ( {transportType === "stdio" && (
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -9,6 +9,7 @@ import {
CreateMessageRequestSchema, CreateMessageRequestSchema,
ListRootsRequestSchema, ListRootsRequestSchema,
ProgressNotificationSchema, ProgressNotificationSchema,
ResourceUpdatedNotificationSchema,
Request, Request,
Result, Result,
ServerCapabilities, ServerCapabilities,
@@ -25,6 +26,7 @@ import { SESSION_KEYS } from "../constants";
import { Notification, StdErrNotificationSchema } from "../notificationTypes"; import { Notification, StdErrNotificationSchema } from "../notificationTypes";
import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
import { authProvider } from "../auth"; import { authProvider } from "../auth";
import packageJson from "../../../package.json";
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const DEFAULT_REQUEST_TIMEOUT_MSEC = const DEFAULT_REQUEST_TIMEOUT_MSEC =
@@ -37,6 +39,7 @@ interface UseConnectionOptions {
sseUrl: string; sseUrl: string;
env: Record<string, string>; env: Record<string, string>;
proxyServerUrl: string; proxyServerUrl: string;
bearerToken?: string;
requestTimeout?: number; requestTimeout?: number;
onNotification?: (notification: Notification) => void; onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void; onStdErrNotification?: (notification: Notification) => void;
@@ -57,6 +60,7 @@ export function useConnection({
sseUrl, sseUrl,
env, env,
proxyServerUrl, proxyServerUrl,
bearerToken,
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC, requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC,
onNotification, onNotification,
onStdErrNotification, onStdErrNotification,
@@ -202,7 +206,7 @@ export function useConnection({
const client = new Client<Request, Notification, Result>( const client = new Client<Request, Notification, Result>(
{ {
name: "mcp-inspector", name: "mcp-inspector",
version: "0.0.1", version: packageJson.version,
}, },
{ {
capabilities: { capabilities: {
@@ -228,9 +232,11 @@ export function useConnection({
// Inject auth manually instead of using SSEClientTransport, because we're // Inject auth manually instead of using SSEClientTransport, because we're
// proxying through the inspector server first. // proxying through the inspector server first.
const headers: HeadersInit = {}; const headers: HeadersInit = {};
const tokens = await authProvider.tokens();
if (tokens) { // Use manually provided bearer token if available, otherwise use OAuth tokens
headers["Authorization"] = `Bearer ${tokens.access_token}`; const token = bearerToken || (await authProvider.tokens())?.access_token;
if (token) {
headers["Authorization"] = `Bearer ${token}`;
} }
const clientTransport = new SSEClientTransport(backendUrl, { const clientTransport = new SSEClientTransport(backendUrl, {
@@ -247,6 +253,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

@@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector", "name": "@modelcontextprotocol/inspector",
"version": "0.5.1", "version": "0.6.0",
"description": "Model Context Protocol inspector", "description": "Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",
@@ -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.6.0",
"@modelcontextprotocol/inspector-server": "0.4.1", "@modelcontextprotocol/inspector-server": "^0.6.0",
"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

@@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-server", "name": "@modelcontextprotocol/inspector-server",
"version": "0.5.1", "version": "0.6.0",
"description": "Server-side application for the Model Context Protocol inspector", "description": "Server-side application for the Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",