Merge branch 'main' into inspector/error-popup

This commit is contained in:
Cliff Hall 2026-01-09 15:52:22 -05:00 • committed by GitHub
commit 8cb845798e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -7,6 +7,7 @@ import {
import {
StreamableHTTPClientTransport,
StreamableHTTPClientTransportOptions,
StreamableHTTPError,
} from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import {
ClientNotification,
@ -30,11 +31,15 @@ import {
Progress,
LoggingLevel,
ElicitRequestSchema,
Implementation,
} from "@modelcontextprotocol/sdk/types.js";
import type {
AnySchema,
SchemaOutput,
} from "@modelcontextprotocol/sdk/server/zod-compat.js";
import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
import { useEffect, useState } from "react";
import { useToast } from "@/lib/hooks/useToast";
import { z } from "zod";
import { ConnectionStatus, CLIENT_IDENTITY } from "../constants";
import { Notification } from "../notificationTypes";
import {
@ -45,6 +50,8 @@ import {
clearClientInformationFromSessionStorage,
InspectorOAuthClientProvider,
saveClientInformationToSessionStorage,
saveScopeToSessionStorage,
clearScopeFromSessionStorage,
discoverScopes,
} from "../auth";
import {
@ -57,6 +64,7 @@ import { getMCPServerRequestTimeout } from "@/utils/configUtils";
import { InspectorConfig } from "../configurationTypes";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { CustomHeaders } from "../types/customHeaders";
import { resolveRefsInMessage } from "@/utils/schemaUtils";
interface UseConnectionOptions {
transportType: "stdio" | "sse" | "streamable-http";
@ -80,6 +88,8 @@ interface UseConnectionOptions {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRoots?: () => any[];
defaultLoggingLevel?: LoggingLevel;
serverImplementation?: Implementation;
metadata?: Record<string, string>;
}
export function useConnection({
@ -99,6 +109,7 @@ export function useConnection({
onElicitationRequest,
getRoots,
defaultLoggingLevel,
metadata = {},
}: UseConnectionOptions) {
const [connectionStatus, setConnectionStatus] =
useState<ConnectionStatus>("disconnected");
@ -117,6 +128,8 @@ export function useConnection({
const [mcpProtocolVersion, setMcpProtocolVersion] = useState<string | null>(
null,
);
const [serverImplementation, setServerImplementation] =
useState<Implementation | null>(null);
useEffect(() => {
if (!oauthClientId) {
@ -142,6 +155,15 @@ export function useConnection({
});
}, [oauthClientId, oauthClientSecret, sseUrl]);
useEffect(() => {
if (!oauthScope) {
clearScopeFromSessionStorage(sseUrl);
return;
}
saveScopeToSessionStorage(sseUrl, oauthScope);
}, [oauthScope, sseUrl]);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
...prev,
@ -152,17 +174,31 @@ export function useConnection({
]);
};
const makeRequest = async <T extends z.ZodType>(
const makeRequest = async <T extends AnySchema>(
request: ClientRequest,
schema: T,
options?: RequestOptions & { suppressToast?: boolean },
): Promise<z.output<T>> => {
): Promise<SchemaOutput<T>> => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
const abortController = new AbortController();
// Add metadata to the request if available, but skip for tool calls
// as they handle metadata merging separately
const shouldAddGeneralMetadata =
request.method !== "tools/call" && Object.keys(metadata).length > 0;
const requestWithMetadata = shouldAddGeneralMetadata
? {
...request,
params: {
...request.params,
_meta: metadata,
},
}
: request;
// prepare MCP Client request options
const mcpRequestOptions: RequestOptions = {
signal: options?.signal ?? abortController.signal,
@ -191,13 +227,17 @@ export function useConnection({
let response;
try {
response = await mcpClient.request(request, schema, mcpRequestOptions);
response = await mcpClient.request(
requestWithMetadata,
schema,
mcpRequestOptions,
);
pushHistory(request, response);
pushHistory(requestWithMetadata, response);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
pushHistory(request, { error: errorMessage });
pushHistory(requestWithMetadata, { error: errorMessage });
throw error;
}
@ -319,8 +359,11 @@ export function useConnection({
const is401Error = (error: unknown): boolean => {
return (
(error instanceof SseError && error.code === 401) ||
(error instanceof StreamableHTTPError && error.code === 401) ||
(error instanceof Error && error.message.includes("401")) ||
(error instanceof Error && error.message.includes("Unauthorized"))
(error instanceof Error && error.message.includes("Unauthorized")) ||
(error instanceof Error &&
error.message.includes("Missing Authorization header"))
);
};
@ -346,10 +389,9 @@ export function useConnection({
}
scope = await discoverScopes(sseUrl, resourceMetadata);
}
const serverAuthProvider = new InspectorOAuthClientProvider(
sseUrl,
scope,
);
saveScopeToSessionStorage(sseUrl, scope);
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
const result = await auth(serverAuthProvider, {
serverUrl: sseUrl,
@ -681,9 +723,19 @@ export function useConnection({
await client.connect(transport as Transport);
const protocolOnMessage = transport.onmessage;
if (protocolOnMessage) {
transport.onmessage = (message) => {
const resolvedMessage = resolveRefsInMessage(message);
protocolOnMessage(resolvedMessage);
};
}
setClientTransport(transport);
capabilities = client.getServerCapabilities();
const serverInfo = client.getServerVersion();
setServerImplementation(serverInfo || null);
const initializeRequest = {
method: "initialize",
};
@ -807,11 +859,13 @@ export function useConnection({
const clearRequestHistory = () => {
setRequestHistory([]);
setServerImplementation(null);
};
return {
connectionStatus,
serverCapabilities,
serverImplementation,
mcpClient,
requestHistory,
clearRequestHistory,