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

@@ -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)",

View File

@@ -97,6 +97,9 @@ const App = () => {
>([]);
const [roots, setRoots] = useState<Root[]>([]);
const [env, setEnv] = useState<Record<string, string>>({});
const [bearerToken, setBearerToken] = useState<string>(() => {
return localStorage.getItem("lastBearerToken") || "";
});
const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array<
@@ -128,6 +131,10 @@ const App = () => {
const [selectedResource, setSelectedResource] = useState<Resource | null>(
null,
);
const [resourceSubscriptions, setResourceSubscriptions] = useState<
Set<string>
>(new Set<string>());
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [selectedTool, setSelectedTool] = useState<Tool | null>(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}

View File

@@ -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<string>;
subscribeToResource: (uri: string) => void;
unsubscribeFromResource: (uri: string) => void;
}) => {
const [selectedTemplate, setSelectedTemplate] =
useState<ResourceTemplate | null>(null);
@@ -164,14 +172,38 @@ const ResourcesTab = ({
: "Select a resource or template"}
</h3>
{selectedResource && (
<Button
variant="outline"
size="sm"
onClick={() => readResource(selectedResource.uri)}
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
<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
variant="outline"
size="sm"
onClick={() => readResource(selectedResource.uri)}
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
</div>
)}
</div>
<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>
{pendingRequests.map((request) => (
<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)}
</pre>
<div className="flex space-x-2">

View File

@@ -35,6 +35,8 @@ interface SidebarProps {
setSseUrl: (url: string) => void;
env: Record<string, string>;
setEnv: (env: Record<string, string>) => 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<Set<string>>(new Set());
return (
@@ -110,15 +115,43 @@ const Sidebar = ({
</div>
</>
) : (
<div className="space-y-2">
<label className="text-sm font-medium">URL</label>
<Input
placeholder="URL"
value={sseUrl}
onChange={(e) => setSseUrl(e.target.value)}
className="font-mono"
/>
</div>
<>
<div className="space-y-2">
<label className="text-sm font-medium">URL</label>
<Input
placeholder="URL"
value={sseUrl}
onChange={(e) => setSseUrl(e.target.value)}
className="font-mono"
/>
</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" && (
<div className="space-y-2">

View File

@@ -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<string, string>;
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<Request, Notification, Result>(
{
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) {

View File

@@ -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<typeof StdErrNotificationSchema>;
export type Notification = z.infer<typeof NotificationSchema>;