Merge branch 'main' into accurate-cli-version

This commit is contained in:
Ola Hungerford 2025-09-22 20:46:14 -07:00 • committed by GitHub
commit 32ea112051
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -56,6 +56,7 @@ import {
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
import { InspectorConfig } from "../configurationTypes";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { CustomHeaders } from "../types/customHeaders";
interface UseConnectionOptions {
transportType: "stdio" | "sse" | "streamable-http";
@ -63,11 +64,12 @@ interface UseConnectionOptions {
args: string;
sseUrl: string;
env: Record<string, string>;
bearerToken?: string;
headerName?: string;
// Custom headers support
customHeaders?: CustomHeaders;
oauthClientId?: string;
oauthScope?: string;
config: InspectorConfig;
connectionType?: "direct" | "proxy";
onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -85,11 +87,11 @@ export function useConnection({
args,
sseUrl,
env,
bearerToken,
headerName,
customHeaders,
oauthClientId,
oauthScope,
config,
connectionType = "proxy",
onNotification,
onPendingRequest,
onElicitationRequest,
@ -109,6 +111,10 @@ export function useConnection({
{ request: string; response?: string }[]
>([]);
const [completionsSupported, setCompletionsSupported] = useState(false);
const [mcpSessionId, setMcpSessionId] = useState<string | null>(null);
const [mcpProtocolVersion, setMcpProtocolVersion] = useState<string | null>(
null,
);
useEffect(() => {
if (!oauthClientId) {
@ -166,7 +172,7 @@ export function useConnection({
// Add progress notification to `Server Notification` window in the UI
if (onNotification) {
onNotification({
method: "notification/progress",
method: "notifications/progress",
params,
});
}
@ -345,6 +351,17 @@ export function useConnection({
return false;
};
const captureResponseHeaders = (response: Response): void => {
const sessionId = response.headers.get("mcp-session-id");
const protocolVersion = response.headers.get("mcp-protocol-version");
if (sessionId && sessionId !== mcpSessionId) {
setMcpSessionId(sessionId);
}
if (protocolVersion && protocolVersion !== mcpProtocolVersion) {
setMcpProtocolVersion(protocolVersion);
}
};
const connect = async (_e?: unknown, retryCount: number = 0) => {
const clientCapabilities = {
capabilities: {
@ -361,11 +378,14 @@ export function useConnection({
clientCapabilities,
);
try {
await checkProxyHealth();
} catch {
setConnectionStatus("error-connecting-to-proxy");
return;
// Only check proxy health for proxy connections
if (connectionType === "proxy") {
try {
await checkProxyHealth();
} catch {
setConnectionStatus("error-connecting-to-proxy");
return;
}
}
let lastRequest = "";
@ -377,27 +397,42 @@ export function useConnection({
// Create an auth provider with the current server URL
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
// Use manually provided bearer token if available, otherwise use OAuth tokens
const token =
bearerToken || (await serverAuthProvider.tokens())?.access_token;
if (token) {
const authHeaderName = headerName || "Authorization";
// Use custom headers (migration is handled in App.tsx)
let finalHeaders: CustomHeaders = customHeaders || [];
// Add custom header name as a special request header to let the server know which header to pass through
if (authHeaderName.toLowerCase() !== "authorization") {
headers[authHeaderName] = token;
headers["x-custom-auth-header"] = authHeaderName;
} else {
headers[authHeaderName] = `Bearer ${token}`;
// Add OAuth token if available and no custom headers are set
if (finalHeaders.length === 0) {
const oauthToken = (await serverAuthProvider.tokens())?.access_token;
if (oauthToken) {
finalHeaders = [
{
name: "Authorization",
value: `Bearer ${oauthToken}`,
enabled: true,
},
];
}
}
// Add proxy authentication
const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
getMCPProxyAuthToken(config);
const proxyHeaders: HeadersInit = {};
if (proxyAuthToken) {
proxyHeaders[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
// Process all enabled custom headers
const customHeaderNames: string[] = [];
finalHeaders.forEach((header) => {
if (header.enabled && header.name.trim() && header.value.trim()) {
const headerName = header.name.trim();
const headerValue = header.value.trim();
headers[headerName] = headerValue;
// Track custom header names for server processing
if (headerName.toLowerCase() !== "authorization") {
customHeaderNames.push(headerName);
}
}
});
// Add custom header names as a special request header for server processing
if (customHeaderNames.length > 0) {
headers["x-custom-auth-headers"] = JSON.stringify(customHeaderNames);
}
// Create appropriate transport
@ -405,82 +440,178 @@ export function useConnection({
| StreamableHTTPClientTransportOptions
| SSEClientTransportOptions;
let mcpProxyServerUrl;
switch (transportType) {
case "stdio":
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);
mcpProxyServerUrl.searchParams.append("command", command);
mcpProxyServerUrl.searchParams.append("args", args);
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env));
transportOptions = {
authProvider: serverAuthProvider,
eventSourceInit: {
fetch: (
url: string | URL | globalThis.Request,
init?: RequestInit,
) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
},
requestInit: {
headers: { ...headers, ...proxyHeaders },
},
};
break;
let serverUrl: URL;
case "sse":
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
mcpProxyServerUrl.searchParams.append("url", sseUrl);
transportOptions = {
eventSourceInit: {
fetch: (
url: string | URL | globalThis.Request,
init?: RequestInit,
) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
},
requestInit: {
headers: { ...headers, ...proxyHeaders },
},
};
break;
// Determine connection URL based on the connection type
if (connectionType === "direct" && transportType !== "stdio") {
// Direct connection - use the provided URL directly (not available for STDIO)
serverUrl = new URL(sseUrl);
case "streamable-http":
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);
mcpProxyServerUrl.searchParams.append("url", sseUrl);
transportOptions = {
eventSourceInit: {
fetch: (
const requestHeaders = { ...headers };
if (mcpSessionId) {
requestHeaders["mcp-session-id"] = mcpSessionId;
}
switch (transportType) {
case "sse":
requestHeaders["Accept"] = "text/event-stream";
requestHeaders["content-type"] = "application/json";
transportOptions = {
fetch: async (
url: string | URL | globalThis.Request,
init?: RequestInit,
) =>
fetch(url, {
) => {
const response = await fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
},
requestInit: {
headers: { ...headers, ...proxyHeaders },
},
// TODO these should be configurable...
reconnectionOptions: {
maxReconnectionDelay: 30000,
initialReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.5,
maxRetries: 2,
},
};
break;
headers: requestHeaders,
});
// Capture protocol-related headers from response
captureResponseHeaders(response);
return response;
},
requestInit: {
headers: requestHeaders,
},
};
break;
case "streamable-http":
transportOptions = {
fetch: async (
url: string | URL | globalThis.Request,
init?: RequestInit,
) => {
requestHeaders["Accept"] =
"text/event-stream, application/json";
requestHeaders["Content-Type"] = "application/json";
const response = await fetch(url, {
headers: requestHeaders,
...init,
});
// Capture protocol-related headers from response
captureResponseHeaders(response);
return response;
},
requestInit: {
headers: requestHeaders,
},
// TODO these should be configurable...
reconnectionOptions: {
maxReconnectionDelay: 30000,
initialReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.5,
maxRetries: 2,
},
};
break;
}
} else {
// Proxy connection (default behavior)
// Add proxy authentication headers for proxy connections only
const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
getMCPProxyAuthToken(config);
const proxyHeaders: HeadersInit = {};
if (proxyAuthToken) {
proxyHeaders[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
}
let mcpProxyServerUrl;
switch (transportType) {
case "stdio": {
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);
mcpProxyServerUrl.searchParams.append("command", command);
mcpProxyServerUrl.searchParams.append("args", args);
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env));
const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS
.value as string;
if (proxyFullAddress) {
mcpProxyServerUrl.searchParams.append(
"proxyFullAddress",
proxyFullAddress,
);
}
transportOptions = {
authProvider: serverAuthProvider,
eventSourceInit: {
fetch: (
url: string | URL | globalThis.Request,
init?: RequestInit,
) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
},
requestInit: {
headers: { ...headers, ...proxyHeaders },
},
};
break;
}
case "sse": {
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
mcpProxyServerUrl.searchParams.append("url", sseUrl);
const proxyFullAddressSSE = config.MCP_PROXY_FULL_ADDRESS
.value as string;
if (proxyFullAddressSSE) {
mcpProxyServerUrl.searchParams.append(
"proxyFullAddress",
proxyFullAddressSSE,
);
}
transportOptions = {
eventSourceInit: {
fetch: (
url: string | URL | globalThis.Request,
init?: RequestInit,
) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
},
requestInit: {
headers: { ...headers, ...proxyHeaders },
},
};
break;
}
case "streamable-http":
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);
mcpProxyServerUrl.searchParams.append("url", sseUrl);
transportOptions = {
eventSourceInit: {
fetch: (
url: string | URL | globalThis.Request,
init?: RequestInit,
) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
},
requestInit: {
headers: { ...headers, ...proxyHeaders },
},
// TODO these should be configurable...
reconnectionOptions: {
maxReconnectionDelay: 30000,
initialReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.5,
maxRetries: 2,
},
};
break;
}
serverUrl = mcpProxyServerUrl as URL;
serverUrl.searchParams.append("transportType", transportType);
}
(mcpProxyServerUrl as URL).searchParams.append(
"transportType",
transportType,
);
if (onNotification) {
[
@ -506,14 +637,11 @@ export function useConnection({
try {
const transport =
transportType === "streamable-http"
? new StreamableHTTPClientTransport(mcpProxyServerUrl as URL, {
? new StreamableHTTPClientTransport(serverUrl, {
sessionId: undefined,
...transportOptions,
})
: new SSEClientTransport(
mcpProxyServerUrl as URL,
transportOptions,
);
: new SSEClientTransport(serverUrl, transportOptions);
await client.connect(transport as Transport);
@ -530,7 +658,9 @@ export function useConnection({
});
} catch (error) {
console.error(
`Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
connectionType === "direct"
? `Failed to connect directly to MCP Server at: ${serverUrl}:`
: `Failed to connect to MCP Server via the MCP Inspector Proxy: ${serverUrl}:`,
error,
);
@ -577,6 +707,15 @@ export function useConnection({
if (capabilities?.logging && defaultLoggingLevel) {
lastRequest = "logging/setLevel";
await client.setLoggingLevel(defaultLoggingLevel);
pushHistory(
{
method: "logging/setLevel",
params: {
level: defaultLoggingLevel,
},
},
{},
);
lastRequest = "";
}
@ -620,6 +759,12 @@ export function useConnection({
setConnectionStatus("disconnected");
setCompletionsSupported(false);
setServerCapabilities(null);
setMcpSessionId(null);
setMcpProtocolVersion(null);
};
const clearRequestHistory = () => {
setRequestHistory([]);
};
return {
@ -627,6 +772,7 @@ export function useConnection({
serverCapabilities,
mcpClient,
requestHistory,
clearRequestHistory,
makeRequest,
sendNotification,
handleCompletion,